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.
Files changed (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
@@ -1,12 +1,16 @@
1
1
  import { tool, jsonSchema, streamText, stepCountIs } from "ai";
2
2
  import { resolveModel } from "../provider.js";
3
- import { bashTool } from "./bash.js";
3
+ import { executeBash } from "./bash.js";
4
4
  import { readTool } from "./read.js";
5
5
  import { globTool } from "./glob.js";
6
6
  import { grepTool } from "./grep.js";
7
7
  import { stripThinkingFromAssistantText } from "../assistant-stream.js";
8
+ import { unwrapXmlSearchTags } from "../xml-search.js";
8
9
  import { truncateToolOutput } from "../tool-output.js";
9
- const EXPLORE_SYSTEM = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
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.
10
14
 
11
15
  Your strengths:
12
16
  - Rapidly finding files using glob patterns
@@ -17,7 +21,7 @@ Guidelines:
17
21
  - Use glob for broad file pattern matching
18
22
  - Use grep for searching file contents with regex
19
23
  - Use read when you know the specific file path
20
- - Use bash ONLY for read-only operations (ls, find, cat, wc, head, tail)
24
+ - Use bash ONLY for read-only inspection (ls, find, cat, wc, head, tail, grep, git status/log/diff)
21
25
  - Adapt your search approach based on the thoroughness level specified
22
26
  - Return file paths as absolute paths in your final response
23
27
  - Do NOT create, modify, or delete any files
@@ -27,8 +31,14 @@ Complete the search request efficiently and report findings clearly.
27
31
 
28
32
  Working directory: ${process.cwd()}
29
33
  Platform: ${process.platform}`;
34
+ }
30
35
  const EXPLORE_MAX_STEPS = 20;
31
- export function createExploreTool(modelId) {
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 = {}) {
32
42
  return tool({
33
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.
34
44
 
@@ -51,27 +61,32 @@ Examples:
51
61
  }),
52
62
  execute: async ({ query, thoroughness }) => {
53
63
  const level = thoroughness ?? "medium";
54
- console.log(`\x1b[90m ┌─ Explore (${level}): ${query.slice(0, 60)}\x1b[0m`);
64
+ console.error(`\x1b[90m ┌─ Explore (${level}): ${query.slice(0, 60)}\x1b[0m`);
55
65
  try {
56
- const result = await runExploreAgent(query, level, modelId);
57
- console.log(`\x1b[90m └─ ✓ Done\x1b[0m`);
58
- return truncateToolOutput(result, { direction: "head" }).content;
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;
59
71
  }
60
72
  catch (err) {
61
- console.log(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
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`);
62
77
  return `Explore error: ${err.message}`;
63
78
  }
64
79
  },
65
80
  });
66
81
  }
67
- async function runExploreAgent(query, thoroughness, modelId) {
82
+ async function runExploreAgent(query, thoroughness, modelId, abortSignal, context = {}) {
68
83
  const model = resolveModel(modelId);
69
84
  // Read-only tools only
70
85
  const tools = {
71
86
  glob: globTool,
72
87
  grep: grepTool,
73
88
  read: readTool,
74
- bash: bashTool, // bash is available but prompt restricts to read-only
89
+ bash: readOnlyBashTool,
75
90
  };
76
91
  const prompt = `Thoroughness level: ${thoroughness}
77
92
  ${thoroughness === "quick" ? "Do a quick search (1-3 tool calls max)." : ""}
@@ -80,14 +95,19 @@ ${thoroughness === "thorough" ? "Do a comprehensive analysis. Check multiple loc
80
95
 
81
96
  Task: ${query}`;
82
97
  const messages = [{ role: "user", content: prompt }];
98
+ const controller = new AbortController();
99
+ const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
83
100
  const result = streamText({
84
101
  model,
85
- system: EXPLORE_SYSTEM,
102
+ system: exploreSystem(),
86
103
  messages,
87
104
  tools,
88
- stopWhen: stepCountIs(EXPLORE_MAX_STEPS),
105
+ stopWhen: stepCountIs(resolveExploreSteps(context.maxSteps)),
89
106
  maxRetries: 2,
90
- onError() { },
107
+ abortSignal: signal,
108
+ onError({ error }) {
109
+ log("warn", `explore step failed (will retry): ${String(error)}`);
110
+ },
91
111
  });
92
112
  let assistantText = "";
93
113
  for await (const event of result.fullStream) {
@@ -96,9 +116,117 @@ Task: ${query}`;
96
116
  assistantText += event.text;
97
117
  break;
98
118
  case "tool-call":
99
- console.log(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
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
+ }
100
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
+ };
101
135
  }
102
136
  }
103
- return stripThinkingFromAssistantText(assistantText) || "(explore agent produced no output)";
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
+ };
104
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,6 +1,7 @@
1
1
  import { tool, jsonSchema } from "ai";
2
- import { globSync } from "glob";
2
+ import { glob } from "glob";
3
3
  import { truncateToolOutput } from "../tool-output.js";
4
+ import { pathAccessError } from "../sandbox.js";
4
5
  export const globTool = tool({
5
6
  description: "Find files matching a glob pattern. Returns a list of file paths. Use this to discover project structure and find files.",
6
7
  inputSchema: jsonSchema({
@@ -12,10 +13,13 @@ export const globTool = tool({
12
13
  required: ["pattern"],
13
14
  }),
14
15
  execute: async ({ pattern, cwd }) => {
16
+ const denied = pathAccessError(cwd ?? ".", "read");
17
+ if (denied)
18
+ return `Error: ${denied}`;
15
19
  try {
16
- const matches = globSync(pattern, {
20
+ const matches = await glob(pattern, {
17
21
  cwd: cwd ?? process.cwd(),
18
- ignore: ["**/node_modules/**", "**/.git/**"],
22
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
19
23
  nodir: true,
20
24
  });
21
25
  if (matches.length === 0)
@@ -1,8 +1,127 @@
1
1
  import { tool, jsonSchema } from "ai";
2
- import { execSync } from "child_process";
2
+ import { readdir, readFile, stat } from "fs/promises";
3
+ import { spawn } from "child_process";
4
+ import path from "path";
3
5
  import { truncateToolOutput } from "../tool-output.js";
6
+ import { pathAccessError } from "../sandbox.js";
7
+ const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".out", "coverage", ".min-agent"]);
8
+ const MAX_RESULTS = 500;
9
+ const MAX_FILE_BYTES = 5 * 1024 * 1024;
10
+ const MAX_RG_OUTPUT_BYTES = 8 * 1024 * 1024;
11
+ const WALK_CONCURRENCY = 8;
12
+ /** Convert a simple glob like "*.ts" into an anchored regex for file names. */
13
+ function globToRegex(glob) {
14
+ const escaped = glob
15
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
16
+ .replace(/\*/g, ".*")
17
+ .replace(/\?/g, ".");
18
+ return new RegExp(`^${escaped}$`);
19
+ }
20
+ /** Fast path: shell out to ripgrep when available. Resolves null if rg is missing or fails. */
21
+ function rgSearch(pattern, root, includeRegex, ignore) {
22
+ const args = [
23
+ "--no-heading",
24
+ "--line-number",
25
+ "-i",
26
+ "-e",
27
+ pattern,
28
+ "--max-count",
29
+ "1",
30
+ "-g",
31
+ "!*.lock",
32
+ "--max-filesize",
33
+ String(MAX_FILE_BYTES),
34
+ ];
35
+ for (const dir of ignore)
36
+ args.push("-g", `!${dir}/**`);
37
+ if (includeRegex) {
38
+ const glob = includeRegex.source.replace(/^\^/, "").replace(/\$$/, "").replace(/\\\./g, ".").replace(/\.\*/g, "*");
39
+ args.push("-g", glob);
40
+ }
41
+ args.push(root);
42
+ return new Promise((resolve) => {
43
+ const child = spawn("rg", args, { stdio: ["ignore", "pipe", "pipe"] });
44
+ const chunks = [];
45
+ let total = 0;
46
+ let settled = false;
47
+ const finish = (value) => {
48
+ if (settled)
49
+ return;
50
+ settled = true;
51
+ clearTimeout(timer);
52
+ resolve(value);
53
+ };
54
+ const timer = setTimeout(() => {
55
+ child.kill("SIGKILL");
56
+ finish(null);
57
+ }, 15000);
58
+ child.stdout.on("data", (chunk) => {
59
+ total += chunk.byteLength;
60
+ if (total > MAX_RG_OUTPUT_BYTES) {
61
+ child.kill("SIGKILL");
62
+ finish(null);
63
+ return;
64
+ }
65
+ chunks.push(chunk);
66
+ });
67
+ child.on("error", () => finish(null));
68
+ child.on("close", (code) => {
69
+ if (code === 1)
70
+ finish([]);
71
+ else if (code === 0)
72
+ finish(Buffer.concat(chunks).toString("utf-8").split("\n").filter(Boolean).slice(0, MAX_RESULTS));
73
+ else
74
+ finish(null);
75
+ });
76
+ });
77
+ }
78
+ async function walk(dir, regex, includeRegex, results) {
79
+ const dirQueue = [dir];
80
+ const worker = async () => {
81
+ while (dirQueue.length > 0 && results.length < MAX_RESULTS) {
82
+ const current = dirQueue.pop();
83
+ let entries;
84
+ try {
85
+ entries = await readdir(current, { withFileTypes: true });
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ await Promise.all(entries.map(async (entry) => {
91
+ const full = path.join(current, entry.name);
92
+ if (entry.isDirectory()) {
93
+ if (!IGNORED_DIRS.has(entry.name) && results.length < MAX_RESULTS)
94
+ dirQueue.push(full);
95
+ return;
96
+ }
97
+ if (!entry.isFile())
98
+ return;
99
+ if (includeRegex && !includeRegex.test(entry.name))
100
+ return;
101
+ try {
102
+ const info = await stat(full);
103
+ if (info.size > MAX_FILE_BYTES)
104
+ return;
105
+ const content = await readFile(full, "utf-8");
106
+ if (content.includes("\0"))
107
+ return;
108
+ const lines = content.split("\n");
109
+ for (let i = 0; i < lines.length && results.length < MAX_RESULTS; i++) {
110
+ if (regex.test(lines[i])) {
111
+ results.push(`${path.relative(process.cwd(), full)}:${i + 1}:${lines[i]}`);
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // unreadable file — skip
117
+ }
118
+ }));
119
+ }
120
+ };
121
+ await Promise.all(Array.from({ length: WALK_CONCURRENCY }, worker));
122
+ }
4
123
  export const grepTool = tool({
5
- description: "Search for a pattern in files using grep. Returns matching lines with file paths and line numbers. Use this to find code references, usages, and definitions.",
124
+ description: "Search for a pattern in files using regex. Returns matching lines with file paths and line numbers. Use this to find code references, usages, and definitions.",
6
125
  inputSchema: jsonSchema({
7
126
  type: "object",
8
127
  properties: {
@@ -13,22 +132,42 @@ export const grepTool = tool({
13
132
  required: ["pattern"],
14
133
  }),
15
134
  execute: async ({ pattern, path: searchPath, include }) => {
16
- const target = searchPath ?? ".";
17
- const includeFlag = include ? `--include='${include}'` : "";
18
- const cmd = `grep -rn ${includeFlag} --color=never -E '${pattern.replace(/'/g, "'\\''")}' '${target}' 2>/dev/null | head -50`;
19
135
  try {
20
- const output = execSync(cmd, {
21
- encoding: "utf-8",
22
- cwd: process.cwd(),
23
- timeout: 10000,
24
- maxBuffer: 512 * 1024,
25
- });
26
- const text = output.trim() || "No matches found";
136
+ const regex = new RegExp(pattern);
137
+ const includeRegex = include ? globToRegex(include) : null;
138
+ const root = path.resolve(process.cwd(), searchPath ?? ".");
139
+ const denied = pathAccessError(root, "read");
140
+ if (denied)
141
+ return `Error: ${denied}`;
142
+ const results = [];
143
+ const info = await stat(root).catch(() => null);
144
+ if (!info)
145
+ return `Error: path not found: ${searchPath ?? "."}`;
146
+ if (info.isFile()) {
147
+ try {
148
+ const content = await readFile(root, "utf-8");
149
+ if (!content.includes("\0")) {
150
+ content.split("\n").forEach((line, i) => {
151
+ if (regex.test(line))
152
+ results.push(`${root}:${i + 1}:${line}`);
153
+ });
154
+ }
155
+ }
156
+ catch { }
157
+ }
158
+ else {
159
+ const rgResults = await rgSearch(pattern, root, includeRegex, [...IGNORED_DIRS]);
160
+ if (rgResults !== null) {
161
+ results.push(...rgResults);
162
+ }
163
+ else {
164
+ await walk(root, regex, includeRegex, results);
165
+ }
166
+ }
167
+ const text = results.length > 0 ? results.join("\n") : "No matches found";
27
168
  return truncateToolOutput(text, { direction: "head" }).content;
28
169
  }
29
170
  catch (err) {
30
- if (err.status === 1)
31
- return "No matches found";
32
171
  return `Error: ${err.message}`;
33
172
  }
34
173
  },
@@ -2,40 +2,29 @@ import { bashTool } from "./bash.js";
2
2
  import { readTool } from "./read.js";
3
3
  import { writeTool } from "./write.js";
4
4
  import { editTool } from "./edit.js";
5
+ import { applyPatchTool } from "./apply_patch.js";
5
6
  import { globTool } from "./glob.js";
6
7
  import { grepTool } from "./grep.js";
7
8
  import { webSearchTool } from "./web_search.js";
8
9
  import { webFetchTool } from "./web_fetch.js";
9
- import { todoTool } from "./todo.js";
10
10
  import { questionTool } from "./question.js";
11
11
  import { codeSearchTool } from "./code_search.js";
12
- /** Chat mode: general assistant tools (fewer tools = less token overhead) */
13
- export function createChatTools() {
14
- const tools = {
15
- bash: bashTool,
16
- read: readTool,
17
- write: writeTool,
18
- edit: editTool,
19
- glob: globTool,
20
- grep: grepTool,
21
- web_search: webSearchTool,
22
- web_fetch: webFetchTool,
23
- question: questionTool,
24
- };
25
- return tools;
26
- }
27
- /** Code mode: full tool set including task tracking and code search */
28
- export function createCodeTools() {
12
+ /**
13
+ * Builtin tools shared by main runs and sub-agents. `todo` is deliberately
14
+ * absent: it must be bound to a per-run TaskState, so callers register their
15
+ * own instance via createTodoTool.
16
+ */
17
+ export function createTools() {
29
18
  const tools = {
30
19
  bash: bashTool,
31
20
  read: readTool,
32
21
  write: writeTool,
33
22
  edit: editTool,
23
+ apply_patch: applyPatchTool,
34
24
  glob: globTool,
35
25
  grep: grepTool,
36
- web_search: webSearchTool,
26
+ search_web: webSearchTool,
37
27
  web_fetch: webFetchTool,
38
- todo: todoTool,
39
28
  question: questionTool,
40
29
  };
41
30
  if (process.env.EXA_API_KEY) {
@@ -43,7 +32,3 @@ export function createCodeTools() {
43
32
  }
44
33
  return tools;
45
34
  }
46
- /** @deprecated Use createChatTools or createCodeTools */
47
- export function createTools() {
48
- return createChatTools();
49
- }
@@ -1,5 +1,6 @@
1
1
  import { tool, jsonSchema } from "ai";
2
- import readline from "readline";
2
+ import { askQuestion } from "../confirm.js";
3
+ import { normalizeQuestionOptions, normalizeQuestionPrompt } from "../question-format.js";
3
4
  export const questionTool = tool({
4
5
  description: `Ask the user a question to get clarification before proceeding. Use this when:
5
6
  - The task is ambiguous and you need more information
@@ -7,47 +8,47 @@ export const questionTool = tool({
7
8
  - You need confirmation before a potentially destructive action
8
9
  - You're unsure about a requirement or preference
9
10
 
10
- Provide clear, specific questions. Optionally include numbered options for the user to choose from.`,
11
+ Provide a clear, specific question. Optional choices should be an array of strings. Object items with label/description are also accepted.`,
11
12
  inputSchema: jsonSchema({
12
13
  type: "object",
13
14
  properties: {
14
15
  question: { type: "string", description: "The question to ask the user" },
15
16
  options: {
16
17
  type: "array",
17
- items: { type: "string" },
18
- description: "Optional list of choices for the user to pick from",
18
+ items: {
19
+ anyOf: [
20
+ { type: "string" },
21
+ {
22
+ type: "object",
23
+ properties: {
24
+ label: { type: "string" },
25
+ description: { type: "string" },
26
+ key: { type: "string" },
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ description: "Optional list of choices. Prefer plain strings; {label, description} objects are also accepted.",
19
32
  },
20
33
  },
21
34
  required: ["question"],
22
35
  }),
23
36
  execute: async ({ question, options }) => {
24
- console.log();
25
- console.log(`\x1b[33m❓ ${question}\x1b[0m`);
26
- if (options && options.length > 0) {
27
- for (let i = 0; i < options.length; i++) {
28
- console.log(`\x1b[90m ${i + 1}. ${options[i]}\x1b[0m`);
29
- }
30
- }
31
- const answer = await askUser();
32
- // If user picked a number and options exist, resolve it
33
- if (options && options.length > 0) {
34
- const idx = parseInt(answer) - 1;
35
- if (idx >= 0 && idx < options.length) {
36
- return `User chose: ${options[idx]}`;
37
- }
37
+ const prompt = normalizeQuestionPrompt(question);
38
+ if (!prompt.trim())
39
+ return "Error: question is required.";
40
+ const choices = normalizeQuestionOptions(options);
41
+ const answer = await askQuestion(prompt, choices.length > 0 ? choices : undefined);
42
+ if (answer === null)
43
+ return "User cancelled the question";
44
+ if (choices.length > 0) {
45
+ const exact = choices.find((c) => c === answer);
46
+ if (exact)
47
+ return `User chose: ${exact}`;
48
+ const idx = parseInt(answer, 10) - 1;
49
+ if (Number.isInteger(idx) && idx >= 0 && idx < choices.length)
50
+ return `User chose: ${choices[idx]}`;
38
51
  }
39
52
  return `User answered: ${answer}`;
40
53
  },
41
54
  });
42
- function askUser() {
43
- return new Promise((resolve) => {
44
- const rl = readline.createInterface({
45
- input: process.stdin,
46
- output: process.stdout,
47
- });
48
- rl.question("\x1b[36m → \x1b[0m", (answer) => {
49
- rl.close();
50
- resolve(answer.trim() || "(no answer)");
51
- });
52
- });
53
- }