minimal-agent 0.11.0 → 0.13.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 +5 -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/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +50 -20
- package/plugins/workflow-runner/src/index.ts +75 -31
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -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 +91 -61
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +137 -14
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +36 -4
- package/src/cli/print.ts +42 -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/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- 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/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- 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 +23 -8
- package/src/plugins/pluginRunner.ts +32 -13
- package/src/prompts/policyRegistry.js +55 -1
- package/src/prompts/policyRegistry.ts +55 -1
- 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/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- 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 +80 -15
- package/src/tools/bash/bash.ts +83 -15
- 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 +134 -27
- package/src/tools/powershell/powershell.ts +171 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +249 -29
- package/src/tools/read/read.ts +396 -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 +57 -0
- package/src/tools/shared/permanentDeleteGuard.ts +71 -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/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- 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 +22 -4
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- package/workflows/youtube-shorts.yaml +25 -25
package/src/tools/bash/bash.ts
CHANGED
|
@@ -63,6 +63,11 @@ import { requestUserChoice } from '../../askBridge.js';
|
|
|
63
63
|
import { classifyCommand } from '../../irreversible/rules.js';
|
|
64
64
|
import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
|
|
65
65
|
import { processExecutionMetadata } from '../tasks/processEvidence.js';
|
|
66
|
+
import {
|
|
67
|
+
permanentDeleteReason,
|
|
68
|
+
type ShellKind,
|
|
69
|
+
} from '../shared/permanentDeleteGuard.js';
|
|
70
|
+
import { ToolOutputStore } from '../shared/toolOutputStore.js';
|
|
66
71
|
|
|
67
72
|
// ---------------- 0. 常量 ----------------
|
|
68
73
|
|
|
@@ -110,6 +115,17 @@ function detectWindowsBash(): string | null {
|
|
|
110
115
|
|
|
111
116
|
const WIN_BASH = detectWindowsBash();
|
|
112
117
|
|
|
118
|
+
/**
|
|
119
|
+
* 本工具这一刻**真正**把命令交给谁执行。
|
|
120
|
+
*
|
|
121
|
+
* 单一出处:description(告诉模型该写哪种语法)与 permanentDeleteGuard(按哪套动词
|
|
122
|
+
* 匹配删除命令)必须看同一个判断,否则就会出现「description 说这是 cmd、守卫却按
|
|
123
|
+
* bash 匹配」这种两头对不上的裂缝 —— del / rd 会因此直接穿过全部三层防护。
|
|
124
|
+
*/
|
|
125
|
+
function effectiveShellKind(): ShellKind {
|
|
126
|
+
return process.platform === 'win32' && !WIN_BASH ? 'cmd' : 'bash';
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
/**
|
|
114
130
|
* G1:stdout / stderr 即时缓冲上限(各自)。命令狂输出(如 cat 大文件 / yes)时,
|
|
115
131
|
* 在最终统一截断之前先把内存封顶,防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响,
|
|
@@ -330,8 +346,11 @@ Some commands return non-zero exit codes for informational (non-error) reasons.
|
|
|
330
346
|
- \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
|
|
331
347
|
Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
|
|
332
348
|
|
|
333
|
-
# Safety
|
|
334
|
-
|
|
349
|
+
# Safety - three levels, from strict to lenient
|
|
350
|
+
|
|
351
|
+
## 1. Blocked outright (fails before execution - do NOT retry with variants)
|
|
352
|
+
|
|
353
|
+
Catastrophic commands (no legitimate use):
|
|
335
354
|
- \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
|
|
336
355
|
- Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
|
|
337
356
|
- Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
|
|
@@ -339,9 +358,37 @@ The following command patterns are blocked at the tool level and will fail befor
|
|
|
339
358
|
- Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
|
|
340
359
|
- \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
|
|
341
360
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
361
|
+
Permanent deletion (bypasses the OS Trash / Recycle Bin - use the Trash tool instead):
|
|
362
|
+
- \`rm\`, \`unlink\`, \`rmdir\` (any shell-level file/dir deletion, any target)
|
|
363
|
+
- \`find ... -delete\`
|
|
364
|
+
- \`git clean -f\` / \`git clean --force\` (\`-n\` / \`--dry-run\` is fine)
|
|
365
|
+
|
|
366
|
+
To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`.
|
|
367
|
+
Do NOT work around this by switching to \`find -delete\`, Python \`os.remove\`, Node \`fs.rm\`, or other shell variants - they are blocked the same way.
|
|
368
|
+
If a permanent deletion is truly required, ask the user to run it themselves in their terminal.
|
|
369
|
+
|
|
370
|
+
## 2. Requires user confirmation (irreversible)
|
|
371
|
+
|
|
372
|
+
These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts (sub-agents, cron, batch mode) they are automatically cancelled.
|
|
373
|
+
- \`git push --force\` / \`-f\`, and \`git push origin +branch\` (+refspec = force push)
|
|
374
|
+
- \`git push --delete <branch>\` / \`git push :<branch>\` (deleting remote branches)
|
|
375
|
+
- \`git filter-branch\` / \`git filter-repo\` (rewriting history)
|
|
376
|
+
- \`git reflog expire --expire=now\` / \`git gc --prune=now\` (destroying reflog)
|
|
377
|
+
- \`shred -u\` / \`shred --remove\` (overwrite and delete - unrecoverable)
|
|
378
|
+
- SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\`
|
|
379
|
+
|
|
380
|
+
## 3. Warning only (command still runs)
|
|
381
|
+
|
|
382
|
+
These execute normally; a \`⚠️ 警告:\` line is prepended to the output as a reminder. They are NOT blocked.
|
|
383
|
+
- \`git reset --hard\` (reflog keeps it recoverable)
|
|
384
|
+
- \`git push --force-with-lease\` (rejects if the remote has unseen commits - the safe force-push form)
|
|
385
|
+
- \`git checkout -- .\` / \`git restore .\` (discards uncommitted changes)
|
|
386
|
+
- \`git branch -D\`, \`git stash drop\` / \`git stash clear\` (local, recoverable via reflog in most cases)
|
|
387
|
+
- \`git commit --amend\`, \`git commit --no-verify\`
|
|
388
|
+
- \`DELETE FROM ...\` (SQL row deletion; DROP/TRUNCATE are in level 2)
|
|
389
|
+
- \`kubectl delete\`, \`terraform destroy\`
|
|
390
|
+
|
|
391
|
+
Regular Git operations (commit, add, status, log, diff, pull, fetch, checkout <branch>, merge, rebase, etc.) run without extra restrictions.${
|
|
345
392
|
process.platform !== 'win32'
|
|
346
393
|
? ''
|
|
347
394
|
: WIN_BASH
|
|
@@ -372,6 +419,12 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
372
419
|
error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
|
|
373
420
|
};
|
|
374
421
|
}
|
|
422
|
+
// 传**实际执行命令的 shell**,不是工具名:win32 上找不到 Git Bash 时下面走的是
|
|
423
|
+
// cmd.exe(见 WIN_BASH 探测),description 也会告诉模型「这里是 cmd」,模型于是写
|
|
424
|
+
// del / rd —— 按 'bash' 匹配的话这两个词一个都拦不住(黑名单与 irreversible 规则表
|
|
425
|
+
// 同样只认 POSIX 形态),等于三层全空。
|
|
426
|
+
const permanentDelete = permanentDeleteReason(command, effectiveShellKind());
|
|
427
|
+
if (permanentDelete) return { ok:false, error:`永久删除检查拒绝执行:${permanentDelete}\n命令:${command}` };
|
|
375
428
|
|
|
376
429
|
// 4a-1. 不可逆动作闸门:毁掉「最后一份副本」的命令要**停下来问人**。
|
|
377
430
|
//
|
|
@@ -422,20 +475,21 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
422
475
|
const cwd = context?.cwd ?? getWorkingDir();
|
|
423
476
|
const task = await startBackgroundTask(
|
|
424
477
|
WIN_BASH
|
|
425
|
-
? { command, executable: WIN_BASH, args: ['-c', command], cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, shellKind: 'bash' as const }
|
|
478
|
+
? { command, executable: WIN_BASH, args: ['-c', command], cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, workspaceId: context?.workspaceId, shellKind: 'bash' as const }
|
|
426
479
|
: {
|
|
427
480
|
command: process.platform === 'win32' ? `chcp 65001 >nul & ${command}` : command,
|
|
428
481
|
cwd,
|
|
429
482
|
env: sanitizedChildEnv(),
|
|
430
483
|
shell: true,
|
|
431
484
|
sessionId: context?.sessionId,
|
|
485
|
+
workspaceId: context?.workspaceId,
|
|
432
486
|
shellKind: 'bash' as const,
|
|
433
487
|
},
|
|
434
488
|
context?.sessionState?.backgroundTasks,
|
|
435
489
|
);
|
|
436
490
|
return {
|
|
437
491
|
ok: true,
|
|
438
|
-
content: `${destructiveWarning ? `⚠️ 警告: ${destructiveWarning}\n` : ''}后台任务已启动。task_id=${task.id}\nlog=${task.logPath}\n使用 TaskOutput 增量读取输出,TaskStop 停止进程。`,
|
|
492
|
+
content: `${destructiveWarning ? `⚠️ 警告: ${destructiveWarning}\n` : ''}后台任务已启动。task_id=${task.id.replace(/^proc_/,'')}\nprocess_id=${task.id}\nlog=${task.logPath}\n使用 TaskOutput 增量读取输出,TaskStop 停止进程。`,
|
|
439
493
|
metadata: processExecutionMetadata({ shell: 'bash', mode: 'background', command, status: 'started', taskId: task.id }),
|
|
440
494
|
};
|
|
441
495
|
} catch (error) {
|
|
@@ -449,6 +503,10 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
449
503
|
// Windows 无 bash: cmd.exe,命令前缀 chcp 65001 强制 UTF-8 输出
|
|
450
504
|
let stdout = '';
|
|
451
505
|
let stderr = '';
|
|
506
|
+
const outputStore = new ToolOutputStore({
|
|
507
|
+
workspaceId: context?.workspaceId,
|
|
508
|
+
sessionId: context?.sessionId,
|
|
509
|
+
});
|
|
452
510
|
let exitCode: number | null = null;
|
|
453
511
|
let timedOut = false;
|
|
454
512
|
let killedBySignal = false;
|
|
@@ -496,12 +554,16 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
496
554
|
child.stdout?.setEncoding('utf8');
|
|
497
555
|
child.stderr?.setEncoding('utf8');
|
|
498
556
|
child.stdout?.on('data', (chunk) => {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
557
|
+
if (!outputStore.append('stdout', chunk)) {
|
|
558
|
+
child.stdout?.pause();
|
|
559
|
+
outputStore.onDrain(() => child.stdout?.resume());
|
|
560
|
+
}
|
|
502
561
|
});
|
|
503
562
|
child.stderr?.on('data', (chunk) => {
|
|
504
|
-
if (stderr
|
|
563
|
+
if (!outputStore.append('stderr', chunk)) {
|
|
564
|
+
child.stderr?.pause();
|
|
565
|
+
outputStore.onDrain(() => child.stderr?.resume());
|
|
566
|
+
}
|
|
505
567
|
});
|
|
506
568
|
child.on('error', (e) => {
|
|
507
569
|
ac.signal.removeEventListener('abort', onKill);
|
|
@@ -524,6 +586,9 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
524
586
|
clearTimeout(timer);
|
|
525
587
|
signal?.removeEventListener('abort', onAbort);
|
|
526
588
|
}
|
|
589
|
+
const storedOutput = await outputStore.finish();
|
|
590
|
+
stdout = storedOutput.stdout;
|
|
591
|
+
stderr = storedOutput.stderr;
|
|
527
592
|
|
|
528
593
|
// 4b-2. ATTACH_IMAGE sentinel:脚本可以打一行 `<<<ATTACH_IMAGE:path>>>` 让模型亲眼看图。
|
|
529
594
|
// 协议行**从 content 剥掉**(那是给宿主看的,不是给模型读的噪声);被拒的条目
|
|
@@ -555,15 +620,18 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
555
620
|
parts.push(`\n[被信号中断]`);
|
|
556
621
|
}
|
|
557
622
|
parts.push(`\n[exit code: ${exitCode === null ? 'killed' : exitCode}]`);
|
|
623
|
+
const outputReference = storedOutput.path
|
|
624
|
+
? `\n[完整输出: ${storedOutput.path}; bytes=${storedOutput.bytes}; sha256=${storedOutput.sha256}; complete=${storedOutput.complete}]`
|
|
625
|
+
: '';
|
|
558
626
|
|
|
559
627
|
let combinedOutput = parts.join('\n');
|
|
560
628
|
|
|
561
629
|
// 4d. 大小兜底
|
|
562
|
-
if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
630
|
+
if (combinedOutput.length + outputReference.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
631
|
+
const suffix = `\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,预览已截断)${outputReference}`;
|
|
563
632
|
combinedOutput =
|
|
564
|
-
combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
|
|
565
|
-
|
|
566
|
-
}
|
|
633
|
+
combinedOutput.slice(0, Math.max(0, DEFAULT_MAX_RESULT_SIZE_CHARS - suffix.length)) + suffix;
|
|
634
|
+
} else combinedOutput += outputReference;
|
|
567
635
|
|
|
568
636
|
// 4e. 中断 / 超时仍然算硬失败
|
|
569
637
|
// 失败也带上已解析的附图:脚本挂掉前截的那张诊断图正是最该看的。
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const TOOL_CAPABILITIES = Object.freeze({
|
|
2
2
|
Read: 'files.read', Glob: 'files.read', Grep: 'files.read',
|
|
3
|
+
Snip: 'files.read',
|
|
3
4
|
Edit: 'files.write', MultiEdit: 'files.write', Write: 'files.write',
|
|
4
5
|
Bash: 'shell',
|
|
5
6
|
PowerShell: 'shell', TaskOutput: 'background.process', TaskStop: 'background.process',
|
|
7
|
+
Trash: 'files.trash',
|
|
8
|
+
TaskCreate: 'task.management', TaskGet: 'task.management', TaskUpdate: 'task.management', TaskList: 'task.management',
|
|
9
|
+
Skill: 'skill.invoke', EnterWorktree: 'worktree.session', ExitWorktree: 'worktree.session',
|
|
10
|
+
TerminalCapture: 'terminal.capture',
|
|
6
11
|
LSP: 'code.intelligence', NotebookEdit: 'files.write',
|
|
7
12
|
WebSearch: 'web.search', WebFetch: 'web.fetch', WebBrowser: 'web.browser',
|
|
8
13
|
Download: 'network.download',
|
|
@@ -14,15 +14,25 @@ export type CapabilityId =
|
|
|
14
14
|
| 'media'
|
|
15
15
|
| 'memory'
|
|
16
16
|
| 'code.intelligence'
|
|
17
|
-
| 'background.process'
|
|
17
|
+
| 'background.process'
|
|
18
|
+
| 'files.trash'
|
|
19
|
+
| 'task.management'
|
|
20
|
+
| 'skill.invoke'
|
|
21
|
+
| 'worktree.session'
|
|
22
|
+
| 'terminal.capture';
|
|
18
23
|
|
|
19
24
|
export type ToolBudgetClass = 'core' | 'task' | 'auxiliary';
|
|
20
25
|
|
|
21
26
|
const TOOL_CAPABILITIES: Readonly<Record<string, CapabilityId>> = Object.freeze({
|
|
22
27
|
Read: 'files.read', Glob: 'files.read', Grep: 'files.read',
|
|
28
|
+
Snip: 'files.read',
|
|
23
29
|
Edit: 'files.write', MultiEdit: 'files.write', Write: 'files.write',
|
|
24
30
|
Bash: 'shell',
|
|
25
31
|
PowerShell: 'shell', TaskOutput: 'background.process', TaskStop: 'background.process',
|
|
32
|
+
Trash: 'files.trash',
|
|
33
|
+
TaskCreate: 'task.management', TaskGet: 'task.management', TaskUpdate: 'task.management', TaskList: 'task.management',
|
|
34
|
+
Skill: 'skill.invoke', EnterWorktree: 'worktree.session', ExitWorktree: 'worktree.session',
|
|
35
|
+
TerminalCapture: 'terminal.capture',
|
|
26
36
|
LSP: 'code.intelligence', NotebookEdit: 'files.write',
|
|
27
37
|
WebSearch: 'web.search', WebFetch: 'web.fetch', WebBrowser: 'web.browser',
|
|
28
38
|
Download: 'network.download',
|
package/src/tools/edit/edit.js
CHANGED
|
@@ -20,6 +20,7 @@ import { filePathField } from '../shared/schemas.js';
|
|
|
20
20
|
import { assertFresh, hashFileBytes, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
21
21
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
22
22
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
23
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
23
24
|
/** 编辑文件大小上限:1GB。防止模型试图 Edit 超大文件撑爆内存。 */
|
|
24
25
|
const MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
25
26
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
@@ -96,10 +97,11 @@ async function call(input, state, cwd) {
|
|
|
96
97
|
await atomicWriteFile(filePath, new_string);
|
|
97
98
|
recordRead(filePath, { contentHash: hashFileBytes(encodeTextFile(new_string, 'utf8')) }, state);
|
|
98
99
|
emitFileMutation(filePath);
|
|
100
|
+
const patch = buildPatchSummary('', new_string, input.file_path);
|
|
99
101
|
return {
|
|
100
102
|
ok: true,
|
|
101
|
-
content: `已创建新文件:${filePath}(${new_string.length}
|
|
102
|
-
metadata: await fileActivityMetadata(filePath, 'created', 'not_run', 'support', cwd),
|
|
103
|
+
content: `已创建新文件:${filePath}(${new_string.length} 字符)\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
104
|
+
metadata: await fileActivityMetadata(filePath, 'created', 'not_run', 'support', cwd, patch),
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
catch (e) {
|
|
@@ -186,12 +188,13 @@ async function call(input, state, cwd) {
|
|
|
186
188
|
// 输出:报告改了几处 + 行变化
|
|
187
189
|
const linesBefore = original.split('\n').length;
|
|
188
190
|
const linesAfter = replaced.split('\n').length;
|
|
191
|
+
const patch = buildPatchSummary(original, normalizedReplaced, input.file_path);
|
|
189
192
|
return {
|
|
190
193
|
ok: true,
|
|
191
194
|
content: `已编辑 ${filePath}\n` +
|
|
192
195
|
`替换次数:${replaceAll ? occurrences : 1}\n` +
|
|
193
|
-
`行数:${linesBefore} → ${linesAfter}
|
|
194
|
-
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd),
|
|
196
|
+
`行数:${linesBefore} → ${linesAfter}\n` + (patch.unifiedDiff ?? '[diff omitted: too large]'),
|
|
197
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
195
198
|
};
|
|
196
199
|
}
|
|
197
200
|
// ---------------- 5. 模糊匹配提示 ----------------
|
package/src/tools/edit/edit.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { filePathField } from '../shared/schemas.js';
|
|
|
35
35
|
import { assertFresh, hashFileBytes, recordRead, wasTruncated, type FileReadState } from '../shared/fileState.js';
|
|
36
36
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
37
37
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
38
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
38
39
|
|
|
39
40
|
/** 编辑文件大小上限:1GB。防止模型试图 Edit 超大文件撑爆内存。 */
|
|
40
41
|
const MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
@@ -124,10 +125,11 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
124
125
|
await atomicWriteFile(filePath, new_string);
|
|
125
126
|
recordRead(filePath, { contentHash: hashFileBytes(encodeTextFile(new_string, 'utf8')) }, state);
|
|
126
127
|
emitFileMutation(filePath);
|
|
128
|
+
const patch = buildPatchSummary('', new_string, input.file_path);
|
|
127
129
|
return {
|
|
128
130
|
ok: true,
|
|
129
|
-
content: `已创建新文件:${filePath}(${new_string.length}
|
|
130
|
-
|
|
131
|
+
content: `已创建新文件:${filePath}(${new_string.length} 字符)\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
132
|
+
metadata: await fileActivityMetadata(filePath, 'created', 'not_run', 'support', cwd, patch),
|
|
131
133
|
};
|
|
132
134
|
} catch (e) {
|
|
133
135
|
return { ok: false, error: `创建文件失败:${(e as Error).message}` };
|
|
@@ -222,13 +224,14 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
222
224
|
// 输出:报告改了几处 + 行变化
|
|
223
225
|
const linesBefore = original.split('\n').length;
|
|
224
226
|
const linesAfter = replaced.split('\n').length;
|
|
227
|
+
const patch = buildPatchSummary(original, normalizedReplaced, input.file_path);
|
|
225
228
|
return {
|
|
226
229
|
ok: true,
|
|
227
230
|
content:
|
|
228
231
|
`已编辑 ${filePath}\n` +
|
|
229
232
|
`替换次数:${replaceAll ? occurrences : 1}\n` +
|
|
230
|
-
`行数:${linesBefore} → ${linesAfter}
|
|
231
|
-
|
|
233
|
+
`行数:${linesBefore} → ${linesAfter}\n` + (patch.unifiedDiff ?? '[diff omitted: too large]'),
|
|
234
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
232
235
|
};
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -22,6 +22,7 @@ import { filePathField } from '../shared/schemas.js';
|
|
|
22
22
|
import { assertFresh, hashFileBytes, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
23
23
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
24
24
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
25
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
25
26
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
26
27
|
const editItemSchema = z.object({
|
|
27
28
|
old_string: z.string().min(1).describe('要替换的原文本(不允许为空 —— 创建新文件请用 Edit 工具)'),
|
|
@@ -168,10 +169,11 @@ async function call(input, state, cwd) {
|
|
|
168
169
|
catch (e) {
|
|
169
170
|
return { ok: false, error: `写入失败:${e.message}` };
|
|
170
171
|
}
|
|
172
|
+
const patch = buildPatchSummary(originalContent, finalContent, input.file_path);
|
|
171
173
|
return {
|
|
172
174
|
ok: true,
|
|
173
|
-
content: `已对 ${filePath} 应用 ${edits.length}
|
|
174
|
-
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd),
|
|
175
|
+
content: `已对 ${filePath} 应用 ${edits.length} 处修改\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
176
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
175
177
|
};
|
|
176
178
|
}
|
|
177
179
|
// ---------------- 5. 导出 ----------------
|
|
@@ -37,6 +37,7 @@ import { filePathField } from '../shared/schemas.js';
|
|
|
37
37
|
import { assertFresh, hashFileBytes, recordRead, wasTruncated, type FileReadState } from '../shared/fileState.js';
|
|
38
38
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
39
39
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
40
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
40
41
|
|
|
41
42
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
42
43
|
const editItemSchema = z.object({
|
|
@@ -199,10 +200,11 @@ async function call(input: MultiEditInput, state?: FileReadState, cwd?: string):
|
|
|
199
200
|
return { ok: false, error: `写入失败:${(e as Error).message}` };
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
const patch = buildPatchSummary(originalContent, finalContent, input.file_path);
|
|
202
204
|
return {
|
|
203
205
|
ok: true,
|
|
204
|
-
content: `已对 ${filePath} 应用 ${edits.length}
|
|
205
|
-
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd),
|
|
206
|
+
content: `已对 ${filePath} 应用 ${edits.length} 处修改\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
207
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
206
208
|
};
|
|
207
209
|
}
|
|
208
210
|
|
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
|
};
|