@hasna/terminal 1.0.1 → 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 +101 -1
- package/dist/test-watchlist.js +4 -3
- package/package.json +1 -1
- package/src/cli.tsx +105 -1
- package/src/test-watchlist.ts +4 -3
package/dist/cli.js
CHANGED
|
@@ -545,7 +545,107 @@ else if (args[0] === "project" && args[1] === "init") {
|
|
|
545
545
|
initProject(process.cwd());
|
|
546
546
|
console.log("✓ Initialized .terminal/recipes.json");
|
|
547
547
|
}
|
|
548
|
-
// ──
|
|
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) ──────────────────────────────────────────────────────
|
|
549
649
|
else {
|
|
550
650
|
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
|
|
551
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
|
@@ -518,7 +518,111 @@ else if (args[0] === "project" && args[1] === "init") {
|
|
|
518
518
|
console.log("✓ Initialized .terminal/recipes.json");
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
-
// ──
|
|
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) ──────────────────────────────────────────────────────
|
|
522
626
|
|
|
523
627
|
else {
|
|
524
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 */
|