karajan-code 1.6.1 → 1.7.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/package.json +1 -1
- package/src/cli.js +7 -1
- package/src/commands/init.js +24 -25
- package/src/mcp/orphan-guard.js +27 -0
- package/src/mcp/server-handlers.js +80 -6
- package/src/mcp/server.js +19 -3
- package/src/mcp/tools.js +3 -6
- package/src/utils/display.js +8 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
2
5
|
import { Command } from "commander";
|
|
3
6
|
import { applyRunOverrides, loadConfig, validateConfig } from "./config.js";
|
|
4
7
|
import { createLogger } from "./utils/logger.js";
|
|
@@ -15,6 +18,9 @@ import { resumeCommand } from "./commands/resume.js";
|
|
|
15
18
|
import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
|
|
16
19
|
import { rolesCommand } from "./commands/roles.js";
|
|
17
20
|
|
|
21
|
+
const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
22
|
+
const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
|
|
23
|
+
|
|
18
24
|
async function withConfig(commandName, flags, fn) {
|
|
19
25
|
const { config } = await loadConfig();
|
|
20
26
|
const merged = applyRunOverrides(config, flags || {});
|
|
@@ -24,7 +30,7 @@ async function withConfig(commandName, flags, fn) {
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
const program = new Command();
|
|
27
|
-
program.name("kj").description("Karajan Code CLI").version(
|
|
33
|
+
program.name("kj").description("Karajan Code CLI").version(PKG_VERSION);
|
|
28
34
|
|
|
29
35
|
program
|
|
30
36
|
.command("init")
|
package/src/commands/init.js
CHANGED
|
@@ -33,33 +33,32 @@ async function runWizard(config, logger) {
|
|
|
33
33
|
}
|
|
34
34
|
logger.info("");
|
|
35
35
|
|
|
36
|
-
if (available.length === 1) {
|
|
37
|
-
const only = available[0].name;
|
|
38
|
-
logger.info(`Only one agent available: ${only}. Using it for all roles.\n`);
|
|
39
|
-
config.coder = only;
|
|
40
|
-
config.reviewer = only;
|
|
41
|
-
config.roles.coder.provider = only;
|
|
42
|
-
config.roles.reviewer.provider = only;
|
|
43
|
-
return config;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
36
|
const wizard = createWizard();
|
|
47
37
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
38
|
+
if (available.length === 1) {
|
|
39
|
+
const only = available[0].name;
|
|
40
|
+
logger.info(`Only one agent available: ${only}. Using it for all roles.\n`);
|
|
41
|
+
config.coder = only;
|
|
42
|
+
config.reviewer = only;
|
|
43
|
+
config.roles.coder.provider = only;
|
|
44
|
+
config.roles.reviewer.provider = only;
|
|
45
|
+
} else {
|
|
46
|
+
const agentOptions = available.map((a) => ({
|
|
47
|
+
label: `${a.name} (${a.version})`,
|
|
48
|
+
value: a.name,
|
|
49
|
+
available: true
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const coder = await wizard.select("Select default CODER agent:", agentOptions);
|
|
53
|
+
config.coder = coder;
|
|
54
|
+
config.roles.coder.provider = coder;
|
|
55
|
+
logger.info(` -> Coder: ${coder}`);
|
|
56
|
+
|
|
57
|
+
const reviewer = await wizard.select("Select default REVIEWER agent:", agentOptions);
|
|
58
|
+
config.reviewer = reviewer;
|
|
59
|
+
config.roles.reviewer.provider = reviewer;
|
|
60
|
+
logger.info(` -> Reviewer: ${reviewer}`);
|
|
61
|
+
}
|
|
63
62
|
|
|
64
63
|
const enableTriage = await wizard.confirm("Enable triage (auto-classify task complexity)?", false);
|
|
65
64
|
config.pipeline.triage = config.pipeline.triage || {};
|
package/src/mcp/orphan-guard.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
|
|
1
4
|
const DEFAULT_INTERVAL_MS = 5000;
|
|
2
5
|
|
|
3
6
|
export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = () => process.exit(0) } = {}) {
|
|
@@ -19,3 +22,27 @@ export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = ()
|
|
|
19
22
|
|
|
20
23
|
return { timer, parentPid };
|
|
21
24
|
}
|
|
25
|
+
|
|
26
|
+
export function setupVersionWatcher({ pkgPath, currentVersion, exitFn = () => process.exit(0) } = {}) {
|
|
27
|
+
if (!pkgPath) return null;
|
|
28
|
+
|
|
29
|
+
function checkVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
32
|
+
if (pkg.version !== currentVersion) {
|
|
33
|
+
exitFn();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
} catch { /* ignore read errors */ }
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let watcher = null;
|
|
41
|
+
try {
|
|
42
|
+
watcher = watch(pkgPath, { persistent: false }, () => {
|
|
43
|
+
checkVersion();
|
|
44
|
+
});
|
|
45
|
+
} catch { /* ignore watch errors */ }
|
|
46
|
+
|
|
47
|
+
return { watcher, checkVersion };
|
|
48
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
7
8
|
import { runKjCommand } from "./run-kj.js";
|
|
8
9
|
import { normalizePlanArgs } from "./tool-arg-normalizers.js";
|
|
9
10
|
import { buildProgressHandler, buildProgressNotifier } from "./progress.js";
|
|
@@ -11,6 +12,13 @@ import { runFlow, resumeFlow } from "../orchestrator.js";
|
|
|
11
12
|
import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../config.js";
|
|
12
13
|
import { createLogger } from "../utils/logger.js";
|
|
13
14
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
15
|
+
import { createAgent } from "../agents/index.js";
|
|
16
|
+
import { buildPlannerPrompt } from "../prompts/planner.js";
|
|
17
|
+
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
18
|
+
import { buildReviewerPrompt } from "../prompts/reviewer.js";
|
|
19
|
+
import { parseMaybeJsonString } from "../review/parser.js";
|
|
20
|
+
import { computeBaseRef, generateDiff } from "../review/diff-generator.js";
|
|
21
|
+
import { resolveReviewProfile } from "../review/profiles.js";
|
|
14
22
|
|
|
15
23
|
export function asObject(value) {
|
|
16
24
|
if (value && typeof value === "object") return value;
|
|
@@ -93,10 +101,10 @@ export function enrichedFailPayload(error, toolName) {
|
|
|
93
101
|
return payload;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
export async function buildConfig(options) {
|
|
104
|
+
export async function buildConfig(options, commandName = "run") {
|
|
97
105
|
const { config } = await loadConfig();
|
|
98
106
|
const merged = applyRunOverrides(config, options || {});
|
|
99
|
-
validateConfig(merged,
|
|
107
|
+
validateConfig(merged, commandName);
|
|
100
108
|
return merged;
|
|
101
109
|
}
|
|
102
110
|
|
|
@@ -173,6 +181,73 @@ export async function handleResumeDirect(a, server, extra) {
|
|
|
173
181
|
return { ok: true, ...result };
|
|
174
182
|
}
|
|
175
183
|
|
|
184
|
+
export async function handlePlanDirect(a, server, extra) {
|
|
185
|
+
const options = normalizePlanArgs(a);
|
|
186
|
+
const config = await buildConfig(options, "plan");
|
|
187
|
+
const logger = createLogger(config.output.log_level, "mcp");
|
|
188
|
+
|
|
189
|
+
const plannerRole = resolveRole(config, "planner");
|
|
190
|
+
await assertAgentsAvailable([plannerRole.provider]);
|
|
191
|
+
|
|
192
|
+
const planner = createAgent(plannerRole.provider, config, logger);
|
|
193
|
+
const prompt = buildPlannerPrompt({ task: a.task, context: a.context });
|
|
194
|
+
const result = await planner.runTask({ prompt, role: "planner" });
|
|
195
|
+
|
|
196
|
+
if (!result.ok) {
|
|
197
|
+
throw new Error(result.error || result.output || "Planner failed");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const parsed = parseMaybeJsonString(result.output);
|
|
201
|
+
return { ok: true, plan: parsed || result.output, raw: result.output };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function handleCodeDirect(a, server, extra) {
|
|
205
|
+
const config = await buildConfig(a, "code");
|
|
206
|
+
const logger = createLogger(config.output.log_level, "mcp");
|
|
207
|
+
|
|
208
|
+
const coderRole = resolveRole(config, "coder");
|
|
209
|
+
await assertAgentsAvailable([coderRole.provider]);
|
|
210
|
+
|
|
211
|
+
const coder = createAgent(coderRole.provider, config, logger);
|
|
212
|
+
let coderRules = null;
|
|
213
|
+
if (config.coder_rules) {
|
|
214
|
+
try {
|
|
215
|
+
coderRules = await fs.readFile(config.coder_rules, "utf8");
|
|
216
|
+
} catch { /* no coder rules file */ }
|
|
217
|
+
}
|
|
218
|
+
const prompt = buildCoderPrompt({ task: a.task, coderRules, methodology: config.development?.methodology || "tdd" });
|
|
219
|
+
const result = await coder.runTask({ prompt, role: "coder" });
|
|
220
|
+
|
|
221
|
+
if (!result.ok) {
|
|
222
|
+
throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { ok: true, output: result.output, exitCode: result.exitCode };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function handleReviewDirect(a, server, extra) {
|
|
229
|
+
const config = await buildConfig(a, "review");
|
|
230
|
+
const logger = createLogger(config.output.log_level, "mcp");
|
|
231
|
+
|
|
232
|
+
const reviewerRole = resolveRole(config, "reviewer");
|
|
233
|
+
await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
|
|
234
|
+
|
|
235
|
+
const reviewer = createAgent(reviewerRole.provider, config, logger);
|
|
236
|
+
const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef: a.baseRef });
|
|
237
|
+
const diff = await generateDiff({ baseRef: resolvedBase });
|
|
238
|
+
const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
|
|
239
|
+
|
|
240
|
+
const prompt = buildReviewerPrompt({ task: a.task, diff, reviewRules: rules, mode: config.review_mode });
|
|
241
|
+
const result = await reviewer.reviewTask({ prompt, role: "reviewer" });
|
|
242
|
+
|
|
243
|
+
if (!result.ok) {
|
|
244
|
+
throw new Error(result.error || result.output || `Reviewer failed (exit ${result.exitCode})`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const parsed = parseMaybeJsonString(result.output);
|
|
248
|
+
return { ok: true, review: parsed || result.output, raw: result.output };
|
|
249
|
+
}
|
|
250
|
+
|
|
176
251
|
export async function handleToolCall(name, args, server, extra) {
|
|
177
252
|
const a = asObject(args);
|
|
178
253
|
|
|
@@ -240,22 +315,21 @@ export async function handleToolCall(name, args, server, extra) {
|
|
|
240
315
|
if (!a.task) {
|
|
241
316
|
return failPayload("Missing required field: task");
|
|
242
317
|
}
|
|
243
|
-
return
|
|
318
|
+
return handleCodeDirect(a, server, extra);
|
|
244
319
|
}
|
|
245
320
|
|
|
246
321
|
if (name === "kj_review") {
|
|
247
322
|
if (!a.task) {
|
|
248
323
|
return failPayload("Missing required field: task");
|
|
249
324
|
}
|
|
250
|
-
return
|
|
325
|
+
return handleReviewDirect(a, server, extra);
|
|
251
326
|
}
|
|
252
327
|
|
|
253
328
|
if (name === "kj_plan") {
|
|
254
329
|
if (!a.task) {
|
|
255
330
|
return failPayload("Missing required field: task");
|
|
256
331
|
}
|
|
257
|
-
|
|
258
|
-
return runKjCommand({ command: "plan", commandArgs: [a.task], options });
|
|
332
|
+
return handlePlanDirect(a, server, extra);
|
|
259
333
|
}
|
|
260
334
|
|
|
261
335
|
return failPayload(`Unknown tool: ${name}`);
|
package/src/mcp/server.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
2
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
7
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
8
|
import { tools } from "./tools.js";
|
|
6
9
|
import { handleToolCall, responseText, enrichedFailPayload } from "./server-handlers.js";
|
|
7
10
|
|
|
11
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PKG_PATH = path.resolve(MODULE_DIR, "../../package.json");
|
|
13
|
+
|
|
14
|
+
function readVersion() {
|
|
15
|
+
return JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LOADED_VERSION = readVersion();
|
|
19
|
+
|
|
8
20
|
const server = new Server(
|
|
9
21
|
{
|
|
10
22
|
name: "karajan-mcp",
|
|
11
|
-
version:
|
|
23
|
+
version: LOADED_VERSION
|
|
12
24
|
},
|
|
13
25
|
{
|
|
14
26
|
capabilities: {
|
|
@@ -22,6 +34,9 @@ const server = new Server(
|
|
|
22
34
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
23
35
|
|
|
24
36
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
37
|
+
// Auto-exit if package version changed (stale process)
|
|
38
|
+
if (readVersion() !== LOADED_VERSION) process.exit(0);
|
|
39
|
+
|
|
25
40
|
const name = request.params?.name;
|
|
26
41
|
const args = request.params?.arguments || {};
|
|
27
42
|
|
|
@@ -33,9 +48,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
33
48
|
}
|
|
34
49
|
});
|
|
35
50
|
|
|
36
|
-
// --- Orphan process protection ---
|
|
37
|
-
import { setupOrphanGuard } from "./orphan-guard.js";
|
|
51
|
+
// --- Orphan process protection + version watcher ---
|
|
52
|
+
import { setupOrphanGuard, setupVersionWatcher } from "./orphan-guard.js";
|
|
38
53
|
setupOrphanGuard();
|
|
54
|
+
setupVersionWatcher({ pkgPath: PKG_PATH, currentVersion: LOADED_VERSION });
|
|
39
55
|
|
|
40
56
|
const transport = new StdioServerTransport();
|
|
41
57
|
await server.connect(transport);
|
package/src/mcp/tools.js
CHANGED
|
@@ -146,8 +146,7 @@ export const tools = [
|
|
|
146
146
|
task: { type: "string" },
|
|
147
147
|
coder: { type: "string" },
|
|
148
148
|
coderModel: { type: "string" },
|
|
149
|
-
kjHome: { type: "string" }
|
|
150
|
-
timeoutMs: { type: "number" }
|
|
149
|
+
kjHome: { type: "string" }
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
152
|
},
|
|
@@ -162,8 +161,7 @@ export const tools = [
|
|
|
162
161
|
reviewer: { type: "string" },
|
|
163
162
|
reviewerModel: { type: "string" },
|
|
164
163
|
baseRef: { type: "string" },
|
|
165
|
-
kjHome: { type: "string" }
|
|
166
|
-
timeoutMs: { type: "number" }
|
|
164
|
+
kjHome: { type: "string" }
|
|
167
165
|
}
|
|
168
166
|
}
|
|
169
167
|
},
|
|
@@ -179,8 +177,7 @@ export const tools = [
|
|
|
179
177
|
plannerModel: { type: "string" },
|
|
180
178
|
coder: { type: "string", description: "Legacy alias for planner" },
|
|
181
179
|
coderModel: { type: "string", description: "Legacy alias for plannerModel" },
|
|
182
|
-
kjHome: { type: "string" }
|
|
183
|
-
timeoutMs: { type: "number" }
|
|
180
|
+
kjHome: { type: "string" }
|
|
184
181
|
}
|
|
185
182
|
}
|
|
186
183
|
}
|
package/src/utils/display.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
6
|
+
const DISPLAY_VERSION = JSON.parse(readFileSync(DISPLAY_PKG_PATH, "utf8")).version;
|
|
7
|
+
|
|
1
8
|
const ANSI = {
|
|
2
9
|
reset: "\x1b[0m",
|
|
3
10
|
bold: "\x1b[1m",
|
|
@@ -57,7 +64,7 @@ export function formatElapsed(ms) {
|
|
|
57
64
|
const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${ANSI.reset}`;
|
|
58
65
|
|
|
59
66
|
export function printHeader({ task, config }) {
|
|
60
|
-
const version =
|
|
67
|
+
const version = DISPLAY_VERSION;
|
|
61
68
|
console.log(BAR);
|
|
62
69
|
console.log(`${ANSI.bold}${ANSI.cyan}\u25b6 Karajan Code v${version}${ANSI.reset}`);
|
|
63
70
|
console.log(BAR);
|