@hasna/terminal 1.2.2 → 1.3.1
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 +23 -2
- package/dist/cli.js +17 -9
- package/dist/output-processor.js +2 -2
- package/package.json +1 -1
- package/src/ai.ts +22 -2
- package/src/cli.tsx +15 -9
- package/src/output-processor.ts +2 -2
package/dist/ai.js
CHANGED
|
@@ -43,6 +43,8 @@ const IRREVERSIBLE_PATTERNS = [
|
|
|
43
43
|
// Code modification / package installation (security risk)
|
|
44
44
|
/\bnpx\s+\S+/, /\bnpm\s+install\b/, /\bbun\s+add\b/, /\bpip\s+install\b/,
|
|
45
45
|
/\bcodemod\b/, /\bsed\s+-i\b/, /\bawk\s.*>/, /\bperl\s+-[pi]\b/,
|
|
46
|
+
// File creation/modification (READ-ONLY terminal)
|
|
47
|
+
/\btouch\b/, /\bmkdir\b/, /\becho\s.*>/, /\btee\b/, /\bcp\b/, /\bmv\b/,
|
|
46
48
|
];
|
|
47
49
|
export function isIrreversible(command) {
|
|
48
50
|
return IRREVERSIBLE_PATTERNS.some((r) => r.test(command));
|
|
@@ -196,8 +198,27 @@ export async function translateToCommand(nl, perms, sessionEntries, onToken) {
|
|
|
196
198
|
}
|
|
197
199
|
if (text.startsWith("BLOCKED:"))
|
|
198
200
|
throw new Error(text);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
// Strip AI reasoning — extract only the shell command
|
|
202
|
+
// AI sometimes prefixes with "Based on..." or wraps in backticks
|
|
203
|
+
let cleaned = text.trim();
|
|
204
|
+
// Remove markdown code blocks
|
|
205
|
+
cleaned = cleaned.replace(/^```(?:bash|sh|shell)?\n?/m, "").replace(/\n?```$/m, "");
|
|
206
|
+
// Remove lines that look like AI reasoning (start with capital letter, contain "I ", "Based on", etc.)
|
|
207
|
+
const lines = cleaned.split("\n");
|
|
208
|
+
const commandLines = lines.filter(l => {
|
|
209
|
+
const t = l.trim();
|
|
210
|
+
if (!t)
|
|
211
|
+
return false;
|
|
212
|
+
// Skip obvious reasoning lines
|
|
213
|
+
if (/^(Based on|I |This |The |Let me|Here|Note:|Since|Looking|To )/.test(t))
|
|
214
|
+
return false;
|
|
215
|
+
if (/^[A-Z][a-z].*\.$/.test(t))
|
|
216
|
+
return false; // English sentence ending with period
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
cleaned = commandLines.join("\n").trim() || cleaned;
|
|
220
|
+
cacheSet(nl, cleaned);
|
|
221
|
+
return cleaned;
|
|
201
222
|
}
|
|
202
223
|
// ── prefetch ──────────────────────────────────────────────────────────────────
|
|
203
224
|
export function prefetchNext(lastNl, perms, sessionEntries) {
|
package/dist/cli.js
CHANGED
|
@@ -471,17 +471,20 @@ else if (args.length > 0) {
|
|
|
471
471
|
console.log(JSON.stringify(lazy, null, 2));
|
|
472
472
|
process.exit(0);
|
|
473
473
|
}
|
|
474
|
-
// AI
|
|
475
|
-
|
|
474
|
+
// AI answer framing — ALWAYS use in NL mode (even for small output)
|
|
475
|
+
// The AI needs to ANSWER the question, not just pass through data
|
|
476
|
+
if (clean.length > 10) {
|
|
476
477
|
const processed = await processOutput(actualCmd, clean, prompt);
|
|
477
|
-
if (processed.aiProcessed
|
|
478
|
-
|
|
478
|
+
if (processed.aiProcessed) {
|
|
479
|
+
if (processed.tokensSaved > 0)
|
|
480
|
+
recordSaving("compressed", processed.tokensSaved);
|
|
479
481
|
console.log(processed.summary);
|
|
480
|
-
|
|
482
|
+
if (processed.tokensSaved > 10)
|
|
483
|
+
console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
|
|
481
484
|
process.exit(0);
|
|
482
485
|
}
|
|
483
486
|
}
|
|
484
|
-
//
|
|
487
|
+
// Fallback: AI unavailable — pass through clean
|
|
485
488
|
console.log(clean);
|
|
486
489
|
const saved = rawTokens - estimateTokens(clean);
|
|
487
490
|
if (saved > 10) {
|
|
@@ -490,9 +493,14 @@ else if (args.length > 0) {
|
|
|
490
493
|
}
|
|
491
494
|
}
|
|
492
495
|
catch (e) {
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
+
// Empty result (grep exit 1 = no matches) — not a real error
|
|
497
|
+
const errStdout = e.stdout?.toString() ?? "";
|
|
498
|
+
const errStderr = e.stderr?.toString() ?? "";
|
|
499
|
+
if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
|
|
500
|
+
console.log(`No results found for: ${prompt}`);
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
const combined = errStderr && errStdout.includes(errStderr.trim()) ? errStdout : errStdout + errStderr;
|
|
496
504
|
console.log(stripNoise(stripAnsi(combined)).cleaned);
|
|
497
505
|
process.exit(e.status ?? 1);
|
|
498
506
|
}
|
package/dist/output-processor.js
CHANGED
|
@@ -22,8 +22,8 @@ RULES:
|
|
|
22
22
|
*/
|
|
23
23
|
export async function processOutput(command, output, originalPrompt) {
|
|
24
24
|
const lines = output.split("\n");
|
|
25
|
-
// Short output —
|
|
26
|
-
if (lines.length <= MIN_LINES_TO_PROCESS) {
|
|
25
|
+
// Short output — skip AI UNLESS we have an original prompt (NL mode needs answer framing)
|
|
26
|
+
if (lines.length <= MIN_LINES_TO_PROCESS && !originalPrompt) {
|
|
27
27
|
return {
|
|
28
28
|
summary: output,
|
|
29
29
|
full: output,
|
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -51,6 +51,8 @@ const IRREVERSIBLE_PATTERNS = [
|
|
|
51
51
|
// Code modification / package installation (security risk)
|
|
52
52
|
/\bnpx\s+\S+/, /\bnpm\s+install\b/, /\bbun\s+add\b/, /\bpip\s+install\b/,
|
|
53
53
|
/\bcodemod\b/, /\bsed\s+-i\b/, /\bawk\s.*>/, /\bperl\s+-[pi]\b/,
|
|
54
|
+
// File creation/modification (READ-ONLY terminal)
|
|
55
|
+
/\btouch\b/, /\bmkdir\b/, /\becho\s.*>/, /\btee\b/, /\bcp\b/, /\bmv\b/,
|
|
54
56
|
];
|
|
55
57
|
|
|
56
58
|
export function isIrreversible(command: string): boolean {
|
|
@@ -237,8 +239,26 @@ export async function translateToCommand(
|
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
if (text.startsWith("BLOCKED:")) throw new Error(text);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
|
|
243
|
+
// Strip AI reasoning — extract only the shell command
|
|
244
|
+
// AI sometimes prefixes with "Based on..." or wraps in backticks
|
|
245
|
+
let cleaned = text.trim();
|
|
246
|
+
// Remove markdown code blocks
|
|
247
|
+
cleaned = cleaned.replace(/^```(?:bash|sh|shell)?\n?/m, "").replace(/\n?```$/m, "");
|
|
248
|
+
// Remove lines that look like AI reasoning (start with capital letter, contain "I ", "Based on", etc.)
|
|
249
|
+
const lines = cleaned.split("\n");
|
|
250
|
+
const commandLines = lines.filter(l => {
|
|
251
|
+
const t = l.trim();
|
|
252
|
+
if (!t) return false;
|
|
253
|
+
// Skip obvious reasoning lines
|
|
254
|
+
if (/^(Based on|I |This |The |Let me|Here|Note:|Since|Looking|To )/.test(t)) return false;
|
|
255
|
+
if (/^[A-Z][a-z].*\.$/.test(t)) return false; // English sentence ending with period
|
|
256
|
+
return true;
|
|
257
|
+
});
|
|
258
|
+
cleaned = commandLines.join("\n").trim() || cleaned;
|
|
259
|
+
|
|
260
|
+
cacheSet(nl, cleaned);
|
|
261
|
+
return cleaned;
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
// ── prefetch ──────────────────────────────────────────────────────────────────
|
package/src/cli.tsx
CHANGED
|
@@ -455,25 +455,31 @@ else if (args.length > 0) {
|
|
|
455
455
|
process.exit(0);
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
// AI
|
|
459
|
-
|
|
458
|
+
// AI answer framing — ALWAYS use in NL mode (even for small output)
|
|
459
|
+
// The AI needs to ANSWER the question, not just pass through data
|
|
460
|
+
if (clean.length > 10) {
|
|
460
461
|
const processed = await processOutput(actualCmd, clean, prompt);
|
|
461
|
-
if (processed.aiProcessed
|
|
462
|
-
recordSaving("compressed", processed.tokensSaved);
|
|
462
|
+
if (processed.aiProcessed) {
|
|
463
|
+
if (processed.tokensSaved > 0) recordSaving("compressed", processed.tokensSaved);
|
|
463
464
|
console.log(processed.summary);
|
|
464
|
-
console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
|
|
465
|
+
if (processed.tokensSaved > 10) console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
|
|
465
466
|
process.exit(0);
|
|
466
467
|
}
|
|
467
468
|
}
|
|
468
469
|
|
|
469
|
-
//
|
|
470
|
+
// Fallback: AI unavailable — pass through clean
|
|
470
471
|
console.log(clean);
|
|
471
472
|
const saved = rawTokens - estimateTokens(clean);
|
|
472
473
|
if (saved > 10) { recordSaving("compressed", saved); console.error(`[open-terminal] saved ${saved} tokens`); }
|
|
473
474
|
} catch (e: any) {
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const
|
|
475
|
+
// Empty result (grep exit 1 = no matches) — not a real error
|
|
476
|
+
const errStdout = e.stdout?.toString() ?? "";
|
|
477
|
+
const errStderr = e.stderr?.toString() ?? "";
|
|
478
|
+
if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
|
|
479
|
+
console.log(`No results found for: ${prompt}`);
|
|
480
|
+
process.exit(0);
|
|
481
|
+
}
|
|
482
|
+
const combined = errStderr && errStdout.includes(errStderr.trim()) ? errStdout : errStdout + errStderr;
|
|
477
483
|
console.log(stripNoise(stripAnsi(combined)).cleaned);
|
|
478
484
|
process.exit(e.status ?? 1);
|
|
479
485
|
}
|
package/src/output-processor.ts
CHANGED
|
@@ -52,8 +52,8 @@ export async function processOutput(
|
|
|
52
52
|
): Promise<ProcessedOutput> {
|
|
53
53
|
const lines = output.split("\n");
|
|
54
54
|
|
|
55
|
-
// Short output —
|
|
56
|
-
if (lines.length <= MIN_LINES_TO_PROCESS) {
|
|
55
|
+
// Short output — skip AI UNLESS we have an original prompt (NL mode needs answer framing)
|
|
56
|
+
if (lines.length <= MIN_LINES_TO_PROCESS && !originalPrompt) {
|
|
57
57
|
return {
|
|
58
58
|
summary: output,
|
|
59
59
|
full: output,
|