@pugi/cli 0.1.0-beta.10 → 0.1.0-beta.100

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 (445) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/README.md +53 -11
  4. package/assets/pugi-prozr2-mascot.ansi +9 -0
  5. package/bin/run.js +33 -1
  6. package/dist/commands/deploy.js +40 -40
  7. package/dist/commands/flatten.js +191 -0
  8. package/dist/commands/jobs-watch.js +201 -0
  9. package/dist/commands/jobs.js +42 -27
  10. package/dist/commands/retro.js +210 -0
  11. package/dist/commands/smoke.js +133 -0
  12. package/dist/core/agent-progress/cleanup.js +134 -0
  13. package/dist/core/agent-progress/schema.js +144 -0
  14. package/dist/core/agent-progress/writer.js +101 -0
  15. package/dist/core/agents/adaptive-router.js +330 -0
  16. package/dist/core/agents/query-decomposer.js +297 -0
  17. package/dist/core/agents/registry.js +3 -3
  18. package/dist/core/approvals/shortcut-resolver.js +98 -0
  19. package/dist/core/artifact-chain/dispatcher.js +148 -0
  20. package/dist/core/artifact-chain/exporter.js +164 -0
  21. package/dist/core/artifact-chain/state.js +243 -0
  22. package/dist/core/artifact-chain/steps.js +169 -0
  23. package/dist/core/ask-user/question.js +92 -0
  24. package/dist/core/audit/audit-trail.js +275 -0
  25. package/dist/core/auth/ensure-authenticated.js +129 -0
  26. package/dist/core/auth/env-provider.js +238 -0
  27. package/dist/core/auto-open-browser.js +4 -4
  28. package/dist/core/auto-update/channels.js +122 -0
  29. package/dist/core/auto-update/checker.js +241 -0
  30. package/dist/core/auto-update/state.js +235 -0
  31. package/dist/core/bare-mode/index.js +107 -0
  32. package/dist/core/bash/redirect.js +281 -0
  33. package/dist/core/bash-classifier.js +436 -40
  34. package/dist/core/checkpoint/resumer.js +149 -0
  35. package/dist/core/checkpoint/rewinder.js +291 -0
  36. package/dist/core/checkpoints/shadow-git.js +670 -0
  37. package/dist/core/citations/parser.js +109 -0
  38. package/dist/core/classifier/yolo-classifier.js +88 -0
  39. package/dist/core/codegraph/db.js +506 -0
  40. package/dist/core/codegraph/decision-store.js +248 -0
  41. package/dist/core/codegraph/detect-repo.js +459 -0
  42. package/dist/core/codegraph/install.js +134 -0
  43. package/dist/core/codegraph/offer-hook.js +220 -0
  44. package/dist/core/codegraph/parser.js +71 -0
  45. package/dist/core/codegraph/types.js +34 -0
  46. package/dist/core/compact/auto-trigger.js +96 -0
  47. package/dist/core/compact/buffer-rewriter.js +115 -0
  48. package/dist/core/compact/summarizer.js +208 -0
  49. package/dist/core/compact/token-counter.js +108 -0
  50. package/dist/core/consensus/anvil-fanout.js +25 -25
  51. package/dist/core/consensus/diff-capture.js +121 -12
  52. package/dist/core/consensus/rubric.js +21 -21
  53. package/dist/core/context/builder.js +6 -6
  54. package/dist/core/context/compaction-events.js +8 -8
  55. package/dist/core/context/compaction.js +31 -31
  56. package/dist/core/context/index.js +15 -8
  57. package/dist/core/context/invariants.js +51 -51
  58. package/dist/core/context/markdown-loader.js +28 -10
  59. package/dist/core/context/markdown-traverse.js +255 -0
  60. package/dist/core/context/pugiignore.js +41 -41
  61. package/dist/core/context/repo-skeleton.js +37 -37
  62. package/dist/core/context/tool-eviction.js +55 -0
  63. package/dist/core/context/watcher.js +32 -32
  64. package/dist/core/context/working-set.js +23 -23
  65. package/dist/core/coordinator/agent-tools.js +77 -0
  66. package/dist/core/coordinator/agent-toolset.js +65 -0
  67. package/dist/core/coordinator/fsm.js +73 -0
  68. package/dist/core/coordinator/mode-fsm.js +70 -0
  69. package/dist/core/cost/rate-card.js +129 -0
  70. package/dist/core/cost/tracker.js +221 -0
  71. package/dist/core/credentials.js +13 -13
  72. package/dist/core/cron/scheduler.js +138 -0
  73. package/dist/core/denial-tracking/index.js +8 -0
  74. package/dist/core/denial-tracking/state.js +264 -0
  75. package/dist/core/diagnostics/probe-runner.js +93 -0
  76. package/dist/core/diagnostics/probes/api.js +46 -0
  77. package/dist/core/diagnostics/probes/auth.js +93 -0
  78. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  79. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  80. package/dist/core/diagnostics/probes/config.js +72 -0
  81. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  82. package/dist/core/diagnostics/probes/disk.js +81 -0
  83. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  84. package/dist/core/diagnostics/probes/git.js +65 -0
  85. package/dist/core/diagnostics/probes/hooks.js +118 -0
  86. package/dist/core/diagnostics/probes/mcp.js +75 -0
  87. package/dist/core/diagnostics/probes/node.js +59 -0
  88. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  89. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  90. package/dist/core/diagnostics/probes/sandbox.js +72 -0
  91. package/dist/core/diagnostics/probes/session.js +74 -0
  92. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  93. package/dist/core/diagnostics/probes/workspace.js +63 -0
  94. package/dist/core/diagnostics/types.js +70 -0
  95. package/dist/core/dispatch/cache-cleanup.js +197 -0
  96. package/dist/core/dispatch/cache-handoff.js +295 -0
  97. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  98. package/dist/core/edits/dispatch.js +333 -7
  99. package/dist/core/edits/format-detector.js +260 -0
  100. package/dist/core/edits/format-matrix.js +26 -0
  101. package/dist/core/edits/fuzzy-ladder.js +650 -0
  102. package/dist/core/edits/index.js +5 -1
  103. package/dist/core/edits/journal.js +199 -0
  104. package/dist/core/edits/layer-a-apply.js +15 -15
  105. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  106. package/dist/core/edits/layer-b-apply.js +9 -9
  107. package/dist/core/edits/layer-c-apply.js +6 -6
  108. package/dist/core/edits/layer-d-ast.js +557 -14
  109. package/dist/core/edits/marker-parser.js +12 -12
  110. package/dist/core/edits/security-gate.js +27 -27
  111. package/dist/core/edits/verify-hook.js +273 -0
  112. package/dist/core/edits/worktree.js +29 -29
  113. package/dist/core/engine/anvil-client.js +214 -26
  114. package/dist/core/engine/auto-compact.js +247 -0
  115. package/dist/core/engine/budgets.js +220 -0
  116. package/dist/core/engine/compact-llm-summarizer.js +124 -0
  117. package/dist/core/engine/context-prefix.js +155 -0
  118. package/dist/core/engine/index.js +1 -1
  119. package/dist/core/engine/intensity.js +163 -0
  120. package/dist/core/engine/intent.js +260 -0
  121. package/dist/core/engine/native-pugi.js +1559 -227
  122. package/dist/core/engine/prompts.js +187 -19
  123. package/dist/core/engine/strip-internal-fields.js +124 -0
  124. package/dist/core/engine/tool-bridge.js +1887 -59
  125. package/dist/core/engine/verification-patterns.js +195 -0
  126. package/dist/core/evaluation/golden-dataset.js +293 -0
  127. package/dist/core/feedback/queue.js +177 -0
  128. package/dist/core/feedback/submitter.js +145 -0
  129. package/dist/core/file-cache.js +113 -1
  130. package/dist/core/flatten/flatten-repo.js +439 -0
  131. package/dist/core/format/osc8-link.js +28 -0
  132. package/dist/core/hook-chains.js +392 -0
  133. package/dist/core/hooks/citation-verify-hook.js +138 -0
  134. package/dist/core/hooks/citation-verify.js +112 -0
  135. package/dist/core/hooks/events.js +46 -0
  136. package/dist/core/hooks/index.js +15 -0
  137. package/dist/core/hooks/registry.js +216 -0
  138. package/dist/core/hooks/runner.js +236 -0
  139. package/dist/core/hooks/v2/event-emitter.js +115 -0
  140. package/dist/core/hooks/v2/executor.js +282 -0
  141. package/dist/core/hooks/v2/index.js +25 -0
  142. package/dist/core/hooks/v2/lifecycle.js +104 -0
  143. package/dist/core/hooks/v2/loader.js +216 -0
  144. package/dist/core/hooks/v2/matcher.js +125 -0
  145. package/dist/core/hooks/v2/trust.js +143 -0
  146. package/dist/core/hooks/v2/types.js +86 -0
  147. package/dist/core/hooks/worktree-events.js +158 -0
  148. package/dist/core/image/renderer.js +71 -0
  149. package/dist/core/init/detector.js +582 -0
  150. package/dist/core/init/template-renderer.js +242 -0
  151. package/dist/core/jobs/registry.js +18 -18
  152. package/dist/core/ledger/results-tsv.js +142 -0
  153. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  154. package/dist/core/lsp/cache.js +105 -0
  155. package/dist/core/lsp/client.js +551 -41
  156. package/dist/core/lsp/language-detect.js +66 -0
  157. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  158. package/dist/core/lsp/server-detect.js +173 -0
  159. package/dist/core/lsp/symbol-cache.js +162 -0
  160. package/dist/core/lsp/symbol-tools.js +664 -0
  161. package/dist/core/mcp/client.js +97 -28
  162. package/dist/core/mcp/http-server.js +553 -0
  163. package/dist/core/mcp/orchestrator-config.js +192 -0
  164. package/dist/core/mcp/orchestrator-tools.js +806 -0
  165. package/dist/core/mcp/permission.js +190 -0
  166. package/dist/core/mcp/registry.js +39 -17
  167. package/dist/core/mcp/server-tools.js +219 -0
  168. package/dist/core/mcp/server.js +397 -0
  169. package/dist/core/mcp/trust.js +10 -10
  170. package/dist/core/memory/dual-write.js +416 -0
  171. package/dist/core/memory/passive-extract.js +130 -0
  172. package/dist/core/memory/phase1-kinds.js +20 -0
  173. package/dist/core/memory/secret-scanner.js +304 -0
  174. package/dist/core/memory-sync/queue.js +170 -0
  175. package/dist/core/metrics/extract.js +113 -0
  176. package/dist/core/modes/roo-modes.js +68 -0
  177. package/dist/core/notes/notes-paths.js +113 -0
  178. package/dist/core/notes/notes-recorder.js +140 -0
  179. package/dist/core/notes/notes-writer.js +53 -0
  180. package/dist/core/notes/renderers.js +0 -0
  181. package/dist/core/notes/slug.js +105 -0
  182. package/dist/core/onboarding/ensure-initialized.js +133 -0
  183. package/dist/core/onboarding/marker.js +111 -0
  184. package/dist/core/onboarding/telemetry-state.js +108 -0
  185. package/dist/core/output-style/presets.js +176 -0
  186. package/dist/core/output-style/state.js +185 -0
  187. package/dist/core/path-security.js +287 -5
  188. package/dist/core/permission.js +82 -22
  189. package/dist/core/permissions/auto-classifier.js +124 -0
  190. package/dist/core/permissions/bash-parser.js +371 -0
  191. package/dist/core/permissions/circuit-breaker.js +83 -0
  192. package/dist/core/permissions/constrained-edit.js +91 -0
  193. package/dist/core/permissions/gate.js +278 -0
  194. package/dist/core/permissions/index.js +20 -0
  195. package/dist/core/permissions/mode.js +174 -0
  196. package/dist/core/permissions/network-egress.js +137 -0
  197. package/dist/core/permissions/state.js +241 -0
  198. package/dist/core/permissions/tool-class.js +107 -0
  199. package/dist/core/plan-mode/ui-state.js +51 -0
  200. package/dist/core/plans/plan-artifact.js +721 -0
  201. package/dist/core/policy-limits/etag-store.js +122 -0
  202. package/dist/core/prd-check/parser.js +215 -0
  203. package/dist/core/prd-check/reporter.js +127 -0
  204. package/dist/core/prd-check/session-review.js +557 -0
  205. package/dist/core/prd-check/verifiers.js +223 -0
  206. package/dist/core/prompt-cache/client-cache.js +99 -0
  207. package/dist/core/prompts/assembly.js +29 -0
  208. package/dist/core/prompts/registry.js +364 -0
  209. package/dist/core/pugi-gitignore.js +52 -0
  210. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  211. package/dist/core/pugi-md/context-injector.js +76 -0
  212. package/dist/core/pugi-md/walk-up.js +207 -0
  213. package/dist/core/python/uv-installer.js +270 -0
  214. package/dist/core/python/uv-resolver.js +83 -0
  215. package/dist/core/rate-limit/narrator.js +146 -0
  216. package/dist/core/recipes/cli-types.js +20 -0
  217. package/dist/core/recipes/loader.js +103 -0
  218. package/dist/core/recipes/runner.js +345 -0
  219. package/dist/core/recipes/schema.js +587 -0
  220. package/dist/core/release-notes/parser.js +241 -0
  221. package/dist/core/release-notes/state.js +116 -0
  222. package/dist/core/repl/ask.js +37 -37
  223. package/dist/core/repl/cancellation.js +26 -26
  224. package/dist/core/repl/cap-warning.js +4 -4
  225. package/dist/core/repl/clipboard-read.js +11 -11
  226. package/dist/core/repl/dispatch-fsm.js +12 -12
  227. package/dist/core/repl/engine-bridge.js +303 -0
  228. package/dist/core/repl/history-search.js +15 -15
  229. package/dist/core/repl/history.js +28 -18
  230. package/dist/core/repl/kill-ring.js +5 -5
  231. package/dist/core/repl/model-pricing.js +135 -0
  232. package/dist/core/repl/privacy-banner.js +22 -22
  233. package/dist/core/repl/session.js +2690 -229
  234. package/dist/core/repl/slash-commands.js +540 -41
  235. package/dist/core/repl/store/index.js +1 -1
  236. package/dist/core/repl/store/jsonl-log.js +22 -22
  237. package/dist/core/repl/store/lockfile.js +10 -10
  238. package/dist/core/repl/store/session-store.js +136 -107
  239. package/dist/core/repl/store/types.js +15 -15
  240. package/dist/core/repl/store/uuid-v7.js +12 -12
  241. package/dist/core/repl/tool-route.js +382 -0
  242. package/dist/core/repl/workspace-context.js +43 -21
  243. package/dist/core/repo-map/build.js +125 -0
  244. package/dist/core/repo-map/cache.js +185 -0
  245. package/dist/core/repo-map/extractor.js +254 -0
  246. package/dist/core/repo-map/formatter.js +145 -0
  247. package/dist/core/repo-map/page-rank.js +105 -0
  248. package/dist/core/repo-map/scanner.js +211 -0
  249. package/dist/core/retro/git-collector.js +251 -0
  250. package/dist/core/retro/health-card.js +25 -0
  251. package/dist/core/retro/metrics.js +342 -0
  252. package/dist/core/retro/narrative.js +249 -0
  253. package/dist/core/retro/plane-collector.js +274 -0
  254. package/dist/core/retro/pr-issue-link.js +65 -0
  255. package/dist/core/retro/types.js +16 -0
  256. package/dist/core/retry-budget/budget.js +284 -0
  257. package/dist/core/retry-budget/index.js +5 -0
  258. package/dist/core/retry-budget/retry-cap.js +74 -0
  259. package/dist/core/routing/lead-worker.js +43 -0
  260. package/dist/core/routing/pre-flight-estimator.js +108 -0
  261. package/dist/core/runs/run-tree.js +103 -0
  262. package/dist/core/sandboxing/adapter.js +29 -0
  263. package/dist/core/sandboxing/index.js +49 -0
  264. package/dist/core/sandboxing/none.js +19 -0
  265. package/dist/core/sandboxing/seatbelt.js +183 -0
  266. package/dist/core/security/injection-scanner.js +367 -0
  267. package/dist/core/security/output-filter.js +418 -0
  268. package/dist/core/session/env-file.js +105 -0
  269. package/dist/core/session/section-budgets.js +140 -0
  270. package/dist/core/session.js +119 -0
  271. package/dist/core/settings.js +378 -5
  272. package/dist/core/share/formatter.js +271 -0
  273. package/dist/core/share/redactor.js +221 -0
  274. package/dist/core/share/uploader.js +267 -0
  275. package/dist/core/skills/defaults.js +30 -30
  276. package/dist/core/skills/loader.js +22 -22
  277. package/dist/core/skills/sources.js +27 -27
  278. package/dist/core/smoke/headless-driver.js +174 -0
  279. package/dist/core/smoke/orchestrator.js +194 -0
  280. package/dist/core/smoke/runner.js +238 -0
  281. package/dist/core/smoke/scenario-parser.js +316 -0
  282. package/dist/core/statusline.js +99 -0
  283. package/dist/core/subagents/dispatcher-real.js +600 -0
  284. package/dist/core/subagents/dispatcher.js +146 -52
  285. package/dist/core/subagents/index.js +19 -6
  286. package/dist/core/subagents/isolation-matrix.js +213 -0
  287. package/dist/core/subagents/spawn.js +19 -4
  288. package/dist/core/telemetry/emitter.js +229 -0
  289. package/dist/core/telemetry/queue.js +251 -0
  290. package/dist/core/theme/context.js +91 -0
  291. package/dist/core/theme/presets.js +228 -0
  292. package/dist/core/theme/state.js +181 -0
  293. package/dist/core/todos/invariant.js +10 -0
  294. package/dist/core/todos/state.js +177 -0
  295. package/dist/core/tool-schema/compressor.js +89 -0
  296. package/dist/core/transport/version-interceptor.js +166 -0
  297. package/dist/core/trust.js +2 -2
  298. package/dist/core/tui/thinking-block.js +64 -0
  299. package/dist/core/vim/keymap.js +288 -0
  300. package/dist/core/vim/state.js +92 -0
  301. package/dist/core/watch-markers/marker-watcher.js +133 -0
  302. package/dist/core/worktree/include-parser.js +249 -0
  303. package/dist/core/worktree-manager/cleanup.js +123 -0
  304. package/dist/core/worktree-manager/manager.js +303 -0
  305. package/dist/index.js +36 -0
  306. package/dist/runtime/bootstrap.js +190 -0
  307. package/dist/runtime/cli.js +4345 -561
  308. package/dist/runtime/commands/agents.js +31 -31
  309. package/dist/runtime/commands/budget.js +5 -5
  310. package/dist/runtime/commands/cancel.js +231 -0
  311. package/dist/runtime/commands/chain.js +489 -0
  312. package/dist/runtime/commands/codegraph-status.js +227 -0
  313. package/dist/runtime/commands/compact.js +297 -0
  314. package/dist/runtime/commands/config.js +74 -40
  315. package/dist/runtime/commands/cost.js +199 -0
  316. package/dist/runtime/commands/delegate.js +27 -4
  317. package/dist/runtime/commands/dispatch.js +126 -0
  318. package/dist/runtime/commands/doctor.js +579 -0
  319. package/dist/runtime/commands/feedback.js +184 -0
  320. package/dist/runtime/commands/hooks.js +187 -0
  321. package/dist/runtime/commands/index-cmd.js +353 -0
  322. package/dist/runtime/commands/init.js +254 -0
  323. package/dist/runtime/commands/lsp.js +200 -38
  324. package/dist/runtime/commands/mcp.js +935 -0
  325. package/dist/runtime/commands/memory.js +582 -0
  326. package/dist/runtime/commands/model.js +237 -0
  327. package/dist/runtime/commands/onboarding.js +275 -0
  328. package/dist/runtime/commands/patch.js +12 -12
  329. package/dist/runtime/commands/permissions.js +112 -0
  330. package/dist/runtime/commands/plan.js +143 -0
  331. package/dist/runtime/commands/prd-check.js +285 -0
  332. package/dist/runtime/commands/privacy.js +17 -17
  333. package/dist/runtime/commands/recipe.js +325 -0
  334. package/dist/runtime/commands/redo-blob-store.js +92 -0
  335. package/dist/runtime/commands/redo.js +361 -0
  336. package/dist/runtime/commands/release-notes.js +229 -0
  337. package/dist/runtime/commands/repo-map.js +95 -0
  338. package/dist/runtime/commands/report.js +299 -0
  339. package/dist/runtime/commands/resume.js +118 -0
  340. package/dist/runtime/commands/review-consensus.js +68 -53
  341. package/dist/runtime/commands/rewind.js +333 -0
  342. package/dist/runtime/commands/roster.js +14 -14
  343. package/dist/runtime/commands/servers.js +236 -0
  344. package/dist/runtime/commands/sessions.js +163 -0
  345. package/dist/runtime/commands/share.js +316 -0
  346. package/dist/runtime/commands/skills.js +31 -31
  347. package/dist/runtime/commands/status.js +186 -0
  348. package/dist/runtime/commands/stickers.js +82 -0
  349. package/dist/runtime/commands/style.js +194 -0
  350. package/dist/runtime/commands/theme.js +196 -0
  351. package/dist/runtime/commands/undo.js +54 -22
  352. package/dist/runtime/commands/update.js +289 -0
  353. package/dist/runtime/commands/vim.js +140 -0
  354. package/dist/runtime/commands/worktree.js +8 -8
  355. package/dist/runtime/commands/worktrees.js +155 -0
  356. package/dist/runtime/deprecation-warning.js +69 -0
  357. package/dist/runtime/engine-exit-code.js +50 -0
  358. package/dist/runtime/headless-repl.js +195 -0
  359. package/dist/runtime/headless.js +548 -0
  360. package/dist/runtime/load-hooks-or-exit.js +71 -0
  361. package/dist/runtime/plan-decompose.js +22 -22
  362. package/dist/runtime/sigint-guard.js +272 -0
  363. package/dist/runtime/stream-renderer.js +195 -0
  364. package/dist/runtime/update-check.js +28 -28
  365. package/dist/runtime/version.js +65 -0
  366. package/dist/runtime/worktree-bootstrap.js +579 -0
  367. package/dist/skills/bundled/batch.js +617 -0
  368. package/dist/skills/bundled/index.js +45 -0
  369. package/dist/skills/bundled/loop.js +358 -0
  370. package/dist/skills/bundled/remember.js +383 -0
  371. package/dist/skills/bundled/simplify.js +289 -0
  372. package/dist/skills/bundled/skillify.js +373 -0
  373. package/dist/skills/bundled/stuck.js +558 -0
  374. package/dist/skills/bundled/verify.js +439 -0
  375. package/dist/testing/vcr.js +486 -0
  376. package/dist/tools/agent-tool.js +229 -0
  377. package/dist/tools/apply-patch.js +89 -28
  378. package/dist/tools/ask-user-question.js +337 -0
  379. package/dist/tools/ask-user.js +115 -0
  380. package/dist/tools/bash.js +624 -46
  381. package/dist/tools/brief.js +224 -0
  382. package/dist/tools/cron.js +433 -0
  383. package/dist/tools/enter-worktree.js +250 -0
  384. package/dist/tools/exit-worktree.js +147 -0
  385. package/dist/tools/file-tools.js +161 -44
  386. package/dist/tools/http-request.js +336 -0
  387. package/dist/tools/lsp-tools.js +377 -1
  388. package/dist/tools/mcp-tool.js +260 -0
  389. package/dist/tools/multi-edit.js +361 -0
  390. package/dist/tools/powershell.js +268 -0
  391. package/dist/tools/registry.js +120 -5
  392. package/dist/tools/server-tools.js +892 -0
  393. package/dist/tools/skill-tool.js +96 -0
  394. package/dist/tools/sleep.js +99 -0
  395. package/dist/tools/synthetic-output.js +133 -0
  396. package/dist/tools/tasks.js +208 -0
  397. package/dist/tools/todo-write.js +184 -0
  398. package/dist/tools/verify-plan-execution.js +295 -0
  399. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  400. package/dist/tools/web-fetch.js +195 -10
  401. package/dist/tools/web-search.js +458 -0
  402. package/dist/tui/agent-progress-card.js +111 -0
  403. package/dist/tui/agent-tree.js +22 -1
  404. package/dist/tui/ask-modal.js +14 -14
  405. package/dist/tui/ask-user-question-chips.js +315 -0
  406. package/dist/tui/ask-user-question-prompt.js +203 -0
  407. package/dist/tui/compact-banner.js +81 -0
  408. package/dist/tui/conversation-pane.js +85 -11
  409. package/dist/tui/cost-table.js +111 -0
  410. package/dist/tui/device-flow.js +2 -2
  411. package/dist/tui/doctor-table.js +46 -0
  412. package/dist/tui/feedback-prompt.js +156 -0
  413. package/dist/tui/input-box.js +247 -32
  414. package/dist/tui/login-picker.js +3 -3
  415. package/dist/tui/markdown-render.js +6 -6
  416. package/dist/tui/multi-file-diff-approval.js +375 -0
  417. package/dist/tui/onboarding-wizard.js +240 -0
  418. package/dist/tui/permissions-picker.js +86 -0
  419. package/dist/tui/render.js +36 -1
  420. package/dist/tui/repl-render.js +239 -25
  421. package/dist/tui/repl-splash-art.js +16 -16
  422. package/dist/tui/repl-splash-mascot.js +48 -24
  423. package/dist/tui/repl-splash.js +22 -22
  424. package/dist/tui/repl.js +125 -45
  425. package/dist/tui/slash-palette.js +6 -6
  426. package/dist/tui/splash.js +2 -2
  427. package/dist/tui/status-bar.js +109 -31
  428. package/dist/tui/status-table.js +7 -0
  429. package/dist/tui/stickers-art.js +136 -0
  430. package/dist/tui/style-table.js +28 -0
  431. package/dist/tui/theme-table.js +29 -0
  432. package/dist/tui/thinking-spinner.js +123 -0
  433. package/dist/tui/tool-stream-pane.js +53 -4
  434. package/dist/tui/update-banner.js +27 -2
  435. package/dist/tui/vim-input.js +267 -0
  436. package/dist/tui/welcome-banner.js +107 -0
  437. package/dist/tui/welcome-data.js +293 -0
  438. package/dist/tui/workspace-context.js +2 -2
  439. package/package.json +21 -5
  440. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  441. package/test/scenarios/compact-force.scenario.txt +12 -0
  442. package/test/scenarios/identity.scenario.txt +11 -0
  443. package/test/scenarios/persona-handoff.scenario.txt +12 -0
  444. package/test/scenarios/walkback.scenario.txt +12 -0
  445. package/dist/core/engine/compaction-hook.js +0 -154
@@ -1,13 +1,42 @@
1
+ /**
2
+ * file-tools - Pugi CLI file/bash/glob/grep tool surface.
3
+ *
4
+ * Workspace-binding contract (CEO red-alert follow-up):
5
+ *
6
+ * Every tool dispatch path threads `ctx.root` from the operator's
7
+ * `process.cwd()` through `EngineTask.workspaceRoot` ->
8
+ * `native-pugi.run()` -> `toolCtx.root` -> here. Tools call
9
+ * `resolveWorkspacePath(ctx.root, path)` for every on-disk operation
10
+ * so a dispatched specialist (e.g. Hiroshi writing tic-tac-toe HTML)
11
+ * produces files in the OPERATOR'S cwd, never in a server-side temp
12
+ * space. The path-security gate refuses traversal (`../etc/passwd`,
13
+ * URL-encoded variants, symlink escapes at the target).
14
+ *
15
+ * Wiring chain:
16
+ * 1. runtime/cli.ts: workspaceRoot = process.cwd()
17
+ * 2. EngineTask.workspaceRoot threads through to native-pugi.run().
18
+ * 3. native-pugi: const root = task.workspaceRoot
19
+ * 4. tool-bridge: passes ctx.root to file-tools / bash.
20
+ * 5. file-tools: resolveWorkspacePath(ctx.root, path).
21
+ *
22
+ * The contract is locked by `test/tools-write-to-workspace.spec.ts`
23
+ * (6 cases covering relative + nested + absolute paths + traversal
24
+ * refusal). If any layer of the chain regressed silently, dispatched
25
+ * files would land in `/tmp` instead of the operator's repo, which
26
+ * is the same failure surface as the menu-mode anti-pattern the
27
+ * sibling commits close.
28
+ */
1
29
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
30
+ import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
3
31
  import { dirname, isAbsolute, relative } from 'node:path';
4
32
  import { globSync } from 'node:fs';
5
33
  import { decidePermission } from '../core/permission.js';
6
- import { createReadRecord, hashContent } from '../core/file-cache.js';
34
+ import { StaleReadError, createReadRecord, hashContent, } from '../core/file-cache.js';
7
35
  import { resolveWorkspacePath } from '../core/path-security.js';
36
+ import { scanForInjection, summarizeFindings } from '../core/security/injection-scanner.js';
8
37
  import { recordFileMutation, recordToolCall, recordToolResult } from '../core/session.js';
9
38
  /**
10
- * α6.9 WriteGate marker — thrown by `gateOnCancellation` when the
39
+ * WriteGate marker — thrown by `gateOnCancellation` when the
11
40
  * caller supplied a cancellation token that has already aborted. The
12
41
  * tool dispatch loop in `tool-bridge.ts` recognises the name and folds
13
42
  * the throw into a `status: 'aborted'` tool result rather than a hard
@@ -19,8 +48,13 @@ export class OperatorAbortedError extends Error {
19
48
  this.name = 'OperatorAbortedError';
20
49
  }
21
50
  }
51
+ // Re-export StaleReadError so tool-bridge / test consumers can import
52
+ // the typed error from a single file-tools surface alongside
53
+ // OperatorAbortedError. Same shape as the existing OperatorAbortedError
54
+ // re-surface pattern.
55
+ export { StaleReadError } from '../core/file-cache.js';
22
56
  /**
23
- * α6.9 WriteGate: refuse the tool dispatch when the active
57
+ * WriteGate: refuse the tool dispatch when the active
24
58
  * cancellation token has aborted. Idempotent (the token's `isAborted`
25
59
  * is a getter, no side effects). Returns void on the happy path so the
26
60
  * tool can proceed; throws `OperatorAbortedError` when cancelled.
@@ -71,7 +105,7 @@ function permissionGatedResolve(ctx, inputPath, action, toolName) {
71
105
  }
72
106
  export function readTool(ctx, path) {
73
107
  const toolCallId = recordToolCall(ctx.session, 'read', path);
74
- // α6.9 WriteGate: fail fast on operator cancel BEFORE permission
108
+ // WriteGate: fail fast on operator cancel BEFORE permission
75
109
  // decision so a half-second post-cancel race never lands the read.
76
110
  if (ctx.cancellation && ctx.cancellation.isAborted) {
77
111
  const reason = 'operator_aborted: read refused';
@@ -100,7 +134,7 @@ export function readTool(ctx, path) {
100
134
  }
101
135
  export function writeTool(ctx, path, content) {
102
136
  const toolCallId = recordToolCall(ctx.session, 'write', path);
103
- // α6.9 WriteGate: refuse the write when the operator has cancelled
137
+ // WriteGate: refuse the write when the operator has cancelled
104
138
  // the dispatch. The audit log captures the cancellation reason so a
105
139
  // post-mortem can distinguish operator_aborted from settings-deny.
106
140
  if (ctx.cancellation && ctx.cancellation.isAborted) {
@@ -124,10 +158,45 @@ export function writeTool(ctx, path, content) {
124
158
  throw error;
125
159
  }
126
160
  const existed = existsSync(resolved);
127
- const before = existed ? readFileSync(resolved, 'utf8') : undefined;
161
+ // stale-read gate for writeTool's update-existing path. The
162
+ // model uses writeTool for two distinct intents:
163
+ //
164
+ // - create-new: path does not exist on disk. There is no prior
165
+ // read to validate against; skip the gate. This is the
166
+ // intentional escape hatch the leak spec also calls out.
167
+ // - overwrite-existing: path exists. Without the gate the model
168
+ // could blind-clobber an externally-modified file, losing the
169
+ // concurrent change silently. Force the model to re-read first.
170
+ //
171
+ // We deliberately apply the SAME stale-validation primitive editTool
172
+ // uses so the two write surfaces stay symmetric and a future fix to
173
+ // either one cannot accidentally weaken the other.
174
+ let before;
175
+ if (existed) {
176
+ before = readFileSync(resolved, 'utf8');
177
+ const currentStat = statSync(resolved);
178
+ const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
179
+ if (validation.stale) {
180
+ const reason = `stale_read: write ${path} refused — ${validation.detail}`;
181
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
182
+ throw new StaleReadError(path, validation.reason, validation.detail);
183
+ }
184
+ }
128
185
  const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
129
186
  writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
130
187
  renameSync(tmp, resolved);
188
+ // Injection scan (ported an external utility,
189
+ // Apache-2.0). Scan the BODY (never the path — path security is
190
+ // owned by `path-security.ts`). Findings are SURFACED as an extra
191
+ // line on the session tool-result, never block the write. Hard-
192
+ // block requires a separate CEO-signed PR. Failure here must NOT
193
+ // throw: a buggy scanner cannot rugpull the write that already
194
+ // landed on disk above.
195
+ surfaceInjectionWarning(ctx, toolCallId, 'write', path, content);
196
+ // Refresh the cache with the post-write content so the model can
197
+ // chain a follow-up read+edit on the same file without an extra
198
+ // round-trip. Same pattern editTool uses below.
199
+ ctx.readCache.set(createReadRecord(ctx.root, path, content, 'read_tool'));
131
200
  recordFileMutation(ctx.session, {
132
201
  toolCallId,
133
202
  path,
@@ -137,9 +206,39 @@ export function writeTool(ctx, path, content) {
137
206
  });
138
207
  recordToolResult(ctx.session, toolCallId, 'success', `${existed ? 'Updated' : 'Created'} ${path}`);
139
208
  }
209
+ /**
210
+ * Surface an injection-scan warning on a file write/edit BODY. The
211
+ * scan never blocks — it folds findings into the session as a
212
+ * `tool_result` with status `warn` so an operator (or SOC pipeline
213
+ * tailing `<workspace>/.pugi/events.jsonl`) sees the signal without a
214
+ * mid-dispatch rollback.
215
+ *
216
+ * Wrapped in try/catch so a malformed scanner never crashes the tool
217
+ * loop — the write itself has already landed on disk by the time we
218
+ * call this.
219
+ */
220
+ function surfaceInjectionWarning(ctx, triggeringToolCallId, tool, path, body) {
221
+ try {
222
+ const findings = scanForInjection(body);
223
+ if (findings.length === 0)
224
+ return;
225
+ const summary = summarizeFindings(findings);
226
+ const warnCallId = recordToolCall(ctx.session, 'injection_warning', path);
227
+ const message = `injection_warning: ${tool} ${path} — ${summary.total} pattern(s) ` +
228
+ `(score=${summary.score}, kinds=${summary.kinds.join('|')}). ` +
229
+ `Triggering call: ${triggeringToolCallId}. ` +
230
+ `Detector: external-injection-patterns. Write was NOT blocked.`;
231
+ recordToolResult(ctx.session, warnCallId, 'success', message);
232
+ }
233
+ catch {
234
+ // Scanner failure must NEVER throw — the write has already
235
+ // landed and the tool loop has to continue. Silent no-op
236
+ // mirrors the audit-trail contract.
237
+ }
238
+ }
140
239
  export function editTool(ctx, path, oldString, newString) {
141
240
  const toolCallId = recordToolCall(ctx.session, 'edit', path);
142
- // α6.9 WriteGate: refuse the edit when the operator has cancelled
241
+ // WriteGate: refuse the edit when the operator has cancelled
143
242
  // the dispatch. Edits are higher-risk than reads — surface the abort
144
243
  // BEFORE we even consult permissions so a cancel-during-tool-loop
145
244
  // never partially mutates the workspace.
@@ -154,10 +253,6 @@ export function editTool(ctx, path, oldString, newString) {
154
253
  recordToolResult(ctx.session, toolCallId, 'error', reason);
155
254
  throw new Error(reason);
156
255
  }
157
- const readRecord = ctx.readCache.get(ctx.root, path);
158
- if (!readRecord) {
159
- throw new Error(`Cannot edit ${path}: file must be read first`);
160
- }
161
256
  let resolved;
162
257
  try {
163
258
  resolved = permissionGatedResolve(ctx, path, 'edit', 'edit');
@@ -167,20 +262,42 @@ export function editTool(ctx, path, oldString, newString) {
167
262
  recordToolResult(ctx.session, toolCallId, 'error', reason);
168
263
  throw error;
169
264
  }
265
+ // stale-read gate. Validate the model's read-time view of
266
+ // the file against the on-disk state BEFORE applying the mutation.
267
+ // We read disk content once and feed it to the validator so a single
268
+ // syscall covers both the gate decision AND the oldString/newString
269
+ // replacement below.
170
270
  const before = readFileSync(resolved, 'utf8');
171
- const currentHash = hashContent(before);
172
- if (currentHash !== readRecord.sha256) {
173
- throw new Error(`Cannot edit ${path}: file changed since last read`);
271
+ const currentStat = statSync(resolved);
272
+ const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
273
+ if (validation.stale) {
274
+ const reason = `stale_read: edit ${path} refused — ${validation.detail}`;
275
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
276
+ throw new StaleReadError(path, validation.reason, validation.detail);
174
277
  }
278
+ const currentHash = hashContent(before);
175
279
  const matches = before.split(oldString).length - 1;
176
- if (matches === 0)
177
- throw new Error(`Cannot edit ${path}: oldString not found`);
178
- if (matches > 1)
179
- throw new Error(`Cannot edit ${path}: oldString is not unique`);
280
+ if (matches === 0) {
281
+ const reason = `Cannot edit ${path}: oldString not found`;
282
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
283
+ throw new Error(reason);
284
+ }
285
+ if (matches > 1) {
286
+ const reason = `Cannot edit ${path}: oldString is not unique`;
287
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
288
+ throw new Error(reason);
289
+ }
180
290
  const after = before.replace(oldString, newString);
181
291
  const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
182
292
  writeFileSync(tmp, after, { encoding: 'utf8', mode: 0o600 });
183
293
  renameSync(tmp, resolved);
294
+ // Injection scan (ported an external utility,
295
+ // Apache-2.0). We scan the NEW SUBSTRING the model is inserting,
296
+ // not the full post-edit file — the rest of the file is operator-
297
+ // owned content that pre-dates this dispatch. False-positive on
298
+ // legitimate prose that mentions banned phrases is the worst
299
+ // outcome and the warn-only contract bounds the cost.
300
+ surfaceInjectionWarning(ctx, toolCallId, 'edit', path, newString);
184
301
  ctx.readCache.set(createReadRecord(ctx.root, path, after, 'read_tool'));
185
302
  recordFileMutation(ctx.session, {
186
303
  toolCallId,
@@ -193,7 +310,7 @@ export function editTool(ctx, path, oldString, newString) {
193
310
  }
194
311
  export function globTool(ctx, pattern) {
195
312
  const toolCallId = recordToolCall(ctx.session, 'glob', pattern);
196
- // α6.9 WriteGate: cancel-aware short-circuit. Glob is read-only but
313
+ // WriteGate: cancel-aware short-circuit. Glob is read-only but
197
314
  // can be expensive on large trees; respecting the abort here keeps
198
315
  // the tool loop responsive when the operator hits Ctrl+C mid-scan.
199
316
  if (ctx.cancellation && ctx.cancellation.isAborted) {
@@ -203,11 +320,11 @@ export function globTool(ctx, pattern) {
203
320
  }
204
321
  // Pugi globs are workspace-scoped. Reject any pattern that could enumerate
205
322
  // outside the workspace:
206
- // 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
207
- // regardless of `cwd`, so they fan out outside the repo.
208
- // 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
209
- // A substring check would over-reject legitimate names like
210
- // `src/v1..v2/*` so we split on `/` instead.
323
+ // 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
324
+ // regardless of `cwd`, so they fan out outside the repo.
325
+ // 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
326
+ // A substring check would over-reject legitimate names like
327
+ // `src/v1..v2/*` so we split on `/` instead.
211
328
  if (isAbsolute(pattern)) {
212
329
  const reason = `Absolute glob patterns are not allowed: ${pattern}`;
213
330
  recordToolResult(ctx.session, toolCallId, 'error', reason);
@@ -230,7 +347,7 @@ export function globTool(ctx, pattern) {
230
347
  }
231
348
  export function grepTool(ctx, query) {
232
349
  const toolCallId = recordToolCall(ctx.session, 'grep', query);
233
- // α6.9 WriteGate: refuse before scanning. Grep walks the whole
350
+ // WriteGate: refuse before scanning. Grep walks the whole
234
351
  // workspace and can take seconds on a large repo; check abort first
235
352
  // so a cancel mid-scan returns immediately rather than after the
236
353
  // full walk completes.
@@ -244,7 +361,7 @@ export function grepTool(ctx, query) {
244
361
  for (const path of files) {
245
362
  if (matches.length >= 200)
246
363
  break;
247
- // α6.9 WriteGate: poll abort inside the file loop so a cancel
364
+ // WriteGate: poll abort inside the file loop so a cancel
248
365
  // arriving mid-scan terminates early. The per-file branch keeps
249
366
  // the responsiveness bounded by the slowest single-file read.
250
367
  if (ctx.cancellation && ctx.cancellation.isAborted) {
@@ -289,18 +406,18 @@ export function grepTool(ctx, query) {
289
406
  }
290
407
  /**
291
408
  * Workspace-scoped bash tool. Sized for the M1 engine adapter:
292
- * - Runs through `/bin/sh -c <command>` so the model can use pipes,
293
- * redirection, and shell builtins (`ls | wc -l`, `git status`).
294
- * - `cwd` is pinned to the workspace root so a stray `cd /` cannot
295
- * leak commands outside the repo (the child process inherits root
296
- * filesystem visibility — destructive patterns are blocked by
297
- * `decidePermission`, not by chroot).
298
- * - Output capped at 64KB combined stdout/stderr to keep the
299
- * transcript bounded; the model gets the head + a `(...truncated)`
300
- * marker if the cap fires.
301
- * - 30s wall-clock timeout. The engine loop's per-tool error path
302
- * surfaces the timeout to the model so it can retry with a narrower
303
- * command or give up.
409
+ * - Runs through `/bin/sh -c <command>` so the model can use pipes,
410
+ * redirection, and shell builtins (`ls | wc -l`, `git status`).
411
+ * - `cwd` is pinned to the workspace root so a stray `cd /` cannot
412
+ * leak commands outside the repo (the child process inherits root
413
+ * filesystem visibility — destructive patterns are blocked by
414
+ * `decidePermission`, not by chroot).
415
+ * - Output capped at 64KB combined stdout/stderr to keep the
416
+ * transcript bounded; the model gets the head + a `(...truncated)`
417
+ * marker if the cap fires.
418
+ * - 30s wall-clock timeout. The engine loop's per-tool error path
419
+ * surfaces the timeout to the model so it can retry with a narrower
420
+ * command or give up.
304
421
  *
305
422
  * Permission gating: `kind: 'bash'`. The CLI's permission module already
306
423
  * hard-denies the destructive-patterns list (rm -rf /, DROP DATABASE,
@@ -311,7 +428,7 @@ export const BASH_OUTPUT_CAP = 64 * 1024;
311
428
  export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
312
429
  // Child-process stdio buffer — large enough that the model-facing
313
430
  // truncation cap (`BASH_OUTPUT_CAP`) is always the gate, never the
314
- // child's internal buffer. Code Reviewer P2 retro 2026-05-23 flagged
431
+ // child's internal buffer. Code Reviewer P2 retro flagged
315
432
  // `BASH_OUTPUT_CAP * 2` as too tight: real builds (`pnpm build`,
316
433
  // `tsc --noEmit`) routinely exceed 128 KB combined and the model
317
434
  // then saw a fatal `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` instead of a
@@ -319,7 +436,7 @@ export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
319
436
  export const BASH_CHILD_MAXBUFFER = 10 * 1024 * 1024;
320
437
  export function bashTool(ctx, command, options = {}) {
321
438
  const toolCallId = recordToolCall(ctx.session, 'bash', command);
322
- // α6.9 WriteGate: bash is the highest-risk tool surface. Refuse
439
+ // WriteGate: bash is the highest-risk tool surface. Refuse
323
440
  // before the destructive-pattern classifier even runs so a
324
441
  // cancelled dispatch never spawns a child process. Note: this is
325
442
  // pre-spawn cancellation only; once the /bin/sh -c process is
@@ -343,7 +460,7 @@ export function bashTool(ctx, command, options = {}) {
343
460
  //
344
461
  // Env sanitisation strategy: build the child env from an explicit
345
462
  // allow-list rather than inheriting `process.env` and trying to
346
- // strip secrets after the fact. Code Reviewer P1 2026-05-23 flagged
463
+ // strip secrets after the fact. Code Reviewer P1 flagged
347
464
  // that the deny-list approach missed ANTHROPIC_API_KEY / GH_TOKEN
348
465
  // / AWS_SECRET_ACCESS_KEY / DATABASE_URL / arbitrary *_TOKEN /
349
466
  // *_SECRET / *_KEY variables — every CI agent rotation would risk
@@ -372,7 +489,7 @@ export function bashTool(ctx, command, options = {}) {
372
489
  }
373
490
  const timeoutMs = options.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
374
491
  // `spawnSync` (vs `execFileSync`) captures stdout AND stderr on
375
- // BOTH success and failure paths. Code Reviewer P1 2026-05-23:
492
+ // BOTH success and failure paths. Code Reviewer P1:
376
493
  // `execFileSync` returns only stdout on exit 0, silently dropping
377
494
  // stderr output from `tsc`, `eslint`, `pytest`, etc. — the model
378
495
  // would see `(no output)` for successful runs that emitted real
@@ -381,7 +498,7 @@ export function bashTool(ctx, command, options = {}) {
381
498
  // maxBuffer is generous (10 MB) so the child process is never the
382
499
  // truncation gate — the post-hoc `.slice(0, BASH_OUTPUT_CAP)` below
383
500
  // is the single source of truth for what the model sees. Code
384
- // Reviewer P2 retro 2026-05-23: the previous `BASH_OUTPUT_CAP * 2`
501
+ // Reviewer P2 retro: the previous `BASH_OUTPUT_CAP * 2`
385
502
  // (128 KB) would hard-throw `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`
386
503
  // on noisy commands (`pnpm build`, `tsc --noEmit` on the whole
387
504
  // monorepo) instead of returning the truncated head.
@@ -0,0 +1,336 @@
1
+ /**
2
+ * http_request - Phase 1 CRUD-smoke primitive (PUGI Phase 1).
3
+ *
4
+ * Generic HTTP request tool the model uses to verify a freshly-spun
5
+ * service end-to-end. Distinct from `web_fetch`:
6
+ * - `web_fetch` is an opt-in marketing-grade content fetch (always
7
+ * GET, response folded to Markdown, sentinel wrap, SSRF guard
8
+ * blocks loopback / RFC1918).
9
+ * - `http_request` is a *development-evidence* primitive: any verb,
10
+ * any localhost URL, JSON-aware body+response. The default
11
+ * allow-list permits localhost / 127.0.0.1 / ::1 so the model can
12
+ * drive a CRUD smoke against a server it just started via
13
+ * `server_start`; non-loopback hosts require an explicit
14
+ * `allowExternal` opt-in.
15
+ *
16
+ * Body is serialised to JSON when a plain object is supplied. Headers
17
+ * are merged on top of a small default set (Accept: application/json,
18
+ * Content-Type when body is JSON). Response body is capped at 64 KB to
19
+ * keep the envelope sized; when the response content-type matches
20
+ * `application/json` the dispatcher attempts a parse and surfaces
21
+ * `json` alongside `body`.
22
+ *
23
+ * Brand voice: English only, no emoji, no banned words.
24
+ */
25
+ import { isIP } from 'node:net';
26
+ export const HTTP_REQUEST_INVALID_ARGS = 'HTTP_REQUEST_INVALID_ARGS';
27
+ export const HTTP_REQUEST_BODY_CAP_BYTES = 64 * 1024;
28
+ export const HTTP_REQUEST_DEFAULT_TIMEOUT_MS = 10_000;
29
+ export const HTTP_REQUEST_MAX_TIMEOUT_MS = 60_000;
30
+ const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
31
+ /**
32
+ * Validate raw arguments. Returns the typed payload on success or a
33
+ * sentinel string on failure (sleep/brief convention).
34
+ */
35
+ export function parseHttpRequestArgs(raw) {
36
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
37
+ return `${HTTP_REQUEST_INVALID_ARGS}: arguments must be a JSON object`;
38
+ }
39
+ const obj = raw;
40
+ const method = obj['method'];
41
+ const url = obj['url'];
42
+ if (typeof method !== 'string' || !ALLOWED_METHODS.has(method.toUpperCase())) {
43
+ return `${HTTP_REQUEST_INVALID_ARGS}: method must be one of ${Array.from(ALLOWED_METHODS).join(', ')}`;
44
+ }
45
+ if (typeof url !== 'string' || url.trim() === '') {
46
+ return `${HTTP_REQUEST_INVALID_ARGS}: url must be a non-empty string`;
47
+ }
48
+ const body = obj['body'];
49
+ if (body !== undefined && body !== null) {
50
+ if (typeof body !== 'string' &&
51
+ !(typeof body === 'object')) {
52
+ return `${HTTP_REQUEST_INVALID_ARGS}: body must be a string or a JSON object/array`;
53
+ }
54
+ }
55
+ const headers = obj['headers'];
56
+ if (headers !== undefined) {
57
+ if (typeof headers !== 'object' || headers === null || Array.isArray(headers)) {
58
+ return `${HTTP_REQUEST_INVALID_ARGS}: headers must be a JSON object of string values`;
59
+ }
60
+ for (const [k, v] of Object.entries(headers)) {
61
+ if (typeof v !== 'string') {
62
+ return `${HTTP_REQUEST_INVALID_ARGS}: headers["${k}"] must be a string`;
63
+ }
64
+ }
65
+ }
66
+ const timeoutMs = obj['timeoutMs'];
67
+ if (timeoutMs !== undefined) {
68
+ if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
69
+ return `${HTTP_REQUEST_INVALID_ARGS}: timeoutMs must be a positive number when provided`;
70
+ }
71
+ }
72
+ const allowExternal = obj['allowExternal'];
73
+ if (allowExternal !== undefined && typeof allowExternal !== 'boolean') {
74
+ return `${HTTP_REQUEST_INVALID_ARGS}: allowExternal must be a boolean when provided`;
75
+ }
76
+ const result = {
77
+ method: method.toUpperCase(),
78
+ url,
79
+ ...(body !== undefined && body !== null
80
+ ? { body: body }
81
+ : {}),
82
+ ...(headers !== undefined ? { headers: headers } : {}),
83
+ ...(typeof timeoutMs === 'number' ? { timeoutMs } : {}),
84
+ ...(typeof allowExternal === 'boolean' ? { allowExternal } : {}),
85
+ };
86
+ return result;
87
+ }
88
+ function clampTimeout(value) {
89
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
90
+ return HTTP_REQUEST_DEFAULT_TIMEOUT_MS;
91
+ }
92
+ return Math.min(value, HTTP_REQUEST_MAX_TIMEOUT_MS);
93
+ }
94
+ /**
95
+ * Decide whether the target host counts as loopback. Accept the
96
+ * familiar shorthand (`localhost`, `127.0.0.1`, `::1`) plus the
97
+ * full 127.0.0.0/8 IPv4 range so a server bound to e.g. `127.0.0.5`
98
+ * still passes the gate.
99
+ */
100
+ function isLoopbackHost(host) {
101
+ const lower = host.toLowerCase();
102
+ if (lower === 'localhost' ||
103
+ lower === '127.0.0.1' ||
104
+ lower === '::1' ||
105
+ lower === '[::1]' ||
106
+ // /triple-review P1 (Claude reviewer): IPv4-mapped
107
+ // IPv6 (`::ffff:127.0.0.1`) reaches loopback at the OS level.
108
+ // Recognize so a model that emits the IPv6 form is gated as
109
+ // loopback (rather than blocked as external — safer to allow
110
+ // explicitly recognised loopback and reject everything else).
111
+ lower === '::ffff:127.0.0.1' ||
112
+ lower.startsWith('::ffff:127.')) {
113
+ return true;
114
+ }
115
+ if (isIP(lower) === 4 && lower.startsWith('127.')) {
116
+ return true;
117
+ }
118
+ // /triple-review P1: `0.0.0.0` resolves к localhost for outbound on
119
+ // Linux and is reachable from inside the host. Treat as loopback so
120
+ // a server bound к `0.0.0.0` is callable from the dispatcher без
121
+ // requiring `allowExternal`. The risk surface is identical к
122
+ // `127.0.0.0/8` — anything reachable via `0.0.0.0` is also reachable
123
+ // via `127.0.0.1` on the same host.
124
+ if (lower === '0.0.0.0') {
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+ function headersToObject(h) {
130
+ const out = {};
131
+ h.forEach((value, key) => {
132
+ out[key.toLowerCase()] = value;
133
+ });
134
+ return out;
135
+ }
136
+ /**
137
+ * Dispatch entry. Pure async - no shell, no filesystem. Loopback URLs
138
+ * always pass; external URLs require `allowExternal: true` so the
139
+ * default posture stays private.
140
+ */
141
+ export async function dispatchHttpRequest(ctx, raw) {
142
+ const parsed = parseHttpRequestArgs(raw);
143
+ if (typeof parsed === 'string')
144
+ return parsed;
145
+ const args = parsed;
146
+ const now = ctx.now ?? (() => Date.now());
147
+ const fetchImpl = ctx.fetch ?? globalThis.fetch;
148
+ if (typeof fetchImpl !== 'function') {
149
+ const result = {
150
+ ok: false,
151
+ status: 0,
152
+ headers: {},
153
+ body: '',
154
+ durationMs: 0,
155
+ error: 'no_fetch_available',
156
+ };
157
+ return JSON.stringify(result);
158
+ }
159
+ // URL parse + loopback gate.
160
+ let parsedUrl;
161
+ try {
162
+ parsedUrl = new URL(args.url);
163
+ }
164
+ catch (error) {
165
+ const result = {
166
+ ok: false,
167
+ status: 0,
168
+ headers: {},
169
+ body: '',
170
+ durationMs: 0,
171
+ error: `invalid_url: ${error.message}`,
172
+ };
173
+ return JSON.stringify(result);
174
+ }
175
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
176
+ const result = {
177
+ ok: false,
178
+ status: 0,
179
+ headers: {},
180
+ body: '',
181
+ durationMs: 0,
182
+ error: `unsupported_protocol: ${parsedUrl.protocol}`,
183
+ };
184
+ return JSON.stringify(result);
185
+ }
186
+ const host = parsedUrl.hostname;
187
+ const loopback = isLoopbackHost(host);
188
+ if (!loopback && args.allowExternal !== true) {
189
+ const result = {
190
+ ok: false,
191
+ status: 0,
192
+ headers: {},
193
+ body: '',
194
+ durationMs: 0,
195
+ error: `external_host_blocked: ${host} (set allowExternal: true to permit)`,
196
+ };
197
+ return JSON.stringify(result);
198
+ }
199
+ // Header + body normalisation. The default Accept header pushes
200
+ // servers that content-negotiate toward JSON so the response parse
201
+ // hits more often.
202
+ const defaultHeaders = {
203
+ accept: 'application/json,text/plain;q=0.9,*/*;q=0.5',
204
+ };
205
+ const merged = { ...defaultHeaders };
206
+ if (args.headers) {
207
+ for (const [k, v] of Object.entries(args.headers)) {
208
+ merged[k.toLowerCase()] = v;
209
+ }
210
+ }
211
+ let serialisedBody;
212
+ if (args.body !== undefined && args.body !== null) {
213
+ if (typeof args.body === 'string') {
214
+ serialisedBody = args.body;
215
+ }
216
+ else {
217
+ try {
218
+ serialisedBody = JSON.stringify(args.body);
219
+ }
220
+ catch (error) {
221
+ const result = {
222
+ ok: false,
223
+ status: 0,
224
+ headers: {},
225
+ body: '',
226
+ durationMs: 0,
227
+ error: `body_serialise_failed: ${error.message}`,
228
+ };
229
+ return JSON.stringify(result);
230
+ }
231
+ if (!('content-type' in merged)) {
232
+ merged['content-type'] = 'application/json';
233
+ }
234
+ }
235
+ }
236
+ const timeoutMs = clampTimeout(args.timeoutMs);
237
+ const start = now();
238
+ const ac = new AbortController();
239
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
240
+ try {
241
+ const init = {
242
+ method: args.method,
243
+ headers: merged,
244
+ signal: ac.signal,
245
+ // /triple-review P1 (Claude reviewer): default `fetch`
246
+ // follows redirects to ANY target. A loopback service can return
247
+ // 30x with Location pointing к cloud metadata IPs (169.254.169.254,
248
+ // 100.64/10 ranges) or arbitrary external hosts, and `fetch` would
249
+ // chase the redirect and surface the body. SSRF bypass class.
250
+ // `redirect: 'manual'` stops the chase; the dispatcher returns the
251
+ // 30x status + Location header to the model so it can decide
252
+ // whether the redirect target is acceptable.
253
+ redirect: 'manual',
254
+ ...(serialisedBody !== undefined ? { body: serialisedBody } : {}),
255
+ };
256
+ const res = await fetchImpl(args.url, init);
257
+ const text = await res.text();
258
+ const truncated = text.length > HTTP_REQUEST_BODY_CAP_BYTES;
259
+ const body = truncated ? text.slice(0, HTTP_REQUEST_BODY_CAP_BYTES) : text;
260
+ const respHeaders = headersToObject(res.headers);
261
+ const contentType = respHeaders['content-type'] ?? '';
262
+ let json;
263
+ if (contentType.includes('application/json') && body.length > 0 && !truncated) {
264
+ try {
265
+ json = JSON.parse(body);
266
+ }
267
+ catch {
268
+ // Body claimed JSON but failed to parse - preserve raw text only.
269
+ }
270
+ }
271
+ const result = {
272
+ ok: res.status >= 200 && res.status < 300,
273
+ status: res.status,
274
+ headers: respHeaders,
275
+ body,
276
+ ...(json !== undefined ? { json } : {}),
277
+ durationMs: now() - start,
278
+ ...(truncated ? { truncated: true } : {}),
279
+ };
280
+ return JSON.stringify(result);
281
+ }
282
+ catch (error) {
283
+ const message = error.message;
284
+ const aborted = error.name === 'AbortError';
285
+ const result = {
286
+ ok: false,
287
+ status: 0,
288
+ headers: {},
289
+ body: '',
290
+ durationMs: now() - start,
291
+ error: aborted ? `timeout_after_${timeoutMs}ms` : `request_failed: ${message}`,
292
+ };
293
+ return JSON.stringify(result);
294
+ }
295
+ finally {
296
+ clearTimeout(timer);
297
+ }
298
+ }
299
+ export const httpRequestJsonSchema = {
300
+ type: 'object',
301
+ additionalProperties: false,
302
+ required: ['method', 'url'],
303
+ properties: {
304
+ method: {
305
+ type: 'string',
306
+ enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
307
+ description: 'HTTP verb. Uppercase preferred but the dispatcher accepts any case.',
308
+ },
309
+ url: {
310
+ type: 'string',
311
+ description: 'Fully-qualified http(s) URL. Loopback hosts always pass; external hosts require allowExternal: true.',
312
+ },
313
+ body: {
314
+ description: 'Request body. A string is sent verbatim; an object/array is JSON-serialised and Content-Type defaults to application/json.',
315
+ oneOf: [
316
+ { type: 'string' },
317
+ { type: 'object', additionalProperties: true },
318
+ { type: 'array' },
319
+ ],
320
+ },
321
+ headers: {
322
+ type: 'object',
323
+ description: 'Custom request headers. Lower-cased keys overwrite the dispatcher defaults.',
324
+ additionalProperties: { type: 'string' },
325
+ },
326
+ timeoutMs: {
327
+ type: 'number',
328
+ description: `Per-request timeout in ms. Default ${HTTP_REQUEST_DEFAULT_TIMEOUT_MS}, max ${HTTP_REQUEST_MAX_TIMEOUT_MS}.`,
329
+ },
330
+ allowExternal: {
331
+ type: 'boolean',
332
+ description: 'Opt-in flag for non-loopback hosts. Defaults to false so the dispatcher refuses external URLs without explicit consent.',
333
+ },
334
+ },
335
+ };
336
+ //# sourceMappingURL=http-request.js.map