min-agent 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tools/explore.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
2
|
import { resolveModel } from "../provider.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
102
|
+
system: exploreSystem(),
|
|
86
103
|
messages,
|
|
87
104
|
tools,
|
|
88
|
-
stopWhen: stepCountIs(
|
|
105
|
+
stopWhen: stepCountIs(resolveExploreSteps(context.maxSteps)),
|
|
89
106
|
maxRetries: 2,
|
|
90
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
});
|
package/dist/tools/glob.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import {
|
|
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 =
|
|
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)
|
package/dist/tools/grep.js
CHANGED
|
@@ -1,8 +1,127 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
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
|
},
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
}
|
package/dist/tools/question.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import
|
|
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
|
|
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: {
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|