@hasna/terminal 1.1.1 → 1.2.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
@@ -36,6 +36,13 @@ const IRREVERSIBLE_PATTERNS = [
36
36
  /\brm\s/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i,
37
37
  /\bdelete\s+from\b/i, /\bmv\b.*\/dev\/null/, /\b>\s*[^>]/,
38
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.*>/, /\bperl\s+-[pi]\b/,
39
46
  ];
40
47
  export function isIrreversible(command) {
41
48
  return IRREVERSIBLE_PATTERNS.some((r) => r.test(command));
@@ -156,6 +163,9 @@ RULES:
156
163
  - For text search in code, use grep -rn, NOT nm or objdump (those are for compiled binaries)
157
164
  - On macOS: for memory use vm_stat or top -l 1, for disk use df -h, for processes use ps aux
158
165
  - NEVER invent commands that don't exist. Stick to standard Unix/macOS commands.
166
+ - NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
167
+ - NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
168
+ - Search src/ directory, NOT dist/ or node_modules/ for code queries.
159
169
  cwd: ${process.cwd()}
160
170
  shell: zsh / macOS${projectContext}${restrictionBlock}${contextBlock}`;
161
171
  }
package/dist/cli.js CHANGED
@@ -430,6 +430,13 @@ else if (args.length > 0) {
430
430
  console.error(`blocked: ${blocked}`);
431
431
  process.exit(1);
432
432
  }
433
+ // Safety: warn about irreversible commands (kill, push, rm, etc.)
434
+ if (isIrreversible(command)) {
435
+ console.error(`⚠ IRREVERSIBLE: $ ${command}`);
436
+ console.error(` This command may kill processes, push code, or delete data.`);
437
+ console.error(` Run with terminal exec "${command}" to bypass, or use the TUI for confirmation.`);
438
+ process.exit(1);
439
+ }
433
440
  // Show what we're running
434
441
  console.error(`$ ${command}`);
435
442
  // Step 2: Rewrite for optimization
@@ -450,7 +457,7 @@ else if (args.length > 0) {
450
457
  const rawTokens = estimateTokens(raw);
451
458
  recordUsage(rawTokens);
452
459
  // Test output detection
453
- if (isTestOutput(clean)) {
460
+ if (isTestOutput(clean, actualCmd)) {
454
461
  const result = trackTests(process.cwd(), clean);
455
462
  console.log(formatWatchResult(result));
456
463
  process.exit(0);
@@ -466,7 +473,7 @@ else if (args.length > 0) {
466
473
  }
467
474
  // AI summary for medium-large output
468
475
  if (shouldProcess(clean)) {
469
- const processed = await processOutput(actualCmd, clean);
476
+ const processed = await processOutput(actualCmd, clean, prompt);
470
477
  if (processed.aiProcessed && processed.tokensSaved > 30) {
471
478
  recordSaving("compressed", processed.tokensSaved);
472
479
  console.log(processed.summary);
@@ -39,7 +39,7 @@ export function toLazy(output, command) {
39
39
  count: lines.length,
40
40
  sample,
41
41
  categories: Object.keys(categories).length > 1 ? categories : undefined,
42
- hint: `${lines.length} results. Showing first 20. Use terminal exec --offset=20 --limit=20 to paginate.`,
42
+ hint: `${lines.length} results. Showing first 20. Use a more specific query to narrow results.`,
43
43
  };
44
44
  }
45
45
  /** Get a slice of output */
@@ -5,26 +5,22 @@ import { estimateTokens } from "./parsers/index.js";
5
5
  import { recordSaving } from "./economy.js";
6
6
  const MIN_LINES_TO_PROCESS = 15;
7
7
  const MAX_OUTPUT_FOR_AI = 8000; // chars to send to AI (truncate if longer)
8
- const SUMMARIZE_PROMPT = `You are an output summarizer for a terminal. Given command output, return a CONCISE structured summary.
8
+ const SUMMARIZE_PROMPT = `You are an intelligent terminal assistant. Given a user's original question and the command output, ANSWER THE QUESTION directly.
9
9
 
10
10
  RULES:
11
- - Return ONLY the summary, no explanations
12
- - For test output: show pass count, fail count, and ONLY the failing test names + errors
13
- - For build output: show status (ok/fail), error count, warning count
14
- - For install output: show package count, time, vulnerabilities
15
- - For file listings: show directory count, file count, notable files
16
- - For git output: show branch, status, key info
17
- - For logs: show line count, error count, latest error
18
- - For search results: show match count, top files
19
- - For ANY output: keep errors/failures/warnings, drop verbose/repetitive/progress lines
20
- - Use symbols: ✓ for success, ✗ for failure, ⚠ for warnings
21
- - Maximum 8 lines in your summary
22
- - If there are errors, ALWAYS include them verbatim`;
11
+ - If the user asked a YES/NO question, start with Yes or No, then explain briefly
12
+ - If the user asked "how many", give the number first, then context
13
+ - If the user asked "show me X", show only X, not everything
14
+ - ANSWER the question using the data don't just summarize the raw output
15
+ - Use symbols: for success/yes, for failure/no, for warnings
16
+ - Maximum 8 lines
17
+ - Keep errors/failures verbatim
18
+ - Be direct and concise the user wants an ANSWER, not a data dump`;
23
19
  /**
24
20
  * Process command output through AI summarization.
25
21
  * Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
26
22
  */
27
- export async function processOutput(command, output) {
23
+ export async function processOutput(command, output, originalPrompt) {
28
24
  const lines = output.split("\n");
29
25
  // Short output — pass through, no AI needed
30
26
  if (lines.length <= MIN_LINES_TO_PROCESS) {
@@ -50,7 +46,7 @@ export async function processOutput(command, output) {
50
46
  }
51
47
  try {
52
48
  const provider = getProvider();
53
- const summary = await provider.complete(`Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`, {
49
+ const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`, {
54
50
  system: SUMMARIZE_PROMPT,
55
51
  maxTokens: 300,
56
52
  });
@@ -37,10 +37,13 @@ function extractTests(output) {
37
37
  return tests;
38
38
  }
39
39
  /** Detect if output looks like test runner output */
40
- export function isTestOutput(output) {
41
- // Must have a summary line with counts (not just words "pass"/"fail" in prose)
42
- const summaryLine = /(?:\d+\s+pass|\d+\s+fail|Tests?:\s+\d+|Ran\s+\d+\s+tests?)/i;
43
- const testMarkers = /(?:✓|✗|✔|✕|PASS\s+\S+\.test|FAIL\s+\S+\.test|bun test|jest|vitest|pytest)/;
40
+ export function isTestOutput(output, command) {
41
+ // If the command is explicitly a test command, trust it
42
+ if (command && /\b(bun\s+test|npm\s+test|jest|vitest|pytest|cargo\s+test|go\s+test)\b/.test(command))
43
+ return true;
44
+ // Otherwise require BOTH a summary line AND a test runner marker in the output
45
+ const summaryLine = /(?:\d+\s+pass|\d+\s+fail|Tests?:\s+\d+|Ran\s+\d+\s+tests?)\s*$/im;
46
+ const testMarkers = /(?:✓|✗|✔|✕|PASS\s+\S+\.test|FAIL\s+\S+\.test|bun test v|jest|vitest|pytest)/;
44
47
  return summaryLine.test(output) && testMarkers.test(output);
45
48
  }
46
49
  /** Track test results and return only changes */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "1.1.1",
3
+ "version": "1.2.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
@@ -44,6 +44,13 @@ const IRREVERSIBLE_PATTERNS = [
44
44
  /\brm\s/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i,
45
45
  /\bdelete\s+from\b/i, /\bmv\b.*\/dev\/null/, /\b>\s*[^>]/,
46
46
  /\bdd\b/, /\bmkfs\b/, /\bformat\b/, /\bshred\b/,
47
+ // Process/service killing
48
+ /\bkill\b/, /\bkillall\b/, /\bpkill\b/,
49
+ // Git push/force operations
50
+ /\bgit\s+push\b/, /\bgit\s+reset\s+--hard\b/, /\bgit\s+force\b/,
51
+ // Code modification / package installation (security risk)
52
+ /\bnpx\s+\S+/, /\bnpm\s+install\b/, /\bbun\s+add\b/, /\bpip\s+install\b/,
53
+ /\bcodemod\b/, /\bsed\s+-i\b/, /\bawk\s.*>/, /\bperl\s+-[pi]\b/,
47
54
  ];
48
55
 
49
56
  export function isIrreversible(command: string): boolean {
@@ -191,6 +198,9 @@ RULES:
191
198
  - For text search in code, use grep -rn, NOT nm or objdump (those are for compiled binaries)
192
199
  - On macOS: for memory use vm_stat or top -l 1, for disk use df -h, for processes use ps aux
193
200
  - NEVER invent commands that don't exist. Stick to standard Unix/macOS commands.
201
+ - NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
202
+ - NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
203
+ - Search src/ directory, NOT dist/ or node_modules/ for code queries.
194
204
  cwd: ${process.cwd()}
195
205
  shell: zsh / macOS${projectContext}${restrictionBlock}${contextBlock}`;
196
206
  }
package/src/cli.tsx CHANGED
@@ -410,6 +410,14 @@ else if (args.length > 0) {
410
410
  const blocked = checkPermissions(command, perms);
411
411
  if (blocked) { console.error(`blocked: ${blocked}`); process.exit(1); }
412
412
 
413
+ // Safety: warn about irreversible commands (kill, push, rm, etc.)
414
+ if (isIrreversible(command)) {
415
+ console.error(`⚠ IRREVERSIBLE: $ ${command}`);
416
+ console.error(` This command may kill processes, push code, or delete data.`);
417
+ console.error(` Run with terminal exec "${command}" to bypass, or use the TUI for confirmation.`);
418
+ process.exit(1);
419
+ }
420
+
413
421
  // Show what we're running
414
422
  console.error(`$ ${command}`);
415
423
 
@@ -432,7 +440,7 @@ else if (args.length > 0) {
432
440
  recordUsage(rawTokens);
433
441
 
434
442
  // Test output detection
435
- if (isTestOutput(clean)) {
443
+ if (isTestOutput(clean, actualCmd)) {
436
444
  const result = trackTests(process.cwd(), clean);
437
445
  console.log(formatWatchResult(result));
438
446
  process.exit(0);
@@ -449,7 +457,7 @@ else if (args.length > 0) {
449
457
 
450
458
  // AI summary for medium-large output
451
459
  if (shouldProcess(clean)) {
452
- const processed = await processOutput(actualCmd, clean);
460
+ const processed = await processOutput(actualCmd, clean, prompt);
453
461
  if (processed.aiProcessed && processed.tokensSaved > 30) {
454
462
  recordSaving("compressed", processed.tokensSaved);
455
463
  console.log(processed.summary);
@@ -54,7 +54,7 @@ export function toLazy(output: string, command: string): LazyResult {
54
54
  count: lines.length,
55
55
  sample,
56
56
  categories: Object.keys(categories).length > 1 ? categories : undefined,
57
- hint: `${lines.length} results. Showing first 20. Use terminal exec --offset=20 --limit=20 to paginate.`,
57
+ hint: `${lines.length} results. Showing first 20. Use a more specific query to narrow results.`,
58
58
  };
59
59
  }
60
60
 
@@ -29,21 +29,17 @@ export interface ProcessedOutput {
29
29
  const MIN_LINES_TO_PROCESS = 15;
30
30
  const MAX_OUTPUT_FOR_AI = 8000; // chars to send to AI (truncate if longer)
31
31
 
32
- const SUMMARIZE_PROMPT = `You are an output summarizer for a terminal. Given command output, return a CONCISE structured summary.
32
+ const SUMMARIZE_PROMPT = `You are an intelligent terminal assistant. Given a user's original question and the command output, ANSWER THE QUESTION directly.
33
33
 
34
34
  RULES:
35
- - Return ONLY the summary, no explanations
36
- - For test output: show pass count, fail count, and ONLY the failing test names + errors
37
- - For build output: show status (ok/fail), error count, warning count
38
- - For install output: show package count, time, vulnerabilities
39
- - For file listings: show directory count, file count, notable files
40
- - For git output: show branch, status, key info
41
- - For logs: show line count, error count, latest error
42
- - For search results: show match count, top files
43
- - For ANY output: keep errors/failures/warnings, drop verbose/repetitive/progress lines
44
- - Use symbols: ✓ for success, ✗ for failure, ⚠ for warnings
45
- - Maximum 8 lines in your summary
46
- - If there are errors, ALWAYS include them verbatim`;
35
+ - If the user asked a YES/NO question, start with Yes or No, then explain briefly
36
+ - If the user asked "how many", give the number first, then context
37
+ - If the user asked "show me X", show only X, not everything
38
+ - ANSWER the question using the data don't just summarize the raw output
39
+ - Use symbols: for success/yes, for failure/no, for warnings
40
+ - Maximum 8 lines
41
+ - Keep errors/failures verbatim
42
+ - Be direct and concise the user wants an ANSWER, not a data dump`;
47
43
 
48
44
  /**
49
45
  * Process command output through AI summarization.
@@ -52,6 +48,7 @@ RULES:
52
48
  export async function processOutput(
53
49
  command: string,
54
50
  output: string,
51
+ originalPrompt?: string,
55
52
  ): Promise<ProcessedOutput> {
56
53
  const lines = output.split("\n");
57
54
 
@@ -82,7 +79,7 @@ export async function processOutput(
82
79
  try {
83
80
  const provider = getProvider();
84
81
  const summary = await provider.complete(
85
- `Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`,
82
+ `${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`,
86
83
  {
87
84
  system: SUMMARIZE_PROMPT,
88
85
  maxTokens: 300,
@@ -63,10 +63,12 @@ function extractTests(output: string): TestStatus[] {
63
63
  }
64
64
 
65
65
  /** Detect if output looks like test runner output */
66
- export function isTestOutput(output: string): boolean {
67
- // Must have a summary line with counts (not just words "pass"/"fail" in prose)
68
- const summaryLine = /(?:\d+\s+pass|\d+\s+fail|Tests?:\s+\d+|Ran\s+\d+\s+tests?)/i;
69
- const testMarkers = /(?:✓|✗|✔|✕|PASS\s+\S+\.test|FAIL\s+\S+\.test|bun test|jest|vitest|pytest)/;
66
+ export function isTestOutput(output: string, command?: string): boolean {
67
+ // If the command is explicitly a test command, trust it
68
+ if (command && /\b(bun\s+test|npm\s+test|jest|vitest|pytest|cargo\s+test|go\s+test)\b/.test(command)) return true;
69
+ // Otherwise require BOTH a summary line AND a test runner marker in the output
70
+ const summaryLine = /(?:\d+\s+pass|\d+\s+fail|Tests?:\s+\d+|Ran\s+\d+\s+tests?)\s*$/im;
71
+ const testMarkers = /(?:✓|✗|✔|✕|PASS\s+\S+\.test|FAIL\s+\S+\.test|bun test v|jest|vitest|pytest)/;
70
72
  return summaryLine.test(output) && testMarkers.test(output);
71
73
  }
72
74