@tekmidian/pai 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +67 -5
- package/dist/aibroker-client-BxzeEHPF.mjs +3 -0
- package/dist/{aibroker-client-CXEYBzbf.mjs → aibroker-client-CQPGqLxL.mjs} +1 -1
- package/dist/atomic-file-BorMa4T3.mjs +21 -0
- package/dist/{auto-route-D5SXrMpB.mjs → auto-route-B7Yu6Uam.mjs} +3 -3
- package/dist/auto-route-CjjQqU1o.mjs +11 -0
- package/dist/budget-Dod6rnA9.mjs +15 -0
- package/dist/{caller-identity-CVTuiLSs.mjs → caller-identity-yLFb7CKq.mjs} +6 -5
- package/dist/{capacity-DaXrVj3J.mjs → capacity-DEveQdFV.mjs} +3 -3
- package/dist/{chain-BcQlIlEd.mjs → chain-7n9Gx56r.mjs} +5 -5
- package/dist/cli/index.mjs +1628 -370
- package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-BBHeyEZI.mjs} +84 -42
- package/dist/cockpit-actions-DR9X514g.mjs +20 -0
- package/dist/cockpit-memory--dnShXBA.mjs +20 -0
- package/dist/{config-BSXKyeJ1.mjs → config-Bu1_vbY8.mjs} +42 -3
- package/dist/{config-B955yYmo.mjs → config-Cxi-aBf4.mjs} +2 -2
- package/dist/{config-TaWmdTLx.mjs → config-DtENJ6cL.mjs} +1 -1
- package/dist/{config-D5chi2LO.mjs → config-XpZWOYOG.mjs} +2 -2
- package/dist/{controls-BGkK_Bdi.mjs → controls-CPoeudou.mjs} +37 -22
- package/dist/daemon/index.mjs +43 -42
- package/dist/daemon-DKdBNKfa.mjs +45 -0
- package/dist/{daemon-CorGh6Ub.mjs → daemon-DPAo6WTa.mjs} +103 -55
- package/dist/daemon-mcp/index.mjs +40 -39
- package/dist/{define-DGFu-sCf.mjs → define-CjJsmq-6.mjs} +1 -1
- package/dist/{detect-C1d5cUkT.mjs → detect-9CZNO0yf.mjs} +2 -2
- package/dist/detect-D1ziFFfA.mjs +10 -0
- package/dist/{embed-pass-CeQD8kRE.mjs → embed-pass-DIWjA68P.mjs} +5 -5
- package/dist/{embedding-gate-BHuBsgHD.mjs → embedding-gate-6rZrWRXa.mjs} +5 -5
- package/dist/{embedding-gate-Z7CJSOJk.mjs → embedding-gate-Cnk4YCyX.mjs} +3 -3
- package/dist/{embeddings-D4t6L-EH.mjs → embeddings-BVq3esBF.mjs} +1 -1
- package/dist/{embeddings-OF9Pt4by.mjs → embeddings-CIkJBKd-.mjs} +1 -1
- package/dist/{endreason-x1KOSrcO.mjs → endreason-C577xIQz.mjs} +7 -6
- package/dist/{env-DArqGq-Z.mjs → env-BE8mrsxr.mjs} +1 -1
- package/dist/factory-Bsib-qDG.mjs +9 -0
- package/dist/{factory-BtjbQ0np.mjs → factory-DjAAsqr_.mjs} +15 -13
- package/dist/{fallback-BIyEpPsQ.mjs → fallback-DFlItw1Z.mjs} +5 -5
- package/dist/{fmt-age-BiSHrTKN.mjs → fmt-age-ByDkvkoU.mjs} +1 -1
- package/dist/{health-C6jZWvRG.mjs → health-Dnym-qcT.mjs} +4 -4
- package/dist/{helpers-DFmYRCOa.mjs → helpers-BD6V-p9w.mjs} +2 -2
- package/dist/helpers-CMGjy_0N.mjs +10 -0
- package/dist/{helpers-CJpb_MNB.mjs → helpers-CRuBUas3.mjs} +87 -38
- package/dist/hooks/capture-all-events.mjs +52 -11
- package/dist/hooks/capture-session-summary.mjs +64 -47
- package/dist/hooks/capture-tool-output.mjs +49 -8
- package/dist/hooks/context-compression-hook.mjs +30 -10
- package/dist/hooks/initialize-session.mjs +1 -0
- package/dist/hooks/load-core-context.mjs +1 -1
- package/dist/hooks/load-project-context.mjs +32 -29
- package/dist/hooks/security-validator.mjs +45 -13
- package/dist/hooks/stop-hook.mjs +54 -16
- package/dist/hooks/supervision-banner.mjs +9 -0
- package/dist/hooks/sync-todo-to-md.mjs +147 -113
- package/dist/hooks/worker-edit-repair.mjs +2 -1
- package/dist/hooks/worker-guard.mjs +40 -16
- package/dist/hooks/worker-note.mjs +12 -5
- package/dist/hooks/worker-path-repair.mjs +1 -1
- package/dist/hooks/worker-pool-host.mjs +329 -231
- package/dist/hooks/worker-status-line.mjs +195 -114
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +12 -12
- package/dist/{indexer-backend-BM2CUSu8.mjs → indexer-backend-Cll7le4P.mjs} +149 -96
- package/dist/{ipc-client-BMqWw_gv.mjs → ipc-client-CaiGJnSM.mjs} +1 -1
- package/dist/{ipc-client-YYDD6zzU.mjs → ipc-client-ZRmDKd5Z.mjs} +1 -1
- package/dist/kill-tree-BG7dRVtp.mjs +10 -0
- package/dist/{kill-tree-BjYz5BxK.mjs → kill-tree-EWYaEk5v.mjs} +8 -8
- package/dist/{launch-CLp0OJxV.mjs → launch-CXVlZjHt.mjs} +8 -8
- package/dist/{launch-DgqQdAjI.mjs → launch-Jh5NVeNe.mjs} +3 -3
- package/dist/local-candidate-DLdwQt0s.mjs +112 -0
- package/dist/local-instances-DOxSgT_L.mjs +210 -0
- package/dist/local-instances-ZeRkkuS5.mjs +6 -0
- package/dist/{local-tiers-DPZYqv9m.mjs → local-tiers-BE4jGaMa.mjs} +2 -2
- package/dist/{local-tiers-DnBIAkbK.mjs → local-tiers-DNMI5W86.mjs} +3 -3
- package/dist/{main-config-ops-C0SflOWk.mjs → main-config-ops-BEMpuhDz.mjs} +3 -3
- package/dist/{main-resolver-DVPpSWVy.mjs → main-resolver-3Ns1HPmS.mjs} +18 -17
- package/dist/{migrate-BUJHadA8.mjs → migrate-DrA_OwlZ.mjs} +45 -14
- package/dist/min-max-DrSRUaWQ.mjs +16 -0
- package/dist/{model-inventory-Dasber3S.mjs → model-inventory-BFVSNFT3.mjs} +13 -12
- package/dist/{models-add-Bt7aUHDs.mjs → models-add-C1QIc1DA.mjs} +39 -38
- package/dist/operator-BTqvTIc8.mjs +7 -0
- package/dist/{operator-DZxcP-g8.mjs → operator-CyuB3aVw.mjs} +3 -3
- package/dist/{pai-marker-kIYuNt3c.mjs → pai-marker-BcZyJguM.mjs} +7 -6
- package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-DKQt71s4.mjs} +7 -7
- package/dist/{paths-DoRuFc60.mjs → paths-CFHTloSe.mjs} +3 -3
- package/dist/{paths-DoIyRnhx.mjs → paths-CHifubDh.mjs} +3 -3
- package/dist/{planner-DLGOYlVb.mjs → planner-BmFACdLa.mjs} +31 -30
- package/dist/{pool-CZ5MZaXg.mjs → pool-Dh-q3iJi.mjs} +45 -6
- package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-BggU8GRp.mjs} +5 -5
- package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DQ6OzRy2.mjs} +6 -6
- package/dist/postgres-BdBNkL_l.mjs +7 -0
- package/dist/{postgres-CDnd_k5f.mjs → postgres-C6ti4haA.mjs} +55 -34
- package/dist/{project-setup-DM4npnL3.mjs → project-setup-jvfONJTQ.mjs} +131 -74
- package/dist/providers-BeDiVEb-.mjs +16 -0
- package/dist/{providers-UsKtLcUR.mjs → providers-CblTFrhz.mjs} +6 -6
- package/dist/{query-feedback-I-txSnld.mjs → query-feedback-BRfPtqki.mjs} +2 -2
- package/dist/registrable-BzH4TObg.mjs +9 -0
- package/dist/{registrable-LrRIVYQc.mjs → registrable-DpyTtajg.mjs} +4 -4
- package/dist/{render-BDbb4CF-.mjs → render-BYmQkuLd.mjs} +8 -8
- package/dist/render-CoMvwMPs.mjs +13 -0
- package/dist/{reranker-B4r4vykM.mjs → reranker-8g6xMwP7.mjs} +1 -1
- package/dist/{reranker-DxxMCtA0.mjs → reranker-CqCbpGnX.mjs} +1 -1
- package/dist/root-FtJsxYnm.mjs +36 -0
- package/dist/{root-BMeJuHOb.mjs → root-Xglu0OPD.mjs} +11 -11
- package/dist/route-table-B7zpoypi.mjs +15 -0
- package/dist/{router-L8Nu-OHF.mjs → router-CQSc3lk3.mjs} +17 -7
- package/dist/{router-D6Ec6Fid.mjs → router-DwHJRgG9.mjs} +1 -1
- package/dist/{routing-CUyiglfu.mjs → routing-BAAST9vT.mjs} +27 -308
- package/dist/{routing-ZuGlc0ge.mjs → routing-CENt4HH1.mjs} +13 -12
- package/dist/{run-ulAi5dFD.mjs → run-CSC7mgPf.mjs} +35 -33
- package/dist/{scan-B7I0gPe-.mjs → scan-C_Y5W9py.mjs} +6 -6
- package/dist/{local-candidate-jOjTNlOL.mjs → scheduler-CtUyWJ3O.mjs} +7 -109
- package/dist/scheduler-dmHXoRvm.mjs +3 -0
- package/dist/{scope-D5VSQxe4.mjs → scope-D-h6F6DN.mjs} +4 -4
- package/dist/{search-CPoE0830.mjs → search-RSyeQqj1.mjs} +18 -18
- package/dist/{session-CNXF6DLP.mjs → session-8iqQDkqb.mjs} +55 -31
- package/dist/session-BeqAbNPd.mjs +35 -0
- package/dist/session-scan-BJI_ZO8M.mjs +9 -0
- package/dist/{session-scan-BDLEc4w1.mjs → session-scan-W-gbwJHM.mjs} +6 -6
- package/dist/{sources-CjhUKKtO.mjs → sources-ESUkZaWv.mjs} +1 -1
- package/dist/{status-DYydKQUI.mjs → status-Dm4dYciN.mjs} +4 -4
- package/dist/{status-CvVi9cDf.mjs → status-ggJ7lgVg.mjs} +17 -9
- package/dist/{stop-words-SdSL1WKH.mjs → stop-words-DSEYQdNe.mjs} +1 -1
- package/dist/{test-isolation-CoDeOVXb.mjs → test-isolation-lqSmYxr0.mjs} +1 -1
- package/dist/{timeouts-OwEONmc8.mjs → timeouts-1MLooEBB.mjs} +1 -1
- package/dist/{tool-errors-AeRQuG3Y.mjs → tool-errors-7E13StWd.mjs} +4 -3
- package/dist/{tool-errors-R5E0fCgf.mjs → tool-errors-CJWODvlo.mjs} +6 -6
- package/dist/viewer-CqozWW39.mjs +22 -0
- package/dist/{viewer-DtWj5Vti.mjs → viewer-mDXUNHNy.mjs} +15 -15
- package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
- package/dist/{worker-config-BLqa1yiD.mjs → worker-config-CJhEMk19.mjs} +294 -92
- package/dist/worker-config-DESkIjMF.mjs +41 -0
- package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
- package/dist/{worktree-zmRf556h.mjs → worktree-CRs2gyg0.mjs} +37 -7
- package/dist/{zettelkasten-nigq7fqs.mjs → zettelkasten-B_rRRRvD.mjs} +5 -5
- package/dist/{zettelkasten-Cr_Xj0mG.mjs → zettelkasten-BzMm8REH.mjs} +29 -19
- package/docs/README.md +2 -0
- package/docs/cockpit.md +10 -4
- package/docs/commands/README.md +4 -4
- package/docs/commands/backup.md +2 -0
- package/docs/commands/install.md +29 -4
- package/docs/commands/providers.md +1 -1
- package/docs/commands/restore.md +4 -0
- package/docs/commands/setup.md +29 -4
- package/docs/commands/worker.md +1 -1
- package/docs/config.md +2 -0
- package/docs/features.md +299 -0
- package/docs/images/architecture.drawio +51 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/architecture.svg +3 -0
- package/docs/images/cockpit-01-system-tokens.jpg +0 -0
- package/docs/images/cockpit-02-totals-cache.jpg +0 -0
- package/docs/images/cockpit-03-local-servers-models-pool.jpg +0 -0
- package/docs/images/cockpit-04-workers-memory.jpg +0 -0
- package/docs/images/cockpit-05-performance.jpg +0 -0
- package/docs/images/cockpit-06-runs-speed-routing.jpg +0 -0
- package/docs/images/cockpit-07-settings-registry.jpg +0 -0
- package/docs/install/index.md +29 -0
- package/docs/three-tier-coding.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -5
- package/pai-plugin.json +1 -1
- package/plugins/context-preservation/plugin.json +1 -1
- package/plugins/core/plugin.json +1 -1
- package/plugins/core/skills/setup/SKILL.md +41 -9
- package/plugins/creative/plugin.json +1 -1
- package/plugins/observability/plugin.json +1 -1
- package/plugins/productivity/plugin.json +1 -1
- package/plugins/semantic-search/plugin.json +1 -1
- package/plugins/ui/plugin.json +1 -1
- package/plugins/zettelkasten/plugin.json +1 -1
- package/src/hooks/session-autosave.sh +8 -1
- package/src/hooks/session-stop.sh +8 -1
- package/src/hooks/ts/capture-all-events.ts +7 -12
- package/src/hooks/ts/lib/context-fill.ts +2 -1
- package/src/hooks/ts/lib/path-repair.ts +1 -1
- package/src/hooks/ts/lib/project-utils/notify.ts +1 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +23 -6
- package/src/hooks/ts/lib/raw-capture.ts +36 -0
- package/src/hooks/ts/lib/session-summary.ts +129 -0
- package/src/hooks/ts/lib/todo-sync.ts +120 -0
- package/src/hooks/ts/lib/worker-git-guard.ts +1 -1
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +6 -10
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +5 -105
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
- package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -14
- package/src/hooks/ts/session-end/capture-session-summary.ts +20 -128
- package/src/hooks/ts/session-start/load-core-context.ts +3 -3
- package/src/hooks/ts/session-start/load-project-context.ts +18 -22
- package/src/hooks/ts/stop/stop-hook.ts +13 -5
- package/src/hooks/ztk-auto.sh +7 -2
- package/dist/aibroker-client-JjE74jo0.mjs +0 -3
- package/dist/auto-route-BRk8NEFv.mjs +0 -11
- package/dist/budget-DuhQ2zzG.mjs +0 -14
- package/dist/cockpit-actions-DcQ-ucU8.mjs +0 -19
- package/dist/cockpit-memory-KV45PPO2.mjs +0 -19
- package/dist/daemon-C7yt_ptC.mjs +0 -44
- package/dist/detect-DRUkmHnb.mjs +0 -10
- package/dist/factory-DAGG9_n8.mjs +0 -9
- package/dist/helpers-DPJEHPq6.mjs +0 -10
- package/dist/kill-tree-LV0Q7Mte.mjs +0 -10
- package/dist/operator-CfL0HiBK.mjs +0 -7
- package/dist/postgres-DgqWyqhN.mjs +0 -7
- package/dist/providers-CTu2s1z2.mjs +0 -15
- package/dist/registrable-D9aNZOTh.mjs +0 -9
- package/dist/render-BSfVlYHj.mjs +0 -13
- package/dist/root-xI9DqVIe.mjs +0 -35
- package/dist/route-table-HjvnLVEU.mjs +0 -14
- package/dist/session-BJHzmUg2.mjs +0 -34
- package/dist/session-scan-YORKE1NO.mjs +0 -9
- package/dist/viewer-B8709jDN.mjs +0 -22
- package/dist/worker-config-D8tRuvo7.mjs +0 -40
- package/docs/images/cockpit-limits-routing.jpg +0 -0
- package/docs/images/cockpit-models.jpg +0 -0
- package/docs/images/cockpit-usage.jpg +0 -0
- package/docs/images/cockpit-workers-system.jpg +0 -0
- package/scripts/build-docs.mjs +0 -41
- package/scripts/build-hooks.mjs +0 -311
- package/scripts/build-skill-stubs.mjs +0 -216
- package/scripts/copy-defaults.mjs +0 -46
package/dist/cli/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
6
|
-
import { A as
|
|
7
|
-
import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-
|
|
8
|
-
import "../worktree-
|
|
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-
|
|
10
|
-
import { S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as
|
|
11
|
-
import { a as unregistrableReason, s as worktreeReason } from "../registrable-
|
|
12
|
-
import "../pai-marker-
|
|
13
|
-
import "../helpers-
|
|
14
|
-
import { t as applyRecencyBoost } from "../search-
|
|
15
|
-
import { t as dropItermBundleId } from "../env-
|
|
16
|
-
import { a as ownerSessionName, d as terminalHasName } from "../scope-
|
|
17
|
-
import {
|
|
18
|
-
import "../
|
|
19
|
-
import "../
|
|
20
|
-
import "../
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import "../
|
|
30
|
-
import "../
|
|
31
|
-
import {
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { a as
|
|
37
|
-
import { a as
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import { a as
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import "../
|
|
44
|
-
import {
|
|
45
|
-
import "../
|
|
46
|
-
import "../
|
|
47
|
-
import {
|
|
48
|
-
import "../
|
|
49
|
-
import
|
|
50
|
-
import {
|
|
51
|
-
import "../
|
|
52
|
-
import { g as
|
|
53
|
-
import {
|
|
54
|
-
import
|
|
55
|
-
import {
|
|
56
|
-
import { n as
|
|
57
|
-
import { a as
|
|
58
|
-
import "../
|
|
59
|
-
import {
|
|
60
|
-
import { n as
|
|
61
|
-
import
|
|
4
|
+
import { a as slugify$1, i as parseSessionFilename } from "../migrate-DrA_OwlZ.mjs";
|
|
5
|
+
import { I as LOOPBACK_HOST, Q as relocateWorkersYaml, _t as contextWindowFromModelId, c as WorkersConfigError, f as escalationCloud, ht as DEFAULT_CONTEXT_WINDOW, j as readWorkersSection, m as isLocalProvider, p as getProviderOrNative, t as ANTHROPIC_NATIVE, tt as workersYamlPath, v as nativeAnthropicModels, vt as stripModelVariant, wt as appendLedger } from "../config-XpZWOYOG.mjs";
|
|
6
|
+
import { A as assertPgName, C as migrateMainConfigToYaml, D as readJsonStrict, M as postgresConnectionString, N as postgresFullTarget, O as writeJsonAtomic, P as quoteIdent, S as MainConfigError, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, j as isContainerTarget, k as DEFAULT_PG_CONNECTION_TIMEOUT_MS, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath, w as resolvedMainConfigPath } from "../config-Bu1_vbY8.mjs";
|
|
7
|
+
import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-CHifubDh.mjs";
|
|
8
|
+
import "../worktree-CRs2gyg0.mjs";
|
|
9
|
+
import { B as loadStatusConfig, G as writeSectionToForge, L as formatStale, U as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, y as loadStatuses } from "../status-ggJ7lgVg.mjs";
|
|
10
|
+
import { A as readClaudeJson, N as writeClaudeJson, S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as registerMcpScript, k as mcpEntryRunsScript, v as readPoolState, w as expandMcpNames } from "../pool-Dh-q3iJi.mjs";
|
|
11
|
+
import { a as unregistrableReason, s as worktreeReason } from "../registrable-DpyTtajg.mjs";
|
|
12
|
+
import "../pai-marker-BcZyJguM.mjs";
|
|
13
|
+
import "../helpers-CRuBUas3.mjs";
|
|
14
|
+
import { t as applyRecencyBoost } from "../search-RSyeQqj1.mjs";
|
|
15
|
+
import { t as dropItermBundleId } from "../env-BE8mrsxr.mjs";
|
|
16
|
+
import { a as ownerSessionName, d as terminalHasName } from "../scope-D-h6F6DN.mjs";
|
|
17
|
+
import { An as saveSetup, Bn as aggregate, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Hn as appendUsage, In as parkConfigFrom, Jn as readUsage, Ln as setParkConfigSource, Nn as setupsPath, On as loadSaved, Rn as sharedParker, Tn as downLocalProviders, U as computeInventory, Un as formatSessionStats, Vn as aggregateBySession, W as currentInventory, Wn as formatStats, Wt as sessionStartTarget, Y as inventoryText, at as readInventoryInput, hn as localProviderForDelegation, m as readRoutingState, pt as isLoopbackProvider, qn as parseSince, sn as SESSION_NAME_ENV, un as readProfiles, wn as allSetups, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, yn as providerHostPort, zn as recordDuration } from "../routing-BAAST9vT.mjs";
|
|
18
|
+
import { n as minOf } from "../min-max-DrSRUaWQ.mjs";
|
|
19
|
+
import "../tool-errors-7E13StWd.mjs";
|
|
20
|
+
import "../wait-ByV0OyMZ.mjs";
|
|
21
|
+
import "../local-tiers-BE4jGaMa.mjs";
|
|
22
|
+
import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-yLFb7CKq.mjs";
|
|
23
|
+
import { d as setSchedConfigSource, f as sharedScheduler, i as mirrorTo, s as schedConfigFrom } from "../scheduler-CtUyWJ3O.mjs";
|
|
24
|
+
import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-DEveQdFV.mjs";
|
|
25
|
+
import "../local-instances-DOxSgT_L.mjs";
|
|
26
|
+
import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-DLdwQt0s.mjs";
|
|
27
|
+
import { n as TAILSCALE_STATUS_TIMEOUT_MS } from "../timeouts-1MLooEBB.mjs";
|
|
28
|
+
import { n as OPT, t as CliError } from "../define-CjJsmq-6.mjs";
|
|
29
|
+
import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-DjAAsqr_.mjs";
|
|
30
|
+
import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-C6ti4haA.mjs";
|
|
31
|
+
import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-6rZrWRXa.mjs";
|
|
32
|
+
import "../embeddings-BVq3esBF.mjs";
|
|
33
|
+
import "../router-CQSc3lk3.mjs";
|
|
34
|
+
import { t as detectProject } from "../detect-9CZNO0yf.mjs";
|
|
35
|
+
import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-BD6V-p9w.mjs";
|
|
36
|
+
import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-C_Y5W9py.mjs";
|
|
37
|
+
import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-8iqQDkqb.mjs";
|
|
38
|
+
import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-Jh5NVeNe.mjs";
|
|
39
|
+
import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-Dnym-qcT.mjs";
|
|
40
|
+
import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-CXVlZjHt.mjs";
|
|
41
|
+
import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-Xglu0OPD.mjs";
|
|
42
|
+
import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CQPGqLxL.mjs";
|
|
43
|
+
import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-W-gbwJHM.mjs";
|
|
44
|
+
import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-CSC7mgPf.mjs";
|
|
45
|
+
import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, jt as runPaneExec, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage, yt as psTable } from "../controls-CPoeudou.mjs";
|
|
46
|
+
import "../endreason-C577xIQz.mjs";
|
|
47
|
+
import { N as endBuildWait, j as beginBuildWait } from "../render-BYmQkuLd.mjs";
|
|
48
|
+
import "../operator-CyuB3aVw.mjs";
|
|
49
|
+
import "../kill-tree-EWYaEk5v.mjs";
|
|
50
|
+
import { s as poolReloadLive } from "../pool-cli-BggU8GRp.mjs";
|
|
51
|
+
import "../chain-7n9Gx56r.mjs";
|
|
52
|
+
import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-jvfONJTQ.mjs";
|
|
53
|
+
import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-DKQt71s4.mjs";
|
|
54
|
+
import "../viewer-mDXUNHNy.mjs";
|
|
55
|
+
import { g as setWorkersEnabled, s as describeProviders } from "../providers-CblTFrhz.mjs";
|
|
56
|
+
import { $ as exportDefaultLayout, B as mergeSettings, G as cachedRows, J as handleCockpit, K as cachedSessionMap, Q as warmCockpit, W as SystemMonitor, X as setEndpointHealth, Y as setActionHooks, Z as setRuntimeSampler, ct as providersCommand, et as createEndpointHealth, it as withWorkerAttribution, n as WORKER_CONFIG_COMMANDS, nt as loadWorkerWindows, q as cachedWorkerWindows, rt as sessionDisplayName, t as PROVIDER_EDIT_COMMANDS, tt as loadSessionMap, z as installWorkers } from "../worker-config-CJhEMk19.mjs";
|
|
57
|
+
import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-BEMpuhDz.mjs";
|
|
58
|
+
import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-BBHeyEZI.mjs";
|
|
59
|
+
import { n as PaiClient } from "../ipc-client-CaiGJnSM.mjs";
|
|
60
|
+
import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-DIWjA68P.mjs";
|
|
61
|
+
import "../fallback-DFlItw1Z.mjs";
|
|
62
|
+
import { At as readQuiet, Bt as appendCheckpointToNote, C as migrateSummaryCooldowns, Et as totalUsageTokens, Gt as restartRouter, Ht as findLatestNote, It as resolveDaemonPid, Nt as writeQuiet, Ot as describeQuiet, Pt as LAUNCHD_LABEL, Rt as readContextHandoverCache, St as loadSessionKeepaliveState, Tt as parseSessionUsage, Ut as findNotesDir$1, Vt as applyContinue, Wt as readBodyFile, bt as reapJob, ht as obsidianEnabled, i as reexecBackground, kt as nextOccurrence, mt as OBSIDIAN_DISABLED_MESSAGE, o as backfillKgFromNotes, s as migrateKgBackfillState, v as migrateWorkQueue, vt as migrateIdentityFile, wt as sessionKeepaliveLedgerSummary, yt as jobPaths } from "../indexer-backend-Cll7le4P.mjs";
|
|
63
|
+
import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-BRfPtqki.mjs";
|
|
64
|
+
import { appendFileSync, chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
65
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
62
66
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
63
67
|
import chalk from "chalk";
|
|
64
68
|
import { cpus, homedir, networkInterfaces, platform, userInfo } from "node:os";
|
|
65
69
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
66
70
|
import { fileURLToPath } from "node:url";
|
|
67
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
68
71
|
import http from "node:http";
|
|
72
|
+
import { createServer } from "node:net";
|
|
69
73
|
import pg from "pg";
|
|
70
74
|
import BetterSqlite3 from "better-sqlite3";
|
|
71
75
|
import { createInterface } from "node:readline";
|
|
72
|
-
import net, { createConnection } from "net";
|
|
76
|
+
import net$1, { createConnection as createConnection$1 } from "net";
|
|
73
77
|
import { promisify } from "node:util";
|
|
74
78
|
import { getEncoding } from "js-tiktoken";
|
|
75
79
|
|
|
@@ -897,7 +901,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
|
|
|
897
901
|
});
|
|
898
902
|
return {
|
|
899
903
|
inventory: async () => {
|
|
900
|
-
const { currentInventory } = await import("../model-inventory-
|
|
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-
|
|
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-
|
|
1279
|
-
const { readLocalConfig } = await import("../local-tiers-
|
|
1280
|
-
const { workersYamlPath } = await import("../workers-config-
|
|
1282
|
+
const { startTier, stopAll } = await import("../worker-config-DESkIjMF.mjs");
|
|
1283
|
+
const { readLocalConfig } = await import("../local-tiers-DNMI5W86.mjs");
|
|
1284
|
+
const { workersYamlPath } = await import("../workers-config-f8KrkY4L.mjs");
|
|
1281
1285
|
const cfg = readLocalConfig(workersYamlPath());
|
|
1282
1286
|
if (kind === "off") return void await stopAll(cfg);
|
|
1283
1287
|
const t = cfg.tiers[tier];
|
|
@@ -1653,7 +1657,7 @@ const modelsCommand = {
|
|
|
1653
1657
|
if (words[0] === "add") {
|
|
1654
1658
|
const source = words[1];
|
|
1655
1659
|
if (!source) throw new Error("usage: pai models add <hf-id|path> [--name n] [--download] [--try]");
|
|
1656
|
-
const { addModel } = await import("../models-add-
|
|
1660
|
+
const { addModel } = await import("../models-add-C1QIc1DA.mjs");
|
|
1657
1661
|
const r = await addModel({
|
|
1658
1662
|
source,
|
|
1659
1663
|
name: typeof ctx.opts.name === "string" ? ctx.opts.name : void 0,
|
|
@@ -1668,7 +1672,7 @@ const modelsCommand = {
|
|
|
1668
1672
|
if (op) {
|
|
1669
1673
|
const runtime = words[1];
|
|
1670
1674
|
if (!runtime) throw new Error(`usage: pai models ${op} <runtime> (configured: ${Object.keys(readRuntimeCommands()).join(", ") || "none"})`);
|
|
1671
|
-
const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-
|
|
1675
|
+
const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-DR9X514g.mjs");
|
|
1672
1676
|
const r = await runtimeStartStop(runtime, op, defaultDeps(), { model: typeof ctx.opts.model === "string" ? ctx.opts.model : void 0 });
|
|
1673
1677
|
process.stdout.write(r.message + "\n");
|
|
1674
1678
|
if (!r.ok) process.exitCode = 1;
|
|
@@ -2513,7 +2517,7 @@ function measureCompactionTrigger(cwd, projectsDir = CLAUDE_PROJECTS_DIR, config
|
|
|
2513
2517
|
if (discarded > 0) console.error(`[context-fill] discarded ${discarded} compaction sample(s) as other-regime for ${cwd} (preTokens below ${Math.round(floor)}, half of configured=${configuredTriggerTokens}): ` + samples.filter((s) => s.preTokens < floor).map((s) => `${s.preTokens}@${s.timestamp}`).join(", "));
|
|
2514
2518
|
if (admissible.length === 0) return null;
|
|
2515
2519
|
}
|
|
2516
|
-
const trigger =
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
4440
|
-
const { createStorageBackend } = await import("../factory-
|
|
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-
|
|
4458
|
-
const { createStorageBackend } = await import("../factory-
|
|
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-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
5913
|
+
console.log(dim$1(" Running pg_dump (this may take a moment)..."));
|
|
5603
5914
|
try {
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6018
|
-
|
|
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
|
-
|
|
6101
|
-
|
|
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
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
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: [
|
|
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({
|
|
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
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
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
|
-
|
|
6959
|
-
|
|
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
|
|
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
|
|
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
|
|
8045
|
+
const backupPath = timestampedBackupPath(skillFile);
|
|
7442
8046
|
writeFileSync(backupPath, content, "utf-8");
|
|
7443
8047
|
console.log(c.ok(`Backed up existing AI-STEERING-RULES.md to ${backupPath}`));
|
|
7444
8048
|
}
|
|
@@ -7489,7 +8093,7 @@ async function stepSkillStubs(rl) {
|
|
|
7489
8093
|
}
|
|
7490
8094
|
console.log(c.dim(` Found ${skillNames.length} skill stubs to install.`));
|
|
7491
8095
|
line();
|
|
7492
|
-
if (!await promptYesNo(rl, `Symlink ${skillNames.length} PAI skill stubs to ~/.claude/skills/?`, true)) {
|
|
8096
|
+
if (!await promptYesNo(rl, `Symlink ${skillNames.length} PAI skill stubs to ~/.claude/skills/?`, true, "skill-stubs")) {
|
|
7493
8097
|
console.log(c.dim(" Skipping skill stub installation."));
|
|
7494
8098
|
return false;
|
|
7495
8099
|
}
|
|
@@ -7549,7 +8153,7 @@ async function stepHooks(rl) {
|
|
|
7549
8153
|
if (useSymlinks$1) line(" Files are symlinked so they auto-update when PAI is rebuilt.");
|
|
7550
8154
|
else line(" Files are copied (Windows — re-run setup after PAI updates).");
|
|
7551
8155
|
line();
|
|
7552
|
-
if (!await promptYesNo(rl, "Install PAI lifecycle hooks (session stop, pre-compact, statusline)?", true)) {
|
|
8156
|
+
if (!await promptYesNo(rl, "Install PAI lifecycle hooks (session stop, pre-compact, statusline)?", true, "hooks")) {
|
|
7553
8157
|
console.log(c.dim(" Skipping hook installation."));
|
|
7554
8158
|
return false;
|
|
7555
8159
|
}
|
|
@@ -7610,7 +8214,7 @@ async function stepTsHooks(rl) {
|
|
|
7610
8214
|
if (useSymlinks) line(" Files are symlinked so they auto-update when PAI is rebuilt.");
|
|
7611
8215
|
else line(" Files are copied (Windows — re-run setup after PAI updates).");
|
|
7612
8216
|
line();
|
|
7613
|
-
if (!await promptYesNo(rl, "Install PAI TypeScript hooks to ~/.claude/Hooks/?", true)) {
|
|
8217
|
+
if (!await promptYesNo(rl, "Install PAI TypeScript hooks to ~/.claude/Hooks/?", true, "ts-hooks")) {
|
|
7614
8218
|
console.log(c.dim(" Skipping TypeScript hooks installation."));
|
|
7615
8219
|
return false;
|
|
7616
8220
|
}
|
|
@@ -7835,7 +8439,7 @@ async function stepDaName(rl) {
|
|
|
7835
8439
|
line(" Choose a name for your AI assistant. This name appears in tab titles");
|
|
7836
8440
|
line(" and session notes when hooks are active.");
|
|
7837
8441
|
line();
|
|
7838
|
-
const daName = await prompt(rl, chalk.bold(" Assistant name [PAI]: "), "") || "PAI";
|
|
8442
|
+
const daName = (setupOptions.daName ?? await prompt(rl, chalk.bold(" Assistant name [PAI]: "), "")) || "PAI";
|
|
7839
8443
|
line();
|
|
7840
8444
|
console.log(c.ok(`Assistant name set to: ${daName}`));
|
|
7841
8445
|
return daName;
|
|
@@ -7846,7 +8450,7 @@ async function stepSettings(rl, daName) {
|
|
|
7846
8450
|
line(" PAI will add env vars, all hook registrations, permissions, and flags");
|
|
7847
8451
|
line(" to ~/.claude/settings.json. Existing values are never overwritten.");
|
|
7848
8452
|
line();
|
|
7849
|
-
if (!await promptYesNo(rl, "Patch ~/.claude/settings.json with PAI hooks, env vars, and settings?", true)) {
|
|
8453
|
+
if (!await promptYesNo(rl, "Patch ~/.claude/settings.json with PAI hooks, env vars, and settings?", true, "settings")) {
|
|
7850
8454
|
console.log(c.dim(" Skipping settings patch."));
|
|
7851
8455
|
return false;
|
|
7852
8456
|
}
|
|
@@ -8255,24 +8859,17 @@ function cmdInstall() {
|
|
|
8255
8859
|
const config = readClaudeJson();
|
|
8256
8860
|
if (typeof config.mcpServers !== "object" || config.mcpServers === null) config.mcpServers = {};
|
|
8257
8861
|
const servers = config.mcpServers;
|
|
8258
|
-
const
|
|
8259
|
-
if (
|
|
8260
|
-
else {
|
|
8261
|
-
if (existing) {
|
|
8262
|
-
servers["pai-legacy"] = existing;
|
|
8263
|
-
console.log(dim$1(" Backed up existing 'pai' entry as 'pai-legacy'."));
|
|
8264
|
-
}
|
|
8265
|
-
servers["pai"] = {
|
|
8266
|
-
command: "node",
|
|
8267
|
-
args: [shimBin]
|
|
8268
|
-
};
|
|
8862
|
+
const outcome = registerMcpScript(servers, "pai", shimBin);
|
|
8863
|
+
if (outcome === "unchanged") console.log(ok$1(" ~/.claude.json already points to the shim."));
|
|
8864
|
+
else {
|
|
8269
8865
|
try {
|
|
8270
8866
|
writeClaudeJson(config);
|
|
8271
|
-
console.log(ok$1(" Updated ~/.claude.json to use daemon shim."));
|
|
8272
8867
|
} catch (e) {
|
|
8273
8868
|
console.error(err(` Failed to write ~/.claude.json: ${e}`));
|
|
8274
8869
|
return 1;
|
|
8275
8870
|
}
|
|
8871
|
+
if (outcome === "legacy-kept") console.log(dim$1(" Existing 'pai' entry runs a different server; kept as 'pai-legacy'."));
|
|
8872
|
+
console.log(ok$1(" Updated ~/.claude.json to use daemon shim."));
|
|
8276
8873
|
}
|
|
8277
8874
|
console.log();
|
|
8278
8875
|
if (!existsSync(daemonBin)) {
|
|
@@ -8432,8 +9029,8 @@ function cmdLogs(opts) {
|
|
|
8432
9029
|
return 0;
|
|
8433
9030
|
}
|
|
8434
9031
|
async function daemonServeCmd() {
|
|
8435
|
-
const { serve } = await import("../daemon-
|
|
8436
|
-
const { loadConfig: lc, ensureConfigDir } = await import("../config-
|
|
9032
|
+
const { serve } = await import("../daemon-DKdBNKfa.mjs");
|
|
9033
|
+
const { loadConfig: lc, ensureConfigDir } = await import("../config-DtENJ6cL.mjs");
|
|
8437
9034
|
ensureConfigDir();
|
|
8438
9035
|
await serve(lc());
|
|
8439
9036
|
return 0;
|
|
@@ -8591,11 +9188,11 @@ async function stepDaemon(rl) {
|
|
|
8591
9188
|
if (existsSync(plistPath)) {
|
|
8592
9189
|
console.log(c.dim(` PAI daemon ${unit} already installed.`));
|
|
8593
9190
|
line();
|
|
8594
|
-
if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false)) {
|
|
9191
|
+
if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false, "daemon")) {
|
|
8595
9192
|
console.log(c.dim(" Keeping existing daemon installation."));
|
|
8596
9193
|
return false;
|
|
8597
9194
|
}
|
|
8598
|
-
} else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true)) {
|
|
9195
|
+
} else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true, "daemon")) {
|
|
8599
9196
|
console.log(c.dim(" Skipping daemon install. Run manually: pai daemon install"));
|
|
8600
9197
|
return false;
|
|
8601
9198
|
}
|
|
@@ -8627,7 +9224,7 @@ async function stepMcp(rl) {
|
|
|
8627
9224
|
return false;
|
|
8628
9225
|
}
|
|
8629
9226
|
} catch {}
|
|
8630
|
-
if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true)) {
|
|
9227
|
+
if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true, "mcp")) {
|
|
8631
9228
|
console.log(c.dim(" Skipping MCP registration. Run manually: pai mcp install"));
|
|
8632
9229
|
return false;
|
|
8633
9230
|
}
|
|
@@ -8660,7 +9257,7 @@ async function stepDirectories(rl) {
|
|
|
8660
9257
|
for (const d of defaults) console.log(chalk.dim(` ${d}`));
|
|
8661
9258
|
line();
|
|
8662
9259
|
}
|
|
8663
|
-
const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false);
|
|
9260
|
+
const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false, "registry-scan");
|
|
8664
9261
|
if (runScan) {
|
|
8665
9262
|
line();
|
|
8666
9263
|
console.log(chalk.dim(" Registry scan will run after setup completes."));
|
|
@@ -8720,7 +9317,7 @@ async function stepInitialIndex(rl) {
|
|
|
8720
9317
|
//#endregion
|
|
8721
9318
|
//#region src/cli/commands/setup/steps/15-verify.ts
|
|
8722
9319
|
/** Step 18: Setup summary — displays all configuration choices made during setup. */
|
|
8723
|
-
function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform) {
|
|
9320
|
+
function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform, aibroker, failures = []) {
|
|
8724
9321
|
section("Setup Complete");
|
|
8725
9322
|
line();
|
|
8726
9323
|
console.log(chalk.green(" PAI Knowledge OS is configured!"));
|
|
@@ -8741,6 +9338,16 @@ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiStee
|
|
|
8741
9338
|
console.log(chalk.dim(" Settings: ") + chalk.cyan(settingsPatched ? "env vars, hooks, permissions, flags (patched)" : "(unchanged)"));
|
|
8742
9339
|
console.log(chalk.dim(" Daemon: ") + chalk.cyan(daemonInstalled ? `${daemonServiceName(plat)} (installed)` : "(not installed by setup)"));
|
|
8743
9340
|
console.log(chalk.dim(" MCP: ") + chalk.cyan(mcpRegistered ? "registered in ~/.claude.json" : "(unchanged)"));
|
|
9341
|
+
if (aibroker) {
|
|
9342
|
+
const label = {
|
|
9343
|
+
ok: "installed and healthy",
|
|
9344
|
+
installed: "installed",
|
|
9345
|
+
skipped: "skipped",
|
|
9346
|
+
failed: "FAILED — retry: npm install -g aibroker@^0.62.4 && aibroker setup && aibroker doctor"
|
|
9347
|
+
}[aibroker];
|
|
9348
|
+
console.log(chalk.dim(" AIBroker: ") + (aibroker === "failed" ? chalk.red(label) : chalk.cyan(label)));
|
|
9349
|
+
}
|
|
9350
|
+
for (const f of failures) console.log(chalk.red(` ${f}`));
|
|
8744
9351
|
line();
|
|
8745
9352
|
console.log(chalk.bold.yellow(" → RESTART Claude Code to activate all changes."));
|
|
8746
9353
|
line();
|
|
@@ -9395,7 +10002,7 @@ async function stepTaskBus(rl) {
|
|
|
9395
10002
|
line();
|
|
9396
10003
|
line(" PAI works fully without this. Skip it unless you want cross-session tasks.");
|
|
9397
10004
|
line();
|
|
9398
|
-
if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false)) {
|
|
10005
|
+
if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false, "task-bus")) {
|
|
9399
10006
|
line();
|
|
9400
10007
|
console.log(c.ok("Skipping the task bus."));
|
|
9401
10008
|
console.log(c.dim(" Add later by re-running `pai setup`."));
|
|
@@ -9482,7 +10089,7 @@ async function stepWorkers(rl) {
|
|
|
9482
10089
|
line(" `pai worker run` is the runner; ps / follow / replay watch the runs;");
|
|
9483
10090
|
line(" the Agent tool is denied and rewritten to it while workers are on.");
|
|
9484
10091
|
line();
|
|
9485
|
-
if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true)) {
|
|
10092
|
+
if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true, "workers")) {
|
|
9486
10093
|
line();
|
|
9487
10094
|
console.log(c.ok("Skipping workers."));
|
|
9488
10095
|
console.log(c.dim(" Later: pai worker on (other providers: pai worker providers add <name> … — see docs/worker.md)"));
|
|
@@ -9501,6 +10108,209 @@ async function stepWorkers(rl) {
|
|
|
9501
10108
|
return {};
|
|
9502
10109
|
}
|
|
9503
10110
|
|
|
10111
|
+
//#endregion
|
|
10112
|
+
//#region src/cli/commands/setup/steps/18-aibroker.ts
|
|
10113
|
+
/** Step 18: AIBroker, PAI's messaging layer (session messages, worker reports, tab names, WhatsApp/Telegram). */
|
|
10114
|
+
/** Longest any one AIBroker command may run before it counts as failed. */
|
|
10115
|
+
const COMMAND_TIMEOUT_MS = 5 * 6e4;
|
|
10116
|
+
/** `aibroker setup` may ask questions: give it the terminal when there is one and --yes is off. */
|
|
10117
|
+
const defaultRunner = (cmd, args) => {
|
|
10118
|
+
const r = spawnSync(cmd, args, {
|
|
10119
|
+
encoding: "utf8",
|
|
10120
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
10121
|
+
...cmd === "aibroker" && args[0] === "setup" && !setupOptions.yes && !!process.stdin.isTTY ? { stdio: "inherit" } : {}
|
|
10122
|
+
});
|
|
10123
|
+
return {
|
|
10124
|
+
status: r.error ? null : r.status,
|
|
10125
|
+
output: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? String(r.error.message) : ""}`.trim()
|
|
10126
|
+
};
|
|
10127
|
+
};
|
|
10128
|
+
/** The package the step installs; the range keeps a breaking major from arriving unannounced. */
|
|
10129
|
+
const AIBROKER_PACKAGE = "aibroker@^0.62.4";
|
|
10130
|
+
/** `aibroker` is not on PATH: spawn error (null) or the shell's "not found". */
|
|
10131
|
+
const isMissing = (r) => r.status === null || r.status === 127;
|
|
10132
|
+
/** The commands the step runs when AIBroker is missing or unhealthy, in order. */
|
|
10133
|
+
const AIBROKER_COMMANDS = [
|
|
10134
|
+
["npm", [
|
|
10135
|
+
"install",
|
|
10136
|
+
"-g",
|
|
10137
|
+
AIBROKER_PACKAGE
|
|
10138
|
+
]],
|
|
10139
|
+
["aibroker", ["setup"]],
|
|
10140
|
+
["aibroker", ["doctor"]]
|
|
10141
|
+
];
|
|
10142
|
+
const show = ([cmd, args]) => [cmd, ...args].join(" ");
|
|
10143
|
+
function aibrokerPlan() {
|
|
10144
|
+
return AIBROKER_COMMANDS.map(show);
|
|
10145
|
+
}
|
|
10146
|
+
function stepAibroker(run = defaultRunner, opts = {
|
|
10147
|
+
skip: setupOptions.noAibroker,
|
|
10148
|
+
dryRun: setupOptions.dryRun
|
|
10149
|
+
}) {
|
|
10150
|
+
section("Step 18: AIBroker (Messaging)");
|
|
10151
|
+
line();
|
|
10152
|
+
line(" AIBroker is PAI's messaging layer: session-to-session messages, worker reports,");
|
|
10153
|
+
line(" terminal tab names, WhatsApp/Telegram/PAILot. PAI works without it, minus messaging.");
|
|
10154
|
+
line();
|
|
10155
|
+
if (opts.skip) {
|
|
10156
|
+
console.log(c.dim(" Skipped (--no-aibroker). Install later: npm install -g aibroker@^0.62.4 && aibroker setup"));
|
|
10157
|
+
return "skipped";
|
|
10158
|
+
}
|
|
10159
|
+
if (opts.dryRun) {
|
|
10160
|
+
line(" Would run (if AIBroker is missing or `aibroker doctor` fails):");
|
|
10161
|
+
for (const p of aibrokerPlan()) line(` ${p}`);
|
|
10162
|
+
return "skipped";
|
|
10163
|
+
}
|
|
10164
|
+
const present = run("aibroker", ["doctor"]);
|
|
10165
|
+
if (present.status === 0) {
|
|
10166
|
+
console.log(c.ok("AIBroker already installed and healthy."));
|
|
10167
|
+
return "ok";
|
|
10168
|
+
}
|
|
10169
|
+
const steps = isMissing(present) ? AIBROKER_COMMANDS : AIBROKER_COMMANDS.slice(1);
|
|
10170
|
+
for (const step of steps) {
|
|
10171
|
+
const r = run(step[0], step[1]);
|
|
10172
|
+
if (r.status !== 0) {
|
|
10173
|
+
console.log(c.warn(` \`${show(step)}\` failed (exit ${r.status ?? "n/a"}).`));
|
|
10174
|
+
if (r.output) line(` ${r.output.split("\n").join("\n ")}`);
|
|
10175
|
+
console.log(c.warn(` Retry with: ${aibrokerPlan().slice(AIBROKER_COMMANDS.indexOf(step)).join(" && ")}`));
|
|
10176
|
+
return "failed";
|
|
10177
|
+
}
|
|
10178
|
+
console.log(c.ok(`${show(step)}: ok`));
|
|
10179
|
+
}
|
|
10180
|
+
console.log(c.ok("AIBroker installed."));
|
|
10181
|
+
return "installed";
|
|
10182
|
+
}
|
|
10183
|
+
|
|
10184
|
+
//#endregion
|
|
10185
|
+
//#region src/cli/commands/setup/steps/19-router.ts
|
|
10186
|
+
/** Step 19: the model router and its cockpit (enabled, port, reach). Answers go to config.yaml as router.enabled / router.port / router.bind. */
|
|
10187
|
+
/** Tries to listen on `host` (loopback by default); when taken, asks lsof who holds it. */
|
|
10188
|
+
const defaultPortProbe = (port, host = "127.0.0.1") => new Promise((resolve) => {
|
|
10189
|
+
const srv = createServer();
|
|
10190
|
+
srv.once("error", () => {
|
|
10191
|
+
resolve({
|
|
10192
|
+
free: false,
|
|
10193
|
+
holder: (spawnSync("lsof", [
|
|
10194
|
+
"-nP",
|
|
10195
|
+
`-iTCP:${port}`,
|
|
10196
|
+
"-sTCP:LISTEN"
|
|
10197
|
+
], { encoding: "utf8" }).stdout ?? "").split("\n")[1]?.trim().split(/\s+/).slice(0, 2).join(" pid ") || void 0
|
|
10198
|
+
});
|
|
10199
|
+
});
|
|
10200
|
+
srv.listen(port, host, () => srv.close(() => resolve({ free: true })));
|
|
10201
|
+
});
|
|
10202
|
+
function validPort(v) {
|
|
10203
|
+
const n = typeof v === "number" ? v : /^\d+$/.test(String(v).trim()) ? Number(v) : NaN;
|
|
10204
|
+
return Number.isInteger(n) && n >= 1024 && n <= 65535 ? n : null;
|
|
10205
|
+
}
|
|
10206
|
+
/** `loopback`, `tailscale` or a dotted IPv4 (never 0.0.0.0); returns the normalised value or null. */
|
|
10207
|
+
function validBind(v) {
|
|
10208
|
+
const b = v.trim().toLowerCase();
|
|
10209
|
+
return resolveBindAddress(b) === null && b !== "tailscale" ? null : b || "loopback";
|
|
10210
|
+
}
|
|
10211
|
+
async function stepRouter(rl, probe = defaultPortProbe, runningPid = routerRunningPid) {
|
|
10212
|
+
section("Step 19: Router and Cockpit (Optional)");
|
|
10213
|
+
line();
|
|
10214
|
+
line(" The router accounts model usage, schedules local models and serves the cockpit page.");
|
|
10215
|
+
line(" The daemon keeps it running when enabled.");
|
|
10216
|
+
line();
|
|
10217
|
+
const cfg = loadConfig().router;
|
|
10218
|
+
const raw = readConfigRaw().router ?? {};
|
|
10219
|
+
const flags = setupOptions.router ?? {};
|
|
10220
|
+
if (!(flags.enabled ?? await promptYesNo(rl, "Enable the router and cockpit?", typeof raw.enabled === "boolean" ? raw.enabled : true))) {
|
|
10221
|
+
console.log(c.ok("Router disabled. Enable later: pai config set router.enabled true"));
|
|
10222
|
+
return { router: {
|
|
10223
|
+
...raw,
|
|
10224
|
+
enabled: false
|
|
10225
|
+
} };
|
|
10226
|
+
}
|
|
10227
|
+
const routerUp = runningPid() !== null;
|
|
10228
|
+
const check = async (port, host) => {
|
|
10229
|
+
if (routerUp && port === cfg.port) return null;
|
|
10230
|
+
const p = await probe(port, host);
|
|
10231
|
+
return p.free ? null : `Port ${port} is in use${p.holder ? ` by ${p.holder}` : ""}.`;
|
|
10232
|
+
};
|
|
10233
|
+
const unavailable = (why) => {
|
|
10234
|
+
console.log(c.warn(` ${why}`));
|
|
10235
|
+
setupFailures.push(`Router not configured: ${why} Retry: pai setup --router-port <free port>`);
|
|
10236
|
+
return {};
|
|
10237
|
+
};
|
|
10238
|
+
let port;
|
|
10239
|
+
if (flags.port !== void 0) {
|
|
10240
|
+
const v = validPort(flags.port);
|
|
10241
|
+
if (v === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${flags.port}"`);
|
|
10242
|
+
const taken = await check(v);
|
|
10243
|
+
if (taken) return unavailable(taken);
|
|
10244
|
+
port = v;
|
|
10245
|
+
} else {
|
|
10246
|
+
port = cfg.port;
|
|
10247
|
+
for (;;) {
|
|
10248
|
+
const answer = await prompt(rl, chalk.bold(` Cockpit port [${cfg.port}]: `), "");
|
|
10249
|
+
const v = answer === "" ? cfg.port : validPort(answer);
|
|
10250
|
+
if (v === null) {
|
|
10251
|
+
console.log(c.warn("Enter a port from 1024 to 65535."));
|
|
10252
|
+
continue;
|
|
10253
|
+
}
|
|
10254
|
+
const taken = await check(v);
|
|
10255
|
+
if (!taken) {
|
|
10256
|
+
port = v;
|
|
10257
|
+
break;
|
|
10258
|
+
}
|
|
10259
|
+
if (setupOptions.yes) return unavailable(taken);
|
|
10260
|
+
console.log(c.warn(taken));
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
10263
|
+
let bind;
|
|
10264
|
+
if (flags.bind !== void 0) {
|
|
10265
|
+
const v = validBind(flags.bind);
|
|
10266
|
+
if (v === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${flags.bind}"`);
|
|
10267
|
+
bind = v;
|
|
10268
|
+
} else {
|
|
10269
|
+
const current = String(raw.bind ?? "loopback");
|
|
10270
|
+
const explicit = current !== "loopback" && current !== "tailscale" && validBind(current) !== null ? current : void 0;
|
|
10271
|
+
line();
|
|
10272
|
+
line(" Who can reach the cockpit? Beyond loopback it is reachable from other devices (behind a token).");
|
|
10273
|
+
line();
|
|
10274
|
+
const idx = await promptMenu(rl, [
|
|
10275
|
+
{
|
|
10276
|
+
label: "loopback",
|
|
10277
|
+
description: "This machine only (127.0.0.1)."
|
|
10278
|
+
},
|
|
10279
|
+
{
|
|
10280
|
+
label: "tailscale",
|
|
10281
|
+
description: "Your other Tailscale devices."
|
|
10282
|
+
},
|
|
10283
|
+
{
|
|
10284
|
+
label: "explicit address",
|
|
10285
|
+
description: "A specific IPv4 address of this machine."
|
|
10286
|
+
}
|
|
10287
|
+
], current === "tailscale" ? 1 : explicit ? 2 : 0);
|
|
10288
|
+
bind = idx === 0 ? "loopback" : "tailscale";
|
|
10289
|
+
if (idx === 2) for (;;) {
|
|
10290
|
+
const a = validBind(await prompt(rl, chalk.bold(` IPv4 address${explicit ? ` [${explicit}]` : ""}: `), explicit ?? ""));
|
|
10291
|
+
if (a && a !== "tailscale") {
|
|
10292
|
+
bind = a;
|
|
10293
|
+
break;
|
|
10294
|
+
}
|
|
10295
|
+
console.log(c.warn("Enter a dotted IPv4 address (not 0.0.0.0)."));
|
|
10296
|
+
}
|
|
10297
|
+
}
|
|
10298
|
+
const host = bind === "loopback" ? void 0 : resolveBindAddress(bind) ?? void 0;
|
|
10299
|
+
if (host && !(routerUp && port === cfg.port)) {
|
|
10300
|
+
const taken = await check(port, host);
|
|
10301
|
+
if (taken) return unavailable(`${taken.slice(0, -1)} on ${host}.`);
|
|
10302
|
+
}
|
|
10303
|
+
if (bind !== "loopback") console.log(c.warn(` The cockpit will be reachable from other devices via ${bind}; it asks for the token in ~/.pai/router-cockpit.token.`));
|
|
10304
|
+
if (bind === "tailscale" && resolveBindAddress("tailscale") === null) console.log(c.warn(" Tailscale is not up here; the extra listener starts once it is."));
|
|
10305
|
+
console.log(c.ok(`Router enabled on port ${port}, reach: ${bind}.`));
|
|
10306
|
+
return { router: {
|
|
10307
|
+
...raw,
|
|
10308
|
+
enabled: true,
|
|
10309
|
+
port,
|
|
10310
|
+
bind
|
|
10311
|
+
} };
|
|
10312
|
+
}
|
|
10313
|
+
|
|
9504
10314
|
//#endregion
|
|
9505
10315
|
//#region src/cli/commands/setup/index.ts
|
|
9506
10316
|
/**
|
|
@@ -9516,6 +10326,11 @@ async function step(name, fn) {
|
|
|
9516
10326
|
}
|
|
9517
10327
|
}
|
|
9518
10328
|
async function runSetup() {
|
|
10329
|
+
setupFailures.length = 0;
|
|
10330
|
+
if (setupOptions.dryRun) {
|
|
10331
|
+
stepAibroker();
|
|
10332
|
+
return;
|
|
10333
|
+
}
|
|
9519
10334
|
const rl = createRl();
|
|
9520
10335
|
try {
|
|
9521
10336
|
if (existsSync(CONFIG_FILE$2)) {
|
|
@@ -9546,6 +10361,8 @@ async function runSetup() {
|
|
|
9546
10361
|
const daName = await step("stepDaName", () => stepDaName(rl));
|
|
9547
10362
|
const settingsPatched = await step("stepSettings", () => stepSettings(rl, daName));
|
|
9548
10363
|
const daemonInstalled = await step("stepDaemon", () => stepDaemon(rl));
|
|
10364
|
+
const routerConfig = await step("stepRouter", () => stepRouter(rl));
|
|
10365
|
+
const aibroker = await step("stepAibroker", () => stepAibroker());
|
|
9549
10366
|
const mcpRegistered = await step("stepMcp", () => stepMcp(rl));
|
|
9550
10367
|
await step("stepDirectories", () => stepDirectories(rl));
|
|
9551
10368
|
const taskConfig = await step("stepTaskBus", () => stepTaskBus(rl));
|
|
@@ -9554,50 +10371,338 @@ async function runSetup() {
|
|
|
9554
10371
|
...storageConfig,
|
|
9555
10372
|
...embeddingConfig,
|
|
9556
10373
|
...taskConfig,
|
|
9557
|
-
...workersConfig
|
|
10374
|
+
...workersConfig,
|
|
10375
|
+
...routerConfig
|
|
9558
10376
|
};
|
|
9559
10377
|
mergeConfig(allUpdates);
|
|
9560
10378
|
line();
|
|
9561
10379
|
console.log(chalk.green(" Configuration saved."));
|
|
9562
10380
|
await step("stepInitialIndex", () => stepInitialIndex(rl));
|
|
9563
|
-
stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered);
|
|
10381
|
+
stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, process.platform, aibroker, setupFailures);
|
|
9564
10382
|
} finally {
|
|
9565
10383
|
rl.close();
|
|
9566
10384
|
}
|
|
9567
10385
|
}
|
|
9568
10386
|
|
|
10387
|
+
//#endregion
|
|
10388
|
+
//#region src/cli/commands/setup/questions.ts
|
|
10389
|
+
readConfigRaw();
|
|
10390
|
+
/** yes/no components: key = setupOptions.answers key, flag form depends on the default. */
|
|
10391
|
+
const COMPONENTS = [
|
|
10392
|
+
{
|
|
10393
|
+
key: "claude-md",
|
|
10394
|
+
step: "Agent configuration",
|
|
10395
|
+
question: "Write ~/.claude/CLAUDE.md from the PAI template?",
|
|
10396
|
+
default: true
|
|
10397
|
+
},
|
|
10398
|
+
{
|
|
10399
|
+
key: "skill",
|
|
10400
|
+
step: "PAI skill",
|
|
10401
|
+
question: "Install the PAI skill?",
|
|
10402
|
+
default: true
|
|
10403
|
+
},
|
|
10404
|
+
{
|
|
10405
|
+
key: "steering-rules",
|
|
10406
|
+
step: "Steering rules",
|
|
10407
|
+
question: "Install the AI steering rules?",
|
|
10408
|
+
default: true
|
|
10409
|
+
},
|
|
10410
|
+
{
|
|
10411
|
+
key: "skill-stubs",
|
|
10412
|
+
step: "Skill stubs",
|
|
10413
|
+
question: "Symlink the PAI skill stubs into ~/.claude/skills/?",
|
|
10414
|
+
default: true
|
|
10415
|
+
},
|
|
10416
|
+
{
|
|
10417
|
+
key: "hooks",
|
|
10418
|
+
step: "Hooks",
|
|
10419
|
+
question: "Install the lifecycle hooks (session stop, pre-compact, statusline)?",
|
|
10420
|
+
default: true
|
|
10421
|
+
},
|
|
10422
|
+
{
|
|
10423
|
+
key: "ts-hooks",
|
|
10424
|
+
step: "TypeScript hooks",
|
|
10425
|
+
question: "Install the TypeScript hooks to ~/.claude/Hooks/?",
|
|
10426
|
+
default: true
|
|
10427
|
+
},
|
|
10428
|
+
{
|
|
10429
|
+
key: "settings",
|
|
10430
|
+
step: "Settings",
|
|
10431
|
+
question: "Patch ~/.claude/settings.json with PAI hooks, env vars and settings?",
|
|
10432
|
+
default: true
|
|
10433
|
+
},
|
|
10434
|
+
{
|
|
10435
|
+
key: "daemon",
|
|
10436
|
+
step: "Daemon",
|
|
10437
|
+
question: "Install the daemon as a login service?",
|
|
10438
|
+
default: true
|
|
10439
|
+
},
|
|
10440
|
+
{
|
|
10441
|
+
key: "mcp",
|
|
10442
|
+
step: "MCP",
|
|
10443
|
+
question: "Register the PAI MCP server in ~/.claude.json?",
|
|
10444
|
+
default: true
|
|
10445
|
+
},
|
|
10446
|
+
{
|
|
10447
|
+
key: "registry-scan",
|
|
10448
|
+
step: "Directories",
|
|
10449
|
+
question: "Run `pai registry scan` after setup?",
|
|
10450
|
+
default: false
|
|
10451
|
+
},
|
|
10452
|
+
{
|
|
10453
|
+
key: "task-bus",
|
|
10454
|
+
step: "Task bus",
|
|
10455
|
+
question: "Enable the task bus (needs a Todoist account)?",
|
|
10456
|
+
default: false
|
|
10457
|
+
},
|
|
10458
|
+
{
|
|
10459
|
+
key: "workers",
|
|
10460
|
+
step: "Workers",
|
|
10461
|
+
question: "Turn workers on with the built-in anthropic provider?",
|
|
10462
|
+
default: true
|
|
10463
|
+
}
|
|
10464
|
+
];
|
|
10465
|
+
/** Components whose answer is not a plain default. */
|
|
10466
|
+
const COMPONENT_NOTES = {
|
|
10467
|
+
"registry-scan": "Off by default: scanning walks your project directories.",
|
|
10468
|
+
"task-bus": "Off by default. Needs a Todoist token in the config; with --yes the token cannot be asked, so set it first."
|
|
10469
|
+
};
|
|
10470
|
+
const componentFlag = (key, def) => def ? `--no-${key}` : `--${key}`;
|
|
10471
|
+
function setupQuestions() {
|
|
10472
|
+
const raw = readConfigRaw();
|
|
10473
|
+
const router = loadConfig().router;
|
|
10474
|
+
const rawRouter = raw.router ?? {};
|
|
10475
|
+
const q = [];
|
|
10476
|
+
const cur = (v) => v === void 0 || v === null || v === "" ? {} : { current: v };
|
|
10477
|
+
q.push({
|
|
10478
|
+
id: "storage",
|
|
10479
|
+
step: "Storage",
|
|
10480
|
+
question: "Where should PAI store its data?",
|
|
10481
|
+
type: "choice",
|
|
10482
|
+
flag: "--storage",
|
|
10483
|
+
default: "sqlite",
|
|
10484
|
+
allowed: ["sqlite", "postgres"],
|
|
10485
|
+
...cur(raw.storageBackend),
|
|
10486
|
+
note: "With --yes and no --storage: postgres when a local PostgreSQL answers, else sqlite."
|
|
10487
|
+
});
|
|
10488
|
+
q.push({
|
|
10489
|
+
id: "pg-url",
|
|
10490
|
+
step: "Storage",
|
|
10491
|
+
question: "PostgreSQL connection string (only when storage is postgres; default: the PAI Docker container)",
|
|
10492
|
+
type: "string",
|
|
10493
|
+
flag: "--pg-url",
|
|
10494
|
+
default: null,
|
|
10495
|
+
...cur((raw.postgres ?? {}).connectionString)
|
|
10496
|
+
});
|
|
10497
|
+
q.push({
|
|
10498
|
+
id: "embedding",
|
|
10499
|
+
step: "Embedding",
|
|
10500
|
+
question: "Which embedding model for semantic search?",
|
|
10501
|
+
type: "choice",
|
|
10502
|
+
flag: "--embedding",
|
|
10503
|
+
default: "snowflake",
|
|
10504
|
+
allowed: [
|
|
10505
|
+
"snowflake",
|
|
10506
|
+
"bge",
|
|
10507
|
+
"nomic",
|
|
10508
|
+
"ollama",
|
|
10509
|
+
"none"
|
|
10510
|
+
],
|
|
10511
|
+
...cur(raw.embeddingModel)
|
|
10512
|
+
});
|
|
10513
|
+
const component = (comp) => {
|
|
10514
|
+
const note = COMPONENT_NOTES[comp.key];
|
|
10515
|
+
q.push({
|
|
10516
|
+
id: comp.key,
|
|
10517
|
+
step: comp.step,
|
|
10518
|
+
question: comp.question,
|
|
10519
|
+
type: "bool",
|
|
10520
|
+
flag: componentFlag(comp.key, comp.default),
|
|
10521
|
+
default: comp.default,
|
|
10522
|
+
...note ? { note } : {}
|
|
10523
|
+
});
|
|
10524
|
+
};
|
|
10525
|
+
const routerQuestions = () => {
|
|
10526
|
+
q.push({
|
|
10527
|
+
id: "router",
|
|
10528
|
+
step: "Router and cockpit",
|
|
10529
|
+
question: "Enable the router and cockpit?",
|
|
10530
|
+
type: "bool",
|
|
10531
|
+
flag: "--router / --no-router",
|
|
10532
|
+
default: true,
|
|
10533
|
+
...cur(typeof rawRouter.enabled === "boolean" ? rawRouter.enabled : void 0),
|
|
10534
|
+
note: "Setup defaults to on; the daemon's own default (key absent) is off. A re-run with router.enabled absent turns it on unless --no-router is given."
|
|
10535
|
+
});
|
|
10536
|
+
q.push({
|
|
10537
|
+
id: "router-port",
|
|
10538
|
+
step: "Router and cockpit",
|
|
10539
|
+
question: "Port for the cockpit?",
|
|
10540
|
+
type: "port",
|
|
10541
|
+
flag: "--router-port",
|
|
10542
|
+
default: router.port,
|
|
10543
|
+
...cur(rawRouter.port)
|
|
10544
|
+
});
|
|
10545
|
+
q.push({
|
|
10546
|
+
id: "router-bind",
|
|
10547
|
+
step: "Router and cockpit",
|
|
10548
|
+
question: "Who can reach the cockpit? Beyond loopback it is reachable from other devices.",
|
|
10549
|
+
type: "choice",
|
|
10550
|
+
flag: "--router-bind",
|
|
10551
|
+
default: "loopback",
|
|
10552
|
+
allowed: [
|
|
10553
|
+
"loopback",
|
|
10554
|
+
"tailscale",
|
|
10555
|
+
"<ipv4 address>"
|
|
10556
|
+
],
|
|
10557
|
+
...cur(rawRouter.bind)
|
|
10558
|
+
});
|
|
10559
|
+
};
|
|
10560
|
+
const daName = () => q.push({
|
|
10561
|
+
id: "da-name",
|
|
10562
|
+
step: "Settings",
|
|
10563
|
+
question: "What should your assistant be called?",
|
|
10564
|
+
type: "string",
|
|
10565
|
+
flag: "--da-name",
|
|
10566
|
+
default: "PAI"
|
|
10567
|
+
});
|
|
10568
|
+
const aibroker = () => q.push({
|
|
10569
|
+
id: "aibroker",
|
|
10570
|
+
step: "AIBroker",
|
|
10571
|
+
question: "Install AIBroker (messaging)?",
|
|
10572
|
+
type: "bool",
|
|
10573
|
+
flag: "--no-aibroker",
|
|
10574
|
+
default: true
|
|
10575
|
+
});
|
|
10576
|
+
for (const comp of COMPONENTS) {
|
|
10577
|
+
if (comp.key === "settings") daName();
|
|
10578
|
+
component(comp);
|
|
10579
|
+
if (comp.key === "daemon") {
|
|
10580
|
+
routerQuestions();
|
|
10581
|
+
aibroker();
|
|
10582
|
+
}
|
|
10583
|
+
}
|
|
10584
|
+
return q;
|
|
10585
|
+
}
|
|
10586
|
+
function renderQuestions(qs) {
|
|
10587
|
+
return qs.map((x, i) => {
|
|
10588
|
+
const parts = [`${i + 1}. [${x.step}] ${x.question}`, ` flag: ${x.flag} default: ${x.default ?? "(none)"}`];
|
|
10589
|
+
if (x.allowed) parts.push(` allowed: ${x.allowed.join(" | ")}`);
|
|
10590
|
+
if (x.current !== void 0) parts.push(` current: ${x.current} (already set)`);
|
|
10591
|
+
if (x.note) parts.push(` note: ${x.note}`);
|
|
10592
|
+
return parts.join("\n");
|
|
10593
|
+
}).join("\n");
|
|
10594
|
+
}
|
|
10595
|
+
|
|
9569
10596
|
//#endregion
|
|
9570
10597
|
//#region src/cli/commands/setup.ts
|
|
9571
10598
|
/** `pai setup`: the first-time setup wizard (steps live in ./setup/). */
|
|
10599
|
+
/** One flag per yes/no component: --no-<name> for default-yes ones, --<name> for default-no ones. */
|
|
10600
|
+
const componentOptions = COMPONENTS.map((comp) => ({
|
|
10601
|
+
name: componentFlag(comp.key, comp.default).slice(2),
|
|
10602
|
+
type: "boolean",
|
|
10603
|
+
default: false,
|
|
10604
|
+
summary: `${comp.default ? "Skip" : "Do"}: ${comp.question}`
|
|
10605
|
+
}));
|
|
9572
10606
|
const setupCommand = {
|
|
9573
10607
|
name: "setup",
|
|
9574
|
-
summary: "First-time setup: storage, search, hooks, daemon",
|
|
9575
|
-
description: "Walks through everything PAI needs: where data is stored, search embeddings, Claude Code hooks and settings, the background daemon and the first index.\nSafe to run again. With --yes it takes every default and asks nothing.",
|
|
9576
|
-
options: [
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
10608
|
+
summary: "First-time setup: storage, search, hooks, daemon, router",
|
|
10609
|
+
description: "Walks through everything PAI needs: where data is stored, search embeddings, Claude Code hooks and settings, the background daemon, the router and cockpit (port, reach), AIBroker (messaging; installed automatically) and the first index.\nSafe to run again. With --yes it takes every default and asks nothing; every question has a flag, so `pai setup --yes <flags>` can express any answer. `pai setup --questions [--json]` lists the questions with their flags, defaults and current values (for callers without a terminal).",
|
|
10610
|
+
options: [
|
|
10611
|
+
{
|
|
10612
|
+
name: "yes",
|
|
10613
|
+
short: "y",
|
|
10614
|
+
type: "boolean",
|
|
10615
|
+
default: false,
|
|
10616
|
+
summary: "Take every default (or the flag given) and read no input"
|
|
10617
|
+
},
|
|
10618
|
+
{
|
|
10619
|
+
name: "questions",
|
|
10620
|
+
type: "boolean",
|
|
10621
|
+
default: false,
|
|
10622
|
+
summary: "Print every question with its flag, default and allowed values; change nothing"
|
|
10623
|
+
},
|
|
10624
|
+
{
|
|
10625
|
+
name: "json",
|
|
10626
|
+
type: "boolean",
|
|
10627
|
+
default: false,
|
|
10628
|
+
summary: "With --questions: machine-readable JSON"
|
|
10629
|
+
},
|
|
10630
|
+
{
|
|
10631
|
+
name: "storage",
|
|
10632
|
+
type: "string",
|
|
10633
|
+
value: "BACKEND",
|
|
10634
|
+
summary: "Where to store data: sqlite or postgres (with --yes: sqlite unless a local Postgres answers)"
|
|
10635
|
+
},
|
|
10636
|
+
{
|
|
10637
|
+
name: "pg-url",
|
|
10638
|
+
type: "string",
|
|
10639
|
+
value: "URL",
|
|
10640
|
+
summary: "PostgreSQL connection string (implies --storage postgres)"
|
|
10641
|
+
},
|
|
10642
|
+
{
|
|
10643
|
+
name: "embedding",
|
|
10644
|
+
type: "string",
|
|
10645
|
+
value: "MODEL",
|
|
10646
|
+
summary: "Embeddings: snowflake, bge, nomic, ollama (Arctic on Ollama) or none"
|
|
10647
|
+
},
|
|
10648
|
+
{
|
|
10649
|
+
name: "da-name",
|
|
10650
|
+
type: "string",
|
|
10651
|
+
value: "NAME",
|
|
10652
|
+
summary: "What the assistant is called (default PAI)"
|
|
10653
|
+
},
|
|
10654
|
+
{
|
|
10655
|
+
name: "router",
|
|
10656
|
+
type: "boolean",
|
|
10657
|
+
summary: "Enable the router and cockpit (default)"
|
|
10658
|
+
},
|
|
10659
|
+
{
|
|
10660
|
+
name: "no-router",
|
|
10661
|
+
type: "boolean",
|
|
10662
|
+
summary: "Do not enable the router and cockpit"
|
|
10663
|
+
},
|
|
10664
|
+
{
|
|
10665
|
+
name: "router-port",
|
|
10666
|
+
type: "number",
|
|
10667
|
+
value: "PORT",
|
|
10668
|
+
summary: "Cockpit port (default: config router.port, 8790)"
|
|
10669
|
+
},
|
|
10670
|
+
{
|
|
10671
|
+
name: "router-bind",
|
|
10672
|
+
type: "string",
|
|
10673
|
+
value: "SCOPE",
|
|
10674
|
+
summary: "Who reaches the cockpit: loopback (default), tailscale or an IPv4 address"
|
|
10675
|
+
},
|
|
10676
|
+
{
|
|
10677
|
+
name: "no-aibroker",
|
|
10678
|
+
type: "boolean",
|
|
10679
|
+
default: false,
|
|
10680
|
+
summary: "Do not install AIBroker (the messaging layer)"
|
|
10681
|
+
},
|
|
10682
|
+
...componentOptions,
|
|
10683
|
+
{
|
|
10684
|
+
name: "dry-run",
|
|
10685
|
+
type: "boolean",
|
|
10686
|
+
default: false,
|
|
10687
|
+
summary: "Validate the flags, print the AIBroker install commands and change nothing (other steps are not previewed)"
|
|
10688
|
+
}
|
|
10689
|
+
],
|
|
10690
|
+
examples: [
|
|
10691
|
+
"pai setup",
|
|
10692
|
+
"pai setup --yes --storage sqlite",
|
|
10693
|
+
"pai setup --questions --json",
|
|
10694
|
+
"pai setup --yes --storage sqlite --embedding none --router-port 9000 --router-bind tailscale --no-hooks"
|
|
10695
|
+
],
|
|
9589
10696
|
seeAlso: ["pai update", "pai daemon status"],
|
|
9590
10697
|
run(c) {
|
|
9591
|
-
const storage = c.opts.storage;
|
|
9592
10698
|
return legacy(async () => {
|
|
9593
|
-
if (
|
|
9594
|
-
|
|
9595
|
-
|
|
10699
|
+
if (c.opts.questions === true) {
|
|
10700
|
+
const qs = setupQuestions();
|
|
10701
|
+
console.log(c.opts.json === true ? JSON.stringify(qs, null, 2) : renderQuestions(qs));
|
|
9596
10702
|
return;
|
|
9597
10703
|
}
|
|
9598
|
-
setupOptions.yes = c.opts.yes === true;
|
|
9599
|
-
setupOptions.storage = storage;
|
|
9600
10704
|
try {
|
|
10705
|
+
applySetupFlags(c.opts);
|
|
9601
10706
|
await runSetup();
|
|
9602
10707
|
} catch (e) {
|
|
9603
10708
|
console.error(chalk.red(`pai setup: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -9606,6 +10711,39 @@ const setupCommand = {
|
|
|
9606
10711
|
});
|
|
9607
10712
|
}
|
|
9608
10713
|
};
|
|
10714
|
+
/** Validates the flags and copies them into setupOptions (throws naming the flag). */
|
|
10715
|
+
function applySetupFlags(opts) {
|
|
10716
|
+
const storage = opts.storage;
|
|
10717
|
+
if (storage && storage !== "sqlite" && storage !== "postgres") throw new Error(`--storage must be sqlite or postgres, got "${storage}"`);
|
|
10718
|
+
const embedding = opts.embedding;
|
|
10719
|
+
if (embedding && ![
|
|
10720
|
+
"snowflake",
|
|
10721
|
+
"bge",
|
|
10722
|
+
"nomic",
|
|
10723
|
+
"ollama",
|
|
10724
|
+
"none"
|
|
10725
|
+
].includes(embedding)) throw new Error(`--embedding must be snowflake, bge, nomic, ollama or none, got "${embedding}"`);
|
|
10726
|
+
const port = opts["router-port"];
|
|
10727
|
+
if (port !== void 0 && validPort(port) === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${port}"`);
|
|
10728
|
+
const bind = opts["router-bind"];
|
|
10729
|
+
if (bind !== void 0 && validBind(String(bind)) === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${bind}"`);
|
|
10730
|
+
if (opts.router === true && opts["no-router"] === true) throw new Error("--router and --no-router contradict each other");
|
|
10731
|
+
const answers = {};
|
|
10732
|
+
for (const comp of COMPONENTS) if (opts[componentFlag(comp.key, comp.default).slice(2)] === true) answers[comp.key] = !comp.default;
|
|
10733
|
+
setupOptions.yes = opts.yes === true;
|
|
10734
|
+
setupOptions.noAibroker = opts["no-aibroker"] === true;
|
|
10735
|
+
setupOptions.dryRun = opts["dry-run"] === true;
|
|
10736
|
+
setupOptions.storage = storage;
|
|
10737
|
+
setupOptions.pgUrl = opts["pg-url"];
|
|
10738
|
+
setupOptions.embedding = embedding;
|
|
10739
|
+
setupOptions.daName = opts["da-name"];
|
|
10740
|
+
setupOptions.router = {
|
|
10741
|
+
enabled: opts["no-router"] === true ? false : opts.router === true ? true : void 0,
|
|
10742
|
+
port: opts["router-port"],
|
|
10743
|
+
bind: opts["router-bind"]
|
|
10744
|
+
};
|
|
10745
|
+
setupOptions.answers = answers;
|
|
10746
|
+
}
|
|
9609
10747
|
const installAlias = {
|
|
9610
10748
|
...setupCommand,
|
|
9611
10749
|
name: "install",
|
|
@@ -10399,29 +11537,46 @@ function migrateFederationDbOrphan(dryRun) {
|
|
|
10399
11537
|
}
|
|
10400
11538
|
return migrateOrphanFederationDb({ dryRun });
|
|
10401
11539
|
}
|
|
10402
|
-
|
|
10403
|
-
|
|
11540
|
+
/**
|
|
11541
|
+
* Flush the old DB's WAL into its main file (before the byte copy, so the copy
|
|
11542
|
+
* holds every committed row) and set the emptied sidecars aside. A non-empty
|
|
11543
|
+
* WAL is never moved away without a successful checkpoint.
|
|
11544
|
+
*/
|
|
11545
|
+
function checkpointWal(oldPath, label) {
|
|
11546
|
+
let checkpointed = false;
|
|
10404
11547
|
try {
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
11548
|
+
const db = new BetterSqlite3(oldPath, { timeout: 500 });
|
|
11549
|
+
try {
|
|
11550
|
+
const [row] = db.pragma("wal_checkpoint(TRUNCATE)");
|
|
11551
|
+
checkpointed = row?.busy === 0;
|
|
11552
|
+
} finally {
|
|
11553
|
+
db.close();
|
|
11554
|
+
}
|
|
10411
11555
|
} catch {}
|
|
10412
11556
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
10413
11557
|
for (const suffix of ["-wal", "-shm"]) {
|
|
10414
11558
|
const sidecar = `${oldPath}${suffix}`;
|
|
10415
|
-
if (existsSync(sidecar))
|
|
11559
|
+
if (!existsSync(sidecar)) continue;
|
|
11560
|
+
if (!checkpointed && statSync(sidecar).size > 0) throw new PaiFileMigrationError(`${label}: could not checkpoint ${sidecar} (non-empty WAL, committed rows may live only there) — left in place`);
|
|
11561
|
+
try {
|
|
10416
11562
|
renameSync(sidecar, `${sidecar}.migrated-${stamp}`);
|
|
10417
11563
|
} catch {}
|
|
10418
11564
|
}
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
11565
|
+
}
|
|
11566
|
+
/** PRAGMA integrity_check on the migrated copy, through the node driver. */
|
|
11567
|
+
function verifyIntegrity(newPath) {
|
|
11568
|
+
let check;
|
|
11569
|
+
try {
|
|
11570
|
+
const db = new BetterSqlite3(newPath, { readonly: true });
|
|
11571
|
+
try {
|
|
11572
|
+
check = String(db.pragma("integrity_check", { simple: true }));
|
|
11573
|
+
} finally {
|
|
11574
|
+
db.close();
|
|
11575
|
+
}
|
|
10422
11576
|
} catch (e) {
|
|
10423
|
-
|
|
11577
|
+
throw new PaiFileMigrationError(`${newPath}: integrity_check could not run (${String(e)})`);
|
|
10424
11578
|
}
|
|
11579
|
+
if (check !== "ok") throw new PaiFileMigrationError(`${newPath}: PRAGMA integrity_check reported "${check}" — investigate before trusting this copy`);
|
|
10425
11580
|
}
|
|
10426
11581
|
/**
|
|
10427
11582
|
* ~/.pai/registry.db — a third per-user location, alongside PAI_HOME and the
|
|
@@ -10448,8 +11603,9 @@ function migrateRegistryDbFile(dryRun) {
|
|
|
10448
11603
|
execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
|
|
10449
11604
|
execFileSync("sleep", ["2"]);
|
|
10450
11605
|
}
|
|
11606
|
+
checkpointWal(oldPath, "registry database");
|
|
10451
11607
|
const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
|
|
10452
|
-
if (result.fromPath)
|
|
11608
|
+
if (result.fromPath) verifyIntegrity(newPath);
|
|
10453
11609
|
return result;
|
|
10454
11610
|
}
|
|
10455
11611
|
/**
|
|
@@ -10477,8 +11633,9 @@ function migrateFederationDbFile(dryRun) {
|
|
|
10477
11633
|
execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
|
|
10478
11634
|
execFileSync("sleep", ["2"]);
|
|
10479
11635
|
}
|
|
11636
|
+
checkpointWal(oldPath, "federation database");
|
|
10480
11637
|
const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
|
|
10481
|
-
if (result.fromPath)
|
|
11638
|
+
if (result.fromPath) verifyIntegrity(newPath);
|
|
10482
11639
|
return result;
|
|
10483
11640
|
}
|
|
10484
11641
|
|
|
@@ -10952,8 +12109,8 @@ async function escalate(task, headline, detail, state, now, opts) {
|
|
|
10952
12109
|
state.alarmedAt[task.id] = now;
|
|
10953
12110
|
let delivered = false;
|
|
10954
12111
|
try {
|
|
10955
|
-
const { routeNotification } = await import("../router-
|
|
10956
|
-
const { loadConfig } = await import("../config-
|
|
12112
|
+
const { routeNotification } = await import("../router-DwHJRgG9.mjs");
|
|
12113
|
+
const { loadConfig } = await import("../config-DtENJ6cL.mjs");
|
|
10957
12114
|
delivered = (await routeNotification({
|
|
10958
12115
|
event: "error",
|
|
10959
12116
|
title: `PAI: ${headline}`,
|
|
@@ -11059,7 +12216,7 @@ const GROSS_OVERRUN_FACTOR = 5;
|
|
|
11059
12216
|
*/
|
|
11060
12217
|
function detectWebhook(bin = "aibroker") {
|
|
11061
12218
|
try {
|
|
11062
|
-
|
|
12219
|
+
return webhookFromStatus(execFileSync(bin, ["todoist", "status"], {
|
|
11063
12220
|
timeout: 1e4,
|
|
11064
12221
|
encoding: "utf-8",
|
|
11065
12222
|
stdio: [
|
|
@@ -11067,13 +12224,21 @@ function detectWebhook(bin = "aibroker") {
|
|
|
11067
12224
|
"pipe",
|
|
11068
12225
|
"pipe"
|
|
11069
12226
|
]
|
|
11070
|
-
});
|
|
11071
|
-
return /authoris|authoriz|token on file|grant/i.test(out) && !/no (token|grant)/i.test(out);
|
|
12227
|
+
}));
|
|
11072
12228
|
} catch {
|
|
11073
12229
|
return false;
|
|
11074
12230
|
}
|
|
11075
12231
|
}
|
|
11076
12232
|
/**
|
|
12233
|
+
* Read `aibroker todoist status` output. Only an explicit positive line counts:
|
|
12234
|
+
* "Authorised <timestamp>" and no "FAILING" verdict. Negative texts ("No Todoist
|
|
12235
|
+
* authorisation on file", "Not authorized", "grant: missing") and a token that
|
|
12236
|
+
* is on file but rejected both read as no webhook.
|
|
12237
|
+
*/
|
|
12238
|
+
function webhookFromStatus(out) {
|
|
12239
|
+
return /^Authori[sz]ed\s+\S/m.test(out) && !/^FAILING\b/m.test(out);
|
|
12240
|
+
}
|
|
12241
|
+
/**
|
|
11077
12242
|
* Discard restore entries whose run is no longer in flight.
|
|
11078
12243
|
*
|
|
11079
12244
|
* `triggeredRestore` belongs to one run: it records the occurrence that run
|
|
@@ -11118,83 +12283,93 @@ async function tick(opts) {
|
|
|
11118
12283
|
const ordered = dispatchOrder(tasks);
|
|
11119
12284
|
if (!opts.dryRun) dropOrphanedRestores(tasks, state);
|
|
11120
12285
|
const webhookActive = opts.webhookActive ?? detectWebhook();
|
|
11121
|
-
|
|
11122
|
-
const
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
if (
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
12286
|
+
try {
|
|
12287
|
+
for (const task of ordered) {
|
|
12288
|
+
const prevDue = state.lastSeenDue[task.id];
|
|
12289
|
+
const d = decide(task, {
|
|
12290
|
+
now,
|
|
12291
|
+
state,
|
|
12292
|
+
history: state.history,
|
|
12293
|
+
lastSeenDue: state.lastSeenDue,
|
|
12294
|
+
claimSeenAt: state.claimSeenAt,
|
|
12295
|
+
webhookActive
|
|
12296
|
+
});
|
|
12297
|
+
let note = "";
|
|
12298
|
+
try {
|
|
12299
|
+
if (!opts.dryRun && task.due) state.lastSeenDue[task.id] = task.due;
|
|
12300
|
+
if (!opts.dryRun) if (isRunning(task)) {
|
|
12301
|
+
if (state.claimSeenAt[task.id] === void 0 && task.recurrence && prevDue !== void 0) {
|
|
12302
|
+
if (wasTicked(task, prevDue)) state.triggeredRestore[task.id] = restoreDueString(task.recurrence, prevDue);
|
|
12303
|
+
}
|
|
12304
|
+
state.claimSeenAt[task.id] ??= now;
|
|
12305
|
+
} else delete state.claimSeenAt[task.id];
|
|
12306
|
+
switch (d.action) {
|
|
12307
|
+
case "wait": continue;
|
|
12308
|
+
case "triggered": {
|
|
12309
|
+
const r = await handleDispatch(task, 0, opts, state, now);
|
|
12310
|
+
note = `ticked off by hand — ${r.note}`;
|
|
12311
|
+
if (!opts.dryRun) {
|
|
12312
|
+
if (!r.parked) report.dispatched++;
|
|
12313
|
+
if (r.alarm) report.stuck++;
|
|
12314
|
+
note += await restoreSchedule(task, d.restoreTo, opts, state);
|
|
12315
|
+
if (d.restoreTo) state.triggeredRestore[task.id] = d.restoreTo;
|
|
12316
|
+
}
|
|
12317
|
+
break;
|
|
12318
|
+
}
|
|
12319
|
+
case "skip":
|
|
12320
|
+
note = d.reason;
|
|
12321
|
+
break;
|
|
12322
|
+
case "running":
|
|
12323
|
+
note = `${d.elapsedMinutes}m elapsed`;
|
|
12324
|
+
break;
|
|
12325
|
+
case "dispatch": {
|
|
12326
|
+
const r = await handleDispatch(task, d.overdueMinutes, opts, state, now);
|
|
12327
|
+
note = r.note;
|
|
12328
|
+
if (!opts.dryRun) {
|
|
12329
|
+
if (!r.parked) report.dispatched++;
|
|
12330
|
+
if (r.alarm) report.stuck++;
|
|
12331
|
+
}
|
|
12332
|
+
break;
|
|
12333
|
+
}
|
|
12334
|
+
case "complete":
|
|
12335
|
+
note = await handleComplete(task, d.durationMinutes, opts, state, now);
|
|
12336
|
+
if (!opts.dryRun) report.completed++;
|
|
12337
|
+
break;
|
|
12338
|
+
case "abandoned":
|
|
12339
|
+
note = `claimed ${d.elapsedMinutes}m ago, past the ${d.thresholdMinutes}m limit — releasing the claim`;
|
|
12340
|
+
if (!opts.dryRun) {
|
|
12341
|
+
await releaseClaim(task, opts.provider, state);
|
|
12342
|
+
report.stuck++;
|
|
12343
|
+
}
|
|
12344
|
+
break;
|
|
12345
|
+
case "probe":
|
|
12346
|
+
case "orphaned": {
|
|
12347
|
+
const result = await handleProbe(task, d.action === "probe" ? d.elapsedMinutes : null, opts, state, now);
|
|
12348
|
+
note = result.note;
|
|
12349
|
+
if (!opts.dryRun) {
|
|
12350
|
+
report.probed++;
|
|
12351
|
+
if (result.stuck) report.stuck++;
|
|
12352
|
+
if (result.dispatched) report.dispatched++;
|
|
12353
|
+
}
|
|
12354
|
+
break;
|
|
12355
|
+
}
|
|
11186
12356
|
}
|
|
11187
|
-
|
|
12357
|
+
report.decisions.push({
|
|
12358
|
+
decision: d,
|
|
12359
|
+
note
|
|
12360
|
+
});
|
|
12361
|
+
} catch (err) {
|
|
12362
|
+
report.decisions.push({
|
|
12363
|
+
decision: d,
|
|
12364
|
+
note: `error: ${err instanceof Error ? err.message : String(err)}`
|
|
12365
|
+
});
|
|
11188
12366
|
}
|
|
11189
12367
|
}
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
if (!opts.dryRun) {
|
|
11196
|
-
reconcileWithDisk(state, stateFile);
|
|
11197
|
-
saveState(state, stateFile);
|
|
12368
|
+
} finally {
|
|
12369
|
+
if (!opts.dryRun) {
|
|
12370
|
+
reconcileWithDisk(state, stateFile);
|
|
12371
|
+
saveState(state, stateFile);
|
|
12372
|
+
}
|
|
11198
12373
|
}
|
|
11199
12374
|
return report;
|
|
11200
12375
|
}
|
|
@@ -11611,7 +12786,7 @@ function cleanBrokenSymlinks(dir) {
|
|
|
11611
12786
|
const full = join(dir, entry);
|
|
11612
12787
|
try {
|
|
11613
12788
|
if (lstatSync(full).isSymbolicLink()) {
|
|
11614
|
-
if (!existsSync(readlinkSync(full))) {
|
|
12789
|
+
if (!existsSync(resolve(dir, readlinkSync(full)))) {
|
|
11615
12790
|
unlinkSync(full);
|
|
11616
12791
|
removed++;
|
|
11617
12792
|
}
|
|
@@ -12667,10 +13842,22 @@ function migrateWorkerLogs(oldPath, newPath, opts = {}) {
|
|
|
12667
13842
|
const active = activeSpawnedWorkerCount(oldPath);
|
|
12668
13843
|
if (active > 0) throw new WorkerLogsMigrationError(`${active} worker(s) other than the interactive session are RUNNING and writing into ${oldPath} — wait for them to finish (see \`pai worker ps\`), then retry`);
|
|
12669
13844
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
12670
|
-
|
|
13845
|
+
const partial = `${newPath}.partial`;
|
|
13846
|
+
rmSync(partial, {
|
|
13847
|
+
recursive: true,
|
|
13848
|
+
force: true
|
|
13849
|
+
});
|
|
13850
|
+
cpSync(oldPath, partial, { recursive: true });
|
|
12671
13851
|
const fromCount = totalFileCount(oldPath);
|
|
12672
|
-
const toCount = totalFileCount(
|
|
12673
|
-
if (fromCount !== toCount)
|
|
13852
|
+
const toCount = totalFileCount(partial);
|
|
13853
|
+
if (fromCount !== toCount) {
|
|
13854
|
+
rmSync(partial, {
|
|
13855
|
+
recursive: true,
|
|
13856
|
+
force: true
|
|
13857
|
+
});
|
|
13858
|
+
throw new WorkerLogsMigrationError(`${newPath}: copy produced ${toCount} files, expected ${fromCount} from ${oldPath} — aborting, old directory left in place`);
|
|
13859
|
+
}
|
|
13860
|
+
renameSync(partial, newPath);
|
|
12674
13861
|
renameSync(oldPath, `${oldPath}.migrated-${stamp}`);
|
|
12675
13862
|
return {
|
|
12676
13863
|
fromPath: oldPath,
|
|
@@ -13847,7 +15034,7 @@ async function migrateToPostgres(opts = {}) {
|
|
|
13847
15034
|
const dump = pgDump(container, pgConfig, dumpPath);
|
|
13848
15035
|
if (!dump.ok) return refused(dump.reason, dryRun);
|
|
13849
15036
|
}
|
|
13850
|
-
const { PostgresBackend } = await import("../postgres-
|
|
15037
|
+
const { PostgresBackend } = await import("../postgres-BdBNkL_l.mjs");
|
|
13851
15038
|
await PostgresBackend.ensureDatabase(pgConfig);
|
|
13852
15039
|
const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
|
|
13853
15040
|
const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
|
|
@@ -14579,6 +15766,35 @@ const homeCommand = {
|
|
|
14579
15766
|
* `firstlast@company.com`. Anything that should count gets listed.
|
|
14580
15767
|
*/
|
|
14581
15768
|
/**
|
|
15769
|
+
* Split a recipient field into its individual entries. Commas and semicolons
|
|
15770
|
+
* separate entries unless inside a quoted display name or an angle-bracket
|
|
15771
|
+
* address. Empty entries are dropped.
|
|
15772
|
+
*/
|
|
15773
|
+
function splitAddresses(raw) {
|
|
15774
|
+
const out = [];
|
|
15775
|
+
let cur = "";
|
|
15776
|
+
let quoted = false;
|
|
15777
|
+
let angled = false;
|
|
15778
|
+
for (let i = 0; i < raw.length; i++) {
|
|
15779
|
+
const c = raw[i];
|
|
15780
|
+
if (quoted && c === "\\" && i + 1 < raw.length) {
|
|
15781
|
+
cur += c + raw[++i];
|
|
15782
|
+
continue;
|
|
15783
|
+
}
|
|
15784
|
+
if (c === "\"") quoted = !quoted;
|
|
15785
|
+
else if (!quoted && c === "<") angled = true;
|
|
15786
|
+
else if (!quoted && c === ">") angled = false;
|
|
15787
|
+
if (!quoted && !angled && (c === "," || c === ";")) {
|
|
15788
|
+
if (cur.trim()) out.push(cur.trim());
|
|
15789
|
+
cur = "";
|
|
15790
|
+
continue;
|
|
15791
|
+
}
|
|
15792
|
+
cur += c;
|
|
15793
|
+
}
|
|
15794
|
+
if (cur.trim()) out.push(cur.trim());
|
|
15795
|
+
return out;
|
|
15796
|
+
}
|
|
15797
|
+
/**
|
|
14582
15798
|
* Reduce an address to a comparable form: trimmed, unwrapped, lowercased.
|
|
14583
15799
|
*
|
|
14584
15800
|
* Handles `Display Name <addr@example.com>` because recipient fields carry that
|
|
@@ -14588,8 +15804,9 @@ const homeCommand = {
|
|
|
14588
15804
|
*/
|
|
14589
15805
|
function normalizeEmail(raw) {
|
|
14590
15806
|
if (!raw) return null;
|
|
14591
|
-
|
|
14592
|
-
if (
|
|
15807
|
+
const parts = splitAddresses(raw);
|
|
15808
|
+
if (parts.length !== 1) return null;
|
|
15809
|
+
let value = parts[0];
|
|
14593
15810
|
const angled = value.match(/<([^>]*)>/);
|
|
14594
15811
|
if (angled) value = angled[1].trim();
|
|
14595
15812
|
value = value.toLowerCase();
|
|
@@ -15204,7 +16421,7 @@ const out = (s) => process.stdout.write(s + "\n");
|
|
|
15204
16421
|
async function repoOf(project) {
|
|
15205
16422
|
if (typeof project === "string" && project) {
|
|
15206
16423
|
if (existsSync(project)) return resolve(project);
|
|
15207
|
-
const { getProject } = await import("../helpers-
|
|
16424
|
+
const { getProject } = await import("../helpers-CMGjy_0N.mjs");
|
|
15208
16425
|
const row = await getProject(project);
|
|
15209
16426
|
if (!row) throw new CliError(`no project ${project}`, "pai projects list");
|
|
15210
16427
|
return row.root_path;
|
|
@@ -15572,7 +16789,8 @@ function mcpInstallCmd() {
|
|
|
15572
16789
|
for (const spec of SERVERS) {
|
|
15573
16790
|
const bin = distEntry(spec.entry);
|
|
15574
16791
|
if (spec.name in servers) {
|
|
15575
|
-
|
|
16792
|
+
const same = mcpEntryRunsScript(servers[spec.name], bin);
|
|
16793
|
+
console.log(warn$1(`${spec.label} is already registered in ~/.claude.json as "${spec.name}"${same ? "" : " (different script)"}.`));
|
|
15576
16794
|
console.log(dim$1(` Entry: ${JSON.stringify(servers[spec.name])}`));
|
|
15577
16795
|
continue;
|
|
15578
16796
|
}
|
|
@@ -16026,7 +17244,7 @@ async function cmdPromote(opts) {
|
|
|
16026
17244
|
console.error(dim$1(` Pick a durable location for the new project.`));
|
|
16027
17245
|
process.exit(1);
|
|
16028
17246
|
}
|
|
16029
|
-
const { getRegistryBackend } = await import("../factory-
|
|
17247
|
+
const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
|
|
16030
17248
|
const registryBackend = await getRegistryBackend();
|
|
16031
17249
|
const encodedDir = encodeDir(targetPath);
|
|
16032
17250
|
if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
|
|
@@ -17270,7 +18488,7 @@ async function cmdUnregister(slug, opts = {}) {
|
|
|
17270
18488
|
* deprecated alias that forwards here with a stderr notice.
|
|
17271
18489
|
*/
|
|
17272
18490
|
async function projectListCmd(opts) {
|
|
17273
|
-
const { cmdMain } = await import("../main-resolver-
|
|
18491
|
+
const { cmdMain } = await import("../main-resolver-3Ns1HPmS.mjs");
|
|
17274
18492
|
await cmdMain(void 0, void 0, opts);
|
|
17275
18493
|
return 0;
|
|
17276
18494
|
}
|
|
@@ -18693,7 +19911,7 @@ async function registryScanCmd(opts) {
|
|
|
18693
19911
|
return 0;
|
|
18694
19912
|
}
|
|
18695
19913
|
if (opts.filterReport) {
|
|
18696
|
-
const { reportUnregistrable } = await import("../registrable-
|
|
19914
|
+
const { reportUnregistrable } = await import("../registrable-BzH4TObg.mjs");
|
|
18697
19915
|
const r = await reportUnregistrable(await getRegistryBackend());
|
|
18698
19916
|
console.log(`Registry rows: ${r.total}`);
|
|
18699
19917
|
console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
|
|
@@ -18899,7 +20117,7 @@ const VALID_OUTCOMES = new Set([
|
|
|
18899
20117
|
*/
|
|
18900
20118
|
function run$1(bin, args, stdin, killAfterMs) {
|
|
18901
20119
|
return new Promise((resolve, reject) => {
|
|
18902
|
-
execFile(bin, args, {
|
|
20120
|
+
const child = execFile(bin, args, {
|
|
18903
20121
|
timeout: killAfterMs,
|
|
18904
20122
|
maxBuffer: 1024 * 1024
|
|
18905
20123
|
}, (error, stdout, stderr) => {
|
|
@@ -18908,15 +20126,26 @@ function run$1(bin, args, stdin, killAfterMs) {
|
|
|
18908
20126
|
return;
|
|
18909
20127
|
}
|
|
18910
20128
|
resolve(stdout);
|
|
18911
|
-
})
|
|
20129
|
+
});
|
|
20130
|
+
child.stdin?.on("error", (e) => reject(e));
|
|
20131
|
+
child.stdin?.end(stdin);
|
|
18912
20132
|
});
|
|
18913
20133
|
}
|
|
20134
|
+
/** A leading dash would be parsed by the CLI as an option, not a project. */
|
|
20135
|
+
function validProject(project) {
|
|
20136
|
+
return project.length > 0 && !project.startsWith("-");
|
|
20137
|
+
}
|
|
18914
20138
|
var AiBrokerTransport = class {
|
|
18915
20139
|
constructor(bin = "aibroker", timeoutSecs = DEFAULT_DISPATCH_TIMEOUT_SECS) {
|
|
18916
20140
|
this.bin = bin;
|
|
18917
20141
|
this.timeoutSecs = timeoutSecs;
|
|
18918
20142
|
}
|
|
18919
20143
|
async dispatch(project, message, opts) {
|
|
20144
|
+
if (!validProject(project)) return {
|
|
20145
|
+
outcome: "unlaunchable",
|
|
20146
|
+
session: project,
|
|
20147
|
+
reason: `invalid project name: ${project}`
|
|
20148
|
+
};
|
|
18920
20149
|
const args = [
|
|
18921
20150
|
"dispatch",
|
|
18922
20151
|
project,
|
|
@@ -18960,6 +20189,10 @@ var AiBrokerProber = class {
|
|
|
18960
20189
|
this.timeoutSecs = timeoutSecs;
|
|
18961
20190
|
}
|
|
18962
20191
|
async ask(project, question) {
|
|
20192
|
+
if (!validProject(project)) return {
|
|
20193
|
+
state: "silent",
|
|
20194
|
+
reason: `invalid project name: ${project}`
|
|
20195
|
+
};
|
|
18963
20196
|
let stdout;
|
|
18964
20197
|
try {
|
|
18965
20198
|
stdout = await run$1(this.bin, [
|
|
@@ -19083,33 +20316,36 @@ function cliPath() {
|
|
|
19083
20316
|
}
|
|
19084
20317
|
throw new Error("cannot locate the PAI CLI entrypoint (dist/cli/index.mjs) — refusing to install a scheduler that would silently do nothing");
|
|
19085
20318
|
}
|
|
20319
|
+
function xmlEscape(value) {
|
|
20320
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
20321
|
+
}
|
|
19086
20322
|
function generateSchedulePlist(intervalSecs, cli) {
|
|
19087
20323
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
19088
20324
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
19089
20325
|
<plist version="1.0">
|
|
19090
20326
|
<dict>
|
|
19091
20327
|
<key>Label</key>
|
|
19092
|
-
<string>${SCHEDULE_LABEL}</string>
|
|
20328
|
+
<string>${xmlEscape(SCHEDULE_LABEL)}</string>
|
|
19093
20329
|
|
|
19094
20330
|
<key>ProgramArguments</key>
|
|
19095
20331
|
<array>
|
|
19096
|
-
<string
|
|
19097
|
-
<string>${cli}</string>
|
|
20332
|
+
<string>${xmlEscape(process.execPath)}</string>
|
|
20333
|
+
<string>${xmlEscape(cli)}</string>
|
|
19098
20334
|
<string>task</string>
|
|
19099
20335
|
<string>poll</string>
|
|
19100
20336
|
</array>
|
|
19101
20337
|
|
|
19102
20338
|
<key>StartInterval</key>
|
|
19103
|
-
<integer>${intervalSecs}</integer>
|
|
20339
|
+
<integer>${Math.trunc(intervalSecs)}</integer>
|
|
19104
20340
|
|
|
19105
20341
|
<key>RunAtLoad</key>
|
|
19106
20342
|
<true/>
|
|
19107
20343
|
|
|
19108
20344
|
<key>StandardOutPath</key>
|
|
19109
|
-
<string>${SCHEDULE_LOG}</string>
|
|
20345
|
+
<string>${xmlEscape(SCHEDULE_LOG)}</string>
|
|
19110
20346
|
|
|
19111
20347
|
<key>StandardErrorPath</key>
|
|
19112
|
-
<string>${SCHEDULE_LOG}</string>
|
|
20348
|
+
<string>${xmlEscape(SCHEDULE_LOG)}</string>
|
|
19113
20349
|
|
|
19114
20350
|
<key>EnvironmentVariables</key>
|
|
19115
20351
|
<dict>
|
|
@@ -20517,8 +21753,8 @@ function embedProgressLine(done, backlog) {
|
|
|
20517
21753
|
async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSize = DEFAULT_PAGE_SIZE, maxMillis, run, shouldStop) {
|
|
20518
21754
|
const label = projectSlug ? `project ${projectSlug}` : "all projects";
|
|
20519
21755
|
console.log(dim$1(`Generating embeddings for ${label} (this may take a while on first run)...`));
|
|
20520
|
-
const { serializeEmbedding } = await import("../embeddings-
|
|
20521
|
-
const { gateBackend } = await import("../embedding-gate-
|
|
21756
|
+
const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
|
|
21757
|
+
const { gateBackend } = await import("../embedding-gate-Cnk4YCyX.mjs");
|
|
20522
21758
|
const gate = await gateBackend(backend, { record: true });
|
|
20523
21759
|
if (!gate.ok) {
|
|
20524
21760
|
console.error(err(gate.reason));
|
|
@@ -20847,7 +22083,7 @@ async function memorySearchCmd(query, opts) {
|
|
|
20847
22083
|
let results;
|
|
20848
22084
|
if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
|
|
20849
22085
|
else {
|
|
20850
|
-
const { embedQuery } = await import("../embedding-gate-
|
|
22086
|
+
const { embedQuery } = await import("../embedding-gate-Cnk4YCyX.mjs");
|
|
20851
22087
|
console.log(dim$1("Generating query embedding..."));
|
|
20852
22088
|
const q = await embedQuery(backend, query);
|
|
20853
22089
|
const queryEmbedding = q.vec;
|
|
@@ -20897,7 +22133,7 @@ async function memorySearchCmd(query, opts) {
|
|
|
20897
22133
|
return 0;
|
|
20898
22134
|
}
|
|
20899
22135
|
if (opts.rerank !== false) {
|
|
20900
|
-
const { rerankResults } = await import("../reranker-
|
|
22136
|
+
const { rerankResults } = await import("../reranker-8g6xMwP7.mjs");
|
|
20901
22137
|
console.log(dim$1("Reranking with cross-encoder..."));
|
|
20902
22138
|
results = await rerankResults(query, results, { topK: maxResults });
|
|
20903
22139
|
}
|
|
@@ -20927,9 +22163,9 @@ async function memorySearchCmd(query, opts) {
|
|
|
20927
22163
|
//#region src/cli/commands/memory/sources-cmd.ts
|
|
20928
22164
|
/** `pai memory sources` — what the indexer has taken in. */
|
|
20929
22165
|
async function memorySourcesCmd(opts) {
|
|
20930
|
-
const { createStorageBackend } = await import("../factory-
|
|
20931
|
-
const { loadConfig } = await import("../config-
|
|
20932
|
-
const { cmdMemorySources } = await import("../sources-
|
|
22166
|
+
const { createStorageBackend } = await import("../factory-Bsib-qDG.mjs");
|
|
22167
|
+
const { loadConfig } = await import("../config-DtENJ6cL.mjs");
|
|
22168
|
+
const { cmdMemorySources } = await import("../sources-ESUkZaWv.mjs");
|
|
20933
22169
|
let backend;
|
|
20934
22170
|
try {
|
|
20935
22171
|
backend = await createStorageBackend(loadConfig());
|
|
@@ -21688,7 +22924,7 @@ async function zettelExploreCmd(note, opts) {
|
|
|
21688
22924
|
const depth = opts.depth ?? 3;
|
|
21689
22925
|
const direction = opts.direction ?? "both";
|
|
21690
22926
|
const mode = opts.mode ?? "all";
|
|
21691
|
-
const { zettelExplore } = await import("../zettelkasten-
|
|
22927
|
+
const { zettelExplore } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21692
22928
|
const result = await zettelExplore(await getFedBackend(), {
|
|
21693
22929
|
startNote: note,
|
|
21694
22930
|
depth,
|
|
@@ -21744,7 +22980,7 @@ async function zettelHealthCmd(opts) {
|
|
|
21744
22980
|
const projectPath = opts.project;
|
|
21745
22981
|
const recentDays = opts.days ?? 30;
|
|
21746
22982
|
const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
|
|
21747
|
-
const { zettelHealth } = await import("../zettelkasten-
|
|
22983
|
+
const { zettelHealth } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21748
22984
|
const result = await zettelHealth(await getFedBackend(), {
|
|
21749
22985
|
scope,
|
|
21750
22986
|
projectPath,
|
|
@@ -21802,7 +23038,7 @@ async function zettelSurpriseCmd(note, opts) {
|
|
|
21802
23038
|
const limit = opts.limit ?? 10;
|
|
21803
23039
|
const minSimilarity = opts.minSimilarity ?? .3;
|
|
21804
23040
|
const minGraphDistance = opts.minDistance ?? 3;
|
|
21805
|
-
const { zettelSurprise } = await import("../zettelkasten-
|
|
23041
|
+
const { zettelSurprise } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21806
23042
|
const backend = await getFedBackend();
|
|
21807
23043
|
console.log();
|
|
21808
23044
|
console.log(header(" PAI Zettel Surprise"));
|
|
@@ -21844,7 +23080,7 @@ async function zettelConverseCmd(question, opts) {
|
|
|
21844
23080
|
const vaultProjectId = Number(opts.vaultProjectId);
|
|
21845
23081
|
const depth = opts.depth ?? 2;
|
|
21846
23082
|
const limit = opts.limit ?? 15;
|
|
21847
|
-
const { zettelConverse } = await import("../zettelkasten-
|
|
23083
|
+
const { zettelConverse } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21848
23084
|
const backend = await getFedBackend();
|
|
21849
23085
|
console.log();
|
|
21850
23086
|
console.log(header(" PAI Zettel Converse"));
|
|
@@ -21894,7 +23130,7 @@ async function zettelThemesCmd(opts) {
|
|
|
21894
23130
|
const minClusterSize = opts.minSize ?? 3;
|
|
21895
23131
|
const maxThemes = opts.maxThemes ?? 10;
|
|
21896
23132
|
const similarityThreshold = opts.threshold ?? .65;
|
|
21897
|
-
const { zettelThemes } = await import("../zettelkasten-
|
|
23133
|
+
const { zettelThemes } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21898
23134
|
const backend = await getFedBackend();
|
|
21899
23135
|
console.log();
|
|
21900
23136
|
console.log(header(" PAI Zettel Themes"));
|
|
@@ -21942,7 +23178,7 @@ async function zettelSuggestCmd(note, opts) {
|
|
|
21942
23178
|
const vaultProjectId = Number(opts.vaultProjectId);
|
|
21943
23179
|
const limit = opts.limit ?? 5;
|
|
21944
23180
|
const excludeLinked = opts.excludeLinked !== true;
|
|
21945
|
-
const { zettelSuggest } = await import("../zettelkasten-
|
|
23181
|
+
const { zettelSuggest } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21946
23182
|
const backend = await getFedBackend();
|
|
21947
23183
|
console.log();
|
|
21948
23184
|
console.log(header(" PAI Zettel Suggest"));
|
|
@@ -22234,7 +23470,7 @@ function ipcCall$1(method, params) {
|
|
|
22234
23470
|
fn();
|
|
22235
23471
|
}
|
|
22236
23472
|
};
|
|
22237
|
-
const client = createConnection(paiSocketPath(), () => {
|
|
23473
|
+
const client = createConnection$1(paiSocketPath(), () => {
|
|
22238
23474
|
client.write(JSON.stringify({
|
|
22239
23475
|
id: 1,
|
|
22240
23476
|
method,
|
|
@@ -22624,7 +23860,7 @@ function ipcCall(method, params) {
|
|
|
22624
23860
|
fn();
|
|
22625
23861
|
}
|
|
22626
23862
|
};
|
|
22627
|
-
const client = createConnection(paiSocketPath(), () => {
|
|
23863
|
+
const client = createConnection$1(paiSocketPath(), () => {
|
|
22628
23864
|
client.write(JSON.stringify({
|
|
22629
23865
|
id: 1,
|
|
22630
23866
|
method,
|
|
@@ -24256,7 +25492,14 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
|
24256
25492
|
* one of the two in whichever catalogue order Claude Code loads them.
|
|
24257
25493
|
*/
|
|
24258
25494
|
/** Recursively collect every file under `dir` whose basename satisfies `matches`. */
|
|
24259
|
-
function findFilesRecursive(dir, matches) {
|
|
25495
|
+
function findFilesRecursive(dir, matches, seen = /* @__PURE__ */ new Set()) {
|
|
25496
|
+
try {
|
|
25497
|
+
const real = realpathSync(dir);
|
|
25498
|
+
if (seen.has(real)) return [];
|
|
25499
|
+
seen.add(real);
|
|
25500
|
+
} catch {
|
|
25501
|
+
return [];
|
|
25502
|
+
}
|
|
24260
25503
|
let entries;
|
|
24261
25504
|
try {
|
|
24262
25505
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -24266,8 +25509,17 @@ function findFilesRecursive(dir, matches) {
|
|
|
24266
25509
|
const found = [];
|
|
24267
25510
|
for (const entry of entries) {
|
|
24268
25511
|
const full = join(dir, entry.name);
|
|
24269
|
-
|
|
24270
|
-
|
|
25512
|
+
let isDir = entry.isDirectory();
|
|
25513
|
+
let isFile = entry.isFile();
|
|
25514
|
+
if (entry.isSymbolicLink()) try {
|
|
25515
|
+
const st = statSync(full);
|
|
25516
|
+
isDir = st.isDirectory();
|
|
25517
|
+
isFile = st.isFile();
|
|
25518
|
+
} catch {
|
|
25519
|
+
continue;
|
|
25520
|
+
}
|
|
25521
|
+
if (isDir) found.push(...findFilesRecursive(full, matches, seen));
|
|
25522
|
+
else if (isFile && matches(entry.name)) found.push(full);
|
|
24271
25523
|
}
|
|
24272
25524
|
return found;
|
|
24273
25525
|
}
|
|
@@ -24750,11 +26002,17 @@ function newestMcpConfig(logDir) {
|
|
|
24750
26002
|
mtime: statSync(path).mtimeMs
|
|
24751
26003
|
})).sort((a, b) => b.mtime - a.mtime)[0].path;
|
|
24752
26004
|
}
|
|
26005
|
+
/** Child env: not a worker, and no API key (same as run-env / daemon-llm). */
|
|
26006
|
+
function ladderChildEnv(base) {
|
|
26007
|
+
const env = { ...base };
|
|
26008
|
+
delete env.PAI_WORKER;
|
|
26009
|
+
delete env.ANTHROPIC_API_KEY;
|
|
26010
|
+
return env;
|
|
26011
|
+
}
|
|
24753
26012
|
/** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
|
|
24754
26013
|
function runRung(rung, cwd) {
|
|
24755
26014
|
return new Promise((resolvePromise) => {
|
|
24756
|
-
const env =
|
|
24757
|
-
delete env.PAI_WORKER;
|
|
26015
|
+
const env = ladderChildEnv(process.env);
|
|
24758
26016
|
const proc = spawn("claude", rung.args, {
|
|
24759
26017
|
cwd,
|
|
24760
26018
|
env,
|