min-agent 0.5.1 → 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.
Files changed (169) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +6 -6
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +4 -2
  6. package/skills/self-config/reference.md +4 -1
  7. package/dist/agent.js +0 -1388
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/ctx.js +0 -7
  11. package/dist/cli/commands/exec.js +0 -35
  12. package/dist/cli/commands/history.js +0 -58
  13. package/dist/cli/commands/index.js +0 -249
  14. package/dist/cli/commands/init.js +0 -18
  15. package/dist/cli/commands/mcp.js +0 -173
  16. package/dist/cli/commands/memory.js +0 -87
  17. package/dist/cli/commands/models.js +0 -21
  18. package/dist/cli/commands/permission.js +0 -12
  19. package/dist/cli/commands/rules.js +0 -33
  20. package/dist/cli/commands/sandbox.js +0 -13
  21. package/dist/cli/commands/serve.js +0 -9
  22. package/dist/cli/commands/setup.js +0 -56
  23. package/dist/cli/commands/shared.js +0 -25
  24. package/dist/cli/commands/skills.js +0 -119
  25. package/dist/cli/commands/think.js +0 -12
  26. package/dist/cli/commands/update.js +0 -7
  27. package/dist/cli/commands/write-config.js +0 -52
  28. package/dist/cli/errors.js +0 -36
  29. package/dist/cli/exec-prompt.js +0 -26
  30. package/dist/cli/option-helpers.js +0 -65
  31. package/dist/cli/program.js +0 -223
  32. package/dist/cli/setup/detect.js +0 -17
  33. package/dist/cli/setup/flags.js +0 -12
  34. package/dist/cli/setup/flow.js +0 -108
  35. package/dist/cli/setup/provider-form.js +0 -102
  36. package/dist/cli/setup/ui.js +0 -534
  37. package/dist/clipboard.js +0 -142
  38. package/dist/code-mode.js +0 -184
  39. package/dist/compaction.js +0 -644
  40. package/dist/config.js +0 -363
  41. package/dist/confirm.js +0 -172
  42. package/dist/context-window.js +0 -256
  43. package/dist/ctx-cli.js +0 -30
  44. package/dist/ctx.js +0 -80
  45. package/dist/doom-loop.js +0 -278
  46. package/dist/fetch-timeout.js +0 -152
  47. package/dist/http-approvals.js +0 -60
  48. package/dist/http.js +0 -119
  49. package/dist/instructions.js +0 -196
  50. package/dist/logger.js +0 -95
  51. package/dist/markdown.js +0 -259
  52. package/dist/mcp.js +0 -1036
  53. package/dist/memory-cli.js +0 -33
  54. package/dist/memory.js +0 -295
  55. package/dist/model-catalog.js +0 -285
  56. package/dist/ollama-model.js +0 -234
  57. package/dist/ollama-openai-bridge.js +0 -383
  58. package/dist/output.js +0 -76
  59. package/dist/paste-handler.js +0 -41
  60. package/dist/permission-cli.js +0 -40
  61. package/dist/plugins.js +0 -131
  62. package/dist/pricing.js +0 -119
  63. package/dist/provider.js +0 -63
  64. package/dist/question-format.js +0 -60
  65. package/dist/reasoning-stream.js +0 -158
  66. package/dist/sandbox-cli.js +0 -79
  67. package/dist/sandbox.js +0 -403
  68. package/dist/save-throttle.js +0 -45
  69. package/dist/scope.js +0 -23
  70. package/dist/serve/common.js +0 -425
  71. package/dist/serve/routes-chat.js +0 -367
  72. package/dist/serve/routes-mcp.js +0 -212
  73. package/dist/serve/routes-memory.js +0 -95
  74. package/dist/serve/routes-meta.js +0 -268
  75. package/dist/serve/routes-sessions.js +0 -61
  76. package/dist/serve/routes-skills.js +0 -70
  77. package/dist/serve.js +0 -138
  78. package/dist/sessions.js +0 -265
  79. package/dist/skills.js +0 -599
  80. package/dist/structured-output.js +0 -29
  81. package/dist/synthetic.js +0 -7
  82. package/dist/think-cli.js +0 -36
  83. package/dist/thinking-wire.js +0 -239
  84. package/dist/thinking.js +0 -166
  85. package/dist/title-gen.js +0 -55
  86. package/dist/token-display.js +0 -39
  87. package/dist/tool-display.js +0 -178
  88. package/dist/tool-output.js +0 -126
  89. package/dist/tools/apply_patch.js +0 -265
  90. package/dist/tools/atomic-file.js +0 -35
  91. package/dist/tools/backend.js +0 -61
  92. package/dist/tools/bash.js +0 -231
  93. package/dist/tools/code_search.js +0 -98
  94. package/dist/tools/edit.js +0 -68
  95. package/dist/tools/explore.js +0 -232
  96. package/dist/tools/glob.js +0 -40
  97. package/dist/tools/grep.js +0 -174
  98. package/dist/tools/index.js +0 -34
  99. package/dist/tools/question.js +0 -54
  100. package/dist/tools/read.js +0 -109
  101. package/dist/tools/search-searxng.js +0 -223
  102. package/dist/tools/search-serper.js +0 -189
  103. package/dist/tools/task.js +0 -165
  104. package/dist/tools/todo.js +0 -213
  105. package/dist/tools/web_fetch.js +0 -195
  106. package/dist/tools/web_search.js +0 -228
  107. package/dist/tools/write.js +0 -55
  108. package/dist/tui/App.js +0 -138
  109. package/dist/tui/ConfirmBar.js +0 -66
  110. package/dist/tui/CtxPicker.js +0 -68
  111. package/dist/tui/InputBar.js +0 -618
  112. package/dist/tui/MessageList.js +0 -718
  113. package/dist/tui/ModelPicker.js +0 -113
  114. package/dist/tui/QuestionBar.js +0 -136
  115. package/dist/tui/SessionPicker.js +0 -79
  116. package/dist/tui/Spinner.js +0 -6
  117. package/dist/tui/StatusBar.js +0 -22
  118. package/dist/tui/ThinkPicker.js +0 -75
  119. package/dist/tui/agent-runner.js +0 -223
  120. package/dist/tui/bracketed-paste.js +0 -37
  121. package/dist/tui/caret-pos.js +0 -179
  122. package/dist/tui/caret.js +0 -69
  123. package/dist/tui/click-count.js +0 -13
  124. package/dist/tui/diff-view.js +0 -61
  125. package/dist/tui/drag-state.js +0 -49
  126. package/dist/tui/hydrate.js +0 -129
  127. package/dist/tui/index.js +0 -264
  128. package/dist/tui/input-history.js +0 -125
  129. package/dist/tui/layout.js +0 -105
  130. package/dist/tui/mouse.js +0 -46
  131. package/dist/tui/overlay-input.js +0 -12
  132. package/dist/tui/paste-draft.js +0 -173
  133. package/dist/tui/prompt-queue.js +0 -24
  134. package/dist/tui/selection.js +0 -232
  135. package/dist/tui/session-switch.js +0 -28
  136. package/dist/tui/slash-commands.js +0 -129
  137. package/dist/tui/slash-handler.js +0 -615
  138. package/dist/tui/text-width.js +0 -113
  139. package/dist/tui/theme.js +0 -12
  140. package/dist/tui/token-info.js +0 -7
  141. package/dist/tui/tool-children.js +0 -19
  142. package/dist/tui/types.js +0 -1
  143. package/dist/tui/undo-stack.js +0 -14
  144. package/dist/tui/use-sgr-mouse.js +0 -29
  145. package/dist/tui-chat.js +0 -478
  146. package/dist/updater.js +0 -116
  147. package/dist/xml-search.js +0 -194
  148. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  149. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  150. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  151. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  152. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  153. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  154. package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
  155. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  156. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  157. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  158. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  159. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  160. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  161. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  162. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  163. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  164. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  165. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  166. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  167. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
  168. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  169. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -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
- }
@@ -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
- });
@@ -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
- });
@@ -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
- });