karajan-code 1.58.1 → 1.59.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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/agents/aider-agent.js +7 -0
  3. package/src/agents/claude-agent.js +3 -0
  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/commands/doctor.js +19 -0
  9. package/src/commands/report.js +13 -0
  10. package/src/commands/status.js +7 -0
  11. package/src/config.js +35 -0
  12. package/src/git/automation.js +32 -4
  13. package/src/guards/policy-guard.js +90 -0
  14. package/src/mcp/handlers/run-handler.js +20 -3
  15. package/src/mcp/shared-helpers.js +4 -2
  16. package/src/orchestrator.js +67 -20
  17. package/src/prompts/coder.js +5 -4
  18. package/src/prompts/reviewer.js +5 -4
  19. package/src/prompts/rtk-snippet.js +12 -0
  20. package/src/proxy/adapters/anthropic.js +131 -0
  21. package/src/proxy/adapters/gemini.js +106 -0
  22. package/src/proxy/adapters/index.js +51 -0
  23. package/src/proxy/adapters/openai.js +71 -0
  24. package/src/proxy/compression/ai-compressor.js +209 -0
  25. package/src/proxy/compression/dedup-cache.js +100 -0
  26. package/src/proxy/compression/deterministic/bash-build.js +60 -0
  27. package/src/proxy/compression/deterministic/bash-git.js +101 -0
  28. package/src/proxy/compression/deterministic/bash-infra.js +64 -0
  29. package/src/proxy/compression/deterministic/bash-misc.js +53 -0
  30. package/src/proxy/compression/deterministic/bash-pkg.js +49 -0
  31. package/src/proxy/compression/deterministic/bash-test.js +61 -0
  32. package/src/proxy/compression/deterministic/glob.js +26 -0
  33. package/src/proxy/compression/deterministic/grep.js +53 -0
  34. package/src/proxy/compression/deterministic/index.js +58 -0
  35. package/src/proxy/compression/deterministic/read.js +22 -0
  36. package/src/proxy/compression/deterministic/utils.js +33 -0
  37. package/src/proxy/compression/pipeline.js +270 -0
  38. package/src/proxy/middleware/prompt-injector.js +165 -0
  39. package/src/proxy/middleware/response-interceptor.js +223 -0
  40. package/src/proxy/progress-monitor.js +98 -0
  41. package/src/proxy/proxy-lifecycle.js +198 -0
  42. package/src/proxy/proxy-server.js +239 -0
  43. package/src/proxy/security.js +45 -0
  44. package/src/review/diff-generator.js +10 -8
  45. package/src/roles/coder-role.js +1 -0
  46. package/src/roles/reviewer-role.js +6 -4
  47. package/src/session-cleanup.js +39 -15
  48. package/src/sonar/cloud-scanner.js +6 -3
  49. package/src/sonar/scanner.js +12 -7
  50. package/src/utils/display.js +44 -4
  51. package/src/utils/git.js +5 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Optional AI-powered compression for content that remains large
3
+ * after deterministic compression.
4
+ * Calls a cheap LLM model to summarize old tool results while
5
+ * preserving key data (file paths, error messages, numbers, status codes).
6
+ *
7
+ * Disabled by default. Enable via config: { enabled: true, provider, apiKey }.
8
+ * Zero external dependencies — uses vanilla Node.js https module.
9
+ */
10
+ import https from "node:https";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Provider definitions
14
+ // ---------------------------------------------------------------------------
15
+ const PROVIDERS = {
16
+ anthropic: {
17
+ hostname: "api.anthropic.com",
18
+ path: "/v1/messages",
19
+ defaultModel: "claude-3-5-haiku-20241022",
20
+ costPerInputToken: 0.25 / 1_000_000,
21
+ costPerOutputToken: 1.25 / 1_000_000,
22
+ buildRequest(text, model, apiKey, maxOutputTokens) {
23
+ const body = JSON.stringify({
24
+ model,
25
+ max_tokens: maxOutputTokens,
26
+ messages: [{ role: "user", content: buildPrompt(text) }]
27
+ });
28
+ return {
29
+ hostname: this.hostname,
30
+ path: this.path,
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ "x-api-key": apiKey,
35
+ "anthropic-version": "2023-06-01"
36
+ },
37
+ body
38
+ };
39
+ },
40
+ parseResponse(data) {
41
+ const content = data.content?.[0]?.text ?? "";
42
+ const input = data.usage?.input_tokens ?? 0;
43
+ const output = data.usage?.output_tokens ?? 0;
44
+ return { text: content, tokensUsed: { input, output } };
45
+ }
46
+ },
47
+
48
+ openai: {
49
+ hostname: "api.openai.com",
50
+ path: "/v1/chat/completions",
51
+ defaultModel: "gpt-4o-mini",
52
+ costPerInputToken: 0.15 / 1_000_000,
53
+ costPerOutputToken: 0.6 / 1_000_000,
54
+ buildRequest(text, model, apiKey, maxOutputTokens) {
55
+ const body = JSON.stringify({
56
+ model,
57
+ max_tokens: maxOutputTokens,
58
+ messages: [
59
+ { role: "system", content: SYSTEM_PROMPT },
60
+ { role: "user", content: text }
61
+ ]
62
+ });
63
+ return {
64
+ hostname: this.hostname,
65
+ path: this.path,
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Authorization: `Bearer ${apiKey}`
70
+ },
71
+ body
72
+ };
73
+ },
74
+ parseResponse(data) {
75
+ const content = data.choices?.[0]?.message?.content ?? "";
76
+ const input = data.usage?.prompt_tokens ?? 0;
77
+ const output = data.usage?.completion_tokens ?? 0;
78
+ return { text: content, tokensUsed: { input, output } };
79
+ }
80
+ },
81
+
82
+ gemini: {
83
+ hostname: "generativelanguage.googleapis.com",
84
+ defaultModel: "gemini-2.0-flash-lite",
85
+ costPerInputToken: 0.075 / 1_000_000,
86
+ costPerOutputToken: 0.3 / 1_000_000,
87
+ buildRequest(text, model, apiKey, maxOutputTokens) {
88
+ const path = `/v1beta/models/${model}:generateContent?key=${apiKey}`;
89
+ const body = JSON.stringify({
90
+ contents: [{ parts: [{ text: buildPrompt(text) }] }],
91
+ generationConfig: { maxOutputTokens }
92
+ });
93
+ return {
94
+ hostname: this.hostname,
95
+ path,
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body
99
+ };
100
+ },
101
+ parseResponse(data) {
102
+ const content = data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
103
+ const input = data.usageMetadata?.promptTokenCount ?? 0;
104
+ const output = data.usageMetadata?.candidatesTokenCount ?? 0;
105
+ return { text: content, tokensUsed: { input, output } };
106
+ }
107
+ }
108
+ };
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Prompt
112
+ // ---------------------------------------------------------------------------
113
+ const SYSTEM_PROMPT =
114
+ "You are a compression assistant. Summarize the following tool output concisely " +
115
+ "while preserving ALL: file paths, error messages, numbers, status codes, " +
116
+ "exit codes, and key technical details. Remove boilerplate, repeated lines, " +
117
+ "and verbose formatting. Output only the summary, no preamble.";
118
+
119
+ function buildPrompt(text) {
120
+ return `${SYSTEM_PROMPT}\n\n---\n${text}`;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // HTTPS helper
125
+ // ---------------------------------------------------------------------------
126
+ function httpsRequest({ hostname, path, method, headers, body }) {
127
+ return new Promise((resolve, reject) => {
128
+ const req = https.request({ hostname, path, method, headers }, (res) => {
129
+ const chunks = [];
130
+ res.on("data", (chunk) => chunks.push(chunk));
131
+ res.on("end", () => {
132
+ const raw = Buffer.concat(chunks).toString();
133
+ if (res.statusCode < 200 || res.statusCode >= 300) {
134
+ reject(new Error(`HTTP ${res.statusCode}: ${raw}`));
135
+ return;
136
+ }
137
+ try {
138
+ resolve(JSON.parse(raw));
139
+ } catch (err) {
140
+ reject(new Error(`Invalid JSON response: ${raw.slice(0, 200)}`));
141
+ }
142
+ });
143
+ res.on("error", reject);
144
+ });
145
+ req.on("error", reject);
146
+ if (body) req.write(body);
147
+ req.end();
148
+ });
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Public API
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * Check whether AI compression can be used with the given config.
157
+ * @param {{ enabled?: boolean, provider?: string, apiKey?: string }} config
158
+ * @returns {boolean}
159
+ */
160
+ export function isAICompressionAvailable(config) {
161
+ if (!config || !config.enabled) return false;
162
+ const provider = config.provider ?? "anthropic";
163
+ if (!PROVIDERS[provider]) return false;
164
+ return Boolean(config.apiKey);
165
+ }
166
+
167
+ /**
168
+ * Compress text using a cheap LLM model.
169
+ * @param {string} text - text to compress
170
+ * @param {{ model?: string, apiKey?: string, provider?: string, maxOutputTokens?: number, enabled?: boolean }} options
171
+ * @returns {Promise<{ compressed: string, tokensUsed: { input: number, output: number }, cost: number }>}
172
+ */
173
+ export async function compressWithAI(text, options = {}) {
174
+ const { enabled = false, provider: providerName = "anthropic", apiKey, maxOutputTokens = 1024 } = options;
175
+
176
+ // Disabled mode — return original text unchanged
177
+ if (!enabled || !apiKey) {
178
+ return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
179
+ }
180
+
181
+ const provider = PROVIDERS[providerName];
182
+ if (!provider) {
183
+ return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
184
+ }
185
+
186
+ const model = options.model ?? provider.defaultModel;
187
+
188
+ try {
189
+ const reqOpts = provider.buildRequest(text, model, apiKey, maxOutputTokens);
190
+ const data = await httpsRequest(reqOpts);
191
+ const { text: compressed, tokensUsed } = provider.parseResponse(data);
192
+
193
+ if (!compressed) {
194
+ return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
195
+ }
196
+
197
+ const cost =
198
+ tokensUsed.input * provider.costPerInputToken +
199
+ tokensUsed.output * provider.costPerOutputToken;
200
+
201
+ return { compressed, tokensUsed, cost };
202
+ } catch {
203
+ // Graceful degradation — return original text on any failure
204
+ return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
205
+ }
206
+ }
207
+
208
+ /** Exported for testing */
209
+ export { PROVIDERS };
@@ -0,0 +1,100 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /**
4
+ * Cross-turn content deduplication using an LRU hash cache.
5
+ * Detects repeated content across conversation turns and returns
6
+ * compact references instead of duplicated text.
7
+ */
8
+ export class DedupCache {
9
+ #maxEntries;
10
+ #cache; // Map preserves insertion order for LRU
11
+ #totalChecks;
12
+ #hits;
13
+ #misses;
14
+
15
+ constructor({ maxEntries = 500 } = {}) {
16
+ this.#maxEntries = maxEntries;
17
+ this.#cache = new Map();
18
+ this.#totalChecks = 0;
19
+ this.#hits = 0;
20
+ this.#misses = 0;
21
+ }
22
+
23
+ /**
24
+ * Check if text has been seen in a prior turn.
25
+ * @param {string} text - Content to check
26
+ * @param {number} turnIndex - Current turn number
27
+ * @returns {{isDuplicate: boolean, reference?: string}}
28
+ */
29
+ check(text, turnIndex) {
30
+ this.#totalChecks++;
31
+
32
+ const tokenCount = Math.floor(text.length / 4);
33
+ const hash = createHash("sha256").update(text).digest("hex");
34
+ const shortHash = hash.slice(0, 6);
35
+
36
+ // Small content is never deduped
37
+ if (tokenCount <= 200) {
38
+ this.#misses++;
39
+ this.#upsert(hash, { hash, turnIndex, tokenCount, insertedAt: Date.now() });
40
+ return { isDuplicate: false };
41
+ }
42
+
43
+ const existing = this.#cache.get(hash);
44
+
45
+ if (existing && existing.turnIndex < turnIndex) {
46
+ // Seen in a prior turn — duplicate
47
+ this.#hits++;
48
+ // Refresh LRU position
49
+ this.#cache.delete(hash);
50
+ this.#cache.set(hash, { ...existing, insertedAt: Date.now() });
51
+ return {
52
+ isDuplicate: true,
53
+ reference: `[Content from turn ${existing.turnIndex}, hash ${shortHash}, ${tokenCount} tokens - unchanged]`,
54
+ };
55
+ }
56
+
57
+ // Not seen or same turn — store and return not duplicate
58
+ this.#misses++;
59
+ this.#upsert(hash, { hash, turnIndex, tokenCount, insertedAt: Date.now() });
60
+ return { isDuplicate: false };
61
+ }
62
+
63
+ /**
64
+ * Insert or update an entry, evicting the oldest if at capacity.
65
+ */
66
+ #upsert(key, entry) {
67
+ // If key already exists, delete first to refresh insertion order
68
+ if (this.#cache.has(key)) {
69
+ this.#cache.delete(key);
70
+ }
71
+
72
+ // Evict oldest (first inserted) if at capacity
73
+ if (this.#cache.size >= this.#maxEntries) {
74
+ const oldestKey = this.#cache.keys().next().value;
75
+ this.#cache.delete(oldestKey);
76
+ }
77
+
78
+ this.#cache.set(key, entry);
79
+ }
80
+
81
+ /**
82
+ * @returns {{totalChecks: number, hits: number, misses: number, entries: number}}
83
+ */
84
+ getStats() {
85
+ return {
86
+ totalChecks: this.#totalChecks,
87
+ hits: this.#hits,
88
+ misses: this.#misses,
89
+ entries: this.#cache.size,
90
+ };
91
+ }
92
+
93
+ /** Clear all state (call between sessions). */
94
+ reset() {
95
+ this.#cache.clear();
96
+ this.#totalChecks = 0;
97
+ this.#hits = 0;
98
+ this.#misses = 0;
99
+ }
100
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Deterministic compression for build tool output.
3
+ * Handles: tsc, eslint, prettier, next build, webpack, vite build.
4
+ */
5
+ import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
+
7
+ const BUILD_PATTERNS = [
8
+ /error TS\d+:/,
9
+ /\d+ errors?/i,
10
+ /\d+ warnings?/i,
11
+ /eslint/i,
12
+ /prettier/i,
13
+ /webpack/i,
14
+ /✔|Built in|compiled/i,
15
+ /Module Error|SyntaxError|TypeError/,
16
+ /next build|vite build/i
17
+ ];
18
+
19
+ export function looksLike(text) {
20
+ const clean = stripAnsi(text);
21
+ return BUILD_PATTERNS.some((p) => p.test(clean));
22
+ }
23
+
24
+ export function compact(text) {
25
+ const clean = stripAnsi(text);
26
+ const lines = clean.split("\n");
27
+
28
+ // Collect error/warning lines and summary lines
29
+ const errors = [];
30
+ const warnings = [];
31
+ const summaries = [];
32
+
33
+ for (const line of lines) {
34
+ const trimmed = line.trim();
35
+ if (!trimmed) continue;
36
+
37
+ if (/error TS\d+:|Error:|SyntaxError:|TypeError:|Module Error/i.test(trimmed)) {
38
+ errors.push(trimmed);
39
+ } else if (/warning|warn\b/i.test(trimmed) && !/node_modules/.test(trimmed)) {
40
+ warnings.push(trimmed);
41
+ } else if (/\d+ errors?|\d+ warnings?|compiled|Built in|✔|Found \d+/i.test(trimmed)) {
42
+ summaries.push(trimmed);
43
+ }
44
+ }
45
+
46
+ const parts = [];
47
+ if (errors.length) {
48
+ parts.push(`Errors (${errors.length}):\n${truncateLines(errors.join("\n"), 20)}`);
49
+ }
50
+ if (warnings.length) {
51
+ parts.push(`Warnings (${warnings.length}):\n${truncateLines(warnings.join("\n"), 10)}`);
52
+ }
53
+ if (summaries.length) {
54
+ parts.push(summaries.join("\n"));
55
+ }
56
+
57
+ if (parts.length) return parts.join("\n\n");
58
+
59
+ return truncateLines(collapseWhitespace(clean), 40);
60
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Deterministic compression for git CLI output.
3
+ * Handles: git status, git diff, git log, git branch.
4
+ */
5
+ import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
+
7
+ const GIT_PATTERNS = [
8
+ /^On branch /m,
9
+ /^(modified|deleted|new file|renamed):/m,
10
+ /^Untracked files:/m,
11
+ /^diff --git/m,
12
+ /^commit [0-9a-f]{7,40}/m,
13
+ /^(Changes to be committed|Changes not staged)/m,
14
+ /^\* /m // git branch current branch marker
15
+ ];
16
+
17
+ export function looksLike(text) {
18
+ const clean = stripAnsi(text);
19
+ return GIT_PATTERNS.some((p) => p.test(clean));
20
+ }
21
+
22
+ export function compact(text) {
23
+ const clean = stripAnsi(text);
24
+
25
+ // git status
26
+ if (/^On branch /m.test(clean) || /Changes (to be committed|not staged)/m.test(clean)) {
27
+ return compactStatus(clean);
28
+ }
29
+ // git diff
30
+ if (/^diff --git/m.test(clean)) {
31
+ return compactDiff(clean);
32
+ }
33
+ // git log
34
+ if (/^commit [0-9a-f]{7,40}/m.test(clean)) {
35
+ return compactLog(clean);
36
+ }
37
+ // git branch
38
+ if (/^\* /m.test(clean) && clean.split("\n").every((l) => /^[\s*]/.test(l) || !l.trim())) {
39
+ return compactBranch(clean);
40
+ }
41
+ return collapseWhitespace(clean);
42
+ }
43
+
44
+ function compactStatus(text) {
45
+ const lines = text.split("\n");
46
+ const kept = [];
47
+ const branchLine = lines.find((l) => l.startsWith("On branch "));
48
+ if (branchLine) kept.push(branchLine);
49
+
50
+ for (const line of lines) {
51
+ const trimmed = line.trim();
52
+ if (
53
+ /^(modified|deleted|new file|renamed|both modified):/.test(trimmed) ||
54
+ /^(Untracked files|Changes to be committed|Changes not staged)/.test(trimmed)
55
+ ) {
56
+ kept.push(trimmed);
57
+ }
58
+ }
59
+ return kept.join("\n") || text;
60
+ }
61
+
62
+ function compactDiff(text) {
63
+ const files = text.split(/(?=^diff --git)/m);
64
+ const summaries = [];
65
+ for (const file of files) {
66
+ if (!file.trim()) continue;
67
+ const header = file.match(/^diff --git a\/(.+?) b\/(.+)/m);
68
+ const fname = header ? header[2] : "unknown";
69
+ const additions = (file.match(/^\+(?!\+\+)/gm) || []).length;
70
+ const deletions = (file.match(/^-(?!--)/gm) || []).length;
71
+ const hunkHeaders = file.match(/^@@.*@@.*$/gm) || [];
72
+ summaries.push(`${fname}: +${additions}/-${deletions} (${hunkHeaders.length} hunks)`);
73
+
74
+ // Keep first few hunk headers for context
75
+ for (const h of hunkHeaders.slice(0, 3)) {
76
+ summaries.push(` ${h.trim()}`);
77
+ }
78
+ }
79
+ return `git diff summary (${files.filter((f) => f.trim()).length} files):\n${summaries.join("\n")}`;
80
+ }
81
+
82
+ function compactLog(text) {
83
+ const entries = text.split(/(?=^commit )/m).filter((e) => e.trim());
84
+ const lines = [];
85
+ for (const entry of entries) {
86
+ const hashMatch = entry.match(/^commit ([0-9a-f]{7})/);
87
+ const msgMatch = entry.match(/^\s{4}(.+)/m);
88
+ const hash = hashMatch ? hashMatch[1] : "???????";
89
+ const msg = msgMatch ? msgMatch[1].trim() : "(no message)";
90
+ lines.push(`${hash} ${msg}`);
91
+ }
92
+ return truncateLines(lines.join("\n"), 30);
93
+ }
94
+
95
+ function compactBranch(text) {
96
+ const lines = text.split("\n").filter((l) => l.trim());
97
+ if (lines.length <= 20) return lines.map((l) => l.trim()).join("\n");
98
+ const current = lines.find((l) => l.startsWith("*"));
99
+ const count = lines.length;
100
+ return `${current ? current.trim() : "(detached)"}\n... (${count} branches total)`;
101
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Deterministic compression for infrastructure CLI output.
3
+ * Handles: docker ps/images/logs, kubectl, terraform plan/apply.
4
+ */
5
+ import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
+
7
+ const INFRA_PATTERNS = [
8
+ /CONTAINER ID/,
9
+ /REPOSITORY\s+TAG/,
10
+ /^NAME\s+READY\s+STATUS/m,
11
+ /^(kubectl|docker|terraform)\s/m,
12
+ /Terraform will perform/,
13
+ /Plan:\s+\d+ to add/,
14
+ /Apply complete/
15
+ ];
16
+
17
+ export function looksLike(text) {
18
+ const clean = stripAnsi(text);
19
+ return INFRA_PATTERNS.some((p) => p.test(clean));
20
+ }
21
+
22
+ export function compact(text) {
23
+ const clean = stripAnsi(text);
24
+
25
+ // docker ps / docker images — compact table
26
+ if (/CONTAINER ID/.test(clean) || /REPOSITORY\s+TAG/.test(clean)) {
27
+ return compactTable(clean);
28
+ }
29
+
30
+ // terraform plan/apply
31
+ if (/Terraform will perform|Plan:\s+\d+|Apply complete/.test(clean)) {
32
+ return compactTerraform(clean);
33
+ }
34
+
35
+ // kubectl — compact table
36
+ if (/^NAME\s+READY\s+STATUS/m.test(clean)) {
37
+ return compactTable(clean);
38
+ }
39
+
40
+ // docker logs — truncate
41
+ return truncateLines(collapseWhitespace(clean), 30);
42
+ }
43
+
44
+ function compactTable(text) {
45
+ const lines = text.split("\n").filter((l) => l.trim());
46
+ if (lines.length <= 15) return lines.map((l) => collapseWhitespace(l)).join("\n");
47
+ const header = lines[0];
48
+ const rows = lines.slice(1, 11);
49
+ return `${collapseWhitespace(header)}\n${rows.map((r) => collapseWhitespace(r)).join("\n")}\n... (${lines.length - 1} total rows)`;
50
+ }
51
+
52
+ function compactTerraform(text) {
53
+ const lines = text.split("\n");
54
+ const kept = [];
55
+ for (const line of lines) {
56
+ if (
57
+ /Plan:|Apply complete|Terraform will perform|^\s*[#~+-]/.test(line) &&
58
+ !/Refreshing state/.test(line)
59
+ ) {
60
+ kept.push(line.trim());
61
+ }
62
+ }
63
+ return truncateLines(kept.join("\n"), 40) || truncateLines(collapseWhitespace(text), 30);
64
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Deterministic compression for miscellaneous Bash output.
3
+ * Handles: curl/wget, gh CLI, npx, general long output.
4
+ */
5
+ import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
+
7
+ const MISC_PATTERNS = [
8
+ /HTTP\/[\d.]+\s+\d{3}/,
9
+ /curl:|wget:/i,
10
+ /gh (pr|issue|repo|release)/i,
11
+ /npx:/i
12
+ ];
13
+
14
+ export function looksLike(text) {
15
+ const clean = stripAnsi(text);
16
+ return MISC_PATTERNS.some((p) => p.test(clean));
17
+ }
18
+
19
+ export function compact(text) {
20
+ const clean = stripAnsi(text);
21
+
22
+ // curl/wget — keep status + relevant headers
23
+ if (/HTTP\/[\d.]+\s+\d{3}/.test(clean)) {
24
+ return compactHttp(clean);
25
+ }
26
+
27
+ // gh CLI — compact lists
28
+ if (/gh (pr|issue|repo|release)/i.test(clean)) {
29
+ return truncateLines(collapseWhitespace(clean), 20);
30
+ }
31
+
32
+ return truncateLines(collapseWhitespace(clean), 40);
33
+ }
34
+
35
+ function compactHttp(text) {
36
+ const lines = text.split("\n");
37
+ const kept = [];
38
+ for (const line of lines) {
39
+ if (
40
+ /HTTP\/[\d.]+\s+\d{3}/.test(line) ||
41
+ /^(content-type|location|x-[-\w]+|authorization):/i.test(line.trim())
42
+ ) {
43
+ kept.push(line.trim());
44
+ }
45
+ }
46
+ // Also keep body, truncated
47
+ const bodyStart = text.indexOf("\r\n\r\n");
48
+ if (bodyStart > -1) {
49
+ const body = text.slice(bodyStart + 4).trim();
50
+ if (body) kept.push(truncateLines(body, 20));
51
+ }
52
+ return kept.join("\n") || truncateLines(collapseWhitespace(text), 30);
53
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Deterministic compression for package manager output.
3
+ * Handles: npm/yarn/pnpm install, npm list, npm outdated.
4
+ */
5
+ import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
+
7
+ const PKG_PATTERNS = [
8
+ /added \d+ packages?/i,
9
+ /npm warn|npm error/i,
10
+ /packages? in \d/,
11
+ /├──|└──|│/,
12
+ /\d+ vulnerabilities/i,
13
+ /npm outdated/i,
14
+ /Current\s+Wanted\s+Latest/i
15
+ ];
16
+
17
+ export function looksLike(text) {
18
+ const clean = stripAnsi(text);
19
+ return PKG_PATTERNS.some((p) => p.test(clean));
20
+ }
21
+
22
+ export function compact(text) {
23
+ const clean = stripAnsi(text);
24
+
25
+ // npm install summary
26
+ if (/added \d+ packages?/i.test(clean)) {
27
+ const lines = clean.split("\n");
28
+ const kept = [];
29
+ for (const line of lines) {
30
+ const trimmed = line.trim();
31
+ if (/added \d+ packages?/i.test(trimmed) || /vulnerabilit/i.test(trimmed)) {
32
+ kept.push(trimmed);
33
+ }
34
+ }
35
+ return kept.join("\n") || clean;
36
+ }
37
+
38
+ // npm list — truncate tree
39
+ if (/├──|└──/.test(clean)) {
40
+ return truncateLines(clean, 25);
41
+ }
42
+
43
+ // npm outdated — compact table
44
+ if (/Current\s+Wanted\s+Latest/i.test(clean)) {
45
+ return truncateLines(collapseWhitespace(clean), 20);
46
+ }
47
+
48
+ return truncateLines(collapseWhitespace(clean), 30);
49
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Deterministic compression for test runner output.
3
+ * Handles: vitest, jest, mocha, playwright, pytest, cargo test, go test.
4
+ */
5
+ import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
+
7
+ const TEST_PATTERNS = [
8
+ /Tests?\s+\d+\s+(passed|failed)/i,
9
+ /✓|✗|✘|PASS|FAIL/,
10
+ /Test Suites?:/,
11
+ /test result:/i,
12
+ /FAILED|PASSED|ERROR/,
13
+ /vitest|jest|mocha|playwright|pytest|cargo test|go test/i,
14
+ /\d+ passing/,
15
+ /\d+ failing/
16
+ ];
17
+
18
+ export function looksLike(text) {
19
+ const clean = stripAnsi(text);
20
+ return TEST_PATTERNS.some((p) => p.test(clean));
21
+ }
22
+
23
+ export function compact(text) {
24
+ const clean = stripAnsi(text);
25
+
26
+ // Extract summary line if present
27
+ const summaryPatterns = [
28
+ /Tests?\s+\d+\s+(passed|failed).*/gi,
29
+ /Test Suites?:.*$/gm,
30
+ /\d+ passing.*$/gm,
31
+ /\d+ failing.*$/gm,
32
+ /test result:.*$/gim,
33
+ /Tests:.*\d+ total$/gm
34
+ ];
35
+
36
+ const summaries = [];
37
+ for (const p of summaryPatterns) {
38
+ const matches = clean.match(p);
39
+ if (matches) summaries.push(...matches.map((m) => m.trim()));
40
+ }
41
+
42
+ // Extract failed test details (name + error)
43
+ const failures = [];
44
+ const failBlocks = clean.split(/(?=(?:FAIL|✗|✘|×)\s)/);
45
+ for (const block of failBlocks) {
46
+ if (/^(?:FAIL|✗|✘|×)\s/.test(block)) {
47
+ failures.push(truncateLines(block.trim(), 8));
48
+ }
49
+ }
50
+
51
+ const parts = [];
52
+ if (summaries.length) parts.push(summaries.join("\n"));
53
+ if (failures.length) {
54
+ parts.push(`Failed tests:\n${failures.join("\n---\n")}`);
55
+ }
56
+
57
+ if (parts.length) return parts.join("\n\n");
58
+
59
+ // Fallback: collapse and truncate
60
+ return truncateLines(collapseWhitespace(clean), 50);
61
+ }