@hasna/terminal 2.3.2 → 3.1.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.
- package/dist/ai.js +78 -85
- 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 +83 -94
- 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/dist/ai.js
CHANGED
|
@@ -21,17 +21,26 @@ const MODEL_DEFAULTS = {
|
|
|
21
21
|
xai: { fast: "grok-code-fast-1", smart: "grok-4-fast-non-reasoning" },
|
|
22
22
|
anthropic: { fast: "claude-haiku-4-5-20251001", smart: "claude-sonnet-4-6" },
|
|
23
23
|
};
|
|
24
|
-
/** Load user model overrides from ~/.terminal/config.json */
|
|
24
|
+
/** Load user model overrides from ~/.terminal/config.json (cached 30s) */
|
|
25
|
+
let _modelOverrides = null;
|
|
26
|
+
let _modelOverridesAt = 0;
|
|
25
27
|
function loadModelOverrides() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (_modelOverrides && now - _modelOverridesAt < 30_000)
|
|
30
|
+
return _modelOverrides;
|
|
26
31
|
try {
|
|
27
32
|
const configPath = join(process.env.HOME ?? "~", ".terminal", "config.json");
|
|
28
33
|
if (existsSync(configPath)) {
|
|
29
34
|
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
30
|
-
|
|
35
|
+
_modelOverrides = config.models ?? {};
|
|
36
|
+
_modelOverridesAt = now;
|
|
37
|
+
return _modelOverrides;
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
catch { }
|
|
34
|
-
|
|
41
|
+
_modelOverrides = {};
|
|
42
|
+
_modelOverridesAt = now;
|
|
43
|
+
return _modelOverrides;
|
|
35
44
|
}
|
|
36
45
|
/** Model routing per provider — config-driven with defaults */
|
|
37
46
|
function pickModel(nl) {
|
|
@@ -122,6 +131,7 @@ function detectProjectContext() {
|
|
|
122
131
|
}
|
|
123
132
|
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
124
133
|
function buildSystemPrompt(perms, sessionEntries, currentPrompt) {
|
|
134
|
+
const nl = currentPrompt?.toLowerCase() ?? "";
|
|
125
135
|
const restrictions = [];
|
|
126
136
|
if (!perms.destructive)
|
|
127
137
|
restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
|
|
@@ -150,7 +160,6 @@ function buildSystemPrompt(perms, sessionEntries, currentPrompt) {
|
|
|
150
160
|
contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
|
|
151
161
|
}
|
|
152
162
|
const projectContext = detectProjectContext();
|
|
153
|
-
// Inject safety hints for the command being generated (AI sees what's risky)
|
|
154
163
|
const safetyBlock = sessionEntries.length > 0
|
|
155
164
|
? (() => {
|
|
156
165
|
const lastCmd = sessionEntries[sessionEntries.length - 1]?.cmd;
|
|
@@ -161,72 +170,50 @@ function buildSystemPrompt(perms, sessionEntries, currentPrompt) {
|
|
|
161
170
|
return "";
|
|
162
171
|
})()
|
|
163
172
|
: "";
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
// ── Conditional sections (only included when relevant) ──
|
|
174
|
+
const wantsStructure = /\b(function|class|interface|export|symbol|structure|hierarchy|outline)\b/i.test(nl);
|
|
175
|
+
const astBlock = wantsStructure ? `\nAST-POWERED QUERIES: For code STRUCTURE questions, use "terminal symbols" instead of grep. It uses AST parsing for TypeScript, Python, Go, Rust.` : "";
|
|
176
|
+
const wantsMultiple = /\b(and|both|also|plus|as well)\b/i.test(nl);
|
|
177
|
+
const compoundBlock = wantsMultiple ? `\nCOMPOUND QUESTIONS: Prefer ONE command that captures all info. NEVER split into separate expensive commands.` : "";
|
|
178
|
+
const wantsAnalysis = /\b(quality|lint|coverage|complexity|unused|dead code|security|audit|scan|dependency)\b/i.test(nl);
|
|
179
|
+
const blockedAltBlock = wantsAnalysis ? `\nBLOCKED ALTERNATIVES: If your preferred command needs installing packages, try READ-ONLY alternatives (grep, cat, wc, awk). NEVER give up on analysis questions.` : "";
|
|
180
|
+
return `Translate to bash. One command. Simplest form. No explanation.
|
|
166
181
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
|
|
199
|
-
- "how many tests and do they pass" → bun test (extract count AND pass/fail from output)
|
|
200
|
-
- "what files changed and how many lines" → git log --stat -3 (shows files AND line counts)
|
|
201
|
-
- "what version of node and bun" → node -v && bun -v (only use && for trivial non-failing commands)
|
|
202
|
-
NEVER split into separate test runs or expensive commands chained with &&.
|
|
203
|
-
|
|
204
|
-
BLOCKED ALTERNATIVES: If your preferred command would require installing packages (npx, npm install), ALWAYS try a READ-ONLY alternative:
|
|
205
|
-
- Code quality analysis → grep -rn "TODO\\|FIXME\\|HACK\\|XXX" src/
|
|
206
|
-
- Linting → check if "lint" or "typecheck" exists in package.json scripts, run that
|
|
207
|
-
- Security scan → grep -rn "eval\\|exec\\|spawn\\|password\\|secret" src/
|
|
208
|
-
- Dependency audit → cat package.json | grep -A 50 dependencies
|
|
209
|
-
- Test coverage → bun test --coverage (or npm run test:coverage if available)
|
|
210
|
-
NEVER give up. NEVER output BLOCKED for analysis questions. Always try a grep/find/cat/wc/awk read-only alternative.
|
|
211
|
-
- Cyclomatic complexity → grep -rn "if\\|else\\|for\\|while\\|switch\\|case\\|catch\\|&&\\|||" src/ --include="*.ts" | wc -l
|
|
212
|
-
- Unused exports → grep -rn "export function\|export const\|export class" src/ --include="*.ts" | sed 's/.*export [a-z]* //' | sed 's/[(<:].*//' | sort -u
|
|
213
|
-
- Dead code → for each exported name, grep -rn "name" src/ --include="*.ts" | wc -l (if only 1 match = unused)
|
|
214
|
-
- Dependency graph → grep -rn "from " src/ --include="*.ts" | sed 's/:.*from "/→/' | sed 's/".*//' | sort -u
|
|
215
|
-
- Most parameters → grep -rn "function " src/ --include="*.ts" | awk -F'[()]' '{print gsub(/,/,",",$2)+1, $0}' | sort -nr | head -10
|
|
216
|
-
ALWAYS try a heuristic shell approach before giving up. NEVER say BLOCKED for analysis questions.
|
|
217
|
-
|
|
218
|
-
SEMANTIC MAPPING: When the user references a concept, search the file tree for RELATED terms:
|
|
219
|
-
- Look at directory names: src/agent/ likely contains "agentic" code
|
|
220
|
-
- Look at file names: lazy-executor.ts likely handles "lazy mode"
|
|
221
|
-
- When uncertain: grep -rn "keyword" src/ --include="*.ts" -l (list matching files)
|
|
222
|
-
|
|
223
|
-
ACTION vs CONCEPTUAL: If the prompt starts with "run", "execute", "check", "test", "build", "show output of" — ALWAYS generate an executable command. NEVER read README for action requests. Only read docs for "explain why", "what does X mean", "how was X designed".
|
|
224
|
-
|
|
225
|
-
EXISTENCE CHECKS: If the prompt starts with "is there", "does this have", "do we have", "does X exist" — NEVER run/start/launch anything. Use ls, find, or test -d to CHECK existence. These are READ-ONLY questions.
|
|
226
|
-
|
|
227
|
-
MONOREPO: If the project context says "MONOREPO", search packages/ or apps/ NOT src/. Use: grep -rn "pattern" packages/ --include="*.ts". For specific packages, use packages/PKGNAME/src/.
|
|
182
|
+
list files in current directory → ls
|
|
183
|
+
list all files including hidden → ls -a
|
|
184
|
+
show open files → lsof
|
|
185
|
+
create copy of a.txt as b.txt → cp a.txt b.txt
|
|
186
|
+
create file test.txt → touch test.txt
|
|
187
|
+
make directory testdir → mkdir testdir
|
|
188
|
+
display routing table → route
|
|
189
|
+
show last logged in users → last
|
|
190
|
+
show file stats → stat file
|
|
191
|
+
print directory tree 2 levels → tree -L 2
|
|
192
|
+
count word occurrences in file → grep -c "word" file
|
|
193
|
+
print number of files in dir → ls -1 | wc -l
|
|
194
|
+
print first line of file → head -1 file
|
|
195
|
+
print last line of file → tail -1 file
|
|
196
|
+
print lines 3 to 5 of file → sed -n '3,5p' file
|
|
197
|
+
print every other line → awk 'NR%2==1' file
|
|
198
|
+
count words in file → wc -w file
|
|
199
|
+
find empty files not in subdirs → find . -maxdepth 1 -type f -empty
|
|
200
|
+
show system load → w
|
|
201
|
+
system utilization stats → vmstat
|
|
202
|
+
DNS servers → cat /etc/resolv.conf | grep nameserver
|
|
203
|
+
long integer size → getconf LONG_BIT
|
|
204
|
+
base64 decode string → echo 'str' | base64 -d
|
|
205
|
+
change owner to nobody → chown nobody file
|
|
206
|
+
unique lines in file → uniq file
|
|
207
|
+
max cpu time → ulimit -t
|
|
208
|
+
memory info → lsmem
|
|
209
|
+
process priority → nice
|
|
210
|
+
bash profile → cat ~/.bashrc
|
|
211
|
+
search recursively → grep -rn "pattern" src/
|
|
212
|
+
${astBlock}${compoundBlock}${blockedAltBlock}
|
|
228
213
|
cwd: ${process.cwd()}
|
|
229
|
-
shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}
|
|
214
|
+
shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}
|
|
215
|
+
|
|
216
|
+
Q:`;
|
|
230
217
|
}
|
|
231
218
|
// ── streaming translate ───────────────────────────────────────────────────────
|
|
232
219
|
export async function translateToCommand(nl, perms, sessionEntries, onToken) {
|
|
@@ -244,12 +231,12 @@ export async function translateToCommand(nl, perms, sessionEntries, onToken) {
|
|
|
244
231
|
const system = buildSystemPrompt(perms, sessionEntries, nl);
|
|
245
232
|
let text;
|
|
246
233
|
if (onToken) {
|
|
247
|
-
text = await provider.stream(nl, { model, maxTokens: 256, system }, {
|
|
234
|
+
text = await provider.stream(nl, { model, maxTokens: 256, temperature: 0, stop: ["\n"], system }, {
|
|
248
235
|
onToken: (partial) => onToken(partial),
|
|
249
236
|
});
|
|
250
237
|
}
|
|
251
238
|
else {
|
|
252
|
-
text = await provider.complete(nl, { model, maxTokens: 256, system });
|
|
239
|
+
text = await provider.complete(nl, { model, maxTokens: 256, temperature: 0, stop: ["\n"], system });
|
|
253
240
|
}
|
|
254
241
|
if (text.startsWith("BLOCKED:"))
|
|
255
242
|
throw new Error(text);
|
|
@@ -294,29 +281,35 @@ export async function explainCommand(command) {
|
|
|
294
281
|
return provider.complete(command, {
|
|
295
282
|
model: routing.fast,
|
|
296
283
|
maxTokens: 128,
|
|
284
|
+
temperature: 0,
|
|
297
285
|
system: "Explain what this shell command does in one plain English sentence. No markdown, no code blocks.",
|
|
298
286
|
});
|
|
299
287
|
}
|
|
300
288
|
// ── auto-fix ──────────────────────────────────────────────────────────────────
|
|
301
|
-
export async function fixCommand(originalNl, failedCommand, errorOutput, perms,
|
|
289
|
+
export async function fixCommand(originalNl, failedCommand, errorOutput, perms, _sessionEntries) {
|
|
302
290
|
const provider = getProvider();
|
|
303
291
|
const routing = pickModel(originalNl);
|
|
304
|
-
|
|
305
|
-
|
|
292
|
+
// Lightweight fix prompt — no full project context, just rules + restrictions
|
|
293
|
+
const restrictions = [];
|
|
294
|
+
if (!perms.destructive)
|
|
295
|
+
restrictions.push("- NEVER delete/remove/overwrite files");
|
|
296
|
+
if (!perms.network)
|
|
297
|
+
restrictions.push("- NEVER make network requests");
|
|
298
|
+
if (!perms.install)
|
|
299
|
+
restrictions.push("- NEVER install packages");
|
|
300
|
+
const fixSystem = `You are a terminal assistant. Output ONLY the corrected shell command — no explanation.
|
|
301
|
+
macOS/BSD tools. NEVER use grep -P. Use grep -E for extended regex.
|
|
302
|
+
NEVER install packages. READ-ONLY terminal.
|
|
303
|
+
cwd: ${process.cwd()}${restrictions.length > 0 ? `\nRESTRICTIONS:\n${restrictions.join("\n")}` : ""}`;
|
|
304
|
+
const text = await provider.complete(`I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput.slice(0, 2000)}\n\nGive me the corrected command only.`, {
|
|
305
|
+
model: routing.smart,
|
|
306
306
|
maxTokens: 256,
|
|
307
|
-
|
|
307
|
+
temperature: 0,
|
|
308
|
+
stop: ["\n"],
|
|
309
|
+
system: fixSystem,
|
|
308
310
|
});
|
|
309
311
|
if (text.startsWith("BLOCKED:"))
|
|
310
312
|
throw new Error(text);
|
|
311
|
-
return text;
|
|
312
|
-
}
|
|
313
|
-
// ── summarize output (for MCP/agent use) ──────────────────────────────────────
|
|
314
|
-
export async function summarizeOutput(command, output, maxTokens = 200) {
|
|
315
|
-
const provider = getProvider();
|
|
316
|
-
const routing = pickModel("summarize");
|
|
317
|
-
return provider.complete(`Command: ${command}\nOutput:\n${output}\n\nSummarize this output concisely for an AI agent. Focus on: status, key results, errors. Be terse.`, {
|
|
318
|
-
model: routing.fast,
|
|
319
|
-
maxTokens,
|
|
320
|
-
system: "You summarize command output for AI agents. Be extremely concise. Return structured info. No prose.",
|
|
321
|
-
});
|
|
313
|
+
return text.trim();
|
|
322
314
|
}
|
|
315
|
+
// summarizeOutput() removed — all output processing goes through processOutput() in output-processor.ts
|
package/dist/cache.js
CHANGED
|
@@ -19,12 +19,13 @@ function persistCache() {
|
|
|
19
19
|
}
|
|
20
20
|
catch { }
|
|
21
21
|
}
|
|
22
|
-
/** Normalize a natural language query for cache lookup
|
|
22
|
+
/** Normalize a natural language query for cache lookup.
|
|
23
|
+
* Keeps . / - _ which are meaningful in file paths and shell context. */
|
|
23
24
|
export function normalizeNl(nl) {
|
|
24
25
|
return nl
|
|
25
26
|
.toLowerCase()
|
|
26
27
|
.trim()
|
|
27
|
-
.replace(/[^a-z0-9\s]/g, "") //
|
|
28
|
+
.replace(/[^a-z0-9\s.\/_-]/g, "") // keep meaningful shell chars
|
|
28
29
|
.replace(/\s+/g, " ");
|
|
29
30
|
}
|
|
30
31
|
export function cacheGet(nl) {
|
package/dist/cli.js
CHANGED
|
@@ -471,7 +471,7 @@ else if (args.length > 0) {
|
|
|
471
471
|
const { rewriteCommand } = await import("./command-rewriter.js");
|
|
472
472
|
const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
|
|
473
473
|
const { saveOutput, formatOutputHint } = await import("./output-store.js");
|
|
474
|
-
const {
|
|
474
|
+
const { estimateTokens } = await import("./tokens.js");
|
|
475
475
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
476
476
|
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
477
477
|
const { detectLoop } = await import("./loop-detector.js");
|
package/dist/compression.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Token compression engine — reduces CLI output to fit within token budgets
|
|
2
|
-
|
|
2
|
+
// No regex parsing — just ANSI stripping, deduplication, and smart truncation.
|
|
3
|
+
// All intelligent output processing goes through AI via processOutput().
|
|
4
|
+
import { estimateTokens } from "./tokens.js";
|
|
3
5
|
/** Strip ANSI escape codes from text */
|
|
4
6
|
export function stripAnsi(text) {
|
|
5
7
|
// eslint-disable-next-line no-control-regex
|
|
@@ -14,7 +16,6 @@ function deduplicateLines(lines) {
|
|
|
14
16
|
let repeatPattern = "";
|
|
15
17
|
for (let i = 0; i < lines.length; i++) {
|
|
16
18
|
const line = lines[i];
|
|
17
|
-
// Extract a "pattern" — the line without numbers, paths, specific identifiers
|
|
18
19
|
const pattern = line.replace(/[0-9]+/g, "N").replace(/\/\S+/g, "/PATH").replace(/\s+/g, " ").trim();
|
|
19
20
|
if (pattern === repeatPattern) {
|
|
20
21
|
repeatCount++;
|
|
@@ -24,7 +25,6 @@ function deduplicateLines(lines) {
|
|
|
24
25
|
result.push(` ... (${repeatCount} similar lines)`);
|
|
25
26
|
}
|
|
26
27
|
else if (repeatCount > 0) {
|
|
27
|
-
// Push the skipped lines back
|
|
28
28
|
for (let j = i - repeatCount; j < i; j++) {
|
|
29
29
|
result.push(lines[j]);
|
|
30
30
|
}
|
|
@@ -44,13 +44,12 @@ function deduplicateLines(lines) {
|
|
|
44
44
|
}
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
47
|
-
/** Smart truncation: keep first
|
|
47
|
+
/** Smart truncation: keep first 60% + last 40% of lines */
|
|
48
48
|
function smartTruncate(text, maxTokens) {
|
|
49
49
|
const lines = text.split("\n");
|
|
50
50
|
const currentTokens = estimateTokens(text);
|
|
51
51
|
if (currentTokens <= maxTokens)
|
|
52
52
|
return text;
|
|
53
|
-
// Keep proportional first/last, with first getting more
|
|
54
53
|
const targetLines = Math.floor((maxTokens * lines.length) / currentTokens);
|
|
55
54
|
const firstCount = Math.ceil(targetLines * 0.6);
|
|
56
55
|
const lastCount = Math.floor(targetLines * 0.4);
|
|
@@ -61,44 +60,23 @@ function smartTruncate(text, maxTokens) {
|
|
|
61
60
|
const hiddenCount = lines.length - firstCount - lastCount;
|
|
62
61
|
return [...first, `\n--- ${hiddenCount} lines hidden ---\n`, ...last].join("\n");
|
|
63
62
|
}
|
|
64
|
-
/** Compress command output
|
|
63
|
+
/** Compress command output — ANSI strip, dedup, truncate. No parsing. */
|
|
65
64
|
export function compress(command, output, options = {}) {
|
|
66
|
-
const { maxTokens,
|
|
65
|
+
const { maxTokens, stripAnsi: doStrip = true } = options;
|
|
67
66
|
const originalTokens = estimateTokens(output);
|
|
68
67
|
// Step 1: Strip ANSI codes
|
|
69
68
|
let text = doStrip ? stripAnsi(output) : output;
|
|
70
|
-
// Step 2:
|
|
71
|
-
if (format === "json" || format === "summary") {
|
|
72
|
-
const parsed = parseOutput(command, text);
|
|
73
|
-
if (parsed) {
|
|
74
|
-
const json = JSON.stringify(parsed.data, null, format === "summary" ? 0 : 2);
|
|
75
|
-
const savings = tokenSavings(output, parsed.data);
|
|
76
|
-
const compressedTokens = estimateTokens(json);
|
|
77
|
-
// ONLY use JSON if it actually saves tokens (never return larger output)
|
|
78
|
-
if (savings.saved > 0 && (!maxTokens || compressedTokens <= maxTokens)) {
|
|
79
|
-
return {
|
|
80
|
-
content: json,
|
|
81
|
-
format: "json",
|
|
82
|
-
originalTokens,
|
|
83
|
-
compressedTokens,
|
|
84
|
-
tokensSaved: savings.saved,
|
|
85
|
-
savingsPercent: savings.percent,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Step 3: Deduplicate similar lines
|
|
69
|
+
// Step 2: Deduplicate similar lines
|
|
91
70
|
const lines = text.split("\n");
|
|
92
71
|
const deduped = deduplicateLines(lines);
|
|
93
72
|
text = deduped.join("\n");
|
|
94
|
-
// Step
|
|
73
|
+
// Step 3: Smart truncation if over budget
|
|
95
74
|
if (maxTokens) {
|
|
96
75
|
text = smartTruncate(text, maxTokens);
|
|
97
76
|
}
|
|
98
77
|
const compressedTokens = estimateTokens(text);
|
|
99
78
|
return {
|
|
100
79
|
content: text,
|
|
101
|
-
format: "text",
|
|
102
80
|
originalTokens,
|
|
103
81
|
compressedTokens,
|
|
104
82
|
tokensSaved: Math.max(0, originalTokens - compressedTokens),
|
package/dist/context-hints.js
CHANGED
|
@@ -31,18 +31,31 @@ export function discoverProjectHints(cwd) {
|
|
|
31
31
|
hints.push(`Project type: ${lang} (${file} found)`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
// Extract
|
|
34
|
+
// Extract metadata from package.json — trimmed to save tokens
|
|
35
35
|
const pkgPath = join(cwd, "package.json");
|
|
36
36
|
if (existsSync(pkgPath)) {
|
|
37
37
|
try {
|
|
38
38
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
39
39
|
if (pkg.name)
|
|
40
|
-
hints.push(`Package
|
|
40
|
+
hints.push(`Package: ${pkg.name}@${pkg.version ?? "?"}`);
|
|
41
41
|
if (pkg.scripts) {
|
|
42
|
-
|
|
42
|
+
// Only top-5 most useful scripts
|
|
43
|
+
const priority = ["dev", "build", "test", "lint", "start", "typecheck", "check"];
|
|
44
|
+
const scripts = Object.keys(pkg.scripts);
|
|
45
|
+
const top = priority.filter(s => scripts.includes(s));
|
|
46
|
+
const rest = scripts.filter(s => !priority.includes(s)).slice(0, Math.max(0, 5 - top.length));
|
|
47
|
+
hints.push(`Scripts: ${[...top, ...rest].join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
if (pkg.dependencies) {
|
|
50
|
+
// Only framework/major deps — skip utility libs
|
|
51
|
+
const major = ["react", "next", "express", "fastify", "hono", "vue", "angular", "svelte",
|
|
52
|
+
"prisma", "drizzle", "mongoose", "typeorm", "zod", "trpc", "graphql", "tailwindcss",
|
|
53
|
+
"electron", "bun", "elysia", "nest", "nuxt", "remix", "astro", "vite"];
|
|
54
|
+
const deps = Object.keys(pkg.dependencies);
|
|
55
|
+
const found = deps.filter(d => major.some(m => d.includes(m)));
|
|
56
|
+
if (found.length > 0)
|
|
57
|
+
hints.push(`Key deps: ${found.slice(0, 10).join(", ")}`);
|
|
43
58
|
}
|
|
44
|
-
if (pkg.dependencies)
|
|
45
|
-
hints.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
46
59
|
}
|
|
47
60
|
catch { }
|
|
48
61
|
}
|
|
@@ -101,21 +114,18 @@ export function discoverProjectHints(cwd) {
|
|
|
101
114
|
}
|
|
102
115
|
catch { }
|
|
103
116
|
}
|
|
104
|
-
// Source directory structure
|
|
117
|
+
// Source directory structure — max 20 files to save tokens
|
|
105
118
|
try {
|
|
106
119
|
const { execSync } = require("child_process");
|
|
107
120
|
const srcDirs = ["src", "lib", "app", "packages"];
|
|
108
121
|
for (const dir of srcDirs) {
|
|
109
122
|
if (existsSync(join(cwd, dir))) {
|
|
110
|
-
const tree = execSync(`find ${dir} -maxdepth
|
|
123
|
+
const tree = execSync(`find ${dir} -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | sort | head -20`, { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
111
124
|
if (tree)
|
|
112
125
|
hints.push(`Files in ${dir}/:\n${tree}`);
|
|
113
126
|
break;
|
|
114
127
|
}
|
|
115
128
|
}
|
|
116
|
-
// Top-level files
|
|
117
|
-
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
118
|
-
hints.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
119
129
|
}
|
|
120
130
|
catch { }
|
|
121
131
|
return hints;
|
package/dist/diff-cache.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Diff-aware output caching — when same command runs again, return only what changed
|
|
2
|
-
import { estimateTokens } from "./
|
|
2
|
+
import { estimateTokens } from "./tokens.js";
|
|
3
3
|
const cache = new Map();
|
|
4
4
|
function cacheKey(command, cwd) {
|
|
5
5
|
return `${cwd}:${command}`;
|
package/dist/discover.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// estimates how much terminal would have saved.
|
|
4
4
|
import { readdirSync, readFileSync, statSync, existsSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
|
-
import { estimateTokens } from "./
|
|
6
|
+
import { estimateTokens } from "./tokens.js";
|
|
7
7
|
/** Find all Claude session JSONL files */
|
|
8
8
|
function findSessionFiles(claudeDir, maxAge) {
|
|
9
9
|
const files = [];
|
package/dist/economy.js
CHANGED
|
@@ -44,12 +44,29 @@ function loadStats() {
|
|
|
44
44
|
};
|
|
45
45
|
return stats;
|
|
46
46
|
}
|
|
47
|
+
let _saveTimer = null;
|
|
47
48
|
function saveStats() {
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Debounce: coalesce multiple writes within 1 second
|
|
50
|
+
if (_saveTimer)
|
|
51
|
+
return;
|
|
52
|
+
_saveTimer = setTimeout(() => {
|
|
53
|
+
_saveTimer = null;
|
|
54
|
+
ensureDir();
|
|
55
|
+
if (stats) {
|
|
56
|
+
writeFileSync(ECONOMY_FILE, JSON.stringify(stats, null, 2));
|
|
57
|
+
}
|
|
58
|
+
}, 1000);
|
|
52
59
|
}
|
|
60
|
+
// Flush on exit
|
|
61
|
+
process.on("exit", () => {
|
|
62
|
+
if (_saveTimer) {
|
|
63
|
+
clearTimeout(_saveTimer);
|
|
64
|
+
_saveTimer = null;
|
|
65
|
+
ensureDir();
|
|
66
|
+
if (stats)
|
|
67
|
+
writeFileSync(ECONOMY_FILE, JSON.stringify(stats, null, 2));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
53
70
|
/** Record token savings from a feature */
|
|
54
71
|
export function recordSaving(feature, tokensSaved) {
|
|
55
72
|
const s = loadStats();
|
|
@@ -89,8 +106,23 @@ const PROVIDER_PRICING = {
|
|
|
89
106
|
"anthropic-sonnet": { input: 3.00, output: 15.00 },
|
|
90
107
|
"anthropic-opus": { input: 5.00, output: 25.00 },
|
|
91
108
|
};
|
|
109
|
+
/** Load configurable turns-before-compaction from ~/.terminal/config.json */
|
|
110
|
+
function loadTurnsMultiplier() {
|
|
111
|
+
try {
|
|
112
|
+
const configPath = join(DIR, "config.json");
|
|
113
|
+
if (existsSync(configPath)) {
|
|
114
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
115
|
+
return config.economy?.turnsBeforeCompaction ?? 5;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
return 5; // Default: tokens saved are repeated ~5 turns before agent compacts context
|
|
120
|
+
}
|
|
92
121
|
/** Estimate USD savings from compressed tokens */
|
|
93
|
-
export function estimateSavingsUsd(tokensSaved, consumerModel = "anthropic-opus", avgTurnsBeforeCompaction
|
|
122
|
+
export function estimateSavingsUsd(tokensSaved, consumerModel = "anthropic-opus", avgTurnsBeforeCompaction) {
|
|
123
|
+
if (avgTurnsBeforeCompaction === undefined) {
|
|
124
|
+
avgTurnsBeforeCompaction = loadTurnsMultiplier();
|
|
125
|
+
}
|
|
94
126
|
const pricing = PROVIDER_PRICING[consumerModel] ?? PROVIDER_PRICING["anthropic-opus"];
|
|
95
127
|
const multipliedTokens = tokensSaved * avgTurnsBeforeCompaction;
|
|
96
128
|
const savingsUsd = (multipliedTokens * pricing.input) / 1_000_000;
|
package/dist/expand-store.js
CHANGED
|
@@ -15,6 +15,10 @@ export function storeOutput(command, output) {
|
|
|
15
15
|
store.set(key, { command, output, timestamp: Date.now() });
|
|
16
16
|
return key;
|
|
17
17
|
}
|
|
18
|
+
/** Escape regex special characters for safe use in new RegExp() */
|
|
19
|
+
function escapeRegex(str) {
|
|
20
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
}
|
|
18
22
|
/** Retrieve full output by key, optionally filtered */
|
|
19
23
|
export function expandOutput(key, grep) {
|
|
20
24
|
const entry = store.get(key);
|
|
@@ -22,7 +26,9 @@ export function expandOutput(key, grep) {
|
|
|
22
26
|
return { found: false };
|
|
23
27
|
let output = entry.output;
|
|
24
28
|
if (grep) {
|
|
25
|
-
|
|
29
|
+
// Escape metacharacters so user input like "[error" or "func()" doesn't crash
|
|
30
|
+
const safe = escapeRegex(grep);
|
|
31
|
+
const pattern = new RegExp(safe, "i");
|
|
26
32
|
output = output.split("\n").filter(l => pattern.test(l)).join("\n");
|
|
27
33
|
}
|
|
28
34
|
return { found: true, output, lines: output.split("\n").length };
|