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.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/docs/karajan-code-logo-small.png +0 -0
- package/package.json +60 -0
- package/scripts/install.js +898 -0
- package/scripts/install.sh +7 -0
- package/scripts/postinstall.js +117 -0
- package/scripts/setup-multi-instance.sh +150 -0
- package/src/activity-log.js +59 -0
- package/src/agents/aider-agent.js +25 -0
- package/src/agents/availability.js +32 -0
- package/src/agents/base-agent.js +27 -0
- package/src/agents/claude-agent.js +24 -0
- package/src/agents/codex-agent.js +27 -0
- package/src/agents/gemini-agent.js +25 -0
- package/src/agents/index.js +19 -0
- package/src/agents/resolve-bin.js +60 -0
- package/src/cli.js +200 -0
- package/src/commands/code.js +32 -0
- package/src/commands/config.js +74 -0
- package/src/commands/doctor.js +155 -0
- package/src/commands/init.js +181 -0
- package/src/commands/plan.js +67 -0
- package/src/commands/report.js +340 -0
- package/src/commands/resume.js +39 -0
- package/src/commands/review.js +26 -0
- package/src/commands/roles.js +117 -0
- package/src/commands/run.js +91 -0
- package/src/commands/scan.js +18 -0
- package/src/commands/sonar.js +53 -0
- package/src/config.js +322 -0
- package/src/git/automation.js +100 -0
- package/src/mcp/progress.js +69 -0
- package/src/mcp/run-kj.js +87 -0
- package/src/mcp/server-handlers.js +259 -0
- package/src/mcp/server.js +37 -0
- package/src/mcp/tool-arg-normalizers.js +16 -0
- package/src/mcp/tools.js +184 -0
- package/src/orchestrator.js +1277 -0
- package/src/planning-game/adapter.js +105 -0
- package/src/planning-game/client.js +81 -0
- package/src/prompts/coder.js +60 -0
- package/src/prompts/planner.js +26 -0
- package/src/prompts/reviewer.js +45 -0
- package/src/repeat-detector.js +77 -0
- package/src/review/diff-generator.js +22 -0
- package/src/review/parser.js +93 -0
- package/src/review/profiles.js +66 -0
- package/src/review/schema.js +31 -0
- package/src/review/tdd-policy.js +57 -0
- package/src/roles/base-role.js +127 -0
- package/src/roles/coder-role.js +60 -0
- package/src/roles/commiter-role.js +94 -0
- package/src/roles/index.js +12 -0
- package/src/roles/planner-role.js +81 -0
- package/src/roles/refactorer-role.js +66 -0
- package/src/roles/researcher-role.js +134 -0
- package/src/roles/reviewer-role.js +132 -0
- package/src/roles/security-role.js +128 -0
- package/src/roles/solomon-role.js +199 -0
- package/src/roles/sonar-role.js +65 -0
- package/src/roles/tester-role.js +114 -0
- package/src/roles/triage-role.js +128 -0
- package/src/session-store.js +80 -0
- package/src/sonar/api.js +78 -0
- package/src/sonar/enforcer.js +19 -0
- package/src/sonar/manager.js +163 -0
- package/src/sonar/project-key.js +83 -0
- package/src/sonar/scanner.js +267 -0
- package/src/utils/agent-detect.js +32 -0
- package/src/utils/budget.js +123 -0
- package/src/utils/display.js +346 -0
- package/src/utils/events.js +23 -0
- package/src/utils/fs.js +19 -0
- package/src/utils/git.js +101 -0
- package/src/utils/logger.js +86 -0
- package/src/utils/paths.js +18 -0
- package/src/utils/pricing.js +28 -0
- package/src/utils/process.js +67 -0
- package/src/utils/wizard.js +41 -0
- package/templates/coder-rules.md +24 -0
- package/templates/docker-compose.sonar.yml +60 -0
- package/templates/kj.config.yml +82 -0
- package/templates/review-rules.md +11 -0
- package/templates/roles/coder.md +42 -0
- package/templates/roles/commiter.md +44 -0
- package/templates/roles/planner.md +45 -0
- package/templates/roles/refactorer.md +39 -0
- package/templates/roles/researcher.md +37 -0
- package/templates/roles/reviewer-paranoid.md +38 -0
- package/templates/roles/reviewer-relaxed.md +34 -0
- package/templates/roles/reviewer-strict.md +37 -0
- package/templates/roles/reviewer.md +55 -0
- package/templates/roles/security.md +54 -0
- package/templates/roles/solomon.md +106 -0
- package/templates/roles/sonar.md +49 -0
- package/templates/roles/tester.md +41 -0
- package/templates/roles/triage.md +25 -0
|
@@ -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
|
+
}
|