karajan-code 1.59.0 → 1.59.1

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.59.0",
3
+ "version": "1.59.1",
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",
@@ -1,7 +1,6 @@
1
1
  import { BaseAgent } from "./base-agent.js";
2
2
  import { runCommand } from "../utils/process.js";
3
3
  import { resolveBin } from "./resolve-bin.js";
4
- import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
5
4
 
6
5
  /**
7
6
  * Safely parse a JSON line, returning null on failure.
@@ -12,6 +11,37 @@ function tryParseJson(line) {
12
11
  } catch { return null; }
13
12
  }
14
13
 
14
+ /**
15
+ * Extract a human-readable error message from Claude's raw NDJSON stderr output.
16
+ * Claude emits system init, api_retry, assistant error, and result lines.
17
+ * We extract just the meaningful error text, discarding the JSON noise.
18
+ */
19
+ export function sanitizeClaudeError(raw) {
20
+ if (!raw) return "Unknown error";
21
+ const lines = raw.split("\n").filter(Boolean);
22
+
23
+ // Look for a result line with the actual error
24
+ for (let i = lines.length - 1; i >= 0; i--) {
25
+ const obj = tryParseJson(lines[i]);
26
+ if (!obj) continue;
27
+ if (obj.type === "result" && obj.result) return String(obj.result);
28
+ if (obj.type === "assistant" && obj.message?.content) {
29
+ const text = obj.message.content
30
+ .filter(b => b.type === "text" && b.text)
31
+ .map(b => b.text)
32
+ .join(" ");
33
+ if (text) return text;
34
+ }
35
+ }
36
+
37
+ // If no JSON parsed, return first non-JSON line or truncated raw
38
+ for (const line of lines) {
39
+ if (!line.startsWith("{")) return line.slice(0, 200);
40
+ }
41
+
42
+ return raw.slice(0, 200);
43
+ }
44
+
15
45
  /**
16
46
  * Try to extract a result string from a parsed JSON object.
17
47
  * Returns the result string or null if the object is not a result message.
@@ -135,8 +165,9 @@ function createStreamJsonFilter(onOutput) {
135
165
  */
136
166
  function cleanExecaOpts(extra = {}) {
137
167
  const { CLAUDECODE, ...env } = process.env;
138
- const proxyEnv = getProxyEnv();
139
- if (proxyEnv) Object.assign(env, proxyEnv);
168
+ // NOTE: Claude Code CLI does not respect ANTHROPIC_BASE_URL — proxy env
169
+ // injection is skipped for claude to avoid ECONNREFUSED errors.
170
+ // Other agents (codex, gemini, aider) use SDKs that do respect base URL vars.
140
171
  return { env, stdin: "ignore", ...extra };
141
172
  }
142
173
 
@@ -197,7 +228,7 @@ export class ClaudeAgent extends BaseAgent {
197
228
  const raw = pickOutput(res);
198
229
  const output = extractTextFromStreamJson(raw);
199
230
  const usage = extractUsageFromStreamJson(raw);
200
- return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
231
+ return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : sanitizeClaudeError(raw), exitCode: res.exitCode, ...usage };
201
232
  }
202
233
 
203
234
  // Without streaming, use json output to get structured response via stderr
@@ -206,7 +237,7 @@ export class ClaudeAgent extends BaseAgent {
206
237
  const raw = pickOutput(res);
207
238
  const output = extractTextFromStreamJson(raw);
208
239
  const usage = extractUsageFromStreamJson(raw);
209
- return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
240
+ return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : sanitizeClaudeError(raw), exitCode: res.exitCode, ...usage };
210
241
  }
211
242
 
212
243
  async _reviewTaskExec(task, model) {
package/src/cli.js CHANGED
@@ -127,6 +127,7 @@ program
127
127
  .option("--smart-models", "Enable smart model selection based on triage complexity")
128
128
  .option("--no-smart-models", "Disable smart model selection")
129
129
  .option("--design", "Activate design refactoring mode (impeccable role applies design changes)")
130
+ .option("--domain <text-or-path>", "Domain knowledge: inline text or path to .md file")
130
131
  .option("--dry-run", "Show what would be executed without running anything")
131
132
  .option("--json", "Output JSON only (no styled display)")
132
133
  .option("-q, --quiet", "Show only stage status lines, suppress raw agent output (default)")
@@ -5,8 +5,8 @@
5
5
  * Project-local domains override user-global domains by directory name.
6
6
  */
7
7
 
8
- import { readdir, readFile } from "node:fs/promises";
9
- import { join } from "node:path";
8
+ import { readdir, readFile, mkdir, writeFile, stat } from "node:fs/promises";
9
+ import { join, isAbsolute } from "node:path";
10
10
  import yaml from "js-yaml";
11
11
  import { getKarajanHome } from "../utils/paths.js";
12
12
 
@@ -103,8 +103,52 @@ function parseSections(content) {
103
103
  return sections;
104
104
  }
105
105
 
106
+ /**
107
+ * Auto-detect project context files (README.md, CLAUDE.md) as lightweight domains.
108
+ * Used as fallback when no explicit .karajan/domains/ are configured.
109
+ *
110
+ * @param {string} projectDir — absolute path to project root
111
+ * @returns {Promise<DomainFile[]>}
112
+ */
113
+ export async function autoDetectDomains(projectDir) {
114
+ if (!projectDir) return [];
115
+
116
+ const candidates = ["README.md", "CLAUDE.md", "docs/README.md"];
117
+ const detected = [];
118
+
119
+ for (const rel of candidates) {
120
+ const filePath = join(projectDir, rel);
121
+ let content;
122
+ try {
123
+ content = await readFile(filePath, "utf-8");
124
+ } catch { continue; }
125
+
126
+ if (!content || content.trim().length < 20) continue;
127
+
128
+ // Truncate to first 4000 chars to avoid bloating context
129
+ const trimmed = content.length > 4000 ? content.slice(0, 4000) + "\n\n[...truncated]" : content;
130
+
131
+ detected.push({
132
+ name: `auto:${rel}`,
133
+ description: `Auto-detected from ${rel}`,
134
+ tags: [],
135
+ version: "0.0.0",
136
+ author: "",
137
+ visibility: "private",
138
+ sources: [{ type: "auto-detect", note: rel }],
139
+ content: trimmed,
140
+ sections: parseSections(trimmed),
141
+ filePath,
142
+ origin: "auto-detect"
143
+ });
144
+ }
145
+
146
+ return detected;
147
+ }
148
+
106
149
  /**
107
150
  * Load all domains from project-local and user-global directories.
151
+ * Falls back to auto-detecting README.md/CLAUDE.md when no explicit domains found.
108
152
  * Project-local domains override user-global domains when directory names match.
109
153
  *
110
154
  * @param {string|null} projectDir — absolute path to project root (null = user-global only)
@@ -119,7 +163,6 @@ export async function loadDomains(projectDir) {
119
163
  const projectDomains = projectDomainDir ? await scanDomainDir(projectDomainDir, "project") : [];
120
164
 
121
165
  // Build a map keyed by directory name for merge.
122
- // We track by directory name (not domain name) because the override is by directory.
123
166
  const merged = new Map();
124
167
 
125
168
  for (const { dirName, domain } of userDomains) {
@@ -129,7 +172,14 @@ export async function loadDomains(projectDir) {
129
172
  merged.set(dirName, domain); // project overrides user
130
173
  }
131
174
 
132
- return Array.from(merged.values());
175
+ const explicit = Array.from(merged.values());
176
+
177
+ // Fallback: auto-detect README.md, CLAUDE.md when no explicit domains found
178
+ if (explicit.length === 0 && projectDir) {
179
+ return autoDetectDomains(projectDir);
180
+ }
181
+
182
+ return explicit;
133
183
  }
134
184
 
135
185
  /**
@@ -162,3 +212,34 @@ async function scanDomainDir(dir, origin) {
162
212
 
163
213
  return results;
164
214
  }
215
+
216
+ /**
217
+ * Persist an inline domain (text or file path) to .karajan/domains/inline/DOMAIN.md.
218
+ * If domainInput is a path to an existing .md file, reads its content.
219
+ * Otherwise treats it as inline text.
220
+ *
221
+ * @param {string} domainInput — inline text or absolute path to a .md file
222
+ * @param {string} projectDir — project root
223
+ */
224
+ export async function persistInlineDomain(domainInput, projectDir) {
225
+ if (!domainInput || !projectDir) return;
226
+
227
+ let content = domainInput;
228
+
229
+ // If it looks like a file path, try to read it
230
+ if (isAbsolute(domainInput) && domainInput.endsWith(".md")) {
231
+ try {
232
+ const s = await stat(domainInput);
233
+ if (s.isFile()) {
234
+ content = await readFile(domainInput, "utf-8");
235
+ }
236
+ } catch { /* treat as inline text */ }
237
+ }
238
+
239
+ const domainDir = join(projectDir, ".karajan", "domains", "inline");
240
+ await mkdir(domainDir, { recursive: true });
241
+
242
+ const domainFile = join(domainDir, DOMAIN_FILE);
243
+ const wrapped = `---\nname: inline-domain\ndescription: Domain knowledge provided via --domain parameter\ntags: []\nversion: "0.0.0"\n---\n\n${content}\n`;
244
+ await writeFile(domainFile, wrapped, "utf-8");
245
+ }
@@ -24,7 +24,7 @@ const ALLOWED_PARAMS = new Set([
24
24
  "autoCommit", "autoPush", "autoPr", "autoRebase", "branchPrefix",
25
25
  "autoSimplify", "smartModels", "checkpointInterval",
26
26
  "taskType", "quiet", "noSonar", "enableSonarcloud",
27
- "kjHome", "sonarToken", "timeoutMs",
27
+ "kjHome", "sonarToken", "timeoutMs", "domain",
28
28
  // Sovereign flags — validated below but still "known"
29
29
  "enableTriage", "enableHuReviewer",
30
30
  ]);
package/src/mcp/tools.js CHANGED
@@ -104,7 +104,8 @@ export const tools = [
104
104
  enableSonarcloud: { type: "boolean", description: "Enable SonarCloud scan (complementary to SonarQube)" },
105
105
  kjHome: { type: "string" },
106
106
  sonarToken: { type: "string" },
107
- timeoutMs: { type: "number" }
107
+ timeoutMs: { type: "number" },
108
+ domain: { type: "string", description: "Domain knowledge: inline text describing the project domain, or absolute path to a .md file. Auto-saved to .karajan/domains/ for the curator." }
108
109
  }
109
110
  }
110
111
  },
@@ -35,6 +35,7 @@ import { msg, getLang } from "./utils/messages.js";
35
35
  import { PipelineContext } from "./orchestrator/pipeline-context.js";
36
36
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
37
37
  import { runDomainCuratorStage } from "./orchestrator/stages/domain-curator-stage.js";
38
+ import { persistInlineDomain } from "./domains/domain-loader.js";
38
39
  import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
39
40
  import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
40
41
  import { needsSubPipeline, runHuSubPipeline } from "./orchestrator/hu-sub-pipeline.js";
@@ -940,6 +941,15 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
940
941
  logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
941
942
  }
942
943
 
944
+ // --- Persist inline domain if provided via --domain flag ---
945
+ if (flags?.domain) {
946
+ try {
947
+ await persistInlineDomain(flags.domain, config.projectDir || process.cwd());
948
+ } catch (err) {
949
+ logger.warn(`Failed to persist inline domain: ${err.message}`);
950
+ }
951
+ }
952
+
943
953
  // --- Domain Curator (after triage + skill auto-install, before planning phases) ---
944
954
  const domainHints = triageResult.stageResult?.domainHints || [];
945
955
  if (domainHints.length > 0 || config.projectDir) {
@@ -144,6 +144,9 @@ export function getProxyEnv() {
144
144
 
145
145
  const base = `http://127.0.0.1:${proxyPort}`;
146
146
  return {
147
+ // SDK-specific base URL vars — used by SDKs that respect them.
148
+ // Claude Code CLI does NOT respect ANTHROPIC_BASE_URL, so claude-agent
149
+ // must skip proxy injection until upstream support is confirmed.
147
150
  ANTHROPIC_BASE_URL: base,
148
151
  OPENAI_BASE_URL: base,
149
152
  GEMINI_API_BASE: base,
@@ -1,6 +1,7 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
3
  import { isDevToolsMcpAvailable, ensureWebPerfSkills } from "../webperf/devtools-detect.js";
4
+ import { extractFirstJson } from "../utils/json-extract.js";
4
5
 
5
6
  const SUBAGENT_PREAMBLE = [
6
7
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -35,10 +36,7 @@ function buildPrompt({ task, diff, instructions }) {
35
36
  }
36
37
 
37
38
  function parseImpeccableOutput(raw) {
38
- const text = raw?.trim() || "";
39
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
40
- if (!jsonMatch) return null;
41
- return JSON.parse(jsonMatch[0]);
39
+ return extractFirstJson(raw);
42
40
  }
43
41
 
44
42
  function buildSummary(parsed) {
@@ -1,5 +1,6 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { extractFirstJson } from "../utils/json-extract.js";
3
4
 
4
5
  const SUBAGENT_PREAMBLE = [
5
6
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -34,10 +35,7 @@ function buildPrompt({ task, instructions }) {
34
35
  }
35
36
 
36
37
  function parseResearchOutput(raw) {
37
- const text = raw?.trim() || "";
38
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
39
- if (!jsonMatch) return null;
40
- return JSON.parse(jsonMatch[0]);
38
+ return extractFirstJson(raw);
41
39
  }
42
40
 
43
41
  function buildSummary(parsed) {
@@ -1,6 +1,7 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
3
  import { buildRtkInstructions } from "../prompts/rtk-snippet.js";
4
+ import { extractFirstJson } from "../utils/json-extract.js";
4
5
 
5
6
  const MAX_DIFF_LENGTH = 12000;
6
7
 
@@ -65,12 +66,11 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAva
65
66
  }
66
67
 
67
68
  function parseReviewOutput(raw) {
68
- const text = raw?.trim() || "";
69
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
70
- if (!jsonMatch) {
69
+ const parsed = extractFirstJson(raw);
70
+ if (!parsed) {
71
71
  throw new Error(`Failed to parse reviewer output: no JSON found`);
72
72
  }
73
- return JSON.parse(jsonMatch[0]);
73
+ return parsed;
74
74
  }
75
75
 
76
76
  export class ReviewerRole extends BaseRole {
@@ -1,5 +1,6 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { extractFirstJson } from "../utils/json-extract.js";
3
4
 
4
5
  const SUBAGENT_PREAMBLE = [
5
6
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -98,10 +99,7 @@ function buildPrompt({ conflict, task, instructions }) {
98
99
  }
99
100
 
100
101
  function parseSolomonOutput(raw) {
101
- const text = raw?.trim() || "";
102
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
103
- if (!jsonMatch) return null;
104
- return JSON.parse(jsonMatch[0]);
102
+ return extractFirstJson(raw);
105
103
  }
106
104
 
107
105
  function buildSummary(parsed) {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Karajan Code ASCII banner with brand colors.
3
+ *
4
+ * Brand palette:
5
+ * #AACCEE steel blue (primary text)
6
+ * #88CC88 green (accents, robot eyes)
7
+ * #464000 dark olive
8
+ * #444411 charcoal (secondary text)
9
+ */
10
+
11
+ const C = {
12
+ blue: "\x1b[38;2;170;204;238m", // #AACCEE
13
+ green: "\x1b[38;2;100;190;100m", // green accents
14
+ gray: "\x1b[38;2;140;140;140m", // borders/dim
15
+ white: "\x1b[38;2;220;220;220m", // bright text
16
+ charcoal: "\x1b[38;2;100;100;80m", // #444411 approx
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[2m",
19
+ reset: "\x1b[0m"
20
+ };
21
+
22
+ const R = C.reset;
23
+
24
+ const BANNER_LINES = [
25
+ ``,
26
+ `${C.gray} ╭───────────╮`,
27
+ `${C.gray} │ ${C.green}~${C.gray} │`,
28
+ `${C.gray} │ ${C.green}|${C.gray} ${C.green}|${C.gray} │ ${C.charcoal}|`,
29
+ `${C.gray} | ${C.green}v${C.gray} │ ${C.charcoal}/`,
30
+ `${C.gray} ${C.charcoal}\\${C.gray} ╰───────────╯${C.charcoal}/`,
31
+ `${C.charcoal} \\${C.gray} ╭───${C.white}⋈${C.gray}───╮ ${C.charcoal}/`,
32
+ `${C.charcoal} \\${C.gray} ${C.charcoal}/`,
33
+ `${C.gray} ╰───────╯`,
34
+ ``,
35
+ ` ${C.bold}${C.blue}╦╔═ ╔═╗ ╦═╗ ╔═╗ ╦ ╔═╗ ╔╗╔${R}`,
36
+ ` ${C.bold}${C.blue}╠╩╗ ╠═╣ ╠╦╝ ╠═╣ ║ ╠═╣ ║║║${R}`,
37
+ ` ${C.bold}${C.blue}╩ ╩ ╩ ╩ ╩╚═ ╩ ╩ ╚╝ ╩ ╩ ╝╚╝${R}`,
38
+ ` ${C.bold}${C.white} C O D E${R} %%VERSION%%`,
39
+ ` ${C.charcoal}multiagent coding orchestrator${R}`,
40
+ ``,
41
+ ];
42
+
43
+ /**
44
+ * Print the Karajan Code ASCII banner.
45
+ * @param {string} version - Package version to display
46
+ * @param {object} [opts]
47
+ * @param {boolean} [opts.compact] - Skip mascot, show only text
48
+ * @param {boolean} [opts.force] - Print even without TTY
49
+ */
50
+ export function printBanner(version, opts = {}) {
51
+ if (!opts.force && !process.stdout.isTTY) return;
52
+
53
+ const versionTag = `${C.dim}v${version}${R}`;
54
+
55
+ if (opts.compact) {
56
+ console.log();
57
+ for (const line of BANNER_LINES.slice(10)) {
58
+ console.log(line.replace("%%VERSION%%", versionTag));
59
+ }
60
+ return;
61
+ }
62
+
63
+ for (const line of BANNER_LINES) {
64
+ console.log(line.replace("%%VERSION%%", versionTag));
65
+ }
66
+ }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { printBanner } from "./banner.js";
4
5
 
5
6
  // TODO: i18n display messages
6
7
  const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
@@ -83,9 +84,7 @@ const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
83
84
 
84
85
  export function printHeader({ task, config }) {
85
86
  const version = DISPLAY_VERSION;
86
- console.log(BAR);
87
- console.log(`${ANSI.bold}${ANSI.cyan}\u25b6 Karajan Code v${version}${ANSI.reset}`);
88
- console.log(BAR);
87
+ printBanner(version);
89
88
  console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
90
89
  console.log(
91
90
  `${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
@@ -1,3 +1,5 @@
1
+ import { printBanner } from "./banner.js";
2
+
1
3
  const A = {
2
4
  reset: "\x1b[0m",
3
5
  bold: "\x1b[1m",
@@ -7,8 +9,6 @@ const A = {
7
9
  yellow: "\x1b[33m",
8
10
  };
9
11
 
10
- const BAR = `${A.dim}${"─".repeat(44)}${A.reset}`;
11
-
12
12
  const QUICK_START = [
13
13
  ["kj run <task>", "Run the full coder+reviewer pipeline"],
14
14
  ["kj init", "Initialize config and setup"],
@@ -24,12 +24,7 @@ const QUICK_START = [
24
24
  * @param {object} [opts.config] - Loaded KJ config (optional, shows configured agents)
25
25
  */
26
26
  export function printWelcomeScreen({ version, config = null }) {
27
- console.log();
28
- console.log(BAR);
29
- console.log(` ${A.bold}${A.cyan}▶ Karajan Code${A.reset} ${A.dim}v${version}${A.reset}`);
30
- console.log(` ${A.dim}AI-powered coding pipeline${A.reset}`);
31
- console.log(BAR);
32
- console.log();
27
+ printBanner(version, { force: true });
33
28
 
34
29
  if (config) {
35
30
  const coder = config.roles?.coder?.provider || config.coder || "claude";