min-agent 0.5.0 → 0.6.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/README.md +32 -18
- package/dist/cli.js +538 -12
- package/docs/API.md +25 -8
- package/package.json +6 -6
- package/skills/self-config/SKILL.md +7 -3
- package/skills/self-config/reference.md +8 -4
- package/dist/agent.js +0 -1342
- package/dist/assistant-stream.js +0 -131
- package/dist/cli/commands/chat.js +0 -13
- package/dist/cli/commands/exec.js +0 -35
- package/dist/cli/commands/history.js +0 -58
- package/dist/cli/commands/index.js +0 -241
- package/dist/cli/commands/init.js +0 -18
- package/dist/cli/commands/mcp.js +0 -173
- package/dist/cli/commands/memory.js +0 -87
- package/dist/cli/commands/models.js +0 -21
- package/dist/cli/commands/permission.js +0 -12
- package/dist/cli/commands/rules.js +0 -33
- package/dist/cli/commands/sandbox.js +0 -13
- package/dist/cli/commands/serve.js +0 -9
- package/dist/cli/commands/setup.js +0 -4
- package/dist/cli/commands/shared.js +0 -16
- package/dist/cli/commands/skills.js +0 -119
- package/dist/cli/commands/think.js +0 -12
- package/dist/cli/commands/update.js +0 -7
- package/dist/cli/commands/write-config.js +0 -52
- package/dist/cli/errors.js +0 -36
- package/dist/cli/exec-prompt.js +0 -26
- package/dist/cli/option-helpers.js +0 -65
- package/dist/cli/program.js +0 -217
- package/dist/clipboard.js +0 -142
- package/dist/code-mode.js +0 -184
- package/dist/compaction.js +0 -644
- package/dist/config.js +0 -470
- package/dist/confirm.js +0 -172
- package/dist/context-window.js +0 -246
- package/dist/doom-loop.js +0 -278
- package/dist/fetch-timeout.js +0 -152
- package/dist/http-approvals.js +0 -60
- package/dist/http.js +0 -119
- package/dist/instructions.js +0 -196
- package/dist/logger.js +0 -95
- package/dist/markdown.js +0 -259
- package/dist/mcp.js +0 -1036
- package/dist/memory-cli.js +0 -33
- package/dist/memory.js +0 -295
- package/dist/model-catalog.js +0 -285
- package/dist/output.js +0 -76
- package/dist/paste-handler.js +0 -41
- package/dist/permission-cli.js +0 -40
- package/dist/plugins.js +0 -131
- package/dist/pricing.js +0 -119
- package/dist/provider.js +0 -63
- package/dist/question-format.js +0 -60
- package/dist/reasoning-stream.js +0 -158
- package/dist/sandbox-cli.js +0 -79
- package/dist/sandbox.js +0 -403
- package/dist/save-throttle.js +0 -45
- package/dist/scope.js +0 -23
- package/dist/serve/common.js +0 -425
- package/dist/serve/routes-chat.js +0 -367
- package/dist/serve/routes-mcp.js +0 -212
- package/dist/serve/routes-memory.js +0 -95
- package/dist/serve/routes-meta.js +0 -233
- package/dist/serve/routes-sessions.js +0 -61
- package/dist/serve/routes-skills.js +0 -70
- package/dist/serve.js +0 -138
- package/dist/sessions.js +0 -265
- package/dist/skills.js +0 -599
- package/dist/structured-output.js +0 -29
- package/dist/synthetic.js +0 -7
- package/dist/think-cli.js +0 -36
- package/dist/thinking-wire.js +0 -228
- package/dist/thinking.js +0 -142
- package/dist/title-gen.js +0 -55
- package/dist/token-display.js +0 -39
- package/dist/tool-display.js +0 -178
- package/dist/tool-output.js +0 -126
- package/dist/tools/apply_patch.js +0 -265
- package/dist/tools/atomic-file.js +0 -35
- package/dist/tools/backend.js +0 -61
- package/dist/tools/bash.js +0 -231
- package/dist/tools/code_search.js +0 -98
- package/dist/tools/edit.js +0 -68
- package/dist/tools/explore.js +0 -232
- package/dist/tools/glob.js +0 -40
- package/dist/tools/grep.js +0 -174
- package/dist/tools/index.js +0 -34
- package/dist/tools/question.js +0 -54
- package/dist/tools/read.js +0 -109
- package/dist/tools/search-searxng.js +0 -223
- package/dist/tools/search-serper.js +0 -189
- package/dist/tools/task.js +0 -165
- package/dist/tools/todo.js +0 -213
- package/dist/tools/web_fetch.js +0 -195
- package/dist/tools/web_search.js +0 -228
- package/dist/tools/write.js +0 -55
- package/dist/tui/App.js +0 -126
- package/dist/tui/ConfirmBar.js +0 -66
- package/dist/tui/InputBar.js +0 -616
- package/dist/tui/MessageList.js +0 -718
- package/dist/tui/ModelPicker.js +0 -113
- package/dist/tui/QuestionBar.js +0 -136
- package/dist/tui/SessionPicker.js +0 -79
- package/dist/tui/Spinner.js +0 -6
- package/dist/tui/StatusBar.js +0 -22
- package/dist/tui/ThinkPicker.js +0 -77
- package/dist/tui/agent-runner.js +0 -223
- package/dist/tui/bracketed-paste.js +0 -37
- package/dist/tui/caret-pos.js +0 -179
- package/dist/tui/caret.js +0 -69
- package/dist/tui/click-count.js +0 -13
- package/dist/tui/diff-view.js +0 -61
- package/dist/tui/drag-state.js +0 -49
- package/dist/tui/hydrate.js +0 -129
- package/dist/tui/index.js +0 -258
- package/dist/tui/input-history.js +0 -125
- package/dist/tui/layout.js +0 -105
- package/dist/tui/mouse.js +0 -46
- package/dist/tui/overlay-input.js +0 -12
- package/dist/tui/paste-draft.js +0 -173
- package/dist/tui/prompt-queue.js +0 -24
- package/dist/tui/selection.js +0 -232
- package/dist/tui/session-switch.js +0 -28
- package/dist/tui/slash-commands.js +0 -123
- package/dist/tui/slash-handler.js +0 -589
- package/dist/tui/text-width.js +0 -113
- package/dist/tui/theme.js +0 -12
- package/dist/tui/token-info.js +0 -7
- package/dist/tui/tool-children.js +0 -19
- package/dist/tui/types.js +0 -1
- package/dist/tui/undo-stack.js +0 -14
- package/dist/tui/use-sgr-mouse.js +0 -29
- package/dist/tui-chat.js +0 -456
- package/dist/updater.js +0 -116
- package/dist/xml-search.js +0 -194
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
- package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
package/dist/tools/bash.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { confirm, isDangerousCommand, isAutoApprove } from "../confirm.js";
|
|
5
|
-
import { writeFullToolOutput } from "../tool-output.js";
|
|
6
|
-
import { planSandboxedSpawn } from "../sandbox.js";
|
|
7
|
-
import { getEffectiveConfig } from "../config.js";
|
|
8
|
-
const MAX_STREAM_BYTES = 200 * 1024;
|
|
9
|
-
const MAX_OUTPUT_BYTES = 100_000;
|
|
10
|
-
/** Default wall-clock limit so one hung command cannot stall the whole turn. */
|
|
11
|
-
export const DEFAULT_BASH_TIMEOUT_MS = 10 * 60 * 1000;
|
|
12
|
-
/** Resolve the timeout for a command: explicit > config > default. 0 disables. */
|
|
13
|
-
export function resolveBashTimeout(explicit) {
|
|
14
|
-
if (typeof explicit === "number" && Number.isFinite(explicit) && explicit >= 0)
|
|
15
|
-
return Math.floor(explicit);
|
|
16
|
-
const configured = getEffectiveConfig().tools?.bashTimeoutMs;
|
|
17
|
-
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0)
|
|
18
|
-
return Math.floor(configured);
|
|
19
|
-
return DEFAULT_BASH_TIMEOUT_MS;
|
|
20
|
-
}
|
|
21
|
-
/** Track active child processes so they can be killed on abort (e.g. ESC). */
|
|
22
|
-
const activeRuns = new Set();
|
|
23
|
-
let sigintRegistered = false;
|
|
24
|
-
/** Sandbox caveats are shown once per process to avoid spamming every command. */
|
|
25
|
-
let sandboxWarningShown = false;
|
|
26
|
-
/** Test hook: allow the sandbox caveat to be shown again. */
|
|
27
|
-
export function resetSandboxWarningShown() {
|
|
28
|
-
sandboxWarningShown = false;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Registered exactly once: Ctrl+C kills all active commands instead of leaking a handler per call.
|
|
32
|
-
* When nothing is running, re-dispatch the signal so the process can exit normally
|
|
33
|
-
* (single-shot mode must not swallow Ctrl+C).
|
|
34
|
-
*/
|
|
35
|
-
function ensureSigintHandler() {
|
|
36
|
-
if (sigintRegistered)
|
|
37
|
-
return;
|
|
38
|
-
sigintRegistered = true;
|
|
39
|
-
const onSigint = () => {
|
|
40
|
-
if (activeRuns.size === 0) {
|
|
41
|
-
process.removeListener("SIGINT", onSigint);
|
|
42
|
-
sigintRegistered = false;
|
|
43
|
-
process.kill(process.pid, "SIGINT");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
for (const run of [...activeRuns]) {
|
|
47
|
-
interruptRun(run);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
process.on("SIGINT", onSigint);
|
|
51
|
-
}
|
|
52
|
-
function interruptRun(run) {
|
|
53
|
-
run.killed = true;
|
|
54
|
-
run.settled = true;
|
|
55
|
-
if (run.timer)
|
|
56
|
-
clearTimeout(run.timer);
|
|
57
|
-
killProcess(run.proc.pid);
|
|
58
|
-
run.resolve(formatOutput(run) + "\n\n[Command interrupted by user.]");
|
|
59
|
-
}
|
|
60
|
-
/** Kill all active child processes spawned by the bash tool. */
|
|
61
|
-
export function killActiveProcesses() {
|
|
62
|
-
for (const run of activeRuns) {
|
|
63
|
-
run.killed = true;
|
|
64
|
-
run.settled = true;
|
|
65
|
-
if (run.timer)
|
|
66
|
-
clearTimeout(run.timer);
|
|
67
|
-
killProcess(run.proc.pid);
|
|
68
|
-
run.resolve(formatOutput(run) + "\n\n[Command interrupted by user.]");
|
|
69
|
-
}
|
|
70
|
-
activeRuns.clear();
|
|
71
|
-
}
|
|
72
|
-
export const bashTool = tool({
|
|
73
|
-
description: "Run a shell command. Use this for system operations, running builds, tests, git commands, etc. The command runs in the current working directory (or `cwd` if specified). Commands are killed after 10 minutes by default; set `timeout` for anything expected to run longer (or shorter).",
|
|
74
|
-
inputSchema: jsonSchema({
|
|
75
|
-
type: "object",
|
|
76
|
-
properties: {
|
|
77
|
-
command: { type: "string", description: "The shell command to execute" },
|
|
78
|
-
timeout: {
|
|
79
|
-
type: "number",
|
|
80
|
-
description: "Timeout in milliseconds. Defaults to 600000 (10 minutes). Set a smaller value for quick commands or a larger one for long builds.",
|
|
81
|
-
},
|
|
82
|
-
cwd: {
|
|
83
|
-
type: "string",
|
|
84
|
-
description: "Working directory for the command (relative to the current directory or absolute)",
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
required: ["command"],
|
|
88
|
-
}),
|
|
89
|
-
execute: async ({ command, timeout, cwd }) => {
|
|
90
|
-
if (!isAutoApprove() && isDangerousCommand(command)) {
|
|
91
|
-
const approved = await confirm(`Execute dangerous command: ${command}`);
|
|
92
|
-
if (!approved)
|
|
93
|
-
return "Command rejected by user.";
|
|
94
|
-
}
|
|
95
|
-
return executeBash(command, timeout, cwd);
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
/** Shared bash execution engine (no permission prompt). Used by bashTool and the read-only variant. */
|
|
99
|
-
export async function executeBash(command, timeout, cwd) {
|
|
100
|
-
ensureSigintHandler();
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
const isWin = process.platform === "win32";
|
|
103
|
-
const resolvedCwd = cwd ? path.resolve(process.cwd(), cwd) : process.cwd();
|
|
104
|
-
const plan = planSandboxedSpawn(command, resolvedCwd, {
|
|
105
|
-
...process.env,
|
|
106
|
-
...(isWin ? { PYTHONIOENCODING: "utf-8" } : {}),
|
|
107
|
-
});
|
|
108
|
-
if ("error" in plan) {
|
|
109
|
-
resolve(plan.error);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
let notice = "";
|
|
113
|
-
if (plan.warning && !sandboxWarningShown) {
|
|
114
|
-
sandboxWarningShown = true;
|
|
115
|
-
notice = `${plan.warning}\n\n`;
|
|
116
|
-
}
|
|
117
|
-
const run = {
|
|
118
|
-
proc: undefined,
|
|
119
|
-
resolve: (value) => resolve(notice + value),
|
|
120
|
-
stdoutChunks: [],
|
|
121
|
-
stderrChunks: [],
|
|
122
|
-
killed: false,
|
|
123
|
-
settled: false,
|
|
124
|
-
stdoutTruncated: false,
|
|
125
|
-
stderrTruncated: false,
|
|
126
|
-
};
|
|
127
|
-
activeRuns.add(run);
|
|
128
|
-
const proc = spawn(plan.file, plan.args, {
|
|
129
|
-
shell: plan.shell,
|
|
130
|
-
cwd: plan.cwd,
|
|
131
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
132
|
-
detached: process.platform !== "win32",
|
|
133
|
-
env: plan.env,
|
|
134
|
-
});
|
|
135
|
-
run.proc = proc;
|
|
136
|
-
const collect = (chunks, truncated) => (chunk) => {
|
|
137
|
-
const total = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
138
|
-
if (total >= MAX_STREAM_BYTES) {
|
|
139
|
-
run[truncated] = true;
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
chunks.push(chunk);
|
|
143
|
-
};
|
|
144
|
-
proc.stdout?.on("data", collect(run.stdoutChunks, "stdoutTruncated"));
|
|
145
|
-
proc.stderr?.on("data", collect(run.stderrChunks, "stderrTruncated"));
|
|
146
|
-
// Timeout kill (explicit value, config default, or the 10 minute fallback)
|
|
147
|
-
const effectiveTimeout = resolveBashTimeout(timeout);
|
|
148
|
-
if (effectiveTimeout > 0) {
|
|
149
|
-
run.timer = setTimeout(() => {
|
|
150
|
-
run.killed = true;
|
|
151
|
-
run.settled = true;
|
|
152
|
-
killProcess(proc.pid);
|
|
153
|
-
run.resolve(formatOutput(run) +
|
|
154
|
-
`\n\n[Command timed out after ${effectiveTimeout}ms and was killed. Retry with a larger timeout if needed.]`);
|
|
155
|
-
}, effectiveTimeout);
|
|
156
|
-
}
|
|
157
|
-
proc.on("close", (code) => {
|
|
158
|
-
activeRuns.delete(run);
|
|
159
|
-
if (run.timer)
|
|
160
|
-
clearTimeout(run.timer);
|
|
161
|
-
if (run.killed || run.settled)
|
|
162
|
-
return;
|
|
163
|
-
run.settled = true;
|
|
164
|
-
const output = formatOutput(run);
|
|
165
|
-
if (code === 0) {
|
|
166
|
-
run.resolve(output || "(no output)");
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
run.resolve(`Exit code ${code}\n${output || "(no output)"}`);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
proc.on("error", (err) => {
|
|
173
|
-
activeRuns.delete(run);
|
|
174
|
-
if (run.timer)
|
|
175
|
-
clearTimeout(run.timer);
|
|
176
|
-
if (run.killed || run.settled)
|
|
177
|
-
return;
|
|
178
|
-
run.settled = true;
|
|
179
|
-
run.resolve(`Error: ${err.message}`);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
/** Merge stdout + stderr, saved to disk when too large for the response budget. */
|
|
184
|
-
function formatOutput(run) {
|
|
185
|
-
const out = Buffer.concat(run.stdoutChunks).toString("utf-8").trim();
|
|
186
|
-
const err = Buffer.concat(run.stderrChunks).toString("utf-8").trim();
|
|
187
|
-
let text = out;
|
|
188
|
-
if (err)
|
|
189
|
-
text = text ? `${text}\n\n--- stderr ---\n${err}` : err;
|
|
190
|
-
if (run.stdoutTruncated || run.stderrTruncated) {
|
|
191
|
-
text += `\n\n[Output capped at ${MAX_STREAM_BYTES} bytes; the remainder was discarded.]`;
|
|
192
|
-
}
|
|
193
|
-
const total = Buffer.byteLength(text, "utf-8");
|
|
194
|
-
if (total <= MAX_OUTPUT_BYTES)
|
|
195
|
-
return text;
|
|
196
|
-
const filePath = writeFullToolOutput(text);
|
|
197
|
-
return `${truncateUtf8(text, MAX_OUTPUT_BYTES)}\n\nFull output saved to: ${filePath}`;
|
|
198
|
-
}
|
|
199
|
-
function truncateUtf8(text, maxBytes) {
|
|
200
|
-
const buf = Buffer.from(text, "utf-8");
|
|
201
|
-
const headLen = Math.floor(maxBytes * 0.5);
|
|
202
|
-
const tailLen = Math.floor(maxBytes * 0.1);
|
|
203
|
-
let h = headLen;
|
|
204
|
-
while (h > 0 && (buf[h] & 0xc0) === 0x80)
|
|
205
|
-
h--;
|
|
206
|
-
let t = buf.length - tailLen;
|
|
207
|
-
while (t < buf.length && (buf[t] & 0xc0) === 0x80)
|
|
208
|
-
t++;
|
|
209
|
-
return (buf.subarray(0, h).toString("utf-8") +
|
|
210
|
-
`\n\n...(truncated, ${buf.length} bytes total)...\n\n` +
|
|
211
|
-
buf.subarray(Math.max(t, h)).toString("utf-8"));
|
|
212
|
-
}
|
|
213
|
-
function killProcess(pid) {
|
|
214
|
-
if (!pid)
|
|
215
|
-
return;
|
|
216
|
-
try {
|
|
217
|
-
if (process.platform !== "win32") {
|
|
218
|
-
process.kill(-pid, "SIGTERM");
|
|
219
|
-
setTimeout(() => {
|
|
220
|
-
try {
|
|
221
|
-
process.kill(-pid, "SIGKILL");
|
|
222
|
-
}
|
|
223
|
-
catch { }
|
|
224
|
-
}, 3000);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
process.kill(pid, "SIGTERM");
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
catch { }
|
|
231
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import { truncateToolOutput } from "../tool-output.js";
|
|
3
|
-
import { getEffectiveSandboxPolicy, networkDeniedMessage } from "../sandbox.js";
|
|
4
|
-
const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
5
|
-
export const codeSearchTool = tool({
|
|
6
|
-
description: `Search and get relevant context for any programming task using Exa Code API.
|
|
7
|
-
Provides high-quality, fresh context for libraries, SDKs, and APIs.
|
|
8
|
-
Use this for ANY question or task related to programming — finding code examples, documentation, API references, and patterns.
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
- Adjustable token count (1000-50000) for focused or comprehensive results
|
|
12
|
-
- Default 5000 tokens for balanced context
|
|
13
|
-
- Examples: 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Rust async trait implementation'`,
|
|
14
|
-
inputSchema: jsonSchema({
|
|
15
|
-
type: "object",
|
|
16
|
-
properties: {
|
|
17
|
-
query: {
|
|
18
|
-
type: "string",
|
|
19
|
-
description: "Search query for APIs, libraries, SDKs. E.g. 'React useState hook examples', 'Express.js middleware'",
|
|
20
|
-
},
|
|
21
|
-
tokensNum: {
|
|
22
|
-
type: "number",
|
|
23
|
-
description: "Number of tokens to return (1000-50000). Default 5000. Use lower for focused queries, higher for comprehensive docs.",
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
required: ["query"],
|
|
27
|
-
}),
|
|
28
|
-
execute: async ({ query, tokensNum }) => {
|
|
29
|
-
if (getEffectiveSandboxPolicy().network === "deny")
|
|
30
|
-
return `Code search error: ${networkDeniedMessage()}`;
|
|
31
|
-
if (!process.env.EXA_API_KEY?.trim()) {
|
|
32
|
-
return "Code search error: EXA_API_KEY is not set. Configure it in your environment to enable code search.";
|
|
33
|
-
}
|
|
34
|
-
const tokens = Math.max(1000, Math.min(50000, tokensNum ?? 5000));
|
|
35
|
-
try {
|
|
36
|
-
const result = await callExaCode(query, tokens);
|
|
37
|
-
if (!result) {
|
|
38
|
-
return "No code snippets or documentation found. Try a different query, be more specific about the library or concept, or check spelling.";
|
|
39
|
-
}
|
|
40
|
-
return truncateToolOutput(result, { direction: "head" }).content;
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
return `Code search error: ${err instanceof Error ? err.message : String(err)}`;
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
/**
|
|
48
|
-
* Call Exa's MCP endpoint for code context.
|
|
49
|
-
* Uses JSON-RPC over HTTP with SSE response format.
|
|
50
|
-
*/
|
|
51
|
-
async function callExaCode(query, tokensNum) {
|
|
52
|
-
const body = JSON.stringify({
|
|
53
|
-
jsonrpc: "2.0",
|
|
54
|
-
id: 1,
|
|
55
|
-
method: "tools/call",
|
|
56
|
-
params: {
|
|
57
|
-
name: "get_code_context_exa",
|
|
58
|
-
arguments: { query, tokensNum },
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
const response = await fetch(EXA_MCP_URL, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: {
|
|
64
|
-
"Content-Type": "application/json",
|
|
65
|
-
Accept: "application/json, text/event-stream",
|
|
66
|
-
...(process.env.EXA_API_KEY ? { Authorization: `Bearer ${process.env.EXA_API_KEY}` } : {}),
|
|
67
|
-
},
|
|
68
|
-
body,
|
|
69
|
-
signal: AbortSignal.timeout(30000),
|
|
70
|
-
});
|
|
71
|
-
if (!response.ok) {
|
|
72
|
-
throw new Error(`Exa API returned ${response.status}`);
|
|
73
|
-
}
|
|
74
|
-
return await parseExaResponse(await response.text());
|
|
75
|
-
}
|
|
76
|
-
/** Parse either an SSE stream or a direct JSON body into the tool result text. */
|
|
77
|
-
async function parseExaResponse(text) {
|
|
78
|
-
for (const line of text.split("\n")) {
|
|
79
|
-
if (!line.startsWith("data: "))
|
|
80
|
-
continue;
|
|
81
|
-
try {
|
|
82
|
-
const data = JSON.parse(line.slice(6));
|
|
83
|
-
const content = data?.result?.content?.[0]?.text;
|
|
84
|
-
if (content)
|
|
85
|
-
return content;
|
|
86
|
-
}
|
|
87
|
-
catch { }
|
|
88
|
-
}
|
|
89
|
-
// Try parsing as direct JSON response
|
|
90
|
-
try {
|
|
91
|
-
const data = JSON.parse(text);
|
|
92
|
-
const content = data?.result?.content?.[0]?.text;
|
|
93
|
-
if (content)
|
|
94
|
-
return content;
|
|
95
|
-
}
|
|
96
|
-
catch { }
|
|
97
|
-
return null;
|
|
98
|
-
}
|
package/dist/tools/edit.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import { readFile } from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { confirm, isEditAutoApprove } from "../confirm.js";
|
|
5
|
-
import { pathAccessError } from "../sandbox.js";
|
|
6
|
-
import { atomicWriteFile } from "./atomic-file.js";
|
|
7
|
-
export const editTool = tool({
|
|
8
|
-
description: "Edit a file by replacing a specific text block with new content. The oldText must match exactly (including whitespace and indentation). Use this for precise edits instead of rewriting entire files.",
|
|
9
|
-
inputSchema: jsonSchema({
|
|
10
|
-
type: "object",
|
|
11
|
-
properties: {
|
|
12
|
-
filePath: { type: "string", description: "Path to the file to edit (relative to cwd or absolute)" },
|
|
13
|
-
oldText: { type: "string", description: "The exact text to find and replace (must match exactly)" },
|
|
14
|
-
newText: { type: "string", description: "The new text to replace it with" },
|
|
15
|
-
},
|
|
16
|
-
required: ["filePath", "oldText", "newText"],
|
|
17
|
-
}),
|
|
18
|
-
execute: async ({ filePath, oldText, newText }) => {
|
|
19
|
-
if (!oldText)
|
|
20
|
-
return "Error: oldText must not be empty";
|
|
21
|
-
if (oldText === newText)
|
|
22
|
-
return "No change: oldText equals newText";
|
|
23
|
-
const denied = pathAccessError(filePath, "write");
|
|
24
|
-
if (denied)
|
|
25
|
-
return `Error: ${denied}`;
|
|
26
|
-
const resolved = path.resolve(process.cwd(), filePath);
|
|
27
|
-
let content;
|
|
28
|
-
try {
|
|
29
|
-
content = await readFile(resolved, "utf-8");
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
return `Error: cannot read ${filePath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
33
|
-
}
|
|
34
|
-
const occurrences = content.split(oldText).length - 1;
|
|
35
|
-
if (occurrences === 0) {
|
|
36
|
-
// Try to help: show nearby content
|
|
37
|
-
const lines = content.split("\n");
|
|
38
|
-
const searchLines = oldText.split("\n");
|
|
39
|
-
const firstLine = searchLines[0].trim();
|
|
40
|
-
const nearbyIdx = lines.findIndex((l) => l.includes(firstLine));
|
|
41
|
-
if (nearbyIdx >= 0) {
|
|
42
|
-
const context = lines.slice(Math.max(0, nearbyIdx - 1), nearbyIdx + 3).join("\n");
|
|
43
|
-
return `Error: oldText not found exactly. Found similar content near line ${nearbyIdx + 1}:\n${context}\n\nMake sure whitespace and indentation match exactly.`;
|
|
44
|
-
}
|
|
45
|
-
return `Error: oldText not found in ${filePath}. Make sure the text matches exactly including whitespace.`;
|
|
46
|
-
}
|
|
47
|
-
if (occurrences > 1) {
|
|
48
|
-
return `Error: oldText found ${occurrences} times in ${filePath}. Please provide more context to make the match unique.`;
|
|
49
|
-
}
|
|
50
|
-
// Confirm edit
|
|
51
|
-
if (!isEditAutoApprove()) {
|
|
52
|
-
const preview = (s) => (s.length > 80 ? s.slice(0, 80) + "..." : s);
|
|
53
|
-
const approved = await confirm(`Edit ${filePath}:\nreplace "${preview(oldText)}"\nwith "${preview(newText)}"`);
|
|
54
|
-
if (!approved)
|
|
55
|
-
return "Edit rejected by user.";
|
|
56
|
-
}
|
|
57
|
-
const updated = content.replace(oldText, newText);
|
|
58
|
-
try {
|
|
59
|
-
await atomicWriteFile(resolved, updated);
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
return `Error: cannot write ${filePath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
63
|
-
}
|
|
64
|
-
const oldLines = oldText.split("\n").length;
|
|
65
|
-
const newLines = newText.split("\n").length;
|
|
66
|
-
return `Edited ${filePath}: replaced ${oldLines} line(s) with ${newLines} line(s)`;
|
|
67
|
-
},
|
|
68
|
-
});
|
package/dist/tools/explore.js
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
|
-
import { resolveModel } from "../provider.js";
|
|
3
|
-
import { executeBash } from "./bash.js";
|
|
4
|
-
import { readTool } from "./read.js";
|
|
5
|
-
import { globTool } from "./glob.js";
|
|
6
|
-
import { grepTool } from "./grep.js";
|
|
7
|
-
import { stripThinkingFromAssistantText } from "../assistant-stream.js";
|
|
8
|
-
import { unwrapXmlSearchTags } from "../xml-search.js";
|
|
9
|
-
import { truncateToolOutput } from "../tool-output.js";
|
|
10
|
-
import { log } from "../logger.js";
|
|
11
|
-
import { getEffectiveConfig } from "../config.js";
|
|
12
|
-
function exploreSystem() {
|
|
13
|
-
return `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
14
|
-
|
|
15
|
-
Your strengths:
|
|
16
|
-
- Rapidly finding files using glob patterns
|
|
17
|
-
- Searching code and text with powerful regex patterns
|
|
18
|
-
- Reading and analyzing file contents
|
|
19
|
-
|
|
20
|
-
Guidelines:
|
|
21
|
-
- Use glob for broad file pattern matching
|
|
22
|
-
- Use grep for searching file contents with regex
|
|
23
|
-
- Use read when you know the specific file path
|
|
24
|
-
- Use bash ONLY for read-only inspection (ls, find, cat, wc, head, tail, grep, git status/log/diff)
|
|
25
|
-
- Adapt your search approach based on the thoroughness level specified
|
|
26
|
-
- Return file paths as absolute paths in your final response
|
|
27
|
-
- Do NOT create, modify, or delete any files
|
|
28
|
-
- Do NOT run commands that change system state
|
|
29
|
-
|
|
30
|
-
Complete the search request efficiently and report findings clearly.
|
|
31
|
-
|
|
32
|
-
Working directory: ${process.cwd()}
|
|
33
|
-
Platform: ${process.platform}`;
|
|
34
|
-
}
|
|
35
|
-
const EXPLORE_MAX_STEPS = 20;
|
|
36
|
-
function resolveExploreSteps(explicit) {
|
|
37
|
-
const configured = getEffectiveConfig().agent?.exploreMaxSteps;
|
|
38
|
-
const value = explicit ?? configured ?? EXPLORE_MAX_STEPS;
|
|
39
|
-
return Number.isFinite(value) && value >= 1 ? Math.floor(value) : EXPLORE_MAX_STEPS;
|
|
40
|
-
}
|
|
41
|
-
export function createExploreTool(modelId, abortSignal, onUsage, context = {}) {
|
|
42
|
-
return tool({
|
|
43
|
-
description: `Deep codebase exploration agent. Use this to understand project structure, find files by patterns, search code for keywords, trace module relationships, or answer questions about the codebase.
|
|
44
|
-
|
|
45
|
-
Specify thoroughness:
|
|
46
|
-
- "quick": basic search, 1-3 tool calls
|
|
47
|
-
- "medium": moderate exploration, follow references
|
|
48
|
-
- "thorough": comprehensive analysis across multiple locations and naming conventions
|
|
49
|
-
|
|
50
|
-
Examples:
|
|
51
|
-
- "Find all API route handlers" (medium)
|
|
52
|
-
- "How does the auth system work?" (thorough)
|
|
53
|
-
- "Where is the database config?" (quick)`,
|
|
54
|
-
inputSchema: jsonSchema({
|
|
55
|
-
type: "object",
|
|
56
|
-
properties: {
|
|
57
|
-
query: { type: "string", description: "What to explore or find in the codebase" },
|
|
58
|
-
thoroughness: { type: "string", description: "Search depth: quick, medium, or thorough (default: medium)" },
|
|
59
|
-
},
|
|
60
|
-
required: ["query"],
|
|
61
|
-
}),
|
|
62
|
-
execute: async ({ query, thoroughness }) => {
|
|
63
|
-
const level = thoroughness ?? "medium";
|
|
64
|
-
console.error(`\x1b[90m ┌─ Explore (${level}): ${query.slice(0, 60)}\x1b[0m`);
|
|
65
|
-
try {
|
|
66
|
-
const result = await runExploreAgent(query, level, modelId, abortSignal, context);
|
|
67
|
-
if (result.usage)
|
|
68
|
-
onUsage?.(result.usage);
|
|
69
|
-
console.error(`\x1b[90m └─ ✓ Done\x1b[0m`);
|
|
70
|
-
return truncateToolOutput(result.text, { direction: "tail" }).content;
|
|
71
|
-
}
|
|
72
|
-
catch (err) {
|
|
73
|
-
if (abortSignal?.aborted)
|
|
74
|
-
return "Explore cancelled by user.";
|
|
75
|
-
log("error", `explore agent failed: ${err?.message ?? String(err)}`);
|
|
76
|
-
console.error(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
|
|
77
|
-
return `Explore error: ${err.message}`;
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
async function runExploreAgent(query, thoroughness, modelId, abortSignal, context = {}) {
|
|
83
|
-
const model = resolveModel(modelId);
|
|
84
|
-
// Read-only tools only
|
|
85
|
-
const tools = {
|
|
86
|
-
glob: globTool,
|
|
87
|
-
grep: grepTool,
|
|
88
|
-
read: readTool,
|
|
89
|
-
bash: readOnlyBashTool,
|
|
90
|
-
};
|
|
91
|
-
const prompt = `Thoroughness level: ${thoroughness}
|
|
92
|
-
${thoroughness === "quick" ? "Do a quick search (1-3 tool calls max)." : ""}
|
|
93
|
-
${thoroughness === "medium" ? "Do a moderate exploration, follow references if needed." : ""}
|
|
94
|
-
${thoroughness === "thorough" ? "Do a comprehensive analysis. Check multiple locations, naming conventions, and cross-references." : ""}
|
|
95
|
-
|
|
96
|
-
Task: ${query}`;
|
|
97
|
-
const messages = [{ role: "user", content: prompt }];
|
|
98
|
-
const controller = new AbortController();
|
|
99
|
-
const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
|
|
100
|
-
const result = streamText({
|
|
101
|
-
model,
|
|
102
|
-
system: exploreSystem(),
|
|
103
|
-
messages,
|
|
104
|
-
tools,
|
|
105
|
-
stopWhen: stepCountIs(resolveExploreSteps(context.maxSteps)),
|
|
106
|
-
maxRetries: 2,
|
|
107
|
-
abortSignal: signal,
|
|
108
|
-
onError({ error }) {
|
|
109
|
-
log("warn", `explore step failed (will retry): ${String(error)}`);
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
let assistantText = "";
|
|
113
|
-
for await (const event of result.fullStream) {
|
|
114
|
-
switch (event.type) {
|
|
115
|
-
case "text-delta":
|
|
116
|
-
assistantText += event.text;
|
|
117
|
-
break;
|
|
118
|
-
case "tool-call":
|
|
119
|
-
console.error(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
|
|
120
|
-
if (context.shouldStop?.()) {
|
|
121
|
-
log("warn", "explore agent stopped: parent budget reached");
|
|
122
|
-
controller.abort();
|
|
123
|
-
return {
|
|
124
|
-
text: unwrapXmlSearchTags(stripThinkingFromAssistantText(assistantText)) +
|
|
125
|
-
"\n\n[Explore stopped: run budget reached]",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
case "error":
|
|
130
|
-
log("error", `explore stream error: ${String(event.error)}`);
|
|
131
|
-
controller.abort();
|
|
132
|
-
return {
|
|
133
|
-
text: unwrapXmlSearchTags(stripThinkingFromAssistantText(assistantText)) || `[Explore error: ${event.error}]`,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
let usage;
|
|
138
|
-
try {
|
|
139
|
-
usage = await result.usage;
|
|
140
|
-
}
|
|
141
|
-
catch { }
|
|
142
|
-
return {
|
|
143
|
-
text: unwrapXmlSearchTags(stripThinkingFromAssistantText(assistantText)) || "(explore agent produced no output)",
|
|
144
|
-
usage,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
/* ── Read-only bash: white-listed commands only, no redirection / chaining ── */
|
|
148
|
-
const READ_ONLY_BINS = new Set([
|
|
149
|
-
"ls",
|
|
150
|
-
"find",
|
|
151
|
-
"cat",
|
|
152
|
-
"wc",
|
|
153
|
-
"head",
|
|
154
|
-
"tail",
|
|
155
|
-
"grep",
|
|
156
|
-
"rg",
|
|
157
|
-
"tree",
|
|
158
|
-
"pwd",
|
|
159
|
-
"stat",
|
|
160
|
-
"du",
|
|
161
|
-
"file",
|
|
162
|
-
"which",
|
|
163
|
-
"echo",
|
|
164
|
-
"printf",
|
|
165
|
-
"sort",
|
|
166
|
-
"uniq",
|
|
167
|
-
"cut",
|
|
168
|
-
"tr",
|
|
169
|
-
"awk",
|
|
170
|
-
"sed",
|
|
171
|
-
"basename",
|
|
172
|
-
"dirname",
|
|
173
|
-
"realpath",
|
|
174
|
-
"env",
|
|
175
|
-
"printenv",
|
|
176
|
-
"git",
|
|
177
|
-
]);
|
|
178
|
-
const READ_ONLY_GIT_CMDS = new Set([
|
|
179
|
-
"status",
|
|
180
|
-
"log",
|
|
181
|
-
"diff",
|
|
182
|
-
"show",
|
|
183
|
-
"rev-parse",
|
|
184
|
-
"branch",
|
|
185
|
-
"remote",
|
|
186
|
-
"tag",
|
|
187
|
-
"ls-files",
|
|
188
|
-
"help",
|
|
189
|
-
"--version",
|
|
190
|
-
"version",
|
|
191
|
-
]);
|
|
192
|
-
/** True if the command only inspects state (no writes, redirection, pipes, or chaining). */
|
|
193
|
-
export function isReadOnlyCommand(command) {
|
|
194
|
-
const trimmed = command.trim();
|
|
195
|
-
if (!trimmed)
|
|
196
|
-
return false;
|
|
197
|
-
if (/[<>|]|&&|;|`|\$\(/.test(trimmed))
|
|
198
|
-
return false;
|
|
199
|
-
const tokens = trimmed.split(/\s+/);
|
|
200
|
-
const bin = tokens[0].toLowerCase();
|
|
201
|
-
if (!READ_ONLY_BINS.has(bin))
|
|
202
|
-
return false;
|
|
203
|
-
if (bin === "git") {
|
|
204
|
-
const sub = tokens[1]?.toLowerCase();
|
|
205
|
-
if (!sub || !READ_ONLY_GIT_CMDS.has(sub))
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
if (bin === "find" && /\s-(delete|exec|ok|execdir)\b/.test(trimmed))
|
|
209
|
-
return false;
|
|
210
|
-
if ((bin === "sed" || bin === "awk" || bin === "perl") && /\s-i(\.[a-zA-Z0-9_-]+)?\b/.test(trimmed))
|
|
211
|
-
return false;
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
export const readOnlyBashTool = tool({
|
|
215
|
-
description: "Run a READ-ONLY shell command (ls, find, cat, wc, head, tail, grep, git status/log/diff, etc.). " +
|
|
216
|
-
"File modification, redirection, and command chaining are blocked.",
|
|
217
|
-
inputSchema: jsonSchema({
|
|
218
|
-
type: "object",
|
|
219
|
-
properties: {
|
|
220
|
-
command: { type: "string", description: "The read-only shell command to execute" },
|
|
221
|
-
timeout: { type: "number", description: "Timeout in milliseconds" },
|
|
222
|
-
cwd: { type: "string", description: "Working directory (relative or absolute)" },
|
|
223
|
-
},
|
|
224
|
-
required: ["command"],
|
|
225
|
-
}),
|
|
226
|
-
execute: async ({ command, timeout, cwd }) => {
|
|
227
|
-
if (!isReadOnlyCommand(command)) {
|
|
228
|
-
return `Error: "${command.slice(0, 80)}" is not a read-only command. Allowed: ${[...READ_ONLY_BINS].join(", ")} (read-only git subcommands only).`;
|
|
229
|
-
}
|
|
230
|
-
return executeBash(command, timeout, cwd);
|
|
231
|
-
},
|
|
232
|
-
});
|
package/dist/tools/glob.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import { glob } from "glob";
|
|
3
|
-
import { truncateToolOutput } from "../tool-output.js";
|
|
4
|
-
import { pathAccessError } from "../sandbox.js";
|
|
5
|
-
export const globTool = tool({
|
|
6
|
-
description: "Find files matching a glob pattern. Returns a list of file paths. Use this to discover project structure and find files.",
|
|
7
|
-
inputSchema: jsonSchema({
|
|
8
|
-
type: "object",
|
|
9
|
-
properties: {
|
|
10
|
-
pattern: { type: "string", description: "Glob pattern to match (e.g. 'src/**/*.ts', '*.json')" },
|
|
11
|
-
cwd: { type: "string", description: "Directory to search in (defaults to working directory)" },
|
|
12
|
-
},
|
|
13
|
-
required: ["pattern"],
|
|
14
|
-
}),
|
|
15
|
-
execute: async ({ pattern, cwd }) => {
|
|
16
|
-
const denied = pathAccessError(cwd ?? ".", "read");
|
|
17
|
-
if (denied)
|
|
18
|
-
return `Error: ${denied}`;
|
|
19
|
-
try {
|
|
20
|
-
const matches = await glob(pattern, {
|
|
21
|
-
cwd: cwd ?? process.cwd(),
|
|
22
|
-
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
|
|
23
|
-
nodir: true,
|
|
24
|
-
});
|
|
25
|
-
if (matches.length === 0)
|
|
26
|
-
return "No files found matching pattern";
|
|
27
|
-
let text;
|
|
28
|
-
if (matches.length > 100) {
|
|
29
|
-
text = matches.slice(0, 100).join("\n") + `\n\n... (${matches.length - 100} more files)`;
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
text = matches.join("\n");
|
|
33
|
-
}
|
|
34
|
-
return truncateToolOutput(text, { direction: "head" }).content;
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
return `Error: ${err.message}`;
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
});
|