Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ agent-orchestrator.yaml
.DS_Store
Thumbs.db
package-lock.json

# Personal developer notes (not for repo)
.developer/
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@
"@aoagents/ao-plugin-notifier-webhook": "workspace:*",
"@aoagents/ao-plugin-runtime-process": "workspace:*",
"@aoagents/ao-plugin-runtime-tmux": "workspace:*",
"@aoagents/ao-plugin-scm-bitbucket": "workspace:*",
"@aoagents/ao-plugin-scm-github": "workspace:*",
"@aoagents/ao-plugin-terminal-iterm2": "workspace:*",
"@aoagents/ao-plugin-terminal-web": "workspace:*",
"@aoagents/ao-plugin-tracker-github": "workspace:*",
"@aoagents/ao-plugin-tracker-jira": "workspace:*",
"@aoagents/ao-plugin-tracker-linear": "workspace:*",
"@aoagents/ao-plugin-workspace-clone": "workspace:*",
"@aoagents/ao-plugin-workspace-worktree": "workspace:*",
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/lib/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,20 @@ const agentPlugins: Record<string, { create(): Agent }> = {
opencode: opencodePlugin,
};

const scmPlugins: Record<string, { create(): SCM }> = {
// SCM plugins — loaded lazily to avoid import errors when credentials are not set
const scmPlugins: Record<string, { create(config?: Record<string, unknown>): SCM }> = {
github: githubSCMPlugin,
};

// Register optional SCM plugins (they may fail if package is not installed)
try {
const bitbucketPlugin = await import("@aoagents/ao-plugin-scm-bitbucket");
scmPlugins.bitbucket = bitbucketPlugin.default ?? bitbucketPlugin;
} catch {
// Bitbucket plugin not available
}


/**
* Resolve the Agent plugin for a project (or fall back to the config default).
* Direct import — no dynamic loading needed since the CLI depends on all agent plugins.
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/__tests__/config-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,15 +316,14 @@ describe("generateConfigFromUrl", () => {
expect(project.tracker).toEqual({ plugin: "github" });
});

it("sets bitbucket SCM and github tracker for Bitbucket repos", () => {
it("sets bitbucket SCM and jira tracker for Bitbucket repos", () => {
const parsed = parseRepoUrl("https://bitbucket.org/team/app");
const config = generateConfigFromUrl({ parsed, repoPath: tmpDir });

const projects = config.projects as Record<string, Record<string, unknown>>;
const project = projects.app;
expect(project.scm).toEqual({ plugin: "bitbucket" });
// Bitbucket tracker not implemented, falls back to github
expect(project.tracker).toEqual({ plugin: "github" });
expect(project.tracker).toEqual({ plugin: "jira" });
});

it("does not set postCreate for non-JS projects", () => {
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/__tests__/events-db.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
Expand Down Expand Up @@ -70,3 +74,18 @@ describe("activity-events DB unavailable warning", () => {
expect(console.warn).toHaveBeenCalledTimes(1);
});
});

describe("createRequire source pattern", () => {
const source = readFileSync(
join(dirname(fileURLToPath(import.meta.url)), "..", "events-db.ts"),
"utf8",
);

// When events-db is bundled into the Next.js server, the bundler's createRequire
// shim rejects file:// URL strings with ERR_INVALID_ARG_VALUE. The URL must be
// converted to a filesystem path first. See #2051.
it("converts import.meta.url to a path before calling createRequire", () => {
expect(source).not.toContain("createRequire(import.meta.url)");
expect(source).toContain("createRequire(fileURLToPath(import.meta.url))");
});
});
20 changes: 19 additions & 1 deletion packages/core/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,16 @@ describe("parsePrFromUrl", () => {
});
});

it("falls back to trailing number for non-GitHub URLs", () => {
it("parses Bitbucket Cloud PR URLs", () => {
expect(parsePrFromUrl("https://bitbucket.org/myworkspace/myrepo/pull-requests/789")).toEqual({
owner: "myworkspace",
repo: "myrepo",
number: 789,
url: "https://bitbucket.org/myworkspace/myrepo/pull-requests/789",
});
});

it("parses GitLab merge request URLs", () => {
expect(parsePrFromUrl("https://gitlab.com/foo/bar/-/merge_requests/456")).toEqual({
owner: "foo",
repo: "bar",
Expand All @@ -274,6 +283,15 @@ describe("parsePrFromUrl", () => {
});
});

it("falls back to trailing number for unknown SCM URLs", () => {
expect(parsePrFromUrl("https://custom-git.example.com/project/123")).toEqual({
owner: "",
repo: "",
number: 123,
url: "https://custom-git.example.com/project/123",
});
});

it("returns null when the URL has no PR number", () => {
expect(parsePrFromUrl("https://example.com/foo/bar/pull/not-a-number")).toBeNull();
});
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/config-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,14 @@ export function generateConfigFromUrl(options: GenerateConfigOptions): Record<st
// (best available option since it's the only fully implemented SCM plugin).
projectConfig.scm = { plugin: platform !== "unknown" ? platform : "github" };

// Tracker — same platform as SCM for known hosts, github as fallback
// Tracker — same platform as SCM for GitHub/GitLab, Jira for Bitbucket, github as fallback
projectConfig.tracker = {
plugin: platform === "github" || platform === "gitlab" ? platform : "github",
plugin:
platform === "github" || platform === "gitlab"
? platform
: platform === "bitbucket"
? "jira"
: "github",
};

// Post-create commands based on detected package manager (JS ecosystem only)
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,22 @@ function inferScmPlugin(project: {
repo?: string;
scm?: Record<string, unknown>;
tracker?: Record<string, unknown>;
}): "github" | "gitlab" {
}): "github" | "gitlab" | "bitbucket" {
const scmPlugin = project.scm?.["plugin"];
if (scmPlugin === "gitlab") {
return "gitlab";
}
if (scmPlugin === "bitbucket") {
return "bitbucket";
}

const scmHost = project.scm?.["host"];
if (typeof scmHost === "string" && scmHost.toLowerCase().includes("gitlab")) {
return "gitlab";
}
if (typeof scmHost === "string" && scmHost.toLowerCase().includes("bitbucket")) {
return "bitbucket";
}

const trackerPlugin = project.tracker?.["plugin"];
if (trackerPlugin === "gitlab") {
Expand Down Expand Up @@ -605,9 +611,10 @@ function applyProjectDefaults(config: OrchestratorConfig): OrchestratorConfig {
project.scm = { plugin: inferredPlugin };
}

// Infer tracker from repo if not set (default to github issues)
// Infer tracker from repo if not set (default to github issues).
// Bitbucket uses Jira for issue tracking (both Atlassian products).
if (!project.tracker && project.repo?.includes("/")) {
project.tracker = { plugin: inferredPlugin };
project.tracker = { plugin: inferredPlugin === "bitbucket" ? "jira" : inferredPlugin };
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/events-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
import { createRequire } from "node:module";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { getAoBaseDir } from "./paths.js";

// Use createRequire so we can try/catch on native module load without top-level await.
const _require = createRequire(import.meta.url);
// Pass a filesystem path, not import.meta.url directly: when this module is bundled
// into the Next.js server the bundler's createRequire shim only accepts absolute path
// strings and rejects file:// URLs with ERR_INVALID_ARG_VALUE.
const _require = createRequire(fileURLToPath(import.meta.url));

type BetterSqlite3Database = {
pragma(source: string, options?: { simple?: boolean }): unknown;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/plugin-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ const BUILTIN_PLUGINS: Array<{ slot: PluginSlot; name: string; pkg: string }> =
{ slot: "tracker", name: "github", pkg: "@aoagents/ao-plugin-tracker-github" },
{ slot: "tracker", name: "linear", pkg: "@aoagents/ao-plugin-tracker-linear" },
{ slot: "tracker", name: "gitlab", pkg: "@aoagents/ao-plugin-tracker-gitlab" },
{ slot: "tracker", name: "jira", pkg: "@aoagents/ao-plugin-tracker-jira" },
// SCM
{ slot: "scm", name: "github", pkg: "@aoagents/ao-plugin-scm-github" },
{ slot: "scm", name: "gitlab", pkg: "@aoagents/ao-plugin-scm-gitlab" },
{ slot: "scm", name: "bitbucket", pkg: "@aoagents/ao-plugin-scm-bitbucket" },
// Notifiers
{ slot: "notifier", name: "composio", pkg: "@aoagents/ao-plugin-notifier-composio" },
{ slot: "notifier", name: "dashboard", pkg: "@aoagents/ao-plugin-notifier-dashboard" },
Expand Down
23 changes: 17 additions & 6 deletions packages/core/src/utils/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ export function parsePrFromUrl(prUrl: string): ParsedPrUrl | null {
}
}

// Bitbucket Cloud: bitbucket.org/{workspace}/{repo}/pull-requests/{id}
const bitbucketPullIndex = pathSegments.findIndex((segment) => segment === "pull-requests");
if (bitbucketPullIndex >= 2 && bitbucketPullIndex + 1 < pathSegments.length) {
const owner = pathSegments[bitbucketPullIndex - 2];
const repo = pathSegments[bitbucketPullIndex - 1];
const prNumber = pathSegments[bitbucketPullIndex + 1];
if (owner && repo && prNumber && /^\d+$/.test(prNumber)) {
return {
owner,
repo,
number: Number.parseInt(prNumber, 10),
url: prUrl,
};
}
}

const gitlabMergeRequestIndex = pathSegments.findIndex(
(segment, index) =>
segment === "-" &&
Expand All @@ -46,12 +62,7 @@ export function parsePrFromUrl(prUrl: string): ParsedPrUrl | null {

const trailingNumberMatch = prUrl.match(TRAILING_NUMBER_REGEX);
if (trailingNumberMatch) {
return {
owner: "",
repo: "",
number: parseInt(trailingNumberMatch[1], 10),
url: prUrl,
};
return { owner: "", repo: "", number: parseInt(trailingNumberMatch[1], 10), url: prUrl };
}

return null;
Expand Down
42 changes: 42 additions & 0 deletions packages/plugins/scm-bitbucket/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@aoagents/ao-plugin-scm-bitbucket",
"version": "0.1.0",
"description": "SCM plugin: Bitbucket Cloud (PRs, CI, reviews, webhooks)",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": ["dist"],
"repository": {
"type": "git",
"url": "https://github.com/ComposioHQ/agent-orchestrator.git",
"directory": "packages/plugins/scm-bitbucket"
},
"homepage": "https://github.com/ComposioHQ/agent-orchestrator",
"bugs": {
"url": "https://github.com/ComposioHQ/agent-orchestrator/issues"
},
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"clean": "rm -rf dist"
},
"dependencies": {
"@aoagents/ao-core": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.2.3",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}
Loading