karajan-code 1.6.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
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("1.6.2");
33
+ program.name("kj").description("Karajan Code CLI").version(PKG_VERSION);
28
34
 
29
35
  program
30
36
  .command("init")
@@ -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, "run");
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 runKjCommand({ command: "code", commandArgs: [a.task], options: a });
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 runKjCommand({ command: "review", commandArgs: [a.task], options: a });
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
- const options = normalizePlanArgs(a);
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: "1.0.0"
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
  }
@@ -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 = "0.1.0";
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);