min-agent 0.2.1 → 0.4.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 +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for the HTTP-backed tools (`search_web`, `web_fetch`):
|
|
3
|
+
* base-URL resolution, consistent "how to configure this" copy, and fetch
|
|
4
|
+
* error description. Kept separate so both tools report problems the same way.
|
|
5
|
+
*/
|
|
6
|
+
/** Human-readable pointer to both configuration channels. */
|
|
7
|
+
export function configHint(spec) {
|
|
8
|
+
return `Configure a working instance via "${spec.configKey}" in ~/.min-agent/config.json or the ${spec.envName} env var.`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a user-supplied base URL: trim whitespace and trailing slashes so
|
|
12
|
+
* callers can always append "/path" without producing a double slash.
|
|
13
|
+
* Returns an error string for anything that is not an absolute http(s) URL.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeBaseURL(raw, source) {
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
if (trimmed === "")
|
|
18
|
+
return { ok: false, error: `${source} is empty` };
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(trimmed);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { ok: false, error: `${source} is not a valid absolute URL: ${trimmed}` };
|
|
25
|
+
}
|
|
26
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
27
|
+
return { ok: false, error: `${source} must use http or https, got "${parsed.protocol}"` };
|
|
28
|
+
}
|
|
29
|
+
// Keep any path prefix (instances behind a reverse proxy subpath) but drop
|
|
30
|
+
// query/hash and trailing slashes.
|
|
31
|
+
parsed.search = "";
|
|
32
|
+
parsed.hash = "";
|
|
33
|
+
return { ok: true, base: parsed.toString().replace(/\/+$/, "") };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a backend base URL. The environment variable wins over the config
|
|
37
|
+
* file so a single run can be redirected from the shell without editing
|
|
38
|
+
* config.json; the built-in default is the last resort.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveBackendBase(spec, configValue) {
|
|
41
|
+
const env = process.env[spec.envName];
|
|
42
|
+
if (env?.trim())
|
|
43
|
+
return normalizeBaseURL(env, spec.envName);
|
|
44
|
+
if (configValue?.trim())
|
|
45
|
+
return normalizeBaseURL(configValue, `"${spec.configKey}" in config.json`);
|
|
46
|
+
return normalizeBaseURL(spec.fallback, "the built-in default backend URL");
|
|
47
|
+
}
|
|
48
|
+
/** Turn a thrown fetch error into a short, actionable sentence. */
|
|
49
|
+
export function describeFetchError(err) {
|
|
50
|
+
if (err instanceof Error) {
|
|
51
|
+
// AbortSignal.timeout rejects with a TimeoutError DOMException.
|
|
52
|
+
if (err.name === "TimeoutError" || err.name === "AbortError")
|
|
53
|
+
return "the request timed out";
|
|
54
|
+
const cause = err.cause;
|
|
55
|
+
const causeCode = typeof cause === "object" && cause !== null ? cause.code : undefined;
|
|
56
|
+
if (typeof causeCode === "string")
|
|
57
|
+
return `${err.message} (${causeCode})`;
|
|
58
|
+
return err.message;
|
|
59
|
+
}
|
|
60
|
+
return String(err);
|
|
61
|
+
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,99 +1,214 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
+
import path from "path";
|
|
3
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
|
+
}
|
|
4
21
|
/** Track active child processes so they can be killed on abort (e.g. ESC). */
|
|
5
|
-
const
|
|
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
|
+
}
|
|
6
60
|
/** Kill all active child processes spawned by the bash tool. */
|
|
7
61
|
export function killActiveProcesses() {
|
|
8
|
-
for (const
|
|
9
|
-
|
|
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.]");
|
|
10
69
|
}
|
|
11
|
-
|
|
70
|
+
activeRuns.clear();
|
|
12
71
|
}
|
|
13
72
|
export const bashTool = tool({
|
|
14
|
-
description: "Run a shell command. Use this for system operations, running builds, tests, git commands, etc. The command runs in the current working directory
|
|
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).",
|
|
15
74
|
inputSchema: jsonSchema({
|
|
16
75
|
type: "object",
|
|
17
76
|
properties: {
|
|
18
77
|
command: { type: "string", description: "The shell command to execute" },
|
|
19
|
-
timeout: {
|
|
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
|
+
},
|
|
20
86
|
},
|
|
21
87
|
required: ["command"],
|
|
22
88
|
}),
|
|
23
|
-
execute: async ({ command, timeout }) => {
|
|
89
|
+
execute: async ({ command, timeout, cwd }) => {
|
|
24
90
|
if (!isAutoApprove() && isDangerousCommand(command)) {
|
|
25
91
|
const approved = await confirm(`Execute dangerous command: ${command}`);
|
|
26
92
|
if (!approved)
|
|
27
93
|
return "Command rejected by user.";
|
|
28
94
|
}
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
141
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
60
152
|
killProcess(proc.pid);
|
|
61
|
-
resolve(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
proc.on("error", (err) => {
|
|
80
|
-
activeProcesses.delete(proc);
|
|
81
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
82
|
-
if (timer)
|
|
83
|
-
clearTimeout(timer);
|
|
84
|
-
if (killed)
|
|
85
|
-
return;
|
|
86
|
-
resolve(`Error: ${err.message}`);
|
|
87
|
-
});
|
|
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
|
+
}
|
|
88
171
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.]`;
|
|
95
192
|
}
|
|
96
|
-
|
|
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"));
|
|
97
212
|
}
|
|
98
213
|
function killProcess(pid) {
|
|
99
214
|
if (!pid)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
2
|
import { truncateToolOutput } from "../tool-output.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
: "https://mcp.exa.ai/mcp";
|
|
3
|
+
import { getEffectiveSandboxPolicy, networkDeniedMessage } from "../sandbox.js";
|
|
4
|
+
const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
6
5
|
export const codeSearchTool = tool({
|
|
7
6
|
description: `Search and get relevant context for any programming task using Exa Code API.
|
|
8
7
|
Provides high-quality, fresh context for libraries, SDKs, and APIs.
|
|
@@ -27,6 +26,11 @@ Usage:
|
|
|
27
26
|
required: ["query"],
|
|
28
27
|
}),
|
|
29
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
|
+
}
|
|
30
34
|
const tokens = Math.max(1000, Math.min(50000, tokensNum ?? 5000));
|
|
31
35
|
try {
|
|
32
36
|
const result = await callExaCode(query, tokens);
|
|
@@ -36,7 +40,7 @@ Usage:
|
|
|
36
40
|
return truncateToolOutput(result, { direction: "head" }).content;
|
|
37
41
|
}
|
|
38
42
|
catch (err) {
|
|
39
|
-
return `Code search error: ${err.message}`;
|
|
43
|
+
return `Code search error: ${err instanceof Error ? err.message : String(err)}`;
|
|
40
44
|
}
|
|
41
45
|
},
|
|
42
46
|
});
|
|
@@ -59,6 +63,7 @@ async function callExaCode(query, tokensNum) {
|
|
|
59
63
|
headers: {
|
|
60
64
|
"Content-Type": "application/json",
|
|
61
65
|
Accept: "application/json, text/event-stream",
|
|
66
|
+
...(process.env.EXA_API_KEY ? { Authorization: `Bearer ${process.env.EXA_API_KEY}` } : {}),
|
|
62
67
|
},
|
|
63
68
|
body,
|
|
64
69
|
signal: AbortSignal.timeout(30000),
|
|
@@ -66,8 +71,10 @@ async function callExaCode(query, tokensNum) {
|
|
|
66
71
|
if (!response.ok) {
|
|
67
72
|
throw new Error(`Exa API returned ${response.status}`);
|
|
68
73
|
}
|
|
69
|
-
|
|
70
|
-
|
|
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) {
|
|
71
78
|
for (const line of text.split("\n")) {
|
|
72
79
|
if (!line.startsWith("data: "))
|
|
73
80
|
continue;
|
package/dist/tools/edit.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { confirm,
|
|
4
|
+
import { confirm, isEditAutoApprove } from "../confirm.js";
|
|
5
|
+
import { pathAccessError } from "../sandbox.js";
|
|
6
|
+
import { atomicWriteFile } from "./atomic-file.js";
|
|
5
7
|
export const editTool = tool({
|
|
6
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.",
|
|
7
9
|
inputSchema: jsonSchema({
|
|
@@ -14,11 +16,21 @@ export const editTool = tool({
|
|
|
14
16
|
required: ["filePath", "oldText", "newText"],
|
|
15
17
|
}),
|
|
16
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}`;
|
|
17
26
|
const resolved = path.resolve(process.cwd(), filePath);
|
|
18
|
-
|
|
19
|
-
|
|
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)}`;
|
|
20
33
|
}
|
|
21
|
-
const content = readFileSync(resolved, "utf-8");
|
|
22
34
|
const occurrences = content.split(oldText).length - 1;
|
|
23
35
|
if (occurrences === 0) {
|
|
24
36
|
// Try to help: show nearby content
|
|
@@ -36,14 +48,19 @@ export const editTool = tool({
|
|
|
36
48
|
return `Error: oldText found ${occurrences} times in ${filePath}. Please provide more context to make the match unique.`;
|
|
37
49
|
}
|
|
38
50
|
// Confirm edit
|
|
39
|
-
if (!
|
|
40
|
-
const preview =
|
|
41
|
-
const approved = await confirm(`Edit ${filePath}
|
|
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)}"`);
|
|
42
54
|
if (!approved)
|
|
43
55
|
return "Edit rejected by user.";
|
|
44
56
|
}
|
|
45
57
|
const updated = content.replace(oldText, newText);
|
|
46
|
-
|
|
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
|
+
}
|
|
47
64
|
const oldLines = oldText.split("\n").length;
|
|
48
65
|
const newLines = newText.split("\n").length;
|
|
49
66
|
return `Edited ${filePath}: replaced ${oldLines} line(s) with ${newLines} line(s)`;
|