@hasna/terminal 0.7.3 → 0.7.5

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
@@ -83,6 +83,12 @@ if (args[0] === "exec") {
83
83
  const { parseOutput, estimateTokens } = await import("./parsers/index.js");
84
84
  const { recordSaving, recordUsage } = await import("./economy.js");
85
85
  const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
86
+ const { detectLoop } = await import("./loop-detector.js");
87
+ // Loop detection — suggest narrowing if running full test suite repeatedly
88
+ const loop = detectLoop(command);
89
+ if (loop.detected) {
90
+ console.error(`[open-terminal] loop detected: test run #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : " — consider narrowing to specific test file"}`);
91
+ }
86
92
  // Rewrite command if possible
87
93
  const rw = rewriteCommand(command);
88
94
  const actualCmd = rw.changed ? rw.rewritten : command;
@@ -4,7 +4,11 @@ import { dirname } from "path";
4
4
  const LAZY_THRESHOLD = 200; // lines before switching to lazy mode (was 100, too aggressive)
5
5
  // Commands where the user explicitly wants full output — never lazify
6
6
  const PASSTHROUGH_COMMANDS = [
7
+ // File reading — user explicitly wants content
7
8
  /\bcat\b/, /\bhead\b/, /\btail\b/, /\bbat\b/, /\bless\b/, /\bmore\b/,
9
+ // Git review commands — truncating diffs/patches loses semantic meaning
10
+ /\bgit\s+diff\b/, /\bgit\s+show\b/, /\bgit\s+log\s+-p\b/, /\bgit\s+log\s+--patch\b/,
11
+ // Summary/report commands — summarizing a summary is pointless
8
12
  /\bsummary\b/i, /\bstatus\b/i, /\breport\b/i, /\bstats\b/i,
9
13
  /\bweek\b/i, /\btoday\b/i, /\bdashboard\b/i,
10
14
  ];
@@ -0,0 +1,75 @@
1
+ // Edit-test loop detector — detects repetitive test→edit→test patterns
2
+ // and suggests narrowing to specific test files
3
+ const history = [];
4
+ const MAX_HISTORY = 20;
5
+ // Detect test commands
6
+ const TEST_PATTERNS = [
7
+ /\bbun\s+test\b/, /\bnpm\s+test\b/, /\bnpx\s+jest\b/, /\bnpx\s+vitest\b/,
8
+ /\bpnpm\s+test\b/, /\byarn\s+test\b/, /\bpytest\b/, /\bgo\s+test\b/,
9
+ /\bcargo\s+test\b/, /\brspec\b/, /\bphpunit\b/, /\bmocha\b/,
10
+ ];
11
+ function isTestCommand(cmd) {
12
+ return TEST_PATTERNS.some(p => p.test(cmd));
13
+ }
14
+ function isFullSuiteCommand(cmd) {
15
+ // Full suite = test command without specific file/pattern
16
+ if (!isTestCommand(cmd))
17
+ return false;
18
+ // If it has a specific file or --grep, it's already narrowed
19
+ if (/\.(test|spec)\.(ts|tsx|js|jsx|py|rs|go)/.test(cmd))
20
+ return false;
21
+ if (/--grep|--filter|-t\s/.test(cmd))
22
+ return false;
23
+ return true;
24
+ }
25
+ /** Record a command execution and detect loops */
26
+ export function detectLoop(command) {
27
+ history.push({ command, timestamp: Date.now() });
28
+ if (history.length > MAX_HISTORY)
29
+ history.shift();
30
+ if (!isTestCommand(command)) {
31
+ return { detected: false, iteration: 0, testCommand: command };
32
+ }
33
+ // Count consecutive test runs (allowing non-test commands between them)
34
+ let testCount = 0;
35
+ for (let i = history.length - 1; i >= 0; i--) {
36
+ if (isTestCommand(history[i].command))
37
+ testCount++;
38
+ // If we hit a non-test, non-edit command, stop counting
39
+ // (edits are invisible to us since we only see exec'd commands)
40
+ }
41
+ if (testCount < 3 || !isFullSuiteCommand(command)) {
42
+ return { detected: false, iteration: testCount, testCommand: command };
43
+ }
44
+ // Detected loop — suggest narrowing
45
+ // Try to find a recently-mentioned test file in recent commands
46
+ let suggestedNarrow;
47
+ // Look for file paths in recent history that could be test targets
48
+ for (let i = history.length - 2; i >= Math.max(0, history.length - 10); i--) {
49
+ const cmd = history[i].command;
50
+ // Look for edited/touched files
51
+ const fileMatch = cmd.match(/(\S+\.(ts|tsx|js|jsx|py|rs|go))\b/);
52
+ if (fileMatch && !isTestCommand(cmd)) {
53
+ const file = fileMatch[1];
54
+ // Suggest corresponding test file
55
+ const testFile = file.replace(/\.(ts|tsx|js|jsx)$/, ".test.$1");
56
+ suggestedNarrow = command.replace(/\b(test)\b/, `test ${testFile}`);
57
+ break;
58
+ }
59
+ }
60
+ // Fallback: suggest adding --grep or specific file
61
+ if (!suggestedNarrow) {
62
+ suggestedNarrow = undefined; // Can't determine which file
63
+ }
64
+ return {
65
+ detected: true,
66
+ iteration: testCount,
67
+ testCommand: command,
68
+ suggestedNarrow,
69
+ reason: `Full test suite run ${testCount} times. Consider narrowing to specific test file.`,
70
+ };
71
+ }
72
+ /** Reset loop detection (e.g., on session start) */
73
+ export function resetLoopDetector() {
74
+ history.length = 0;
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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
@@ -87,6 +87,13 @@ if (args[0] === "exec") {
87
87
  const { parseOutput, estimateTokens } = await import("./parsers/index.js");
88
88
  const { recordSaving, recordUsage } = await import("./economy.js");
89
89
  const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
90
+ const { detectLoop } = await import("./loop-detector.js");
91
+
92
+ // Loop detection — suggest narrowing if running full test suite repeatedly
93
+ const loop = detectLoop(command);
94
+ if (loop.detected) {
95
+ console.error(`[open-terminal] loop detected: test run #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : " — consider narrowing to specific test file"}`);
96
+ }
90
97
 
91
98
  // Rewrite command if possible
92
99
  const rw = rewriteCommand(command);
@@ -15,7 +15,11 @@ export interface LazyResult {
15
15
 
16
16
  // Commands where the user explicitly wants full output — never lazify
17
17
  const PASSTHROUGH_COMMANDS = [
18
+ // File reading — user explicitly wants content
18
19
  /\bcat\b/, /\bhead\b/, /\btail\b/, /\bbat\b/, /\bless\b/, /\bmore\b/,
20
+ // Git review commands — truncating diffs/patches loses semantic meaning
21
+ /\bgit\s+diff\b/, /\bgit\s+show\b/, /\bgit\s+log\s+-p\b/, /\bgit\s+log\s+--patch\b/,
22
+ // Summary/report commands — summarizing a summary is pointless
19
23
  /\bsummary\b/i, /\bstatus\b/i, /\breport\b/i, /\bstats\b/i,
20
24
  /\bweek\b/i, /\btoday\b/i, /\bdashboard\b/i,
21
25
  ];
@@ -0,0 +1,96 @@
1
+ // Edit-test loop detector — detects repetitive test→edit→test patterns
2
+ // and suggests narrowing to specific test files
3
+
4
+ interface CommandRecord {
5
+ command: string;
6
+ timestamp: number;
7
+ }
8
+
9
+ const history: CommandRecord[] = [];
10
+ const MAX_HISTORY = 20;
11
+
12
+ // Detect test commands
13
+ const TEST_PATTERNS = [
14
+ /\bbun\s+test\b/, /\bnpm\s+test\b/, /\bnpx\s+jest\b/, /\bnpx\s+vitest\b/,
15
+ /\bpnpm\s+test\b/, /\byarn\s+test\b/, /\bpytest\b/, /\bgo\s+test\b/,
16
+ /\bcargo\s+test\b/, /\brspec\b/, /\bphpunit\b/, /\bmocha\b/,
17
+ ];
18
+
19
+ function isTestCommand(cmd: string): boolean {
20
+ return TEST_PATTERNS.some(p => p.test(cmd));
21
+ }
22
+
23
+ function isFullSuiteCommand(cmd: string): boolean {
24
+ // Full suite = test command without specific file/pattern
25
+ if (!isTestCommand(cmd)) return false;
26
+ // If it has a specific file or --grep, it's already narrowed
27
+ if (/\.(test|spec)\.(ts|tsx|js|jsx|py|rs|go)/.test(cmd)) return false;
28
+ if (/--grep|--filter|-t\s/.test(cmd)) return false;
29
+ return true;
30
+ }
31
+
32
+ export interface LoopContext {
33
+ detected: boolean;
34
+ iteration: number;
35
+ testCommand: string;
36
+ suggestedNarrow?: string;
37
+ reason?: string;
38
+ }
39
+
40
+ /** Record a command execution and detect loops */
41
+ export function detectLoop(command: string): LoopContext {
42
+ history.push({ command, timestamp: Date.now() });
43
+ if (history.length > MAX_HISTORY) history.shift();
44
+
45
+ if (!isTestCommand(command)) {
46
+ return { detected: false, iteration: 0, testCommand: command };
47
+ }
48
+
49
+ // Count consecutive test runs (allowing non-test commands between them)
50
+ let testCount = 0;
51
+ for (let i = history.length - 1; i >= 0; i--) {
52
+ if (isTestCommand(history[i].command)) testCount++;
53
+ // If we hit a non-test, non-edit command, stop counting
54
+ // (edits are invisible to us since we only see exec'd commands)
55
+ }
56
+
57
+ if (testCount < 3 || !isFullSuiteCommand(command)) {
58
+ return { detected: false, iteration: testCount, testCommand: command };
59
+ }
60
+
61
+ // Detected loop — suggest narrowing
62
+ // Try to find a recently-mentioned test file in recent commands
63
+ let suggestedNarrow: string | undefined;
64
+
65
+ // Look for file paths in recent history that could be test targets
66
+ for (let i = history.length - 2; i >= Math.max(0, history.length - 10); i--) {
67
+ const cmd = history[i].command;
68
+ // Look for edited/touched files
69
+ const fileMatch = cmd.match(/(\S+\.(ts|tsx|js|jsx|py|rs|go))\b/);
70
+ if (fileMatch && !isTestCommand(cmd)) {
71
+ const file = fileMatch[1];
72
+ // Suggest corresponding test file
73
+ const testFile = file.replace(/\.(ts|tsx|js|jsx)$/, ".test.$1");
74
+ suggestedNarrow = command.replace(/\b(test)\b/, `test ${testFile}`);
75
+ break;
76
+ }
77
+ }
78
+
79
+ // Fallback: suggest adding --grep or specific file
80
+ if (!suggestedNarrow) {
81
+ suggestedNarrow = undefined; // Can't determine which file
82
+ }
83
+
84
+ return {
85
+ detected: true,
86
+ iteration: testCount,
87
+ testCommand: command,
88
+ suggestedNarrow,
89
+ reason: `Full test suite run ${testCount} times. Consider narrowing to specific test file.`,
90
+ };
91
+ }
92
+
93
+ /** Reset loop detection (e.g., on session start) */
94
+ export function resetLoopDetector(): void {
95
+ history.length = 0;
96
+ }