@tekmidian/pai 1.0.0 → 1.0.3

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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +67 -5
  3. package/dist/aibroker-client-BxzeEHPF.mjs +3 -0
  4. package/dist/{aibroker-client-CXEYBzbf.mjs → aibroker-client-CQPGqLxL.mjs} +1 -1
  5. package/dist/atomic-file-BorMa4T3.mjs +21 -0
  6. package/dist/{auto-route-D5SXrMpB.mjs → auto-route-B7Yu6Uam.mjs} +3 -3
  7. package/dist/auto-route-CjjQqU1o.mjs +11 -0
  8. package/dist/budget-Dod6rnA9.mjs +15 -0
  9. package/dist/{caller-identity-CVTuiLSs.mjs → caller-identity-yLFb7CKq.mjs} +6 -5
  10. package/dist/{capacity-DaXrVj3J.mjs → capacity-DEveQdFV.mjs} +3 -3
  11. package/dist/{chain-BcQlIlEd.mjs → chain-7n9Gx56r.mjs} +5 -5
  12. package/dist/cli/index.mjs +1628 -370
  13. package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-BBHeyEZI.mjs} +84 -42
  14. package/dist/cockpit-actions-DR9X514g.mjs +20 -0
  15. package/dist/cockpit-memory--dnShXBA.mjs +20 -0
  16. package/dist/{config-BSXKyeJ1.mjs → config-Bu1_vbY8.mjs} +42 -3
  17. package/dist/{config-B955yYmo.mjs → config-Cxi-aBf4.mjs} +2 -2
  18. package/dist/{config-TaWmdTLx.mjs → config-DtENJ6cL.mjs} +1 -1
  19. package/dist/{config-D5chi2LO.mjs → config-XpZWOYOG.mjs} +2 -2
  20. package/dist/{controls-BGkK_Bdi.mjs → controls-CPoeudou.mjs} +37 -22
  21. package/dist/daemon/index.mjs +43 -42
  22. package/dist/daemon-DKdBNKfa.mjs +45 -0
  23. package/dist/{daemon-CorGh6Ub.mjs → daemon-DPAo6WTa.mjs} +103 -55
  24. package/dist/daemon-mcp/index.mjs +40 -39
  25. package/dist/{define-DGFu-sCf.mjs → define-CjJsmq-6.mjs} +1 -1
  26. package/dist/{detect-C1d5cUkT.mjs → detect-9CZNO0yf.mjs} +2 -2
  27. package/dist/detect-D1ziFFfA.mjs +10 -0
  28. package/dist/{embed-pass-CeQD8kRE.mjs → embed-pass-DIWjA68P.mjs} +5 -5
  29. package/dist/{embedding-gate-BHuBsgHD.mjs → embedding-gate-6rZrWRXa.mjs} +5 -5
  30. package/dist/{embedding-gate-Z7CJSOJk.mjs → embedding-gate-Cnk4YCyX.mjs} +3 -3
  31. package/dist/{embeddings-D4t6L-EH.mjs → embeddings-BVq3esBF.mjs} +1 -1
  32. package/dist/{embeddings-OF9Pt4by.mjs → embeddings-CIkJBKd-.mjs} +1 -1
  33. package/dist/{endreason-x1KOSrcO.mjs → endreason-C577xIQz.mjs} +7 -6
  34. package/dist/{env-DArqGq-Z.mjs → env-BE8mrsxr.mjs} +1 -1
  35. package/dist/factory-Bsib-qDG.mjs +9 -0
  36. package/dist/{factory-BtjbQ0np.mjs → factory-DjAAsqr_.mjs} +15 -13
  37. package/dist/{fallback-BIyEpPsQ.mjs → fallback-DFlItw1Z.mjs} +5 -5
  38. package/dist/{fmt-age-BiSHrTKN.mjs → fmt-age-ByDkvkoU.mjs} +1 -1
  39. package/dist/{health-C6jZWvRG.mjs → health-Dnym-qcT.mjs} +4 -4
  40. package/dist/{helpers-DFmYRCOa.mjs → helpers-BD6V-p9w.mjs} +2 -2
  41. package/dist/helpers-CMGjy_0N.mjs +10 -0
  42. package/dist/{helpers-CJpb_MNB.mjs → helpers-CRuBUas3.mjs} +87 -38
  43. package/dist/hooks/capture-all-events.mjs +52 -11
  44. package/dist/hooks/capture-session-summary.mjs +64 -47
  45. package/dist/hooks/capture-tool-output.mjs +49 -8
  46. package/dist/hooks/context-compression-hook.mjs +30 -10
  47. package/dist/hooks/initialize-session.mjs +1 -0
  48. package/dist/hooks/load-core-context.mjs +1 -1
  49. package/dist/hooks/load-project-context.mjs +32 -29
  50. package/dist/hooks/security-validator.mjs +45 -13
  51. package/dist/hooks/stop-hook.mjs +54 -16
  52. package/dist/hooks/supervision-banner.mjs +9 -0
  53. package/dist/hooks/sync-todo-to-md.mjs +147 -113
  54. package/dist/hooks/worker-edit-repair.mjs +2 -1
  55. package/dist/hooks/worker-guard.mjs +40 -16
  56. package/dist/hooks/worker-note.mjs +12 -5
  57. package/dist/hooks/worker-path-repair.mjs +1 -1
  58. package/dist/hooks/worker-pool-host.mjs +329 -231
  59. package/dist/hooks/worker-status-line.mjs +195 -114
  60. package/dist/index.d.mts +18 -0
  61. package/dist/index.mjs +12 -12
  62. package/dist/{indexer-backend-BM2CUSu8.mjs → indexer-backend-Cll7le4P.mjs} +149 -96
  63. package/dist/{ipc-client-BMqWw_gv.mjs → ipc-client-CaiGJnSM.mjs} +1 -1
  64. package/dist/{ipc-client-YYDD6zzU.mjs → ipc-client-ZRmDKd5Z.mjs} +1 -1
  65. package/dist/kill-tree-BG7dRVtp.mjs +10 -0
  66. package/dist/{kill-tree-BjYz5BxK.mjs → kill-tree-EWYaEk5v.mjs} +8 -8
  67. package/dist/{launch-CLp0OJxV.mjs → launch-CXVlZjHt.mjs} +8 -8
  68. package/dist/{launch-DgqQdAjI.mjs → launch-Jh5NVeNe.mjs} +3 -3
  69. package/dist/local-candidate-DLdwQt0s.mjs +112 -0
  70. package/dist/local-instances-DOxSgT_L.mjs +210 -0
  71. package/dist/local-instances-ZeRkkuS5.mjs +6 -0
  72. package/dist/{local-tiers-DPZYqv9m.mjs → local-tiers-BE4jGaMa.mjs} +2 -2
  73. package/dist/{local-tiers-DnBIAkbK.mjs → local-tiers-DNMI5W86.mjs} +3 -3
  74. package/dist/{main-config-ops-C0SflOWk.mjs → main-config-ops-BEMpuhDz.mjs} +3 -3
  75. package/dist/{main-resolver-DVPpSWVy.mjs → main-resolver-3Ns1HPmS.mjs} +18 -17
  76. package/dist/{migrate-BUJHadA8.mjs → migrate-DrA_OwlZ.mjs} +45 -14
  77. package/dist/min-max-DrSRUaWQ.mjs +16 -0
  78. package/dist/{model-inventory-Dasber3S.mjs → model-inventory-BFVSNFT3.mjs} +13 -12
  79. package/dist/{models-add-Bt7aUHDs.mjs → models-add-C1QIc1DA.mjs} +39 -38
  80. package/dist/operator-BTqvTIc8.mjs +7 -0
  81. package/dist/{operator-DZxcP-g8.mjs → operator-CyuB3aVw.mjs} +3 -3
  82. package/dist/{pai-marker-kIYuNt3c.mjs → pai-marker-BcZyJguM.mjs} +7 -6
  83. package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-DKQt71s4.mjs} +7 -7
  84. package/dist/{paths-DoRuFc60.mjs → paths-CFHTloSe.mjs} +3 -3
  85. package/dist/{paths-DoIyRnhx.mjs → paths-CHifubDh.mjs} +3 -3
  86. package/dist/{planner-DLGOYlVb.mjs → planner-BmFACdLa.mjs} +31 -30
  87. package/dist/{pool-CZ5MZaXg.mjs → pool-Dh-q3iJi.mjs} +45 -6
  88. package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-BggU8GRp.mjs} +5 -5
  89. package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DQ6OzRy2.mjs} +6 -6
  90. package/dist/postgres-BdBNkL_l.mjs +7 -0
  91. package/dist/{postgres-CDnd_k5f.mjs → postgres-C6ti4haA.mjs} +55 -34
  92. package/dist/{project-setup-DM4npnL3.mjs → project-setup-jvfONJTQ.mjs} +131 -74
  93. package/dist/providers-BeDiVEb-.mjs +16 -0
  94. package/dist/{providers-UsKtLcUR.mjs → providers-CblTFrhz.mjs} +6 -6
  95. package/dist/{query-feedback-I-txSnld.mjs → query-feedback-BRfPtqki.mjs} +2 -2
  96. package/dist/registrable-BzH4TObg.mjs +9 -0
  97. package/dist/{registrable-LrRIVYQc.mjs → registrable-DpyTtajg.mjs} +4 -4
  98. package/dist/{render-BDbb4CF-.mjs → render-BYmQkuLd.mjs} +8 -8
  99. package/dist/render-CoMvwMPs.mjs +13 -0
  100. package/dist/{reranker-B4r4vykM.mjs → reranker-8g6xMwP7.mjs} +1 -1
  101. package/dist/{reranker-DxxMCtA0.mjs → reranker-CqCbpGnX.mjs} +1 -1
  102. package/dist/root-FtJsxYnm.mjs +36 -0
  103. package/dist/{root-BMeJuHOb.mjs → root-Xglu0OPD.mjs} +11 -11
  104. package/dist/route-table-B7zpoypi.mjs +15 -0
  105. package/dist/{router-L8Nu-OHF.mjs → router-CQSc3lk3.mjs} +17 -7
  106. package/dist/{router-D6Ec6Fid.mjs → router-DwHJRgG9.mjs} +1 -1
  107. package/dist/{routing-CUyiglfu.mjs → routing-BAAST9vT.mjs} +27 -308
  108. package/dist/{routing-ZuGlc0ge.mjs → routing-CENt4HH1.mjs} +13 -12
  109. package/dist/{run-ulAi5dFD.mjs → run-CSC7mgPf.mjs} +35 -33
  110. package/dist/{scan-B7I0gPe-.mjs → scan-C_Y5W9py.mjs} +6 -6
  111. package/dist/{local-candidate-jOjTNlOL.mjs → scheduler-CtUyWJ3O.mjs} +7 -109
  112. package/dist/scheduler-dmHXoRvm.mjs +3 -0
  113. package/dist/{scope-D5VSQxe4.mjs → scope-D-h6F6DN.mjs} +4 -4
  114. package/dist/{search-CPoE0830.mjs → search-RSyeQqj1.mjs} +18 -18
  115. package/dist/{session-CNXF6DLP.mjs → session-8iqQDkqb.mjs} +55 -31
  116. package/dist/session-BeqAbNPd.mjs +35 -0
  117. package/dist/session-scan-BJI_ZO8M.mjs +9 -0
  118. package/dist/{session-scan-BDLEc4w1.mjs → session-scan-W-gbwJHM.mjs} +6 -6
  119. package/dist/{sources-CjhUKKtO.mjs → sources-ESUkZaWv.mjs} +1 -1
  120. package/dist/{status-DYydKQUI.mjs → status-Dm4dYciN.mjs} +4 -4
  121. package/dist/{status-CvVi9cDf.mjs → status-ggJ7lgVg.mjs} +17 -9
  122. package/dist/{stop-words-SdSL1WKH.mjs → stop-words-DSEYQdNe.mjs} +1 -1
  123. package/dist/{test-isolation-CoDeOVXb.mjs → test-isolation-lqSmYxr0.mjs} +1 -1
  124. package/dist/{timeouts-OwEONmc8.mjs → timeouts-1MLooEBB.mjs} +1 -1
  125. package/dist/{tool-errors-AeRQuG3Y.mjs → tool-errors-7E13StWd.mjs} +4 -3
  126. package/dist/{tool-errors-R5E0fCgf.mjs → tool-errors-CJWODvlo.mjs} +6 -6
  127. package/dist/viewer-CqozWW39.mjs +22 -0
  128. package/dist/{viewer-DtWj5Vti.mjs → viewer-mDXUNHNy.mjs} +15 -15
  129. package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
  130. package/dist/{worker-config-BLqa1yiD.mjs → worker-config-CJhEMk19.mjs} +294 -92
  131. package/dist/worker-config-DESkIjMF.mjs +41 -0
  132. package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
  133. package/dist/{worktree-zmRf556h.mjs → worktree-CRs2gyg0.mjs} +37 -7
  134. package/dist/{zettelkasten-nigq7fqs.mjs → zettelkasten-B_rRRRvD.mjs} +5 -5
  135. package/dist/{zettelkasten-Cr_Xj0mG.mjs → zettelkasten-BzMm8REH.mjs} +29 -19
  136. package/docs/README.md +2 -0
  137. package/docs/cockpit.md +10 -4
  138. package/docs/commands/README.md +4 -4
  139. package/docs/commands/backup.md +2 -0
  140. package/docs/commands/install.md +29 -4
  141. package/docs/commands/providers.md +1 -1
  142. package/docs/commands/restore.md +4 -0
  143. package/docs/commands/setup.md +29 -4
  144. package/docs/commands/worker.md +1 -1
  145. package/docs/config.md +2 -0
  146. package/docs/features.md +299 -0
  147. package/docs/images/architecture.drawio +51 -0
  148. package/docs/images/architecture.png +0 -0
  149. package/docs/images/architecture.svg +3 -0
  150. package/docs/images/cockpit-01-system-tokens.jpg +0 -0
  151. package/docs/images/cockpit-02-totals-cache.jpg +0 -0
  152. package/docs/images/cockpit-03-local-servers-models-pool.jpg +0 -0
  153. package/docs/images/cockpit-04-workers-memory.jpg +0 -0
  154. package/docs/images/cockpit-05-performance.jpg +0 -0
  155. package/docs/images/cockpit-06-runs-speed-routing.jpg +0 -0
  156. package/docs/images/cockpit-07-settings-registry.jpg +0 -0
  157. package/docs/install/index.md +29 -0
  158. package/docs/three-tier-coding.md +1 -1
  159. package/gemini-extension.json +1 -1
  160. package/package.json +1 -5
  161. package/pai-plugin.json +1 -1
  162. package/plugins/context-preservation/plugin.json +1 -1
  163. package/plugins/core/plugin.json +1 -1
  164. package/plugins/core/skills/setup/SKILL.md +41 -9
  165. package/plugins/creative/plugin.json +1 -1
  166. package/plugins/observability/plugin.json +1 -1
  167. package/plugins/productivity/plugin.json +1 -1
  168. package/plugins/semantic-search/plugin.json +1 -1
  169. package/plugins/ui/plugin.json +1 -1
  170. package/plugins/zettelkasten/plugin.json +1 -1
  171. package/src/hooks/session-autosave.sh +8 -1
  172. package/src/hooks/session-stop.sh +8 -1
  173. package/src/hooks/ts/capture-all-events.ts +7 -12
  174. package/src/hooks/ts/lib/context-fill.ts +2 -1
  175. package/src/hooks/ts/lib/path-repair.ts +1 -1
  176. package/src/hooks/ts/lib/project-utils/notify.ts +1 -0
  177. package/src/hooks/ts/lib/project-utils/session-notes.ts +23 -6
  178. package/src/hooks/ts/lib/raw-capture.ts +36 -0
  179. package/src/hooks/ts/lib/session-summary.ts +129 -0
  180. package/src/hooks/ts/lib/todo-sync.ts +120 -0
  181. package/src/hooks/ts/lib/worker-git-guard.ts +1 -1
  182. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +6 -10
  183. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +5 -105
  184. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  185. package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -14
  186. package/src/hooks/ts/session-end/capture-session-summary.ts +20 -128
  187. package/src/hooks/ts/session-start/load-core-context.ts +3 -3
  188. package/src/hooks/ts/session-start/load-project-context.ts +18 -22
  189. package/src/hooks/ts/stop/stop-hook.ts +13 -5
  190. package/src/hooks/ztk-auto.sh +7 -2
  191. package/dist/aibroker-client-JjE74jo0.mjs +0 -3
  192. package/dist/auto-route-BRk8NEFv.mjs +0 -11
  193. package/dist/budget-DuhQ2zzG.mjs +0 -14
  194. package/dist/cockpit-actions-DcQ-ucU8.mjs +0 -19
  195. package/dist/cockpit-memory-KV45PPO2.mjs +0 -19
  196. package/dist/daemon-C7yt_ptC.mjs +0 -44
  197. package/dist/detect-DRUkmHnb.mjs +0 -10
  198. package/dist/factory-DAGG9_n8.mjs +0 -9
  199. package/dist/helpers-DPJEHPq6.mjs +0 -10
  200. package/dist/kill-tree-LV0Q7Mte.mjs +0 -10
  201. package/dist/operator-CfL0HiBK.mjs +0 -7
  202. package/dist/postgres-DgqWyqhN.mjs +0 -7
  203. package/dist/providers-CTu2s1z2.mjs +0 -15
  204. package/dist/registrable-D9aNZOTh.mjs +0 -9
  205. package/dist/render-BSfVlYHj.mjs +0 -13
  206. package/dist/root-xI9DqVIe.mjs +0 -35
  207. package/dist/route-table-HjvnLVEU.mjs +0 -14
  208. package/dist/session-BJHzmUg2.mjs +0 -34
  209. package/dist/session-scan-YORKE1NO.mjs +0 -9
  210. package/dist/viewer-B8709jDN.mjs +0 -22
  211. package/dist/worker-config-D8tRuvo7.mjs +0 -40
  212. package/docs/images/cockpit-limits-routing.jpg +0 -0
  213. package/docs/images/cockpit-models.jpg +0 -0
  214. package/docs/images/cockpit-usage.jpg +0 -0
  215. package/docs/images/cockpit-workers-system.jpg +0 -0
  216. package/scripts/build-docs.mjs +0 -41
  217. package/scripts/build-hooks.mjs +0 -311
  218. package/scripts/build-skill-stubs.mjs +0 -216
  219. package/scripts/copy-defaults.mjs +0 -46
@@ -1,75 +1,79 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as warn$1, a as fmtDate, c as ok$1, d as scaffoldProjectDirs, f as shortenPath, g as stripAnsi, h as smartDecodeDir, i as err, l as renderTable$1, m as slugify, n as dim$1, o as header, r as encodeDir, s as now, t as bold$1, u as resolvePath } from "../utils-J2b9y8PC.mjs";
3
3
  import { _ as resolvePaiFile, a as paiCacheDir, c as PaiFileMigrationError, d as defaultPaiHomeDir, f as legacyConfigDir, g as paiHomePath, h as paiHomeDir, l as claudeHomeDir, m as migratePaiFile, o as paiSocketPath, p as migratePaiDir, r as daemonLogPath, s as schedulerLogPath, u as claudeProjectsDir } from "../runtime-paths-WsO9sJFc.mjs";
4
- import { a as slugify$1, i as parseSessionFilename } from "../migrate-BUJHadA8.mjs";
5
- import { I as LOOPBACK_HOST, Q as relocateWorkersYaml, _t as contextWindowFromModelId, c as WorkersConfigError, f as escalationCloud, ht as DEFAULT_CONTEXT_WINDOW, j as readWorkersSection, m as isLocalProvider, p as getProviderOrNative, t as ANTHROPIC_NATIVE, tt as workersYamlPath, v as nativeAnthropicModels, vt as stripModelVariant, wt as appendLedger } from "../config-D5chi2LO.mjs";
6
- import { A as postgresConnectionString, C as migrateMainConfigToYaml, D as readJsonStrict, O as writeJsonAtomic, S as MainConfigError, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, k as DEFAULT_PG_CONNECTION_TIMEOUT_MS, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath, w as resolvedMainConfigPath } from "../config-BSXKyeJ1.mjs";
7
- import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-DoIyRnhx.mjs";
8
- import "../worktree-zmRf556h.mjs";
9
- import { B as loadStatusConfig, G as writeSectionToForge, L as formatStale, U as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, y as loadStatuses } from "../status-CvVi9cDf.mjs";
10
- import { S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as writeClaudeJson, k as readClaudeJson, v as readPoolState, w as expandMcpNames } from "../pool-CZ5MZaXg.mjs";
11
- import { a as unregistrableReason, s as worktreeReason } from "../registrable-LrRIVYQc.mjs";
12
- import "../pai-marker-kIYuNt3c.mjs";
13
- import "../helpers-CJpb_MNB.mjs";
14
- import { t as applyRecencyBoost } from "../search-CPoE0830.mjs";
15
- import { t as dropItermBundleId } from "../env-DArqGq-Z.mjs";
16
- import { a as ownerSessionName, d as terminalHasName } from "../scope-D5VSQxe4.mjs";
17
- import { $n as parseSince, Cn as allSetups, Dn as loadSaved, E as budgetCache, Et as routerPort, F as LimitsCache, Gn as recordDuration, Hn as parkConfigFrom, Jn as appendUsage, Kn as aggregate, Mn as setupsPath, Tt as routerHandler, U as computeInventory, Un as setParkConfigSource, Ut as sessionStartTarget, W as currentInventory, Wn as sharedParker, Xn as formatStats, Y as inventoryText, Yn as formatSessionStats, at as readInventoryInput, bt as routeVerdictLine, er as readUsage, kn as saveSetup, ln as readProfiles, m as readRoutingState, mn as localProviderForDelegation, on as SESSION_NAME_ENV, pt as isLoopbackProvider, qn as aggregateBySession, vn as providerHostPort, wn as downLocalProviders, x as SESSION_WORKER_PROVIDER_ENV } from "../routing-CUyiglfu.mjs";
18
- import "../tool-errors-AeRQuG3Y.mjs";
19
- import "../wait-BMUR-U-U.mjs";
20
- import "../local-tiers-DPZYqv9m.mjs";
21
- import { _ as poolSlots, a as enqueue, d as removeTicket, f as renderBuildQueue, i as buildsDir, l as patchTicket, m as tryStart, n as buildLogPath, o as getTicket, r as buildNext, s as listBuilds, v as slotsFor } from "../caller-identity-CVTuiLSs.mjs";
22
- import { c as mirrorTo, d as schedConfigFrom, g as sharedScheduler, h as setSchedConfigSource, n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-jOjTNlOL.mjs";
23
- import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-DaXrVj3J.mjs";
24
- import { n as TAILSCALE_STATUS_TIMEOUT_MS } from "../timeouts-OwEONmc8.mjs";
25
- import { n as OPT, t as CliError } from "../define-DGFu-sCf.mjs";
26
- import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-BtjbQ0np.mjs";
27
- import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-CDnd_k5f.mjs";
28
- import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-BHuBsgHD.mjs";
29
- import "../embeddings-D4t6L-EH.mjs";
30
- import "../router-L8Nu-OHF.mjs";
31
- import { t as detectProject } from "../detect-C1d5cUkT.mjs";
32
- import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-DFmYRCOa.mjs";
33
- import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-B7I0gPe-.mjs";
34
- import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-CNXF6DLP.mjs";
35
- import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-DgqQdAjI.mjs";
36
- import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-C6jZWvRG.mjs";
37
- import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-CLp0OJxV.mjs";
38
- import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-BMeJuHOb.mjs";
39
- import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CXEYBzbf.mjs";
40
- import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-BDLEc4w1.mjs";
41
- import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-ulAi5dFD.mjs";
42
- import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, jt as runPaneExec, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage, yt as psTable } from "../controls-BGkK_Bdi.mjs";
43
- import "../endreason-x1KOSrcO.mjs";
44
- import { N as endBuildWait, j as beginBuildWait } from "../render-BDbb4CF-.mjs";
45
- import "../operator-DZxcP-g8.mjs";
46
- import "../kill-tree-BjYz5BxK.mjs";
47
- import { s as poolReloadLive } from "../pool-cli-BxNSPzzY.mjs";
48
- import "../chain-BcQlIlEd.mjs";
49
- import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-DM4npnL3.mjs";
50
- import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-OGYrOAik.mjs";
51
- import "../viewer-DtWj5Vti.mjs";
52
- import { g as setWorkersEnabled, s as describeProviders } from "../providers-UsKtLcUR.mjs";
53
- import { $ as createEndpointHealth, G as cachedSessionMap, J as setActionHooks, K as cachedWorkerWindows, Q as exportDefaultLayout, R as installWorkers, U as SystemMonitor, W as cachedRows, X as setRuntimeSampler, Y as setEndpointHealth, Z as warmCockpit, et as loadSessionMap, n as WORKER_CONFIG_COMMANDS, nt as sessionDisplayName, q as handleCockpit, rt as withWorkerAttribution, st as providersCommand, t as PROVIDER_EDIT_COMMANDS, tt as loadWorkerWindows, z as mergeSettings } from "../worker-config-BLqa1yiD.mjs";
54
- import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-C0SflOWk.mjs";
55
- import { I as cockpitProbeSettings, L as acquireRun, R as readRun, w as readRuntimeCommands } from "../cockpit-actions-ODmt8yF9.mjs";
56
- import { n as PaiClient } from "../ipc-client-BMqWw_gv.mjs";
57
- import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-CeQD8kRE.mjs";
58
- import "../fallback-BIyEpPsQ.mjs";
59
- import { At as readQuiet, Bt as appendCheckpointToNote, C as migrateSummaryCooldowns, Et as totalUsageTokens, Gt as restartRouter, Ht as findLatestNote, It as resolveDaemonPid, Nt as writeQuiet, Ot as describeQuiet, Pt as LAUNCHD_LABEL, Rt as readContextHandoverCache, St as loadSessionKeepaliveState, Tt as parseSessionUsage, Ut as findNotesDir$1, Vt as applyContinue, Wt as readBodyFile, bt as reapJob, ht as obsidianEnabled, i as reexecBackground, kt as nextOccurrence, mt as OBSIDIAN_DISABLED_MESSAGE, o as backfillKgFromNotes, s as migrateKgBackfillState, v as migrateWorkQueue, vt as migrateIdentityFile, wt as sessionKeepaliveLedgerSummary, yt as jobPaths } from "../indexer-backend-BM2CUSu8.mjs";
60
- import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-I-txSnld.mjs";
61
- import { appendFileSync, chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { a as slugify$1, i as parseSessionFilename } from "../migrate-DrA_OwlZ.mjs";
5
+ import { I as LOOPBACK_HOST, Q as relocateWorkersYaml, _t as contextWindowFromModelId, c as WorkersConfigError, f as escalationCloud, ht as DEFAULT_CONTEXT_WINDOW, j as readWorkersSection, m as isLocalProvider, p as getProviderOrNative, t as ANTHROPIC_NATIVE, tt as workersYamlPath, v as nativeAnthropicModels, vt as stripModelVariant, wt as appendLedger } from "../config-XpZWOYOG.mjs";
6
+ import { A as assertPgName, C as migrateMainConfigToYaml, D as readJsonStrict, M as postgresConnectionString, N as postgresFullTarget, O as writeJsonAtomic, P as quoteIdent, S as MainConfigError, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, j as isContainerTarget, k as DEFAULT_PG_CONNECTION_TIMEOUT_MS, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath, w as resolvedMainConfigPath } from "../config-Bu1_vbY8.mjs";
7
+ import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-CHifubDh.mjs";
8
+ import "../worktree-CRs2gyg0.mjs";
9
+ import { B as loadStatusConfig, G as writeSectionToForge, L as formatStale, U as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, y as loadStatuses } from "../status-ggJ7lgVg.mjs";
10
+ import { A as readClaudeJson, N as writeClaudeJson, S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as registerMcpScript, k as mcpEntryRunsScript, v as readPoolState, w as expandMcpNames } from "../pool-Dh-q3iJi.mjs";
11
+ import { a as unregistrableReason, s as worktreeReason } from "../registrable-DpyTtajg.mjs";
12
+ import "../pai-marker-BcZyJguM.mjs";
13
+ import "../helpers-CRuBUas3.mjs";
14
+ import { t as applyRecencyBoost } from "../search-RSyeQqj1.mjs";
15
+ import { t as dropItermBundleId } from "../env-BE8mrsxr.mjs";
16
+ import { a as ownerSessionName, d as terminalHasName } from "../scope-D-h6F6DN.mjs";
17
+ import { An as saveSetup, Bn as aggregate, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Hn as appendUsage, In as parkConfigFrom, Jn as readUsage, Ln as setParkConfigSource, Nn as setupsPath, On as loadSaved, Rn as sharedParker, Tn as downLocalProviders, U as computeInventory, Un as formatSessionStats, Vn as aggregateBySession, W as currentInventory, Wn as formatStats, Wt as sessionStartTarget, Y as inventoryText, at as readInventoryInput, hn as localProviderForDelegation, m as readRoutingState, pt as isLoopbackProvider, qn as parseSince, sn as SESSION_NAME_ENV, un as readProfiles, wn as allSetups, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, yn as providerHostPort, zn as recordDuration } from "../routing-BAAST9vT.mjs";
18
+ import { n as minOf } from "../min-max-DrSRUaWQ.mjs";
19
+ import "../tool-errors-7E13StWd.mjs";
20
+ import "../wait-ByV0OyMZ.mjs";
21
+ import "../local-tiers-BE4jGaMa.mjs";
22
+ import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-yLFb7CKq.mjs";
23
+ import { d as setSchedConfigSource, f as sharedScheduler, i as mirrorTo, s as schedConfigFrom } from "../scheduler-CtUyWJ3O.mjs";
24
+ import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-DEveQdFV.mjs";
25
+ import "../local-instances-DOxSgT_L.mjs";
26
+ import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-DLdwQt0s.mjs";
27
+ import { n as TAILSCALE_STATUS_TIMEOUT_MS } from "../timeouts-1MLooEBB.mjs";
28
+ import { n as OPT, t as CliError } from "../define-CjJsmq-6.mjs";
29
+ import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-DjAAsqr_.mjs";
30
+ import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-C6ti4haA.mjs";
31
+ import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-6rZrWRXa.mjs";
32
+ import "../embeddings-BVq3esBF.mjs";
33
+ import "../router-CQSc3lk3.mjs";
34
+ import { t as detectProject } from "../detect-9CZNO0yf.mjs";
35
+ import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-BD6V-p9w.mjs";
36
+ import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-C_Y5W9py.mjs";
37
+ import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-8iqQDkqb.mjs";
38
+ import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-Jh5NVeNe.mjs";
39
+ import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-Dnym-qcT.mjs";
40
+ import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-CXVlZjHt.mjs";
41
+ import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-Xglu0OPD.mjs";
42
+ import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CQPGqLxL.mjs";
43
+ import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-W-gbwJHM.mjs";
44
+ import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-CSC7mgPf.mjs";
45
+ import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, jt as runPaneExec, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage, yt as psTable } from "../controls-CPoeudou.mjs";
46
+ import "../endreason-C577xIQz.mjs";
47
+ import { N as endBuildWait, j as beginBuildWait } from "../render-BYmQkuLd.mjs";
48
+ import "../operator-CyuB3aVw.mjs";
49
+ import "../kill-tree-EWYaEk5v.mjs";
50
+ import { s as poolReloadLive } from "../pool-cli-BggU8GRp.mjs";
51
+ import "../chain-7n9Gx56r.mjs";
52
+ import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-jvfONJTQ.mjs";
53
+ import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-DKQt71s4.mjs";
54
+ import "../viewer-mDXUNHNy.mjs";
55
+ import { g as setWorkersEnabled, s as describeProviders } from "../providers-CblTFrhz.mjs";
56
+ import { $ as exportDefaultLayout, B as mergeSettings, G as cachedRows, J as handleCockpit, K as cachedSessionMap, Q as warmCockpit, W as SystemMonitor, X as setEndpointHealth, Y as setActionHooks, Z as setRuntimeSampler, ct as providersCommand, et as createEndpointHealth, it as withWorkerAttribution, n as WORKER_CONFIG_COMMANDS, nt as loadWorkerWindows, q as cachedWorkerWindows, rt as sessionDisplayName, t as PROVIDER_EDIT_COMMANDS, tt as loadSessionMap, z as installWorkers } from "../worker-config-CJhEMk19.mjs";
57
+ import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-BEMpuhDz.mjs";
58
+ import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-BBHeyEZI.mjs";
59
+ import { n as PaiClient } from "../ipc-client-CaiGJnSM.mjs";
60
+ import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-DIWjA68P.mjs";
61
+ import "../fallback-DFlItw1Z.mjs";
62
+ import { At as readQuiet, Bt as appendCheckpointToNote, C as migrateSummaryCooldowns, Et as totalUsageTokens, Gt as restartRouter, Ht as findLatestNote, It as resolveDaemonPid, Nt as writeQuiet, Ot as describeQuiet, Pt as LAUNCHD_LABEL, Rt as readContextHandoverCache, St as loadSessionKeepaliveState, Tt as parseSessionUsage, Ut as findNotesDir$1, Vt as applyContinue, Wt as readBodyFile, bt as reapJob, ht as obsidianEnabled, i as reexecBackground, kt as nextOccurrence, mt as OBSIDIAN_DISABLED_MESSAGE, o as backfillKgFromNotes, s as migrateKgBackfillState, v as migrateWorkQueue, vt as migrateIdentityFile, wt as sessionKeepaliveLedgerSummary, yt as jobPaths } from "../indexer-backend-Cll7le4P.mjs";
63
+ import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-BRfPtqki.mjs";
64
+ import { appendFileSync, chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
65
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
62
66
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
63
67
  import chalk from "chalk";
64
68
  import { cpus, homedir, networkInterfaces, platform, userInfo } from "node:os";
65
69
  import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
66
70
  import { fileURLToPath } from "node:url";
67
- import { createHash, randomBytes, randomUUID } from "node:crypto";
68
71
  import http from "node:http";
72
+ import { createServer } from "node:net";
69
73
  import pg from "pg";
70
74
  import BetterSqlite3 from "better-sqlite3";
71
75
  import { createInterface } from "node:readline";
72
- import net, { createConnection } from "net";
76
+ import net$1, { createConnection as createConnection$1 } from "net";
73
77
  import { promisify } from "node:util";
74
78
  import { getEncoding } from "js-tiktoken";
75
79
 
@@ -897,7 +901,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
897
901
  });
898
902
  return {
899
903
  inventory: async () => {
900
- const { currentInventory } = await import("../model-inventory-Dasber3S.mjs");
904
+ const { currentInventory } = await import("../model-inventory-BFVSNFT3.mjs");
901
905
  return currentInventory();
902
906
  },
903
907
  sched,
@@ -912,7 +916,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
912
916
  }
913
917
  },
914
918
  async rssMb(port) {
915
- const { processTreeRssMb } = await import("../model-inventory-Dasber3S.mjs");
919
+ const { processTreeRssMb } = await import("../model-inventory-BFVSNFT3.mjs");
916
920
  const pid = (await exec("lsof", [
917
921
  "-ti",
918
922
  `tcp:${port}`,
@@ -1103,7 +1107,7 @@ function drainServers(servers, opts) {
1103
1107
  for (const server of servers) if (server.listening) {
1104
1108
  closingServers.add(server);
1105
1109
  const closePromise = new Promise((resolve) => {
1106
- net.Server.prototype.close.call(server, (err) => {
1110
+ net$1.Server.prototype.close.call(server, (err) => {
1107
1111
  if (err && err.code === "ERR_SERVER_NOT_RUNNING") {
1108
1112
  handleServerClose(server);
1109
1113
  resolve();
@@ -1275,9 +1279,9 @@ const runCommand = {
1275
1279
  sampler.start();
1276
1280
  setRuntimeSampler(sampler);
1277
1281
  setActionHooks({ runLocalJob: async (kind, tier) => {
1278
- const { startTier, stopAll } = await import("../worker-config-D8tRuvo7.mjs");
1279
- const { readLocalConfig } = await import("../local-tiers-DnBIAkbK.mjs");
1280
- const { workersYamlPath } = await import("../workers-config-DEI9lrV4.mjs");
1282
+ const { startTier, stopAll } = await import("../worker-config-DESkIjMF.mjs");
1283
+ const { readLocalConfig } = await import("../local-tiers-DNMI5W86.mjs");
1284
+ const { workersYamlPath } = await import("../workers-config-f8KrkY4L.mjs");
1281
1285
  const cfg = readLocalConfig(workersYamlPath());
1282
1286
  if (kind === "off") return void await stopAll(cfg);
1283
1287
  const t = cfg.tiers[tier];
@@ -1653,7 +1657,7 @@ const modelsCommand = {
1653
1657
  if (words[0] === "add") {
1654
1658
  const source = words[1];
1655
1659
  if (!source) throw new Error("usage: pai models add <hf-id|path> [--name n] [--download] [--try]");
1656
- const { addModel } = await import("../models-add-Bt7aUHDs.mjs");
1660
+ const { addModel } = await import("../models-add-C1QIc1DA.mjs");
1657
1661
  const r = await addModel({
1658
1662
  source,
1659
1663
  name: typeof ctx.opts.name === "string" ? ctx.opts.name : void 0,
@@ -1668,7 +1672,7 @@ const modelsCommand = {
1668
1672
  if (op) {
1669
1673
  const runtime = words[1];
1670
1674
  if (!runtime) throw new Error(`usage: pai models ${op} <runtime> (configured: ${Object.keys(readRuntimeCommands()).join(", ") || "none"})`);
1671
- const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-DcQ-ucU8.mjs");
1675
+ const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-DR9X514g.mjs");
1672
1676
  const r = await runtimeStartStop(runtime, op, defaultDeps(), { model: typeof ctx.opts.model === "string" ? ctx.opts.model : void 0 });
1673
1677
  process.stdout.write(r.message + "\n");
1674
1678
  if (!r.ok) process.exitCode = 1;
@@ -2513,7 +2517,7 @@ function measureCompactionTrigger(cwd, projectsDir = CLAUDE_PROJECTS_DIR, config
2513
2517
  if (discarded > 0) console.error(`[context-fill] discarded ${discarded} compaction sample(s) as other-regime for ${cwd} (preTokens below ${Math.round(floor)}, half of configured=${configuredTriggerTokens}): ` + samples.filter((s) => s.preTokens < floor).map((s) => `${s.preTokens}@${s.timestamp}`).join(", "));
2514
2518
  if (admissible.length === 0) return null;
2515
2519
  }
2516
- const trigger = Math.min(...admissible.map((s) => s.preTokens));
2520
+ const trigger = minOf(admissible.map((s) => s.preTokens));
2517
2521
  console.error(`[context-fill] measured trigger for ${cwd}: ${trigger} (minimum of ` + admissible.map((s) => `${s.preTokens}@${s.timestamp}`).join(", ") + ")");
2518
2522
  return trigger;
2519
2523
  }
@@ -2983,7 +2987,7 @@ async function cmdPause(opts) {
2983
2987
  process.exitCode = 1;
2984
2988
  return;
2985
2989
  }
2986
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
2990
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
2987
2991
  const registryBackend = await getRegistryBackend();
2988
2992
  const cwd = process.cwd();
2989
2993
  const project = await resolveProjectByCwd(registryBackend, cwd);
@@ -3083,7 +3087,7 @@ function touchSentinel(rootPath) {
3083
3087
  async function cmdAutosave(opts) {
3084
3088
  const minGap = parseInt(opts.minGap ?? String(DEFAULT_MIN_GAP), 10);
3085
3089
  const cwd = process.cwd();
3086
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
3090
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
3087
3091
  const registryBackend = await getRegistryBackend();
3088
3092
  const project = await resolveProjectByCwd(registryBackend, cwd);
3089
3093
  if (!project) return;
@@ -3309,7 +3313,7 @@ function finalizeNote(notePath) {
3309
3313
  }
3310
3314
  async function cmdEnd(opts) {
3311
3315
  await cmdPause(opts);
3312
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
3316
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
3313
3317
  const registryBackend = await getRegistryBackend();
3314
3318
  const cwd = process.cwd();
3315
3319
  const project = await registryBackend.findProjectByCwdPrefix(cwd);
@@ -3490,7 +3494,7 @@ function buildStopBody(project, sessionId) {
3490
3494
  }
3491
3495
  }
3492
3496
  async function cmdHandover(projectSlug, numberOrLatest, sessionId) {
3493
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
3497
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
3494
3498
  const registryBackend = await getRegistryBackend();
3495
3499
  let project;
3496
3500
  if (projectSlug) {
@@ -4436,8 +4440,8 @@ async function analyzeProject(registryBackend, project) {
4436
4440
  async function countVectorDbPaths(oldPaths) {
4437
4441
  if (oldPaths.length === 0) return 0;
4438
4442
  try {
4439
- const { loadConfig } = await import("../config-TaWmdTLx.mjs");
4440
- const { createStorageBackend } = await import("../factory-DAGG9_n8.mjs");
4443
+ const { loadConfig } = await import("../config-DtENJ6cL.mjs");
4444
+ const { createStorageBackend } = await import("../factory-Bsib-qDG.mjs");
4441
4445
  const config = loadConfig();
4442
4446
  if (config.storageBackend !== "postgres") return 0;
4443
4447
  const backend = await createStorageBackend(config);
@@ -4454,8 +4458,8 @@ async function countVectorDbPaths(oldPaths) {
4454
4458
  async function updateVectorDbPaths(moves) {
4455
4459
  if (moves.length === 0) return 0;
4456
4460
  try {
4457
- const { loadConfig } = await import("../config-TaWmdTLx.mjs");
4458
- const { createStorageBackend } = await import("../factory-DAGG9_n8.mjs");
4461
+ const { loadConfig } = await import("../config-DtENJ6cL.mjs");
4462
+ const { createStorageBackend } = await import("../factory-Bsib-qDG.mjs");
4459
4463
  const config = loadConfig();
4460
4464
  if (config.storageBackend !== "postgres") return 0;
4461
4465
  const backend = await createStorageBackend(config);
@@ -4542,7 +4546,7 @@ async function displayDryRun(plans) {
4542
4546
  console.log();
4543
4547
  }
4544
4548
  async function executeCleanup(plans, skipReindex) {
4545
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
4549
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
4546
4550
  const registryBackend = await getRegistryBackend();
4547
4551
  let deleted = 0;
4548
4552
  let renamed = 0;
@@ -4742,7 +4746,7 @@ async function executeCleanup(plans, skipReindex) {
4742
4746
  * them into YYYY/MM/ folders and renumber. Dry run unless `execute` is set.
4743
4747
  */
4744
4748
  async function runCleanup(projectSlug, opts) {
4745
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
4749
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
4746
4750
  const registryBackend = await getRegistryBackend();
4747
4751
  const dryRun = !opts.execute;
4748
4752
  const skipReindex = opts.reindex === false;
@@ -5405,11 +5409,65 @@ function migrateOrphanFederationDb(opts = {}) {
5405
5409
  //#region src/storage/backup.ts
5406
5410
  /**
5407
5411
  * storage/backup.ts — the only place `pai backup`/`pai restore` touch the
5408
- * *.db filenames. SQLite backend: byte-copy registry.db (+ federation.db,
5412
+ * *.db filenames. SQLite backend: snapshot registry.db (VACUUM INTO) (+ federation.db,
5409
5413
  * if present) into/from the backup dir. Postgres backend: the file-copy
5410
5414
  * step is a no-op — pg_dump/pg_restore (still in cli/commands/backup.ts and
5411
5415
  * restore.ts, via docker exec) is the real backup for that backend.
5412
5416
  */
5417
+ const SQLITE_MAGIC = "SQLite format 3\0";
5418
+ function isSqliteFile(path) {
5419
+ if (statSync(path).size < 16) return false;
5420
+ const fd = openSync(path, "r");
5421
+ try {
5422
+ const buf = Buffer.alloc(16);
5423
+ readSync(fd, buf, 0, 16, 0);
5424
+ return buf.toString("latin1") === SQLITE_MAGIC;
5425
+ } finally {
5426
+ closeSync(fd);
5427
+ }
5428
+ }
5429
+ /**
5430
+ * Consistent copy of a possibly live WAL-mode database: VACUUM INTO goes
5431
+ * through SQLite, so committed rows still sitting in the -wal are included
5432
+ * and no sidecars are needed. Non-SQLite files fall back to a byte copy.
5433
+ */
5434
+ function copyDb(src, dest) {
5435
+ if (!isSqliteFile(src)) {
5436
+ copyFileSync(src, dest);
5437
+ return;
5438
+ }
5439
+ rmSync(dest, { force: true });
5440
+ const db = new BetterSqlite3(src, { readonly: true });
5441
+ try {
5442
+ db.prepare("VACUUM INTO ?").run(dest);
5443
+ } finally {
5444
+ db.close();
5445
+ }
5446
+ }
5447
+ /** True when another process holds the file open (the daemon). Undetectable (no lsof) counts as not held. */
5448
+ function heldOpen(path) {
5449
+ if (!existsSync(path)) return false;
5450
+ try {
5451
+ execFileSync("lsof", ["-t", path], { stdio: "pipe" });
5452
+ return true;
5453
+ } catch {
5454
+ return false;
5455
+ }
5456
+ }
5457
+ /** Restores one db file: refuses while held open, drops stale -wal/-shm, then copies. */
5458
+ function restoreDb(src, dest) {
5459
+ if (heldOpen(dest)) throw new Error(`${dest} is held open by another process (the daemon?) — stop it first (pai daemon stop), then restore`);
5460
+ mkdirSync(dirname(dest), { recursive: true });
5461
+ if (existsSync(dest)) {
5462
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5463
+ let safety = `${dest}.pre-restore-${stamp}`;
5464
+ for (let i = 1; existsSync(safety); i++) safety = `${dest}.pre-restore-${stamp}-${i}`;
5465
+ copyDb(dest, safety);
5466
+ chmodSync(safety, 384);
5467
+ }
5468
+ for (const suffix of ["-wal", "-shm"]) rmSync(`${dest}${suffix}`, { force: true });
5469
+ copyFileSync(src, dest);
5470
+ }
5413
5471
  /** Copies registry.db (and federation.db, if present) into destDir. No-op on the postgres backend. */
5414
5472
  function backupStorageFiles(config, destDir) {
5415
5473
  if (config.storageBackend === "postgres") return [];
@@ -5418,7 +5476,7 @@ function backupStorageFiles(config, destDir) {
5418
5476
  if (existsSync(registrySrc)) {
5419
5477
  const dest = join(destDir, "registry.db");
5420
5478
  try {
5421
- copyFileSync(registrySrc, dest);
5479
+ copyDb(registrySrc, dest);
5422
5480
  results.push({
5423
5481
  label: "Registry DB",
5424
5482
  path: dest,
@@ -5442,7 +5500,7 @@ function backupStorageFiles(config, destDir) {
5442
5500
  if (existsSync(fedSrc)) {
5443
5501
  const dest = join(destDir, "federation.db");
5444
5502
  try {
5445
- copyFileSync(fedSrc, dest);
5503
+ copyDb(fedSrc, dest);
5446
5504
  results.push({
5447
5505
  label: "Federation DB (legacy)",
5448
5506
  path: dest,
@@ -5474,8 +5532,7 @@ function restoreStorageFiles(config, srcDir) {
5474
5532
  if (inv.hasRegistry) {
5475
5533
  const dest = registryDbPath();
5476
5534
  try {
5477
- mkdirSync(dirname(dest), { recursive: true });
5478
- copyFileSync(join(srcDir, "registry.db"), dest);
5535
+ restoreDb(join(srcDir, "registry.db"), dest);
5479
5536
  results.push({
5480
5537
  label: "Registry DB",
5481
5538
  path: dest,
@@ -5498,8 +5555,7 @@ function restoreStorageFiles(config, srcDir) {
5498
5555
  if (inv.hasFederation) {
5499
5556
  const dest = federationDbPath();
5500
5557
  try {
5501
- mkdirSync(dirname(dest), { recursive: true });
5502
- copyFileSync(join(srcDir, "federation.db"), dest);
5558
+ restoreDb(join(srcDir, "federation.db"), dest);
5503
5559
  results.push({
5504
5560
  label: "Federation DB (legacy)",
5505
5561
  path: dest,
@@ -5517,6 +5573,200 @@ function restoreStorageFiles(config, srcDir) {
5517
5573
  return results;
5518
5574
  }
5519
5575
 
5576
+ //#endregion
5577
+ //#region src/storage/postgres/snapshot.ts
5578
+ /**
5579
+ * One REPEATABLE READ transaction that exports its snapshot and counts rows in it, so a
5580
+ * pg_dump --snapshot run while it is open sees exactly the counted state.
5581
+ */
5582
+ /**
5583
+ * Opens the transaction, counts `tables` (null when a table does not exist), calls `dump(snapshotId)`
5584
+ * while the transaction is still open, and returns the counts.
5585
+ */
5586
+ async function countInExportedSnapshot(connectionString, tables, dump) {
5587
+ const client = new pg.Client({ connectionString });
5588
+ await client.connect();
5589
+ try {
5590
+ await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY");
5591
+ const snap = (await client.query("SELECT pg_export_snapshot() AS s")).rows[0].s;
5592
+ const counts = {};
5593
+ for (const table of tables) counts[table] = (await client.query("SELECT to_regclass($1) IS NOT NULL AS e", [table])).rows[0].e ? Number((await client.query(`SELECT count(*) AS n FROM "${table.replace(/"/g, "\"\"")}"`)).rows[0].n) : null;
5594
+ await dump(snap);
5595
+ await client.query("COMMIT");
5596
+ return counts;
5597
+ } finally {
5598
+ await client.end().catch(() => void 0);
5599
+ }
5600
+ }
5601
+ /** The statement `psql -tAc` runs to count a table's rows. */
5602
+ function countSql(table) {
5603
+ return `SELECT count(*) FROM "${table.replace(/"/g, "\"\"")}"`;
5604
+ }
5605
+
5606
+ //#endregion
5607
+ //#region src/cli/commands/lifecycle/backup-postgres.ts
5608
+ /**
5609
+ * Postgres part of `pai backup`: pick the database the running storage uses,
5610
+ * dump it, and check the dump against the live row counts.
5611
+ */
5612
+ const BACKUP_TABLES = [
5613
+ "pai_files",
5614
+ "pai_chunks",
5615
+ "pai_observations",
5616
+ "sessions"
5617
+ ];
5618
+ /** The server, database and role to dump: resolved by the same function the storage backend connects with. */
5619
+ function resolveBackupTarget(pg) {
5620
+ return postgresFullTarget(pg ?? {});
5621
+ }
5622
+ /** A plain pg_dump ends with this marker; a truncated dump does not. */
5623
+ function validatePgDump(dump) {
5624
+ if (dump.trim().length === 0) return "dump is empty";
5625
+ if (!dump.slice(-2048).includes("PostgreSQL database dump complete")) return "dump is truncated (no pg_dump completion marker)";
5626
+ return null;
5627
+ }
5628
+ /** Accumulates per-table row counts from the lines of a plain-format pg_dump, one line at a time. */
5629
+ var DumpRowCounter = class {
5630
+ counts = /* @__PURE__ */ new Map();
5631
+ current = null;
5632
+ push(line) {
5633
+ if (this.current === null) {
5634
+ const m = /^COPY (?:\S+\.)?"?(\w+)"? .* FROM stdin;/.exec(line);
5635
+ if (m) {
5636
+ this.current = m[1];
5637
+ this.counts.set(this.current, 0);
5638
+ }
5639
+ } else if (line === "\\.") this.current = null;
5640
+ else this.counts.set(this.current, this.counts.get(this.current) + 1);
5641
+ }
5642
+ };
5643
+ /** Last `n` bytes of a file, without reading the rest. */
5644
+ function readFileTail(path, n = 2048) {
5645
+ const fd = openSync(path, "r");
5646
+ try {
5647
+ const size = fstatSync(fd).size;
5648
+ const len = Math.min(n, size);
5649
+ const buf = Buffer.alloc(len);
5650
+ readSync(fd, buf, 0, len, size - len);
5651
+ return buf.toString("utf8");
5652
+ } finally {
5653
+ closeSync(fd);
5654
+ }
5655
+ }
5656
+ /** Streams a dump file line by line (bounded memory): row counts per table, plus the completion-marker check. */
5657
+ async function scanDumpFile(path) {
5658
+ const counter = new DumpRowCounter();
5659
+ const rl = createInterface({
5660
+ input: createReadStream(path, {
5661
+ encoding: "utf8",
5662
+ highWaterMark: 1 << 20
5663
+ }),
5664
+ crlfDelay: Infinity
5665
+ });
5666
+ for await (const line of rl) counter.push(line);
5667
+ return {
5668
+ counts: counter.counts,
5669
+ invalid: validatePgDump(readFileTail(path))
5670
+ };
5671
+ }
5672
+ /**
5673
+ * Dump the target and verify it against the counts taken in the dump's own snapshot.
5674
+ * A table with no COPY block counts as 0 rows (pg_dump emits none for empty tables), so a fresh
5675
+ * install verifies. On any failure the dump file is removed so it cannot be mistaken for a backup.
5676
+ */
5677
+ async function backupPostgres(target, destPath, runner) {
5678
+ try {
5679
+ const live = await runner.dump(target, destPath);
5680
+ const { counts: dumped, invalid } = await scanDumpFile(destPath);
5681
+ if (invalid) throw new Error(`database "${target.database}" backup failed verification (${invalid})`);
5682
+ const counts = {};
5683
+ const problems = [];
5684
+ for (const table of BACKUP_TABLES) {
5685
+ const liveCount = live[table];
5686
+ if (liveCount === null || liveCount === void 0) continue;
5687
+ const got = dumped.get(table) ?? 0;
5688
+ counts[table] = got;
5689
+ if (got !== liveCount) problems.push(`${table}: dump ${got}, live ${liveCount}`);
5690
+ }
5691
+ if (problems.length > 0) throw new Error(`database "${target.database}" backup failed verification (${problems.join("; ")})`);
5692
+ if (Object.keys(counts).length === 0) throw new Error(`database "${target.database}" has none of the main tables (${BACKUP_TABLES.join(", ")})`);
5693
+ return {
5694
+ database: target.database,
5695
+ counts
5696
+ };
5697
+ } catch (e) {
5698
+ try {
5699
+ unlinkSync(destPath);
5700
+ } catch {}
5701
+ throw e;
5702
+ }
5703
+ }
5704
+ const BACKUP_META_FILE = "postgres-meta.json";
5705
+ /** Records the source database and dumped row counts next to the dump. */
5706
+ function writeBackupMeta(backupDir, meta) {
5707
+ writeFileSync(join(backupDir, BACKUP_META_FILE), JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
5708
+ }
5709
+ /** Reads the metadata of a backup; null when absent or unreadable (backups made before it existed). */
5710
+ function readBackupMeta(backupDir) {
5711
+ try {
5712
+ const m = JSON.parse(readFileSync(join(backupDir, BACKUP_META_FILE), "utf8"));
5713
+ if (typeof m.database !== "string" || m.database === "") return null;
5714
+ return {
5715
+ database: m.database,
5716
+ counts: m.counts ?? {}
5717
+ };
5718
+ } catch {
5719
+ return null;
5720
+ }
5721
+ }
5722
+ /** Target = the daemon's database unless `into` overrides it; refuses when the dump came from another database (or an unknown one) and `into` is not given. */
5723
+ function planPostgresRestore(pg, meta, into) {
5724
+ const daemon = resolveBackupTarget(pg);
5725
+ const source = meta?.database ?? null;
5726
+ const target = {
5727
+ ...daemon,
5728
+ database: into ?? daemon.database
5729
+ };
5730
+ try {
5731
+ assertPgName(target.database, "database", RESTORE_DB_MAX_BYTES);
5732
+ } catch (e) {
5733
+ return {
5734
+ target,
5735
+ source,
5736
+ refusal: e instanceof Error ? e.message : String(e)
5737
+ };
5738
+ }
5739
+ if (into !== void 0) return {
5740
+ target,
5741
+ source
5742
+ };
5743
+ if (source === null) return {
5744
+ target,
5745
+ source,
5746
+ refusal: `backup does not record its source database (made before 1.0.2); pass --into <db> to restore into a database explicitly, usually --into ${daemon.database} (daemon database)`
5747
+ };
5748
+ if (source !== target.database) return {
5749
+ target,
5750
+ source,
5751
+ refusal: `backup was taken from database "${source}" but the daemon uses "${target.database}"; pass --into <db> to restore anyway`
5752
+ };
5753
+ return {
5754
+ target,
5755
+ source
5756
+ };
5757
+ }
5758
+ /** Restore derives `<db>_prerestore_<19-char stamp>` names; keep them inside Postgres' 63 bytes so they are never truncated onto another name. */
5759
+ const RESTORE_DB_MAX_BYTES = 32;
5760
+ /** Warning when the config.json inside the backup names a different database than the restore target; null when equal or unreadable. */
5761
+ function configDatabaseMismatch(backupConfigPath, target) {
5762
+ try {
5763
+ const db = resolveBackupTarget(JSON.parse(readFileSync(backupConfigPath, "utf8")).postgres).database;
5764
+ return db === target.database ? null : `config.json from the backup names database "${db}" but the dump was restored into "${target.database}"; the daemon will use "${db}"`;
5765
+ } catch {
5766
+ return null;
5767
+ }
5768
+ }
5769
+
5520
5770
  //#endregion
5521
5771
  //#region src/cli/commands/lifecycle/backup.ts
5522
5772
  /**
@@ -5528,7 +5778,7 @@ function restoreStorageFiles(config, srcDir) {
5528
5778
  * Contents:
5529
5779
  * registry file — SQLite registry database (sqlite backend only)
5530
5780
  * config.json — PAI daemon config
5531
- * postgres-pai.sql — pg_dump of the Postgres "pai" database (via docker exec)
5781
+ * postgres-pai.sql — pg_dump of the daemon's Postgres database (via docker exec), row counts verified
5532
5782
  */
5533
5783
  const CONFIG_FILE$1 = paiConfigFilePath();
5534
5784
  /** Old backups dir, inside ~/.pai/ (pre-2026-09-19). */
@@ -5542,8 +5792,6 @@ function backupsDirPath() {
5542
5792
  }
5543
5793
  const BACKUPS_DIR$1 = backupsDirPath();
5544
5794
  const DOCKER_CONTAINER$1 = "pai-pgvector";
5545
- const PG_DATABASE$1 = "pai";
5546
- const PG_USER$1 = "pai";
5547
5795
  function timestamp() {
5548
5796
  const now = /* @__PURE__ */ new Date();
5549
5797
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
@@ -5560,7 +5808,68 @@ function fileSize(path) {
5560
5808
  return "unknown";
5561
5809
  }
5562
5810
  }
5811
+ /**
5812
+ * Dumps the server the connection string names (the PAI container when that is the local server,
5813
+ * else the host's pg_dump against the external server). Counts are read in the REPEATABLE READ
5814
+ * transaction whose exported snapshot pg_dump then uses, so they match the dump exactly.
5815
+ */
5816
+ const snapshotRunner = { async dump(target, destPath) {
5817
+ const container = isContainerTarget(target);
5818
+ if (container) execFileSync("docker", [
5819
+ "inspect",
5820
+ DOCKER_CONTAINER$1,
5821
+ "--format={{.State.Status}}"
5822
+ ], { stdio: "pipe" });
5823
+ return countInExportedSnapshot(target.connectionString ?? postgresConnectionString(loadConfig().postgres ?? {}), BACKUP_TABLES, (snap) => {
5824
+ const sqlFd = openSync(destPath, "w");
5825
+ try {
5826
+ const common = [
5827
+ `--snapshot=${snap}`,
5828
+ "-U",
5829
+ target.user
5830
+ ];
5831
+ const dump = container ? spawnSync("docker", [
5832
+ "exec",
5833
+ DOCKER_CONTAINER$1,
5834
+ "pg_dump",
5835
+ ...common,
5836
+ target.database
5837
+ ], { stdio: [
5838
+ "pipe",
5839
+ sqlFd,
5840
+ "pipe"
5841
+ ] }) : spawnSync("pg_dump", [
5842
+ "-h",
5843
+ target.host,
5844
+ "-p",
5845
+ String(target.port),
5846
+ ...common,
5847
+ "-d",
5848
+ target.database
5849
+ ], {
5850
+ stdio: [
5851
+ "pipe",
5852
+ sqlFd,
5853
+ "pipe"
5854
+ ],
5855
+ env: target.password !== void 0 ? {
5856
+ ...process.env,
5857
+ PGPASSWORD: target.password
5858
+ } : process.env
5859
+ });
5860
+ if (dump.error) throw dump.error;
5861
+ if (dump.status !== 0) throw new Error(dump.stderr?.toString().trim() || `pg_dump exited with code ${dump.status}`);
5862
+ } finally {
5863
+ closeSync(sqlFd);
5864
+ }
5865
+ });
5866
+ } };
5563
5867
  async function runBackup(opts) {
5868
+ if (opts.dryRun) {
5869
+ const t = resolveBackupTarget(loadConfig().postgres);
5870
+ console.log(`Would dump Postgres database "${t.database}" (user ${t.user}) from ${isContainerTarget(t) ? `container ${DOCKER_CONTAINER$1}` : `${t.host}:${t.port}`}`);
5871
+ return;
5872
+ }
5564
5873
  const backupDir = join(BACKUPS_DIR$1, timestamp());
5565
5874
  console.log(dim$1(`Creating backup: ${backupDir}`));
5566
5875
  mkdirSync(backupDir, { recursive: true });
@@ -5570,7 +5879,8 @@ async function runBackup(opts) {
5570
5879
  label: r.label,
5571
5880
  path: r.path,
5572
5881
  size: r.status === "ok" ? fileSize(r.path) : "-",
5573
- status: r.status === "ok" ? ok$1("ok") : r.status === "skipped" ? warn$1("not found — skipped") : err(`failed: ${r.error}`)
5882
+ status: r.status === "ok" ? ok$1("ok") : r.status === "skipped" ? warn$1("not found — skipped") : err(`failed: ${r.error}`),
5883
+ failed: r.status !== "ok" && r.status !== "skipped"
5574
5884
  });
5575
5885
  if (existsSync(CONFIG_FILE$1)) {
5576
5886
  const dest = join(backupDir, "config.json");
@@ -5588,7 +5898,8 @@ async function runBackup(opts) {
5588
5898
  label: "Config",
5589
5899
  path: dest,
5590
5900
  size: "-",
5591
- status: err(`failed: ${e}`)
5901
+ status: err(`failed: ${e}`),
5902
+ failed: true
5592
5903
  });
5593
5904
  }
5594
5905
  } else results.push({
@@ -5599,36 +5910,16 @@ async function runBackup(opts) {
5599
5910
  });
5600
5911
  if (opts.postgres) {
5601
5912
  const sqlDest = join(backupDir, "postgres-pai.sql");
5602
- console.log(dim$1(` Running pg_dump on ${DOCKER_CONTAINER$1} (this may take a moment)...`));
5913
+ console.log(dim$1(" Running pg_dump (this may take a moment)..."));
5603
5914
  try {
5604
- execFileSync("docker", [
5605
- "inspect",
5606
- DOCKER_CONTAINER$1,
5607
- "--format={{.State.Status}}"
5608
- ], { stdio: "pipe" });
5609
- const sqlFd = openSync(sqlDest, "w");
5610
- try {
5611
- const dump = spawnSync("docker", [
5612
- "exec",
5613
- DOCKER_CONTAINER$1,
5614
- "pg_dump",
5615
- "-U",
5616
- PG_USER$1,
5617
- PG_DATABASE$1
5618
- ], { stdio: [
5619
- "pipe",
5620
- sqlFd,
5621
- "pipe"
5622
- ] });
5623
- if (dump.status !== 0) throw new Error(dump.stderr?.toString().trim() || `pg_dump exited with code ${dump.status}`);
5624
- } finally {
5625
- closeSync(sqlFd);
5626
- }
5915
+ const target = resolveBackupTarget(config.postgres);
5916
+ console.log(dim$1(` Database: ${target.database}`));
5917
+ writeBackupMeta(backupDir, await backupPostgres(target, sqlDest, snapshotRunner));
5627
5918
  results.push({
5628
5919
  label: "Postgres DB",
5629
5920
  path: sqlDest,
5630
5921
  size: fileSize(sqlDest),
5631
- status: ok$1("ok")
5922
+ status: ok$1("ok (verified)")
5632
5923
  });
5633
5924
  } catch (e) {
5634
5925
  const msg = e instanceof Error ? e.message.split("\n")[0] : String(e);
@@ -5636,9 +5927,10 @@ async function runBackup(opts) {
5636
5927
  label: "Postgres DB",
5637
5928
  path: sqlDest,
5638
5929
  size: "-",
5639
- status: err(`failed: ${msg}`)
5930
+ status: err(`failed: ${msg}`),
5931
+ failed: true
5640
5932
  });
5641
- console.log(warn$1(` Postgres backup failed. Is Docker running with container '${DOCKER_CONTAINER$1}'?`));
5933
+ console.log(warn$1(` Postgres backup failed; no dump was kept. Is the server reachable (Docker container '${DOCKER_CONTAINER$1}' for a local database, pg_dump on PATH for an external one)?`));
5642
5934
  }
5643
5935
  } else results.push({
5644
5936
  label: "Postgres DB",
@@ -5646,13 +5938,19 @@ async function runBackup(opts) {
5646
5938
  size: "-",
5647
5939
  status: dim$1("skipped (--no-postgres)")
5648
5940
  });
5649
- console.log(`\n${bold$1("Backup complete:")} ${backupDir}\n`);
5941
+ const failed = results.filter((r) => r.failed);
5942
+ console.log(`\n${failed.length === 0 ? bold$1("Backup complete:") : bold$1(err("Backup INCOMPLETE:"))} ${backupDir}\n`);
5650
5943
  const labelWidth = Math.max(...results.map((r) => r.label.length)) + 2;
5651
5944
  for (const r of results) {
5652
5945
  const label = r.label.padEnd(labelWidth);
5653
5946
  console.log(` ${bold$1(label)} ${r.status} ${dim$1(r.size)}`);
5654
5947
  }
5655
5948
  console.log(`\n ${dim$1("Path:")} ${backupDir}`);
5949
+ if (failed.length > 0) {
5950
+ console.error(err(`Backup failed: ${failed.map((r) => r.label).join(", ")}`));
5951
+ process.exitCode = 1;
5952
+ return;
5953
+ }
5656
5954
  console.log(` ${dim$1("To restore:")} pai restore ${backupDir}\n`);
5657
5955
  }
5658
5956
 
@@ -6014,8 +6312,249 @@ async function runLaunch(args, opts) {
6014
6312
  const CONFIG_FILE = paiConfigFilePath();
6015
6313
  const BACKUPS_DIR = backupsDirPath();
6016
6314
  const DOCKER_CONTAINER = "pai-pgvector";
6017
- const PG_DATABASE = "pai";
6018
- const PG_USER = "pai";
6315
+ function stampNow() {
6316
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6317
+ }
6318
+ /** Copies the current config to a timestamped 0600 file next to it, then installs src. Returns the safety path (null when there was no current config). */
6319
+ function restoreConfigWithSafety(src, dest) {
6320
+ mkdirSync(dirname(dest), { recursive: true });
6321
+ let safety = null;
6322
+ if (existsSync(dest)) {
6323
+ safety = `${dest}.pre-restore-${stampNow()}`;
6324
+ for (let i = 1; existsSync(safety); i++) safety = `${dest}.pre-restore-${stampNow()}-${i}`;
6325
+ copyFileSync(dest, safety);
6326
+ chmodSync(safety, 384);
6327
+ }
6328
+ copyFileSync(src, dest);
6329
+ chmodSync(dest, 384);
6330
+ return safety;
6331
+ }
6332
+ /**
6333
+ * Runner for a target: inside the PAI container when the target is the local PAI server,
6334
+ * otherwise the host's psql/pg_dump pointed at the server the connection string names.
6335
+ */
6336
+ function makePgRunner(target) {
6337
+ return (args, input, output) => {
6338
+ const inContainer = isContainerTarget(target);
6339
+ const [prog, ...rest] = args;
6340
+ const cmd = inContainer ? "docker" : prog;
6341
+ const argv = inContainer ? [
6342
+ "exec",
6343
+ ...input !== void 0 ? ["-i"] : [],
6344
+ DOCKER_CONTAINER,
6345
+ ...args
6346
+ ] : [
6347
+ ...target.host ? ["-h", target.host] : [],
6348
+ ...target.port ? ["-p", String(target.port)] : [],
6349
+ ...rest
6350
+ ];
6351
+ const fd = typeof input === "object" ? openSync(input.file, "r") : void 0;
6352
+ const outFd = output ? openSync(output.file, "w", 384) : void 0;
6353
+ try {
6354
+ const r = spawnSync(cmd, argv, {
6355
+ input: typeof input === "string" ? input : void 0,
6356
+ encoding: "utf8",
6357
+ stdio: [
6358
+ fd ?? "pipe",
6359
+ outFd ?? "pipe",
6360
+ "pipe"
6361
+ ],
6362
+ maxBuffer: 1024 * 1024 * 1024,
6363
+ env: target.password !== void 0 ? {
6364
+ ...process.env,
6365
+ PGPASSWORD: target.password
6366
+ } : process.env
6367
+ });
6368
+ return {
6369
+ status: r.status,
6370
+ stdout: r.stdout ?? "",
6371
+ stderr: r.stderr ?? ""
6372
+ };
6373
+ } finally {
6374
+ if (fd !== void 0) closeSync(fd);
6375
+ if (outFd !== void 0) closeSync(outFd);
6376
+ }
6377
+ };
6378
+ }
6379
+ /**
6380
+ * Restores a dump without destroying the live database first:
6381
+ * validate -> pre-restore dump of live DB -> restore into a NEW database -> swap by rename.
6382
+ * Any failure before the swap leaves the live database untouched.
6383
+ */
6384
+ async function restorePostgres(dumpFile, safetyDir, target, run = makePgRunner(target), recordedCounts, opts = {}) {
6385
+ const PG_DATABASE = target.database;
6386
+ const PG_USER = target.user;
6387
+ try {
6388
+ assertPgName(PG_DATABASE, "database", RESTORE_DB_MAX_BYTES);
6389
+ assertPgName(PG_USER, "user");
6390
+ } catch (e) {
6391
+ return {
6392
+ ok: false,
6393
+ message: `${e instanceof Error ? e.message : String(e)}; live database untouched`
6394
+ };
6395
+ }
6396
+ const { counts: dumpCounts, invalid } = await scanDumpFile(dumpFile);
6397
+ if (invalid) return {
6398
+ ok: false,
6399
+ message: `${invalid}; live database untouched`
6400
+ };
6401
+ const stamp = stampNow().replace(/[^0-9A-Za-z]/g, "").toLowerCase();
6402
+ const newDb = `${PG_DATABASE}_restore_${stamp}`;
6403
+ const oldDb = `${PG_DATABASE}_prerestore_${stamp}`;
6404
+ const qNew = quoteIdent(newDb);
6405
+ const qOld = quoteIdent(oldDb);
6406
+ const qLive = quoteIdent(PG_DATABASE);
6407
+ const qUser = quoteIdent(PG_USER);
6408
+ const firstLine = (r) => r.stderr.split("\n")[0] || "unknown error";
6409
+ let safetyDump;
6410
+ if (!opts.noSafetyDump) {
6411
+ const path = join(safetyDir, `postgres-pai.pre-restore-${stampNow()}.sql`);
6412
+ mkdirSync(safetyDir, { recursive: true });
6413
+ const live = run([
6414
+ "pg_dump",
6415
+ "-U",
6416
+ PG_USER,
6417
+ PG_DATABASE
6418
+ ], void 0, { file: path });
6419
+ if (live.status === 0) {
6420
+ const { invalid: safetyInvalid } = await scanDumpFile(path);
6421
+ if (safetyInvalid) {
6422
+ rmSync(path, { force: true });
6423
+ return {
6424
+ ok: false,
6425
+ message: `pre-restore dump of live database is incomplete (${safetyInvalid}); live database untouched (use --no-safety-dump to skip it)`
6426
+ };
6427
+ }
6428
+ safetyDump = path;
6429
+ } else {
6430
+ rmSync(path, { force: true });
6431
+ if (!/does not exist/.test(live.stderr)) return {
6432
+ ok: false,
6433
+ message: `pre-restore dump failed: ${firstLine(live)}; live database untouched (use --no-safety-dump to skip it)`
6434
+ };
6435
+ }
6436
+ }
6437
+ const cleanup = () => run([
6438
+ "psql",
6439
+ "-U",
6440
+ PG_USER,
6441
+ "-d",
6442
+ "postgres",
6443
+ "-c",
6444
+ `DROP DATABASE IF EXISTS ${qNew};`
6445
+ ]);
6446
+ const created = run([
6447
+ "psql",
6448
+ "-U",
6449
+ PG_USER,
6450
+ "-d",
6451
+ "postgres",
6452
+ "-c",
6453
+ `CREATE DATABASE ${qNew} OWNER ${qUser};`
6454
+ ]);
6455
+ if (created.status !== 0) return {
6456
+ ok: false,
6457
+ message: `could not create staging database: ${firstLine(created)}; live database untouched`,
6458
+ safetyDump
6459
+ };
6460
+ const loaded = run([
6461
+ "psql",
6462
+ "-U",
6463
+ PG_USER,
6464
+ "-v",
6465
+ "ON_ERROR_STOP=1",
6466
+ "-d",
6467
+ newDb
6468
+ ], { file: dumpFile });
6469
+ if (loaded.status !== 0) {
6470
+ cleanup();
6471
+ return {
6472
+ ok: false,
6473
+ message: `psql failed: ${firstLine(loaded)}; live database untouched`,
6474
+ safetyDump
6475
+ };
6476
+ }
6477
+ const swap1 = run([
6478
+ "psql",
6479
+ "-U",
6480
+ PG_USER,
6481
+ "-d",
6482
+ "postgres",
6483
+ "-c",
6484
+ `ALTER DATABASE ${qLive} RENAME TO ${qOld};`
6485
+ ]);
6486
+ const liveExisted = swap1.status === 0;
6487
+ if (!liveExisted && !/does not exist/.test(swap1.stderr)) {
6488
+ cleanup();
6489
+ return {
6490
+ ok: false,
6491
+ message: `could not rename live database (active connections? stop the daemon): ${firstLine(swap1)}; live database untouched`,
6492
+ safetyDump
6493
+ };
6494
+ }
6495
+ const swap2 = run([
6496
+ "psql",
6497
+ "-U",
6498
+ PG_USER,
6499
+ "-d",
6500
+ "postgres",
6501
+ "-c",
6502
+ `ALTER DATABASE ${qNew} RENAME TO ${qLive};`
6503
+ ]);
6504
+ if (swap2.status !== 0) {
6505
+ if (liveExisted) run([
6506
+ "psql",
6507
+ "-U",
6508
+ PG_USER,
6509
+ "-d",
6510
+ "postgres",
6511
+ "-c",
6512
+ `ALTER DATABASE ${qOld} RENAME TO ${qLive};`
6513
+ ]);
6514
+ cleanup();
6515
+ return {
6516
+ ok: false,
6517
+ message: `swap failed: ${firstLine(swap2)}; live database restored`,
6518
+ safetyDump
6519
+ };
6520
+ }
6521
+ const oldDbKept = liveExisted ? oldDb : void 0;
6522
+ const expected = recordedCounts ?? Object.fromEntries(dumpCounts);
6523
+ const counts = {};
6524
+ const bad = [];
6525
+ for (const table of BACKUP_TABLES) {
6526
+ if (!(table in expected)) continue;
6527
+ const r = run([
6528
+ "psql",
6529
+ "-U",
6530
+ PG_USER,
6531
+ "-d",
6532
+ PG_DATABASE,
6533
+ "-tAc",
6534
+ countSql(table)
6535
+ ]);
6536
+ const actual = r.status === 0 ? Number(r.stdout.trim()) : null;
6537
+ counts[table] = {
6538
+ expected: expected[table],
6539
+ actual
6540
+ };
6541
+ if (actual !== expected[table]) bad.push(`${table}: expected ${expected[table]}, got ${actual ?? "n/a"}`);
6542
+ }
6543
+ if (bad.length > 0) return {
6544
+ ok: false,
6545
+ message: `restored into "${PG_DATABASE}" but row counts differ (${bad.join("; ")})`,
6546
+ safetyDump,
6547
+ oldDb: oldDbKept,
6548
+ counts
6549
+ };
6550
+ return {
6551
+ ok: true,
6552
+ message: "restored",
6553
+ safetyDump,
6554
+ oldDb: oldDbKept,
6555
+ counts
6556
+ };
6557
+ }
6019
6558
  function confirm(question) {
6020
6559
  return new Promise((resolve) => {
6021
6560
  const rl = createInterface({
@@ -6084,6 +6623,24 @@ async function runRestore(backupPath, opts) {
6084
6623
  console.log(` config.json ${hasConfig ? ok$1("present") : warn$1("missing")}`);
6085
6624
  console.log(` postgres-pai.sql ${hasSql && opts.postgres ? ok$1("present") : hasSql ? warn$1("present (skipped via --no-postgres)") : warn$1("missing")}`);
6086
6625
  if (inventory.hasFederation) console.log(` Federation DB ${ok$1("present")} ${dim$1("(legacy)")}`);
6626
+ const meta = readBackupMeta(resolvedDir);
6627
+ const plan = planPostgresRestore(loadConfig().postgres, meta, opts.into);
6628
+ if (hasSql && opts.postgres) {
6629
+ console.log(`\n${bold$1("Postgres:")}`);
6630
+ console.log(` Target database ${plan.target.database} (user ${plan.target.user})`);
6631
+ console.log(` Dump source database ${plan.source ?? "unknown"}`);
6632
+ if (plan.refusal) {
6633
+ console.error(err(`Refusing to restore: ${plan.refusal}`));
6634
+ if (!opts.dryRun) {
6635
+ process.exitCode = 1;
6636
+ return;
6637
+ }
6638
+ }
6639
+ }
6640
+ if (opts.dryRun) {
6641
+ console.log(dim$1("\nDry run: nothing was changed."));
6642
+ return;
6643
+ }
6087
6644
  console.log(`\n${warn$1("WARNING:")} This will OVERWRITE your current PAI data.`);
6088
6645
  if (hasSql && opts.postgres) console.log(warn$1(" Postgres database will be dropped and recreated from the backup."));
6089
6646
  if (!(opts.yes || await confirm("Proceed with restore?"))) {
@@ -6097,13 +6654,16 @@ async function runRestore(backupPath, opts) {
6097
6654
  status: r.status === "ok" ? ok$1("restored") : r.status === "skipped" ? warn$1("missing in backup — skipped") : err(`failed: ${r.error}`)
6098
6655
  });
6099
6656
  if (hasConfig) try {
6100
- mkdirSync(dirname(CONFIG_FILE), { recursive: true });
6101
- copyFileSync(join(resolvedDir, "config.json"), CONFIG_FILE);
6102
- chmodSync(CONFIG_FILE, 384);
6657
+ const safety = restoreConfigWithSafety(join(resolvedDir, "config.json"), CONFIG_FILE);
6658
+ if (safety) console.log(dim$1(` Previous config.json saved to ${safety}`));
6103
6659
  results.push({
6104
6660
  label: "Config",
6105
6661
  status: ok$1("restored")
6106
6662
  });
6663
+ if (hasSql && opts.postgres) {
6664
+ const mismatch = configDatabaseMismatch(join(resolvedDir, "config.json"), plan.target);
6665
+ if (mismatch) console.log(warn$1(` ${mismatch}`));
6666
+ }
6107
6667
  } catch (e) {
6108
6668
  results.push({
6109
6669
  label: "Config",
@@ -6117,47 +6677,18 @@ async function runRestore(backupPath, opts) {
6117
6677
  if (hasSql && opts.postgres) {
6118
6678
  console.log(dim$1(" Restoring Postgres database (this may take a while)..."));
6119
6679
  try {
6120
- execFileSync("docker", [
6680
+ if (isContainerTarget(plan.target)) execFileSync("docker", [
6121
6681
  "inspect",
6122
6682
  DOCKER_CONTAINER,
6123
6683
  "--format={{.State.Status}}"
6124
6684
  ], { stdio: "pipe" });
6125
- execFileSync("docker", [
6126
- "exec",
6127
- DOCKER_CONTAINER,
6128
- "psql",
6129
- "-U",
6130
- PG_USER,
6131
- "-c",
6132
- `DROP DATABASE IF EXISTS ${PG_DATABASE}; CREATE DATABASE ${PG_DATABASE} OWNER ${PG_USER};`
6133
- ], { stdio: "pipe" });
6134
- const sqlContent = readFileSync(join(resolvedDir, "postgres-pai.sql"), "utf8");
6135
- const psqlResult = spawnSync("docker", [
6136
- "exec",
6137
- "-i",
6138
- DOCKER_CONTAINER,
6139
- "psql",
6140
- "-U",
6141
- PG_USER,
6142
- PG_DATABASE
6143
- ], {
6144
- input: sqlContent,
6145
- encoding: "utf8",
6146
- stdio: [
6147
- "pipe",
6148
- "pipe",
6149
- "pipe"
6150
- ]
6151
- });
6152
- if (psqlResult.status !== 0) {
6153
- const errMsg = psqlResult.stderr?.split("\n")[0] ?? "unknown error";
6154
- results.push({
6155
- label: "Postgres DB",
6156
- status: err(`failed: ${errMsg}`)
6157
- });
6158
- } else results.push({
6685
+ const pg = await restorePostgres(join(resolvedDir, "postgres-pai.sql"), dirname(CONFIG_FILE), plan.target, void 0, meta?.counts, { noSafetyDump: opts.noSafetyDump });
6686
+ if (pg.safetyDump) console.log(dim$1(` Pre-restore dump of live database: ${pg.safetyDump}`));
6687
+ if (pg.oldDb) console.log(dim$1(` Previous database kept as '${pg.oldDb}' (drop it when satisfied)`));
6688
+ for (const [t, c] of Object.entries(pg.counts ?? {})) console.log(dim$1(` ${t}: expected ${c.expected}, restored ${c.actual ?? "n/a"}`));
6689
+ results.push({
6159
6690
  label: "Postgres DB",
6160
- status: ok$1("restored")
6691
+ status: pg.ok ? ok$1("restored") : err(`failed: ${pg.message}`)
6161
6692
  });
6162
6693
  } catch (e) {
6163
6694
  const msg = e instanceof Error ? e.message.split("\n")[0] : String(e);
@@ -6165,7 +6696,7 @@ async function runRestore(backupPath, opts) {
6165
6696
  label: "Postgres DB",
6166
6697
  status: err(`failed: ${msg}`)
6167
6698
  });
6168
- console.log(warn$1(` Is Docker running with container '${DOCKER_CONTAINER}'?`));
6699
+ if (isContainerTarget(plan.target)) console.log(warn$1(` Is Docker running with container '${DOCKER_CONTAINER}'?`));
6169
6700
  }
6170
6701
  } else if (!hasSql || !opts.postgres) {
6171
6702
  const reason = !hasSql ? "no SQL dump in backup" : "--no-postgres";
@@ -6796,10 +7327,20 @@ const backupTop = {
6796
7327
  type: "boolean",
6797
7328
  default: false,
6798
7329
  summary: "Skip the Postgres dump (faster; registry and config only)"
7330
+ }, {
7331
+ ...OPT.dryRun,
7332
+ summary: "Print the database that would be dumped, write nothing"
6799
7333
  }],
6800
- examples: ["pai backup", "pai backup --no-postgres"],
7334
+ examples: [
7335
+ "pai backup",
7336
+ "pai backup --no-postgres",
7337
+ "pai backup --dry-run"
7338
+ ],
6801
7339
  seeAlso: ["pai restore"],
6802
- run: (c) => legacy(() => runBackup({ postgres: c.opts["no-postgres"] !== true }))
7340
+ run: (c) => legacy(() => runBackup({
7341
+ postgres: c.opts["no-postgres"] !== true,
7342
+ dryRun: c.dryRun
7343
+ }))
6803
7344
  };
6804
7345
  const restoreTop = {
6805
7346
  name: "restore",
@@ -6809,22 +7350,48 @@ const restoreTop = {
6809
7350
  name: "BACKUP",
6810
7351
  summary: "Backup folder (default: the newest one)"
6811
7352
  }],
6812
- options: [{
6813
- name: "no-postgres",
6814
- type: "boolean",
6815
- default: false,
6816
- summary: "Do not restore the Postgres database"
6817
- }, {
6818
- name: "yes",
6819
- type: "boolean",
6820
- default: false,
6821
- summary: "Do not ask for confirmation"
6822
- }],
6823
- examples: ["pai restore", "pai restore ~/.pai/backups/20260101-120000 --no-postgres --yes"],
7353
+ options: [
7354
+ {
7355
+ name: "no-postgres",
7356
+ type: "boolean",
7357
+ default: false,
7358
+ summary: "Do not restore the Postgres database"
7359
+ },
7360
+ {
7361
+ name: "yes",
7362
+ type: "boolean",
7363
+ default: false,
7364
+ summary: "Do not ask for confirmation"
7365
+ },
7366
+ {
7367
+ name: "into",
7368
+ type: "string",
7369
+ value: "DB",
7370
+ summary: "Restore into this Postgres database instead of the daemon's (required when the backup came from another database)"
7371
+ },
7372
+ {
7373
+ name: "no-safety-dump",
7374
+ type: "boolean",
7375
+ default: false,
7376
+ summary: "Do not dump the live database before replacing it (a failed safety dump otherwise aborts the restore)"
7377
+ },
7378
+ {
7379
+ ...OPT.dryRun,
7380
+ summary: "Print the target and source databases, change nothing"
7381
+ }
7382
+ ],
7383
+ examples: [
7384
+ "pai restore",
7385
+ "pai restore --dry-run",
7386
+ "pai restore ~/.pai/backups/20260101-120000 --no-postgres --yes"
7387
+ ],
6824
7388
  seeAlso: ["pai backup"],
6825
7389
  run: (c) => legacy(() => runRestore(c.args.BACKUP, {
6826
7390
  postgres: c.opts["no-postgres"] !== true,
6827
- yes: c.opts.yes === true
7391
+ yes: c.opts.yes === true,
7392
+ dryRun: c.dryRun,
7393
+ into: c.opts.into,
7394
+ noSafetyDump: c.opts["no-safety-dump"] === true
6828
7395
  }))
6829
7396
  };
6830
7397
  const updateTop = {
@@ -6955,8 +7522,12 @@ function createRl() {
6955
7522
  });
6956
7523
  return rl;
6957
7524
  }
6958
- /** Unattended-mode switches (`pai setup --yes --storage …`), set once by the command. */
6959
- const setupOptions = { yes: false };
7525
+ const setupOptions = {
7526
+ yes: false,
7527
+ answers: {}
7528
+ };
7529
+ /** Non-fatal step failures (e.g. a busy router port); setup finishes and the summary lists them. */
7530
+ const setupFailures = [];
6960
7531
  /**
6961
7532
  * The one place every wizard question passes through. Under --yes it answers
6962
7533
  * `defaultValue` without touching stdin; a question with no default cannot be
@@ -6993,7 +7564,12 @@ async function promptMenu(rl, options, defaultIdx = 0) {
6993
7564
  }
6994
7565
  }
6995
7566
  /** Prompt for a yes/no answer. Returns true for yes. */
6996
- async function promptYesNo(rl, question, defaultYes = true) {
7567
+ async function promptYesNo(rl, question, defaultYes = true, key) {
7568
+ const forced = key === void 0 ? void 0 : setupOptions.answers[key];
7569
+ if (forced !== void 0) {
7570
+ console.log(chalk.dim(` ${question.trim()} -> ${forced ? "yes" : "no"} (flag)`));
7571
+ return forced;
7572
+ }
6997
7573
  const hint = defaultYes ? "[Y/n]" : "[y/N]";
6998
7574
  const answer = await prompt(rl, ` ${question} ${chalk.dim(hint)}: `, "");
6999
7575
  if (answer === "") return defaultYes;
@@ -7149,7 +7725,7 @@ async function stepStorage(rl) {
7149
7725
  line();
7150
7726
  line(" Choose how PAI stores your indexed knowledge:");
7151
7727
  line();
7152
- const forced = setupOptions.storage ?? (setupOptions.yes ? await testPostgresConnection(postgresConnectionString({})) ? "postgres" : "sqlite" : void 0);
7728
+ const forced = setupOptions.storage ?? (setupOptions.pgUrl ? "postgres" : void 0) ?? (setupOptions.yes ? await testPostgresConnection(postgresConnectionString({})) ? "postgres" : "sqlite" : void 0);
7153
7729
  if ((forced ? forced === "sqlite" ? 1 : 0 : await promptMenu(rl, [{
7154
7730
  label: "PostgreSQL with pgvector",
7155
7731
  description: "Best for large collections, semantic search, and production use. Requires Docker or a Postgres server."
@@ -7161,6 +7737,15 @@ async function stepStorage(rl) {
7161
7737
  console.log(c.ok("SQLite selected. No additional setup needed."));
7162
7738
  return { storageBackend: "sqlite" };
7163
7739
  }
7740
+ if (setupOptions.pgUrl) {
7741
+ const connStr = setupOptions.pgUrl;
7742
+ const connected = await testPostgresConnection(connStr);
7743
+ console.log(connected ? c.ok("Connection successful!") : c.warn("Connection test failed — check --pg-url and try again later."));
7744
+ return {
7745
+ storageBackend: "postgres",
7746
+ postgres: { connectionString: connStr }
7747
+ };
7748
+ }
7164
7749
  line();
7165
7750
  line(" PostgreSQL requires a running Postgres server with the pgvector extension.");
7166
7751
  line();
@@ -7227,23 +7812,33 @@ async function stepBackend(rl, fresh) {
7227
7812
  const { results, recommended } = await detectBackends();
7228
7813
  line();
7229
7814
  for (const r of results) console.log(` ${r.ok ? c.ok(r.id) : c.dim(r.id)}: ${r.reason}`);
7815
+ if (fresh && setupOptions.embedding === "ollama" && recommended !== OLLAMA_ID) console.log(c.warn(" --embedding ollama: Ollama was not detected, so the local Arctic model is used. Start Ollama and run `pai memory backend use ollama`."));
7230
7816
  if (!recommended) return {};
7231
7817
  if (!fresh) {
7232
7818
  console.log(c.dim(` Recommended: ${recommended}. Switch with \`pai memory backend use ${recommended}\` + \`pai memory reembed\`.`));
7233
7819
  return {};
7234
7820
  }
7235
- if (recommended === OLLAMA_ID && await promptYesNo(rl, "Use Ollama (GPU, several times faster)?", true)) return { embedding: {
7821
+ if (recommended === OLLAMA_ID && await promptYesNo(rl, "Use Ollama (GPU, several times faster)?", true, "ollama")) return { embedding: {
7236
7822
  backend: OLLAMA_ID,
7237
7823
  model: OLLAMA_DEFAULT_MODEL,
7238
7824
  ollama: { baseUrl: ollamaDefaultUrl() }
7239
7825
  } };
7240
7826
  return {};
7241
7827
  }
7828
+ /** `--embedding` value to menu index (ollama = Arctic through the Ollama backend). */
7829
+ const EMBEDDING_FLAG_CHOICE = {
7830
+ ollama: 0,
7831
+ snowflake: 0,
7832
+ bge: 1,
7833
+ nomic: 2,
7834
+ none: 3
7835
+ };
7242
7836
  async function stepEmbedding(rl) {
7243
7837
  section("Step 3: Embedding Model");
7244
7838
  const existing = readConfigRaw();
7245
7839
  if (existing.embeddingModel) {
7246
7840
  console.log(c.ok(`Embedding model: ${existing.embeddingModel}. Skipping.`));
7841
+ if (setupOptions.embedding !== void 0) console.log(c.warn(` --embedding ${setupOptions.embedding} ignored: a model is already set. Change it with \`pai memory backend use\` or by editing embeddingModel in ${CONFIG_FILE$2}.`));
7247
7842
  return {
7248
7843
  embeddingModel: existing.embeddingModel,
7249
7844
  ...await stepBackend(rl, false)
@@ -7253,7 +7848,9 @@ async function stepEmbedding(rl) {
7253
7848
  line(" An embedding model converts your text into vectors for semantic search.");
7254
7849
  line(" Models are downloaded from HuggingFace on first use.");
7255
7850
  line();
7256
- const choice = await promptMenu(rl, [
7851
+ const flagged = setupOptions.embedding === void 0 ? void 0 : EMBEDDING_FLAG_CHOICE[setupOptions.embedding];
7852
+ if (setupOptions.embedding === "ollama") setupOptions.answers.ollama = true;
7853
+ const choice = flagged ?? await promptMenu(rl, [
7257
7854
  {
7258
7855
  label: "Snowflake Arctic Embed m v1.5",
7259
7856
  description: "768 dims, ~118MB download. Best retrieval quality per MB (MTEB score 55.14). Asymmetric retrieval — different handling for queries vs documents. Best for most users."
@@ -7321,7 +7918,7 @@ async function stepClaudeMd(rl) {
7321
7918
  console.log(c.dim(" A backup will be created before overwriting."));
7322
7919
  }
7323
7920
  line();
7324
- if (!await promptYesNo(rl, "Update ~/.claude/CLAUDE.md with the latest PAI template?", isGenerated)) {
7921
+ if (!await promptYesNo(rl, "Update ~/.claude/CLAUDE.md with the latest PAI template?", isGenerated, "claude-md")) {
7325
7922
  console.log(c.dim(" Keeping existing CLAUDE.md unchanged."));
7326
7923
  return false;
7327
7924
  }
@@ -7362,6 +7959,13 @@ async function stepClaudeMd(rl) {
7362
7959
  //#endregion
7363
7960
  //#region src/cli/commands/setup/steps/05-pai-skill.ts
7364
7961
  /** Step 5: PAI SKILL.md installation to ~/.claude/skills/PAI/. */
7962
+ /** `<file>.backup-<timestamp>`; never reuses an existing name, so earlier backups survive. */
7963
+ function timestampedBackupPath(file) {
7964
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7965
+ let p = `${file}.backup-${stamp}`;
7966
+ for (let i = 1; existsSync(p); i++) p = `${file}.backup-${stamp}-${i}`;
7967
+ return p;
7968
+ }
7365
7969
  async function stepPaiSkill(rl) {
7366
7970
  section("Step 5: PAI Skill Installation");
7367
7971
  line();
@@ -7384,12 +7988,12 @@ async function stepPaiSkill(rl) {
7384
7988
  console.log(c.dim(" A backup will be created before overwriting."));
7385
7989
  }
7386
7990
  line();
7387
- if (!await promptYesNo(rl, "Update ~/.claude/skills/PAI/SKILL.md with the latest PAI skill?", isGenerated)) {
7991
+ if (!await promptYesNo(rl, "Update ~/.claude/skills/PAI/SKILL.md with the latest PAI skill?", isGenerated, "skill")) {
7388
7992
  console.log(c.dim(" Keeping existing SKILL.md unchanged."));
7389
7993
  return false;
7390
7994
  }
7391
7995
  if (!isGenerated) {
7392
- const backupPath = skillFile + ".backup";
7996
+ const backupPath = timestampedBackupPath(skillFile);
7393
7997
  writeFileSync(backupPath, content, "utf-8");
7394
7998
  console.log(c.ok(`Backed up existing SKILL.md to ${backupPath}`));
7395
7999
  }
@@ -7433,12 +8037,12 @@ async function stepAiSteeringRules(rl) {
7433
8037
  console.log(c.dim(" A backup will be created before overwriting."));
7434
8038
  }
7435
8039
  line();
7436
- if (!await promptYesNo(rl, "Update ~/.claude/skills/PAI/AI-STEERING-RULES.md with the latest rules?", isGenerated)) {
8040
+ if (!await promptYesNo(rl, "Update ~/.claude/skills/PAI/AI-STEERING-RULES.md with the latest rules?", isGenerated, "steering-rules")) {
7437
8041
  console.log(c.dim(" Keeping existing AI-STEERING-RULES.md unchanged."));
7438
8042
  return false;
7439
8043
  }
7440
8044
  if (!isGenerated) {
7441
- const backupPath = skillFile + ".backup";
8045
+ const backupPath = timestampedBackupPath(skillFile);
7442
8046
  writeFileSync(backupPath, content, "utf-8");
7443
8047
  console.log(c.ok(`Backed up existing AI-STEERING-RULES.md to ${backupPath}`));
7444
8048
  }
@@ -7489,7 +8093,7 @@ async function stepSkillStubs(rl) {
7489
8093
  }
7490
8094
  console.log(c.dim(` Found ${skillNames.length} skill stubs to install.`));
7491
8095
  line();
7492
- if (!await promptYesNo(rl, `Symlink ${skillNames.length} PAI skill stubs to ~/.claude/skills/?`, true)) {
8096
+ if (!await promptYesNo(rl, `Symlink ${skillNames.length} PAI skill stubs to ~/.claude/skills/?`, true, "skill-stubs")) {
7493
8097
  console.log(c.dim(" Skipping skill stub installation."));
7494
8098
  return false;
7495
8099
  }
@@ -7549,7 +8153,7 @@ async function stepHooks(rl) {
7549
8153
  if (useSymlinks$1) line(" Files are symlinked so they auto-update when PAI is rebuilt.");
7550
8154
  else line(" Files are copied (Windows — re-run setup after PAI updates).");
7551
8155
  line();
7552
- if (!await promptYesNo(rl, "Install PAI lifecycle hooks (session stop, pre-compact, statusline)?", true)) {
8156
+ if (!await promptYesNo(rl, "Install PAI lifecycle hooks (session stop, pre-compact, statusline)?", true, "hooks")) {
7553
8157
  console.log(c.dim(" Skipping hook installation."));
7554
8158
  return false;
7555
8159
  }
@@ -7610,7 +8214,7 @@ async function stepTsHooks(rl) {
7610
8214
  if (useSymlinks) line(" Files are symlinked so they auto-update when PAI is rebuilt.");
7611
8215
  else line(" Files are copied (Windows — re-run setup after PAI updates).");
7612
8216
  line();
7613
- if (!await promptYesNo(rl, "Install PAI TypeScript hooks to ~/.claude/Hooks/?", true)) {
8217
+ if (!await promptYesNo(rl, "Install PAI TypeScript hooks to ~/.claude/Hooks/?", true, "ts-hooks")) {
7614
8218
  console.log(c.dim(" Skipping TypeScript hooks installation."));
7615
8219
  return false;
7616
8220
  }
@@ -7835,7 +8439,7 @@ async function stepDaName(rl) {
7835
8439
  line(" Choose a name for your AI assistant. This name appears in tab titles");
7836
8440
  line(" and session notes when hooks are active.");
7837
8441
  line();
7838
- const daName = await prompt(rl, chalk.bold(" Assistant name [PAI]: "), "") || "PAI";
8442
+ const daName = (setupOptions.daName ?? await prompt(rl, chalk.bold(" Assistant name [PAI]: "), "")) || "PAI";
7839
8443
  line();
7840
8444
  console.log(c.ok(`Assistant name set to: ${daName}`));
7841
8445
  return daName;
@@ -7846,7 +8450,7 @@ async function stepSettings(rl, daName) {
7846
8450
  line(" PAI will add env vars, all hook registrations, permissions, and flags");
7847
8451
  line(" to ~/.claude/settings.json. Existing values are never overwritten.");
7848
8452
  line();
7849
- if (!await promptYesNo(rl, "Patch ~/.claude/settings.json with PAI hooks, env vars, and settings?", true)) {
8453
+ if (!await promptYesNo(rl, "Patch ~/.claude/settings.json with PAI hooks, env vars, and settings?", true, "settings")) {
7850
8454
  console.log(c.dim(" Skipping settings patch."));
7851
8455
  return false;
7852
8456
  }
@@ -8255,24 +8859,17 @@ function cmdInstall() {
8255
8859
  const config = readClaudeJson();
8256
8860
  if (typeof config.mcpServers !== "object" || config.mcpServers === null) config.mcpServers = {};
8257
8861
  const servers = config.mcpServers;
8258
- const existing = servers["pai"];
8259
- if ((existing?.args)?.includes(shimBin)) console.log(ok$1(" ~/.claude.json already points to the shim."));
8260
- else {
8261
- if (existing) {
8262
- servers["pai-legacy"] = existing;
8263
- console.log(dim$1(" Backed up existing 'pai' entry as 'pai-legacy'."));
8264
- }
8265
- servers["pai"] = {
8266
- command: "node",
8267
- args: [shimBin]
8268
- };
8862
+ const outcome = registerMcpScript(servers, "pai", shimBin);
8863
+ if (outcome === "unchanged") console.log(ok$1(" ~/.claude.json already points to the shim."));
8864
+ else {
8269
8865
  try {
8270
8866
  writeClaudeJson(config);
8271
- console.log(ok$1(" Updated ~/.claude.json to use daemon shim."));
8272
8867
  } catch (e) {
8273
8868
  console.error(err(` Failed to write ~/.claude.json: ${e}`));
8274
8869
  return 1;
8275
8870
  }
8871
+ if (outcome === "legacy-kept") console.log(dim$1(" Existing 'pai' entry runs a different server; kept as 'pai-legacy'."));
8872
+ console.log(ok$1(" Updated ~/.claude.json to use daemon shim."));
8276
8873
  }
8277
8874
  console.log();
8278
8875
  if (!existsSync(daemonBin)) {
@@ -8432,8 +9029,8 @@ function cmdLogs(opts) {
8432
9029
  return 0;
8433
9030
  }
8434
9031
  async function daemonServeCmd() {
8435
- const { serve } = await import("../daemon-C7yt_ptC.mjs");
8436
- const { loadConfig: lc, ensureConfigDir } = await import("../config-TaWmdTLx.mjs");
9032
+ const { serve } = await import("../daemon-DKdBNKfa.mjs");
9033
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-DtENJ6cL.mjs");
8437
9034
  ensureConfigDir();
8438
9035
  await serve(lc());
8439
9036
  return 0;
@@ -8591,11 +9188,11 @@ async function stepDaemon(rl) {
8591
9188
  if (existsSync(plistPath)) {
8592
9189
  console.log(c.dim(` PAI daemon ${unit} already installed.`));
8593
9190
  line();
8594
- if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false)) {
9191
+ if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false, "daemon")) {
8595
9192
  console.log(c.dim(" Keeping existing daemon installation."));
8596
9193
  return false;
8597
9194
  }
8598
- } else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true)) {
9195
+ } else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true, "daemon")) {
8599
9196
  console.log(c.dim(" Skipping daemon install. Run manually: pai daemon install"));
8600
9197
  return false;
8601
9198
  }
@@ -8627,7 +9224,7 @@ async function stepMcp(rl) {
8627
9224
  return false;
8628
9225
  }
8629
9226
  } catch {}
8630
- if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true)) {
9227
+ if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true, "mcp")) {
8631
9228
  console.log(c.dim(" Skipping MCP registration. Run manually: pai mcp install"));
8632
9229
  return false;
8633
9230
  }
@@ -8660,7 +9257,7 @@ async function stepDirectories(rl) {
8660
9257
  for (const d of defaults) console.log(chalk.dim(` ${d}`));
8661
9258
  line();
8662
9259
  }
8663
- const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false);
9260
+ const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false, "registry-scan");
8664
9261
  if (runScan) {
8665
9262
  line();
8666
9263
  console.log(chalk.dim(" Registry scan will run after setup completes."));
@@ -8720,7 +9317,7 @@ async function stepInitialIndex(rl) {
8720
9317
  //#endregion
8721
9318
  //#region src/cli/commands/setup/steps/15-verify.ts
8722
9319
  /** Step 18: Setup summary — displays all configuration choices made during setup. */
8723
- function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform) {
9320
+ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform, aibroker, failures = []) {
8724
9321
  section("Setup Complete");
8725
9322
  line();
8726
9323
  console.log(chalk.green(" PAI Knowledge OS is configured!"));
@@ -8741,6 +9338,16 @@ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiStee
8741
9338
  console.log(chalk.dim(" Settings: ") + chalk.cyan(settingsPatched ? "env vars, hooks, permissions, flags (patched)" : "(unchanged)"));
8742
9339
  console.log(chalk.dim(" Daemon: ") + chalk.cyan(daemonInstalled ? `${daemonServiceName(plat)} (installed)` : "(not installed by setup)"));
8743
9340
  console.log(chalk.dim(" MCP: ") + chalk.cyan(mcpRegistered ? "registered in ~/.claude.json" : "(unchanged)"));
9341
+ if (aibroker) {
9342
+ const label = {
9343
+ ok: "installed and healthy",
9344
+ installed: "installed",
9345
+ skipped: "skipped",
9346
+ failed: "FAILED — retry: npm install -g aibroker@^0.62.4 && aibroker setup && aibroker doctor"
9347
+ }[aibroker];
9348
+ console.log(chalk.dim(" AIBroker: ") + (aibroker === "failed" ? chalk.red(label) : chalk.cyan(label)));
9349
+ }
9350
+ for (const f of failures) console.log(chalk.red(` ${f}`));
8744
9351
  line();
8745
9352
  console.log(chalk.bold.yellow(" → RESTART Claude Code to activate all changes."));
8746
9353
  line();
@@ -9395,7 +10002,7 @@ async function stepTaskBus(rl) {
9395
10002
  line();
9396
10003
  line(" PAI works fully without this. Skip it unless you want cross-session tasks.");
9397
10004
  line();
9398
- if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false)) {
10005
+ if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false, "task-bus")) {
9399
10006
  line();
9400
10007
  console.log(c.ok("Skipping the task bus."));
9401
10008
  console.log(c.dim(" Add later by re-running `pai setup`."));
@@ -9482,7 +10089,7 @@ async function stepWorkers(rl) {
9482
10089
  line(" `pai worker run` is the runner; ps / follow / replay watch the runs;");
9483
10090
  line(" the Agent tool is denied and rewritten to it while workers are on.");
9484
10091
  line();
9485
- if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true)) {
10092
+ if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true, "workers")) {
9486
10093
  line();
9487
10094
  console.log(c.ok("Skipping workers."));
9488
10095
  console.log(c.dim(" Later: pai worker on (other providers: pai worker providers add <name> … — see docs/worker.md)"));
@@ -9501,6 +10108,209 @@ async function stepWorkers(rl) {
9501
10108
  return {};
9502
10109
  }
9503
10110
 
10111
+ //#endregion
10112
+ //#region src/cli/commands/setup/steps/18-aibroker.ts
10113
+ /** Step 18: AIBroker, PAI's messaging layer (session messages, worker reports, tab names, WhatsApp/Telegram). */
10114
+ /** Longest any one AIBroker command may run before it counts as failed. */
10115
+ const COMMAND_TIMEOUT_MS = 5 * 6e4;
10116
+ /** `aibroker setup` may ask questions: give it the terminal when there is one and --yes is off. */
10117
+ const defaultRunner = (cmd, args) => {
10118
+ const r = spawnSync(cmd, args, {
10119
+ encoding: "utf8",
10120
+ timeout: COMMAND_TIMEOUT_MS,
10121
+ ...cmd === "aibroker" && args[0] === "setup" && !setupOptions.yes && !!process.stdin.isTTY ? { stdio: "inherit" } : {}
10122
+ });
10123
+ return {
10124
+ status: r.error ? null : r.status,
10125
+ output: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? String(r.error.message) : ""}`.trim()
10126
+ };
10127
+ };
10128
+ /** The package the step installs; the range keeps a breaking major from arriving unannounced. */
10129
+ const AIBROKER_PACKAGE = "aibroker@^0.62.4";
10130
+ /** `aibroker` is not on PATH: spawn error (null) or the shell's "not found". */
10131
+ const isMissing = (r) => r.status === null || r.status === 127;
10132
+ /** The commands the step runs when AIBroker is missing or unhealthy, in order. */
10133
+ const AIBROKER_COMMANDS = [
10134
+ ["npm", [
10135
+ "install",
10136
+ "-g",
10137
+ AIBROKER_PACKAGE
10138
+ ]],
10139
+ ["aibroker", ["setup"]],
10140
+ ["aibroker", ["doctor"]]
10141
+ ];
10142
+ const show = ([cmd, args]) => [cmd, ...args].join(" ");
10143
+ function aibrokerPlan() {
10144
+ return AIBROKER_COMMANDS.map(show);
10145
+ }
10146
+ function stepAibroker(run = defaultRunner, opts = {
10147
+ skip: setupOptions.noAibroker,
10148
+ dryRun: setupOptions.dryRun
10149
+ }) {
10150
+ section("Step 18: AIBroker (Messaging)");
10151
+ line();
10152
+ line(" AIBroker is PAI's messaging layer: session-to-session messages, worker reports,");
10153
+ line(" terminal tab names, WhatsApp/Telegram/PAILot. PAI works without it, minus messaging.");
10154
+ line();
10155
+ if (opts.skip) {
10156
+ console.log(c.dim(" Skipped (--no-aibroker). Install later: npm install -g aibroker@^0.62.4 && aibroker setup"));
10157
+ return "skipped";
10158
+ }
10159
+ if (opts.dryRun) {
10160
+ line(" Would run (if AIBroker is missing or `aibroker doctor` fails):");
10161
+ for (const p of aibrokerPlan()) line(` ${p}`);
10162
+ return "skipped";
10163
+ }
10164
+ const present = run("aibroker", ["doctor"]);
10165
+ if (present.status === 0) {
10166
+ console.log(c.ok("AIBroker already installed and healthy."));
10167
+ return "ok";
10168
+ }
10169
+ const steps = isMissing(present) ? AIBROKER_COMMANDS : AIBROKER_COMMANDS.slice(1);
10170
+ for (const step of steps) {
10171
+ const r = run(step[0], step[1]);
10172
+ if (r.status !== 0) {
10173
+ console.log(c.warn(` \`${show(step)}\` failed (exit ${r.status ?? "n/a"}).`));
10174
+ if (r.output) line(` ${r.output.split("\n").join("\n ")}`);
10175
+ console.log(c.warn(` Retry with: ${aibrokerPlan().slice(AIBROKER_COMMANDS.indexOf(step)).join(" && ")}`));
10176
+ return "failed";
10177
+ }
10178
+ console.log(c.ok(`${show(step)}: ok`));
10179
+ }
10180
+ console.log(c.ok("AIBroker installed."));
10181
+ return "installed";
10182
+ }
10183
+
10184
+ //#endregion
10185
+ //#region src/cli/commands/setup/steps/19-router.ts
10186
+ /** Step 19: the model router and its cockpit (enabled, port, reach). Answers go to config.yaml as router.enabled / router.port / router.bind. */
10187
+ /** Tries to listen on `host` (loopback by default); when taken, asks lsof who holds it. */
10188
+ const defaultPortProbe = (port, host = "127.0.0.1") => new Promise((resolve) => {
10189
+ const srv = createServer();
10190
+ srv.once("error", () => {
10191
+ resolve({
10192
+ free: false,
10193
+ holder: (spawnSync("lsof", [
10194
+ "-nP",
10195
+ `-iTCP:${port}`,
10196
+ "-sTCP:LISTEN"
10197
+ ], { encoding: "utf8" }).stdout ?? "").split("\n")[1]?.trim().split(/\s+/).slice(0, 2).join(" pid ") || void 0
10198
+ });
10199
+ });
10200
+ srv.listen(port, host, () => srv.close(() => resolve({ free: true })));
10201
+ });
10202
+ function validPort(v) {
10203
+ const n = typeof v === "number" ? v : /^\d+$/.test(String(v).trim()) ? Number(v) : NaN;
10204
+ return Number.isInteger(n) && n >= 1024 && n <= 65535 ? n : null;
10205
+ }
10206
+ /** `loopback`, `tailscale` or a dotted IPv4 (never 0.0.0.0); returns the normalised value or null. */
10207
+ function validBind(v) {
10208
+ const b = v.trim().toLowerCase();
10209
+ return resolveBindAddress(b) === null && b !== "tailscale" ? null : b || "loopback";
10210
+ }
10211
+ async function stepRouter(rl, probe = defaultPortProbe, runningPid = routerRunningPid) {
10212
+ section("Step 19: Router and Cockpit (Optional)");
10213
+ line();
10214
+ line(" The router accounts model usage, schedules local models and serves the cockpit page.");
10215
+ line(" The daemon keeps it running when enabled.");
10216
+ line();
10217
+ const cfg = loadConfig().router;
10218
+ const raw = readConfigRaw().router ?? {};
10219
+ const flags = setupOptions.router ?? {};
10220
+ if (!(flags.enabled ?? await promptYesNo(rl, "Enable the router and cockpit?", typeof raw.enabled === "boolean" ? raw.enabled : true))) {
10221
+ console.log(c.ok("Router disabled. Enable later: pai config set router.enabled true"));
10222
+ return { router: {
10223
+ ...raw,
10224
+ enabled: false
10225
+ } };
10226
+ }
10227
+ const routerUp = runningPid() !== null;
10228
+ const check = async (port, host) => {
10229
+ if (routerUp && port === cfg.port) return null;
10230
+ const p = await probe(port, host);
10231
+ return p.free ? null : `Port ${port} is in use${p.holder ? ` by ${p.holder}` : ""}.`;
10232
+ };
10233
+ const unavailable = (why) => {
10234
+ console.log(c.warn(` ${why}`));
10235
+ setupFailures.push(`Router not configured: ${why} Retry: pai setup --router-port <free port>`);
10236
+ return {};
10237
+ };
10238
+ let port;
10239
+ if (flags.port !== void 0) {
10240
+ const v = validPort(flags.port);
10241
+ if (v === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${flags.port}"`);
10242
+ const taken = await check(v);
10243
+ if (taken) return unavailable(taken);
10244
+ port = v;
10245
+ } else {
10246
+ port = cfg.port;
10247
+ for (;;) {
10248
+ const answer = await prompt(rl, chalk.bold(` Cockpit port [${cfg.port}]: `), "");
10249
+ const v = answer === "" ? cfg.port : validPort(answer);
10250
+ if (v === null) {
10251
+ console.log(c.warn("Enter a port from 1024 to 65535."));
10252
+ continue;
10253
+ }
10254
+ const taken = await check(v);
10255
+ if (!taken) {
10256
+ port = v;
10257
+ break;
10258
+ }
10259
+ if (setupOptions.yes) return unavailable(taken);
10260
+ console.log(c.warn(taken));
10261
+ }
10262
+ }
10263
+ let bind;
10264
+ if (flags.bind !== void 0) {
10265
+ const v = validBind(flags.bind);
10266
+ if (v === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${flags.bind}"`);
10267
+ bind = v;
10268
+ } else {
10269
+ const current = String(raw.bind ?? "loopback");
10270
+ const explicit = current !== "loopback" && current !== "tailscale" && validBind(current) !== null ? current : void 0;
10271
+ line();
10272
+ line(" Who can reach the cockpit? Beyond loopback it is reachable from other devices (behind a token).");
10273
+ line();
10274
+ const idx = await promptMenu(rl, [
10275
+ {
10276
+ label: "loopback",
10277
+ description: "This machine only (127.0.0.1)."
10278
+ },
10279
+ {
10280
+ label: "tailscale",
10281
+ description: "Your other Tailscale devices."
10282
+ },
10283
+ {
10284
+ label: "explicit address",
10285
+ description: "A specific IPv4 address of this machine."
10286
+ }
10287
+ ], current === "tailscale" ? 1 : explicit ? 2 : 0);
10288
+ bind = idx === 0 ? "loopback" : "tailscale";
10289
+ if (idx === 2) for (;;) {
10290
+ const a = validBind(await prompt(rl, chalk.bold(` IPv4 address${explicit ? ` [${explicit}]` : ""}: `), explicit ?? ""));
10291
+ if (a && a !== "tailscale") {
10292
+ bind = a;
10293
+ break;
10294
+ }
10295
+ console.log(c.warn("Enter a dotted IPv4 address (not 0.0.0.0)."));
10296
+ }
10297
+ }
10298
+ const host = bind === "loopback" ? void 0 : resolveBindAddress(bind) ?? void 0;
10299
+ if (host && !(routerUp && port === cfg.port)) {
10300
+ const taken = await check(port, host);
10301
+ if (taken) return unavailable(`${taken.slice(0, -1)} on ${host}.`);
10302
+ }
10303
+ if (bind !== "loopback") console.log(c.warn(` The cockpit will be reachable from other devices via ${bind}; it asks for the token in ~/.pai/router-cockpit.token.`));
10304
+ if (bind === "tailscale" && resolveBindAddress("tailscale") === null) console.log(c.warn(" Tailscale is not up here; the extra listener starts once it is."));
10305
+ console.log(c.ok(`Router enabled on port ${port}, reach: ${bind}.`));
10306
+ return { router: {
10307
+ ...raw,
10308
+ enabled: true,
10309
+ port,
10310
+ bind
10311
+ } };
10312
+ }
10313
+
9504
10314
  //#endregion
9505
10315
  //#region src/cli/commands/setup/index.ts
9506
10316
  /**
@@ -9516,6 +10326,11 @@ async function step(name, fn) {
9516
10326
  }
9517
10327
  }
9518
10328
  async function runSetup() {
10329
+ setupFailures.length = 0;
10330
+ if (setupOptions.dryRun) {
10331
+ stepAibroker();
10332
+ return;
10333
+ }
9519
10334
  const rl = createRl();
9520
10335
  try {
9521
10336
  if (existsSync(CONFIG_FILE$2)) {
@@ -9546,6 +10361,8 @@ async function runSetup() {
9546
10361
  const daName = await step("stepDaName", () => stepDaName(rl));
9547
10362
  const settingsPatched = await step("stepSettings", () => stepSettings(rl, daName));
9548
10363
  const daemonInstalled = await step("stepDaemon", () => stepDaemon(rl));
10364
+ const routerConfig = await step("stepRouter", () => stepRouter(rl));
10365
+ const aibroker = await step("stepAibroker", () => stepAibroker());
9549
10366
  const mcpRegistered = await step("stepMcp", () => stepMcp(rl));
9550
10367
  await step("stepDirectories", () => stepDirectories(rl));
9551
10368
  const taskConfig = await step("stepTaskBus", () => stepTaskBus(rl));
@@ -9554,50 +10371,338 @@ async function runSetup() {
9554
10371
  ...storageConfig,
9555
10372
  ...embeddingConfig,
9556
10373
  ...taskConfig,
9557
- ...workersConfig
10374
+ ...workersConfig,
10375
+ ...routerConfig
9558
10376
  };
9559
10377
  mergeConfig(allUpdates);
9560
10378
  line();
9561
10379
  console.log(chalk.green(" Configuration saved."));
9562
10380
  await step("stepInitialIndex", () => stepInitialIndex(rl));
9563
- stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered);
10381
+ stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, process.platform, aibroker, setupFailures);
9564
10382
  } finally {
9565
10383
  rl.close();
9566
10384
  }
9567
10385
  }
9568
10386
 
10387
+ //#endregion
10388
+ //#region src/cli/commands/setup/questions.ts
10389
+ readConfigRaw();
10390
+ /** yes/no components: key = setupOptions.answers key, flag form depends on the default. */
10391
+ const COMPONENTS = [
10392
+ {
10393
+ key: "claude-md",
10394
+ step: "Agent configuration",
10395
+ question: "Write ~/.claude/CLAUDE.md from the PAI template?",
10396
+ default: true
10397
+ },
10398
+ {
10399
+ key: "skill",
10400
+ step: "PAI skill",
10401
+ question: "Install the PAI skill?",
10402
+ default: true
10403
+ },
10404
+ {
10405
+ key: "steering-rules",
10406
+ step: "Steering rules",
10407
+ question: "Install the AI steering rules?",
10408
+ default: true
10409
+ },
10410
+ {
10411
+ key: "skill-stubs",
10412
+ step: "Skill stubs",
10413
+ question: "Symlink the PAI skill stubs into ~/.claude/skills/?",
10414
+ default: true
10415
+ },
10416
+ {
10417
+ key: "hooks",
10418
+ step: "Hooks",
10419
+ question: "Install the lifecycle hooks (session stop, pre-compact, statusline)?",
10420
+ default: true
10421
+ },
10422
+ {
10423
+ key: "ts-hooks",
10424
+ step: "TypeScript hooks",
10425
+ question: "Install the TypeScript hooks to ~/.claude/Hooks/?",
10426
+ default: true
10427
+ },
10428
+ {
10429
+ key: "settings",
10430
+ step: "Settings",
10431
+ question: "Patch ~/.claude/settings.json with PAI hooks, env vars and settings?",
10432
+ default: true
10433
+ },
10434
+ {
10435
+ key: "daemon",
10436
+ step: "Daemon",
10437
+ question: "Install the daemon as a login service?",
10438
+ default: true
10439
+ },
10440
+ {
10441
+ key: "mcp",
10442
+ step: "MCP",
10443
+ question: "Register the PAI MCP server in ~/.claude.json?",
10444
+ default: true
10445
+ },
10446
+ {
10447
+ key: "registry-scan",
10448
+ step: "Directories",
10449
+ question: "Run `pai registry scan` after setup?",
10450
+ default: false
10451
+ },
10452
+ {
10453
+ key: "task-bus",
10454
+ step: "Task bus",
10455
+ question: "Enable the task bus (needs a Todoist account)?",
10456
+ default: false
10457
+ },
10458
+ {
10459
+ key: "workers",
10460
+ step: "Workers",
10461
+ question: "Turn workers on with the built-in anthropic provider?",
10462
+ default: true
10463
+ }
10464
+ ];
10465
+ /** Components whose answer is not a plain default. */
10466
+ const COMPONENT_NOTES = {
10467
+ "registry-scan": "Off by default: scanning walks your project directories.",
10468
+ "task-bus": "Off by default. Needs a Todoist token in the config; with --yes the token cannot be asked, so set it first."
10469
+ };
10470
+ const componentFlag = (key, def) => def ? `--no-${key}` : `--${key}`;
10471
+ function setupQuestions() {
10472
+ const raw = readConfigRaw();
10473
+ const router = loadConfig().router;
10474
+ const rawRouter = raw.router ?? {};
10475
+ const q = [];
10476
+ const cur = (v) => v === void 0 || v === null || v === "" ? {} : { current: v };
10477
+ q.push({
10478
+ id: "storage",
10479
+ step: "Storage",
10480
+ question: "Where should PAI store its data?",
10481
+ type: "choice",
10482
+ flag: "--storage",
10483
+ default: "sqlite",
10484
+ allowed: ["sqlite", "postgres"],
10485
+ ...cur(raw.storageBackend),
10486
+ note: "With --yes and no --storage: postgres when a local PostgreSQL answers, else sqlite."
10487
+ });
10488
+ q.push({
10489
+ id: "pg-url",
10490
+ step: "Storage",
10491
+ question: "PostgreSQL connection string (only when storage is postgres; default: the PAI Docker container)",
10492
+ type: "string",
10493
+ flag: "--pg-url",
10494
+ default: null,
10495
+ ...cur((raw.postgres ?? {}).connectionString)
10496
+ });
10497
+ q.push({
10498
+ id: "embedding",
10499
+ step: "Embedding",
10500
+ question: "Which embedding model for semantic search?",
10501
+ type: "choice",
10502
+ flag: "--embedding",
10503
+ default: "snowflake",
10504
+ allowed: [
10505
+ "snowflake",
10506
+ "bge",
10507
+ "nomic",
10508
+ "ollama",
10509
+ "none"
10510
+ ],
10511
+ ...cur(raw.embeddingModel)
10512
+ });
10513
+ const component = (comp) => {
10514
+ const note = COMPONENT_NOTES[comp.key];
10515
+ q.push({
10516
+ id: comp.key,
10517
+ step: comp.step,
10518
+ question: comp.question,
10519
+ type: "bool",
10520
+ flag: componentFlag(comp.key, comp.default),
10521
+ default: comp.default,
10522
+ ...note ? { note } : {}
10523
+ });
10524
+ };
10525
+ const routerQuestions = () => {
10526
+ q.push({
10527
+ id: "router",
10528
+ step: "Router and cockpit",
10529
+ question: "Enable the router and cockpit?",
10530
+ type: "bool",
10531
+ flag: "--router / --no-router",
10532
+ default: true,
10533
+ ...cur(typeof rawRouter.enabled === "boolean" ? rawRouter.enabled : void 0),
10534
+ note: "Setup defaults to on; the daemon's own default (key absent) is off. A re-run with router.enabled absent turns it on unless --no-router is given."
10535
+ });
10536
+ q.push({
10537
+ id: "router-port",
10538
+ step: "Router and cockpit",
10539
+ question: "Port for the cockpit?",
10540
+ type: "port",
10541
+ flag: "--router-port",
10542
+ default: router.port,
10543
+ ...cur(rawRouter.port)
10544
+ });
10545
+ q.push({
10546
+ id: "router-bind",
10547
+ step: "Router and cockpit",
10548
+ question: "Who can reach the cockpit? Beyond loopback it is reachable from other devices.",
10549
+ type: "choice",
10550
+ flag: "--router-bind",
10551
+ default: "loopback",
10552
+ allowed: [
10553
+ "loopback",
10554
+ "tailscale",
10555
+ "<ipv4 address>"
10556
+ ],
10557
+ ...cur(rawRouter.bind)
10558
+ });
10559
+ };
10560
+ const daName = () => q.push({
10561
+ id: "da-name",
10562
+ step: "Settings",
10563
+ question: "What should your assistant be called?",
10564
+ type: "string",
10565
+ flag: "--da-name",
10566
+ default: "PAI"
10567
+ });
10568
+ const aibroker = () => q.push({
10569
+ id: "aibroker",
10570
+ step: "AIBroker",
10571
+ question: "Install AIBroker (messaging)?",
10572
+ type: "bool",
10573
+ flag: "--no-aibroker",
10574
+ default: true
10575
+ });
10576
+ for (const comp of COMPONENTS) {
10577
+ if (comp.key === "settings") daName();
10578
+ component(comp);
10579
+ if (comp.key === "daemon") {
10580
+ routerQuestions();
10581
+ aibroker();
10582
+ }
10583
+ }
10584
+ return q;
10585
+ }
10586
+ function renderQuestions(qs) {
10587
+ return qs.map((x, i) => {
10588
+ const parts = [`${i + 1}. [${x.step}] ${x.question}`, ` flag: ${x.flag} default: ${x.default ?? "(none)"}`];
10589
+ if (x.allowed) parts.push(` allowed: ${x.allowed.join(" | ")}`);
10590
+ if (x.current !== void 0) parts.push(` current: ${x.current} (already set)`);
10591
+ if (x.note) parts.push(` note: ${x.note}`);
10592
+ return parts.join("\n");
10593
+ }).join("\n");
10594
+ }
10595
+
9569
10596
  //#endregion
9570
10597
  //#region src/cli/commands/setup.ts
9571
10598
  /** `pai setup`: the first-time setup wizard (steps live in ./setup/). */
10599
+ /** One flag per yes/no component: --no-<name> for default-yes ones, --<name> for default-no ones. */
10600
+ const componentOptions = COMPONENTS.map((comp) => ({
10601
+ name: componentFlag(comp.key, comp.default).slice(2),
10602
+ type: "boolean",
10603
+ default: false,
10604
+ summary: `${comp.default ? "Skip" : "Do"}: ${comp.question}`
10605
+ }));
9572
10606
  const setupCommand = {
9573
10607
  name: "setup",
9574
- summary: "First-time setup: storage, search, hooks, daemon",
9575
- description: "Walks through everything PAI needs: where data is stored, search embeddings, Claude Code hooks and settings, the background daemon and the first index.\nSafe to run again. With --yes it takes every default and asks nothing.",
9576
- options: [{
9577
- name: "yes",
9578
- short: "y",
9579
- type: "boolean",
9580
- default: false,
9581
- summary: "Take every default and read no input"
9582
- }, {
9583
- name: "storage",
9584
- type: "string",
9585
- value: "BACKEND",
9586
- summary: "Where to store data: sqlite or postgres (with --yes: sqlite unless a local Postgres answers)"
9587
- }],
9588
- examples: ["pai setup", "pai setup --yes --storage sqlite"],
10608
+ summary: "First-time setup: storage, search, hooks, daemon, router",
10609
+ description: "Walks through everything PAI needs: where data is stored, search embeddings, Claude Code hooks and settings, the background daemon, the router and cockpit (port, reach), AIBroker (messaging; installed automatically) and the first index.\nSafe to run again. With --yes it takes every default and asks nothing; every question has a flag, so `pai setup --yes <flags>` can express any answer. `pai setup --questions [--json]` lists the questions with their flags, defaults and current values (for callers without a terminal).",
10610
+ options: [
10611
+ {
10612
+ name: "yes",
10613
+ short: "y",
10614
+ type: "boolean",
10615
+ default: false,
10616
+ summary: "Take every default (or the flag given) and read no input"
10617
+ },
10618
+ {
10619
+ name: "questions",
10620
+ type: "boolean",
10621
+ default: false,
10622
+ summary: "Print every question with its flag, default and allowed values; change nothing"
10623
+ },
10624
+ {
10625
+ name: "json",
10626
+ type: "boolean",
10627
+ default: false,
10628
+ summary: "With --questions: machine-readable JSON"
10629
+ },
10630
+ {
10631
+ name: "storage",
10632
+ type: "string",
10633
+ value: "BACKEND",
10634
+ summary: "Where to store data: sqlite or postgres (with --yes: sqlite unless a local Postgres answers)"
10635
+ },
10636
+ {
10637
+ name: "pg-url",
10638
+ type: "string",
10639
+ value: "URL",
10640
+ summary: "PostgreSQL connection string (implies --storage postgres)"
10641
+ },
10642
+ {
10643
+ name: "embedding",
10644
+ type: "string",
10645
+ value: "MODEL",
10646
+ summary: "Embeddings: snowflake, bge, nomic, ollama (Arctic on Ollama) or none"
10647
+ },
10648
+ {
10649
+ name: "da-name",
10650
+ type: "string",
10651
+ value: "NAME",
10652
+ summary: "What the assistant is called (default PAI)"
10653
+ },
10654
+ {
10655
+ name: "router",
10656
+ type: "boolean",
10657
+ summary: "Enable the router and cockpit (default)"
10658
+ },
10659
+ {
10660
+ name: "no-router",
10661
+ type: "boolean",
10662
+ summary: "Do not enable the router and cockpit"
10663
+ },
10664
+ {
10665
+ name: "router-port",
10666
+ type: "number",
10667
+ value: "PORT",
10668
+ summary: "Cockpit port (default: config router.port, 8790)"
10669
+ },
10670
+ {
10671
+ name: "router-bind",
10672
+ type: "string",
10673
+ value: "SCOPE",
10674
+ summary: "Who reaches the cockpit: loopback (default), tailscale or an IPv4 address"
10675
+ },
10676
+ {
10677
+ name: "no-aibroker",
10678
+ type: "boolean",
10679
+ default: false,
10680
+ summary: "Do not install AIBroker (the messaging layer)"
10681
+ },
10682
+ ...componentOptions,
10683
+ {
10684
+ name: "dry-run",
10685
+ type: "boolean",
10686
+ default: false,
10687
+ summary: "Validate the flags, print the AIBroker install commands and change nothing (other steps are not previewed)"
10688
+ }
10689
+ ],
10690
+ examples: [
10691
+ "pai setup",
10692
+ "pai setup --yes --storage sqlite",
10693
+ "pai setup --questions --json",
10694
+ "pai setup --yes --storage sqlite --embedding none --router-port 9000 --router-bind tailscale --no-hooks"
10695
+ ],
9589
10696
  seeAlso: ["pai update", "pai daemon status"],
9590
10697
  run(c) {
9591
- const storage = c.opts.storage;
9592
10698
  return legacy(async () => {
9593
- if (storage && storage !== "sqlite" && storage !== "postgres") {
9594
- console.error(`pai setup: --storage must be sqlite or postgres, got "${storage}"`);
9595
- process.exitCode = 1;
10699
+ if (c.opts.questions === true) {
10700
+ const qs = setupQuestions();
10701
+ console.log(c.opts.json === true ? JSON.stringify(qs, null, 2) : renderQuestions(qs));
9596
10702
  return;
9597
10703
  }
9598
- setupOptions.yes = c.opts.yes === true;
9599
- setupOptions.storage = storage;
9600
10704
  try {
10705
+ applySetupFlags(c.opts);
9601
10706
  await runSetup();
9602
10707
  } catch (e) {
9603
10708
  console.error(chalk.red(`pai setup: ${e instanceof Error ? e.message : String(e)}`));
@@ -9606,6 +10711,39 @@ const setupCommand = {
9606
10711
  });
9607
10712
  }
9608
10713
  };
10714
+ /** Validates the flags and copies them into setupOptions (throws naming the flag). */
10715
+ function applySetupFlags(opts) {
10716
+ const storage = opts.storage;
10717
+ if (storage && storage !== "sqlite" && storage !== "postgres") throw new Error(`--storage must be sqlite or postgres, got "${storage}"`);
10718
+ const embedding = opts.embedding;
10719
+ if (embedding && ![
10720
+ "snowflake",
10721
+ "bge",
10722
+ "nomic",
10723
+ "ollama",
10724
+ "none"
10725
+ ].includes(embedding)) throw new Error(`--embedding must be snowflake, bge, nomic, ollama or none, got "${embedding}"`);
10726
+ const port = opts["router-port"];
10727
+ if (port !== void 0 && validPort(port) === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${port}"`);
10728
+ const bind = opts["router-bind"];
10729
+ if (bind !== void 0 && validBind(String(bind)) === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${bind}"`);
10730
+ if (opts.router === true && opts["no-router"] === true) throw new Error("--router and --no-router contradict each other");
10731
+ const answers = {};
10732
+ for (const comp of COMPONENTS) if (opts[componentFlag(comp.key, comp.default).slice(2)] === true) answers[comp.key] = !comp.default;
10733
+ setupOptions.yes = opts.yes === true;
10734
+ setupOptions.noAibroker = opts["no-aibroker"] === true;
10735
+ setupOptions.dryRun = opts["dry-run"] === true;
10736
+ setupOptions.storage = storage;
10737
+ setupOptions.pgUrl = opts["pg-url"];
10738
+ setupOptions.embedding = embedding;
10739
+ setupOptions.daName = opts["da-name"];
10740
+ setupOptions.router = {
10741
+ enabled: opts["no-router"] === true ? false : opts.router === true ? true : void 0,
10742
+ port: opts["router-port"],
10743
+ bind: opts["router-bind"]
10744
+ };
10745
+ setupOptions.answers = answers;
10746
+ }
9609
10747
  const installAlias = {
9610
10748
  ...setupCommand,
9611
10749
  name: "install",
@@ -10399,29 +11537,46 @@ function migrateFederationDbOrphan(dryRun) {
10399
11537
  }
10400
11538
  return migrateOrphanFederationDb({ dryRun });
10401
11539
  }
10402
- function checkpointAndVerify(oldPath, newPath, label) {
10403
- let hasSqlite3 = true;
11540
+ /**
11541
+ * Flush the old DB's WAL into its main file (before the byte copy, so the copy
11542
+ * holds every committed row) and set the emptied sidecars aside. A non-empty
11543
+ * WAL is never moved away without a successful checkpoint.
11544
+ */
11545
+ function checkpointWal(oldPath, label) {
11546
+ let checkpointed = false;
10404
11547
  try {
10405
- execFileSync("which", ["sqlite3"], { stdio: "pipe" });
10406
- } catch {
10407
- hasSqlite3 = false;
10408
- }
10409
- if (hasSqlite3) try {
10410
- execFileSync("sqlite3", [oldPath, "PRAGMA wal_checkpoint(TRUNCATE);"], { stdio: "pipe" });
11548
+ const db = new BetterSqlite3(oldPath, { timeout: 500 });
11549
+ try {
11550
+ const [row] = db.pragma("wal_checkpoint(TRUNCATE)");
11551
+ checkpointed = row?.busy === 0;
11552
+ } finally {
11553
+ db.close();
11554
+ }
10411
11555
  } catch {}
10412
11556
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
10413
11557
  for (const suffix of ["-wal", "-shm"]) {
10414
11558
  const sidecar = `${oldPath}${suffix}`;
10415
- if (existsSync(sidecar)) try {
11559
+ if (!existsSync(sidecar)) continue;
11560
+ if (!checkpointed && statSync(sidecar).size > 0) throw new PaiFileMigrationError(`${label}: could not checkpoint ${sidecar} (non-empty WAL, committed rows may live only there) — left in place`);
11561
+ try {
10416
11562
  renameSync(sidecar, `${sidecar}.migrated-${stamp}`);
10417
11563
  } catch {}
10418
11564
  }
10419
- if (hasSqlite3) try {
10420
- const check = execFileSync("sqlite3", [newPath, "PRAGMA integrity_check;"], { stdio: "pipe" }).toString().trim();
10421
- if (check !== "ok") throw new PaiFileMigrationError(`${newPath}: PRAGMA integrity_check reported "${check}" — investigate before trusting this copy`);
11565
+ }
11566
+ /** PRAGMA integrity_check on the migrated copy, through the node driver. */
11567
+ function verifyIntegrity(newPath) {
11568
+ let check;
11569
+ try {
11570
+ const db = new BetterSqlite3(newPath, { readonly: true });
11571
+ try {
11572
+ check = String(db.pragma("integrity_check", { simple: true }));
11573
+ } finally {
11574
+ db.close();
11575
+ }
10422
11576
  } catch (e) {
10423
- if (e instanceof PaiFileMigrationError) throw e;
11577
+ throw new PaiFileMigrationError(`${newPath}: integrity_check could not run (${String(e)})`);
10424
11578
  }
11579
+ if (check !== "ok") throw new PaiFileMigrationError(`${newPath}: PRAGMA integrity_check reported "${check}" — investigate before trusting this copy`);
10425
11580
  }
10426
11581
  /**
10427
11582
  * ~/.pai/registry.db — a third per-user location, alongside PAI_HOME and the
@@ -10448,8 +11603,9 @@ function migrateRegistryDbFile(dryRun) {
10448
11603
  execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
10449
11604
  execFileSync("sleep", ["2"]);
10450
11605
  }
11606
+ checkpointWal(oldPath, "registry database");
10451
11607
  const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
10452
- if (result.fromPath) checkpointAndVerify(oldPath, newPath, "registry database");
11608
+ if (result.fromPath) verifyIntegrity(newPath);
10453
11609
  return result;
10454
11610
  }
10455
11611
  /**
@@ -10477,8 +11633,9 @@ function migrateFederationDbFile(dryRun) {
10477
11633
  execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
10478
11634
  execFileSync("sleep", ["2"]);
10479
11635
  }
11636
+ checkpointWal(oldPath, "federation database");
10480
11637
  const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
10481
- if (result.fromPath) checkpointAndVerify(oldPath, newPath, "federation database");
11638
+ if (result.fromPath) verifyIntegrity(newPath);
10482
11639
  return result;
10483
11640
  }
10484
11641
 
@@ -10952,8 +12109,8 @@ async function escalate(task, headline, detail, state, now, opts) {
10952
12109
  state.alarmedAt[task.id] = now;
10953
12110
  let delivered = false;
10954
12111
  try {
10955
- const { routeNotification } = await import("../router-D6Ec6Fid.mjs");
10956
- const { loadConfig } = await import("../config-TaWmdTLx.mjs");
12112
+ const { routeNotification } = await import("../router-DwHJRgG9.mjs");
12113
+ const { loadConfig } = await import("../config-DtENJ6cL.mjs");
10957
12114
  delivered = (await routeNotification({
10958
12115
  event: "error",
10959
12116
  title: `PAI: ${headline}`,
@@ -11059,7 +12216,7 @@ const GROSS_OVERRUN_FACTOR = 5;
11059
12216
  */
11060
12217
  function detectWebhook(bin = "aibroker") {
11061
12218
  try {
11062
- const out = execFileSync(bin, ["todoist", "status"], {
12219
+ return webhookFromStatus(execFileSync(bin, ["todoist", "status"], {
11063
12220
  timeout: 1e4,
11064
12221
  encoding: "utf-8",
11065
12222
  stdio: [
@@ -11067,13 +12224,21 @@ function detectWebhook(bin = "aibroker") {
11067
12224
  "pipe",
11068
12225
  "pipe"
11069
12226
  ]
11070
- });
11071
- return /authoris|authoriz|token on file|grant/i.test(out) && !/no (token|grant)/i.test(out);
12227
+ }));
11072
12228
  } catch {
11073
12229
  return false;
11074
12230
  }
11075
12231
  }
11076
12232
  /**
12233
+ * Read `aibroker todoist status` output. Only an explicit positive line counts:
12234
+ * "Authorised <timestamp>" and no "FAILING" verdict. Negative texts ("No Todoist
12235
+ * authorisation on file", "Not authorized", "grant: missing") and a token that
12236
+ * is on file but rejected both read as no webhook.
12237
+ */
12238
+ function webhookFromStatus(out) {
12239
+ return /^Authori[sz]ed\s+\S/m.test(out) && !/^FAILING\b/m.test(out);
12240
+ }
12241
+ /**
11077
12242
  * Discard restore entries whose run is no longer in flight.
11078
12243
  *
11079
12244
  * `triggeredRestore` belongs to one run: it records the occurrence that run
@@ -11118,83 +12283,93 @@ async function tick(opts) {
11118
12283
  const ordered = dispatchOrder(tasks);
11119
12284
  if (!opts.dryRun) dropOrphanedRestores(tasks, state);
11120
12285
  const webhookActive = opts.webhookActive ?? detectWebhook();
11121
- for (const task of ordered) {
11122
- const prevDue = state.lastSeenDue[task.id];
11123
- const d = decide(task, {
11124
- now,
11125
- state,
11126
- history: state.history,
11127
- lastSeenDue: state.lastSeenDue,
11128
- claimSeenAt: state.claimSeenAt,
11129
- webhookActive
11130
- });
11131
- let note = "";
11132
- if (!opts.dryRun && task.due) state.lastSeenDue[task.id] = task.due;
11133
- if (!opts.dryRun) if (isRunning(task)) {
11134
- if (state.claimSeenAt[task.id] === void 0 && task.recurrence && prevDue !== void 0) {
11135
- if (wasTicked(task, prevDue)) state.triggeredRestore[task.id] = restoreDueString(task.recurrence, prevDue);
11136
- }
11137
- state.claimSeenAt[task.id] ??= now;
11138
- } else delete state.claimSeenAt[task.id];
11139
- switch (d.action) {
11140
- case "wait": continue;
11141
- case "triggered": {
11142
- const r = await handleDispatch(task, 0, opts, state, now);
11143
- note = `ticked off by hand — ${r.note}`;
11144
- if (!opts.dryRun) {
11145
- if (!r.parked) report.dispatched++;
11146
- if (r.alarm) report.stuck++;
11147
- note += await restoreSchedule(task, d.restoreTo, opts, state);
11148
- if (d.restoreTo) state.triggeredRestore[task.id] = d.restoreTo;
11149
- }
11150
- break;
11151
- }
11152
- case "skip":
11153
- note = d.reason;
11154
- break;
11155
- case "running":
11156
- note = `${d.elapsedMinutes}m elapsed`;
11157
- break;
11158
- case "dispatch": {
11159
- const r = await handleDispatch(task, d.overdueMinutes, opts, state, now);
11160
- note = r.note;
11161
- if (!opts.dryRun) {
11162
- if (!r.parked) report.dispatched++;
11163
- if (r.alarm) report.stuck++;
11164
- }
11165
- break;
11166
- }
11167
- case "complete":
11168
- note = await handleComplete(task, d.durationMinutes, opts, state, now);
11169
- if (!opts.dryRun) report.completed++;
11170
- break;
11171
- case "abandoned":
11172
- note = `claimed ${d.elapsedMinutes}m ago, past the ${d.thresholdMinutes}m limit — releasing the claim`;
11173
- if (!opts.dryRun) {
11174
- await releaseClaim(task, opts.provider, state);
11175
- report.stuck++;
11176
- }
11177
- break;
11178
- case "probe":
11179
- case "orphaned": {
11180
- const result = await handleProbe(task, d.action === "probe" ? d.elapsedMinutes : null, opts, state, now);
11181
- note = result.note;
11182
- if (!opts.dryRun) {
11183
- report.probed++;
11184
- if (result.stuck) report.stuck++;
11185
- if (result.dispatched) report.dispatched++;
12286
+ try {
12287
+ for (const task of ordered) {
12288
+ const prevDue = state.lastSeenDue[task.id];
12289
+ const d = decide(task, {
12290
+ now,
12291
+ state,
12292
+ history: state.history,
12293
+ lastSeenDue: state.lastSeenDue,
12294
+ claimSeenAt: state.claimSeenAt,
12295
+ webhookActive
12296
+ });
12297
+ let note = "";
12298
+ try {
12299
+ if (!opts.dryRun && task.due) state.lastSeenDue[task.id] = task.due;
12300
+ if (!opts.dryRun) if (isRunning(task)) {
12301
+ if (state.claimSeenAt[task.id] === void 0 && task.recurrence && prevDue !== void 0) {
12302
+ if (wasTicked(task, prevDue)) state.triggeredRestore[task.id] = restoreDueString(task.recurrence, prevDue);
12303
+ }
12304
+ state.claimSeenAt[task.id] ??= now;
12305
+ } else delete state.claimSeenAt[task.id];
12306
+ switch (d.action) {
12307
+ case "wait": continue;
12308
+ case "triggered": {
12309
+ const r = await handleDispatch(task, 0, opts, state, now);
12310
+ note = `ticked off by hand — ${r.note}`;
12311
+ if (!opts.dryRun) {
12312
+ if (!r.parked) report.dispatched++;
12313
+ if (r.alarm) report.stuck++;
12314
+ note += await restoreSchedule(task, d.restoreTo, opts, state);
12315
+ if (d.restoreTo) state.triggeredRestore[task.id] = d.restoreTo;
12316
+ }
12317
+ break;
12318
+ }
12319
+ case "skip":
12320
+ note = d.reason;
12321
+ break;
12322
+ case "running":
12323
+ note = `${d.elapsedMinutes}m elapsed`;
12324
+ break;
12325
+ case "dispatch": {
12326
+ const r = await handleDispatch(task, d.overdueMinutes, opts, state, now);
12327
+ note = r.note;
12328
+ if (!opts.dryRun) {
12329
+ if (!r.parked) report.dispatched++;
12330
+ if (r.alarm) report.stuck++;
12331
+ }
12332
+ break;
12333
+ }
12334
+ case "complete":
12335
+ note = await handleComplete(task, d.durationMinutes, opts, state, now);
12336
+ if (!opts.dryRun) report.completed++;
12337
+ break;
12338
+ case "abandoned":
12339
+ note = `claimed ${d.elapsedMinutes}m ago, past the ${d.thresholdMinutes}m limit — releasing the claim`;
12340
+ if (!opts.dryRun) {
12341
+ await releaseClaim(task, opts.provider, state);
12342
+ report.stuck++;
12343
+ }
12344
+ break;
12345
+ case "probe":
12346
+ case "orphaned": {
12347
+ const result = await handleProbe(task, d.action === "probe" ? d.elapsedMinutes : null, opts, state, now);
12348
+ note = result.note;
12349
+ if (!opts.dryRun) {
12350
+ report.probed++;
12351
+ if (result.stuck) report.stuck++;
12352
+ if (result.dispatched) report.dispatched++;
12353
+ }
12354
+ break;
12355
+ }
11186
12356
  }
11187
- break;
12357
+ report.decisions.push({
12358
+ decision: d,
12359
+ note
12360
+ });
12361
+ } catch (err) {
12362
+ report.decisions.push({
12363
+ decision: d,
12364
+ note: `error: ${err instanceof Error ? err.message : String(err)}`
12365
+ });
11188
12366
  }
11189
12367
  }
11190
- report.decisions.push({
11191
- decision: d,
11192
- note
11193
- });
11194
- }
11195
- if (!opts.dryRun) {
11196
- reconcileWithDisk(state, stateFile);
11197
- saveState(state, stateFile);
12368
+ } finally {
12369
+ if (!opts.dryRun) {
12370
+ reconcileWithDisk(state, stateFile);
12371
+ saveState(state, stateFile);
12372
+ }
11198
12373
  }
11199
12374
  return report;
11200
12375
  }
@@ -11611,7 +12786,7 @@ function cleanBrokenSymlinks(dir) {
11611
12786
  const full = join(dir, entry);
11612
12787
  try {
11613
12788
  if (lstatSync(full).isSymbolicLink()) {
11614
- if (!existsSync(readlinkSync(full))) {
12789
+ if (!existsSync(resolve(dir, readlinkSync(full)))) {
11615
12790
  unlinkSync(full);
11616
12791
  removed++;
11617
12792
  }
@@ -12667,10 +13842,22 @@ function migrateWorkerLogs(oldPath, newPath, opts = {}) {
12667
13842
  const active = activeSpawnedWorkerCount(oldPath);
12668
13843
  if (active > 0) throw new WorkerLogsMigrationError(`${active} worker(s) other than the interactive session are RUNNING and writing into ${oldPath} — wait for them to finish (see \`pai worker ps\`), then retry`);
12669
13844
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
12670
- cpSync(oldPath, newPath, { recursive: true });
13845
+ const partial = `${newPath}.partial`;
13846
+ rmSync(partial, {
13847
+ recursive: true,
13848
+ force: true
13849
+ });
13850
+ cpSync(oldPath, partial, { recursive: true });
12671
13851
  const fromCount = totalFileCount(oldPath);
12672
- const toCount = totalFileCount(newPath);
12673
- if (fromCount !== toCount) throw new WorkerLogsMigrationError(`${newPath}: copy produced ${toCount} files, expected ${fromCount} from ${oldPath} — aborting, old directory left in place`);
13852
+ const toCount = totalFileCount(partial);
13853
+ if (fromCount !== toCount) {
13854
+ rmSync(partial, {
13855
+ recursive: true,
13856
+ force: true
13857
+ });
13858
+ throw new WorkerLogsMigrationError(`${newPath}: copy produced ${toCount} files, expected ${fromCount} from ${oldPath} — aborting, old directory left in place`);
13859
+ }
13860
+ renameSync(partial, newPath);
12674
13861
  renameSync(oldPath, `${oldPath}.migrated-${stamp}`);
12675
13862
  return {
12676
13863
  fromPath: oldPath,
@@ -13847,7 +15034,7 @@ async function migrateToPostgres(opts = {}) {
13847
15034
  const dump = pgDump(container, pgConfig, dumpPath);
13848
15035
  if (!dump.ok) return refused(dump.reason, dryRun);
13849
15036
  }
13850
- const { PostgresBackend } = await import("../postgres-DgqWyqhN.mjs");
15037
+ const { PostgresBackend } = await import("../postgres-BdBNkL_l.mjs");
13851
15038
  await PostgresBackend.ensureDatabase(pgConfig);
13852
15039
  const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
13853
15040
  const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
@@ -14579,6 +15766,35 @@ const homeCommand = {
14579
15766
  * `firstlast@company.com`. Anything that should count gets listed.
14580
15767
  */
14581
15768
  /**
15769
+ * Split a recipient field into its individual entries. Commas and semicolons
15770
+ * separate entries unless inside a quoted display name or an angle-bracket
15771
+ * address. Empty entries are dropped.
15772
+ */
15773
+ function splitAddresses(raw) {
15774
+ const out = [];
15775
+ let cur = "";
15776
+ let quoted = false;
15777
+ let angled = false;
15778
+ for (let i = 0; i < raw.length; i++) {
15779
+ const c = raw[i];
15780
+ if (quoted && c === "\\" && i + 1 < raw.length) {
15781
+ cur += c + raw[++i];
15782
+ continue;
15783
+ }
15784
+ if (c === "\"") quoted = !quoted;
15785
+ else if (!quoted && c === "<") angled = true;
15786
+ else if (!quoted && c === ">") angled = false;
15787
+ if (!quoted && !angled && (c === "," || c === ";")) {
15788
+ if (cur.trim()) out.push(cur.trim());
15789
+ cur = "";
15790
+ continue;
15791
+ }
15792
+ cur += c;
15793
+ }
15794
+ if (cur.trim()) out.push(cur.trim());
15795
+ return out;
15796
+ }
15797
+ /**
14582
15798
  * Reduce an address to a comparable form: trimmed, unwrapped, lowercased.
14583
15799
  *
14584
15800
  * Handles `Display Name <addr@example.com>` because recipient fields carry that
@@ -14588,8 +15804,9 @@ const homeCommand = {
14588
15804
  */
14589
15805
  function normalizeEmail(raw) {
14590
15806
  if (!raw) return null;
14591
- let value = raw.trim();
14592
- if (!value) return null;
15807
+ const parts = splitAddresses(raw);
15808
+ if (parts.length !== 1) return null;
15809
+ let value = parts[0];
14593
15810
  const angled = value.match(/<([^>]*)>/);
14594
15811
  if (angled) value = angled[1].trim();
14595
15812
  value = value.toLowerCase();
@@ -15204,7 +16421,7 @@ const out = (s) => process.stdout.write(s + "\n");
15204
16421
  async function repoOf(project) {
15205
16422
  if (typeof project === "string" && project) {
15206
16423
  if (existsSync(project)) return resolve(project);
15207
- const { getProject } = await import("../helpers-DPJEHPq6.mjs");
16424
+ const { getProject } = await import("../helpers-CMGjy_0N.mjs");
15208
16425
  const row = await getProject(project);
15209
16426
  if (!row) throw new CliError(`no project ${project}`, "pai projects list");
15210
16427
  return row.root_path;
@@ -15572,7 +16789,8 @@ function mcpInstallCmd() {
15572
16789
  for (const spec of SERVERS) {
15573
16790
  const bin = distEntry(spec.entry);
15574
16791
  if (spec.name in servers) {
15575
- console.log(warn$1(`${spec.label} is already registered in ~/.claude.json as "${spec.name}".`));
16792
+ const same = mcpEntryRunsScript(servers[spec.name], bin);
16793
+ console.log(warn$1(`${spec.label} is already registered in ~/.claude.json as "${spec.name}"${same ? "" : " (different script)"}.`));
15576
16794
  console.log(dim$1(` Entry: ${JSON.stringify(servers[spec.name])}`));
15577
16795
  continue;
15578
16796
  }
@@ -16026,7 +17244,7 @@ async function cmdPromote(opts) {
16026
17244
  console.error(dim$1(` Pick a durable location for the new project.`));
16027
17245
  process.exit(1);
16028
17246
  }
16029
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
17247
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
16030
17248
  const registryBackend = await getRegistryBackend();
16031
17249
  const encodedDir = encodeDir(targetPath);
16032
17250
  if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
@@ -17270,7 +18488,7 @@ async function cmdUnregister(slug, opts = {}) {
17270
18488
  * deprecated alias that forwards here with a stderr notice.
17271
18489
  */
17272
18490
  async function projectListCmd(opts) {
17273
- const { cmdMain } = await import("../main-resolver-DVPpSWVy.mjs");
18491
+ const { cmdMain } = await import("../main-resolver-3Ns1HPmS.mjs");
17274
18492
  await cmdMain(void 0, void 0, opts);
17275
18493
  return 0;
17276
18494
  }
@@ -18693,7 +19911,7 @@ async function registryScanCmd(opts) {
18693
19911
  return 0;
18694
19912
  }
18695
19913
  if (opts.filterReport) {
18696
- const { reportUnregistrable } = await import("../registrable-D9aNZOTh.mjs");
19914
+ const { reportUnregistrable } = await import("../registrable-BzH4TObg.mjs");
18697
19915
  const r = await reportUnregistrable(await getRegistryBackend());
18698
19916
  console.log(`Registry rows: ${r.total}`);
18699
19917
  console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
@@ -18899,7 +20117,7 @@ const VALID_OUTCOMES = new Set([
18899
20117
  */
18900
20118
  function run$1(bin, args, stdin, killAfterMs) {
18901
20119
  return new Promise((resolve, reject) => {
18902
- execFile(bin, args, {
20120
+ const child = execFile(bin, args, {
18903
20121
  timeout: killAfterMs,
18904
20122
  maxBuffer: 1024 * 1024
18905
20123
  }, (error, stdout, stderr) => {
@@ -18908,15 +20126,26 @@ function run$1(bin, args, stdin, killAfterMs) {
18908
20126
  return;
18909
20127
  }
18910
20128
  resolve(stdout);
18911
- }).stdin?.end(stdin);
20129
+ });
20130
+ child.stdin?.on("error", (e) => reject(e));
20131
+ child.stdin?.end(stdin);
18912
20132
  });
18913
20133
  }
20134
+ /** A leading dash would be parsed by the CLI as an option, not a project. */
20135
+ function validProject(project) {
20136
+ return project.length > 0 && !project.startsWith("-");
20137
+ }
18914
20138
  var AiBrokerTransport = class {
18915
20139
  constructor(bin = "aibroker", timeoutSecs = DEFAULT_DISPATCH_TIMEOUT_SECS) {
18916
20140
  this.bin = bin;
18917
20141
  this.timeoutSecs = timeoutSecs;
18918
20142
  }
18919
20143
  async dispatch(project, message, opts) {
20144
+ if (!validProject(project)) return {
20145
+ outcome: "unlaunchable",
20146
+ session: project,
20147
+ reason: `invalid project name: ${project}`
20148
+ };
18920
20149
  const args = [
18921
20150
  "dispatch",
18922
20151
  project,
@@ -18960,6 +20189,10 @@ var AiBrokerProber = class {
18960
20189
  this.timeoutSecs = timeoutSecs;
18961
20190
  }
18962
20191
  async ask(project, question) {
20192
+ if (!validProject(project)) return {
20193
+ state: "silent",
20194
+ reason: `invalid project name: ${project}`
20195
+ };
18963
20196
  let stdout;
18964
20197
  try {
18965
20198
  stdout = await run$1(this.bin, [
@@ -19083,33 +20316,36 @@ function cliPath() {
19083
20316
  }
19084
20317
  throw new Error("cannot locate the PAI CLI entrypoint (dist/cli/index.mjs) — refusing to install a scheduler that would silently do nothing");
19085
20318
  }
20319
+ function xmlEscape(value) {
20320
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
20321
+ }
19086
20322
  function generateSchedulePlist(intervalSecs, cli) {
19087
20323
  return `<?xml version="1.0" encoding="UTF-8"?>
19088
20324
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19089
20325
  <plist version="1.0">
19090
20326
  <dict>
19091
20327
  <key>Label</key>
19092
- <string>${SCHEDULE_LABEL}</string>
20328
+ <string>${xmlEscape(SCHEDULE_LABEL)}</string>
19093
20329
 
19094
20330
  <key>ProgramArguments</key>
19095
20331
  <array>
19096
- <string>/usr/local/bin/node</string>
19097
- <string>${cli}</string>
20332
+ <string>${xmlEscape(process.execPath)}</string>
20333
+ <string>${xmlEscape(cli)}</string>
19098
20334
  <string>task</string>
19099
20335
  <string>poll</string>
19100
20336
  </array>
19101
20337
 
19102
20338
  <key>StartInterval</key>
19103
- <integer>${intervalSecs}</integer>
20339
+ <integer>${Math.trunc(intervalSecs)}</integer>
19104
20340
 
19105
20341
  <key>RunAtLoad</key>
19106
20342
  <true/>
19107
20343
 
19108
20344
  <key>StandardOutPath</key>
19109
- <string>${SCHEDULE_LOG}</string>
20345
+ <string>${xmlEscape(SCHEDULE_LOG)}</string>
19110
20346
 
19111
20347
  <key>StandardErrorPath</key>
19112
- <string>${SCHEDULE_LOG}</string>
20348
+ <string>${xmlEscape(SCHEDULE_LOG)}</string>
19113
20349
 
19114
20350
  <key>EnvironmentVariables</key>
19115
20351
  <dict>
@@ -20517,8 +21753,8 @@ function embedProgressLine(done, backlog) {
20517
21753
  async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSize = DEFAULT_PAGE_SIZE, maxMillis, run, shouldStop) {
20518
21754
  const label = projectSlug ? `project ${projectSlug}` : "all projects";
20519
21755
  console.log(dim$1(`Generating embeddings for ${label} (this may take a while on first run)...`));
20520
- const { serializeEmbedding } = await import("../embeddings-OF9Pt4by.mjs");
20521
- const { gateBackend } = await import("../embedding-gate-Z7CJSOJk.mjs");
21756
+ const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
21757
+ const { gateBackend } = await import("../embedding-gate-Cnk4YCyX.mjs");
20522
21758
  const gate = await gateBackend(backend, { record: true });
20523
21759
  if (!gate.ok) {
20524
21760
  console.error(err(gate.reason));
@@ -20847,7 +22083,7 @@ async function memorySearchCmd(query, opts) {
20847
22083
  let results;
20848
22084
  if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
20849
22085
  else {
20850
- const { embedQuery } = await import("../embedding-gate-Z7CJSOJk.mjs");
22086
+ const { embedQuery } = await import("../embedding-gate-Cnk4YCyX.mjs");
20851
22087
  console.log(dim$1("Generating query embedding..."));
20852
22088
  const q = await embedQuery(backend, query);
20853
22089
  const queryEmbedding = q.vec;
@@ -20897,7 +22133,7 @@ async function memorySearchCmd(query, opts) {
20897
22133
  return 0;
20898
22134
  }
20899
22135
  if (opts.rerank !== false) {
20900
- const { rerankResults } = await import("../reranker-B4r4vykM.mjs");
22136
+ const { rerankResults } = await import("../reranker-8g6xMwP7.mjs");
20901
22137
  console.log(dim$1("Reranking with cross-encoder..."));
20902
22138
  results = await rerankResults(query, results, { topK: maxResults });
20903
22139
  }
@@ -20927,9 +22163,9 @@ async function memorySearchCmd(query, opts) {
20927
22163
  //#region src/cli/commands/memory/sources-cmd.ts
20928
22164
  /** `pai memory sources` — what the indexer has taken in. */
20929
22165
  async function memorySourcesCmd(opts) {
20930
- const { createStorageBackend } = await import("../factory-DAGG9_n8.mjs");
20931
- const { loadConfig } = await import("../config-TaWmdTLx.mjs");
20932
- const { cmdMemorySources } = await import("../sources-CjhUKKtO.mjs");
22166
+ const { createStorageBackend } = await import("../factory-Bsib-qDG.mjs");
22167
+ const { loadConfig } = await import("../config-DtENJ6cL.mjs");
22168
+ const { cmdMemorySources } = await import("../sources-ESUkZaWv.mjs");
20933
22169
  let backend;
20934
22170
  try {
20935
22171
  backend = await createStorageBackend(loadConfig());
@@ -21688,7 +22924,7 @@ async function zettelExploreCmd(note, opts) {
21688
22924
  const depth = opts.depth ?? 3;
21689
22925
  const direction = opts.direction ?? "both";
21690
22926
  const mode = opts.mode ?? "all";
21691
- const { zettelExplore } = await import("../zettelkasten-nigq7fqs.mjs");
22927
+ const { zettelExplore } = await import("../zettelkasten-B_rRRRvD.mjs");
21692
22928
  const result = await zettelExplore(await getFedBackend(), {
21693
22929
  startNote: note,
21694
22930
  depth,
@@ -21744,7 +22980,7 @@ async function zettelHealthCmd(opts) {
21744
22980
  const projectPath = opts.project;
21745
22981
  const recentDays = opts.days ?? 30;
21746
22982
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
21747
- const { zettelHealth } = await import("../zettelkasten-nigq7fqs.mjs");
22983
+ const { zettelHealth } = await import("../zettelkasten-B_rRRRvD.mjs");
21748
22984
  const result = await zettelHealth(await getFedBackend(), {
21749
22985
  scope,
21750
22986
  projectPath,
@@ -21802,7 +23038,7 @@ async function zettelSurpriseCmd(note, opts) {
21802
23038
  const limit = opts.limit ?? 10;
21803
23039
  const minSimilarity = opts.minSimilarity ?? .3;
21804
23040
  const minGraphDistance = opts.minDistance ?? 3;
21805
- const { zettelSurprise } = await import("../zettelkasten-nigq7fqs.mjs");
23041
+ const { zettelSurprise } = await import("../zettelkasten-B_rRRRvD.mjs");
21806
23042
  const backend = await getFedBackend();
21807
23043
  console.log();
21808
23044
  console.log(header(" PAI Zettel Surprise"));
@@ -21844,7 +23080,7 @@ async function zettelConverseCmd(question, opts) {
21844
23080
  const vaultProjectId = Number(opts.vaultProjectId);
21845
23081
  const depth = opts.depth ?? 2;
21846
23082
  const limit = opts.limit ?? 15;
21847
- const { zettelConverse } = await import("../zettelkasten-nigq7fqs.mjs");
23083
+ const { zettelConverse } = await import("../zettelkasten-B_rRRRvD.mjs");
21848
23084
  const backend = await getFedBackend();
21849
23085
  console.log();
21850
23086
  console.log(header(" PAI Zettel Converse"));
@@ -21894,7 +23130,7 @@ async function zettelThemesCmd(opts) {
21894
23130
  const minClusterSize = opts.minSize ?? 3;
21895
23131
  const maxThemes = opts.maxThemes ?? 10;
21896
23132
  const similarityThreshold = opts.threshold ?? .65;
21897
- const { zettelThemes } = await import("../zettelkasten-nigq7fqs.mjs");
23133
+ const { zettelThemes } = await import("../zettelkasten-B_rRRRvD.mjs");
21898
23134
  const backend = await getFedBackend();
21899
23135
  console.log();
21900
23136
  console.log(header(" PAI Zettel Themes"));
@@ -21942,7 +23178,7 @@ async function zettelSuggestCmd(note, opts) {
21942
23178
  const vaultProjectId = Number(opts.vaultProjectId);
21943
23179
  const limit = opts.limit ?? 5;
21944
23180
  const excludeLinked = opts.excludeLinked !== true;
21945
- const { zettelSuggest } = await import("../zettelkasten-nigq7fqs.mjs");
23181
+ const { zettelSuggest } = await import("../zettelkasten-B_rRRRvD.mjs");
21946
23182
  const backend = await getFedBackend();
21947
23183
  console.log();
21948
23184
  console.log(header(" PAI Zettel Suggest"));
@@ -22234,7 +23470,7 @@ function ipcCall$1(method, params) {
22234
23470
  fn();
22235
23471
  }
22236
23472
  };
22237
- const client = createConnection(paiSocketPath(), () => {
23473
+ const client = createConnection$1(paiSocketPath(), () => {
22238
23474
  client.write(JSON.stringify({
22239
23475
  id: 1,
22240
23476
  method,
@@ -22624,7 +23860,7 @@ function ipcCall(method, params) {
22624
23860
  fn();
22625
23861
  }
22626
23862
  };
22627
- const client = createConnection(paiSocketPath(), () => {
23863
+ const client = createConnection$1(paiSocketPath(), () => {
22628
23864
  client.write(JSON.stringify({
22629
23865
  id: 1,
22630
23866
  method,
@@ -24256,7 +25492,14 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
24256
25492
  * one of the two in whichever catalogue order Claude Code loads them.
24257
25493
  */
24258
25494
  /** Recursively collect every file under `dir` whose basename satisfies `matches`. */
24259
- function findFilesRecursive(dir, matches) {
25495
+ function findFilesRecursive(dir, matches, seen = /* @__PURE__ */ new Set()) {
25496
+ try {
25497
+ const real = realpathSync(dir);
25498
+ if (seen.has(real)) return [];
25499
+ seen.add(real);
25500
+ } catch {
25501
+ return [];
25502
+ }
24260
25503
  let entries;
24261
25504
  try {
24262
25505
  entries = readdirSync(dir, { withFileTypes: true });
@@ -24266,8 +25509,17 @@ function findFilesRecursive(dir, matches) {
24266
25509
  const found = [];
24267
25510
  for (const entry of entries) {
24268
25511
  const full = join(dir, entry.name);
24269
- if (entry.isDirectory()) found.push(...findFilesRecursive(full, matches));
24270
- else if (entry.isFile() && matches(entry.name)) found.push(full);
25512
+ let isDir = entry.isDirectory();
25513
+ let isFile = entry.isFile();
25514
+ if (entry.isSymbolicLink()) try {
25515
+ const st = statSync(full);
25516
+ isDir = st.isDirectory();
25517
+ isFile = st.isFile();
25518
+ } catch {
25519
+ continue;
25520
+ }
25521
+ if (isDir) found.push(...findFilesRecursive(full, matches, seen));
25522
+ else if (isFile && matches(entry.name)) found.push(full);
24271
25523
  }
24272
25524
  return found;
24273
25525
  }
@@ -24750,11 +26002,17 @@ function newestMcpConfig(logDir) {
24750
26002
  mtime: statSync(path).mtimeMs
24751
26003
  })).sort((a, b) => b.mtime - a.mtime)[0].path;
24752
26004
  }
26005
+ /** Child env: not a worker, and no API key (same as run-env / daemon-llm). */
26006
+ function ladderChildEnv(base) {
26007
+ const env = { ...base };
26008
+ delete env.PAI_WORKER;
26009
+ delete env.ANTHROPIC_API_KEY;
26010
+ return env;
26011
+ }
24753
26012
  /** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
24754
26013
  function runRung(rung, cwd) {
24755
26014
  return new Promise((resolvePromise) => {
24756
- const env = { ...process.env };
24757
- delete env.PAI_WORKER;
26015
+ const env = ladderChildEnv(process.env);
24758
26016
  const proc = spawn("claude", rung.args, {
24759
26017
  cwd,
24760
26018
  env,