@tekmidian/pai 0.35.1 → 0.36.0
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/dist/.metadata_never_index +0 -0
- package/dist/{auto-route-DVM3U2ZY.mjs → auto-route-Byf8ENXj.mjs} +4 -4
- package/dist/{auto-route-DVM3U2ZY.mjs.map → auto-route-Byf8ENXj.mjs.map} +1 -1
- package/dist/{checkpoint-block-D3rm4dAJ.mjs → checkpoint-block-DKYxCkBL.mjs} +7 -6
- package/dist/checkpoint-block-DKYxCkBL.mjs.map +1 -0
- package/dist/cli/index.mjs +25 -17
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/probe2.mjs +2 -0
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +16 -267
- package/dist/{clusters-CzGxefB7.mjs → clusters-wZgTCYCB.mjs} +2 -2
- package/dist/{clusters-CzGxefB7.mjs.map → clusters-wZgTCYCB.mjs.map} +1 -1
- package/dist/{config-BSkVcvfq.mjs → config-B64vFg14.mjs} +3 -12
- package/dist/{config-BSkVcvfq.mjs.map → config-B64vFg14.mjs.map} +1 -1
- package/dist/config-C_ErGddD.mjs +3 -0
- package/dist/daemon/index.mjs +19 -19
- package/dist/{daemon-Hnu6-HDD.mjs → daemon--N2JFnUs.mjs} +39 -39
- package/dist/daemon--N2JFnUs.mjs.map +1 -0
- package/dist/daemon-DJEFqV84.mjs +20 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/{db-BtuN768f.mjs → db-Ca5qfsMC.mjs} +2 -4
- package/dist/{db-BtuN768f.mjs.map → db-Ca5qfsMC.mjs.map} +1 -1
- package/dist/db-O-cyAPfS.mjs +3 -0
- package/dist/db-XEwJbuGO.mjs +3 -0
- package/dist/{db-CYmBWcjh.mjs → db-a1ixZQjr.mjs} +2 -4
- package/dist/{db-CYmBWcjh.mjs.map → db-a1ixZQjr.mjs.map} +1 -1
- package/dist/{detect-Bf2z-oKB.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-Bf2z-oKB.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-BU-bsDXs.mjs → detector--Gg5JRN5.mjs} +3 -5
- package/dist/{detector-BU-bsDXs.mjs.map → detector--Gg5JRN5.mjs.map} +1 -1
- package/dist/detector-DGAk1iBR.mjs +5 -0
- package/dist/embeddings-CEBGrzwu.mjs +3 -0
- package/dist/{embeddings-Bn86ssxR.mjs → embeddings-DOLZnT1X.mjs} +2 -12
- package/dist/{embeddings-Bn86ssxR.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
- package/dist/{factory-BGH0COXb.mjs → factory-Bsp7xOpO.mjs} +9 -12
- package/dist/{factory-BGH0COXb.mjs.map → factory-Bsp7xOpO.mjs.map} +1 -1
- package/dist/factory-CrokPMk2.mjs +3 -0
- package/dist/{helpers-crDEr6S2.mjs → helpers-IjZkXBhj.mjs} +1 -1
- package/dist/{helpers-crDEr6S2.mjs.map → helpers-IjZkXBhj.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +209 -70
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/whisper-reinject.mjs +59 -0
- package/dist/hooks/whisper-reinject.mjs.map +7 -0
- package/dist/hooks/whisper-rules.mjs +24 -9
- package/dist/hooks/whisper-rules.mjs.map +2 -2
- package/dist/index.mjs +10 -10
- package/dist/{indexer-backend-Bg7VDpGt.mjs → indexer-backend-nQZuEx6N.mjs} +3 -3
- package/dist/{indexer-backend-Bg7VDpGt.mjs.map → indexer-backend-nQZuEx6N.mjs.map} +1 -1
- package/dist/{ipc-client-aVKVERjJ.mjs → ipc-client-BmypMNYk.mjs} +13 -7
- package/dist/ipc-client-BmypMNYk.mjs.map +1 -0
- package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
- package/dist/{latent-ideas-BL9m2HF9.mjs → latent-ideas-Bn6A5-5P.mjs} +4 -4
- package/dist/{latent-ideas-BL9m2HF9.mjs.map → latent-ideas-Bn6A5-5P.mjs.map} +1 -1
- package/dist/{link-boost-QFLrJwD6.mjs → link-boost-fYjUnxCN.mjs} +1 -1
- package/dist/{link-boost-QFLrJwD6.mjs.map → link-boost-fYjUnxCN.mjs.map} +1 -1
- package/dist/{main-resolver-CNSqU8wo.mjs → main-resolver-BAbhKpeX.mjs} +12 -14
- package/dist/main-resolver-BAbhKpeX.mjs.map +1 -0
- package/dist/main-resolver-Dxh444GO.mjs +4 -0
- package/dist/{migrate-fLD6rAdO.mjs → migrate-Cjzeefn9.mjs} +2 -2
- package/dist/{migrate-fLD6rAdO.mjs.map → migrate-Cjzeefn9.mjs.map} +1 -1
- package/dist/{neighborhood-BX89_nty.mjs → neighborhood-DpaEM991.mjs} +2 -2
- package/dist/{neighborhood-BX89_nty.mjs.map → neighborhood-DpaEM991.mjs.map} +1 -1
- package/dist/{note-context-d1wT_-GA.mjs → note-context-DrcY4cWm.mjs} +1 -1
- package/dist/{note-context-d1wT_-GA.mjs.map → note-context-DrcY4cWm.mjs.map} +1 -1
- package/dist/{pai-marker-B20KqhA8.mjs → pai-marker-CHtbJMwJ.mjs} +1 -1
- package/dist/{pai-marker-B20KqhA8.mjs.map → pai-marker-CHtbJMwJ.mjs.map} +1 -1
- package/dist/{postgres-BALUE11K.mjs → postgres-BVme6qX0.mjs} +7 -4
- package/dist/postgres-BVme6qX0.mjs.map +1 -0
- package/dist/{pick-aWhenqjE.mjs → program-BnMNFb4O.mjs} +1171 -223
- package/dist/program-BnMNFb4O.mjs.map +1 -0
- package/dist/query-feedback-BBMBp96K.mjs +3 -0
- package/dist/{query-feedback-D4U56Hz6.mjs → query-feedback-C1T6kS18.mjs} +2 -4
- package/dist/{query-feedback-D4U56Hz6.mjs.map → query-feedback-C1T6kS18.mjs.map} +1 -1
- package/dist/reranker-CwTCNsgA.mjs +3 -0
- package/dist/{reranker-CMNZcfVx.mjs → reranker-xPm04PXx.mjs} +2 -8
- package/dist/{reranker-CMNZcfVx.mjs.map → reranker-xPm04PXx.mjs.map} +1 -1
- package/dist/router-BMkOb62X.mjs +3 -0
- package/dist/{router-i9S19Usg.mjs → router-CsDm7HvK.mjs} +2 -4
- package/dist/{router-i9S19Usg.mjs.map → router-CsDm7HvK.mjs.map} +1 -1
- package/dist/{runtime-paths-B0P1TvUr.mjs → runtime-paths-rni52zHX.mjs} +1 -1
- package/dist/{runtime-paths-B0P1TvUr.mjs.map → runtime-paths-rni52zHX.mjs.map} +1 -1
- package/dist/search-CfPpJAWQ.mjs +4 -0
- package/dist/{search-C32zQ0V0.mjs → search-Rpk1cSBC.mjs} +4 -15
- package/dist/{search-C32zQ0V0.mjs.map → search-Rpk1cSBC.mjs.map} +1 -1
- package/dist/{sources-BDwN0B8i.mjs → sources-D8ZdNfvK.mjs} +2 -2
- package/dist/{sources-BDwN0B8i.mjs.map → sources-D8ZdNfvK.mjs.map} +1 -1
- package/dist/{sqlite-C6FHnMkn.mjs → sqlite-D1IaR8Am.mjs} +3 -3
- package/dist/{sqlite-C6FHnMkn.mjs.map → sqlite-D1IaR8Am.mjs.map} +1 -1
- package/dist/state-WaXhLr6R.mjs +70 -0
- package/dist/{state-DTvy-jRB.mjs.map → state-WaXhLr6R.mjs.map} +1 -1
- package/dist/state-qtmrBWCm.mjs +3 -0
- package/dist/{stop-words-BaMEGVeY.mjs → stop-words-Hfu8u22w.mjs} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs.map → stop-words-Hfu8u22w.mjs.map} +1 -1
- package/dist/{sync--BoxBBok.mjs → sync-BWbe8JTg.mjs} +3 -3
- package/dist/{sync--BoxBBok.mjs.map → sync-BWbe8JTg.mjs.map} +1 -1
- package/dist/{themes-BObEGMWn.mjs → themes-XPkj_bfP.mjs} +3 -3
- package/dist/{themes-BObEGMWn.mjs.map → themes-XPkj_bfP.mjs.map} +1 -1
- package/dist/tools-DEt6YPfc.mjs +5 -0
- package/dist/{tools-C1lCHerL.mjs → tools-ceiy7ANX.mjs} +28 -65
- package/dist/tools-ceiy7ANX.mjs.map +1 -0
- package/dist/{trace-h23JCcFD.mjs → trace-DfyGmMG_.mjs} +1 -1
- package/dist/{trace-h23JCcFD.mjs.map → trace-DfyGmMG_.mjs.map} +1 -1
- package/dist/{utils-BAxjW3j8.mjs → utils-9Err2RBW.mjs} +2 -22
- package/dist/{utils-BAxjW3j8.mjs.map → utils-9Err2RBW.mjs.map} +1 -1
- package/dist/utils-DhMex3Ox.mjs +3 -0
- package/dist/{vault-indexer-CUF9edbW.mjs → vault-indexer-CFvlPUMB.mjs} +2 -2
- package/dist/{vault-indexer-CUF9edbW.mjs.map → vault-indexer-CFvlPUMB.mjs.map} +1 -1
- package/dist/{work-queue-worker-BcDGAcF3.mjs → work-queue-worker-228XjABm.mjs} +234 -14
- package/dist/work-queue-worker-228XjABm.mjs.map +1 -0
- package/dist/work-queue-worker-LRA9Fj9z.mjs +11 -0
- package/dist/{zettelkasten-W-h8G2is.mjs → zettelkasten-CvjmMghT.mjs} +4 -4
- package/dist/{zettelkasten-W-h8G2is.mjs.map → zettelkasten-CvjmMghT.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +515 -0
- package/src/hooks/ts/lib/context-fill.ts +585 -0
- package/src/hooks/ts/lib/context-handover-cache.ts +46 -0
- package/src/hooks/ts/lib/transcript-text.test.ts +125 -0
- package/src/hooks/ts/lib/transcript-text.ts +71 -0
- package/src/hooks/ts/post-tool-use/whisper-reinject.ts +88 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +112 -30
- package/src/hooks/ts/user-prompt/whisper-rules.ts +62 -8
- package/statusline-command.sh +16 -0
- package/dist/checkpoint-block-D3rm4dAJ.mjs.map +0 -1
- package/dist/daemon-Hnu6-HDD.mjs.map +0 -1
- package/dist/ipc-client-aVKVERjJ.mjs.map +0 -1
- package/dist/main-resolver-CNSqU8wo.mjs.map +0 -1
- package/dist/pick-aWhenqjE.mjs.map +0 -1
- package/dist/postgres-BALUE11K.mjs.map +0 -1
- package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
- package/dist/state-DTvy-jRB.mjs +0 -102
- package/dist/tools-C1lCHerL.mjs.map +0 -1
- package/dist/work-queue-worker-BcDGAcF3.mjs.map +0 -1
- /package/dist/{indexer-AEcT8wHf.mjs → indexer-D7MvSQPY.mjs} +0 -0
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as warn$1, a as fmtDate, c as ok$1, d as
|
|
3
|
-
import { a as slugify$1, i as parseSessionFilename } from "./migrate-
|
|
4
|
-
import { n as ensurePaiMarker } from "./pai-marker-
|
|
5
|
-
import {
|
|
6
|
-
import { i as indexProject, n as indexAll, t as embedChunks } from "./sync
|
|
7
|
-
import { t as STOP_WORDS } from "./stop-words-
|
|
8
|
-
import {
|
|
9
|
-
import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "./detect-
|
|
10
|
-
import { _ as transcriptFiles, a as readBodyFile, c as loadScanConfig, d as saveScanConfig, f as upsertProject, g as scanTranscriptFolders, h as findMovedProjects, i as findNotesDir$1, m as claudeProjectsDir, n as applyContinue, o as extractAndStoreTriples, p as upsertSession, r as findLatestNote, s as cmdScan, t as appendCheckpointToNote, u as resolveHome } from "./checkpoint-block-
|
|
11
|
-
import { a as schedulerLogPath, i as paiSocketPath, n as daemonLogPath, r as daemonPidPath } from "./runtime-paths-
|
|
12
|
-
import { t as PaiClient } from "./ipc-client-
|
|
13
|
-
import { a as expandHome, c as UNROUTED, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, s as OWNER_LABEL_PREFIX, t as CONFIG_DIR } from "./config-
|
|
14
|
-
import {
|
|
15
|
-
import { s as kgQuery } from "./kg-entity-
|
|
16
|
-
import { _ as scanSessions, a as renderDedupedSessions, c as probeResume, d as callAiBroker, f as fetchLiveSessions, g as resolveSessionByNameOrId, h as fmtAge, i as normalizeName$2, l as restoreTopLevel, m as sendToSession, o as hasConversation, p as revealItermSession, r as buildDeduped, s as launchInDir, u as printExitDir } from "./main-resolver-
|
|
1
|
+
import { t as openRegistry } from "./db-Ca5qfsMC.mjs";
|
|
2
|
+
import { _ as warn$1, a as fmtDate, c as ok$1, d as resolvePath, f as scaffoldProjectDirs, g as smartDecodeDir, h as slugify, i as err, m as slugFromPath, n as dim$1, o as header, p as shortenPath, r as encodeDir, s as now, t as bold$1, u as renderTable } from "./utils-9Err2RBW.mjs";
|
|
3
|
+
import { a as slugify$1, i as parseSessionFilename } from "./migrate-Cjzeefn9.mjs";
|
|
4
|
+
import { n as ensurePaiMarker } from "./pai-marker-CHtbJMwJ.mjs";
|
|
5
|
+
import { t as openFederation } from "./db-a1ixZQjr.mjs";
|
|
6
|
+
import { i as indexProject, n as indexAll, t as embedChunks } from "./sync-BWbe8JTg.mjs";
|
|
7
|
+
import { t as STOP_WORDS } from "./stop-words-Hfu8u22w.mjs";
|
|
8
|
+
import { a as searchMemory, i as populateSlugs } from "./search-Rpk1cSBC.mjs";
|
|
9
|
+
import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "./detect-CdaA48EI.mjs";
|
|
10
|
+
import { _ as transcriptFiles, a as readBodyFile, c as loadScanConfig, d as saveScanConfig, f as upsertProject, g as scanTranscriptFolders, h as findMovedProjects, i as findNotesDir$1, m as claudeProjectsDir, n as applyContinue, o as extractAndStoreTriples, p as upsertSession, r as findLatestNote, s as cmdScan, t as appendCheckpointToNote, u as resolveHome } from "./checkpoint-block-DKYxCkBL.mjs";
|
|
11
|
+
import { a as schedulerLogPath, i as paiSocketPath, n as daemonLogPath, r as daemonPidPath } from "./runtime-paths-rni52zHX.mjs";
|
|
12
|
+
import { t as PaiClient } from "./ipc-client-BmypMNYk.mjs";
|
|
13
|
+
import { a as expandHome, c as UNROUTED, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, s as OWNER_LABEL_PREFIX, t as CONFIG_DIR } from "./config-B64vFg14.mjs";
|
|
14
|
+
import { r as humanDuration, t as createStorageBackend } from "./factory-Bsp7xOpO.mjs";
|
|
15
|
+
import { s as kgQuery } from "./kg-entity-DbOMPdF9.mjs";
|
|
16
|
+
import { _ as scanSessions, a as renderDedupedSessions, c as probeResume, d as callAiBroker, f as fetchLiveSessions, g as resolveSessionByNameOrId, h as fmtAge, i as normalizeName$2, l as restoreTopLevel, m as sendToSession, o as hasConversation, p as revealItermSession, r as buildDeduped, s as launchInDir, t as cmdMain, u as printExitDir } from "./main-resolver-BAbhKpeX.mjs";
|
|
17
17
|
import { appendFileSync, chmodSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { homedir, platform, tmpdir } from "node:os";
|
|
19
19
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
import chalk from "chalk";
|
|
21
|
+
import { Command } from "commander";
|
|
21
22
|
import { fileURLToPath } from "node:url";
|
|
22
23
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
23
24
|
import { createInterface } from "node:readline";
|
|
@@ -214,10 +215,7 @@ function getProject$2(db, slug) {
|
|
|
214
215
|
}
|
|
215
216
|
function requireProject(db, slug) {
|
|
216
217
|
const project = getProject$2(db, slug);
|
|
217
|
-
if (!project) {
|
|
218
|
-
console.error(err(`Project not found: ${slug}`));
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
218
|
+
if (!project) throw new Error(`Project not found: ${slug}`);
|
|
221
219
|
return project;
|
|
222
220
|
}
|
|
223
221
|
/**
|
|
@@ -420,18 +418,21 @@ function cmdAdd(db, rawPath, opts) {
|
|
|
420
418
|
];
|
|
421
419
|
if (!validTypes.includes(type)) {
|
|
422
420
|
console.error(err(`Invalid type "${type}". Valid: ${validTypes.join(", ")}`));
|
|
423
|
-
process.
|
|
421
|
+
process.exitCode = 1;
|
|
422
|
+
return;
|
|
424
423
|
}
|
|
425
424
|
const ephemeral = unregistrableReason(rootPath);
|
|
426
425
|
if (ephemeral) {
|
|
427
426
|
console.error(err(`Refusing to register ${shortenPath(rootPath, 60)}`));
|
|
428
427
|
console.error(dim$1(` That is ${ephemeral}.`));
|
|
429
428
|
console.error(dim$1(` A project needs a durable location — move it, then register.`));
|
|
430
|
-
process.
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
return;
|
|
431
431
|
}
|
|
432
432
|
if (db.prepare("SELECT id FROM projects WHERE slug = ? OR root_path = ?").get(slug, rootPath)) {
|
|
433
433
|
console.error(err(`Project already registered (slug: ${slug} or path: ${rootPath})`));
|
|
434
|
-
process.
|
|
434
|
+
process.exitCode = 1;
|
|
435
|
+
return;
|
|
435
436
|
}
|
|
436
437
|
const dirName = basename(rootPath).toLowerCase();
|
|
437
438
|
const matches = db.prepare(`SELECT slug, root_path FROM projects WHERE status = 'active' AND slug != ?`).all(slug).filter((s) => basename(s.root_path).toLowerCase() === dirName || s.slug.replace(/-\d+$/, "") === slug.replace(/-\d+$/, ""));
|
|
@@ -600,7 +601,8 @@ function cmdAlias(db, slug, alias) {
|
|
|
600
601
|
requireProject(db, slug);
|
|
601
602
|
if (db.prepare("SELECT id FROM projects WHERE slug = ?").get(alias)) {
|
|
602
603
|
console.error(err(`"${alias}" is already a project slug — cannot use as alias.`));
|
|
603
|
-
process.
|
|
604
|
+
process.exitCode = 1;
|
|
605
|
+
return;
|
|
604
606
|
}
|
|
605
607
|
const project = getProject$2(db, slug);
|
|
606
608
|
try {
|
|
@@ -608,7 +610,7 @@ function cmdAlias(db, slug, alias) {
|
|
|
608
610
|
console.log(ok$1(`Alias added: ${bold$1(alias)} → ${slug}`));
|
|
609
611
|
} catch {
|
|
610
612
|
console.error(err(`Alias "${alias}" is already registered.`));
|
|
611
|
-
process.
|
|
613
|
+
process.exitCode = 1;
|
|
612
614
|
}
|
|
613
615
|
}
|
|
614
616
|
function cmdEdit(db, slug, opts) {
|
|
@@ -625,7 +627,8 @@ function cmdEdit(db, slug, opts) {
|
|
|
625
627
|
];
|
|
626
628
|
if (opts.type && !validTypes.includes(opts.type)) {
|
|
627
629
|
console.error(err(`Invalid type "${opts.type}". Valid: ${validTypes.join(", ")}`));
|
|
628
|
-
process.
|
|
630
|
+
process.exitCode = 1;
|
|
631
|
+
return;
|
|
629
632
|
}
|
|
630
633
|
const ts = now();
|
|
631
634
|
if (opts.displayName) {
|
|
@@ -649,7 +652,6 @@ function cmdDetect(db, pathArg, opts) {
|
|
|
649
652
|
console.log(warn$1(`No registered project found for: ${cwd}`));
|
|
650
653
|
console.log(dim$1(" Run 'pai project add .' to register this directory."));
|
|
651
654
|
}
|
|
652
|
-
process.exit(0);
|
|
653
655
|
return;
|
|
654
656
|
}
|
|
655
657
|
if (opts.json) {
|
|
@@ -747,7 +749,8 @@ function cmdGo(db, query) {
|
|
|
747
749
|
const all = db.prepare("SELECT * FROM projects WHERE status = 'active' ORDER BY updated_at DESC").all();
|
|
748
750
|
if (!all.length) {
|
|
749
751
|
console.error(err("No active projects registered. Run: pai project add <path>"));
|
|
750
|
-
process.
|
|
752
|
+
process.exitCode = 1;
|
|
753
|
+
return;
|
|
751
754
|
}
|
|
752
755
|
const q = query.trim().toLowerCase();
|
|
753
756
|
const exact = getProject$2(db, query);
|
|
@@ -786,7 +789,8 @@ function cmdGo(db, query) {
|
|
|
786
789
|
});
|
|
787
790
|
console.error();
|
|
788
791
|
console.error(dim$1(" Use a more specific name or the exact slug."));
|
|
789
|
-
process.
|
|
792
|
+
process.exitCode = 1;
|
|
793
|
+
return;
|
|
790
794
|
}
|
|
791
795
|
const scored = all.map((p) => {
|
|
792
796
|
const distSlug = levenshtein(q, p.slug.toLowerCase());
|
|
@@ -805,14 +809,15 @@ function cmdGo(db, query) {
|
|
|
805
809
|
console.error();
|
|
806
810
|
console.error(dim$1(" Run: pai project list (to see all projects)"));
|
|
807
811
|
}
|
|
808
|
-
process.
|
|
812
|
+
process.exitCode = 1;
|
|
809
813
|
}
|
|
810
814
|
function cmdRebind(db, slug, newPath) {
|
|
811
815
|
const project = requireProject(db, slug);
|
|
812
816
|
const resolved = resolve(newPath.startsWith("~/") ? join(homedir(), newPath.slice(2)) : newPath);
|
|
813
817
|
if (!existsSync(resolved)) {
|
|
814
818
|
console.error(err(`Path does not exist: ${resolved}`));
|
|
815
|
-
process.
|
|
819
|
+
process.exitCode = 1;
|
|
820
|
+
return;
|
|
816
821
|
}
|
|
817
822
|
let isDir = false;
|
|
818
823
|
try {
|
|
@@ -820,13 +825,15 @@ function cmdRebind(db, slug, newPath) {
|
|
|
820
825
|
} catch {}
|
|
821
826
|
if (!isDir) {
|
|
822
827
|
console.error(err(`Path is not a directory: ${resolved}`));
|
|
823
|
-
process.
|
|
828
|
+
process.exitCode = 1;
|
|
829
|
+
return;
|
|
824
830
|
}
|
|
825
831
|
const newEncoded = encodeDir(resolved);
|
|
826
832
|
const conflict = db.prepare("SELECT slug FROM projects WHERE encoded_dir = ? AND id != ?").get(newEncoded, project.id);
|
|
827
833
|
if (conflict) {
|
|
828
834
|
console.error(err(`Path is already registered to project: ${bold$1(conflict.slug)}\n`) + dim$1(` ${resolved}\n`) + dim$1(` Archive or move that project first, or choose a different path.`));
|
|
829
|
-
process.
|
|
835
|
+
process.exitCode = 1;
|
|
836
|
+
return;
|
|
830
837
|
}
|
|
831
838
|
const ts = now();
|
|
832
839
|
db.prepare("UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?").run(resolved, newEncoded, ts, project.id);
|
|
@@ -960,16 +967,19 @@ function cmdName(db, identifier, shortname, opts) {
|
|
|
960
967
|
const project = resolveIdentifier(db, identifier) ?? requireProject(db, identifier);
|
|
961
968
|
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(shortname)) {
|
|
962
969
|
console.error(err(`Invalid name "${shortname}". Use letters, digits, hyphens, underscores. Must start with a letter.`));
|
|
963
|
-
process.
|
|
970
|
+
process.exitCode = 1;
|
|
971
|
+
return;
|
|
964
972
|
}
|
|
965
973
|
if (db.prepare("SELECT id FROM projects WHERE slug = ? AND id != ?").get(shortname, project.id)) {
|
|
966
974
|
console.error(err(`"${shortname}" is already a project slug.`));
|
|
967
|
-
process.
|
|
975
|
+
process.exitCode = 1;
|
|
976
|
+
return;
|
|
968
977
|
}
|
|
969
978
|
const conflictAlias = db.prepare("SELECT project_id FROM aliases WHERE alias = ?").get(shortname);
|
|
970
979
|
if (conflictAlias && conflictAlias.project_id !== project.id) {
|
|
971
980
|
console.error(err(`"${shortname}" is already used by another project.`));
|
|
972
|
-
process.
|
|
981
|
+
process.exitCode = 1;
|
|
982
|
+
return;
|
|
973
983
|
}
|
|
974
984
|
if (!conflictAlias) db.prepare("INSERT INTO aliases (alias, project_id) VALUES (?, ?)").run(shortname, project.id);
|
|
975
985
|
if (opts.permission) {
|
|
@@ -983,7 +993,8 @@ function cmdUnname(db, shortname) {
|
|
|
983
993
|
const alias = db.prepare("SELECT project_id FROM aliases WHERE alias = ?").get(shortname);
|
|
984
994
|
if (!alias) {
|
|
985
995
|
console.error(err(`No named project found: "${shortname}"`));
|
|
986
|
-
process.
|
|
996
|
+
process.exitCode = 1;
|
|
997
|
+
return;
|
|
987
998
|
}
|
|
988
999
|
db.prepare("DELETE FROM aliases WHERE alias = ?").run(shortname);
|
|
989
1000
|
const remaining = db.prepare("SELECT COUNT(*) AS cnt FROM aliases WHERE project_id = ?").get(alias.project_id);
|
|
@@ -1147,7 +1158,8 @@ function cmdConfig(db, identifier, opts) {
|
|
|
1147
1158
|
if (!identifier) {
|
|
1148
1159
|
console.error(err("Specify a project: pai project config <name-or-slug>"));
|
|
1149
1160
|
console.error(dim$1(" Or use --defaults for global defaults, --options for available keys."));
|
|
1150
|
-
process.
|
|
1161
|
+
process.exitCode = 1;
|
|
1162
|
+
return;
|
|
1151
1163
|
}
|
|
1152
1164
|
const project = resolveIdentifier(db, identifier) ?? requireProject(db, identifier);
|
|
1153
1165
|
let config = getSessionConfig(project);
|
|
@@ -1692,7 +1704,8 @@ function cmdMerge(db, fromSlug, intoSlug, opts = {}) {
|
|
|
1692
1704
|
} catch (e) {
|
|
1693
1705
|
if (e instanceof MergeError) {
|
|
1694
1706
|
console.error(err(` ${e.message}`));
|
|
1695
|
-
process.
|
|
1707
|
+
process.exitCode = 1;
|
|
1708
|
+
return;
|
|
1696
1709
|
}
|
|
1697
1710
|
throw e;
|
|
1698
1711
|
}
|
|
@@ -1766,14 +1779,14 @@ function cmdHere(db, name, opts = {}) {
|
|
|
1766
1779
|
target = realpathSync(opts.cwd ?? process.cwd());
|
|
1767
1780
|
} catch {
|
|
1768
1781
|
console.error(err(`Cannot resolve the current directory.`));
|
|
1769
|
-
process.
|
|
1782
|
+
process.exitCode = 1;
|
|
1770
1783
|
return;
|
|
1771
1784
|
}
|
|
1772
1785
|
const encoded = encodeDir(target);
|
|
1773
1786
|
const matches = findProjectsByName(db, name);
|
|
1774
1787
|
if (matches.length > 1) {
|
|
1775
1788
|
console.error(err(`"${name}" matches ${matches.length} projects — name one exactly:\n`) + matches.map((m) => dim$1(` ${m.slug} (${m.display_name})\n`)).join(""));
|
|
1776
|
-
process.
|
|
1789
|
+
process.exitCode = 1;
|
|
1777
1790
|
return;
|
|
1778
1791
|
}
|
|
1779
1792
|
const owner = db.prepare("SELECT id, slug, display_name FROM projects WHERE encoded_dir = ?").get(encoded);
|
|
@@ -1781,7 +1794,7 @@ function cmdHere(db, name, opts = {}) {
|
|
|
1781
1794
|
const p = matches[0];
|
|
1782
1795
|
if (owner && owner.id !== p.id) {
|
|
1783
1796
|
console.error(err(`This directory is already registered to ${bold$1(owner.slug)}.\n`) + dim$1(` ${target}\n`) + dim$1(` Merge them: pai project merge ${owner.slug} ${p.slug} --execute`));
|
|
1784
|
-
process.
|
|
1797
|
+
process.exitCode = 1;
|
|
1785
1798
|
return;
|
|
1786
1799
|
}
|
|
1787
1800
|
let sameAlready = false;
|
|
@@ -1809,7 +1822,7 @@ function cmdHere(db, name, opts = {}) {
|
|
|
1809
1822
|
}
|
|
1810
1823
|
if (owner) {
|
|
1811
1824
|
console.error(err(`This directory is already registered as ${bold$1(owner.slug)} (${owner.display_name}).\n`) + dim$1(` Rename it instead: pai project edit ${owner.slug} --display-name "${name}"`));
|
|
1812
|
-
process.
|
|
1825
|
+
process.exitCode = 1;
|
|
1813
1826
|
return;
|
|
1814
1827
|
}
|
|
1815
1828
|
const slug = slugFromName(name);
|
|
@@ -1834,7 +1847,8 @@ function cmdUnregister(db, slug, opts = {}) {
|
|
|
1834
1847
|
FROM projects p WHERE p.slug = ?`).get(slug);
|
|
1835
1848
|
if (!row) {
|
|
1836
1849
|
console.error(err(` No project with slug "${slug}".`));
|
|
1837
|
-
process.
|
|
1850
|
+
process.exitCode = 1;
|
|
1851
|
+
return;
|
|
1838
1852
|
}
|
|
1839
1853
|
console.log();
|
|
1840
1854
|
console.log(bold$1(` Unregister ${row.slug}`));
|
|
@@ -1847,7 +1861,8 @@ function cmdUnregister(db, slug, opts = {}) {
|
|
|
1847
1861
|
console.log(dim$1(` pai project merge ${row.slug} <into> --execute`));
|
|
1848
1862
|
console.log(dim$1(` Or pass --force if the sessions are genuinely worthless.`));
|
|
1849
1863
|
console.log();
|
|
1850
|
-
process.
|
|
1864
|
+
process.exitCode = 1;
|
|
1865
|
+
return;
|
|
1851
1866
|
}
|
|
1852
1867
|
if (!opts.execute) {
|
|
1853
1868
|
console.log(dim$1(" Preview — nothing was changed. Re-run with --execute to unregister."));
|
|
@@ -1873,7 +1888,7 @@ function cmdUnregister(db, slug, opts = {}) {
|
|
|
1873
1888
|
//#region src/cli/commands/project/projects-index.ts
|
|
1874
1889
|
function registerProjectsCommands(projectsCmd, getDb) {
|
|
1875
1890
|
projectsCmd.command("list", { isDefault: true }).description("Show the unified deduped listing (same as `pai`).\nUse --all to include cold / zero-session / archived projects.").option("--all", "Show all entries including cold / zero-session / archived projects").option("-n, --n <count>", "Max rows to show", "20").action(async (opts) => {
|
|
1876
|
-
const { cmdMain } = await import("./main-resolver-
|
|
1891
|
+
const { cmdMain } = await import("./main-resolver-Dxh444GO.mjs");
|
|
1877
1892
|
await cmdMain(getDb(), void 0, void 0, opts);
|
|
1878
1893
|
});
|
|
1879
1894
|
projectsCmd.command("list-raw").description("Raw registry listing with slug, path, status, last_active columns").option("--all", "Include archived projects (default: active only)").option("--status <status>", "Filter by status: active | archived").option("--tag <tag>", "Filter by tag").option("--type <type>", "Filter by type").action((opts) => {
|
|
@@ -1884,7 +1899,7 @@ function registerProjectsCommands(projectsCmd, getDb) {
|
|
|
1884
1899
|
const project = resolveIdentifier(db, identifier);
|
|
1885
1900
|
if (!project) {
|
|
1886
1901
|
console.error(`Project not found: ${identifier}`);
|
|
1887
|
-
process.
|
|
1902
|
+
process.exitCode = 1;
|
|
1888
1903
|
return;
|
|
1889
1904
|
}
|
|
1890
1905
|
if (existsSync(project.root_path)) {
|
|
@@ -1984,7 +1999,8 @@ const SESSION_REGISTRY_PATH = join(homedir(), ".claude", "session-registry.json"
|
|
|
1984
1999
|
function cmdMigrate$1(db) {
|
|
1985
2000
|
if (!existsSync(SESSION_REGISTRY_PATH)) {
|
|
1986
2001
|
console.error(err(`session-registry.json not found: ${SESSION_REGISTRY_PATH}`));
|
|
1987
|
-
process.
|
|
2002
|
+
process.exitCode = 1;
|
|
2003
|
+
return;
|
|
1988
2004
|
}
|
|
1989
2005
|
let registry;
|
|
1990
2006
|
try {
|
|
@@ -1992,7 +2008,8 @@ function cmdMigrate$1(db) {
|
|
|
1992
2008
|
registry = JSON.parse(raw);
|
|
1993
2009
|
} catch (e) {
|
|
1994
2010
|
console.error(err(`Failed to parse session-registry.json: ${e}`));
|
|
1995
|
-
process.
|
|
2011
|
+
process.exitCode = 1;
|
|
2012
|
+
return;
|
|
1996
2013
|
}
|
|
1997
2014
|
const projects = registry.projects ?? [];
|
|
1998
2015
|
if (!projects.length) {
|
|
@@ -2326,7 +2343,8 @@ function cmdDedupe(db, opts) {
|
|
|
2326
2343
|
if (backup) console.log(ok$1(`Registry backed up to ${bold$1(backup)}`));
|
|
2327
2344
|
else {
|
|
2328
2345
|
console.error(err("Could not back up the registry — refusing to merge without one."));
|
|
2329
|
-
process.
|
|
2346
|
+
process.exitCode = 1;
|
|
2347
|
+
return;
|
|
2330
2348
|
}
|
|
2331
2349
|
}
|
|
2332
2350
|
const totals = {
|
|
@@ -2362,7 +2380,8 @@ function cmdDedupe(db, opts) {
|
|
|
2362
2380
|
run();
|
|
2363
2381
|
} catch (e) {
|
|
2364
2382
|
console.error(err(`Merge failed and was rolled back: ${String(e)}`));
|
|
2365
|
-
process.
|
|
2383
|
+
process.exitCode = 1;
|
|
2384
|
+
return;
|
|
2366
2385
|
}
|
|
2367
2386
|
console.log();
|
|
2368
2387
|
console.log(ok$1(`Merged ${totals.deletedRows} duplicate row(s).`));
|
|
@@ -2481,7 +2500,10 @@ function cmdRebuild(db) {
|
|
|
2481
2500
|
function cmdLookup(db, fsPath) {
|
|
2482
2501
|
const resolved = resolve(fsPath);
|
|
2483
2502
|
const row = db.prepare("SELECT slug FROM projects WHERE root_path = ?").get(resolved);
|
|
2484
|
-
if (!row)
|
|
2503
|
+
if (!row) {
|
|
2504
|
+
process.exitCode = 1;
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2485
2507
|
process.stdout.write(row.slug + "\n");
|
|
2486
2508
|
}
|
|
2487
2509
|
function registerRegistryCommands(registryCmd, getDb) {
|
|
@@ -2502,7 +2524,8 @@ function registerRegistryCommands(registryCmd, getDb) {
|
|
|
2502
2524
|
const resolved = resolveHome(opts.addDir);
|
|
2503
2525
|
if (!existsSync(resolved)) {
|
|
2504
2526
|
console.error(err(`Directory not found: ${resolved}`));
|
|
2505
|
-
process.
|
|
2527
|
+
process.exitCode = 1;
|
|
2528
|
+
return;
|
|
2506
2529
|
}
|
|
2507
2530
|
const display = resolved.startsWith(homedir()) ? "~" + resolved.slice(homedir().length) : resolved;
|
|
2508
2531
|
if (config.scan_dirs.includes(display) || config.scan_dirs.includes(resolved)) console.log(warn$1(`Already configured: ${display}`));
|
|
@@ -2567,13 +2590,15 @@ function registerEmbedCommand(memoryCmd, getDb) {
|
|
|
2567
2590
|
federation = openFederation();
|
|
2568
2591
|
} catch (e) {
|
|
2569
2592
|
console.error(err(`Failed to open federation database: ${e}`));
|
|
2570
|
-
process.
|
|
2593
|
+
process.exitCode = 1;
|
|
2594
|
+
return;
|
|
2571
2595
|
}
|
|
2572
2596
|
if (projectSlug) {
|
|
2573
2597
|
const project = registryDb.prepare("SELECT id, slug FROM projects WHERE slug = ?").get(projectSlug);
|
|
2574
2598
|
if (!project) {
|
|
2575
2599
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
2576
|
-
process.
|
|
2600
|
+
process.exitCode = 1;
|
|
2601
|
+
return;
|
|
2577
2602
|
}
|
|
2578
2603
|
await runEmbed(federation, project.id, project.slug, parseInt(opts.batchSize ?? "50", 10));
|
|
2579
2604
|
} else await runEmbed(federation, void 0, void 0, parseInt(opts.batchSize ?? "50", 10));
|
|
@@ -2598,13 +2623,15 @@ function registerIndexCommand(memoryCmd, getDb) {
|
|
|
2598
2623
|
federation = openFederation();
|
|
2599
2624
|
} catch (e) {
|
|
2600
2625
|
console.error(err(`Failed to open federation database: ${e}`));
|
|
2601
|
-
process.
|
|
2626
|
+
process.exitCode = 1;
|
|
2627
|
+
return;
|
|
2602
2628
|
}
|
|
2603
2629
|
if (projectSlug) {
|
|
2604
2630
|
const project = registryDb.prepare("SELECT id, slug, display_name, root_path FROM projects WHERE slug = ? AND status = 'active'").get(projectSlug);
|
|
2605
2631
|
if (!project) {
|
|
2606
2632
|
console.error(err(`Project not found or not active: ${projectSlug}`));
|
|
2607
|
-
process.
|
|
2633
|
+
process.exitCode = 1;
|
|
2634
|
+
return;
|
|
2608
2635
|
}
|
|
2609
2636
|
console.log(dim$1(`Indexing ${project.display_name} (${project.slug})...`));
|
|
2610
2637
|
const result = await indexProject(federation, project.id, project.root_path);
|
|
@@ -2638,7 +2665,8 @@ function registerSearchCommand(memoryCmd, getDb) {
|
|
|
2638
2665
|
federation = openFederation();
|
|
2639
2666
|
} catch (e) {
|
|
2640
2667
|
console.error(err(`Failed to open federation database: ${e}`));
|
|
2641
|
-
process.
|
|
2668
|
+
process.exitCode = 1;
|
|
2669
|
+
return;
|
|
2642
2670
|
}
|
|
2643
2671
|
const config = loadConfig();
|
|
2644
2672
|
const searchConfig = config.search;
|
|
@@ -2650,7 +2678,8 @@ function registerSearchCommand(memoryCmd, getDb) {
|
|
|
2650
2678
|
"hybrid"
|
|
2651
2679
|
].includes(mode)) {
|
|
2652
2680
|
console.error(err(`Invalid mode: ${mode}. Use keyword, semantic, or hybrid.`));
|
|
2653
|
-
process.
|
|
2681
|
+
process.exitCode = 1;
|
|
2682
|
+
return;
|
|
2654
2683
|
}
|
|
2655
2684
|
let projectIds;
|
|
2656
2685
|
if (opts.project) {
|
|
@@ -2669,7 +2698,7 @@ function registerSearchCommand(memoryCmd, getDb) {
|
|
|
2669
2698
|
else if (mode === "semantic" || mode === "hybrid") {
|
|
2670
2699
|
const backend = await createStorageBackend(config);
|
|
2671
2700
|
try {
|
|
2672
|
-
const { generateEmbedding } = await import("./embeddings-
|
|
2701
|
+
const { generateEmbedding } = await import("./embeddings-CEBGrzwu.mjs");
|
|
2673
2702
|
console.log(dim$1("Generating query embedding..."));
|
|
2674
2703
|
const queryEmbedding = await generateEmbedding(query, true);
|
|
2675
2704
|
if (mode === "semantic") results = await backend.searchSemantic(queryEmbedding, searchOpts);
|
|
@@ -2718,13 +2747,13 @@ function registerSearchCommand(memoryCmd, getDb) {
|
|
|
2718
2747
|
return;
|
|
2719
2748
|
}
|
|
2720
2749
|
if (opts.rerank !== false) {
|
|
2721
|
-
const { rerankResults } = await import("./reranker-
|
|
2750
|
+
const { rerankResults } = await import("./reranker-CwTCNsgA.mjs");
|
|
2722
2751
|
console.log(dim$1("Reranking with cross-encoder..."));
|
|
2723
2752
|
results = await rerankResults(query, results, { topK: maxResults });
|
|
2724
2753
|
}
|
|
2725
2754
|
const recencyDays = parseInt(opts.recency ?? String(searchConfig.recencyBoostDays), 10);
|
|
2726
2755
|
if (recencyDays > 0) {
|
|
2727
|
-
const { applyRecencyBoost } = await import("./search-
|
|
2756
|
+
const { applyRecencyBoost } = await import("./search-CfPpJAWQ.mjs");
|
|
2728
2757
|
console.log(dim$1(`Applying recency boost (half-life: ${recencyDays} days)...`));
|
|
2729
2758
|
results = applyRecencyBoost(results, recencyDays);
|
|
2730
2759
|
}
|
|
@@ -2764,7 +2793,8 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2764
2793
|
federation = openFederation();
|
|
2765
2794
|
} catch (e) {
|
|
2766
2795
|
console.error(err(`Failed to open federation database: ${e}`));
|
|
2767
|
-
process.
|
|
2796
|
+
process.exitCode = 1;
|
|
2797
|
+
return;
|
|
2768
2798
|
}
|
|
2769
2799
|
try {
|
|
2770
2800
|
federation.pragma("busy_timeout = 4000");
|
|
@@ -2787,7 +2817,8 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2787
2817
|
const project = registryDb.prepare("SELECT id, slug, display_name FROM projects WHERE slug = ?").get(projectSlug);
|
|
2788
2818
|
if (!project) {
|
|
2789
2819
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
2790
|
-
process.
|
|
2820
|
+
process.exitCode = 1;
|
|
2821
|
+
return;
|
|
2791
2822
|
}
|
|
2792
2823
|
const fileStats = federation.prepare("SELECT COUNT(*) as files FROM memory_files WHERE project_id = ?").get(project.id);
|
|
2793
2824
|
const chunkStats = federation.prepare("SELECT COUNT(*) as chunks FROM memory_chunks WHERE project_id = ?").get(project.id);
|
|
@@ -2863,7 +2894,8 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2863
2894
|
if (val === void 0) {
|
|
2864
2895
|
console.error(err(`Unknown setting: ${key}`));
|
|
2865
2896
|
console.log(dim$1(` Valid keys: mode, rerank, recencyBoostDays, defaultLimit, snippetLength`));
|
|
2866
|
-
process.
|
|
2897
|
+
process.exitCode = 1;
|
|
2898
|
+
return;
|
|
2867
2899
|
}
|
|
2868
2900
|
console.log(String(val));
|
|
2869
2901
|
return;
|
|
@@ -2878,14 +2910,16 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2878
2910
|
if (!validKeys.has(key)) {
|
|
2879
2911
|
console.error(err(`Unknown setting: ${key}`));
|
|
2880
2912
|
console.log(dim$1(` Valid keys: ${[...validKeys].join(", ")}`));
|
|
2881
|
-
process.
|
|
2913
|
+
process.exitCode = 1;
|
|
2914
|
+
return;
|
|
2882
2915
|
}
|
|
2883
2916
|
let fileConfig = {};
|
|
2884
2917
|
if (existsSync(CONFIG_FILE$2)) try {
|
|
2885
2918
|
fileConfig = JSON.parse(readFileSync(CONFIG_FILE$2, "utf-8"));
|
|
2886
2919
|
} catch {
|
|
2887
2920
|
console.error(err(`Could not parse ${CONFIG_FILE$2}`));
|
|
2888
|
-
process.
|
|
2921
|
+
process.exitCode = 1;
|
|
2922
|
+
return;
|
|
2889
2923
|
}
|
|
2890
2924
|
if (!fileConfig.search || typeof fileConfig.search !== "object") fileConfig.search = {};
|
|
2891
2925
|
let parsed;
|
|
@@ -2896,7 +2930,8 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2896
2930
|
"hybrid"
|
|
2897
2931
|
].includes(value)) {
|
|
2898
2932
|
console.error(err(`Invalid mode: ${value}. Must be keyword, semantic, or hybrid.`));
|
|
2899
|
-
process.
|
|
2933
|
+
process.exitCode = 1;
|
|
2934
|
+
return;
|
|
2900
2935
|
}
|
|
2901
2936
|
parsed = value;
|
|
2902
2937
|
} else if (key === "rerank") parsed = value === "true" || value === "1" || value === "on";
|
|
@@ -2904,7 +2939,8 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2904
2939
|
parsed = parseInt(value, 10);
|
|
2905
2940
|
if (isNaN(parsed)) {
|
|
2906
2941
|
console.error(err(`Invalid number: ${value}`));
|
|
2907
|
-
process.
|
|
2942
|
+
process.exitCode = 1;
|
|
2943
|
+
return;
|
|
2908
2944
|
}
|
|
2909
2945
|
}
|
|
2910
2946
|
fileConfig.search[key] = parsed;
|
|
@@ -2915,7 +2951,7 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2915
2951
|
console.log(dim$1(` Restart daemon to apply: pai daemon restart`));
|
|
2916
2952
|
} catch (e) {
|
|
2917
2953
|
console.error(err(`Could not write config: ${e}`));
|
|
2918
|
-
process.
|
|
2954
|
+
process.exitCode = 1;
|
|
2919
2955
|
}
|
|
2920
2956
|
});
|
|
2921
2957
|
}
|
|
@@ -2924,9 +2960,9 @@ function registerStatsCommands(memoryCmd, getDb) {
|
|
|
2924
2960
|
//#region src/cli/commands/memory/sources-cmd.ts
|
|
2925
2961
|
function registerSourcesCommand(memoryCmd) {
|
|
2926
2962
|
memoryCmd.command("sources").description("Show what the indexer has taken in: composition by source, which roots\ncontent enters from, the heaviest single files, and how many chunks are\nrewritten per day (which distinguishes a finite backlog from a treadmill).").option("--limit <n>", "Rows per section (default 8)", "8").action(async (opts) => {
|
|
2927
|
-
const { createStorageBackend } = await import("./factory-
|
|
2928
|
-
const { loadConfig } = await import("./config-
|
|
2929
|
-
const { cmdMemorySources } = await import("./sources-
|
|
2963
|
+
const { createStorageBackend } = await import("./factory-CrokPMk2.mjs");
|
|
2964
|
+
const { loadConfig } = await import("./config-C_ErGddD.mjs");
|
|
2965
|
+
const { cmdMemorySources } = await import("./sources-D8ZdNfvK.mjs");
|
|
2930
2966
|
let backend;
|
|
2931
2967
|
try {
|
|
2932
2968
|
backend = await createStorageBackend(loadConfig());
|
|
@@ -3079,7 +3115,8 @@ function registerIdentityCommands(identityCmd) {
|
|
|
3079
3115
|
const normalized = normalizeEmail(email);
|
|
3080
3116
|
if (!normalized) {
|
|
3081
3117
|
console.error(err("pai identity: ") + `Not a usable email address: ${email}`);
|
|
3082
|
-
process.
|
|
3118
|
+
process.exitCode = 1;
|
|
3119
|
+
return;
|
|
3083
3120
|
}
|
|
3084
3121
|
const config = readConfig();
|
|
3085
3122
|
const id = identityOf(config);
|
|
@@ -3118,7 +3155,8 @@ function registerIdentityCommands(identityCmd) {
|
|
|
3118
3155
|
const normalized = normalizeEmail(email);
|
|
3119
3156
|
if (!normalized) {
|
|
3120
3157
|
console.error(err("pai identity: ") + `Not a usable email address: ${email}`);
|
|
3121
|
-
process.
|
|
3158
|
+
process.exitCode = 1;
|
|
3159
|
+
return;
|
|
3122
3160
|
}
|
|
3123
3161
|
const config = readConfig();
|
|
3124
3162
|
const id = identityOf(config);
|
|
@@ -3236,7 +3274,8 @@ function cmdInstall$1() {
|
|
|
3236
3274
|
writeClaudeJson(config);
|
|
3237
3275
|
} catch (e) {
|
|
3238
3276
|
console.error(err(`Failed to write ~/.claude.json: ${e}`));
|
|
3239
|
-
process.
|
|
3277
|
+
process.exitCode = 1;
|
|
3278
|
+
return;
|
|
3240
3279
|
}
|
|
3241
3280
|
console.log(ok$1("PAI MCP server registered in ~/.claude.json."));
|
|
3242
3281
|
console.log(dim$1(` Binary: ${mcpBin}`));
|
|
@@ -3443,7 +3482,7 @@ async function cmdStatus$2() {
|
|
|
3443
3482
|
console.log(dim$1(" Start with: pai daemon serve"));
|
|
3444
3483
|
console.log(dim$1(" Or install as service: pai daemon install"));
|
|
3445
3484
|
console.log();
|
|
3446
|
-
process.
|
|
3485
|
+
process.exitCode = 1;
|
|
3447
3486
|
}
|
|
3448
3487
|
}
|
|
3449
3488
|
function cmdRestart() {
|
|
@@ -3470,7 +3509,7 @@ function cmdRestart() {
|
|
|
3470
3509
|
} catch (e) {
|
|
3471
3510
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3472
3511
|
console.error(err(`restart error: ${msg}`));
|
|
3473
|
-
process.
|
|
3512
|
+
process.exitCode = 1;
|
|
3474
3513
|
}
|
|
3475
3514
|
}
|
|
3476
3515
|
function cmdInstall() {
|
|
@@ -3486,7 +3525,8 @@ function cmdInstall() {
|
|
|
3486
3525
|
console.log(ok$1(` Wrote launchd plist: ${PLIST_PATH}`));
|
|
3487
3526
|
} catch (e) {
|
|
3488
3527
|
console.error(err(` Failed to write plist: ${e}`));
|
|
3489
|
-
process.
|
|
3528
|
+
process.exitCode = 1;
|
|
3529
|
+
return;
|
|
3490
3530
|
}
|
|
3491
3531
|
try {
|
|
3492
3532
|
spawnSync("launchctl", ["unload", PLIST_PATH], { encoding: "utf8" });
|
|
@@ -3516,7 +3556,8 @@ function cmdInstall() {
|
|
|
3516
3556
|
console.log(ok$1(" Updated ~/.claude.json to use daemon shim."));
|
|
3517
3557
|
} catch (e) {
|
|
3518
3558
|
console.error(err(` Failed to write ~/.claude.json: ${e}`));
|
|
3519
|
-
process.
|
|
3559
|
+
process.exitCode = 1;
|
|
3560
|
+
return;
|
|
3520
3561
|
}
|
|
3521
3562
|
}
|
|
3522
3563
|
console.log();
|
|
@@ -3585,7 +3626,7 @@ async function cmdMigrate(connectionString) {
|
|
|
3585
3626
|
});
|
|
3586
3627
|
if (result.status !== 0) {
|
|
3587
3628
|
console.error(err(" Migration failed. Check output above."));
|
|
3588
|
-
process.
|
|
3629
|
+
process.exitCode = result.status ?? 1;
|
|
3589
3630
|
}
|
|
3590
3631
|
}
|
|
3591
3632
|
function cmdLogs(opts) {
|
|
@@ -3603,13 +3644,13 @@ function cmdLogs(opts) {
|
|
|
3603
3644
|
} catch (e) {
|
|
3604
3645
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3605
3646
|
console.error(err(`Could not read log: ${msg}`));
|
|
3606
|
-
process.
|
|
3647
|
+
process.exitCode = 1;
|
|
3607
3648
|
}
|
|
3608
3649
|
}
|
|
3609
3650
|
function registerDaemonCommands(daemonCmd) {
|
|
3610
3651
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
3611
|
-
const { serve } = await import("./daemon-
|
|
3612
|
-
const { loadConfig: lc, ensureConfigDir } = await import("./config-
|
|
3652
|
+
const { serve } = await import("./daemon-DJEFqV84.mjs");
|
|
3653
|
+
const { loadConfig: lc, ensureConfigDir } = await import("./config-C_ErGddD.mjs");
|
|
3613
3654
|
ensureConfigDir();
|
|
3614
3655
|
await serve(lc());
|
|
3615
3656
|
});
|
|
@@ -3824,14 +3865,16 @@ function registerRestoreCommands(program) {
|
|
|
3824
3865
|
resolvedDir = backupPath.startsWith("~") ? backupPath.replace(/^~/, HOME) : backupPath;
|
|
3825
3866
|
if (!existsSync(resolvedDir)) {
|
|
3826
3867
|
console.error(err(`Backup directory not found: ${resolvedDir}`));
|
|
3827
|
-
process.
|
|
3868
|
+
process.exitCode = 1;
|
|
3869
|
+
return;
|
|
3828
3870
|
}
|
|
3829
3871
|
} else {
|
|
3830
3872
|
const backups = listBackups();
|
|
3831
3873
|
if (backups.length === 0) {
|
|
3832
3874
|
console.error(err(`No backups found in ${BACKUPS_DIR}`));
|
|
3833
3875
|
console.log(dim$1(" Run 'pai backup' to create one."));
|
|
3834
|
-
process.
|
|
3876
|
+
process.exitCode = 1;
|
|
3877
|
+
return;
|
|
3835
3878
|
}
|
|
3836
3879
|
console.log(`\n${bold$1("Available backups")} (newest first):\n`);
|
|
3837
3880
|
backups.forEach((name, i) => {
|
|
@@ -3859,7 +3902,7 @@ function registerRestoreCommands(program) {
|
|
|
3859
3902
|
if (hasSql && opts.postgres) console.log(warn$1(" Postgres database will be dropped and recreated from the backup."));
|
|
3860
3903
|
if (!(opts.yes || await confirm("Proceed with restore?"))) {
|
|
3861
3904
|
console.log(dim$1("Restore cancelled."));
|
|
3862
|
-
|
|
3905
|
+
return;
|
|
3863
3906
|
}
|
|
3864
3907
|
console.log();
|
|
3865
3908
|
const results = [];
|
|
@@ -3968,7 +4011,8 @@ function registerRestoreCommands(program) {
|
|
|
3968
4011
|
console.log(` ${dim$1("Restart:")} pai daemon restart\n`);
|
|
3969
4012
|
} else {
|
|
3970
4013
|
console.log(`\n ${warn$1("Some items failed — check output above.")}\n`);
|
|
3971
|
-
process.
|
|
4014
|
+
process.exitCode = 1;
|
|
4015
|
+
return;
|
|
3972
4016
|
}
|
|
3973
4017
|
});
|
|
3974
4018
|
}
|
|
@@ -4053,6 +4097,65 @@ function writeJsonAtomic(path, data, opts = {}) {
|
|
|
4053
4097
|
}
|
|
4054
4098
|
}
|
|
4055
4099
|
|
|
4100
|
+
//#endregion
|
|
4101
|
+
//#region src/cli/lib/exit.ts
|
|
4102
|
+
/**
|
|
4103
|
+
* Safe process termination for the CLI.
|
|
4104
|
+
*
|
|
4105
|
+
* Node's stdout/stderr are ASYNCHRONOUS when they point at a pipe (and
|
|
4106
|
+
* synchronous when they point at a TTY or a regular file). `process.exit()`
|
|
4107
|
+
* tears the process down immediately, before any still-in-flight async
|
|
4108
|
+
* write completes — so a command whose output is piped (`pai ... | less`,
|
|
4109
|
+
* `| wc -c`, a subshell capture, etc.) can have its output silently
|
|
4110
|
+
* truncated or dropped entirely, while the same command run against a TTY
|
|
4111
|
+
* or redirected to a file prints correctly. Exit code is still 0, so the
|
|
4112
|
+
* failure is invisible.
|
|
4113
|
+
*
|
|
4114
|
+
* The robust fix, used everywhere it is possible, is to never call
|
|
4115
|
+
* `process.exit()` at all: set `process.exitCode` and let the command
|
|
4116
|
+
* return normally. Once the event loop has nothing left to do, Node exits
|
|
4117
|
+
* on its own — and a natural exit always flushes pending writes first.
|
|
4118
|
+
*
|
|
4119
|
+
* `exitAfterFlush` exists for the rare call site where control flow
|
|
4120
|
+
* genuinely cannot return (e.g. deep inside a callback with no path back
|
|
4121
|
+
* to the caller). It waits for both stdout and stderr to drain before
|
|
4122
|
+
* calling `process.exit()`.
|
|
4123
|
+
*/
|
|
4124
|
+
function streamDrained(stream) {
|
|
4125
|
+
if (!stream.writableLength) return Promise.resolve();
|
|
4126
|
+
return new Promise((resolve) => {
|
|
4127
|
+
stream.once("drain", () => resolve());
|
|
4128
|
+
const check = () => {
|
|
4129
|
+
if (!stream.writableLength) resolve();
|
|
4130
|
+
};
|
|
4131
|
+
setImmediate(check);
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
/**
|
|
4135
|
+
* Wait for stdout and stderr to finish flushing, then exit with `code`.
|
|
4136
|
+
* Never returns (the return type documents that; callers should not
|
|
4137
|
+
* expect code after this call to run).
|
|
4138
|
+
*/
|
|
4139
|
+
async function exitAfterFlush(code) {
|
|
4140
|
+
await drainStdio();
|
|
4141
|
+
process.exit(code);
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Wait for stdout and stderr to finish flushing, without exiting.
|
|
4145
|
+
*
|
|
4146
|
+
* Belt-and-suspenders backstop for cli/index.ts: every command path avoids
|
|
4147
|
+
* process.exit() so a natural process exit — which always flushes pending
|
|
4148
|
+
* writes — can do the flushing. This makes that explicit and awaited at the
|
|
4149
|
+
* one place every command path funnels through, instead of trusting that
|
|
4150
|
+
* nothing between here and process shutdown (a dependency's keep-alive
|
|
4151
|
+
* socket being unref'd, a timer, anything else that can make the event loop
|
|
4152
|
+
* look "empty") lets Node begin tearing down before a write that's still
|
|
4153
|
+
* asynchronously in flight to a pipe has actually reached the OS.
|
|
4154
|
+
*/
|
|
4155
|
+
async function drainStdio() {
|
|
4156
|
+
await Promise.all([streamDrained(process.stdout), streamDrained(process.stderr)]);
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4056
4159
|
//#endregion
|
|
4057
4160
|
//#region src/cli/commands/setup/utils.ts
|
|
4058
4161
|
/**
|
|
@@ -4088,7 +4191,7 @@ function createRl() {
|
|
|
4088
4191
|
line$1();
|
|
4089
4192
|
line$1(c.dim(" Setup cancelled. Run `pai setup` again to restart."));
|
|
4090
4193
|
line$1();
|
|
4091
|
-
|
|
4194
|
+
exitAfterFlush(0);
|
|
4092
4195
|
});
|
|
4093
4196
|
return rl;
|
|
4094
4197
|
}
|
|
@@ -6948,7 +7051,8 @@ function cmdOpen(opts) {
|
|
|
6948
7051
|
if (!existsSync(vaultPath)) {
|
|
6949
7052
|
console.error(err(`Vault not found at: ${vaultPath}`));
|
|
6950
7053
|
console.error(dim$1(" Run: pai obsidian sync"));
|
|
6951
|
-
process.
|
|
7054
|
+
process.exitCode = 1;
|
|
7055
|
+
return;
|
|
6952
7056
|
}
|
|
6953
7057
|
const url = `obsidian://open?vault=${encodeURIComponent(vaultName)}`;
|
|
6954
7058
|
console.log(dim$1(` Opening: ${url}`));
|
|
@@ -6958,7 +7062,8 @@ function cmdOpen(opts) {
|
|
|
6958
7062
|
} catch (e) {
|
|
6959
7063
|
console.error(err(` Failed to open Obsidian: ${e}`));
|
|
6960
7064
|
console.error(dim$1(" Ensure Obsidian is installed and the vault is registered in Obsidian."));
|
|
6961
|
-
process.
|
|
7065
|
+
process.exitCode = 1;
|
|
7066
|
+
return;
|
|
6962
7067
|
}
|
|
6963
7068
|
}
|
|
6964
7069
|
function registerObsidianCommands(obsidianCmd, getDb) {
|
|
@@ -6997,7 +7102,7 @@ async function cmdExplore(note, opts) {
|
|
|
6997
7102
|
const depth = parseInt(opts.depth ?? "3", 10);
|
|
6998
7103
|
const direction = opts.direction ?? "both";
|
|
6999
7104
|
const mode = opts.mode ?? "all";
|
|
7000
|
-
const { zettelExplore } = await import("./zettelkasten-
|
|
7105
|
+
const { zettelExplore } = await import("./zettelkasten-CvjmMghT.mjs");
|
|
7001
7106
|
const result = zettelExplore(getFedDb(), {
|
|
7002
7107
|
startNote: note,
|
|
7003
7108
|
depth,
|
|
@@ -7049,7 +7154,8 @@ function registerExploreCommand(parent) {
|
|
|
7049
7154
|
await cmdExplore(note, opts);
|
|
7050
7155
|
} catch (e) {
|
|
7051
7156
|
console.error(err(` Error: ${e}`));
|
|
7052
|
-
process.
|
|
7157
|
+
process.exitCode = 1;
|
|
7158
|
+
return;
|
|
7053
7159
|
}
|
|
7054
7160
|
});
|
|
7055
7161
|
}
|
|
@@ -7061,7 +7167,7 @@ async function cmdHealth(opts) {
|
|
|
7061
7167
|
const projectPath = opts.project;
|
|
7062
7168
|
const recentDays = parseInt(opts.days ?? "30", 10);
|
|
7063
7169
|
const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
|
|
7064
|
-
const { zettelHealth } = await import("./zettelkasten-
|
|
7170
|
+
const { zettelHealth } = await import("./zettelkasten-CvjmMghT.mjs");
|
|
7065
7171
|
const result = zettelHealth(getFedDb(), {
|
|
7066
7172
|
scope,
|
|
7067
7173
|
projectPath,
|
|
@@ -7114,7 +7220,8 @@ function registerHealthCommand(parent) {
|
|
|
7114
7220
|
await cmdHealth(opts);
|
|
7115
7221
|
} catch (e) {
|
|
7116
7222
|
console.error(err(` Error: ${e}`));
|
|
7117
|
-
process.
|
|
7223
|
+
process.exitCode = 1;
|
|
7224
|
+
return;
|
|
7118
7225
|
}
|
|
7119
7226
|
});
|
|
7120
7227
|
}
|
|
@@ -7124,13 +7231,14 @@ function registerHealthCommand(parent) {
|
|
|
7124
7231
|
async function cmdSurprise(note, opts) {
|
|
7125
7232
|
if (!opts.vaultProjectId) {
|
|
7126
7233
|
console.error(err(" --vault-project-id is required"));
|
|
7127
|
-
process.
|
|
7234
|
+
process.exitCode = 1;
|
|
7235
|
+
return;
|
|
7128
7236
|
}
|
|
7129
7237
|
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
7130
7238
|
const limit = parseInt(opts.limit ?? "10", 10);
|
|
7131
7239
|
const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
|
|
7132
7240
|
const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
|
|
7133
|
-
const { zettelSurprise } = await import("./zettelkasten-
|
|
7241
|
+
const { zettelSurprise } = await import("./zettelkasten-CvjmMghT.mjs");
|
|
7134
7242
|
const db = getFedDb();
|
|
7135
7243
|
console.log();
|
|
7136
7244
|
console.log(header(" PAI Zettel Surprise"));
|
|
@@ -7168,7 +7276,8 @@ function registerSurpriseCommand(parent) {
|
|
|
7168
7276
|
await cmdSurprise(note, opts);
|
|
7169
7277
|
} catch (e) {
|
|
7170
7278
|
console.error(err(` Error: ${e}`));
|
|
7171
|
-
process.
|
|
7279
|
+
process.exitCode = 1;
|
|
7280
|
+
return;
|
|
7172
7281
|
}
|
|
7173
7282
|
});
|
|
7174
7283
|
}
|
|
@@ -7178,12 +7287,13 @@ function registerSurpriseCommand(parent) {
|
|
|
7178
7287
|
async function cmdSuggest(note, opts) {
|
|
7179
7288
|
if (!opts.vaultProjectId) {
|
|
7180
7289
|
console.error(err(" --vault-project-id is required"));
|
|
7181
|
-
process.
|
|
7290
|
+
process.exitCode = 1;
|
|
7291
|
+
return;
|
|
7182
7292
|
}
|
|
7183
7293
|
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
7184
7294
|
const limit = parseInt(opts.limit ?? "5", 10);
|
|
7185
7295
|
const excludeLinked = opts.excludeLinked !== false;
|
|
7186
|
-
const { zettelSuggest } = await import("./zettelkasten-
|
|
7296
|
+
const { zettelSuggest } = await import("./zettelkasten-CvjmMghT.mjs");
|
|
7187
7297
|
const db = getFedDb();
|
|
7188
7298
|
console.log();
|
|
7189
7299
|
console.log(header(" PAI Zettel Suggest"));
|
|
@@ -7219,7 +7329,8 @@ function registerSuggestCommand(parent) {
|
|
|
7219
7329
|
await cmdSuggest(note, opts);
|
|
7220
7330
|
} catch (e) {
|
|
7221
7331
|
console.error(err(` Error: ${e}`));
|
|
7222
|
-
process.
|
|
7332
|
+
process.exitCode = 1;
|
|
7333
|
+
return;
|
|
7223
7334
|
}
|
|
7224
7335
|
});
|
|
7225
7336
|
}
|
|
@@ -7229,12 +7340,13 @@ function registerSuggestCommand(parent) {
|
|
|
7229
7340
|
async function cmdConverse(question, opts) {
|
|
7230
7341
|
if (!opts.vaultProjectId) {
|
|
7231
7342
|
console.error(err(" --vault-project-id is required"));
|
|
7232
|
-
process.
|
|
7343
|
+
process.exitCode = 1;
|
|
7344
|
+
return;
|
|
7233
7345
|
}
|
|
7234
7346
|
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
7235
7347
|
const depth = parseInt(opts.depth ?? "2", 10);
|
|
7236
7348
|
const limit = parseInt(opts.limit ?? "15", 10);
|
|
7237
|
-
const { zettelConverse } = await import("./zettelkasten-
|
|
7349
|
+
const { zettelConverse } = await import("./zettelkasten-CvjmMghT.mjs");
|
|
7238
7350
|
const db = getFedDb();
|
|
7239
7351
|
console.log();
|
|
7240
7352
|
console.log(header(" PAI Zettel Converse"));
|
|
@@ -7278,7 +7390,8 @@ function registerConverseCommand(parent) {
|
|
|
7278
7390
|
await cmdConverse(question, opts);
|
|
7279
7391
|
} catch (e) {
|
|
7280
7392
|
console.error(err(` Error: ${e}`));
|
|
7281
|
-
process.
|
|
7393
|
+
process.exitCode = 1;
|
|
7394
|
+
return;
|
|
7282
7395
|
}
|
|
7283
7396
|
});
|
|
7284
7397
|
}
|
|
@@ -7288,14 +7401,15 @@ function registerConverseCommand(parent) {
|
|
|
7288
7401
|
async function cmdThemes(opts) {
|
|
7289
7402
|
if (!opts.vaultProjectId) {
|
|
7290
7403
|
console.error(err(" --vault-project-id is required"));
|
|
7291
|
-
process.
|
|
7404
|
+
process.exitCode = 1;
|
|
7405
|
+
return;
|
|
7292
7406
|
}
|
|
7293
7407
|
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
7294
7408
|
const lookbackDays = parseInt(opts.days ?? "30", 10);
|
|
7295
7409
|
const minClusterSize = parseInt(opts.minSize ?? "3", 10);
|
|
7296
7410
|
const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
|
|
7297
7411
|
const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
|
|
7298
|
-
const { zettelThemes } = await import("./zettelkasten-
|
|
7412
|
+
const { zettelThemes } = await import("./zettelkasten-CvjmMghT.mjs");
|
|
7299
7413
|
const db = getFedDb();
|
|
7300
7414
|
console.log();
|
|
7301
7415
|
console.log(header(" PAI Zettel Themes"));
|
|
@@ -7339,7 +7453,8 @@ function registerThemesCommand(parent) {
|
|
|
7339
7453
|
await cmdThemes(opts);
|
|
7340
7454
|
} catch (e) {
|
|
7341
7455
|
console.error(err(` Error: ${e}`));
|
|
7342
|
-
process.
|
|
7456
|
+
process.exitCode = 1;
|
|
7457
|
+
return;
|
|
7343
7458
|
}
|
|
7344
7459
|
});
|
|
7345
7460
|
}
|
|
@@ -7436,7 +7551,8 @@ async function cmdList$2(opts) {
|
|
|
7436
7551
|
} catch (e) {
|
|
7437
7552
|
console.error(err(` Failed to reach PAI daemon: ${e}`));
|
|
7438
7553
|
console.error(dim$1(" Is the daemon running? Try: pai daemon status"));
|
|
7439
|
-
process.
|
|
7554
|
+
process.exitCode = 1;
|
|
7555
|
+
return;
|
|
7440
7556
|
}
|
|
7441
7557
|
console.log();
|
|
7442
7558
|
console.log(header(" PAI Observations"));
|
|
@@ -7489,7 +7605,8 @@ async function cmdSearch(query, opts) {
|
|
|
7489
7605
|
} catch (e) {
|
|
7490
7606
|
console.error(err(` Failed to reach PAI daemon: ${e}`));
|
|
7491
7607
|
console.error(dim$1(" Is the daemon running? Try: pai daemon status"));
|
|
7492
|
-
process.
|
|
7608
|
+
process.exitCode = 1;
|
|
7609
|
+
return;
|
|
7493
7610
|
}
|
|
7494
7611
|
const q = query.toLowerCase();
|
|
7495
7612
|
const observations = allObservations.filter((o) => (o.title ?? "").toLowerCase().includes(q) || (o.narrative ?? "").toLowerCase().includes(q) || (o.tool_input_summary ?? "").toLowerCase().includes(q)).slice(0, limit);
|
|
@@ -7528,7 +7645,8 @@ async function cmdStats$1() {
|
|
|
7528
7645
|
} catch (e) {
|
|
7529
7646
|
console.error(err(` Failed to reach PAI daemon: ${e}`));
|
|
7530
7647
|
console.error(dim$1(" Is the daemon running? Try: pai daemon status"));
|
|
7531
|
-
process.
|
|
7648
|
+
process.exitCode = 1;
|
|
7649
|
+
return;
|
|
7532
7650
|
}
|
|
7533
7651
|
console.log();
|
|
7534
7652
|
console.log(header(" PAI Observation Statistics"));
|
|
@@ -7573,7 +7691,8 @@ function registerObservationCommands(parent) {
|
|
|
7573
7691
|
await cmdList$2(opts);
|
|
7574
7692
|
} catch (e) {
|
|
7575
7693
|
console.error(err(` Error: ${e}`));
|
|
7576
|
-
process.
|
|
7694
|
+
process.exitCode = 1;
|
|
7695
|
+
return;
|
|
7577
7696
|
}
|
|
7578
7697
|
});
|
|
7579
7698
|
parent.command("search <query>").description("Search observations by title or narrative text").option("--project <slug>", "Filter by project slug").option("--type <type>", "Filter by type").option("--limit <n>", "Maximum results", "20").action(async (query, opts) => {
|
|
@@ -7581,7 +7700,8 @@ function registerObservationCommands(parent) {
|
|
|
7581
7700
|
await cmdSearch(query, opts);
|
|
7582
7701
|
} catch (e) {
|
|
7583
7702
|
console.error(err(` Error: ${e}`));
|
|
7584
|
-
process.
|
|
7703
|
+
process.exitCode = 1;
|
|
7704
|
+
return;
|
|
7585
7705
|
}
|
|
7586
7706
|
});
|
|
7587
7707
|
parent.command("stats").description("Show observation statistics: totals, by type, by project").action(async () => {
|
|
@@ -7589,7 +7709,8 @@ function registerObservationCommands(parent) {
|
|
|
7589
7709
|
await cmdStats$1();
|
|
7590
7710
|
} catch (e) {
|
|
7591
7711
|
console.error(err(` Error: ${e}`));
|
|
7592
|
-
process.
|
|
7712
|
+
process.exitCode = 1;
|
|
7713
|
+
return;
|
|
7593
7714
|
}
|
|
7594
7715
|
});
|
|
7595
7716
|
}
|
|
@@ -7671,7 +7792,8 @@ async function cmdTelemetry(opts) {
|
|
|
7671
7792
|
} catch (e) {
|
|
7672
7793
|
console.error(err(` Failed to reach PAI daemon: ${e}`));
|
|
7673
7794
|
console.error(dim$1(" Is the daemon running? Try: pai daemon status"));
|
|
7674
|
-
process.
|
|
7795
|
+
process.exitCode = 1;
|
|
7796
|
+
return;
|
|
7675
7797
|
}
|
|
7676
7798
|
if (opts.json) {
|
|
7677
7799
|
console.log(JSON.stringify(rows ?? [], null, 2));
|
|
@@ -7714,7 +7836,8 @@ function registerSkillCommands(parent) {
|
|
|
7714
7836
|
await cmdTelemetry(opts);
|
|
7715
7837
|
} catch (e) {
|
|
7716
7838
|
console.error(err(` Error: ${e}`));
|
|
7717
|
-
process.
|
|
7839
|
+
process.exitCode = 1;
|
|
7840
|
+
return;
|
|
7718
7841
|
}
|
|
7719
7842
|
});
|
|
7720
7843
|
}
|
|
@@ -7993,7 +8116,8 @@ async function runUpdate() {
|
|
|
7993
8116
|
const repoDir = stepVerifyRepo();
|
|
7994
8117
|
if (!repoDir) {
|
|
7995
8118
|
line();
|
|
7996
|
-
process.
|
|
8119
|
+
process.exitCode = 1;
|
|
8120
|
+
return;
|
|
7997
8121
|
}
|
|
7998
8122
|
const didStash = stepStash(repoDir);
|
|
7999
8123
|
const newCommitsLog = stepPull(repoDir);
|
|
@@ -8002,7 +8126,8 @@ async function runUpdate() {
|
|
|
8002
8126
|
line();
|
|
8003
8127
|
error("Update failed at build step. Check errors above.");
|
|
8004
8128
|
line();
|
|
8005
|
-
process.
|
|
8129
|
+
process.exitCode = 1;
|
|
8130
|
+
return;
|
|
8006
8131
|
}
|
|
8007
8132
|
stepRestartDaemon(repoDir);
|
|
8008
8133
|
stepRefreshClaudeMd(repoDir, newCommitsLog);
|
|
@@ -8070,7 +8195,8 @@ async function cmdStatus() {
|
|
|
8070
8195
|
console.log(dim$1(` ${msg}`));
|
|
8071
8196
|
console.log(dim$1(" Start it with: pai daemon serve"));
|
|
8072
8197
|
console.log();
|
|
8073
|
-
process.
|
|
8198
|
+
process.exitCode = 1;
|
|
8199
|
+
return;
|
|
8074
8200
|
}
|
|
8075
8201
|
}
|
|
8076
8202
|
async function cmdSet(opts) {
|
|
@@ -8088,7 +8214,8 @@ async function cmdSet(opts) {
|
|
|
8088
8214
|
];
|
|
8089
8215
|
if (!validModes.includes(opts.mode)) {
|
|
8090
8216
|
console.error(err(`Invalid mode: ${opts.mode}. Valid: ${validModes.join(", ")}`));
|
|
8091
|
-
process.
|
|
8217
|
+
process.exitCode = 1;
|
|
8218
|
+
return;
|
|
8092
8219
|
}
|
|
8093
8220
|
patch.mode = opts.mode;
|
|
8094
8221
|
}
|
|
@@ -8112,7 +8239,8 @@ async function cmdSet(opts) {
|
|
|
8112
8239
|
console.error(err("No changes specified. Use --mode, --enable, --disable, etc."));
|
|
8113
8240
|
console.log(dim$1(" Example: pai notify set --mode voice"));
|
|
8114
8241
|
console.log(dim$1(" Example: pai notify set --mode auto --enable macos --disable ntfy"));
|
|
8115
|
-
process.
|
|
8242
|
+
process.exitCode = 1;
|
|
8243
|
+
return;
|
|
8116
8244
|
}
|
|
8117
8245
|
try {
|
|
8118
8246
|
const { config } = await client.setNotificationConfig(patch);
|
|
@@ -8123,7 +8251,8 @@ async function cmdSet(opts) {
|
|
|
8123
8251
|
} catch (e) {
|
|
8124
8252
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8125
8253
|
console.error(err(` Failed: ${msg}`));
|
|
8126
|
-
process.
|
|
8254
|
+
process.exitCode = 1;
|
|
8255
|
+
return;
|
|
8127
8256
|
}
|
|
8128
8257
|
}
|
|
8129
8258
|
async function cmdTest(opts) {
|
|
@@ -8146,7 +8275,8 @@ async function cmdTest(opts) {
|
|
|
8146
8275
|
} catch (e) {
|
|
8147
8276
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8148
8277
|
console.error(err(` Failed: ${msg}`));
|
|
8149
|
-
process.
|
|
8278
|
+
process.exitCode = 1;
|
|
8279
|
+
return;
|
|
8150
8280
|
}
|
|
8151
8281
|
}
|
|
8152
8282
|
async function cmdSend(event, message, opts) {
|
|
@@ -8160,7 +8290,8 @@ async function cmdSend(event, message, opts) {
|
|
|
8160
8290
|
];
|
|
8161
8291
|
if (!validEvents.includes(event)) {
|
|
8162
8292
|
console.error(err(`Invalid event: ${event}. Valid: ${validEvents.join(", ")}`));
|
|
8163
|
-
process.
|
|
8293
|
+
process.exitCode = 1;
|
|
8294
|
+
return;
|
|
8164
8295
|
}
|
|
8165
8296
|
try {
|
|
8166
8297
|
const result = await client.sendNotification({
|
|
@@ -8174,7 +8305,8 @@ async function cmdSend(event, message, opts) {
|
|
|
8174
8305
|
} catch (e) {
|
|
8175
8306
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8176
8307
|
console.error(err(` Failed: ${msg}`));
|
|
8177
|
-
process.
|
|
8308
|
+
process.exitCode = 1;
|
|
8309
|
+
return;
|
|
8178
8310
|
}
|
|
8179
8311
|
}
|
|
8180
8312
|
function registerNotifyCommands(notifyCmd) {
|
|
@@ -8852,8 +8984,8 @@ async function escalate(task, headline, detail, state, now, opts) {
|
|
|
8852
8984
|
state.alarmedAt[task.id] = now;
|
|
8853
8985
|
let delivered = false;
|
|
8854
8986
|
try {
|
|
8855
|
-
const { routeNotification } = await import("./router-
|
|
8856
|
-
const { loadConfig } = await import("./config-
|
|
8987
|
+
const { routeNotification } = await import("./router-BMkOb62X.mjs");
|
|
8988
|
+
const { loadConfig } = await import("./config-C_ErGddD.mjs");
|
|
8857
8989
|
delivered = (await routeNotification({
|
|
8858
8990
|
event: "error",
|
|
8859
8991
|
title: `PAI: ${headline}`,
|
|
@@ -8885,6 +9017,51 @@ function saveState$1(state, file) {
|
|
|
8885
9017
|
writeJsonAtomic(file, state, { backup: false });
|
|
8886
9018
|
}
|
|
8887
9019
|
/**
|
|
9020
|
+
* Fold in any completion a concurrent `pai task done` wrote while this tick
|
|
9021
|
+
* was busy with the rest of the list.
|
|
9022
|
+
*
|
|
9023
|
+
* A tick holds one in-memory `state` for its whole run and saves it exactly
|
|
9024
|
+
* once, at the end. For a morning routine with several due tasks that run is
|
|
9025
|
+
* not instantaneous — each dispatch waits out AIBroker's spawn-readiness
|
|
9026
|
+
* window in turn, so the tick can still be working through task 4 minutes
|
|
9027
|
+
* after task 1 was launched. `pai task done` has no such window: it loads,
|
|
9028
|
+
* releases the claim, stamps `lastSeenDue` to the next occurrence, and saves,
|
|
9029
|
+
* all in well under a second. If the session for task 1 finishes and closes
|
|
9030
|
+
* it while the tick is still on task 4, the CLI's write lands on disk first —
|
|
9031
|
+
* and the tick's blind save at the end, built from the snapshot it took
|
|
9032
|
+
* before any of this happened, overwrites it: `lastSeenDue` reverts to the
|
|
9033
|
+
* occurrence that was just completed, and `startedAt`/`claimSeenAt` come back
|
|
9034
|
+
* because this tick never learned they had been released. The next tick then
|
|
9035
|
+
* reads that reverted date as "due advanced by one period, unclaimed" — which
|
|
9036
|
+
* is `wasTicked`'s exact definition of a hand-tick — and redispatches a task
|
|
9037
|
+
* that already finished. Reproduced against a live poller run; see the
|
|
9038
|
+
* negative control referenced in Notes/TODO.md.
|
|
9039
|
+
*
|
|
9040
|
+
* `lastSeenDue` only ever moves forward in normal operation, so a disk value
|
|
9041
|
+
* strictly ahead of what this tick computed can only mean a concurrent writer
|
|
9042
|
+
* — there is exactly one, `pai task done` — saw a later completion than this
|
|
9043
|
+
* tick's stale snapshot knew about. Whenever that happens, this tick's own
|
|
9044
|
+
* value is the stale one: defer to disk for that task's date, and carry the
|
|
9045
|
+
* claim release forward too, since a more-advanced date with no such release
|
|
9046
|
+
* is not a state `pai task done` ever produces.
|
|
9047
|
+
*
|
|
9048
|
+
* Scoped to exactly the fields `pai task done` touches (`releaseClaim` plus
|
|
9049
|
+
* the `lastSeenDue` stamp) — the two writers race over nothing else, so a
|
|
9050
|
+
* broader merge would only add risk without removing any.
|
|
9051
|
+
*/
|
|
9052
|
+
function reconcileWithDisk(state, stateFile) {
|
|
9053
|
+
const onDisk = loadState$1(stateFile);
|
|
9054
|
+
for (const [id, oursDue] of Object.entries(state.lastSeenDue)) {
|
|
9055
|
+
const diskDue = onDisk.lastSeenDue[id];
|
|
9056
|
+
if (diskDue !== void 0 && diskDue > oursDue) {
|
|
9057
|
+
state.lastSeenDue[id] = diskDue;
|
|
9058
|
+
delete state.startedAt[id];
|
|
9059
|
+
delete state.claimSeenAt[id];
|
|
9060
|
+
delete state.failedProbes[id];
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
/**
|
|
8888
9065
|
* Multiple of the expected duration past which a task is flagged even though it
|
|
8889
9066
|
* keeps reporting `busy`. A session can be alive and still wrong — stuck in a
|
|
8890
9067
|
* loop, waiting on a prompt that will never come. Liveness is not progress.
|
|
@@ -9018,11 +9195,7 @@ async function tick(opts) {
|
|
|
9018
9195
|
case "abandoned":
|
|
9019
9196
|
note = `claimed ${d.elapsedMinutes}m ago, past the ${d.thresholdMinutes}m limit — releasing the claim`;
|
|
9020
9197
|
if (!opts.dryRun) {
|
|
9021
|
-
await
|
|
9022
|
-
delete state.startedAt[task.id];
|
|
9023
|
-
delete state.failedProbes[task.id];
|
|
9024
|
-
delete state.claimSeenAt[task.id];
|
|
9025
|
-
await clearRunningMark(task, opts);
|
|
9198
|
+
await releaseClaim(task, opts.provider, state);
|
|
9026
9199
|
report.stuck++;
|
|
9027
9200
|
}
|
|
9028
9201
|
break;
|
|
@@ -9042,7 +9215,10 @@ async function tick(opts) {
|
|
|
9042
9215
|
note
|
|
9043
9216
|
});
|
|
9044
9217
|
}
|
|
9045
|
-
if (!opts.dryRun)
|
|
9218
|
+
if (!opts.dryRun) {
|
|
9219
|
+
reconcileWithDisk(state, stateFile);
|
|
9220
|
+
saveState$1(state, stateFile);
|
|
9221
|
+
}
|
|
9046
9222
|
return report;
|
|
9047
9223
|
}
|
|
9048
9224
|
/**
|
|
@@ -9127,7 +9303,7 @@ async function handleDispatch(task, overdue, opts, state, now) {
|
|
|
9127
9303
|
};
|
|
9128
9304
|
}
|
|
9129
9305
|
await opts.provider.setLabels(task.id, task.labels);
|
|
9130
|
-
await clearRunningMark(task, opts);
|
|
9306
|
+
await clearRunningMark(task, opts.provider);
|
|
9131
9307
|
const fails = (state.failedDispatches[task.id] ?? 0) + 1;
|
|
9132
9308
|
state.failedDispatches[task.id] = fails;
|
|
9133
9309
|
const detail = `${result.outcome}${result.reason ? " — " + result.reason : ""}`;
|
|
@@ -9205,15 +9381,44 @@ async function markRunning(task, opts, startedAt) {
|
|
|
9205
9381
|
* exactly the stale-state problem this whole subsystem keeps producing, so the
|
|
9206
9382
|
* cleanup is attached to the release rather than to the happy path.
|
|
9207
9383
|
*/
|
|
9208
|
-
async function clearRunningMark(task,
|
|
9209
|
-
const { listComments, deleteComment } =
|
|
9384
|
+
async function clearRunningMark(task, provider) {
|
|
9385
|
+
const { listComments, deleteComment } = provider;
|
|
9210
9386
|
if (!listComments || !deleteComment) return;
|
|
9211
9387
|
try {
|
|
9212
|
-
const comments = await listComments.call(
|
|
9213
|
-
for (const c of comments) if (c.content.includes(RUNNING_COMMENT_MARK)) await deleteComment.call(
|
|
9388
|
+
const comments = await listComments.call(provider, task.id);
|
|
9389
|
+
for (const c of comments) if (c.content.includes(RUNNING_COMMENT_MARK)) await deleteComment.call(provider, c.id);
|
|
9214
9390
|
} catch {}
|
|
9215
9391
|
}
|
|
9216
9392
|
/**
|
|
9393
|
+
* Release a claim on a task: strip the interlock label, remove the progress
|
|
9394
|
+
* marker, and forget the transient run state that made this poller believe
|
|
9395
|
+
* something was still working on it.
|
|
9396
|
+
*
|
|
9397
|
+
* This is every effect of ending a run except the tracker-side completion
|
|
9398
|
+
* itself — factored out so `handleComplete` and the "abandoned" release below
|
|
9399
|
+
* cannot drift apart, and so anything else that ends a claim (`pai task done`
|
|
9400
|
+
* included) gets the identical cleanup rather than a hand-rolled subset of it.
|
|
9401
|
+
* A partial version of this — closing the task but skipping this call — is
|
|
9402
|
+
* exactly what left `pai-running` and the transient state behind on every
|
|
9403
|
+
* `pai task done` before this existed: the next occurrence of a recurring task
|
|
9404
|
+
* inherited a live-looking claim it never made, and the poller spent the
|
|
9405
|
+
* morning asking a session whether it was still working on a task it had
|
|
9406
|
+
* already finished.
|
|
9407
|
+
*
|
|
9408
|
+
* Only ever removes `RUNNING_LABEL` from the label array — never replaces it —
|
|
9409
|
+
* so any other label the user put on the task survives. Only ever deletes this
|
|
9410
|
+
* task's own entries from `startedAt` / `claimSeenAt` / `failedProbes` —
|
|
9411
|
+
* `lastSeenDue` and `history` are untouched, exactly as `handleComplete` always
|
|
9412
|
+
* left them.
|
|
9413
|
+
*/
|
|
9414
|
+
async function releaseClaim(task, provider, state) {
|
|
9415
|
+
await provider.setLabels(task.id, task.labels.filter((l) => l.toLowerCase() !== RUNNING_LABEL));
|
|
9416
|
+
await clearRunningMark(task, provider);
|
|
9417
|
+
delete state.startedAt[task.id];
|
|
9418
|
+
delete state.failedProbes[task.id];
|
|
9419
|
+
delete state.claimSeenAt[task.id];
|
|
9420
|
+
}
|
|
9421
|
+
/**
|
|
9217
9422
|
* Put a hand-ticked task's schedule back.
|
|
9218
9423
|
*
|
|
9219
9424
|
* Deliberately after the dispatch and never in its place: the run is the point,
|
|
@@ -9235,12 +9440,8 @@ async function restoreSchedule(task, restoreTo, opts, state) {
|
|
|
9235
9440
|
}
|
|
9236
9441
|
async function handleComplete(task, durationMinutes, opts, state, now) {
|
|
9237
9442
|
if (opts.dryRun) return `would clear ${RUNNING_LABEL}, ${durationMinutes ?? "?"}m`;
|
|
9238
|
-
await opts.provider.setLabels(task.id, task.labels.filter((l) => l.toLowerCase() !== RUNNING_LABEL));
|
|
9239
|
-
await clearRunningMark(task, opts);
|
|
9240
9443
|
const wasStuck = (state.failedProbes[task.id] ?? 0) > 0;
|
|
9241
|
-
|
|
9242
|
-
delete state.failedProbes[task.id];
|
|
9243
|
-
delete state.claimSeenAt[task.id];
|
|
9444
|
+
await releaseClaim(task, opts.provider, state);
|
|
9244
9445
|
if (durationMinutes !== null && !wasStuck) {
|
|
9245
9446
|
const hist = state.history[task.id] ?? [];
|
|
9246
9447
|
hist.push(durationMinutes);
|
|
@@ -10005,6 +10206,15 @@ function registerTaskCommands(taskCmd) {
|
|
|
10005
10206
|
if (opts.archive !== false) archived = await archiveTaskThread(provider, id);
|
|
10006
10207
|
await provider.complete(id);
|
|
10007
10208
|
console.log(chalk.green(` Completed ${id}.`));
|
|
10209
|
+
try {
|
|
10210
|
+
const task = await provider.getTask(id);
|
|
10211
|
+
if (task) {
|
|
10212
|
+
const state = loadState$1(STATE_FILE$1);
|
|
10213
|
+
await releaseClaim(task, provider, state);
|
|
10214
|
+
if (task.due) state.lastSeenDue[id] = task.due;
|
|
10215
|
+
saveState$1(state, STATE_FILE$1);
|
|
10216
|
+
}
|
|
10217
|
+
} catch {}
|
|
10008
10218
|
if (archived) {
|
|
10009
10219
|
const what = `${archived.commentCount} comment${archived.commentCount === 1 ? "" : "s"}`;
|
|
10010
10220
|
console.log(archived.written ? dim(` Discussion saved (${what}) → ${archived.path}`) : dim(` Discussion already saved (${what}), unchanged.`));
|
|
@@ -10166,7 +10376,8 @@ async function cmdCheck(context, opts) {
|
|
|
10166
10376
|
const threshold = opts.threshold ? parseFloat(opts.threshold) : void 0;
|
|
10167
10377
|
if (threshold !== void 0 && (isNaN(threshold) || threshold < 0 || threshold > 1)) {
|
|
10168
10378
|
console.error(err(" --threshold must be a number between 0 and 1"));
|
|
10169
|
-
process.
|
|
10379
|
+
process.exitCode = 1;
|
|
10380
|
+
return;
|
|
10170
10381
|
}
|
|
10171
10382
|
try {
|
|
10172
10383
|
const result = await client.topicCheck({
|
|
@@ -10179,7 +10390,10 @@ async function cmdCheck(context, opts) {
|
|
|
10179
10390
|
return;
|
|
10180
10391
|
}
|
|
10181
10392
|
printTopicResult(result);
|
|
10182
|
-
if (result.shifted)
|
|
10393
|
+
if (result.shifted) {
|
|
10394
|
+
process.exitCode = 1;
|
|
10395
|
+
return;
|
|
10396
|
+
}
|
|
10183
10397
|
} catch (e) {
|
|
10184
10398
|
const msg = e instanceof Error ? e.message : String(e);
|
|
10185
10399
|
console.log();
|
|
@@ -10187,7 +10401,8 @@ async function cmdCheck(context, opts) {
|
|
|
10187
10401
|
console.log(dim$1(` ${msg}`));
|
|
10188
10402
|
console.log(dim$1(" Start it with: pai daemon serve"));
|
|
10189
10403
|
console.log();
|
|
10190
|
-
process.
|
|
10404
|
+
process.exitCode = 1;
|
|
10405
|
+
return;
|
|
10191
10406
|
}
|
|
10192
10407
|
}
|
|
10193
10408
|
function registerTopicCommands(topicCmd) {
|
|
@@ -10378,7 +10593,8 @@ async function cmdBackfill(opts) {
|
|
|
10378
10593
|
const limit = opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
10379
10594
|
if (limit !== void 0 && (isNaN(limit) || limit < 1)) {
|
|
10380
10595
|
console.error(err(" --limit must be a positive integer"));
|
|
10381
|
-
process.
|
|
10596
|
+
process.exitCode = 1;
|
|
10597
|
+
return;
|
|
10382
10598
|
}
|
|
10383
10599
|
console.log();
|
|
10384
10600
|
console.log(header(" PAI KG Backfill"));
|
|
@@ -10408,7 +10624,8 @@ async function cmdBackfill(opts) {
|
|
|
10408
10624
|
} catch (e) {
|
|
10409
10625
|
const msg = e instanceof Error ? e.message : String(e);
|
|
10410
10626
|
console.error(err(` ${msg}`));
|
|
10411
|
-
process.
|
|
10627
|
+
process.exitCode = 1;
|
|
10628
|
+
return;
|
|
10412
10629
|
}
|
|
10413
10630
|
}
|
|
10414
10631
|
async function cmdQuery$1(opts) {
|
|
@@ -10423,7 +10640,8 @@ async function cmdQuery$1(opts) {
|
|
|
10423
10640
|
const asOf = opts.asOf ? new Date(opts.asOf) : void 0;
|
|
10424
10641
|
if (asOf && isNaN(asOf.getTime())) {
|
|
10425
10642
|
console.error(err(` Invalid --as-of date: ${opts.asOf}`));
|
|
10426
|
-
process.
|
|
10643
|
+
process.exitCode = 1;
|
|
10644
|
+
return;
|
|
10427
10645
|
}
|
|
10428
10646
|
const triples = await kgQuery(pool, {
|
|
10429
10647
|
subject: opts.subject,
|
|
@@ -10685,7 +10903,8 @@ async function cmdQuery(sql, opts) {
|
|
|
10685
10903
|
} catch (e) {
|
|
10686
10904
|
const msg = e instanceof Error ? e.message : String(e);
|
|
10687
10905
|
console.error(err(` ${msg}`));
|
|
10688
|
-
process.
|
|
10906
|
+
process.exitCode = 1;
|
|
10907
|
+
return;
|
|
10689
10908
|
}
|
|
10690
10909
|
}
|
|
10691
10910
|
async function cmdTables(opts) {
|
|
@@ -10709,7 +10928,8 @@ async function cmdTables(opts) {
|
|
|
10709
10928
|
} catch (e) {
|
|
10710
10929
|
const msg = e instanceof Error ? e.message : String(e);
|
|
10711
10930
|
console.error(err(` ${msg}`));
|
|
10712
|
-
process.
|
|
10931
|
+
process.exitCode = 1;
|
|
10932
|
+
return;
|
|
10713
10933
|
}
|
|
10714
10934
|
}
|
|
10715
10935
|
async function cmdSchema(table, opts) {
|
|
@@ -10730,7 +10950,8 @@ async function cmdSchema(table, opts) {
|
|
|
10730
10950
|
} catch (e) {
|
|
10731
10951
|
const msg = e instanceof Error ? e.message : String(e);
|
|
10732
10952
|
console.error(err(` ${msg}`));
|
|
10733
|
-
process.
|
|
10953
|
+
process.exitCode = 1;
|
|
10954
|
+
return;
|
|
10734
10955
|
}
|
|
10735
10956
|
}
|
|
10736
10957
|
function registerDbCommands(dbCmd) {
|
|
@@ -11052,7 +11273,8 @@ function cmdList(db, projectSlug, opts) {
|
|
|
11052
11273
|
const project = getProject$1(db, projectSlug);
|
|
11053
11274
|
if (!project) {
|
|
11054
11275
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
11055
|
-
process.
|
|
11276
|
+
process.exitCode = 1;
|
|
11277
|
+
return;
|
|
11056
11278
|
}
|
|
11057
11279
|
where.push("s.project_id = ?");
|
|
11058
11280
|
params.push(project.id);
|
|
@@ -11117,7 +11339,8 @@ function cmdInfo(db, projectSlug, sessionNumber) {
|
|
|
11117
11339
|
const project = getProject$1(db, projectSlug);
|
|
11118
11340
|
if (!project) {
|
|
11119
11341
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
11120
|
-
process.
|
|
11342
|
+
process.exitCode = 1;
|
|
11343
|
+
return;
|
|
11121
11344
|
}
|
|
11122
11345
|
const session = resolveSession(db, project, sessionNumber);
|
|
11123
11346
|
console.log();
|
|
@@ -11138,13 +11361,15 @@ function cmdRename(db, projectSlug, numberOrLatest, newSlug) {
|
|
|
11138
11361
|
const project = getProject$1(db, projectSlug);
|
|
11139
11362
|
if (!project) {
|
|
11140
11363
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
11141
|
-
process.
|
|
11364
|
+
process.exitCode = 1;
|
|
11365
|
+
return;
|
|
11142
11366
|
}
|
|
11143
11367
|
const session = resolveSession(db, project, numberOrLatest);
|
|
11144
11368
|
const notesDir = getNotesDir(project);
|
|
11145
11369
|
if (!existsSync(notesDir)) {
|
|
11146
11370
|
console.error(err(`Notes directory not found: ${notesDir}`));
|
|
11147
|
-
process.
|
|
11371
|
+
process.exitCode = 1;
|
|
11372
|
+
return;
|
|
11148
11373
|
}
|
|
11149
11374
|
const titleSlug = toTitleCase$1(newSlug);
|
|
11150
11375
|
const newFilename = formatFilename(session.number, session.date, titleSlug);
|
|
@@ -11155,7 +11380,8 @@ function cmdRename(db, projectSlug, numberOrLatest, newSlug) {
|
|
|
11155
11380
|
renameSync(oldPath, newPath);
|
|
11156
11381
|
} catch (e) {
|
|
11157
11382
|
console.error(err(`Failed to rename file: ${e}`));
|
|
11158
|
-
process.
|
|
11383
|
+
process.exitCode = 1;
|
|
11384
|
+
return;
|
|
11159
11385
|
}
|
|
11160
11386
|
} else {
|
|
11161
11387
|
console.log(warn$1(` Note: file not found at expected path: ${session.filename}`));
|
|
@@ -11190,7 +11416,8 @@ function cmdSlug(db, projectSlug, numberOrLatest, opts) {
|
|
|
11190
11416
|
const project = getProject$1(db, projectSlug);
|
|
11191
11417
|
if (!project) {
|
|
11192
11418
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
11193
|
-
process.
|
|
11419
|
+
process.exitCode = 1;
|
|
11420
|
+
return;
|
|
11194
11421
|
}
|
|
11195
11422
|
const session = resolveSession(db, project, numberOrLatest);
|
|
11196
11423
|
const transcriptPath = findLatestTranscript(project.encoded_dir);
|
|
@@ -11217,7 +11444,8 @@ function cmdTag(db, projectSlug, sessionNumber, rawTags) {
|
|
|
11217
11444
|
const project = getProject$1(db, projectSlug);
|
|
11218
11445
|
if (!project) {
|
|
11219
11446
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
11220
|
-
process.
|
|
11447
|
+
process.exitCode = 1;
|
|
11448
|
+
return;
|
|
11221
11449
|
}
|
|
11222
11450
|
const session = resolveSession(db, project, sessionNumber);
|
|
11223
11451
|
if (rawTags.length === 0) {
|
|
@@ -11254,13 +11482,15 @@ function cmdRoute(db, projectSlug, sessionNumber, targetProjectSlug, opts) {
|
|
|
11254
11482
|
const project = getProject$1(db, projectSlug);
|
|
11255
11483
|
if (!project) {
|
|
11256
11484
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
11257
|
-
process.
|
|
11485
|
+
process.exitCode = 1;
|
|
11486
|
+
return;
|
|
11258
11487
|
}
|
|
11259
11488
|
const session = resolveSession(db, project, sessionNumber);
|
|
11260
11489
|
const targetProject = db.prepare("SELECT id, slug, display_name FROM projects WHERE slug = ?").get(targetProjectSlug);
|
|
11261
11490
|
if (!targetProject) {
|
|
11262
11491
|
console.error(err(`Target project not found: ${targetProjectSlug}`));
|
|
11263
|
-
process.
|
|
11492
|
+
process.exitCode = 1;
|
|
11493
|
+
return;
|
|
11264
11494
|
}
|
|
11265
11495
|
const validTypes = [
|
|
11266
11496
|
"related",
|
|
@@ -11270,7 +11500,8 @@ function cmdRoute(db, projectSlug, sessionNumber, targetProjectSlug, opts) {
|
|
|
11270
11500
|
const linkType = opts.type ?? "related";
|
|
11271
11501
|
if (!validTypes.includes(linkType)) {
|
|
11272
11502
|
console.error(err(`Invalid link type "${linkType}". Valid: ${validTypes.join(", ")}`));
|
|
11273
|
-
process.
|
|
11503
|
+
process.exitCode = 1;
|
|
11504
|
+
return;
|
|
11274
11505
|
}
|
|
11275
11506
|
try {
|
|
11276
11507
|
db.prepare(`INSERT INTO links (session_id, target_project_id, link_type, created_at)
|
|
@@ -11370,10 +11601,10 @@ function cmdActive(db, opts) {
|
|
|
11370
11601
|
], rows));
|
|
11371
11602
|
}
|
|
11372
11603
|
async function cmdAutoRoute(opts) {
|
|
11373
|
-
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("./auto-route-
|
|
11374
|
-
const { openRegistry } = await import("./db-
|
|
11375
|
-
const { createStorageBackend } = await import("./factory-
|
|
11376
|
-
const { loadConfig } = await import("./config-
|
|
11604
|
+
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("./auto-route-Byf8ENXj.mjs");
|
|
11605
|
+
const { openRegistry } = await import("./db-XEwJbuGO.mjs");
|
|
11606
|
+
const { createStorageBackend } = await import("./factory-CrokPMk2.mjs");
|
|
11607
|
+
const { loadConfig } = await import("./config-C_ErGddD.mjs");
|
|
11377
11608
|
const config = loadConfig();
|
|
11378
11609
|
const registryDb = openRegistry();
|
|
11379
11610
|
const federation = await createStorageBackend(config);
|
|
@@ -11484,10 +11715,10 @@ function touchCheckpointSentinel(notesDir) {
|
|
|
11484
11715
|
function cmdCheckpoint(message, opts) {
|
|
11485
11716
|
const minGapSeconds = parseInt(opts.minGap ?? "300", 10);
|
|
11486
11717
|
const notesDir = findNotesDirForCwd();
|
|
11487
|
-
if (!notesDir)
|
|
11488
|
-
if (checkpointTooRecent(notesDir, minGapSeconds))
|
|
11718
|
+
if (!notesDir) return;
|
|
11719
|
+
if (checkpointTooRecent(notesDir, minGapSeconds)) return;
|
|
11489
11720
|
const notePath = findLatestNoteFile(notesDir);
|
|
11490
|
-
if (!notePath)
|
|
11721
|
+
if (!notePath) return;
|
|
11491
11722
|
const block = `\n## Checkpoint — ${(/* @__PURE__ */ new Date()).toISOString()}\n${message}\n`;
|
|
11492
11723
|
const tmpPath = `${notePath}.checkpoint.tmp`;
|
|
11493
11724
|
try {
|
|
@@ -11497,10 +11728,9 @@ function cmdCheckpoint(message, opts) {
|
|
|
11497
11728
|
try {
|
|
11498
11729
|
if (existsSync(tmpPath)) renameSync(tmpPath, tmpPath + ".dead");
|
|
11499
11730
|
} catch {}
|
|
11500
|
-
|
|
11731
|
+
return;
|
|
11501
11732
|
}
|
|
11502
11733
|
touchCheckpointSentinel(notesDir);
|
|
11503
|
-
process.exit(0);
|
|
11504
11734
|
}
|
|
11505
11735
|
|
|
11506
11736
|
//#endregion
|
|
@@ -11573,22 +11803,26 @@ function cmdPause(db, opts) {
|
|
|
11573
11803
|
body = readBodyFile(opts.bodyFile).trim();
|
|
11574
11804
|
} catch (err) {
|
|
11575
11805
|
console.error(chalk.red("pai pause: ") + `Could not read checkpoint body from ${opts.bodyFile}: ${String(err)}`);
|
|
11576
|
-
process.
|
|
11806
|
+
process.exitCode = 1;
|
|
11807
|
+
return;
|
|
11577
11808
|
}
|
|
11578
11809
|
if (!body) {
|
|
11579
11810
|
console.error(chalk.red("pai pause: ") + `Checkpoint body at ${opts.bodyFile} is empty.`);
|
|
11580
|
-
process.
|
|
11811
|
+
process.exitCode = 1;
|
|
11812
|
+
return;
|
|
11581
11813
|
}
|
|
11582
11814
|
} else if (!opts.noBody) {
|
|
11583
11815
|
printMissingBodyError();
|
|
11584
|
-
process.
|
|
11816
|
+
process.exitCode = 1;
|
|
11817
|
+
return;
|
|
11585
11818
|
}
|
|
11586
11819
|
const cwd = process.cwd();
|
|
11587
11820
|
const project = resolveProjectByCwd(db, cwd);
|
|
11588
11821
|
if (!project) {
|
|
11589
11822
|
console.error(chalk.red("pai pause: ") + `Current directory is not within a registered PAI project.
|
|
11590
11823
|
cwd: ${cwd}`);
|
|
11591
|
-
process.
|
|
11824
|
+
process.exitCode = 1;
|
|
11825
|
+
return;
|
|
11592
11826
|
}
|
|
11593
11827
|
const session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
|
|
11594
11828
|
const notePath = resolveNotePath(project);
|
|
@@ -11615,7 +11849,8 @@ function cmdPause(db, opts) {
|
|
|
11615
11849
|
});
|
|
11616
11850
|
if (result.action === "failed") {
|
|
11617
11851
|
console.error(chalk.red("pai pause: ") + `Failed to write TODO.md: ${result.error}`);
|
|
11618
|
-
process.
|
|
11852
|
+
process.exitCode = 1;
|
|
11853
|
+
return;
|
|
11619
11854
|
}
|
|
11620
11855
|
if (opts.dryRun) {
|
|
11621
11856
|
console.log("\n" + chalk.bold("Dry run — would write to:") + " " + chalk.cyan(result.path ?? "?"));
|
|
@@ -11884,31 +12119,71 @@ function readWorkingTree(cwd) {
|
|
|
11884
12119
|
* nothing asked". The handover survives in the session note either way, but
|
|
11885
12120
|
* `## Continue` is what the next session is shown, so what sits there matters.
|
|
11886
12121
|
*/
|
|
12122
|
+
/** Cap on `@n=` file-declaration lines, so the whole message stays ≤25 lines. */
|
|
12123
|
+
const MAX_AT_LINES = 17;
|
|
12124
|
+
/** Flatten a prompt (or any field value) to a single trimmed line. */
|
|
12125
|
+
function flattenField(text) {
|
|
12126
|
+
return text.replace(/\s*\n\s*/g, " ").trim();
|
|
12127
|
+
}
|
|
12128
|
+
/**
|
|
12129
|
+
* Strip a git-porcelain status prefix off a `changes` entry, leaving the path.
|
|
12130
|
+
*
|
|
12131
|
+
* Porcelain lines are a 1-2 char status field, a space, then the path (e.g.
|
|
12132
|
+
* `"M src/foo.ts"`, `" M src/foo.ts"`, `"?? x.ts"`). The stripped result falls
|
|
12133
|
+
* back to the trimmed original whenever the pattern does not match, so a
|
|
12134
|
+
* surprising entry still produces a usable `@n=` line instead of an empty one.
|
|
12135
|
+
*/
|
|
12136
|
+
function pathOf(entry) {
|
|
12137
|
+
return entry.replace(/^\s*\S{1,2}\s+/, "").trim() || entry.trim();
|
|
12138
|
+
}
|
|
12139
|
+
/**
|
|
12140
|
+
* Build the auto-checkpoint body as a single AG2 (Agentish v2) message of kind
|
|
12141
|
+
* `T` (task/handover). Pure function of its inputs so it can be unit-tested
|
|
12142
|
+
* without touching the filesystem or git.
|
|
12143
|
+
*
|
|
12144
|
+
* Field layout — see the AG2 validator for the authoritative rules:
|
|
12145
|
+
* i = ckpt-<sanitised timestamp>
|
|
12146
|
+
* g = the most recent prompt (goal)
|
|
12147
|
+
* d = the earlier prompts, joined by " | " ("?" when there are none)
|
|
12148
|
+
* t = "?" always — a mechanical handover runs no tests
|
|
12149
|
+
* @n = one declaration per changed file, capped at MAX_AT_LINES so the
|
|
12150
|
+
* whole message stays within AG2's 25-line limit
|
|
12151
|
+
* z = a note summarising the working tree, capped at 200 chars
|
|
12152
|
+
*/
|
|
12153
|
+
function ag2Checkpoint(prompts, tree, ts) {
|
|
12154
|
+
const id = `ckpt-${ts.replace(/[^A-Za-z0-9._-]/g, "-")}`;
|
|
12155
|
+
const g = flattenField(prompts[0]);
|
|
12156
|
+
const d = prompts.length > 1 ? prompts.slice(1).map(flattenField).join(" | ") : "?";
|
|
12157
|
+
const lines = [
|
|
12158
|
+
"T",
|
|
12159
|
+
`i=${id}`,
|
|
12160
|
+
`g=${g}`,
|
|
12161
|
+
`d=${d}`,
|
|
12162
|
+
"t=?"
|
|
12163
|
+
];
|
|
12164
|
+
let extra = 0;
|
|
12165
|
+
if (tree) {
|
|
12166
|
+
const shown = tree.changes.slice(0, MAX_AT_LINES);
|
|
12167
|
+
extra = Math.max(0, tree.changes.length - MAX_AT_LINES) + tree.overflow;
|
|
12168
|
+
shown.forEach((entry, i) => lines.push(`@${i + 1}=${pathOf(entry)}`));
|
|
12169
|
+
}
|
|
12170
|
+
let z;
|
|
12171
|
+
if (!tree) z = "auto ckpt (no model)";
|
|
12172
|
+
else {
|
|
12173
|
+
const dirty = tree.changes.length;
|
|
12174
|
+
const head = tree.head ?? "nohead";
|
|
12175
|
+
z = `auto ckpt (no model); ${tree.branch}@${head}; ${dirty} dirty`;
|
|
12176
|
+
if (extra > 0) z += `; +${extra} paths`;
|
|
12177
|
+
z += "; git status for detail";
|
|
12178
|
+
}
|
|
12179
|
+
lines.push(`z=${z.length > 200 ? z.slice(0, 200) : z}`);
|
|
12180
|
+
return lines.join("\n");
|
|
12181
|
+
}
|
|
11887
12182
|
function buildAutosaveBody(input) {
|
|
11888
12183
|
const ts = input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11889
12184
|
const prompts = recentPrompts(input.transcriptPaths ?? []);
|
|
11890
12185
|
if (prompts.length === 0) return "";
|
|
11891
|
-
|
|
11892
|
-
const dirty = tree?.changes.length ?? 0;
|
|
11893
|
-
const out = [`_Automatic checkpoint — ${ts}. Written without the model, from the transcript and the working tree. A model-authored checkpoint replaces this; it is here so an interrupted session still leaves something._`];
|
|
11894
|
-
if (prompts.length > 0) {
|
|
11895
|
-
out.push("", "### What was being asked", "");
|
|
11896
|
-
for (const p of prompts) out.push(`- ${p.replace(/\s*\n\s*/g, " ")}`);
|
|
11897
|
-
}
|
|
11898
|
-
if (tree) {
|
|
11899
|
-
out.push("", "### Working tree", "");
|
|
11900
|
-
out.push(`- Branch: \`${tree.branch}\``);
|
|
11901
|
-
if (tree.head) out.push(`- HEAD: ${tree.head}`);
|
|
11902
|
-
if (dirty === 0) out.push("- Clean — nothing uncommitted.");
|
|
11903
|
-
else {
|
|
11904
|
-
out.push(`- ${dirty} uncommitted path(s)${tree.overflow > 0 ? ` (+${tree.overflow} more)` : ""}:`);
|
|
11905
|
-
out.push("");
|
|
11906
|
-
out.push("```");
|
|
11907
|
-
out.push(...tree.changes);
|
|
11908
|
-
out.push("```");
|
|
11909
|
-
}
|
|
11910
|
-
}
|
|
11911
|
-
return out.join("\n");
|
|
12186
|
+
return ag2Checkpoint(prompts, readWorkingTree(input.cwd), ts);
|
|
11912
12187
|
}
|
|
11913
12188
|
|
|
11914
12189
|
//#endregion
|
|
@@ -11936,10 +12211,10 @@ function cmdHandover(db, projectSlug, numberOrLatest, sessionId) {
|
|
|
11936
12211
|
let project;
|
|
11937
12212
|
if (projectSlug) {
|
|
11938
12213
|
project = db.prepare("SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?").get(projectSlug);
|
|
11939
|
-
if (!project)
|
|
12214
|
+
if (!project) return;
|
|
11940
12215
|
} else {
|
|
11941
12216
|
const row = resolveProjectByCwd(db, process.cwd());
|
|
11942
|
-
if (!row)
|
|
12217
|
+
if (!row) return;
|
|
11943
12218
|
project = row;
|
|
11944
12219
|
}
|
|
11945
12220
|
let session;
|
|
@@ -11957,7 +12232,422 @@ function cmdHandover(db, projectSlug, numberOrLatest, sessionId) {
|
|
|
11957
12232
|
cwd: process.cwd(),
|
|
11958
12233
|
body: buildStopBody(project, sessionId)
|
|
11959
12234
|
});
|
|
11960
|
-
|
|
12235
|
+
}
|
|
12236
|
+
|
|
12237
|
+
//#endregion
|
|
12238
|
+
//#region src/hooks/ts/lib/context-fill.ts
|
|
12239
|
+
/**
|
|
12240
|
+
* context-fill.ts — "how full is this session's context window, right now?"
|
|
12241
|
+
*
|
|
12242
|
+
* No hook payload carries this number directly (verified: PreCompact,
|
|
12243
|
+
* UserPromptSubmit and PostToolUse stdin never include `context_window`).
|
|
12244
|
+
* Two sources can reconstruct it, in order of preference:
|
|
12245
|
+
*
|
|
12246
|
+
* 1. STATUSLINE STATE FILE — statusline-command.sh receives the exact
|
|
12247
|
+
* figure from Claude Code (`.context_window.used_percentage` /
|
|
12248
|
+
* `.context_window.context_window_size`) on every render and persists
|
|
12249
|
+
* it to `${TMPDIR}/pai-context-<session_id>.json`. This is authoritative
|
|
12250
|
+
* but depends on the status line having rendered recently — a session
|
|
12251
|
+
* whose terminal isn't drawing a status line (headless, backgrounded)
|
|
12252
|
+
* leaves this file missing or stale.
|
|
12253
|
+
*
|
|
12254
|
+
* 2. TRANSCRIPT USAGE — every `message.usage` entry in the session's own
|
|
12255
|
+
* .jsonl transcript already reports the token accounting for that one
|
|
12256
|
+
* API call: `input_tokens + cache_read_input_tokens +
|
|
12257
|
+
* cache_creation_input_tokens` on the MOST RECENT such entry *is* the
|
|
12258
|
+
* current context fill, because each request resends the full context.
|
|
12259
|
+
* This is why it must be the last entry's fields summed, never a sum
|
|
12260
|
+
* across entries — summing across entries is a running token-spend
|
|
12261
|
+
* total, not a fill reading (see calculateSessionTokens, a different
|
|
12262
|
+
* metric answering a different question).
|
|
12263
|
+
*
|
|
12264
|
+
* When neither source is available, the answer is UNKNOWN, not zero. A zero
|
|
12265
|
+
* reads as "plenty of room" and would silently suppress every downstream
|
|
12266
|
+
* decision that depends on this number (the compaction handover chief among
|
|
12267
|
+
* them) for the entire session.
|
|
12268
|
+
*/
|
|
12269
|
+
/** Fallback window size when nothing on hand reports one. Matches the
|
|
12270
|
+
* default statusline-command.sh already falls back to. */
|
|
12271
|
+
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
12272
|
+
/** How many trailing lines of the transcript to scan for a usage entry.
|
|
12273
|
+
* A single turn is rarely more than a handful of JSONL lines (assistant
|
|
12274
|
+
* text + tool_use/tool_result pairs), so 40 comfortably covers one turn
|
|
12275
|
+
* even a busy one, without reading the whole file on every check. */
|
|
12276
|
+
const TRANSCRIPT_TAIL_LINES = 40;
|
|
12277
|
+
/** A statusline reading older than this is treated as not there at all —
|
|
12278
|
+
* a stale number is worse than none, because it looks confident. */
|
|
12279
|
+
const STATUSLINE_STALE_MS = 300 * 1e3;
|
|
12280
|
+
function unknownReading(windowSize) {
|
|
12281
|
+
return {
|
|
12282
|
+
status: "unknown",
|
|
12283
|
+
usedTokens: null,
|
|
12284
|
+
windowSize,
|
|
12285
|
+
fraction: null,
|
|
12286
|
+
source: "unknown"
|
|
12287
|
+
};
|
|
12288
|
+
}
|
|
12289
|
+
function statuslineStateFilePath(sessionId) {
|
|
12290
|
+
return join(tmpdir(), `pai-context-${sessionId}.json`);
|
|
12291
|
+
}
|
|
12292
|
+
/**
|
|
12293
|
+
* Read the state file statusline-command.sh writes on every render.
|
|
12294
|
+
* Returns null when the file is missing, unparsable, or older than
|
|
12295
|
+
* STATUSLINE_STALE_MS — all three mean "not a source right now".
|
|
12296
|
+
*/
|
|
12297
|
+
function readStatuslineFill(sessionId, now = Date.now()) {
|
|
12298
|
+
if (!sessionId) return null;
|
|
12299
|
+
const path = statuslineStateFilePath(sessionId);
|
|
12300
|
+
if (!existsSync(path)) return null;
|
|
12301
|
+
let raw;
|
|
12302
|
+
try {
|
|
12303
|
+
raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
12304
|
+
} catch {
|
|
12305
|
+
return null;
|
|
12306
|
+
}
|
|
12307
|
+
if (typeof raw.timestamp !== "number" || now - raw.timestamp > STATUSLINE_STALE_MS) return null;
|
|
12308
|
+
if (typeof raw.used_percentage !== "number" || typeof raw.context_window_size !== "number") return null;
|
|
12309
|
+
const windowSize = raw.context_window_size;
|
|
12310
|
+
return {
|
|
12311
|
+
status: "ok",
|
|
12312
|
+
usedTokens: Math.round(raw.used_percentage / 100 * windowSize),
|
|
12313
|
+
windowSize,
|
|
12314
|
+
fraction: raw.used_percentage / 100,
|
|
12315
|
+
source: "statusline"
|
|
12316
|
+
};
|
|
12317
|
+
}
|
|
12318
|
+
/**
|
|
12319
|
+
* Sum the three context-carrying fields of ONE usage object — never across
|
|
12320
|
+
* usage objects. A single `message.usage` already reports what that one API
|
|
12321
|
+
* call sent as context; adding another entry's numbers to it produces a
|
|
12322
|
+
* cumulative spend figure, not a fill reading, and can exceed the window
|
|
12323
|
+
* size many times over on a long session (the exact defect this helper
|
|
12324
|
+
* exists to not repeat — see the PreCompact header fix in the same change).
|
|
12325
|
+
*/
|
|
12326
|
+
function usageTotal(usage) {
|
|
12327
|
+
return (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0);
|
|
12328
|
+
}
|
|
12329
|
+
/**
|
|
12330
|
+
* Derive context fill from the tail of a .jsonl transcript: the most recent
|
|
12331
|
+
* `message.usage` entry, read from the end backwards so a trailing line with
|
|
12332
|
+
* no usage field (a plain text turn, a tool_result) doesn't hide one just
|
|
12333
|
+
* before it.
|
|
12334
|
+
*/
|
|
12335
|
+
function contextFillFromTranscript(transcriptPath, windowSize = DEFAULT_CONTEXT_WINDOW) {
|
|
12336
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return unknownReading(windowSize);
|
|
12337
|
+
let raw;
|
|
12338
|
+
try {
|
|
12339
|
+
raw = readFileSync(transcriptPath, "utf-8");
|
|
12340
|
+
} catch {
|
|
12341
|
+
return unknownReading(windowSize);
|
|
12342
|
+
}
|
|
12343
|
+
const tail = raw.trim().split("\n").filter((l) => l.trim()).slice(-TRANSCRIPT_TAIL_LINES);
|
|
12344
|
+
for (let i = tail.length - 1; i >= 0; i--) {
|
|
12345
|
+
let entry;
|
|
12346
|
+
try {
|
|
12347
|
+
entry = JSON.parse(tail[i]);
|
|
12348
|
+
} catch {
|
|
12349
|
+
continue;
|
|
12350
|
+
}
|
|
12351
|
+
const usage = entry?.message?.usage;
|
|
12352
|
+
if (usage && typeof usage === "object") {
|
|
12353
|
+
const usedTokens = usageTotal(usage);
|
|
12354
|
+
return {
|
|
12355
|
+
status: "ok",
|
|
12356
|
+
usedTokens,
|
|
12357
|
+
windowSize,
|
|
12358
|
+
fraction: usedTokens / windowSize,
|
|
12359
|
+
source: "transcript"
|
|
12360
|
+
};
|
|
12361
|
+
}
|
|
12362
|
+
}
|
|
12363
|
+
return unknownReading(windowSize);
|
|
12364
|
+
}
|
|
12365
|
+
function getContextFill(input, now = Date.now()) {
|
|
12366
|
+
const windowSize = input.windowSize ?? DEFAULT_CONTEXT_WINDOW;
|
|
12367
|
+
if (input.sessionId) {
|
|
12368
|
+
const fromStatusline = readStatuslineFill(input.sessionId, now);
|
|
12369
|
+
if (fromStatusline) return fromStatusline;
|
|
12370
|
+
}
|
|
12371
|
+
if (input.transcriptPath) return contextFillFromTranscript(input.transcriptPath, windowSize);
|
|
12372
|
+
return unknownReading(windowSize);
|
|
12373
|
+
}
|
|
12374
|
+
const DEFAULT_AUTOCOMPACT_PCT = 80;
|
|
12375
|
+
/** How many of the most recent compact_boundary events to consider, and to
|
|
12376
|
+
* take the minimum of. */
|
|
12377
|
+
const MEASURED_TRIGGER_SAMPLE_SIZE = 3;
|
|
12378
|
+
/** How many of a project's most-recently-modified transcripts to scan for
|
|
12379
|
+
* compact_boundary events. compact_boundary events cluster in whichever
|
|
12380
|
+
* files were touched most recently — a full-history scan would cost a lot
|
|
12381
|
+
* for a long-lived project and buy nothing this doesn't already get from
|
|
12382
|
+
* the last handful of files. */
|
|
12383
|
+
const MEASURED_TRIGGER_MAX_FILES = 8;
|
|
12384
|
+
const THRESHOLD_MARGIN_TOKENS = {
|
|
12385
|
+
warmup: 1e5,
|
|
12386
|
+
refresh: 4e4,
|
|
12387
|
+
immediate: 15e3
|
|
12388
|
+
};
|
|
12389
|
+
/** Real default — overridable per-call so tests never touch the user's
|
|
12390
|
+
* actual ~/.claude/projects/ (a live directory this very session writes to). */
|
|
12391
|
+
const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
12392
|
+
function encodeProjectPath(cwd) {
|
|
12393
|
+
return cwd.replace(/[/\s.-]/g, "-");
|
|
12394
|
+
}
|
|
12395
|
+
function listProjectTranscripts(cwd, projectsDir) {
|
|
12396
|
+
const projectDir = join(projectsDir, encodeProjectPath(cwd));
|
|
12397
|
+
if (!existsSync(projectDir)) return [];
|
|
12398
|
+
const candidates = [];
|
|
12399
|
+
const collect = (dir) => {
|
|
12400
|
+
if (!existsSync(dir)) return;
|
|
12401
|
+
let entries;
|
|
12402
|
+
try {
|
|
12403
|
+
entries = readdirSync(dir);
|
|
12404
|
+
} catch {
|
|
12405
|
+
return;
|
|
12406
|
+
}
|
|
12407
|
+
for (const entry of entries) {
|
|
12408
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
12409
|
+
const full = join(dir, entry);
|
|
12410
|
+
try {
|
|
12411
|
+
candidates.push({
|
|
12412
|
+
path: full,
|
|
12413
|
+
mtimeMs: statSync(full).mtimeMs
|
|
12414
|
+
});
|
|
12415
|
+
} catch {}
|
|
12416
|
+
}
|
|
12417
|
+
};
|
|
12418
|
+
collect(projectDir);
|
|
12419
|
+
collect(join(projectDir, "sessions"));
|
|
12420
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
12421
|
+
return candidates.slice(0, MEASURED_TRIGGER_MAX_FILES).map((c) => c.path);
|
|
12422
|
+
}
|
|
12423
|
+
/**
|
|
12424
|
+
* Every compact_boundary sample found in a project's most-recently-modified
|
|
12425
|
+
* transcripts, newest first.
|
|
12426
|
+
*/
|
|
12427
|
+
function readCompactBoundarySamples(cwd, projectsDir) {
|
|
12428
|
+
const samples = [];
|
|
12429
|
+
for (const path of listProjectTranscripts(cwd, projectsDir)) {
|
|
12430
|
+
let raw;
|
|
12431
|
+
try {
|
|
12432
|
+
raw = readFileSync(path, "utf-8");
|
|
12433
|
+
} catch {
|
|
12434
|
+
continue;
|
|
12435
|
+
}
|
|
12436
|
+
for (const line of raw.split("\n")) {
|
|
12437
|
+
if (!line.trim()) continue;
|
|
12438
|
+
let entry;
|
|
12439
|
+
try {
|
|
12440
|
+
entry = JSON.parse(line);
|
|
12441
|
+
} catch {
|
|
12442
|
+
continue;
|
|
12443
|
+
}
|
|
12444
|
+
if (entry.type !== "system" || entry.subtype !== "compact_boundary") continue;
|
|
12445
|
+
const preTokens = entry.compactMetadata?.preTokens;
|
|
12446
|
+
if (typeof preTokens !== "number" || !Number.isFinite(preTokens)) continue;
|
|
12447
|
+
const timestampMs = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
|
|
12448
|
+
if (!Number.isFinite(timestampMs)) continue;
|
|
12449
|
+
samples.push({
|
|
12450
|
+
preTokens,
|
|
12451
|
+
timestampMs
|
|
12452
|
+
});
|
|
12453
|
+
}
|
|
12454
|
+
}
|
|
12455
|
+
samples.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
12456
|
+
return samples;
|
|
12457
|
+
}
|
|
12458
|
+
/**
|
|
12459
|
+
* The measured compaction trigger for a project, or null when it has no
|
|
12460
|
+
* compaction history yet (a brand-new project, or one whose transcripts
|
|
12461
|
+
* this process cannot read). Minimum of the most recent
|
|
12462
|
+
* MEASURED_TRIGGER_SAMPLE_SIZE compact_boundary events — see the module
|
|
12463
|
+
* comment above for why minimum, not mean.
|
|
12464
|
+
*/
|
|
12465
|
+
function measureCompactionTrigger(cwd, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
12466
|
+
if (!cwd) return null;
|
|
12467
|
+
const samples = readCompactBoundarySamples(cwd, projectsDir).slice(0, MEASURED_TRIGGER_SAMPLE_SIZE);
|
|
12468
|
+
if (samples.length === 0) return null;
|
|
12469
|
+
return Math.min(...samples.map((s) => s.preTokens));
|
|
12470
|
+
}
|
|
12471
|
+
/**
|
|
12472
|
+
* Read CLAUDE_AUTOCOMPACT_PCT_OVERRIDE from the environment. Absent →
|
|
12473
|
+
* DEFAULT_AUTOCOMPACT_PCT. Present but not a finite number in (0, 100] →
|
|
12474
|
+
* also DEFAULT_AUTOCOMPACT_PCT, logged, so a typo in the override degrades
|
|
12475
|
+
* to the documented default instead of silently producing nonsense
|
|
12476
|
+
* thresholds (0, negative, or a fraction so large no session ever reaches
|
|
12477
|
+
* it).
|
|
12478
|
+
*/
|
|
12479
|
+
function resolveAutocompactPct(env = process.env) {
|
|
12480
|
+
const raw = env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE;
|
|
12481
|
+
if (raw === void 0 || raw === "") return DEFAULT_AUTOCOMPACT_PCT;
|
|
12482
|
+
const parsed = Number(raw);
|
|
12483
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) {
|
|
12484
|
+
console.error(`[context-fill] CLAUDE_AUTOCOMPACT_PCT_OVERRIDE="${raw}" is not a usable percentage — falling back to the default ${DEFAULT_AUTOCOMPACT_PCT}.`);
|
|
12485
|
+
return DEFAULT_AUTOCOMPACT_PCT;
|
|
12486
|
+
}
|
|
12487
|
+
return parsed;
|
|
12488
|
+
}
|
|
12489
|
+
/**
|
|
12490
|
+
* Derive warmup/refresh/immediate thresholds from a fill reading, preferring
|
|
12491
|
+
* this project's own measured compaction history over the configured
|
|
12492
|
+
* override chain — see the module comment above for why. Margins are
|
|
12493
|
+
* clamped at 0 (and logged) for a window small enough that a margin would
|
|
12494
|
+
* otherwise go negative — a pathological input should degrade to "fire
|
|
12495
|
+
* immediately", never to a threshold below zero.
|
|
12496
|
+
*/
|
|
12497
|
+
function contextFillThresholds(reading, env = process.env, opts = {}) {
|
|
12498
|
+
const windowConfirmed = reading.source === "statusline";
|
|
12499
|
+
const autocompactPct = resolveAutocompactPct(env);
|
|
12500
|
+
const configuredTriggerTokens = Math.round(reading.windowSize * (autocompactPct / 100));
|
|
12501
|
+
const measured = opts.measuredTrigger !== void 0 ? opts.measuredTrigger : opts.cwd ? measureCompactionTrigger(opts.cwd) : null;
|
|
12502
|
+
const triggerSource = measured !== null ? "measured" : "configured";
|
|
12503
|
+
const effectiveTriggerTokens = measured !== null ? measured : configuredTriggerTokens;
|
|
12504
|
+
if (triggerSource === "measured") console.error(`[context-fill] trigger source: MEASURED — ${effectiveTriggerTokens} tokens (minimum of the most recent ${MEASURED_TRIGGER_SAMPLE_SIZE} compact_boundary events for this project; the configured chain would have given ${configuredTriggerTokens}).`);
|
|
12505
|
+
else console.error(`[context-fill] trigger source: CONFIGURED — no compaction history for this project yet, using ${effectiveTriggerTokens} tokens (${autocompactPct}% of a ${reading.windowSize}-token window).`);
|
|
12506
|
+
const clamp = (name, value) => {
|
|
12507
|
+
if (value >= 0) return value;
|
|
12508
|
+
console.error(`[context-fill] ${name} threshold went negative (${value}) for a ${reading.windowSize}-token window — clamping to 0.`);
|
|
12509
|
+
return 0;
|
|
12510
|
+
};
|
|
12511
|
+
return {
|
|
12512
|
+
warmupTokens: clamp("warmup", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.warmup),
|
|
12513
|
+
refreshTokens: clamp("refresh", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.refresh),
|
|
12514
|
+
immediateTokens: clamp("immediate", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.immediate),
|
|
12515
|
+
effectiveTriggerTokens,
|
|
12516
|
+
autocompactPct,
|
|
12517
|
+
triggerSource,
|
|
12518
|
+
windowConfirmed
|
|
12519
|
+
};
|
|
12520
|
+
}
|
|
12521
|
+
/**
|
|
12522
|
+
* Which named thresholds `usedTokens` has newly crossed, given the ones that
|
|
12523
|
+
* have already fired this session. Ascending order (warmup before refresh).
|
|
12524
|
+
*
|
|
12525
|
+
* A single check can return more than one name — e.g. a session first
|
|
12526
|
+
* observed at 99% of its window crosses warmup and refresh in the same tick,
|
|
12527
|
+
* because there was no earlier clean crossing to catch it at. That is the
|
|
12528
|
+
* "fire immediately" case: the caller enqueues one handover and marks every
|
|
12529
|
+
* newly-crossed name fired, rather than waiting for a crossing that already
|
|
12530
|
+
* happened.
|
|
12531
|
+
*/
|
|
12532
|
+
function crossedThresholds(usedTokens, thresholds, alreadyFired) {
|
|
12533
|
+
const fired = new Set(alreadyFired);
|
|
12534
|
+
return [["warmup", thresholds.warmupTokens], ["refresh", thresholds.refreshTokens]].filter(([name, tokens]) => !fired.has(name) && usedTokens >= tokens).map(([name]) => name);
|
|
12535
|
+
}
|
|
12536
|
+
/**
|
|
12537
|
+
* True once a session is at or above the "no time left for a clean crossing"
|
|
12538
|
+
* floor (0.985 of the window). Purely informational for callers — it never
|
|
12539
|
+
* gates whether `crossedThresholds` fires (a poll-based check already fires
|
|
12540
|
+
* any unfired threshold the moment `usedTokens` reaches it, on whatever tick
|
|
12541
|
+
* observes it) — but it distinguishes "this crossed on schedule" from "this
|
|
12542
|
+
* was first observed already almost out of room", which is worth a different
|
|
12543
|
+
* log line and, for a caller that queues work, a higher priority.
|
|
12544
|
+
*/
|
|
12545
|
+
function isImmediate(usedTokens, thresholds) {
|
|
12546
|
+
return usedTokens >= thresholds.immediateTokens;
|
|
12547
|
+
}
|
|
12548
|
+
|
|
12549
|
+
//#endregion
|
|
12550
|
+
//#region src/session/context-handover-trigger.ts
|
|
12551
|
+
/**
|
|
12552
|
+
* context-handover-trigger.ts — decide whether THIS check is the moment a
|
|
12553
|
+
* session should get its threshold-triggered pre-compaction handover, and
|
|
12554
|
+
* enqueue it with the daemon if so.
|
|
12555
|
+
*
|
|
12556
|
+
* Called from the same live hooks that already drive the AG2 rolling
|
|
12557
|
+
* autosave (UserPromptSubmit, PostToolUse) — see
|
|
12558
|
+
* cli/commands/session/autosave.ts. Reuses the context-fill reading and the
|
|
12559
|
+
* derived thresholds from hooks/ts/lib/context-fill.ts; this module only
|
|
12560
|
+
* adds the "have we already fired this one" bookkeeping and the enqueue.
|
|
12561
|
+
*
|
|
12562
|
+
* Never throws. A daemon that isn't running, a stale reading, an unknown
|
|
12563
|
+
* fill — every one of those is a reason to do nothing this check and try
|
|
12564
|
+
* again next time, not a reason to interrupt the hook that called this.
|
|
12565
|
+
*/
|
|
12566
|
+
/** The enqueue only needs to hand the item to the daemon's queue file — the
|
|
12567
|
+
* actual LLM call happens later, asynchronously, in the worker loop. A
|
|
12568
|
+
* short bound keeps a rare (at most twice per session) daemon hiccup from
|
|
12569
|
+
* making the calling hook wait anywhere near as long as a normal IPC call
|
|
12570
|
+
* is allowed to. */
|
|
12571
|
+
const ENQUEUE_TIMEOUT_MS = 2e3;
|
|
12572
|
+
/** Both thresholds fired — nothing left to check for the rest of the session. */
|
|
12573
|
+
const ALL_THRESHOLDS = ["warmup", "refresh"];
|
|
12574
|
+
function firedThresholdsPath(sessionId) {
|
|
12575
|
+
return join(tmpdir(), `pai-context-handover-fired-${sessionId}.json`);
|
|
12576
|
+
}
|
|
12577
|
+
function loadFiredThresholds(sessionId) {
|
|
12578
|
+
const path = firedThresholdsPath(sessionId);
|
|
12579
|
+
if (!existsSync(path)) return [];
|
|
12580
|
+
try {
|
|
12581
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
12582
|
+
return Array.isArray(raw.fired) ? raw.fired : [];
|
|
12583
|
+
} catch {
|
|
12584
|
+
return [];
|
|
12585
|
+
}
|
|
12586
|
+
}
|
|
12587
|
+
function saveFiredThresholds(sessionId, fired) {
|
|
12588
|
+
try {
|
|
12589
|
+
writeFileSync(firedThresholdsPath(sessionId), JSON.stringify({ fired }), "utf-8");
|
|
12590
|
+
} catch {}
|
|
12591
|
+
}
|
|
12592
|
+
async function enqueueContextHandover(payload) {
|
|
12593
|
+
await new PaiClient().call("work_queue_enqueue", {
|
|
12594
|
+
type: "context-handover",
|
|
12595
|
+
priority: 3,
|
|
12596
|
+
payload: {
|
|
12597
|
+
sessionId: payload.sessionId,
|
|
12598
|
+
cwd: payload.cwd,
|
|
12599
|
+
transcriptPath: payload.transcriptPath,
|
|
12600
|
+
threshold: payload.threshold,
|
|
12601
|
+
urgent: payload.urgent
|
|
12602
|
+
}
|
|
12603
|
+
}, ENQUEUE_TIMEOUT_MS);
|
|
12604
|
+
}
|
|
12605
|
+
const defaultDeps = {
|
|
12606
|
+
getReading: (input) => getContextFill({
|
|
12607
|
+
sessionId: input.sessionId,
|
|
12608
|
+
transcriptPath: input.transcriptPath
|
|
12609
|
+
}),
|
|
12610
|
+
loadFired: loadFiredThresholds,
|
|
12611
|
+
saveFired: saveFiredThresholds,
|
|
12612
|
+
enqueue: enqueueContextHandover
|
|
12613
|
+
};
|
|
12614
|
+
/**
|
|
12615
|
+
* Check this session's current context fill against the derived thresholds
|
|
12616
|
+
* and enqueue a context-handover job for any newly-crossed one. Returns the
|
|
12617
|
+
* threshold name(s) newly fired this call — empty when nothing crossed, the
|
|
12618
|
+
* reading is unknown, or every threshold has already fired.
|
|
12619
|
+
*
|
|
12620
|
+
* A single check that crosses both warmup and refresh in one jump (a
|
|
12621
|
+
* session first observed already close to compaction) enqueues ONE job,
|
|
12622
|
+
* tagged with the more urgent of the two, and marks both fired — one
|
|
12623
|
+
* model-written handover already covers what either alone would have.
|
|
12624
|
+
*/
|
|
12625
|
+
async function checkAndEnqueueContextHandover(input, deps = defaultDeps) {
|
|
12626
|
+
const alreadyFired = deps.loadFired(input.sessionId);
|
|
12627
|
+
if (ALL_THRESHOLDS.every((t) => alreadyFired.includes(t))) return [];
|
|
12628
|
+
const reading = deps.getReading(input);
|
|
12629
|
+
if (reading.status !== "ok" || reading.usedTokens === null) return [];
|
|
12630
|
+
const thresholds = contextFillThresholds(reading, process.env, { cwd: input.cwd });
|
|
12631
|
+
if (!thresholds.windowConfirmed) console.error(`[context-handover-trigger] session ${input.sessionId}: window size not confirmed — using the assumed default (${reading.windowSize} tokens) rather than a reading Claude Code reported.`);
|
|
12632
|
+
const newlyCrossed = crossedThresholds(reading.usedTokens, thresholds, alreadyFired);
|
|
12633
|
+
if (newlyCrossed.length === 0) return [];
|
|
12634
|
+
const urgent = isImmediate(reading.usedTokens, thresholds);
|
|
12635
|
+
const mostUrgent = newlyCrossed[newlyCrossed.length - 1];
|
|
12636
|
+
try {
|
|
12637
|
+
await deps.enqueue({
|
|
12638
|
+
sessionId: input.sessionId,
|
|
12639
|
+
cwd: input.cwd,
|
|
12640
|
+
transcriptPath: input.transcriptPath,
|
|
12641
|
+
threshold: mostUrgent,
|
|
12642
|
+
urgent
|
|
12643
|
+
});
|
|
12644
|
+
} catch (err) {
|
|
12645
|
+
console.error(`[context-handover-trigger] session ${input.sessionId}: enqueue failed, will retry next check: ${err}`);
|
|
12646
|
+
return [];
|
|
12647
|
+
}
|
|
12648
|
+
deps.saveFired(input.sessionId, [...alreadyFired, ...newlyCrossed]);
|
|
12649
|
+
console.error(`[context-handover-trigger] session ${input.sessionId}: crossed [${newlyCrossed.join(", ")}] at ${reading.usedTokens} tokens (trigger=${thresholds.effectiveTriggerTokens}, triggerSource=${thresholds.triggerSource}, autocompactPct=${thresholds.autocompactPct}, urgent=${urgent}).`);
|
|
12650
|
+
return newlyCrossed;
|
|
11961
12651
|
}
|
|
11962
12652
|
|
|
11963
12653
|
//#endregion
|
|
@@ -11990,17 +12680,28 @@ function touchSentinel(rootPath) {
|
|
|
11990
12680
|
writeFileSync(sentinelPath(rootPath), String(Date.now()), "utf8");
|
|
11991
12681
|
} catch {}
|
|
11992
12682
|
}
|
|
11993
|
-
function cmdAutosave(db, opts) {
|
|
12683
|
+
async function cmdAutosave(db, opts) {
|
|
11994
12684
|
const minGap = parseInt(opts.minGap ?? String(DEFAULT_MIN_GAP), 10);
|
|
11995
12685
|
const cwd = process.cwd();
|
|
11996
12686
|
const project = resolveProjectByCwd(db, cwd);
|
|
11997
|
-
if (!project)
|
|
11998
|
-
if (!opts.dryRun && tooRecent(project.root_path, minGap))
|
|
12687
|
+
if (!project) return;
|
|
12688
|
+
if (!opts.dryRun && tooRecent(project.root_path, minGap)) return;
|
|
12689
|
+
const transcriptPaths = findTranscripts(resolveTranscriptDir(project), opts.sessionId);
|
|
12690
|
+
if (opts.sessionId && !opts.dryRun) {
|
|
12691
|
+
const liveTranscript = transcriptPaths[transcriptPaths.length - 1];
|
|
12692
|
+
try {
|
|
12693
|
+
await checkAndEnqueueContextHandover({
|
|
12694
|
+
sessionId: opts.sessionId,
|
|
12695
|
+
cwd,
|
|
12696
|
+
transcriptPath: liveTranscript
|
|
12697
|
+
});
|
|
12698
|
+
} catch {}
|
|
12699
|
+
}
|
|
11999
12700
|
const body = buildAutosaveBody({
|
|
12000
12701
|
cwd,
|
|
12001
|
-
transcriptPaths
|
|
12702
|
+
transcriptPaths
|
|
12002
12703
|
});
|
|
12003
|
-
if (!body)
|
|
12704
|
+
if (!body) return;
|
|
12004
12705
|
const session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
|
|
12005
12706
|
const result = applyContinue({
|
|
12006
12707
|
rootPath: project.root_path,
|
|
@@ -12014,10 +12715,9 @@ function cmdAutosave(db, opts) {
|
|
|
12014
12715
|
if (opts.dryRun) {
|
|
12015
12716
|
console.log(result.block);
|
|
12016
12717
|
console.log(`\n[dry-run] action would be: ${result.action}`);
|
|
12017
|
-
|
|
12718
|
+
return;
|
|
12018
12719
|
}
|
|
12019
12720
|
touchSentinel(project.root_path);
|
|
12020
|
-
process.exit(0);
|
|
12021
12721
|
}
|
|
12022
12722
|
|
|
12023
12723
|
//#endregion
|
|
@@ -12154,7 +12854,8 @@ function cmdGoto(db, query, opts) {
|
|
|
12154
12854
|
let msg = String(resolveErr).replace(/^Error: /, "");
|
|
12155
12855
|
if (msg.includes("No session found matching")) msg += `\n\nTip: pai find "${query}" — search prompt history by keywords`;
|
|
12156
12856
|
console.error(err(msg));
|
|
12157
|
-
process.
|
|
12857
|
+
process.exitCode = 1;
|
|
12858
|
+
return;
|
|
12158
12859
|
}
|
|
12159
12860
|
const { session: matchedSession, friendlyName } = resolved;
|
|
12160
12861
|
let resumableUuid;
|
|
@@ -12176,7 +12877,8 @@ function cmdGoto(db, query, opts) {
|
|
|
12176
12877
|
projectDir = realpathSync(rawDir);
|
|
12177
12878
|
} catch {
|
|
12178
12879
|
console.error(err(`session "${query}": directory does not exist or cannot be resolved.\n Registry says: ${rawDir}\n The directory may have moved or been deleted.`));
|
|
12179
|
-
process.
|
|
12880
|
+
process.exitCode = 1;
|
|
12881
|
+
return;
|
|
12180
12882
|
}
|
|
12181
12883
|
const name = friendlyName ?? query;
|
|
12182
12884
|
const promptArg = `/Name ${name}\ngo`;
|
|
@@ -12220,10 +12922,12 @@ function cmdGoto(db, query, opts) {
|
|
|
12220
12922
|
});
|
|
12221
12923
|
if (result.error) {
|
|
12222
12924
|
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
12223
|
-
process.
|
|
12925
|
+
process.exitCode = 1;
|
|
12926
|
+
return;
|
|
12224
12927
|
}
|
|
12225
12928
|
printExitDir(projectDir);
|
|
12226
|
-
process.
|
|
12929
|
+
process.exitCode = result.status ?? 0;
|
|
12930
|
+
return;
|
|
12227
12931
|
} else {
|
|
12228
12932
|
process.stderr.write(chalk.yellow(`\n Resume failed for ${resumableUuid.slice(0, 8)}: ${probe.reason ?? "unknown error"}\n Starting fresh session in same directory.\n\n`));
|
|
12229
12933
|
const result = spawnSync("claude", [
|
|
@@ -12237,10 +12941,12 @@ function cmdGoto(db, query, opts) {
|
|
|
12237
12941
|
});
|
|
12238
12942
|
if (result.error) {
|
|
12239
12943
|
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
12240
|
-
process.
|
|
12944
|
+
process.exitCode = 1;
|
|
12945
|
+
return;
|
|
12241
12946
|
}
|
|
12242
12947
|
printExitDir(projectDir);
|
|
12243
|
-
process.
|
|
12948
|
+
process.exitCode = result.status ?? 0;
|
|
12949
|
+
return;
|
|
12244
12950
|
}
|
|
12245
12951
|
} else {
|
|
12246
12952
|
const result = spawnSync("claude", [
|
|
@@ -12254,10 +12960,11 @@ function cmdGoto(db, query, opts) {
|
|
|
12254
12960
|
});
|
|
12255
12961
|
if (result.error) {
|
|
12256
12962
|
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
12257
|
-
process.
|
|
12963
|
+
process.exitCode = 1;
|
|
12964
|
+
return;
|
|
12258
12965
|
}
|
|
12259
12966
|
printExitDir(projectDir);
|
|
12260
|
-
process.
|
|
12967
|
+
process.exitCode = result.status ?? 0;
|
|
12261
12968
|
}
|
|
12262
12969
|
}
|
|
12263
12970
|
|
|
@@ -12505,8 +13212,8 @@ function registerSessionCommands(sessionCmd, getDb) {
|
|
|
12505
13212
|
sessionCmd.command("checkpoint <message>").description("Append a timestamped checkpoint to the active session note.\nDesigned for hooks (PostToolUse, UserPromptSubmit) — fast and silent.\nRate-limited: skips silently if last checkpoint was < --min-gap seconds ago.").option("--min-gap <seconds>", "Minimum seconds between checkpoints (default: 300 = 5 minutes)", "300").action((message, opts) => {
|
|
12506
13213
|
cmdCheckpoint(message, opts);
|
|
12507
13214
|
});
|
|
12508
|
-
sessionCmd.command("autosave").description("Refresh the ## Continue checkpoint from the transcript and working tree.\nRuns unattended from live hooks so an interrupted session still leaves\na usable handover. Writes in auto mode, so a model-authored checkpoint\nfor the same session is preserved untouched. Silent; always exits 0.").option("--session-id <uuid>", "Claude session UUID — the key that decides whether an existing authored\ncheckpoint belongs to this session and must be left alone.").option("--min-gap <seconds>", "Minimum seconds between autosaves, shared across all triggers (default: 240)", "240").option("--dry-run", "Print the block that would be written and exit").action((opts) => {
|
|
12509
|
-
cmdAutosave(getDb(), opts);
|
|
13215
|
+
sessionCmd.command("autosave").description("Refresh the ## Continue checkpoint from the transcript and working tree.\nRuns unattended from live hooks so an interrupted session still leaves\na usable handover. Writes in auto mode, so a model-authored checkpoint\nfor the same session is preserved untouched. Silent; always exits 0.").option("--session-id <uuid>", "Claude session UUID — the key that decides whether an existing authored\ncheckpoint belongs to this session and must be left alone.").option("--min-gap <seconds>", "Minimum seconds between autosaves, shared across all triggers (default: 240)", "240").option("--dry-run", "Print the block that would be written and exit").action(async (opts) => {
|
|
13216
|
+
await cmdAutosave(getDb(), opts);
|
|
12510
13217
|
});
|
|
12511
13218
|
sessionCmd.command("active").description("Show currently active Claude Code sessions.\nDetects live sessions by checking which JSONL transcript files\nwere recently modified in ~/.claude/projects/.").option("--minutes <n>", "Consider sessions active if modified within N minutes (default: 60)", "60").option("--json", "Output raw JSON instead of formatted display").action((opts) => {
|
|
12512
13219
|
cmdActive(getDb(), opts);
|
|
@@ -12900,8 +13607,8 @@ function analyzeProject(db, project) {
|
|
|
12900
13607
|
async function countVectorDbPaths(oldPaths) {
|
|
12901
13608
|
if (oldPaths.length === 0) return 0;
|
|
12902
13609
|
try {
|
|
12903
|
-
const { loadConfig } = await import("./config-
|
|
12904
|
-
const { PostgresBackend } = await import("./postgres-
|
|
13610
|
+
const { loadConfig } = await import("./config-C_ErGddD.mjs");
|
|
13611
|
+
const { PostgresBackend } = await import("./postgres-BVme6qX0.mjs");
|
|
12905
13612
|
const config = loadConfig();
|
|
12906
13613
|
if (config.storageBackend !== "postgres") return 0;
|
|
12907
13614
|
const pgBackend = new PostgresBackend(config.postgres ?? {});
|
|
@@ -12921,8 +13628,8 @@ async function countVectorDbPaths(oldPaths) {
|
|
|
12921
13628
|
async function updateVectorDbPaths(moves) {
|
|
12922
13629
|
if (moves.length === 0) return 0;
|
|
12923
13630
|
try {
|
|
12924
|
-
const { loadConfig } = await import("./config-
|
|
12925
|
-
const { PostgresBackend } = await import("./postgres-
|
|
13631
|
+
const { loadConfig } = await import("./config-C_ErGddD.mjs");
|
|
13632
|
+
const { PostgresBackend } = await import("./postgres-BVme6qX0.mjs");
|
|
12926
13633
|
const config = loadConfig();
|
|
12927
13634
|
if (config.storageBackend !== "postgres") return 0;
|
|
12928
13635
|
const pgBackend = new PostgresBackend(config.postgres ?? {});
|
|
@@ -13228,7 +13935,8 @@ function registerSessionCleanupCommand(sessionCmd, getDb) {
|
|
|
13228
13935
|
const p = getProject(db, projectSlug);
|
|
13229
13936
|
if (!p) {
|
|
13230
13937
|
console.error(err(`Project not found: ${projectSlug}`));
|
|
13231
|
-
process.
|
|
13938
|
+
process.exitCode = 1;
|
|
13939
|
+
return;
|
|
13232
13940
|
}
|
|
13233
13941
|
projects = [p];
|
|
13234
13942
|
} else projects = getAllProjects(db);
|
|
@@ -14204,5 +14912,245 @@ async function cmdPick(db, opts = {}) {
|
|
|
14204
14912
|
}
|
|
14205
14913
|
|
|
14206
14914
|
//#endregion
|
|
14207
|
-
|
|
14208
|
-
|
|
14915
|
+
//#region src/cli/program.ts
|
|
14916
|
+
/**
|
|
14917
|
+
* PAI CLI — Commander program builder.
|
|
14918
|
+
*
|
|
14919
|
+
* Constructs the full `pai` command tree WITHOUT parsing argv. This separation
|
|
14920
|
+
* lets the documentation generator (scripts/build-docs.mjs) import and introspect
|
|
14921
|
+
* the live program object — the single source of truth for `--help`, the
|
|
14922
|
+
* generated man pages, and `pai help <area>`.
|
|
14923
|
+
*
|
|
14924
|
+
* The thin entry point (cli/index.ts) imports buildProgram() and calls parse().
|
|
14925
|
+
*/
|
|
14926
|
+
function getVersion() {
|
|
14927
|
+
try {
|
|
14928
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
14929
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
|
|
14930
|
+
} catch {
|
|
14931
|
+
return "0.0.0";
|
|
14932
|
+
}
|
|
14933
|
+
}
|
|
14934
|
+
let _db = null;
|
|
14935
|
+
function getDb() {
|
|
14936
|
+
if (!_db) try {
|
|
14937
|
+
_db = openRegistry();
|
|
14938
|
+
} catch (e) {
|
|
14939
|
+
throw new Error(`Failed to open PAI registry: ${e}`);
|
|
14940
|
+
}
|
|
14941
|
+
return _db;
|
|
14942
|
+
}
|
|
14943
|
+
/**
|
|
14944
|
+
* Build and return the fully configured `pai` Commander program.
|
|
14945
|
+
* Does NOT call parse — the caller (cli/index.ts) does that.
|
|
14946
|
+
*/
|
|
14947
|
+
function buildProgram() {
|
|
14948
|
+
const program = new Command();
|
|
14949
|
+
program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit").addHelpText("after", `
|
|
14950
|
+
Daily commands:
|
|
14951
|
+
pai Show your work: live tabs + recent sessions + recent projects
|
|
14952
|
+
pai <name> Switch / resume / fresh — universal
|
|
14953
|
+
pai pause [all] Pause current (or all live) sessions
|
|
14954
|
+
pai end Finalize current session
|
|
14955
|
+
|
|
14956
|
+
Subcommands (rarely needed):
|
|
14957
|
+
pai projects ... Project registry management (cd, rebind, info, ...)
|
|
14958
|
+
pai registry ... Registry maintenance (scan, ...)
|
|
14959
|
+
pai memory ... Memory engine (index, search, ...)
|
|
14960
|
+
pai shell-init Shell integration (eval "$(pai shell-init)")
|
|
14961
|
+
|
|
14962
|
+
Recovery:
|
|
14963
|
+
pai clear-names Wipe corrupt iTerm tab-name store
|
|
14964
|
+
|
|
14965
|
+
Examples:
|
|
14966
|
+
pai aibroker Switch to the live AIBroker tab in iTerm
|
|
14967
|
+
pai mdf Find sessions where you worked on MDF
|
|
14968
|
+
pai 81c5c3dc Resume session by UUID prefix
|
|
14969
|
+
pai --all Show everything including cold and zero-session projects`);
|
|
14970
|
+
registerProjectsCommands(program.command("projects").description("Manage registered projects (list, cd, add, info, ...)"), getDb);
|
|
14971
|
+
registerProjectsCommands(program.command("project").description("Alias for `pai projects`"), getDb);
|
|
14972
|
+
const sessionCmd = program.command("session").description("Session management: list, info, checkpoint, handover, cleanup, slug, tag, route.\nShort forms: `pai pause`, `pai end`, `pai resume <name>`.");
|
|
14973
|
+
registerSessionCommands(sessionCmd, getDb);
|
|
14974
|
+
registerSessionCleanupCommand(sessionCmd, getDb);
|
|
14975
|
+
program.command("sessions").description("Alias for `pai` — show the unified deduped listing.").option("--all", "Show all entries including cold / zero-session / archived projects").option("-n, --n <count>", "Max candidates for history search", "20").action(async (opts) => {
|
|
14976
|
+
await cmdMain(getDb(), void 0, void 0, opts);
|
|
14977
|
+
});
|
|
14978
|
+
registerRegistryCommands(program.command("registry").description("Registry maintenance: scan, migrate, stats, rebuild"), getDb);
|
|
14979
|
+
registerMemoryCommands(program.command("memory").description("Memory engine: index, search, and status"), getDb);
|
|
14980
|
+
registerIdentityCommands(program.command("identity").description("Declare who you are: self addresses and where mail is delivered"));
|
|
14981
|
+
registerMcpCommands(program.command("mcp").description("MCP server management: install and status"));
|
|
14982
|
+
registerDaemonCommands(program.command("daemon").description("PAI daemon management: serve, status, restart, install, uninstall, logs"));
|
|
14983
|
+
registerBackupCommands(program);
|
|
14984
|
+
registerRestoreCommands(program);
|
|
14985
|
+
registerSetupCommand(program);
|
|
14986
|
+
registerUpdateCommand(program);
|
|
14987
|
+
registerNotifyCommands(program.command("notify").description("Notification config: status, get, set, test, send"));
|
|
14988
|
+
registerTaskCommands(program.command("task").alias("tasks").description("Task bus: list, add, dispatch, and complete cross-session work"));
|
|
14989
|
+
registerTopicCommands(program.command("topic").description("Topic shift detection: check whether context has drifted to a different project"));
|
|
14990
|
+
registerKgCommands(program.command("kg").description("Temporal knowledge graph: backfill, query, list, stats"));
|
|
14991
|
+
registerDbCommands(program.command("db").description("Database inspection: query, tables, schema (sqlite or postgres)"));
|
|
14992
|
+
registerObsidianCommands(program.command("obsidian").description("Obsidian vault: sync project notes, view status, open in Obsidian"), getDb);
|
|
14993
|
+
registerZettelCommands(program.command("zettel").description("Zettelkasten intelligence: explore, surprise, converse, themes, health, suggest"), getDb);
|
|
14994
|
+
registerObservationCommands(program.command("observation").description("Observation capture: list, search, and stats"));
|
|
14995
|
+
registerSkillCommands(program.command("skill").description("Skill telemetry and (future) discovery for the self-educating skill system"));
|
|
14996
|
+
registerHelpCommand(program);
|
|
14997
|
+
program.command("pause [target]").description("Save state and display safe-exit instructions for the current session.\nWrites a ## Continue checkpoint to the project's TODO.md.\nUse `pai pause all` to pause every live session via AIBroker.").option("--dry-run", "Preview changes without writing them").option("--body-file <path>", "File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.").option("--session-id <uuid>", "Claude Code session UUID — recorded as the `claude --resume` handle").option("--no-body", "Deliberately write a metadata-only checkpoint (no content)").option("--exit", "(pause all only) Also send /exit to each session after pausing").option("--only <name>", "(pause all only) Pause just the sessions whose name or id contains <name>.\nFor rehearsing the real thing against one session before trusting it with\nevery open tab — a dry run exercises enumeration but never sends anything,\nwhich is exactly how a broken send survived until it was needed.").option("--wait <ms>", "(pause all only) Max milliseconds to wait for a session to finish its\ncheckpoint before /exit. Not a fixed delay — sessions are exited as soon\nas they return to the prompt, and any still busy at the deadline are left\nopen rather than killed mid-write. (default: 180000)", "180000").action(async (target, opts) => {
|
|
14998
|
+
if (target === "all") await cmdPauseAll({
|
|
14999
|
+
exit: opts.exit,
|
|
15000
|
+
dryRun: opts.dryRun,
|
|
15001
|
+
only: opts.only,
|
|
15002
|
+
wait: opts.wait !== void 0 ? parseInt(opts.wait, 10) : void 0
|
|
15003
|
+
});
|
|
15004
|
+
else {
|
|
15005
|
+
if (target !== void 0) {
|
|
15006
|
+
console.error(err(`Unknown target: ${target}. Did you mean 'pai pause all'?`));
|
|
15007
|
+
process.exitCode = 1;
|
|
15008
|
+
return;
|
|
15009
|
+
}
|
|
15010
|
+
cmdPause(getDb(), {
|
|
15011
|
+
dryRun: opts.dryRun,
|
|
15012
|
+
bodyFile: opts.bodyFile,
|
|
15013
|
+
sessionId: opts.sessionId,
|
|
15014
|
+
noBody: opts.body === false
|
|
15015
|
+
});
|
|
15016
|
+
}
|
|
15017
|
+
});
|
|
15018
|
+
program.command("end").description("Finalize a session: save state, mark note Completed, display safe-exit instructions.").option("--dry-run", "Preview all changes without writing them").option("--body-file <path>", "File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.").option("--session-id <uuid>", "Claude Code session UUID — recorded as the `claude --resume` handle").option("--no-body", "Deliberately write a metadata-only checkpoint (no content)").action((opts) => {
|
|
15019
|
+
cmdEnd(getDb(), {
|
|
15020
|
+
dryRun: opts.dryRun,
|
|
15021
|
+
bodyFile: opts.bodyFile,
|
|
15022
|
+
sessionId: opts.sessionId,
|
|
15023
|
+
noBody: opts.body === false
|
|
15024
|
+
});
|
|
15025
|
+
});
|
|
15026
|
+
program.command("clear-names").description("Recovery: wipe corrupted iTerm2 session name state.\nClears ~/.aibroker/session-names.json AND user.paiName from all live iTerm2 sessions.\nAfter running this, use /Name to re-label each tab.").option("--dry-run", "Preview what would be cleared without making changes").action(async (opts) => {
|
|
15027
|
+
await cmdClearNames(opts);
|
|
15028
|
+
});
|
|
15029
|
+
program.command("resume <name>", { hidden: true }).description("Go to a session by name or UUID (prefer: pai <name>)").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((name, opts) => {
|
|
15030
|
+
cmdGoto(getDb(), name, { dryRun: opts.dryRun });
|
|
15031
|
+
});
|
|
15032
|
+
program.command("cd <identifier>", { hidden: true }).description("cd to a project directory (shell wrapper handles the actual cd)").action((identifier) => {
|
|
15033
|
+
const db = getDb();
|
|
15034
|
+
const project = resolveIdentifier(db, identifier);
|
|
15035
|
+
if (!project) {
|
|
15036
|
+
console.error(`Project not found: ${identifier}`);
|
|
15037
|
+
process.exitCode = 1;
|
|
15038
|
+
return;
|
|
15039
|
+
}
|
|
15040
|
+
if (existsSync(project.root_path)) {
|
|
15041
|
+
process.stdout.write(project.root_path + "\n");
|
|
15042
|
+
return;
|
|
15043
|
+
}
|
|
15044
|
+
process.stderr.write(warn$1(`Path not found: ${project.root_path}\n`) + dim$1(" Searching for moved location...\n"));
|
|
15045
|
+
const result = findMovedPath(project.root_path);
|
|
15046
|
+
if (result.found) {
|
|
15047
|
+
const newPath = result.found;
|
|
15048
|
+
const newEncoded = encodeDir(newPath);
|
|
15049
|
+
const ts = now();
|
|
15050
|
+
db.prepare("UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?").run(newPath, newEncoded, ts, project.id);
|
|
15051
|
+
process.stderr.write(ok$1(`Project moved: ${shortenPath(project.root_path, 50)}\n`) + dim$1(` → ${newPath}\n`) + ok$1("Registry updated.\n"));
|
|
15052
|
+
process.stdout.write(newPath + "\n");
|
|
15053
|
+
return;
|
|
15054
|
+
}
|
|
15055
|
+
if (result.ambiguous) {
|
|
15056
|
+
process.stderr.write(warn$1(`Multiple directories named "${basename(project.root_path)}" found:\n`));
|
|
15057
|
+
for (const candidate of result.ambiguous) process.stderr.write(dim$1(` ${candidate}\n`));
|
|
15058
|
+
process.stderr.write(dim$1(`\n Disambiguate with: pai projects rebind ${project.slug} <path>\n`));
|
|
15059
|
+
process.exitCode = 1;
|
|
15060
|
+
return;
|
|
15061
|
+
}
|
|
15062
|
+
process.stderr.write(err(`Project "${project.slug}" root_path "${project.root_path}" does not exist on disk\n and no folder named "${basename(project.root_path)}" was found in scan dirs.\n`) + dim$1(` Fix with: pai projects rebind ${project.slug} <new-path>\n`));
|
|
15063
|
+
process.exitCode = 1;
|
|
15064
|
+
});
|
|
15065
|
+
program.command("notes [project-slug]", { hidden: true }).description("Markdown session notes — list notes, optionally filtered to a project.").option("--limit <n>", "Maximum number of notes to show", "20").option("--status <status>", "Filter by status: open | completed | compacted").action((projectSlug, opts) => {
|
|
15066
|
+
cmdList(getDb(), projectSlug, opts);
|
|
15067
|
+
}).command("list [project-slug]").description("List markdown session notes (same as: pai notes)").option("--limit <n>", "Maximum number of notes to show", "20").option("--status <status>", "Filter by status: open | completed | compacted").action((projectSlug, opts) => {
|
|
15068
|
+
cmdList(getDb(), projectSlug, opts);
|
|
15069
|
+
});
|
|
15070
|
+
program.command("find <query>", { hidden: true }).description("Search prompt history for matching sessions.").option("-n, --n <count>", "Maximum number of sessions to show", "20").option("--json", "Output as JSON array").action(async (query, opts) => {
|
|
15071
|
+
await cmdFind(query, opts);
|
|
15072
|
+
});
|
|
15073
|
+
program.command("shell-init").description("Emit shell integration code. Add to ~/.zshrc: eval \"$(pai shell-init)\"").action(() => {
|
|
15074
|
+
process.stdout.write(`# PAI shell integration — generated by: pai shell-init
|
|
15075
|
+
pai() {
|
|
15076
|
+
if [[ "$1" == "cd" ]]; then
|
|
15077
|
+
local dir=$(command pai cd "$2" 2>/dev/null)
|
|
15078
|
+
if [[ -n "$dir" && -d "$dir" ]]; then
|
|
15079
|
+
builtin cd "$dir"
|
|
15080
|
+
echo "-> $dir"
|
|
15081
|
+
else
|
|
15082
|
+
echo "Project not found: $2" >&2
|
|
15083
|
+
return 1
|
|
15084
|
+
fi
|
|
15085
|
+
elif [[ "$1" == "projects" && "$2" == "cd" ]]; then
|
|
15086
|
+
local dir=$(command pai projects cd "$3" 2>/dev/null)
|
|
15087
|
+
if [[ -n "$dir" && -d "$dir" ]]; then
|
|
15088
|
+
builtin cd "$dir"
|
|
15089
|
+
echo "-> $dir"
|
|
15090
|
+
else
|
|
15091
|
+
echo "Project not found: $3" >&2
|
|
15092
|
+
return 1
|
|
15093
|
+
fi
|
|
15094
|
+
elif [[ $# -eq 0 ]]; then
|
|
15095
|
+
# Bare \`pai\` → interactive picker. fzf draws on /dev/tty; the picker writes
|
|
15096
|
+
# a chosen directory to PAI_PICK_OUT when you press Ctrl-G (cd only), which we
|
|
15097
|
+
# then cd into here (a child process can't change this shell's cwd itself).
|
|
15098
|
+
local __pai_f
|
|
15099
|
+
__pai_f=$(mktemp -t pai-pick 2>/dev/null) || { command pai; return $?; }
|
|
15100
|
+
PAI_PICK_OUT="$__pai_f" command pai
|
|
15101
|
+
local __pai_rc=$?
|
|
15102
|
+
local __pai_dir=""
|
|
15103
|
+
[[ -s "$__pai_f" ]] && __pai_dir=$(cat "$__pai_f")
|
|
15104
|
+
command rm -f "$__pai_f"
|
|
15105
|
+
if [[ -n "$__pai_dir" && -d "$__pai_dir" ]]; then
|
|
15106
|
+
builtin cd "$__pai_dir"
|
|
15107
|
+
echo "-> $__pai_dir"
|
|
15108
|
+
fi
|
|
15109
|
+
return $__pai_rc
|
|
15110
|
+
else
|
|
15111
|
+
command pai "$@"
|
|
15112
|
+
fi
|
|
15113
|
+
}
|
|
15114
|
+
|
|
15115
|
+
# Print the working directory after an interactive Claude Code session exits.
|
|
15116
|
+
# A SessionEnd hook can't do this reliably: Claude Code does not fire SessionEnd
|
|
15117
|
+
# on /exit (anthropics/claude-code#17885) and may reap the hook mid-run (#41577).
|
|
15118
|
+
# Running it from the shell, after \`claude\` returns, sidesteps both.
|
|
15119
|
+
claude() {
|
|
15120
|
+
local __pai_skip=0 __pai_arg
|
|
15121
|
+
for __pai_arg in "$@"; do
|
|
15122
|
+
case "$__pai_arg" in
|
|
15123
|
+
-p|--print) __pai_skip=1 ;;
|
|
15124
|
+
esac
|
|
15125
|
+
done
|
|
15126
|
+
command claude "$@"
|
|
15127
|
+
local __pai_rc=$?
|
|
15128
|
+
if [[ -t 1 && $__pai_skip -eq 0 ]]; then
|
|
15129
|
+
printf '\\n\\033[2m📂 Working directory:\\033[0m %s\\n\\033[2m cd "%s"\\033[0m\\n' "$PWD" "$PWD"
|
|
15130
|
+
fi
|
|
15131
|
+
return $__pai_rc
|
|
15132
|
+
}
|
|
15133
|
+
`);
|
|
15134
|
+
});
|
|
15135
|
+
program.command("query [query] [pick]", {
|
|
15136
|
+
isDefault: true,
|
|
15137
|
+
hidden: true
|
|
15138
|
+
}).description("Find and launch a session by topic, name, or UUID.\nNo arg → deduped session listing\nName → switch live tab, or resume, or start fresh\nUUID → direct resume via filesystem scan\nTopic → history search → candidate list → pick #").option("-y, --auto", "Auto-pick #1 without prompting").option("--dry-run", "Print what would happen without launching").option("-n, --n <count>", "Max candidates for history search", "20").option("--all", "Show all entries including cold / zero-session / archived projects").option("--list", "Skip the interactive picker; print the static session listing").action(async (query, pick, opts) => {
|
|
15139
|
+
if (query === void 0 && !opts.list) {
|
|
15140
|
+
await cmdPick(getDb(), {
|
|
15141
|
+
all: opts.all,
|
|
15142
|
+
dryRun: opts.dryRun
|
|
15143
|
+
});
|
|
15144
|
+
return;
|
|
15145
|
+
}
|
|
15146
|
+
const pickN = pick !== void 0 ? parseInt(pick, 10) : void 0;
|
|
15147
|
+
await cmdMain(getDb(), query, pickN, opts);
|
|
15148
|
+
});
|
|
15149
|
+
program.configureOutput({ writeErr: (str) => process.stderr.write(err(str)) });
|
|
15150
|
+
program.exitOverride();
|
|
15151
|
+
return program;
|
|
15152
|
+
}
|
|
15153
|
+
|
|
15154
|
+
//#endregion
|
|
15155
|
+
export { drainStdio as n, buildProgram as t };
|
|
15156
|
+
//# sourceMappingURL=program-BnMNFb4O.mjs.map
|