@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 +111 -3
- package/dist/test-watchlist.js +4 -3
- package/package.json +1 -1
- package/src/cli.tsx +112 -3
- package/src/test-watchlist.ts +4 -3
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
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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.");
|
package/dist/test-watchlist.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
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
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
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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) {
|
package/src/test-watchlist.ts
CHANGED
|
@@ -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
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
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 */
|