@hasna/terminal 1.0.0 → 1.1.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/cli.js CHANGED
@@ -4,7 +4,7 @@ import { render } from "ink";
4
4
  const args = process.argv.slice(2);
5
5
  // ── Help / Version ───────────────────────────────────────────────────────────
6
6
  if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
7
- console.log(`open-terminal v0.6.1 — Smart terminal for AI agents and humans
7
+ console.log(`open-terminal v1.0.0 — Smart terminal for AI agents and humans
8
8
 
9
9
  USAGE:
10
10
  terminal Launch interactive NL terminal (TUI)
@@ -48,7 +48,15 @@ ENVIRONMENT:
48
48
  process.exit(0);
49
49
  }
50
50
  if (args[0] === "--version" || args[0] === "-v") {
51
- console.log("0.6.1");
51
+ const { readFileSync } = await import("fs");
52
+ const { join, dirname } = await import("path");
53
+ try {
54
+ const pkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf8"));
55
+ console.log(pkg.version);
56
+ }
57
+ catch {
58
+ console.log("1.0.0");
59
+ }
52
60
  process.exit(0);
53
61
  }
54
62
  // ── Exec command — smart execution for agents ────────────────────────────────
@@ -537,7 +545,107 @@ else if (args[0] === "project" && args[1] === "init") {
537
545
  initProject(process.cwd());
538
546
  console.log("✓ Initialized .terminal/recipes.json");
539
547
  }
540
- // ── TUI mode (default) ──────────────────────────────────────────────────────
548
+ // ── NL mode: terminal "natural language prompt" ─────────────────────────────
549
+ else if (args.length > 0) {
550
+ // Everything that doesn't match a subcommand is treated as natural language
551
+ const prompt = args.join(" ");
552
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
553
+ console.error("terminal: No API key found.");
554
+ console.error("Set one of:");
555
+ console.error(" export CEREBRAS_API_KEY=your_key (free, open-source)");
556
+ console.error(" export ANTHROPIC_API_KEY=your_key (Claude)");
557
+ process.exit(1);
558
+ }
559
+ const { translateToCommand, checkPermissions, isIrreversible } = await import("./ai.js");
560
+ const { execSync } = await import("child_process");
561
+ const { compress, stripAnsi } = await import("./compression.js");
562
+ const { stripNoise } = await import("./noise-filter.js");
563
+ const { processOutput, shouldProcess } = await import("./output-processor.js");
564
+ const { rewriteCommand } = await import("./command-rewriter.js");
565
+ const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
566
+ const { parseOutput, estimateTokens } = await import("./parsers/index.js");
567
+ const { recordSaving, recordUsage } = await import("./economy.js");
568
+ const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
569
+ const { detectLoop } = await import("./loop-detector.js");
570
+ const { loadConfig } = await import("./history.js");
571
+ const config = loadConfig();
572
+ const perms = config.permissions;
573
+ // Step 1: AI translates NL → shell command
574
+ let command;
575
+ try {
576
+ command = await translateToCommand(prompt, perms, []);
577
+ }
578
+ catch (e) {
579
+ console.error(e.message);
580
+ process.exit(1);
581
+ }
582
+ // Check permissions
583
+ const blocked = checkPermissions(command, perms);
584
+ if (blocked) {
585
+ console.error(`blocked: ${blocked}`);
586
+ process.exit(1);
587
+ }
588
+ // Show what we're running
589
+ console.error(`$ ${command}`);
590
+ // Step 2: Rewrite for optimization
591
+ const rw = rewriteCommand(command);
592
+ const actualCmd = rw.changed ? rw.rewritten : command;
593
+ if (rw.changed)
594
+ console.error(`[open-terminal] optimized: ${actualCmd}`);
595
+ // Loop detection
596
+ const loop = detectLoop(actualCmd);
597
+ if (loop.detected)
598
+ console.error(`[open-terminal] loop #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : ""}`);
599
+ // Step 3: Execute
600
+ try {
601
+ const start = Date.now();
602
+ const raw = execSync(actualCmd, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
603
+ const duration = Date.now() - start;
604
+ const clean = stripNoise(stripAnsi(raw)).cleaned;
605
+ const rawTokens = estimateTokens(raw);
606
+ recordUsage(rawTokens);
607
+ // Test output detection
608
+ if (isTestOutput(clean)) {
609
+ const result = trackTests(process.cwd(), clean);
610
+ console.log(formatWatchResult(result));
611
+ process.exit(0);
612
+ }
613
+ // Lazy mode
614
+ if (shouldBeLazy(clean, actualCmd)) {
615
+ const lazy = toLazy(clean, actualCmd);
616
+ const saved = rawTokens - estimateTokens(JSON.stringify(lazy));
617
+ if (saved > 0)
618
+ recordSaving("compressed", saved);
619
+ console.log(JSON.stringify(lazy, null, 2));
620
+ process.exit(0);
621
+ }
622
+ // AI summary for medium-large output
623
+ if (shouldProcess(clean)) {
624
+ const processed = await processOutput(actualCmd, clean);
625
+ if (processed.aiProcessed && processed.tokensSaved > 30) {
626
+ recordSaving("compressed", processed.tokensSaved);
627
+ console.log(processed.summary);
628
+ console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
629
+ process.exit(0);
630
+ }
631
+ }
632
+ // Small output — pass through clean
633
+ console.log(clean);
634
+ const saved = rawTokens - estimateTokens(clean);
635
+ if (saved > 10) {
636
+ recordSaving("compressed", saved);
637
+ console.error(`[open-terminal] saved ${saved} tokens`);
638
+ }
639
+ }
640
+ catch (e) {
641
+ const stderr = e.stderr?.toString() ?? "";
642
+ const stdout = e.stdout?.toString() ?? "";
643
+ const combined = stderr && stdout.includes(stderr.trim()) ? stdout : stdout + stderr;
644
+ console.log(stripNoise(stripAnsi(combined)).cleaned);
645
+ process.exit(e.status ?? 1);
646
+ }
647
+ }
648
+ // ── TUI mode (no args) ──────────────────────────────────────────────────────
541
649
  else {
542
650
  if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
543
651
  console.error("terminal: No API key found.");
@@ -38,9 +38,10 @@ function extractTests(output) {
38
38
  }
39
39
  /** Detect if output looks like test runner output */
40
40
  export function isTestOutput(output) {
41
- const markers = /(?:Tests?:|PASS|FAIL|✓|✗|passed|failed|\d+\s+pass|\d+\s+fail)/i;
42
- const lines = output.split("\n");
43
- return lines.filter(l => markers.test(l)).length >= 2;
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)/;
44
+ return summaryLine.test(output) && testMarkers.test(output);
44
45
  }
45
46
  /** Track test results and return only changes */
46
47
  export function trackTests(cwd, output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "1.0.0",
3
+ "version": "1.1.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/cli.tsx CHANGED
@@ -7,7 +7,7 @@ const args = process.argv.slice(2);
7
7
  // ── Help / Version ───────────────────────────────────────────────────────────
8
8
 
9
9
  if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
10
- console.log(`open-terminal v0.6.1 — Smart terminal for AI agents and humans
10
+ console.log(`open-terminal v1.0.0 — Smart terminal for AI agents and humans
11
11
 
12
12
  USAGE:
13
13
  terminal Launch interactive NL terminal (TUI)
@@ -52,7 +52,12 @@ ENVIRONMENT:
52
52
  }
53
53
 
54
54
  if (args[0] === "--version" || args[0] === "-v") {
55
- console.log("0.6.1");
55
+ const { readFileSync } = await import("fs");
56
+ const { join, dirname } = await import("path");
57
+ try {
58
+ const pkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf8"));
59
+ console.log(pkg.version);
60
+ } catch { console.log("1.0.0"); }
56
61
  process.exit(0);
57
62
  }
58
63
 
@@ -513,7 +518,111 @@ else if (args[0] === "project" && args[1] === "init") {
513
518
  console.log("✓ Initialized .terminal/recipes.json");
514
519
  }
515
520
 
516
- // ── TUI mode (default) ──────────────────────────────────────────────────────
521
+ // ── NL mode: terminal "natural language prompt" ─────────────────────────────
522
+
523
+ else if (args.length > 0) {
524
+ // Everything that doesn't match a subcommand is treated as natural language
525
+ const prompt = args.join(" ");
526
+
527
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
528
+ console.error("terminal: No API key found.");
529
+ console.error("Set one of:");
530
+ console.error(" export CEREBRAS_API_KEY=your_key (free, open-source)");
531
+ console.error(" export ANTHROPIC_API_KEY=your_key (Claude)");
532
+ process.exit(1);
533
+ }
534
+
535
+ const { translateToCommand, checkPermissions, isIrreversible } = await import("./ai.js");
536
+ const { execSync } = await import("child_process");
537
+ const { compress, stripAnsi } = await import("./compression.js");
538
+ const { stripNoise } = await import("./noise-filter.js");
539
+ const { processOutput, shouldProcess } = await import("./output-processor.js");
540
+ const { rewriteCommand } = await import("./command-rewriter.js");
541
+ const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
542
+ const { parseOutput, estimateTokens } = await import("./parsers/index.js");
543
+ const { recordSaving, recordUsage } = await import("./economy.js");
544
+ const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
545
+ const { detectLoop } = await import("./loop-detector.js");
546
+ const { loadConfig } = await import("./history.js");
547
+
548
+ const config = loadConfig();
549
+ const perms = config.permissions;
550
+
551
+ // Step 1: AI translates NL → shell command
552
+ let command: string;
553
+ try {
554
+ command = await translateToCommand(prompt, perms, []);
555
+ } catch (e: any) {
556
+ console.error(e.message);
557
+ process.exit(1);
558
+ }
559
+
560
+ // Check permissions
561
+ const blocked = checkPermissions(command, perms);
562
+ if (blocked) { console.error(`blocked: ${blocked}`); process.exit(1); }
563
+
564
+ // Show what we're running
565
+ console.error(`$ ${command}`);
566
+
567
+ // Step 2: Rewrite for optimization
568
+ const rw = rewriteCommand(command);
569
+ const actualCmd = rw.changed ? rw.rewritten : command;
570
+ if (rw.changed) console.error(`[open-terminal] optimized: ${actualCmd}`);
571
+
572
+ // Loop detection
573
+ const loop = detectLoop(actualCmd);
574
+ if (loop.detected) console.error(`[open-terminal] loop #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : ""}`);
575
+
576
+ // Step 3: Execute
577
+ try {
578
+ const start = Date.now();
579
+ const raw = execSync(actualCmd, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
580
+ const duration = Date.now() - start;
581
+ const clean = stripNoise(stripAnsi(raw)).cleaned;
582
+ const rawTokens = estimateTokens(raw);
583
+ recordUsage(rawTokens);
584
+
585
+ // Test output detection
586
+ if (isTestOutput(clean)) {
587
+ const result = trackTests(process.cwd(), clean);
588
+ console.log(formatWatchResult(result));
589
+ process.exit(0);
590
+ }
591
+
592
+ // Lazy mode
593
+ if (shouldBeLazy(clean, actualCmd)) {
594
+ const lazy = toLazy(clean, actualCmd);
595
+ const saved = rawTokens - estimateTokens(JSON.stringify(lazy));
596
+ if (saved > 0) recordSaving("compressed", saved);
597
+ console.log(JSON.stringify(lazy, null, 2));
598
+ process.exit(0);
599
+ }
600
+
601
+ // AI summary for medium-large output
602
+ if (shouldProcess(clean)) {
603
+ const processed = await processOutput(actualCmd, clean);
604
+ if (processed.aiProcessed && processed.tokensSaved > 30) {
605
+ recordSaving("compressed", processed.tokensSaved);
606
+ console.log(processed.summary);
607
+ console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
608
+ process.exit(0);
609
+ }
610
+ }
611
+
612
+ // Small output — pass through clean
613
+ console.log(clean);
614
+ const saved = rawTokens - estimateTokens(clean);
615
+ if (saved > 10) { recordSaving("compressed", saved); console.error(`[open-terminal] saved ${saved} tokens`); }
616
+ } catch (e: any) {
617
+ const stderr = e.stderr?.toString() ?? "";
618
+ const stdout = e.stdout?.toString() ?? "";
619
+ const combined = stderr && stdout.includes(stderr.trim()) ? stdout : stdout + stderr;
620
+ console.log(stripNoise(stripAnsi(combined)).cleaned);
621
+ process.exit(e.status ?? 1);
622
+ }
623
+ }
624
+
625
+ // ── TUI mode (no args) ──────────────────────────────────────────────────────
517
626
 
518
627
  else {
519
628
  if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
@@ -64,9 +64,10 @@ function extractTests(output: string): TestStatus[] {
64
64
 
65
65
  /** Detect if output looks like test runner output */
66
66
  export function isTestOutput(output: string): boolean {
67
- const markers = /(?:Tests?:|PASS|FAIL|✓|✗|passed|failed|\d+\s+pass|\d+\s+fail)/i;
68
- const lines = output.split("\n");
69
- return lines.filter(l => markers.test(l)).length >= 2;
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)/;
70
+ return summaryLine.test(output) && testMarkers.test(output);
70
71
  }
71
72
 
72
73
  /** Track test results and return only changes */