@hasna/terminal 4.2.0 → 4.3.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/package.json +5 -3
- package/src/ai.ts +4 -4
- package/src/mcp/server.ts +36 -1640
- package/src/mcp/tools/batch.ts +106 -0
- package/src/mcp/tools/execute.ts +248 -0
- package/src/mcp/tools/files.ts +369 -0
- package/src/mcp/tools/git.ts +306 -0
- package/src/mcp/tools/helpers.ts +92 -0
- package/src/mcp/tools/memory.ts +170 -0
- package/src/mcp/tools/meta.ts +202 -0
- package/src/mcp/tools/process.ts +94 -0
- package/src/mcp/tools/project.ts +297 -0
- package/src/mcp/tools/search.ts +118 -0
- package/src/output-processor.ts +7 -2
- package/src/snapshots.ts +2 -2
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -315
- package/dist/cache.js +0 -42
- package/dist/cli.js +0 -778
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -86
- package/dist/compression.js +0 -91
- package/dist/context-hints.js +0 -285
- package/dist/diff-cache.js +0 -107
- package/dist/discover.js +0 -212
- package/dist/economy.js +0 -155
- package/dist/expand-store.js +0 -44
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -189
- package/dist/mcp/server.js +0 -1306
- package/dist/noise-filter.js +0 -94
- package/dist/output-processor.js +0 -229
- package/dist/output-router.js +0 -41
- package/dist/output-store.js +0 -111
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -43
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -8
- package/dist/providers/groq.js +0 -8
- package/dist/providers/index.js +0 -142
- package/dist/providers/openai-compat.js +0 -93
- package/dist/providers/xai.js +0 -8
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -153
- package/dist/search/content-search.js +0 -70
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -346
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -231
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tokens.js +0 -17
- package/dist/tool-profiles.js +0 -129
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Batch tools: batch
|
|
2
|
+
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z, type ToolHelpers } from "./helpers.js";
|
|
5
|
+
import { stripAnsi } from "../../compression.js";
|
|
6
|
+
import { processOutput } from "../../output-processor.js";
|
|
7
|
+
import { getOutputProvider } from "../../providers/index.js";
|
|
8
|
+
import { searchContent } from "../../search/index.js";
|
|
9
|
+
import { cachedRead } from "../../file-cache.js";
|
|
10
|
+
|
|
11
|
+
export function registerBatchTools(server: McpServer, h: ToolHelpers): void {
|
|
12
|
+
|
|
13
|
+
server.tool(
|
|
14
|
+
"batch",
|
|
15
|
+
"Run multiple operations in ONE call. Saves N-1 round trips. Each op can be: execute (run command), read (file read/summarize), search (grep pattern), or symbols (file outline).",
|
|
16
|
+
{
|
|
17
|
+
ops: z.array(z.object({
|
|
18
|
+
type: z.enum(["execute", "read", "write", "search", "symbols"]).describe("Operation type"),
|
|
19
|
+
command: z.string().optional().describe("Shell command (for execute)"),
|
|
20
|
+
path: z.string().optional().describe("File path (for read/write/symbols)"),
|
|
21
|
+
content: z.string().optional().describe("File content (for write)"),
|
|
22
|
+
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
23
|
+
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
24
|
+
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
25
|
+
})).describe("Array of operations to run"),
|
|
26
|
+
cwd: z.string().optional().describe("Working directory for all ops"),
|
|
27
|
+
},
|
|
28
|
+
async ({ ops, cwd }) => {
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
const workDir = cwd ?? process.cwd();
|
|
31
|
+
const results: Record<string, any>[] = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < ops.slice(0, 10).length; i++) {
|
|
34
|
+
const op = ops[i];
|
|
35
|
+
try {
|
|
36
|
+
if (op.type === "execute" && op.command) {
|
|
37
|
+
const result = await h.exec(op.command, workDir, 30000);
|
|
38
|
+
const output = (result.stdout + result.stderr).trim();
|
|
39
|
+
if (op.format === "summary" && output.split("\n").length > 15) {
|
|
40
|
+
const processed = await processOutput(op.command, output);
|
|
41
|
+
results.push({ op: i, type: "execute", summary: processed.summary, exitCode: result.exitCode, tokensSaved: processed.tokensSaved });
|
|
42
|
+
} else {
|
|
43
|
+
results.push({ op: i, type: "execute", output: stripAnsi(output).slice(0, 2000), exitCode: result.exitCode });
|
|
44
|
+
}
|
|
45
|
+
} else if (op.type === "read" && op.path) {
|
|
46
|
+
const filePath = h.resolvePath(op.path, workDir);
|
|
47
|
+
const result = cachedRead(filePath, {});
|
|
48
|
+
if (op.summarize && result.content.length > 500) {
|
|
49
|
+
const provider = getOutputProvider();
|
|
50
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
51
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
52
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
53
|
+
model: outputModel,
|
|
54
|
+
system: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose. Be specific.`,
|
|
55
|
+
maxTokens: 300, temperature: 0.2,
|
|
56
|
+
});
|
|
57
|
+
results.push({ op: i, type: "read", path: op.path, summary, lines: result.content.split("\n").length });
|
|
58
|
+
} else {
|
|
59
|
+
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
60
|
+
}
|
|
61
|
+
} else if (op.type === "write" && op.path && op.content !== undefined) {
|
|
62
|
+
const filePath = h.resolvePath(op.path, workDir);
|
|
63
|
+
const { writeFileSync, mkdirSync, existsSync } = await import("fs");
|
|
64
|
+
const { dirname } = await import("path");
|
|
65
|
+
const dir = dirname(filePath);
|
|
66
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
67
|
+
writeFileSync(filePath, op.content);
|
|
68
|
+
results.push({ op: i, type: "write", path: op.path, ok: true, bytes: op.content.length });
|
|
69
|
+
} else if (op.type === "search" && op.pattern) {
|
|
70
|
+
// Search accepts both files and directories — resolve to parent dir if file
|
|
71
|
+
let searchPath = op.path ? h.resolvePath(op.path, workDir) : workDir;
|
|
72
|
+
try {
|
|
73
|
+
const { statSync } = await import("fs");
|
|
74
|
+
if (statSync(searchPath).isFile()) searchPath = searchPath.replace(/\/[^/]+$/, "");
|
|
75
|
+
} catch {}
|
|
76
|
+
const result = await searchContent(op.pattern, searchPath, {});
|
|
77
|
+
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
78
|
+
} else if (op.type === "symbols" && op.path) {
|
|
79
|
+
const filePath = h.resolvePath(op.path, workDir);
|
|
80
|
+
const result = cachedRead(filePath, {});
|
|
81
|
+
if (result.content && !result.content.startsWith("Error:")) {
|
|
82
|
+
const provider = getOutputProvider();
|
|
83
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
84
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
85
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
86
|
+
model: outputModel,
|
|
87
|
+
system: `Extract all symbols. Return ONLY a JSON array. Each: {"name":"x","kind":"function|class|method|interface|type","line":N,"signature":"brief"}. For class methods use "Class.method". Exclude imports.`,
|
|
88
|
+
maxTokens: 2000, temperature: 0,
|
|
89
|
+
});
|
|
90
|
+
let symbols: any[] = [];
|
|
91
|
+
try { const m = summary.match(/\[[\s\S]*\]/); if (m) symbols = JSON.parse(m[0]); } catch {}
|
|
92
|
+
results.push({ op: i, type: "symbols", path: op.path, symbols });
|
|
93
|
+
} else {
|
|
94
|
+
results.push({ op: i, type: "symbols", path: op.path, error: "Cannot read file" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
results.push({ op: i, type: op.type, error: err.message?.slice(0, 200) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
h.logCall("batch", { command: `${ops.length} ops`, durationMs: Date.now() - start, aiProcessed: true });
|
|
103
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ results, total: results.length, durationMs: Date.now() - start }) }] };
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Execute tools: execute, execute_smart, execute_diff, expand, browse, explain_error
|
|
2
|
+
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z, type ToolHelpers } from "./helpers.js";
|
|
5
|
+
import { compress, stripAnsi } from "../../compression.js";
|
|
6
|
+
import { estimateTokens } from "../../tokens.js";
|
|
7
|
+
import { processOutput } from "../../output-processor.js";
|
|
8
|
+
import { storeOutput, expandOutput } from "../../expand-store.js";
|
|
9
|
+
import { shouldBeLazy, toLazy } from "../../lazy-executor.js";
|
|
10
|
+
import { diffOutput } from "../../diff-cache.js";
|
|
11
|
+
import { recordSaving } from "../../economy.js";
|
|
12
|
+
|
|
13
|
+
export function registerExecuteTools(server: McpServer, h: ToolHelpers): void {
|
|
14
|
+
|
|
15
|
+
// ── execute: run a command, return structured result ──────────────────────
|
|
16
|
+
|
|
17
|
+
server.tool(
|
|
18
|
+
"execute",
|
|
19
|
+
"Run a shell command. Format guide: no format/raw for git commit/push (<50 tokens). format=compressed for long build output (CPU-only, no AI). format=json or format=summary for AI-summarized output (234ms, saves 80% tokens). Prefer execute_smart for most tasks.",
|
|
20
|
+
{
|
|
21
|
+
command: z.string().describe("Shell command to execute"),
|
|
22
|
+
cwd: z.string().optional().describe("Working directory (default: server cwd)"),
|
|
23
|
+
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
24
|
+
format: z.enum(["raw", "json", "compressed", "summary"]).optional().describe("Output format"),
|
|
25
|
+
maxTokens: z.number().optional().describe("Token budget for compressed/summary format"),
|
|
26
|
+
},
|
|
27
|
+
async ({ command, cwd, timeout, format, maxTokens }) => {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
const result = await h.exec(command, cwd, timeout ?? 30000);
|
|
30
|
+
const output = (result.stdout + result.stderr).trim();
|
|
31
|
+
|
|
32
|
+
// Raw mode — with lazy execution for large results
|
|
33
|
+
if (!format || format === "raw") {
|
|
34
|
+
const clean = stripAnsi(output);
|
|
35
|
+
if (shouldBeLazy(clean, command)) {
|
|
36
|
+
const lazy = toLazy(clean, command);
|
|
37
|
+
const detailKey = storeOutput(command, clean);
|
|
38
|
+
h.logCall("execute", { command, outputTokens: estimateTokens(clean), tokensSaved: 0, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
41
|
+
exitCode: result.exitCode, ...lazy, detailKey, duration: result.duration,
|
|
42
|
+
...(result.rewritten ? { rewrittenFrom: command } : {}),
|
|
43
|
+
}) }],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
h.logCall("execute", { command, outputTokens: estimateTokens(clean), tokensSaved: 0, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
49
|
+
exitCode: result.exitCode, output: clean, duration: result.duration, tokens: estimateTokens(clean),
|
|
50
|
+
...(result.rewritten ? { rewrittenFrom: command } : {}),
|
|
51
|
+
}) }],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// JSON and Summary modes — both go through AI processing
|
|
56
|
+
if (format === "json" || format === "summary") {
|
|
57
|
+
try {
|
|
58
|
+
const processed = await processOutput(command, output);
|
|
59
|
+
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
60
|
+
h.logCall("execute", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
63
|
+
exitCode: result.exitCode,
|
|
64
|
+
summary: processed.summary,
|
|
65
|
+
structured: processed.structured,
|
|
66
|
+
duration: result.duration,
|
|
67
|
+
tokensSaved: processed.tokensSaved,
|
|
68
|
+
aiProcessed: processed.aiProcessed,
|
|
69
|
+
...(detailKey ? { detailKey, expandable: true } : {}),
|
|
70
|
+
}) }],
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
const compressed = compress(command, output, { maxTokens });
|
|
74
|
+
h.logCall("execute", { command, outputTokens: estimateTokens(output), tokensSaved: compressed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
77
|
+
exitCode: result.exitCode, output: compressed.content, duration: result.duration,
|
|
78
|
+
tokensSaved: compressed.tokensSaved,
|
|
79
|
+
}) }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Compressed mode — fast non-AI: strip + dedup + truncate
|
|
85
|
+
if (format === "compressed") {
|
|
86
|
+
const compressed = compress(command, output, { maxTokens });
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
89
|
+
exitCode: result.exitCode, output: compressed.content, duration: result.duration,
|
|
90
|
+
...(compressed.tokensSaved > 0 ? { tokensSaved: compressed.tokensSaved, savingsPercent: compressed.savingsPercent } : {}),
|
|
91
|
+
}) }],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { content: [{ type: "text" as const, text: output }] };
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ── execute_smart: AI-powered output processing ────────────────────────────
|
|
100
|
+
|
|
101
|
+
server.tool(
|
|
102
|
+
"execute_smart",
|
|
103
|
+
"Run a command and get AI-summarized output (80-95% token savings). Use this for: test runs, builds, git operations, process management, system info. Do NOT use for file read/write — use your native Read/Write/Edit tools instead (they're faster, no shell overhead).",
|
|
104
|
+
{
|
|
105
|
+
command: z.string().describe("Shell command to execute"),
|
|
106
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
107
|
+
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
108
|
+
verbosity: z.enum(["minimal", "normal", "detailed"]).optional().describe("Summary detail level (default: normal)"),
|
|
109
|
+
},
|
|
110
|
+
async ({ command, cwd, timeout, verbosity }) => {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
const result = await h.exec(command, cwd, timeout ?? 30000, true);
|
|
113
|
+
const output = (result.stdout + result.stderr).trim();
|
|
114
|
+
const processed = await processOutput(command, output, undefined, verbosity);
|
|
115
|
+
|
|
116
|
+
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
117
|
+
h.logCall("execute_smart", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
121
|
+
exitCode: result.exitCode,
|
|
122
|
+
summary: processed.summary,
|
|
123
|
+
structured: processed.structured,
|
|
124
|
+
duration: result.duration,
|
|
125
|
+
totalLines: output.split("\n").length,
|
|
126
|
+
tokensSaved: processed.tokensSaved,
|
|
127
|
+
aiProcessed: processed.aiProcessed,
|
|
128
|
+
...(detailKey ? { detailKey, expandable: true } : {}),
|
|
129
|
+
}) }],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// ── execute_diff: run command with diff from last run ───────────────────────
|
|
135
|
+
|
|
136
|
+
server.tool(
|
|
137
|
+
"execute_diff",
|
|
138
|
+
"Run a command and return diff from its last execution. Ideal for edit→test loops — only shows what changed.",
|
|
139
|
+
{
|
|
140
|
+
command: z.string().describe("Shell command to execute"),
|
|
141
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
142
|
+
timeout: z.number().optional().describe("Timeout in ms"),
|
|
143
|
+
},
|
|
144
|
+
async ({ command, cwd, timeout }) => {
|
|
145
|
+
const start = Date.now();
|
|
146
|
+
const workDir = cwd ?? process.cwd();
|
|
147
|
+
const result = await h.exec(command, workDir, timeout ?? 30000);
|
|
148
|
+
const output = (result.stdout + result.stderr).trim();
|
|
149
|
+
const diff = diffOutput(command, workDir, output);
|
|
150
|
+
|
|
151
|
+
if (diff.tokensSaved > 0) {
|
|
152
|
+
recordSaving("diff", diff.tokensSaved);
|
|
153
|
+
}
|
|
154
|
+
h.logCall("execute_diff", { command, outputTokens: estimateTokens(output), tokensSaved: diff.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
155
|
+
|
|
156
|
+
if (diff.unchanged) {
|
|
157
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
158
|
+
exitCode: result.exitCode, unchanged: true, diffSummary: diff.diffSummary,
|
|
159
|
+
duration: result.duration, tokensSaved: diff.tokensSaved,
|
|
160
|
+
}) }] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (diff.hasPrevious) {
|
|
164
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
165
|
+
exitCode: result.exitCode, diffSummary: diff.diffSummary,
|
|
166
|
+
added: diff.added.slice(0, 50), removed: diff.removed.slice(0, 50),
|
|
167
|
+
duration: result.duration, tokensSaved: diff.tokensSaved,
|
|
168
|
+
}) }] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// First run — return full output (ANSI stripped)
|
|
172
|
+
const clean = stripAnsi(output);
|
|
173
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
174
|
+
exitCode: result.exitCode, output: clean,
|
|
175
|
+
diffSummary: "first run", duration: result.duration,
|
|
176
|
+
}) }] };
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── expand: retrieve full output on demand ────────────────────────────────
|
|
181
|
+
|
|
182
|
+
server.tool(
|
|
183
|
+
"expand",
|
|
184
|
+
"Retrieve full output from a previous execute_smart call. Only call this when you need details (e.g., to see failing test errors). Use the detailKey from execute_smart response.",
|
|
185
|
+
{
|
|
186
|
+
key: z.string().describe("The detailKey from a previous execute_smart response"),
|
|
187
|
+
grep: z.string().optional().describe("Filter output lines by pattern (e.g., 'FAIL', 'error')"),
|
|
188
|
+
},
|
|
189
|
+
async ({ key, grep }) => {
|
|
190
|
+
const result = expandOutput(key, grep);
|
|
191
|
+
if (!result.found) {
|
|
192
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Output expired or not found" }) }] };
|
|
193
|
+
}
|
|
194
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ output: result.output, lines: result.lines }) }] };
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// ── browse: list files/dirs as structured JSON ────────────────────────────
|
|
199
|
+
|
|
200
|
+
server.tool(
|
|
201
|
+
"browse",
|
|
202
|
+
"List files and directories as structured JSON. Auto-filters node_modules, .git, dist by default.",
|
|
203
|
+
{
|
|
204
|
+
path: z.string().optional().describe("Directory path (default: cwd)"),
|
|
205
|
+
recursive: z.boolean().optional().describe("List recursively (default: false)"),
|
|
206
|
+
maxDepth: z.number().optional().describe("Max depth for recursive listing (default: 2)"),
|
|
207
|
+
includeHidden: z.boolean().optional().describe("Include hidden files (default: false)"),
|
|
208
|
+
},
|
|
209
|
+
async ({ path, recursive, maxDepth, includeHidden }) => {
|
|
210
|
+
const target = path ?? process.cwd();
|
|
211
|
+
const depth = maxDepth ?? 2;
|
|
212
|
+
|
|
213
|
+
let command: string;
|
|
214
|
+
if (recursive) {
|
|
215
|
+
command = `find "${target}" -maxdepth ${depth} -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.next/*'`;
|
|
216
|
+
if (!includeHidden) command += " -not -name '.*'";
|
|
217
|
+
} else {
|
|
218
|
+
command = includeHidden ? `ls -la "${target}"` : `ls -l "${target}"`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = await h.exec(command);
|
|
222
|
+
const files = result.stdout.split("\n").filter(l => l.trim());
|
|
223
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ cwd: target, files, count: files.length }) }] };
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// ── explain_error: structured error diagnosis ─────────────────────────────
|
|
228
|
+
|
|
229
|
+
server.tool(
|
|
230
|
+
"explain_error",
|
|
231
|
+
"Parse error output and return structured diagnosis with root cause and fix suggestion.",
|
|
232
|
+
{
|
|
233
|
+
error: z.string().describe("Error output text"),
|
|
234
|
+
command: z.string().optional().describe("The command that produced the error"),
|
|
235
|
+
},
|
|
236
|
+
async ({ error, command }) => {
|
|
237
|
+
// AI processes the error — no regex guessing
|
|
238
|
+
const processed = await processOutput(command ?? "unknown", error);
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
241
|
+
summary: processed.summary,
|
|
242
|
+
structured: processed.structured,
|
|
243
|
+
aiProcessed: processed.aiProcessed,
|
|
244
|
+
}) }],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
}
|