@hasna/terminal 2.0.5 → 2.2.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/cli.js +23 -9
- package/package.json +1 -1
- package/src/ai.ts +46 -113
- package/src/cli.tsx +22 -9
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +202 -0
- package/src/output-processor.ts +7 -18
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
package/dist/ai.js
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import { cacheGet, cacheSet } from "./cache.js";
|
|
2
|
-
import { getProvider } from "./providers/index.js";
|
|
3
|
-
import { existsSync, readFileSync } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
// ── model routing ─────────────────────────────────────────────────────────────
|
|
6
|
-
// Simple queries → fast model. Complex/ambiguous → smart model.
|
|
7
|
-
const COMPLEX_SIGNALS = [
|
|
8
|
-
/\b(undo|revert|rollback|previous|last)\b/i,
|
|
9
|
-
/\b(all files?|recursively|bulk|batch)\b/i,
|
|
10
|
-
/\b(pipeline|chain|then|and then|after)\b/i,
|
|
11
|
-
/\b(if|when|unless|only if)\b/i,
|
|
12
|
-
/\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i, // multi-step navigation
|
|
13
|
-
/\b(inside|within|under)\b/i, // relative references need context awareness
|
|
14
|
-
/[|&;]{2}/, // pipes / && in NL (unusual = complex intent)
|
|
15
|
-
];
|
|
16
|
-
/** Model routing per provider */
|
|
17
|
-
function pickModel(nl) {
|
|
18
|
-
const isComplex = COMPLEX_SIGNALS.some((r) => r.test(nl)) || nl.split(" ").length > 10;
|
|
19
|
-
const provider = getProvider();
|
|
20
|
-
if (provider.name === "anthropic") {
|
|
21
|
-
return {
|
|
22
|
-
fast: "claude-haiku-4-5-20251001",
|
|
23
|
-
smart: "claude-sonnet-4-6",
|
|
24
|
-
pick: isComplex ? "smart" : "fast",
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
// Cerebras — qwen for everything (llama3.1-8b too unreliable)
|
|
28
|
-
return {
|
|
29
|
-
fast: "qwen-3-235b-a22b-instruct-2507",
|
|
30
|
-
smart: "qwen-3-235b-a22b-instruct-2507",
|
|
31
|
-
pick: isComplex ? "smart" : "fast",
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
// ── irreversibility ───────────────────────────────────────────────────────────
|
|
35
|
-
const IRREVERSIBLE_PATTERNS = [
|
|
36
|
-
/\brm\s/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i,
|
|
37
|
-
/\bdelete\s+from\b/i, /\bmv\b.*\/dev\/null/, /\becho\b.*>\s*[^>]/, /\bcat\b.*>\s*[^>]/,
|
|
38
|
-
/\bdd\b/, /\bmkfs\b/, /\bformat\b/, /\bshred\b/,
|
|
39
|
-
// Process/service killing
|
|
40
|
-
/\bkill\b/, /\bkillall\b/, /\bpkill\b/,
|
|
41
|
-
// Git push/force operations
|
|
42
|
-
/\bgit\s+push\b/, /\bgit\s+reset\s+--hard\b/, /\bgit\s+force\b/,
|
|
43
|
-
// Code modification / package installation (security risk)
|
|
44
|
-
/\bnpx\s+\S+/, /\bnpm\s+install\b/, /\bbun\s+add\b/, /\bpip\s+install\b/,
|
|
45
|
-
/\bcodemod\b/, /\bsed\s+-i\b/, /\bawk\s.*>\s*\S+\.\w+/, /\bperl\s+-[pi]\b/,
|
|
46
|
-
// File creation/modification (READ-ONLY terminal)
|
|
47
|
-
/\btouch\b/, /\bmkdir\b/, /\becho\s.*>/, /\btee\b/, /\bcp\b/, /\bmv\b/,
|
|
48
|
-
// Starting servers/processes (dangerous from NL)
|
|
49
|
-
/\b(bun|npm|pnpm|yarn)\s+run\s+dev\b/, /\b(bun|npm)\s+start\b/,
|
|
50
|
-
];
|
|
51
|
-
// Commands that are ALWAYS safe (read-only git, etc.)
|
|
52
|
-
const SAFE_OVERRIDES = [
|
|
53
|
-
/^\s*git\s+(log|show|diff|branch|status|blame|tag|remote|stash\s+list)\b/,
|
|
54
|
-
/^\s*git\s+log\b/,
|
|
55
|
-
// find -exec with read-only tools is safe
|
|
56
|
-
/\bfind\b.*-exec\s+(wc|cat|head|tail|grep|stat|file|du|ls)\b/,
|
|
57
|
-
// find without -exec is always safe
|
|
58
|
-
/^\s*find\b(?!.*-exec\s+(rm|mv|chmod|chown|sed))/,
|
|
59
|
-
// xargs with read-only tools is safe
|
|
60
|
-
/\bxargs\s+(wc|cat|head|tail|grep|stat|file|du|ls|git\s+log|git\s+show|git\s+blame)\b/,
|
|
61
|
-
/\bxargs\s+-I\s*\S+\s+(wc|cat|head|tail|grep|stat|git)\b/,
|
|
62
|
-
];
|
|
63
|
-
export function isIrreversible(command) {
|
|
64
|
-
// Safe overrides take priority
|
|
65
|
-
if (SAFE_OVERRIDES.some((r) => r.test(command)))
|
|
66
|
-
return false;
|
|
67
|
-
return IRREVERSIBLE_PATTERNS.some((r) => r.test(command));
|
|
68
|
-
}
|
|
69
|
-
// ── permissions ───────────────────────────────────────────────────────────────
|
|
70
|
-
const DESTRUCTIVE_PATTERNS = [/\brm\b/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i, /\bdelete\s+from\b/i];
|
|
71
|
-
const NETWORK_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bssh\b/, /\bscp\b/, /\bping\b/, /\bnc\b/, /\bnetcat\b/];
|
|
72
|
-
const SUDO_PATTERNS = [/\bsudo\b/];
|
|
73
|
-
const INSTALL_PATTERNS = [/\bbrew\s+install\b/, /\bnpm\s+install\s+-g\b/, /\bpip\s+install\b/, /\bapt\s+install\b/, /\byum\s+install\b/];
|
|
74
|
-
const WRITE_OUTSIDE_PATTERNS = [/\s(\/etc|\/usr|\/var|\/opt|\/root|~\/[^.])/, />\s*\//];
|
|
75
|
-
export function checkPermissions(command, perms) {
|
|
76
|
-
if (!perms.destructive && DESTRUCTIVE_PATTERNS.some((r) => r.test(command)))
|
|
77
|
-
return "destructive commands are disabled";
|
|
78
|
-
if (!perms.network && NETWORK_PATTERNS.some((r) => r.test(command)))
|
|
79
|
-
return "network commands are disabled";
|
|
80
|
-
if (!perms.sudo && SUDO_PATTERNS.some((r) => r.test(command)))
|
|
81
|
-
return "sudo is disabled";
|
|
82
|
-
if (!perms.install && INSTALL_PATTERNS.some((r) => r.test(command)))
|
|
83
|
-
return "package installation is disabled";
|
|
84
|
-
if (!perms.write_outside_cwd && WRITE_OUTSIDE_PATTERNS.some((r) => r.test(command)))
|
|
85
|
-
return "writing outside cwd is disabled";
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
// ── project context ──────────────────────────────────────────────────────────
|
|
89
|
-
function detectProjectContext() {
|
|
90
|
-
const cwd = process.cwd();
|
|
91
|
-
const parts = [];
|
|
92
|
-
// Node.js / TypeScript
|
|
93
|
-
const pkgPath = join(cwd, "package.json");
|
|
94
|
-
if (existsSync(pkgPath)) {
|
|
95
|
-
try {
|
|
96
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
97
|
-
parts.push(`Project: ${pkg.name}@${pkg.version} (Node.js/TypeScript)`);
|
|
98
|
-
parts.push(`npm package: ${pkg.name} (use this name for npm view, npm info, etc.)`);
|
|
99
|
-
if (pkg.scripts) {
|
|
100
|
-
const scripts = Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).slice(0, 8);
|
|
101
|
-
parts.push(`Available scripts: ${scripts.join(", ")}`);
|
|
102
|
-
}
|
|
103
|
-
if (pkg.dependencies)
|
|
104
|
-
parts.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
105
|
-
parts.push(`Use npm/bun/pnpm commands, NOT maven/gradle/cargo.`);
|
|
106
|
-
}
|
|
107
|
-
catch { }
|
|
108
|
-
}
|
|
109
|
-
// Python
|
|
110
|
-
if (existsSync(join(cwd, "pyproject.toml"))) {
|
|
111
|
-
try {
|
|
112
|
-
const pyproject = readFileSync(join(cwd, "pyproject.toml"), "utf8");
|
|
113
|
-
const nameMatch = pyproject.match(/name\s*=\s*"([^"]+)"/);
|
|
114
|
-
const versionMatch = pyproject.match(/version\s*=\s*"([^"]+)"/);
|
|
115
|
-
parts.push(`Project: ${nameMatch?.[1] ?? "Python"}${versionMatch ? `@${versionMatch[1]}` : ""} (Python)`);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
parts.push("Project: Python (pyproject.toml found)");
|
|
119
|
-
}
|
|
120
|
-
parts.push("Use pip/python/pytest commands. Test: pytest. Build: python -m build.");
|
|
121
|
-
}
|
|
122
|
-
else if (existsSync(join(cwd, "requirements.txt"))) {
|
|
123
|
-
parts.push("Project: Python (requirements.txt). Use pip/python/pytest commands.");
|
|
124
|
-
}
|
|
125
|
-
// Go
|
|
126
|
-
if (existsSync(join(cwd, "go.mod"))) {
|
|
127
|
-
try {
|
|
128
|
-
const gomod = readFileSync(join(cwd, "go.mod"), "utf8");
|
|
129
|
-
const moduleMatch = gomod.match(/module\s+(\S+)/);
|
|
130
|
-
parts.push(`Project: ${moduleMatch?.[1] ?? "Go"} (Go module)`);
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
parts.push("Project: Go (go.mod found)");
|
|
134
|
-
}
|
|
135
|
-
parts.push("Use go build/test/run. Test: go test ./... Build: go build.");
|
|
136
|
-
}
|
|
137
|
-
// Rust
|
|
138
|
-
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
139
|
-
try {
|
|
140
|
-
const cargo = readFileSync(join(cwd, "Cargo.toml"), "utf8");
|
|
141
|
-
const nameMatch = cargo.match(/name\s*=\s*"([^"]+)"/);
|
|
142
|
-
const versionMatch = cargo.match(/version\s*=\s*"([^"]+)"/);
|
|
143
|
-
parts.push(`Project: ${nameMatch?.[1] ?? "Rust"}${versionMatch ? `@${versionMatch[1]}` : ""} (Rust/Cargo)`);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
parts.push("Project: Rust (Cargo.toml found)");
|
|
147
|
-
}
|
|
148
|
-
parts.push("Use cargo build/test/run. Test: cargo test. Build: cargo build --release.");
|
|
149
|
-
}
|
|
150
|
-
// Java
|
|
151
|
-
if (existsSync(join(cwd, "pom.xml"))) {
|
|
152
|
-
parts.push("Project: Java/Maven. Use mvn commands. Test: mvn test. Build: mvn package.");
|
|
153
|
-
}
|
|
154
|
-
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
|
|
155
|
-
parts.push("Project: Java/Gradle. Use gradle commands. Test: gradle test. Build: gradle build.");
|
|
156
|
-
}
|
|
157
|
-
// Docker
|
|
158
|
-
if (existsSync(join(cwd, "Dockerfile")) || existsSync(join(cwd, "docker-compose.yml")) || existsSync(join(cwd, "docker-compose.yaml"))) {
|
|
159
|
-
parts.push("Docker: Dockerfile/docker-compose present. Container commands available.");
|
|
160
|
-
}
|
|
161
|
-
// Makefile
|
|
162
|
-
if (existsSync(join(cwd, "Makefile"))) {
|
|
163
|
-
try {
|
|
164
|
-
const { execSync: execS } = require("child_process");
|
|
165
|
-
const targets = execS("grep -E '^[a-zA-Z_-]+:' Makefile | head -10 | cut -d: -f1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
166
|
-
if (targets)
|
|
167
|
-
parts.push(`Makefile targets: ${targets.split("\n").join(", ")}`);
|
|
168
|
-
}
|
|
169
|
-
catch { }
|
|
170
|
-
}
|
|
171
|
-
// Directory structure — so AI knows actual paths (not guessed ones)
|
|
172
|
-
try {
|
|
173
|
-
const { execSync } = require("child_process");
|
|
174
|
-
// Top-level dirs
|
|
175
|
-
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
176
|
-
parts.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
177
|
-
// Detect monorepo (packages/ or workspaces in package.json)
|
|
178
|
-
const isMonorepo = existsSync(join(cwd, "packages")) || existsSync(join(cwd, "apps"));
|
|
179
|
-
if (isMonorepo) {
|
|
180
|
-
const pkgDirs = execSync(`ls -d packages/*/src 2>/dev/null || ls -d apps/*/src 2>/dev/null || echo ""`, { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
181
|
-
if (pkgDirs) {
|
|
182
|
-
parts.push(`MONOREPO: Source is in packages/*/src/, NOT src/. Search packages/ not src/.`);
|
|
183
|
-
parts.push(`Package sources:\n${pkgDirs}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// src/ structure — include FILES so AI knows exact filenames + extensions
|
|
187
|
-
for (const srcDir of isMonorepo ? ["packages"] : ["src", "lib", "app"]) {
|
|
188
|
-
if (existsSync(join(cwd, srcDir))) {
|
|
189
|
-
const tree = execSync(`find ${srcDir} -maxdepth ${isMonorepo ? 4 : 3} -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | sort | head -80`, { cwd, encoding: "utf8", timeout: 3000 }).trim();
|
|
190
|
-
if (tree)
|
|
191
|
-
parts.push(`Files in ${srcDir}/:\n${tree}`);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
catch { /* timeout or no exec — skip */ }
|
|
197
|
-
return parts.length > 0 ? `\n\nPROJECT CONTEXT:\n${parts.join("\n")}` : "";
|
|
198
|
-
}
|
|
199
|
-
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
200
|
-
function buildSystemPrompt(perms, sessionEntries) {
|
|
201
|
-
const restrictions = [];
|
|
202
|
-
if (!perms.destructive)
|
|
203
|
-
restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
|
|
204
|
-
if (!perms.network)
|
|
205
|
-
restrictions.push("- NEVER generate commands that make network requests (curl, wget, ssh, etc.)");
|
|
206
|
-
if (!perms.sudo)
|
|
207
|
-
restrictions.push("- NEVER generate commands requiring sudo");
|
|
208
|
-
if (!perms.write_outside_cwd)
|
|
209
|
-
restrictions.push("- NEVER write to paths outside the current working directory");
|
|
210
|
-
if (!perms.install)
|
|
211
|
-
restrictions.push("- NEVER install packages (brew, npm -g, pip, apt, etc.)");
|
|
212
|
-
const restrictionBlock = restrictions.length > 0
|
|
213
|
-
? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
|
|
214
|
-
: "";
|
|
215
|
-
let contextBlock = "";
|
|
216
|
-
if (sessionEntries.length > 0) {
|
|
217
|
-
const lines = [];
|
|
218
|
-
for (const e of sessionEntries.slice(-5)) {
|
|
219
|
-
lines.push(`> ${e.nl}`);
|
|
220
|
-
lines.push(`$ ${e.cmd}`);
|
|
221
|
-
if (e.output)
|
|
222
|
-
lines.push(e.output);
|
|
223
|
-
if (e.error)
|
|
224
|
-
lines.push("(command failed)");
|
|
225
|
-
}
|
|
226
|
-
contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
|
|
227
|
-
}
|
|
228
|
-
const projectContext = detectProjectContext();
|
|
229
|
-
return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
|
|
230
|
-
The user describes what they want in plain English. You translate to the exact shell command.
|
|
231
|
-
|
|
232
|
-
RULES:
|
|
233
|
-
- When user refers to items from previous output, use the EXACT names shown (e.g., "feature/auth" not "auth", "open-skills" not "open_skills")
|
|
234
|
-
- When user says "the largest/smallest/first/second", look at the previous output to identify the correct item
|
|
235
|
-
- When user says "them all" or "combine them", refer to items from the most recent command output
|
|
236
|
-
- For "show who changed each line" use git blame, for "show remote urls" use git remote -v
|
|
237
|
-
- For text search in code, use grep -rn, NOT nm or objdump (those are for compiled binaries)
|
|
238
|
-
- On macOS: for memory use vm_stat or top -l 1, for disk use df -h, for processes use ps aux
|
|
239
|
-
- 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)
|
|
240
|
-
- NEVER invent commands that don't exist. Stick to standard Unix/macOS commands.
|
|
241
|
-
- NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
|
|
242
|
-
- NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
|
|
243
|
-
- Search src/ directory, NOT dist/ or node_modules/ for code queries.
|
|
244
|
-
- Use exact file paths from the project context below. Do NOT guess paths.
|
|
245
|
-
- For "what would break if I deleted X": use grep -rn "from.*X\\|import.*X\\|require.*X" src/ to find all importers.
|
|
246
|
-
- For "find where X is defined": use grep -rn "export.*function X\\|export.*class X\\|export.*const X" src/
|
|
247
|
-
- For "show me the code of function X": use grep -A 20 "function X" src/ to show the function body.
|
|
248
|
-
- For conceptual questions about what code does: use cat on the relevant file, the AI summary will explain it.
|
|
249
|
-
|
|
250
|
-
COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
|
|
251
|
-
- "how many tests and do they pass" → bun test (extract count AND pass/fail from output)
|
|
252
|
-
- "what files changed and how many lines" → git log --stat -3 (shows files AND line counts)
|
|
253
|
-
- "what version of node and bun" → node -v && bun -v (only use && for trivial non-failing commands)
|
|
254
|
-
NEVER split into separate test runs or expensive commands chained with &&.
|
|
255
|
-
|
|
256
|
-
BLOCKED ALTERNATIVES: If your preferred command would require installing packages (npx, npm install), ALWAYS try a READ-ONLY alternative:
|
|
257
|
-
- Code quality analysis → grep -rn "TODO\\|FIXME\\|HACK\\|XXX" src/
|
|
258
|
-
- Linting → check if "lint" or "typecheck" exists in package.json scripts, run that
|
|
259
|
-
- Security scan → grep -rn "eval\\|exec\\|spawn\\|password\\|secret" src/
|
|
260
|
-
- Dependency audit → cat package.json | grep -A 50 dependencies
|
|
261
|
-
- Test coverage → bun test --coverage (or npm run test:coverage if available)
|
|
262
|
-
NEVER give up. Always try a grep/find/cat read-only alternative.
|
|
263
|
-
|
|
264
|
-
SEMANTIC MAPPING: When the user references a concept, search the file tree for RELATED terms:
|
|
265
|
-
- Look at directory names: src/agent/ likely contains "agentic" code
|
|
266
|
-
- Look at file names: lazy-executor.ts likely handles "lazy mode"
|
|
267
|
-
- When uncertain: grep -rn "keyword" src/ --include="*.ts" -l (list matching files)
|
|
268
|
-
|
|
269
|
-
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".
|
|
270
|
-
|
|
271
|
-
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.
|
|
272
|
-
|
|
273
|
-
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/.
|
|
274
|
-
cwd: ${process.cwd()}
|
|
275
|
-
shell: zsh / macOS${projectContext}${restrictionBlock}${contextBlock}`;
|
|
276
|
-
}
|
|
277
|
-
// ── streaming translate ───────────────────────────────────────────────────────
|
|
278
|
-
export async function translateToCommand(nl, perms, sessionEntries, onToken) {
|
|
279
|
-
// Only use cache when there's no session context (context makes same NL produce different commands)
|
|
280
|
-
if (sessionEntries.length === 0) {
|
|
281
|
-
const cached = cacheGet(nl);
|
|
282
|
-
if (cached) {
|
|
283
|
-
onToken?.(cached);
|
|
284
|
-
return cached;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const provider = getProvider();
|
|
288
|
-
const routing = pickModel(nl);
|
|
289
|
-
const model = routing.pick === "smart" ? routing.smart : routing.fast;
|
|
290
|
-
const system = buildSystemPrompt(perms, sessionEntries);
|
|
291
|
-
let text;
|
|
292
|
-
if (onToken) {
|
|
293
|
-
text = await provider.stream(nl, { model, maxTokens: 256, system }, {
|
|
294
|
-
onToken: (partial) => onToken(partial),
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
text = await provider.complete(nl, { model, maxTokens: 256, system });
|
|
299
|
-
}
|
|
300
|
-
if (text.startsWith("BLOCKED:"))
|
|
301
|
-
throw new Error(text);
|
|
302
|
-
// Strip AI reasoning — extract ONLY the shell command (first line)
|
|
303
|
-
let cleaned = text.trim();
|
|
304
|
-
// Remove ALL markdown code blocks and their content markers
|
|
305
|
-
cleaned = cleaned.replace(/```(?:bash|sh|shell)?\n?/g, "").replace(/```/g, "");
|
|
306
|
-
// Split into lines and find the FIRST one that looks like a SHELL COMMAND
|
|
307
|
-
const lines = cleaned.split("\n");
|
|
308
|
-
let command = "";
|
|
309
|
-
for (const line of lines) {
|
|
310
|
-
const t = line.trim();
|
|
311
|
-
if (!t)
|
|
312
|
-
continue;
|
|
313
|
-
// Skip lines that are clearly English prose, not commands
|
|
314
|
-
if (/^(Based on|I |This |The |Let me|Here|Note:|Since|Looking|To |However|BLOCKED:|If |You |We |For |It |A |An |That )/.test(t))
|
|
315
|
-
continue;
|
|
316
|
-
if (/^[A-Z][a-z].*[.;:!?,]/.test(t))
|
|
317
|
-
continue; // English sentence with punctuation anywhere
|
|
318
|
-
if (t.split(" ").length > 15 && !/[|&;><$]/.test(t))
|
|
319
|
-
continue; // Long line without shell operators = prose
|
|
320
|
-
// Must start with a plausible command character (lowercase, /, ., $, or common tool)
|
|
321
|
-
if (/^[a-z./$~(]/.test(t) || /^[A-Z]+[_=]/.test(t)) {
|
|
322
|
-
command = t;
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
cleaned = command || lines[0]?.trim() || cleaned;
|
|
327
|
-
cacheSet(nl, cleaned);
|
|
328
|
-
return cleaned;
|
|
329
|
-
}
|
|
330
|
-
// ── prefetch ──────────────────────────────────────────────────────────────────
|
|
331
|
-
export function prefetchNext(lastNl, perms, sessionEntries) {
|
|
332
|
-
if (sessionEntries.length === 0 && cacheGet(lastNl))
|
|
333
|
-
return;
|
|
334
|
-
translateToCommand(lastNl, perms, sessionEntries).catch(() => { });
|
|
335
|
-
}
|
|
336
|
-
// ── explain ───────────────────────────────────────────────────────────────────
|
|
337
|
-
export async function explainCommand(command) {
|
|
338
|
-
const provider = getProvider();
|
|
339
|
-
const routing = pickModel("explain"); // simple = fast model
|
|
340
|
-
return provider.complete(command, {
|
|
341
|
-
model: routing.fast,
|
|
342
|
-
maxTokens: 128,
|
|
343
|
-
system: "Explain what this shell command does in one plain English sentence. No markdown, no code blocks.",
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
// ── auto-fix ──────────────────────────────────────────────────────────────────
|
|
347
|
-
export async function fixCommand(originalNl, failedCommand, errorOutput, perms, sessionEntries) {
|
|
348
|
-
const provider = getProvider();
|
|
349
|
-
const routing = pickModel(originalNl);
|
|
350
|
-
const text = await provider.complete(`I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput}\n\nGive me the corrected command only.`, {
|
|
351
|
-
model: routing.smart, // always use smart model for fixes
|
|
352
|
-
maxTokens: 256,
|
|
353
|
-
system: buildSystemPrompt(perms, sessionEntries),
|
|
354
|
-
});
|
|
355
|
-
if (text.startsWith("BLOCKED:"))
|
|
356
|
-
throw new Error(text);
|
|
357
|
-
return text;
|
|
358
|
-
}
|
|
359
|
-
// ── summarize output (for MCP/agent use) ──────────────────────────────────────
|
|
360
|
-
export async function summarizeOutput(command, output, maxTokens = 200) {
|
|
361
|
-
const provider = getProvider();
|
|
362
|
-
const routing = pickModel("summarize");
|
|
363
|
-
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.`, {
|
|
364
|
-
model: routing.fast,
|
|
365
|
-
maxTokens,
|
|
366
|
-
system: "You summarize command output for AI agents. Be extremely concise. Return structured info. No prose.",
|
|
367
|
-
});
|
|
368
|
-
}
|
package/dist/cache.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// In-memory LRU cache + disk persistence for command translations
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
const CACHE_FILE = join(homedir(), ".terminal", "cache.json");
|
|
6
|
-
const MAX_ENTRIES = 500;
|
|
7
|
-
let mem = {};
|
|
8
|
-
export function loadCache() {
|
|
9
|
-
if (!existsSync(CACHE_FILE))
|
|
10
|
-
return;
|
|
11
|
-
try {
|
|
12
|
-
mem = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
13
|
-
}
|
|
14
|
-
catch { }
|
|
15
|
-
}
|
|
16
|
-
function persistCache() {
|
|
17
|
-
try {
|
|
18
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mem));
|
|
19
|
-
}
|
|
20
|
-
catch { }
|
|
21
|
-
}
|
|
22
|
-
/** Normalize a natural language query for cache lookup */
|
|
23
|
-
export function normalizeNl(nl) {
|
|
24
|
-
return nl
|
|
25
|
-
.toLowerCase()
|
|
26
|
-
.trim()
|
|
27
|
-
.replace(/[^a-z0-9\s]/g, "") // strip punctuation
|
|
28
|
-
.replace(/\s+/g, " ");
|
|
29
|
-
}
|
|
30
|
-
export function cacheGet(nl) {
|
|
31
|
-
return mem[normalizeNl(nl)] ?? null;
|
|
32
|
-
}
|
|
33
|
-
export function cacheSet(nl, command) {
|
|
34
|
-
const key = normalizeNl(nl);
|
|
35
|
-
// evict oldest if full
|
|
36
|
-
const keys = Object.keys(mem);
|
|
37
|
-
if (keys.length >= MAX_ENTRIES)
|
|
38
|
-
delete mem[keys[0]];
|
|
39
|
-
mem[key] = command;
|
|
40
|
-
persistCache();
|
|
41
|
-
}
|
package/dist/command-rewriter.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// Command rewriter — auto-optimize commands to produce less output
|
|
2
|
-
// Only rewrites when semantic result is identical
|
|
3
|
-
const rules = [
|
|
4
|
-
// find | grep -v node_modules → find -not -path
|
|
5
|
-
{
|
|
6
|
-
pattern: /find\s+(\S+)\s+(.*?)\|\s*grep\s+-v\s+node_modules/,
|
|
7
|
-
rewrite: (m, cmd) => cmd.replace(m[0], `find ${m[1]} ${m[2]}-not -path '*/node_modules/*'`),
|
|
8
|
-
reason: "avoid pipe, filter in-kernel",
|
|
9
|
-
},
|
|
10
|
-
// cat file | grep X → grep X file
|
|
11
|
-
{
|
|
12
|
-
pattern: /cat\s+(\S+)\s*\|\s*grep\s+(.*)/,
|
|
13
|
-
rewrite: (m) => `grep ${m[2]} ${m[1]}`,
|
|
14
|
-
reason: "useless cat",
|
|
15
|
-
},
|
|
16
|
-
// find without node_modules exclusion → add it
|
|
17
|
-
{
|
|
18
|
-
pattern: /^find\s+\.\s+(.*)(?!.*node_modules)/,
|
|
19
|
-
rewrite: (m, cmd) => {
|
|
20
|
-
if (cmd.includes("node_modules") || cmd.includes("-not -path"))
|
|
21
|
-
return cmd;
|
|
22
|
-
return cmd.replace(/^find\s+\.\s+/, "find . -not -path '*/node_modules/*' -not -path '*/.git/*' ");
|
|
23
|
-
},
|
|
24
|
-
reason: "auto-exclude node_modules and .git",
|
|
25
|
-
},
|
|
26
|
-
// git log without limit → add --oneline -20
|
|
27
|
-
{
|
|
28
|
-
pattern: /^git\s+log\s*$/,
|
|
29
|
-
rewrite: () => "git log --oneline -20",
|
|
30
|
-
reason: "prevent unbounded log output",
|
|
31
|
-
},
|
|
32
|
-
// git diff without stat → add --stat for overview
|
|
33
|
-
{
|
|
34
|
-
pattern: /^git\s+diff\s*$/,
|
|
35
|
-
rewrite: () => "git diff --stat",
|
|
36
|
-
reason: "stat overview is usually sufficient",
|
|
37
|
-
},
|
|
38
|
-
// npm ls without depth → add --depth=0
|
|
39
|
-
{
|
|
40
|
-
pattern: /^npm\s+ls\s*$/,
|
|
41
|
-
rewrite: () => "npm ls --depth=0",
|
|
42
|
-
reason: "full tree is massive, top-level usually enough",
|
|
43
|
-
},
|
|
44
|
-
// ps aux without filter → sort by memory and head (macOS compatible)
|
|
45
|
-
{
|
|
46
|
-
pattern: /^ps\s+aux\s*$/,
|
|
47
|
-
rewrite: () => "ps aux | sort -k4 -rn | head -20",
|
|
48
|
-
reason: "full process list is noise, show top consumers",
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
/** Rewrite a command to produce less output */
|
|
52
|
-
export function rewriteCommand(cmd) {
|
|
53
|
-
const trimmed = cmd.trim();
|
|
54
|
-
for (const rule of rules) {
|
|
55
|
-
const match = trimmed.match(rule.pattern);
|
|
56
|
-
if (match) {
|
|
57
|
-
const rewritten = rule.rewrite(match, trimmed);
|
|
58
|
-
if (rewritten !== trimmed) {
|
|
59
|
-
return { original: trimmed, rewritten, changed: true, reason: rule.reason };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return { original: trimmed, rewritten: trimmed, changed: false };
|
|
64
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Command validator — catch invalid commands BEFORE executing
|
|
2
|
-
// Prevents shell errors from hallucinated flags, wrong paths, bad syntax
|
|
3
|
-
import { existsSync } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
/** Extract file paths referenced in a command */
|
|
6
|
-
function extractPaths(command) {
|
|
7
|
-
const paths = [];
|
|
8
|
-
// Match quoted paths
|
|
9
|
-
const quoted = command.match(/["']([^"']+\.\w+)["']/g);
|
|
10
|
-
if (quoted)
|
|
11
|
-
paths.push(...quoted.map(q => q.replace(/["']/g, "")));
|
|
12
|
-
// Match unquoted paths with extensions or directory separators
|
|
13
|
-
const tokens = command.split(/\s+/);
|
|
14
|
-
for (const t of tokens) {
|
|
15
|
-
if (t.includes("/") && !t.startsWith("-") && !t.startsWith("|") && !t.startsWith("&")) {
|
|
16
|
-
// Clean shell operators from end
|
|
17
|
-
const clean = t.replace(/[;|&>]+$/, "");
|
|
18
|
-
if (clean && !clean.startsWith("-"))
|
|
19
|
-
paths.push(clean);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return [...new Set(paths)];
|
|
23
|
-
}
|
|
24
|
-
/** Check for obviously broken shell syntax */
|
|
25
|
-
function checkSyntax(command) {
|
|
26
|
-
const issues = [];
|
|
27
|
-
// Unmatched quotes
|
|
28
|
-
const singleQuotes = (command.match(/'/g) || []).length;
|
|
29
|
-
const doubleQuotes = (command.match(/"/g) || []).length;
|
|
30
|
-
if (singleQuotes % 2 !== 0)
|
|
31
|
-
issues.push("unmatched single quote");
|
|
32
|
-
if (doubleQuotes % 2 !== 0)
|
|
33
|
-
issues.push("unmatched double quote");
|
|
34
|
-
// Unmatched parentheses
|
|
35
|
-
const openParens = (command.match(/\(/g) || []).length;
|
|
36
|
-
const closeParens = (command.match(/\)/g) || []).length;
|
|
37
|
-
if (openParens !== closeParens)
|
|
38
|
-
issues.push("unmatched parentheses");
|
|
39
|
-
// Empty pipe targets
|
|
40
|
-
if (/\|\s*$/.test(command))
|
|
41
|
-
issues.push("pipe with no target");
|
|
42
|
-
if (/^\s*\|/.test(command))
|
|
43
|
-
issues.push("pipe with no source");
|
|
44
|
-
return issues;
|
|
45
|
-
}
|
|
46
|
-
/** Validate a command before execution */
|
|
47
|
-
export function validateCommand(command, cwd) {
|
|
48
|
-
const issues = [];
|
|
49
|
-
// Check syntax
|
|
50
|
-
issues.push(...checkSyntax(command));
|
|
51
|
-
// Check file paths exist
|
|
52
|
-
const paths = extractPaths(command);
|
|
53
|
-
for (const p of paths) {
|
|
54
|
-
const fullPath = p.startsWith("/") ? p : join(cwd, p);
|
|
55
|
-
if (p.includes("*") || p.includes("?"))
|
|
56
|
-
continue; // skip globs
|
|
57
|
-
if (p.startsWith("-"))
|
|
58
|
-
continue; // skip flags
|
|
59
|
-
if ([".", "..", "/", "~"].includes(p))
|
|
60
|
-
continue; // skip special
|
|
61
|
-
if (!existsSync(fullPath) && !existsSync(p)) {
|
|
62
|
-
// Only flag source file paths, not output paths
|
|
63
|
-
if (/\.(ts|tsx|js|jsx|json|md|yaml|yml|py|go|rs)$/.test(p)) {
|
|
64
|
-
issues.push(`file not found: ${p}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Check for common GNU flags on macOS
|
|
69
|
-
const gnuFlags = command.match(/--max-depth|--color=|--sort=|--field-type|--no-deps/g);
|
|
70
|
-
if (gnuFlags) {
|
|
71
|
-
issues.push(`GNU flag on macOS: ${gnuFlags.join(", ")}`);
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
valid: issues.length === 0,
|
|
75
|
-
issues,
|
|
76
|
-
};
|
|
77
|
-
}
|
package/dist/compression.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// Token compression engine — reduces CLI output to fit within token budgets
|
|
2
|
-
import { parseOutput, estimateTokens, tokenSavings } from "./parsers/index.js";
|
|
3
|
-
/** Strip ANSI escape codes from text */
|
|
4
|
-
export function stripAnsi(text) {
|
|
5
|
-
// eslint-disable-next-line no-control-regex
|
|
6
|
-
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
7
|
-
}
|
|
8
|
-
/** Deduplicate consecutive similar lines (e.g., "Compiling X... Compiling Y...") */
|
|
9
|
-
function deduplicateLines(lines) {
|
|
10
|
-
if (lines.length <= 3)
|
|
11
|
-
return lines;
|
|
12
|
-
const result = [];
|
|
13
|
-
let repeatCount = 0;
|
|
14
|
-
let repeatPattern = "";
|
|
15
|
-
for (let i = 0; i < lines.length; i++) {
|
|
16
|
-
const line = lines[i];
|
|
17
|
-
// Extract a "pattern" — the line without numbers, paths, specific identifiers
|
|
18
|
-
const pattern = line.replace(/[0-9]+/g, "N").replace(/\/\S+/g, "/PATH").replace(/\s+/g, " ").trim();
|
|
19
|
-
if (pattern === repeatPattern) {
|
|
20
|
-
repeatCount++;
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
if (repeatCount > 2) {
|
|
24
|
-
result.push(` ... (${repeatCount} similar lines)`);
|
|
25
|
-
}
|
|
26
|
-
else if (repeatCount > 0) {
|
|
27
|
-
// Push the skipped lines back
|
|
28
|
-
for (let j = i - repeatCount; j < i; j++) {
|
|
29
|
-
result.push(lines[j]);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
result.push(line);
|
|
33
|
-
repeatPattern = pattern;
|
|
34
|
-
repeatCount = 0;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (repeatCount > 2) {
|
|
38
|
-
result.push(` ... (${repeatCount} similar lines)`);
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
for (let j = lines.length - repeatCount; j < lines.length; j++) {
|
|
42
|
-
result.push(lines[j]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
/** Smart truncation: keep first N + last M lines */
|
|
48
|
-
function smartTruncate(text, maxTokens) {
|
|
49
|
-
const lines = text.split("\n");
|
|
50
|
-
const currentTokens = estimateTokens(text);
|
|
51
|
-
if (currentTokens <= maxTokens)
|
|
52
|
-
return text;
|
|
53
|
-
// Keep proportional first/last, with first getting more
|
|
54
|
-
const targetLines = Math.floor((maxTokens * lines.length) / currentTokens);
|
|
55
|
-
const firstCount = Math.ceil(targetLines * 0.6);
|
|
56
|
-
const lastCount = Math.floor(targetLines * 0.4);
|
|
57
|
-
if (firstCount + lastCount >= lines.length)
|
|
58
|
-
return text;
|
|
59
|
-
const first = lines.slice(0, firstCount);
|
|
60
|
-
const last = lines.slice(-lastCount);
|
|
61
|
-
const hiddenCount = lines.length - firstCount - lastCount;
|
|
62
|
-
return [...first, `\n--- ${hiddenCount} lines hidden ---\n`, ...last].join("\n");
|
|
63
|
-
}
|
|
64
|
-
/** Compress command output to fit within a token budget */
|
|
65
|
-
export function compress(command, output, options = {}) {
|
|
66
|
-
const { maxTokens, format = "text", stripAnsi: doStrip = true } = options;
|
|
67
|
-
const originalTokens = estimateTokens(output);
|
|
68
|
-
// Step 1: Strip ANSI codes
|
|
69
|
-
let text = doStrip ? stripAnsi(output) : output;
|
|
70
|
-
// Step 2: Try structured parsing (format=json or when it saves tokens)
|
|
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
|
|
91
|
-
const lines = text.split("\n");
|
|
92
|
-
const deduped = deduplicateLines(lines);
|
|
93
|
-
text = deduped.join("\n");
|
|
94
|
-
// Step 4: Smart truncation if over budget
|
|
95
|
-
if (maxTokens) {
|
|
96
|
-
text = smartTruncate(text, maxTokens);
|
|
97
|
-
}
|
|
98
|
-
const compressedTokens = estimateTokens(text);
|
|
99
|
-
return {
|
|
100
|
-
content: text,
|
|
101
|
-
format: "text",
|
|
102
|
-
originalTokens,
|
|
103
|
-
compressedTokens,
|
|
104
|
-
tokensSaved: Math.max(0, originalTokens - compressedTokens),
|
|
105
|
-
savingsPercent: originalTokens > 0 ? Math.round(((originalTokens - compressedTokens) / originalTokens) * 100) : 0,
|
|
106
|
-
};
|
|
107
|
-
}
|