@hasna/terminal 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.js 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));
@@ -109,12 +111,12 @@ function detectProjectContext() {
109
111
  // Top-level dirs
110
112
  const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 2000 }).trim();
111
113
  parts.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
112
- // src/ structure (2 levels deep, most important for path resolution)
114
+ // src/ structure include FILES so AI knows exact filenames + extensions
113
115
  for (const srcDir of ["src", "lib", "app"]) {
114
116
  if (existsSync(join(cwd, srcDir))) {
115
- const tree = execSync(`find ${srcDir} -maxdepth 2 -type d -not -path '*/node_modules/*' 2>/dev/null | head -30`, { cwd, encoding: "utf8", timeout: 2000 }).trim();
117
+ const tree = execSync(`find ${srcDir} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | sort | head -60`, { cwd, encoding: "utf8", timeout: 2000 }).trim();
116
118
  if (tree)
117
- parts.push(`Directories in ${srcDir}/:\n${tree}`);
119
+ parts.push(`Files in ${srcDir}/:\n${tree}`);
118
120
  break;
119
121
  }
120
122
  }
@@ -166,6 +168,8 @@ RULES:
166
168
  - NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
167
169
  - NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
168
170
  - Search src/ directory, NOT dist/ or node_modules/ for code queries.
171
+ - For compound questions ("how many X and are they Y"), prefer ONE command that captures all info. Do NOT chain with &&.
172
+ - Use exact file paths from the project context below. Do NOT guess paths.
169
173
  cwd: ${process.cwd()}
170
174
  shell: zsh / macOS${projectContext}${restrictionBlock}${contextBlock}`;
171
175
  }
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.1",
3
+ "version": "1.3.0",
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 {
@@ -138,14 +140,14 @@ function detectProjectContext(): string {
138
140
  const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 2000 }).trim();
139
141
  parts.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
140
142
 
141
- // src/ structure (2 levels deep, most important for path resolution)
143
+ // src/ structure include FILES so AI knows exact filenames + extensions
142
144
  for (const srcDir of ["src", "lib", "app"]) {
143
145
  if (existsSync(join(cwd, srcDir))) {
144
146
  const tree = execSync(
145
- `find ${srcDir} -maxdepth 2 -type d -not -path '*/node_modules/*' 2>/dev/null | head -30`,
147
+ `find ${srcDir} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | sort | head -60`,
146
148
  { cwd, encoding: "utf8", timeout: 2000 }
147
149
  ).trim();
148
- if (tree) parts.push(`Directories in ${srcDir}/:\n${tree}`);
150
+ if (tree) parts.push(`Files in ${srcDir}/:\n${tree}`);
149
151
  break;
150
152
  }
151
153
  }
@@ -201,6 +203,8 @@ RULES:
201
203
  - NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
202
204
  - NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
203
205
  - Search src/ directory, NOT dist/ or node_modules/ for code queries.
206
+ - For compound questions ("how many X and are they Y"), prefer ONE command that captures all info. Do NOT chain with &&.
207
+ - Use exact file paths from the project context below. Do NOT guess paths.
204
208
  cwd: ${process.cwd()}
205
209
  shell: zsh / macOS${projectContext}${restrictionBlock}${contextBlock}`;
206
210
  }
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,