@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 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 v1.0.0 Smart terminal for AI agents and humans
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
- terminal <subcommand> Run a specific command
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
- mcp serve Start MCP server (stdio transport)
15
- mcp install --claude|--codex|--gemini|--all
16
- Install as MCP server for AI agents
17
- hook install --claude Install Claude Code PostToolUse hook
18
- hook uninstall Remove hooks
19
- recipe add <name> <cmd> Save a reusable command recipe
20
- recipe list List saved recipes
21
- recipe run <name> [--var=X] Run a recipe with variable substitution
22
- recipe delete <name> Delete a recipe
23
- collection create <name> Create a recipe collection
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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/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 v1.0.0 Smart terminal for AI agents and humans
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
- terminal <subcommand> Run a specific command
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
- mcp serve Start MCP server (stdio transport)
18
- mcp install --claude|--codex|--gemini|--all
19
- Install as MCP server for AI agents
20
- hook install --claude Install Claude Code PostToolUse hook
21
- hook uninstall Remove hooks
22
- recipe add <name> <cmd> Save a reusable command recipe
23
- recipe list List saved recipes
24
- recipe run <name> [--var=X] Run a recipe with variable substitution
25
- recipe delete <name> Delete a recipe
26
- collection create <name> Create a recipe collection
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") {