minimal-agent 0.11.0 → 0.12.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/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
package/src/tools/glob/glob.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* src/tools/glob.ts —— Glob 工具(按文件名模式匹配)
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
5
|
* 对应 kakadeai 主仓库的 GlobTool。
|
|
6
|
-
* 实现用 fast-glob(npm
|
|
6
|
+
* 实现用 fast-glob(npm 包),默认按发现顺序流式返回。
|
|
7
7
|
* 支持限制候选数量,并通过结构化元数据返回大小和修改时间。
|
|
8
8
|
* ============================================================
|
|
9
9
|
*/
|
|
10
10
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
11
11
|
import fg from 'fast-glob';
|
|
12
|
+
import { stat } from 'node:fs/promises';
|
|
12
13
|
import { z } from 'zod';
|
|
13
14
|
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
14
15
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
@@ -23,10 +24,34 @@ const inputSchema = z.object({
|
|
|
23
24
|
.string()
|
|
24
25
|
.optional()
|
|
25
26
|
.describe('搜索的根目录(默认当前工作目录);省略时不要传 "undefined" 字符串'),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
strategy: z
|
|
28
|
+
.enum(['fast', 'complete'])
|
|
29
|
+
.default('fast')
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('fast 在收集够分页候选后提前停止;完整排序请用 complete'),
|
|
32
|
+
sort_by: z
|
|
33
|
+
.enum(['discovery', 'modified', 'path'])
|
|
34
|
+
.default('discovery')
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('排序字段,默认 discovery'),
|
|
37
|
+
order: z
|
|
38
|
+
.enum(['asc', 'desc'])
|
|
39
|
+
.default('desc')
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('排序方向,默认 desc(最新在前)'),
|
|
42
|
+
offset: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.min(0)
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('跳过前 N 个结果,默认 0'),
|
|
48
|
+
limit: z
|
|
49
|
+
.number()
|
|
50
|
+
.int()
|
|
51
|
+
.min(0)
|
|
52
|
+
.max(10_000)
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('最多返回多少个候选文件;默认 200,0 表示不限制'),
|
|
30
55
|
});
|
|
31
56
|
// ---------------- 2. JSON Schema(由 Zod 自动派生) ----------------
|
|
32
57
|
const parameters = toToolParameters(inputSchema);
|
|
@@ -34,12 +59,12 @@ const parameters = toToolParameters(inputSchema);
|
|
|
34
59
|
const description = `- Fast file pattern matching tool that works with any codebase size
|
|
35
60
|
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
36
61
|
- Returns structured file candidates with path, size, and modification time
|
|
37
|
-
- Defaults to
|
|
62
|
+
- Defaults to streaming discovery order; request sort_by=modified with strategy=complete for newest-file queries
|
|
38
63
|
- Discovery does not present files to the user; use Read for selected source files and PresentResult for delivered files
|
|
39
64
|
- Use this tool when you need to find files by name patterns
|
|
40
65
|
- When you need to do an open ended search that may require multiple rounds, prefer the Grep tool for content search`;
|
|
41
66
|
// ---------------- 4. call() ----------------
|
|
42
|
-
async function call(input, runCwd = getWorkingDir()) {
|
|
67
|
+
async function call(input, runCwd = getWorkingDir(), signal) {
|
|
43
68
|
// 以 getWorkingDir() 为锚,而非裸 resolve()(同 grep.ts 的坑:后者落回 process.cwd(),
|
|
44
69
|
// 多工作区宿主进程里会与当前工作区脱节)。resolve 对绝对路径行为不变。
|
|
45
70
|
const cwd = input.path ? resolve(runCwd, input.path) : runCwd;
|
|
@@ -47,69 +72,160 @@ async function call(input, runCwd = getWorkingDir()) {
|
|
|
47
72
|
// Windows 用户/模型可能传 "src\**\*.ts",这里统一替换。
|
|
48
73
|
// (cwd 用原生路径没问题;fast-glob 内部会处理)
|
|
49
74
|
const pattern = input.pattern.replace(/\\/g, '/');
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
const offset = input.offset ?? 0, limit = input.limit ?? 200, strategy = input.strategy ?? 'fast', sortBy = input.sort_by ?? 'discovery';
|
|
76
|
+
const needed = limit === 0 ? Number.POSITIVE_INFINITY : offset + limit + 1;
|
|
77
|
+
const ignore = ['node_modules', '.git', '.svn', '.hg', '.jj', '.sl', 'dist']
|
|
78
|
+
.filter((name) => !pattern.split('/').includes(name))
|
|
79
|
+
.map((name) => `**/${name}/**`);
|
|
80
|
+
const candidates = [];
|
|
81
|
+
let scanned = 0;
|
|
82
|
+
let stoppedEarly = false;
|
|
83
|
+
const effectiveOrder = input.order ?? (sortBy === 'discovery' ? 'asc' : 'desc');
|
|
84
|
+
const compare = (a, b) => {
|
|
85
|
+
const value = sortBy === 'path'
|
|
86
|
+
? a.path.localeCompare(b.path)
|
|
87
|
+
: sortBy === 'modified'
|
|
88
|
+
? a.mtime - b.mtime
|
|
89
|
+
: a.discovery - b.discovery;
|
|
90
|
+
return value * (effectiveOrder === 'desc' ? -1 : 1);
|
|
91
|
+
};
|
|
92
|
+
// 最大堆的根始终是当前保留集合里“最差”的候选,完整扫描只占 O(offset+limit) 内存。
|
|
93
|
+
const bubbleUp = (index) => {
|
|
94
|
+
while (index > 0) {
|
|
95
|
+
const parent = Math.floor((index - 1) / 2);
|
|
96
|
+
if (compare(candidates[parent], candidates[index]) >= 0)
|
|
97
|
+
break;
|
|
98
|
+
[candidates[parent], candidates[index]] = [
|
|
99
|
+
candidates[index],
|
|
100
|
+
candidates[parent],
|
|
101
|
+
];
|
|
102
|
+
index = parent;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const bubbleDown = (index) => {
|
|
106
|
+
while (true) {
|
|
107
|
+
const left = index * 2 + 1, right = left + 1;
|
|
108
|
+
let worst = index;
|
|
109
|
+
if (left < candidates.length &&
|
|
110
|
+
compare(candidates[left], candidates[worst]) > 0)
|
|
111
|
+
worst = left;
|
|
112
|
+
if (right < candidates.length &&
|
|
113
|
+
compare(candidates[right], candidates[worst]) > 0)
|
|
114
|
+
worst = right;
|
|
115
|
+
if (worst === index)
|
|
116
|
+
break;
|
|
117
|
+
[candidates[index], candidates[worst]] = [
|
|
118
|
+
candidates[worst],
|
|
119
|
+
candidates[index],
|
|
120
|
+
];
|
|
121
|
+
index = worst;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const retain = (item) => {
|
|
125
|
+
if (limit === 0) {
|
|
126
|
+
candidates.push(item);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (candidates.length < needed) {
|
|
130
|
+
candidates.push(item);
|
|
131
|
+
bubbleUp(candidates.length - 1);
|
|
132
|
+
}
|
|
133
|
+
else if (compare(item, candidates[0]) < 0) {
|
|
134
|
+
candidates[0] = item;
|
|
135
|
+
bubbleDown(0);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
52
138
|
try {
|
|
53
|
-
|
|
139
|
+
const stream = fg.stream(pattern, {
|
|
54
140
|
cwd,
|
|
55
141
|
dot: true,
|
|
56
142
|
onlyFiles: true,
|
|
57
143
|
followSymbolicLinks: false,
|
|
58
|
-
|
|
59
|
-
ignore: ['node_modules', '.git', '.svn', '.hg', '.jj', '.sl', 'dist']
|
|
60
|
-
.filter((name) => !pattern.split('/').includes(name))
|
|
61
|
-
.map((name) => `**/${name}/**`),
|
|
144
|
+
ignore,
|
|
62
145
|
absolute: false,
|
|
63
|
-
stats: true,
|
|
64
146
|
});
|
|
147
|
+
for await (const raw of stream) {
|
|
148
|
+
if (signal?.aborted) {
|
|
149
|
+
stream.destroy?.();
|
|
150
|
+
throw new Error('aborted');
|
|
151
|
+
}
|
|
152
|
+
const path = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw);
|
|
153
|
+
scanned++;
|
|
154
|
+
if (strategy === 'fast' && sortBy === 'discovery') {
|
|
155
|
+
candidates.push({ path, mtime: 0, size: 0, discovery: scanned });
|
|
156
|
+
if (candidates.length >= needed) {
|
|
157
|
+
stoppedEarly = true;
|
|
158
|
+
stream.destroy?.();
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const info = sortBy === 'modified'
|
|
164
|
+
? await stat(resolve(cwd, path))
|
|
165
|
+
: { mtimeMs: 0, size: 0 };
|
|
166
|
+
const item = {
|
|
167
|
+
path,
|
|
168
|
+
mtime: info.mtimeMs,
|
|
169
|
+
size: info.size,
|
|
170
|
+
discovery: scanned,
|
|
171
|
+
};
|
|
172
|
+
retain(item);
|
|
173
|
+
}
|
|
65
174
|
}
|
|
66
175
|
catch (e) {
|
|
67
176
|
return { ok: false, error: `glob 失败:${e.message}` };
|
|
68
177
|
}
|
|
69
|
-
if (
|
|
70
|
-
return {
|
|
178
|
+
if (candidates.length === 0) {
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
content: `(no files matched pattern "${pattern}" in ${cwd})`,
|
|
182
|
+
};
|
|
71
183
|
}
|
|
72
184
|
// 候选携带稳定元数据。发现候选本身不代表文件已经成为会话上下文。
|
|
73
|
-
const withMtime =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const truncated = offset + selected.length < withMtime.length;
|
|
185
|
+
const withMtime = candidates;
|
|
186
|
+
withMtime.sort(compare);
|
|
187
|
+
const selected = limit === 0
|
|
188
|
+
? withMtime.slice(offset)
|
|
189
|
+
: withMtime.slice(offset, offset + limit);
|
|
190
|
+
const truncated = stoppedEarly || offset + selected.length < withMtime.length;
|
|
191
|
+
for (const item of selected) {
|
|
192
|
+
if (item.size === 0 && item.mtime === 0) {
|
|
193
|
+
const info = await stat(resolve(cwd, item.path)).catch(() => undefined);
|
|
194
|
+
if (info) {
|
|
195
|
+
item.size = info.size;
|
|
196
|
+
item.mtime = info.mtimeMs;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
89
200
|
// 输出:每行一个相对路径
|
|
90
201
|
let content = selected.map((m) => m.path).join('\n');
|
|
91
202
|
// 超条数截断:保留旧文件(头部),丢弃新文件(尾部)
|
|
92
203
|
if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
93
204
|
content =
|
|
94
205
|
content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
|
|
95
|
-
`\n\n... (
|
|
206
|
+
`\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,已截断)`;
|
|
96
207
|
}
|
|
97
208
|
const workspaceRoot = resolve(runCwd);
|
|
98
209
|
return {
|
|
99
210
|
ok: true,
|
|
100
|
-
content: `${content}\n\n(
|
|
211
|
+
content: `${content}\n\n(${stoppedEarly ? `至少扫描 ${scanned} 个` : `匹配 ${scanned} 个`},返回 ${selected.length} 个,offset=${offset}${truncated ? ',仍有后续结果' : ''},按 ${sortBy} ${effectiveOrder} 排序)`,
|
|
101
212
|
metadata: {
|
|
102
213
|
kind: 'file_discovery',
|
|
103
214
|
files: selected.flatMap((entry) => {
|
|
104
215
|
const absolute = resolve(cwd, entry.path);
|
|
105
216
|
const rel = relative(workspaceRoot, absolute);
|
|
106
|
-
if (!rel ||
|
|
217
|
+
if (!rel ||
|
|
218
|
+
rel === '..' ||
|
|
219
|
+
rel.startsWith(`..${sep}`) ||
|
|
220
|
+
isAbsolute(rel))
|
|
107
221
|
return [];
|
|
108
|
-
return [
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
109
224
|
relativePath: rel.split(sep).join('/'),
|
|
110
225
|
size: entry.size,
|
|
111
226
|
modifiedAt: entry.mtime,
|
|
112
|
-
}
|
|
227
|
+
},
|
|
228
|
+
];
|
|
113
229
|
}),
|
|
114
230
|
},
|
|
115
231
|
};
|
|
@@ -124,6 +240,6 @@ export const globTool = {
|
|
|
124
240
|
isConcurrencySafe: true,
|
|
125
241
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
126
242
|
interruptBehavior: () => 'cancel',
|
|
127
|
-
call: (input) => call(input),
|
|
128
|
-
callWithContext: (input,
|
|
243
|
+
call: (input, signal) => call(input, undefined, signal),
|
|
244
|
+
callWithContext: (input, signal, context) => call(input, context.cwd, signal),
|
|
129
245
|
};
|
package/src/tools/glob/glob.ts
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* src/tools/glob.ts —— Glob 工具(按文件名模式匹配)
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
5
|
* 对应 kakadeai 主仓库的 GlobTool。
|
|
6
|
-
* 实现用 fast-glob(npm
|
|
6
|
+
* 实现用 fast-glob(npm 包),默认按发现顺序流式返回。
|
|
7
7
|
* 支持限制候选数量,并通过结构化元数据返回大小和修改时间。
|
|
8
8
|
* ============================================================
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
12
12
|
import fg from 'fast-glob';
|
|
13
|
+
import { stat } from 'node:fs/promises';
|
|
13
14
|
import { z } from 'zod';
|
|
14
15
|
|
|
15
16
|
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
@@ -26,11 +27,37 @@ const inputSchema = z.object({
|
|
|
26
27
|
path: z
|
|
27
28
|
.string()
|
|
28
29
|
.optional()
|
|
29
|
-
.describe(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
.describe(
|
|
31
|
+
'搜索的根目录(默认当前工作目录);省略时不要传 "undefined" 字符串',
|
|
32
|
+
),
|
|
33
|
+
strategy: z
|
|
34
|
+
.enum(['fast', 'complete'])
|
|
35
|
+
.default('fast')
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('fast 在收集够分页候选后提前停止;完整排序请用 complete'),
|
|
38
|
+
sort_by: z
|
|
39
|
+
.enum(['discovery', 'modified', 'path'])
|
|
40
|
+
.default('discovery')
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('排序字段,默认 discovery'),
|
|
43
|
+
order: z
|
|
44
|
+
.enum(['asc', 'desc'])
|
|
45
|
+
.default('desc')
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('排序方向,默认 desc(最新在前)'),
|
|
48
|
+
offset: z
|
|
49
|
+
.number()
|
|
50
|
+
.int()
|
|
51
|
+
.min(0)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('跳过前 N 个结果,默认 0'),
|
|
54
|
+
limit: z
|
|
55
|
+
.number()
|
|
56
|
+
.int()
|
|
57
|
+
.min(0)
|
|
58
|
+
.max(10_000)
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('最多返回多少个候选文件;默认 200,0 表示不限制'),
|
|
34
61
|
});
|
|
35
62
|
type GlobInput = z.infer<typeof inputSchema>;
|
|
36
63
|
|
|
@@ -41,13 +68,17 @@ const parameters = toToolParameters(inputSchema);
|
|
|
41
68
|
const description = `- Fast file pattern matching tool that works with any codebase size
|
|
42
69
|
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
43
70
|
- Returns structured file candidates with path, size, and modification time
|
|
44
|
-
- Defaults to
|
|
71
|
+
- Defaults to streaming discovery order; request sort_by=modified with strategy=complete for newest-file queries
|
|
45
72
|
- Discovery does not present files to the user; use Read for selected source files and PresentResult for delivered files
|
|
46
73
|
- Use this tool when you need to find files by name patterns
|
|
47
74
|
- When you need to do an open ended search that may require multiple rounds, prefer the Grep tool for content search`;
|
|
48
75
|
|
|
49
76
|
// ---------------- 4. call() ----------------
|
|
50
|
-
async function call(
|
|
77
|
+
async function call(
|
|
78
|
+
input: GlobInput,
|
|
79
|
+
runCwd = getWorkingDir(),
|
|
80
|
+
signal?: AbortSignal,
|
|
81
|
+
): Promise<ToolResult> {
|
|
51
82
|
// 以 getWorkingDir() 为锚,而非裸 resolve()(同 grep.ts 的坑:后者落回 process.cwd(),
|
|
52
83
|
// 多工作区宿主进程里会与当前工作区脱节)。resolve 对绝对路径行为不变。
|
|
53
84
|
const cwd = input.path ? resolve(runCwd, input.path) : runCwd;
|
|
@@ -57,48 +88,147 @@ async function call(input: GlobInput, runCwd = getWorkingDir()): Promise<ToolRes
|
|
|
57
88
|
// (cwd 用原生路径没问题;fast-glob 内部会处理)
|
|
58
89
|
const pattern = input.pattern.replace(/\\/g, '/');
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
const offset = input.offset ?? 0,
|
|
92
|
+
limit = input.limit ?? 200,
|
|
93
|
+
strategy = input.strategy ?? 'fast',
|
|
94
|
+
sortBy = input.sort_by ?? 'discovery';
|
|
95
|
+
const needed = limit === 0 ? Number.POSITIVE_INFINITY : offset + limit + 1;
|
|
96
|
+
const ignore = ['node_modules', '.git', '.svn', '.hg', '.jj', '.sl', 'dist']
|
|
97
|
+
.filter((name) => !pattern.split('/').includes(name))
|
|
98
|
+
.map((name) => `**/${name}/**`);
|
|
99
|
+
type Candidate = {
|
|
100
|
+
path: string;
|
|
101
|
+
mtime: number;
|
|
102
|
+
size: number;
|
|
103
|
+
discovery: number;
|
|
104
|
+
};
|
|
105
|
+
const candidates: Candidate[] = [];
|
|
106
|
+
let scanned = 0;
|
|
107
|
+
let stoppedEarly = false;
|
|
108
|
+
const effectiveOrder =
|
|
109
|
+
input.order ?? (sortBy === 'discovery' ? 'asc' : 'desc');
|
|
110
|
+
const compare = (a: Candidate, b: Candidate): number => {
|
|
111
|
+
const value =
|
|
112
|
+
sortBy === 'path'
|
|
113
|
+
? a.path.localeCompare(b.path)
|
|
114
|
+
: sortBy === 'modified'
|
|
115
|
+
? a.mtime - b.mtime
|
|
116
|
+
: a.discovery - b.discovery;
|
|
117
|
+
return value * (effectiveOrder === 'desc' ? -1 : 1);
|
|
118
|
+
};
|
|
119
|
+
// 最大堆的根始终是当前保留集合里“最差”的候选,完整扫描只占 O(offset+limit) 内存。
|
|
120
|
+
const bubbleUp = (index: number) => {
|
|
121
|
+
while (index > 0) {
|
|
122
|
+
const parent = Math.floor((index - 1) / 2);
|
|
123
|
+
if (compare(candidates[parent]!, candidates[index]!) >= 0) break;
|
|
124
|
+
[candidates[parent], candidates[index]] = [
|
|
125
|
+
candidates[index]!,
|
|
126
|
+
candidates[parent]!,
|
|
127
|
+
];
|
|
128
|
+
index = parent;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const bubbleDown = (index: number) => {
|
|
132
|
+
while (true) {
|
|
133
|
+
const left = index * 2 + 1,
|
|
134
|
+
right = left + 1;
|
|
135
|
+
let worst = index;
|
|
136
|
+
if (
|
|
137
|
+
left < candidates.length &&
|
|
138
|
+
compare(candidates[left]!, candidates[worst]!) > 0
|
|
139
|
+
)
|
|
140
|
+
worst = left;
|
|
141
|
+
if (
|
|
142
|
+
right < candidates.length &&
|
|
143
|
+
compare(candidates[right]!, candidates[worst]!) > 0
|
|
144
|
+
)
|
|
145
|
+
worst = right;
|
|
146
|
+
if (worst === index) break;
|
|
147
|
+
[candidates[index], candidates[worst]] = [
|
|
148
|
+
candidates[worst]!,
|
|
149
|
+
candidates[index]!,
|
|
150
|
+
];
|
|
151
|
+
index = worst;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const retain = (item: Candidate) => {
|
|
155
|
+
if (limit === 0) {
|
|
156
|
+
candidates.push(item);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (candidates.length < needed) {
|
|
160
|
+
candidates.push(item);
|
|
161
|
+
bubbleUp(candidates.length - 1);
|
|
162
|
+
} else if (compare(item, candidates[0]!) < 0) {
|
|
163
|
+
candidates[0] = item;
|
|
164
|
+
bubbleDown(0);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
62
167
|
try {
|
|
63
|
-
|
|
168
|
+
const stream = fg.stream(pattern, {
|
|
64
169
|
cwd,
|
|
65
170
|
dot: true,
|
|
66
171
|
onlyFiles: true,
|
|
67
172
|
followSymbolicLinks: false,
|
|
68
|
-
|
|
69
|
-
ignore: ['node_modules', '.git', '.svn', '.hg', '.jj', '.sl', 'dist']
|
|
70
|
-
.filter((name) => !pattern.split('/').includes(name))
|
|
71
|
-
.map((name) => `**/${name}/**`),
|
|
173
|
+
ignore,
|
|
72
174
|
absolute: false,
|
|
73
|
-
stats: true,
|
|
74
175
|
});
|
|
176
|
+
for await (const raw of stream) {
|
|
177
|
+
if (signal?.aborted) {
|
|
178
|
+
(stream as unknown as { destroy?: () => void }).destroy?.();
|
|
179
|
+
throw new Error('aborted');
|
|
180
|
+
}
|
|
181
|
+
const path = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw);
|
|
182
|
+
scanned++;
|
|
183
|
+
if (strategy === 'fast' && sortBy === 'discovery') {
|
|
184
|
+
candidates.push({ path, mtime: 0, size: 0, discovery: scanned });
|
|
185
|
+
if (candidates.length >= needed) {
|
|
186
|
+
stoppedEarly = true;
|
|
187
|
+
(stream as unknown as { destroy?: () => void }).destroy?.();
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const info =
|
|
193
|
+
sortBy === 'modified'
|
|
194
|
+
? await stat(resolve(cwd, path))
|
|
195
|
+
: { mtimeMs: 0, size: 0 };
|
|
196
|
+
const item = {
|
|
197
|
+
path,
|
|
198
|
+
mtime: info.mtimeMs,
|
|
199
|
+
size: info.size,
|
|
200
|
+
discovery: scanned,
|
|
201
|
+
};
|
|
202
|
+
retain(item);
|
|
203
|
+
}
|
|
75
204
|
} catch (e) {
|
|
76
205
|
return { ok: false, error: `glob 失败:${(e as Error).message}` };
|
|
77
206
|
}
|
|
78
207
|
|
|
79
|
-
if (
|
|
80
|
-
return {
|
|
208
|
+
if (candidates.length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
content: `(no files matched pattern "${pattern}" in ${cwd})`,
|
|
212
|
+
};
|
|
81
213
|
}
|
|
82
214
|
|
|
83
215
|
// 候选携带稳定元数据。发现候选本身不代表文件已经成为会话上下文。
|
|
84
|
-
const withMtime =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const selected = limit === 0 ? withMtime.slice(offset) : withMtime.slice(offset, offset + limit);
|
|
101
|
-
const truncated = offset + selected.length < withMtime.length;
|
|
216
|
+
const withMtime = candidates;
|
|
217
|
+
withMtime.sort(compare);
|
|
218
|
+
const selected =
|
|
219
|
+
limit === 0
|
|
220
|
+
? withMtime.slice(offset)
|
|
221
|
+
: withMtime.slice(offset, offset + limit);
|
|
222
|
+
const truncated = stoppedEarly || offset + selected.length < withMtime.length;
|
|
223
|
+
for (const item of selected) {
|
|
224
|
+
if (item.size === 0 && item.mtime === 0) {
|
|
225
|
+
const info = await stat(resolve(cwd, item.path)).catch(() => undefined);
|
|
226
|
+
if (info) {
|
|
227
|
+
item.size = info.size;
|
|
228
|
+
item.mtime = info.mtimeMs;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
102
232
|
|
|
103
233
|
// 输出:每行一个相对路径
|
|
104
234
|
let content = selected.map((m) => m.path).join('\n');
|
|
@@ -107,23 +237,31 @@ async function call(input: GlobInput, runCwd = getWorkingDir()): Promise<ToolRes
|
|
|
107
237
|
if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
108
238
|
content =
|
|
109
239
|
content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
|
|
110
|
-
`\n\n... (
|
|
240
|
+
`\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,已截断)`;
|
|
111
241
|
}
|
|
112
242
|
const workspaceRoot = resolve(runCwd);
|
|
113
243
|
return {
|
|
114
244
|
ok: true,
|
|
115
|
-
content: `${content}\n\n(
|
|
245
|
+
content: `${content}\n\n(${stoppedEarly ? `至少扫描 ${scanned} 个` : `匹配 ${scanned} 个`},返回 ${selected.length} 个,offset=${offset}${truncated ? ',仍有后续结果' : ''},按 ${sortBy} ${effectiveOrder} 排序)`,
|
|
116
246
|
metadata: {
|
|
117
247
|
kind: 'file_discovery',
|
|
118
248
|
files: selected.flatMap((entry) => {
|
|
119
249
|
const absolute = resolve(cwd, entry.path);
|
|
120
250
|
const rel = relative(workspaceRoot, absolute);
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
251
|
+
if (
|
|
252
|
+
!rel ||
|
|
253
|
+
rel === '..' ||
|
|
254
|
+
rel.startsWith(`..${sep}`) ||
|
|
255
|
+
isAbsolute(rel)
|
|
256
|
+
)
|
|
257
|
+
return [];
|
|
258
|
+
return [
|
|
259
|
+
{
|
|
260
|
+
relativePath: rel.split(sep).join('/'),
|
|
261
|
+
size: entry.size,
|
|
262
|
+
modifiedAt: entry.mtime,
|
|
263
|
+
},
|
|
264
|
+
];
|
|
127
265
|
}),
|
|
128
266
|
},
|
|
129
267
|
};
|
|
@@ -139,6 +277,7 @@ export const globTool: Tool<GlobInput> = {
|
|
|
139
277
|
isConcurrencySafe: true,
|
|
140
278
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
141
279
|
interruptBehavior: () => 'cancel',
|
|
142
|
-
call: (input) => call(input),
|
|
143
|
-
callWithContext: (input,
|
|
280
|
+
call: (input, signal) => call(input, undefined, signal),
|
|
281
|
+
callWithContext: (input, signal, context: ToolCallContext) =>
|
|
282
|
+
call(input, context.cwd, signal),
|
|
144
283
|
};
|