@armadra/agent 0.4.0 → 0.5.1

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 (432) hide show
  1. package/CHANGELOG.md +184 -1
  2. package/README.md +202 -83
  3. package/THIRD_PARTY_NOTICES.md +34 -0
  4. package/dist/acp.d.ts +18 -0
  5. package/dist/acp.js +19 -0
  6. package/dist/agent/limits.d.ts +38 -0
  7. package/dist/agent/limits.js +127 -0
  8. package/dist/agent/loop-guard.d.ts +38 -0
  9. package/dist/agent/loop-guard.js +95 -0
  10. package/dist/agent/loop.d.ts +4 -0
  11. package/dist/agent/loop.js +7 -0
  12. package/dist/agent/prompt-rules.d.ts +12 -0
  13. package/dist/agent/prompt-rules.js +18 -0
  14. package/dist/agent/reminders.d.ts +59 -0
  15. package/dist/agent/reminders.js +327 -0
  16. package/dist/agent/session-cache.d.ts +5 -0
  17. package/dist/agent/session-cache.js +17 -1
  18. package/dist/agent/session-classifier.d.ts +1 -1
  19. package/dist/agent/session-classifier.js +3 -1
  20. package/dist/agent/session-compaction.d.ts +35 -6
  21. package/dist/agent/session-compaction.js +157 -31
  22. package/dist/agent/session-core.d.ts +27 -1
  23. package/dist/agent/session-extensions.d.ts +61 -0
  24. package/dist/agent/session-extensions.js +129 -0
  25. package/dist/agent/session-images.d.ts +23 -0
  26. package/dist/agent/session-images.js +97 -0
  27. package/dist/agent/session-plan.d.ts +21 -0
  28. package/dist/agent/session-plan.js +524 -0
  29. package/dist/agent/session-rewind.d.ts +81 -0
  30. package/dist/agent/session-rewind.js +354 -0
  31. package/dist/agent/session-run.d.ts +28 -1
  32. package/dist/agent/session-run.js +157 -6
  33. package/dist/agent/session-settings.d.ts +49 -0
  34. package/dist/agent/session-settings.js +95 -0
  35. package/dist/agent/session-subagent.d.ts +36 -24
  36. package/dist/agent/session-subagent.js +259 -129
  37. package/dist/agent/session-sync.d.ts +10 -3
  38. package/dist/agent/session-sync.js +37 -8
  39. package/dist/agent/session-telemetry.d.ts +39 -0
  40. package/dist/agent/session-telemetry.js +231 -0
  41. package/dist/agent/session-tools.js +40 -1
  42. package/dist/agent/session.d.ts +30 -29
  43. package/dist/agent/session.js +79 -123
  44. package/dist/agent/subagent-registry.d.ts +91 -0
  45. package/dist/agent/subagent-registry.js +494 -0
  46. package/dist/agent/system-prompt.d.ts +6 -3
  47. package/dist/agent/system-prompt.js +7 -2
  48. package/dist/agent/tool-runner.d.ts +8 -1
  49. package/dist/agent/tool-runner.js +57 -4
  50. package/dist/agent/types-w5.d.ts +192 -0
  51. package/dist/agent/types-w5.js +10 -0
  52. package/dist/agent/types.d.ts +47 -4
  53. package/dist/agent/types.js +2 -0
  54. package/dist/agent/worktree.d.ts +36 -0
  55. package/dist/agent/worktree.js +86 -0
  56. package/dist/agents/builtin.d.ts +14 -0
  57. package/dist/agents/builtin.js +39 -0
  58. package/dist/agents/catalog.d.ts +43 -0
  59. package/dist/agents/catalog.js +82 -0
  60. package/dist/agents/discover.d.ts +35 -0
  61. package/dist/agents/discover.js +83 -0
  62. package/dist/agents/external.d.ts +82 -0
  63. package/dist/agents/external.js +326 -0
  64. package/dist/agents/parse.d.ts +18 -0
  65. package/dist/agents/parse.js +116 -0
  66. package/dist/agents/result.d.ts +32 -0
  67. package/dist/agents/result.js +67 -0
  68. package/dist/agents/task-control.d.ts +17 -0
  69. package/dist/agents/task-control.js +17 -0
  70. package/dist/agents/task-record.d.ts +78 -0
  71. package/dist/agents/task-record.js +141 -0
  72. package/dist/agents/types.d.ts +46 -0
  73. package/dist/agents/types.js +8 -0
  74. package/dist/ai/apis/anthropic-compat.d.ts +36 -0
  75. package/dist/ai/apis/anthropic-compat.js +92 -0
  76. package/dist/ai/apis/anthropic-request.d.ts +7 -6
  77. package/dist/ai/apis/anthropic-request.js +10 -28
  78. package/dist/ai/apis/cache-params.d.ts +11 -2
  79. package/dist/ai/apis/cache-params.js +25 -6
  80. package/dist/ai/apis/openai-compat.d.ts +4 -1
  81. package/dist/ai/apis/openai-compat.js +8 -1
  82. package/dist/ai/image-limits.d.ts +45 -0
  83. package/dist/ai/image-limits.js +68 -0
  84. package/dist/ai/providers/builtin.d.ts +18 -6
  85. package/dist/ai/providers/builtin.js +135 -6
  86. package/dist/ai/providers/catalog-data.js +17 -13
  87. package/dist/ai/providers/catalog.d.ts +60 -7
  88. package/dist/ai/providers/catalog.js +197 -38
  89. package/dist/ai/providers/channels.d.ts +33 -3
  90. package/dist/ai/providers/channels.js +108 -7
  91. package/dist/ai/providers/enrich.d.ts +6 -3
  92. package/dist/ai/providers/enrich.js +19 -4
  93. package/dist/ai/providers/models-dev-cache.d.ts +28 -24
  94. package/dist/ai/providers/models-dev-cache.js +92 -78
  95. package/dist/ai/providers/models-dev-data.d.ts +7 -0
  96. package/dist/ai/providers/models-dev-data.js +32 -0
  97. package/dist/ai/providers/models-dev-snapshot.d.ts +50 -0
  98. package/dist/ai/providers/models-dev-snapshot.js +137 -0
  99. package/dist/ai/providers/models-dev.d.ts +51 -10
  100. package/dist/ai/providers/models-dev.js +132 -39
  101. package/dist/ai/providers/registry.d.ts +20 -10
  102. package/dist/ai/providers/registry.js +82 -43
  103. package/dist/ai/types.d.ts +28 -2
  104. package/dist/ai/types.js +5 -0
  105. package/dist/bundle/ama.cjs +37605 -21619
  106. package/dist/checkpoints/backend.d.ts +61 -0
  107. package/dist/checkpoints/backend.js +135 -0
  108. package/dist/checkpoints/blobs.d.ts +60 -0
  109. package/dist/checkpoints/blobs.js +203 -0
  110. package/dist/checkpoints/gc.d.ts +47 -0
  111. package/dist/checkpoints/gc.js +144 -0
  112. package/dist/checkpoints/git-head.d.ts +15 -0
  113. package/dist/checkpoints/git-head.js +89 -0
  114. package/dist/checkpoints/index.d.ts +17 -0
  115. package/dist/checkpoints/index.js +17 -0
  116. package/dist/checkpoints/replay.d.ts +48 -0
  117. package/dist/checkpoints/replay.js +154 -0
  118. package/dist/checkpoints/restore.d.ts +86 -0
  119. package/dist/checkpoints/restore.js +263 -0
  120. package/dist/checkpoints/settings.d.ts +14 -0
  121. package/dist/checkpoints/settings.js +23 -0
  122. package/dist/checkpoints/shadow-git.d.ts +108 -0
  123. package/dist/checkpoints/shadow-git.js +491 -0
  124. package/dist/checkpoints/shadow-restore.d.ts +25 -0
  125. package/dist/checkpoints/shadow-restore.js +128 -0
  126. package/dist/checkpoints/tracker.d.ts +64 -0
  127. package/dist/checkpoints/tracker.js +192 -0
  128. package/dist/checkpoints/types.d.ts +99 -0
  129. package/dist/checkpoints/types.js +12 -0
  130. package/dist/cli/args.d.ts +8 -4
  131. package/dist/cli/args.js +17 -97
  132. package/dist/cli/bootstrap.js +13 -1
  133. package/dist/cli/choice-prompt.d.ts +58 -0
  134. package/dist/cli/choice-prompt.js +140 -0
  135. package/dist/cli/codemode-notice.d.ts +3 -2
  136. package/dist/cli/codemode-notice.js +3 -2
  137. package/dist/cli/compose-agents.d.ts +27 -0
  138. package/dist/cli/compose-agents.js +111 -0
  139. package/dist/cli/compose-extensions.d.ts +29 -0
  140. package/dist/cli/compose-extensions.js +44 -0
  141. package/dist/cli/compose-session.d.ts +3 -0
  142. package/dist/cli/compose-session.js +47 -4
  143. package/dist/cli/compose.d.ts +3 -0
  144. package/dist/cli/compose.js +33 -6
  145. package/dist/cli/deps.d.ts +6 -0
  146. package/dist/cli/exit-codes.d.ts +10 -0
  147. package/dist/cli/exit-codes.js +12 -0
  148. package/dist/cli/help-text.d.ts +4 -0
  149. package/dist/cli/help-text.js +102 -0
  150. package/dist/cli/startup-screen.js +5 -3
  151. package/dist/cli/startup-steps.js +7 -1
  152. package/dist/cli/subcommands/config.d.ts +4 -1
  153. package/dist/cli/subcommands/config.js +17 -6
  154. package/dist/cli/subcommands/doctor.js +21 -0
  155. package/dist/cli/subcommands/models-cache-probe.d.ts +1 -1
  156. package/dist/cli/subcommands/models-cache-probe.js +3 -12
  157. package/dist/cli/subcommands/models-discover.js +5 -6
  158. package/dist/cli/subcommands/models.d.ts +2 -0
  159. package/dist/cli/subcommands/models.js +33 -14
  160. package/dist/cli/subcommands/providers.d.ts +1 -1
  161. package/dist/cli/subcommands/providers.js +8 -15
  162. package/dist/cli/subcommands/sessions.d.ts +3 -1
  163. package/dist/cli/subcommands/sessions.js +36 -2
  164. package/dist/codemode/capability.d.ts +24 -7
  165. package/dist/codemode/capability.js +38 -10
  166. package/dist/codemode/host-side.d.ts +14 -0
  167. package/dist/codemode/host-side.js +24 -1
  168. package/dist/codemode/tool.d.ts +1 -1
  169. package/dist/codemode/tool.js +6 -4
  170. package/dist/compaction/breaker.d.ts +25 -9
  171. package/dist/compaction/breaker.js +47 -17
  172. package/dist/compaction/estimate.d.ts +11 -3
  173. package/dist/compaction/estimate.js +64 -22
  174. package/dist/compaction/image-budget.d.ts +45 -0
  175. package/dist/compaction/image-budget.js +115 -0
  176. package/dist/compaction/post-compact.d.ts +38 -0
  177. package/dist/compaction/post-compact.js +152 -0
  178. package/dist/compaction/protect.d.ts +27 -0
  179. package/dist/compaction/protect.js +56 -0
  180. package/dist/compaction/prune-tier.d.ts +58 -12
  181. package/dist/compaction/prune-tier.js +120 -54
  182. package/dist/compaction/summarize-tier.d.ts +23 -4
  183. package/dist/compaction/summarize-tier.js +100 -29
  184. package/dist/config/checker.d.ts +28 -0
  185. package/dist/config/checker.js +97 -0
  186. package/dist/config/json-schema.js +63 -2
  187. package/dist/config/key-docs.js +72 -3
  188. package/dist/config/merge.d.ts +5 -3
  189. package/dist/config/merge.js +81 -7
  190. package/dist/config/profile.d.ts +2 -0
  191. package/dist/config/profile.js +2 -0
  192. package/dist/config/schema-w5.d.ts +18 -0
  193. package/dist/config/schema-w5.js +109 -0
  194. package/dist/config/schema.d.ts +5 -9
  195. package/dist/config/schema.js +68 -114
  196. package/dist/config/types-w5.d.ts +118 -0
  197. package/dist/config/types-w5.js +20 -0
  198. package/dist/config/types.d.ts +50 -1
  199. package/dist/config/types.js +12 -0
  200. package/dist/drivers/acp/client.d.ts +57 -0
  201. package/dist/drivers/acp/client.js +160 -0
  202. package/dist/drivers/acp/driver.d.ts +31 -0
  203. package/dist/drivers/acp/driver.js +274 -0
  204. package/dist/drivers/acp/testing/fake-agent-main.d.ts +5 -0
  205. package/dist/drivers/acp/testing/fake-agent-main.js +7 -0
  206. package/dist/drivers/acp/testing/fake-agent.d.ts +27 -0
  207. package/dist/drivers/acp/testing/fake-agent.js +211 -0
  208. package/dist/drivers/acp/types.d.ts +285 -0
  209. package/dist/drivers/acp/types.js +36 -0
  210. package/dist/drivers/agents.d.ts +58 -0
  211. package/dist/drivers/agents.js +142 -0
  212. package/dist/drivers/base.d.ts +35 -0
  213. package/dist/drivers/base.js +59 -0
  214. package/dist/drivers/catalog.d.ts +46 -0
  215. package/dist/drivers/catalog.js +167 -0
  216. package/dist/drivers/env.d.ts +20 -0
  217. package/dist/drivers/env.js +46 -0
  218. package/dist/drivers/host-runners.d.ts +18 -0
  219. package/dist/drivers/host-runners.js +45 -0
  220. package/dist/drivers/jsonrpc.d.ts +58 -0
  221. package/dist/drivers/jsonrpc.js +180 -0
  222. package/dist/drivers/native/claude-normalize.d.ts +46 -0
  223. package/dist/drivers/native/claude-normalize.js +106 -0
  224. package/dist/drivers/native/claude-stream.d.ts +29 -0
  225. package/dist/drivers/native/claude-stream.js +456 -0
  226. package/dist/drivers/native/codex-app-server.d.ts +26 -0
  227. package/dist/drivers/native/codex-app-server.js +407 -0
  228. package/dist/drivers/native/codex-normalize.d.ts +38 -0
  229. package/dist/drivers/native/codex-normalize.js +124 -0
  230. package/dist/drivers/native/oneshot.d.ts +28 -0
  231. package/dist/drivers/native/oneshot.js +312 -0
  232. package/dist/drivers/permissions.d.ts +40 -0
  233. package/dist/drivers/permissions.js +92 -0
  234. package/dist/drivers/pids.d.ts +32 -0
  235. package/dist/drivers/pids.js +93 -0
  236. package/dist/drivers/pool.d.ts +28 -0
  237. package/dist/drivers/pool.js +96 -0
  238. package/dist/drivers/probe.d.ts +47 -0
  239. package/dist/drivers/probe.js +172 -0
  240. package/dist/drivers/process.d.ts +48 -0
  241. package/dist/drivers/process.js +100 -0
  242. package/dist/drivers/runner.d.ts +61 -0
  243. package/dist/drivers/runner.js +388 -0
  244. package/dist/drivers/store.d.ts +49 -0
  245. package/dist/drivers/store.js +91 -0
  246. package/dist/drivers/turn.d.ts +41 -0
  247. package/dist/drivers/turn.js +99 -0
  248. package/dist/drivers/types.d.ts +122 -0
  249. package/dist/drivers/types.js +11 -0
  250. package/dist/git/info.d.ts +75 -0
  251. package/dist/git/info.js +191 -0
  252. package/dist/hooks/protocol.js +6 -1
  253. package/dist/hooks/types.d.ts +10 -1
  254. package/dist/hooks/types.js +2 -0
  255. package/dist/host/api-impl.d.ts +7 -0
  256. package/dist/host/api-impl.js +15 -0
  257. package/dist/host/types.d.ts +24 -2
  258. package/dist/host/types.js +4 -0
  259. package/dist/index.d.ts +3 -0
  260. package/dist/index.js +1 -0
  261. package/dist/modes/acp/acp-events.d.ts +46 -0
  262. package/dist/modes/acp/acp-events.js +233 -0
  263. package/dist/modes/acp/acp-mode.d.ts +17 -0
  264. package/dist/modes/acp/acp-mode.js +47 -0
  265. package/dist/modes/acp/acp-server.d.ts +79 -0
  266. package/dist/modes/acp/acp-server.js +315 -0
  267. package/dist/modes/commands-core.d.ts +13 -1
  268. package/dist/modes/commands-core.js +64 -0
  269. package/dist/modes/image-input.d.ts +12 -4
  270. package/dist/modes/image-input.js +22 -5
  271. package/dist/modes/interactive/agent-panels.d.ts +14 -0
  272. package/dist/modes/interactive/agent-panels.js +103 -0
  273. package/dist/modes/interactive/agent-ui.d.ts +73 -0
  274. package/dist/modes/interactive/agent-ui.js +234 -0
  275. package/dist/modes/interactive/approval-dialog.d.ts +21 -4
  276. package/dist/modes/interactive/approval-dialog.js +98 -20
  277. package/dist/modes/interactive/approval-merge.d.ts +41 -0
  278. package/dist/modes/interactive/approval-merge.js +89 -0
  279. package/dist/modes/interactive/clipboard-paste.d.ts +18 -0
  280. package/dist/modes/interactive/clipboard-paste.js +28 -0
  281. package/dist/modes/interactive/commands.d.ts +12 -1
  282. package/dist/modes/interactive/commands.js +31 -1
  283. package/dist/modes/interactive/confirm-dialog.d.ts +49 -0
  284. package/dist/modes/interactive/confirm-dialog.js +121 -0
  285. package/dist/modes/interactive/double-esc.d.ts +20 -0
  286. package/dist/modes/interactive/double-esc.js +34 -0
  287. package/dist/modes/interactive/event-notices.d.ts +32 -0
  288. package/dist/modes/interactive/event-notices.js +79 -0
  289. package/dist/modes/interactive/external-editor.d.ts +16 -0
  290. package/dist/modes/interactive/external-editor.js +39 -0
  291. package/dist/modes/interactive/interactive-mode.d.ts +14 -1
  292. package/dist/modes/interactive/interactive-mode.js +140 -136
  293. package/dist/modes/interactive/key-dispatch.d.ts +22 -2
  294. package/dist/modes/interactive/key-dispatch.js +66 -7
  295. package/dist/modes/interactive/line/line-mode.d.ts +4 -1
  296. package/dist/modes/interactive/line/line-mode.js +31 -5
  297. package/dist/modes/interactive/line/line-render.d.ts +2 -1
  298. package/dist/modes/interactive/line/line-render.js +40 -4
  299. package/dist/modes/interactive/message-view.d.ts +1 -1
  300. package/dist/modes/interactive/message-view.js +24 -2
  301. package/dist/modes/interactive/panels.d.ts +18 -1
  302. package/dist/modes/interactive/panels.js +19 -5
  303. package/dist/modes/interactive/plan-command.d.ts +40 -0
  304. package/dist/modes/interactive/plan-command.js +129 -0
  305. package/dist/modes/interactive/plan-dialog.d.ts +98 -0
  306. package/dist/modes/interactive/plan-dialog.js +290 -0
  307. package/dist/modes/interactive/plan-flow.d.ts +34 -0
  308. package/dist/modes/interactive/plan-flow.js +98 -0
  309. package/dist/modes/interactive/rewind-command.d.ts +31 -0
  310. package/dist/modes/interactive/rewind-command.js +106 -0
  311. package/dist/modes/interactive/rewind-flow.d.ts +37 -0
  312. package/dist/modes/interactive/rewind-flow.js +151 -0
  313. package/dist/modes/interactive/rewind-list.d.ts +36 -0
  314. package/dist/modes/interactive/rewind-list.js +123 -0
  315. package/dist/modes/interactive/rewind-panel.d.ts +77 -0
  316. package/dist/modes/interactive/rewind-panel.js +309 -0
  317. package/dist/modes/interactive/rewind-text.d.ts +40 -0
  318. package/dist/modes/interactive/rewind-text.js +139 -0
  319. package/dist/modes/interactive/session-events.d.ts +24 -0
  320. package/dist/modes/interactive/session-events.js +96 -0
  321. package/dist/modes/interactive/startup-ui.js +5 -1
  322. package/dist/modes/interactive/status-area.d.ts +73 -0
  323. package/dist/modes/interactive/status-area.js +190 -0
  324. package/dist/modes/interactive/status-bar.d.ts +99 -11
  325. package/dist/modes/interactive/status-bar.js +273 -90
  326. package/dist/modes/interactive/status-line.d.ts +33 -0
  327. package/dist/modes/interactive/status-line.js +113 -0
  328. package/dist/modes/interactive/subagent-view.d.ts +56 -0
  329. package/dist/modes/interactive/subagent-view.js +154 -0
  330. package/dist/modes/interactive/tasks-report.d.ts +27 -0
  331. package/dist/modes/interactive/tasks-report.js +114 -0
  332. package/dist/modes/interactive/tool-view.d.ts +12 -2
  333. package/dist/modes/interactive/tool-view.js +34 -4
  334. package/dist/modes/print/print-mode.d.ts +13 -4
  335. package/dist/modes/print/print-mode.js +56 -12
  336. package/dist/modes/rpc/commands.d.ts +9 -0
  337. package/dist/modes/rpc/commands.js +44 -0
  338. package/dist/modes/rpc/rpc-mode.js +7 -1
  339. package/dist/modes/session-report.d.ts +11 -0
  340. package/dist/modes/session-report.js +48 -1
  341. package/dist/permissions/auto-safe.d.ts +5 -0
  342. package/dist/permissions/auto-safe.js +18 -0
  343. package/dist/permissions/bypass.d.ts +32 -0
  344. package/dist/permissions/bypass.js +36 -0
  345. package/dist/permissions/classifier.d.ts +4 -0
  346. package/dist/permissions/classifier.js +2 -0
  347. package/dist/permissions/modes.js +1 -1
  348. package/dist/permissions/pipeline.d.ts +43 -4
  349. package/dist/permissions/pipeline.js +114 -9
  350. package/dist/permissions/readonly-bash.d.ts +30 -0
  351. package/dist/permissions/readonly-bash.js +118 -0
  352. package/dist/permissions/types.d.ts +42 -0
  353. package/dist/plan/compose.d.ts +9 -0
  354. package/dist/plan/compose.js +19 -0
  355. package/dist/plan/controller.d.ts +71 -0
  356. package/dist/plan/controller.js +27 -0
  357. package/dist/plan/done-markers.d.ts +17 -0
  358. package/dist/plan/done-markers.js +51 -0
  359. package/dist/plan/extract.d.ts +29 -0
  360. package/dist/plan/extract.js +160 -0
  361. package/dist/plan/prompts.d.ts +36 -0
  362. package/dist/plan/prompts.js +89 -0
  363. package/dist/plan/store.d.ts +37 -0
  364. package/dist/plan/store.js +90 -0
  365. package/dist/rpc.d.ts +58 -2
  366. package/dist/rpc.js +7 -0
  367. package/dist/sandbox/bash.d.ts +74 -0
  368. package/dist/sandbox/bash.js +126 -0
  369. package/dist/sandbox/detect.d.ts +48 -0
  370. package/dist/sandbox/detect.js +149 -0
  371. package/dist/sandbox/index.d.ts +5 -0
  372. package/dist/sandbox/index.js +5 -0
  373. package/dist/sandbox/profile.d.ts +33 -0
  374. package/dist/sandbox/profile.js +76 -0
  375. package/dist/sandbox/wrap.d.ts +29 -0
  376. package/dist/sandbox/wrap.js +117 -0
  377. package/dist/sdk.d.ts +33 -2
  378. package/dist/sdk.js +24 -2
  379. package/dist/session/types.d.ts +3 -1
  380. package/dist/session/types.js +1 -0
  381. package/dist/skills/builtin.d.ts +31 -0
  382. package/dist/skills/builtin.js +109 -0
  383. package/dist/skills/discover.d.ts +2 -1
  384. package/dist/skills/index-prompt.js +5 -8
  385. package/dist/tools/background-jobs.d.ts +71 -0
  386. package/dist/tools/background-jobs.js +198 -0
  387. package/dist/tools/bash.d.ts +53 -1
  388. package/dist/tools/bash.js +158 -7
  389. package/dist/tools/clipboard-image.d.ts +44 -0
  390. package/dist/tools/clipboard-image.js +148 -0
  391. package/dist/tools/edit.d.ts +2 -0
  392. package/dist/tools/edit.js +18 -3
  393. package/dist/tools/image-file.d.ts +38 -3
  394. package/dist/tools/image-file.js +81 -10
  395. package/dist/tools/image-resize.d.ts +58 -0
  396. package/dist/tools/image-resize.js +97 -0
  397. package/dist/tools/presets.d.ts +7 -2
  398. package/dist/tools/presets.js +11 -4
  399. package/dist/tools/read.d.ts +5 -2
  400. package/dist/tools/read.js +13 -10
  401. package/dist/tools/registry.d.ts +7 -1
  402. package/dist/tools/registry.js +27 -4
  403. package/dist/tools/task-ctl.d.ts +22 -0
  404. package/dist/tools/task-ctl.js +116 -0
  405. package/dist/tools/task.d.ts +22 -10
  406. package/dist/tools/task.js +66 -26
  407. package/dist/tools/todo.d.ts +22 -2
  408. package/dist/tools/todo.js +58 -14
  409. package/dist/tools/truncate.d.ts +15 -0
  410. package/dist/tools/truncate.js +25 -0
  411. package/dist/tools/types.d.ts +113 -0
  412. package/dist/tools/types.js +5 -0
  413. package/dist/tools/write.js +3 -0
  414. package/dist/tui/glyphs.d.ts +4 -0
  415. package/dist/tui/glyphs.js +4 -0
  416. package/dist/tui/keybindings.d.ts +6 -1
  417. package/dist/tui/keybindings.js +6 -1
  418. package/docs/acp.md +78 -0
  419. package/docs/agents.md +250 -0
  420. package/docs/codemode.md +12 -8
  421. package/docs/hooks.md +26 -22
  422. package/docs/permissions.md +78 -13
  423. package/docs/plan.md +107 -0
  424. package/docs/providers.md +228 -52
  425. package/docs/rewind-plan.md +181 -0
  426. package/docs/rpc.md +92 -5
  427. package/docs/sandbox.md +215 -0
  428. package/docs/session-format.md +41 -21
  429. package/docs/sessions.md +41 -1
  430. package/docs/tui-design.md +751 -0
  431. package/docs/tui.md +179 -21
  432. package/package.json +13 -2
@@ -7,6 +7,12 @@
7
7
  * - 结构化结果 `{ output, exit_code, truncated, full_output_path?, wall_time_seconds }`;
8
8
  * 信号退出 `128 + signo`;非零退出码 → isError;
9
9
  * - 注入 `AMA_*` 环境变量(shell.ts)。shell 退出后不等仍占着管道的后台进程。
10
+ * - [W5-H2] 后台命令(§8.3 H6,不加新工具):`{ command, background: true }` 立即返回
11
+ * `{ jobId, outputPath, pid }`;`{ job, action: "wait" | "output" | "stop" }` 查询 / 结束
12
+ * (background-jobs.ts,按会话登记,会话 dispose 时回收进程树;退出经提醒通道通知)。
13
+ * - **进程只在 `spawnShell` 里创建**(前台与后台共用)。[S2] `sandbox.bash: auto` 且有能限制写入的
14
+ * OS 沙箱时在这一处包一层(src/sandbox/bash.ts);`sandbox: false` 不包装(权限管线照常审批)。
15
+ * 失败输出像是被沙箱拒绝时末尾追加一行说明。
10
16
  */
11
17
  import { spawn } from "node:child_process";
12
18
  import { stat } from "node:fs/promises";
@@ -16,11 +22,32 @@ import { buildShellArgs, buildToolEnv, resolveShell } from "./shell.js";
16
22
  import { exitCodeOf, isGroupAlive, killProcessTree, trackProcessGroup, untrackProcessGroup, } from "./process-tree.js";
17
23
  import { OutputAccumulator } from "./output-accumulator.js";
18
24
  import { formatSize, resolveOutputDir, safeFileName } from "./truncate.js";
25
+ import { jobsForSession } from "./background-jobs.js";
26
+ import { looksLikeSandboxDenial, runsSandboxed, sandboxDenialHint, wrapBashCommand, } from "../sandbox/bash.js";
19
27
  export const DEFAULT_BASH_TIMEOUT_MS = 120_000;
20
28
  export const MAX_BASH_TIMEOUT_MS = 600_000;
21
29
  const UPDATE_THROTTLE_MS = 100;
22
30
  /** shell 退出后等管道关闭的上限(后台子进程可能一直占着)。 */
23
31
  const STDIO_DRAIN_MS = 200;
32
+ /** [W5-H2] `wait` 的缺省等待(毫秒)。 */
33
+ export const DEFAULT_JOB_WAIT_MS = 30_000;
34
+ /**
35
+ * bash 工具唯一的进程创建点(前台 / 后台共用);[S2] 给了 `sandbox` 时经 OS 沙箱包装(包不上抛错,
36
+ * 不悄悄裸跑)。
37
+ */
38
+ export function spawnShell(shell, command, options) {
39
+ const args = buildShellArgs(shell, command);
40
+ const wrapped = options.sandbox === undefined
41
+ ? { command: shell.shell, args }
42
+ : wrapBashCommand(options.sandbox.settings, shell.shell, args, options.sandbox.place);
43
+ return spawn(wrapped.command, wrapped.args, {
44
+ cwd: options.cwd,
45
+ env: options.env,
46
+ detached: options.detached,
47
+ stdio: options.stdio,
48
+ windowsHide: true,
49
+ });
50
+ }
24
51
  function fail(message) {
25
52
  return { content: message, isError: true };
26
53
  }
@@ -56,9 +83,14 @@ function waitForExit(child) {
56
83
  });
57
84
  }
58
85
  export async function executeBash(input, ctx, options = {}) {
86
+ if (input.job !== undefined)
87
+ return jobAction(input, ctx, options);
88
+ if (input.action !== undefined)
89
+ return fail("action needs job (a background job id)");
59
90
  if (typeof input.command !== "string" || input.command.trim() === "") {
60
91
  return fail("command must be a non-empty string");
61
92
  }
93
+ const command = input.command;
62
94
  const timeoutMs = input.timeoutMs ?? options.defaultTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS;
63
95
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_BASH_TIMEOUT_MS) {
64
96
  return fail(`timeoutMs must be between 1 and ${MAX_BASH_TIMEOUT_MS}`);
@@ -83,18 +115,29 @@ export async function executeBash(input, ctx, options = {}) {
83
115
  depth: ctx.depth,
84
116
  });
85
117
  const isWindows = (options.processDeps?.platform ?? process.platform) === "win32";
118
+ const sandbox = options.sandbox !== undefined && runsSandboxed(options.sandbox, input)
119
+ ? {
120
+ settings: options.sandbox,
121
+ place: {
122
+ workspace: ctx.cwd,
123
+ outputDir: resolveOutputDir(ctx.outputDir),
124
+ },
125
+ }
126
+ : undefined;
127
+ if (input.background === true)
128
+ return startBackground(command, { cwd, env, shell, isWindows, sandbox }, ctx, options);
86
129
  const started = Date.now();
87
130
  const acc = new OutputAccumulator({
88
131
  spillPath: () => join(resolveOutputDir(ctx.outputDir), safeFileName(`bash-${ctx.toolCallId}.log`)),
89
132
  });
90
133
  let child;
91
134
  try {
92
- child = spawn(shell.shell, buildShellArgs(shell, input.command), {
135
+ child = spawnShell(shell, command, {
93
136
  cwd,
94
137
  env,
95
138
  detached: !isWindows,
96
139
  stdio: ["ignore", "pipe", "pipe"],
97
- windowsHide: true,
140
+ ...(sandbox === undefined ? {} : { sandbox }),
98
141
  });
99
142
  }
100
143
  catch (err) {
@@ -165,6 +208,7 @@ export async function executeBash(input, ctx, options = {}) {
165
208
  truncated: out.truncated,
166
209
  ...(out.fullOutputPath !== undefined ? { full_output_path: out.fullOutputPath } : {}),
167
210
  wall_time_seconds: wall,
211
+ ...(sandbox === undefined ? {} : { sandboxed: true }),
168
212
  };
169
213
  const details = {
170
214
  ...structured,
@@ -188,6 +232,10 @@ export async function executeBash(input, ctx, options = {}) {
188
232
  parts.push(`[Command timed out after ${timeoutMs} ms and was killed]`);
189
233
  if (exitCode !== 0)
190
234
  parts.push(`[exit code: ${exitCode}]`);
235
+ const networkDenied = sandbox?.settings.network === "deny";
236
+ if (sandbox !== undefined && exitCode !== 0 && !aborted && !timedOut)
237
+ if (looksLikeSandboxDenial(out.output, networkDenied))
238
+ parts.push(sandboxDenialHint(networkDenied));
191
239
  return {
192
240
  content: parts.join("\n\n"),
193
241
  isError: exitCode !== 0 || timedOut || aborted,
@@ -195,22 +243,125 @@ export async function executeBash(input, ctx, options = {}) {
195
243
  details,
196
244
  };
197
245
  }
246
+ // ---------------------------------------------------------------------------
247
+ // [W5-H2] 后台命令
248
+ // ---------------------------------------------------------------------------
249
+ function jobsOf(ctx, options) {
250
+ return options.jobs ? options.jobs(ctx.sessionId) : jobsForSession(ctx.sessionId);
251
+ }
252
+ function startBackground(command, spawnInfo, ctx, options) {
253
+ const { cwd, env, shell, isWindows, sandbox } = spawnInfo;
254
+ const outputPath = join(resolveOutputDir(ctx.outputDir), safeFileName(`bash-bg-${ctx.toolCallId}.log`));
255
+ let job;
256
+ try {
257
+ job = jobsOf(ctx, options).start(command, {
258
+ cwd,
259
+ outputPath,
260
+ ...(options.processDeps === undefined ? {} : { processDeps: options.processDeps }),
261
+ spawn: (fd) => spawnShell(shell, command, {
262
+ cwd,
263
+ env,
264
+ detached: !isWindows,
265
+ stdio: ["ignore", fd, fd],
266
+ ...(sandbox === undefined ? {} : { sandbox }),
267
+ }),
268
+ });
269
+ }
270
+ catch (err) {
271
+ return fail(`Failed to start shell ${shell.shell}: ${err.message}`);
272
+ }
273
+ const structured = { jobId: job.id, outputPath: job.outputPath };
274
+ if (job.pid !== undefined)
275
+ structured.pid = job.pid;
276
+ if (sandbox !== undefined)
277
+ structured.sandboxed = true;
278
+ return {
279
+ content: `Started background job ${job.id}${job.pid !== undefined ? ` (pid ${job.pid})` : ""}; ` +
280
+ `output: ${job.outputPath}. Check it with bash {"job":"${job.id}","action":"wait"|"output"|"stop"}; ` +
281
+ "you will be told when it exits.",
282
+ structured,
283
+ details: { ...structured, background: true, command, cwd },
284
+ };
285
+ }
286
+ function jobStatusLine(job) {
287
+ const state = job.status === "running"
288
+ ? "still running"
289
+ : job.status === "stopped"
290
+ ? `stopped (exit code ${job.exitCode ?? "?"})`
291
+ : `exited with code ${job.exitCode ?? "?"}`;
292
+ return `[job ${job.id} ${state}; output: ${job.outputPath}]`;
293
+ }
294
+ async function jobAction(input, ctx, options) {
295
+ const id = input.job;
296
+ if (input.command !== undefined || input.background === true || input.sandbox !== undefined)
297
+ return fail("job queries take only job, action and timeoutMs");
298
+ const action = input.action ?? "output";
299
+ const jobs = jobsOf(ctx, options);
300
+ if (jobs.get(id) === undefined) {
301
+ const known = jobs.list().map((job) => job.id);
302
+ return fail(`Unknown background job ${id}${known.length > 0 ? ` (jobs: ${known.join(", ")})` : ""}`);
303
+ }
304
+ if (action === "stop") {
305
+ const job = (await jobs.stop(id));
306
+ const out = jobs.output(id);
307
+ const text = out?.content ?? "";
308
+ return {
309
+ content: `${text === "" ? "(no output)" : text}\n\n${jobStatusLine(job)}`,
310
+ structured: { jobId: id, status: job.status, exit_code: job.exitCode ?? null },
311
+ };
312
+ }
313
+ if (action === "wait") {
314
+ const waitMs = input.timeoutMs ?? DEFAULT_JOB_WAIT_MS;
315
+ if (!Number.isFinite(waitMs) || waitMs <= 0 || waitMs > MAX_BASH_TIMEOUT_MS)
316
+ return fail(`timeoutMs must be between 1 and ${MAX_BASH_TIMEOUT_MS}`);
317
+ await jobs.wait(id, waitMs, ctx.signal);
318
+ }
319
+ const out = jobs.output(id);
320
+ if (out === undefined)
321
+ return fail(`Unknown background job ${id}`);
322
+ const job = out.job;
323
+ if (job.status !== "running")
324
+ jobs.markSeen(id);
325
+ const parts = [out.content === "" ? "(no output)" : out.content];
326
+ if (out.truncated)
327
+ parts.push(`[Output truncated: showing the last ${out.outputLines} of ${out.totalLines} lines ` +
328
+ `(${formatSize(out.totalBytes)} total). Full output: ${job.outputPath}]`);
329
+ parts.push(jobStatusLine(job));
330
+ return {
331
+ content: parts.join("\n\n"),
332
+ isError: job.status === "exited" && job.exitCode !== 0,
333
+ structured: { jobId: id, status: job.status, exit_code: job.exitCode ?? null },
334
+ };
335
+ }
336
+ /** [S2] 沙箱生效时追加到描述末尾的一句(会话内不变)。 */
337
+ export function sandboxDescription(sandbox) {
338
+ if (sandbox?.active !== true)
339
+ return "";
340
+ const net = sandbox.network === "deny" ? ", no network" : "";
341
+ return ` Runs in an OS sandbox (writes only in workspace/temp${net}); sandbox:false needs approval.`;
342
+ }
198
343
  export function createBashTool(options = {}) {
344
+ const sandboxed = options.sandbox?.active === true;
199
345
  return {
200
346
  name: "bash",
201
347
  label: "Bash",
202
- description: "Run a shell command. stdout+stderr combined; over 2000 lines or 50 KB keeps the tail " +
203
- "and saves the full output to a file. " +
204
- `Timeout default ${DEFAULT_BASH_TIMEOUT_MS} ms, max ${MAX_BASH_TIMEOUT_MS} ms.`,
348
+ description: "Run a shell command; stdout+stderr combined, keeps the last 2000 lines / 50 KB (full " +
349
+ `output saved). Timeout ${DEFAULT_BASH_TIMEOUT_MS / 1000} s, max ${MAX_BASH_TIMEOUT_MS / 1000} s. ` +
350
+ "background:true gives a job id for {job, action}." +
351
+ sandboxDescription(options.sandbox),
205
352
  parameters: {
206
353
  type: "object",
207
354
  properties: {
208
355
  command: { type: "string" },
209
356
  timeoutMs: { type: "integer" },
210
357
  cwd: { type: "string", description: "Default: session cwd" },
211
- description: { type: "string", description: "What this does, briefly" },
358
+ description: { type: "string" },
359
+ background: { type: "boolean" },
360
+ job: { type: "string" },
361
+ action: { type: "string", enum: ["wait", "output", "stop"] },
362
+ // [S2] 只在沙箱生效时出现:沙箱关闭的会话工具 schema 与以前逐字节相同
363
+ ...(sandboxed ? { sandbox: { type: "boolean" } } : {}),
212
364
  },
213
- required: ["command"],
214
365
  additionalProperties: false,
215
366
  },
216
367
  permission: "execute",
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 剪贴板图片(docs/wave5-plan.md §4、D12)。[W5-I] 界面接线(`Ctrl+V`、`/paste`)归 W5-U。
3
+ *
4
+ * 零依赖读不了剪贴板位图,按平台依次调系统命令,成功写 `<dataDir>/clipboard/<时间戳>.png` 并返回
5
+ * 路径(编辑器插入 `@<路径>`,之后走 loadImageFile);没有工具或剪贴板里没有图返回 undefined:
6
+ *
7
+ * | 平台 | 依次尝试 |
8
+ * | ------- | --------------------------------------------------------------------------- |
9
+ * | macOS | `osascript`(`the clipboard as «class PNGf»` 写文件)、`pngpaste <文件>` |
10
+ * | Linux | Wayland(有 WAYLAND_DISPLAY)`wl-paste -t image/png`;`xclip -selection clipboard -t image/png -o` |
11
+ * | Windows | `powershell` 的 `Get-Clipboard -Format Image` 存 PNG(路径经环境变量传入) |
12
+ *
13
+ * 写出的文件必须以 PNG 文件头开头,否则删掉按「没有图」处理。`ama sessions prune` 清理超过 7 天的
14
+ * 剪贴板文件(`gcClipboardImages`)。
15
+ */
16
+ import { type RunCommand } from "./image-resize.js";
17
+ export declare const CLIPBOARD_DIR = "clipboard";
18
+ export declare const CLIPBOARD_MAX_AGE_DAYS = 7;
19
+ export interface ClipboardDeps {
20
+ platform?: NodeJS.Platform;
21
+ run?: RunCommand;
22
+ env?: NodeJS.ProcessEnv;
23
+ now?(): number;
24
+ }
25
+ export type ClipboardResult = {
26
+ ok: true;
27
+ path: string;
28
+ tool: string;
29
+ }
30
+ /** `no_tool`:一个可用命令都没有;`no_image`:命令能用但剪贴板里没有图。 */
31
+ | {
32
+ ok: false;
33
+ reason: "no_tool" | "no_image";
34
+ };
35
+ /** 读剪贴板图片到数据目录;返回结果与原因(界面据此给一行提示)。 */
36
+ export declare function readClipboardImage(dataDir: string, deps?: ClipboardDeps): Promise<ClipboardResult>;
37
+ /** §10.2 的签名:成功返回 PNG 路径,没有工具或没有图返回 undefined。 */
38
+ export declare function pasteClipboardImage(dataDir: string, deps?: ClipboardDeps): Promise<string | undefined>;
39
+ /** 清理 `<dataDir>/clipboard/` 里超过 `maxAgeDays`(缺省 7)天的文件;返回(将)删除的路径。 */
40
+ export declare function gcClipboardImages(dataDir: string, options?: {
41
+ dryRun?: boolean;
42
+ maxAgeDays?: number;
43
+ now?: number;
44
+ }): Promise<string[]>;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * 剪贴板图片(docs/wave5-plan.md §4、D12)。[W5-I] 界面接线(`Ctrl+V`、`/paste`)归 W5-U。
3
+ *
4
+ * 零依赖读不了剪贴板位图,按平台依次调系统命令,成功写 `<dataDir>/clipboard/<时间戳>.png` 并返回
5
+ * 路径(编辑器插入 `@<路径>`,之后走 loadImageFile);没有工具或剪贴板里没有图返回 undefined:
6
+ *
7
+ * | 平台 | 依次尝试 |
8
+ * | ------- | --------------------------------------------------------------------------- |
9
+ * | macOS | `osascript`(`the clipboard as «class PNGf»` 写文件)、`pngpaste <文件>` |
10
+ * | Linux | Wayland(有 WAYLAND_DISPLAY)`wl-paste -t image/png`;`xclip -selection clipboard -t image/png -o` |
11
+ * | Windows | `powershell` 的 `Get-Clipboard -Format Image` 存 PNG(路径经环境变量传入) |
12
+ *
13
+ * 写出的文件必须以 PNG 文件头开头,否则删掉按「没有图」处理。`ama sessions prune` 清理超过 7 天的
14
+ * 剪贴板文件(`gcClipboardImages`)。
15
+ */
16
+ import { existsSync } from "node:fs";
17
+ import { mkdir, readdir, rm, stat, writeFile, readFile } from "node:fs/promises";
18
+ import { join } from "node:path";
19
+ import { sniffImageMime } from "./image-file.js";
20
+ import { runCommand } from "./image-resize.js";
21
+ export const CLIPBOARD_DIR = "clipboard";
22
+ export const CLIPBOARD_MAX_AGE_DAYS = 7;
23
+ const OSASCRIPT = [
24
+ "on run argv",
25
+ "try",
26
+ "set png to the clipboard as «class PNGf»",
27
+ "on error",
28
+ 'return "none"',
29
+ "end try",
30
+ "set f to open for access (POSIX file (item 1 of argv)) with write permission",
31
+ "set eof f to 0",
32
+ "write png to f",
33
+ "close access f",
34
+ 'return "ok"',
35
+ "end run",
36
+ ];
37
+ const POWERSHELL = "$img = Get-Clipboard -Format Image; " +
38
+ "if ($null -eq $img) { exit 3 }; " +
39
+ "$img.Save($env:AMA_CLIPBOARD_OUT, [System.Drawing.Imaging.ImageFormat]::Png)";
40
+ function attempts(platform, file, env) {
41
+ if (platform === "darwin") {
42
+ return [
43
+ {
44
+ tool: "osascript",
45
+ command: "osascript",
46
+ args: [...OSASCRIPT.flatMap((line) => ["-e", line]), file],
47
+ via: "file",
48
+ },
49
+ { tool: "pngpaste", command: "pngpaste", args: [file], via: "file" },
50
+ ];
51
+ }
52
+ if (platform === "win32") {
53
+ return [
54
+ {
55
+ tool: "powershell",
56
+ command: "powershell",
57
+ args: ["-NoProfile", "-NonInteractive", "-STA", "-Command", POWERSHELL],
58
+ via: "file",
59
+ env: { ...env, AMA_CLIPBOARD_OUT: file },
60
+ },
61
+ ];
62
+ }
63
+ const list = [];
64
+ if (env["WAYLAND_DISPLAY"]) {
65
+ list.push({ tool: "wl-paste", command: "wl-paste", args: ["-t", "image/png"], via: "stdout" });
66
+ }
67
+ list.push({
68
+ tool: "xclip",
69
+ command: "xclip",
70
+ args: ["-selection", "clipboard", "-t", "image/png", "-o"],
71
+ via: "stdout",
72
+ });
73
+ return list;
74
+ }
75
+ async function isPng(file) {
76
+ try {
77
+ return sniffImageMime(await readFile(file)) === "image/png";
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ /** 读剪贴板图片到数据目录;返回结果与原因(界面据此给一行提示)。 */
84
+ export async function readClipboardImage(dataDir, deps = {}) {
85
+ const platform = deps.platform ?? process.platform;
86
+ const env = deps.env ?? process.env;
87
+ const run = deps.run ?? runCommand;
88
+ const dir = join(dataDir, CLIPBOARD_DIR);
89
+ await mkdir(dir, { recursive: true });
90
+ const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[:.]/g, "-");
91
+ // 同一毫秒再贴:加序号,不能把上一张当成这次的结果
92
+ let file = join(dir, `${stamp}.png`);
93
+ for (let n = 1; existsSync(file); n++)
94
+ file = join(dir, `${stamp}-${n}.png`);
95
+ let anyTool = false;
96
+ for (const attempt of attempts(platform, file, env)) {
97
+ const options = attempt.via === "file" && attempt.env !== undefined ? { env: attempt.env } : {};
98
+ const result = await run(attempt.command, attempt.args, options);
99
+ if (result.missing === true)
100
+ continue;
101
+ anyTool = true;
102
+ if (result.code !== 0)
103
+ continue;
104
+ if (attempt.via === "stdout") {
105
+ if (sniffImageMime(result.stdout) !== "image/png")
106
+ continue;
107
+ await writeFile(file, result.stdout);
108
+ }
109
+ if (await isPng(file))
110
+ return { ok: true, path: file, tool: attempt.tool };
111
+ await rm(file, { force: true });
112
+ }
113
+ await rm(file, { force: true });
114
+ return { ok: false, reason: anyTool ? "no_image" : "no_tool" };
115
+ }
116
+ /** §10.2 的签名:成功返回 PNG 路径,没有工具或没有图返回 undefined。 */
117
+ export async function pasteClipboardImage(dataDir, deps = {}) {
118
+ const result = await readClipboardImage(dataDir, deps);
119
+ return result.ok ? result.path : undefined;
120
+ }
121
+ /** 清理 `<dataDir>/clipboard/` 里超过 `maxAgeDays`(缺省 7)天的文件;返回(将)删除的路径。 */
122
+ export async function gcClipboardImages(dataDir, options = {}) {
123
+ const dir = join(dataDir, CLIPBOARD_DIR);
124
+ let names;
125
+ try {
126
+ names = await readdir(dir);
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ const cutoff = (options.now ?? Date.now()) - (options.maxAgeDays ?? CLIPBOARD_MAX_AGE_DAYS) * 86_400_000;
132
+ const removed = [];
133
+ for (const name of names) {
134
+ const file = join(dir, name);
135
+ try {
136
+ const info = await stat(file);
137
+ if (!info.isFile() || info.mtimeMs >= cutoff)
138
+ continue;
139
+ if (options.dryRun !== true)
140
+ await rm(file, { force: true });
141
+ removed.push(file);
142
+ }
143
+ catch {
144
+ // 并发删除等:跳过
145
+ }
146
+ }
147
+ return removed;
148
+ }
@@ -34,5 +34,7 @@ export interface EditPlan {
34
34
  export declare function planEdits(content: string, edits: readonly EditOp[], replaceAll?: boolean): EditPlan;
35
35
  /** 统一 diff(LF 文本,3 行上下文);无差异返回空串。 */
36
36
  export declare function unifiedDiff(oldText: string, newText: string, path: string, context?: number): string;
37
+ /** 检查点:写前备份(docs/rewind-plan.md §2);钩子按约定不抛,这里再兜一层,不让编辑失败。 */
38
+ export declare function beforeWrite(ctx: ToolContext, abs: string): Promise<void>;
37
39
  export declare function executeEdit(input: EditInput, ctx: ToolContext): Promise<ToolResult>;
38
40
  export declare function createEditTool(): ToolDefinition<EditInput>;
@@ -164,6 +164,17 @@ function firstChangedLine(oldText, newText) {
164
164
  // ---------------------------------------------------------------------------
165
165
  // 工具
166
166
  // ---------------------------------------------------------------------------
167
+ /** 检查点:写前备份(docs/rewind-plan.md §2);钩子按约定不抛,这里再兜一层,不让编辑失败。 */
168
+ export async function beforeWrite(ctx, abs) {
169
+ if (ctx.checkpoint === undefined)
170
+ return;
171
+ try {
172
+ await ctx.checkpoint.beforeWrite(abs);
173
+ }
174
+ catch (error) {
175
+ ctx.log("warn", `checkpoint: ${error instanceof Error ? error.message : String(error)}`);
176
+ }
177
+ }
167
178
  export async function executeEdit(input, ctx) {
168
179
  const abs = resolvePath(input.path, ctx.cwd);
169
180
  const shown = displayPath(abs, ctx.cwd);
@@ -192,7 +203,10 @@ export async function executeEdit(input, ctx) {
192
203
  return { content: `${shown}: ${err.message}`, isError: true };
193
204
  throw err;
194
205
  }
195
- await writeFile(abs, bom + restoreLineEndings(plan.result, ending), "utf8");
206
+ const output = bom + restoreLineEndings(plan.result, ending);
207
+ await beforeWrite(ctx, abs);
208
+ await writeFile(abs, output, "utf8");
209
+ ctx.checkpoint?.afterWrite(abs, output);
196
210
  const details = {
197
211
  path: abs,
198
212
  diff: unifiedDiff(content, plan.result, shown),
@@ -209,8 +223,9 @@ export function createEditTool() {
209
223
  return {
210
224
  name: "edit",
211
225
  label: "Edit",
212
- description: "Replace text in a file (read it first). Each oldText must be unique in the original " +
213
- "(unless replaceAll); edits must not overlap.",
226
+ description: "Replace text in a file (read it first). Give all changes to a file as edits[] in one call; " +
227
+ "each oldText matches the original file and must be unique (unless replaceAll), short and " +
228
+ "non-overlapping.",
214
229
  parameters: {
215
230
  type: "object",
216
231
  properties: {
@@ -2,11 +2,15 @@
2
2
  * 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
3
3
  *
4
4
  * - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
5
- * - 单张上限 5 MB(各家图片上限里最小的 Anthropic);
5
+ * - [W5-I] 单张上限按 **base64 后**计算,按端点分档(ai/image-limits.ts,缺省 5 MB);任一边
6
+ * > 8000 px 拒绝;超限时按 `images.resize`(缺省 auto)用系统工具缩放(image-resize.ts),
7
+ * 没有工具则拒绝并提示;
6
8
  * - 不解码像素,只读文件头取尺寸。
7
9
  */
8
10
  import type { ImageBlock } from "../ai/types.js";
9
- export declare const MAX_IMAGE_BYTES: number;
11
+ import { type ResizeDeps } from "./image-resize.js";
12
+ /** 读进内存前的硬上限(原始字节):再大的文件缩放也不现实,直接拒绝。 */
13
+ export declare const MAX_IMAGE_FILE_BYTES: number;
10
14
  export declare const IMAGE_EXTENSIONS: Readonly<Record<string, string>>;
11
15
  /** 按扩展名(不分大小写);不是图片返回 undefined。 */
12
16
  export declare function imageMimeFromPath(path: string): string | undefined;
@@ -22,10 +26,41 @@ export interface LoadedImage {
22
26
  block: ImageBlock;
23
27
  path: string;
24
28
  mimeType: string;
29
+ /** 实际附上的字节数(缩放后为缩放结果的大小)。 */
25
30
  bytes: number;
26
31
  size?: ImageSize;
27
32
  }
33
+ export interface ImageFitOptions {
34
+ /** 单张 base64 后的上限(字节);缺省 5 MB(未知端点 / 中转)。 */
35
+ maxBase64Bytes?: number;
36
+ /** 超限时缩放:`auto`(缺省)找 sips / magick,`off` 从不缩放。 */
37
+ resize?: "auto" | "off";
38
+ /** 测试注入(平台、命令执行器)。 */
39
+ resizeDeps?: ResizeDeps;
40
+ }
41
+ export type ImageFit = {
42
+ ok: true;
43
+ buf: Buffer;
44
+ mimeType: string;
45
+ size?: ImageSize;
46
+ /** 缩放过:用的工具与原尺寸。 */
47
+ resized?: {
48
+ tool: string;
49
+ from?: ImageSize;
50
+ };
51
+ } | {
52
+ ok: false;
53
+ mimeType: string;
54
+ size?: ImageSize;
55
+ reason: "too_large" | "too_wide";
56
+ /** 给模型 / 日志的英文说明(read 工具原样放进结果)。 */
57
+ note: string;
58
+ };
59
+ /**
60
+ * 检查一张图是否在上限内(base64 后大小、边长);超限且给了 `path`、`resize` 不是 off 时先缩放再查。
61
+ */
62
+ export declare function fitImage(buf: Buffer, mimeType: string, options?: ImageFitOptions, path?: string): Promise<ImageFit>;
28
63
  /**
29
64
  * 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
30
65
  */
31
- export declare function loadImageFile(path: string): Promise<LoadedImage>;
66
+ export declare function loadImageFile(path: string, options?: ImageFitOptions): Promise<LoadedImage>;
@@ -2,13 +2,18 @@
2
2
  * 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
3
3
  *
4
4
  * - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
5
- * - 单张上限 5 MB(各家图片上限里最小的 Anthropic);
5
+ * - [W5-I] 单张上限按 **base64 后**计算,按端点分档(ai/image-limits.ts,缺省 5 MB);任一边
6
+ * > 8000 px 拒绝;超限时按 `images.resize`(缺省 auto)用系统工具缩放(image-resize.ts),
7
+ * 没有工具则拒绝并提示;
6
8
  * - 不解码像素,只读文件头取尺寸。
7
9
  */
8
10
  import { readFile, stat } from "node:fs/promises";
9
11
  import { extname } from "node:path";
10
12
  import { AmaError } from "../errors.js";
11
- export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
13
+ import { DEFAULT_IMAGE_BASE64_LIMIT, MAX_IMAGE_EDGE, base64Size, formatMb, } from "../ai/image-limits.js";
14
+ import { resizeImage } from "./image-resize.js";
15
+ /** 读进内存前的硬上限(原始字节):再大的文件缩放也不现实,直接拒绝。 */
16
+ export const MAX_IMAGE_FILE_BYTES = 64 * 1024 * 1024;
12
17
  export const IMAGE_EXTENSIONS = {
13
18
  ".png": "image/png",
14
19
  ".jpg": "image/jpeg",
@@ -84,10 +89,73 @@ function webpSize(buf) {
84
89
  }
85
90
  return undefined;
86
91
  }
92
+ function longEdge(size) {
93
+ return size === undefined ? 0 : Math.max(size.width, size.height);
94
+ }
95
+ function checkFit(buf, mimeType, limit) {
96
+ const size = imageSize(buf, mimeType);
97
+ const sized = size !== undefined ? { size } : {};
98
+ if (longEdge(size) > MAX_IMAGE_EDGE) {
99
+ return {
100
+ ok: false,
101
+ mimeType,
102
+ ...sized,
103
+ reason: "too_wide",
104
+ note: `Image is larger than ${MAX_IMAGE_EDGE}px on a side; not attached.`,
105
+ };
106
+ }
107
+ if (base64Size(buf.length) > limit) {
108
+ return {
109
+ ok: false,
110
+ mimeType,
111
+ ...sized,
112
+ reason: "too_large",
113
+ note: `Image exceeds the ${formatMb(limit)} attachment limit (base64); not attached.`,
114
+ };
115
+ }
116
+ return { ok: true, buf, mimeType, ...sized };
117
+ }
118
+ /**
119
+ * 检查一张图是否在上限内(base64 后大小、边长);超限且给了 `path`、`resize` 不是 off 时先缩放再查。
120
+ */
121
+ export async function fitImage(buf, mimeType, options = {}, path) {
122
+ const limit = options.maxBase64Bytes ?? DEFAULT_IMAGE_BASE64_LIMIT;
123
+ const fit = checkFit(buf, mimeType, limit);
124
+ if (fit.ok || path === undefined || options.resize === "off")
125
+ return fit;
126
+ const resized = await resizeImage(path, { maxEdge: MAX_IMAGE_EDGE, maxBase64Bytes: limit, mimeType, size: fit.size }, options.resizeDeps);
127
+ if (!resized.ok) {
128
+ const hint = resized.reason === "no_tool"
129
+ ? " (no resize tool found: install ImageMagick, or sips on macOS)"
130
+ : " (resizing did not bring it under the limit)";
131
+ return { ...fit, note: fit.note.replace(/; not attached\.$/, `${hint}; not attached.`) };
132
+ }
133
+ const again = checkFit(resized.buf, resized.mimeType, limit);
134
+ if (!again.ok)
135
+ return fit;
136
+ return {
137
+ ...again,
138
+ resized: { tool: resized.tool, ...(fit.size !== undefined ? { from: fit.size } : {}) },
139
+ };
140
+ }
141
+ function fitError(path, fit, limit, bytes, resizeOff) {
142
+ const hint = resizeOff
143
+ ? "(images.resize 为 off)"
144
+ : fit.note.includes("no resize tool")
145
+ ? "(没找到缩放工具:装 ImageMagick,macOS 自带 sips)"
146
+ : fit.note.includes("resizing did not")
147
+ ? "(缩放后仍超限)"
148
+ : "";
149
+ if (fit.reason === "too_wide") {
150
+ const dims = fit.size !== undefined ? `(${fit.size.width}×${fit.size.height})` : "";
151
+ return new AmaError("invalid_arguments", `图片任一边超过 ${MAX_IMAGE_EDGE} px:${path}${dims}${hint}`);
152
+ }
153
+ return new AmaError("invalid_arguments", `图片超过 ${formatMb(limit)} 上限(按 base64 后计算):${path}(${formatMb(base64Size(bytes))})${hint}`);
154
+ }
87
155
  /**
88
156
  * 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
89
157
  */
90
- export async function loadImageFile(path) {
158
+ export async function loadImageFile(path, options = {}) {
91
159
  let info;
92
160
  try {
93
161
  info = await stat(path);
@@ -97,18 +165,21 @@ export async function loadImageFile(path) {
97
165
  }
98
166
  if (!info.isFile())
99
167
  throw new AmaError("invalid_arguments", `不是文件:${path}`);
100
- if (info.size > MAX_IMAGE_BYTES)
101
- throw new AmaError("invalid_arguments", `图片超过 ${MAX_IMAGE_BYTES / 1024 / 1024} MB 上限:${path}(${(info.size / 1024 / 1024).toFixed(1)} MB)`);
168
+ const limit = options.maxBase64Bytes ?? DEFAULT_IMAGE_BASE64_LIMIT;
169
+ if (info.size > MAX_IMAGE_FILE_BYTES)
170
+ throw new AmaError("invalid_arguments", `图片超过 ${formatMb(limit)} 上限(按 base64 后计算):${path}(${formatMb(base64Size(info.size))})`);
102
171
  const buf = await readFile(path);
103
172
  const mimeType = sniffImageMime(buf) ?? imageMimeFromPath(path);
104
173
  if (mimeType === undefined)
105
174
  throw new AmaError("invalid_arguments", `不是支持的图片(PNG / JPEG / GIF / WebP):${path}`);
106
- const size = imageSize(buf, mimeType);
175
+ const fit = await fitImage(buf, mimeType, options, path);
176
+ if (!fit.ok)
177
+ throw fitError(path, fit, limit, buf.length, options.resize === "off");
107
178
  return {
108
- block: { type: "image", data: buf.toString("base64"), mimeType },
179
+ block: { type: "image", data: fit.buf.toString("base64"), mimeType: fit.mimeType },
109
180
  path,
110
- mimeType,
111
- bytes: buf.length,
112
- ...(size ? { size } : {}),
181
+ mimeType: fit.mimeType,
182
+ bytes: fit.buf.length,
183
+ ...(fit.size ? { size: fit.size } : {}),
113
184
  };
114
185
  }