@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 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
- cacheSet(nl, text);
200
- return text;
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 summary for medium-large output
475
- if (shouldProcess(clean)) {
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 && processed.tokensSaved > 30) {
478
- recordSaving("compressed", processed.tokensSaved);
478
+ if (processed.aiProcessed) {
479
+ if (processed.tokensSaved > 0)
480
+ recordSaving("compressed", processed.tokensSaved);
479
481
  console.log(processed.summary);
480
- console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
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
- // Small output — pass through clean
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
- const stderr = e.stderr?.toString() ?? "";
494
- const stdout = e.stdout?.toString() ?? "";
495
- const combined = stderr && stdout.includes(stderr.trim()) ? stdout : stdout + stderr;
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
  }
@@ -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 — pass through, no AI needed
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "bin": {
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
- cacheSet(nl, text);
241
- return text;
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 summary for medium-large output
459
- if (shouldProcess(clean)) {
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 && processed.tokensSaved > 30) {
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
- // Small output — pass through clean
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
- const stderr = e.stderr?.toString() ?? "";
475
- const stdout = e.stdout?.toString() ?? "";
476
- const combined = stderr && stdout.includes(stderr.trim()) ? stdout : stdout + stderr;
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
  }
@@ -52,8 +52,8 @@ export async function processOutput(
52
52
  ): Promise<ProcessedOutput> {
53
53
  const lines = output.split("\n");
54
54
 
55
- // Short output — pass through, no AI needed
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,