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