minimal-agent 0.10.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.
Files changed (573) hide show
  1. package/README.md +11 -3
  2. package/package.json +52 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
  4. package/plugins/computer_use/README.md +94 -0
  5. package/plugins/computer_use/app/_template.md +142 -0
  6. package/plugins/computer_use/app/chrome.md +165 -0
  7. package/plugins/computer_use/app/finder.md +76 -0
  8. package/plugins/computer_use/app/macos-system-settings.md +81 -0
  9. package/plugins/computer_use/app/microsoft-excel.md +80 -0
  10. package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
  11. package/plugins/computer_use/app/microsoft-word.md +79 -0
  12. package/plugins/computer_use/app/wechat.md +413 -0
  13. package/plugins/computer_use/app/weread.md +319 -0
  14. package/plugins/computer_use/commands/computeruse.md +25 -0
  15. package/plugins/computer_use/commands/cu.md +9 -0
  16. package/plugins/computer_use/plugin.js +26 -0
  17. package/plugins/computer_use/plugin.ts +30 -0
  18. package/plugins/computer_use/src/capture.js +90 -0
  19. package/plugins/computer_use/src/capture.ts +104 -0
  20. package/plugins/computer_use/src/commands.js +91 -0
  21. package/plugins/computer_use/src/commands.ts +103 -0
  22. package/plugins/computer_use/src/daemon.js +134 -0
  23. package/plugins/computer_use/src/daemon.ts +161 -0
  24. package/plugins/computer_use/src/manuals.js +135 -0
  25. package/plugins/computer_use/src/manuals.ts +159 -0
  26. package/plugins/computer_use/src/verify.js +168 -0
  27. package/plugins/computer_use/src/verify.ts +219 -0
  28. package/plugins/computer_use/test/commands.test.ts +73 -0
  29. package/plugins/cron-runner/plugin.ts +38 -0
  30. package/plugins/cron-runner/src/commands.ts +373 -0
  31. package/plugins/cron-runner/src/cronTime.ts +155 -0
  32. package/plugins/cron-runner/src/lock.ts +79 -0
  33. package/plugins/cron-runner/src/runs.ts +99 -0
  34. package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
  35. package/plugins/cron-runner/src/scheduler.ts +376 -0
  36. package/plugins/cron-runner/src/schema.ts +139 -0
  37. package/plugins/cron-runner/src/service.ts +193 -0
  38. package/plugins/cron-runner/src/store.ts +333 -0
  39. package/plugins/cron-runner/src/types.ts +141 -0
  40. package/plugins/cron-runner/test/cronTime.test.ts +41 -0
  41. package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
  42. package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
  43. package/plugins/cron-runner/test/service.test.ts +76 -0
  44. package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
  45. package/plugins/workflow-runner/plugin.js +56 -2
  46. package/plugins/workflow-runner/plugin.ts +113 -0
  47. package/plugins/workflow-runner/src/agentCommon.js +43 -4
  48. package/plugins/workflow-runner/src/agentCommon.ts +435 -0
  49. package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
  50. package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
  51. package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
  52. package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
  53. package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
  54. package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
  55. package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
  56. package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
  57. package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
  58. package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
  59. package/plugins/workflow-runner/src/dslManifest.ts +127 -0
  60. package/plugins/workflow-runner/src/expressions.ts +622 -0
  61. package/plugins/workflow-runner/src/index.js +13 -4
  62. package/plugins/workflow-runner/src/index.ts +338 -0
  63. package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
  64. package/plugins/workflow-runner/src/loader.js +23 -0
  65. package/plugins/workflow-runner/src/loader.ts +721 -0
  66. package/plugins/workflow-runner/src/multimodal.js +2 -1
  67. package/plugins/workflow-runner/src/multimodal.ts +64 -0
  68. package/plugins/workflow-runner/src/recovery.ts +149 -0
  69. package/plugins/workflow-runner/src/runner.js +75 -659
  70. package/plugins/workflow-runner/src/runner.ts +604 -0
  71. package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
  72. package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
  73. package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
  74. package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
  75. package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
  76. package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
  77. package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
  78. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  79. package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
  80. package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
  81. package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
  82. package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
  83. package/plugins/workflow-runner/src/stepPipeline.js +277 -0
  84. package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
  85. package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
  86. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  87. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  88. package/plugins/workflow-runner/src/types.js +37 -1
  89. package/plugins/workflow-runner/src/types.ts +602 -0
  90. package/plugins/workflow-runner/src/workflowState.ts +733 -0
  91. package/plugins/workflow-runner/test/agent.test.ts +587 -0
  92. package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
  93. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
  94. package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
  95. package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
  96. package/plugins/workflow-runner/test/context-files.test.ts +320 -0
  97. package/plugins/workflow-runner/test/e2e.test.ts +370 -0
  98. package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
  99. package/plugins/workflow-runner/test/expressions.test.ts +397 -0
  100. package/plugins/workflow-runner/test/foundation.test.ts +126 -0
  101. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  102. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
  103. package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
  104. package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
  105. package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
  106. package/plugins/workflow-runner/test/loader.test.ts +617 -0
  107. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
  108. package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
  109. package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
  110. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
  111. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
  112. package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
  113. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
  114. package/plugins/workflow-runner/test/recovery.test.ts +265 -0
  115. package/plugins/workflow-runner/test/runner.test.ts +911 -0
  116. package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
  117. package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
  118. package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
  119. package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
  120. package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
  121. package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
  122. package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
  123. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
  124. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
  125. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  126. package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
  127. package/plugins/workflow-runner/test/word.load.test.ts +208 -0
  128. package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
  129. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
  130. package/skills/browser-automation/SKILL.md +9 -19
  131. package/skills/web-crawler/SKILL.md +9 -11
  132. package/src/askBridge.ts +249 -0
  133. package/src/bootstrap/cwdArg.js +10 -3
  134. package/src/bootstrap/cwdArg.ts +31 -0
  135. package/src/bootstrap/workingDir.ts +48 -0
  136. package/src/cli/args.js +70 -7
  137. package/src/cli/args.ts +146 -0
  138. package/src/cli/computerUse.js +185 -0
  139. package/src/cli/computerUse.ts +221 -0
  140. package/src/cli/configMenu.js +274 -202
  141. package/src/cli/configMenu.tsx +521 -0
  142. package/src/cli/memoryAccess.js +45 -0
  143. package/src/cli/memoryAccess.ts +53 -0
  144. package/src/cli/print.js +119 -5
  145. package/src/cli/print.ts +597 -0
  146. package/src/cli/quickModelSetup.js +60 -0
  147. package/src/cli/quickModelSetup.tsx +96 -0
  148. package/src/cli/streamJson.js +80 -4
  149. package/src/cli/streamJson.ts +433 -0
  150. package/src/cli/wizards/advisorWizard.js +1 -0
  151. package/src/cli/wizards/advisorWizard.tsx +195 -0
  152. package/src/cli/wizards/agentRuntimeWizard.js +84 -0
  153. package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
  154. package/src/cli/wizards/browserWizard.js +55 -0
  155. package/src/cli/wizards/browserWizard.tsx +77 -0
  156. package/src/cli/wizards/contextAdvisorWizard.js +84 -0
  157. package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
  158. package/src/cli/wizards/fieldForm.js +7 -4
  159. package/src/cli/wizards/fieldForm.tsx +250 -0
  160. package/src/cli/wizards/imageWizard.js +4 -5
  161. package/src/cli/wizards/imageWizard.tsx +96 -0
  162. package/src/cli/wizards/openRouterWizard.tsx +48 -0
  163. package/src/cli/wizards/providerPresets.js +1 -8
  164. package/src/cli/wizards/providerPresets.ts +106 -0
  165. package/src/cli/wizards/providerWizard.js +152 -205
  166. package/src/cli/wizards/providerWizard.tsx +266 -0
  167. package/src/cli/wizards/searchWizard.tsx +47 -0
  168. package/src/cli/wizards/ttsWizard.js +4 -3
  169. package/src/cli/wizards/ttsWizard.tsx +61 -0
  170. package/src/config/configDefaults.js +25 -0
  171. package/src/config/configDefaults.ts +33 -0
  172. package/src/config/configDiagnostics.js +117 -0
  173. package/src/config/configDiagnostics.ts +166 -0
  174. package/src/config/configFile.js +52 -32
  175. package/src/config/configFile.ts +633 -0
  176. package/src/config/configManagement.js +126 -0
  177. package/src/config/configManagement.ts +159 -0
  178. package/src/config/configService.js +66 -11
  179. package/src/config/configService.ts +554 -0
  180. package/src/config/numericLimits.js +61 -0
  181. package/src/config/numericLimits.ts +80 -0
  182. package/src/config.ts +109 -0
  183. package/src/context/agentHome.ts +11 -0
  184. package/src/context/archive.ts +144 -0
  185. package/src/context/budgetPlan.js +54 -0
  186. package/src/context/budgetPlan.ts +88 -0
  187. package/src/context/compact.js +179 -106
  188. package/src/context/compact.ts +584 -0
  189. package/src/context/contextBundle.js +119 -0
  190. package/src/context/contextBundle.ts +176 -0
  191. package/src/context/contextLock.ts +84 -0
  192. package/src/context/microCompactLite.ts +218 -0
  193. package/src/context/persistContext.ts +177 -0
  194. package/src/context/reactiveCompact.js +19 -4
  195. package/src/context/reactiveCompact.ts +240 -0
  196. package/src/context/recentDirs.ts +42 -0
  197. package/src/context/sessionPath.ts +206 -0
  198. package/src/context/sessionRegistry.js +8 -2
  199. package/src/context/sessionRegistry.ts +414 -0
  200. package/src/context/snipCompact.ts +163 -0
  201. package/src/context/tokenCounter.ts +106 -0
  202. package/src/context/workspaceIndex.js +2 -2
  203. package/src/context/workspaceIndex.ts +751 -0
  204. package/src/entry.compiled.tsx +20 -0
  205. package/src/harness/claimValidation.js +9 -0
  206. package/src/harness/claimValidation.ts +22 -0
  207. package/src/harness/evidenceLedger.js +92 -0
  208. package/src/harness/evidenceLedger.ts +96 -0
  209. package/src/harness/stallGuard.js +60 -0
  210. package/src/harness/stallGuard.ts +81 -0
  211. package/src/humanIntervention.js +129 -0
  212. package/src/humanIntervention.ts +216 -0
  213. package/src/infra/verification.ts +295 -0
  214. package/src/irreversible/rules.js +216 -0
  215. package/src/irreversible/rules.ts +261 -0
  216. package/src/llm/client.js +40 -3
  217. package/src/llm/client.ts +621 -0
  218. package/src/llm/promptCache.js +27 -0
  219. package/src/llm/promptCache.ts +41 -0
  220. package/src/llm/providers/anthropicFormat.js +41 -5
  221. package/src/llm/providers/anthropicFormat.ts +290 -0
  222. package/src/llm/providers/openaiFormat.js +186 -8
  223. package/src/llm/providers/openaiFormat.ts +428 -0
  224. package/src/loop.js +614 -63
  225. package/src/loop.ts +1454 -0
  226. package/src/main.js +67 -10
  227. package/src/main.tsx +344 -0
  228. package/src/memory/codec.js +204 -0
  229. package/src/memory/codec.ts +249 -0
  230. package/src/memory/constants.js +34 -0
  231. package/src/memory/constants.ts +50 -0
  232. package/src/memory/episodicMemory.js +684 -0
  233. package/src/memory/episodicMemory.ts +815 -0
  234. package/src/memory/fileStore.js +148 -0
  235. package/src/memory/fileStore.ts +184 -0
  236. package/src/memory/memoryService.js +570 -0
  237. package/src/memory/memoryService.ts +859 -0
  238. package/src/memory/migration.js +128 -0
  239. package/src/memory/migration.ts +160 -0
  240. package/src/memory/repository.js +789 -0
  241. package/src/memory/repository.ts +893 -0
  242. package/src/memory/retrieval.js +108 -0
  243. package/src/memory/retrieval.ts +145 -0
  244. package/src/memory/searchIndex.js +107 -0
  245. package/src/memory/searchIndex.ts +153 -0
  246. package/src/memory/topicView.js +82 -0
  247. package/src/memory/topicView.ts +95 -0
  248. package/src/memory/turnJournal.js +95 -0
  249. package/src/memory/turnJournal.ts +120 -0
  250. package/src/memory/types.js +2 -0
  251. package/src/memory/types.ts +186 -0
  252. package/src/messageQueue.ts +123 -0
  253. package/src/multimodal.js +128 -46
  254. package/src/multimodal.ts +349 -0
  255. package/src/plugin-sdk.js +21 -1
  256. package/src/plugin-sdk.ts +134 -0
  257. package/src/plugins/builtinPluginIds.js +47 -0
  258. package/src/plugins/builtinPluginIds.ts +51 -0
  259. package/src/plugins/builtinRegistry.ts +43 -0
  260. package/src/plugins/commandRouter.js +40 -26
  261. package/src/plugins/commandRouter.ts +261 -0
  262. package/src/plugins/hookEngine.ts +341 -0
  263. package/src/plugins/pluginApi.ts +133 -0
  264. package/src/plugins/pluginLifecycle.ts +34 -0
  265. package/src/plugins/pluginLoader.ts +147 -0
  266. package/src/plugins/pluginManifest.js +127 -0
  267. package/src/plugins/pluginManifest.ts +152 -0
  268. package/src/plugins/pluginRunner.js +47 -2
  269. package/src/plugins/pluginRunner.ts +207 -0
  270. package/src/plugins/transcript.ts +208 -0
  271. package/src/prompts/lint.js +169 -0
  272. package/src/prompts/lint.ts +220 -0
  273. package/src/prompts/pluginList.js +32 -16
  274. package/src/prompts/pluginList.ts +86 -0
  275. package/src/prompts/policyRegistry.js +318 -0
  276. package/src/prompts/policyRegistry.ts +358 -0
  277. package/src/prompts/projectInstructions.js +24 -0
  278. package/src/prompts/projectInstructions.ts +81 -0
  279. package/src/prompts/promptAssembly.js +311 -202
  280. package/src/prompts/promptAssembly.ts +510 -0
  281. package/src/prompts/skillList.js +106 -45
  282. package/src/prompts/skillList.ts +198 -0
  283. package/src/prompts/system.js +31 -6
  284. package/src/prompts/system.ts +81 -0
  285. package/src/runtime/agentRuntimeContext.js +53 -0
  286. package/src/runtime/agentRuntimeContext.ts +112 -0
  287. package/src/session/runAgentTurn.js +71 -12
  288. package/src/session/runAgentTurn.ts +181 -0
  289. package/src/session/runTurn.js +29 -0
  290. package/src/session/runTurn.ts +160 -0
  291. package/src/session/runtimeStore.js +149 -0
  292. package/src/session/runtimeStore.ts +197 -0
  293. package/src/session/sessionState.js +79 -3
  294. package/src/session/sessionState.ts +134 -0
  295. package/src/toolGate.ts +50 -0
  296. package/src/tools/advisor/advisor.js +76 -15
  297. package/src/tools/advisor/advisor.ts +512 -0
  298. package/src/tools/agent/agent.js +245 -29
  299. package/src/tools/agent/agent.ts +837 -0
  300. package/src/tools/agent/agentTaskService.js +60 -0
  301. package/src/tools/agent/agentTaskService.ts +113 -0
  302. package/src/tools/agent/agentTypes.js +20 -8
  303. package/src/tools/agent/agentTypes.ts +94 -0
  304. package/src/tools/agent/coordinator.ts +111 -0
  305. package/src/tools/agent/worktree.ts +296 -0
  306. package/src/tools/ask/ask.js +17 -0
  307. package/src/tools/ask/ask.ts +122 -0
  308. package/src/tools/bash/attachImage.js +223 -0
  309. package/src/tools/bash/attachImage.ts +281 -0
  310. package/src/tools/bash/bash.js +132 -14
  311. package/src/tools/bash/bash.ts +642 -0
  312. package/src/tools/bash/semantics.ts +103 -0
  313. package/src/tools/bash/warnings.js +17 -11
  314. package/src/tools/bash/warnings.ts +117 -0
  315. package/src/tools/capabilities.js +42 -0
  316. package/src/tools/capabilities.ts +69 -0
  317. package/src/tools/computeruse/accessibilityPermission.js +261 -0
  318. package/src/tools/computeruse/accessibilityPermission.ts +294 -0
  319. package/src/tools/computeruse/axSearch.js +96 -0
  320. package/src/tools/computeruse/axSearch.ts +120 -0
  321. package/src/tools/computeruse/axSnap.js +272 -0
  322. package/src/tools/computeruse/axSnap.ts +349 -0
  323. package/src/tools/computeruse/capabilityLabels.js +95 -0
  324. package/src/tools/computeruse/capabilityLabels.ts +115 -0
  325. package/src/tools/computeruse/computerUseService.js +2235 -0
  326. package/src/tools/computeruse/computerUseService.ts +2686 -0
  327. package/src/tools/computeruse/computeruse.js +2742 -0
  328. package/src/tools/computeruse/computeruse.ts +3265 -0
  329. package/src/tools/computeruse/diagnosticTrace.js +154 -0
  330. package/src/tools/computeruse/diagnosticTrace.ts +195 -0
  331. package/src/tools/computeruse/diagnostics.js +369 -0
  332. package/src/tools/computeruse/diagnostics.ts +453 -0
  333. package/src/tools/computeruse/grants.js +53 -0
  334. package/src/tools/computeruse/grants.ts +70 -0
  335. package/src/tools/computeruse/humanBoundary.js +159 -0
  336. package/src/tools/computeruse/humanBoundary.ts +199 -0
  337. package/src/tools/computeruse/intervention.js +77 -0
  338. package/src/tools/computeruse/intervention.ts +110 -0
  339. package/src/tools/computeruse/manualSchema.js +709 -0
  340. package/src/tools/computeruse/manualSchema.ts +916 -0
  341. package/src/tools/computeruse/observationImages.js +705 -0
  342. package/src/tools/computeruse/observationImages.ts +889 -0
  343. package/src/tools/computeruse/protocol.js +60 -0
  344. package/src/tools/computeruse/protocol.ts +515 -0
  345. package/src/tools/computeruse/telemetryLog.js +57 -0
  346. package/src/tools/computeruse/telemetryLog.ts +67 -0
  347. package/src/tools/computeruse/vision/appGuide.js +325 -0
  348. package/src/tools/computeruse/vision/appGuide.ts +482 -0
  349. package/src/tools/computeruse/vision/focus.js +291 -0
  350. package/src/tools/computeruse/vision/focus.ts +382 -0
  351. package/src/tools/computeruse/vision/frames.js +248 -0
  352. package/src/tools/computeruse/vision/frames.ts +329 -0
  353. package/src/tools/computeruse/vision/geometry.js +131 -0
  354. package/src/tools/computeruse/vision/geometry.ts +163 -0
  355. package/src/tools/computeruse/vision/imaging.js +201 -0
  356. package/src/tools/computeruse/vision/imaging.ts +281 -0
  357. package/src/tools/computeruse/vision/png.js +274 -0
  358. package/src/tools/computeruse/vision/png.ts +317 -0
  359. package/src/tools/computeruse/vision/raster.js +320 -0
  360. package/src/tools/computeruse/vision/raster.ts +408 -0
  361. package/src/tools/computeruse/vision/settle.js +131 -0
  362. package/src/tools/computeruse/vision/settle.ts +220 -0
  363. package/src/tools/computeruse/vision/telemetry.js +196 -0
  364. package/src/tools/computeruse/vision/telemetry.ts +241 -0
  365. package/src/tools/computeruse/visionOcr.js +115 -0
  366. package/src/tools/computeruse/visionOcr.ts +128 -0
  367. package/src/tools/computeruse/waitFor.js +237 -0
  368. package/src/tools/computeruse/waitFor.ts +291 -0
  369. package/src/tools/download/download.js +10 -6
  370. package/src/tools/download/download.ts +187 -0
  371. package/src/tools/edit/edit.js +30 -18
  372. package/src/tools/edit/edit.ts +349 -0
  373. package/src/tools/edit/multi-edit.js +25 -14
  374. package/src/tools/edit/multi-edit.ts +223 -0
  375. package/src/tools/effectiveToolSet.js +73 -0
  376. package/src/tools/effectiveToolSet.ts +91 -0
  377. package/src/tools/genimage/genimage.ts +257 -0
  378. package/src/tools/genspeech/genspeech.ts +83 -0
  379. package/src/tools/glob/glob.js +176 -29
  380. package/src/tools/glob/glob.ts +283 -0
  381. package/src/tools/grep/grep.js +61 -19
  382. package/src/tools/grep/grep.ts +239 -0
  383. package/src/tools/grep/rgPath.ts +206 -0
  384. package/src/tools/index.js +99 -5
  385. package/src/tools/index.ts +293 -0
  386. package/src/tools/lsp/config.js +113 -0
  387. package/src/tools/lsp/config.ts +113 -0
  388. package/src/tools/lsp/lsp.js +256 -0
  389. package/src/tools/lsp/lsp.ts +274 -0
  390. package/src/tools/lsp/service.js +303 -0
  391. package/src/tools/lsp/service.ts +296 -0
  392. package/src/tools/memory/memory.js +520 -0
  393. package/src/tools/memory/memory.ts +625 -0
  394. package/src/tools/memory/policy.js +53 -0
  395. package/src/tools/memory/policy.ts +72 -0
  396. package/src/tools/notebook/notebookEdit.js +114 -0
  397. package/src/tools/notebook/notebookEdit.ts +126 -0
  398. package/src/tools/powershell/powershell.js +210 -0
  399. package/src/tools/powershell/powershell.ts +239 -0
  400. package/src/tools/presentresult/presentresult.js +145 -0
  401. package/src/tools/presentresult/presentresult.ts +156 -0
  402. package/src/tools/read/read.js +371 -163
  403. package/src/tools/read/read.ts +558 -0
  404. package/src/tools/shared/diffSummary.js +44 -0
  405. package/src/tools/shared/diffSummary.ts +63 -0
  406. package/src/tools/shared/fileActivity.js +51 -0
  407. package/src/tools/shared/fileActivity.ts +66 -0
  408. package/src/tools/shared/fileMutationBus.js +13 -0
  409. package/src/tools/shared/fileMutationBus.ts +14 -0
  410. package/src/tools/shared/fileState.js +73 -65
  411. package/src/tools/shared/fileState.ts +145 -0
  412. package/src/tools/shared/fileUtils.js +81 -4
  413. package/src/tools/shared/fileUtils.ts +504 -0
  414. package/src/tools/shared/killTree.ts +50 -0
  415. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  416. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  417. package/src/tools/shared/schemas.ts +20 -0
  418. package/src/tools/shared/toolOutputStore.js +97 -0
  419. package/src/tools/shared/toolOutputStore.ts +110 -0
  420. package/src/tools/skill/skill.js +86 -0
  421. package/src/tools/skill/skill.ts +94 -0
  422. package/src/tools/snip/snip.js +67 -0
  423. package/src/tools/snip/snip.ts +80 -0
  424. package/src/tools/tasks/backgroundTaskService.js +275 -0
  425. package/src/tools/tasks/backgroundTaskService.ts +369 -0
  426. package/src/tools/tasks/processEvidence.js +24 -0
  427. package/src/tools/tasks/processEvidence.ts +34 -0
  428. package/src/tools/tasks/taskManagement.js +163 -0
  429. package/src/tools/tasks/taskManagement.ts +180 -0
  430. package/src/tools/tasks/taskOutput.js +113 -0
  431. package/src/tools/tasks/taskOutput.ts +145 -0
  432. package/src/tools/tasks/taskStop.js +74 -0
  433. package/src/tools/tasks/taskStop.ts +99 -0
  434. package/src/tools/tasks/taskStore.js +46 -0
  435. package/src/tools/tasks/taskStore.ts +77 -0
  436. package/src/tools/terminal/terminalCapture.js +167 -0
  437. package/src/tools/terminal/terminalCapture.ts +179 -0
  438. package/src/tools/trash/trash.js +233 -0
  439. package/src/tools/trash/trash.ts +283 -0
  440. package/src/tools/types.js +1 -1
  441. package/src/tools/types.ts +24 -0
  442. package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
  443. package/src/tools/webbrowser/browser.js +214 -11
  444. package/src/tools/webbrowser/browser.ts +293 -0
  445. package/src/tools/webbrowser/browserControl.js +153 -48
  446. package/src/tools/webbrowser/browserControl.ts +262 -0
  447. package/src/tools/webbrowser/browserDaemon.js +147 -39
  448. package/src/tools/webbrowser/browserDaemon.ts +270 -0
  449. package/src/tools/webbrowser/browserOutcome.js +273 -0
  450. package/src/tools/webbrowser/browserOutcome.ts +306 -0
  451. package/src/tools/webbrowser/browserRunRegistry.js +150 -0
  452. package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
  453. package/src/tools/webbrowser/browserService.js +20 -6
  454. package/src/tools/webbrowser/browserService.ts +56 -0
  455. package/src/tools/webbrowser/browserStream.js +30 -1
  456. package/src/tools/webbrowser/browserStream.ts +216 -0
  457. package/src/tools/webbrowser/browserTabs.js +143 -0
  458. package/src/tools/webbrowser/browserTabs.ts +210 -0
  459. package/src/tools/webbrowser/webbrowser.js +831 -150
  460. package/src/tools/webbrowser/webbrowser.ts +1537 -0
  461. package/src/tools/webfetch/preapproved.ts +274 -0
  462. package/src/tools/webfetch/webfetch.js +24 -23
  463. package/src/tools/webfetch/webfetch.ts +383 -0
  464. package/src/tools/websearch/websearch.js +12 -11
  465. package/src/tools/websearch/websearch.ts +292 -0
  466. package/src/tools/worktree/sessionWorktree.js +128 -0
  467. package/src/tools/worktree/sessionWorktree.ts +158 -0
  468. package/src/tools/write/write.js +27 -44
  469. package/src/tools/write/write.ts +136 -0
  470. package/src/types.ts +960 -0
  471. package/src/ui/App.js +216 -12
  472. package/src/ui/App.tsx +879 -0
  473. package/src/ui/AskQuestion.tsx +300 -0
  474. package/src/ui/InputBox.js +77 -1
  475. package/src/ui/InputBox.tsx +567 -0
  476. package/src/ui/LiveArea.tsx +344 -0
  477. package/src/ui/MessageList.js +27 -6
  478. package/src/ui/MessageList.tsx +559 -0
  479. package/src/ui/ModelPicker.tsx +107 -0
  480. package/src/ui/QueuedMessagesBanner.tsx +51 -0
  481. package/src/ui/Root.js +6 -8
  482. package/src/ui/Root.tsx +100 -0
  483. package/src/ui/SessionPicker.tsx +169 -0
  484. package/src/ui/StatusLine.js +22 -1
  485. package/src/ui/StatusLine.tsx +145 -0
  486. package/src/ui/WorkspacePicker.tsx +84 -0
  487. package/src/ui/advisorCommand.js +54 -0
  488. package/src/ui/advisorCommand.ts +63 -0
  489. package/src/ui/computerCommand.js +19 -0
  490. package/src/ui/computerCommand.ts +35 -0
  491. package/src/ui/figures.ts +45 -0
  492. package/src/ui/hooks/askSlot.ts +112 -0
  493. package/src/ui/hooks/useChat.js +271 -7
  494. package/src/ui/hooks/useChat.ts +1578 -0
  495. package/src/ui/hooks/useInputHistory.ts +235 -0
  496. package/src/ui/hooks/usePasteHandler.ts +220 -0
  497. package/src/ui/hooks/useTerminalSize.ts +41 -0
  498. package/src/ui/hooks/useTextBuffer.ts +108 -0
  499. package/src/ui/hooks/useTokenUsage.ts +44 -0
  500. package/src/ui/liveViewport.ts +189 -0
  501. package/src/ui/markdown.ts +230 -0
  502. package/src/ui/textBuffer.ts +239 -0
  503. package/src/ui/theme.ts +35 -0
  504. package/src/utils/clipboardImage.js +358 -0
  505. package/src/utils/clipboardImage.ts +420 -0
  506. package/src/utils/debugLog.ts +50 -0
  507. package/src/utils/greenRoot.ts +35 -0
  508. package/src/utils/packageRoot.ts +40 -0
  509. package/src/utils/pathContains.js +28 -0
  510. package/src/utils/pathContains.ts +31 -0
  511. package/src/utils/resourcePaths.ts +71 -0
  512. package/src/utils/stripThink.ts +49 -0
  513. package/src/utils/zodToJson.ts +73 -0
  514. package/src/web/browserBridge.js +365 -66
  515. package/src/web/browserBridge.ts +672 -0
  516. package/src/web/protocol.ts +1804 -0
  517. package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
  518. package/workflows/ppt.yaml +13 -2
  519. package/workflows/wechat-reading-shots.yaml +315 -0
  520. package/workflows/word.yaml +11 -1
  521. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  522. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  523. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  524. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  526. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  527. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  528. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  529. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  530. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  531. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  532. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  533. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  534. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  535. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  536. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  537. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  538. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  539. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  540. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  541. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  542. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  543. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  544. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  545. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  546. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  547. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  548. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  549. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  550. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  551. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  552. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  553. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  554. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  555. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  556. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  557. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  558. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  559. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  560. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  561. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  562. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  563. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  564. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  565. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  566. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  567. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  568. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  569. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  570. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  571. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  572. /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
  573. /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * ============================================================
3
+ * src/tools/agent/worktree.ts —— 子 agent 的 git worktree 隔离助手
4
+ * ------------------------------------------------------------
5
+ * 搬自 Claude Code 官方 utils/worktree.ts 的核心路径(createAgentWorktree
6
+ * 那条**无全局状态**的路径——并行子 agent 不能共享单例 session 状态),
7
+ * 砍掉 settings/hooks/tmux/sparse-checkout/symlink 等锦上添花件。
8
+ *
9
+ * 保留的官方实战经验(每条都对应真实事故教训):
10
+ * - slug 白名单校验:slug 会被拼进目录路径与分支名,防路径穿越。
11
+ * - `git worktree add -B`(大写 B):上次目录被删但分支残留时强制重置,
12
+ * 免去额外的 git branch -D 前置清理。
13
+ * - fail-closed 改动判定:任何 git 命令失败一律视为「有改动」,宁可多留
14
+ * 一个 worktree,绝不误删并行子 agent 的工作成果。
15
+ * - 清理序列:worktree remove --force → 等 100ms 锁释放 → branch -D;
16
+ * 清理失败只记日志不抛——绝不让清理问题卡死主流程。
17
+ * - GIT_TERMINAL_PROMPT=0 + stdin ignore:防 git/ssh 弹交互式凭证输入挂死。
18
+ *
19
+ * 目录选址 `<gitRoot>/.agent-worktrees/`:刻意避开 `.minimal-agent*` 保留前缀
20
+ * (/new 会按前缀整目录 rm -rf,直接删 worktree 目录会留下悬空的 git 注册项);
21
+ * 首次创建时追加进 `<gitdir>/info/exclude`(仓库本地 ignore,不污染共享 .gitignore)。
22
+ * ============================================================
23
+ */
24
+
25
+ import { spawn } from 'node:child_process';
26
+ import { appendFile, readFile, writeFile, unlink } from 'node:fs/promises';
27
+ import { join } from 'node:path';
28
+ import { getWorkingDir } from '../../bootstrap/workingDir.js';
29
+ import { dlog } from '../../utils/debugLog.js';
30
+
31
+ const WORKTREES_DIR = '.agent-worktrees';
32
+
33
+ interface GitResult {
34
+ ok: boolean;
35
+ stdout: string;
36
+ stderr: string;
37
+ }
38
+
39
+ /** 跑一条 git 命令(防挂死 env + stdin ignore),spawn 失败也归一成 ok:false。 */
40
+ async function runGit(args: string[], cwd: string): Promise<GitResult> {
41
+ return new Promise((resolve) => {
42
+ const child = spawn('git', args, {
43
+ cwd,
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '' },
46
+ });
47
+ let stdout = '';
48
+ let stderr = '';
49
+ child.stdout.on('data', (d: Buffer) => { stdout += d.toString('utf8'); });
50
+ child.stderr.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
51
+ child.on('error', (e) => resolve({ ok: false, stdout, stderr: String(e) }));
52
+ child.on('close', (code) =>
53
+ resolve({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() }),
54
+ );
55
+ });
56
+ }
57
+
58
+ /** slug 白名单(官方同款语义):会拼进路径和分支名,只放行安全字符。 */
59
+ export function validateSlug(slug: string): boolean {
60
+ return /^[a-zA-Z0-9._-]{1,64}$/.test(slug) && slug !== '.' && slug !== '..';
61
+ }
62
+
63
+ export interface AgentWorktree {
64
+ ok: true;
65
+ /** worktree 目录绝对路径 */
66
+ path: string;
67
+ /** 临时分支名(agent/<slug>) */
68
+ branch: string;
69
+ /** 创建时刻的基线提交(改动判定 / diff 摘要的锚点) */
70
+ baseSha: string;
71
+ /** 主仓库根 */
72
+ gitRoot: string;
73
+ }
74
+
75
+ export type CreateWorktreeResult = AgentWorktree | { ok: false; error: string };
76
+
77
+ /** 把 worktree 目录加进仓库本地 ignore(best-effort,失败静默——只是降噪)。 */
78
+ async function ensureLocalExclude(cwd: string): Promise<void> {
79
+ try {
80
+ const r = await runGit(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
81
+ if (!r.ok || !r.stdout) return;
82
+ const excludePath = join(r.stdout, 'info', 'exclude');
83
+ const line = `${WORKTREES_DIR}/`;
84
+ const existing = await readFile(excludePath, 'utf8').catch(() => '');
85
+ if (existing.split(/\r?\n/).includes(line)) return;
86
+ await appendFile(excludePath, `${existing.endsWith('\n') || existing === '' ? '' : '\n'}${line}\n`);
87
+ } catch {
88
+ /* ignore 失败只影响 git status 观感,不影响功能 */
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 在当前仓库创建一个子 agent 专用 worktree(基于 HEAD 的独立分支)。
94
+ * 非 git 仓库 / 无提交仓库返回 ok:false——把决策权交回主 agent(降级无隔离或串行)。
95
+ */
96
+ export async function createAgentWorktree(
97
+ slug: string,
98
+ fromCwd: string = getWorkingDir(),
99
+ ): Promise<CreateWorktreeResult> {
100
+ if (!validateSlug(slug)) {
101
+ return { ok: false, error: `非法的 worktree 名称「${slug}」(只允许字母数字._-,≤64 字符)` };
102
+ }
103
+ const root = await runGit(['rev-parse', '--show-toplevel'], fromCwd);
104
+ if (!root.ok || !root.stdout) {
105
+ return { ok: false, error: '当前目录不是 git 仓库,无法创建 worktree 隔离' };
106
+ }
107
+ const gitRoot = root.stdout;
108
+ const head = await runGit(['rev-parse', 'HEAD'], gitRoot);
109
+ if (!head.ok || !head.stdout) {
110
+ return { ok: false, error: '仓库还没有任何提交(HEAD 不存在),无法创建 worktree——先 git commit 一次' };
111
+ }
112
+ const baseSha = head.stdout;
113
+ const path = join(gitRoot, WORKTREES_DIR, slug);
114
+ const branch = `agent/${slug}`;
115
+
116
+ await ensureLocalExclude(gitRoot);
117
+
118
+ const add = await runGit(['worktree', 'add', '-B', branch, path, 'HEAD'], gitRoot);
119
+ if (!add.ok) {
120
+ return { ok: false, error: `git worktree add 失败:${add.stderr || add.stdout}` };
121
+ }
122
+ return { ok: true, path, branch, baseSha, gitRoot };
123
+ }
124
+
125
+ /**
126
+ * worktree 相对基线是否有改动(未提交文件 或 新增提交)。
127
+ * fail-closed:任何 git 命令失败一律返回 true(有改动)——宁多留不误删。
128
+ */
129
+ export async function hasWorktreeChanges(path: string, baseSha: string): Promise<boolean> {
130
+ const status = await runGit(['status', '--porcelain'], path);
131
+ if (!status.ok) return true;
132
+ if (status.stdout.length > 0) return true;
133
+ const revList = await runGit(['rev-list', '--count', `${baseSha}..HEAD`], path);
134
+ if (!revList.ok) return true;
135
+ return Number.parseInt(revList.stdout, 10) > 0;
136
+ }
137
+
138
+ /** 相对基线的 diff 摘要(best-effort,失败返回空串)——给主 agent 收口审阅用。 */
139
+ export async function worktreeDiffStat(path: string, baseSha: string): Promise<string> {
140
+ const diff = await runGit(['diff', '--stat', baseSha], path);
141
+ const parts: string[] = [];
142
+ if (diff.ok && diff.stdout) parts.push(diff.stdout);
143
+ // diff --stat 看不到未跟踪的新文件,用 status 补上
144
+ const status = await runGit(['status', '--porcelain'], path);
145
+ if (status.ok && status.stdout) {
146
+ const untracked = status.stdout
147
+ .split('\n')
148
+ .filter((l) => l.startsWith('??'))
149
+ .map((l) => ` 新文件(未跟踪): ${l.slice(3)}`);
150
+ if (untracked.length > 0) parts.push(untracked.join('\n'));
151
+ }
152
+ return parts.join('\n');
153
+ }
154
+
155
+ /**
156
+ * 把 worktree 里的未提交改动确定性提交到临时分支(幂等:无改动直接 ok)。
157
+ * 为什么需要:子 agent 用 Edit/Write 改文件后通常**不会** git commit——
158
+ * 分支上没有提交的话,主 agent 收口时 `git merge` 合不到任何东西(改动只活在
159
+ * worktree 工作区里)。收尾时由引擎代提交,不依赖模型自觉。
160
+ * -c 内联身份兜底:机器没配 user.name/email 也能提交(squash 合并时最终
161
+ * 提交人仍是用户自己,临时分支上的作者名无所谓)。
162
+ */
163
+ export async function commitWorktreeChanges(
164
+ path: string,
165
+ message: string,
166
+ ): Promise<{ ok: boolean; error?: string }> {
167
+ const status = await runGit(['status', '--porcelain'], path);
168
+ if (status.ok && status.stdout.length === 0) return { ok: true };
169
+ if (status.ok) {
170
+ const sensitive = status.stdout.split('\n').map((line) => line.slice(3).trim()).filter(isSensitiveWorktreePath);
171
+ if (sensitive.length > 0) {
172
+ return { ok: false, error: `拒绝自动提交敏感文件:${sensitive.join(', ')}` };
173
+ }
174
+ }
175
+ const add = await runGit(['add', '-A'], path);
176
+ if (!add.ok) return { ok: false, error: add.stderr || add.stdout };
177
+ const commit = await runGit(
178
+ [
179
+ '-c', 'user.name=minimal-agent',
180
+ '-c', 'user.email=agent@minimal-agent.local',
181
+ 'commit', '-m', message,
182
+ ],
183
+ path,
184
+ );
185
+ if (!commit.ok) return { ok: false, error: commit.stderr || commit.stdout };
186
+ return { ok: true };
187
+ }
188
+
189
+ function isSensitiveWorktreePath(path: string): boolean {
190
+ const normalized = path.replaceAll('\\', '/').toLowerCase();
191
+ const base = normalized.split('/').at(-1) ?? normalized;
192
+ return base === '.env' || base.startsWith('.env.') || normalized.includes('/.ssh/') ||
193
+ /(^|\/)(credentials?|secrets?)(\.|\/|$)/.test(normalized) ||
194
+ /\.(pem|key|p12|pfx|jks|keystore)$/.test(base) || base === 'id_rsa' || base === 'id_ed25519';
195
+ }
196
+
197
+ export async function worktreeChangedFiles(path: string, baseSha: string): Promise<string[]> {
198
+ const result = await runGit(['diff', '--name-only', baseSha], path);
199
+ const status = await runGit(['status', '--porcelain'], path);
200
+ const files = new Set<string>();
201
+ if (result.ok) for (const row of result.stdout.split('\n')) if (row.trim()) files.add(row.trim());
202
+ if (status.ok) for (const row of status.stdout.split('\n')) if (row.length > 3) files.add(row.slice(3).trim());
203
+ return [...files].sort();
204
+ }
205
+
206
+ /**
207
+ * 清理 worktree 目录 + 临时分支。尽力而为:失败只记 debug 日志,不抛——
208
+ * 清理残留可由用户随时 `git worktree remove` / `git branch -D` 手动处理。
209
+ */
210
+ export async function removeAgentWorktree(
211
+ path: string,
212
+ branch: string,
213
+ gitRoot: string,
214
+ ): Promise<void> {
215
+ const rm = await runGit(['worktree', 'remove', '--force', path], gitRoot);
216
+ if (!rm.ok) dlog('agent', 'worktree remove 失败', { path, err: rm.stderr });
217
+ // 官方经验:remove 后立即 branch -D 偶发撞锁,等 100ms 释放
218
+ await new Promise((r) => setTimeout(r, 100));
219
+ const del = await runGit(['branch', '-D', branch], gitRoot);
220
+ if (!del.ok) dlog('agent', 'worktree 分支删除失败', { branch, err: del.stderr });
221
+ }
222
+
223
+ export interface AgentWorktreeDiff {
224
+ id: string;
225
+ branch: string;
226
+ baseSha: string;
227
+ diff: string;
228
+ changedFiles: string[];
229
+ }
230
+
231
+ async function resolveManagedWorktree(cwd: string, id: string): Promise<AgentWorktree | { ok: false; error: string }> {
232
+ if (!validateSlug(id)) return { ok: false, error: '非法的子任务 ID' };
233
+ const root = await runGit(['rev-parse', '--show-toplevel'], cwd);
234
+ if (!root.ok || !root.stdout) return { ok: false, error: '当前目录不是 Git 仓库' };
235
+ const path = join(root.stdout, WORKTREES_DIR, id);
236
+ const branch = `agent/${id}`;
237
+ const base = await runGit(['merge-base', branch, 'HEAD'], root.stdout);
238
+ if (!base.ok || !base.stdout) return { ok: false, error: '未找到该隔离子任务' };
239
+ return { ok: true, path, branch, baseSha: base.stdout, gitRoot: root.stdout };
240
+ }
241
+
242
+ export async function inspectAgentWorktree(cwd: string, id: string): Promise<AgentWorktreeDiff | { error: string }> {
243
+ const wt = await resolveManagedWorktree(cwd, id);
244
+ if (!wt.ok) return { error: wt.error };
245
+ const diff = await runGit(['diff', '--binary', wt.baseSha, wt.branch], wt.gitRoot);
246
+ if (!diff.ok) return { error: diff.stderr || '读取差异失败' };
247
+ const status = await runGit(['status', '--short'], wt.path);
248
+ const pending = status.ok && status.stdout
249
+ ? `\n\n尚未提交的隔离工作区改动(不会自动应用):\n${status.stdout}`
250
+ : '';
251
+ return {
252
+ id,
253
+ branch: wt.branch,
254
+ baseSha: wt.baseSha,
255
+ diff: `${diff.stdout}${pending}`.trim(),
256
+ changedFiles: await worktreeChangedFiles(wt.path, wt.baseSha),
257
+ };
258
+ }
259
+
260
+ export async function applyAgentWorktree(cwd: string, id: string): Promise<{ ok: boolean; error?: string }> {
261
+ const wt = await resolveManagedWorktree(cwd, id);
262
+ if (!wt.ok) return wt;
263
+ const worktreeStatus = await runGit(['status', '--porcelain'], wt.path);
264
+ if (!worktreeStatus.ok) return { ok: false, error: '无法确认隔离工作区状态,已停止自动应用' };
265
+ if (worktreeStatus.stdout) {
266
+ return {
267
+ ok: false,
268
+ error: '隔离工作区仍有未提交改动(可能包含被安全策略拒绝提交的文件),为防止丢失,不能自动应用或清理',
269
+ };
270
+ }
271
+ const head = await runGit(['rev-parse', 'HEAD'], wt.gitRoot);
272
+ if (!head.ok || head.stdout !== wt.baseSha) return { ok: false, error: '当前 HEAD 已变化,不能安全应用;请先人工审阅和同步分支' };
273
+ const diff = await runGit(['diff', '--binary', wt.baseSha, wt.branch], wt.gitRoot);
274
+ if (!diff.ok) return { ok: false, error: diff.stderr || '生成差异失败' };
275
+ if (!diff.stdout) { await removeAgentWorktree(wt.path, wt.branch, wt.gitRoot); return { ok: true }; }
276
+ const patchFile = join(wt.gitRoot, '.git', `minimal-agent-${process.pid}-${Date.now()}.patch`);
277
+ try {
278
+ // runGit 会 trim 文本输出;git apply 要求补丁保留终止换行。
279
+ await writeFile(patchFile, `${diff.stdout}\n`, { mode: 0o600 });
280
+ const check = await runGit(['apply', '--check', patchFile], wt.gitRoot);
281
+ if (!check.ok) return { ok: false, error: `变更检查失败:${check.stderr || check.stdout}` };
282
+ const apply = await runGit(['apply', patchFile], wt.gitRoot);
283
+ if (!apply.ok) return { ok: false, error: `应用变更失败:${apply.stderr || apply.stdout}` };
284
+ await removeAgentWorktree(wt.path, wt.branch, wt.gitRoot);
285
+ return { ok: true };
286
+ } finally {
287
+ await unlink(patchFile).catch(() => {});
288
+ }
289
+ }
290
+
291
+ export async function discardAgentWorktree(cwd: string, id: string): Promise<{ ok: boolean; error?: string }> {
292
+ const wt = await resolveManagedWorktree(cwd, id);
293
+ if (!wt.ok) return wt;
294
+ await removeAgentWorktree(wt.path, wt.branch, wt.gitRoot);
295
+ return { ok: true };
296
+ }
@@ -20,6 +20,7 @@ const optionSchema = z.object({
20
20
  label: z
21
21
  .string()
22
22
  .min(1)
23
+ .max(40)
23
24
  .describe('选项显示文本(简洁);把推荐项放第一个,并在其 label 末尾标「(推荐)」'),
24
25
  description: z
25
26
  .string()
@@ -31,6 +32,7 @@ const questionSchema = z.object({
31
32
  header: z
32
33
  .string()
33
34
  .min(1)
35
+ .max(12, 'header 最多 12 个字符')
34
36
  .describe('问题的短标签(≤12 字),如「交互底座」「鉴权方式」'),
35
37
  multiSelect: z
36
38
  .boolean()
@@ -49,6 +51,21 @@ const inputSchema = z.object({
49
51
  .max(4) // 与工具描述/UI 上限硬对齐:超发在参数校验层被拒(模型拿 Zod 错误自纠),
50
52
  // 避免「TUI 静默截断到 4 题 vs 非交互全部自动作答」的行为分叉
51
53
  .describe('1-4 个问题(通常 1 个;确需一次问多件事时才多个)'),
54
+ }).superRefine((value, context) => {
55
+ const questions = new Set();
56
+ value.questions.forEach((question, questionIndex) => {
57
+ if (questions.has(question.question)) {
58
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['questions', questionIndex, 'question'], message: '问题文本不能重复' });
59
+ }
60
+ questions.add(question.question);
61
+ const labels = new Set();
62
+ question.options.forEach((option, optionIndex) => {
63
+ if (labels.has(option.label)) {
64
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['questions', questionIndex, 'options', optionIndex, 'label'], message: '同一问题的选项标签不能重复' });
65
+ }
66
+ labels.add(option.label);
67
+ });
68
+ });
52
69
  });
53
70
  // ---------------- 2. JSON Schema(由 Zod 自动派生) ----------------
54
71
  const parameters = toToolParameters(inputSchema);
@@ -0,0 +1,122 @@
1
+ /**
2
+ * ============================================================
3
+ * src/tools/ask/ask.ts —— AskUserQuestion 工具(向用户提结构化选择题)
4
+ * ------------------------------------------------------------
5
+ * 对齐上游 Claude Code 的 AskUserQuestion:让模型在「需要用户拍板」时停下来问,
6
+ * 而不是替用户瞎猜。它本身只是一个普通工具 —— call() 调 askBridge 的
7
+ * requestUserChoice(),TUI 弹选择界面、用户选完把答案当 tool_result 喂回模型。
8
+ * 「等用户」= 这个工具返回得慢;非交互宿主(-p/gateway)自动选推荐项(见 askBridge)。
9
+ *
10
+ * 这一个工具同时让主循环 / workflow(tool: AskUserQuestion)/ agent: 节点
11
+ * 都获得「问用户」的能力 —— 动态工作流的交互底座。
12
+ * ============================================================
13
+ */
14
+
15
+ import { z } from 'zod';
16
+
17
+ import { requestUserChoice } from '../../askBridge.js';
18
+ import type { Tool, ToolResult } from '../../types.js';
19
+ import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
20
+ import { toToolParameters } from '../../utils/zodToJson.js';
21
+
22
+ // ---------------- 1. Zod 输入 schema ----------------
23
+ const optionSchema = z.object({
24
+ label: z
25
+ .string()
26
+ .min(1)
27
+ .max(40)
28
+ .describe('选项显示文本(简洁);把推荐项放第一个,并在其 label 末尾标「(推荐)」'),
29
+ description: z
30
+ .string()
31
+ .optional()
32
+ .describe('该选项的说明 / 取舍,帮用户理解后果'),
33
+ });
34
+
35
+ const questionSchema = z.object({
36
+ question: z.string().min(1).describe('要问用户的完整问题'),
37
+ header: z
38
+ .string()
39
+ .min(1)
40
+ .max(12, 'header 最多 12 个字符')
41
+ .describe('问题的短标签(≤12 字),如「交互底座」「鉴权方式」'),
42
+ multiSelect: z
43
+ .boolean()
44
+ .optional()
45
+ .describe('是否允许多选;默认 false(单选)'),
46
+ options: z
47
+ .array(optionSchema)
48
+ .min(2)
49
+ .max(4)
50
+ .describe('2-4 个互斥选项;第一个应为你的推荐项'),
51
+ });
52
+
53
+ const inputSchema = z.object({
54
+ questions: z
55
+ .array(questionSchema)
56
+ .min(1)
57
+ .max(4) // 与工具描述/UI 上限硬对齐:超发在参数校验层被拒(模型拿 Zod 错误自纠),
58
+ // 避免「TUI 静默截断到 4 题 vs 非交互全部自动作答」的行为分叉
59
+ .describe('1-4 个问题(通常 1 个;确需一次问多件事时才多个)'),
60
+ }).superRefine((value, context) => {
61
+ const questions = new Set<string>();
62
+ value.questions.forEach((question, questionIndex) => {
63
+ if (questions.has(question.question)) {
64
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['questions', questionIndex, 'question'], message: '问题文本不能重复' });
65
+ }
66
+ questions.add(question.question);
67
+ const labels = new Set<string>();
68
+ question.options.forEach((option, optionIndex) => {
69
+ if (labels.has(option.label)) {
70
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['questions', questionIndex, 'options', optionIndex, 'label'], message: '同一问题的选项标签不能重复' });
71
+ }
72
+ labels.add(option.label);
73
+ });
74
+ });
75
+ });
76
+ type AskInput = z.infer<typeof inputSchema>;
77
+
78
+ // ---------------- 2. JSON Schema(由 Zod 自动派生) ----------------
79
+ const parameters = toToolParameters(inputSchema);
80
+
81
+ // ---------------- 3. Description(指导模型何时/如何用) ----------------
82
+ const description = `向用户提出结构化的选择题(最多 4 题,每题 2-4 个选项),用于需要用户做决定的场合。
83
+ 使用时机:
84
+ - 需求 / 方案有取舍、没有绝对对错(多个合理方案各有利弊)→ 让用户拍板;
85
+ - 关键信息缺失或模糊、继续做风险大 → 让用户补充。
86
+ 不要用于:你自己能从代码 / 上下文 / 常识判断出答案的问题(那样只会打断用户)。
87
+ 约定:每题第一个选项放你的推荐,并在其 label 末尾标「(推荐)」。
88
+ 界面会自动附带「其他(自由输入)」逃生项,**无需**你手动加。
89
+ 返回值是用户的选择文本,据此继续。`;
90
+
91
+ // ---------------- 4. call() ----------------
92
+ async function call(input: AskInput, signal?: AbortSignal): Promise<ToolResult> {
93
+ try {
94
+ const answers = await requestUserChoice(input.questions, signal);
95
+ const lines: string[] = ['用户已回答 AskUserQuestion:', ''];
96
+ answers.forEach((a, i) => {
97
+ lines.push(`Q${i + 1} [${a.header}] ${a.question}`);
98
+ lines.push(`A${i + 1}: ${a.selected.join('、') || '(未选)'}`);
99
+ lines.push('');
100
+ });
101
+ return { ok: true, content: lines.join('\n').trimEnd() };
102
+ } catch (e) {
103
+ // 提问被 reject → 当普通工具失败喂回。注意:TUI 里 ESC 经 App 的
104
+ // onCancel={chat.abort} 中断**整轮**(喂回后 loop 见 signal.aborted 收口
105
+ // interrupted,模型不会继续);「让模型换思路」只在宿主 reject 单个提问
106
+ // 而不中断整轮时成立(workflow onError / 非 TUI 宿主自定义 handler)。
107
+ return { ok: false, error: `提问被取消 / 中断:${(e as Error).message}` };
108
+ }
109
+ }
110
+
111
+ // ---------------- 5. 导出 ----------------
112
+ export const askUserQuestionTool: Tool<AskInput> = {
113
+ name: 'AskUserQuestion',
114
+ description,
115
+ inputSchema,
116
+ parameters,
117
+ isReadOnly: true,
118
+ isConcurrencySafe: false,
119
+ maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
120
+ interruptBehavior: () => 'cancel',
121
+ call,
122
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * ============================================================
3
+ * src/tools/bash/attachImage.ts —— Bash 的「让模型看一眼这张图」协议
4
+ * ------------------------------------------------------------
5
+ * 背景(为什么需要这个口子):
6
+ * Read 工具明确拒绝二进制/图片(read.ts 的黑名单),Bash 只能回文本,
7
+ * 全仓库唯一会产 `ToolResult.visionImages` 的是 WebBrowser。于是任何
8
+ * 「skill 脚本生成/截取了一张图,想让模型亲眼看看」的场景都物理上做不到 ——
9
+ * computer-use(截屏)、生图核验、图表渲染核验全卡在这一处。
10
+ *
11
+ * 方案:约定一个 stdout sentinel。脚本自己打一行
12
+ * <<<ATTACH_IMAGE:/abs/path.png|detail=low|maxLongSide=960>>>
13
+ * Bash 解析出来填进 visionImages,并把该行**从给模型看的 content 里剥掉**
14
+ * (协议是给宿主看的,不是给模型读的噪声)。
15
+ *
16
+ * 为什么是 Bash 的 sentinel 而不是新工具:
17
+ * 工具 schema 已占稳定系统提示词前缀的绝大头,新增一个多 action 的工具
18
+ * 等于让**所有用户的每次请求**都为它付 token。sentinel 是零新增 schema
19
+ * 字节的加性改动,而且天然通用 —— 任何脚本都能用,不绑任何具体 skill。
20
+ *
21
+ * 安全边界(重要):
22
+ * `multimodal.ts::imageToContentPart` 对路径**没有任何沙箱**,它会老实读
23
+ * 任何给它的绝对路径并 base64 进请求体。既然现在允许「命令输出决定读哪个
24
+ * 文件」,防线就必须建在这里:
25
+ * ① 路径必须落在 cwd 或系统临时目录之下(realpath 之后比较,防软链逃逸)
26
+ * ② 数量上限(图很贵,一次回 20 张能直接打爆上下文)
27
+ * ③ 单图与总字节上限
28
+ * 越界的一律拒绝,并**如实写进 content 告诉模型哪张被拒、为什么** ——
29
+ * 静默丢弃会让脚本作者以为图发出去了,debug 时无从下手。
30
+ * ============================================================
31
+ */
32
+ import { open, realpath, stat } from 'node:fs/promises';
33
+ import { tmpdir } from 'node:os';
34
+ import { basename, dirname, join, resolve } from 'node:path';
35
+ import { getWorkingDir } from '../../bootstrap/workingDir.js';
36
+ import { pathContains } from '../../utils/pathContains.js';
37
+ import { MAX_IMAGE_BYTES, sniffImageMime } from '../../multimodal.js';
38
+ /** 一次 Bash 调用最多附多少张图。图是上下文里最贵的东西,宁少勿多。 */
39
+ export const MAX_ATTACH_IMAGES = 4;
40
+ /**
41
+ * 一次 Bash 调用附图的总字节上限。取单图上限的 2 倍而非 4 倍:
42
+ * base64 会把体积撑到约 4/3,且 provider 侧有请求体上限(Claude 32MB),
43
+ * 留足余量给同一请求里的文本与历史。
44
+ */
45
+ export const MAX_ATTACH_TOTAL_BYTES = MAX_IMAGE_BYTES * 2;
46
+ /** 整行 sentinel(必须独占一行;行首尾允许空白)。 */
47
+ const SENTINEL_LINE = /^[ \t]*<<<ATTACH_IMAGE:(.*?)>>>[ \t]*$/;
48
+ /** 已知的可选段,从 payload 尾部往前剥;剩下的(可能含 `|`)才是路径。 */
49
+ const OPTION_SEGMENT = /^(detail|maxLongSide)=(.*)$/;
50
+ const VALID_DETAILS = new Set(['low', 'default', 'high']);
51
+ /**
52
+ * 纯函数:从 stdout 里摘出 sentinel 行并剥除。不碰文件系统,便于单测。
53
+ *
54
+ * 无法识别的 payload(如空路径、只有选项没有路径)当作**普通输出行保留** ——
55
+ * 宁可让模型看到一行奇怪的文本,也不要把用户真实的输出悄悄吃掉。
56
+ */
57
+ export function extractAttachImageDirectives(stdout) {
58
+ if (!stdout.includes('<<<ATTACH_IMAGE:')) {
59
+ return { text: stdout, directives: [] };
60
+ }
61
+ const directives = [];
62
+ const kept = [];
63
+ for (const line of stdout.split('\n')) {
64
+ const match = SENTINEL_LINE.exec(line);
65
+ if (!match) {
66
+ kept.push(line);
67
+ continue;
68
+ }
69
+ const directive = parsePayload(match[1] ?? '');
70
+ if (!directive) {
71
+ kept.push(line); // 认不出来的形状原样保留,不静默吞
72
+ continue;
73
+ }
74
+ directives.push(directive);
75
+ }
76
+ return { text: kept.join('\n'), directives };
77
+ }
78
+ /** 从尾部剥已知选项段,剩余部分(重新用 `|` 拼回)是路径 —— 容忍路径里含 `|`。 */
79
+ function parsePayload(payload) {
80
+ const segments = payload.split('|');
81
+ let detail;
82
+ let maxLongSidePixel;
83
+ while (segments.length > 1) {
84
+ const tail = segments[segments.length - 1];
85
+ const option = OPTION_SEGMENT.exec(tail.trim());
86
+ if (!option)
87
+ break;
88
+ const [, key, rawValue] = option;
89
+ const value = (rawValue ?? '').trim();
90
+ if (key === 'detail') {
91
+ if (!VALID_DETAILS.has(value))
92
+ return null; // 写错了就整条不认,别猜
93
+ detail = value;
94
+ }
95
+ else {
96
+ const n = Number(value);
97
+ if (!Number.isFinite(n) || n <= 0)
98
+ return null;
99
+ maxLongSidePixel = Math.floor(n);
100
+ }
101
+ segments.pop();
102
+ }
103
+ const rawPath = segments.join('|').trim();
104
+ if (!rawPath)
105
+ return null;
106
+ return {
107
+ rawPath,
108
+ ...(detail ? { detail } : {}),
109
+ ...(maxLongSidePixel !== undefined ? { maxLongSidePixel } : {}),
110
+ };
111
+ }
112
+ /**
113
+ * 校验并解析成 `ImageInputSpec`。逐条独立判定 —— 一张被拒不影响其它张。
114
+ */
115
+ export async function resolveAttachImages(directives, options = {}) {
116
+ if (directives.length === 0)
117
+ return { images: [], rejections: [] };
118
+ const maxImages = options.maxImages ?? MAX_ATTACH_IMAGES;
119
+ const maxTotalBytes = options.maxTotalBytes ?? MAX_ATTACH_TOTAL_BYTES;
120
+ const roots = await canonicalRoots(options.allowedRoots ?? [getWorkingDir(), tmpdir()]);
121
+ const images = [];
122
+ const rejections = [];
123
+ let totalBytes = 0;
124
+ for (const directive of directives) {
125
+ if (images.length >= maxImages) {
126
+ rejections.push(`${directive.rawPath}:一次最多附 ${maxImages} 张图,已超出`);
127
+ continue;
128
+ }
129
+ const absolute = resolve(getWorkingDir(), directive.rawPath);
130
+ const canonical = await canonicalize(absolute);
131
+ if (!roots.some((root) => isInside(root, canonical))) {
132
+ rejections.push(`${directive.rawPath}:路径不在当前工作目录或系统临时目录之下,已拒绝`);
133
+ continue;
134
+ }
135
+ const st = await stat(canonical).catch(() => null);
136
+ if (!st) {
137
+ rejections.push(`${directive.rawPath}:文件不存在或不可读`);
138
+ continue;
139
+ }
140
+ if (!st.isFile()) {
141
+ rejections.push(`${directive.rawPath}:不是普通文件`);
142
+ continue;
143
+ }
144
+ if (st.size > MAX_IMAGE_BYTES) {
145
+ rejections.push(`${directive.rawPath}:单图 ${(st.size / 1024 / 1024).toFixed(1)}MB 超过 ${MAX_IMAGE_BYTES / 1024 / 1024}MB 上限`);
146
+ continue;
147
+ }
148
+ if (totalBytes + st.size > maxTotalBytes) {
149
+ rejections.push(`${directive.rawPath}:累计体积超过 ${maxTotalBytes / 1024 / 1024}MB 上限,已跳过`);
150
+ continue;
151
+ }
152
+ const mime = await sniffFileImageMime(canonical);
153
+ if (!mime) {
154
+ rejections.push(`${directive.rawPath}:文件内容不是受支持的 JPEG/PNG/GIF/WEBP 图片,已跳过`);
155
+ continue;
156
+ }
157
+ totalBytes += st.size;
158
+ images.push({
159
+ source: canonical,
160
+ ...(directive.detail ? { detail: directive.detail } : {}),
161
+ ...(directive.maxLongSidePixel !== undefined
162
+ ? { maxLongSidePixel: directive.maxLongSidePixel }
163
+ : {}),
164
+ });
165
+ }
166
+ return { images, rejections };
167
+ }
168
+ /**
169
+ * 在接受 sentinel 时验魔数,不依赖扩展名。只读取头部,避免为拒绝一张大文件
170
+ * 再做一次完整读取。逐条返回 null,由调用方记录 rejection,不抛到整个回合。
171
+ */
172
+ async function sniffFileImageMime(path) {
173
+ let handle;
174
+ try {
175
+ handle = await open(path, 'r');
176
+ const header = Buffer.alloc(16);
177
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
178
+ return sniffImageMime(header.subarray(0, bytesRead));
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ finally {
184
+ await handle?.close().catch(() => { });
185
+ }
186
+ }
187
+ /**
188
+ * realpath 之后再比较 —— 否则一条指向 /etc 的软链就能绕过根目录白名单。
189
+ *
190
+ * 目标不存在时不能简单退回 `resolve()`:macOS 的 `os.tmpdir()` 是
191
+ * `/var/folders/…`(软链到 `/private/var/folders/…`),根目录已归一化而目标没有,
192
+ * 于是「临时目录下一个还没生成的文件」会被判成**越界**而不是**不存在** ——
193
+ * 报错指向错误的原因,脚本作者会去查权限而不是去查文件名。
194
+ * 因此回退到「归一化最近的存在祖先,再接回剩余段」。
195
+ */
196
+ async function canonicalize(path) {
197
+ const absolute = resolve(path);
198
+ try {
199
+ return await realpath(absolute);
200
+ }
201
+ catch {
202
+ const parent = dirname(absolute);
203
+ if (parent === absolute)
204
+ return absolute; // 已到根,停止递归
205
+ return join(await canonicalize(parent), basename(absolute));
206
+ }
207
+ }
208
+ /**
209
+ * 根目录也必须 realpath:macOS 的 `os.tmpdir()` 返回 `/var/folders/...`,
210
+ * 而它其实是 `/private/var/folders/...` 的软链 —— 不归一化的话临时目录里的
211
+ * 图会被自己的边界检查误杀。
212
+ */
213
+ async function canonicalRoots(roots) {
214
+ const out = [];
215
+ for (const root of roots) {
216
+ const canonical = await canonicalize(resolve(root));
217
+ if (!out.includes(canonical))
218
+ out.push(canonical);
219
+ }
220
+ return out;
221
+ }
222
+ /** target 是否落在 root 之内(含 root 自身)。判据实现见 `utils/pathContains.ts`(全仓唯一一份)。 */
223
+ const isInside = pathContains;