@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88

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 (405) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  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/smoke.js +133 -0
  11. package/dist/core/agent-progress/cleanup.js +134 -0
  12. package/dist/core/agent-progress/schema.js +144 -0
  13. package/dist/core/agent-progress/writer.js +101 -0
  14. package/dist/core/agents/adaptive-router.js +330 -0
  15. package/dist/core/agents/query-decomposer.js +297 -0
  16. package/dist/core/agents/registry.js +3 -3
  17. package/dist/core/approvals/shortcut-resolver.js +98 -0
  18. package/dist/core/artifact-chain/dispatcher.js +148 -0
  19. package/dist/core/artifact-chain/exporter.js +164 -0
  20. package/dist/core/artifact-chain/state.js +243 -0
  21. package/dist/core/artifact-chain/steps.js +169 -0
  22. package/dist/core/ask-user/question.js +92 -0
  23. package/dist/core/audit/audit-trail.js +275 -0
  24. package/dist/core/auth/ensure-authenticated.js +129 -0
  25. package/dist/core/auth/env-provider.js +238 -0
  26. package/dist/core/auto-open-browser.js +4 -4
  27. package/dist/core/auto-update/channels.js +122 -0
  28. package/dist/core/auto-update/checker.js +241 -0
  29. package/dist/core/auto-update/state.js +235 -0
  30. package/dist/core/bare-mode/index.js +107 -0
  31. package/dist/core/bash/redirect.js +281 -0
  32. package/dist/core/bash-classifier.js +436 -40
  33. package/dist/core/checkpoint/resumer.js +149 -0
  34. package/dist/core/checkpoint/rewinder.js +291 -0
  35. package/dist/core/checkpoints/shadow-git.js +670 -0
  36. package/dist/core/citations/parser.js +109 -0
  37. package/dist/core/classifier/yolo-classifier.js +88 -0
  38. package/dist/core/codegraph/decision-store.js +248 -0
  39. package/dist/core/codegraph/detect-repo.js +459 -0
  40. package/dist/core/codegraph/install.js +134 -0
  41. package/dist/core/codegraph/offer-hook.js +220 -0
  42. package/dist/core/compact/auto-trigger.js +96 -0
  43. package/dist/core/compact/buffer-rewriter.js +115 -0
  44. package/dist/core/compact/summarizer.js +208 -0
  45. package/dist/core/compact/token-counter.js +108 -0
  46. package/dist/core/consensus/anvil-fanout.js +25 -25
  47. package/dist/core/consensus/diff-capture.js +121 -12
  48. package/dist/core/consensus/rubric.js +21 -21
  49. package/dist/core/context/builder.js +6 -6
  50. package/dist/core/context/compaction-events.js +8 -8
  51. package/dist/core/context/compaction.js +31 -31
  52. package/dist/core/context/index.js +15 -8
  53. package/dist/core/context/invariants.js +51 -51
  54. package/dist/core/context/markdown-loader.js +28 -10
  55. package/dist/core/context/markdown-traverse.js +255 -0
  56. package/dist/core/context/pugiignore.js +41 -41
  57. package/dist/core/context/repo-skeleton.js +37 -37
  58. package/dist/core/context/tool-eviction.js +55 -0
  59. package/dist/core/context/watcher.js +32 -32
  60. package/dist/core/context/working-set.js +23 -23
  61. package/dist/core/coordinator/agent-tools.js +77 -0
  62. package/dist/core/coordinator/agent-toolset.js +65 -0
  63. package/dist/core/coordinator/fsm.js +73 -0
  64. package/dist/core/coordinator/mode-fsm.js +70 -0
  65. package/dist/core/cost/rate-card.js +129 -0
  66. package/dist/core/cost/tracker.js +221 -0
  67. package/dist/core/credentials.js +12 -12
  68. package/dist/core/cron/scheduler.js +138 -0
  69. package/dist/core/denial-tracking/index.js +8 -0
  70. package/dist/core/denial-tracking/state.js +264 -0
  71. package/dist/core/diagnostics/probe-runner.js +93 -0
  72. package/dist/core/diagnostics/probes/api.js +46 -0
  73. package/dist/core/diagnostics/probes/auth.js +93 -0
  74. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  75. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  76. package/dist/core/diagnostics/probes/config.js +72 -0
  77. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  78. package/dist/core/diagnostics/probes/disk.js +81 -0
  79. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  80. package/dist/core/diagnostics/probes/git.js +65 -0
  81. package/dist/core/diagnostics/probes/hooks.js +118 -0
  82. package/dist/core/diagnostics/probes/mcp.js +75 -0
  83. package/dist/core/diagnostics/probes/node.js +59 -0
  84. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  85. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  86. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  87. package/dist/core/diagnostics/probes/session.js +74 -0
  88. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  89. package/dist/core/diagnostics/probes/workspace.js +63 -0
  90. package/dist/core/diagnostics/types.js +70 -0
  91. package/dist/core/dispatch/cache-cleanup.js +197 -0
  92. package/dist/core/dispatch/cache-handoff.js +295 -0
  93. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  94. package/dist/core/edits/dispatch.js +293 -7
  95. package/dist/core/edits/format-matrix.js +26 -0
  96. package/dist/core/edits/fuzzy-ladder.js +650 -0
  97. package/dist/core/edits/index.js +3 -1
  98. package/dist/core/edits/journal.js +199 -0
  99. package/dist/core/edits/layer-a-apply.js +15 -15
  100. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  101. package/dist/core/edits/layer-b-apply.js +9 -9
  102. package/dist/core/edits/layer-c-apply.js +6 -6
  103. package/dist/core/edits/layer-d-ast.js +557 -14
  104. package/dist/core/edits/marker-parser.js +12 -12
  105. package/dist/core/edits/security-gate.js +27 -27
  106. package/dist/core/edits/verify-hook.js +273 -0
  107. package/dist/core/edits/worktree.js +322 -0
  108. package/dist/core/engine/anvil-client.js +151 -26
  109. package/dist/core/engine/auto-compact.js +179 -0
  110. package/dist/core/engine/budgets.js +186 -0
  111. package/dist/core/engine/context-prefix.js +155 -0
  112. package/dist/core/engine/index.js +1 -1
  113. package/dist/core/engine/intensity.js +158 -0
  114. package/dist/core/engine/intent.js +260 -0
  115. package/dist/core/engine/native-pugi.js +1295 -227
  116. package/dist/core/engine/prompts.js +134 -16
  117. package/dist/core/engine/strip-internal-fields.js +124 -0
  118. package/dist/core/engine/tool-bridge.js +1295 -59
  119. package/dist/core/evaluation/golden-dataset.js +293 -0
  120. package/dist/core/feedback/queue.js +177 -0
  121. package/dist/core/feedback/submitter.js +145 -0
  122. package/dist/core/file-cache.js +113 -1
  123. package/dist/core/flatten/flatten-repo.js +439 -0
  124. package/dist/core/format/osc8-link.js +28 -0
  125. package/dist/core/hook-chains.js +392 -0
  126. package/dist/core/hooks/citation-verify-hook.js +138 -0
  127. package/dist/core/hooks/citation-verify.js +112 -0
  128. package/dist/core/hooks/events.js +44 -0
  129. package/dist/core/hooks/index.js +15 -0
  130. package/dist/core/hooks/registry.js +213 -0
  131. package/dist/core/hooks/runner.js +236 -0
  132. package/dist/core/hooks/v2/event-emitter.js +115 -0
  133. package/dist/core/hooks/v2/executor.js +282 -0
  134. package/dist/core/hooks/v2/index.js +25 -0
  135. package/dist/core/hooks/v2/lifecycle.js +104 -0
  136. package/dist/core/hooks/v2/loader.js +216 -0
  137. package/dist/core/hooks/v2/matcher.js +125 -0
  138. package/dist/core/hooks/v2/trust.js +143 -0
  139. package/dist/core/hooks/v2/types.js +86 -0
  140. package/dist/core/image/renderer.js +71 -0
  141. package/dist/core/init/detector.js +582 -0
  142. package/dist/core/init/template-renderer.js +242 -0
  143. package/dist/core/jobs/registry.js +18 -18
  144. package/dist/core/ledger/results-tsv.js +142 -0
  145. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  146. package/dist/core/lsp/cache.js +105 -0
  147. package/dist/core/lsp/client.js +776 -0
  148. package/dist/core/lsp/language-detect.js +66 -0
  149. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  150. package/dist/core/lsp/symbol-tools.js +372 -0
  151. package/dist/core/mcp/client.js +97 -28
  152. package/dist/core/mcp/http-server.js +553 -0
  153. package/dist/core/mcp/orchestrator-tools.js +662 -0
  154. package/dist/core/mcp/permission.js +190 -0
  155. package/dist/core/mcp/registry.js +39 -17
  156. package/dist/core/mcp/server-tools.js +219 -0
  157. package/dist/core/mcp/server.js +397 -0
  158. package/dist/core/mcp/trust.js +10 -10
  159. package/dist/core/memory/dual-write.js +416 -0
  160. package/dist/core/memory/passive-extract.js +130 -0
  161. package/dist/core/memory/phase1-kinds.js +20 -0
  162. package/dist/core/memory/secret-scanner.js +304 -0
  163. package/dist/core/memory-sync/queue.js +170 -0
  164. package/dist/core/metrics/extract.js +113 -0
  165. package/dist/core/modes/roo-modes.js +68 -0
  166. package/dist/core/onboarding/ensure-initialized.js +133 -0
  167. package/dist/core/onboarding/marker.js +111 -0
  168. package/dist/core/onboarding/telemetry-state.js +108 -0
  169. package/dist/core/output-style/presets.js +176 -0
  170. package/dist/core/output-style/state.js +185 -0
  171. package/dist/core/path-security.js +287 -5
  172. package/dist/core/permission.js +82 -22
  173. package/dist/core/permissions/auto-classifier.js +124 -0
  174. package/dist/core/permissions/bash-parser.js +371 -0
  175. package/dist/core/permissions/circuit-breaker.js +83 -0
  176. package/dist/core/permissions/constrained-edit.js +91 -0
  177. package/dist/core/permissions/gate.js +278 -0
  178. package/dist/core/permissions/index.js +20 -0
  179. package/dist/core/permissions/mode.js +174 -0
  180. package/dist/core/permissions/network-egress.js +137 -0
  181. package/dist/core/permissions/state.js +241 -0
  182. package/dist/core/permissions/tool-class.js +93 -0
  183. package/dist/core/plan-mode/ui-state.js +51 -0
  184. package/dist/core/plans/plan-artifact.js +721 -0
  185. package/dist/core/policy-limits/etag-store.js +122 -0
  186. package/dist/core/prd-check/parser.js +215 -0
  187. package/dist/core/prd-check/reporter.js +127 -0
  188. package/dist/core/prd-check/session-review.js +557 -0
  189. package/dist/core/prd-check/verifiers.js +223 -0
  190. package/dist/core/prompt-cache/client-cache.js +99 -0
  191. package/dist/core/prompts/assembly.js +29 -0
  192. package/dist/core/prompts/registry.js +364 -0
  193. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  194. package/dist/core/pugi-md/context-injector.js +76 -0
  195. package/dist/core/pugi-md/walk-up.js +207 -0
  196. package/dist/core/python/uv-installer.js +270 -0
  197. package/dist/core/python/uv-resolver.js +83 -0
  198. package/dist/core/rate-limit/narrator.js +146 -0
  199. package/dist/core/recipes/cli-types.js +20 -0
  200. package/dist/core/recipes/loader.js +103 -0
  201. package/dist/core/recipes/runner.js +345 -0
  202. package/dist/core/recipes/schema.js +587 -0
  203. package/dist/core/release-notes/parser.js +241 -0
  204. package/dist/core/release-notes/state.js +116 -0
  205. package/dist/core/repl/ask.js +37 -37
  206. package/dist/core/repl/cancellation.js +26 -26
  207. package/dist/core/repl/cap-warning.js +4 -4
  208. package/dist/core/repl/clipboard-read.js +11 -11
  209. package/dist/core/repl/dispatch-fsm.js +12 -12
  210. package/dist/core/repl/history-search.js +15 -15
  211. package/dist/core/repl/history.js +28 -18
  212. package/dist/core/repl/kill-ring.js +5 -5
  213. package/dist/core/repl/model-pricing.js +135 -0
  214. package/dist/core/repl/privacy-banner.js +22 -22
  215. package/dist/core/repl/session.js +2157 -214
  216. package/dist/core/repl/slash-commands.js +533 -40
  217. package/dist/core/repl/store/index.js +1 -1
  218. package/dist/core/repl/store/jsonl-log.js +22 -22
  219. package/dist/core/repl/store/lockfile.js +10 -10
  220. package/dist/core/repl/store/session-store.js +136 -107
  221. package/dist/core/repl/store/types.js +15 -15
  222. package/dist/core/repl/store/uuid-v7.js +12 -12
  223. package/dist/core/repl/workspace-context.js +43 -21
  224. package/dist/core/repo-map/build.js +125 -0
  225. package/dist/core/repo-map/cache.js +185 -0
  226. package/dist/core/repo-map/extractor.js +254 -0
  227. package/dist/core/repo-map/formatter.js +145 -0
  228. package/dist/core/repo-map/page-rank.js +105 -0
  229. package/dist/core/repo-map/scanner.js +211 -0
  230. package/dist/core/retry-budget/budget.js +284 -0
  231. package/dist/core/retry-budget/index.js +5 -0
  232. package/dist/core/retry-budget/retry-cap.js +74 -0
  233. package/dist/core/routing/lead-worker.js +43 -0
  234. package/dist/core/routing/pre-flight-estimator.js +108 -0
  235. package/dist/core/runs/run-tree.js +103 -0
  236. package/dist/core/security/injection-scanner.js +367 -0
  237. package/dist/core/security/output-filter.js +418 -0
  238. package/dist/core/session/env-file.js +105 -0
  239. package/dist/core/session/section-budgets.js +140 -0
  240. package/dist/core/session.js +92 -0
  241. package/dist/core/settings.js +298 -5
  242. package/dist/core/share/formatter.js +271 -0
  243. package/dist/core/share/redactor.js +221 -0
  244. package/dist/core/share/uploader.js +267 -0
  245. package/dist/core/skills/defaults.js +457 -0
  246. package/dist/core/skills/loader.js +22 -22
  247. package/dist/core/skills/sources.js +27 -27
  248. package/dist/core/smoke/headless-driver.js +174 -0
  249. package/dist/core/smoke/orchestrator.js +194 -0
  250. package/dist/core/smoke/runner.js +238 -0
  251. package/dist/core/smoke/scenario-parser.js +316 -0
  252. package/dist/core/statusline.js +99 -0
  253. package/dist/core/subagents/dispatcher-real.js +600 -0
  254. package/dist/core/subagents/dispatcher.js +132 -43
  255. package/dist/core/subagents/index.js +19 -6
  256. package/dist/core/subagents/isolation-matrix.js +213 -0
  257. package/dist/core/subagents/spawn.js +19 -4
  258. package/dist/core/telemetry/emitter.js +229 -0
  259. package/dist/core/telemetry/queue.js +251 -0
  260. package/dist/core/theme/context.js +91 -0
  261. package/dist/core/theme/presets.js +228 -0
  262. package/dist/core/theme/state.js +181 -0
  263. package/dist/core/todos/invariant.js +10 -0
  264. package/dist/core/todos/state.js +177 -0
  265. package/dist/core/tool-schema/compressor.js +89 -0
  266. package/dist/core/transport/version-interceptor.js +166 -0
  267. package/dist/core/trust.js +2 -2
  268. package/dist/core/tui/thinking-block.js +64 -0
  269. package/dist/core/vim/keymap.js +288 -0
  270. package/dist/core/vim/state.js +92 -0
  271. package/dist/core/watch-markers/marker-watcher.js +133 -0
  272. package/dist/core/worktree-manager/cleanup.js +123 -0
  273. package/dist/core/worktree-manager/manager.js +303 -0
  274. package/dist/index.js +36 -0
  275. package/dist/runtime/bootstrap.js +190 -0
  276. package/dist/runtime/cli.js +4203 -493
  277. package/dist/runtime/commands/agents.js +30 -30
  278. package/dist/runtime/commands/budget.js +5 -5
  279. package/dist/runtime/commands/cancel.js +231 -0
  280. package/dist/runtime/commands/chain.js +489 -0
  281. package/dist/runtime/commands/codegraph-status.js +227 -0
  282. package/dist/runtime/commands/compact.js +297 -0
  283. package/dist/runtime/commands/config.js +73 -39
  284. package/dist/runtime/commands/cost.js +199 -0
  285. package/dist/runtime/commands/delegate.js +244 -13
  286. package/dist/runtime/commands/dispatch.js +126 -0
  287. package/dist/runtime/commands/doctor.js +579 -0
  288. package/dist/runtime/commands/feedback.js +184 -0
  289. package/dist/runtime/commands/hooks.js +184 -0
  290. package/dist/runtime/commands/init.js +254 -0
  291. package/dist/runtime/commands/lsp.js +368 -0
  292. package/dist/runtime/commands/mcp.js +879 -0
  293. package/dist/runtime/commands/memory.js +582 -0
  294. package/dist/runtime/commands/model.js +237 -0
  295. package/dist/runtime/commands/onboarding.js +275 -0
  296. package/dist/runtime/commands/patch.js +128 -0
  297. package/dist/runtime/commands/permissions.js +112 -0
  298. package/dist/runtime/commands/plan.js +143 -0
  299. package/dist/runtime/commands/prd-check.js +285 -0
  300. package/dist/runtime/commands/privacy.js +17 -17
  301. package/dist/runtime/commands/recipe.js +325 -0
  302. package/dist/runtime/commands/redo-blob-store.js +92 -0
  303. package/dist/runtime/commands/redo.js +361 -0
  304. package/dist/runtime/commands/release-notes.js +229 -0
  305. package/dist/runtime/commands/repo-map.js +95 -0
  306. package/dist/runtime/commands/report.js +299 -0
  307. package/dist/runtime/commands/resume.js +118 -0
  308. package/dist/runtime/commands/review-consensus.js +68 -53
  309. package/dist/runtime/commands/rewind.js +333 -0
  310. package/dist/runtime/commands/roster.js +14 -14
  311. package/dist/runtime/commands/sessions.js +163 -0
  312. package/dist/runtime/commands/share.js +316 -0
  313. package/dist/runtime/commands/skills.js +31 -31
  314. package/dist/runtime/commands/status.js +186 -0
  315. package/dist/runtime/commands/stickers.js +82 -0
  316. package/dist/runtime/commands/style.js +194 -0
  317. package/dist/runtime/commands/theme.js +196 -0
  318. package/dist/runtime/commands/undo.js +54 -22
  319. package/dist/runtime/commands/update.js +289 -0
  320. package/dist/runtime/commands/vim.js +140 -0
  321. package/dist/runtime/commands/worktree.js +177 -0
  322. package/dist/runtime/commands/worktrees.js +155 -0
  323. package/dist/runtime/headless-repl.js +195 -0
  324. package/dist/runtime/headless.js +543 -0
  325. package/dist/runtime/load-hooks-or-exit.js +71 -0
  326. package/dist/runtime/plan-decompose.js +531 -0
  327. package/dist/runtime/sigint-guard.js +272 -0
  328. package/dist/runtime/update-check.js +28 -28
  329. package/dist/runtime/version.js +65 -0
  330. package/dist/skills/bundled/batch.js +617 -0
  331. package/dist/skills/bundled/index.js +45 -0
  332. package/dist/skills/bundled/loop.js +358 -0
  333. package/dist/skills/bundled/remember.js +383 -0
  334. package/dist/skills/bundled/simplify.js +289 -0
  335. package/dist/skills/bundled/skillify.js +373 -0
  336. package/dist/skills/bundled/stuck.js +558 -0
  337. package/dist/skills/bundled/verify.js +439 -0
  338. package/dist/testing/vcr.js +486 -0
  339. package/dist/tools/agent-tool.js +229 -0
  340. package/dist/tools/apply-patch.js +556 -0
  341. package/dist/tools/ask-user-question.js +288 -0
  342. package/dist/tools/ask-user.js +115 -0
  343. package/dist/tools/bash.js +624 -46
  344. package/dist/tools/brief.js +224 -0
  345. package/dist/tools/enter-worktree.js +250 -0
  346. package/dist/tools/exit-worktree.js +147 -0
  347. package/dist/tools/file-tools.js +161 -44
  348. package/dist/tools/lsp-tools.js +189 -0
  349. package/dist/tools/mcp-tool.js +260 -0
  350. package/dist/tools/multi-edit.js +361 -0
  351. package/dist/tools/powershell.js +268 -0
  352. package/dist/tools/registry.js +85 -0
  353. package/dist/tools/skill-tool.js +96 -0
  354. package/dist/tools/sleep.js +99 -0
  355. package/dist/tools/synthetic-output.js +133 -0
  356. package/dist/tools/tasks.js +208 -0
  357. package/dist/tools/todo-write.js +184 -0
  358. package/dist/tools/verify-plan-execution.js +295 -0
  359. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  360. package/dist/tools/web-fetch.js +195 -10
  361. package/dist/tools/web-search.js +458 -0
  362. package/dist/tui/agent-progress-card.js +111 -0
  363. package/dist/tui/agent-tree.js +11 -1
  364. package/dist/tui/ask-modal.js +14 -14
  365. package/dist/tui/ask-user-question-chips.js +257 -0
  366. package/dist/tui/ask-user-question-prompt.js +203 -0
  367. package/dist/tui/compact-banner.js +81 -0
  368. package/dist/tui/conversation-pane.js +85 -11
  369. package/dist/tui/cost-table.js +111 -0
  370. package/dist/tui/device-flow.js +2 -2
  371. package/dist/tui/doctor-table.js +46 -0
  372. package/dist/tui/feedback-prompt.js +156 -0
  373. package/dist/tui/input-box.js +247 -32
  374. package/dist/tui/login-picker.js +3 -3
  375. package/dist/tui/markdown-render.js +6 -6
  376. package/dist/tui/onboarding-wizard.js +240 -0
  377. package/dist/tui/permissions-picker.js +86 -0
  378. package/dist/tui/render.js +35 -0
  379. package/dist/tui/repl-render.js +332 -54
  380. package/dist/tui/repl-splash-art.js +16 -16
  381. package/dist/tui/repl-splash-mascot.js +48 -24
  382. package/dist/tui/repl-splash.js +22 -22
  383. package/dist/tui/repl.js +124 -44
  384. package/dist/tui/slash-palette.js +6 -6
  385. package/dist/tui/splash.js +2 -2
  386. package/dist/tui/status-bar.js +109 -31
  387. package/dist/tui/status-table.js +7 -0
  388. package/dist/tui/stickers-art.js +136 -0
  389. package/dist/tui/style-table.js +28 -0
  390. package/dist/tui/theme-table.js +29 -0
  391. package/dist/tui/thinking-spinner.js +123 -0
  392. package/dist/tui/tool-stream-pane.js +53 -4
  393. package/dist/tui/update-banner.js +27 -2
  394. package/dist/tui/vim-input.js +267 -0
  395. package/dist/tui/welcome-banner.js +107 -0
  396. package/dist/tui/welcome-data.js +293 -0
  397. package/dist/tui/workspace-context.js +2 -2
  398. package/docs/examples/codegraph.mcp.json +10 -0
  399. package/package.json +25 -7
  400. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  401. package/test/scenarios/compact-force.scenario.txt +11 -0
  402. package/test/scenarios/identity.scenario.txt +11 -0
  403. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  404. package/test/scenarios/walkback.scenario.txt +12 -0
  405. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,122 @@
1
+ /**
2
+ * — Auto-update channel model.
3
+ *
4
+ * the upstream tool's self-update flow exposes three release channels (stable
5
+ * / beta / canary) so operators can dial in their own risk tolerance:
6
+ * a finance-team operator pins `stable`, a tinkerer rides `canary`,
7
+ * and the default `beta` track sits in the middle. Pugi parity ships
8
+ * the same vocabulary on top of npm's dist-tags so we do NOT need a
9
+ * custom registry hop:
10
+ *
11
+ * - `stable` → npm dist-tag `latest`
12
+ * - `beta` → npm dist-tag `beta`
13
+ * - `canary` → npm dist-tag `next`
14
+ *
15
+ * The mapping is centralised here because the dispatcher + the probe
16
+ * + the slash command + the doctor probe all need to agree on which
17
+ * tag they query. The channel name is what shows up in operator UI
18
+ * (`pugi update --channel beta`); the npm tag is the wire-format that
19
+ * gets concatenated into `npm i -g @pugi/cli@<tag>`.
20
+ *
21
+ * Module contract:
22
+ *
23
+ * - Pure constants + a small parse helper. No I/O, no module-level
24
+ * side effects. The persistence layer (`state.ts`) and the probe
25
+ * (`checker.ts`) wrap this module without ever mutating it.
26
+ *
27
+ * - Default channel is `beta` because Pugi currently publishes ONLY
28
+ * beta releases on npm. Defaulting to `stable` would leave fresh
29
+ * installs polling a tag that does not exist yet, surfacing a
30
+ * confusing "no update available" message even though the binary
31
+ * they just installed is already behind. The default is revisited
32
+ * when `latest` (stable) starts shipping; the constant lives on
33
+ * a single line so the bump is a one-character diff.
34
+ *
35
+ * - All channel <-> tag conversions go through `npmTagForChannel` /
36
+ * `channelForNpmTag`. Hard-coding the string `'beta'` anywhere
37
+ * else is a bug.
38
+ */
39
+ /**
40
+ * Immutable list of every supported channel. Drives the `--channel`
41
+ * flag validator + the `/update --channel <name>` slash parser +
42
+ * any future Ink picker that wants to render the full set.
43
+ */
44
+ export const UPDATE_CHANNELS = Object.freeze([
45
+ 'stable',
46
+ 'beta',
47
+ 'canary',
48
+ ]);
49
+ /**
50
+ * Default channel when neither `~/.pugi/config.json::updateChannel`
51
+ * nor a CLI flag specifies one. See module header for rationale.
52
+ */
53
+ export const DEFAULT_UPDATE_CHANNEL = 'beta';
54
+ /**
55
+ * npm dist-tag → channel mapping. Centralised so a future channel
56
+ * rename (e.g. dropping `next` in favour of `canary` as the npm tag)
57
+ * is a single-line change. The reverse-lookup helpers below derive
58
+ * from this map so the two surfaces never drift.
59
+ */
60
+ const CHANNEL_TO_NPM_TAG = Object.freeze({
61
+ stable: 'latest',
62
+ beta: 'beta',
63
+ canary: 'next',
64
+ });
65
+ /**
66
+ * Map a Pugi channel name to the npm dist-tag the registry exposes.
67
+ * Used to build both the registry query URL and the
68
+ * `npm i -g @pugi/cli@<tag>` install command surfaced to the operator.
69
+ */
70
+ export function npmTagForChannel(channel) {
71
+ return CHANNEL_TO_NPM_TAG[channel];
72
+ }
73
+ /**
74
+ * Reverse lookup: given an npm dist-tag, return the matching Pugi
75
+ * channel name. Returns `null` for any tag we do not officially
76
+ * support; the caller layers in its own fallback (typically
77
+ * `DEFAULT_UPDATE_CHANNEL`) so a one-off dist-tag never crashes the
78
+ * channel picker.
79
+ */
80
+ export function channelForNpmTag(tag) {
81
+ for (const channel of UPDATE_CHANNELS) {
82
+ if (CHANNEL_TO_NPM_TAG[channel] === tag)
83
+ return channel;
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Type guard / parser for an unknown string. Trims + lowercases the
89
+ * input so `--channel BETA` and `--channel beta ` both work. Returns
90
+ * `null` for unknown channel names; the caller decides whether to
91
+ * fall back, error out, or prompt the operator.
92
+ */
93
+ export function parseUpdateChannel(raw) {
94
+ if (typeof raw !== 'string')
95
+ return null;
96
+ const normalised = raw.trim().toLowerCase();
97
+ if (normalised.length === 0)
98
+ return null;
99
+ for (const channel of UPDATE_CHANNELS) {
100
+ if (channel === normalised)
101
+ return channel;
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Pretty operator-readable description of a channel. Surfaced in the
107
+ * `pugi update --check` JSON envelope + the interactive picker so the
108
+ * operator sees what they are switching into. Keeps the copy single-
109
+ * sourced because the same line lands in the doctor table, the
110
+ * `--check` JSON, and the slash-command response.
111
+ */
112
+ export function describeChannel(channel) {
113
+ switch (channel) {
114
+ case 'stable':
115
+ return 'stable (npm latest — finance / prod operators)';
116
+ case 'beta':
117
+ return 'beta (npm beta — current Pugi default)';
118
+ case 'canary':
119
+ return 'canary (npm next — tinkerers, early adopters)';
120
+ }
121
+ }
122
+ //# sourceMappingURL=channels.js.map
@@ -0,0 +1,241 @@
1
+ /**
2
+ * — Channel-aware npm registry probe.
3
+ *
4
+ * Polls `https://registry.npmjs.org/@pugi/cli` and reads the
5
+ * `dist-tags` object — npm publishes one entry per dist-tag (`latest`,
6
+ * `beta`, `next`) mapped to the highest semver under that tag. The
7
+ * channel resolver in `channels.ts` translates Pugi's user-visible
8
+ * channel name (`stable` / `beta` / `canary`) into the matching tag
9
+ * before we read it off the response.
10
+ *
11
+ * Why this lives next to (rather than INSIDE) the existing
12
+ * `runtime/update-check.ts`:
13
+ *
14
+ * - `update-check.ts` already poll `/@pugi/cli/latest`. That endpoint
15
+ * ONLY surfaces the `latest` dist-tag — channel selection requires
16
+ * the full package document `/@pugi/cli` so we can pluck any of
17
+ * the three tags. Two endpoints, two probes.
18
+ *
19
+ * - The legacy banner (REPL cold start) keeps polling `latest` and
20
+ * does NOT need to know about channels yet — it is a single-track
21
+ * cosmetic hint. Splitting into two modules means we can ship L27
22
+ * without churning the existing banner cache shape.
23
+ *
24
+ * - A future sprint may unify the two probes once the channel
25
+ * selection percolates into the REPL banner. For now the modules
26
+ * coexist with a clear boundary: this file owns the `pugi update`
27
+ * surface, `runtime/update-check.ts` owns the REPL cold-start
28
+ * cosmetic banner.
29
+ *
30
+ * Module contract:
31
+ *
32
+ * - The probe is pure with respect to disk + clock — every IO/clock
33
+ * dependency is injected. The same module is used both at the CLI
34
+ * entry point (real `fetch` + real `Date.now`) and in tests
35
+ * (mock fetch + frozen clock) without a single environment shim.
36
+ *
37
+ * - Failures NEVER throw. A 5xx, a network error, a JSON parse
38
+ * failure, or an unknown channel/tag all collapse to a structured
39
+ * `{ available: false, error: <reason> }` envelope. The caller
40
+ * decides whether to surface the error to the operator (the
41
+ * `pugi update --check` JSON surface) or swallow it (the cold-
42
+ * start banner).
43
+ *
44
+ * - The probe DOES NOT touch persistence. Writing the
45
+ * `~/.pugi/.last-update-check` timestamp is the caller's
46
+ * responsibility (the dispatcher in `runtime/commands/update.ts`)
47
+ * so a smoke-test sweep that probes the registry 50 times in a
48
+ * row does not accidentally write 50 timestamps.
49
+ */
50
+ import { npmTagForChannel, } from './channels.js';
51
+ import { compareVersions } from '../../runtime/update-check.js';
52
+ /**
53
+ * Base registry URL — overridable per call so the spec can drive the
54
+ * full request lifecycle through undici's MockAgent.
55
+ */
56
+ const PUGI_CLI_PACKAGE_URL = 'https://registry.npmjs.org/@pugi/cli';
57
+ /**
58
+ * Hard cap on the registry round-trip. Mirrors the existing
59
+ * `runtime/update-check.ts::FETCH_TIMEOUT_MS` so both surfaces
60
+ * timeout-by-the-same-budget — operators never wait longer for `pugi
61
+ * update --check` than they would for the cold-start banner.
62
+ */
63
+ const FETCH_TIMEOUT_MS = 3_000;
64
+ /**
65
+ * Build the install command the operator copy-pastes (or `pugi update
66
+ * --apply` shells out to). Centralised so the dispatcher + the
67
+ * checker share a single source of truth on the literal string.
68
+ */
69
+ export function buildInstallCommand(channel) {
70
+ return `npm install -g @pugi/cli@${npmTagForChannel(channel)}`;
71
+ }
72
+ /**
73
+ * Probe the npm registry for the latest version under `channel`. The
74
+ * dispatcher wraps this with persistence + confirmation prompts; this
75
+ * function is intentionally pure so the doctor probe + the cold-
76
+ * start banner can reuse it without dragging the dispatcher's I/O
77
+ * surface in.
78
+ */
79
+ export async function checkForChannelUpdate(deps) {
80
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch.bind(globalThis);
81
+ const registryUrl = deps.registryUrl ?? PUGI_CLI_PACKAGE_URL;
82
+ const timeoutMs = deps.timeoutMs ?? FETCH_TIMEOUT_MS;
83
+ const tag = npmTagForChannel(deps.channel);
84
+ const installCommand = buildInstallCommand(deps.channel);
85
+ // A controller-driven timeout — the spec's MockAgent never trips it
86
+ // but the production path on a stuck registry must bail in 3s.
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
89
+ let response;
90
+ try {
91
+ response = await fetchImpl(registryUrl, {
92
+ method: 'GET',
93
+ headers: { Accept: 'application/json' },
94
+ signal: controller.signal,
95
+ });
96
+ }
97
+ catch (error) {
98
+ clearTimeout(timer);
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ return {
101
+ channel: deps.channel,
102
+ npmTag: tag,
103
+ current: deps.currentVersion,
104
+ latest: null,
105
+ available: false,
106
+ gap: null,
107
+ installCommand,
108
+ error: `registry_unreachable: ${message}`,
109
+ };
110
+ }
111
+ clearTimeout(timer);
112
+ if (!response.ok) {
113
+ return {
114
+ channel: deps.channel,
115
+ npmTag: tag,
116
+ current: deps.currentVersion,
117
+ latest: null,
118
+ available: false,
119
+ gap: null,
120
+ installCommand,
121
+ error: `registry_status_${response.status}`,
122
+ };
123
+ }
124
+ let body;
125
+ try {
126
+ body = await response.json();
127
+ }
128
+ catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ return {
131
+ channel: deps.channel,
132
+ npmTag: tag,
133
+ current: deps.currentVersion,
134
+ latest: null,
135
+ available: false,
136
+ gap: null,
137
+ installCommand,
138
+ error: `registry_json_unparseable: ${message}`,
139
+ };
140
+ }
141
+ const distTags = extractDistTags(body);
142
+ if (!distTags) {
143
+ return {
144
+ channel: deps.channel,
145
+ npmTag: tag,
146
+ current: deps.currentVersion,
147
+ latest: null,
148
+ available: false,
149
+ gap: null,
150
+ installCommand,
151
+ error: 'registry_missing_dist_tags',
152
+ };
153
+ }
154
+ const latest = distTags[tag];
155
+ if (typeof latest !== 'string' || latest.length === 0) {
156
+ return {
157
+ channel: deps.channel,
158
+ npmTag: tag,
159
+ current: deps.currentVersion,
160
+ latest: null,
161
+ available: false,
162
+ gap: null,
163
+ installCommand,
164
+ error: `registry_missing_tag_${tag}`,
165
+ };
166
+ }
167
+ const cmp = compareVersions(deps.currentVersion, latest);
168
+ const available = cmp < 0;
169
+ const gap = classifyGap(deps.currentVersion, latest);
170
+ return {
171
+ channel: deps.channel,
172
+ npmTag: tag,
173
+ current: deps.currentVersion,
174
+ latest,
175
+ available,
176
+ gap,
177
+ installCommand,
178
+ error: null,
179
+ };
180
+ }
181
+ /**
182
+ * Extract the `dist-tags` object from a registry response. The npm
183
+ * package document is large (often >100kB); we only need this one
184
+ * key. Returns null when the field is missing or not a string-record
185
+ * — both indicate a registry response the probe should treat as
186
+ * unusable rather than panic on.
187
+ */
188
+ function extractDistTags(body) {
189
+ if (!body || typeof body !== 'object')
190
+ return null;
191
+ const obj = body;
192
+ const raw = obj['dist-tags'];
193
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
194
+ return null;
195
+ const result = {};
196
+ for (const [tag, value] of Object.entries(raw)) {
197
+ if (typeof value === 'string' && value.length > 0) {
198
+ result[tag] = value;
199
+ }
200
+ }
201
+ return result;
202
+ }
203
+ /**
204
+ * Coarse semver gap classification used to colour the dispatcher's
205
+ * diff line. We do NOT roll our own semver parser — `compareVersions`
206
+ * (already in `runtime/update-check.ts`) is the canonical comparator;
207
+ * here we only need to bucket the diff into major / minor / patch /
208
+ * prerelease for UX hints.
209
+ *
210
+ * The classifier is intentionally tolerant: anything that does not
211
+ * parse as `X.Y.Z[-pre]` collapses to `prerelease` so we surface a
212
+ * conservative "investigate manually" hint instead of crashing.
213
+ */
214
+ function classifyGap(current, latest) {
215
+ if (current === latest)
216
+ return 'same';
217
+ const a = parseCoarseSemver(current);
218
+ const b = parseCoarseSemver(latest);
219
+ if (!a || !b)
220
+ return 'prerelease';
221
+ if (b.major !== a.major)
222
+ return 'major';
223
+ if (b.minor !== a.minor)
224
+ return 'minor';
225
+ if (b.patch !== a.patch)
226
+ return 'patch';
227
+ return 'prerelease';
228
+ }
229
+ function parseCoarseSemver(version) {
230
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
231
+ if (!match)
232
+ return null;
233
+ const major = Number(match[1]);
234
+ const minor = Number(match[2]);
235
+ const patch = Number(match[3]);
236
+ if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) {
237
+ return null;
238
+ }
239
+ return { major, minor, patch };
240
+ }
241
+ //# sourceMappingURL=checker.js.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * — Auto-update channel + last-check persistence.
3
+ *
4
+ * Two pieces of disk state are managed here:
5
+ *
6
+ * 1. **Channel selection** — `~/.pugi/config.json::updateChannel`.
7
+ * Persisted across sessions so `pugi update` keeps polling the
8
+ * same track the operator opted into via `pugi update --channel
9
+ * <name>`. Mirrors the read/write pattern used by
10
+ * `core/permissions/state.ts::getGlobalDefaultMode` (passthrough
11
+ * schema, atomic tmp+rename, defensive parse).
12
+ *
13
+ * 2. **Last-check timestamp** — `~/.pugi/.last-update-check` (ISO
14
+ * string, single-line). Read by the cold-start banner gate so
15
+ * operators only see the "update available" hint once per
16
+ * `UPDATE_CHECK_INTERVAL_HOURS` (default 24h). Living on its own
17
+ * file (NOT a JSON object inside config.json) is intentional:
18
+ * the timestamp is a hot path — every CLI invocation touches it —
19
+ * and a single-line read+write is materially faster than the
20
+ * JSON parse + serialise of the broader config doc, with no
21
+ * schema coupling cost.
22
+ *
23
+ * Module contract:
24
+ *
25
+ * - Every file path resolver accepts a `homeDir` override so the
26
+ * test suite can drive the module through a per-test mkdtemp
27
+ * directory without polluting the real `~/.pugi/`.
28
+ *
29
+ * - Parse / read helpers NEVER throw on a malformed file. A
30
+ * corrupted JSON blob, a missing field, or an unreadable file all
31
+ * collapse to "no persisted value" so the next layer (the CLI
32
+ * flag or the hard default `beta`) takes over. A future-self
33
+ * debugging an update flow against a corrupt config never has the
34
+ * CLI crash on them.
35
+ *
36
+ * - Write helpers use the atomic tmp+rename idiom so a kill mid-
37
+ * write never produces a half-flushed JSON document. The
38
+ * timestamp file is small enough that POSIX `rename` is itself
39
+ * atomic in practice, but we keep the idiom uniform with the
40
+ * config write so reviewers do not have to context-switch.
41
+ */
42
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
43
+ import { homedir } from 'node:os';
44
+ import { resolve, dirname } from 'node:path';
45
+ import { z } from 'zod';
46
+ import { DEFAULT_UPDATE_CHANNEL, UPDATE_CHANNELS, } from './channels.js';
47
+ /**
48
+ * Default rate-limit window between registry probes. Operators see the
49
+ * cold-start banner at most once per window. Override per call via
50
+ * `shouldCheckForUpdate({ intervalHours })` — the cron-style scheduler
51
+ * passes 0 to force a check on every invocation, the doctor probe
52
+ * passes 24 to match the operator-visible cadence.
53
+ */
54
+ export const UPDATE_CHECK_INTERVAL_HOURS = 24;
55
+ /** Filename of the per-user channel + misc config. Mirrors L6 / L25. */
56
+ const CONFIG_FILE = '.pugi/config.json';
57
+ /** Filename of the standalone last-check ISO timestamp. */
58
+ const LAST_CHECK_FILE = '.pugi/.last-update-check';
59
+ /**
60
+ * Zod schema for the channel slice of `~/.pugi/config.json`. The
61
+ * passthrough lets sibling skills (L6 `defaultPermissionMode`, L25
62
+ * onboarding marker, etc.) coexist in the same JSON document without
63
+ * dropping their fields on a channel write.
64
+ */
65
+ const channelConfigSchema = z
66
+ .object({
67
+ updateChannel: z.enum(['stable', 'beta', 'canary']).optional(),
68
+ })
69
+ .partial()
70
+ .passthrough();
71
+ /**
72
+ * Resolve the absolute path of the per-user config file. Defaults to
73
+ * the real home dir, but every caller in the spec passes an explicit
74
+ * tmpdir so the persisted writes never escape the test sandbox.
75
+ */
76
+ export function configPath(homeDir = homedir()) {
77
+ return resolve(homeDir, CONFIG_FILE);
78
+ }
79
+ /**
80
+ * Resolve the absolute path of the single-line last-check file.
81
+ */
82
+ export function lastCheckPath(homeDir = homedir()) {
83
+ return resolve(homeDir, LAST_CHECK_FILE);
84
+ }
85
+ /**
86
+ * Read the persisted channel selection. Returns `null` when the
87
+ * config file is absent, the field is unset, or the file is unparse-
88
+ * able. The caller layers in the CLI flag + the hard default
89
+ * `DEFAULT_UPDATE_CHANNEL`.
90
+ *
91
+ * Defensive parse is intentional — a half-written config from a
92
+ * crashed session should never block `pugi update` from finishing the
93
+ * channel switch.
94
+ */
95
+ export function getUpdateChannel(homeDir = homedir()) {
96
+ const path = configPath(homeDir);
97
+ if (!existsSync(path))
98
+ return null;
99
+ try {
100
+ const raw = readFileSync(path, 'utf8');
101
+ const parsed = channelConfigSchema.parse(JSON.parse(raw));
102
+ return parsed.updateChannel ?? null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Resolve the effective channel for an invocation. Resolution order:
110
+ *
111
+ * 1. `cliFlag` (when provided + parses to a known channel).
112
+ * 2. `~/.pugi/config.json::updateChannel`.
113
+ * 3. `DEFAULT_UPDATE_CHANNEL` (currently `beta`).
114
+ *
115
+ * An invalid `cliFlag` (e.g. `--channel yolo`) falls through to the
116
+ * next layer rather than crashing — the dispatcher already validates
117
+ * the flag up front and surfaces a deterministic error for unknown
118
+ * names. This helper exists for code paths (the doctor probe, the
119
+ * cold-start banner) where no CLI flag is in play and a silent fall-
120
+ * through is the correct behaviour.
121
+ */
122
+ export function resolveEffectiveChannel(options = {}) {
123
+ const cli = options.cliFlag;
124
+ if (cli && typeof cli === 'string') {
125
+ const trimmed = cli.trim().toLowerCase();
126
+ for (const channel of UPDATE_CHANNELS) {
127
+ if (channel === trimmed)
128
+ return channel;
129
+ }
130
+ }
131
+ const persisted = getUpdateChannel(options.homeDir ?? homedir());
132
+ if (persisted)
133
+ return persisted;
134
+ return DEFAULT_UPDATE_CHANNEL;
135
+ }
136
+ /**
137
+ * Persist the channel to `~/.pugi/config.json::updateChannel`. Creates
138
+ * `~/.pugi/` when missing; preserves any unrelated keys in the file
139
+ * (passthrough schema). Atomic tmp+rename so a kill mid-write never
140
+ * leaves the config half-flushed.
141
+ */
142
+ export function setUpdateChannel(channel, homeDir = homedir()) {
143
+ const path = configPath(homeDir);
144
+ mkdirSync(dirname(path), { recursive: true });
145
+ const existing = existsSync(path)
146
+ ? safeParseObject(readFileSync(path, 'utf8'))
147
+ : {};
148
+ const next = { ...existing, updateChannel: channel };
149
+ const tmpPath = `${path}.tmp`;
150
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, {
151
+ encoding: 'utf8',
152
+ mode: 0o600,
153
+ });
154
+ renameSync(tmpPath, path);
155
+ }
156
+ /**
157
+ * Read the ISO timestamp of the most recent registry probe. Returns
158
+ * `null` when the file is absent or the contents do not parse as a
159
+ * valid Date. The caller treats `null` as "never checked" and runs an
160
+ * immediate probe.
161
+ */
162
+ export function readLastCheckedAt(homeDir = homedir()) {
163
+ const path = lastCheckPath(homeDir);
164
+ if (!existsSync(path))
165
+ return null;
166
+ try {
167
+ const raw = readFileSync(path, 'utf8').trim();
168
+ if (raw.length === 0)
169
+ return null;
170
+ const ts = Date.parse(raw);
171
+ if (!Number.isFinite(ts))
172
+ return null;
173
+ return new Date(ts);
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ /**
180
+ * Persist the timestamp of the most recent registry probe. Atomic
181
+ * tmp+rename for the same reasons as `setUpdateChannel` — the file is
182
+ * small but we keep the idiom uniform.
183
+ */
184
+ export function writeLastCheckedAt(when, homeDir = homedir()) {
185
+ const path = lastCheckPath(homeDir);
186
+ mkdirSync(dirname(path), { recursive: true });
187
+ const tmpPath = `${path}.tmp`;
188
+ writeFileSync(tmpPath, `${when.toISOString()}\n`, {
189
+ encoding: 'utf8',
190
+ mode: 0o600,
191
+ });
192
+ renameSync(tmpPath, path);
193
+ }
194
+ /**
195
+ * Decide whether the cold-start hint should run a fresh registry
196
+ * probe. Returns true when the last probe was more than
197
+ * `intervalHours` ago OR the timestamp file is missing entirely.
198
+ *
199
+ * Pass `intervalHours = 0` to force a probe on every call (used by
200
+ * the `pugi update --check` JSON surface where the operator is
201
+ * explicitly asking for a fresh result).
202
+ */
203
+ export function shouldCheckForUpdate(options = {}) {
204
+ const now = options.now ? options.now() : Date.now();
205
+ const intervalHours = options.intervalHours ?? UPDATE_CHECK_INTERVAL_HOURS;
206
+ if (intervalHours <= 0)
207
+ return true;
208
+ const last = readLastCheckedAt(options.homeDir ?? homedir());
209
+ if (!last)
210
+ return true;
211
+ const ageMs = now - last.getTime();
212
+ const windowMs = intervalHours * 60 * 60 * 1_000;
213
+ return ageMs >= windowMs;
214
+ }
215
+ /**
216
+ * Defensive helper — parse JSON to an object; non-object payloads
217
+ * (top-level array, primitive) collapse to an empty object so the
218
+ * channel-write merge does not surface a TypeError. Mirrors the
219
+ * `safeParseObject` in `core/permissions/state.ts` — duplicating the
220
+ * 10 lines is cheaper than threading a shared util module through
221
+ * two unrelated leak surfaces.
222
+ */
223
+ function safeParseObject(raw) {
224
+ try {
225
+ const parsed = JSON.parse(raw);
226
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
227
+ return parsed;
228
+ }
229
+ return {};
230
+ }
231
+ catch {
232
+ return {};
233
+ }
234
+ }
235
+ //# sourceMappingURL=state.js.map