@hasna/terminal 1.1.0 → 1.1.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 +17 -0
- package/dist/cli.js +20 -175
- package/package.json +1 -1
- package/src/ai.ts +20 -0
- package/src/cli.tsx +20 -171
package/dist/ai.js
CHANGED
|
@@ -96,6 +96,23 @@ function detectProjectContext() {
|
|
|
96
96
|
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
|
|
97
97
|
parts.push("Project: Java/Gradle. Use gradle commands.");
|
|
98
98
|
}
|
|
99
|
+
// Directory structure — so AI knows actual paths (not guessed ones)
|
|
100
|
+
try {
|
|
101
|
+
const { execSync } = require("child_process");
|
|
102
|
+
// Top-level dirs
|
|
103
|
+
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
104
|
+
parts.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
105
|
+
// src/ structure (2 levels deep, most important for path resolution)
|
|
106
|
+
for (const srcDir of ["src", "lib", "app"]) {
|
|
107
|
+
if (existsSync(join(cwd, srcDir))) {
|
|
108
|
+
const tree = execSync(`find ${srcDir} -maxdepth 2 -type d -not -path '*/node_modules/*' 2>/dev/null | head -30`, { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
109
|
+
if (tree)
|
|
110
|
+
parts.push(`Directories in ${srcDir}/:\n${tree}`);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { /* timeout or no exec — skip */ }
|
|
99
116
|
return parts.length > 0 ? `\n\nPROJECT CONTEXT:\n${parts.join("\n")}` : "";
|
|
100
117
|
}
|
|
101
118
|
// ── system prompt ─────────────────────────────────────────────────────────────
|
package/dist/cli.js
CHANGED
|
@@ -4,32 +4,31 @@ 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 — Natural language shell for AI agents and humans
|
|
8
8
|
|
|
9
9
|
USAGE:
|
|
10
|
+
terminal "your request" NL → AI picks command → runs → smart output
|
|
10
11
|
terminal Launch interactive NL terminal (TUI)
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
EXAMPLES:
|
|
14
|
+
terminal "list all typescript files"
|
|
15
|
+
terminal "run tests"
|
|
16
|
+
terminal "what changed in git"
|
|
17
|
+
terminal "show me the auth functions"
|
|
18
|
+
terminal "kill port 3000"
|
|
19
|
+
terminal "how many lines of code"
|
|
12
20
|
|
|
13
21
|
SUBCOMMANDS:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
recipe add
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
collection list List collections
|
|
25
|
-
project init Initialize project-scoped recipes
|
|
26
|
-
repo Show git repo state (branch + status + log)
|
|
27
|
-
symbols <file> Show file outline (functions, classes, exports)
|
|
28
|
-
stats Show token economy dashboard
|
|
29
|
-
sessions List recent terminal sessions
|
|
30
|
-
sessions stats Show session analytics
|
|
31
|
-
sessions <id> Show session details
|
|
32
|
-
snapshot Capture terminal state as JSON
|
|
22
|
+
repo Git repo state (branch + status + log)
|
|
23
|
+
symbols <file> File outline (functions, classes, exports)
|
|
24
|
+
overview Project overview (deps, scripts, structure)
|
|
25
|
+
stats Token economy dashboard
|
|
26
|
+
sessions [stats|<id>] Session history and analytics
|
|
27
|
+
recipe add|list|run|delete Reusable command recipes
|
|
28
|
+
collection create|list Recipe collections
|
|
29
|
+
mcp serve Start MCP server for AI agents
|
|
30
|
+
mcp install --claude|--codex Install MCP server
|
|
31
|
+
snapshot Terminal state as JSON
|
|
33
32
|
--help Show this help
|
|
34
33
|
--version Show version
|
|
35
34
|
|
|
@@ -59,160 +58,6 @@ if (args[0] === "--version" || args[0] === "-v") {
|
|
|
59
58
|
}
|
|
60
59
|
process.exit(0);
|
|
61
60
|
}
|
|
62
|
-
// ── Exec command — smart execution for agents ────────────────────────────────
|
|
63
|
-
if (args[0] === "exec") {
|
|
64
|
-
// Parse flags: --json, --offset=N, --limit=N, --raw
|
|
65
|
-
const flags = {};
|
|
66
|
-
const cmdParts = [];
|
|
67
|
-
for (const arg of args.slice(1)) {
|
|
68
|
-
const flagMatch = arg.match(/^--(\w+)(?:=(.+))?$/);
|
|
69
|
-
if (flagMatch) {
|
|
70
|
-
flags[flagMatch[1]] = flagMatch[2] ?? "true";
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
cmdParts.push(arg);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const command = cmdParts.join(" ");
|
|
77
|
-
const jsonMode = flags.json === "true";
|
|
78
|
-
const rawMode = flags.raw === "true";
|
|
79
|
-
const offset = flags.offset ? parseInt(flags.offset) : undefined;
|
|
80
|
-
const limit = flags.limit ? parseInt(flags.limit) : undefined;
|
|
81
|
-
if (!command) {
|
|
82
|
-
console.error("Usage: terminal exec <command> [--json] [--raw] [--offset=N] [--limit=N]");
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
const { execSync } = await import("child_process");
|
|
86
|
-
const { compress, stripAnsi } = await import("./compression.js");
|
|
87
|
-
const { stripNoise } = await import("./noise-filter.js");
|
|
88
|
-
const { processOutput, shouldProcess } = await import("./output-processor.js");
|
|
89
|
-
const { rewriteCommand } = await import("./command-rewriter.js");
|
|
90
|
-
const { shouldBeLazy, toLazy, getSlice } = await import("./lazy-executor.js");
|
|
91
|
-
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
92
|
-
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
93
|
-
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
94
|
-
const { detectLoop } = await import("./loop-detector.js");
|
|
95
|
-
// Loop detection — suggest narrowing if running full test suite repeatedly
|
|
96
|
-
const loop = detectLoop(command);
|
|
97
|
-
if (loop.detected) {
|
|
98
|
-
console.error(`[open-terminal] loop detected: test run #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : " — consider narrowing to specific test file"}`);
|
|
99
|
-
}
|
|
100
|
-
// Rewrite command if possible
|
|
101
|
-
const rw = rewriteCommand(command);
|
|
102
|
-
const actualCmd = rw.changed ? rw.rewritten : command;
|
|
103
|
-
if (rw.changed)
|
|
104
|
-
console.error(`[open-terminal] rewritten: ${actualCmd} (${rw.reason})`);
|
|
105
|
-
try {
|
|
106
|
-
const start = Date.now();
|
|
107
|
-
const raw = execSync(actualCmd, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
108
|
-
const duration = Date.now() - start;
|
|
109
|
-
const clean = stripNoise(stripAnsi(raw)).cleaned;
|
|
110
|
-
const rawTokens = estimateTokens(raw);
|
|
111
|
-
// Track usage
|
|
112
|
-
recordUsage(rawTokens);
|
|
113
|
-
// --raw flag: skip all processing
|
|
114
|
-
if (rawMode) {
|
|
115
|
-
console.log(clean);
|
|
116
|
-
process.exit(0);
|
|
117
|
-
}
|
|
118
|
-
// --json flag: always return structured JSON
|
|
119
|
-
if (jsonMode) {
|
|
120
|
-
const parsed = parseOutput(actualCmd, clean);
|
|
121
|
-
if (parsed) {
|
|
122
|
-
const saved = rawTokens - estimateTokens(JSON.stringify(parsed.data));
|
|
123
|
-
if (saved > 0)
|
|
124
|
-
recordSaving("structured", saved);
|
|
125
|
-
console.log(JSON.stringify({ exitCode: 0, parser: parsed.parser, data: parsed.data, duration, tokensSaved: Math.max(0, saved) }));
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
const compressed = compress(actualCmd, clean, { format: "json" });
|
|
129
|
-
console.log(JSON.stringify({ exitCode: 0, output: compressed.content, duration, tokensSaved: compressed.tokensSaved }));
|
|
130
|
-
}
|
|
131
|
-
process.exit(0);
|
|
132
|
-
}
|
|
133
|
-
// Pagination: --offset + --limit on a previous large result
|
|
134
|
-
if (offset !== undefined || limit !== undefined) {
|
|
135
|
-
const slice = getSlice(clean, offset ?? 0, limit ?? 50);
|
|
136
|
-
console.log(slice.lines.join("\n"));
|
|
137
|
-
if (slice.hasMore)
|
|
138
|
-
console.error(`[open-terminal] showing ${slice.lines.length}/${slice.total}, ${slice.total - (offset ?? 0) - slice.lines.length} remaining`);
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
// Test output detection — use watchlist for structured test tracking
|
|
142
|
-
if (isTestOutput(clean)) {
|
|
143
|
-
const result = trackTests(process.cwd(), clean);
|
|
144
|
-
const formatted = formatWatchResult(result);
|
|
145
|
-
const savedTokens = rawTokens - estimateTokens(formatted);
|
|
146
|
-
if (savedTokens > 20)
|
|
147
|
-
recordSaving("structured", savedTokens);
|
|
148
|
-
if (jsonMode) {
|
|
149
|
-
console.log(JSON.stringify({ exitCode: 0, type: "test-results", ...result, duration: Date.now() - start }));
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
console.log(formatted);
|
|
153
|
-
}
|
|
154
|
-
if (savedTokens > 10)
|
|
155
|
-
console.error(`[open-terminal] test watchlist: saved ${savedTokens} tokens`);
|
|
156
|
-
process.exit(0);
|
|
157
|
-
}
|
|
158
|
-
// Lazy mode for huge output (threshold 200, skip cat/summary commands)
|
|
159
|
-
if (shouldBeLazy(clean, actualCmd)) {
|
|
160
|
-
const lazy = toLazy(clean, actualCmd);
|
|
161
|
-
const savedTokens = rawTokens - estimateTokens(JSON.stringify(lazy));
|
|
162
|
-
if (savedTokens > 0)
|
|
163
|
-
recordSaving("compressed", savedTokens);
|
|
164
|
-
console.log(JSON.stringify({ ...lazy, duration, tokensSaved: savedTokens }));
|
|
165
|
-
process.exit(0);
|
|
166
|
-
}
|
|
167
|
-
// AI summary for medium-large output (>15 lines)
|
|
168
|
-
if (shouldProcess(clean)) {
|
|
169
|
-
const processed = await processOutput(actualCmd, clean);
|
|
170
|
-
if (processed.aiProcessed && processed.tokensSaved > 30) {
|
|
171
|
-
recordSaving("compressed", processed.tokensSaved);
|
|
172
|
-
console.log(processed.summary);
|
|
173
|
-
console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved}, ${Math.round(processed.tokensSaved / rawTokens * 100)}%)`);
|
|
174
|
-
process.exit(0);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Small/medium output — just noise-strip and return
|
|
178
|
-
console.log(clean);
|
|
179
|
-
const savedTokens = rawTokens - estimateTokens(clean);
|
|
180
|
-
if (savedTokens > 10) {
|
|
181
|
-
recordSaving("compressed", savedTokens);
|
|
182
|
-
console.error(`[open-terminal] saved ${savedTokens} tokens (noise filter)`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
catch (e) {
|
|
186
|
-
// Command failed — parse error output for structured diagnosis
|
|
187
|
-
const stderr = e.stderr?.toString() ?? "";
|
|
188
|
-
const stdout = e.stdout?.toString() ?? "";
|
|
189
|
-
// Deduplicate: if stderr content appears in stdout, skip it
|
|
190
|
-
const combined = stderr && stdout.includes(stderr.trim()) ? stdout : stdout + stderr;
|
|
191
|
-
const errorOutput = stripNoise(stripAnsi(combined)).cleaned;
|
|
192
|
-
// Try structured error parsing
|
|
193
|
-
const { errorParser } = await import("./parsers/errors.js");
|
|
194
|
-
if (errorOutput.length > 200 && errorParser.detect(actualCmd, errorOutput)) {
|
|
195
|
-
const info = errorParser.parse(actualCmd, errorOutput);
|
|
196
|
-
if (jsonMode) {
|
|
197
|
-
console.log(JSON.stringify({ exitCode: e.status ?? 1, error: info }));
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
console.log(`Error: ${info.type}`);
|
|
201
|
-
console.log(` ${info.message}`);
|
|
202
|
-
if (info.file)
|
|
203
|
-
console.log(` File: ${info.file}${info.line ? `:${info.line}` : ""}`);
|
|
204
|
-
if (info.suggestion)
|
|
205
|
-
console.log(` Fix: ${info.suggestion}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// Short error or no parser match — pass through cleaned
|
|
210
|
-
console.log(errorOutput);
|
|
211
|
-
}
|
|
212
|
-
process.exit(e.status ?? 1);
|
|
213
|
-
}
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
216
61
|
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
217
62
|
if (args[0] === "mcp") {
|
|
218
63
|
if (args[1] === "serve" || args.length === 1) {
|
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -124,6 +124,26 @@ function detectProjectContext(): string {
|
|
|
124
124
|
parts.push("Project: Java/Gradle. Use gradle commands.");
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Directory structure — so AI knows actual paths (not guessed ones)
|
|
128
|
+
try {
|
|
129
|
+
const { execSync } = require("child_process");
|
|
130
|
+
// Top-level dirs
|
|
131
|
+
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
132
|
+
parts.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
133
|
+
|
|
134
|
+
// src/ structure (2 levels deep, most important for path resolution)
|
|
135
|
+
for (const srcDir of ["src", "lib", "app"]) {
|
|
136
|
+
if (existsSync(join(cwd, srcDir))) {
|
|
137
|
+
const tree = execSync(
|
|
138
|
+
`find ${srcDir} -maxdepth 2 -type d -not -path '*/node_modules/*' 2>/dev/null | head -30`,
|
|
139
|
+
{ cwd, encoding: "utf8", timeout: 2000 }
|
|
140
|
+
).trim();
|
|
141
|
+
if (tree) parts.push(`Directories in ${srcDir}/:\n${tree}`);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch { /* timeout or no exec — skip */ }
|
|
146
|
+
|
|
127
147
|
return parts.length > 0 ? `\n\nPROJECT CONTEXT:\n${parts.join("\n")}` : "";
|
|
128
148
|
}
|
|
129
149
|
|
package/src/cli.tsx
CHANGED
|
@@ -7,32 +7,31 @@ 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 — Natural language shell for AI agents and humans
|
|
11
11
|
|
|
12
12
|
USAGE:
|
|
13
|
+
terminal "your request" NL → AI picks command → runs → smart output
|
|
13
14
|
terminal Launch interactive NL terminal (TUI)
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
EXAMPLES:
|
|
17
|
+
terminal "list all typescript files"
|
|
18
|
+
terminal "run tests"
|
|
19
|
+
terminal "what changed in git"
|
|
20
|
+
terminal "show me the auth functions"
|
|
21
|
+
terminal "kill port 3000"
|
|
22
|
+
terminal "how many lines of code"
|
|
15
23
|
|
|
16
24
|
SUBCOMMANDS:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
recipe add
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
collection list List collections
|
|
28
|
-
project init Initialize project-scoped recipes
|
|
29
|
-
repo Show git repo state (branch + status + log)
|
|
30
|
-
symbols <file> Show file outline (functions, classes, exports)
|
|
31
|
-
stats Show token economy dashboard
|
|
32
|
-
sessions List recent terminal sessions
|
|
33
|
-
sessions stats Show session analytics
|
|
34
|
-
sessions <id> Show session details
|
|
35
|
-
snapshot Capture terminal state as JSON
|
|
25
|
+
repo Git repo state (branch + status + log)
|
|
26
|
+
symbols <file> File outline (functions, classes, exports)
|
|
27
|
+
overview Project overview (deps, scripts, structure)
|
|
28
|
+
stats Token economy dashboard
|
|
29
|
+
sessions [stats|<id>] Session history and analytics
|
|
30
|
+
recipe add|list|run|delete Reusable command recipes
|
|
31
|
+
collection create|list Recipe collections
|
|
32
|
+
mcp serve Start MCP server for AI agents
|
|
33
|
+
mcp install --claude|--codex Install MCP server
|
|
34
|
+
snapshot Terminal state as JSON
|
|
36
35
|
--help Show this help
|
|
37
36
|
--version Show version
|
|
38
37
|
|
|
@@ -61,156 +60,6 @@ if (args[0] === "--version" || args[0] === "-v") {
|
|
|
61
60
|
process.exit(0);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// ── Exec command — smart execution for agents ────────────────────────────────
|
|
65
|
-
|
|
66
|
-
if (args[0] === "exec") {
|
|
67
|
-
// Parse flags: --json, --offset=N, --limit=N, --raw
|
|
68
|
-
const flags: Record<string, string> = {};
|
|
69
|
-
const cmdParts: string[] = [];
|
|
70
|
-
for (const arg of args.slice(1)) {
|
|
71
|
-
const flagMatch = arg.match(/^--(\w+)(?:=(.+))?$/);
|
|
72
|
-
if (flagMatch) { flags[flagMatch[1]] = flagMatch[2] ?? "true"; }
|
|
73
|
-
else { cmdParts.push(arg); }
|
|
74
|
-
}
|
|
75
|
-
const command = cmdParts.join(" ");
|
|
76
|
-
const jsonMode = flags.json === "true";
|
|
77
|
-
const rawMode = flags.raw === "true";
|
|
78
|
-
const offset = flags.offset ? parseInt(flags.offset) : undefined;
|
|
79
|
-
const limit = flags.limit ? parseInt(flags.limit) : undefined;
|
|
80
|
-
|
|
81
|
-
if (!command) {
|
|
82
|
-
console.error("Usage: terminal exec <command> [--json] [--raw] [--offset=N] [--limit=N]");
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const { execSync } = await import("child_process");
|
|
87
|
-
const { compress, stripAnsi } = await import("./compression.js");
|
|
88
|
-
const { stripNoise } = await import("./noise-filter.js");
|
|
89
|
-
const { processOutput, shouldProcess } = await import("./output-processor.js");
|
|
90
|
-
const { rewriteCommand } = await import("./command-rewriter.js");
|
|
91
|
-
const { shouldBeLazy, toLazy, getSlice } = await import("./lazy-executor.js");
|
|
92
|
-
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
93
|
-
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
94
|
-
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
95
|
-
const { detectLoop } = await import("./loop-detector.js");
|
|
96
|
-
|
|
97
|
-
// Loop detection — suggest narrowing if running full test suite repeatedly
|
|
98
|
-
const loop = detectLoop(command);
|
|
99
|
-
if (loop.detected) {
|
|
100
|
-
console.error(`[open-terminal] loop detected: test run #${loop.iteration}${loop.suggestedNarrow ? ` — try: ${loop.suggestedNarrow}` : " — consider narrowing to specific test file"}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Rewrite command if possible
|
|
104
|
-
const rw = rewriteCommand(command);
|
|
105
|
-
const actualCmd = rw.changed ? rw.rewritten : command;
|
|
106
|
-
if (rw.changed) console.error(`[open-terminal] rewritten: ${actualCmd} (${rw.reason})`);
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const start = Date.now();
|
|
110
|
-
const raw = execSync(actualCmd, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
111
|
-
const duration = Date.now() - start;
|
|
112
|
-
const clean = stripNoise(stripAnsi(raw)).cleaned;
|
|
113
|
-
const rawTokens = estimateTokens(raw);
|
|
114
|
-
|
|
115
|
-
// Track usage
|
|
116
|
-
recordUsage(rawTokens);
|
|
117
|
-
|
|
118
|
-
// --raw flag: skip all processing
|
|
119
|
-
if (rawMode) { console.log(clean); process.exit(0); }
|
|
120
|
-
|
|
121
|
-
// --json flag: always return structured JSON
|
|
122
|
-
if (jsonMode) {
|
|
123
|
-
const parsed = parseOutput(actualCmd, clean);
|
|
124
|
-
if (parsed) {
|
|
125
|
-
const saved = rawTokens - estimateTokens(JSON.stringify(parsed.data));
|
|
126
|
-
if (saved > 0) recordSaving("structured", saved);
|
|
127
|
-
console.log(JSON.stringify({ exitCode: 0, parser: parsed.parser, data: parsed.data, duration, tokensSaved: Math.max(0, saved) }));
|
|
128
|
-
} else {
|
|
129
|
-
const compressed = compress(actualCmd, clean, { format: "json" });
|
|
130
|
-
console.log(JSON.stringify({ exitCode: 0, output: compressed.content, duration, tokensSaved: compressed.tokensSaved }));
|
|
131
|
-
}
|
|
132
|
-
process.exit(0);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Pagination: --offset + --limit on a previous large result
|
|
136
|
-
if (offset !== undefined || limit !== undefined) {
|
|
137
|
-
const slice = getSlice(clean, offset ?? 0, limit ?? 50);
|
|
138
|
-
console.log(slice.lines.join("\n"));
|
|
139
|
-
if (slice.hasMore) console.error(`[open-terminal] showing ${slice.lines.length}/${slice.total}, ${slice.total - (offset ?? 0) - slice.lines.length} remaining`);
|
|
140
|
-
process.exit(0);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Test output detection — use watchlist for structured test tracking
|
|
144
|
-
if (isTestOutput(clean)) {
|
|
145
|
-
const result = trackTests(process.cwd(), clean);
|
|
146
|
-
const formatted = formatWatchResult(result);
|
|
147
|
-
const savedTokens = rawTokens - estimateTokens(formatted);
|
|
148
|
-
if (savedTokens > 20) recordSaving("structured", savedTokens);
|
|
149
|
-
if (jsonMode) {
|
|
150
|
-
console.log(JSON.stringify({ exitCode: 0, type: "test-results", ...result, duration: Date.now() - start }));
|
|
151
|
-
} else {
|
|
152
|
-
console.log(formatted);
|
|
153
|
-
}
|
|
154
|
-
if (savedTokens > 10) console.error(`[open-terminal] test watchlist: saved ${savedTokens} tokens`);
|
|
155
|
-
process.exit(0);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Lazy mode for huge output (threshold 200, skip cat/summary commands)
|
|
159
|
-
if (shouldBeLazy(clean, actualCmd)) {
|
|
160
|
-
const lazy = toLazy(clean, actualCmd);
|
|
161
|
-
const savedTokens = rawTokens - estimateTokens(JSON.stringify(lazy));
|
|
162
|
-
if (savedTokens > 0) recordSaving("compressed", savedTokens);
|
|
163
|
-
console.log(JSON.stringify({ ...lazy, duration, tokensSaved: savedTokens }));
|
|
164
|
-
process.exit(0);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// AI summary for medium-large output (>15 lines)
|
|
168
|
-
if (shouldProcess(clean)) {
|
|
169
|
-
const processed = await processOutput(actualCmd, clean);
|
|
170
|
-
if (processed.aiProcessed && processed.tokensSaved > 30) {
|
|
171
|
-
recordSaving("compressed", processed.tokensSaved);
|
|
172
|
-
console.log(processed.summary);
|
|
173
|
-
console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved}, ${Math.round(processed.tokensSaved/rawTokens*100)}%)`);
|
|
174
|
-
process.exit(0);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Small/medium output — just noise-strip and return
|
|
179
|
-
console.log(clean);
|
|
180
|
-
const savedTokens = rawTokens - estimateTokens(clean);
|
|
181
|
-
if (savedTokens > 10) {
|
|
182
|
-
recordSaving("compressed", savedTokens);
|
|
183
|
-
console.error(`[open-terminal] saved ${savedTokens} tokens (noise filter)`);
|
|
184
|
-
}
|
|
185
|
-
} catch (e: any) {
|
|
186
|
-
// Command failed — parse error output for structured diagnosis
|
|
187
|
-
const stderr = e.stderr?.toString() ?? "";
|
|
188
|
-
const stdout = e.stdout?.toString() ?? "";
|
|
189
|
-
// Deduplicate: if stderr content appears in stdout, skip it
|
|
190
|
-
const combined = stderr && stdout.includes(stderr.trim()) ? stdout : stdout + stderr;
|
|
191
|
-
const errorOutput = stripNoise(stripAnsi(combined)).cleaned;
|
|
192
|
-
|
|
193
|
-
// Try structured error parsing
|
|
194
|
-
const { errorParser } = await import("./parsers/errors.js");
|
|
195
|
-
if (errorOutput.length > 200 && errorParser.detect(actualCmd, errorOutput)) {
|
|
196
|
-
const info = errorParser.parse(actualCmd, errorOutput);
|
|
197
|
-
if (jsonMode) {
|
|
198
|
-
console.log(JSON.stringify({ exitCode: e.status ?? 1, error: info }));
|
|
199
|
-
} else {
|
|
200
|
-
console.log(`Error: ${info.type}`);
|
|
201
|
-
console.log(` ${info.message}`);
|
|
202
|
-
if (info.file) console.log(` File: ${info.file}${info.line ? `:${info.line}` : ""}`);
|
|
203
|
-
if (info.suggestion) console.log(` Fix: ${info.suggestion}`);
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
// Short error or no parser match — pass through cleaned
|
|
207
|
-
console.log(errorOutput);
|
|
208
|
-
}
|
|
209
|
-
process.exit(e.status ?? 1);
|
|
210
|
-
}
|
|
211
|
-
process.exit(0);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
63
|
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
215
64
|
|
|
216
65
|
if (args[0] === "mcp") {
|