@hasna/terminal 2.3.2 → 3.0.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/dist/ai.js +56 -82
- package/dist/cache.js +3 -2
- package/dist/cli.js +1 -1
- package/dist/compression.js +8 -30
- package/dist/context-hints.js +20 -10
- package/dist/diff-cache.js +1 -1
- package/dist/discover.js +1 -1
- package/dist/economy.js +37 -5
- package/dist/expand-store.js +7 -1
- package/dist/mcp/server.js +44 -68
- package/dist/output-processor.js +10 -7
- package/dist/providers/anthropic.js +6 -2
- package/dist/providers/cerebras.js +6 -93
- package/dist/providers/groq.js +6 -93
- package/dist/providers/index.js +85 -36
- package/dist/providers/openai-compat.js +93 -0
- package/dist/providers/xai.js +6 -93
- package/dist/tokens.js +17 -0
- package/dist/tool-profiles.js +9 -2
- package/package.json +1 -1
- package/src/ai.ts +60 -90
- package/src/cache.ts +3 -2
- package/src/cli.tsx +1 -1
- package/src/compression.ts +8 -35
- package/src/context-hints.ts +20 -10
- package/src/diff-cache.ts +1 -1
- package/src/discover.ts +1 -1
- package/src/economy.ts +37 -5
- package/src/expand-store.ts +8 -1
- package/src/mcp/server.ts +45 -73
- package/src/output-processor.ts +11 -8
- package/src/providers/anthropic.ts +6 -2
- package/src/providers/base.ts +2 -0
- package/src/providers/cerebras.ts +6 -105
- package/src/providers/groq.ts +6 -105
- package/src/providers/index.ts +84 -33
- package/src/providers/openai-compat.ts +109 -0
- package/src/providers/xai.ts +6 -105
- package/src/tokens.ts +18 -0
- package/src/tool-profiles.ts +9 -2
- package/src/compression.test.ts +0 -49
- package/src/output-router.ts +0 -56
- package/src/parsers/base.ts +0 -72
- package/src/parsers/build.ts +0 -73
- package/src/parsers/errors.ts +0 -107
- package/src/parsers/files.ts +0 -91
- package/src/parsers/git.ts +0 -101
- package/src/parsers/index.ts +0 -66
- package/src/parsers/parsers.test.ts +0 -153
- package/src/parsers/tests.ts +0 -98
package/src/context-hints.ts
CHANGED
|
@@ -43,16 +43,29 @@ export function discoverProjectHints(cwd: string): string[] {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Extract
|
|
46
|
+
// Extract metadata from package.json — trimmed to save tokens
|
|
47
47
|
const pkgPath = join(cwd, "package.json");
|
|
48
48
|
if (existsSync(pkgPath)) {
|
|
49
49
|
try {
|
|
50
50
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
51
|
-
if (pkg.name) hints.push(`Package
|
|
51
|
+
if (pkg.name) hints.push(`Package: ${pkg.name}@${pkg.version ?? "?"}`);
|
|
52
52
|
if (pkg.scripts) {
|
|
53
|
-
|
|
53
|
+
// Only top-5 most useful scripts
|
|
54
|
+
const priority = ["dev", "build", "test", "lint", "start", "typecheck", "check"];
|
|
55
|
+
const scripts = Object.keys(pkg.scripts);
|
|
56
|
+
const top = priority.filter(s => scripts.includes(s));
|
|
57
|
+
const rest = scripts.filter(s => !priority.includes(s)).slice(0, Math.max(0, 5 - top.length));
|
|
58
|
+
hints.push(`Scripts: ${[...top, ...rest].join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
if (pkg.dependencies) {
|
|
61
|
+
// Only framework/major deps — skip utility libs
|
|
62
|
+
const major = ["react", "next", "express", "fastify", "hono", "vue", "angular", "svelte",
|
|
63
|
+
"prisma", "drizzle", "mongoose", "typeorm", "zod", "trpc", "graphql", "tailwindcss",
|
|
64
|
+
"electron", "bun", "elysia", "nest", "nuxt", "remix", "astro", "vite"];
|
|
65
|
+
const deps = Object.keys(pkg.dependencies);
|
|
66
|
+
const found = deps.filter(d => major.some(m => d.includes(m)));
|
|
67
|
+
if (found.length > 0) hints.push(`Key deps: ${found.slice(0, 10).join(", ")}`);
|
|
54
68
|
}
|
|
55
|
-
if (pkg.dependencies) hints.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
56
69
|
} catch {}
|
|
57
70
|
}
|
|
58
71
|
|
|
@@ -107,23 +120,20 @@ export function discoverProjectHints(cwd: string): string[] {
|
|
|
107
120
|
} catch {}
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
// Source directory structure
|
|
123
|
+
// Source directory structure — max 20 files to save tokens
|
|
111
124
|
try {
|
|
112
125
|
const { execSync } = require("child_process");
|
|
113
126
|
const srcDirs = ["src", "lib", "app", "packages"];
|
|
114
127
|
for (const dir of srcDirs) {
|
|
115
128
|
if (existsSync(join(cwd, dir))) {
|
|
116
129
|
const tree = execSync(
|
|
117
|
-
`find ${dir} -maxdepth
|
|
118
|
-
{ cwd, encoding: "utf8", timeout:
|
|
130
|
+
`find ${dir} -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | sort | head -20`,
|
|
131
|
+
{ cwd, encoding: "utf8", timeout: 2000 }
|
|
119
132
|
).trim();
|
|
120
133
|
if (tree) hints.push(`Files in ${dir}/:\n${tree}`);
|
|
121
134
|
break;
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
|
-
// Top-level files
|
|
125
|
-
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
126
|
-
hints.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
127
137
|
} catch {}
|
|
128
138
|
|
|
129
139
|
return hints;
|
package/src/diff-cache.ts
CHANGED
package/src/discover.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { readdirSync, readFileSync, statSync, existsSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
import { estimateTokens } from "./
|
|
7
|
+
import { estimateTokens } from "./tokens.js";
|
|
8
8
|
|
|
9
9
|
export interface DiscoveredCommand {
|
|
10
10
|
command: string;
|
package/src/economy.ts
CHANGED
|
@@ -62,13 +62,30 @@ function loadStats(): EconomyStats {
|
|
|
62
62
|
return stats;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
let _saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
|
|
65
67
|
function saveStats() {
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Debounce: coalesce multiple writes within 1 second
|
|
69
|
+
if (_saveTimer) return;
|
|
70
|
+
_saveTimer = setTimeout(() => {
|
|
71
|
+
_saveTimer = null;
|
|
72
|
+
ensureDir();
|
|
73
|
+
if (stats) {
|
|
74
|
+
writeFileSync(ECONOMY_FILE, JSON.stringify(stats, null, 2));
|
|
75
|
+
}
|
|
76
|
+
}, 1000);
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
// Flush on exit
|
|
80
|
+
process.on("exit", () => {
|
|
81
|
+
if (_saveTimer) {
|
|
82
|
+
clearTimeout(_saveTimer);
|
|
83
|
+
_saveTimer = null;
|
|
84
|
+
ensureDir();
|
|
85
|
+
if (stats) writeFileSync(ECONOMY_FILE, JSON.stringify(stats, null, 2));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
72
89
|
/** Record token savings from a feature */
|
|
73
90
|
export function recordSaving(feature: keyof EconomyStats["savingsByFeature"], tokensSaved: number) {
|
|
74
91
|
const s = loadStats();
|
|
@@ -112,12 +129,27 @@ const PROVIDER_PRICING: Record<string, { input: number; output: number }> = {
|
|
|
112
129
|
"anthropic-opus": { input: 5.00, output: 25.00 },
|
|
113
130
|
};
|
|
114
131
|
|
|
132
|
+
/** Load configurable turns-before-compaction from ~/.terminal/config.json */
|
|
133
|
+
function loadTurnsMultiplier(): number {
|
|
134
|
+
try {
|
|
135
|
+
const configPath = join(DIR, "config.json");
|
|
136
|
+
if (existsSync(configPath)) {
|
|
137
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
138
|
+
return config.economy?.turnsBeforeCompaction ?? 5;
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
return 5; // Default: tokens saved are repeated ~5 turns before agent compacts context
|
|
142
|
+
}
|
|
143
|
+
|
|
115
144
|
/** Estimate USD savings from compressed tokens */
|
|
116
145
|
export function estimateSavingsUsd(
|
|
117
146
|
tokensSaved: number,
|
|
118
147
|
consumerModel: string = "anthropic-opus",
|
|
119
|
-
avgTurnsBeforeCompaction
|
|
148
|
+
avgTurnsBeforeCompaction?: number,
|
|
120
149
|
): { savingsUsd: number; multipliedTokens: number; ratePerMillion: number } {
|
|
150
|
+
if (avgTurnsBeforeCompaction === undefined) {
|
|
151
|
+
avgTurnsBeforeCompaction = loadTurnsMultiplier();
|
|
152
|
+
}
|
|
121
153
|
const pricing = PROVIDER_PRICING[consumerModel] ?? PROVIDER_PRICING["anthropic-opus"];
|
|
122
154
|
const multipliedTokens = tokensSaved * avgTurnsBeforeCompaction;
|
|
123
155
|
const savingsUsd = (multipliedTokens * pricing.input) / 1_000_000;
|
package/src/expand-store.ts
CHANGED
|
@@ -26,6 +26,11 @@ export function storeOutput(command: string, output: string): string {
|
|
|
26
26
|
return key;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Escape regex special characters for safe use in new RegExp() */
|
|
30
|
+
function escapeRegex(str: string): string {
|
|
31
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
/** Retrieve full output by key, optionally filtered */
|
|
30
35
|
export function expandOutput(key: string, grep?: string): { found: boolean; output?: string; lines?: number } {
|
|
31
36
|
const entry = store.get(key);
|
|
@@ -33,7 +38,9 @@ export function expandOutput(key: string, grep?: string): { found: boolean; outp
|
|
|
33
38
|
|
|
34
39
|
let output = entry.output;
|
|
35
40
|
if (grep) {
|
|
36
|
-
|
|
41
|
+
// Escape metacharacters so user input like "[error" or "func()" doesn't crash
|
|
42
|
+
const safe = escapeRegex(grep);
|
|
43
|
+
const pattern = new RegExp(safe, "i");
|
|
37
44
|
output = output.split("\n").filter(l => pattern.test(l)).join("\n");
|
|
38
45
|
}
|
|
39
46
|
|
package/src/mcp/server.ts
CHANGED
|
@@ -6,17 +6,16 @@ import { z } from "zod";
|
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
import { compress, stripAnsi } from "../compression.js";
|
|
8
8
|
import { stripNoise } from "../noise-filter.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { estimateTokens } from "../tokens.js";
|
|
10
|
+
import { processOutput } from "../output-processor.js";
|
|
11
11
|
import { searchFiles, searchContent, semanticSearch } from "../search/index.js";
|
|
12
12
|
import { listRecipes, listCollections, getRecipe, createRecipe } from "../recipes/storage.js";
|
|
13
13
|
import { substituteVariables } from "../recipes/model.js";
|
|
14
14
|
import { bgStart, bgStatus, bgStop, bgLogs, bgWaitPort } from "../supervisor.js";
|
|
15
15
|
import { diffOutput } from "../diff-cache.js";
|
|
16
|
-
import { processOutput } from "../output-processor.js";
|
|
17
16
|
import { listSessions, getSessionInteractions, getSessionStats } from "../sessions-db.js";
|
|
18
17
|
import { cachedRead, cacheStats } from "../file-cache.js";
|
|
19
|
-
import { getBootContext } from "../session-boot.js";
|
|
18
|
+
import { getBootContext, invalidateBootCache } from "../session-boot.js";
|
|
20
19
|
import { storeOutput, expandOutput } from "../expand-store.js";
|
|
21
20
|
import { rewriteCommand } from "../command-rewriter.js";
|
|
22
21
|
import { shouldBeLazy, toLazy } from "../lazy-executor.js";
|
|
@@ -49,6 +48,10 @@ function exec(command: string, cwd?: string, timeout?: number): Promise<{ exitCo
|
|
|
49
48
|
// Strip noise before returning (npm fund, progress bars, etc.)
|
|
50
49
|
const cleanStdout = stripNoise(stdout).cleaned;
|
|
51
50
|
const cleanStderr = stripNoise(stderr).cleaned;
|
|
51
|
+
// Invalidate boot cache after state-changing git commands
|
|
52
|
+
if (/\bgit\s+(commit|checkout|branch|merge|reset|push|pull|rebase|stash)\b/.test(actualCommand)) {
|
|
53
|
+
invalidateBootCache();
|
|
54
|
+
}
|
|
52
55
|
resolve({ exitCode: code ?? 0, stdout: cleanStdout, stderr: cleanStderr, duration: Date.now() - start, rewritten: rw.changed ? rw.rewritten : undefined });
|
|
53
56
|
});
|
|
54
57
|
});
|
|
@@ -100,44 +103,20 @@ export function createServer(): McpServer {
|
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
// JSON
|
|
104
|
-
if (format === "json") {
|
|
105
|
-
const parsed = parseOutput(command, output);
|
|
106
|
-
if (parsed) {
|
|
107
|
-
const savings = tokenSavings(output, parsed.data);
|
|
108
|
-
if (savings.saved > 0) {
|
|
109
|
-
return {
|
|
110
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
111
|
-
exitCode: result.exitCode, parsed: parsed.data, parser: parsed.parser,
|
|
112
|
-
duration: result.duration, tokensSaved: savings.saved, savingsPercent: savings.percent,
|
|
113
|
-
}) }],
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
// JSON was larger — fall through to compression
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Compressed mode (also fallback for json when no parser matches)
|
|
121
|
-
if (format === "compressed" || format === "json") {
|
|
122
|
-
const compressed = compress(command, output, { maxTokens, format: "json" });
|
|
123
|
-
return {
|
|
124
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
125
|
-
exitCode: result.exitCode, output: compressed.content, format: compressed.format,
|
|
126
|
-
duration: result.duration, tokensSaved: compressed.tokensSaved, savingsPercent: compressed.savingsPercent,
|
|
127
|
-
}) }],
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Summary mode — AI-powered
|
|
132
|
-
if (format === "summary") {
|
|
106
|
+
// JSON and Summary modes — both go through AI processing
|
|
107
|
+
if (format === "json" || format === "summary") {
|
|
133
108
|
try {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const summaryTokens = estimateTokens(summary);
|
|
109
|
+
const processed = await processOutput(command, output);
|
|
110
|
+
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
137
111
|
return {
|
|
138
112
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
139
|
-
exitCode: result.exitCode,
|
|
140
|
-
|
|
113
|
+
exitCode: result.exitCode,
|
|
114
|
+
summary: processed.summary,
|
|
115
|
+
structured: processed.structured,
|
|
116
|
+
duration: result.duration,
|
|
117
|
+
tokensSaved: processed.tokensSaved,
|
|
118
|
+
aiProcessed: processed.aiProcessed,
|
|
119
|
+
...(detailKey ? { detailKey, expandable: true } : {}),
|
|
141
120
|
}) }],
|
|
142
121
|
};
|
|
143
122
|
} catch {
|
|
@@ -151,6 +130,17 @@ export function createServer(): McpServer {
|
|
|
151
130
|
}
|
|
152
131
|
}
|
|
153
132
|
|
|
133
|
+
// Compressed mode — fast non-AI: strip + dedup + truncate
|
|
134
|
+
if (format === "compressed") {
|
|
135
|
+
const compressed = compress(command, output, { maxTokens });
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
138
|
+
exitCode: result.exitCode, output: compressed.content, duration: result.duration,
|
|
139
|
+
tokensSaved: compressed.tokensSaved, savingsPercent: compressed.savingsPercent,
|
|
140
|
+
}) }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
154
144
|
return { content: [{ type: "text" as const, text: output }] };
|
|
155
145
|
}
|
|
156
146
|
);
|
|
@@ -230,16 +220,8 @@ export function createServer(): McpServer {
|
|
|
230
220
|
}
|
|
231
221
|
|
|
232
222
|
const result = await exec(command);
|
|
233
|
-
const parsed = parseOutput(command, result.stdout);
|
|
234
|
-
|
|
235
|
-
if (parsed) {
|
|
236
|
-
return {
|
|
237
|
-
content: [{ type: "text" as const, text: JSON.stringify({ cwd: target, ...parsed.data as object, parser: parsed.parser }) }],
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
223
|
const files = result.stdout.split("\n").filter(l => l.trim());
|
|
242
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({ cwd: target, files }) }] };
|
|
224
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ cwd: target, files, count: files.length }) }] };
|
|
243
225
|
}
|
|
244
226
|
);
|
|
245
227
|
|
|
@@ -253,14 +235,13 @@ export function createServer(): McpServer {
|
|
|
253
235
|
command: z.string().optional().describe("The command that produced the error"),
|
|
254
236
|
},
|
|
255
237
|
async ({ error, command }) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const info = errorParser.parse(command ?? "", error);
|
|
259
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(info) }] };
|
|
260
|
-
}
|
|
238
|
+
// AI processes the error — no regex guessing
|
|
239
|
+
const processed = await processOutput(command ?? "unknown", error);
|
|
261
240
|
return {
|
|
262
241
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
263
|
-
|
|
242
|
+
summary: processed.summary,
|
|
243
|
+
structured: processed.structured,
|
|
244
|
+
aiProcessed: processed.aiProcessed,
|
|
264
245
|
}) }],
|
|
265
246
|
};
|
|
266
247
|
}
|
|
@@ -274,9 +255,8 @@ export function createServer(): McpServer {
|
|
|
274
255
|
async () => {
|
|
275
256
|
return {
|
|
276
257
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
277
|
-
name: "open-terminal", version: "0.
|
|
278
|
-
|
|
279
|
-
features: ["structured-output", "token-compression", "ai-summary", "error-diagnosis"],
|
|
258
|
+
name: "open-terminal", version: "0.3.0", cwd: process.cwd(),
|
|
259
|
+
features: ["ai-output-processing", "token-compression", "noise-filtering", "diff-caching", "lazy-execution", "progressive-disclosure"],
|
|
280
260
|
}) }],
|
|
281
261
|
};
|
|
282
262
|
}
|
|
@@ -376,20 +356,12 @@ export function createServer(): McpServer {
|
|
|
376
356
|
const result = await exec(command, cwd, 30000);
|
|
377
357
|
const output = (result.stdout + result.stderr).trim();
|
|
378
358
|
|
|
379
|
-
if (format === "json") {
|
|
380
|
-
const
|
|
381
|
-
if (parsed) {
|
|
382
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
383
|
-
recipe: name, exitCode: result.exitCode, parsed: parsed.data, duration: result.duration,
|
|
384
|
-
}) }] };
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (format === "compressed") {
|
|
389
|
-
const compressed = compress(command, output, { format: "json" });
|
|
359
|
+
if (format === "json" || format === "compressed") {
|
|
360
|
+
const processed = await processOutput(command, output);
|
|
390
361
|
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
391
|
-
recipe: name, exitCode: result.exitCode,
|
|
392
|
-
|
|
362
|
+
recipe: name, exitCode: result.exitCode, summary: processed.summary,
|
|
363
|
+
structured: processed.structured, duration: result.duration,
|
|
364
|
+
tokensSaved: processed.tokensSaved, aiProcessed: processed.aiProcessed,
|
|
393
365
|
}) }] };
|
|
394
366
|
}
|
|
395
367
|
|
|
@@ -534,10 +506,10 @@ export function createServer(): McpServer {
|
|
|
534
506
|
}) }] };
|
|
535
507
|
}
|
|
536
508
|
|
|
537
|
-
// First run — return full output
|
|
538
|
-
const
|
|
509
|
+
// First run — return full output (ANSI stripped)
|
|
510
|
+
const clean = stripAnsi(output);
|
|
539
511
|
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
540
|
-
exitCode: result.exitCode, output:
|
|
512
|
+
exitCode: result.exitCode, output: clean,
|
|
541
513
|
diffSummary: "first run", duration: result.duration,
|
|
542
514
|
}) }] };
|
|
543
515
|
}
|
package/src/output-processor.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// NOTHING is hardcoded. The AI decides what's important, what's noise, what to keep.
|
|
3
3
|
|
|
4
4
|
import { getProvider } from "./providers/index.js";
|
|
5
|
-
import { estimateTokens } from "./
|
|
5
|
+
import { estimateTokens } from "./tokens.js";
|
|
6
6
|
import { recordSaving } from "./economy.js";
|
|
7
7
|
import { discoverOutputHints } from "./context-hints.js";
|
|
8
8
|
import { formatProfileHints } from "./tool-profiles.js";
|
|
9
|
+
import { stripAnsi } from "./compression.js";
|
|
10
|
+
import { stripNoise } from "./noise-filter.js";
|
|
9
11
|
|
|
10
12
|
export interface ProcessedOutput {
|
|
11
13
|
/** AI-generated summary (concise, structured) */
|
|
@@ -29,7 +31,9 @@ export interface ProcessedOutput {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
const MIN_LINES_TO_PROCESS = 15;
|
|
32
|
-
|
|
34
|
+
// Reserve ~2000 chars for system prompt + hints + profile + overhead
|
|
35
|
+
const PROMPT_OVERHEAD_CHARS = 2000;
|
|
36
|
+
const MAX_OUTPUT_FOR_AI = 6000; // chars of output to send to AI (leaves room for prompt overhead)
|
|
33
37
|
|
|
34
38
|
const SUMMARIZE_PROMPT = `You are an intelligent terminal assistant. Given a user's original question and the command output, ANSWER THE QUESTION directly.
|
|
35
39
|
|
|
@@ -70,8 +74,10 @@ export async function processOutput(
|
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
//
|
|
74
|
-
let toSummarize = output;
|
|
77
|
+
// Clean output before AI processing — strip ANSI codes and noise
|
|
78
|
+
let toSummarize = stripAnsi(output);
|
|
79
|
+
toSummarize = stripNoise(toSummarize).cleaned;
|
|
80
|
+
|
|
75
81
|
if (toSummarize.length > MAX_OUTPUT_FOR_AI) {
|
|
76
82
|
const headChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.6);
|
|
77
83
|
const tailChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.3);
|
|
@@ -97,6 +103,7 @@ export async function processOutput(
|
|
|
97
103
|
{
|
|
98
104
|
system: SUMMARIZE_PROMPT,
|
|
99
105
|
maxTokens: 300,
|
|
106
|
+
temperature: 0.2,
|
|
100
107
|
}
|
|
101
108
|
);
|
|
102
109
|
|
|
@@ -104,10 +111,6 @@ export async function processOutput(
|
|
|
104
111
|
const summaryTokens = estimateTokens(summary);
|
|
105
112
|
const saved = Math.max(0, originalTokens - summaryTokens);
|
|
106
113
|
|
|
107
|
-
if (saved > 0) {
|
|
108
|
-
recordSaving("compressed", saved);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
114
|
// Try to extract structured JSON if the AI returned it
|
|
112
115
|
let structured: Record<string, unknown> | undefined;
|
|
113
116
|
try {
|
|
@@ -17,7 +17,9 @@ export class AnthropicProvider implements LLMProvider {
|
|
|
17
17
|
const message = await this.client.messages.create({
|
|
18
18
|
model: options.model ?? "claude-haiku-4-5-20251001",
|
|
19
19
|
max_tokens: options.maxTokens ?? 256,
|
|
20
|
-
|
|
20
|
+
temperature: options.temperature ?? 0,
|
|
21
|
+
...(options.stop ? { stop_sequences: options.stop } : {}),
|
|
22
|
+
system: [{ type: "text", text: options.system, cache_control: { type: "ephemeral" } }],
|
|
21
23
|
messages: [{ role: "user", content: prompt }],
|
|
22
24
|
});
|
|
23
25
|
const block = message.content[0];
|
|
@@ -30,7 +32,9 @@ export class AnthropicProvider implements LLMProvider {
|
|
|
30
32
|
const stream = await this.client.messages.stream({
|
|
31
33
|
model: options.model ?? "claude-haiku-4-5-20251001",
|
|
32
34
|
max_tokens: options.maxTokens ?? 256,
|
|
33
|
-
|
|
35
|
+
temperature: options.temperature ?? 0,
|
|
36
|
+
...(options.stop ? { stop_sequences: options.stop } : {}),
|
|
37
|
+
system: [{ type: "text", text: options.system, cache_control: { type: "ephemeral" } }],
|
|
34
38
|
messages: [{ role: "user", content: prompt }],
|
|
35
39
|
});
|
|
36
40
|
for await (const chunk of stream) {
|
package/src/providers/base.ts
CHANGED
|
@@ -1,108 +1,9 @@
|
|
|
1
|
-
// Cerebras provider —
|
|
2
|
-
|
|
1
|
+
// Cerebras provider — fast inference on Qwen/Llama models
|
|
2
|
+
import { OpenAICompatibleProvider } from "./openai-compat.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1";
|
|
7
|
-
const DEFAULT_MODEL = "qwen-3-235b-a22b-instruct-2507";
|
|
8
|
-
|
|
9
|
-
export class CerebrasProvider implements LLMProvider {
|
|
4
|
+
export class CerebrasProvider extends OpenAICompatibleProvider {
|
|
10
5
|
readonly name = "cerebras";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.apiKey = process.env.CEREBRAS_API_KEY ?? "";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
isAvailable(): boolean {
|
|
18
|
-
return !!process.env.CEREBRAS_API_KEY;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async complete(prompt: string, options: ProviderOptions): Promise<string> {
|
|
22
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
23
|
-
const res = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, {
|
|
24
|
-
method: "POST",
|
|
25
|
-
headers: {
|
|
26
|
-
"Content-Type": "application/json",
|
|
27
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
28
|
-
},
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
model,
|
|
31
|
-
max_tokens: options.maxTokens ?? 256,
|
|
32
|
-
messages: [
|
|
33
|
-
{ role: "system", content: options.system },
|
|
34
|
-
{ role: "user", content: prompt },
|
|
35
|
-
],
|
|
36
|
-
}),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
if (!res.ok) {
|
|
40
|
-
const text = await res.text();
|
|
41
|
-
throw new Error(`Cerebras API error ${res.status}: ${text}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const json = (await res.json()) as any;
|
|
45
|
-
return (json.choices?.[0]?.message?.content ?? "").trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async stream(prompt: string, options: ProviderOptions, callbacks: StreamCallbacks): Promise<string> {
|
|
49
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
50
|
-
const res = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: {
|
|
53
|
-
"Content-Type": "application/json",
|
|
54
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
-
model,
|
|
58
|
-
max_tokens: options.maxTokens ?? 256,
|
|
59
|
-
stream: true,
|
|
60
|
-
messages: [
|
|
61
|
-
{ role: "system", content: options.system },
|
|
62
|
-
{ role: "user", content: prompt },
|
|
63
|
-
],
|
|
64
|
-
}),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const text = await res.text();
|
|
69
|
-
throw new Error(`Cerebras API error ${res.status}: ${text}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let result = "";
|
|
73
|
-
const reader = res.body?.getReader();
|
|
74
|
-
if (!reader) throw new Error("No response body");
|
|
75
|
-
|
|
76
|
-
const decoder = new TextDecoder();
|
|
77
|
-
let buffer = "";
|
|
78
|
-
|
|
79
|
-
while (true) {
|
|
80
|
-
const { done, value } = await reader.read();
|
|
81
|
-
if (done) break;
|
|
82
|
-
|
|
83
|
-
buffer += decoder.decode(value, { stream: true });
|
|
84
|
-
const lines = buffer.split("\n");
|
|
85
|
-
buffer = lines.pop() ?? "";
|
|
86
|
-
|
|
87
|
-
for (const line of lines) {
|
|
88
|
-
const trimmed = line.trim();
|
|
89
|
-
if (!trimmed.startsWith("data: ")) continue;
|
|
90
|
-
const data = trimmed.slice(6);
|
|
91
|
-
if (data === "[DONE]") break;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const parsed = JSON.parse(data) as any;
|
|
95
|
-
const delta = parsed.choices?.[0]?.delta?.content;
|
|
96
|
-
if (delta) {
|
|
97
|
-
result += delta;
|
|
98
|
-
callbacks.onToken(result.trim());
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// skip malformed chunks
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return result.trim();
|
|
107
|
-
}
|
|
6
|
+
protected readonly baseUrl = "https://api.cerebras.ai/v1";
|
|
7
|
+
protected readonly defaultModel = "qwen-3-235b-a22b-instruct-2507";
|
|
8
|
+
protected readonly apiKeyEnvVar = "CEREBRAS_API_KEY";
|
|
108
9
|
}
|