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.
Files changed (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. package/workflows/youtube-shorts.yaml +25 -25
@@ -0,0 +1,63 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createTwoFilesPatch, diffLines } from 'diff';
3
+
4
+ import type { FilePatchSummary } from '../../types.js';
5
+
6
+ const MAX_BYTES = 5 * 1024 * 1024;
7
+ const MAX_LINES = 200_000;
8
+ const MAX_DIFF_CHARS = 16_000;
9
+
10
+ function hash(value: string): string {
11
+ return createHash('sha256').update(value).digest('hex');
12
+ }
13
+
14
+ export function buildPatchSummary(
15
+ before: string,
16
+ after: string,
17
+ label: string,
18
+ ): FilePatchSummary {
19
+ const beforeHash = hash(before);
20
+ const afterHash = hash(after);
21
+ const totalBytes = Buffer.byteLength(before) + Buffer.byteLength(after);
22
+ const totalLines =
23
+ (before.match(/\n/g)?.length ?? 0) + (after.match(/\n/g)?.length ?? 0) + 2;
24
+ if (totalBytes > MAX_BYTES || totalLines > MAX_LINES) {
25
+ return {
26
+ beforeHash,
27
+ afterHash,
28
+ additions: 0,
29
+ deletions: 0,
30
+ truncated: true,
31
+ omittedReason: 'too_large',
32
+ };
33
+ }
34
+
35
+ const changes = diffLines(before, after);
36
+ const additions = changes
37
+ .filter((change) => change.added)
38
+ .reduce((sum, change) => sum + (change.count ?? 0), 0);
39
+ const deletions = changes
40
+ .filter((change) => change.removed)
41
+ .reduce((sum, change) => sum + (change.count ?? 0), 0);
42
+ let unifiedDiff = createTwoFilesPatch(
43
+ `a/${label}`,
44
+ `b/${label}`,
45
+ before,
46
+ after,
47
+ undefined,
48
+ undefined,
49
+ { context: 3 },
50
+ );
51
+ const truncated = unifiedDiff.length > MAX_DIFF_CHARS;
52
+ if (truncated) {
53
+ unifiedDiff = `${unifiedDiff.slice(0, MAX_DIFF_CHARS)}\n... (diff preview truncated)`;
54
+ }
55
+ return {
56
+ unifiedDiff,
57
+ beforeHash,
58
+ afterHash,
59
+ additions,
60
+ deletions,
61
+ truncated,
62
+ };
63
+ }
@@ -22,7 +22,7 @@ function within(root, candidate) {
22
22
  return rel === '' ? '' : null;
23
23
  return rel.split(sep).join('/');
24
24
  }
25
- export async function fileActivityMetadata(filePath, activity, verification = 'not_run', purpose = activity === 'read' ? 'source' : 'support', cwd = getWorkingDir()) {
25
+ export async function fileActivityMetadata(filePath, activity, verification = 'not_run', purpose = activity === 'read' ? 'source' : 'support', cwd = getWorkingDir(), patch) {
26
26
  const lexicalRoot = resolve(cwd);
27
27
  const root = await realpath(lexicalRoot).catch(() => lexicalRoot);
28
28
  const candidate = await realpath(filePath).catch(() => resolve(filePath));
@@ -45,6 +45,7 @@ export async function fileActivityMetadata(filePath, activity, verification = 'n
45
45
  ...(MIME_BY_EXT[extname(relativePath).toLowerCase()] ? { mime: MIME_BY_EXT[extname(relativePath).toLowerCase()] } : {}),
46
46
  verification,
47
47
  purpose,
48
+ ...(patch ? { patch } : {}),
48
49
  }],
49
50
  };
50
51
  }
@@ -8,6 +8,7 @@ import type {
8
8
  FileActivityKind,
9
9
  FileActivityMetadata,
10
10
  FileVerification,
11
+ FilePatchSummary,
11
12
  } from '../../types.js';
12
13
 
13
14
  const MIME_BY_EXT: Record<string, string> = {
@@ -37,6 +38,7 @@ export async function fileActivityMetadata(
37
38
  verification: FileVerification = 'not_run',
38
39
  purpose: ConversationFilePurpose = activity === 'read' ? 'source' : 'support',
39
40
  cwd = getWorkingDir(),
41
+ patch?: FilePatchSummary,
40
42
  ): Promise<FileActivityMetadata | undefined> {
41
43
  const lexicalRoot = resolve(cwd);
42
44
  const root = await realpath(lexicalRoot).catch(() => lexicalRoot);
@@ -58,6 +60,7 @@ export async function fileActivityMetadata(
58
60
  ...(MIME_BY_EXT[extname(relativePath).toLowerCase()] ? { mime: MIME_BY_EXT[extname(relativePath).toLowerCase()] } : {}),
59
61
  verification,
60
62
  purpose,
63
+ ...(patch ? { patch } : {}),
61
64
  }],
62
65
  };
63
66
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 永久删除守卫:把「绕过回收站的删除」从 shell 里挡回去,逼模型改用可恢复的 Trash。
3
+ *
4
+ * ⚠️ 可靠性边界(与 src/irreversible/rules.ts 同一标准,别高估它):
5
+ * 这是**防模型误删**的护栏,不是对抗性沙盒。判据是对命令文本做正则匹配,
6
+ * 因此下列写法一律漏过,且**有意不追**(越追越像军备竞赛,误伤率先爆):
7
+ * · 绝对路径调用 /bin/rm -rf x
8
+ * · 间接执行 xargs rm / find -exec rm / echo $(rm x)
9
+ * · 换语言 python -c "import shutil; shutil.rmtree('x')"
10
+ * · 跨 shell 转发 cmd 里 powershell -c "Remove-Item x"
11
+ * 真正兜底的是「模型没有恶意」+ Trash 提供了更顺手的替代路径,不是这张正则表。
12
+ *
13
+ * ⚠️ `shell` 必须是**实际执行命令的那个 shell**,不是调用它的工具名。
14
+ * Bash 工具在 win32 找不到 Git Bash 时会回落 cmd.exe(见 tools/bash/bash.ts 的
15
+ * WIN_BASH 探测,且它的 description 会主动告诉模型「这里是 cmd」),那时模型写的是
16
+ * cmd 语法(del / rd),若仍按 'bash' 匹配 —— 三层防护全部落空、文件直接永久丢失。
17
+ */
18
+ /** POSIX shell:删除动词在命令位置(串首或 ; & | 换行之后)。 */
19
+ const BASH_DELETE = [
20
+ /(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
21
+ /(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
22
+ ];
23
+ /**
24
+ * cmd.exe:del / erase 删文件,rd / rmdir 删目录,一律不进回收站。
25
+ * 分隔符**必须含 `&`** —— cmd 用 `&` / `&&` 串命令,这正是 POWERSHELL_DELETE
26
+ * 的分隔符类 `[;|\n]` 不能直接复用的原因。
27
+ * `rm` 一并收进来:Git 的 usr\bin 可能在 PATH 上(此时 rm.exe 可用),而 cmd 本身没有 rm。
28
+ */
29
+ const CMD_DELETE = [/(?:^|[;&|\n])\s*(?:del|erase|rd|rmdir|rm)\b/i];
30
+ const GIT_CLEAN = /(?:^|[;&|\n])\s*git\b[^\n;&|]*\bclean\b([^\n;&|]*)/i;
31
+ const POWERSHELL_DELETE = [
32
+ /(?:^|[;|\n])\s*(?:Remove-Item|Clear-RecycleBin)\b/i,
33
+ /(?:^|[;|\n])\s*(?:del|erase|rd|rmdir|rm|ri)\b/i,
34
+ /\.Delete\s*\(/i,
35
+ ];
36
+ const RULES = {
37
+ bash: BASH_DELETE,
38
+ cmd: CMD_DELETE,
39
+ powershell: POWERSHELL_DELETE,
40
+ };
41
+ export function permanentDeleteReason(command, shell) {
42
+ let hit = RULES[shell].some((pattern) => pattern.test(command));
43
+ // `git clean -f` 三种 shell 里都能跑、都永久删除,所以不按 shell 分支。
44
+ // `-n` / `--dry-run` 是只读预演,必须放行(否则模型连"先看看会删什么"都做不到)。
45
+ if (!hit) {
46
+ const gitClean = GIT_CLEAN.exec(command);
47
+ const options = gitClean?.[1] ?? '';
48
+ const dryRun = /--dry-run\b/.test(options) ||
49
+ /(?:^|\s)-[a-z]*n[a-z]*(?:\s|$)/i.test(options);
50
+ hit = Boolean(gitClean &&
51
+ !dryRun &&
52
+ /(?:^|\s)-[a-z]*f[a-z]*(?:\s|$)|--force\b/i.test(options));
53
+ }
54
+ return hit
55
+ ? '该命令会绕过系统回收站永久删除文件。请改用 Trash;清理 Git 未跟踪文件请用 Trash(action="git-clean")。'
56
+ : undefined;
57
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 永久删除守卫:把「绕过回收站的删除」从 shell 里挡回去,逼模型改用可恢复的 Trash。
3
+ *
4
+ * ⚠️ 可靠性边界(与 src/irreversible/rules.ts 同一标准,别高估它):
5
+ * 这是**防模型误删**的护栏,不是对抗性沙盒。判据是对命令文本做正则匹配,
6
+ * 因此下列写法一律漏过,且**有意不追**(越追越像军备竞赛,误伤率先爆):
7
+ * · 绝对路径调用 /bin/rm -rf x
8
+ * · 间接执行 xargs rm / find -exec rm / echo $(rm x)
9
+ * · 换语言 python -c "import shutil; shutil.rmtree('x')"
10
+ * · 跨 shell 转发 cmd 里 powershell -c "Remove-Item x"
11
+ * 真正兜底的是「模型没有恶意」+ Trash 提供了更顺手的替代路径,不是这张正则表。
12
+ *
13
+ * ⚠️ `shell` 必须是**实际执行命令的那个 shell**,不是调用它的工具名。
14
+ * Bash 工具在 win32 找不到 Git Bash 时会回落 cmd.exe(见 tools/bash/bash.ts 的
15
+ * WIN_BASH 探测,且它的 description 会主动告诉模型「这里是 cmd」),那时模型写的是
16
+ * cmd 语法(del / rd),若仍按 'bash' 匹配 —— 三层防护全部落空、文件直接永久丢失。
17
+ */
18
+
19
+ /** POSIX shell:删除动词在命令位置(串首或 ; & | 换行之后)。 */
20
+ const BASH_DELETE = [
21
+ /(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
22
+ /(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
23
+ ];
24
+
25
+ /**
26
+ * cmd.exe:del / erase 删文件,rd / rmdir 删目录,一律不进回收站。
27
+ * 分隔符**必须含 `&`** —— cmd 用 `&` / `&&` 串命令,这正是 POWERSHELL_DELETE
28
+ * 的分隔符类 `[;|\n]` 不能直接复用的原因。
29
+ * `rm` 一并收进来:Git 的 usr\bin 可能在 PATH 上(此时 rm.exe 可用),而 cmd 本身没有 rm。
30
+ */
31
+ const CMD_DELETE = [/(?:^|[;&|\n])\s*(?:del|erase|rd|rmdir|rm)\b/i];
32
+
33
+ const GIT_CLEAN = /(?:^|[;&|\n])\s*git\b[^\n;&|]*\bclean\b([^\n;&|]*)/i;
34
+
35
+ const POWERSHELL_DELETE = [
36
+ /(?:^|[;|\n])\s*(?:Remove-Item|Clear-RecycleBin)\b/i,
37
+ /(?:^|[;|\n])\s*(?:del|erase|rd|rmdir|rm|ri)\b/i,
38
+ /\.Delete\s*\(/i,
39
+ ];
40
+
41
+ export type ShellKind = 'bash' | 'cmd' | 'powershell';
42
+
43
+ const RULES: Readonly<Record<ShellKind, RegExp[]>> = {
44
+ bash: BASH_DELETE,
45
+ cmd: CMD_DELETE,
46
+ powershell: POWERSHELL_DELETE,
47
+ };
48
+
49
+ export function permanentDeleteReason(
50
+ command: string,
51
+ shell: ShellKind,
52
+ ): string | undefined {
53
+ let hit = RULES[shell].some((pattern) => pattern.test(command));
54
+ // `git clean -f` 三种 shell 里都能跑、都永久删除,所以不按 shell 分支。
55
+ // `-n` / `--dry-run` 是只读预演,必须放行(否则模型连"先看看会删什么"都做不到)。
56
+ if (!hit) {
57
+ const gitClean = GIT_CLEAN.exec(command);
58
+ const options = gitClean?.[1] ?? '';
59
+ const dryRun =
60
+ /--dry-run\b/.test(options) ||
61
+ /(?:^|\s)-[a-z]*n[a-z]*(?:\s|$)/i.test(options);
62
+ hit = Boolean(
63
+ gitClean &&
64
+ !dryRun &&
65
+ /(?:^|\s)-[a-z]*f[a-z]*(?:\s|$)|--force\b/i.test(options),
66
+ );
67
+ }
68
+ return hit
69
+ ? '该命令会绕过系统回收站永久删除文件。请改用 Trash;清理 Git 未跟踪文件请用 Trash(action="git-clean")。'
70
+ : undefined;
71
+ }
@@ -0,0 +1,97 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createWriteStream, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { sessionRuntimeDir, pruneRuntimeOutputs, registerRuntimeOutput, unregisterRuntimeOutput, } from '../../session/runtimeStore.js';
5
+ const SPILL_AT = 64 * 1024, MAX_BYTES = 512 * 1024 * 1024, PREVIEW = 32 * 1024;
6
+ export class ToolOutputStore {
7
+ stdout = '';
8
+ stderr = '';
9
+ stdoutTail = '';
10
+ stderrTail = '';
11
+ stream;
12
+ pending = [];
13
+ hash = createHash('sha256');
14
+ diskHash;
15
+ diskBytes = 0;
16
+ bytes = 0;
17
+ complete = true;
18
+ path;
19
+ constructor(identity) {
20
+ const dir = join(sessionRuntimeDir(identity), 'outputs');
21
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
22
+ this.path = join(dir, `procout_${randomUUID()}.log`);
23
+ }
24
+ append(source, value) {
25
+ const raw = Buffer.from(value);
26
+ if (this.bytes >= MAX_BYTES) {
27
+ this.complete = false;
28
+ return true;
29
+ }
30
+ const selected = raw.subarray(0, MAX_BYTES - this.bytes);
31
+ if (selected.length < raw.length)
32
+ this.complete = false;
33
+ const shouldSpill = !this.stream && this.bytes + selected.length > SPILL_AT;
34
+ if (shouldSpill) {
35
+ this.stream = createWriteStream(this.path, { flags: 'wx', mode: 0o600 });
36
+ this.diskHash = createHash('sha256');
37
+ registerRuntimeOutput(this.path);
38
+ for (const item of this.pending)
39
+ this.writeFrame(item.source, item.data);
40
+ this.pending = [];
41
+ }
42
+ this.bytes += selected.length;
43
+ this.hash.update(selected);
44
+ const text = selected.toString('utf8');
45
+ if (source === 'stdout') {
46
+ if (this.stdout.length < PREVIEW)
47
+ this.stdout += text.slice(0, PREVIEW - this.stdout.length);
48
+ this.stdoutTail = (this.stdoutTail + text).slice(-PREVIEW);
49
+ }
50
+ else {
51
+ if (this.stderr.length < PREVIEW)
52
+ this.stderr += text.slice(0, PREVIEW - this.stderr.length);
53
+ this.stderrTail = (this.stderrTail + text).slice(-PREVIEW);
54
+ }
55
+ if (this.stream)
56
+ return this.writeFrame(source, selected);
57
+ this.pending.push({ source, data: Buffer.from(selected) });
58
+ return true;
59
+ }
60
+ frame(source, data) {
61
+ return Buffer.concat([Buffer.from(`[${source}] `), data]);
62
+ }
63
+ writeFrame(source, data) {
64
+ const framed = this.frame(source, data);
65
+ this.diskBytes += framed.length;
66
+ this.diskHash.update(framed);
67
+ return this.stream.write(framed);
68
+ }
69
+ onDrain(callback) {
70
+ this.stream?.once('drain', callback);
71
+ }
72
+ async finish() {
73
+ if (this.stream) {
74
+ try {
75
+ await new Promise((resolve, reject) => {
76
+ this.stream.once('error', reject);
77
+ this.stream.end(() => resolve());
78
+ });
79
+ }
80
+ finally {
81
+ unregisterRuntimeOutput(this.path);
82
+ void pruneRuntimeOutputs();
83
+ }
84
+ }
85
+ const preview = (head, tail) => this.stream && tail && !head.endsWith(tail)
86
+ ? `${head}\n... [middle stored on disk] ...\n${tail}`
87
+ : head;
88
+ return {
89
+ stdout: preview(this.stdout, this.stdoutTail),
90
+ stderr: preview(this.stderr, this.stderrTail),
91
+ ...(this.stream ? { path: this.path } : {}),
92
+ bytes: this.stream ? this.diskBytes : this.bytes,
93
+ sha256: this.stream ? this.diskHash.digest('hex') : this.hash.digest('hex'),
94
+ complete: this.complete,
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,110 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createWriteStream, mkdirSync, type WriteStream } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import {
5
+ sessionRuntimeDir,
6
+ pruneRuntimeOutputs,
7
+ registerRuntimeOutput,
8
+ type RuntimeIdentity,
9
+ unregisterRuntimeOutput,
10
+ } from '../../session/runtimeStore.js';
11
+
12
+ const SPILL_AT = 64 * 1024,
13
+ MAX_BYTES = 512 * 1024 * 1024,
14
+ PREVIEW = 32 * 1024;
15
+ export interface StoredOutput {
16
+ stdout: string;
17
+ stderr: string;
18
+ path?: string;
19
+ bytes: number;
20
+ sha256: string;
21
+ complete: boolean;
22
+ }
23
+ export class ToolOutputStore {
24
+ private stdout = '';
25
+ private stderr = '';
26
+ private stdoutTail = '';
27
+ private stderrTail = '';
28
+ private stream?: WriteStream;
29
+ private pending: Array<{ source: 'stdout' | 'stderr'; data: Buffer }> = [];
30
+ private hash = createHash('sha256');
31
+ private diskHash?: ReturnType<typeof createHash>;
32
+ private diskBytes = 0;
33
+ private bytes = 0;
34
+ private complete = true;
35
+ readonly path: string;
36
+ constructor(identity: RuntimeIdentity) {
37
+ const dir = join(sessionRuntimeDir(identity), 'outputs');
38
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
39
+ this.path = join(dir, `procout_${randomUUID()}.log`);
40
+ }
41
+ append(source: 'stdout' | 'stderr', value: Buffer | string): boolean {
42
+ const raw = Buffer.from(value);
43
+ if (this.bytes >= MAX_BYTES) {
44
+ this.complete = false;
45
+ return true;
46
+ }
47
+ const selected = raw.subarray(0, MAX_BYTES - this.bytes);
48
+ if (selected.length < raw.length) this.complete = false;
49
+ const shouldSpill = !this.stream && this.bytes + selected.length > SPILL_AT;
50
+ if (shouldSpill) {
51
+ this.stream = createWriteStream(this.path, { flags: 'wx', mode: 0o600 });
52
+ this.diskHash = createHash('sha256');
53
+ registerRuntimeOutput(this.path);
54
+ for (const item of this.pending) this.writeFrame(item.source, item.data);
55
+ this.pending = [];
56
+ }
57
+ this.bytes += selected.length;
58
+ this.hash.update(selected);
59
+ const text = selected.toString('utf8');
60
+ if (source === 'stdout') {
61
+ if (this.stdout.length < PREVIEW)
62
+ this.stdout += text.slice(0, PREVIEW - this.stdout.length);
63
+ this.stdoutTail = (this.stdoutTail + text).slice(-PREVIEW);
64
+ } else {
65
+ if (this.stderr.length < PREVIEW)
66
+ this.stderr += text.slice(0, PREVIEW - this.stderr.length);
67
+ this.stderrTail = (this.stderrTail + text).slice(-PREVIEW);
68
+ }
69
+ if (this.stream) return this.writeFrame(source, selected);
70
+ this.pending.push({ source, data: Buffer.from(selected) });
71
+ return true;
72
+ }
73
+ private frame(source: 'stdout' | 'stderr', data: Buffer): Buffer {
74
+ return Buffer.concat([Buffer.from(`[${source}] `), data]);
75
+ }
76
+ private writeFrame(source: 'stdout' | 'stderr', data: Buffer): boolean {
77
+ const framed = this.frame(source, data);
78
+ this.diskBytes += framed.length;
79
+ this.diskHash!.update(framed);
80
+ return this.stream!.write(framed);
81
+ }
82
+ onDrain(callback: () => void): void {
83
+ this.stream?.once('drain', callback);
84
+ }
85
+ async finish(): Promise<StoredOutput> {
86
+ if (this.stream) {
87
+ try {
88
+ await new Promise<void>((resolve, reject) => {
89
+ this.stream!.once('error', reject);
90
+ this.stream!.end(() => resolve());
91
+ });
92
+ } finally {
93
+ unregisterRuntimeOutput(this.path);
94
+ void pruneRuntimeOutputs();
95
+ }
96
+ }
97
+ const preview = (head: string, tail: string) =>
98
+ this.stream && tail && !head.endsWith(tail)
99
+ ? `${head}\n... [middle stored on disk] ...\n${tail}`
100
+ : head;
101
+ return {
102
+ stdout: preview(this.stdout, this.stdoutTail),
103
+ stderr: preview(this.stderr, this.stderrTail),
104
+ ...(this.stream ? { path: this.path } : {}),
105
+ bytes: this.stream ? this.diskBytes : this.bytes,
106
+ sha256: this.stream ? this.diskHash!.digest('hex') : this.hash.digest('hex'),
107
+ complete: this.complete,
108
+ };
109
+ }
110
+ }
@@ -0,0 +1,86 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { parse as parseYaml } from 'yaml';
6
+ import { getResourceSearchPaths } from '../../utils/resourcePaths.js';
7
+ import { toToolParameters } from '../../utils/zodToJson.js';
8
+ const MAX_SKILL_BYTES = 256 * 1024;
9
+ const schema = z.object({
10
+ name: z
11
+ .string()
12
+ .min(1)
13
+ .max(200)
14
+ .regex(/^[^/\\]+$/),
15
+ arguments: z.string().max(20_000).optional(),
16
+ });
17
+ const activated = new Map();
18
+ async function candidates(name, cwd) {
19
+ const exact = [];
20
+ for (const root of getResourceSearchPaths('skills', import.meta.url, cwd)) {
21
+ let entries;
22
+ try {
23
+ entries = await readdir(root, { withFileTypes: true });
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ for (const entry of entries) {
29
+ if (!entry.isDirectory())
30
+ continue;
31
+ const path = join(root, entry.name, 'SKILL.md');
32
+ try {
33
+ const text = await readFile(path, 'utf8');
34
+ const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
35
+ const metadata = frontmatter
36
+ ? parseYaml(frontmatter[1])
37
+ : undefined;
38
+ const declared = typeof metadata?.name === 'string' ? metadata.name.trim() : undefined;
39
+ if (entry.name === name || declared === name)
40
+ exact.push(path);
41
+ }
42
+ catch { }
43
+ }
44
+ }
45
+ return [...new Set(exact)];
46
+ }
47
+ async function call(input, context) {
48
+ const found = await candidates(input.name, context.cwd);
49
+ if (!found.length)
50
+ return { ok: false, error: `没有找到名称精确匹配的 Skill:${input.name}` };
51
+ // Search roots are ordered: project skill intentionally overrides the bundled skill.
52
+ const path = found[0];
53
+ const content = await readFile(path);
54
+ if (content.length > MAX_SKILL_BYTES)
55
+ return {
56
+ ok: false,
57
+ error: `Skill 指令过大(${content.length} > ${MAX_SKILL_BYTES} bytes),拒绝加载半份指令。`,
58
+ };
59
+ const hash = createHash('sha256').update(content).digest('hex');
60
+ const key = `${context.workspaceId ?? context.cwd ?? 'local'}:${input.name}`;
61
+ const activationSignature = createHash('sha256')
62
+ .update(hash)
63
+ .update('\0')
64
+ .update(input.arguments ?? '')
65
+ .digest('hex');
66
+ if (activated.get(key) === activationSignature)
67
+ return {
68
+ ok: true,
69
+ content: `Skill ${input.name} 已按相同内容激活(sha256=${hash},目录=${join(path, '..')})。无需重复注入。`,
70
+ };
71
+ activated.set(key, activationSignature);
72
+ return {
73
+ ok: true,
74
+ content: `Skill: ${input.name}\nRoot: ${join(path, '..')}\nSHA-256: ${hash}${input.arguments ? `\nUser arguments: ${input.arguments}` : ''}\n\n${content.toString('utf8')}`,
75
+ };
76
+ }
77
+ export const skillTool = {
78
+ name: 'Skill',
79
+ description: 'Loads one installed Skill by exact name and returns its complete SKILL.md instructions, root directory and content hash. Use this instead of manually locating SKILL.md.',
80
+ inputSchema: schema,
81
+ parameters: toToolParameters(schema),
82
+ isReadOnly: true,
83
+ isConcurrencySafe: true,
84
+ call: async () => ({ ok: false, error: 'Skill requires runtime context' }),
85
+ callWithContext: (input, _signal, context) => call(input, context),
86
+ };
@@ -0,0 +1,94 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { parse as parseYaml } from 'yaml';
6
+
7
+ import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
8
+ import { getResourceSearchPaths } from '../../utils/resourcePaths.js';
9
+ import { toToolParameters } from '../../utils/zodToJson.js';
10
+
11
+ const MAX_SKILL_BYTES = 256 * 1024;
12
+ const schema = z.object({
13
+ name: z
14
+ .string()
15
+ .min(1)
16
+ .max(200)
17
+ .regex(/^[^/\\]+$/),
18
+ arguments: z.string().max(20_000).optional(),
19
+ });
20
+ type Input = z.infer<typeof schema>;
21
+ const activated = new Map<string, string>();
22
+
23
+ async function candidates(name: string, cwd?: string): Promise<string[]> {
24
+ const exact: string[] = [];
25
+ for (const root of getResourceSearchPaths('skills', import.meta.url, cwd)) {
26
+ let entries;
27
+ try {
28
+ entries = await readdir(root, { withFileTypes: true });
29
+ } catch {
30
+ continue;
31
+ }
32
+ for (const entry of entries) {
33
+ if (!entry.isDirectory()) continue;
34
+ const path = join(root, entry.name, 'SKILL.md');
35
+ try {
36
+ const text = await readFile(path, 'utf8');
37
+ const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
38
+ const metadata = frontmatter
39
+ ? (parseYaml(frontmatter[1]!) as { name?: unknown })
40
+ : undefined;
41
+ const declared =
42
+ typeof metadata?.name === 'string' ? metadata.name.trim() : undefined;
43
+ if (entry.name === name || declared === name) exact.push(path);
44
+ } catch {}
45
+ }
46
+ }
47
+ return [...new Set(exact)];
48
+ }
49
+
50
+ async function call(
51
+ input: Input,
52
+ context: ToolCallContext,
53
+ ): Promise<ToolResult> {
54
+ const found = await candidates(input.name, context.cwd);
55
+ if (!found.length)
56
+ return { ok: false, error: `没有找到名称精确匹配的 Skill:${input.name}` };
57
+ // Search roots are ordered: project skill intentionally overrides the bundled skill.
58
+ const path = found[0]!;
59
+ const content = await readFile(path);
60
+ if (content.length > MAX_SKILL_BYTES)
61
+ return {
62
+ ok: false,
63
+ error: `Skill 指令过大(${content.length} > ${MAX_SKILL_BYTES} bytes),拒绝加载半份指令。`,
64
+ };
65
+ const hash = createHash('sha256').update(content).digest('hex');
66
+ const key = `${context.workspaceId ?? context.cwd ?? 'local'}:${input.name}`;
67
+ const activationSignature = createHash('sha256')
68
+ .update(hash)
69
+ .update('\0')
70
+ .update(input.arguments ?? '')
71
+ .digest('hex');
72
+ if (activated.get(key) === activationSignature)
73
+ return {
74
+ ok: true,
75
+ content: `Skill ${input.name} 已按相同内容激活(sha256=${hash},目录=${join(path, '..')})。无需重复注入。`,
76
+ };
77
+ activated.set(key, activationSignature);
78
+ return {
79
+ ok: true,
80
+ content: `Skill: ${input.name}\nRoot: ${join(path, '..')}\nSHA-256: ${hash}${input.arguments ? `\nUser arguments: ${input.arguments}` : ''}\n\n${content.toString('utf8')}`,
81
+ };
82
+ }
83
+
84
+ export const skillTool: Tool<Input> = {
85
+ name: 'Skill',
86
+ description:
87
+ 'Loads one installed Skill by exact name and returns its complete SKILL.md instructions, root directory and content hash. Use this instead of manually locating SKILL.md.',
88
+ inputSchema: schema,
89
+ parameters: toToolParameters(schema),
90
+ isReadOnly: true,
91
+ isConcurrencySafe: true,
92
+ call: async () => ({ ok: false, error: 'Skill requires runtime context' }),
93
+ callWithContext: (input, _signal, context) => call(input, context),
94
+ };