@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.
Files changed (50) hide show
  1. package/dist/ai.js +78 -85
  2. package/dist/cache.js +3 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/compression.js +8 -30
  5. package/dist/context-hints.js +20 -10
  6. package/dist/diff-cache.js +1 -1
  7. package/dist/discover.js +1 -1
  8. package/dist/economy.js +37 -5
  9. package/dist/expand-store.js +7 -1
  10. package/dist/mcp/server.js +44 -68
  11. package/dist/output-processor.js +10 -7
  12. package/dist/providers/anthropic.js +6 -2
  13. package/dist/providers/cerebras.js +6 -93
  14. package/dist/providers/groq.js +6 -93
  15. package/dist/providers/index.js +85 -36
  16. package/dist/providers/openai-compat.js +93 -0
  17. package/dist/providers/xai.js +6 -93
  18. package/dist/tokens.js +17 -0
  19. package/dist/tool-profiles.js +9 -2
  20. package/package.json +1 -1
  21. package/src/ai.ts +83 -94
  22. package/src/cache.ts +3 -2
  23. package/src/cli.tsx +1 -1
  24. package/src/compression.ts +8 -35
  25. package/src/context-hints.ts +20 -10
  26. package/src/diff-cache.ts +1 -1
  27. package/src/discover.ts +1 -1
  28. package/src/economy.ts +37 -5
  29. package/src/expand-store.ts +8 -1
  30. package/src/mcp/server.ts +45 -73
  31. package/src/output-processor.ts +11 -8
  32. package/src/providers/anthropic.ts +6 -2
  33. package/src/providers/base.ts +2 -0
  34. package/src/providers/cerebras.ts +6 -105
  35. package/src/providers/groq.ts +6 -105
  36. package/src/providers/index.ts +84 -33
  37. package/src/providers/openai-compat.ts +109 -0
  38. package/src/providers/xai.ts +6 -105
  39. package/src/tokens.ts +18 -0
  40. package/src/tool-profiles.ts +9 -2
  41. package/src/compression.test.ts +0 -49
  42. package/src/output-router.ts +0 -56
  43. package/src/parsers/base.ts +0 -72
  44. package/src/parsers/build.ts +0 -73
  45. package/src/parsers/errors.ts +0 -107
  46. package/src/parsers/files.ts +0 -91
  47. package/src/parsers/git.ts +0 -101
  48. package/src/parsers/index.ts +0 -66
  49. package/src/parsers/parsers.test.ts +0 -153
  50. package/src/parsers/tests.ts +0 -98
@@ -0,0 +1,93 @@
1
+ // Shared base class for OpenAI-compatible providers (Cerebras, Groq, xAI)
2
+ // Eliminates ~200 lines of duplicated streaming SSE parsing
3
+ export class OpenAICompatibleProvider {
4
+ get apiKey() {
5
+ return process.env[this.apiKeyEnvVar] ?? "";
6
+ }
7
+ isAvailable() {
8
+ return !!process.env[this.apiKeyEnvVar];
9
+ }
10
+ async complete(prompt, options) {
11
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
12
+ method: "POST",
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ Authorization: `Bearer ${this.apiKey}`,
16
+ },
17
+ body: JSON.stringify({
18
+ model: options.model ?? this.defaultModel,
19
+ max_tokens: options.maxTokens ?? 256,
20
+ temperature: options.temperature ?? 0,
21
+ ...(options.stop ? { stop: options.stop } : {}),
22
+ messages: [
23
+ { role: "system", content: options.system },
24
+ { role: "user", content: prompt },
25
+ ],
26
+ }),
27
+ });
28
+ if (!res.ok) {
29
+ const text = await res.text();
30
+ throw new Error(`${this.name} API error ${res.status}: ${text}`);
31
+ }
32
+ const json = (await res.json());
33
+ return (json.choices?.[0]?.message?.content ?? "").trim();
34
+ }
35
+ async stream(prompt, options, callbacks) {
36
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ Authorization: `Bearer ${this.apiKey}`,
41
+ },
42
+ body: JSON.stringify({
43
+ model: options.model ?? this.defaultModel,
44
+ max_tokens: options.maxTokens ?? 256,
45
+ temperature: options.temperature ?? 0,
46
+ stream: true,
47
+ ...(options.stop ? { stop: options.stop } : {}),
48
+ messages: [
49
+ { role: "system", content: options.system },
50
+ { role: "user", content: prompt },
51
+ ],
52
+ }),
53
+ });
54
+ if (!res.ok) {
55
+ const text = await res.text();
56
+ throw new Error(`${this.name} API error ${res.status}: ${text}`);
57
+ }
58
+ let result = "";
59
+ const reader = res.body?.getReader();
60
+ if (!reader)
61
+ throw new Error("No response body");
62
+ const decoder = new TextDecoder();
63
+ let buffer = "";
64
+ while (true) {
65
+ const { done, value } = await reader.read();
66
+ if (done)
67
+ break;
68
+ buffer += decoder.decode(value, { stream: true });
69
+ const lines = buffer.split("\n");
70
+ buffer = lines.pop() ?? "";
71
+ for (const line of lines) {
72
+ const trimmed = line.trim();
73
+ if (!trimmed.startsWith("data: "))
74
+ continue;
75
+ const data = trimmed.slice(6);
76
+ if (data === "[DONE]")
77
+ break;
78
+ try {
79
+ const parsed = JSON.parse(data);
80
+ const delta = parsed.choices?.[0]?.delta?.content;
81
+ if (delta) {
82
+ result += delta;
83
+ callbacks.onToken(result.trim());
84
+ }
85
+ }
86
+ catch {
87
+ // skip malformed chunks
88
+ }
89
+ }
90
+ }
91
+ return result.trim();
92
+ }
93
+ }
@@ -1,95 +1,8 @@
1
- // xAI/Grok provider — uses OpenAI-compatible API
2
- // grok-code-fast-1 for code tasks, grok-4-fast for general queries.
3
- const XAI_BASE_URL = "https://api.x.ai/v1";
4
- const DEFAULT_MODEL = "grok-code-fast-1";
5
- export class XaiProvider {
1
+ // xAI/Grok provider — code-optimized models
2
+ import { OpenAICompatibleProvider } from "./openai-compat.js";
3
+ export class XaiProvider extends OpenAICompatibleProvider {
6
4
  name = "xai";
7
- apiKey;
8
- constructor() {
9
- this.apiKey = process.env.XAI_API_KEY ?? "";
10
- }
11
- isAvailable() {
12
- return !!process.env.XAI_API_KEY;
13
- }
14
- async complete(prompt, options) {
15
- const model = options.model ?? DEFAULT_MODEL;
16
- const res = await fetch(`${XAI_BASE_URL}/chat/completions`, {
17
- method: "POST",
18
- headers: {
19
- "Content-Type": "application/json",
20
- Authorization: `Bearer ${this.apiKey}`,
21
- },
22
- body: JSON.stringify({
23
- model,
24
- max_tokens: options.maxTokens ?? 256,
25
- messages: [
26
- { role: "system", content: options.system },
27
- { role: "user", content: prompt },
28
- ],
29
- }),
30
- });
31
- if (!res.ok) {
32
- const text = await res.text();
33
- throw new Error(`xAI API error ${res.status}: ${text}`);
34
- }
35
- const json = (await res.json());
36
- return (json.choices?.[0]?.message?.content ?? "").trim();
37
- }
38
- async stream(prompt, options, callbacks) {
39
- const model = options.model ?? DEFAULT_MODEL;
40
- const res = await fetch(`${XAI_BASE_URL}/chat/completions`, {
41
- method: "POST",
42
- headers: {
43
- "Content-Type": "application/json",
44
- Authorization: `Bearer ${this.apiKey}`,
45
- },
46
- body: JSON.stringify({
47
- model,
48
- max_tokens: options.maxTokens ?? 256,
49
- stream: true,
50
- messages: [
51
- { role: "system", content: options.system },
52
- { role: "user", content: prompt },
53
- ],
54
- }),
55
- });
56
- if (!res.ok) {
57
- const text = await res.text();
58
- throw new Error(`xAI API error ${res.status}: ${text}`);
59
- }
60
- let result = "";
61
- const reader = res.body?.getReader();
62
- if (!reader)
63
- throw new Error("No response body");
64
- const decoder = new TextDecoder();
65
- let buffer = "";
66
- while (true) {
67
- const { done, value } = await reader.read();
68
- if (done)
69
- break;
70
- buffer += decoder.decode(value, { stream: true });
71
- const lines = buffer.split("\n");
72
- buffer = lines.pop() ?? "";
73
- for (const line of lines) {
74
- const trimmed = line.trim();
75
- if (!trimmed.startsWith("data: "))
76
- continue;
77
- const data = trimmed.slice(6);
78
- if (data === "[DONE]")
79
- break;
80
- try {
81
- const parsed = JSON.parse(data);
82
- const delta = parsed.choices?.[0]?.delta?.content;
83
- if (delta) {
84
- result += delta;
85
- callbacks.onToken(result.trim());
86
- }
87
- }
88
- catch {
89
- // skip malformed chunks
90
- }
91
- }
92
- }
93
- return result.trim();
94
- }
5
+ baseUrl = "https://api.x.ai/v1";
6
+ defaultModel = "grok-code-fast-1";
7
+ apiKeyEnvVar = "XAI_API_KEY";
95
8
  }
package/dist/tokens.js ADDED
@@ -0,0 +1,17 @@
1
+ // Token estimation utility — shared across all modules
2
+ // Uses content-aware heuristic: code/JSON averages ~3.3 chars/token,
3
+ // English prose averages ~4.2 chars/token.
4
+ /** Detect if content is primarily code/JSON vs English prose */
5
+ function isCodeLike(text) {
6
+ // Count structural characters common in code/JSON
7
+ const structural = (text.match(/[{}[\]();:=<>,"'`|&\\/@#$%^*+~!?]/g) || []).length;
8
+ const ratio = structural / Math.max(text.length, 1);
9
+ return ratio > 0.08; // >8% structural chars = code-like
10
+ }
11
+ /** Estimate token count for a string with content-aware heuristic */
12
+ export function estimateTokens(text) {
13
+ if (!text)
14
+ return 0;
15
+ const charsPerToken = isCodeLike(text) ? 3.3 : 4.2;
16
+ return Math.ceil(text.length / charsPerToken);
17
+ }
@@ -88,12 +88,19 @@ function loadUserProfiles() {
88
88
  catch { }
89
89
  return profiles;
90
90
  }
91
- /** Get all profiles — user profiles override builtins by name */
91
+ /** Get all profiles — user profiles override builtins by name (cached 30s) */
92
+ let _cachedProfiles = null;
93
+ let _cachedProfilesAt = 0;
92
94
  export function getProfiles() {
95
+ const now = Date.now();
96
+ if (_cachedProfiles && now - _cachedProfilesAt < 30_000)
97
+ return _cachedProfiles;
93
98
  const user = loadUserProfiles();
94
99
  const userNames = new Set(user.map(p => p.name));
95
100
  const builtins = BUILTIN_PROFILES.filter(p => !userNames.has(p.name));
96
- return [...user, ...builtins];
101
+ _cachedProfiles = [...user, ...builtins];
102
+ _cachedProfilesAt = now;
103
+ return _cachedProfiles;
97
104
  }
98
105
  /** Find the matching profile for a command */
99
106
  export function matchProfile(command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "2.3.2",
3
+ "version": "3.1.0",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "files": [
package/src/ai.ts CHANGED
@@ -26,16 +26,25 @@ const MODEL_DEFAULTS: Record<string, { fast: string; smart: string }> = {
26
26
  anthropic: { fast: "claude-haiku-4-5-20251001", smart: "claude-sonnet-4-6" },
27
27
  };
28
28
 
29
- /** Load user model overrides from ~/.terminal/config.json */
29
+ /** Load user model overrides from ~/.terminal/config.json (cached 30s) */
30
+ let _modelOverrides: Record<string, { fast?: string; smart?: string }> | null = null;
31
+ let _modelOverridesAt = 0;
32
+
30
33
  function loadModelOverrides(): Record<string, { fast?: string; smart?: string }> {
34
+ const now = Date.now();
35
+ if (_modelOverrides && now - _modelOverridesAt < 30_000) return _modelOverrides;
31
36
  try {
32
37
  const configPath = join(process.env.HOME ?? "~", ".terminal", "config.json");
33
38
  if (existsSync(configPath)) {
34
39
  const config = JSON.parse(readFileSync(configPath, "utf8"));
35
- return config.models ?? {};
40
+ _modelOverrides = config.models ?? {};
41
+ _modelOverridesAt = now;
42
+ return _modelOverrides!;
36
43
  }
37
44
  } catch {}
38
- return {};
45
+ _modelOverrides = {};
46
+ _modelOverridesAt = now;
47
+ return _modelOverrides;
39
48
  }
40
49
 
41
50
  /** Model routing per provider — config-driven with defaults */
@@ -148,6 +157,8 @@ function detectProjectContext(): string {
148
157
  // ── system prompt ─────────────────────────────────────────────────────────────
149
158
 
150
159
  function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[], currentPrompt?: string): string {
160
+ const nl = currentPrompt?.toLowerCase() ?? "";
161
+
151
162
  const restrictions: string[] = [];
152
163
  if (!perms.destructive)
153
164
  restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
@@ -178,7 +189,6 @@ function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[], c
178
189
 
179
190
  const projectContext = detectProjectContext();
180
191
 
181
- // Inject safety hints for the command being generated (AI sees what's risky)
182
192
  const safetyBlock = sessionEntries.length > 0
183
193
  ? (() => {
184
194
  const lastCmd = sessionEntries[sessionEntries.length - 1]?.cmd;
@@ -190,72 +200,53 @@ function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[], c
190
200
  })()
191
201
  : "";
192
202
 
193
- return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
194
- The user describes what they want in plain English. You translate to the exact shell command.
195
-
196
- RULES:
197
- - SIMPLICITY FIRST: Use the simplest command that works. Prefer grep | sort | head over 10-pipe chains. Complex pipelines are OK when needed, but NEVER pass file:line output to wc or xargs without cleaning it first.
198
- - ALWAYS use grep -rn (with -r) when searching directories. NEVER use grep without -r on src/ or any directory.
199
- - When user refers to items from previous output, use the EXACT names shown (e.g., "feature/auth" not "auth", "open-skills" not "open_skills")
200
- - When user says "the largest/smallest/first/second", look at the previous output to identify the correct item
201
- - When user says "them all" or "combine them", refer to items from the most recent command output
202
- - For "show who changed each line" use git blame, for "show remote urls" use git remote -v
203
- - For text search in code, use grep -rn, NOT nm or objdump (those are for compiled binaries)
204
- - On macOS: for memory use vm_stat or top -l 1, for disk use df -h, for processes use ps aux
205
- - macOS uses BSD tools, NOT GNU. Use: du -d 1 (not --max-depth), ls (not ls --color), sort -r (not sort --reverse), ps aux (not ps --sort)
206
- - NEVER use grep -P (PCRE). macOS grep has NO -P flag. Use grep -E for extended regex, or sed/awk for complex extraction.
207
- - NEVER invent commands that don't exist. Stick to standard Unix/macOS commands.
208
- - NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
209
- - NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
210
- - Search src/ directory, NOT dist/ or node_modules/ for code queries.
211
- - Use exact file paths from the project context below. Do NOT guess paths.
212
- - For "what would break if I deleted X": use grep -rn "from.*X\\|import.*X\\|require.*X" src/ to find all importers.
213
- - For "find where X is defined": use grep -rn "export.*function X\\|export.*class X\\|export.*const X" src/
214
- - For "show me the code of function X": if you know the file, use grep -A 30 "function X" src/file.ts. If not, use grep -rn -A 30 "function X" src/ --include="*.ts"
215
- - ALWAYS use grep -rn (recursive) when searching directories. NEVER use grep without -r on a directory — it will fail.
216
- - For conceptual questions about what code does: use cat on the relevant file, the AI summary will explain it.
217
- - For DESTRUCTIVE requests (delete, remove, install, push): output BLOCKED: <reason>. NEVER try to execute destructive commands.
218
-
219
- AST-POWERED QUERIES: For code STRUCTURE questions, use the built-in AST tool instead of grep:
220
- - "find all exported functions" terminal symbols src/ (lists all functions, classes, interfaces with line numbers)
221
- - "show all interfaces"terminal symbols src/ | grep interface
222
- - "what does file X export"terminal symbols src/file.ts
223
- - "show me the class hierarchy" terminal symbols src/
224
- The "terminal symbols" command uses AST parsing (not regex) — it understands TypeScript, Python, Go, Rust code structure.
225
- For TEXT search (TODO, string matches, imports) → use grep as normal.
226
-
227
- COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
228
- - "how many tests and do they pass" bun test (extract count AND pass/fail from output)
229
- - "what files changed and how many lines" git log --stat -3 (shows files AND line counts)
230
- - "what version of node and bun" node -v && bun -v (only use && for trivial non-failing commands)
231
- NEVER split into separate test runs or expensive commands chained with &&.
232
-
233
- BLOCKED ALTERNATIVES: If your preferred command would require installing packages (npx, npm install), ALWAYS try a READ-ONLY alternative:
234
- - Code quality analysis → grep -rn "TODO\\|FIXME\\|HACK\\|XXX" src/
235
- - Linting → check if "lint" or "typecheck" exists in package.json scripts, run that
236
- - Security scan → grep -rn "eval\\|exec\\|spawn\\|password\\|secret" src/
237
- - Dependency audit → cat package.json | grep -A 50 dependencies
238
- - Test coverage → bun test --coverage (or npm run test:coverage if available)
239
- NEVER give up. NEVER output BLOCKED for analysis questions. Always try a grep/find/cat/wc/awk read-only alternative.
240
- - Cyclomatic complexity → grep -rn "if\\|else\\|for\\|while\\|switch\\|case\\|catch\\|&&\\|||" src/ --include="*.ts" | wc -l
241
- - Unused exports → grep -rn "export function\|export const\|export class" src/ --include="*.ts" | sed 's/.*export [a-z]* //' | sed 's/[(<:].*//' | sort -u
242
- - Dead code → for each exported name, grep -rn "name" src/ --include="*.ts" | wc -l (if only 1 match = unused)
243
- - Dependency graph → grep -rn "from " src/ --include="*.ts" | sed 's/:.*from "/→/' | sed 's/".*//' | sort -u
244
- - Most parameters → grep -rn "function " src/ --include="*.ts" | awk -F'[()]' '{print gsub(/,/,",",$2)+1, $0}' | sort -nr | head -10
245
- ALWAYS try a heuristic shell approach before giving up. NEVER say BLOCKED for analysis questions.
246
-
247
- SEMANTIC MAPPING: When the user references a concept, search the file tree for RELATED terms:
248
- - Look at directory names: src/agent/ likely contains "agentic" code
249
- - Look at file names: lazy-executor.ts likely handles "lazy mode"
250
- - When uncertain: grep -rn "keyword" src/ --include="*.ts" -l (list matching files)
251
-
252
- 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".
253
-
254
- 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.
255
-
256
- 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/.
203
+ // ── Conditional sections (only included when relevant) ──
204
+ const wantsStructure = /\b(function|class|interface|export|symbol|structure|hierarchy|outline)\b/i.test(nl);
205
+ 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.` : "";
206
+
207
+ const wantsMultiple = /\b(and|both|also|plus|as well)\b/i.test(nl);
208
+ const compoundBlock = wantsMultiple ? `\nCOMPOUND QUESTIONS: Prefer ONE command that captures all info. NEVER split into separate expensive commands.` : "";
209
+
210
+ const wantsAnalysis = /\b(quality|lint|coverage|complexity|unused|dead code|security|audit|scan|dependency)\b/i.test(nl);
211
+ 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.` : "";
212
+
213
+ return `Translate to bash. One command. Simplest form. No explanation.
214
+
215
+ list files in current directory ls
216
+ list all files including hidden ls -a
217
+ show open files lsof
218
+ create copy of a.txt as b.txt cp a.txt b.txt
219
+ create file test.txt touch test.txt
220
+ make directory testdir mkdir testdir
221
+ display routing table route
222
+ show last logged in users last
223
+ show file stats stat file
224
+ print directory tree 2 levels tree -L 2
225
+ count word occurrences in file grep -c "word" file
226
+ print number of files in dir ls -1 | wc -l
227
+ print first line of file head -1 file
228
+ print last line of file → tail -1 file
229
+ print lines 3 to 5 of file sed -n '3,5p' file
230
+ print every other lineawk 'NR%2==1' file
231
+ count words in filewc -w file
232
+ find empty files not in subdirsfind . -maxdepth 1 -type f -empty
233
+ show system loadw
234
+ system utilization stats vmstat
235
+ DNS servers cat /etc/resolv.conf | grep nameserver
236
+ long integer size → getconf LONG_BIT
237
+ base64 decode string echo 'str' | base64 -d
238
+ change owner to nobodychown nobody file
239
+ unique lines in fileuniq file
240
+ max cpu timeulimit -t
241
+ memory info lsmem
242
+ process priority → nice
243
+ bash profile cat ~/.bashrc
244
+ search recursively → grep -rn "pattern" src/
245
+ ${astBlock}${compoundBlock}${blockedAltBlock}
257
246
  cwd: ${process.cwd()}
258
- shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}`;
247
+ shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}
248
+
249
+ Q:`;
259
250
  }
260
251
 
261
252
  // ── streaming translate ───────────────────────────────────────────────────────
@@ -280,11 +271,11 @@ export async function translateToCommand(
280
271
  let text: string;
281
272
 
282
273
  if (onToken) {
283
- text = await provider.stream(nl, { model, maxTokens: 256, system }, {
274
+ text = await provider.stream(nl, { model, maxTokens: 256, temperature: 0, stop: ["\n"], system }, {
284
275
  onToken: (partial) => onToken(partial),
285
276
  });
286
277
  } else {
287
- text = await provider.complete(nl, { model, maxTokens: 256, system });
278
+ text = await provider.complete(nl, { model, maxTokens: 256, temperature: 0, stop: ["\n"], system });
288
279
  }
289
280
 
290
281
  if (text.startsWith("BLOCKED:")) throw new Error(text);
@@ -334,6 +325,7 @@ export async function explainCommand(command: string): Promise<string> {
334
325
  return provider.complete(command, {
335
326
  model: routing.fast,
336
327
  maxTokens: 128,
328
+ temperature: 0,
337
329
  system: "Explain what this shell command does in one plain English sentence. No markdown, no code blocks.",
338
330
  });
339
331
  }
@@ -345,37 +337,34 @@ export async function fixCommand(
345
337
  failedCommand: string,
346
338
  errorOutput: string,
347
339
  perms: Permissions,
348
- sessionEntries: SessionEntry[]
340
+ _sessionEntries: SessionEntry[]
349
341
  ): Promise<string> {
350
342
  const provider = getProvider();
351
343
  const routing = pickModel(originalNl);
344
+
345
+ // Lightweight fix prompt — no full project context, just rules + restrictions
346
+ const restrictions: string[] = [];
347
+ if (!perms.destructive) restrictions.push("- NEVER delete/remove/overwrite files");
348
+ if (!perms.network) restrictions.push("- NEVER make network requests");
349
+ if (!perms.install) restrictions.push("- NEVER install packages");
350
+
351
+ const fixSystem = `You are a terminal assistant. Output ONLY the corrected shell command — no explanation.
352
+ macOS/BSD tools. NEVER use grep -P. Use grep -E for extended regex.
353
+ NEVER install packages. READ-ONLY terminal.
354
+ cwd: ${process.cwd()}${restrictions.length > 0 ? `\nRESTRICTIONS:\n${restrictions.join("\n")}` : ""}`;
355
+
352
356
  const text = await provider.complete(
353
- `I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput}\n\nGive me the corrected command only.`,
357
+ `I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput.slice(0, 2000)}\n\nGive me the corrected command only.`,
354
358
  {
355
- model: routing.smart, // always use smart model for fixes
359
+ model: routing.smart,
356
360
  maxTokens: 256,
357
- system: buildSystemPrompt(perms, sessionEntries, originalNl),
361
+ temperature: 0,
362
+ stop: ["\n"],
363
+ system: fixSystem,
358
364
  }
359
365
  );
360
366
  if (text.startsWith("BLOCKED:")) throw new Error(text);
361
- return text;
367
+ return text.trim();
362
368
  }
363
369
 
364
- // ── summarize output (for MCP/agent use) ──────────────────────────────────────
365
-
366
- export async function summarizeOutput(
367
- command: string,
368
- output: string,
369
- maxTokens: number = 200
370
- ): Promise<string> {
371
- const provider = getProvider();
372
- const routing = pickModel("summarize");
373
- return provider.complete(
374
- `Command: ${command}\nOutput:\n${output}\n\nSummarize this output concisely for an AI agent. Focus on: status, key results, errors. Be terse.`,
375
- {
376
- model: routing.fast,
377
- maxTokens,
378
- system: "You summarize command output for AI agents. Be extremely concise. Return structured info. No prose.",
379
- }
380
- );
381
- }
370
+ // summarizeOutput() removed — all output processing goes through processOutput() in output-processor.ts
package/src/cache.ts CHANGED
@@ -20,12 +20,13 @@ function persistCache() {
20
20
  try { writeFileSync(CACHE_FILE, JSON.stringify(mem)); } catch {}
21
21
  }
22
22
 
23
- /** Normalize a natural language query for cache lookup */
23
+ /** Normalize a natural language query for cache lookup.
24
+ * Keeps . / - _ which are meaningful in file paths and shell context. */
24
25
  export function normalizeNl(nl: string): string {
25
26
  return nl
26
27
  .toLowerCase()
27
28
  .trim()
28
- .replace(/[^a-z0-9\s]/g, "") // strip punctuation
29
+ .replace(/[^a-z0-9\s.\/_-]/g, "") // keep meaningful shell chars
29
30
  .replace(/\s+/g, " ");
30
31
  }
31
32
 
package/src/cli.tsx CHANGED
@@ -446,7 +446,7 @@ else if (args.length > 0) {
446
446
  const { rewriteCommand } = await import("./command-rewriter.js");
447
447
  const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
448
448
  const { saveOutput, formatOutputHint } = await import("./output-store.js");
449
- const { parseOutput, estimateTokens } = await import("./parsers/index.js");
449
+ const { estimateTokens } = await import("./tokens.js");
450
450
  const { recordSaving, recordUsage } = await import("./economy.js");
451
451
  const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
452
452
  const { detectLoop } = await import("./loop-detector.js");
@@ -1,19 +1,18 @@
1
1
  // Token compression engine — reduces CLI output to fit within token budgets
2
+ // No regex parsing — just ANSI stripping, deduplication, and smart truncation.
3
+ // All intelligent output processing goes through AI via processOutput().
2
4
 
3
- import { parseOutput, estimateTokens, tokenSavings } from "./parsers/index.js";
5
+ import { estimateTokens } from "./tokens.js";
4
6
 
5
7
  export interface CompressOptions {
6
8
  /** Max tokens for the output (default: unlimited) */
7
9
  maxTokens?: number;
8
- /** Output format */
9
- format?: "text" | "json" | "summary";
10
10
  /** Strip ANSI escape codes (default: true) */
11
11
  stripAnsi?: boolean;
12
12
  }
13
13
 
14
14
  export interface CompressedOutput {
15
15
  content: string;
16
- format: "text" | "json" | "summary";
17
16
  originalTokens: number;
18
17
  compressedTokens: number;
19
18
  tokensSaved: number;
@@ -36,7 +35,6 @@ function deduplicateLines(lines: string[]): string[] {
36
35
 
37
36
  for (let i = 0; i < lines.length; i++) {
38
37
  const line = lines[i];
39
- // Extract a "pattern" — the line without numbers, paths, specific identifiers
40
38
  const pattern = line.replace(/[0-9]+/g, "N").replace(/\/\S+/g, "/PATH").replace(/\s+/g, " ").trim();
41
39
 
42
40
  if (pattern === repeatPattern) {
@@ -45,7 +43,6 @@ function deduplicateLines(lines: string[]): string[] {
45
43
  if (repeatCount > 2) {
46
44
  result.push(` ... (${repeatCount} similar lines)`);
47
45
  } else if (repeatCount > 0) {
48
- // Push the skipped lines back
49
46
  for (let j = i - repeatCount; j < i; j++) {
50
47
  result.push(lines[j]);
51
48
  }
@@ -67,14 +64,13 @@ function deduplicateLines(lines: string[]): string[] {
67
64
  return result;
68
65
  }
69
66
 
70
- /** Smart truncation: keep first N + last M lines */
67
+ /** Smart truncation: keep first 60% + last 40% of lines */
71
68
  function smartTruncate(text: string, maxTokens: number): string {
72
69
  const lines = text.split("\n");
73
70
  const currentTokens = estimateTokens(text);
74
71
 
75
72
  if (currentTokens <= maxTokens) return text;
76
73
 
77
- // Keep proportional first/last, with first getting more
78
74
  const targetLines = Math.floor((maxTokens * lines.length) / currentTokens);
79
75
  const firstCount = Math.ceil(targetLines * 0.6);
80
76
  const lastCount = Math.floor(targetLines * 0.4);
@@ -88,42 +84,20 @@ function smartTruncate(text: string, maxTokens: number): string {
88
84
  return [...first, `\n--- ${hiddenCount} lines hidden ---\n`, ...last].join("\n");
89
85
  }
90
86
 
91
- /** Compress command output to fit within a token budget */
87
+ /** Compress command output ANSI strip, dedup, truncate. No parsing. */
92
88
  export function compress(command: string, output: string, options: CompressOptions = {}): CompressedOutput {
93
- const { maxTokens, format = "text", stripAnsi: doStrip = true } = options;
89
+ const { maxTokens, stripAnsi: doStrip = true } = options;
94
90
  const originalTokens = estimateTokens(output);
95
91
 
96
92
  // Step 1: Strip ANSI codes
97
93
  let text = doStrip ? stripAnsi(output) : output;
98
94
 
99
- // Step 2: Try structured parsing (format=json or when it saves tokens)
100
- if (format === "json" || format === "summary") {
101
- const parsed = parseOutput(command, text);
102
- if (parsed) {
103
- const json = JSON.stringify(parsed.data, null, format === "summary" ? 0 : 2);
104
- const savings = tokenSavings(output, parsed.data);
105
- const compressedTokens = estimateTokens(json);
106
-
107
- // ONLY use JSON if it actually saves tokens (never return larger output)
108
- if (savings.saved > 0 && (!maxTokens || compressedTokens <= maxTokens)) {
109
- return {
110
- content: json,
111
- format: "json",
112
- originalTokens,
113
- compressedTokens,
114
- tokensSaved: savings.saved,
115
- savingsPercent: savings.percent,
116
- };
117
- }
118
- }
119
- }
120
-
121
- // Step 3: Deduplicate similar lines
95
+ // Step 2: Deduplicate similar lines
122
96
  const lines = text.split("\n");
123
97
  const deduped = deduplicateLines(lines);
124
98
  text = deduped.join("\n");
125
99
 
126
- // Step 4: Smart truncation if over budget
100
+ // Step 3: Smart truncation if over budget
127
101
  if (maxTokens) {
128
102
  text = smartTruncate(text, maxTokens);
129
103
  }
@@ -131,7 +105,6 @@ export function compress(command: string, output: string, options: CompressOptio
131
105
  const compressedTokens = estimateTokens(text);
132
106
  return {
133
107
  content: text,
134
- format: "text",
135
108
  originalTokens,
136
109
  compressedTokens,
137
110
  tokensSaved: Math.max(0, originalTokens - compressedTokens),