karajan-code 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/docs/karajan-code-logo-small.png +0 -0
  4. package/package.json +60 -0
  5. package/scripts/install.js +898 -0
  6. package/scripts/install.sh +7 -0
  7. package/scripts/postinstall.js +117 -0
  8. package/scripts/setup-multi-instance.sh +150 -0
  9. package/src/activity-log.js +59 -0
  10. package/src/agents/aider-agent.js +25 -0
  11. package/src/agents/availability.js +32 -0
  12. package/src/agents/base-agent.js +27 -0
  13. package/src/agents/claude-agent.js +24 -0
  14. package/src/agents/codex-agent.js +27 -0
  15. package/src/agents/gemini-agent.js +25 -0
  16. package/src/agents/index.js +19 -0
  17. package/src/agents/resolve-bin.js +60 -0
  18. package/src/cli.js +200 -0
  19. package/src/commands/code.js +32 -0
  20. package/src/commands/config.js +74 -0
  21. package/src/commands/doctor.js +155 -0
  22. package/src/commands/init.js +181 -0
  23. package/src/commands/plan.js +67 -0
  24. package/src/commands/report.js +340 -0
  25. package/src/commands/resume.js +39 -0
  26. package/src/commands/review.js +26 -0
  27. package/src/commands/roles.js +117 -0
  28. package/src/commands/run.js +91 -0
  29. package/src/commands/scan.js +18 -0
  30. package/src/commands/sonar.js +53 -0
  31. package/src/config.js +322 -0
  32. package/src/git/automation.js +100 -0
  33. package/src/mcp/progress.js +69 -0
  34. package/src/mcp/run-kj.js +87 -0
  35. package/src/mcp/server-handlers.js +259 -0
  36. package/src/mcp/server.js +37 -0
  37. package/src/mcp/tool-arg-normalizers.js +16 -0
  38. package/src/mcp/tools.js +184 -0
  39. package/src/orchestrator.js +1277 -0
  40. package/src/planning-game/adapter.js +105 -0
  41. package/src/planning-game/client.js +81 -0
  42. package/src/prompts/coder.js +60 -0
  43. package/src/prompts/planner.js +26 -0
  44. package/src/prompts/reviewer.js +45 -0
  45. package/src/repeat-detector.js +77 -0
  46. package/src/review/diff-generator.js +22 -0
  47. package/src/review/parser.js +93 -0
  48. package/src/review/profiles.js +66 -0
  49. package/src/review/schema.js +31 -0
  50. package/src/review/tdd-policy.js +57 -0
  51. package/src/roles/base-role.js +127 -0
  52. package/src/roles/coder-role.js +60 -0
  53. package/src/roles/commiter-role.js +94 -0
  54. package/src/roles/index.js +12 -0
  55. package/src/roles/planner-role.js +81 -0
  56. package/src/roles/refactorer-role.js +66 -0
  57. package/src/roles/researcher-role.js +134 -0
  58. package/src/roles/reviewer-role.js +132 -0
  59. package/src/roles/security-role.js +128 -0
  60. package/src/roles/solomon-role.js +199 -0
  61. package/src/roles/sonar-role.js +65 -0
  62. package/src/roles/tester-role.js +114 -0
  63. package/src/roles/triage-role.js +128 -0
  64. package/src/session-store.js +80 -0
  65. package/src/sonar/api.js +78 -0
  66. package/src/sonar/enforcer.js +19 -0
  67. package/src/sonar/manager.js +163 -0
  68. package/src/sonar/project-key.js +83 -0
  69. package/src/sonar/scanner.js +267 -0
  70. package/src/utils/agent-detect.js +32 -0
  71. package/src/utils/budget.js +123 -0
  72. package/src/utils/display.js +346 -0
  73. package/src/utils/events.js +23 -0
  74. package/src/utils/fs.js +19 -0
  75. package/src/utils/git.js +101 -0
  76. package/src/utils/logger.js +86 -0
  77. package/src/utils/paths.js +18 -0
  78. package/src/utils/pricing.js +28 -0
  79. package/src/utils/process.js +67 -0
  80. package/src/utils/wizard.js +41 -0
  81. package/templates/coder-rules.md +24 -0
  82. package/templates/docker-compose.sonar.yml +60 -0
  83. package/templates/kj.config.yml +82 -0
  84. package/templates/review-rules.md +11 -0
  85. package/templates/roles/coder.md +42 -0
  86. package/templates/roles/commiter.md +44 -0
  87. package/templates/roles/planner.md +45 -0
  88. package/templates/roles/refactorer.md +39 -0
  89. package/templates/roles/researcher.md +37 -0
  90. package/templates/roles/reviewer-paranoid.md +38 -0
  91. package/templates/roles/reviewer-relaxed.md +34 -0
  92. package/templates/roles/reviewer-strict.md +37 -0
  93. package/templates/roles/reviewer.md +55 -0
  94. package/templates/roles/security.md +54 -0
  95. package/templates/roles/solomon.md +106 -0
  96. package/templates/roles/sonar.md +49 -0
  97. package/templates/roles/tester.md +41 -0
  98. package/templates/roles/triage.md +25 -0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ node scripts/install.js "$@"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall hook – registers karajan-mcp in Claude Code and Codex
4
+ * automatically after `npm install`.
5
+ *
6
+ * - Non-interactive, silent on success.
7
+ * - Idempotent: safe to run many times.
8
+ * - Never fails hard (exits 0 even on error) so `npm install` is not blocked.
9
+ */
10
+ import fs from "node:fs/promises";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const ROOT_DIR = path.resolve(__dirname, "..");
17
+ const REGISTRY_PATH = path.join(os.homedir(), ".karajan", "instances.json");
18
+
19
+ async function readJson(file) {
20
+ const raw = await fs.readFile(file, "utf8");
21
+ return JSON.parse(raw);
22
+ }
23
+
24
+ async function writeJson(file, obj) {
25
+ await fs.mkdir(path.dirname(file), { recursive: true });
26
+ await fs.writeFile(file, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
27
+ }
28
+
29
+ function resolveKjHome() {
30
+ if (process.env.KJ_HOME) return process.env.KJ_HOME;
31
+ return path.join(ROOT_DIR, ".karajan");
32
+ }
33
+
34
+ async function resolveKjHomeFromRegistry() {
35
+ try {
36
+ const registry = await readJson(REGISTRY_PATH);
37
+ const names = Object.keys(registry.instances || {});
38
+ if (names.length > 0) {
39
+ const first = registry.instances[names.includes("default") ? "default" : names[0]];
40
+ if (first?.kjHome) return first.kjHome;
41
+ }
42
+ } catch {
43
+ // No registry yet — use default
44
+ }
45
+ return resolveKjHome();
46
+ }
47
+
48
+ async function setupClaudeMcp(kjHome) {
49
+ const claudeJsonPath = path.join(os.homedir(), ".claude.json");
50
+ let config = {};
51
+ try {
52
+ config = await readJson(claudeJsonPath);
53
+ } catch {
54
+ config = {};
55
+ }
56
+
57
+ config.mcpServers = config.mcpServers || {};
58
+ config.mcpServers["karajan-mcp"] = {
59
+ type: "stdio",
60
+ command: "node",
61
+ args: [path.join(ROOT_DIR, "src", "mcp", "server.js")],
62
+ cwd: ROOT_DIR,
63
+ env: { KJ_HOME: kjHome }
64
+ };
65
+
66
+ await writeJson(claudeJsonPath, config);
67
+ }
68
+
69
+ function upsertCodexMcpBlock(toml, block) {
70
+ const begin = "# BEGIN karajan-mcp";
71
+ const end = "# END karajan-mcp";
72
+ const startIdx = toml.indexOf(begin);
73
+ const endIdx = toml.indexOf(end);
74
+ let base = toml;
75
+
76
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
77
+ base = `${toml.slice(0, startIdx).trimEnd()}\n\n${toml.slice(endIdx + end.length).trimStart()}`;
78
+ }
79
+
80
+ return `${base.trimEnd()}\n\n${begin}\n${block}\n${end}\n`;
81
+ }
82
+
83
+ async function setupCodexMcp(kjHome) {
84
+ const configPath = path.join(os.homedir(), ".codex", "config.toml");
85
+ let toml = "";
86
+ try {
87
+ toml = await fs.readFile(configPath, "utf8");
88
+ } catch {
89
+ toml = "";
90
+ }
91
+
92
+ const block = [
93
+ '[mcp_servers."karajan-mcp"]',
94
+ 'command = "node"',
95
+ `args = ["${path.join(ROOT_DIR, "src", "mcp", "server.js")}"]`,
96
+ `cwd = "${ROOT_DIR}"`,
97
+ '[mcp_servers."karajan-mcp".env]',
98
+ `KJ_HOME = "${kjHome}"`
99
+ ].join("\n");
100
+
101
+ const updated = upsertCodexMcpBlock(toml, block);
102
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
103
+ await fs.writeFile(configPath, updated, "utf8");
104
+ }
105
+
106
+ async function main() {
107
+ const kjHome = await resolveKjHomeFromRegistry();
108
+
109
+ await setupClaudeMcp(kjHome);
110
+ await setupCodexMcp(kjHome);
111
+
112
+ console.log("karajan-mcp registered in Claude Code and Codex.");
113
+ }
114
+
115
+ main().catch(() => {
116
+ // Silent failure — never block npm install
117
+ });
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ INSTALLER="$ROOT_DIR/scripts/install.sh"
6
+
7
+ if [[ ! -x "$INSTALLER" ]]; then
8
+ echo "Installer not found: $INSTALLER"
9
+ exit 1
10
+ fi
11
+
12
+ echo "Karajan multi-instance setup (personal + pro)"
13
+ echo
14
+
15
+ read -r -p "Sonar host [http://localhost:9000]: " SONAR_HOST
16
+ SONAR_HOST="${SONAR_HOST:-http://localhost:9000}"
17
+
18
+ read -r -p "Personal KJ_HOME [$HOME/.karajan-personal]: " PERSONAL_HOME
19
+ PERSONAL_HOME="${PERSONAL_HOME:-$HOME/.karajan-personal}"
20
+
21
+ read -r -p "Pro KJ_HOME [$HOME/.karajan-pro]: " PRO_HOME
22
+ PRO_HOME="${PRO_HOME:-$HOME/.karajan-pro}"
23
+
24
+ echo
25
+ read -r -p "Personal Sonar token (KJ_SONAR_TOKEN): " PERSONAL_TOKEN
26
+ read -r -p "Pro Sonar token (KJ_SONAR_TOKEN): " PRO_TOKEN
27
+
28
+ read -r -p "Coder default [codex]: " CODER
29
+ CODER="${CODER:-codex}"
30
+
31
+ read -r -p "Reviewer default [claude]: " REVIEWER
32
+ REVIEWER="${REVIEWER:-claude}"
33
+
34
+ read -r -p "Reviewer fallback [codex]: " REVIEWER_FALLBACK
35
+ REVIEWER_FALLBACK="${REVIEWER_FALLBACK:-codex}"
36
+
37
+ echo
38
+ echo "Setting up PERSONAL instance..."
39
+ "$INSTALLER" \
40
+ --non-interactive \
41
+ --link-global false \
42
+ --kj-home "$PERSONAL_HOME" \
43
+ --sonar-host "$SONAR_HOST" \
44
+ --sonar-token "$PERSONAL_TOKEN" \
45
+ --coder "$CODER" \
46
+ --reviewer "$REVIEWER" \
47
+ --reviewer-fallback "$REVIEWER_FALLBACK" \
48
+ --setup-mcp-claude false \
49
+ --setup-mcp-codex false \
50
+ --run-doctor true
51
+
52
+ echo
53
+ echo "Setting up PRO instance..."
54
+ "$INSTALLER" \
55
+ --non-interactive \
56
+ --link-global false \
57
+ --kj-home "$PRO_HOME" \
58
+ --sonar-host "$SONAR_HOST" \
59
+ --sonar-token "$PRO_TOKEN" \
60
+ --coder "$CODER" \
61
+ --reviewer "$REVIEWER" \
62
+ --reviewer-fallback "$REVIEWER_FALLBACK" \
63
+ --setup-mcp-claude false \
64
+ --setup-mcp-codex false \
65
+ --run-doctor true
66
+
67
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
68
+ CODEX_CONFIG="$HOME/.codex/config.toml"
69
+ SERVER_PATH="$ROOT_DIR/src/mcp/server.js"
70
+
71
+ mkdir -p "$HOME/.claude" "$HOME/.codex"
72
+
73
+ if [[ ! -f "$CLAUDE_SETTINGS" ]]; then
74
+ cat > "$CLAUDE_SETTINGS" <<JSON
75
+ {
76
+ "mcpServers": {}
77
+ }
78
+ JSON
79
+ fi
80
+
81
+ node - <<NODE
82
+ const fs = require('fs');
83
+ const p = "$CLAUDE_SETTINGS";
84
+ const serverPath = "$SERVER_PATH";
85
+ const rootDir = "$ROOT_DIR";
86
+ const personalHome = "$PERSONAL_HOME";
87
+ const proHome = "$PRO_HOME";
88
+ const personalToken = "$PERSONAL_TOKEN";
89
+ const proToken = "$PRO_TOKEN";
90
+ let settings = {};
91
+ try { settings = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { settings = {}; }
92
+ settings.mcpServers = settings.mcpServers || {};
93
+ settings.mcpServers['karajan-personal'] = {
94
+ command: 'node',
95
+ args: [serverPath],
96
+ cwd: rootDir,
97
+ env: { KJ_HOME: personalHome, KJ_SONAR_TOKEN: personalToken }
98
+ };
99
+ settings.mcpServers['karajan-pro'] = {
100
+ command: 'node',
101
+ args: [serverPath],
102
+ cwd: rootDir,
103
+ env: { KJ_HOME: proHome, KJ_SONAR_TOKEN: proToken }
104
+ };
105
+ fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\\n');
106
+ NODE
107
+
108
+ if [[ ! -f "$CODEX_CONFIG" ]]; then
109
+ touch "$CODEX_CONFIG"
110
+ fi
111
+
112
+ TMP_CODEX="$(mktemp)"
113
+ cp "$CODEX_CONFIG" "$TMP_CODEX"
114
+
115
+ sed -i '/# BEGIN karajan-multi/,/# END karajan-multi/d' "$TMP_CODEX"
116
+ cat >> "$TMP_CODEX" <<TOML
117
+
118
+ # BEGIN karajan-multi
119
+ [mcp_servers."karajan-personal"]
120
+ command = "node"
121
+ args = ["$SERVER_PATH"]
122
+ cwd = "$ROOT_DIR"
123
+
124
+ [mcp_servers."karajan-personal".env]
125
+ KJ_HOME = "$PERSONAL_HOME"
126
+ KJ_SONAR_TOKEN = "$PERSONAL_TOKEN"
127
+
128
+ [mcp_servers."karajan-pro"]
129
+ command = "node"
130
+ args = ["$SERVER_PATH"]
131
+ cwd = "$ROOT_DIR"
132
+
133
+ [mcp_servers."karajan-pro".env]
134
+ KJ_HOME = "$PRO_HOME"
135
+ KJ_SONAR_TOKEN = "$PRO_TOKEN"
136
+ # END karajan-multi
137
+ TOML
138
+
139
+ mv "$TMP_CODEX" "$CODEX_CONFIG"
140
+
141
+ echo
142
+ echo "Multi-instance setup completed."
143
+ echo "- Personal KJ_HOME: $PERSONAL_HOME"
144
+ echo "- Pro KJ_HOME: $PRO_HOME"
145
+ echo "- Claude MCP updated: $CLAUDE_SETTINGS"
146
+ echo "- Codex MCP updated: $CODEX_CONFIG"
147
+ echo
148
+ echo "Next steps:"
149
+ echo "1) Restart Claude and Codex."
150
+ echo "2) In each client, use MCP server karajan-personal or karajan-pro."
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getSessionRoot } from "./utils/paths.js";
4
+ import { ensureDir } from "./utils/fs.js";
5
+
6
+ export function createActivityLog(sessionId) {
7
+ const logPath = path.join(getSessionRoot(), sessionId, "activity.log");
8
+ let buffer = [];
9
+ let flushing = false;
10
+
11
+ function formatLine(entry) {
12
+ const ts = entry.timestamp || new Date().toISOString();
13
+ const lvl = (entry.level || "info").toUpperCase().padEnd(5);
14
+ const ctx = entry.context
15
+ ? Object.entries(entry.context)
16
+ .filter(([, v]) => v !== undefined)
17
+ .map(([k, v]) => `${k}=${v}`)
18
+ .join(" ")
19
+ : "";
20
+ const ctxStr = ctx ? ` ${ctx}` : "";
21
+ return `${ts} [${lvl}]${ctxStr} ${entry.message || ""}`;
22
+ }
23
+
24
+ async function flush() {
25
+ if (flushing || buffer.length === 0) return;
26
+ flushing = true;
27
+ const lines = buffer.splice(0);
28
+ try {
29
+ await ensureDir(path.dirname(logPath));
30
+ await fs.appendFile(logPath, lines.join("\n") + "\n", "utf8");
31
+ } catch {
32
+ // best-effort: I/O errors do not crash the flow
33
+ }
34
+ flushing = false;
35
+ if (buffer.length > 0) flush();
36
+ }
37
+
38
+ return {
39
+ write(logEntry) {
40
+ buffer.push(formatLine(logEntry));
41
+ flush();
42
+ },
43
+ writeEvent(progressEvent) {
44
+ const entry = {
45
+ timestamp: progressEvent.timestamp,
46
+ level: progressEvent.status === "fail" ? "error" : "info",
47
+ context: {
48
+ iteration: progressEvent.iteration,
49
+ stage: progressEvent.stage
50
+ },
51
+ message: progressEvent.message || progressEvent.type
52
+ };
53
+ this.write(entry);
54
+ },
55
+ get path() {
56
+ return logPath;
57
+ }
58
+ };
59
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseAgent } from "./base-agent.js";
2
+ import { runCommand } from "../utils/process.js";
3
+ import { resolveBin } from "./resolve-bin.js";
4
+
5
+ export class AiderAgent extends BaseAgent {
6
+ async runTask(task) {
7
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
8
+ const role = task.role || "coder";
9
+ const args = ["--yes", "--message", task.prompt];
10
+ const model = this.getRoleModel(role);
11
+ if (model) args.push("--model", model);
12
+ const res = await runCommand(resolveBin("aider"), args, { timeout, onOutput: task.onOutput });
13
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
14
+ }
15
+
16
+ async reviewTask(task) {
17
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
18
+ const role = task.role || "reviewer";
19
+ const args = ["--yes", "--message", task.prompt];
20
+ const model = this.getRoleModel(role);
21
+ if (model) args.push("--model", model);
22
+ const res = await runCommand(resolveBin("aider"), args, { timeout, onOutput: task.onOutput });
23
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
24
+ }
25
+ }
@@ -0,0 +1,32 @@
1
+ import { runCommand } from "../utils/process.js";
2
+ import { resolveBin } from "./resolve-bin.js";
3
+
4
+ const AGENT_META = {
5
+ codex: { bin: "codex", installUrl: "https://developers.openai.com/codex/cli" },
6
+ claude: { bin: "claude", installUrl: "https://docs.anthropic.com/en/docs/claude-code" },
7
+ gemini: { bin: "gemini", installUrl: "https://github.com/google-gemini/gemini-cli" },
8
+ aider: { bin: "aider", installUrl: "https://aider.chat/docs/install.html" }
9
+ };
10
+
11
+ export async function assertAgentsAvailable(agentNames = []) {
12
+ const unique = [...new Set(agentNames.filter(Boolean))];
13
+ const missing = [];
14
+
15
+ for (const name of unique) {
16
+ const meta = AGENT_META[name];
17
+ if (!meta) continue;
18
+ const res = await runCommand(resolveBin(meta.bin), ["--version"]);
19
+ if (res.exitCode !== 0) {
20
+ missing.push({ name, ...meta });
21
+ }
22
+ }
23
+
24
+ if (missing.length === 0) return;
25
+
26
+ const lines = ["Missing required AI CLIs for this command:"];
27
+ for (const m of missing) {
28
+ lines.push(`- ${m.name}: command '${m.bin}' not found`);
29
+ lines.push(` Install: ${m.installUrl}`);
30
+ }
31
+ throw new Error(lines.join("\n"));
32
+ }
@@ -0,0 +1,27 @@
1
+ export class BaseAgent {
2
+ constructor(name, config, logger) {
3
+ this.name = name;
4
+ this.config = config;
5
+ this.logger = logger;
6
+ }
7
+
8
+ async runTask(_task) {
9
+ throw new Error("runTask not implemented");
10
+ }
11
+
12
+ async reviewTask(_task) {
13
+ throw new Error("reviewTask not implemented");
14
+ }
15
+
16
+ getRoleModel(role) {
17
+ const roleModel = this.config?.roles?.[role]?.model;
18
+ if (roleModel) return roleModel;
19
+ if (role === "reviewer") return this.config?.reviewer_options?.model || null;
20
+ return this.config?.coder_options?.model || null;
21
+ }
22
+
23
+ isAutoApproveEnabled(role) {
24
+ if (role === "reviewer") return false;
25
+ return Boolean(this.config?.coder_options?.auto_approve);
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseAgent } from "./base-agent.js";
2
+ import { runCommand } from "../utils/process.js";
3
+ import { resolveBin } from "./resolve-bin.js";
4
+
5
+ export class ClaudeAgent extends BaseAgent {
6
+ async runTask(task) {
7
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
8
+ const role = task.role || "coder";
9
+ const args = ["-p", task.prompt];
10
+ const model = this.getRoleModel(role);
11
+ if (model) args.push("--model", model);
12
+ const res = await runCommand(resolveBin("claude"), args, { timeout, onOutput: task.onOutput });
13
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
14
+ }
15
+
16
+ async reviewTask(task) {
17
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
18
+ const args = ["-p", task.prompt, "--output-format", "json"];
19
+ const model = this.getRoleModel(task.role || "reviewer");
20
+ if (model) args.push("--model", model);
21
+ const res = await runCommand(resolveBin("claude"), args, { timeout, onOutput: task.onOutput });
22
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
23
+ }
24
+ }
@@ -0,0 +1,27 @@
1
+ import { BaseAgent } from "./base-agent.js";
2
+ import { runCommand } from "../utils/process.js";
3
+ import { resolveBin } from "./resolve-bin.js";
4
+
5
+ export class CodexAgent extends BaseAgent {
6
+ async runTask(task) {
7
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
8
+ const role = task.role || "coder";
9
+ const args = ["exec"];
10
+ const model = this.getRoleModel(role);
11
+ if (model) args.push("--model", model);
12
+ if (this.isAutoApproveEnabled(role)) args.push("--full-auto");
13
+ args.push(task.prompt);
14
+ const res = await runCommand(resolveBin("codex"), args, { timeout, onOutput: task.onOutput });
15
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
16
+ }
17
+
18
+ async reviewTask(task) {
19
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
20
+ const args = ["exec"];
21
+ const model = this.getRoleModel(task.role || "reviewer");
22
+ if (model) args.push("--model", model);
23
+ args.push(task.prompt);
24
+ const res = await runCommand(resolveBin("codex"), args, { timeout, onOutput: task.onOutput });
25
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
26
+ }
27
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseAgent } from "./base-agent.js";
2
+ import { runCommand } from "../utils/process.js";
3
+ import { resolveBin } from "./resolve-bin.js";
4
+
5
+ export class GeminiAgent extends BaseAgent {
6
+ async runTask(task) {
7
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
8
+ const role = task.role || "coder";
9
+ const args = ["-p", task.prompt];
10
+ const model = this.getRoleModel(role);
11
+ if (model) args.push("--model", model);
12
+ const res = await runCommand(resolveBin("gemini"), args, { timeout, onOutput: task.onOutput });
13
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
14
+ }
15
+
16
+ async reviewTask(task) {
17
+ const timeout = this.config.session.max_iteration_minutes * 60 * 1000;
18
+ const role = task.role || "reviewer";
19
+ const args = ["-p", task.prompt, "--output-format", "json"];
20
+ const model = this.getRoleModel(role);
21
+ if (model) args.push("--model", model);
22
+ const res = await runCommand(resolveBin("gemini"), args, { timeout, onOutput: task.onOutput });
23
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ import { ClaudeAgent } from "./claude-agent.js";
2
+ import { CodexAgent } from "./codex-agent.js";
3
+ import { GeminiAgent } from "./gemini-agent.js";
4
+ import { AiderAgent } from "./aider-agent.js";
5
+
6
+ export function createAgent(agentName, config, logger) {
7
+ switch (agentName) {
8
+ case "claude":
9
+ return new ClaudeAgent(agentName, config, logger);
10
+ case "codex":
11
+ return new CodexAgent(agentName, config, logger);
12
+ case "gemini":
13
+ return new GeminiAgent(agentName, config, logger);
14
+ case "aider":
15
+ return new AiderAgent(agentName, config, logger);
16
+ default:
17
+ throw new Error(`Unsupported agent: ${agentName}`);
18
+ }
19
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ const cache = new Map();
7
+
8
+ const SEARCH_DIRS = [
9
+ "/opt/node/bin",
10
+ path.join(os.homedir(), ".npm-global", "bin"),
11
+ "/usr/local/bin",
12
+ path.join(os.homedir(), ".local", "bin"),
13
+ ];
14
+
15
+ function getNvmDirs() {
16
+ const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), ".nvm");
17
+ const versionsDir = path.join(nvmDir, "versions", "node");
18
+ try {
19
+ return readdirSync(versionsDir).map(v => path.join(versionsDir, v, "bin"));
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ export function resolveBin(name) {
26
+ if (cache.has(name)) return cache.get(name);
27
+
28
+ // 1. Try system PATH via `which`
29
+ try {
30
+ const resolved = execFileSync("which", [name], {
31
+ encoding: "utf8",
32
+ timeout: 3000,
33
+ stdio: ["pipe", "pipe", "pipe"],
34
+ }).trim();
35
+ if (resolved) {
36
+ cache.set(name, resolved);
37
+ return resolved;
38
+ }
39
+ } catch {
40
+ /* not in PATH */
41
+ }
42
+
43
+ // 2. Search known directories
44
+ const dirs = [...SEARCH_DIRS, ...getNvmDirs()];
45
+ for (const dir of dirs) {
46
+ const candidate = path.join(dir, name);
47
+ if (existsSync(candidate)) {
48
+ cache.set(name, candidate);
49
+ return candidate;
50
+ }
51
+ }
52
+
53
+ // 3. Fallback: return name as-is (let execa try PATH)
54
+ cache.set(name, name);
55
+ return name;
56
+ }
57
+
58
+ export function clearBinCache() {
59
+ cache.clear();
60
+ }