@tekmidian/pai 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-B4azPk4l.mjs} +3 -3
- package/dist/auto-route-BjoMHKyI.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-D-yvET_y.mjs} +5 -5
- package/dist/cli/index.mjs +1621 -357
- package/dist/cockpit-actions-BWunx3rY.mjs +20 -0
- package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-C8GlTzts.mjs} +84 -42
- package/dist/cockpit-memory-vTlFWV7F.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-BcKFHeG1.mjs} +37 -22
- package/dist/daemon/index.mjs +43 -42
- package/dist/{daemon-CorGh6Ub.mjs → daemon-ColVHwGZ.mjs} +103 -55
- package/dist/daemon-N7WhYi6I.mjs +45 -0
- 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 +214 -116
- 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-_a6UNhXJ.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-BtB3Se6f.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-DurzIbw8.mjs} +7 -6
- package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-BQytYa60.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-BM27GBfC.mjs} +31 -30
- package/dist/{pool-CZ5MZaXg.mjs → pool-BRvoOkv6.mjs} +3 -3
- package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-B_YR8_Gl.mjs} +5 -5
- package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DkZTAlSW.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-CfpApz5o.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-C7b6givx.mjs +9 -0
- package/dist/{registrable-LrRIVYQc.mjs → registrable-Uzgmfyhz.mjs} +4 -4
- package/dist/render-Cc2j5QG9.mjs +13 -0
- package/dist/{render-BDbb4CF-.mjs → render-CwIrgxLi.mjs} +8 -8
- package/dist/{reranker-B4r4vykM.mjs → reranker-8g6xMwP7.mjs} +1 -1
- package/dist/{reranker-DxxMCtA0.mjs → reranker-CqCbpGnX.mjs} +1 -1
- package/dist/{root-BMeJuHOb.mjs → root-BWy0c2Lq.mjs} +11 -11
- package/dist/root-wC_6Fx0B.mjs +36 -0
- 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-6Q9kCPem.mjs} +35 -33
- package/dist/{scan-B7I0gPe-.mjs → scan-ZQWm-_se.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-Bok2PqRS.mjs +35 -0
- package/dist/{session-CNXF6DLP.mjs → session-DVUExUnX.mjs} +55 -31
- 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-DtWj5Vti.mjs → viewer-DcpJwGKE.mjs} +15 -15
- package/dist/viewer-MWBVJP3u.mjs +22 -0
- package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
- package/dist/{worker-config-BLqa1yiD.mjs → worker-config-BDYNxbYz.mjs} +294 -92
- package/dist/worker-config-gmAq_-rI.mjs +41 -0
- package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
- package/dist/{worktree-zmRf556h.mjs → worktree-CrnOqTBW.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.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 writeClaudeJson, k as readClaudeJson, v as readPoolState, w as expandMcpNames } from "../pool-
|
|
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-CrnOqTBW.mjs";
|
|
9
|
+
import { B as loadStatusConfig, G as writeSectionToForge, L as formatStale, U as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, y as loadStatuses } from "../status-ggJ7lgVg.mjs";
|
|
10
|
+
import { S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as writeClaudeJson, k as readClaudeJson, v as readPoolState, w as expandMcpNames } from "../pool-BRvoOkv6.mjs";
|
|
11
|
+
import { a as unregistrableReason, s as worktreeReason } from "../registrable-Uzgmfyhz.mjs";
|
|
12
|
+
import "../pai-marker-DurzIbw8.mjs";
|
|
13
|
+
import "../helpers-CRuBUas3.mjs";
|
|
14
|
+
import { t as applyRecencyBoost } from "../search-RSyeQqj1.mjs";
|
|
15
|
+
import { t as dropItermBundleId } from "../env-BE8mrsxr.mjs";
|
|
16
|
+
import { a as ownerSessionName, d as terminalHasName } from "../scope-D-h6F6DN.mjs";
|
|
17
|
+
import { An as saveSetup, Bn as aggregate, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Hn as appendUsage, In as parkConfigFrom, Jn as readUsage, Ln as setParkConfigSource, Nn as setupsPath, On as loadSaved, Rn as sharedParker, Tn as downLocalProviders, U as computeInventory, Un as formatSessionStats, Vn as aggregateBySession, W as currentInventory, Wn as formatStats, Wt as sessionStartTarget, Y as inventoryText, at as readInventoryInput, hn as localProviderForDelegation, m as readRoutingState, pt as isLoopbackProvider, qn as parseSince, sn as SESSION_NAME_ENV, un as readProfiles, wn as allSetups, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, yn as providerHostPort, zn as recordDuration } from "../routing-BAAST9vT.mjs";
|
|
18
|
+
import { n as minOf } from "../min-max-DrSRUaWQ.mjs";
|
|
19
|
+
import "../tool-errors-7E13StWd.mjs";
|
|
20
|
+
import "../wait-ByV0OyMZ.mjs";
|
|
21
|
+
import "../local-tiers-BE4jGaMa.mjs";
|
|
22
|
+
import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-yLFb7CKq.mjs";
|
|
23
|
+
import { d as setSchedConfigSource, f as sharedScheduler, i as mirrorTo, s as schedConfigFrom } from "../scheduler-CtUyWJ3O.mjs";
|
|
24
|
+
import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-DEveQdFV.mjs";
|
|
25
|
+
import "../local-instances-DOxSgT_L.mjs";
|
|
26
|
+
import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-DLdwQt0s.mjs";
|
|
27
|
+
import { n as TAILSCALE_STATUS_TIMEOUT_MS } from "../timeouts-1MLooEBB.mjs";
|
|
28
|
+
import { n as OPT, t as CliError } from "../define-CjJsmq-6.mjs";
|
|
29
|
+
import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-DjAAsqr_.mjs";
|
|
30
|
+
import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-C6ti4haA.mjs";
|
|
31
|
+
import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-6rZrWRXa.mjs";
|
|
32
|
+
import "../embeddings-BVq3esBF.mjs";
|
|
33
|
+
import "../router-CQSc3lk3.mjs";
|
|
34
|
+
import { t as detectProject } from "../detect-9CZNO0yf.mjs";
|
|
35
|
+
import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-BD6V-p9w.mjs";
|
|
36
|
+
import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-ZQWm-_se.mjs";
|
|
37
|
+
import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-DVUExUnX.mjs";
|
|
38
|
+
import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-Jh5NVeNe.mjs";
|
|
39
|
+
import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-Dnym-qcT.mjs";
|
|
40
|
+
import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-CXVlZjHt.mjs";
|
|
41
|
+
import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-BWy0c2Lq.mjs";
|
|
42
|
+
import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CQPGqLxL.mjs";
|
|
43
|
+
import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-W-gbwJHM.mjs";
|
|
44
|
+
import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-6Q9kCPem.mjs";
|
|
45
|
+
import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, jt as runPaneExec, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage, yt as psTable } from "../controls-BcKFHeG1.mjs";
|
|
46
|
+
import "../endreason-C577xIQz.mjs";
|
|
47
|
+
import { N as endBuildWait, j as beginBuildWait } from "../render-CwIrgxLi.mjs";
|
|
48
|
+
import "../operator-CyuB3aVw.mjs";
|
|
49
|
+
import "../kill-tree-EWYaEk5v.mjs";
|
|
50
|
+
import { s as poolReloadLive } from "../pool-cli-B_YR8_Gl.mjs";
|
|
51
|
+
import "../chain-D-yvET_y.mjs";
|
|
52
|
+
import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-CfpApz5o.mjs";
|
|
53
|
+
import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-BQytYa60.mjs";
|
|
54
|
+
import "../viewer-DcpJwGKE.mjs";
|
|
55
|
+
import { g as setWorkersEnabled, s as describeProviders } from "../providers-CblTFrhz.mjs";
|
|
56
|
+
import { $ as exportDefaultLayout, B as mergeSettings, G as cachedRows, J as handleCockpit, K as cachedSessionMap, Q as warmCockpit, W as SystemMonitor, X as setEndpointHealth, Y as setActionHooks, Z as setRuntimeSampler, ct as providersCommand, et as createEndpointHealth, it as withWorkerAttribution, n as WORKER_CONFIG_COMMANDS, nt as loadWorkerWindows, q as cachedWorkerWindows, rt as sessionDisplayName, t as PROVIDER_EDIT_COMMANDS, tt as loadSessionMap, z as installWorkers } from "../worker-config-BDYNxbYz.mjs";
|
|
57
|
+
import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-BEMpuhDz.mjs";
|
|
58
|
+
import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-C8GlTzts.mjs";
|
|
59
|
+
import { n as PaiClient } from "../ipc-client-CaiGJnSM.mjs";
|
|
60
|
+
import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-DIWjA68P.mjs";
|
|
61
|
+
import "../fallback-DFlItw1Z.mjs";
|
|
62
|
+
import { At as readQuiet, Bt as appendCheckpointToNote, C as migrateSummaryCooldowns, Et as totalUsageTokens, Gt as restartRouter, Ht as findLatestNote, It as resolveDaemonPid, Nt as writeQuiet, Ot as describeQuiet, Pt as LAUNCHD_LABEL, Rt as readContextHandoverCache, St as loadSessionKeepaliveState, Tt as parseSessionUsage, Ut as findNotesDir$1, Vt as applyContinue, Wt as readBodyFile, bt as reapJob, ht as obsidianEnabled, i as reexecBackground, kt as nextOccurrence, mt as OBSIDIAN_DISABLED_MESSAGE, o as backfillKgFromNotes, s as migrateKgBackfillState, v as migrateWorkQueue, vt as migrateIdentityFile, wt as sessionKeepaliveLedgerSummary, yt as jobPaths } from "../indexer-backend-_a6UNhXJ.mjs";
|
|
63
|
+
import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-BRfPtqki.mjs";
|
|
64
|
+
import { appendFileSync, chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
65
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
62
66
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
63
67
|
import chalk from "chalk";
|
|
64
68
|
import { cpus, homedir, networkInterfaces, platform, userInfo } from "node:os";
|
|
65
69
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
66
70
|
import { fileURLToPath } from "node:url";
|
|
67
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
68
71
|
import http from "node:http";
|
|
72
|
+
import { createServer } from "node:net";
|
|
69
73
|
import pg from "pg";
|
|
70
74
|
import BetterSqlite3 from "better-sqlite3";
|
|
71
75
|
import { createInterface } from "node:readline";
|
|
72
|
-
import net, { createConnection } from "net";
|
|
76
|
+
import net$1, { createConnection as createConnection$1 } from "net";
|
|
73
77
|
import { promisify } from "node:util";
|
|
74
78
|
import { getEncoding } from "js-tiktoken";
|
|
75
79
|
|
|
@@ -897,7 +901,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
|
|
|
897
901
|
});
|
|
898
902
|
return {
|
|
899
903
|
inventory: async () => {
|
|
900
|
-
const { currentInventory } = await import("../model-inventory-
|
|
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-gmAq_-rI.mjs");
|
|
1283
|
+
const { readLocalConfig } = await import("../local-tiers-DNMI5W86.mjs");
|
|
1284
|
+
const { workersYamlPath } = await import("../workers-config-f8KrkY4L.mjs");
|
|
1281
1285
|
const cfg = readLocalConfig(workersYamlPath());
|
|
1282
1286
|
if (kind === "off") return void await stopAll(cfg);
|
|
1283
1287
|
const t = cfg.tiers[tier];
|
|
@@ -1653,7 +1657,7 @@ const modelsCommand = {
|
|
|
1653
1657
|
if (words[0] === "add") {
|
|
1654
1658
|
const source = words[1];
|
|
1655
1659
|
if (!source) throw new Error("usage: pai models add <hf-id|path> [--name n] [--download] [--try]");
|
|
1656
|
-
const { addModel } = await import("../models-add-
|
|
1660
|
+
const { addModel } = await import("../models-add-BtB3Se6f.mjs");
|
|
1657
1661
|
const r = await addModel({
|
|
1658
1662
|
source,
|
|
1659
1663
|
name: typeof ctx.opts.name === "string" ? ctx.opts.name : void 0,
|
|
@@ -1668,7 +1672,7 @@ const modelsCommand = {
|
|
|
1668
1672
|
if (op) {
|
|
1669
1673
|
const runtime = words[1];
|
|
1670
1674
|
if (!runtime) throw new Error(`usage: pai models ${op} <runtime> (configured: ${Object.keys(readRuntimeCommands()).join(", ") || "none"})`);
|
|
1671
|
-
const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-
|
|
1675
|
+
const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-BWunx3rY.mjs");
|
|
1672
1676
|
const r = await runtimeStartStop(runtime, op, defaultDeps(), { model: typeof ctx.opts.model === "string" ? ctx.opts.model : void 0 });
|
|
1673
1677
|
process.stdout.write(r.message + "\n");
|
|
1674
1678
|
if (!r.ok) process.exitCode = 1;
|
|
@@ -2513,7 +2517,7 @@ function measureCompactionTrigger(cwd, projectsDir = CLAUDE_PROJECTS_DIR, config
|
|
|
2513
2517
|
if (discarded > 0) console.error(`[context-fill] discarded ${discarded} compaction sample(s) as other-regime for ${cwd} (preTokens below ${Math.round(floor)}, half of configured=${configuredTriggerTokens}): ` + samples.filter((s) => s.preTokens < floor).map((s) => `${s.preTokens}@${s.timestamp}`).join(", "));
|
|
2514
2518
|
if (admissible.length === 0) return null;
|
|
2515
2519
|
}
|
|
2516
|
-
const trigger =
|
|
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
|
}
|
|
@@ -8432,8 +9036,8 @@ function cmdLogs(opts) {
|
|
|
8432
9036
|
return 0;
|
|
8433
9037
|
}
|
|
8434
9038
|
async function daemonServeCmd() {
|
|
8435
|
-
const { serve } = await import("../daemon-
|
|
8436
|
-
const { loadConfig: lc, ensureConfigDir } = await import("../config-
|
|
9039
|
+
const { serve } = await import("../daemon-N7WhYi6I.mjs");
|
|
9040
|
+
const { loadConfig: lc, ensureConfigDir } = await import("../config-DtENJ6cL.mjs");
|
|
8437
9041
|
ensureConfigDir();
|
|
8438
9042
|
await serve(lc());
|
|
8439
9043
|
return 0;
|
|
@@ -8591,11 +9195,11 @@ async function stepDaemon(rl) {
|
|
|
8591
9195
|
if (existsSync(plistPath)) {
|
|
8592
9196
|
console.log(c.dim(` PAI daemon ${unit} already installed.`));
|
|
8593
9197
|
line();
|
|
8594
|
-
if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false)) {
|
|
9198
|
+
if (!await promptYesNo(rl, `Reinstall the PAI daemon ${unit}?`, false, "daemon")) {
|
|
8595
9199
|
console.log(c.dim(" Keeping existing daemon installation."));
|
|
8596
9200
|
return false;
|
|
8597
9201
|
}
|
|
8598
|
-
} else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true)) {
|
|
9202
|
+
} else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true, "daemon")) {
|
|
8599
9203
|
console.log(c.dim(" Skipping daemon install. Run manually: pai daemon install"));
|
|
8600
9204
|
return false;
|
|
8601
9205
|
}
|
|
@@ -8627,7 +9231,7 @@ async function stepMcp(rl) {
|
|
|
8627
9231
|
return false;
|
|
8628
9232
|
}
|
|
8629
9233
|
} catch {}
|
|
8630
|
-
if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true)) {
|
|
9234
|
+
if (!await promptYesNo(rl, "Register the PAI MCP server (pai) in ~/.claude.json?", true, "mcp")) {
|
|
8631
9235
|
console.log(c.dim(" Skipping MCP registration. Run manually: pai mcp install"));
|
|
8632
9236
|
return false;
|
|
8633
9237
|
}
|
|
@@ -8660,7 +9264,7 @@ async function stepDirectories(rl) {
|
|
|
8660
9264
|
for (const d of defaults) console.log(chalk.dim(` ${d}`));
|
|
8661
9265
|
line();
|
|
8662
9266
|
}
|
|
8663
|
-
const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false);
|
|
9267
|
+
const runScan = await promptYesNo(rl, "Run `pai registry scan` to auto-detect projects after setup?", false, "registry-scan");
|
|
8664
9268
|
if (runScan) {
|
|
8665
9269
|
line();
|
|
8666
9270
|
console.log(chalk.dim(" Registry scan will run after setup completes."));
|
|
@@ -8720,7 +9324,7 @@ async function stepInitialIndex(rl) {
|
|
|
8720
9324
|
//#endregion
|
|
8721
9325
|
//#region src/cli/commands/setup/steps/15-verify.ts
|
|
8722
9326
|
/** Step 18: Setup summary — displays all configuration choices made during setup. */
|
|
8723
|
-
function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform) {
|
|
9327
|
+
function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, plat = process.platform, aibroker, failures = []) {
|
|
8724
9328
|
section("Setup Complete");
|
|
8725
9329
|
line();
|
|
8726
9330
|
console.log(chalk.green(" PAI Knowledge OS is configured!"));
|
|
@@ -8741,6 +9345,16 @@ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiStee
|
|
|
8741
9345
|
console.log(chalk.dim(" Settings: ") + chalk.cyan(settingsPatched ? "env vars, hooks, permissions, flags (patched)" : "(unchanged)"));
|
|
8742
9346
|
console.log(chalk.dim(" Daemon: ") + chalk.cyan(daemonInstalled ? `${daemonServiceName(plat)} (installed)` : "(not installed by setup)"));
|
|
8743
9347
|
console.log(chalk.dim(" MCP: ") + chalk.cyan(mcpRegistered ? "registered in ~/.claude.json" : "(unchanged)"));
|
|
9348
|
+
if (aibroker) {
|
|
9349
|
+
const label = {
|
|
9350
|
+
ok: "installed and healthy",
|
|
9351
|
+
installed: "installed",
|
|
9352
|
+
skipped: "skipped",
|
|
9353
|
+
failed: "FAILED — retry: npm install -g aibroker@^0.62.4 && aibroker setup && aibroker doctor"
|
|
9354
|
+
}[aibroker];
|
|
9355
|
+
console.log(chalk.dim(" AIBroker: ") + (aibroker === "failed" ? chalk.red(label) : chalk.cyan(label)));
|
|
9356
|
+
}
|
|
9357
|
+
for (const f of failures) console.log(chalk.red(` ${f}`));
|
|
8744
9358
|
line();
|
|
8745
9359
|
console.log(chalk.bold.yellow(" → RESTART Claude Code to activate all changes."));
|
|
8746
9360
|
line();
|
|
@@ -9395,7 +10009,7 @@ async function stepTaskBus(rl) {
|
|
|
9395
10009
|
line();
|
|
9396
10010
|
line(" PAI works fully without this. Skip it unless you want cross-session tasks.");
|
|
9397
10011
|
line();
|
|
9398
|
-
if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false)) {
|
|
10012
|
+
if (!await promptYesNo(rl, "Enable the task bus (requires a Todoist account)?", false, "task-bus")) {
|
|
9399
10013
|
line();
|
|
9400
10014
|
console.log(c.ok("Skipping the task bus."));
|
|
9401
10015
|
console.log(c.dim(" Add later by re-running `pai setup`."));
|
|
@@ -9482,7 +10096,7 @@ async function stepWorkers(rl) {
|
|
|
9482
10096
|
line(" `pai worker run` is the runner; ps / follow / replay watch the runs;");
|
|
9483
10097
|
line(" the Agent tool is denied and rewritten to it while workers are on.");
|
|
9484
10098
|
line();
|
|
9485
|
-
if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true)) {
|
|
10099
|
+
if (!await promptYesNo(rl, "Turn workers on now with the built-in anthropic provider?", true, "workers")) {
|
|
9486
10100
|
line();
|
|
9487
10101
|
console.log(c.ok("Skipping workers."));
|
|
9488
10102
|
console.log(c.dim(" Later: pai worker on (other providers: pai worker providers add <name> … — see docs/worker.md)"));
|
|
@@ -9501,6 +10115,209 @@ async function stepWorkers(rl) {
|
|
|
9501
10115
|
return {};
|
|
9502
10116
|
}
|
|
9503
10117
|
|
|
10118
|
+
//#endregion
|
|
10119
|
+
//#region src/cli/commands/setup/steps/18-aibroker.ts
|
|
10120
|
+
/** Step 18: AIBroker, PAI's messaging layer (session messages, worker reports, tab names, WhatsApp/Telegram). */
|
|
10121
|
+
/** Longest any one AIBroker command may run before it counts as failed. */
|
|
10122
|
+
const COMMAND_TIMEOUT_MS = 5 * 6e4;
|
|
10123
|
+
/** `aibroker setup` may ask questions: give it the terminal when there is one and --yes is off. */
|
|
10124
|
+
const defaultRunner = (cmd, args) => {
|
|
10125
|
+
const r = spawnSync(cmd, args, {
|
|
10126
|
+
encoding: "utf8",
|
|
10127
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
10128
|
+
...cmd === "aibroker" && args[0] === "setup" && !setupOptions.yes && !!process.stdin.isTTY ? { stdio: "inherit" } : {}
|
|
10129
|
+
});
|
|
10130
|
+
return {
|
|
10131
|
+
status: r.error ? null : r.status,
|
|
10132
|
+
output: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? String(r.error.message) : ""}`.trim()
|
|
10133
|
+
};
|
|
10134
|
+
};
|
|
10135
|
+
/** The package the step installs; the range keeps a breaking major from arriving unannounced. */
|
|
10136
|
+
const AIBROKER_PACKAGE = "aibroker@^0.62.4";
|
|
10137
|
+
/** `aibroker` is not on PATH: spawn error (null) or the shell's "not found". */
|
|
10138
|
+
const isMissing = (r) => r.status === null || r.status === 127;
|
|
10139
|
+
/** The commands the step runs when AIBroker is missing or unhealthy, in order. */
|
|
10140
|
+
const AIBROKER_COMMANDS = [
|
|
10141
|
+
["npm", [
|
|
10142
|
+
"install",
|
|
10143
|
+
"-g",
|
|
10144
|
+
AIBROKER_PACKAGE
|
|
10145
|
+
]],
|
|
10146
|
+
["aibroker", ["setup"]],
|
|
10147
|
+
["aibroker", ["doctor"]]
|
|
10148
|
+
];
|
|
10149
|
+
const show = ([cmd, args]) => [cmd, ...args].join(" ");
|
|
10150
|
+
function aibrokerPlan() {
|
|
10151
|
+
return AIBROKER_COMMANDS.map(show);
|
|
10152
|
+
}
|
|
10153
|
+
function stepAibroker(run = defaultRunner, opts = {
|
|
10154
|
+
skip: setupOptions.noAibroker,
|
|
10155
|
+
dryRun: setupOptions.dryRun
|
|
10156
|
+
}) {
|
|
10157
|
+
section("Step 18: AIBroker (Messaging)");
|
|
10158
|
+
line();
|
|
10159
|
+
line(" AIBroker is PAI's messaging layer: session-to-session messages, worker reports,");
|
|
10160
|
+
line(" terminal tab names, WhatsApp/Telegram/PAILot. PAI works without it, minus messaging.");
|
|
10161
|
+
line();
|
|
10162
|
+
if (opts.skip) {
|
|
10163
|
+
console.log(c.dim(" Skipped (--no-aibroker). Install later: npm install -g aibroker@^0.62.4 && aibroker setup"));
|
|
10164
|
+
return "skipped";
|
|
10165
|
+
}
|
|
10166
|
+
if (opts.dryRun) {
|
|
10167
|
+
line(" Would run (if AIBroker is missing or `aibroker doctor` fails):");
|
|
10168
|
+
for (const p of aibrokerPlan()) line(` ${p}`);
|
|
10169
|
+
return "skipped";
|
|
10170
|
+
}
|
|
10171
|
+
const present = run("aibroker", ["doctor"]);
|
|
10172
|
+
if (present.status === 0) {
|
|
10173
|
+
console.log(c.ok("AIBroker already installed and healthy."));
|
|
10174
|
+
return "ok";
|
|
10175
|
+
}
|
|
10176
|
+
const steps = isMissing(present) ? AIBROKER_COMMANDS : AIBROKER_COMMANDS.slice(1);
|
|
10177
|
+
for (const step of steps) {
|
|
10178
|
+
const r = run(step[0], step[1]);
|
|
10179
|
+
if (r.status !== 0) {
|
|
10180
|
+
console.log(c.warn(` \`${show(step)}\` failed (exit ${r.status ?? "n/a"}).`));
|
|
10181
|
+
if (r.output) line(` ${r.output.split("\n").join("\n ")}`);
|
|
10182
|
+
console.log(c.warn(` Retry with: ${aibrokerPlan().slice(AIBROKER_COMMANDS.indexOf(step)).join(" && ")}`));
|
|
10183
|
+
return "failed";
|
|
10184
|
+
}
|
|
10185
|
+
console.log(c.ok(`${show(step)}: ok`));
|
|
10186
|
+
}
|
|
10187
|
+
console.log(c.ok("AIBroker installed."));
|
|
10188
|
+
return "installed";
|
|
10189
|
+
}
|
|
10190
|
+
|
|
10191
|
+
//#endregion
|
|
10192
|
+
//#region src/cli/commands/setup/steps/19-router.ts
|
|
10193
|
+
/** Step 19: the model router and its cockpit (enabled, port, reach). Answers go to config.yaml as router.enabled / router.port / router.bind. */
|
|
10194
|
+
/** Tries to listen on `host` (loopback by default); when taken, asks lsof who holds it. */
|
|
10195
|
+
const defaultPortProbe = (port, host = "127.0.0.1") => new Promise((resolve) => {
|
|
10196
|
+
const srv = createServer();
|
|
10197
|
+
srv.once("error", () => {
|
|
10198
|
+
resolve({
|
|
10199
|
+
free: false,
|
|
10200
|
+
holder: (spawnSync("lsof", [
|
|
10201
|
+
"-nP",
|
|
10202
|
+
`-iTCP:${port}`,
|
|
10203
|
+
"-sTCP:LISTEN"
|
|
10204
|
+
], { encoding: "utf8" }).stdout ?? "").split("\n")[1]?.trim().split(/\s+/).slice(0, 2).join(" pid ") || void 0
|
|
10205
|
+
});
|
|
10206
|
+
});
|
|
10207
|
+
srv.listen(port, host, () => srv.close(() => resolve({ free: true })));
|
|
10208
|
+
});
|
|
10209
|
+
function validPort(v) {
|
|
10210
|
+
const n = typeof v === "number" ? v : /^\d+$/.test(String(v).trim()) ? Number(v) : NaN;
|
|
10211
|
+
return Number.isInteger(n) && n >= 1024 && n <= 65535 ? n : null;
|
|
10212
|
+
}
|
|
10213
|
+
/** `loopback`, `tailscale` or a dotted IPv4 (never 0.0.0.0); returns the normalised value or null. */
|
|
10214
|
+
function validBind(v) {
|
|
10215
|
+
const b = v.trim().toLowerCase();
|
|
10216
|
+
return resolveBindAddress(b) === null && b !== "tailscale" ? null : b || "loopback";
|
|
10217
|
+
}
|
|
10218
|
+
async function stepRouter(rl, probe = defaultPortProbe, runningPid = routerRunningPid) {
|
|
10219
|
+
section("Step 19: Router and Cockpit (Optional)");
|
|
10220
|
+
line();
|
|
10221
|
+
line(" The router accounts model usage, schedules local models and serves the cockpit page.");
|
|
10222
|
+
line(" The daemon keeps it running when enabled.");
|
|
10223
|
+
line();
|
|
10224
|
+
const cfg = loadConfig().router;
|
|
10225
|
+
const raw = readConfigRaw().router ?? {};
|
|
10226
|
+
const flags = setupOptions.router ?? {};
|
|
10227
|
+
if (!(flags.enabled ?? await promptYesNo(rl, "Enable the router and cockpit?", typeof raw.enabled === "boolean" ? raw.enabled : true))) {
|
|
10228
|
+
console.log(c.ok("Router disabled. Enable later: pai config set router.enabled true"));
|
|
10229
|
+
return { router: {
|
|
10230
|
+
...raw,
|
|
10231
|
+
enabled: false
|
|
10232
|
+
} };
|
|
10233
|
+
}
|
|
10234
|
+
const routerUp = runningPid() !== null;
|
|
10235
|
+
const check = async (port, host) => {
|
|
10236
|
+
if (routerUp && port === cfg.port) return null;
|
|
10237
|
+
const p = await probe(port, host);
|
|
10238
|
+
return p.free ? null : `Port ${port} is in use${p.holder ? ` by ${p.holder}` : ""}.`;
|
|
10239
|
+
};
|
|
10240
|
+
const unavailable = (why) => {
|
|
10241
|
+
console.log(c.warn(` ${why}`));
|
|
10242
|
+
setupFailures.push(`Router not configured: ${why} Retry: pai setup --router-port <free port>`);
|
|
10243
|
+
return {};
|
|
10244
|
+
};
|
|
10245
|
+
let port;
|
|
10246
|
+
if (flags.port !== void 0) {
|
|
10247
|
+
const v = validPort(flags.port);
|
|
10248
|
+
if (v === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${flags.port}"`);
|
|
10249
|
+
const taken = await check(v);
|
|
10250
|
+
if (taken) return unavailable(taken);
|
|
10251
|
+
port = v;
|
|
10252
|
+
} else {
|
|
10253
|
+
port = cfg.port;
|
|
10254
|
+
for (;;) {
|
|
10255
|
+
const answer = await prompt(rl, chalk.bold(` Cockpit port [${cfg.port}]: `), "");
|
|
10256
|
+
const v = answer === "" ? cfg.port : validPort(answer);
|
|
10257
|
+
if (v === null) {
|
|
10258
|
+
console.log(c.warn("Enter a port from 1024 to 65535."));
|
|
10259
|
+
continue;
|
|
10260
|
+
}
|
|
10261
|
+
const taken = await check(v);
|
|
10262
|
+
if (!taken) {
|
|
10263
|
+
port = v;
|
|
10264
|
+
break;
|
|
10265
|
+
}
|
|
10266
|
+
if (setupOptions.yes) return unavailable(taken);
|
|
10267
|
+
console.log(c.warn(taken));
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10270
|
+
let bind;
|
|
10271
|
+
if (flags.bind !== void 0) {
|
|
10272
|
+
const v = validBind(flags.bind);
|
|
10273
|
+
if (v === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${flags.bind}"`);
|
|
10274
|
+
bind = v;
|
|
10275
|
+
} else {
|
|
10276
|
+
const current = String(raw.bind ?? "loopback");
|
|
10277
|
+
const explicit = current !== "loopback" && current !== "tailscale" && validBind(current) !== null ? current : void 0;
|
|
10278
|
+
line();
|
|
10279
|
+
line(" Who can reach the cockpit? Beyond loopback it is reachable from other devices (behind a token).");
|
|
10280
|
+
line();
|
|
10281
|
+
const idx = await promptMenu(rl, [
|
|
10282
|
+
{
|
|
10283
|
+
label: "loopback",
|
|
10284
|
+
description: "This machine only (127.0.0.1)."
|
|
10285
|
+
},
|
|
10286
|
+
{
|
|
10287
|
+
label: "tailscale",
|
|
10288
|
+
description: "Your other Tailscale devices."
|
|
10289
|
+
},
|
|
10290
|
+
{
|
|
10291
|
+
label: "explicit address",
|
|
10292
|
+
description: "A specific IPv4 address of this machine."
|
|
10293
|
+
}
|
|
10294
|
+
], current === "tailscale" ? 1 : explicit ? 2 : 0);
|
|
10295
|
+
bind = idx === 0 ? "loopback" : "tailscale";
|
|
10296
|
+
if (idx === 2) for (;;) {
|
|
10297
|
+
const a = validBind(await prompt(rl, chalk.bold(` IPv4 address${explicit ? ` [${explicit}]` : ""}: `), explicit ?? ""));
|
|
10298
|
+
if (a && a !== "tailscale") {
|
|
10299
|
+
bind = a;
|
|
10300
|
+
break;
|
|
10301
|
+
}
|
|
10302
|
+
console.log(c.warn("Enter a dotted IPv4 address (not 0.0.0.0)."));
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
const host = bind === "loopback" ? void 0 : resolveBindAddress(bind) ?? void 0;
|
|
10306
|
+
if (host && !(routerUp && port === cfg.port)) {
|
|
10307
|
+
const taken = await check(port, host);
|
|
10308
|
+
if (taken) return unavailable(`${taken.slice(0, -1)} on ${host}.`);
|
|
10309
|
+
}
|
|
10310
|
+
if (bind !== "loopback") console.log(c.warn(` The cockpit will be reachable from other devices via ${bind}; it asks for the token in ~/.pai/router-cockpit.token.`));
|
|
10311
|
+
if (bind === "tailscale" && resolveBindAddress("tailscale") === null) console.log(c.warn(" Tailscale is not up here; the extra listener starts once it is."));
|
|
10312
|
+
console.log(c.ok(`Router enabled on port ${port}, reach: ${bind}.`));
|
|
10313
|
+
return { router: {
|
|
10314
|
+
...raw,
|
|
10315
|
+
enabled: true,
|
|
10316
|
+
port,
|
|
10317
|
+
bind
|
|
10318
|
+
} };
|
|
10319
|
+
}
|
|
10320
|
+
|
|
9504
10321
|
//#endregion
|
|
9505
10322
|
//#region src/cli/commands/setup/index.ts
|
|
9506
10323
|
/**
|
|
@@ -9516,6 +10333,11 @@ async function step(name, fn) {
|
|
|
9516
10333
|
}
|
|
9517
10334
|
}
|
|
9518
10335
|
async function runSetup() {
|
|
10336
|
+
setupFailures.length = 0;
|
|
10337
|
+
if (setupOptions.dryRun) {
|
|
10338
|
+
stepAibroker();
|
|
10339
|
+
return;
|
|
10340
|
+
}
|
|
9519
10341
|
const rl = createRl();
|
|
9520
10342
|
try {
|
|
9521
10343
|
if (existsSync(CONFIG_FILE$2)) {
|
|
@@ -9546,6 +10368,8 @@ async function runSetup() {
|
|
|
9546
10368
|
const daName = await step("stepDaName", () => stepDaName(rl));
|
|
9547
10369
|
const settingsPatched = await step("stepSettings", () => stepSettings(rl, daName));
|
|
9548
10370
|
const daemonInstalled = await step("stepDaemon", () => stepDaemon(rl));
|
|
10371
|
+
const routerConfig = await step("stepRouter", () => stepRouter(rl));
|
|
10372
|
+
const aibroker = await step("stepAibroker", () => stepAibroker());
|
|
9549
10373
|
const mcpRegistered = await step("stepMcp", () => stepMcp(rl));
|
|
9550
10374
|
await step("stepDirectories", () => stepDirectories(rl));
|
|
9551
10375
|
const taskConfig = await step("stepTaskBus", () => stepTaskBus(rl));
|
|
@@ -9554,50 +10378,338 @@ async function runSetup() {
|
|
|
9554
10378
|
...storageConfig,
|
|
9555
10379
|
...embeddingConfig,
|
|
9556
10380
|
...taskConfig,
|
|
9557
|
-
...workersConfig
|
|
10381
|
+
...workersConfig,
|
|
10382
|
+
...routerConfig
|
|
9558
10383
|
};
|
|
9559
10384
|
mergeConfig(allUpdates);
|
|
9560
10385
|
line();
|
|
9561
10386
|
console.log(chalk.green(" Configuration saved."));
|
|
9562
10387
|
await step("stepInitialIndex", () => stepInitialIndex(rl));
|
|
9563
|
-
stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered);
|
|
10388
|
+
stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered, process.platform, aibroker, setupFailures);
|
|
9564
10389
|
} finally {
|
|
9565
10390
|
rl.close();
|
|
9566
10391
|
}
|
|
9567
10392
|
}
|
|
9568
10393
|
|
|
10394
|
+
//#endregion
|
|
10395
|
+
//#region src/cli/commands/setup/questions.ts
|
|
10396
|
+
readConfigRaw();
|
|
10397
|
+
/** yes/no components: key = setupOptions.answers key, flag form depends on the default. */
|
|
10398
|
+
const COMPONENTS = [
|
|
10399
|
+
{
|
|
10400
|
+
key: "claude-md",
|
|
10401
|
+
step: "Agent configuration",
|
|
10402
|
+
question: "Write ~/.claude/CLAUDE.md from the PAI template?",
|
|
10403
|
+
default: true
|
|
10404
|
+
},
|
|
10405
|
+
{
|
|
10406
|
+
key: "skill",
|
|
10407
|
+
step: "PAI skill",
|
|
10408
|
+
question: "Install the PAI skill?",
|
|
10409
|
+
default: true
|
|
10410
|
+
},
|
|
10411
|
+
{
|
|
10412
|
+
key: "steering-rules",
|
|
10413
|
+
step: "Steering rules",
|
|
10414
|
+
question: "Install the AI steering rules?",
|
|
10415
|
+
default: true
|
|
10416
|
+
},
|
|
10417
|
+
{
|
|
10418
|
+
key: "skill-stubs",
|
|
10419
|
+
step: "Skill stubs",
|
|
10420
|
+
question: "Symlink the PAI skill stubs into ~/.claude/skills/?",
|
|
10421
|
+
default: true
|
|
10422
|
+
},
|
|
10423
|
+
{
|
|
10424
|
+
key: "hooks",
|
|
10425
|
+
step: "Hooks",
|
|
10426
|
+
question: "Install the lifecycle hooks (session stop, pre-compact, statusline)?",
|
|
10427
|
+
default: true
|
|
10428
|
+
},
|
|
10429
|
+
{
|
|
10430
|
+
key: "ts-hooks",
|
|
10431
|
+
step: "TypeScript hooks",
|
|
10432
|
+
question: "Install the TypeScript hooks to ~/.claude/Hooks/?",
|
|
10433
|
+
default: true
|
|
10434
|
+
},
|
|
10435
|
+
{
|
|
10436
|
+
key: "settings",
|
|
10437
|
+
step: "Settings",
|
|
10438
|
+
question: "Patch ~/.claude/settings.json with PAI hooks, env vars and settings?",
|
|
10439
|
+
default: true
|
|
10440
|
+
},
|
|
10441
|
+
{
|
|
10442
|
+
key: "daemon",
|
|
10443
|
+
step: "Daemon",
|
|
10444
|
+
question: "Install the daemon as a login service?",
|
|
10445
|
+
default: true
|
|
10446
|
+
},
|
|
10447
|
+
{
|
|
10448
|
+
key: "mcp",
|
|
10449
|
+
step: "MCP",
|
|
10450
|
+
question: "Register the PAI MCP server in ~/.claude.json?",
|
|
10451
|
+
default: true
|
|
10452
|
+
},
|
|
10453
|
+
{
|
|
10454
|
+
key: "registry-scan",
|
|
10455
|
+
step: "Directories",
|
|
10456
|
+
question: "Run `pai registry scan` after setup?",
|
|
10457
|
+
default: false
|
|
10458
|
+
},
|
|
10459
|
+
{
|
|
10460
|
+
key: "task-bus",
|
|
10461
|
+
step: "Task bus",
|
|
10462
|
+
question: "Enable the task bus (needs a Todoist account)?",
|
|
10463
|
+
default: false
|
|
10464
|
+
},
|
|
10465
|
+
{
|
|
10466
|
+
key: "workers",
|
|
10467
|
+
step: "Workers",
|
|
10468
|
+
question: "Turn workers on with the built-in anthropic provider?",
|
|
10469
|
+
default: true
|
|
10470
|
+
}
|
|
10471
|
+
];
|
|
10472
|
+
/** Components whose answer is not a plain default. */
|
|
10473
|
+
const COMPONENT_NOTES = {
|
|
10474
|
+
"registry-scan": "Off by default: scanning walks your project directories.",
|
|
10475
|
+
"task-bus": "Off by default. Needs a Todoist token in the config; with --yes the token cannot be asked, so set it first."
|
|
10476
|
+
};
|
|
10477
|
+
const componentFlag = (key, def) => def ? `--no-${key}` : `--${key}`;
|
|
10478
|
+
function setupQuestions() {
|
|
10479
|
+
const raw = readConfigRaw();
|
|
10480
|
+
const router = loadConfig().router;
|
|
10481
|
+
const rawRouter = raw.router ?? {};
|
|
10482
|
+
const q = [];
|
|
10483
|
+
const cur = (v) => v === void 0 || v === null || v === "" ? {} : { current: v };
|
|
10484
|
+
q.push({
|
|
10485
|
+
id: "storage",
|
|
10486
|
+
step: "Storage",
|
|
10487
|
+
question: "Where should PAI store its data?",
|
|
10488
|
+
type: "choice",
|
|
10489
|
+
flag: "--storage",
|
|
10490
|
+
default: "sqlite",
|
|
10491
|
+
allowed: ["sqlite", "postgres"],
|
|
10492
|
+
...cur(raw.storageBackend),
|
|
10493
|
+
note: "With --yes and no --storage: postgres when a local PostgreSQL answers, else sqlite."
|
|
10494
|
+
});
|
|
10495
|
+
q.push({
|
|
10496
|
+
id: "pg-url",
|
|
10497
|
+
step: "Storage",
|
|
10498
|
+
question: "PostgreSQL connection string (only when storage is postgres; default: the PAI Docker container)",
|
|
10499
|
+
type: "string",
|
|
10500
|
+
flag: "--pg-url",
|
|
10501
|
+
default: null,
|
|
10502
|
+
...cur((raw.postgres ?? {}).connectionString)
|
|
10503
|
+
});
|
|
10504
|
+
q.push({
|
|
10505
|
+
id: "embedding",
|
|
10506
|
+
step: "Embedding",
|
|
10507
|
+
question: "Which embedding model for semantic search?",
|
|
10508
|
+
type: "choice",
|
|
10509
|
+
flag: "--embedding",
|
|
10510
|
+
default: "snowflake",
|
|
10511
|
+
allowed: [
|
|
10512
|
+
"snowflake",
|
|
10513
|
+
"bge",
|
|
10514
|
+
"nomic",
|
|
10515
|
+
"ollama",
|
|
10516
|
+
"none"
|
|
10517
|
+
],
|
|
10518
|
+
...cur(raw.embeddingModel)
|
|
10519
|
+
});
|
|
10520
|
+
const component = (comp) => {
|
|
10521
|
+
const note = COMPONENT_NOTES[comp.key];
|
|
10522
|
+
q.push({
|
|
10523
|
+
id: comp.key,
|
|
10524
|
+
step: comp.step,
|
|
10525
|
+
question: comp.question,
|
|
10526
|
+
type: "bool",
|
|
10527
|
+
flag: componentFlag(comp.key, comp.default),
|
|
10528
|
+
default: comp.default,
|
|
10529
|
+
...note ? { note } : {}
|
|
10530
|
+
});
|
|
10531
|
+
};
|
|
10532
|
+
const routerQuestions = () => {
|
|
10533
|
+
q.push({
|
|
10534
|
+
id: "router",
|
|
10535
|
+
step: "Router and cockpit",
|
|
10536
|
+
question: "Enable the router and cockpit?",
|
|
10537
|
+
type: "bool",
|
|
10538
|
+
flag: "--router / --no-router",
|
|
10539
|
+
default: true,
|
|
10540
|
+
...cur(typeof rawRouter.enabled === "boolean" ? rawRouter.enabled : void 0),
|
|
10541
|
+
note: "Setup defaults to on; the daemon's own default (key absent) is off. A re-run with router.enabled absent turns it on unless --no-router is given."
|
|
10542
|
+
});
|
|
10543
|
+
q.push({
|
|
10544
|
+
id: "router-port",
|
|
10545
|
+
step: "Router and cockpit",
|
|
10546
|
+
question: "Port for the cockpit?",
|
|
10547
|
+
type: "port",
|
|
10548
|
+
flag: "--router-port",
|
|
10549
|
+
default: router.port,
|
|
10550
|
+
...cur(rawRouter.port)
|
|
10551
|
+
});
|
|
10552
|
+
q.push({
|
|
10553
|
+
id: "router-bind",
|
|
10554
|
+
step: "Router and cockpit",
|
|
10555
|
+
question: "Who can reach the cockpit? Beyond loopback it is reachable from other devices.",
|
|
10556
|
+
type: "choice",
|
|
10557
|
+
flag: "--router-bind",
|
|
10558
|
+
default: "loopback",
|
|
10559
|
+
allowed: [
|
|
10560
|
+
"loopback",
|
|
10561
|
+
"tailscale",
|
|
10562
|
+
"<ipv4 address>"
|
|
10563
|
+
],
|
|
10564
|
+
...cur(rawRouter.bind)
|
|
10565
|
+
});
|
|
10566
|
+
};
|
|
10567
|
+
const daName = () => q.push({
|
|
10568
|
+
id: "da-name",
|
|
10569
|
+
step: "Settings",
|
|
10570
|
+
question: "What should your assistant be called?",
|
|
10571
|
+
type: "string",
|
|
10572
|
+
flag: "--da-name",
|
|
10573
|
+
default: "PAI"
|
|
10574
|
+
});
|
|
10575
|
+
const aibroker = () => q.push({
|
|
10576
|
+
id: "aibroker",
|
|
10577
|
+
step: "AIBroker",
|
|
10578
|
+
question: "Install AIBroker (messaging)?",
|
|
10579
|
+
type: "bool",
|
|
10580
|
+
flag: "--no-aibroker",
|
|
10581
|
+
default: true
|
|
10582
|
+
});
|
|
10583
|
+
for (const comp of COMPONENTS) {
|
|
10584
|
+
if (comp.key === "settings") daName();
|
|
10585
|
+
component(comp);
|
|
10586
|
+
if (comp.key === "daemon") {
|
|
10587
|
+
routerQuestions();
|
|
10588
|
+
aibroker();
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
return q;
|
|
10592
|
+
}
|
|
10593
|
+
function renderQuestions(qs) {
|
|
10594
|
+
return qs.map((x, i) => {
|
|
10595
|
+
const parts = [`${i + 1}. [${x.step}] ${x.question}`, ` flag: ${x.flag} default: ${x.default ?? "(none)"}`];
|
|
10596
|
+
if (x.allowed) parts.push(` allowed: ${x.allowed.join(" | ")}`);
|
|
10597
|
+
if (x.current !== void 0) parts.push(` current: ${x.current} (already set)`);
|
|
10598
|
+
if (x.note) parts.push(` note: ${x.note}`);
|
|
10599
|
+
return parts.join("\n");
|
|
10600
|
+
}).join("\n");
|
|
10601
|
+
}
|
|
10602
|
+
|
|
9569
10603
|
//#endregion
|
|
9570
10604
|
//#region src/cli/commands/setup.ts
|
|
9571
10605
|
/** `pai setup`: the first-time setup wizard (steps live in ./setup/). */
|
|
10606
|
+
/** One flag per yes/no component: --no-<name> for default-yes ones, --<name> for default-no ones. */
|
|
10607
|
+
const componentOptions = COMPONENTS.map((comp) => ({
|
|
10608
|
+
name: componentFlag(comp.key, comp.default).slice(2),
|
|
10609
|
+
type: "boolean",
|
|
10610
|
+
default: false,
|
|
10611
|
+
summary: `${comp.default ? "Skip" : "Do"}: ${comp.question}`
|
|
10612
|
+
}));
|
|
9572
10613
|
const setupCommand = {
|
|
9573
10614
|
name: "setup",
|
|
9574
|
-
summary: "First-time setup: storage, search, hooks, daemon",
|
|
9575
|
-
description: "Walks through everything PAI needs: where data is stored, search embeddings, Claude Code hooks and settings, the background daemon and the first index.\nSafe to run again. With --yes it takes every default and asks nothing.",
|
|
9576
|
-
options: [
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
10615
|
+
summary: "First-time setup: storage, search, hooks, daemon, router",
|
|
10616
|
+
description: "Walks through everything PAI needs: where data is stored, search embeddings, Claude Code hooks and settings, the background daemon, the router and cockpit (port, reach), AIBroker (messaging; installed automatically) and the first index.\nSafe to run again. With --yes it takes every default and asks nothing; every question has a flag, so `pai setup --yes <flags>` can express any answer. `pai setup --questions [--json]` lists the questions with their flags, defaults and current values (for callers without a terminal).",
|
|
10617
|
+
options: [
|
|
10618
|
+
{
|
|
10619
|
+
name: "yes",
|
|
10620
|
+
short: "y",
|
|
10621
|
+
type: "boolean",
|
|
10622
|
+
default: false,
|
|
10623
|
+
summary: "Take every default (or the flag given) and read no input"
|
|
10624
|
+
},
|
|
10625
|
+
{
|
|
10626
|
+
name: "questions",
|
|
10627
|
+
type: "boolean",
|
|
10628
|
+
default: false,
|
|
10629
|
+
summary: "Print every question with its flag, default and allowed values; change nothing"
|
|
10630
|
+
},
|
|
10631
|
+
{
|
|
10632
|
+
name: "json",
|
|
10633
|
+
type: "boolean",
|
|
10634
|
+
default: false,
|
|
10635
|
+
summary: "With --questions: machine-readable JSON"
|
|
10636
|
+
},
|
|
10637
|
+
{
|
|
10638
|
+
name: "storage",
|
|
10639
|
+
type: "string",
|
|
10640
|
+
value: "BACKEND",
|
|
10641
|
+
summary: "Where to store data: sqlite or postgres (with --yes: sqlite unless a local Postgres answers)"
|
|
10642
|
+
},
|
|
10643
|
+
{
|
|
10644
|
+
name: "pg-url",
|
|
10645
|
+
type: "string",
|
|
10646
|
+
value: "URL",
|
|
10647
|
+
summary: "PostgreSQL connection string (implies --storage postgres)"
|
|
10648
|
+
},
|
|
10649
|
+
{
|
|
10650
|
+
name: "embedding",
|
|
10651
|
+
type: "string",
|
|
10652
|
+
value: "MODEL",
|
|
10653
|
+
summary: "Embeddings: snowflake, bge, nomic, ollama (Arctic on Ollama) or none"
|
|
10654
|
+
},
|
|
10655
|
+
{
|
|
10656
|
+
name: "da-name",
|
|
10657
|
+
type: "string",
|
|
10658
|
+
value: "NAME",
|
|
10659
|
+
summary: "What the assistant is called (default PAI)"
|
|
10660
|
+
},
|
|
10661
|
+
{
|
|
10662
|
+
name: "router",
|
|
10663
|
+
type: "boolean",
|
|
10664
|
+
summary: "Enable the router and cockpit (default)"
|
|
10665
|
+
},
|
|
10666
|
+
{
|
|
10667
|
+
name: "no-router",
|
|
10668
|
+
type: "boolean",
|
|
10669
|
+
summary: "Do not enable the router and cockpit"
|
|
10670
|
+
},
|
|
10671
|
+
{
|
|
10672
|
+
name: "router-port",
|
|
10673
|
+
type: "number",
|
|
10674
|
+
value: "PORT",
|
|
10675
|
+
summary: "Cockpit port (default: config router.port, 8790)"
|
|
10676
|
+
},
|
|
10677
|
+
{
|
|
10678
|
+
name: "router-bind",
|
|
10679
|
+
type: "string",
|
|
10680
|
+
value: "SCOPE",
|
|
10681
|
+
summary: "Who reaches the cockpit: loopback (default), tailscale or an IPv4 address"
|
|
10682
|
+
},
|
|
10683
|
+
{
|
|
10684
|
+
name: "no-aibroker",
|
|
10685
|
+
type: "boolean",
|
|
10686
|
+
default: false,
|
|
10687
|
+
summary: "Do not install AIBroker (the messaging layer)"
|
|
10688
|
+
},
|
|
10689
|
+
...componentOptions,
|
|
10690
|
+
{
|
|
10691
|
+
name: "dry-run",
|
|
10692
|
+
type: "boolean",
|
|
10693
|
+
default: false,
|
|
10694
|
+
summary: "Validate the flags, print the AIBroker install commands and change nothing (other steps are not previewed)"
|
|
10695
|
+
}
|
|
10696
|
+
],
|
|
10697
|
+
examples: [
|
|
10698
|
+
"pai setup",
|
|
10699
|
+
"pai setup --yes --storage sqlite",
|
|
10700
|
+
"pai setup --questions --json",
|
|
10701
|
+
"pai setup --yes --storage sqlite --embedding none --router-port 9000 --router-bind tailscale --no-hooks"
|
|
10702
|
+
],
|
|
9589
10703
|
seeAlso: ["pai update", "pai daemon status"],
|
|
9590
10704
|
run(c) {
|
|
9591
|
-
const storage = c.opts.storage;
|
|
9592
10705
|
return legacy(async () => {
|
|
9593
|
-
if (
|
|
9594
|
-
|
|
9595
|
-
|
|
10706
|
+
if (c.opts.questions === true) {
|
|
10707
|
+
const qs = setupQuestions();
|
|
10708
|
+
console.log(c.opts.json === true ? JSON.stringify(qs, null, 2) : renderQuestions(qs));
|
|
9596
10709
|
return;
|
|
9597
10710
|
}
|
|
9598
|
-
setupOptions.yes = c.opts.yes === true;
|
|
9599
|
-
setupOptions.storage = storage;
|
|
9600
10711
|
try {
|
|
10712
|
+
applySetupFlags(c.opts);
|
|
9601
10713
|
await runSetup();
|
|
9602
10714
|
} catch (e) {
|
|
9603
10715
|
console.error(chalk.red(`pai setup: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -9606,6 +10718,39 @@ const setupCommand = {
|
|
|
9606
10718
|
});
|
|
9607
10719
|
}
|
|
9608
10720
|
};
|
|
10721
|
+
/** Validates the flags and copies them into setupOptions (throws naming the flag). */
|
|
10722
|
+
function applySetupFlags(opts) {
|
|
10723
|
+
const storage = opts.storage;
|
|
10724
|
+
if (storage && storage !== "sqlite" && storage !== "postgres") throw new Error(`--storage must be sqlite or postgres, got "${storage}"`);
|
|
10725
|
+
const embedding = opts.embedding;
|
|
10726
|
+
if (embedding && ![
|
|
10727
|
+
"snowflake",
|
|
10728
|
+
"bge",
|
|
10729
|
+
"nomic",
|
|
10730
|
+
"ollama",
|
|
10731
|
+
"none"
|
|
10732
|
+
].includes(embedding)) throw new Error(`--embedding must be snowflake, bge, nomic, ollama or none, got "${embedding}"`);
|
|
10733
|
+
const port = opts["router-port"];
|
|
10734
|
+
if (port !== void 0 && validPort(port) === null) throw new Error(`--router-port must be a number from 1024 to 65535, got "${port}"`);
|
|
10735
|
+
const bind = opts["router-bind"];
|
|
10736
|
+
if (bind !== void 0 && validBind(String(bind)) === null) throw new Error(`--router-bind must be loopback, tailscale or an IPv4 address (not 0.0.0.0), got "${bind}"`);
|
|
10737
|
+
if (opts.router === true && opts["no-router"] === true) throw new Error("--router and --no-router contradict each other");
|
|
10738
|
+
const answers = {};
|
|
10739
|
+
for (const comp of COMPONENTS) if (opts[componentFlag(comp.key, comp.default).slice(2)] === true) answers[comp.key] = !comp.default;
|
|
10740
|
+
setupOptions.yes = opts.yes === true;
|
|
10741
|
+
setupOptions.noAibroker = opts["no-aibroker"] === true;
|
|
10742
|
+
setupOptions.dryRun = opts["dry-run"] === true;
|
|
10743
|
+
setupOptions.storage = storage;
|
|
10744
|
+
setupOptions.pgUrl = opts["pg-url"];
|
|
10745
|
+
setupOptions.embedding = embedding;
|
|
10746
|
+
setupOptions.daName = opts["da-name"];
|
|
10747
|
+
setupOptions.router = {
|
|
10748
|
+
enabled: opts["no-router"] === true ? false : opts.router === true ? true : void 0,
|
|
10749
|
+
port: opts["router-port"],
|
|
10750
|
+
bind: opts["router-bind"]
|
|
10751
|
+
};
|
|
10752
|
+
setupOptions.answers = answers;
|
|
10753
|
+
}
|
|
9609
10754
|
const installAlias = {
|
|
9610
10755
|
...setupCommand,
|
|
9611
10756
|
name: "install",
|
|
@@ -10399,29 +11544,46 @@ function migrateFederationDbOrphan(dryRun) {
|
|
|
10399
11544
|
}
|
|
10400
11545
|
return migrateOrphanFederationDb({ dryRun });
|
|
10401
11546
|
}
|
|
10402
|
-
|
|
10403
|
-
|
|
11547
|
+
/**
|
|
11548
|
+
* Flush the old DB's WAL into its main file (before the byte copy, so the copy
|
|
11549
|
+
* holds every committed row) and set the emptied sidecars aside. A non-empty
|
|
11550
|
+
* WAL is never moved away without a successful checkpoint.
|
|
11551
|
+
*/
|
|
11552
|
+
function checkpointWal(oldPath, label) {
|
|
11553
|
+
let checkpointed = false;
|
|
10404
11554
|
try {
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
11555
|
+
const db = new BetterSqlite3(oldPath, { timeout: 500 });
|
|
11556
|
+
try {
|
|
11557
|
+
const [row] = db.pragma("wal_checkpoint(TRUNCATE)");
|
|
11558
|
+
checkpointed = row?.busy === 0;
|
|
11559
|
+
} finally {
|
|
11560
|
+
db.close();
|
|
11561
|
+
}
|
|
10411
11562
|
} catch {}
|
|
10412
11563
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
10413
11564
|
for (const suffix of ["-wal", "-shm"]) {
|
|
10414
11565
|
const sidecar = `${oldPath}${suffix}`;
|
|
10415
|
-
if (existsSync(sidecar))
|
|
11566
|
+
if (!existsSync(sidecar)) continue;
|
|
11567
|
+
if (!checkpointed && statSync(sidecar).size > 0) throw new PaiFileMigrationError(`${label}: could not checkpoint ${sidecar} (non-empty WAL, committed rows may live only there) — left in place`);
|
|
11568
|
+
try {
|
|
10416
11569
|
renameSync(sidecar, `${sidecar}.migrated-${stamp}`);
|
|
10417
11570
|
} catch {}
|
|
10418
11571
|
}
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
11572
|
+
}
|
|
11573
|
+
/** PRAGMA integrity_check on the migrated copy, through the node driver. */
|
|
11574
|
+
function verifyIntegrity(newPath) {
|
|
11575
|
+
let check;
|
|
11576
|
+
try {
|
|
11577
|
+
const db = new BetterSqlite3(newPath, { readonly: true });
|
|
11578
|
+
try {
|
|
11579
|
+
check = String(db.pragma("integrity_check", { simple: true }));
|
|
11580
|
+
} finally {
|
|
11581
|
+
db.close();
|
|
11582
|
+
}
|
|
10422
11583
|
} catch (e) {
|
|
10423
|
-
|
|
11584
|
+
throw new PaiFileMigrationError(`${newPath}: integrity_check could not run (${String(e)})`);
|
|
10424
11585
|
}
|
|
11586
|
+
if (check !== "ok") throw new PaiFileMigrationError(`${newPath}: PRAGMA integrity_check reported "${check}" — investigate before trusting this copy`);
|
|
10425
11587
|
}
|
|
10426
11588
|
/**
|
|
10427
11589
|
* ~/.pai/registry.db — a third per-user location, alongside PAI_HOME and the
|
|
@@ -10448,8 +11610,9 @@ function migrateRegistryDbFile(dryRun) {
|
|
|
10448
11610
|
execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
|
|
10449
11611
|
execFileSync("sleep", ["2"]);
|
|
10450
11612
|
}
|
|
11613
|
+
checkpointWal(oldPath, "registry database");
|
|
10451
11614
|
const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
|
|
10452
|
-
if (result.fromPath)
|
|
11615
|
+
if (result.fromPath) verifyIntegrity(newPath);
|
|
10453
11616
|
return result;
|
|
10454
11617
|
}
|
|
10455
11618
|
/**
|
|
@@ -10477,8 +11640,9 @@ function migrateFederationDbFile(dryRun) {
|
|
|
10477
11640
|
execFileSync("pai", ["daemon", "restart"], { stdio: "inherit" });
|
|
10478
11641
|
execFileSync("sleep", ["2"]);
|
|
10479
11642
|
}
|
|
11643
|
+
checkpointWal(oldPath, "federation database");
|
|
10480
11644
|
const result = migratePaiFile(newPath, [oldPath], { dryRun: false });
|
|
10481
|
-
if (result.fromPath)
|
|
11645
|
+
if (result.fromPath) verifyIntegrity(newPath);
|
|
10482
11646
|
return result;
|
|
10483
11647
|
}
|
|
10484
11648
|
|
|
@@ -10952,8 +12116,8 @@ async function escalate(task, headline, detail, state, now, opts) {
|
|
|
10952
12116
|
state.alarmedAt[task.id] = now;
|
|
10953
12117
|
let delivered = false;
|
|
10954
12118
|
try {
|
|
10955
|
-
const { routeNotification } = await import("../router-
|
|
10956
|
-
const { loadConfig } = await import("../config-
|
|
12119
|
+
const { routeNotification } = await import("../router-DwHJRgG9.mjs");
|
|
12120
|
+
const { loadConfig } = await import("../config-DtENJ6cL.mjs");
|
|
10957
12121
|
delivered = (await routeNotification({
|
|
10958
12122
|
event: "error",
|
|
10959
12123
|
title: `PAI: ${headline}`,
|
|
@@ -11059,7 +12223,7 @@ const GROSS_OVERRUN_FACTOR = 5;
|
|
|
11059
12223
|
*/
|
|
11060
12224
|
function detectWebhook(bin = "aibroker") {
|
|
11061
12225
|
try {
|
|
11062
|
-
|
|
12226
|
+
return webhookFromStatus(execFileSync(bin, ["todoist", "status"], {
|
|
11063
12227
|
timeout: 1e4,
|
|
11064
12228
|
encoding: "utf-8",
|
|
11065
12229
|
stdio: [
|
|
@@ -11067,13 +12231,21 @@ function detectWebhook(bin = "aibroker") {
|
|
|
11067
12231
|
"pipe",
|
|
11068
12232
|
"pipe"
|
|
11069
12233
|
]
|
|
11070
|
-
});
|
|
11071
|
-
return /authoris|authoriz|token on file|grant/i.test(out) && !/no (token|grant)/i.test(out);
|
|
12234
|
+
}));
|
|
11072
12235
|
} catch {
|
|
11073
12236
|
return false;
|
|
11074
12237
|
}
|
|
11075
12238
|
}
|
|
11076
12239
|
/**
|
|
12240
|
+
* Read `aibroker todoist status` output. Only an explicit positive line counts:
|
|
12241
|
+
* "Authorised <timestamp>" and no "FAILING" verdict. Negative texts ("No Todoist
|
|
12242
|
+
* authorisation on file", "Not authorized", "grant: missing") and a token that
|
|
12243
|
+
* is on file but rejected both read as no webhook.
|
|
12244
|
+
*/
|
|
12245
|
+
function webhookFromStatus(out) {
|
|
12246
|
+
return /^Authori[sz]ed\s+\S/m.test(out) && !/^FAILING\b/m.test(out);
|
|
12247
|
+
}
|
|
12248
|
+
/**
|
|
11077
12249
|
* Discard restore entries whose run is no longer in flight.
|
|
11078
12250
|
*
|
|
11079
12251
|
* `triggeredRestore` belongs to one run: it records the occurrence that run
|
|
@@ -11118,83 +12290,93 @@ async function tick(opts) {
|
|
|
11118
12290
|
const ordered = dispatchOrder(tasks);
|
|
11119
12291
|
if (!opts.dryRun) dropOrphanedRestores(tasks, state);
|
|
11120
12292
|
const webhookActive = opts.webhookActive ?? detectWebhook();
|
|
11121
|
-
|
|
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
|
-
|
|
12293
|
+
try {
|
|
12294
|
+
for (const task of ordered) {
|
|
12295
|
+
const prevDue = state.lastSeenDue[task.id];
|
|
12296
|
+
const d = decide(task, {
|
|
12297
|
+
now,
|
|
12298
|
+
state,
|
|
12299
|
+
history: state.history,
|
|
12300
|
+
lastSeenDue: state.lastSeenDue,
|
|
12301
|
+
claimSeenAt: state.claimSeenAt,
|
|
12302
|
+
webhookActive
|
|
12303
|
+
});
|
|
12304
|
+
let note = "";
|
|
12305
|
+
try {
|
|
12306
|
+
if (!opts.dryRun && task.due) state.lastSeenDue[task.id] = task.due;
|
|
12307
|
+
if (!opts.dryRun) if (isRunning(task)) {
|
|
12308
|
+
if (state.claimSeenAt[task.id] === void 0 && task.recurrence && prevDue !== void 0) {
|
|
12309
|
+
if (wasTicked(task, prevDue)) state.triggeredRestore[task.id] = restoreDueString(task.recurrence, prevDue);
|
|
12310
|
+
}
|
|
12311
|
+
state.claimSeenAt[task.id] ??= now;
|
|
12312
|
+
} else delete state.claimSeenAt[task.id];
|
|
12313
|
+
switch (d.action) {
|
|
12314
|
+
case "wait": continue;
|
|
12315
|
+
case "triggered": {
|
|
12316
|
+
const r = await handleDispatch(task, 0, opts, state, now);
|
|
12317
|
+
note = `ticked off by hand — ${r.note}`;
|
|
12318
|
+
if (!opts.dryRun) {
|
|
12319
|
+
if (!r.parked) report.dispatched++;
|
|
12320
|
+
if (r.alarm) report.stuck++;
|
|
12321
|
+
note += await restoreSchedule(task, d.restoreTo, opts, state);
|
|
12322
|
+
if (d.restoreTo) state.triggeredRestore[task.id] = d.restoreTo;
|
|
12323
|
+
}
|
|
12324
|
+
break;
|
|
12325
|
+
}
|
|
12326
|
+
case "skip":
|
|
12327
|
+
note = d.reason;
|
|
12328
|
+
break;
|
|
12329
|
+
case "running":
|
|
12330
|
+
note = `${d.elapsedMinutes}m elapsed`;
|
|
12331
|
+
break;
|
|
12332
|
+
case "dispatch": {
|
|
12333
|
+
const r = await handleDispatch(task, d.overdueMinutes, opts, state, now);
|
|
12334
|
+
note = r.note;
|
|
12335
|
+
if (!opts.dryRun) {
|
|
12336
|
+
if (!r.parked) report.dispatched++;
|
|
12337
|
+
if (r.alarm) report.stuck++;
|
|
12338
|
+
}
|
|
12339
|
+
break;
|
|
12340
|
+
}
|
|
12341
|
+
case "complete":
|
|
12342
|
+
note = await handleComplete(task, d.durationMinutes, opts, state, now);
|
|
12343
|
+
if (!opts.dryRun) report.completed++;
|
|
12344
|
+
break;
|
|
12345
|
+
case "abandoned":
|
|
12346
|
+
note = `claimed ${d.elapsedMinutes}m ago, past the ${d.thresholdMinutes}m limit — releasing the claim`;
|
|
12347
|
+
if (!opts.dryRun) {
|
|
12348
|
+
await releaseClaim(task, opts.provider, state);
|
|
12349
|
+
report.stuck++;
|
|
12350
|
+
}
|
|
12351
|
+
break;
|
|
12352
|
+
case "probe":
|
|
12353
|
+
case "orphaned": {
|
|
12354
|
+
const result = await handleProbe(task, d.action === "probe" ? d.elapsedMinutes : null, opts, state, now);
|
|
12355
|
+
note = result.note;
|
|
12356
|
+
if (!opts.dryRun) {
|
|
12357
|
+
report.probed++;
|
|
12358
|
+
if (result.stuck) report.stuck++;
|
|
12359
|
+
if (result.dispatched) report.dispatched++;
|
|
12360
|
+
}
|
|
12361
|
+
break;
|
|
12362
|
+
}
|
|
11186
12363
|
}
|
|
11187
|
-
|
|
12364
|
+
report.decisions.push({
|
|
12365
|
+
decision: d,
|
|
12366
|
+
note
|
|
12367
|
+
});
|
|
12368
|
+
} catch (err) {
|
|
12369
|
+
report.decisions.push({
|
|
12370
|
+
decision: d,
|
|
12371
|
+
note: `error: ${err instanceof Error ? err.message : String(err)}`
|
|
12372
|
+
});
|
|
11188
12373
|
}
|
|
11189
12374
|
}
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
if (!opts.dryRun) {
|
|
11196
|
-
reconcileWithDisk(state, stateFile);
|
|
11197
|
-
saveState(state, stateFile);
|
|
12375
|
+
} finally {
|
|
12376
|
+
if (!opts.dryRun) {
|
|
12377
|
+
reconcileWithDisk(state, stateFile);
|
|
12378
|
+
saveState(state, stateFile);
|
|
12379
|
+
}
|
|
11198
12380
|
}
|
|
11199
12381
|
return report;
|
|
11200
12382
|
}
|
|
@@ -11611,7 +12793,7 @@ function cleanBrokenSymlinks(dir) {
|
|
|
11611
12793
|
const full = join(dir, entry);
|
|
11612
12794
|
try {
|
|
11613
12795
|
if (lstatSync(full).isSymbolicLink()) {
|
|
11614
|
-
if (!existsSync(readlinkSync(full))) {
|
|
12796
|
+
if (!existsSync(resolve(dir, readlinkSync(full)))) {
|
|
11615
12797
|
unlinkSync(full);
|
|
11616
12798
|
removed++;
|
|
11617
12799
|
}
|
|
@@ -12667,10 +13849,22 @@ function migrateWorkerLogs(oldPath, newPath, opts = {}) {
|
|
|
12667
13849
|
const active = activeSpawnedWorkerCount(oldPath);
|
|
12668
13850
|
if (active > 0) throw new WorkerLogsMigrationError(`${active} worker(s) other than the interactive session are RUNNING and writing into ${oldPath} — wait for them to finish (see \`pai worker ps\`), then retry`);
|
|
12669
13851
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
12670
|
-
|
|
13852
|
+
const partial = `${newPath}.partial`;
|
|
13853
|
+
rmSync(partial, {
|
|
13854
|
+
recursive: true,
|
|
13855
|
+
force: true
|
|
13856
|
+
});
|
|
13857
|
+
cpSync(oldPath, partial, { recursive: true });
|
|
12671
13858
|
const fromCount = totalFileCount(oldPath);
|
|
12672
|
-
const toCount = totalFileCount(
|
|
12673
|
-
if (fromCount !== toCount)
|
|
13859
|
+
const toCount = totalFileCount(partial);
|
|
13860
|
+
if (fromCount !== toCount) {
|
|
13861
|
+
rmSync(partial, {
|
|
13862
|
+
recursive: true,
|
|
13863
|
+
force: true
|
|
13864
|
+
});
|
|
13865
|
+
throw new WorkerLogsMigrationError(`${newPath}: copy produced ${toCount} files, expected ${fromCount} from ${oldPath} — aborting, old directory left in place`);
|
|
13866
|
+
}
|
|
13867
|
+
renameSync(partial, newPath);
|
|
12674
13868
|
renameSync(oldPath, `${oldPath}.migrated-${stamp}`);
|
|
12675
13869
|
return {
|
|
12676
13870
|
fromPath: oldPath,
|
|
@@ -13847,7 +15041,7 @@ async function migrateToPostgres(opts = {}) {
|
|
|
13847
15041
|
const dump = pgDump(container, pgConfig, dumpPath);
|
|
13848
15042
|
if (!dump.ok) return refused(dump.reason, dryRun);
|
|
13849
15043
|
}
|
|
13850
|
-
const { PostgresBackend } = await import("../postgres-
|
|
15044
|
+
const { PostgresBackend } = await import("../postgres-BdBNkL_l.mjs");
|
|
13851
15045
|
await PostgresBackend.ensureDatabase(pgConfig);
|
|
13852
15046
|
const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
|
|
13853
15047
|
const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
|
|
@@ -14579,6 +15773,35 @@ const homeCommand = {
|
|
|
14579
15773
|
* `firstlast@company.com`. Anything that should count gets listed.
|
|
14580
15774
|
*/
|
|
14581
15775
|
/**
|
|
15776
|
+
* Split a recipient field into its individual entries. Commas and semicolons
|
|
15777
|
+
* separate entries unless inside a quoted display name or an angle-bracket
|
|
15778
|
+
* address. Empty entries are dropped.
|
|
15779
|
+
*/
|
|
15780
|
+
function splitAddresses(raw) {
|
|
15781
|
+
const out = [];
|
|
15782
|
+
let cur = "";
|
|
15783
|
+
let quoted = false;
|
|
15784
|
+
let angled = false;
|
|
15785
|
+
for (let i = 0; i < raw.length; i++) {
|
|
15786
|
+
const c = raw[i];
|
|
15787
|
+
if (quoted && c === "\\" && i + 1 < raw.length) {
|
|
15788
|
+
cur += c + raw[++i];
|
|
15789
|
+
continue;
|
|
15790
|
+
}
|
|
15791
|
+
if (c === "\"") quoted = !quoted;
|
|
15792
|
+
else if (!quoted && c === "<") angled = true;
|
|
15793
|
+
else if (!quoted && c === ">") angled = false;
|
|
15794
|
+
if (!quoted && !angled && (c === "," || c === ";")) {
|
|
15795
|
+
if (cur.trim()) out.push(cur.trim());
|
|
15796
|
+
cur = "";
|
|
15797
|
+
continue;
|
|
15798
|
+
}
|
|
15799
|
+
cur += c;
|
|
15800
|
+
}
|
|
15801
|
+
if (cur.trim()) out.push(cur.trim());
|
|
15802
|
+
return out;
|
|
15803
|
+
}
|
|
15804
|
+
/**
|
|
14582
15805
|
* Reduce an address to a comparable form: trimmed, unwrapped, lowercased.
|
|
14583
15806
|
*
|
|
14584
15807
|
* Handles `Display Name <addr@example.com>` because recipient fields carry that
|
|
@@ -14588,8 +15811,9 @@ const homeCommand = {
|
|
|
14588
15811
|
*/
|
|
14589
15812
|
function normalizeEmail(raw) {
|
|
14590
15813
|
if (!raw) return null;
|
|
14591
|
-
|
|
14592
|
-
if (
|
|
15814
|
+
const parts = splitAddresses(raw);
|
|
15815
|
+
if (parts.length !== 1) return null;
|
|
15816
|
+
let value = parts[0];
|
|
14593
15817
|
const angled = value.match(/<([^>]*)>/);
|
|
14594
15818
|
if (angled) value = angled[1].trim();
|
|
14595
15819
|
value = value.toLowerCase();
|
|
@@ -15204,7 +16428,7 @@ const out = (s) => process.stdout.write(s + "\n");
|
|
|
15204
16428
|
async function repoOf(project) {
|
|
15205
16429
|
if (typeof project === "string" && project) {
|
|
15206
16430
|
if (existsSync(project)) return resolve(project);
|
|
15207
|
-
const { getProject } = await import("../helpers-
|
|
16431
|
+
const { getProject } = await import("../helpers-CMGjy_0N.mjs");
|
|
15208
16432
|
const row = await getProject(project);
|
|
15209
16433
|
if (!row) throw new CliError(`no project ${project}`, "pai projects list");
|
|
15210
16434
|
return row.root_path;
|
|
@@ -16026,7 +17250,7 @@ async function cmdPromote(opts) {
|
|
|
16026
17250
|
console.error(dim$1(` Pick a durable location for the new project.`));
|
|
16027
17251
|
process.exit(1);
|
|
16028
17252
|
}
|
|
16029
|
-
const { getRegistryBackend } = await import("../factory-
|
|
17253
|
+
const { getRegistryBackend } = await import("../factory-Bsib-qDG.mjs");
|
|
16030
17254
|
const registryBackend = await getRegistryBackend();
|
|
16031
17255
|
const encodedDir = encodeDir(targetPath);
|
|
16032
17256
|
if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
|
|
@@ -17270,7 +18494,7 @@ async function cmdUnregister(slug, opts = {}) {
|
|
|
17270
18494
|
* deprecated alias that forwards here with a stderr notice.
|
|
17271
18495
|
*/
|
|
17272
18496
|
async function projectListCmd(opts) {
|
|
17273
|
-
const { cmdMain } = await import("../main-resolver-
|
|
18497
|
+
const { cmdMain } = await import("../main-resolver-3Ns1HPmS.mjs");
|
|
17274
18498
|
await cmdMain(void 0, void 0, opts);
|
|
17275
18499
|
return 0;
|
|
17276
18500
|
}
|
|
@@ -18693,7 +19917,7 @@ async function registryScanCmd(opts) {
|
|
|
18693
19917
|
return 0;
|
|
18694
19918
|
}
|
|
18695
19919
|
if (opts.filterReport) {
|
|
18696
|
-
const { reportUnregistrable } = await import("../registrable-
|
|
19920
|
+
const { reportUnregistrable } = await import("../registrable-C7b6givx.mjs");
|
|
18697
19921
|
const r = await reportUnregistrable(await getRegistryBackend());
|
|
18698
19922
|
console.log(`Registry rows: ${r.total}`);
|
|
18699
19923
|
console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
|
|
@@ -18899,7 +20123,7 @@ const VALID_OUTCOMES = new Set([
|
|
|
18899
20123
|
*/
|
|
18900
20124
|
function run$1(bin, args, stdin, killAfterMs) {
|
|
18901
20125
|
return new Promise((resolve, reject) => {
|
|
18902
|
-
execFile(bin, args, {
|
|
20126
|
+
const child = execFile(bin, args, {
|
|
18903
20127
|
timeout: killAfterMs,
|
|
18904
20128
|
maxBuffer: 1024 * 1024
|
|
18905
20129
|
}, (error, stdout, stderr) => {
|
|
@@ -18908,15 +20132,26 @@ function run$1(bin, args, stdin, killAfterMs) {
|
|
|
18908
20132
|
return;
|
|
18909
20133
|
}
|
|
18910
20134
|
resolve(stdout);
|
|
18911
|
-
})
|
|
20135
|
+
});
|
|
20136
|
+
child.stdin?.on("error", (e) => reject(e));
|
|
20137
|
+
child.stdin?.end(stdin);
|
|
18912
20138
|
});
|
|
18913
20139
|
}
|
|
20140
|
+
/** A leading dash would be parsed by the CLI as an option, not a project. */
|
|
20141
|
+
function validProject(project) {
|
|
20142
|
+
return project.length > 0 && !project.startsWith("-");
|
|
20143
|
+
}
|
|
18914
20144
|
var AiBrokerTransport = class {
|
|
18915
20145
|
constructor(bin = "aibroker", timeoutSecs = DEFAULT_DISPATCH_TIMEOUT_SECS) {
|
|
18916
20146
|
this.bin = bin;
|
|
18917
20147
|
this.timeoutSecs = timeoutSecs;
|
|
18918
20148
|
}
|
|
18919
20149
|
async dispatch(project, message, opts) {
|
|
20150
|
+
if (!validProject(project)) return {
|
|
20151
|
+
outcome: "unlaunchable",
|
|
20152
|
+
session: project,
|
|
20153
|
+
reason: `invalid project name: ${project}`
|
|
20154
|
+
};
|
|
18920
20155
|
const args = [
|
|
18921
20156
|
"dispatch",
|
|
18922
20157
|
project,
|
|
@@ -18960,6 +20195,10 @@ var AiBrokerProber = class {
|
|
|
18960
20195
|
this.timeoutSecs = timeoutSecs;
|
|
18961
20196
|
}
|
|
18962
20197
|
async ask(project, question) {
|
|
20198
|
+
if (!validProject(project)) return {
|
|
20199
|
+
state: "silent",
|
|
20200
|
+
reason: `invalid project name: ${project}`
|
|
20201
|
+
};
|
|
18963
20202
|
let stdout;
|
|
18964
20203
|
try {
|
|
18965
20204
|
stdout = await run$1(this.bin, [
|
|
@@ -19083,33 +20322,36 @@ function cliPath() {
|
|
|
19083
20322
|
}
|
|
19084
20323
|
throw new Error("cannot locate the PAI CLI entrypoint (dist/cli/index.mjs) — refusing to install a scheduler that would silently do nothing");
|
|
19085
20324
|
}
|
|
20325
|
+
function xmlEscape(value) {
|
|
20326
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
20327
|
+
}
|
|
19086
20328
|
function generateSchedulePlist(intervalSecs, cli) {
|
|
19087
20329
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
19088
20330
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
19089
20331
|
<plist version="1.0">
|
|
19090
20332
|
<dict>
|
|
19091
20333
|
<key>Label</key>
|
|
19092
|
-
<string>${SCHEDULE_LABEL}</string>
|
|
20334
|
+
<string>${xmlEscape(SCHEDULE_LABEL)}</string>
|
|
19093
20335
|
|
|
19094
20336
|
<key>ProgramArguments</key>
|
|
19095
20337
|
<array>
|
|
19096
|
-
<string
|
|
19097
|
-
<string>${cli}</string>
|
|
20338
|
+
<string>${xmlEscape(process.execPath)}</string>
|
|
20339
|
+
<string>${xmlEscape(cli)}</string>
|
|
19098
20340
|
<string>task</string>
|
|
19099
20341
|
<string>poll</string>
|
|
19100
20342
|
</array>
|
|
19101
20343
|
|
|
19102
20344
|
<key>StartInterval</key>
|
|
19103
|
-
<integer>${intervalSecs}</integer>
|
|
20345
|
+
<integer>${Math.trunc(intervalSecs)}</integer>
|
|
19104
20346
|
|
|
19105
20347
|
<key>RunAtLoad</key>
|
|
19106
20348
|
<true/>
|
|
19107
20349
|
|
|
19108
20350
|
<key>StandardOutPath</key>
|
|
19109
|
-
<string>${SCHEDULE_LOG}</string>
|
|
20351
|
+
<string>${xmlEscape(SCHEDULE_LOG)}</string>
|
|
19110
20352
|
|
|
19111
20353
|
<key>StandardErrorPath</key>
|
|
19112
|
-
<string>${SCHEDULE_LOG}</string>
|
|
20354
|
+
<string>${xmlEscape(SCHEDULE_LOG)}</string>
|
|
19113
20355
|
|
|
19114
20356
|
<key>EnvironmentVariables</key>
|
|
19115
20357
|
<dict>
|
|
@@ -20517,8 +21759,8 @@ function embedProgressLine(done, backlog) {
|
|
|
20517
21759
|
async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSize = DEFAULT_PAGE_SIZE, maxMillis, run, shouldStop) {
|
|
20518
21760
|
const label = projectSlug ? `project ${projectSlug}` : "all projects";
|
|
20519
21761
|
console.log(dim$1(`Generating embeddings for ${label} (this may take a while on first run)...`));
|
|
20520
|
-
const { serializeEmbedding } = await import("../embeddings-
|
|
20521
|
-
const { gateBackend } = await import("../embedding-gate-
|
|
21762
|
+
const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
|
|
21763
|
+
const { gateBackend } = await import("../embedding-gate-Cnk4YCyX.mjs");
|
|
20522
21764
|
const gate = await gateBackend(backend, { record: true });
|
|
20523
21765
|
if (!gate.ok) {
|
|
20524
21766
|
console.error(err(gate.reason));
|
|
@@ -20847,7 +22089,7 @@ async function memorySearchCmd(query, opts) {
|
|
|
20847
22089
|
let results;
|
|
20848
22090
|
if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
|
|
20849
22091
|
else {
|
|
20850
|
-
const { embedQuery } = await import("../embedding-gate-
|
|
22092
|
+
const { embedQuery } = await import("../embedding-gate-Cnk4YCyX.mjs");
|
|
20851
22093
|
console.log(dim$1("Generating query embedding..."));
|
|
20852
22094
|
const q = await embedQuery(backend, query);
|
|
20853
22095
|
const queryEmbedding = q.vec;
|
|
@@ -20897,7 +22139,7 @@ async function memorySearchCmd(query, opts) {
|
|
|
20897
22139
|
return 0;
|
|
20898
22140
|
}
|
|
20899
22141
|
if (opts.rerank !== false) {
|
|
20900
|
-
const { rerankResults } = await import("../reranker-
|
|
22142
|
+
const { rerankResults } = await import("../reranker-8g6xMwP7.mjs");
|
|
20901
22143
|
console.log(dim$1("Reranking with cross-encoder..."));
|
|
20902
22144
|
results = await rerankResults(query, results, { topK: maxResults });
|
|
20903
22145
|
}
|
|
@@ -20927,9 +22169,9 @@ async function memorySearchCmd(query, opts) {
|
|
|
20927
22169
|
//#region src/cli/commands/memory/sources-cmd.ts
|
|
20928
22170
|
/** `pai memory sources` — what the indexer has taken in. */
|
|
20929
22171
|
async function memorySourcesCmd(opts) {
|
|
20930
|
-
const { createStorageBackend } = await import("../factory-
|
|
20931
|
-
const { loadConfig } = await import("../config-
|
|
20932
|
-
const { cmdMemorySources } = await import("../sources-
|
|
22172
|
+
const { createStorageBackend } = await import("../factory-Bsib-qDG.mjs");
|
|
22173
|
+
const { loadConfig } = await import("../config-DtENJ6cL.mjs");
|
|
22174
|
+
const { cmdMemorySources } = await import("../sources-ESUkZaWv.mjs");
|
|
20933
22175
|
let backend;
|
|
20934
22176
|
try {
|
|
20935
22177
|
backend = await createStorageBackend(loadConfig());
|
|
@@ -21688,7 +22930,7 @@ async function zettelExploreCmd(note, opts) {
|
|
|
21688
22930
|
const depth = opts.depth ?? 3;
|
|
21689
22931
|
const direction = opts.direction ?? "both";
|
|
21690
22932
|
const mode = opts.mode ?? "all";
|
|
21691
|
-
const { zettelExplore } = await import("../zettelkasten-
|
|
22933
|
+
const { zettelExplore } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21692
22934
|
const result = await zettelExplore(await getFedBackend(), {
|
|
21693
22935
|
startNote: note,
|
|
21694
22936
|
depth,
|
|
@@ -21744,7 +22986,7 @@ async function zettelHealthCmd(opts) {
|
|
|
21744
22986
|
const projectPath = opts.project;
|
|
21745
22987
|
const recentDays = opts.days ?? 30;
|
|
21746
22988
|
const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
|
|
21747
|
-
const { zettelHealth } = await import("../zettelkasten-
|
|
22989
|
+
const { zettelHealth } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21748
22990
|
const result = await zettelHealth(await getFedBackend(), {
|
|
21749
22991
|
scope,
|
|
21750
22992
|
projectPath,
|
|
@@ -21802,7 +23044,7 @@ async function zettelSurpriseCmd(note, opts) {
|
|
|
21802
23044
|
const limit = opts.limit ?? 10;
|
|
21803
23045
|
const minSimilarity = opts.minSimilarity ?? .3;
|
|
21804
23046
|
const minGraphDistance = opts.minDistance ?? 3;
|
|
21805
|
-
const { zettelSurprise } = await import("../zettelkasten-
|
|
23047
|
+
const { zettelSurprise } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21806
23048
|
const backend = await getFedBackend();
|
|
21807
23049
|
console.log();
|
|
21808
23050
|
console.log(header(" PAI Zettel Surprise"));
|
|
@@ -21844,7 +23086,7 @@ async function zettelConverseCmd(question, opts) {
|
|
|
21844
23086
|
const vaultProjectId = Number(opts.vaultProjectId);
|
|
21845
23087
|
const depth = opts.depth ?? 2;
|
|
21846
23088
|
const limit = opts.limit ?? 15;
|
|
21847
|
-
const { zettelConverse } = await import("../zettelkasten-
|
|
23089
|
+
const { zettelConverse } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21848
23090
|
const backend = await getFedBackend();
|
|
21849
23091
|
console.log();
|
|
21850
23092
|
console.log(header(" PAI Zettel Converse"));
|
|
@@ -21894,7 +23136,7 @@ async function zettelThemesCmd(opts) {
|
|
|
21894
23136
|
const minClusterSize = opts.minSize ?? 3;
|
|
21895
23137
|
const maxThemes = opts.maxThemes ?? 10;
|
|
21896
23138
|
const similarityThreshold = opts.threshold ?? .65;
|
|
21897
|
-
const { zettelThemes } = await import("../zettelkasten-
|
|
23139
|
+
const { zettelThemes } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21898
23140
|
const backend = await getFedBackend();
|
|
21899
23141
|
console.log();
|
|
21900
23142
|
console.log(header(" PAI Zettel Themes"));
|
|
@@ -21942,7 +23184,7 @@ async function zettelSuggestCmd(note, opts) {
|
|
|
21942
23184
|
const vaultProjectId = Number(opts.vaultProjectId);
|
|
21943
23185
|
const limit = opts.limit ?? 5;
|
|
21944
23186
|
const excludeLinked = opts.excludeLinked !== true;
|
|
21945
|
-
const { zettelSuggest } = await import("../zettelkasten-
|
|
23187
|
+
const { zettelSuggest } = await import("../zettelkasten-B_rRRRvD.mjs");
|
|
21946
23188
|
const backend = await getFedBackend();
|
|
21947
23189
|
console.log();
|
|
21948
23190
|
console.log(header(" PAI Zettel Suggest"));
|
|
@@ -22234,7 +23476,7 @@ function ipcCall$1(method, params) {
|
|
|
22234
23476
|
fn();
|
|
22235
23477
|
}
|
|
22236
23478
|
};
|
|
22237
|
-
const client = createConnection(paiSocketPath(), () => {
|
|
23479
|
+
const client = createConnection$1(paiSocketPath(), () => {
|
|
22238
23480
|
client.write(JSON.stringify({
|
|
22239
23481
|
id: 1,
|
|
22240
23482
|
method,
|
|
@@ -22624,7 +23866,7 @@ function ipcCall(method, params) {
|
|
|
22624
23866
|
fn();
|
|
22625
23867
|
}
|
|
22626
23868
|
};
|
|
22627
|
-
const client = createConnection(paiSocketPath(), () => {
|
|
23869
|
+
const client = createConnection$1(paiSocketPath(), () => {
|
|
22628
23870
|
client.write(JSON.stringify({
|
|
22629
23871
|
id: 1,
|
|
22630
23872
|
method,
|
|
@@ -24256,7 +25498,14 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
|
24256
25498
|
* one of the two in whichever catalogue order Claude Code loads them.
|
|
24257
25499
|
*/
|
|
24258
25500
|
/** Recursively collect every file under `dir` whose basename satisfies `matches`. */
|
|
24259
|
-
function findFilesRecursive(dir, matches) {
|
|
25501
|
+
function findFilesRecursive(dir, matches, seen = /* @__PURE__ */ new Set()) {
|
|
25502
|
+
try {
|
|
25503
|
+
const real = realpathSync(dir);
|
|
25504
|
+
if (seen.has(real)) return [];
|
|
25505
|
+
seen.add(real);
|
|
25506
|
+
} catch {
|
|
25507
|
+
return [];
|
|
25508
|
+
}
|
|
24260
25509
|
let entries;
|
|
24261
25510
|
try {
|
|
24262
25511
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -24266,8 +25515,17 @@ function findFilesRecursive(dir, matches) {
|
|
|
24266
25515
|
const found = [];
|
|
24267
25516
|
for (const entry of entries) {
|
|
24268
25517
|
const full = join(dir, entry.name);
|
|
24269
|
-
|
|
24270
|
-
|
|
25518
|
+
let isDir = entry.isDirectory();
|
|
25519
|
+
let isFile = entry.isFile();
|
|
25520
|
+
if (entry.isSymbolicLink()) try {
|
|
25521
|
+
const st = statSync(full);
|
|
25522
|
+
isDir = st.isDirectory();
|
|
25523
|
+
isFile = st.isFile();
|
|
25524
|
+
} catch {
|
|
25525
|
+
continue;
|
|
25526
|
+
}
|
|
25527
|
+
if (isDir) found.push(...findFilesRecursive(full, matches, seen));
|
|
25528
|
+
else if (isFile && matches(entry.name)) found.push(full);
|
|
24271
25529
|
}
|
|
24272
25530
|
return found;
|
|
24273
25531
|
}
|
|
@@ -24750,11 +26008,17 @@ function newestMcpConfig(logDir) {
|
|
|
24750
26008
|
mtime: statSync(path).mtimeMs
|
|
24751
26009
|
})).sort((a, b) => b.mtime - a.mtime)[0].path;
|
|
24752
26010
|
}
|
|
26011
|
+
/** Child env: not a worker, and no API key (same as run-env / daemon-llm). */
|
|
26012
|
+
function ladderChildEnv(base) {
|
|
26013
|
+
const env = { ...base };
|
|
26014
|
+
delete env.PAI_WORKER;
|
|
26015
|
+
delete env.ANTHROPIC_API_KEY;
|
|
26016
|
+
return env;
|
|
26017
|
+
}
|
|
24753
26018
|
/** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
|
|
24754
26019
|
function runRung(rung, cwd) {
|
|
24755
26020
|
return new Promise((resolvePromise) => {
|
|
24756
|
-
const env =
|
|
24757
|
-
delete env.PAI_WORKER;
|
|
26021
|
+
const env = ladderChildEnv(process.env);
|
|
24758
26022
|
const proc = spawn("claude", rung.args, {
|
|
24759
26023
|
cwd,
|
|
24760
26024
|
env,
|