karajan-code 1.58.2 → 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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/agents/aider-agent.js +7 -0
  3. package/src/agents/claude-agent.js +36 -2
  4. package/src/agents/codex-agent.js +4 -0
  5. package/src/agents/gemini-agent.js +4 -0
  6. package/src/agents/opencode-agent.js +4 -0
  7. package/src/bootstrap.js +20 -9
  8. package/src/cli.js +1 -0
  9. package/src/commands/doctor.js +19 -0
  10. package/src/commands/report.js +13 -0
  11. package/src/commands/status.js +7 -0
  12. package/src/config.js +35 -0
  13. package/src/domains/domain-loader.js +85 -4
  14. package/src/git/automation.js +32 -4
  15. package/src/guards/policy-guard.js +90 -0
  16. package/src/mcp/handlers/run-handler.js +20 -3
  17. package/src/mcp/shared-helpers.js +4 -2
  18. package/src/mcp/sovereignty-guard.js +1 -1
  19. package/src/mcp/tools.js +2 -1
  20. package/src/orchestrator.js +77 -20
  21. package/src/prompts/coder.js +5 -4
  22. package/src/prompts/reviewer.js +5 -4
  23. package/src/prompts/rtk-snippet.js +12 -0
  24. package/src/proxy/adapters/anthropic.js +131 -0
  25. package/src/proxy/adapters/gemini.js +106 -0
  26. package/src/proxy/adapters/index.js +51 -0
  27. package/src/proxy/adapters/openai.js +71 -0
  28. package/src/proxy/compression/ai-compressor.js +209 -0
  29. package/src/proxy/compression/dedup-cache.js +100 -0
  30. package/src/proxy/compression/deterministic/bash-build.js +60 -0
  31. package/src/proxy/compression/deterministic/bash-git.js +101 -0
  32. package/src/proxy/compression/deterministic/bash-infra.js +64 -0
  33. package/src/proxy/compression/deterministic/bash-misc.js +53 -0
  34. package/src/proxy/compression/deterministic/bash-pkg.js +49 -0
  35. package/src/proxy/compression/deterministic/bash-test.js +61 -0
  36. package/src/proxy/compression/deterministic/glob.js +26 -0
  37. package/src/proxy/compression/deterministic/grep.js +53 -0
  38. package/src/proxy/compression/deterministic/index.js +58 -0
  39. package/src/proxy/compression/deterministic/read.js +22 -0
  40. package/src/proxy/compression/deterministic/utils.js +33 -0
  41. package/src/proxy/compression/pipeline.js +270 -0
  42. package/src/proxy/middleware/prompt-injector.js +165 -0
  43. package/src/proxy/middleware/response-interceptor.js +223 -0
  44. package/src/proxy/progress-monitor.js +98 -0
  45. package/src/proxy/proxy-lifecycle.js +201 -0
  46. package/src/proxy/proxy-server.js +239 -0
  47. package/src/proxy/security.js +45 -0
  48. package/src/review/diff-generator.js +10 -8
  49. package/src/roles/coder-role.js +1 -0
  50. package/src/roles/impeccable-role.js +2 -4
  51. package/src/roles/researcher-role.js +2 -4
  52. package/src/roles/reviewer-role.js +10 -8
  53. package/src/roles/solomon-role.js +2 -4
  54. package/src/session-cleanup.js +39 -15
  55. package/src/sonar/cloud-scanner.js +6 -3
  56. package/src/sonar/scanner.js +12 -7
  57. package/src/utils/banner.js +66 -0
  58. package/src/utils/display.js +46 -7
  59. package/src/utils/git.js +5 -0
  60. package/src/utils/welcome.js +3 -8
@@ -0,0 +1,239 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { isRequestTooLarge } from "./security.js";
4
+
5
+ /**
6
+ * Create a lightweight HTTP forward proxy with middleware pipeline.
7
+ *
8
+ * The proxy receives plain HTTP on localhost, detects the AI provider from the
9
+ * Host header, runs the middleware chain, then forwards the request as HTTPS
10
+ * (or HTTP for testing) to the real API endpoint.
11
+ *
12
+ * @param {object} options
13
+ * @param {number} options.port - Port to listen on (0 = OS-assigned)
14
+ * @param {Record<string, string>} options.targetHosts - Map host → provider name
15
+ * @param {object} [options._testTarget] - Override target for testing (hostname, port, protocol)
16
+ * @returns {ProxyServer}
17
+ */
18
+ export function createProxyServer({ port = 0, targetHosts = {}, _testTarget } = {}) {
19
+ const middlewares = [];
20
+ const startTime = Date.now();
21
+ const stats = { requests: 0, bytes_in: 0, bytes_out: 0 };
22
+ let activeConnections = 0;
23
+ let closing = false;
24
+ let closeResolve = null;
25
+
26
+ function detectProvider(host) {
27
+ // Strip port from host if present
28
+ const hostname = (host || "").split(":")[0];
29
+ return targetHosts[hostname] || "unknown";
30
+ }
31
+
32
+ /**
33
+ * Compose middlewares into a single function using Koa-style next() chaining.
34
+ */
35
+ function compose(mws, finalHandler) {
36
+ return function run(ctx) {
37
+ let index = -1;
38
+ function dispatch(i) {
39
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
40
+ index = i;
41
+ const fn = i < mws.length ? mws[i] : finalHandler;
42
+ if (!fn) return Promise.resolve();
43
+ return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
44
+ }
45
+ return dispatch(0);
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Forward the request to the upstream target and stream the response back.
51
+ */
52
+ function forwardRequest(ctx) {
53
+ return new Promise((resolve, reject) => {
54
+ const { req, res } = ctx;
55
+ const host = (req.headers.host || "").split(":")[0];
56
+ const body = ctx.modifiedBody != null ? ctx.modifiedBody : ctx.body;
57
+
58
+ // Determine target
59
+ let targetHostname, targetPort, useHttps;
60
+ if (_testTarget) {
61
+ targetHostname = _testTarget.hostname;
62
+ targetPort = _testTarget.port;
63
+ useHttps = _testTarget.protocol === "https:";
64
+ } else {
65
+ targetHostname = host;
66
+ targetPort = 443;
67
+ useHttps = true;
68
+ }
69
+
70
+ const transport = useHttps ? https : http;
71
+
72
+ // Clone headers, remove hop-by-hop headers, update content-length
73
+ const headers = { ...req.headers };
74
+ delete headers["transfer-encoding"];
75
+ delete headers["connection"];
76
+ delete headers["proxy-connection"];
77
+ delete headers["keep-alive"];
78
+ if (body != null) {
79
+ headers["content-length"] = String(Buffer.byteLength(body));
80
+ }
81
+
82
+ const proxyReq = transport.request(
83
+ {
84
+ hostname: targetHostname,
85
+ port: targetPort,
86
+ path: req.url,
87
+ method: req.method,
88
+ headers,
89
+ },
90
+ (proxyRes) => {
91
+ // Track bytes out by instrumenting the response
92
+ proxyRes.on("data", (chunk) => {
93
+ stats.bytes_out += chunk.length;
94
+ });
95
+
96
+ // Stream the response back — write head then pipe
97
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
98
+ proxyRes.pipe(res);
99
+ proxyRes.on("end", resolve);
100
+ proxyRes.on("error", reject);
101
+ },
102
+ );
103
+
104
+ proxyReq.on("error", (err) => {
105
+ if (!res.headersSent) {
106
+ res.writeHead(502, { "content-type": "application/json" });
107
+ res.end(JSON.stringify({ error: "Bad Gateway", message: err.message }));
108
+ }
109
+ resolve(); // Don't reject — we handled the error via 502
110
+ });
111
+
112
+ if (body) {
113
+ proxyReq.write(body);
114
+ }
115
+ proxyReq.end();
116
+ });
117
+ }
118
+
119
+ function handleRequest(req, res) {
120
+ // Health check — bypass middleware and proxy
121
+ if (req.url === "/_kj/health") {
122
+ res.writeHead(200, { "content-type": "application/json" });
123
+ res.end(
124
+ JSON.stringify({
125
+ status: "ok",
126
+ uptime: Math.round((Date.now() - startTime) / 1000),
127
+ requests: stats.requests,
128
+ }),
129
+ );
130
+ return;
131
+ }
132
+
133
+ // Reject oversized requests early (50 MB default)
134
+ const MAX_BODY_BYTES = 50 * 1024 * 1024;
135
+ if (isRequestTooLarge(req.headers["content-length"], MAX_BODY_BYTES)) {
136
+ res.writeHead(413, { "content-type": "application/json" });
137
+ res.end(JSON.stringify({ error: "Payload Too Large", maxBytes: MAX_BODY_BYTES }));
138
+ return;
139
+ }
140
+
141
+ activeConnections++;
142
+ stats.requests++;
143
+
144
+ // Collect body — enforce streaming size limit even without Content-Length
145
+ let bodySize = 0;
146
+ const chunks = [];
147
+ req.on("data", (c) => {
148
+ bodySize += c.length;
149
+ if (bodySize > MAX_BODY_BYTES) {
150
+ res.writeHead(413, { "content-type": "application/json" });
151
+ res.end(JSON.stringify({ error: "Payload Too Large", maxBytes: MAX_BODY_BYTES }));
152
+ req.destroy();
153
+ return;
154
+ }
155
+ chunks.push(c);
156
+ stats.bytes_in += c.length;
157
+ });
158
+ req.on("end", () => {
159
+ const body = Buffer.concat(chunks).toString();
160
+ const provider = detectProvider(req.headers.host);
161
+
162
+ const ctx = {
163
+ req,
164
+ res,
165
+ body,
166
+ provider,
167
+ modifiedBody: null,
168
+ };
169
+
170
+ const chain = compose(middlewares, (c) => forwardRequest(c));
171
+ chain(ctx)
172
+ .catch((err) => {
173
+ if (!res.headersSent) {
174
+ res.writeHead(500, { "content-type": "application/json" });
175
+ res.end(JSON.stringify({ error: "Internal Server Error", message: err.message }));
176
+ }
177
+ })
178
+ .finally(() => {
179
+ activeConnections--;
180
+ if (closing && activeConnections === 0 && closeResolve) {
181
+ closeResolve();
182
+ }
183
+ });
184
+ });
185
+ }
186
+
187
+ const server = http.createServer(handleRequest);
188
+
189
+ const instance = {
190
+ get port() {
191
+ const addr = server.address();
192
+ return addr ? addr.port : port;
193
+ },
194
+
195
+ get stats() {
196
+ return { ...stats };
197
+ },
198
+
199
+ /**
200
+ * Register a middleware function.
201
+ * @param {(ctx: object, next: () => Promise<void>) => Promise<void>} fn
202
+ */
203
+ use(fn) {
204
+ middlewares.push(fn);
205
+ return instance;
206
+ },
207
+
208
+ /**
209
+ * Start listening on the configured port.
210
+ * @returns {Promise<void>}
211
+ */
212
+ listen() {
213
+ return new Promise((resolve, reject) => {
214
+ server.on("error", reject);
215
+ server.listen(port, "127.0.0.1", () => resolve());
216
+ });
217
+ },
218
+
219
+ /**
220
+ * Graceful shutdown — stop accepting new connections,
221
+ * drain active ones, then resolve.
222
+ * @returns {Promise<void>}
223
+ */
224
+ close() {
225
+ closing = true;
226
+ return new Promise((resolve) => {
227
+ server.close(() => {
228
+ if (activeConnections === 0) {
229
+ resolve();
230
+ } else {
231
+ closeResolve = resolve;
232
+ }
233
+ });
234
+ });
235
+ },
236
+ };
237
+
238
+ return instance;
239
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Security utilities for the proxy layer.
3
+ *
4
+ * - sanitizeHeaders: strips sensitive values before logging
5
+ * - isRequestTooLarge: checks Content-Length against a limit
6
+ *
7
+ * @module proxy/security
8
+ */
9
+
10
+ /** Headers whose values must never appear in logs. */
11
+ const SENSITIVE_HEADERS = new Set([
12
+ "authorization",
13
+ "x-api-key",
14
+ "x-goog-api-key",
15
+ ]);
16
+
17
+ /**
18
+ * Return a shallow copy of `headers` with sensitive values replaced by "[REDACTED]".
19
+ *
20
+ * @param {Record<string, string>} headers
21
+ * @returns {Record<string, string>}
22
+ */
23
+ export function sanitizeHeaders(headers) {
24
+ if (!headers || typeof headers !== "object") return {};
25
+
26
+ const result = {};
27
+ for (const [key, value] of Object.entries(headers)) {
28
+ result[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
29
+ }
30
+ return result;
31
+ }
32
+
33
+ /**
34
+ * Check whether the declared Content-Length exceeds the allowed maximum.
35
+ *
36
+ * @param {number|string|undefined} contentLength - Value of the content-length header
37
+ * @param {number} maxBytes - Maximum allowed body size in bytes (default 50 MB)
38
+ * @returns {boolean} true if the request is too large
39
+ */
40
+ export function isRequestTooLarge(contentLength, maxBytes = 50 * 1024 * 1024) {
41
+ if (contentLength == null) return false;
42
+ const len = Number(contentLength);
43
+ if (Number.isNaN(len)) return false;
44
+ return len > maxBytes;
45
+ }
@@ -19,14 +19,16 @@ function run(command, args, ...rest) {
19
19
  export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
20
20
  if (baseRef) return baseRef;
21
21
  const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
22
- if (mergeBase.exitCode !== 0) {
23
- const fallback = await run("git", ["rev-parse", "HEAD~1"]);
24
- if (fallback.exitCode !== 0) {
25
- throw new Error("Could not compute diff base reference");
26
- }
27
- return fallback.stdout.trim();
28
- }
29
- return mergeBase.stdout.trim();
22
+ if (mergeBase.exitCode === 0) return mergeBase.stdout.trim();
23
+
24
+ const fallback = await run("git", ["rev-parse", "HEAD~1"]);
25
+ if (fallback.exitCode === 0) return fallback.stdout.trim();
26
+
27
+ // Empty repo (zero commits): diff against the empty tree
28
+ const emptyTree = await run("git", ["hash-object", "-t", "tree", "/dev/null"]);
29
+ if (emptyTree.exitCode === 0) return emptyTree.stdout.trim();
30
+
31
+ throw new Error("Could not compute diff base reference");
30
32
  }
31
33
 
32
34
  export async function generateDiff({ baseRef, stageNewFiles = false }) {
@@ -43,6 +43,7 @@ export class CoderRole extends BaseRole {
43
43
  methodology: this.config?.development?.methodology || "tdd",
44
44
  serenaEnabled: Boolean(this.config?.serena?.enabled),
45
45
  rtkAvailable: Boolean(this.config?.rtk?.available),
46
+ proxyEnabled: Boolean(this.config?.proxy?.enabled),
46
47
  productContext: this.config?.productContext || null,
47
48
  domainContext: this.config?.domainContext || null
48
49
  });
@@ -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
- import { RTK_INSTRUCTIONS } from "../prompts/rtk-snippet.js";
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
 
@@ -25,7 +26,7 @@ function truncateDiff(diff) {
25
26
  : diff;
26
27
  }
27
28
 
28
- function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false, productContext = null, domainContext = null }) {
29
+ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false, proxyEnabled = false, productContext = null, domainContext = null }) {
29
30
  const sections = [];
30
31
 
31
32
  sections.push(SUBAGENT_PREAMBLE);
@@ -50,8 +51,9 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAva
50
51
  sections.push(`## Domain Context\n${domainContext}`);
51
52
  }
52
53
 
53
- if (rtkAvailable) {
54
- sections.push(RTK_INSTRUCTIONS);
54
+ const rtkSnippet = buildRtkInstructions({ rtkAvailable, proxyEnabled });
55
+ if (rtkSnippet) {
56
+ sections.push(rtkSnippet);
55
57
  }
56
58
 
57
59
  if (reviewRules) {
@@ -64,12 +66,11 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAva
64
66
  }
65
67
 
66
68
  function parseReviewOutput(raw) {
67
- const text = raw?.trim() || "";
68
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
69
- if (!jsonMatch) {
69
+ const parsed = extractFirstJson(raw);
70
+ if (!parsed) {
70
71
  throw new Error(`Failed to parse reviewer output: no JSON found`);
71
72
  }
72
- return JSON.parse(jsonMatch[0]);
73
+ return parsed;
73
74
  }
74
75
 
75
76
  export class ReviewerRole extends BaseRole {
@@ -93,6 +94,7 @@ export class ReviewerRole extends BaseRole {
93
94
  reviewMode: this.config?.review_mode || "standard",
94
95
  instructions: this.instructions,
95
96
  rtkAvailable: Boolean(this.config?.rtk?.available),
97
+ proxyEnabled: Boolean(this.config?.proxy?.enabled),
96
98
  productContext: this.config?.productContext || null,
97
99
  domainContext: this.config?.domainContext || null
98
100
  });
@@ -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) {
@@ -15,25 +15,31 @@ import path from "node:path";
15
15
  import { getSessionRoot } from "./utils/paths.js";
16
16
 
17
17
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
18
+ const DEFAULT_APPROVED_EXPIRY_DAYS = 7;
18
19
 
19
- const POLICY = {
20
- failed: { expiryMs: ONE_DAY_MS },
21
- stopped: { expiryMs: ONE_DAY_MS },
22
- running: { expiryMs: ONE_DAY_MS }, // stale — crashed without marking failed
23
- approved: { expiryMs: 7 * ONE_DAY_MS },
24
- paused: null // never auto-delete
25
- };
20
+ function buildPolicy(expiryDays) {
21
+ const approvedMs = (expiryDays ?? DEFAULT_APPROVED_EXPIRY_DAYS) * ONE_DAY_MS;
22
+ return {
23
+ failed: { expiryMs: ONE_DAY_MS },
24
+ stopped: { expiryMs: ONE_DAY_MS },
25
+ running: { expiryMs: ONE_DAY_MS }, // stale — crashed without marking failed
26
+ approved: { expiryMs: approvedMs },
27
+ paused: null // never auto-delete
28
+ };
29
+ }
30
+
31
+ const POLICY = buildPolicy();
26
32
 
27
- function shouldRemove(session) {
33
+ function shouldRemove(session, policy = POLICY) {
28
34
  const status = session.status || "unknown";
29
- const policy = POLICY[status];
30
- if (!policy) return false;
35
+ const entry = policy[status];
36
+ if (!entry) return false;
31
37
 
32
38
  const updatedAt = new Date(session.updated_at || session.created_at).getTime();
33
- return Date.now() - updatedAt > policy.expiryMs;
39
+ return Date.now() - updatedAt > entry.expiryMs;
34
40
  }
35
41
 
36
- async function tryCleanupSession({ sessionDir, dirName, removed, errors, logger }) {
42
+ async function tryCleanupSession({ sessionDir, dirName, removed, errors, logger, policy }) {
37
43
  const sessionFile = path.join(sessionDir, "session.json");
38
44
  let session;
39
45
  try {
@@ -54,7 +60,7 @@ async function tryCleanupSession({ sessionDir, dirName, removed, errors, logger
54
60
  return;
55
61
  }
56
62
 
57
- if (!shouldRemove(session)) return;
63
+ if (!shouldRemove(session, policy)) return;
58
64
 
59
65
  try {
60
66
  await fs.rm(sessionDir, { recursive: true, force: true });
@@ -65,7 +71,9 @@ async function tryCleanupSession({ sessionDir, dirName, removed, errors, logger
65
71
  }
66
72
  }
67
73
 
68
- export async function cleanupExpiredSessions({ logger } = {}) {
74
+ export async function cleanupExpiredSessions({ logger, config } = {}) {
75
+ const expiryDays = config?.session?.expiry_days;
76
+ const policy = expiryDays ? buildPolicy(expiryDays) : POLICY;
69
77
  const sessionRoot = getSessionRoot();
70
78
 
71
79
  let entries;
@@ -81,12 +89,28 @@ export async function cleanupExpiredSessions({ logger } = {}) {
81
89
 
82
90
  for (const dir of dirs) {
83
91
  const sessionDir = path.join(sessionRoot, dir.name);
84
- await tryCleanupSession({ sessionDir, dirName: dir.name, removed, errors, logger });
92
+ await tryCleanupSession({ sessionDir, dirName: dir.name, removed, errors, logger, policy });
85
93
  }
86
94
 
87
95
  if (removed.length > 0) {
88
96
  logger?.info?.(`Cleaned up ${removed.length} expired session(s)`);
89
97
  }
90
98
 
99
+ // Truncate oversized project run logs
100
+ const maxLogSizeMb = config?.output?.max_log_size_mb ?? 50;
101
+ await truncateOversizedLog(path.join(process.cwd(), ".kj", "run.log"), maxLogSizeMb, logger);
102
+
91
103
  return { removed: removed.length, errors };
92
104
  }
105
+
106
+ async function truncateOversizedLog(logPath, maxSizeMb, logger) {
107
+ if (maxSizeMb <= 0) return;
108
+ try {
109
+ const stat = await fs.stat(logPath);
110
+ const maxBytes = maxSizeMb * 1024 * 1024;
111
+ if (stat.size > maxBytes) {
112
+ await fs.truncate(logPath, 0);
113
+ logger?.info?.(`Truncated oversized log ${logPath} (was ${(stat.size / 1024 / 1024).toFixed(1)}MB, limit ${maxSizeMb}MB)`);
114
+ }
115
+ } catch { /* file doesn't exist or can't be read */ }
116
+ }
@@ -13,7 +13,7 @@ function buildCloudScannerArgs(projectKey, config) {
13
13
  `-Dsonar.projectKey=${projectKey}`
14
14
  ];
15
15
 
16
- if (token) args.push(`-Dsonar.token=${token}`);
16
+ // Token passed via SONAR_TOKEN env var, not CLI args (invisible in `ps aux`)
17
17
  if (organization) args.push(`-Dsonar.organization=${organization}`);
18
18
  if (scanner.sources) args.push(`-Dsonar.sources=${scanner.sources}`);
19
19
  if (scanner.exclusions) args.push(`-Dsonar.exclusions=${scanner.exclusions}`);
@@ -63,8 +63,11 @@ export async function runSonarCloudScan(config, projectKey = null) {
63
63
  const scannerTimeout = 15 * 60 * 1000;
64
64
  const args = buildCloudScannerArgs(effectiveProjectKey, config);
65
65
 
66
- // Use npx @sonar/scan (no Docker needed)
67
- const result = await runCommand("npx", ["@sonar/scan", ...args], { timeout: scannerTimeout });
66
+ // Pass token via process env, not CLI args — invisible in `ps aux`
67
+ const env = { ...process.env };
68
+ if (token) env.SONAR_TOKEN = token;
69
+
70
+ const result = await runCommand("npx", ["@sonar/scan", ...args], { timeout: scannerTimeout, env });
68
71
 
69
72
  return {
70
73
  ok: result.exitCode === 0,
@@ -243,16 +243,21 @@ export async function runSonarScan(config, projectKey = null) {
243
243
  `${process.cwd()}:/usr/src`,
244
244
  ...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
245
245
  ...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
246
- "-e",
247
- `SONAR_HOST_URL=${host}`,
248
- "-e",
249
- `SONAR_TOKEN=${token || ""}`,
250
- "-e",
251
- `SONAR_SCANNER_OPTS=${buildScannerOpts(effectiveProjectKey, scannerConfig)}`,
246
+ "-e", "SONAR_HOST_URL",
247
+ "-e", "SONAR_TOKEN",
248
+ "-e", "SONAR_SCANNER_OPTS",
252
249
  "sonarsource/sonar-scanner-cli"
253
250
  ];
254
251
 
255
- const result = await runCommand("docker", args, { timeout: scannerTimeout });
252
+ // Pass secrets via process env, not CLI args — invisible in `ps aux`
253
+ const env = {
254
+ ...process.env,
255
+ SONAR_HOST_URL: host,
256
+ SONAR_TOKEN: token || "",
257
+ SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig)
258
+ };
259
+
260
+ const result = await runCommand("docker", args, { timeout: scannerTimeout, env });
256
261
  return {
257
262
  ok: result.exitCode === 0,
258
263
  projectKey: effectiveProjectKey,
@@ -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
+ }