@hasna/terminal 4.3.1 → 4.3.3
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/App.js +404 -0
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/Onboarding.js +51 -0
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +49 -0
- package/dist/ai.js +316 -0
- package/dist/cache.js +42 -0
- package/dist/cli.js +778 -0
- package/dist/command-rewriter.js +64 -0
- package/dist/command-validator.js +86 -0
- package/dist/compression.js +91 -0
- package/dist/context-hints.js +285 -0
- package/dist/db/pg-migrations.js +70 -0
- package/dist/diff-cache.js +107 -0
- package/dist/discover.js +212 -0
- package/dist/economy.js +155 -0
- package/dist/expand-store.js +44 -0
- package/dist/file-cache.js +72 -0
- package/dist/file-index.js +62 -0
- package/dist/history.js +62 -0
- package/dist/lazy-executor.js +54 -0
- package/dist/line-dedup.js +59 -0
- package/dist/loop-detector.js +75 -0
- package/dist/mcp/install.js +189 -0
- package/dist/mcp/server.js +90 -0
- package/dist/mcp/tools/batch.js +111 -0
- package/dist/mcp/tools/execute.js +194 -0
- package/dist/mcp/tools/files.js +290 -0
- package/dist/mcp/tools/git.js +233 -0
- package/dist/mcp/tools/helpers.js +63 -0
- package/dist/mcp/tools/memory.js +151 -0
- package/dist/mcp/tools/meta.js +138 -0
- package/dist/mcp/tools/process.js +50 -0
- package/dist/mcp/tools/project.js +251 -0
- package/dist/mcp/tools/search.js +86 -0
- package/dist/noise-filter.js +94 -0
- package/dist/output-processor.js +233 -0
- package/dist/output-store.js +112 -0
- package/dist/paths.js +28 -0
- package/dist/providers/anthropic.js +43 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +8 -0
- package/dist/providers/groq.js +8 -0
- package/dist/providers/index.js +142 -0
- package/dist/providers/openai-compat.js +93 -0
- package/dist/providers/xai.js +8 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/storage.js +153 -0
- package/dist/search/content-search.js +70 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +5 -0
- package/dist/search/semantic.js +346 -0
- package/dist/session-boot.js +59 -0
- package/dist/session-context.js +55 -0
- package/dist/sessions-db.js +240 -0
- package/dist/smart-display.js +286 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/test-watchlist.js +131 -0
- package/dist/tokens.js +17 -0
- package/dist/tool-profiles.js +130 -0
- package/dist/tree.js +94 -0
- package/dist/usage-cache.js +65 -0
- package/package.json +2 -1
- package/src/Onboarding.tsx +1 -1
- package/src/ai.ts +5 -4
- package/src/cache.ts +2 -2
- package/src/db/pg-migrations.ts +77 -0
- package/src/economy.ts +3 -3
- package/src/history.ts +2 -2
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tools/memory.ts +4 -2
- package/src/output-store.ts +2 -1
- package/src/paths.ts +32 -0
- package/src/recipes/storage.ts +3 -3
- package/src/session-context.ts +2 -2
- package/src/sessions-db.ts +15 -4
- package/src/tool-profiles.ts +4 -3
- package/src/usage-cache.ts +2 -2
package/dist/cli.js
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
// ── Help / Version ───────────────────────────────────────────────────────────
|
|
6
|
+
if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
|
|
7
|
+
console.log(`open-terminal — Natural language shell for AI agents and humans
|
|
8
|
+
|
|
9
|
+
USAGE:
|
|
10
|
+
terminal "your request" NL → AI picks command → runs → smart output
|
|
11
|
+
terminal Launch interactive NL terminal (TUI)
|
|
12
|
+
|
|
13
|
+
EXAMPLES:
|
|
14
|
+
terminal "list all typescript files"
|
|
15
|
+
terminal "run tests"
|
|
16
|
+
terminal "what changed in git"
|
|
17
|
+
terminal "show me the auth functions"
|
|
18
|
+
terminal "kill port 3000"
|
|
19
|
+
terminal "how many lines of code"
|
|
20
|
+
|
|
21
|
+
SETUP:
|
|
22
|
+
install Set up MCP server for all AI agents (Claude, Codex, Gemini)
|
|
23
|
+
install --claude Set up for Claude Code only
|
|
24
|
+
install --codex Set up for Codex only
|
|
25
|
+
install --gemini Set up for Gemini CLI only
|
|
26
|
+
uninstall Remove from all agents
|
|
27
|
+
|
|
28
|
+
SUBCOMMANDS:
|
|
29
|
+
repo Git repo state (branch + status + log)
|
|
30
|
+
symbols <file> File outline (functions, classes, exports)
|
|
31
|
+
overview Project overview (deps, scripts, structure)
|
|
32
|
+
stats Token economy dashboard
|
|
33
|
+
sessions [stats|<id>] Session history and analytics
|
|
34
|
+
recipe add|list|run|delete Reusable command recipes
|
|
35
|
+
collection create|list Recipe collections
|
|
36
|
+
mcp serve Start MCP server (called by agents, not you)
|
|
37
|
+
discover [--days=N] [--json] Scan Claude sessions, show token savings potential
|
|
38
|
+
snapshot Terminal state as JSON
|
|
39
|
+
--help Show this help
|
|
40
|
+
--version Show version
|
|
41
|
+
|
|
42
|
+
MCP TOOLS (20+):
|
|
43
|
+
execute, execute_smart, execute_diff, expand, browse,
|
|
44
|
+
search_files, search_content, search_semantic, read_file,
|
|
45
|
+
read_symbol, symbols, repo_state, explain_error, status,
|
|
46
|
+
bg_start, bg_stop, bg_status, bg_logs, bg_wait_port,
|
|
47
|
+
list_recipes, run_recipe, save_recipe, list_collections,
|
|
48
|
+
snapshot, token_stats, session_history
|
|
49
|
+
|
|
50
|
+
ENVIRONMENT:
|
|
51
|
+
XAI_API_KEY xAI API key (Grok, code-optimized — default)
|
|
52
|
+
CEREBRAS_API_KEY Cerebras API key (free, open-source)
|
|
53
|
+
GROQ_API_KEY Groq API key (free, ultra-fast inference)
|
|
54
|
+
ANTHROPIC_API_KEY Anthropic API key (Claude models)
|
|
55
|
+
`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
59
|
+
const { readFileSync } = await import("fs");
|
|
60
|
+
const { join, dirname } = await import("path");
|
|
61
|
+
try {
|
|
62
|
+
const pkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf8"));
|
|
63
|
+
console.log(pkg.version);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
console.log("1.0.0");
|
|
67
|
+
}
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
// ── Install / Uninstall ──────────────────────────────────────────────────────
|
|
71
|
+
if (args[0] === "install") {
|
|
72
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
73
|
+
handleInstall(args.slice(1));
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
if (args[0] === "uninstall") {
|
|
77
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
78
|
+
handleInstall(["uninstall"]);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
// ── Prune ────────────────────────────────────────────────────────────────────
|
|
82
|
+
if (args[0] === "prune") {
|
|
83
|
+
const days = parseInt(args.find(a => a.startsWith("--older-than="))?.split("=")[1] ?? "90");
|
|
84
|
+
const { pruneSessions } = await import("./sessions-db.js");
|
|
85
|
+
const result = pruneSessions(days);
|
|
86
|
+
console.log(` Pruned ${result.sessionsDeleted} sessions, ${result.interactionsDeleted} interactions older than ${days}d`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
90
|
+
if (args[0] === "mcp") {
|
|
91
|
+
if (args[1] === "serve" || args.length === 1) {
|
|
92
|
+
const { startMcpServer } = await import("./mcp/server.js");
|
|
93
|
+
startMcpServer().catch((err) => {
|
|
94
|
+
console.error("MCP server error:", err);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else if (args[1] === "install") {
|
|
99
|
+
// Legacy: `terminal mcp install` still works
|
|
100
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
101
|
+
handleInstall(args.slice(2));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log("Usage: terminal mcp serve");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ── Hook commands ────────────────────────────────────────────────────────────
|
|
108
|
+
else if (args[0] === "hook") {
|
|
109
|
+
const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import("fs");
|
|
110
|
+
const { join, dirname } = await import("path");
|
|
111
|
+
const { execSync } = await import("child_process");
|
|
112
|
+
const sub = args[1];
|
|
113
|
+
const target = args[2]; // --claude, --codex
|
|
114
|
+
if (sub === "install" && (target === "--claude" || target === "claude")) {
|
|
115
|
+
// Find the hook script
|
|
116
|
+
const hookSrc = join(dirname(new URL(import.meta.url).pathname), "hooks", "claude-hook.sh");
|
|
117
|
+
const hookDest = join(process.env.HOME ?? "~", ".claude", "hooks", "PostToolUse-open-terminal.sh");
|
|
118
|
+
// Copy hook script
|
|
119
|
+
const destDir = dirname(hookDest);
|
|
120
|
+
if (!existsSync(destDir))
|
|
121
|
+
mkdirSync(destDir, { recursive: true });
|
|
122
|
+
// Generate hook with stable paths (resolve npm global root, not fnm temp shell)
|
|
123
|
+
const npmRoot = execSync("npm root -g", { encoding: "utf8" }).trim();
|
|
124
|
+
const distPath = join(npmRoot, "@hasna/terminal/dist");
|
|
125
|
+
const hookScript = `#!/usr/bin/env bash
|
|
126
|
+
# open-terminal PostToolUse hook — compresses Bash output
|
|
127
|
+
# Installed by: t hook install --claude
|
|
128
|
+
# Docs: https://github.com/hasna/terminal
|
|
129
|
+
|
|
130
|
+
if [ "$TOOL_NAME" != "Bash" ]; then exit 0; fi
|
|
131
|
+
OUTPUT=$(cat)
|
|
132
|
+
if [ \${#OUTPUT} -lt 500 ]; then echo "$OUTPUT"; exit 0; fi
|
|
133
|
+
|
|
134
|
+
LINE_COUNT=$(echo "$OUTPUT" | wc -l | tr -d ' ')
|
|
135
|
+
if [ "$LINE_COUNT" -gt 15 ]; then
|
|
136
|
+
# Find the dist path (stable, not fnm temp shell)
|
|
137
|
+
DIST="${distPath}"
|
|
138
|
+
if [ ! -d "$DIST" ]; then
|
|
139
|
+
DIST="$(npm root -g 2>/dev/null)/@hasna/terminal/dist"
|
|
140
|
+
fi
|
|
141
|
+
COMPRESSED=$(echo "$OUTPUT" | bun -e "
|
|
142
|
+
import{compress,stripAnsi}from'$DIST/compression.js';
|
|
143
|
+
import{stripNoise}from'$DIST/noise-filter.js';
|
|
144
|
+
let i='';process.stdin.on('data',d=>i+=d);process.stdin.on('end',()=>{
|
|
145
|
+
const c=stripNoise(stripAnsi(i)).cleaned;
|
|
146
|
+
const r=compress('bash',c,{maxTokens:500});
|
|
147
|
+
console.log(r.tokensSaved>50?r.content:c);
|
|
148
|
+
});
|
|
149
|
+
" 2>/dev/null)
|
|
150
|
+
if [ $? -eq 0 ] && [ -n "$COMPRESSED" ]; then echo "$COMPRESSED"; exit 0; fi
|
|
151
|
+
fi
|
|
152
|
+
echo "$OUTPUT"
|
|
153
|
+
`;
|
|
154
|
+
writeFileSync(hookDest, hookScript, { mode: 0o755 });
|
|
155
|
+
// Register in Claude settings
|
|
156
|
+
const settingsPath = join(process.env.HOME ?? "~", ".claude", "settings.json");
|
|
157
|
+
let settings = {};
|
|
158
|
+
if (existsSync(settingsPath)) {
|
|
159
|
+
try {
|
|
160
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
if (!settings.hooks)
|
|
165
|
+
settings.hooks = {};
|
|
166
|
+
if (!settings.hooks.PostToolUse)
|
|
167
|
+
settings.hooks.PostToolUse = [];
|
|
168
|
+
const hookEntry = { command: hookDest, event: "PostToolUse", tools: ["Bash"] };
|
|
169
|
+
const exists = settings.hooks.PostToolUse.some((h) => h.command?.includes("open-terminal"));
|
|
170
|
+
if (!exists) {
|
|
171
|
+
settings.hooks.PostToolUse.push(hookEntry);
|
|
172
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
173
|
+
}
|
|
174
|
+
console.log("✓ Installed open-terminal PostToolUse hook for Claude Code");
|
|
175
|
+
console.log(" Hook: " + hookDest);
|
|
176
|
+
console.log(" Bash output >15 lines will be auto-compressed");
|
|
177
|
+
}
|
|
178
|
+
else if (sub === "uninstall") {
|
|
179
|
+
const settingsPath = join(process.env.HOME ?? "~", ".claude", "settings.json");
|
|
180
|
+
if (existsSync(settingsPath)) {
|
|
181
|
+
try {
|
|
182
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
183
|
+
if (settings.hooks?.PostToolUse) {
|
|
184
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((h) => !h.command?.includes("open-terminal"));
|
|
185
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
}
|
|
190
|
+
console.log("✓ Uninstalled open-terminal hook");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.log("Usage: t hook install --claude");
|
|
194
|
+
console.log(" t hook uninstall");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ── Recipe commands ──────────────────────────────────────────────────────────
|
|
198
|
+
else if (args[0] === "recipe") {
|
|
199
|
+
const { listRecipes, getRecipe, createRecipe, deleteRecipe, listCollections, createCollection } = await import("./recipes/storage.js");
|
|
200
|
+
const { substituteVariables } = await import("./recipes/model.js");
|
|
201
|
+
const sub = args[1];
|
|
202
|
+
if (sub === "list") {
|
|
203
|
+
const collection = args.find(a => a.startsWith("--collection="))?.split("=")[1];
|
|
204
|
+
let recipes = listRecipes(process.cwd());
|
|
205
|
+
if (collection)
|
|
206
|
+
recipes = recipes.filter(r => r.collection === collection);
|
|
207
|
+
if (recipes.length === 0) {
|
|
208
|
+
console.log("No recipes found.");
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
for (const r of recipes) {
|
|
212
|
+
const scope = r.project ? "(project)" : "(global)";
|
|
213
|
+
const col = r.collection ? ` [${r.collection}]` : "";
|
|
214
|
+
console.log(` ${r.name}${col} ${scope} → ${r.command}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (sub === "add" && args[2] && args[3]) {
|
|
219
|
+
const name = args[2];
|
|
220
|
+
const command = args[3];
|
|
221
|
+
const collection = args.find(a => a.startsWith("--collection="))?.split("=")[1];
|
|
222
|
+
const project = args.includes("--project") ? process.cwd() : undefined;
|
|
223
|
+
const recipe = createRecipe({ name, command, collection, project });
|
|
224
|
+
console.log(`✓ Saved recipe '${recipe.name}' → ${recipe.command}`);
|
|
225
|
+
}
|
|
226
|
+
else if (sub === "run" && args[2]) {
|
|
227
|
+
const recipe = getRecipe(args[2], process.cwd());
|
|
228
|
+
if (!recipe) {
|
|
229
|
+
console.error(`Recipe '${args[2]}' not found.`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
// Parse --var=value arguments
|
|
233
|
+
const vars = {};
|
|
234
|
+
for (const arg of args.slice(3)) {
|
|
235
|
+
const match = arg.match(/^--(\w+)=(.+)$/);
|
|
236
|
+
if (match)
|
|
237
|
+
vars[match[1]] = match[2];
|
|
238
|
+
}
|
|
239
|
+
const cmd = substituteVariables(recipe.command, vars);
|
|
240
|
+
console.log(`$ ${cmd}`);
|
|
241
|
+
const { execSync } = await import("child_process");
|
|
242
|
+
try {
|
|
243
|
+
execSync(cmd, { stdio: "inherit", cwd: process.cwd() });
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
process.exit(e.status ?? 1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (sub === "delete" && args[2]) {
|
|
250
|
+
const ok = deleteRecipe(args[2], process.cwd());
|
|
251
|
+
console.log(ok ? `✓ Deleted recipe '${args[2]}'` : `Recipe '${args[2]}' not found.`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.log("Usage: t recipe [add|list|run|delete]");
|
|
255
|
+
console.log(" t recipe add <name> <command> [--collection=X] [--project]");
|
|
256
|
+
console.log(" t recipe list [--collection=X]");
|
|
257
|
+
console.log(" t recipe run <name> [--var=value]");
|
|
258
|
+
console.log(" t recipe delete <name>");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ── Collection commands ──────────────────────────────────────────────────────
|
|
262
|
+
else if (args[0] === "collection") {
|
|
263
|
+
const { listCollections, createCollection } = await import("./recipes/storage.js");
|
|
264
|
+
const sub = args[1];
|
|
265
|
+
if (sub === "create" && args[2]) {
|
|
266
|
+
const col = createCollection({ name: args[2], description: args[3], project: args.includes("--project") ? process.cwd() : undefined });
|
|
267
|
+
console.log(`✓ Created collection '${col.name}'`);
|
|
268
|
+
}
|
|
269
|
+
else if (sub === "list") {
|
|
270
|
+
const cols = listCollections(process.cwd());
|
|
271
|
+
if (cols.length === 0)
|
|
272
|
+
console.log("No collections.");
|
|
273
|
+
else
|
|
274
|
+
for (const c of cols)
|
|
275
|
+
console.log(` ${c.name}${c.description ? ` — ${c.description}` : ""}`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.log("Usage: t collection [create|list]");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ── Stats command ────────────────────────────────────────────────────────────
|
|
282
|
+
else if (args[0] === "stats") {
|
|
283
|
+
const { formatEconomicsSummary } = await import("./economy.js");
|
|
284
|
+
console.log(formatEconomicsSummary());
|
|
285
|
+
}
|
|
286
|
+
// ── Sessions command ─────────────────────────────────────────────────────────
|
|
287
|
+
else if (args[0] === "sessions") {
|
|
288
|
+
const { listSessions, getSession, getSessionInteractions, getSessionStats } = await import("./sessions-db.js");
|
|
289
|
+
if (args[1] === "stats") {
|
|
290
|
+
const stats = getSessionStats();
|
|
291
|
+
console.log("Session Stats:");
|
|
292
|
+
console.log(` Total sessions: ${stats.totalSessions}`);
|
|
293
|
+
console.log(` Total interactions: ${stats.totalInteractions}`);
|
|
294
|
+
console.log(` Tokens saved: ${stats.totalTokensSaved}`);
|
|
295
|
+
console.log(` Tokens used: ${stats.totalTokensUsed}`);
|
|
296
|
+
console.log(` Cache hit rate: ${(stats.cacheHitRate * 100).toFixed(1)}%`);
|
|
297
|
+
console.log(` Avg per session: ${stats.avgInteractionsPerSession.toFixed(1)}`);
|
|
298
|
+
console.log(` Error rate: ${(stats.errorRate * 100).toFixed(1)}%`);
|
|
299
|
+
}
|
|
300
|
+
else if (args[1]) {
|
|
301
|
+
// Show specific session
|
|
302
|
+
const session = getSession(args[1]);
|
|
303
|
+
if (!session) {
|
|
304
|
+
console.error(`Session '${args[1]}' not found.`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
console.log(`Session: ${session.id}`);
|
|
308
|
+
console.log(` Started: ${new Date(session.started_at).toLocaleString()}`);
|
|
309
|
+
console.log(` CWD: ${session.cwd}`);
|
|
310
|
+
console.log(` Provider: ${session.provider ?? "auto"}`);
|
|
311
|
+
console.log("");
|
|
312
|
+
const interactions = getSessionInteractions(session.id);
|
|
313
|
+
for (const i of interactions) {
|
|
314
|
+
const status = i.exit_code === 0 ? "✓" : i.exit_code ? "✗" : "·";
|
|
315
|
+
console.log(` ${status} ${i.nl}`);
|
|
316
|
+
if (i.command)
|
|
317
|
+
console.log(` $ ${i.command}`);
|
|
318
|
+
}
|
|
319
|
+
console.log(`\n ${interactions.length} interactions`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// List recent sessions
|
|
323
|
+
const sessions = listSessions(20);
|
|
324
|
+
if (sessions.length === 0) {
|
|
325
|
+
console.log("No sessions yet.");
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
for (const s of sessions) {
|
|
329
|
+
const date = new Date(s.started_at).toLocaleString();
|
|
330
|
+
const dir = s.cwd.split("/").pop() || s.cwd;
|
|
331
|
+
console.log(` ${s.id.slice(0, 8)} ${date} ${dir} ${s.provider ?? "auto"}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ── Overview command ─────────────────────────────────────────────────────────
|
|
337
|
+
else if (args[0] === "overview") {
|
|
338
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
339
|
+
const { execSync } = await import("child_process");
|
|
340
|
+
const run = (cmd) => { try {
|
|
341
|
+
return execSync(cmd, { encoding: "utf8", cwd: process.cwd() }).trim();
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return "";
|
|
345
|
+
} };
|
|
346
|
+
let pkg = null;
|
|
347
|
+
try {
|
|
348
|
+
pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
349
|
+
}
|
|
350
|
+
catch { }
|
|
351
|
+
if (pkg) {
|
|
352
|
+
console.log(`${pkg.name}@${pkg.version}`);
|
|
353
|
+
if (pkg.scripts) {
|
|
354
|
+
console.log("\nScripts:");
|
|
355
|
+
for (const [k, v] of Object.entries(pkg.scripts).slice(0, 10))
|
|
356
|
+
console.log(` ${k}: ${v}`);
|
|
357
|
+
}
|
|
358
|
+
if (pkg.dependencies)
|
|
359
|
+
console.log(`\nDeps: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
const src = run("ls -1 src/ 2>/dev/null || ls -1 lib/ 2>/dev/null");
|
|
362
|
+
if (src)
|
|
363
|
+
console.log(`\nSource:\n${src.split("\n").map(f => " " + f).join("\n")}`);
|
|
364
|
+
}
|
|
365
|
+
// ── Repo command ─────────────────────────────────────────────────────────────
|
|
366
|
+
else if (args[0] === "repo") {
|
|
367
|
+
const { execSync } = await import("child_process");
|
|
368
|
+
const run = (cmd) => { try {
|
|
369
|
+
return execSync(cmd, { encoding: "utf8", cwd: process.cwd() }).trim();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return "";
|
|
373
|
+
} };
|
|
374
|
+
const branch = run("git branch --show-current");
|
|
375
|
+
const status = run("git status --short");
|
|
376
|
+
const log = run("git log --oneline -8 --decorate");
|
|
377
|
+
console.log(`Branch: ${branch}`);
|
|
378
|
+
if (status) {
|
|
379
|
+
console.log(`\nChanges:\n${status}`);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
console.log("\nClean working tree");
|
|
383
|
+
}
|
|
384
|
+
console.log(`\nRecent:\n${log}`);
|
|
385
|
+
}
|
|
386
|
+
// ── Symbols command ──────────────────────────────────────────────────────────
|
|
387
|
+
else if (args[0] === "symbols" && args[1]) {
|
|
388
|
+
const { extractSymbolsFromFile } = await import("./search/semantic.js");
|
|
389
|
+
const { resolve } = await import("path");
|
|
390
|
+
const { statSync, readdirSync } = await import("fs");
|
|
391
|
+
const target = resolve(args[1]);
|
|
392
|
+
const filter = args[2]; // optional: grep-like filter on symbol name
|
|
393
|
+
// Support directories — recurse and extract symbols from all source files
|
|
394
|
+
const files = [];
|
|
395
|
+
try {
|
|
396
|
+
if (statSync(target).isDirectory()) {
|
|
397
|
+
const walk = (dir) => {
|
|
398
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
399
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
400
|
+
continue;
|
|
401
|
+
const full = resolve(dir, entry.name);
|
|
402
|
+
if (entry.isDirectory())
|
|
403
|
+
walk(full);
|
|
404
|
+
else if (/\.(ts|tsx|py|go|rs)$/.test(entry.name) && !/\.(test|spec)\.\w+$/.test(entry.name))
|
|
405
|
+
files.push(full);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
walk(target);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
files.push(target);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
files.push(target);
|
|
416
|
+
}
|
|
417
|
+
let totalSymbols = 0;
|
|
418
|
+
for (const file of files) {
|
|
419
|
+
const symbols = extractSymbolsFromFile(file);
|
|
420
|
+
const filtered = filter ? symbols.filter(s => s.name.toLowerCase().includes(filter.toLowerCase()) || s.kind.toLowerCase().includes(filter.toLowerCase())) : symbols;
|
|
421
|
+
if (filtered.length === 0)
|
|
422
|
+
continue;
|
|
423
|
+
const relPath = file.replace(process.cwd() + "/", "");
|
|
424
|
+
if (files.length > 1)
|
|
425
|
+
console.log(`\n${relPath}:`);
|
|
426
|
+
for (const s of filtered) {
|
|
427
|
+
const exp = s.exported ? "⬡" : "·";
|
|
428
|
+
console.log(` ${exp} ${s.kind.padEnd(10)} L${String(s.line).padStart(4)} ${s.name}`);
|
|
429
|
+
}
|
|
430
|
+
totalSymbols += filtered.length;
|
|
431
|
+
}
|
|
432
|
+
if (totalSymbols === 0)
|
|
433
|
+
console.log("No symbols found.");
|
|
434
|
+
else if (files.length > 1)
|
|
435
|
+
console.log(`\n${totalSymbols} symbols across ${files.length} files`);
|
|
436
|
+
}
|
|
437
|
+
// ── History command ──────────────────────────────────────────────────────────
|
|
438
|
+
else if (args[0] === "history") {
|
|
439
|
+
const { loadContext } = await import("./session-context.js");
|
|
440
|
+
const entries = loadContext();
|
|
441
|
+
if (entries.length === 0) {
|
|
442
|
+
console.log("No recent history.");
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
for (const e of entries) {
|
|
446
|
+
const time = new Date(e.timestamp).toLocaleTimeString();
|
|
447
|
+
console.log(` ${time} ${e.prompt}`);
|
|
448
|
+
console.log(` $ ${e.command}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ── Explain command ─────────────────────────────────────────────────────────
|
|
453
|
+
else if (args[0] === "explain" && args[1]) {
|
|
454
|
+
const command = args.slice(1).join(" ");
|
|
455
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY) {
|
|
456
|
+
console.error("explain requires an API key");
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
const { explainCommand } = await import("./ai.js");
|
|
460
|
+
const explanation = await explainCommand(command);
|
|
461
|
+
console.log(explanation);
|
|
462
|
+
}
|
|
463
|
+
// ── Discover command ─────────────────────────────────────────────────────────
|
|
464
|
+
else if (args[0] === "discover") {
|
|
465
|
+
const { discover, formatDiscoverReport } = await import("./discover.js");
|
|
466
|
+
const days = parseInt(args.find(a => a.startsWith("--days="))?.split("=")[1] ?? "30");
|
|
467
|
+
const json = args.includes("--json");
|
|
468
|
+
const report = discover({ maxAgeDays: days });
|
|
469
|
+
if (json) {
|
|
470
|
+
console.log(JSON.stringify(report, null, 2));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
console.log(formatDiscoverReport(report));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ── Snapshot command ─────────────────────────────────────────────────────────
|
|
477
|
+
else if (args[0] === "snapshot") {
|
|
478
|
+
const { captureSnapshot } = await import("./snapshots.js");
|
|
479
|
+
console.log(JSON.stringify(captureSnapshot(), null, 2));
|
|
480
|
+
}
|
|
481
|
+
// ── Project init ─────────────────────────────────────────────────────────────
|
|
482
|
+
else if (args[0] === "project" && args[1] === "init") {
|
|
483
|
+
const { initProject } = await import("./recipes/storage.js");
|
|
484
|
+
initProject(process.cwd());
|
|
485
|
+
console.log("✓ Initialized .terminal/recipes.json");
|
|
486
|
+
}
|
|
487
|
+
// ── NL mode: terminal "natural language prompt" ─────────────────────────────
|
|
488
|
+
else if (args.length > 0) {
|
|
489
|
+
// Everything that doesn't match a subcommand is treated as natural language
|
|
490
|
+
const prompt = args.join(" ");
|
|
491
|
+
const offlineMode = !process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY;
|
|
492
|
+
const { translateToCommand, checkPermissions, isIrreversible } = await import("./ai.js");
|
|
493
|
+
const { execSync } = await import("child_process");
|
|
494
|
+
const { compress, stripAnsi } = await import("./compression.js");
|
|
495
|
+
const { stripNoise } = await import("./noise-filter.js");
|
|
496
|
+
const { processOutput, shouldProcess } = await import("./output-processor.js");
|
|
497
|
+
const { rewriteCommand } = await import("./command-rewriter.js");
|
|
498
|
+
const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
|
|
499
|
+
const { saveOutput, formatOutputHint } = await import("./output-store.js");
|
|
500
|
+
const { estimateTokens } = await import("./tokens.js");
|
|
501
|
+
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
502
|
+
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
503
|
+
const { detectLoop } = await import("./loop-detector.js");
|
|
504
|
+
const { loadConfig } = await import("./history.js");
|
|
505
|
+
const { loadContext, saveContext, formatContext } = await import("./session-context.js");
|
|
506
|
+
const { getLearned, recordMapping } = await import("./usage-cache.js");
|
|
507
|
+
const { recordCorrection, findSimilarCorrections, recordOutput } = await import("./sessions-db.js");
|
|
508
|
+
const config = loadConfig();
|
|
509
|
+
const perms = config.permissions;
|
|
510
|
+
const sessionCtx = formatContext();
|
|
511
|
+
// ── Direct command detection ──
|
|
512
|
+
// If input looks like a shell command (starts with known binary), skip AI translation entirely.
|
|
513
|
+
// This saves one AI call ($0.0008) per invocation for agents that already know the command.
|
|
514
|
+
const KNOWN_BINARIES = /^(ls|cd|cat|head|tail|grep|rg|find|wc|du|df|git|bun|npm|pnpm|yarn|node|python3?|pip|curl|wget|ssh|scp|chmod|chown|chgrp|mkdir|rmdir|rm|cp|mv|touch|ln|tar|gzip|gunzip|zip|unzip|sed|awk|sort|uniq|cut|tr|tee|xargs|echo|printf|env|export|source|which|whereis|whatis|man|date|cal|uptime|whoami|hostname|uname|ps|top|htop|kill|killall|lsof|netstat|ss|ifconfig|ip|ping|dig|nslookup|docker|kubectl|make|cmake|cargo|go|rustc|gcc|g\+\+|clang|java|javac|mvn|gradle|npx|bunx|tsx|deno|tree|file|stat|readlink|realpath|basename|dirname|pwd|test|true|false|sleep|timeout|time|watch|diff|patch|rsync|lsblk|mount|umount|fdisk|free|vmstat|iostat|sar|strace|ltrace|gdb|lldb|sqlite3|psql|mysql|redis-cli|mongosh|jq|yq|bat|fd|exa|fzf|gh|hub|terraform|ansible|helm|k9s|lazygit|tmux|screen|nc|nmap|openssl|base64|md5|shasum|xxd|od|hexdump|strings|nm|objdump|readelf|ldd|ldconfig|pkg-config|brew|apt|yum|dnf|pacman|snap|flatpak|systemctl|service|journalctl|dmesg|crontab|at|nohup|bg|fg|jobs|disown|wait|nice|renice|ionice|chrt|taskset|ulimit|sysctl|getconf|locale|iconv|perl|ruby|php|lua|R|julia|swift|kotlin|scala|elixir|mix|rebar3|tsc|eslint|prettier|biome|ruff|black|isort|mypy|pyright|pylint|flake8|pytest|vitest|jest|mocha|ava|tap|phpunit|rspec|minitest|unittest2|nose2|coverage|nyc|c8|v8-profiler)(\s|$)/;
|
|
515
|
+
const isDirectCommand = KNOWN_BINARIES.test(prompt.trim()) || /^[.\/~]/.test(prompt.trim()) || /\|/.test(prompt);
|
|
516
|
+
// Check usage learning cache first (zero AI cost for repeated queries)
|
|
517
|
+
const learned = getLearned(prompt);
|
|
518
|
+
if (learned && !offlineMode) {
|
|
519
|
+
console.error(`[open-terminal] cached: $ ${learned}`);
|
|
520
|
+
}
|
|
521
|
+
// Step 1: Determine command — either direct passthrough or AI translation
|
|
522
|
+
let command;
|
|
523
|
+
if (isDirectCommand) {
|
|
524
|
+
// Direct command — skip AI translation entirely (saves 1 AI call)
|
|
525
|
+
command = prompt;
|
|
526
|
+
}
|
|
527
|
+
else if (offlineMode) {
|
|
528
|
+
// Offline: treat prompt as literal command
|
|
529
|
+
console.error("[open-terminal] offline mode (no API key) — running as literal command");
|
|
530
|
+
command = prompt;
|
|
531
|
+
}
|
|
532
|
+
else if (learned) {
|
|
533
|
+
command = learned;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
try {
|
|
537
|
+
command = await translateToCommand(sessionCtx ? `${prompt}\n${sessionCtx}` : prompt, perms, []);
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
// If BLOCKED, try README fallback ONLY for conceptual questions (not file access)
|
|
541
|
+
if (e.message?.startsWith("BLOCKED:")) {
|
|
542
|
+
const isConceptual = /\b(explain|why|what does|how does|describe|architecture|overview|summary)\b/i.test(prompt);
|
|
543
|
+
const isFileAccess = /\b(cat|show|read|find|ls|list)\b.*\b(\.\w+\/|src\/|packages\/)/i.test(prompt);
|
|
544
|
+
if (isConceptual && !isFileAccess) {
|
|
545
|
+
try {
|
|
546
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
547
|
+
if (existsSync("README.md")) {
|
|
548
|
+
const readme = readFileSync("README.md", "utf8").slice(0, 3000);
|
|
549
|
+
const processed = await processOutput("cat README.md", readme, prompt);
|
|
550
|
+
if (processed.aiProcessed) {
|
|
551
|
+
console.log(processed.summary);
|
|
552
|
+
process.exit(0);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch { }
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Show the block reason clearly
|
|
560
|
+
if (e.message?.startsWith("BLOCKED:")) {
|
|
561
|
+
console.log(`⚠ ${e.message}`);
|
|
562
|
+
console.log(` This is a READ-ONLY terminal. Run directly in your shell if you're sure.`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
console.error(e.message);
|
|
566
|
+
}
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
} // close the else (learned/offline) block
|
|
570
|
+
// Record the mapping for usage learning
|
|
571
|
+
if (!offlineMode && !learned)
|
|
572
|
+
recordMapping(prompt, command);
|
|
573
|
+
// Check permissions
|
|
574
|
+
const blocked = checkPermissions(command, perms);
|
|
575
|
+
if (blocked) {
|
|
576
|
+
console.error(`blocked: ${blocked}`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
// Safety: when command is irreversible, try a safer read-only alternative
|
|
580
|
+
if (isIrreversible(command)) {
|
|
581
|
+
// Try to generate a safe alternative via AI
|
|
582
|
+
try {
|
|
583
|
+
const safeCommand = await translateToCommand(`${prompt} (IMPORTANT: use ONLY read-only commands like grep, find, cat, wc, ls. Do NOT use npx, install, kill, push, sed, or any modifying command.)`, perms, []);
|
|
584
|
+
if (!isIrreversible(safeCommand) && !checkPermissions(safeCommand, perms)) {
|
|
585
|
+
console.error(`$ ${safeCommand} (safe alternative)`);
|
|
586
|
+
command = safeCommand;
|
|
587
|
+
// Continue to execution below
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
console.error(`⚠ BLOCKED: $ ${command}`);
|
|
591
|
+
console.error(` Run directly in your shell if you're sure.`);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
console.error(`⚠ BLOCKED: $ ${command}`);
|
|
597
|
+
console.error(` Run directly in your shell if you're sure.`);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Step 2: Validate command before executing
|
|
602
|
+
const { validateCommand } = await import("./command-validator.js");
|
|
603
|
+
const validation = validateCommand(command, process.cwd());
|
|
604
|
+
if (!validation.valid) {
|
|
605
|
+
// Auto-retry: re-translate with simpler constraints
|
|
606
|
+
console.error(`[open-terminal] invalid command detected: ${validation.issues.join(", ")}`);
|
|
607
|
+
try {
|
|
608
|
+
const retryCommand = await translateToCommand(`${prompt} (Previous command had issues: ${validation.issues.join(", ")}. Fix those specific issues. Keep the approach but correct the errors.)`, perms, []);
|
|
609
|
+
if (retryCommand && retryCommand !== command) {
|
|
610
|
+
const retryValidation = validateCommand(retryCommand, process.cwd());
|
|
611
|
+
if (retryValidation.valid || retryValidation.issues.length < validation.issues.length) {
|
|
612
|
+
command = retryCommand;
|
|
613
|
+
console.error(`[open-terminal] retried: $ ${command}`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
// Retry also invalid — use the simpler of the two
|
|
617
|
+
const retryPipes = (retryCommand.match(/\|/g) || []).length;
|
|
618
|
+
const origPipes = (command.match(/\|/g) || []).length;
|
|
619
|
+
if (retryPipes < origPipes) {
|
|
620
|
+
command = retryCommand;
|
|
621
|
+
console.error(`[open-terminal] retried (simpler): $ ${command}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch { }
|
|
627
|
+
}
|
|
628
|
+
// Show what we're running
|
|
629
|
+
console.error(`$ ${command}`);
|
|
630
|
+
// Step 3: Rewrite for optimization
|
|
631
|
+
const rw = rewriteCommand(command);
|
|
632
|
+
const actualCmd = rw.changed ? rw.rewritten : command;
|
|
633
|
+
if (rw.changed)
|
|
634
|
+
console.error(`[open-terminal] optimized: ${actualCmd}`);
|
|
635
|
+
// Loop detection
|
|
636
|
+
const loop = detectLoop(actualCmd);
|
|
637
|
+
if (loop.detected)
|
|
638
|
+
console.error(`[open-terminal] loop #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : ""}`);
|
|
639
|
+
// Step 3: Execute
|
|
640
|
+
try {
|
|
641
|
+
const start = Date.now();
|
|
642
|
+
const raw = execSync(actualCmd + " 2>&1", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
643
|
+
const duration = Date.now() - start;
|
|
644
|
+
const clean = stripNoise(stripAnsi(raw)).cleaned;
|
|
645
|
+
const rawTokens = estimateTokens(raw);
|
|
646
|
+
recordUsage(rawTokens);
|
|
647
|
+
saveContext(prompt, actualCmd, clean.slice(0, 500));
|
|
648
|
+
// Test output detection
|
|
649
|
+
// Test output: skip watchlist, let AI framing handle it
|
|
650
|
+
// The AI reads "42 pass, 0 fail" better than regex parsing bun's mixed output
|
|
651
|
+
// Frame-first pipeline: AI answers the question, lazy is fallback
|
|
652
|
+
// For question-type prompts, answer framing runs BEFORE lazy mode
|
|
653
|
+
const isQuestion = /^(what|which|how|is|are|does|do|can|should|where|who|why|am|was|were|has|have|will)\b/i.test(prompt) || prompt.includes("?");
|
|
654
|
+
if (clean.length > 10) {
|
|
655
|
+
// Try AI answer framing first (especially for questions)
|
|
656
|
+
const processed = await processOutput(actualCmd, clean, prompt);
|
|
657
|
+
if (processed.aiProcessed) {
|
|
658
|
+
if (processed.tokensSaved > 0)
|
|
659
|
+
recordSaving("compressed", processed.tokensSaved);
|
|
660
|
+
// Save full output for lazy recovery — agents can read the file
|
|
661
|
+
if (processed.tokensSaved > 50) {
|
|
662
|
+
const outputPath = saveOutput(actualCmd, clean);
|
|
663
|
+
console.log(processed.summary);
|
|
664
|
+
console.log(formatOutputHint(outputPath));
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
console.log(processed.summary);
|
|
668
|
+
}
|
|
669
|
+
if (processed.tokensSaved > 10)
|
|
670
|
+
console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
|
|
671
|
+
process.exit(0);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Lazy mode — fallback when AI framing didn't run or failed
|
|
675
|
+
if (shouldBeLazy(clean, actualCmd)) {
|
|
676
|
+
const lazy = toLazy(clean, actualCmd);
|
|
677
|
+
const saved = rawTokens - estimateTokens(JSON.stringify(lazy));
|
|
678
|
+
if (saved > 0)
|
|
679
|
+
recordSaving("compressed", saved);
|
|
680
|
+
console.log(JSON.stringify(lazy, null, 2));
|
|
681
|
+
process.exit(0);
|
|
682
|
+
}
|
|
683
|
+
// Fallback: AI unavailable — pass through clean
|
|
684
|
+
console.log(clean);
|
|
685
|
+
const saved = rawTokens - estimateTokens(clean);
|
|
686
|
+
if (saved > 10) {
|
|
687
|
+
recordSaving("compressed", saved);
|
|
688
|
+
console.error(`[open-terminal] saved ${saved} tokens`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (e) {
|
|
692
|
+
// Empty result (grep exit 1 = no matches) — not a real error
|
|
693
|
+
const errStdout = e.stdout?.toString() ?? "";
|
|
694
|
+
let errStderr = e.stderr?.toString() ?? "";
|
|
695
|
+
if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
|
|
696
|
+
// Empty result — retry with broader scope before giving up
|
|
697
|
+
if (!actualCmd.includes("#(broadened)")) {
|
|
698
|
+
try {
|
|
699
|
+
const broaderCmd = await translateToCommand(`${prompt} (Previous command found NOTHING. Try searching a BROADER scope: use . or packages/ instead of src/. Use simpler grep pattern.)`, perms, []);
|
|
700
|
+
if (broaderCmd && !isIrreversible(broaderCmd) && !checkPermissions(broaderCmd, perms)) {
|
|
701
|
+
console.error(`[open-terminal] broadening search...`);
|
|
702
|
+
const broaderResult = execSync(broaderCmd + " #(broadened)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
703
|
+
const broaderClean = stripNoise(stripAnsi(broaderResult)).cleaned;
|
|
704
|
+
if (broaderClean.trim()) {
|
|
705
|
+
const processed = await processOutput(broaderCmd, broaderClean, prompt);
|
|
706
|
+
console.log(processed.aiProcessed ? processed.summary : broaderClean);
|
|
707
|
+
process.exit(0);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch { /* broader also failed */ }
|
|
712
|
+
}
|
|
713
|
+
console.log(`No results found for: ${prompt}`);
|
|
714
|
+
process.exit(0);
|
|
715
|
+
}
|
|
716
|
+
// 3-retry learning loop: each attempt learns from the previous failure
|
|
717
|
+
if (e.status >= 2) {
|
|
718
|
+
const retryStrategies = [
|
|
719
|
+
// Attempt 2: inject error context
|
|
720
|
+
`${prompt} (Command "${actualCmd}" failed with: ${errStderr.slice(0, 300)}. Fix this specific error. Keep the approach but correct the issue.)`,
|
|
721
|
+
// Attempt 3: inject corrections + force simplicity
|
|
722
|
+
`${prompt} (TWO commands already failed for this query. Use the ABSOLUTE SIMPLEST approach: basic grep -rn, find, wc -l, cat. No awk, no xargs, no subshells. Must work on macOS BSD.)`,
|
|
723
|
+
];
|
|
724
|
+
for (let attempt = 0; attempt < retryStrategies.length; attempt++) {
|
|
725
|
+
try {
|
|
726
|
+
const retryCmd = await translateToCommand(retryStrategies[attempt], perms, []);
|
|
727
|
+
if (!retryCmd || retryCmd === actualCmd || isIrreversible(retryCmd) || checkPermissions(retryCmd, perms))
|
|
728
|
+
continue;
|
|
729
|
+
console.error(`[open-terminal] retry ${attempt + 2}/3: $ ${retryCmd}`);
|
|
730
|
+
const retryResult = execSync(retryCmd + ` #(retry${attempt + 2})`, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
731
|
+
const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
|
|
732
|
+
if (retryClean.length > 5) {
|
|
733
|
+
// Record correction — AI learns for next time
|
|
734
|
+
recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
|
|
735
|
+
const processed = await processOutput(retryCmd, retryClean, prompt);
|
|
736
|
+
console.log(processed.aiProcessed ? processed.summary : retryClean);
|
|
737
|
+
process.exit(0);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch (retryErr) {
|
|
741
|
+
// This attempt also failed — record it and try next strategy
|
|
742
|
+
const retryStderr = retryErr.stderr?.toString() ?? "";
|
|
743
|
+
errStderr = retryStderr; // update for next attempt's context
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// Combine stdout+stderr and try AI answer framing (for audit/lint/test commands)
|
|
749
|
+
const combined = errStderr && errStdout.includes(errStderr.trim()) ? errStdout : errStdout + errStderr;
|
|
750
|
+
const errorClean = stripNoise(stripAnsi(combined)).cleaned;
|
|
751
|
+
if (errorClean.length > 20) {
|
|
752
|
+
try {
|
|
753
|
+
const processed = await processOutput(actualCmd, errorClean, prompt);
|
|
754
|
+
if (processed.aiProcessed) {
|
|
755
|
+
console.log(processed.summary);
|
|
756
|
+
process.exit(e.status ?? 1);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch { }
|
|
760
|
+
}
|
|
761
|
+
console.log(errorClean);
|
|
762
|
+
process.exit(e.status ?? 1);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// ── TUI mode (no args) ──────────────────────────────────────────────────────
|
|
766
|
+
else {
|
|
767
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY) {
|
|
768
|
+
console.error("terminal: No API key found.");
|
|
769
|
+
console.error("Set one of:");
|
|
770
|
+
console.error(" export XAI_API_KEY=your_key (Grok, code-optimized — default)");
|
|
771
|
+
console.error(" export CEREBRAS_API_KEY=your_key (free, open-source)");
|
|
772
|
+
console.error(" export GROQ_API_KEY=your_key (free, ultra-fast)");
|
|
773
|
+
console.error(" export ANTHROPIC_API_KEY=your_key (Claude)");
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const App = (await import("./App.js")).default;
|
|
777
|
+
render(_jsx(App, {}));
|
|
778
|
+
}
|