@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.87

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 (402) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/deploy.js +40 -40
  6. package/dist/commands/flatten.js +191 -0
  7. package/dist/commands/jobs-watch.js +201 -0
  8. package/dist/commands/jobs.js +42 -27
  9. package/dist/commands/smoke.js +133 -0
  10. package/dist/core/agent-progress/cleanup.js +134 -0
  11. package/dist/core/agent-progress/schema.js +144 -0
  12. package/dist/core/agent-progress/writer.js +101 -0
  13. package/dist/core/agents/adaptive-router.js +330 -0
  14. package/dist/core/agents/query-decomposer.js +297 -0
  15. package/dist/core/agents/registry.js +2 -2
  16. package/dist/core/approvals/shortcut-resolver.js +98 -0
  17. package/dist/core/artifact-chain/dispatcher.js +148 -0
  18. package/dist/core/artifact-chain/exporter.js +164 -0
  19. package/dist/core/artifact-chain/state.js +243 -0
  20. package/dist/core/artifact-chain/steps.js +169 -0
  21. package/dist/core/ask-user/question.js +92 -0
  22. package/dist/core/audit/audit-trail.js +275 -0
  23. package/dist/core/auth/ensure-authenticated.js +129 -0
  24. package/dist/core/auth/env-provider.js +238 -0
  25. package/dist/core/auto-open-browser.js +4 -4
  26. package/dist/core/auto-update/channels.js +122 -0
  27. package/dist/core/auto-update/checker.js +241 -0
  28. package/dist/core/auto-update/state.js +235 -0
  29. package/dist/core/bare-mode/index.js +107 -0
  30. package/dist/core/bash/redirect.js +281 -0
  31. package/dist/core/bash-classifier.js +436 -40
  32. package/dist/core/checkpoint/resumer.js +149 -0
  33. package/dist/core/checkpoint/rewinder.js +291 -0
  34. package/dist/core/checkpoints/shadow-git.js +670 -0
  35. package/dist/core/citations/parser.js +109 -0
  36. package/dist/core/classifier/yolo-classifier.js +88 -0
  37. package/dist/core/codegraph/decision-store.js +248 -0
  38. package/dist/core/codegraph/detect-repo.js +459 -0
  39. package/dist/core/codegraph/install.js +134 -0
  40. package/dist/core/codegraph/offer-hook.js +220 -0
  41. package/dist/core/compact/auto-trigger.js +96 -0
  42. package/dist/core/compact/buffer-rewriter.js +115 -0
  43. package/dist/core/compact/summarizer.js +208 -0
  44. package/dist/core/compact/token-counter.js +108 -0
  45. package/dist/core/consensus/anvil-fanout.js +25 -25
  46. package/dist/core/consensus/diff-capture.js +121 -12
  47. package/dist/core/consensus/rubric.js +21 -21
  48. package/dist/core/context/builder.js +6 -6
  49. package/dist/core/context/compaction-events.js +8 -8
  50. package/dist/core/context/compaction.js +31 -31
  51. package/dist/core/context/index.js +15 -8
  52. package/dist/core/context/invariants.js +51 -51
  53. package/dist/core/context/markdown-loader.js +28 -10
  54. package/dist/core/context/markdown-traverse.js +255 -0
  55. package/dist/core/context/pugiignore.js +41 -41
  56. package/dist/core/context/repo-skeleton.js +37 -37
  57. package/dist/core/context/tool-eviction.js +55 -0
  58. package/dist/core/context/watcher.js +32 -32
  59. package/dist/core/context/working-set.js +23 -23
  60. package/dist/core/coordinator/agent-tools.js +77 -0
  61. package/dist/core/coordinator/agent-toolset.js +65 -0
  62. package/dist/core/coordinator/fsm.js +73 -0
  63. package/dist/core/coordinator/mode-fsm.js +70 -0
  64. package/dist/core/cost/rate-card.js +129 -0
  65. package/dist/core/cost/tracker.js +221 -0
  66. package/dist/core/credentials.js +12 -12
  67. package/dist/core/cron/scheduler.js +138 -0
  68. package/dist/core/denial-tracking/index.js +8 -0
  69. package/dist/core/denial-tracking/state.js +264 -0
  70. package/dist/core/diagnostics/probe-runner.js +93 -0
  71. package/dist/core/diagnostics/probes/api.js +46 -0
  72. package/dist/core/diagnostics/probes/auth.js +93 -0
  73. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  74. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  75. package/dist/core/diagnostics/probes/config.js +72 -0
  76. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  77. package/dist/core/diagnostics/probes/disk.js +81 -0
  78. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  79. package/dist/core/diagnostics/probes/git.js +65 -0
  80. package/dist/core/diagnostics/probes/hooks.js +118 -0
  81. package/dist/core/diagnostics/probes/mcp.js +75 -0
  82. package/dist/core/diagnostics/probes/node.js +59 -0
  83. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  84. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  85. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  86. package/dist/core/diagnostics/probes/session.js +74 -0
  87. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  88. package/dist/core/diagnostics/probes/workspace.js +63 -0
  89. package/dist/core/diagnostics/types.js +70 -0
  90. package/dist/core/dispatch/cache-cleanup.js +197 -0
  91. package/dist/core/dispatch/cache-handoff.js +295 -0
  92. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  93. package/dist/core/edits/dispatch.js +293 -7
  94. package/dist/core/edits/format-matrix.js +26 -0
  95. package/dist/core/edits/fuzzy-ladder.js +650 -0
  96. package/dist/core/edits/index.js +3 -1
  97. package/dist/core/edits/journal.js +199 -0
  98. package/dist/core/edits/layer-a-apply.js +15 -15
  99. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  100. package/dist/core/edits/layer-b-apply.js +9 -9
  101. package/dist/core/edits/layer-c-apply.js +6 -6
  102. package/dist/core/edits/layer-d-ast.js +557 -14
  103. package/dist/core/edits/marker-parser.js +12 -12
  104. package/dist/core/edits/security-gate.js +27 -27
  105. package/dist/core/edits/verify-hook.js +273 -0
  106. package/dist/core/edits/worktree.js +322 -0
  107. package/dist/core/engine/anvil-client.js +140 -26
  108. package/dist/core/engine/auto-compact.js +179 -0
  109. package/dist/core/engine/budgets.js +186 -0
  110. package/dist/core/engine/context-prefix.js +155 -0
  111. package/dist/core/engine/index.js +1 -1
  112. package/dist/core/engine/intensity.js +158 -0
  113. package/dist/core/engine/intent.js +260 -0
  114. package/dist/core/engine/native-pugi.js +1295 -227
  115. package/dist/core/engine/prompts.js +134 -16
  116. package/dist/core/engine/strip-internal-fields.js +124 -0
  117. package/dist/core/engine/tool-bridge.js +1295 -59
  118. package/dist/core/evaluation/golden-dataset.js +293 -0
  119. package/dist/core/feedback/queue.js +177 -0
  120. package/dist/core/feedback/submitter.js +145 -0
  121. package/dist/core/file-cache.js +113 -1
  122. package/dist/core/flatten/flatten-repo.js +439 -0
  123. package/dist/core/format/osc8-link.js +28 -0
  124. package/dist/core/hook-chains.js +392 -0
  125. package/dist/core/hooks/citation-verify-hook.js +138 -0
  126. package/dist/core/hooks/citation-verify.js +112 -0
  127. package/dist/core/hooks/events.js +44 -0
  128. package/dist/core/hooks/index.js +15 -0
  129. package/dist/core/hooks/registry.js +213 -0
  130. package/dist/core/hooks/runner.js +236 -0
  131. package/dist/core/hooks/v2/event-emitter.js +115 -0
  132. package/dist/core/hooks/v2/executor.js +282 -0
  133. package/dist/core/hooks/v2/index.js +25 -0
  134. package/dist/core/hooks/v2/lifecycle.js +104 -0
  135. package/dist/core/hooks/v2/loader.js +216 -0
  136. package/dist/core/hooks/v2/matcher.js +125 -0
  137. package/dist/core/hooks/v2/trust.js +143 -0
  138. package/dist/core/hooks/v2/types.js +86 -0
  139. package/dist/core/image/renderer.js +71 -0
  140. package/dist/core/init/detector.js +582 -0
  141. package/dist/core/init/template-renderer.js +242 -0
  142. package/dist/core/jobs/registry.js +18 -18
  143. package/dist/core/ledger/results-tsv.js +142 -0
  144. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  145. package/dist/core/lsp/cache.js +105 -0
  146. package/dist/core/lsp/client.js +776 -0
  147. package/dist/core/lsp/language-detect.js +66 -0
  148. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  149. package/dist/core/lsp/symbol-tools.js +372 -0
  150. package/dist/core/mcp/client.js +97 -28
  151. package/dist/core/mcp/http-server.js +553 -0
  152. package/dist/core/mcp/orchestrator-tools.js +662 -0
  153. package/dist/core/mcp/permission.js +190 -0
  154. package/dist/core/mcp/registry.js +39 -17
  155. package/dist/core/mcp/server-tools.js +219 -0
  156. package/dist/core/mcp/server.js +397 -0
  157. package/dist/core/mcp/trust.js +10 -10
  158. package/dist/core/memory/dual-write.js +416 -0
  159. package/dist/core/memory/passive-extract.js +130 -0
  160. package/dist/core/memory/phase1-kinds.js +20 -0
  161. package/dist/core/memory/secret-scanner.js +304 -0
  162. package/dist/core/memory-sync/queue.js +170 -0
  163. package/dist/core/metrics/extract.js +113 -0
  164. package/dist/core/modes/roo-modes.js +68 -0
  165. package/dist/core/onboarding/ensure-initialized.js +133 -0
  166. package/dist/core/onboarding/marker.js +111 -0
  167. package/dist/core/onboarding/telemetry-state.js +108 -0
  168. package/dist/core/output-style/presets.js +176 -0
  169. package/dist/core/output-style/state.js +185 -0
  170. package/dist/core/path-security.js +287 -5
  171. package/dist/core/permission.js +82 -22
  172. package/dist/core/permissions/auto-classifier.js +124 -0
  173. package/dist/core/permissions/bash-parser.js +371 -0
  174. package/dist/core/permissions/circuit-breaker.js +83 -0
  175. package/dist/core/permissions/constrained-edit.js +91 -0
  176. package/dist/core/permissions/gate.js +278 -0
  177. package/dist/core/permissions/index.js +20 -0
  178. package/dist/core/permissions/mode.js +174 -0
  179. package/dist/core/permissions/network-egress.js +137 -0
  180. package/dist/core/permissions/state.js +241 -0
  181. package/dist/core/permissions/tool-class.js +93 -0
  182. package/dist/core/plan-mode/ui-state.js +51 -0
  183. package/dist/core/plans/plan-artifact.js +721 -0
  184. package/dist/core/policy-limits/etag-store.js +122 -0
  185. package/dist/core/prd-check/parser.js +215 -0
  186. package/dist/core/prd-check/reporter.js +127 -0
  187. package/dist/core/prd-check/session-review.js +557 -0
  188. package/dist/core/prd-check/verifiers.js +223 -0
  189. package/dist/core/prompt-cache/client-cache.js +99 -0
  190. package/dist/core/prompts/assembly.js +29 -0
  191. package/dist/core/prompts/registry.js +364 -0
  192. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  193. package/dist/core/pugi-md/context-injector.js +76 -0
  194. package/dist/core/pugi-md/walk-up.js +207 -0
  195. package/dist/core/python/uv-installer.js +270 -0
  196. package/dist/core/python/uv-resolver.js +83 -0
  197. package/dist/core/rate-limit/narrator.js +146 -0
  198. package/dist/core/recipes/cli-types.js +20 -0
  199. package/dist/core/recipes/loader.js +103 -0
  200. package/dist/core/recipes/runner.js +345 -0
  201. package/dist/core/recipes/schema.js +587 -0
  202. package/dist/core/release-notes/parser.js +241 -0
  203. package/dist/core/release-notes/state.js +116 -0
  204. package/dist/core/repl/ask.js +37 -37
  205. package/dist/core/repl/cancellation.js +26 -26
  206. package/dist/core/repl/cap-warning.js +4 -4
  207. package/dist/core/repl/clipboard-read.js +11 -11
  208. package/dist/core/repl/dispatch-fsm.js +12 -12
  209. package/dist/core/repl/history-search.js +15 -15
  210. package/dist/core/repl/history.js +28 -18
  211. package/dist/core/repl/kill-ring.js +5 -5
  212. package/dist/core/repl/model-pricing.js +135 -0
  213. package/dist/core/repl/privacy-banner.js +22 -22
  214. package/dist/core/repl/session.js +2157 -214
  215. package/dist/core/repl/slash-commands.js +533 -40
  216. package/dist/core/repl/store/index.js +1 -1
  217. package/dist/core/repl/store/jsonl-log.js +22 -22
  218. package/dist/core/repl/store/lockfile.js +10 -10
  219. package/dist/core/repl/store/session-store.js +136 -107
  220. package/dist/core/repl/store/types.js +15 -15
  221. package/dist/core/repl/store/uuid-v7.js +12 -12
  222. package/dist/core/repl/workspace-context.js +43 -21
  223. package/dist/core/repo-map/build.js +125 -0
  224. package/dist/core/repo-map/cache.js +185 -0
  225. package/dist/core/repo-map/extractor.js +254 -0
  226. package/dist/core/repo-map/formatter.js +145 -0
  227. package/dist/core/repo-map/page-rank.js +105 -0
  228. package/dist/core/repo-map/scanner.js +211 -0
  229. package/dist/core/retry-budget/budget.js +284 -0
  230. package/dist/core/retry-budget/index.js +5 -0
  231. package/dist/core/retry-budget/retry-cap.js +74 -0
  232. package/dist/core/routing/lead-worker.js +43 -0
  233. package/dist/core/routing/pre-flight-estimator.js +108 -0
  234. package/dist/core/runs/run-tree.js +103 -0
  235. package/dist/core/security/injection-scanner.js +367 -0
  236. package/dist/core/security/output-filter.js +418 -0
  237. package/dist/core/session/env-file.js +105 -0
  238. package/dist/core/session/section-budgets.js +140 -0
  239. package/dist/core/session.js +92 -0
  240. package/dist/core/settings.js +286 -5
  241. package/dist/core/share/formatter.js +271 -0
  242. package/dist/core/share/redactor.js +221 -0
  243. package/dist/core/share/uploader.js +267 -0
  244. package/dist/core/skills/defaults.js +457 -0
  245. package/dist/core/skills/loader.js +22 -22
  246. package/dist/core/skills/sources.js +27 -27
  247. package/dist/core/smoke/headless-driver.js +174 -0
  248. package/dist/core/smoke/orchestrator.js +194 -0
  249. package/dist/core/smoke/runner.js +238 -0
  250. package/dist/core/smoke/scenario-parser.js +316 -0
  251. package/dist/core/statusline.js +99 -0
  252. package/dist/core/subagents/dispatcher-real.js +600 -0
  253. package/dist/core/subagents/dispatcher.js +132 -43
  254. package/dist/core/subagents/index.js +19 -6
  255. package/dist/core/subagents/isolation-matrix.js +213 -0
  256. package/dist/core/subagents/spawn.js +19 -4
  257. package/dist/core/telemetry/emitter.js +229 -0
  258. package/dist/core/telemetry/queue.js +251 -0
  259. package/dist/core/theme/context.js +91 -0
  260. package/dist/core/theme/presets.js +228 -0
  261. package/dist/core/theme/state.js +181 -0
  262. package/dist/core/todos/invariant.js +10 -0
  263. package/dist/core/todos/state.js +177 -0
  264. package/dist/core/tool-schema/compressor.js +89 -0
  265. package/dist/core/transport/version-interceptor.js +166 -0
  266. package/dist/core/trust.js +2 -2
  267. package/dist/core/tui/thinking-block.js +64 -0
  268. package/dist/core/vim/keymap.js +288 -0
  269. package/dist/core/vim/state.js +92 -0
  270. package/dist/core/watch-markers/marker-watcher.js +133 -0
  271. package/dist/core/worktree-manager/cleanup.js +123 -0
  272. package/dist/core/worktree-manager/manager.js +303 -0
  273. package/dist/index.js +28 -0
  274. package/dist/runtime/bootstrap.js +190 -0
  275. package/dist/runtime/cli.js +4162 -488
  276. package/dist/runtime/commands/agents.js +30 -30
  277. package/dist/runtime/commands/budget.js +5 -5
  278. package/dist/runtime/commands/cancel.js +231 -0
  279. package/dist/runtime/commands/chain.js +489 -0
  280. package/dist/runtime/commands/codegraph-status.js +227 -0
  281. package/dist/runtime/commands/compact.js +297 -0
  282. package/dist/runtime/commands/config.js +32 -32
  283. package/dist/runtime/commands/cost.js +199 -0
  284. package/dist/runtime/commands/delegate.js +244 -13
  285. package/dist/runtime/commands/dispatch.js +126 -0
  286. package/dist/runtime/commands/doctor.js +579 -0
  287. package/dist/runtime/commands/feedback.js +184 -0
  288. package/dist/runtime/commands/hooks.js +184 -0
  289. package/dist/runtime/commands/init.js +254 -0
  290. package/dist/runtime/commands/lsp.js +368 -0
  291. package/dist/runtime/commands/mcp.js +879 -0
  292. package/dist/runtime/commands/memory.js +582 -0
  293. package/dist/runtime/commands/model.js +237 -0
  294. package/dist/runtime/commands/onboarding.js +275 -0
  295. package/dist/runtime/commands/patch.js +128 -0
  296. package/dist/runtime/commands/permissions.js +112 -0
  297. package/dist/runtime/commands/plan.js +143 -0
  298. package/dist/runtime/commands/prd-check.js +285 -0
  299. package/dist/runtime/commands/privacy.js +17 -17
  300. package/dist/runtime/commands/recipe.js +325 -0
  301. package/dist/runtime/commands/redo-blob-store.js +92 -0
  302. package/dist/runtime/commands/redo.js +361 -0
  303. package/dist/runtime/commands/release-notes.js +229 -0
  304. package/dist/runtime/commands/repo-map.js +95 -0
  305. package/dist/runtime/commands/report.js +299 -0
  306. package/dist/runtime/commands/resume.js +118 -0
  307. package/dist/runtime/commands/review-consensus.js +68 -53
  308. package/dist/runtime/commands/rewind.js +333 -0
  309. package/dist/runtime/commands/roster.js +14 -14
  310. package/dist/runtime/commands/sessions.js +163 -0
  311. package/dist/runtime/commands/share.js +316 -0
  312. package/dist/runtime/commands/skills.js +31 -31
  313. package/dist/runtime/commands/status.js +186 -0
  314. package/dist/runtime/commands/stickers.js +82 -0
  315. package/dist/runtime/commands/style.js +194 -0
  316. package/dist/runtime/commands/theme.js +196 -0
  317. package/dist/runtime/commands/undo.js +54 -22
  318. package/dist/runtime/commands/update.js +289 -0
  319. package/dist/runtime/commands/vim.js +140 -0
  320. package/dist/runtime/commands/worktree.js +177 -0
  321. package/dist/runtime/commands/worktrees.js +155 -0
  322. package/dist/runtime/headless-repl.js +195 -0
  323. package/dist/runtime/headless.js +543 -0
  324. package/dist/runtime/load-hooks-or-exit.js +71 -0
  325. package/dist/runtime/plan-decompose.js +531 -0
  326. package/dist/runtime/update-check.js +28 -28
  327. package/dist/runtime/version.js +65 -0
  328. package/dist/skills/bundled/batch.js +617 -0
  329. package/dist/skills/bundled/index.js +45 -0
  330. package/dist/skills/bundled/loop.js +358 -0
  331. package/dist/skills/bundled/remember.js +383 -0
  332. package/dist/skills/bundled/simplify.js +289 -0
  333. package/dist/skills/bundled/skillify.js +373 -0
  334. package/dist/skills/bundled/stuck.js +558 -0
  335. package/dist/skills/bundled/verify.js +439 -0
  336. package/dist/testing/vcr.js +486 -0
  337. package/dist/tools/agent-tool.js +229 -0
  338. package/dist/tools/apply-patch.js +556 -0
  339. package/dist/tools/ask-user-question.js +222 -0
  340. package/dist/tools/ask-user.js +115 -0
  341. package/dist/tools/bash.js +623 -45
  342. package/dist/tools/brief.js +224 -0
  343. package/dist/tools/enter-worktree.js +250 -0
  344. package/dist/tools/exit-worktree.js +147 -0
  345. package/dist/tools/file-tools.js +161 -44
  346. package/dist/tools/lsp-tools.js +189 -0
  347. package/dist/tools/mcp-tool.js +260 -0
  348. package/dist/tools/multi-edit.js +361 -0
  349. package/dist/tools/powershell.js +268 -0
  350. package/dist/tools/registry.js +85 -0
  351. package/dist/tools/skill-tool.js +96 -0
  352. package/dist/tools/sleep.js +99 -0
  353. package/dist/tools/synthetic-output.js +133 -0
  354. package/dist/tools/tasks.js +208 -0
  355. package/dist/tools/todo-write.js +184 -0
  356. package/dist/tools/verify-plan-execution.js +295 -0
  357. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  358. package/dist/tools/web-fetch.js +195 -10
  359. package/dist/tools/web-search.js +458 -0
  360. package/dist/tui/agent-progress-card.js +111 -0
  361. package/dist/tui/agent-tree.js +11 -1
  362. package/dist/tui/ask-modal.js +14 -14
  363. package/dist/tui/ask-user-question-prompt.js +203 -0
  364. package/dist/tui/compact-banner.js +81 -0
  365. package/dist/tui/conversation-pane.js +85 -11
  366. package/dist/tui/cost-table.js +111 -0
  367. package/dist/tui/device-flow.js +2 -2
  368. package/dist/tui/doctor-table.js +46 -0
  369. package/dist/tui/feedback-prompt.js +156 -0
  370. package/dist/tui/input-box.js +247 -32
  371. package/dist/tui/login-picker.js +3 -3
  372. package/dist/tui/markdown-render.js +6 -6
  373. package/dist/tui/onboarding-wizard.js +240 -0
  374. package/dist/tui/permissions-picker.js +86 -0
  375. package/dist/tui/render.js +35 -0
  376. package/dist/tui/repl-render.js +332 -54
  377. package/dist/tui/repl-splash-art.js +16 -16
  378. package/dist/tui/repl-splash-mascot.js +48 -24
  379. package/dist/tui/repl-splash.js +22 -22
  380. package/dist/tui/repl.js +124 -44
  381. package/dist/tui/slash-palette.js +6 -6
  382. package/dist/tui/splash.js +2 -2
  383. package/dist/tui/status-bar.js +109 -31
  384. package/dist/tui/status-table.js +7 -0
  385. package/dist/tui/stickers-art.js +136 -0
  386. package/dist/tui/style-table.js +28 -0
  387. package/dist/tui/theme-table.js +29 -0
  388. package/dist/tui/thinking-spinner.js +123 -0
  389. package/dist/tui/tool-stream-pane.js +53 -4
  390. package/dist/tui/update-banner.js +27 -2
  391. package/dist/tui/vim-input.js +267 -0
  392. package/dist/tui/welcome-banner.js +107 -0
  393. package/dist/tui/welcome-data.js +293 -0
  394. package/dist/tui/workspace-context.js +2 -2
  395. package/docs/examples/codegraph.mcp.json +10 -0
  396. package/package.json +23 -6
  397. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  398. package/test/scenarios/compact-force.scenario.txt +11 -0
  399. package/test/scenarios/identity.scenario.txt +11 -0
  400. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  401. package/test/scenarios/walkback.scenario.txt +12 -0
  402. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Tenant-wide JSONL audit trail .
3
+ *
4
+ * Pugi already records every tool_call / tool_result in two places:
5
+ *
6
+ * 1. The global per-workspace log at `<workspace>/.pugi/events.jsonl`
7
+ * (audit-replay source of truth; see `core/session.ts`).
8
+ * 2. The per-session mirror at
9
+ * `<workspace>/.pugi/sessions/<sessionId>/events.jsonl`
10
+ * (operator-friendly per-run copy, see `native-pugi.ts`).
11
+ *
12
+ * Both live under the workspace directory and disappear when the
13
+ * operator wipes the workspace or runs many ephemeral sandboxes.
14
+ * What's missing is a TENANT-wide structured audit log: a single
15
+ * append-only NDJSON stream per (tenant, workspace) pair that the
16
+ * operator (or a SOC pipeline) can tail across every session over
17
+ * the lifetime of the host.
18
+ *
19
+ * Spec :
20
+ *
21
+ * - Path: `~/.pugi/audit/<tenant>/<workspace-slug>-<hash>.jsonl`
22
+ * - One JSON line per event with shared shape:
23
+ * `{ ts, tenant, workspace, workspaceHash, event, sessionId, data }`
24
+ * - Events covered: `tool_call`, `tool_result`, `dispatch_start`,
25
+ * `dispatch_end`, `permission_denied`, `auto_compact`,
26
+ * `budget_exhausted`.
27
+ * - Append-only — no rotation logic. Operators wire `logrotate`
28
+ * themselves if they want size caps.
29
+ * - Opt-out: `PUGI_AUDIT_TRAIL_DISABLE=1`.
30
+ * - Failures NEVER throw. Audit MUST NOT break a dispatch.
31
+ * - Tenant fallback: when `PUGI_API_KEY` is unset, tenant is `local`.
32
+ *
33
+ * Why duplicate the per-session log on disk:
34
+ *
35
+ * The per-session mirror clusters by `sessionId` (one dir per run).
36
+ * To answer "what did this tenant DO across every session this week
37
+ * from this workspace" an operator otherwise has to glob hundreds of
38
+ * session dirs and merge by timestamp. The audit trail flattens that
39
+ * into one tail-able stream per (tenant, workspace) — same shape an
40
+ * ops pipeline would expect from a hosted log surface.
41
+ */
42
+ import { appendFileSync, mkdirSync } from 'node:fs';
43
+ import { createHash } from 'node:crypto';
44
+ import { homedir } from 'node:os';
45
+ import { basename, dirname, join, resolve } from 'node:path';
46
+ import { collectStrings, scanForInjection, summarizeFindings, } from '../security/injection-scanner.js';
47
+ /**
48
+ * Opt-out env var. Mirrors the convention every other Pugi feature uses
49
+ * (`PUGI_BARE`, `PUGI_AGENTMEMORY_RECALL_ENABLED=false`, etc.).
50
+ * Operators set this when they pipe the CLI through a sandbox that
51
+ * already captures audit upstream and they want to skip the duplicate.
52
+ */
53
+ export const PUGI_AUDIT_TRAIL_DISABLE_VAR = 'PUGI_AUDIT_TRAIL_DISABLE';
54
+ /**
55
+ * Tenant fallback used when the operator has not exported
56
+ * `PUGI_API_KEY`. The audit trail still flows — it just lives under
57
+ * `~/.pugi/audit/local/...` so a single-user workstation gets a useful
58
+ * forensic log without needing API-key plumbing.
59
+ */
60
+ export const LOCAL_TENANT_FALLBACK = 'local';
61
+ /**
62
+ * Sanitize the workspace basename to a safe filesystem slug:
63
+ * lowercase a-z + 0-9 + `-`. Anything else collapses to `-`. We avoid
64
+ * the empty case (root workspace) by falling back to `workspace`.
65
+ *
66
+ * Why not a hash here too: the hash is appended separately so two
67
+ * workspaces with the same basename (e.g. two clones of the same repo
68
+ * sitting in different parent dirs) get distinct files. The slug is
69
+ * the human-readable half operators eyeball at `ls ~/.pugi/audit/...`.
70
+ */
71
+ export function sanitizeWorkspaceSlug(workspaceRoot) {
72
+ const base = basename(resolve(workspaceRoot));
73
+ const sanitized = base
74
+ .toLowerCase()
75
+ .replace(/[^a-z0-9-]+/g, '-')
76
+ .replace(/-+/g, '-')
77
+ .replace(/^-|-$/g, '');
78
+ return sanitized.length > 0 ? sanitized : 'workspace';
79
+ }
80
+ /**
81
+ * Stable, anonymous workspace handle. We use the FIRST 8 hex of
82
+ * sha256(workspaceRoot). 8 hex = 32 bits = ~4 billion buckets, more
83
+ * than enough to disambiguate `~/code/foo` from `~/other/foo` on the
84
+ * same host without leaking the absolute path through the file name.
85
+ *
86
+ * The hash is over the RESOLVED path so symlink trickery cannot point
87
+ * two different audit streams at the same file by accident.
88
+ */
89
+ export function computeWorkspaceHash(workspaceRoot) {
90
+ return createHash('sha256')
91
+ .update(resolve(workspaceRoot))
92
+ .digest('hex')
93
+ .slice(0, 8);
94
+ }
95
+ /**
96
+ * Derive the tenant slug from `PUGI_API_KEY`. We hash the key (sha256,
97
+ * 12 hex prefix) rather than emitting the raw key — the audit trail is
98
+ * a plaintext file on the local FS and the tenant slug shows up in
99
+ * every path under `~/.pugi/audit/`. A truncated hash is enough to
100
+ * cluster every (tenant, workspace) over time without leaking the key
101
+ * if the operator accidentally `tar`s their `~/.pugi` for support.
102
+ *
103
+ * The hash is purely a CLI-local clustering key — the runtime backend
104
+ * has its own (different) tenant identifier and never sees this.
105
+ */
106
+ export function resolveTenant(env = process.env) {
107
+ const key = env.PUGI_API_KEY?.trim();
108
+ if (!key)
109
+ return LOCAL_TENANT_FALLBACK;
110
+ // 12 hex = 48 bits — enough disambiguation for any realistic per-host
111
+ // tenant cardinality; still short enough for operators to eyeball at
112
+ // `ls ~/.pugi/audit/`.
113
+ return createHash('sha256').update(key).digest('hex').slice(0, 12);
114
+ }
115
+ /**
116
+ * Resolve the audit file path for a given (tenant, workspace) pair.
117
+ * Pure path arithmetic — the caller is responsible for `mkdir -p`
118
+ * before append (handled inside `writeAuditEvent`).
119
+ */
120
+ export function resolveAuditPath(workspaceRoot, tenant, home = homedir()) {
121
+ const slug = sanitizeWorkspaceSlug(workspaceRoot);
122
+ const hash = computeWorkspaceHash(workspaceRoot);
123
+ return join(home, '.pugi', 'audit', tenant, `${slug}-${hash}.jsonl`);
124
+ }
125
+ /**
126
+ * Predicate: is the audit trail disabled via env opt-out?
127
+ *
128
+ * Accept `1`, `true`, `yes` (case-insensitive) as positive; anything
129
+ * else — including `0`, `false`, `''`, and the var being absent — keeps
130
+ * the trail enabled. Mirrors the convention used in `bare-mode/` and
131
+ * elsewhere in the CLI.
132
+ */
133
+ export function isAuditDisabled(env = process.env) {
134
+ const raw = env[PUGI_AUDIT_TRAIL_DISABLE_VAR]?.trim().toLowerCase();
135
+ if (!raw)
136
+ return false;
137
+ return raw === '1' || raw === 'true' || raw === 'yes';
138
+ }
139
+ /**
140
+ * Append a single audit event to the per-tenant per-workspace NDJSON
141
+ * trail. Never throws — failures (FS unwritable, opt-out, malformed
142
+ * input) are silently swallowed so a misconfigured audit surface
143
+ * cannot break a dispatch. The engine adapter's existing per-session
144
+ * mirror remains intact as a redundant copy.
145
+ *
146
+ * Append-only: every call writes exactly one line. No rotation, no
147
+ * truncation. Operators wire `logrotate` if they want size caps.
148
+ *
149
+ * macOS hardening: we `mkdir -p` the parent dir on every call (cheap
150
+ * in practice — Node short-circuits when the dir exists) so a manual
151
+ * `rm -rf ~/.pugi/audit/<tenant>/` between runs does not turn the next
152
+ * append into ENOENT. The mode is `0o700` for the tenant dir and
153
+ * `0o600` for the JSONL file so curious users on a shared host cannot
154
+ * read another tenant's trail.
155
+ */
156
+ export function writeAuditEvent(input) {
157
+ const env = input.env ?? process.env;
158
+ if (isAuditDisabled(env))
159
+ return;
160
+ try {
161
+ const tenant = (input.tenant?.trim() || resolveTenant(env)) || LOCAL_TENANT_FALLBACK;
162
+ const home = input.home ?? homedir();
163
+ const path = resolveAuditPath(input.workspaceRoot, tenant, home);
164
+ const now = input.now ? input.now() : new Date().toISOString();
165
+ const envelope = {
166
+ ts: now,
167
+ tenant,
168
+ workspace: sanitizeWorkspaceSlug(input.workspaceRoot),
169
+ workspaceHash: computeWorkspaceHash(input.workspaceRoot),
170
+ event: input.event,
171
+ sessionId: input.sessionId,
172
+ data: input.data,
173
+ };
174
+ try {
175
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
176
+ }
177
+ catch {
178
+ // mkdir failure is silent — the appendFileSync below will surface
179
+ // the real error and the outer catch swallows it. We still try
180
+ // the write so EEXIST on the dir (the only real path here) does
181
+ // not block the append.
182
+ }
183
+ appendFileSync(path, `${JSON.stringify(envelope)}\n`, {
184
+ encoding: 'utf8',
185
+ mode: 0o600,
186
+ });
187
+ // Injection scan (ported an external utility,
188
+ // Apache-2.0). Wrap the OUTBOUND `data` payload through the
189
+ // scanner. Findings emit a SECOND audit line of type
190
+ // `injection_detected` so an operator (or SOC pipeline) sees a
191
+ // structured, append-only record without losing the original
192
+ // event. Never blocks the write — hard-block requires a separate
193
+ // CEO-signed PR.
194
+ //
195
+ // Recursion guard: the `injection_detected` event itself carries
196
+ // matched substrings (intentional — they are the evidence). We
197
+ // skip scanning it to avoid an infinite loop of self-detections.
198
+ if (input.event !== 'injection_detected') {
199
+ const findings = scanAuditPayload(input.data);
200
+ if (findings.length > 0) {
201
+ emitInjectionDetected({
202
+ findings,
203
+ triggeringEvent: input.event,
204
+ sessionId: input.sessionId,
205
+ workspaceRoot: input.workspaceRoot,
206
+ tenant: input.tenant,
207
+ env: input.env,
208
+ home: input.home,
209
+ now: input.now,
210
+ });
211
+ }
212
+ }
213
+ }
214
+ catch {
215
+ // Audit failures must NEVER break a dispatch. The session log + the
216
+ // per-session mirror under `<workspace>/.pugi/` remain as redundant
217
+ // surfaces. A future telemetry pass can surface the failure count
218
+ // via the doctor probe; for now silent no-op is the contract.
219
+ }
220
+ }
221
+ /**
222
+ * Fold the audit `data` payload into a single string and scan it for
223
+ * prompt-injection / invisible-unicode / secret markers. Returns the
224
+ * empty array on clean payloads.
225
+ *
226
+ * Exported for the spec — the scanner module owns the algorithm, this
227
+ * helper owns the payload-walking glue.
228
+ */
229
+ export function scanAuditPayload(data) {
230
+ // Fold every string anywhere in the payload (keys included) into a
231
+ // single buffer separated by NULs. NUL keeps regex anchors honest
232
+ // (no accidental cross-field match for a `^system:` pattern) without
233
+ // adding bytes that themselves could become a pattern.
234
+ const fragments = collectStrings(data);
235
+ if (fragments.length === 0)
236
+ return [];
237
+ const joined = fragments.join('\0');
238
+ return scanForInjection(joined);
239
+ }
240
+ /**
241
+ * Build the `injection_detected` envelope payload and recurse into
242
+ * `writeAuditEvent` to append it. The recursion is bounded — the
243
+ * recursion guard in `writeAuditEvent` short-circuits on the
244
+ * `injection_detected` event so we never re-scan ourselves.
245
+ */
246
+ function emitInjectionDetected(input) {
247
+ const summary = summarizeFindings(input.findings);
248
+ // Cap the findings array in the audit line so a payload with
249
+ // hundreds of invisible-unicode hits does not bloat the JSONL row.
250
+ // The summary still carries `total` so operators see the real count.
251
+ const MAX_FINDINGS_PER_EVENT = 32;
252
+ const truncated = input.findings.length > MAX_FINDINGS_PER_EVENT;
253
+ const capped = truncated
254
+ ? input.findings.slice(0, MAX_FINDINGS_PER_EVENT)
255
+ : [...input.findings];
256
+ writeAuditEvent({
257
+ event: 'injection_detected',
258
+ sessionId: input.sessionId,
259
+ workspaceRoot: input.workspaceRoot,
260
+ tenant: input.tenant,
261
+ env: input.env,
262
+ home: input.home,
263
+ now: input.now,
264
+ data: {
265
+ triggeringEvent: input.triggeringEvent,
266
+ summary,
267
+ findings: capped,
268
+ truncated,
269
+ // External attribution is recorded inline so a SOC pipeline
270
+ // grepping for the upstream project name lands here.
271
+ detector: 'external-injection-patterns',
272
+ },
273
+ });
274
+ }
275
+ //# sourceMappingURL=audit-trail.js.map
@@ -0,0 +1,129 @@
1
+ /**
2
+ * UX — `ensureAuthenticated` helper.
3
+ *
4
+ * Auto-login pre-flight for every Pugi command that needs an Anvil
5
+ * credential. Before this helper landed, cold-start without a stored
6
+ * credential surfaced a generic "Login required" message and the
7
+ * operator had к run `pugi login` separately, which broke the muscle-
8
+ * memory of "open terminal, type the command, see the answer".
9
+ *
10
+ * The helper exposes a single contract: `ensureAuthenticated(opts)`.
11
+ * On a happy path (credential resolves) it returns the credential.
12
+ * On a cold-start it either:
13
+ *
14
+ * - launches the device-flow login inline (interactive TTY only,
15
+ * `--no-login` not set), waits for completion, then re-resolves
16
+ * the credential and returns it. The surrounding command continues
17
+ * transparently;
18
+ * - returns `{ status: 'missing' }` with a reason describing why no
19
+ * auto-login was attempted (non-interactive, opted-out, or login
20
+ * aborted by user). The caller bails with a clean message.
21
+ *
22
+ * Cross-command parity: the helper is wired into every command that
23
+ * needs auth (engine commands `code`/`fix`/`build`/`explain`/`plan`,
24
+ * plus `sync`, `chain new`, `smoke`, `review`, `deploy`, ...). The
25
+ * previous patchwork of `resolveActiveCredential() ?? throw` /
26
+ * `if (!config) writeOutput unauthenticated` calls now all funnel
27
+ * through here so future auth changes are one-edit.
28
+ *
29
+ * Session cache: the helper caches the resolved credential per-process.
30
+ * A second command in the same process never re-launches login even if
31
+ * the operator deletes credentials.json mid-run (that is a footgun, not
32
+ * a supported use case — the cached credential is still valid because
33
+ * the auth token in memory has not been revoked).
34
+ *
35
+ * Framework-free: the actual login call is injected via the `login`
36
+ * callback. The CLI passes a closure that calls
37
+ * `performDeviceFlowLogin` (or the interactive picker for token /
38
+ * env). The spec passes a fake that flips an in-memory env var so
39
+ * subsequent `resolveActiveCredential` calls see a credential.
40
+ */
41
+ /**
42
+ * Process-local cache of resolved credentials. Keyed by `apiUrl` so a
43
+ * future `pugi accounts switch` invocation does not return stale data
44
+ * (different apiUrl → cache miss). Cache is additive-only.
45
+ */
46
+ const credentialCache = new Map();
47
+ /**
48
+ * Reset the cache. Exported for spec teardown — production callers
49
+ * never need this.
50
+ */
51
+ export function resetAuthenticatedCache() {
52
+ credentialCache.clear();
53
+ }
54
+ /**
55
+ * Auth pre-flight. Returns the resolved credential or a structured
56
+ * `missing` envelope. The cached path skips the `resolve()` callback
57
+ * entirely — useful when `resolveActiveCredential` is expensive
58
+ * (filesystem read of ~/.pugi/credentials.json + Zod parse).
59
+ *
60
+ * Headless contract: even on a TTY, when `headless === true` the
61
+ * helper bails with `non_interactive`. Reason: a browser-popup login
62
+ * in the middle of an automated stdin → engine → stdout loop would
63
+ * silently freeze the run.
64
+ */
65
+ export async function ensureAuthenticated(opts) {
66
+ // Resolve once. Cache by the resolved apiUrl so a subsequent call
67
+ // after `pugi accounts switch` produces a fresh resolution.
68
+ const initial = opts.resolve();
69
+ if (initial) {
70
+ credentialCache.set(initial.apiUrl, initial);
71
+ return { status: 'ready', credential: initial };
72
+ }
73
+ if (opts.skip) {
74
+ return {
75
+ status: 'missing',
76
+ reason: 'disabled',
77
+ detail: 'Authentication skipped (--no-login or PUGI_NO_AUTO_LOGIN). Run `pugi login` to authenticate.',
78
+ };
79
+ }
80
+ if (opts.headless) {
81
+ return {
82
+ status: 'missing',
83
+ reason: 'non_interactive',
84
+ detail: 'Headless mode cannot launch browser-popup login. Run `pugi login` once with a TTY, then re-run with --headless.',
85
+ };
86
+ }
87
+ if (!opts.interactive) {
88
+ return {
89
+ status: 'missing',
90
+ reason: 'non_interactive',
91
+ detail: 'No credential found and stdin is not a TTY. Run `pugi login` with a TTY OR set PUGI_API_KEY before invoking Pugi in CI.',
92
+ };
93
+ }
94
+ const write = opts.write ?? ((line) => process.stderr.write(line));
95
+ write('No Pugi credential found. Launching login...\n');
96
+ let succeeded;
97
+ try {
98
+ succeeded = await opts.login();
99
+ }
100
+ catch (error) {
101
+ return {
102
+ status: 'missing',
103
+ reason: 'login_failed',
104
+ detail: `Login failed: ${error.message ?? String(error)}`,
105
+ };
106
+ }
107
+ if (!succeeded) {
108
+ return {
109
+ status: 'missing',
110
+ reason: 'login_cancelled',
111
+ detail: 'Authentication required to continue. Run `pugi login` when ready.',
112
+ };
113
+ }
114
+ // Re-resolve. If a successful login was reported but no credential
115
+ // landed on disk, surface that as `login_failed` rather than a
116
+ // silent miss — would otherwise produce a confusing "you said it
117
+ // worked but I still see nothing" loop.
118
+ const resolved = opts.resolve();
119
+ if (!resolved) {
120
+ return {
121
+ status: 'missing',
122
+ reason: 'login_failed',
123
+ detail: 'Login reported success but no credential persisted. Check `pugi whoami`.',
124
+ };
125
+ }
126
+ credentialCache.set(resolved.apiUrl, resolved);
127
+ return { status: 'ready', credential: resolved };
128
+ }
129
+ //# sourceMappingURL=ensure-authenticated.js.map
@@ -0,0 +1,238 @@
1
+ /**
2
+ * `pugi login --provider env` — env-var auth path ().
3
+ *
4
+ * the upstream tool, Codex CLI, and gh CLI all ship a way to authenticate via
5
+ * an environment variable so CI / container / scripted contexts can
6
+ * skip the device flow entirely. This module backs that path:
7
+ *
8
+ * 1. Resolve the candidate token (explicit `--key` flag beats
9
+ * `PUGI_API_KEY` env — same precedence as `gh auth login --token`).
10
+ * 2. Run a cheap local format check so an obviously malformed key
11
+ * (empty, whitespace, suspiciously short) fails fast WITHOUT
12
+ * shipping it to the server (no observability leak into the
13
+ * Anvil access log).
14
+ * 3. Call `GET /api/pugi/health` with `Authorization: Bearer <key>`
15
+ * so an expired / revoked / typo'd token surfaces immediately
16
+ * and the credential file never lands on disk for a dead key.
17
+ * 4. Map response to typed outcome the CLI dispatcher can render.
18
+ *
19
+ * The module is intentionally pure — fetch + reading env are injected,
20
+ * the writer is a separate concern. The CLI dispatcher composes
21
+ * `resolveEnvCandidateToken` + `assertTokenFormat` + `validateTokenAgainstHealth`
22
+ * and then writes the credential via `storeApiKey` on success.
23
+ *
24
+ * Failure modes are explicit so the dispatcher can pick the user-facing
25
+ * remediation string without re-parsing strings:
26
+ *
27
+ * - `missing` → no token in env or --key, halt with hint
28
+ * - `invalid-format` → token failed local format check, halt
29
+ * - `unauthorized` → server rejected the token (401 / 403)
30
+ * - `network-error` → fetch threw (DNS, refused, TLS)
31
+ * - `server-error` → server returned 5xx (transient — operator
32
+ * may want to retry once)
33
+ * - `unexpected-status`→ anything else non-2xx (treat as failure)
34
+ *
35
+ * NEVER log the raw token. Memory hits
36
+ * `feedback_no_claude_attribution_anywhere_hard_rule` plus the
37
+ * CSO bearer-leak sweep apply here. Use `maskApiKey` from
38
+ * `core/credentials.ts` when the dispatcher needs to surface the key
39
+ * to the operator.
40
+ */
41
+ /**
42
+ * The minimum length below which we refuse to even ship the token to
43
+ * the server. Pugi-issued PATs are 48+ chars (`pugi_<32 base32>`), JWTs
44
+ * issued by the device flow are ~250 chars, legacy `sk-*` PATs we
45
+ * accept for compatibility are 32+. 16 is well below all three real
46
+ * shapes so it only catches obvious paste mistakes.
47
+ */
48
+ export const MIN_TOKEN_LENGTH = 16;
49
+ /**
50
+ * The set of prefixes we recognise as plausibly-real Pugi-shaped
51
+ * tokens. Loose by design — the real validator is the server-side
52
+ * health probe. We just want to catch an operator who pasted the
53
+ * wrong string entirely (a username, a URL, a placeholder like
54
+ * "<your-key>") before it reaches the network.
55
+ *
56
+ * Three-segment JWTs are also accepted via the `looksLikeJwt`
57
+ * predicate so device-flow tokens copied out of `~/.pugi/credentials.json`
58
+ * on a different machine work.
59
+ */
60
+ export const RECOGNISED_TOKEN_PREFIXES = ['pugi_', 'sk_', 'sk-', 'pat_'];
61
+ /**
62
+ * Returns the trimmed candidate token, or `null` when neither path
63
+ * produced one. Precedence: explicit flag arg beats env var (matches
64
+ * `gh auth login --with-token`, `aws configure set`, and `pugi config`
65
+ * which all prefer the most-specific operator intent over the ambient
66
+ * env).
67
+ */
68
+ export function resolveEnvCandidateToken(input) {
69
+ const explicit = input.explicitKey?.trim();
70
+ if (explicit)
71
+ return explicit;
72
+ const env = input.env ?? process.env;
73
+ const fromEnv = env.PUGI_API_KEY?.trim();
74
+ if (fromEnv)
75
+ return fromEnv;
76
+ return null;
77
+ }
78
+ /**
79
+ * Local-only format check. Returns `null` on accept, a human-readable
80
+ * error string on reject. Deliberately lenient — the server-side
81
+ * health probe is the source of truth. We only catch obvious paste
82
+ * mistakes (empty, whitespace-laden, too short, looks like a URL or
83
+ * a placeholder).
84
+ */
85
+ export function assertTokenFormat(token) {
86
+ if (!token)
87
+ return 'Token is empty';
88
+ if (/\s/.test(token)) {
89
+ return 'Token contains whitespace — check for shell quoting issues or a stray newline';
90
+ }
91
+ if (token.length < MIN_TOKEN_LENGTH) {
92
+ return `Token too short (${token.length} chars; Pugi tokens are >= ${MIN_TOKEN_LENGTH})`;
93
+ }
94
+ if (token.startsWith('<') && token.endsWith('>')) {
95
+ return 'Token looks like a placeholder (`<your-key>`) — replace with the actual key';
96
+ }
97
+ if (/^https?:\/\//i.test(token)) {
98
+ return 'Token looks like a URL — did you mean --api-url?';
99
+ }
100
+ // Accept either a recognised prefix OR a JWT three-segment shape.
101
+ // Anything else still passes — the server probe will catch genuinely
102
+ // unknown keys. We just want to surface an obvious mistake.
103
+ const hasKnownPrefix = RECOGNISED_TOKEN_PREFIXES.some((p) => token.startsWith(p));
104
+ if (!hasKnownPrefix && !looksLikeJwt(token)) {
105
+ // Soft-fail: warn the operator but proceed. Returning null here
106
+ // would mask the case where the operator pasted something
107
+ // genuinely wrong but the server happens to accept it (impossible
108
+ // for real keys but defence-in-depth). Returning the warning
109
+ // string would block legacy keys. We choose to proceed — the
110
+ // server is the source of truth — and let the CLI dispatcher
111
+ // decide whether to surface a note. Tracked via a separate
112
+ // `warnUnknownPrefix` return on a future revision.
113
+ return null;
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * JWT three-segment check. Does NOT verify the signature — we just
119
+ * want to recognise the shape so device-flow tokens copied from one
120
+ * machine to another pass the format gate.
121
+ */
122
+ export function looksLikeJwt(token) {
123
+ const parts = token.split('.');
124
+ if (parts.length !== 3)
125
+ return false;
126
+ return parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p) && p.length > 0);
127
+ }
128
+ /**
129
+ * Call `GET /api/pugi/health` with the candidate token. Returns a
130
+ * typed outcome that the CLI dispatcher can map directly to an exit
131
+ * code + remediation string.
132
+ *
133
+ * Health endpoint conventions (see apps/admin-api):
134
+ * - 200 → token is valid, account is active
135
+ * - 401 → token unknown / malformed at the server boundary
136
+ * - 403 → token recognised but the account is suspended / paused
137
+ * - 5xx → server-side issue, operator can retry
138
+ * - network throw → DNS, refused, TLS — operator's connectivity issue
139
+ *
140
+ * We do not parse the body — the health endpoint's contract is the
141
+ * status code. Any future field (latency, region, build sha) can be
142
+ * surfaced by a separate `pugi doctor` probe without touching the
143
+ * login path.
144
+ */
145
+ export async function validateTokenAgainstHealth(input) {
146
+ const fetchImpl = input.fetchImpl ?? fetch;
147
+ const now = input.now ?? Date.now;
148
+ const url = `${stripTrailingSlash(input.apiUrl)}/api/pugi/health`;
149
+ const started = now();
150
+ let response;
151
+ try {
152
+ response = await fetchImpl(url, {
153
+ method: 'GET',
154
+ headers: {
155
+ Authorization: `Bearer ${input.apiKey}`,
156
+ Accept: 'application/json',
157
+ },
158
+ });
159
+ }
160
+ catch (error) {
161
+ // DNS failure, ECONNREFUSED, TLS handshake — anything that makes
162
+ // fetch throw before a status code is observable. We deliberately
163
+ // do NOT echo the URL host in the message body if it could leak a
164
+ // self-hosted Anvil hostname into a public CI log; the dispatcher
165
+ // composes the user-facing remediation.
166
+ const cause = error instanceof Error ? error.message : String(error);
167
+ return {
168
+ kind: 'network-error',
169
+ message: `Cannot reach ${input.apiUrl}; check your connection`,
170
+ cause,
171
+ };
172
+ }
173
+ const latencyMs = now() - started;
174
+ const { status } = response;
175
+ if (status === 200) {
176
+ return { kind: 'ok', latencyMs };
177
+ }
178
+ if (status === 401 || status === 403) {
179
+ return {
180
+ kind: 'unauthorized',
181
+ status,
182
+ message: status === 401
183
+ ? 'Token invalid or expired — run `pugi login --provider device` to get a fresh one'
184
+ : 'Token recognised but the account is suspended — check `pugi whoami` on a working machine or contact support',
185
+ };
186
+ }
187
+ if (status >= 500) {
188
+ return {
189
+ kind: 'server-error',
190
+ status,
191
+ message: `${input.apiUrl} returned ${status}; retry in a moment`,
192
+ };
193
+ }
194
+ return {
195
+ kind: 'unexpected-status',
196
+ status,
197
+ message: `Unexpected ${status} from /api/pugi/health; treat as login failure`,
198
+ };
199
+ }
200
+ export async function resolveAndValidateEnvLogin(input) {
201
+ const token = resolveEnvCandidateToken({
202
+ explicitKey: input.explicitKey,
203
+ env: input.env,
204
+ });
205
+ if (!token) {
206
+ return {
207
+ kind: 'missing',
208
+ message: 'pugi login --provider env requires a token. Export PUGI_API_KEY in the current shell or pass --key <value>.',
209
+ };
210
+ }
211
+ const formatError = assertTokenFormat(token);
212
+ if (formatError) {
213
+ return {
214
+ kind: 'invalid-format',
215
+ message: `pugi login --provider env: ${formatError}`,
216
+ };
217
+ }
218
+ if (input.skipValidate) {
219
+ // Used by the existing `login-variants.spec.ts` regression suite
220
+ // so the test plane does not require a live network. Production
221
+ // path always validates.
222
+ return { kind: 'ok', token, latencyMs: 0 };
223
+ }
224
+ const probe = await validateTokenAgainstHealth({
225
+ apiUrl: input.apiUrl,
226
+ apiKey: token,
227
+ fetchImpl: input.fetchImpl,
228
+ now: input.now,
229
+ });
230
+ if (probe.kind === 'ok') {
231
+ return { kind: 'ok', token, latencyMs: probe.latencyMs };
232
+ }
233
+ return probe;
234
+ }
235
+ function stripTrailingSlash(url) {
236
+ return url.endsWith('/') ? url.slice(0, -1) : url;
237
+ }
238
+ //# sourceMappingURL=env-provider.js.map
@@ -18,7 +18,7 @@ export async function autoOpenBrowser(url, deps = {}) {
18
18
  return { opened: spawnDetached('open', [url]) };
19
19
  }
20
20
  if (platform === 'win32') {
21
- // P1-3 (triple-review 2026-05-24): cmd.exe parses `&` as a command
21
+ // P1-3 (triple-review): cmd.exe parses `&` as a command
22
22
  // separator BEFORE Node hands argv to the child, regardless of
23
23
  // `shell: false`. A device-flow URL like
24
24
  // `https://app.pugi.io/devices/authorize?user_code=ABC&trace=xyz`
@@ -67,7 +67,7 @@ function isSafeHttpUrl(candidate) {
67
67
  }
68
68
  }
69
69
  /**
70
- * P1-3 (triple-review 2026-05-24): wrap a URL for PowerShell's
70
+ * P1-3 (triple-review): wrap a URL for PowerShell's
71
71
  * `Start-Process` invocation. PowerShell's single-quote string literal
72
72
  * does NOT process escape sequences; the only metachar inside is the
73
73
  * single quote itself (escaped by doubling). URLs do not contain
@@ -78,7 +78,7 @@ function quoteForPowerShell(url) {
78
78
  return `'${url.replace(/'/g, "''")}'`;
79
79
  }
80
80
  /**
81
- * P1-3 (triple-review 2026-05-24): wrap a URL for cmd.exe's
81
+ * P1-3 (triple-review): wrap a URL for cmd.exe's
82
82
  * `start ""` invocation. Double quotes pin the URL as a single token
83
83
  * so cmd does NOT split on `&`, `|`, `^`, `<`, `>`. Embedded `"` (rare
84
84
  * in URLs but defensible) is escaped via cmd's caret-quote convention:
@@ -102,7 +102,7 @@ function defaultSpawnDetached(cmd, args) {
102
102
  shell: false,
103
103
  };
104
104
  const child = spawn(cmd, args.slice(), options);
105
- // P3 polish (triple-review 2026-05-24): swallow the async `error`
105
+ // P3 polish (triple-review): swallow the async `error`
106
106
  // event (ENOENT for a missing binary, EACCES for a sandboxed
107
107
  // permission denial). The old `errored` flag was always false at
108
108
  // the synchronous `return !errored` point (the `error` event is