@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
|
@@ -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
|
+
}
|
package/dist/providers/xai.js
CHANGED
|
@@ -1,95 +1,8 @@
|
|
|
1
|
-
// xAI/Grok provider —
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|
package/dist/tool-profiles.js
CHANGED
|
@@ -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
|
-
|
|
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
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
|
-
|
|
40
|
+
_modelOverrides = config.models ?? {};
|
|
41
|
+
_modelOverridesAt = now;
|
|
42
|
+
return _modelOverrides!;
|
|
36
43
|
}
|
|
37
44
|
} catch {}
|
|
38
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 line → awk 'NR%2==1' file
|
|
231
|
+
count words in file → wc -w file
|
|
232
|
+
find empty files not in subdirs → find . -maxdepth 1 -type f -empty
|
|
233
|
+
show system load → w
|
|
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 nobody → chown nobody file
|
|
239
|
+
unique lines in file → uniq file
|
|
240
|
+
max cpu time → ulimit -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
|
-
|
|
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,
|
|
359
|
+
model: routing.smart,
|
|
356
360
|
maxTokens: 256,
|
|
357
|
-
|
|
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
|
-
//
|
|
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, "") //
|
|
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 {
|
|
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");
|
package/src/compression.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
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),
|