@tekmidian/pai 1.0.0 → 1.0.2

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 (218) 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-B4azPk4l.mjs} +3 -3
  7. package/dist/auto-route-BjoMHKyI.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-D-yvET_y.mjs} +5 -5
  12. package/dist/cli/index.mjs +1621 -357
  13. package/dist/cockpit-actions-BWunx3rY.mjs +20 -0
  14. package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-C8GlTzts.mjs} +84 -42
  15. package/dist/cockpit-memory-vTlFWV7F.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-BcKFHeG1.mjs} +37 -22
  21. package/dist/daemon/index.mjs +43 -42
  22. package/dist/{daemon-CorGh6Ub.mjs → daemon-ColVHwGZ.mjs} +103 -55
  23. package/dist/daemon-N7WhYi6I.mjs +45 -0
  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 +214 -116
  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-_a6UNhXJ.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-BtB3Se6f.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-DurzIbw8.mjs} +7 -6
  83. package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-BQytYa60.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-BM27GBfC.mjs} +31 -30
  87. package/dist/{pool-CZ5MZaXg.mjs → pool-BRvoOkv6.mjs} +3 -3
  88. package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-B_YR8_Gl.mjs} +5 -5
  89. package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DkZTAlSW.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-CfpApz5o.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-C7b6givx.mjs +9 -0
  97. package/dist/{registrable-LrRIVYQc.mjs → registrable-Uzgmfyhz.mjs} +4 -4
  98. package/dist/render-Cc2j5QG9.mjs +13 -0
  99. package/dist/{render-BDbb4CF-.mjs → render-CwIrgxLi.mjs} +8 -8
  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-BMeJuHOb.mjs → root-BWy0c2Lq.mjs} +11 -11
  103. package/dist/root-wC_6Fx0B.mjs +36 -0
  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-6Q9kCPem.mjs} +35 -33
  110. package/dist/{scan-B7I0gPe-.mjs → scan-ZQWm-_se.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-Bok2PqRS.mjs +35 -0
  116. package/dist/{session-CNXF6DLP.mjs → session-DVUExUnX.mjs} +55 -31
  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-DtWj5Vti.mjs → viewer-DcpJwGKE.mjs} +15 -15
  128. package/dist/viewer-MWBVJP3u.mjs +22 -0
  129. package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
  130. package/dist/{worker-config-BLqa1yiD.mjs → worker-config-BDYNxbYz.mjs} +294 -92
  131. package/dist/worker-config-gmAq_-rI.mjs +41 -0
  132. package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
  133. package/dist/{worktree-zmRf556h.mjs → worktree-CrnOqTBW.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.svg +3 -0
  149. package/docs/images/cockpit-01-system-tokens.jpg +0 -0
  150. package/docs/images/cockpit-02-totals-cache.jpg +0 -0
  151. package/docs/images/cockpit-03-local-servers-models-pool.jpg +0 -0
  152. package/docs/images/cockpit-04-workers-memory.jpg +0 -0
  153. package/docs/images/cockpit-05-performance.jpg +0 -0
  154. package/docs/images/cockpit-06-runs-speed-routing.jpg +0 -0
  155. package/docs/images/cockpit-07-settings-registry.jpg +0 -0
  156. package/docs/install/index.md +29 -0
  157. package/docs/three-tier-coding.md +1 -1
  158. package/gemini-extension.json +1 -1
  159. package/package.json +1 -5
  160. package/pai-plugin.json +1 -1
  161. package/plugins/context-preservation/plugin.json +1 -1
  162. package/plugins/core/plugin.json +1 -1
  163. package/plugins/core/skills/setup/SKILL.md +41 -9
  164. package/plugins/creative/plugin.json +1 -1
  165. package/plugins/observability/plugin.json +1 -1
  166. package/plugins/productivity/plugin.json +1 -1
  167. package/plugins/semantic-search/plugin.json +1 -1
  168. package/plugins/ui/plugin.json +1 -1
  169. package/plugins/zettelkasten/plugin.json +1 -1
  170. package/src/hooks/session-autosave.sh +8 -1
  171. package/src/hooks/session-stop.sh +8 -1
  172. package/src/hooks/ts/capture-all-events.ts +7 -12
  173. package/src/hooks/ts/lib/context-fill.ts +2 -1
  174. package/src/hooks/ts/lib/path-repair.ts +1 -1
  175. package/src/hooks/ts/lib/project-utils/notify.ts +1 -0
  176. package/src/hooks/ts/lib/project-utils/session-notes.ts +23 -6
  177. package/src/hooks/ts/lib/raw-capture.ts +36 -0
  178. package/src/hooks/ts/lib/session-summary.ts +129 -0
  179. package/src/hooks/ts/lib/todo-sync.ts +120 -0
  180. package/src/hooks/ts/lib/worker-git-guard.ts +1 -1
  181. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +6 -10
  182. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +5 -105
  183. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  184. package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -14
  185. package/src/hooks/ts/session-end/capture-session-summary.ts +20 -128
  186. package/src/hooks/ts/session-start/load-core-context.ts +3 -3
  187. package/src/hooks/ts/session-start/load-project-context.ts +18 -22
  188. package/src/hooks/ts/stop/stop-hook.ts +13 -5
  189. package/src/hooks/ztk-auto.sh +7 -2
  190. package/dist/aibroker-client-JjE74jo0.mjs +0 -3
  191. package/dist/auto-route-BRk8NEFv.mjs +0 -11
  192. package/dist/budget-DuhQ2zzG.mjs +0 -14
  193. package/dist/cockpit-actions-DcQ-ucU8.mjs +0 -19
  194. package/dist/cockpit-memory-KV45PPO2.mjs +0 -19
  195. package/dist/daemon-C7yt_ptC.mjs +0 -44
  196. package/dist/detect-DRUkmHnb.mjs +0 -10
  197. package/dist/factory-DAGG9_n8.mjs +0 -9
  198. package/dist/helpers-DPJEHPq6.mjs +0 -10
  199. package/dist/kill-tree-LV0Q7Mte.mjs +0 -10
  200. package/dist/operator-CfL0HiBK.mjs +0 -7
  201. package/dist/postgres-DgqWyqhN.mjs +0 -7
  202. package/dist/providers-CTu2s1z2.mjs +0 -15
  203. package/dist/registrable-D9aNZOTh.mjs +0 -9
  204. package/dist/render-BSfVlYHj.mjs +0 -13
  205. package/dist/root-xI9DqVIe.mjs +0 -35
  206. package/dist/route-table-HjvnLVEU.mjs +0 -14
  207. package/dist/session-BJHzmUg2.mjs +0 -34
  208. package/dist/session-scan-YORKE1NO.mjs +0 -9
  209. package/dist/viewer-B8709jDN.mjs +0 -22
  210. package/dist/worker-config-D8tRuvo7.mjs +0 -40
  211. package/docs/images/cockpit-limits-routing.jpg +0 -0
  212. package/docs/images/cockpit-models.jpg +0 -0
  213. package/docs/images/cockpit-usage.jpg +0 -0
  214. package/docs/images/cockpit-workers-system.jpg +0 -0
  215. package/scripts/build-docs.mjs +0 -41
  216. package/scripts/build-hooks.mjs +0 -311
  217. package/scripts/build-skill-stubs.mjs +0 -216
  218. 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-CrnOqTBW.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 { S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as writeClaudeJson, k as readClaudeJson, v as readPoolState, w as expandMcpNames } from "../pool-BRvoOkv6.mjs";
11
+ import { a as unregistrableReason, s as worktreeReason } from "../registrable-Uzgmfyhz.mjs";
12
+ import "../pai-marker-DurzIbw8.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-ZQWm-_se.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-DVUExUnX.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-BWy0c2Lq.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-6Q9kCPem.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-BcKFHeG1.mjs";
46
+ import "../endreason-C577xIQz.mjs";
47
+ import { N as endBuildWait, j as beginBuildWait } from "../render-CwIrgxLi.mjs";
48
+ import "../operator-CyuB3aVw.mjs";
49
+ import "../kill-tree-EWYaEk5v.mjs";
50
+ import { s as poolReloadLive } from "../pool-cli-B_YR8_Gl.mjs";
51
+ import "../chain-D-yvET_y.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-CfpApz5o.mjs";
53
+ import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-BQytYa60.mjs";
54
+ import "../viewer-DcpJwGKE.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-BDYNxbYz.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-C8GlTzts.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-_a6UNhXJ.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-gmAq_-rI.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-BtB3Se6f.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-BWunx3rY.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
  }
@@ -8432,8 +9036,8 @@ function cmdLogs(opts) {
8432
9036
  return 0;
8433
9037
  }
8434
9038
  async function daemonServeCmd() {
8435
- const { serve } = await import("../daemon-C7yt_ptC.mjs");
8436
- const { loadConfig: lc, ensureConfigDir } = await import("../config-TaWmdTLx.mjs");
9039
+ const { serve } = await import("../daemon-N7WhYi6I.mjs");
9040
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-DtENJ6cL.mjs");
8437
9041
  ensureConfigDir();
8438
9042
  await serve(lc());
8439
9043
  return 0;
@@ -8591,11 +9195,11 @@ async function stepDaemon(rl) {
8591
9195
  if (existsSync(plistPath)) {
8592
9196
  console.log(c.dim(` PAI daemon ${unit} already installed.`));
8593
9197
  line();
8594
- if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false)) {
9198
+ if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false, "daemon")) {
8595
9199
  console.log(c.dim(" Keeping existing daemon installation."));
8596
9200
  return false;
8597
9201
  }
8598
- } else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true)) {
9202
+ } else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true, "daemon")) {
8599
9203
  console.log(c.dim(" Skipping daemon install. Run manually: pai daemon install"));
8600
9204
  return false;
8601
9205
  }
@@ -8627,7 +9231,7 @@ async function stepMcp(rl) {
8627
9231
  return false;
8628
9232
  }
8629
9233
  } catch {}
8630
- if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true)) {
9234
+ if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true, "mcp")) {
8631
9235
  console.log(c.dim(" Skipping MCP registration. Run manually: pai mcp install"));
8632
9236
  return false;
8633
9237
  }
@@ -8660,7 +9264,7 @@ async function stepDirectories(rl) {
8660
9264
  for (const d of defaults) console.log(chalk.dim(` ${d}`));
8661
9265
  line();
8662
9266
  }
8663
- const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false);
9267
+ const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false, "registry-scan");
8664
9268
  if (runScan) {
8665
9269
  line();
8666
9270
  console.log(chalk.dim(" Registry scan will run after setup completes."));
@@ -8720,7 +9324,7 @@ async function stepInitialIndex(rl) {
8720
9324
  //#endregion
8721
9325
  //#region src/cli/commands/setup/steps/15-verify.ts
8722
9326
  /** 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) {
9327
+ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform, aibroker, failures = []) {
8724
9328
  section("Setup Complete");
8725
9329
  line();
8726
9330
  console.log(chalk.green(" PAI Knowledge OS is configured!"));
@@ -8741,6 +9345,16 @@ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiStee
8741
9345
  console.log(chalk.dim(" Settings: ") + chalk.cyan(settingsPatched ? "env vars, hooks, permissions, flags (patched)" : "(unchanged)"));
8742
9346
  console.log(chalk.dim(" Daemon: ") + chalk.cyan(daemonInstalled ? `${daemonServiceName(plat)} (installed)` : "(not installed by setup)"));
8743
9347
  console.log(chalk.dim(" MCP: ") + chalk.cyan(mcpRegistered ? "registered in ~/.claude.json" : "(unchanged)"));
9348
+ if (aibroker) {
9349
+ const label = {
9350
+ ok: "installed and healthy",
9351
+ installed: "installed",
9352
+ skipped: "skipped",
9353
+ failed: "FAILED — retry: npm install -g aibroker@^0.62.4 && aibroker setup && aibroker doctor"
9354
+ }[aibroker];
9355
+ console.log(chalk.dim(" AIBroker: ") + (aibroker === "failed" ? chalk.red(label) : chalk.cyan(label)));
9356
+ }
9357
+ for (const f of failures) console.log(chalk.red(` ${f}`));
8744
9358
  line();
8745
9359
  console.log(chalk.bold.yellow(" → RESTART Claude Code to activate all changes."));
8746
9360
  line();
@@ -9395,7 +10009,7 @@ async function stepTaskBus(rl) {
9395
10009
  line();
9396
10010
  line(" PAI works fully without this. Skip it unless you want cross-session tasks.");
9397
10011
  line();
9398
- if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false)) {
10012
+ if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false, "task-bus")) {
9399
10013
  line();
9400
10014
  console.log(c.ok("Skipping the task bus."));
9401
10015
  console.log(c.dim(" Add later by re-running `pai setup`."));
@@ -9482,7 +10096,7 @@ async function stepWorkers(rl) {
9482
10096
  line(" `pai worker run` is the runner; ps / follow / replay watch the runs;");
9483
10097
  line(" the Agent tool is denied and rewritten to it while workers are on.");
9484
10098
  line();
9485
- if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true)) {
10099
+ if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true, "workers")) {
9486
10100
  line();
9487
10101
  console.log(c.ok("Skipping workers."));
9488
10102
  console.log(c.dim(" Later: pai worker on (other providers: pai worker providers add <name> … — see docs/worker.md)"));
@@ -9501,6 +10115,209 @@ async function stepWorkers(rl) {
9501
10115
  return {};
9502
10116
  }
9503
10117
 
10118
+ //#endregion
10119
+ //#region src/cli/commands/setup/steps/18-aibroker.ts
10120
+ /** Step 18: AIBroker, PAI's messaging layer (session messages, worker reports, tab names, WhatsApp/Telegram). */
10121
+ /** Longest any one AIBroker command may run before it counts as failed. */
10122
+ const COMMAND_TIMEOUT_MS = 5 * 6e4;
10123
+ /** `aibroker setup` may ask questions: give it the terminal when there is one and --yes is off. */
10124
+ const defaultRunner = (cmd, args) => {
10125
+ const r = spawnSync(cmd, args, {
10126
+ encoding: "utf8",
10127
+ timeout: COMMAND_TIMEOUT_MS,
10128
+ ...cmd === "aibroker" && args[0] === "setup" && !setupOptions.yes && !!process.stdin.isTTY ? { stdio: "inherit" } : {}
10129
+ });
10130
+ return {
10131
+ status: r.error ? null : r.status,
10132
+ output: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? String(r.error.message) : ""}`.trim()
10133
+ };
10134
+ };
10135
+ /** The package the step installs; the range keeps a breaking major from arriving unannounced. */
10136
+ const AIBROKER_PACKAGE = "aibroker@^0.62.4";
10137
+ /** `aibroker` is not on PATH: spawn error (null) or the shell's "not found". */
10138
+ const isMissing = (r) => r.status === null || r.status === 127;
10139
+ /** The commands the step runs when AIBroker is missing or unhealthy, in order. */
10140
+ const AIBROKER_COMMANDS = [
10141
+ ["npm", [
10142
+ "install",
10143
+ "-g",
10144
+ AIBROKER_PACKAGE
10145
+ ]],
10146
+ ["aibroker", ["setup"]],
10147
+ ["aibroker", ["doctor"]]
10148
+ ];
10149
+ const show = ([cmd, args]) => [cmd, ...args].join(" ");
10150
+ function aibrokerPlan() {
10151
+ return AIBROKER_COMMANDS.map(show);
10152
+ }
10153
+ function stepAibroker(run = defaultRunner, opts = {
10154
+ skip: setupOptions.noAibroker,
10155
+ dryRun: setupOptions.dryRun
10156
+ }) {
10157
+ section("Step 18: AIBroker (Messaging)");
10158
+ line();
10159
+ line(" AIBroker is PAI's messaging layer: session-to-session messages, worker reports,");
10160
+ line(" terminal tab names, WhatsApp/Telegram/PAILot. PAI works without it, minus messaging.");
10161
+ line();
10162
+ if (opts.skip) {
10163
+ console.log(c.dim(" Skipped (--no-aibroker). Install later: npm install -g aibroker@^0.62.4 && aibroker setup"));
10164
+ return "skipped";
10165
+ }
10166
+ if (opts.dryRun) {
10167
+ line(" Would run (if AIBroker is missing or `aibroker doctor` fails):");
10168
+ for (const p of aibrokerPlan()) line(` ${p}`);
10169
+ return "skipped";
10170
+ }
10171
+ const present = run("aibroker", ["doctor"]);
10172
+ if (present.status === 0) {
10173
+ console.log(c.ok("AIBroker already installed and healthy."));
10174
+ return "ok";
10175
+ }
10176
+ const steps = isMissing(present) ? AIBROKER_COMMANDS : AIBROKER_COMMANDS.slice(1);
10177
+ for (const step of steps) {
10178
+ const r = run(step[0], step[1]);
10179
+ if (r.status !== 0) {
10180
+ console.log(c.warn(` \`${show(step)}\` failed (exit ${r.status ?? "n/a"}).`));
10181
+ if (r.output) line(` ${r.output.split("\n").join("\n ")}`);
10182
+ console.log(c.warn(` Retry with: ${aibrokerPlan().slice(AIBROKER_COMMANDS.indexOf(step)).join(" && ")}`));
10183
+ return "failed";
10184
+ }
10185
+ console.log(c.ok(`${show(step)}: ok`));
10186
+ }
10187
+ console.log(c.ok("AIBroker installed."));
10188
+ return "installed";
10189
+ }
10190
+
10191
+ //#endregion
10192
+ //#region src/cli/commands/setup/steps/19-router.ts
10193
+ /** Step 19: the model router and its cockpit (enabled, port, reach). Answers go to config.yaml as router.enabled / router.port / router.bind. */
10194
+ /** Tries to listen on `host` (loopback by default); when taken, asks lsof who holds it. */
10195
+ const defaultPortProbe = (port, host = "127.0.0.1") => new Promise((resolve) => {
10196
+ const srv = createServer();
10197
+ srv.once("error", () => {
10198
+ resolve({
10199
+ free: false,
10200
+ holder: (spawnSync("lsof", [
10201
+ "-nP",
10202
+ `-iTCP:${port}`,
10203
+ "-sTCP:LISTEN"
10204
+ ], { encoding: "utf8" }).stdout ?? "").split("\n")[1]?.trim().split(/\s+/).slice(0, 2).join(" pid ") || void 0
10205
+ });
10206
+ });
10207
+ srv.listen(port, host, () => srv.close(() => resolve({ free: true })));
10208
+ });
10209
+ function validPort(v) {
10210
+ const n = typeof v === "number" ? v : /^\d+$/.test(String(v).trim()) ? Number(v) : NaN;
10211
+ return Number.isInteger(n) && n >= 1024 && n <= 65535 ? n : null;
10212
+ }
10213
+ /** `loopback`, `tailscale` or a dotted IPv4 (never 0.0.0.0); returns the normalised value or null. */
10214
+ function validBind(v) {
10215
+ const b = v.trim().toLowerCase();
10216
+ return resolveBindAddress(b) === null && b !== "tailscale" ? null : b || "loopback";
10217
+ }
10218
+ async function stepRouter(rl, probe = defaultPortProbe, runningPid = routerRunningPid) {
10219
+ section("Step 19: Router and Cockpit (Optional)");
10220
+ line();
10221
+ line(" The router accounts model usage, schedules local models and serves the cockpit page.");
10222
+ line(" The daemon keeps it running when enabled.");
10223
+ line();
10224
+ const cfg = loadConfig().router;
10225
+ const raw = readConfigRaw().router ?? {};
10226
+ const flags = setupOptions.router ?? {};
10227
+ if (!(flags.enabled ?? await promptYesNo(rl, "Enable the router and cockpit?", typeof raw.enabled === "boolean" ? raw.enabled : true))) {
10228
+ console.log(c.ok("Router disabled. Enable later: pai config set router.enabled true"));
10229
+ return { router: {
10230
+ ...raw,
10231
+ enabled: false
10232
+ } };
10233
+ }
10234
+ const routerUp = runningPid() !== null;
10235
+ const check = async (port, host) => {
10236
+ if (routerUp && port === cfg.port) return null;
10237
+ const p = await probe(port, host);
10238
+ return p.free ? null : `Port ${port} is in use${p.holder ? ` by ${p.holder}` : ""}.`;
10239
+ };
10240
+ const unavailable = (why) => {
10241
+ console.log(c.warn(` ${why}`));
10242
+ setupFailures.push(`Router not configured: ${why} Retry: pai setup --router-port <free port>`);
10243
+ return {};
10244
+ };
10245
+ let port;
10246
+ if (flags.port !== void 0) {
10247
+ const v = validPort(flags.port);
10248
+ if (v === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${flags.port}"`);
10249
+ const taken = await check(v);
10250
+ if (taken) return unavailable(taken);
10251
+ port = v;
10252
+ } else {
10253
+ port = cfg.port;
10254
+ for (;;) {
10255
+ const answer = await prompt(rl, chalk.bold(` Cockpit port [${cfg.port}]: `), "");
10256
+ const v = answer === "" ? cfg.port : validPort(answer);
10257
+ if (v === null) {
10258
+ console.log(c.warn("Enter a port from 1024 to 65535."));
10259
+ continue;
10260
+ }
10261
+ const taken = await check(v);
10262
+ if (!taken) {
10263
+ port = v;
10264
+ break;
10265
+ }
10266
+ if (setupOptions.yes) return unavailable(taken);
10267
+ console.log(c.warn(taken));
10268
+ }
10269
+ }
10270
+ let bind;
10271
+ if (flags.bind !== void 0) {
10272
+ const v = validBind(flags.bind);
10273
+ if (v === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${flags.bind}"`);
10274
+ bind = v;
10275
+ } else {
10276
+ const current = String(raw.bind ?? "loopback");
10277
+ const explicit = current !== "loopback" && current !== "tailscale" && validBind(current) !== null ? current : void 0;
10278
+ line();
10279
+ line(" Who can reach the cockpit? Beyond loopback it is reachable from other devices (behind a token).");
10280
+ line();
10281
+ const idx = await promptMenu(rl, [
10282
+ {
10283
+ label: "loopback",
10284
+ description: "This machine only (127.0.0.1)."
10285
+ },
10286
+ {
10287
+ label: "tailscale",
10288
+ description: "Your other Tailscale devices."
10289
+ },
10290
+ {
10291
+ label: "explicit address",
10292
+ description: "A specific IPv4 address of this machine."
10293
+ }
10294
+ ], current === "tailscale" ? 1 : explicit ? 2 : 0);
10295
+ bind = idx === 0 ? "loopback" : "tailscale";
10296
+ if (idx === 2) for (;;) {
10297
+ const a = validBind(await prompt(rl, chalk.bold(` IPv4 address${explicit ? ` [${explicit}]` : ""}: `), explicit ?? ""));
10298
+ if (a && a !== "tailscale") {
10299
+ bind = a;
10300
+ break;
10301
+ }
10302
+ console.log(c.warn("Enter a dotted IPv4 address (not 0.0.0.0)."));
10303
+ }
10304
+ }
10305
+ const host = bind === "loopback" ? void 0 : resolveBindAddress(bind) ?? void 0;
10306
+ if (host && !(routerUp && port === cfg.port)) {
10307
+ const taken = await check(port, host);
10308
+ if (taken) return unavailable(`${taken.slice(0, -1)} on ${host}.`);
10309
+ }
10310
+ 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.`));
10311
+ if (bind === "tailscale" && resolveBindAddress("tailscale") === null) console.log(c.warn(" Tailscale is not up here; the extra listener starts once it is."));
10312
+ console.log(c.ok(`Router enabled on port ${port}, reach: ${bind}.`));
10313
+ return { router: {
10314
+ ...raw,
10315
+ enabled: true,
10316
+ port,
10317
+ bind
10318
+ } };
10319
+ }
10320
+
9504
10321
  //#endregion
9505
10322
  //#region src/cli/commands/setup/index.ts
9506
10323
  /**
@@ -9516,6 +10333,11 @@ async function step(name, fn) {
9516
10333
  }
9517
10334
  }
9518
10335
  async function runSetup() {
10336
+ setupFailures.length = 0;
10337
+ if (setupOptions.dryRun) {
10338
+ stepAibroker();
10339
+ return;
10340
+ }
9519
10341
  const rl = createRl();
9520
10342
  try {
9521
10343
  if (existsSync(CONFIG_FILE$2)) {
@@ -9546,6 +10368,8 @@ async function runSetup() {
9546
10368
  const daName = await step("stepDaName", () => stepDaName(rl));
9547
10369
  const settingsPatched = await step("stepSettings", () => stepSettings(rl, daName));
9548
10370
  const daemonInstalled = await step("stepDaemon", () => stepDaemon(rl));
10371
+ const routerConfig = await step("stepRouter", () => stepRouter(rl));
10372
+ const aibroker = await step("stepAibroker", () => stepAibroker());
9549
10373
  const mcpRegistered = await step("stepMcp", () => stepMcp(rl));
9550
10374
  await step("stepDirectories", () => stepDirectories(rl));
9551
10375
  const taskConfig = await step("stepTaskBus", () => stepTaskBus(rl));
@@ -9554,50 +10378,338 @@ async function runSetup() {
9554
10378
  ...storageConfig,
9555
10379
  ...embeddingConfig,
9556
10380
  ...taskConfig,
9557
- ...workersConfig
10381
+ ...workersConfig,
10382
+ ...routerConfig
9558
10383
  };
9559
10384
  mergeConfig(allUpdates);
9560
10385
  line();
9561
10386
  console.log(chalk.green(" Configuration saved."));
9562
10387
  await step("stepInitialIndex", () => stepInitialIndex(rl));
9563
- stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered);
10388
+ stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, process.platform, aibroker, setupFailures);
9564
10389
  } finally {
9565
10390
  rl.close();
9566
10391
  }
9567
10392
  }
9568
10393
 
10394
+ //#endregion
10395
+ //#region src/cli/commands/setup/questions.ts
10396
+ readConfigRaw();
10397
+ /** yes/no components: key = setupOptions.answers key, flag form depends on the default. */
10398
+ const COMPONENTS = [
10399
+ {
10400
+ key: "claude-md",
10401
+ step: "Agent configuration",
10402
+ question: "Write ~/.claude/CLAUDE.md from the PAI template?",
10403
+ default: true
10404
+ },
10405
+ {
10406
+ key: "skill",
10407
+ step: "PAI skill",
10408
+ question: "Install the PAI skill?",
10409
+ default: true
10410
+ },
10411
+ {
10412
+ key: "steering-rules",
10413
+ step: "Steering rules",
10414
+ question: "Install the AI steering rules?",
10415
+ default: true
10416
+ },
10417
+ {
10418
+ key: "skill-stubs",
10419
+ step: "Skill stubs",
10420
+ question: "Symlink the PAI skill stubs into ~/.claude/skills/?",
10421
+ default: true
10422
+ },
10423
+ {
10424
+ key: "hooks",
10425
+ step: "Hooks",
10426
+ question: "Install the lifecycle hooks (session stop, pre-compact, statusline)?",
10427
+ default: true
10428
+ },
10429
+ {
10430
+ key: "ts-hooks",
10431
+ step: "TypeScript hooks",
10432
+ question: "Install the TypeScript hooks to ~/.claude/Hooks/?",
10433
+ default: true
10434
+ },
10435
+ {
10436
+ key: "settings",
10437
+ step: "Settings",
10438
+ question: "Patch ~/.claude/settings.json with PAI hooks, env vars and settings?",
10439
+ default: true
10440
+ },
10441
+ {
10442
+ key: "daemon",
10443
+ step: "Daemon",
10444
+ question: "Install the daemon as a login service?",
10445
+ default: true
10446
+ },
10447
+ {
10448
+ key: "mcp",
10449
+ step: "MCP",
10450
+ question: "Register the PAI MCP server in ~/.claude.json?",
10451
+ default: true
10452
+ },
10453
+ {
10454
+ key: "registry-scan",
10455
+ step: "Directories",
10456
+ question: "Run `pai registry scan` after setup?",
10457
+ default: false
10458
+ },
10459
+ {
10460
+ key: "task-bus",
10461
+ step: "Task bus",
10462
+ question: "Enable the task bus (needs a Todoist account)?",
10463
+ default: false
10464
+ },
10465
+ {
10466
+ key: "workers",
10467
+ step: "Workers",
10468
+ question: "Turn workers on with the built-in anthropic provider?",
10469
+ default: true
10470
+ }
10471
+ ];
10472
+ /** Components whose answer is not a plain default. */
10473
+ const COMPONENT_NOTES = {
10474
+ "registry-scan": "Off by default: scanning walks your project directories.",
10475
+ "task-bus": "Off by default. Needs a Todoist token in the config; with --yes the token cannot be asked, so set it first."
10476
+ };
10477
+ const componentFlag = (key, def) => def ? `--no-${key}` : `--${key}`;
10478
+ function setupQuestions() {
10479
+ const raw = readConfigRaw();
10480
+ const router = loadConfig().router;
10481
+ const rawRouter = raw.router ?? {};
10482
+ const q = [];
10483
+ const cur = (v) => v === void 0 || v === null || v === "" ? {} : { current: v };
10484
+ q.push({
10485
+ id: "storage",
10486
+ step: "Storage",
10487
+ question: "Where should PAI store its data?",
10488
+ type: "choice",
10489
+ flag: "--storage",
10490
+ default: "sqlite",
10491
+ allowed: ["sqlite", "postgres"],
10492
+ ...cur(raw.storageBackend),
10493
+ note: "With --yes and no --storage: postgres when a local PostgreSQL answers, else sqlite."
10494
+ });
10495
+ q.push({
10496
+ id: "pg-url",
10497
+ step: "Storage",
10498
+ question: "PostgreSQL connection string (only when storage is postgres; default: the PAI Docker container)",
10499
+ type: "string",
10500
+ flag: "--pg-url",
10501
+ default: null,
10502
+ ...cur((raw.postgres ?? {}).connectionString)
10503
+ });
10504
+ q.push({
10505
+ id: "embedding",
10506
+ step: "Embedding",
10507
+ question: "Which embedding model for semantic search?",
10508
+ type: "choice",
10509
+ flag: "--embedding",
10510
+ default: "snowflake",
10511
+ allowed: [
10512
+ "snowflake",
10513
+ "bge",
10514
+ "nomic",
10515
+ "ollama",
10516
+ "none"
10517
+ ],
10518
+ ...cur(raw.embeddingModel)
10519
+ });
10520
+ const component = (comp) => {
10521
+ const note = COMPONENT_NOTES[comp.key];
10522
+ q.push({
10523
+ id: comp.key,
10524
+ step: comp.step,
10525
+ question: comp.question,
10526
+ type: "bool",
10527
+ flag: componentFlag(comp.key, comp.default),
10528
+ default: comp.default,
10529
+ ...note ? { note } : {}
10530
+ });
10531
+ };
10532
+ const routerQuestions = () => {
10533
+ q.push({
10534
+ id: "router",
10535
+ step: "Router and cockpit",
10536
+ question: "Enable the router and cockpit?",
10537
+ type: "bool",
10538
+ flag: "--router / --no-router",
10539
+ default: true,
10540
+ ...cur(typeof rawRouter.enabled === "boolean" ? rawRouter.enabled : void 0),
10541
+ 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."
10542
+ });
10543
+ q.push({
10544
+ id: "router-port",
10545
+ step: "Router and cockpit",
10546
+ question: "Port for the cockpit?",
10547
+ type: "port",
10548
+ flag: "--router-port",
10549
+ default: router.port,
10550
+ ...cur(rawRouter.port)
10551
+ });
10552
+ q.push({
10553
+ id: "router-bind",
10554
+ step: "Router and cockpit",
10555
+ question: "Who can reach the cockpit? Beyond loopback it is reachable from other devices.",
10556
+ type: "choice",
10557
+ flag: "--router-bind",
10558
+ default: "loopback",
10559
+ allowed: [
10560
+ "loopback",
10561
+ "tailscale",
10562
+ "<ipv4 address>"
10563
+ ],
10564
+ ...cur(rawRouter.bind)
10565
+ });
10566
+ };
10567
+ const daName = () => q.push({
10568
+ id: "da-name",
10569
+ step: "Settings",
10570
+ question: "What should your assistant be called?",
10571
+ type: "string",
10572
+ flag: "--da-name",
10573
+ default: "PAI"
10574
+ });
10575
+ const aibroker = () => q.push({
10576
+ id: "aibroker",
10577
+ step: "AIBroker",
10578
+ question: "Install AIBroker (messaging)?",
10579
+ type: "bool",
10580
+ flag: "--no-aibroker",
10581
+ default: true
10582
+ });
10583
+ for (const comp of COMPONENTS) {
10584
+ if (comp.key === "settings") daName();
10585
+ component(comp);
10586
+ if (comp.key === "daemon") {
10587
+ routerQuestions();
10588
+ aibroker();
10589
+ }
10590
+ }
10591
+ return q;
10592
+ }
10593
+ function renderQuestions(qs) {
10594
+ return qs.map((x, i) => {
10595
+ const parts = [`${i + 1}. [${x.step}] ${x.question}`, ` flag: ${x.flag} default: ${x.default ?? "(none)"}`];
10596
+ if (x.allowed) parts.push(` allowed: ${x.allowed.join(" | ")}`);
10597
+ if (x.current !== void 0) parts.push(` current: ${x.current} (already set)`);
10598
+ if (x.note) parts.push(` note: ${x.note}`);
10599
+ return parts.join("\n");
10600
+ }).join("\n");
10601
+ }
10602
+
9569
10603
  //#endregion
9570
10604
  //#region src/cli/commands/setup.ts
9571
10605
  /** `pai setup`: the first-time setup wizard (steps live in ./setup/). */
10606
+ /** One flag per yes/no component: --no-<name> for default-yes ones, --<name> for default-no ones. */
10607
+ const componentOptions = COMPONENTS.map((comp) => ({
10608
+ name: componentFlag(comp.key, comp.default).slice(2),
10609
+ type: "boolean",
10610
+ default: false,
10611
+ summary: `${comp.default ? "Skip" : "Do"}: ${comp.question}`
10612
+ }));
9572
10613
  const setupCommand = {
9573
10614
  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"],
10615
+ summary: "First-time setup: storage, search, hooks, daemon, router",
10616
+ 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).",
10617
+ options: [
10618
+ {
10619
+ name: "yes",
10620
+ short: "y",
10621
+ type: "boolean",
10622
+ default: false,
10623
+ summary: "Take every default (or the flag given) and read no input"
10624
+ },
10625
+ {
10626
+ name: "questions",
10627
+ type: "boolean",
10628
+ default: false,
10629
+ summary: "Print every question with its flag, default and allowed values; change nothing"
10630
+ },
10631
+ {
10632
+ name: "json",
10633
+ type: "boolean",
10634
+ default: false,
10635
+ summary: "With --questions: machine-readable JSON"
10636
+ },
10637
+ {
10638
+ name: "storage",
10639
+ type: "string",
10640
+ value: "BACKEND",
10641
+ summary: "Where to store data: sqlite or postgres (with --yes: sqlite unless a local Postgres answers)"
10642
+ },
10643
+ {
10644
+ name: "pg-url",
10645
+ type: "string",
10646
+ value: "URL",
10647
+ summary: "PostgreSQL connection string (implies --storage postgres)"
10648
+ },
10649
+ {
10650
+ name: "embedding",
10651
+ type: "string",
10652
+ value: "MODEL",
10653
+ summary: "Embeddings: snowflake, bge, nomic, ollama (Arctic on Ollama) or none"
10654
+ },
10655
+ {
10656
+ name: "da-name",
10657
+ type: "string",
10658
+ value: "NAME",
10659
+ summary: "What the assistant is called (default PAI)"
10660
+ },
10661
+ {
10662
+ name: "router",
10663
+ type: "boolean",
10664
+ summary: "Enable the router and cockpit (default)"
10665
+ },
10666
+ {
10667
+ name: "no-router",
10668
+ type: "boolean",
10669
+ summary: "Do not enable the router and cockpit"
10670
+ },
10671
+ {
10672
+ name: "router-port",
10673
+ type: "number",
10674
+ value: "PORT",
10675
+ summary: "Cockpit port (default: config router.port, 8790)"
10676
+ },
10677
+ {
10678
+ name: "router-bind",
10679
+ type: "string",
10680
+ value: "SCOPE",
10681
+ summary: "Who reaches the cockpit: loopback (default), tailscale or an IPv4 address"
10682
+ },
10683
+ {
10684
+ name: "no-aibroker",
10685
+ type: "boolean",
10686
+ default: false,
10687
+ summary: "Do not install AIBroker (the messaging layer)"
10688
+ },
10689
+ ...componentOptions,
10690
+ {
10691
+ name: "dry-run",
10692
+ type: "boolean",
10693
+ default: false,
10694
+ summary: "Validate the flags, print the AIBroker install commands and change nothing (other steps are not previewed)"
10695
+ }
10696
+ ],
10697
+ examples: [
10698
+ "pai setup",
10699
+ "pai setup --yes --storage sqlite",
10700
+ "pai setup --questions --json",
10701
+ "pai setup --yes --storage sqlite --embedding none --router-port 9000 --router-bind tailscale --no-hooks"
10702
+ ],
9589
10703
  seeAlso: ["pai update", "pai daemon status"],
9590
10704
  run(c) {
9591
- const storage = c.opts.storage;
9592
10705
  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;
10706
+ if (c.opts.questions === true) {
10707
+ const qs = setupQuestions();
10708
+ console.log(c.opts.json === true ? JSON.stringify(qs, null, 2) : renderQuestions(qs));
9596
10709
  return;
9597
10710
  }
9598
- setupOptions.yes = c.opts.yes === true;
9599
- setupOptions.storage = storage;
9600
10711
  try {
10712
+ applySetupFlags(c.opts);
9601
10713
  await runSetup();
9602
10714
  } catch (e) {
9603
10715
  console.error(chalk.red(`pai setup: ${e instanceof Error ? e.message : String(e)}`));
@@ -9606,6 +10718,39 @@ const setupCommand = {
9606
10718
  });
9607
10719
  }
9608
10720
  };
10721
+ /** Validates the flags and copies them into setupOptions (throws naming the flag). */
10722
+ function applySetupFlags(opts) {
10723
+ const storage = opts.storage;
10724
+ if (storage && storage !== "sqlite" && storage !== "postgres") throw new Error(`--storage must be sqlite or postgres, got "${storage}"`);
10725
+ const embedding = opts.embedding;
10726
+ if (embedding && ![
10727
+ "snowflake",
10728
+ "bge",
10729
+ "nomic",
10730
+ "ollama",
10731
+ "none"
10732
+ ].includes(embedding)) throw new Error(`--embedding must be snowflake, bge, nomic, ollama or none, got "${embedding}"`);
10733
+ const port = opts["router-port"];
10734
+ if (port !== void 0 && validPort(port) === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${port}"`);
10735
+ const bind = opts["router-bind"];
10736
+ 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}"`);
10737
+ if (opts.router === true && opts["no-router"] === true) throw new Error("--router and --no-router contradict each other");
10738
+ const answers = {};
10739
+ for (const comp of COMPONENTS) if (opts[componentFlag(comp.key, comp.default).slice(2)] === true) answers[comp.key] = !comp.default;
10740
+ setupOptions.yes = opts.yes === true;
10741
+ setupOptions.noAibroker = opts["no-aibroker"] === true;
10742
+ setupOptions.dryRun = opts["dry-run"] === true;
10743
+ setupOptions.storage = storage;
10744
+ setupOptions.pgUrl = opts["pg-url"];
10745
+ setupOptions.embedding = embedding;
10746
+ setupOptions.daName = opts["da-name"];
10747
+ setupOptions.router = {
10748
+ enabled: opts["no-router"] === true ? false : opts.router === true ? true : void 0,
10749
+ port: opts["router-port"],
10750
+ bind: opts["router-bind"]
10751
+ };
10752
+ setupOptions.answers = answers;
10753
+ }
9609
10754
  const installAlias = {
9610
10755
  ...setupCommand,
9611
10756
  name: "install",
@@ -10399,29 +11544,46 @@ function migrateFederationDbOrphan(dryRun) {
10399
11544
  }
10400
11545
  return migrateOrphanFederationDb({ dryRun });
10401
11546
  }
10402
- function checkpointAndVerify(oldPath, newPath, label) {
10403
- let hasSqlite3 = true;
11547
+ /**
11548
+ * Flush the old DB's WAL into its main file (before the byte copy, so the copy
11549
+ * holds every committed row) and set the emptied sidecars aside. A non-empty
11550
+ * WAL is never moved away without a successful checkpoint.
11551
+ */
11552
+ function checkpointWal(oldPath, label) {
11553
+ let checkpointed = false;
10404
11554
  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" });
11555
+ const db = new BetterSqlite3(oldPath, { timeout: 500 });
11556
+ try {
11557
+ const [row] = db.pragma("wal_checkpoint(TRUNCATE)");
11558
+ checkpointed = row?.busy === 0;
11559
+ } finally {
11560
+ db.close();
11561
+ }
10411
11562
  } catch {}
10412
11563
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
10413
11564
  for (const suffix of ["-wal", "-shm"]) {
10414
11565
  const sidecar = `${oldPath}${suffix}`;
10415
- if (existsSync(sidecar)) try {
11566
+ if (!existsSync(sidecar)) continue;
11567
+ 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`);
11568
+ try {
10416
11569
  renameSync(sidecar, `${sidecar}.migrated-${stamp}`);
10417
11570
  } catch {}
10418
11571
  }
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`);
11572
+ }
11573
+ /** PRAGMA integrity_check on the migrated copy, through the node driver. */
11574
+ function verifyIntegrity(newPath) {
11575
+ let check;
11576
+ try {
11577
+ const db = new BetterSqlite3(newPath, { readonly: true });
11578
+ try {
11579
+ check = String(db.pragma("integrity_check", { simple: true }));
11580
+ } finally {
11581
+ db.close();
11582
+ }
10422
11583
  } catch (e) {
10423
- if (e instanceof PaiFileMigrationError) throw e;
11584
+ throw new PaiFileMigrationError(`${newPath}: integrity_check could not run (${String(e)})`);
10424
11585
  }
11586
+ if (check !== "ok") throw new PaiFileMigrationError(`${newPath}: PRAGMA integrity_check reported "${check}" — investigate before trusting this copy`);
10425
11587
  }
10426
11588
  /**
10427
11589
  * ~/.pai/registry.db — a third per-user location, alongside PAI_HOME and the
@@ -10448,8 +11610,9 @@ function migrateRegistryDbFile(dryRun) {
10448
11610
  execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
10449
11611
  execFileSync("sleep", ["2"]);
10450
11612
  }
11613
+ checkpointWal(oldPath, "registry database");
10451
11614
  const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
10452
- if (result.fromPath) checkpointAndVerify(oldPath, newPath, "registry database");
11615
+ if (result.fromPath) verifyIntegrity(newPath);
10453
11616
  return result;
10454
11617
  }
10455
11618
  /**
@@ -10477,8 +11640,9 @@ function migrateFederationDbFile(dryRun) {
10477
11640
  execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
10478
11641
  execFileSync("sleep", ["2"]);
10479
11642
  }
11643
+ checkpointWal(oldPath, "federation database");
10480
11644
  const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
10481
- if (result.fromPath) checkpointAndVerify(oldPath, newPath, "federation database");
11645
+ if (result.fromPath) verifyIntegrity(newPath);
10482
11646
  return result;
10483
11647
  }
10484
11648
 
@@ -10952,8 +12116,8 @@ async function escalate(task, headline, detail, state, now, opts) {
10952
12116
  state.alarmedAt[task.id] = now;
10953
12117
  let delivered = false;
10954
12118
  try {
10955
- const { routeNotification } = await import("../router-D6Ec6Fid.mjs");
10956
- const { loadConfig } = await import("../config-TaWmdTLx.mjs");
12119
+ const { routeNotification } = await import("../router-DwHJRgG9.mjs");
12120
+ const { loadConfig } = await import("../config-DtENJ6cL.mjs");
10957
12121
  delivered = (await routeNotification({
10958
12122
  event: "error",
10959
12123
  title: `PAI: ${headline}`,
@@ -11059,7 +12223,7 @@ const GROSS_OVERRUN_FACTOR = 5;
11059
12223
  */
11060
12224
  function detectWebhook(bin = "aibroker") {
11061
12225
  try {
11062
- const out = execFileSync(bin, ["todoist", "status"], {
12226
+ return webhookFromStatus(execFileSync(bin, ["todoist", "status"], {
11063
12227
  timeout: 1e4,
11064
12228
  encoding: "utf-8",
11065
12229
  stdio: [
@@ -11067,13 +12231,21 @@ function detectWebhook(bin = "aibroker") {
11067
12231
  "pipe",
11068
12232
  "pipe"
11069
12233
  ]
11070
- });
11071
- return /authoris|authoriz|token on file|grant/i.test(out) && !/no (token|grant)/i.test(out);
12234
+ }));
11072
12235
  } catch {
11073
12236
  return false;
11074
12237
  }
11075
12238
  }
11076
12239
  /**
12240
+ * Read `aibroker todoist status` output. Only an explicit positive line counts:
12241
+ * "Authorised <timestamp>" and no "FAILING" verdict. Negative texts ("No Todoist
12242
+ * authorisation on file", "Not authorized", "grant: missing") and a token that
12243
+ * is on file but rejected both read as no webhook.
12244
+ */
12245
+ function webhookFromStatus(out) {
12246
+ return /^Authori[sz]ed\s+\S/m.test(out) && !/^FAILING\b/m.test(out);
12247
+ }
12248
+ /**
11077
12249
  * Discard restore entries whose run is no longer in flight.
11078
12250
  *
11079
12251
  * `triggeredRestore` belongs to one run: it records the occurrence that run
@@ -11118,83 +12290,93 @@ async function tick(opts) {
11118
12290
  const ordered = dispatchOrder(tasks);
11119
12291
  if (!opts.dryRun) dropOrphanedRestores(tasks, state);
11120
12292
  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++;
12293
+ try {
12294
+ for (const task of ordered) {
12295
+ const prevDue = state.lastSeenDue[task.id];
12296
+ const d = decide(task, {
12297
+ now,
12298
+ state,
12299
+ history: state.history,
12300
+ lastSeenDue: state.lastSeenDue,
12301
+ claimSeenAt: state.claimSeenAt,
12302
+ webhookActive
12303
+ });
12304
+ let note = "";
12305
+ try {
12306
+ if (!opts.dryRun && task.due) state.lastSeenDue[task.id] = task.due;
12307
+ if (!opts.dryRun) if (isRunning(task)) {
12308
+ if (state.claimSeenAt[task.id] === void 0 && task.recurrence && prevDue !== void 0) {
12309
+ if (wasTicked(task, prevDue)) state.triggeredRestore[task.id] = restoreDueString(task.recurrence, prevDue);
12310
+ }
12311
+ state.claimSeenAt[task.id] ??= now;
12312
+ } else delete state.claimSeenAt[task.id];
12313
+ switch (d.action) {
12314
+ case "wait": continue;
12315
+ case "triggered": {
12316
+ const r = await handleDispatch(task, 0, opts, state, now);
12317
+ note = `ticked off by hand — ${r.note}`;
12318
+ if (!opts.dryRun) {
12319
+ if (!r.parked) report.dispatched++;
12320
+ if (r.alarm) report.stuck++;
12321
+ note += await restoreSchedule(task, d.restoreTo, opts, state);
12322
+ if (d.restoreTo) state.triggeredRestore[task.id] = d.restoreTo;
12323
+ }
12324
+ break;
12325
+ }
12326
+ case "skip":
12327
+ note = d.reason;
12328
+ break;
12329
+ case "running":
12330
+ note = `${d.elapsedMinutes}m elapsed`;
12331
+ break;
12332
+ case "dispatch": {
12333
+ const r = await handleDispatch(task, d.overdueMinutes, opts, state, now);
12334
+ note = r.note;
12335
+ if (!opts.dryRun) {
12336
+ if (!r.parked) report.dispatched++;
12337
+ if (r.alarm) report.stuck++;
12338
+ }
12339
+ break;
12340
+ }
12341
+ case "complete":
12342
+ note = await handleComplete(task, d.durationMinutes, opts, state, now);
12343
+ if (!opts.dryRun) report.completed++;
12344
+ break;
12345
+ case "abandoned":
12346
+ note = `claimed ${d.elapsedMinutes}m ago, past the ${d.thresholdMinutes}m limit — releasing the claim`;
12347
+ if (!opts.dryRun) {
12348
+ await releaseClaim(task, opts.provider, state);
12349
+ report.stuck++;
12350
+ }
12351
+ break;
12352
+ case "probe":
12353
+ case "orphaned": {
12354
+ const result = await handleProbe(task, d.action === "probe" ? d.elapsedMinutes : null, opts, state, now);
12355
+ note = result.note;
12356
+ if (!opts.dryRun) {
12357
+ report.probed++;
12358
+ if (result.stuck) report.stuck++;
12359
+ if (result.dispatched) report.dispatched++;
12360
+ }
12361
+ break;
12362
+ }
11186
12363
  }
11187
- break;
12364
+ report.decisions.push({
12365
+ decision: d,
12366
+ note
12367
+ });
12368
+ } catch (err) {
12369
+ report.decisions.push({
12370
+ decision: d,
12371
+ note: `error: ${err instanceof Error ? err.message : String(err)}`
12372
+ });
11188
12373
  }
11189
12374
  }
11190
- report.decisions.push({
11191
- decision: d,
11192
- note
11193
- });
11194
- }
11195
- if (!opts.dryRun) {
11196
- reconcileWithDisk(state, stateFile);
11197
- saveState(state, stateFile);
12375
+ } finally {
12376
+ if (!opts.dryRun) {
12377
+ reconcileWithDisk(state, stateFile);
12378
+ saveState(state, stateFile);
12379
+ }
11198
12380
  }
11199
12381
  return report;
11200
12382
  }
@@ -11611,7 +12793,7 @@ function cleanBrokenSymlinks(dir) {
11611
12793
  const full = join(dir, entry);
11612
12794
  try {
11613
12795
  if (lstatSync(full).isSymbolicLink()) {
11614
- if (!existsSync(readlinkSync(full))) {
12796
+ if (!existsSync(resolve(dir, readlinkSync(full)))) {
11615
12797
  unlinkSync(full);
11616
12798
  removed++;
11617
12799
  }
@@ -12667,10 +13849,22 @@ function migrateWorkerLogs(oldPath, newPath, opts = {}) {
12667
13849
  const active = activeSpawnedWorkerCount(oldPath);
12668
13850
  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
13851
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
12670
- cpSync(oldPath, newPath, { recursive: true });
13852
+ const partial = `${newPath}.partial`;
13853
+ rmSync(partial, {
13854
+ recursive: true,
13855
+ force: true
13856
+ });
13857
+ cpSync(oldPath, partial, { recursive: true });
12671
13858
  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`);
13859
+ const toCount = totalFileCount(partial);
13860
+ if (fromCount !== toCount) {
13861
+ rmSync(partial, {
13862
+ recursive: true,
13863
+ force: true
13864
+ });
13865
+ throw new WorkerLogsMigrationError(`${newPath}: copy produced ${toCount} files, expected ${fromCount} from ${oldPath} — aborting, old directory left in place`);
13866
+ }
13867
+ renameSync(partial, newPath);
12674
13868
  renameSync(oldPath, `${oldPath}.migrated-${stamp}`);
12675
13869
  return {
12676
13870
  fromPath: oldPath,
@@ -13847,7 +15041,7 @@ async function migrateToPostgres(opts = {}) {
13847
15041
  const dump = pgDump(container, pgConfig, dumpPath);
13848
15042
  if (!dump.ok) return refused(dump.reason, dryRun);
13849
15043
  }
13850
- const { PostgresBackend } = await import("../postgres-DgqWyqhN.mjs");
15044
+ const { PostgresBackend } = await import("../postgres-BdBNkL_l.mjs");
13851
15045
  await PostgresBackend.ensureDatabase(pgConfig);
13852
15046
  const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
13853
15047
  const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
@@ -14579,6 +15773,35 @@ const homeCommand = {
14579
15773
  * `firstlast@company.com`. Anything that should count gets listed.
14580
15774
  */
14581
15775
  /**
15776
+ * Split a recipient field into its individual entries. Commas and semicolons
15777
+ * separate entries unless inside a quoted display name or an angle-bracket
15778
+ * address. Empty entries are dropped.
15779
+ */
15780
+ function splitAddresses(raw) {
15781
+ const out = [];
15782
+ let cur = "";
15783
+ let quoted = false;
15784
+ let angled = false;
15785
+ for (let i = 0; i < raw.length; i++) {
15786
+ const c = raw[i];
15787
+ if (quoted && c === "\\" && i + 1 < raw.length) {
15788
+ cur += c + raw[++i];
15789
+ continue;
15790
+ }
15791
+ if (c === "\"") quoted = !quoted;
15792
+ else if (!quoted && c === "<") angled = true;
15793
+ else if (!quoted && c === ">") angled = false;
15794
+ if (!quoted && !angled && (c === "," || c === ";")) {
15795
+ if (cur.trim()) out.push(cur.trim());
15796
+ cur = "";
15797
+ continue;
15798
+ }
15799
+ cur += c;
15800
+ }
15801
+ if (cur.trim()) out.push(cur.trim());
15802
+ return out;
15803
+ }
15804
+ /**
14582
15805
  * Reduce an address to a comparable form: trimmed, unwrapped, lowercased.
14583
15806
  *
14584
15807
  * Handles `Display Name <addr@example.com>` because recipient fields carry that
@@ -14588,8 +15811,9 @@ const homeCommand = {
14588
15811
  */
14589
15812
  function normalizeEmail(raw) {
14590
15813
  if (!raw) return null;
14591
- let value = raw.trim();
14592
- if (!value) return null;
15814
+ const parts = splitAddresses(raw);
15815
+ if (parts.length !== 1) return null;
15816
+ let value = parts[0];
14593
15817
  const angled = value.match(/<([^>]*)>/);
14594
15818
  if (angled) value = angled[1].trim();
14595
15819
  value = value.toLowerCase();
@@ -15204,7 +16428,7 @@ const out = (s) => process.stdout.write(s + "\n");
15204
16428
  async function repoOf(project) {
15205
16429
  if (typeof project === "string" && project) {
15206
16430
  if (existsSync(project)) return resolve(project);
15207
- const { getProject } = await import("../helpers-DPJEHPq6.mjs");
16431
+ const { getProject } = await import("../helpers-CMGjy_0N.mjs");
15208
16432
  const row = await getProject(project);
15209
16433
  if (!row) throw new CliError(`no project ${project}`, "pai projects list");
15210
16434
  return row.root_path;
@@ -16026,7 +17250,7 @@ async function cmdPromote(opts) {
16026
17250
  console.error(dim$1(` Pick a durable location for the new project.`));
16027
17251
  process.exit(1);
16028
17252
  }
16029
- const { getRegistryBackend } = await import("../factory-DAGG9_n8.mjs");
17253
+ const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
16030
17254
  const registryBackend = await getRegistryBackend();
16031
17255
  const encodedDir = encodeDir(targetPath);
16032
17256
  if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
@@ -17270,7 +18494,7 @@ async function cmdUnregister(slug, opts = {}) {
17270
18494
  * deprecated alias that forwards here with a stderr notice.
17271
18495
  */
17272
18496
  async function projectListCmd(opts) {
17273
- const { cmdMain } = await import("../main-resolver-DVPpSWVy.mjs");
18497
+ const { cmdMain } = await import("../main-resolver-3Ns1HPmS.mjs");
17274
18498
  await cmdMain(void 0, void 0, opts);
17275
18499
  return 0;
17276
18500
  }
@@ -18693,7 +19917,7 @@ async function registryScanCmd(opts) {
18693
19917
  return 0;
18694
19918
  }
18695
19919
  if (opts.filterReport) {
18696
- const { reportUnregistrable } = await import("../registrable-D9aNZOTh.mjs");
19920
+ const { reportUnregistrable } = await import("../registrable-C7b6givx.mjs");
18697
19921
  const r = await reportUnregistrable(await getRegistryBackend());
18698
19922
  console.log(`Registry rows: ${r.total}`);
18699
19923
  console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
@@ -18899,7 +20123,7 @@ const VALID_OUTCOMES = new Set([
18899
20123
  */
18900
20124
  function run$1(bin, args, stdin, killAfterMs) {
18901
20125
  return new Promise((resolve, reject) => {
18902
- execFile(bin, args, {
20126
+ const child = execFile(bin, args, {
18903
20127
  timeout: killAfterMs,
18904
20128
  maxBuffer: 1024 * 1024
18905
20129
  }, (error, stdout, stderr) => {
@@ -18908,15 +20132,26 @@ function run$1(bin, args, stdin, killAfterMs) {
18908
20132
  return;
18909
20133
  }
18910
20134
  resolve(stdout);
18911
- }).stdin?.end(stdin);
20135
+ });
20136
+ child.stdin?.on("error", (e) => reject(e));
20137
+ child.stdin?.end(stdin);
18912
20138
  });
18913
20139
  }
20140
+ /** A leading dash would be parsed by the CLI as an option, not a project. */
20141
+ function validProject(project) {
20142
+ return project.length > 0 && !project.startsWith("-");
20143
+ }
18914
20144
  var AiBrokerTransport = class {
18915
20145
  constructor(bin = "aibroker", timeoutSecs = DEFAULT_DISPATCH_TIMEOUT_SECS) {
18916
20146
  this.bin = bin;
18917
20147
  this.timeoutSecs = timeoutSecs;
18918
20148
  }
18919
20149
  async dispatch(project, message, opts) {
20150
+ if (!validProject(project)) return {
20151
+ outcome: "unlaunchable",
20152
+ session: project,
20153
+ reason: `invalid project name: ${project}`
20154
+ };
18920
20155
  const args = [
18921
20156
  "dispatch",
18922
20157
  project,
@@ -18960,6 +20195,10 @@ var AiBrokerProber = class {
18960
20195
  this.timeoutSecs = timeoutSecs;
18961
20196
  }
18962
20197
  async ask(project, question) {
20198
+ if (!validProject(project)) return {
20199
+ state: "silent",
20200
+ reason: `invalid project name: ${project}`
20201
+ };
18963
20202
  let stdout;
18964
20203
  try {
18965
20204
  stdout = await run$1(this.bin, [
@@ -19083,33 +20322,36 @@ function cliPath() {
19083
20322
  }
19084
20323
  throw new Error("cannot locate the PAI CLI entrypoint (dist/cli/index.mjs) — refusing to install a scheduler that would silently do nothing");
19085
20324
  }
20325
+ function xmlEscape(value) {
20326
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
20327
+ }
19086
20328
  function generateSchedulePlist(intervalSecs, cli) {
19087
20329
  return `<?xml version="1.0" encoding="UTF-8"?>
19088
20330
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19089
20331
  <plist version="1.0">
19090
20332
  <dict>
19091
20333
  <key>Label</key>
19092
- <string>${SCHEDULE_LABEL}</string>
20334
+ <string>${xmlEscape(SCHEDULE_LABEL)}</string>
19093
20335
 
19094
20336
  <key>ProgramArguments</key>
19095
20337
  <array>
19096
- <string>/usr/local/bin/node</string>
19097
- <string>${cli}</string>
20338
+ <string>${xmlEscape(process.execPath)}</string>
20339
+ <string>${xmlEscape(cli)}</string>
19098
20340
  <string>task</string>
19099
20341
  <string>poll</string>
19100
20342
  </array>
19101
20343
 
19102
20344
  <key>StartInterval</key>
19103
- <integer>${intervalSecs}</integer>
20345
+ <integer>${Math.trunc(intervalSecs)}</integer>
19104
20346
 
19105
20347
  <key>RunAtLoad</key>
19106
20348
  <true/>
19107
20349
 
19108
20350
  <key>StandardOutPath</key>
19109
- <string>${SCHEDULE_LOG}</string>
20351
+ <string>${xmlEscape(SCHEDULE_LOG)}</string>
19110
20352
 
19111
20353
  <key>StandardErrorPath</key>
19112
- <string>${SCHEDULE_LOG}</string>
20354
+ <string>${xmlEscape(SCHEDULE_LOG)}</string>
19113
20355
 
19114
20356
  <key>EnvironmentVariables</key>
19115
20357
  <dict>
@@ -20517,8 +21759,8 @@ function embedProgressLine(done, backlog) {
20517
21759
  async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSize = DEFAULT_PAGE_SIZE, maxMillis, run, shouldStop) {
20518
21760
  const label = projectSlug ? `project ${projectSlug}` : "all projects";
20519
21761
  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");
21762
+ const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
21763
+ const { gateBackend } = await import("../embedding-gate-Cnk4YCyX.mjs");
20522
21764
  const gate = await gateBackend(backend, { record: true });
20523
21765
  if (!gate.ok) {
20524
21766
  console.error(err(gate.reason));
@@ -20847,7 +22089,7 @@ async function memorySearchCmd(query, opts) {
20847
22089
  let results;
20848
22090
  if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
20849
22091
  else {
20850
- const { embedQuery } = await import("../embedding-gate-Z7CJSOJk.mjs");
22092
+ const { embedQuery } = await import("../embedding-gate-Cnk4YCyX.mjs");
20851
22093
  console.log(dim$1("Generating query embedding..."));
20852
22094
  const q = await embedQuery(backend, query);
20853
22095
  const queryEmbedding = q.vec;
@@ -20897,7 +22139,7 @@ async function memorySearchCmd(query, opts) {
20897
22139
  return 0;
20898
22140
  }
20899
22141
  if (opts.rerank !== false) {
20900
- const { rerankResults } = await import("../reranker-B4r4vykM.mjs");
22142
+ const { rerankResults } = await import("../reranker-8g6xMwP7.mjs");
20901
22143
  console.log(dim$1("Reranking with cross-encoder..."));
20902
22144
  results = await rerankResults(query, results, { topK: maxResults });
20903
22145
  }
@@ -20927,9 +22169,9 @@ async function memorySearchCmd(query, opts) {
20927
22169
  //#region src/cli/commands/memory/sources-cmd.ts
20928
22170
  /** `pai memory sources` — what the indexer has taken in. */
20929
22171
  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");
22172
+ const { createStorageBackend } = await import("../factory-Bsib-qDG.mjs");
22173
+ const { loadConfig } = await import("../config-DtENJ6cL.mjs");
22174
+ const { cmdMemorySources } = await import("../sources-ESUkZaWv.mjs");
20933
22175
  let backend;
20934
22176
  try {
20935
22177
  backend = await createStorageBackend(loadConfig());
@@ -21688,7 +22930,7 @@ async function zettelExploreCmd(note, opts) {
21688
22930
  const depth = opts.depth ?? 3;
21689
22931
  const direction = opts.direction ?? "both";
21690
22932
  const mode = opts.mode ?? "all";
21691
- const { zettelExplore } = await import("../zettelkasten-nigq7fqs.mjs");
22933
+ const { zettelExplore } = await import("../zettelkasten-B_rRRRvD.mjs");
21692
22934
  const result = await zettelExplore(await getFedBackend(), {
21693
22935
  startNote: note,
21694
22936
  depth,
@@ -21744,7 +22986,7 @@ async function zettelHealthCmd(opts) {
21744
22986
  const projectPath = opts.project;
21745
22987
  const recentDays = opts.days ?? 30;
21746
22988
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
21747
- const { zettelHealth } = await import("../zettelkasten-nigq7fqs.mjs");
22989
+ const { zettelHealth } = await import("../zettelkasten-B_rRRRvD.mjs");
21748
22990
  const result = await zettelHealth(await getFedBackend(), {
21749
22991
  scope,
21750
22992
  projectPath,
@@ -21802,7 +23044,7 @@ async function zettelSurpriseCmd(note, opts) {
21802
23044
  const limit = opts.limit ?? 10;
21803
23045
  const minSimilarity = opts.minSimilarity ?? .3;
21804
23046
  const minGraphDistance = opts.minDistance ?? 3;
21805
- const { zettelSurprise } = await import("../zettelkasten-nigq7fqs.mjs");
23047
+ const { zettelSurprise } = await import("../zettelkasten-B_rRRRvD.mjs");
21806
23048
  const backend = await getFedBackend();
21807
23049
  console.log();
21808
23050
  console.log(header(" PAI Zettel Surprise"));
@@ -21844,7 +23086,7 @@ async function zettelConverseCmd(question, opts) {
21844
23086
  const vaultProjectId = Number(opts.vaultProjectId);
21845
23087
  const depth = opts.depth ?? 2;
21846
23088
  const limit = opts.limit ?? 15;
21847
- const { zettelConverse } = await import("../zettelkasten-nigq7fqs.mjs");
23089
+ const { zettelConverse } = await import("../zettelkasten-B_rRRRvD.mjs");
21848
23090
  const backend = await getFedBackend();
21849
23091
  console.log();
21850
23092
  console.log(header(" PAI Zettel Converse"));
@@ -21894,7 +23136,7 @@ async function zettelThemesCmd(opts) {
21894
23136
  const minClusterSize = opts.minSize ?? 3;
21895
23137
  const maxThemes = opts.maxThemes ?? 10;
21896
23138
  const similarityThreshold = opts.threshold ?? .65;
21897
- const { zettelThemes } = await import("../zettelkasten-nigq7fqs.mjs");
23139
+ const { zettelThemes } = await import("../zettelkasten-B_rRRRvD.mjs");
21898
23140
  const backend = await getFedBackend();
21899
23141
  console.log();
21900
23142
  console.log(header(" PAI Zettel Themes"));
@@ -21942,7 +23184,7 @@ async function zettelSuggestCmd(note, opts) {
21942
23184
  const vaultProjectId = Number(opts.vaultProjectId);
21943
23185
  const limit = opts.limit ?? 5;
21944
23186
  const excludeLinked = opts.excludeLinked !== true;
21945
- const { zettelSuggest } = await import("../zettelkasten-nigq7fqs.mjs");
23187
+ const { zettelSuggest } = await import("../zettelkasten-B_rRRRvD.mjs");
21946
23188
  const backend = await getFedBackend();
21947
23189
  console.log();
21948
23190
  console.log(header(" PAI Zettel Suggest"));
@@ -22234,7 +23476,7 @@ function ipcCall$1(method, params) {
22234
23476
  fn();
22235
23477
  }
22236
23478
  };
22237
- const client = createConnection(paiSocketPath(), () => {
23479
+ const client = createConnection$1(paiSocketPath(), () => {
22238
23480
  client.write(JSON.stringify({
22239
23481
  id: 1,
22240
23482
  method,
@@ -22624,7 +23866,7 @@ function ipcCall(method, params) {
22624
23866
  fn();
22625
23867
  }
22626
23868
  };
22627
- const client = createConnection(paiSocketPath(), () => {
23869
+ const client = createConnection$1(paiSocketPath(), () => {
22628
23870
  client.write(JSON.stringify({
22629
23871
  id: 1,
22630
23872
  method,
@@ -24256,7 +25498,14 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
24256
25498
  * one of the two in whichever catalogue order Claude Code loads them.
24257
25499
  */
24258
25500
  /** Recursively collect every file under `dir` whose basename satisfies `matches`. */
24259
- function findFilesRecursive(dir, matches) {
25501
+ function findFilesRecursive(dir, matches, seen = /* @__PURE__ */ new Set()) {
25502
+ try {
25503
+ const real = realpathSync(dir);
25504
+ if (seen.has(real)) return [];
25505
+ seen.add(real);
25506
+ } catch {
25507
+ return [];
25508
+ }
24260
25509
  let entries;
24261
25510
  try {
24262
25511
  entries = readdirSync(dir, { withFileTypes: true });
@@ -24266,8 +25515,17 @@ function findFilesRecursive(dir, matches) {
24266
25515
  const found = [];
24267
25516
  for (const entry of entries) {
24268
25517
  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);
25518
+ let isDir = entry.isDirectory();
25519
+ let isFile = entry.isFile();
25520
+ if (entry.isSymbolicLink()) try {
25521
+ const st = statSync(full);
25522
+ isDir = st.isDirectory();
25523
+ isFile = st.isFile();
25524
+ } catch {
25525
+ continue;
25526
+ }
25527
+ if (isDir) found.push(...findFilesRecursive(full, matches, seen));
25528
+ else if (isFile && matches(entry.name)) found.push(full);
24271
25529
  }
24272
25530
  return found;
24273
25531
  }
@@ -24750,11 +26008,17 @@ function newestMcpConfig(logDir) {
24750
26008
  mtime: statSync(path).mtimeMs
24751
26009
  })).sort((a, b) => b.mtime - a.mtime)[0].path;
24752
26010
  }
26011
+ /** Child env: not a worker, and no API key (same as run-env / daemon-llm). */
26012
+ function ladderChildEnv(base) {
26013
+ const env = { ...base };
26014
+ delete env.PAI_WORKER;
26015
+ delete env.ANTHROPIC_API_KEY;
26016
+ return env;
26017
+ }
24753
26018
  /** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
24754
26019
  function runRung(rung, cwd) {
24755
26020
  return new Promise((resolvePromise) => {
24756
- const env = { ...process.env };
24757
- delete env.PAI_WORKER;
26021
+ const env = ladderChildEnv(process.env);
24758
26022
  const proc = spawn("claude", rung.args, {
24759
26023
  cwd,
24760
26024
  env,