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 +1 -1
- package/src/agents/claude-agent.js +36 -5
- package/src/cli.js +1 -0
- package/src/domains/domain-loader.js +85 -4
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/orchestrator.js +10 -0
- package/src/proxy/proxy-lifecycle.js +3 -0
- package/src/roles/impeccable-role.js +2 -4
- package/src/roles/researcher-role.js +2 -4
- package/src/roles/reviewer-role.js +4 -4
- package/src/roles/solomon-role.js +2 -4
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +2 -3
- package/src/utils/welcome.js +3 -8
package/package.json
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
},
|
package/src/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/display.js
CHANGED
|
@@ -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
|
-
|
|
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}`
|
package/src/utils/welcome.js
CHANGED
|
@@ -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
|
-
|
|
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";
|