@tekmidian/pai 0.9.13 → 0.9.14
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/{auto-route-DL4ViDKC.mjs → auto-route-D54Kv7yI.mjs} +2 -2
- package/dist/{auto-route-DL4ViDKC.mjs.map → auto-route-D54Kv7yI.mjs.map} +1 -1
- package/dist/cli/index.mjs +808 -345
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{clusters-CRlPBpq8.mjs → clusters-BwmdKB-V.mjs} +1 -1
- package/dist/{clusters-CRlPBpq8.mjs.map → clusters-BwmdKB-V.mjs.map} +1 -1
- package/dist/{config-BuhHWyOK.mjs → config-DqBY3aT0.mjs} +1 -1
- package/dist/{config-BuhHWyOK.mjs.map → config-DqBY3aT0.mjs.map} +1 -1
- package/dist/daemon/index.mjs +12 -8
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BryEC3as.mjs +1426 -0
- package/dist/daemon-BryEC3as.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/{detector-AzVtGLtL.mjs → detector-Q_lVXtj-.mjs} +1 -1
- package/dist/{detector-AzVtGLtL.mjs.map → detector-Q_lVXtj-.mjs.map} +1 -1
- package/dist/{factory-BDBIfTDC.mjs → factory-BufouUQ1.mjs} +3 -3
- package/dist/{factory-BDBIfTDC.mjs.map → factory-BufouUQ1.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +39 -0
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/stop-hook.mjs +36 -0
- package/dist/hooks/stop-hook.mjs.map +2 -2
- package/dist/index.mjs +2 -2
- package/dist/indexer-D7MvSQPY.mjs +1 -0
- package/dist/{indexer-backend-DXkDh-kc.mjs → indexer-backend-BrrCnasS.mjs} +1 -1
- package/dist/{indexer-backend-DXkDh-kc.mjs.map → indexer-backend-BrrCnasS.mjs.map} +1 -1
- package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-C3pjwy2m.mjs} +1 -1
- package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-C3pjwy2m.mjs.map} +1 -1
- package/dist/{kg-entity-D5v7RCDi.mjs → kg-entity-DVzTsy6G.mjs} +1 -1
- package/dist/{kg-entity-D5v7RCDi.mjs.map → kg-entity-DVzTsy6G.mjs.map} +1 -1
- package/dist/kg-extraction-r5KkTmWY.mjs +524 -0
- package/dist/kg-extraction-r5KkTmWY.mjs.map +1 -0
- package/dist/{latent-ideas-DvWBRHsy.mjs → latent-ideas-dWVuI_dg.mjs} +2 -2
- package/dist/{latent-ideas-DvWBRHsy.mjs.map → latent-ideas-dWVuI_dg.mjs.map} +1 -1
- package/dist/{migrate-jokLenje.mjs → migrate-fLD6rAdO.mjs} +2 -2
- package/dist/{migrate-jokLenje.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
- package/dist/{neighborhood-u8ytjmWq.mjs → neighborhood-DixmpfPT.mjs} +1 -1
- package/dist/{neighborhood-u8ytjmWq.mjs.map → neighborhood-DixmpfPT.mjs.map} +1 -1
- package/dist/{note-context-CG2_e-0W.mjs → note-context-BY7rfGdl.mjs} +1 -1
- package/dist/{note-context-CG2_e-0W.mjs.map → note-context-BY7rfGdl.mjs.map} +1 -1
- package/dist/{postgres-BGERehmX.mjs → postgres-Cp25xZes.mjs} +1 -1
- package/dist/{postgres-BGERehmX.mjs.map → postgres-Cp25xZes.mjs.map} +1 -1
- package/dist/{query-feedback-CQSumXDy.mjs → query-feedback-C0hKUHVz.mjs} +1 -1
- package/dist/{query-feedback-CQSumXDy.mjs.map → query-feedback-C0hKUHVz.mjs.map} +1 -1
- package/dist/{sqlite-xUe94oUZ.mjs → sqlite-B3JHfJc2.mjs} +1 -1
- package/dist/{sqlite-xUe94oUZ.mjs.map → sqlite-B3JHfJc2.mjs.map} +1 -1
- package/dist/{state-BIlxNRUn.mjs → state-BHZAhLou.mjs} +1 -1
- package/dist/{state-BIlxNRUn.mjs.map → state-BHZAhLou.mjs.map} +1 -1
- package/dist/{themes-9jxFn3Rf.mjs → themes-DO8mUVwD.mjs} +1 -1
- package/dist/{themes-9jxFn3Rf.mjs.map → themes-DO8mUVwD.mjs.map} +1 -1
- package/dist/{tools-CF0lB5lW.mjs → tools-C1JNgya7.mjs} +14 -14
- package/dist/{tools-CF0lB5lW.mjs.map → tools-C1JNgya7.mjs.map} +1 -1
- package/dist/{trace-C2XrzssW.mjs → trace-TYCLMCkY.mjs} +1 -1
- package/dist/{trace-C2XrzssW.mjs.map → trace-TYCLMCkY.mjs.map} +1 -1
- package/dist/{utils-QSfKagcj.mjs → utils-BAxjW3j8.mjs} +2 -2
- package/dist/{utils-QSfKagcj.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
- package/dist/{vault-indexer-iHRjcy6e.mjs → vault-indexer-DsPC6wQf.mjs} +1 -1
- package/dist/{vault-indexer-iHRjcy6e.mjs.map → vault-indexer-DsPC6wQf.mjs.map} +1 -1
- package/dist/{daemon-w2fE3UMz.mjs → work-queue-worker-94INtUiz.mjs} +54 -1398
- package/dist/work-queue-worker-94INtUiz.mjs.map +1 -0
- package/dist/{zettelkasten-BdaMzTGQ.mjs → zettelkasten-B-94vghH.mjs} +3 -3
- package/dist/{zettelkasten-BdaMzTGQ.mjs.map → zettelkasten-B-94vghH.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +48 -0
- package/src/hooks/ts/stop/stop-hook.ts +40 -0
- package/dist/daemon-w2fE3UMz.mjs.map +0 -1
- package/dist/kg-extraction-Uvr9fTlz.mjs +0 -246
- package/dist/kg-extraction-Uvr9fTlz.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as openRegistry } from "../db-BtuN768f.mjs";
|
|
3
|
-
import { _ as warn, a as fmtDate, c as ok, d as scaffoldProjectDirs, f as shortenPath, i as err, l as renderTable, m as slugify, n as dim, o as header, p as slugFromPath, r as encodeDir, s as now, t as bold, u as resolvePath } from "../utils-
|
|
4
|
-
import { a as slugify$1, i as parseSessionFilename
|
|
5
|
-
import { n as ensurePaiMarker
|
|
3
|
+
import { _ as warn, a as fmtDate, c as ok, d as scaffoldProjectDirs, f as shortenPath, h as smartDecodeDir, i as err, l as renderTable, m as slugify, n as dim, o as header, p as slugFromPath, r as encodeDir, s as now, t as bold, u as resolvePath } from "../utils-BAxjW3j8.mjs";
|
|
4
|
+
import { a as slugify$1, i as parseSessionFilename } from "../migrate-fLD6rAdO.mjs";
|
|
5
|
+
import { n as ensurePaiMarker } from "../pai-marker-CXQPX2P6.mjs";
|
|
6
6
|
import { n as openFederation } from "../db-CYmBWcjh.mjs";
|
|
7
7
|
import "../helpers-OCVFgprQ.mjs";
|
|
8
8
|
import { i as indexProject, n as indexAll, t as embedChunks } from "../sync-CdHSL9Kc.mjs";
|
|
@@ -10,12 +10,13 @@ import "../embeddings-DGRAPAYb.mjs";
|
|
|
10
10
|
import { t as STOP_WORDS } from "../stop-words-BaMEGVeY.mjs";
|
|
11
11
|
import { n as populateSlugs, r as searchMemory } from "../search-i2nlQ-JM.mjs";
|
|
12
12
|
import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-CdaA48EI.mjs";
|
|
13
|
-
import { t as extractAndStoreTriples } from "../kg-extraction-
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import { t as
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
13
|
+
import { a as resolveHome, c as upsertSession, n as cmdScan, o as saveScanConfig, r as loadScanConfig, s as upsertProject, t as extractAndStoreTriples } from "../kg-extraction-r5KkTmWY.mjs";
|
|
14
|
+
import "../indexer-D7MvSQPY.mjs";
|
|
15
|
+
import { t as PaiClient } from "../ipc-client-C3pjwy2m.mjs";
|
|
16
|
+
import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, t as CONFIG_DIR } from "../config-DqBY3aT0.mjs";
|
|
17
|
+
import { t as createStorageBackend } from "../factory-BufouUQ1.mjs";
|
|
18
|
+
import { s as kgQuery } from "../kg-entity-DVzTsy6G.mjs";
|
|
19
|
+
import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19
20
|
import { homedir, platform, tmpdir } from "node:os";
|
|
20
21
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
21
22
|
import chalk from "chalk";
|
|
@@ -1094,68 +1095,68 @@ function cmdHealth$1(db, opts) {
|
|
|
1094
1095
|
}
|
|
1095
1096
|
|
|
1096
1097
|
//#endregion
|
|
1097
|
-
//#region src/cli/commands/project/index.ts
|
|
1098
|
-
function
|
|
1099
|
-
|
|
1100
|
-
cmdAdd(getDb(), rawPath, opts);
|
|
1101
|
-
});
|
|
1102
|
-
projectCmd.command("list").description("List registered projects").option("--status <status>", "Filter by status: active | archived").option("--tag <tag>", "Filter by tag").option("--type <type>", "Filter by type").action((opts) => {
|
|
1098
|
+
//#region src/cli/commands/project/projects-index.ts
|
|
1099
|
+
function registerProjectsCommands(projectsCmd, getDb) {
|
|
1100
|
+
projectsCmd.command("list", { isDefault: true }).description("List registered projects. Short form: pai projects (bare, no subcommand)").option("--status <status>", "Filter by status: active | archived").option("--tag <tag>", "Filter by tag").option("--type <type>", "Filter by type").action((opts) => {
|
|
1103
1101
|
cmdList$3(getDb(), opts);
|
|
1104
1102
|
});
|
|
1105
|
-
|
|
1103
|
+
projectsCmd.command("cd <identifier>").description("cd to a project directory. Short form: pai cd <name>\n(The shell wrapper handles the actual cd; pure output here.)").action((identifier) => {
|
|
1104
|
+
const project = resolveIdentifier(getDb(), identifier);
|
|
1105
|
+
if (!project) {
|
|
1106
|
+
console.error(`Project not found: ${identifier}`);
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
process.stdout.write(project.root_path + "\n");
|
|
1110
|
+
});
|
|
1111
|
+
projectsCmd.command("add <path>").description("Register a project directory in the PAI registry").option("--slug <slug>", "Override auto-generated slug").option("--type <type>", "Project type: local | central | obsidian-linked | external", "local").option("--display-name <name>", "Human-readable display name").action((rawPath, opts) => {
|
|
1112
|
+
cmdAdd(getDb(), rawPath, opts);
|
|
1113
|
+
});
|
|
1114
|
+
projectsCmd.command("info <slug>").description("Show full details for a project").action((slug) => {
|
|
1106
1115
|
cmdInfo$1(getDb(), slug);
|
|
1107
1116
|
});
|
|
1108
|
-
|
|
1117
|
+
projectsCmd.command("archive <slug>").description("Archive a project").action((slug) => {
|
|
1109
1118
|
cmdArchive(getDb(), slug);
|
|
1110
1119
|
});
|
|
1111
|
-
|
|
1120
|
+
projectsCmd.command("unarchive <slug>").description("Restore an archived project to active status").action((slug) => {
|
|
1112
1121
|
cmdUnarchive(getDb(), slug);
|
|
1113
1122
|
});
|
|
1114
|
-
|
|
1123
|
+
projectsCmd.command("move <slug> <new-path>").description("Update the root path for a project").action((slug, newPath) => {
|
|
1115
1124
|
cmdMove(getDb(), slug, newPath);
|
|
1116
1125
|
});
|
|
1117
|
-
|
|
1126
|
+
projectsCmd.command("tag <slug> <tags...>").description("Add one or more tags to a project").action((slug, tags) => {
|
|
1118
1127
|
cmdTag$1(getDb(), slug, tags);
|
|
1119
1128
|
});
|
|
1120
|
-
|
|
1129
|
+
projectsCmd.command("alias <slug> <alias>").description("Register an alternative slug for a project").action((slug, alias) => {
|
|
1121
1130
|
cmdAlias(getDb(), slug, alias);
|
|
1122
1131
|
});
|
|
1123
|
-
|
|
1132
|
+
projectsCmd.command("edit <slug>").description("Edit project metadata").option("--display-name <name>", "New display name").option("--type <type>", "New type").action((slug, opts) => {
|
|
1124
1133
|
cmdEdit(getDb(), slug, opts);
|
|
1125
1134
|
});
|
|
1126
|
-
|
|
1127
|
-
const project = resolveIdentifier(getDb(), identifier);
|
|
1128
|
-
if (!project) {
|
|
1129
|
-
console.error(`Project not found: ${identifier}`);
|
|
1130
|
-
process.exit(1);
|
|
1131
|
-
}
|
|
1132
|
-
process.stdout.write(project.root_path + "\n");
|
|
1133
|
-
});
|
|
1134
|
-
projectCmd.command("detect [path]").description("Detect which registered project the given path (or CWD) belongs to").option("--json", "Output raw JSON instead of human-readable text").action((pathArg, opts) => {
|
|
1135
|
+
projectsCmd.command("detect [path]").description("Detect which registered project the given path (or CWD) belongs to").option("--json", "Output raw JSON instead of human-readable text").action((pathArg, opts) => {
|
|
1135
1136
|
cmdDetect(getDb(), pathArg, opts);
|
|
1136
1137
|
});
|
|
1137
|
-
|
|
1138
|
+
projectsCmd.command("health").description("Audit all registered projects: check which paths still exist, find moved/dead projects").option("--fix", "Auto-remediate where possible").option("--json", "Output raw JSON report").option("--status <category>", "Filter output to: active | stale | dead").action((opts) => {
|
|
1138
1139
|
cmdHealth$1(getDb(), opts);
|
|
1139
1140
|
});
|
|
1140
|
-
|
|
1141
|
+
projectsCmd.command("consolidate <identifier>").description("Consolidate scattered ~/.claude/projects/.../Notes/ directories for a project into its canonical Notes/ location").option("--yes", "Perform consolidation without confirmation prompt").option("--dry-run", "Preview what would be moved without making changes").action((identifier, opts) => {
|
|
1141
1142
|
cmdConsolidate(getDb(), identifier, opts);
|
|
1142
1143
|
});
|
|
1143
|
-
|
|
1144
|
+
projectsCmd.command("promote").description("Promote a session note into a new standalone project").requiredOption("--from-session <path>", "Path to the session note markdown file").requiredOption("--to <path>", "Directory path for the new project (must not exist)").option("--name <name>", "Display name for the new project (derived from filename if omitted)").action((opts) => {
|
|
1144
1145
|
cmdPromote(getDb(), opts);
|
|
1145
1146
|
});
|
|
1146
|
-
|
|
1147
|
+
projectsCmd.command("go <query>").description("Print the root path for a project by slug, partial name, or fuzzy match.\nDesigned for shell integration: cd $(pai projects go <query>)").action((query) => {
|
|
1147
1148
|
cmdGo(getDb(), query);
|
|
1148
1149
|
});
|
|
1149
|
-
|
|
1150
|
+
projectsCmd.command("name <identifier> <shortname>").description("Give a project a short name for quick access").option("--permission <level>", "Permission level: full | trusted | default").action((identifier, shortname, opts) => {
|
|
1150
1151
|
cmdName(getDb(), identifier, shortname, opts);
|
|
1151
1152
|
});
|
|
1152
|
-
|
|
1153
|
+
projectsCmd.command("unname <shortname>").description("Remove a project's short name").action((shortname) => {
|
|
1153
1154
|
cmdUnname(getDb(), shortname);
|
|
1154
1155
|
});
|
|
1155
|
-
|
|
1156
|
+
projectsCmd.command("names").description("List named projects (your curated shortlist)").option("--json", "Output JSON for AIBroker consumption").action((opts) => {
|
|
1156
1157
|
cmdNames(getDb(), opts);
|
|
1157
1158
|
});
|
|
1158
|
-
|
|
1159
|
+
projectsCmd.command("config [identifier]").description("View or modify session launch config for a project.\nUse --options to discover available keys and presets.").option("--set <key=value...>", "Set config values (repeatable)", (v, prev) => [...prev, v], []).option("--unset <key...>", "Remove config keys (repeatable, use env.KEY for env vars)", (v, prev) => [...prev, v], []).option("--preset <name>", "Apply a permission preset: full | trusted | default").option("--defaults", "Manage global session defaults instead of a project").option("--options", "List available config keys and presets").option("--json", "Output JSON").option("--reset", "Reset config to empty (inherit global defaults)").action((identifier, opts) => {
|
|
1159
1160
|
cmdConfig(getDb(), identifier, opts);
|
|
1160
1161
|
});
|
|
1161
1162
|
}
|
|
@@ -1682,10 +1683,10 @@ function cmdActive(db, opts) {
|
|
|
1682
1683
|
], rows));
|
|
1683
1684
|
}
|
|
1684
1685
|
async function cmdAutoRoute(opts) {
|
|
1685
|
-
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-
|
|
1686
|
+
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-D54Kv7yI.mjs");
|
|
1686
1687
|
const { openRegistry } = await import("../db-BtuN768f.mjs").then((n) => n.t);
|
|
1687
|
-
const { createStorageBackend } = await import("../factory-
|
|
1688
|
-
const { loadConfig } = await import("../config-
|
|
1688
|
+
const { createStorageBackend } = await import("../factory-BufouUQ1.mjs").then((n) => n.n);
|
|
1689
|
+
const { loadConfig } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
|
|
1689
1690
|
const config = loadConfig();
|
|
1690
1691
|
const registryDb = openRegistry();
|
|
1691
1692
|
const federation = await createStorageBackend(config);
|
|
@@ -1817,14 +1818,14 @@ function cmdCheckpoint(message, opts) {
|
|
|
1817
1818
|
|
|
1818
1819
|
//#endregion
|
|
1819
1820
|
//#region src/cli/commands/session/handover.ts
|
|
1820
|
-
const HANDOVER_TODO_LOCATIONS = [
|
|
1821
|
+
const HANDOVER_TODO_LOCATIONS$1 = [
|
|
1821
1822
|
"Notes/TODO.md",
|
|
1822
1823
|
".claude/Notes/TODO.md",
|
|
1823
1824
|
"tasks/todo.md",
|
|
1824
1825
|
"TODO.md"
|
|
1825
1826
|
];
|
|
1826
|
-
function findProjectTodo(rootPath) {
|
|
1827
|
-
for (const rel of HANDOVER_TODO_LOCATIONS) {
|
|
1827
|
+
function findProjectTodo$1(rootPath) {
|
|
1828
|
+
for (const rel of HANDOVER_TODO_LOCATIONS$1) {
|
|
1828
1829
|
const full = join(rootPath, rel);
|
|
1829
1830
|
if (existsSync(full)) try {
|
|
1830
1831
|
return {
|
|
@@ -1835,7 +1836,7 @@ function findProjectTodo(rootPath) {
|
|
|
1835
1836
|
}
|
|
1836
1837
|
return null;
|
|
1837
1838
|
}
|
|
1838
|
-
function stripContinueSection(content) {
|
|
1839
|
+
function stripContinueSection$1(content) {
|
|
1839
1840
|
const lines = content.split("\n");
|
|
1840
1841
|
const startIdx = lines.findIndex((l) => l.trim() === "## Continue");
|
|
1841
1842
|
if (startIdx === -1) return content;
|
|
@@ -1876,7 +1877,7 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
|
|
|
1876
1877
|
const num = parseInt(nol, 10);
|
|
1877
1878
|
if (!isNaN(num)) session = db.prepare("SELECT * FROM sessions WHERE project_id = ? AND number = ?").get(project.id, num);
|
|
1878
1879
|
}
|
|
1879
|
-
const todo = findProjectTodo(project.root_path);
|
|
1880
|
+
const todo = findProjectTodo$1(project.root_path);
|
|
1880
1881
|
let todoPath;
|
|
1881
1882
|
let existingContent;
|
|
1882
1883
|
if (todo) {
|
|
@@ -1910,7 +1911,7 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
|
|
|
1910
1911
|
"",
|
|
1911
1912
|
"---",
|
|
1912
1913
|
""
|
|
1913
|
-
].join("\n") + stripContinueSection(existingContent).trimStart();
|
|
1914
|
+
].join("\n") + stripContinueSection$1(existingContent).trimStart();
|
|
1914
1915
|
const tmpPath = `${todoPath}.handover.tmp`;
|
|
1915
1916
|
try {
|
|
1916
1917
|
writeFileSync(tmpPath, newContent, "utf8");
|
|
@@ -1925,36 +1926,689 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
|
|
|
1925
1926
|
}
|
|
1926
1927
|
|
|
1927
1928
|
//#endregion
|
|
1928
|
-
//#region src/cli/
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1929
|
+
//#region src/cli/lib/session-scan.ts
|
|
1930
|
+
/**
|
|
1931
|
+
* Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .
|
|
1932
|
+
*
|
|
1933
|
+
* Storage layout (Claude Code v2.1.143+):
|
|
1934
|
+
*
|
|
1935
|
+
* <project>/<uuid>.jsonl — top-level: metadata, system snapshots, hook events.
|
|
1936
|
+
* REQUIRED for claude --resume to work.
|
|
1937
|
+
* <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.
|
|
1938
|
+
* 3000+ files, almost none resumable on their own.
|
|
1939
|
+
*
|
|
1940
|
+
* Resumability rule (empirically verified):
|
|
1941
|
+
* A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one
|
|
1942
|
+
* line of type "system". Sessions that only have a sessions/ counterpart cannot be
|
|
1943
|
+
* resumed by Claude Code regardless of how much transcript content they have.
|
|
1944
|
+
*
|
|
1945
|
+
* Scan strategy:
|
|
1946
|
+
* 1. Walk top-level <project>/<uuid>.jsonl files (fast: ~20 files total).
|
|
1947
|
+
* 2. For each, check system line count to set the resumable flag.
|
|
1948
|
+
* 3. Join with sessions/<uuid>.jsonl (if present) for user lines, last prompt, ai-title.
|
|
1949
|
+
* 4. Enrich with clc name registry and PAI registry root path.
|
|
1950
|
+
*
|
|
1951
|
+
* Used by: pai session recent, pai session resume
|
|
1952
|
+
*/
|
|
1953
|
+
const CLC_SESSIONS_FILE = join(homedir(), ".claude", "session.json");
|
|
1954
|
+
/** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */
|
|
1955
|
+
function buildClcInfoMap() {
|
|
1956
|
+
const map = /* @__PURE__ */ new Map();
|
|
1957
|
+
try {
|
|
1958
|
+
const raw = readFileSync(CLC_SESSIONS_FILE, "utf8");
|
|
1959
|
+
const data = JSON.parse(raw);
|
|
1960
|
+
for (const entry of data.sessions ?? []) {
|
|
1961
|
+
const name = entry.name?.trim();
|
|
1962
|
+
const uuid = (entry.resume ?? entry.session ?? "").trim();
|
|
1963
|
+
if (name && uuid) map.set(uuid, {
|
|
1964
|
+
name,
|
|
1965
|
+
directory: entry.directory?.trim() || void 0
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
} catch {}
|
|
1969
|
+
return map;
|
|
1970
|
+
}
|
|
1971
|
+
/** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */
|
|
1972
|
+
function buildRegistryRootPathMap(db) {
|
|
1973
|
+
try {
|
|
1974
|
+
const rows = db.prepare(`SELECT root_path, encoded_dir FROM projects
|
|
1975
|
+
WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`).all();
|
|
1976
|
+
const map = /* @__PURE__ */ new Map();
|
|
1977
|
+
for (const row of rows) map.set(row.encoded_dir, row.root_path);
|
|
1978
|
+
return map;
|
|
1979
|
+
} catch {
|
|
1980
|
+
return /* @__PURE__ */ new Map();
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function parseTopLevel(filePath) {
|
|
1984
|
+
let systemLines = 0;
|
|
1985
|
+
let size = 0;
|
|
1986
|
+
let mtime = 0;
|
|
1987
|
+
try {
|
|
1988
|
+
const st = statSync(filePath);
|
|
1989
|
+
size = st.size;
|
|
1990
|
+
mtime = st.mtimeMs;
|
|
1991
|
+
const content = readFileSync(filePath, "utf8");
|
|
1992
|
+
for (const line of content.split("\n")) {
|
|
1993
|
+
const t = line.trim();
|
|
1994
|
+
if (!t) continue;
|
|
1995
|
+
if (t.includes("\"type\":\"system\"") || t.includes("\"type\": \"system\"")) systemLines++;
|
|
1996
|
+
}
|
|
1997
|
+
} catch {}
|
|
1998
|
+
return {
|
|
1999
|
+
systemLines,
|
|
2000
|
+
size,
|
|
2001
|
+
mtime
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function parseTranscript(filePath) {
|
|
2005
|
+
let userLines = 0;
|
|
2006
|
+
let lastUserPrompt = "";
|
|
2007
|
+
let msgCount = 0;
|
|
2008
|
+
let aiTitle;
|
|
2009
|
+
let mtime = 0;
|
|
2010
|
+
try {
|
|
2011
|
+
mtime = statSync(filePath).mtimeMs;
|
|
2012
|
+
} catch {
|
|
2013
|
+
return {
|
|
2014
|
+
userLines: 0,
|
|
2015
|
+
lastUserPrompt: "",
|
|
2016
|
+
msgCount: 0,
|
|
2017
|
+
mtime: 0
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
let content;
|
|
2021
|
+
try {
|
|
2022
|
+
content = readFileSync(filePath, "utf8");
|
|
2023
|
+
} catch {
|
|
2024
|
+
return {
|
|
2025
|
+
userLines: 0,
|
|
2026
|
+
lastUserPrompt: "",
|
|
2027
|
+
msgCount: 0,
|
|
2028
|
+
mtime
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
for (const line of content.split("\n")) {
|
|
2032
|
+
const t = line.trim();
|
|
2033
|
+
if (!t) continue;
|
|
2034
|
+
msgCount++;
|
|
2035
|
+
const hasUser = t.includes("\"type\":\"user\"") || t.includes("\"type\": \"user\"");
|
|
2036
|
+
const hasTitle = t.includes("\"type\":\"ai-title\"") || t.includes("\"type\": \"ai-title\"");
|
|
2037
|
+
if (!hasUser && !hasTitle) continue;
|
|
2038
|
+
let parsed;
|
|
2039
|
+
try {
|
|
2040
|
+
parsed = JSON.parse(t);
|
|
2041
|
+
} catch {
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
if (parsed.type === "ai-title") {
|
|
2045
|
+
const v = parsed.title ?? parsed.name;
|
|
2046
|
+
if (typeof v === "string" && v.trim()) aiTitle = v.trim();
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
if (parsed.type !== "user") continue;
|
|
2050
|
+
userLines++;
|
|
2051
|
+
const msg = parsed.message;
|
|
2052
|
+
if (!msg) continue;
|
|
2053
|
+
const c = msg.content;
|
|
2054
|
+
let text = "";
|
|
2055
|
+
if (typeof c === "string") text = c;
|
|
2056
|
+
else if (Array.isArray(c) && c.length > 0) {
|
|
2057
|
+
const first = c[0];
|
|
2058
|
+
if (typeof first.text === "string") text = first.text;
|
|
2059
|
+
}
|
|
2060
|
+
if (text) lastUserPrompt = text.slice(0, 80).replace(/\n/g, " ");
|
|
2061
|
+
}
|
|
2062
|
+
return {
|
|
2063
|
+
userLines,
|
|
2064
|
+
lastUserPrompt,
|
|
2065
|
+
msgCount,
|
|
2066
|
+
aiTitle,
|
|
2067
|
+
mtime
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
2071
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2072
|
+
/**
|
|
2073
|
+
* Compute the effective filter mode, resolving the legacy resumableOnly flag.
|
|
2074
|
+
*/
|
|
2075
|
+
function resolveFilter(opts) {
|
|
2076
|
+
if (opts.filter) return opts.filter;
|
|
2077
|
+
if (opts.resumableOnly === true) return "resumable";
|
|
2078
|
+
if (opts.resumableOnly === false) return "all";
|
|
2079
|
+
return "named";
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Scan ~/.claude/projects/ for all Claude Code sessions.
|
|
2083
|
+
*
|
|
2084
|
+
* Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).
|
|
2085
|
+
* Pass 2: add clc registry entries that have no top-level jsonl (transcript-only).
|
|
2086
|
+
*
|
|
2087
|
+
* Filter modes:
|
|
2088
|
+
* "named" — resumable + registry-known stubs + transcript-only (default)
|
|
2089
|
+
* "all" — all of the above + unnamed orphans from sessions/ subdirs
|
|
2090
|
+
* "resumable" — only sessions claude --resume accepts
|
|
2091
|
+
*
|
|
2092
|
+
* Returns results sorted by mtime descending.
|
|
2093
|
+
*/
|
|
2094
|
+
function scanSessions(db, opts = {}) {
|
|
2095
|
+
const limit = opts.limit ?? 200;
|
|
2096
|
+
const filterMode = resolveFilter(opts);
|
|
2097
|
+
if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];
|
|
2098
|
+
const clcInfoMap = buildClcInfoMap();
|
|
2099
|
+
const rootPathMap = buildRegistryRootPathMap(db);
|
|
2100
|
+
const results = [];
|
|
2101
|
+
const seenUuids = /* @__PURE__ */ new Set();
|
|
2102
|
+
let encodedDirs;
|
|
2103
|
+
try {
|
|
2104
|
+
encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);
|
|
2105
|
+
} catch {
|
|
2106
|
+
return [];
|
|
2107
|
+
}
|
|
2108
|
+
for (const encodedDir of encodedDirs) {
|
|
2109
|
+
const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);
|
|
2110
|
+
try {
|
|
2111
|
+
if (!statSync(projectDir).isDirectory()) continue;
|
|
2112
|
+
} catch {
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
let files;
|
|
2116
|
+
try {
|
|
2117
|
+
files = readdirSync(projectDir);
|
|
2118
|
+
} catch {
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, "/");
|
|
2122
|
+
const registryRootPath = rootPathMap.get(encodedDir);
|
|
2123
|
+
const projectBasename = basename(decodedPath);
|
|
2124
|
+
for (const file of files) {
|
|
2125
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
2126
|
+
const uuid = file.slice(0, -6);
|
|
2127
|
+
if (!UUID_RE.test(uuid)) continue;
|
|
2128
|
+
const topLevelPath = join(projectDir, file);
|
|
2129
|
+
const topInfo = parseTopLevel(topLevelPath);
|
|
2130
|
+
const resumable = topInfo.systemLines > 0;
|
|
2131
|
+
const clcInfo = clcInfoMap.get(uuid);
|
|
2132
|
+
const inRegistry = !!clcInfo;
|
|
2133
|
+
const sessionStatus = resumable ? "resumable" : inRegistry ? "stub" : "orphan";
|
|
2134
|
+
if (filterMode === "resumable" && !resumable) continue;
|
|
2135
|
+
if (filterMode === "named" && !resumable && !inRegistry) continue;
|
|
2136
|
+
const sessionJsonlPath = join(projectDir, "sessions", `${uuid}.jsonl`);
|
|
2137
|
+
const hasTranscript = existsSync(sessionJsonlPath);
|
|
2138
|
+
const transcript = hasTranscript ? parseTranscript(sessionJsonlPath) : {
|
|
2139
|
+
userLines: 0,
|
|
2140
|
+
lastUserPrompt: "",
|
|
2141
|
+
msgCount: 0,
|
|
2142
|
+
mtime: 0
|
|
2143
|
+
};
|
|
2144
|
+
const mtime = topInfo.mtime || transcript.mtime;
|
|
2145
|
+
const friendlyName = clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? void 0;
|
|
2146
|
+
seenUuids.add(uuid);
|
|
2147
|
+
results.push({
|
|
2148
|
+
uuid,
|
|
2149
|
+
shortId: uuid.slice(0, 8),
|
|
2150
|
+
encodedDir,
|
|
2151
|
+
decodedPath,
|
|
2152
|
+
topLevelPath,
|
|
2153
|
+
topLevelSystemLines: topInfo.systemLines,
|
|
2154
|
+
topLevelSize: topInfo.size,
|
|
2155
|
+
resumable,
|
|
2156
|
+
sessionStatus,
|
|
2157
|
+
sessionJsonlPath: hasTranscript ? sessionJsonlPath : void 0,
|
|
2158
|
+
userLines: transcript.userLines,
|
|
2159
|
+
lastUserPrompt: transcript.lastUserPrompt,
|
|
2160
|
+
msgCount: transcript.msgCount,
|
|
2161
|
+
aiTitle: transcript.aiTitle,
|
|
2162
|
+
mtime,
|
|
2163
|
+
friendlyName,
|
|
2164
|
+
clcDirectory: clcInfo?.directory,
|
|
2165
|
+
registryRootPath
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (filterMode !== "resumable") for (const [uuid, clcInfo] of clcInfoMap) {
|
|
2170
|
+
if (seenUuids.has(uuid)) continue;
|
|
2171
|
+
let foundEncodedDir;
|
|
2172
|
+
let foundTranscriptPath;
|
|
2173
|
+
if (clcInfo.directory) try {
|
|
2174
|
+
const real = realpathSyncSafe(clcInfo.directory);
|
|
2175
|
+
if (real) {
|
|
2176
|
+
const encoded = encodeProjectDir(real);
|
|
2177
|
+
const candidateTranscript = join(CLAUDE_PROJECTS_DIR, encoded, "sessions", `${uuid}.jsonl`);
|
|
2178
|
+
if (existsSync(candidateTranscript)) {
|
|
2179
|
+
foundEncodedDir = encoded;
|
|
2180
|
+
foundTranscriptPath = candidateTranscript;
|
|
2181
|
+
} else if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) foundEncodedDir = encoded;
|
|
2182
|
+
}
|
|
2183
|
+
} catch {}
|
|
2184
|
+
if (!foundEncodedDir) for (const encodedDir of encodedDirs) {
|
|
2185
|
+
const candidateTranscript = join(CLAUDE_PROJECTS_DIR, encodedDir, "sessions", `${uuid}.jsonl`);
|
|
2186
|
+
if (existsSync(candidateTranscript)) {
|
|
2187
|
+
foundEncodedDir = encodedDir;
|
|
2188
|
+
foundTranscriptPath = candidateTranscript;
|
|
2189
|
+
break;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const encodedDir = foundEncodedDir ?? "";
|
|
2193
|
+
const decodedPath = clcInfo.directory ?? (foundEncodedDir ? smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, "/") : uuid);
|
|
2194
|
+
const registryRootPath = foundEncodedDir ? rootPathMap.get(foundEncodedDir) : void 0;
|
|
2195
|
+
const topLevelPath = foundEncodedDir ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, `${uuid}.jsonl`) : "";
|
|
2196
|
+
const transcript = foundTranscriptPath ? parseTranscript(foundTranscriptPath) : {
|
|
2197
|
+
userLines: 0,
|
|
2198
|
+
lastUserPrompt: "",
|
|
2199
|
+
msgCount: 0,
|
|
2200
|
+
mtime: 0
|
|
2201
|
+
};
|
|
2202
|
+
const mtime = transcript.mtime;
|
|
2203
|
+
seenUuids.add(uuid);
|
|
2204
|
+
results.push({
|
|
2205
|
+
uuid,
|
|
2206
|
+
shortId: uuid.slice(0, 8),
|
|
2207
|
+
encodedDir,
|
|
2208
|
+
decodedPath,
|
|
2209
|
+
topLevelPath,
|
|
2210
|
+
topLevelSystemLines: 0,
|
|
2211
|
+
topLevelSize: 0,
|
|
2212
|
+
resumable: false,
|
|
2213
|
+
sessionStatus: "transcript-only",
|
|
2214
|
+
sessionJsonlPath: foundTranscriptPath,
|
|
2215
|
+
userLines: transcript.userLines,
|
|
2216
|
+
lastUserPrompt: transcript.lastUserPrompt,
|
|
2217
|
+
msgCount: transcript.msgCount,
|
|
2218
|
+
aiTitle: transcript.aiTitle,
|
|
2219
|
+
mtime,
|
|
2220
|
+
friendlyName: clcInfo.name,
|
|
2221
|
+
clcDirectory: clcInfo.directory,
|
|
2222
|
+
registryRootPath
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
2226
|
+
return results.slice(0, limit);
|
|
2227
|
+
}
|
|
2228
|
+
/** Claude Code's project-dir encoding: replace / . - (space) with - */
|
|
2229
|
+
function encodeProjectDir(realPath) {
|
|
2230
|
+
return realPath.replace(/[/.\- ]/g, "-");
|
|
2231
|
+
}
|
|
2232
|
+
/** realpathSync that returns null instead of throwing */
|
|
2233
|
+
function realpathSyncSafe(p) {
|
|
2234
|
+
try {
|
|
2235
|
+
return realpathSync(p);
|
|
2236
|
+
} catch {
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
function fmtAge(mtime) {
|
|
2241
|
+
const diffMs = Date.now() - mtime;
|
|
2242
|
+
const diffMin = Math.floor(diffMs / 6e4);
|
|
2243
|
+
const diffHr = Math.floor(diffMs / 36e5);
|
|
2244
|
+
const diffDay = Math.floor(diffMs / 864e5);
|
|
2245
|
+
if (diffMin < 60) return `${diffMin}m`;
|
|
2246
|
+
if (diffHr < 24) return `${diffHr}h`;
|
|
2247
|
+
if (diffDay < 30) return `${diffDay}d`;
|
|
2248
|
+
return `${Math.floor(diffDay / 30)}mo`;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Resolve a name-or-id-or-prefix to a single ScannedSession.
|
|
2252
|
+
*
|
|
2253
|
+
* Comparisons are case-insensitive; stored casing is preserved in output.
|
|
2254
|
+
*
|
|
2255
|
+
* Priority:
|
|
2256
|
+
* 1. Exact case-insensitive match on friendlyName
|
|
2257
|
+
* 2. Partial case-insensitive match (contains)
|
|
2258
|
+
* 3. UUID prefix match
|
|
2259
|
+
*/
|
|
2260
|
+
function resolveSessionByNameOrId(sessions, query) {
|
|
2261
|
+
const qLower = query.toLowerCase().trim();
|
|
2262
|
+
const byExact = sessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower);
|
|
2263
|
+
if (byExact.length >= 1) return {
|
|
2264
|
+
session: byExact[0],
|
|
2265
|
+
friendlyName: byExact[0].friendlyName
|
|
2266
|
+
};
|
|
2267
|
+
const byPartial = sessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower));
|
|
2268
|
+
if (byPartial.length === 1) return {
|
|
2269
|
+
session: byPartial[0],
|
|
2270
|
+
friendlyName: byPartial[0].friendlyName
|
|
2271
|
+
};
|
|
2272
|
+
if (byPartial.length > 1) {
|
|
2273
|
+
const candidates = byPartial.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
|
|
2274
|
+
throw new Error(`Ambiguous name "${query}" — ${byPartial.length} matches:\n${candidates}\n\nBe more specific or use a UUID prefix.`);
|
|
2275
|
+
}
|
|
2276
|
+
const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));
|
|
2277
|
+
if (byUuid.length === 1) return {
|
|
2278
|
+
session: byUuid[0],
|
|
2279
|
+
friendlyName: byUuid[0].friendlyName
|
|
2280
|
+
};
|
|
2281
|
+
if (byUuid.length > 1) {
|
|
2282
|
+
const candidates = byUuid.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
|
|
2283
|
+
throw new Error(`UUID prefix "${query}" is ambiguous — ${byUuid.length} matches:\n${candidates}\n\nProvide more characters.`);
|
|
2284
|
+
}
|
|
2285
|
+
throw new Error(`No resumable session found matching "${query}".\n\nRun: pai session recent to list resumable sessions.\nRun: pai session recent --all to include transcript-only sessions.`);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
//#endregion
|
|
2289
|
+
//#region src/cli/commands/session/recent.ts
|
|
2290
|
+
function fmtSize(bytes) {
|
|
2291
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
2292
|
+
if (bytes < 1048576) return `${Math.round(bytes / 1024)}K`;
|
|
2293
|
+
return `${(bytes / 1048576).toFixed(1)}M`;
|
|
2294
|
+
}
|
|
2295
|
+
function fmtStatus(status) {
|
|
2296
|
+
switch (status) {
|
|
2297
|
+
case "resumable": return chalk.green("resumable");
|
|
2298
|
+
case "stub": return chalk.yellow("stub");
|
|
2299
|
+
case "transcript-only": return chalk.yellow("transcript-only");
|
|
2300
|
+
case "orphan": return chalk.dim("orphan");
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function cmdRecent(db, opts) {
|
|
2304
|
+
const limit = parseInt(opts.n ?? "20", 10);
|
|
2305
|
+
const includeAll = opts.all === true;
|
|
2306
|
+
const sessions = scanSessions(db, {
|
|
2307
|
+
limit,
|
|
2308
|
+
filter: includeAll ? "all" : "named"
|
|
2309
|
+
});
|
|
2310
|
+
if (sessions.length === 0) {
|
|
2311
|
+
if (includeAll) console.log(err("No sessions found in ~/.claude/projects/."));
|
|
2312
|
+
else console.log(err("No named sessions found.\n\n Named sessions appear when you have entries in ~/.claude/session.json\n (set via /Name inside Claude Code) or resumable top-level jsonl files.\n Run: pai session recent --all to list all sessions including unnamed orphans."));
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (opts.json) {
|
|
2316
|
+
const output = sessions.map((s, idx) => ({
|
|
2317
|
+
idx: idx + 1,
|
|
2318
|
+
uuid: s.uuid,
|
|
2319
|
+
shortId: s.shortId,
|
|
2320
|
+
resumable: s.resumable,
|
|
2321
|
+
sessionStatus: s.sessionStatus,
|
|
2322
|
+
age: fmtAge(s.mtime),
|
|
2323
|
+
mtime: s.mtime,
|
|
2324
|
+
name: s.friendlyName,
|
|
2325
|
+
lastUserPrompt: s.lastUserPrompt,
|
|
2326
|
+
userLines: s.userLines,
|
|
2327
|
+
msgCount: s.msgCount,
|
|
2328
|
+
topLevelSize: s.topLevelSize,
|
|
2329
|
+
decodedPath: s.decodedPath
|
|
2330
|
+
}));
|
|
2331
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
const title = includeAll ? "All Sessions (named + orphans)" : "Named Sessions";
|
|
2335
|
+
console.log("\n" + header(title) + "\n");
|
|
2336
|
+
const headers = [
|
|
2337
|
+
"#",
|
|
2338
|
+
"id",
|
|
2339
|
+
"age",
|
|
2340
|
+
"name / project",
|
|
2341
|
+
"last prompt",
|
|
2342
|
+
"msgs",
|
|
2343
|
+
"size",
|
|
2344
|
+
"status"
|
|
2345
|
+
];
|
|
2346
|
+
const rows = sessions.map((s, idx) => {
|
|
2347
|
+
const name = s.friendlyName ?? s.decodedPath;
|
|
2348
|
+
const prompt = s.lastUserPrompt ? chalk.dim(s.lastUserPrompt.length > 40 ? s.lastUserPrompt.slice(0, 39) + "…" : s.lastUserPrompt) : dim("—");
|
|
2349
|
+
return [
|
|
2350
|
+
chalk.dim(String(idx + 1)),
|
|
2351
|
+
chalk.cyan(s.shortId),
|
|
2352
|
+
dim(fmtAge(s.mtime)),
|
|
2353
|
+
name.length > 26 ? name.slice(0, 25) + "…" : name,
|
|
2354
|
+
prompt,
|
|
2355
|
+
s.userLines > 0 ? String(s.userLines) : dim("—"),
|
|
2356
|
+
dim(fmtSize(s.topLevelSize)),
|
|
2357
|
+
fmtStatus(s.sessionStatus)
|
|
2358
|
+
];
|
|
2359
|
+
});
|
|
2360
|
+
console.log(renderTable(headers, rows));
|
|
2361
|
+
console.log("\n" + dim("Go to a session: ") + chalk.white("pai resume <name>") + "\n" + (includeAll ? "" : dim("Show all sessions: ") + chalk.white("pai sessions --all") + "\n"));
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region src/cli/commands/session/goto.ts
|
|
2366
|
+
function cmdGoto(db, query, opts) {
|
|
2367
|
+
const allSessions = scanSessions(db, {
|
|
2368
|
+
limit: 500,
|
|
2369
|
+
filter: "named"
|
|
2370
|
+
});
|
|
2371
|
+
let resolved;
|
|
2372
|
+
try {
|
|
2373
|
+
resolved = resolveSessionByNameOrId(allSessions, query);
|
|
2374
|
+
} catch (resolveErr) {
|
|
2375
|
+
console.error(err(String(resolveErr).replace(/^Error: /, "")));
|
|
2376
|
+
process.exit(1);
|
|
2377
|
+
}
|
|
2378
|
+
const { session: matchedSession, friendlyName } = resolved;
|
|
2379
|
+
let resumableUuid;
|
|
2380
|
+
let resumableSession;
|
|
2381
|
+
if (matchedSession.resumable) {
|
|
2382
|
+
resumableUuid = matchedSession.uuid;
|
|
2383
|
+
resumableSession = matchedSession;
|
|
2384
|
+
} else if (matchedSession.encodedDir) {
|
|
2385
|
+
const sameProject = allSessions.filter((s) => s.encodedDir === matchedSession.encodedDir && s.resumable);
|
|
2386
|
+
sameProject.sort((a, b) => b.mtime - a.mtime);
|
|
2387
|
+
if (sameProject.length > 0) {
|
|
2388
|
+
resumableSession = sameProject[0];
|
|
2389
|
+
resumableUuid = resumableSession.uuid;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
const rawDir = matchedSession.clcDirectory ?? matchedSession.registryRootPath ?? matchedSession.decodedPath;
|
|
2393
|
+
let projectDir;
|
|
2394
|
+
try {
|
|
2395
|
+
projectDir = realpathSync(rawDir);
|
|
2396
|
+
} catch {
|
|
2397
|
+
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.`));
|
|
2398
|
+
process.exit(1);
|
|
2399
|
+
}
|
|
2400
|
+
const useName = !opts.noName && !!friendlyName;
|
|
2401
|
+
const useGo = !opts.noGo;
|
|
2402
|
+
let promptArg = null;
|
|
2403
|
+
if (useName && friendlyName) promptArg = useGo ? `/Name ${friendlyName}\ngo` : `/Name ${friendlyName}`;
|
|
2404
|
+
else if (useGo) promptArg = "go";
|
|
2405
|
+
const fullArgv = resumableUuid ? [
|
|
2406
|
+
"claude",
|
|
2407
|
+
"--resume",
|
|
2408
|
+
resumableUuid,
|
|
2409
|
+
...promptArg ? [promptArg] : []
|
|
2410
|
+
] : ["claude", ...promptArg ? [promptArg] : []];
|
|
2411
|
+
if (opts.dryRun) {
|
|
2412
|
+
const mode = resumableUuid ? "RESUME" : "FRESH";
|
|
2413
|
+
console.log("\n" + chalk.bold(`Dry run — would execute (${mode}):`) + "\n");
|
|
2414
|
+
console.log(` cwd: ${chalk.cyan(projectDir)}`);
|
|
2415
|
+
const argDisplay = fullArgv.map((a) => a.includes("\n") ? `"${a.replace(/\n/g, "\\n")}"` : a.startsWith("/Name") ? `"${a}"` : a).join(" ");
|
|
2416
|
+
console.log(` argv: ${chalk.white(argDisplay)}`);
|
|
2417
|
+
if (useName && friendlyName) console.log(` name: ${chalk.green(friendlyName)}`);
|
|
2418
|
+
if (resumableSession) {
|
|
2419
|
+
console.log(`\n session: ${resumableSession.uuid}`);
|
|
2420
|
+
console.log(` age: ${fmtAge(resumableSession.mtime)}`);
|
|
2421
|
+
console.log(` system lines: ${resumableSession.topLevelSystemLines}`);
|
|
2422
|
+
console.log(` user msgs: ${resumableSession.userLines > 0 ? resumableSession.userLines : "—"}`);
|
|
2423
|
+
} else console.log(`\n mode: ${chalk.yellow("fresh session (no resumable snapshot)")}`);
|
|
2424
|
+
console.log(` project path: ${matchedSession.decodedPath}`);
|
|
2425
|
+
console.log();
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
const result = spawnSync(fullArgv[0], fullArgv.slice(1), {
|
|
2429
|
+
cwd: projectDir,
|
|
2430
|
+
stdio: "inherit",
|
|
2431
|
+
env: process.env
|
|
1932
2432
|
});
|
|
1933
|
-
|
|
2433
|
+
if (result.error) {
|
|
2434
|
+
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
2435
|
+
process.exit(1);
|
|
2436
|
+
}
|
|
2437
|
+
process.exit(result.status ?? 0);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
//#endregion
|
|
2441
|
+
//#region src/cli/commands/session/pause.ts
|
|
2442
|
+
const HANDOVER_TODO_LOCATIONS = [
|
|
2443
|
+
"Notes/TODO.md",
|
|
2444
|
+
".claude/Notes/TODO.md",
|
|
2445
|
+
"tasks/todo.md",
|
|
2446
|
+
"TODO.md"
|
|
2447
|
+
];
|
|
2448
|
+
function findProjectTodo(rootPath) {
|
|
2449
|
+
for (const rel of HANDOVER_TODO_LOCATIONS) {
|
|
2450
|
+
const full = join(rootPath, rel);
|
|
2451
|
+
if (existsSync(full)) try {
|
|
2452
|
+
return {
|
|
2453
|
+
path: full,
|
|
2454
|
+
content: readFileSync(full, "utf8")
|
|
2455
|
+
};
|
|
2456
|
+
} catch {}
|
|
2457
|
+
}
|
|
2458
|
+
return null;
|
|
2459
|
+
}
|
|
2460
|
+
function stripContinueSection(content) {
|
|
2461
|
+
const lines = content.split("\n");
|
|
2462
|
+
const startIdx = lines.findIndex((l) => l.trim() === "## Continue");
|
|
2463
|
+
if (startIdx === -1) return content;
|
|
2464
|
+
let endIdx = lines.length;
|
|
2465
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
2466
|
+
const trimmed = lines[i].trim();
|
|
2467
|
+
if (trimmed === "---" || trimmed.startsWith("##") && trimmed !== "## Continue") {
|
|
2468
|
+
endIdx = i;
|
|
2469
|
+
break;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
let trailingEnd = endIdx;
|
|
2473
|
+
if (trailingEnd < lines.length && lines[trailingEnd].trim() === "---") trailingEnd += 1;
|
|
2474
|
+
const before = lines.slice(0, startIdx);
|
|
2475
|
+
const after = lines.slice(trailingEnd);
|
|
2476
|
+
while (after.length > 0 && after[0].trim() === "") after.shift();
|
|
2477
|
+
return [...before, ...after].join("\n");
|
|
2478
|
+
}
|
|
2479
|
+
function cmdPause(db, opts) {
|
|
2480
|
+
const cwd = process.cwd();
|
|
2481
|
+
const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
|
|
2482
|
+
FROM projects
|
|
2483
|
+
WHERE ? LIKE root_path || '%'
|
|
2484
|
+
ORDER BY length(root_path) DESC
|
|
2485
|
+
LIMIT 1`).get(cwd);
|
|
2486
|
+
if (!project) {
|
|
2487
|
+
console.error(chalk.red("pai session pause: ") + `Current directory is not within a registered PAI project.
|
|
2488
|
+
cwd: ${cwd}`);
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
const session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
|
|
2492
|
+
const todo = findProjectTodo(project.root_path);
|
|
2493
|
+
let todoPath;
|
|
2494
|
+
let existingContent;
|
|
2495
|
+
if (todo) {
|
|
2496
|
+
todoPath = todo.path;
|
|
2497
|
+
existingContent = todo.content;
|
|
2498
|
+
} else {
|
|
2499
|
+
const notesDir = join(project.root_path, "Notes");
|
|
2500
|
+
if (!opts.dryRun) try {
|
|
2501
|
+
if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });
|
|
2502
|
+
} catch (mkdirErr) {
|
|
2503
|
+
console.error(chalk.red("pai session pause: ") + `Could not create Notes directory: ${String(mkdirErr)}`);
|
|
2504
|
+
process.exit(1);
|
|
2505
|
+
}
|
|
2506
|
+
todoPath = join(notesDir, "TODO.md");
|
|
2507
|
+
existingContent = "";
|
|
2508
|
+
}
|
|
2509
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2510
|
+
let sessionLine;
|
|
2511
|
+
if (session) {
|
|
2512
|
+
const num = String(session.number).padStart(4, "0");
|
|
2513
|
+
const titlePart = session.title || session.slug || "Session";
|
|
2514
|
+
sessionLine = `${num} - ${session.date} - ${titlePart}`;
|
|
2515
|
+
} else sessionLine = "Unknown session";
|
|
2516
|
+
const continueBlock = [
|
|
2517
|
+
"## Continue",
|
|
2518
|
+
"",
|
|
2519
|
+
`> **Last session:** ${sessionLine}`,
|
|
2520
|
+
`> **Paused at:** ${timestamp}`,
|
|
2521
|
+
">",
|
|
2522
|
+
`> Working directory: ${cwd}. Check the latest session note for details.`,
|
|
2523
|
+
"",
|
|
2524
|
+
"---",
|
|
2525
|
+
""
|
|
2526
|
+
].join("\n");
|
|
2527
|
+
const newContent = continueBlock + stripContinueSection(existingContent).trimStart();
|
|
2528
|
+
if (opts.dryRun) {
|
|
2529
|
+
console.log("\n" + chalk.bold("Dry run — would write to:") + " " + chalk.cyan(todoPath));
|
|
2530
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
2531
|
+
console.log(chalk.dim(continueBlock));
|
|
2532
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
2533
|
+
} else {
|
|
2534
|
+
const tmpPath = `${todoPath}.pause.tmp`;
|
|
2535
|
+
try {
|
|
2536
|
+
writeFileSync(tmpPath, newContent, "utf8");
|
|
2537
|
+
renameSync(tmpPath, todoPath);
|
|
2538
|
+
} catch (writeErr) {
|
|
2539
|
+
try {
|
|
2540
|
+
if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
|
|
2541
|
+
} catch {}
|
|
2542
|
+
console.error(chalk.red("pai session pause: ") + `Failed to write TODO.md: ${String(writeErr)}`);
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
console.log(chalk.green(" ## Continue written to: ") + chalk.cyan(todoPath));
|
|
2546
|
+
}
|
|
2547
|
+
const box = [
|
|
2548
|
+
"",
|
|
2549
|
+
chalk.bgRed.white.bold(" IMPORTANT: How to exit safely "),
|
|
2550
|
+
"",
|
|
2551
|
+
chalk.yellow(" Inside the Claude Code session, type: " + chalk.white.bold("/exit") + chalk.yellow(" (then press Enter)")),
|
|
2552
|
+
"",
|
|
2553
|
+
chalk.red.bold(" DO NOT press Ctrl+C."),
|
|
2554
|
+
"",
|
|
2555
|
+
chalk.dim(" Ctrl+C bypasses PAI's stop-hook, which means:"),
|
|
2556
|
+
chalk.dim(" - No project snapshot is written to the top-level jsonl"),
|
|
2557
|
+
chalk.dim(" - The session becomes orphaned (cannot --resume next time)"),
|
|
2558
|
+
chalk.dim(" - pai session goto will launch a fresh session instead of resuming"),
|
|
2559
|
+
"",
|
|
2560
|
+
chalk.dim(" The ## Continue checkpoint in TODO.md is already saved."),
|
|
2561
|
+
chalk.dim(" Use /exit to preserve full session resumability."),
|
|
2562
|
+
""
|
|
2563
|
+
].join("\n");
|
|
2564
|
+
console.log(box);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
//#endregion
|
|
2568
|
+
//#region src/cli/commands/session/sessions-index.ts
|
|
2569
|
+
function registerSessionsCommands(sessionsCmd, getDb) {
|
|
2570
|
+
sessionsCmd.command("list", { isDefault: true }).description("Resumable sessions catalog — named sessions with resume status.\nShort form: pai sessions (bare, no subcommand)\nUse --all to also show unnamed orphan sessions.").option("-n <count>", "Maximum sessions to show (default: 20)", "20").option("--all", "Include unnamed orphan sessions (not in clc registry)").option("--json", "Output raw JSON instead of formatted table").action((opts) => {
|
|
2571
|
+
cmdRecent(getDb(), opts);
|
|
2572
|
+
});
|
|
2573
|
+
sessionsCmd.command("goto <name-or-id>").description("Go to a session: resume if a resumable snapshot exists, start fresh otherwise.\nRecommended short form: pai resume <name>\nResolves by clc/registry name (case-insensitive) or UUID prefix.").option("--skip-name", "Do not prepend /Name to restore the session name").option("--skip-go", "Do not append \\ngo to trigger PAI auto-resume").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((nameOrId, opts) => {
|
|
2574
|
+
cmdGoto(getDb(), nameOrId, {
|
|
2575
|
+
noName: opts.skipName,
|
|
2576
|
+
noGo: opts.skipGo,
|
|
2577
|
+
dryRun: opts.dryRun
|
|
2578
|
+
});
|
|
2579
|
+
});
|
|
2580
|
+
sessionsCmd.command("pause").description("Write a ## Continue checkpoint to the project's TODO.md.\nRecommended short form: pai pause\nUse /exit inside Claude Code to preserve full session resumability.").option("--dry-run", "Preview the ## Continue block without writing it").action((opts) => {
|
|
2581
|
+
cmdPause(getDb(), opts);
|
|
2582
|
+
});
|
|
2583
|
+
sessionsCmd.command("recent").description("[Deprecated] Use `pai sessions list` instead.").option("-n <count>", "Maximum sessions to show (default: 20)", "20").option("--all", "Include unnamed orphan sessions (not in clc registry)").option("--json", "Output raw JSON instead of formatted table").action((opts) => {
|
|
2584
|
+
process.stderr.write("Deprecated: `pai sessions recent` — use `pai sessions list` instead.\n");
|
|
2585
|
+
cmdRecent(getDb(), opts);
|
|
2586
|
+
});
|
|
2587
|
+
sessionsCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
|
|
1934
2588
|
cmdInfo(getDb(), projectSlug, number);
|
|
1935
2589
|
});
|
|
1936
|
-
|
|
2590
|
+
sessionsCmd.command("rename <project-slug> <number> <new-slug>").description("Rename a session note — updates file on disk, H1 title, and registry").action((projectSlug, number, newSlug) => {
|
|
1937
2591
|
cmdRename(getDb(), projectSlug, number, newSlug);
|
|
1938
2592
|
});
|
|
1939
|
-
|
|
2593
|
+
sessionsCmd.command("slug <project-slug> <number>").description("Generate a descriptive slug from the session JSONL transcript").option("--apply", "Rename the session note using the generated slug").action((projectSlug, number, opts) => {
|
|
1940
2594
|
cmdSlug(getDb(), projectSlug, number, opts);
|
|
1941
2595
|
});
|
|
1942
|
-
|
|
2596
|
+
sessionsCmd.command("tag <project-slug> <number> [tags...]").description("Set or show tags on a session.").action((projectSlug, number, tags) => {
|
|
1943
2597
|
cmdTag(getDb(), projectSlug, number, tags);
|
|
1944
2598
|
});
|
|
1945
|
-
|
|
2599
|
+
sessionsCmd.command("route <project-slug> <number> <target-project>").description("Create a cross-reference link from a session to a target project").option("--type <type>", "Link type: related | follow-up | reference", "related").action((projectSlug, number, targetProject, opts) => {
|
|
1946
2600
|
cmdRoute(getDb(), projectSlug, number, targetProject, opts);
|
|
1947
2601
|
});
|
|
1948
|
-
|
|
2602
|
+
sessionsCmd.command("handover [project-slug] [session-id]").description("Write a ## Continue section to the project's TODO.md.\nCalled automatically from session-stop and pre-compact hooks.").action((projectSlug, sessionId) => {
|
|
1949
2603
|
cmdHandover(getDb(), projectSlug, sessionId);
|
|
1950
2604
|
});
|
|
1951
|
-
|
|
2605
|
+
sessionsCmd.command("checkpoint <message>").description("Append a timestamped checkpoint to the active session note.\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) => {
|
|
1952
2606
|
cmdCheckpoint(message, opts);
|
|
1953
2607
|
});
|
|
1954
|
-
|
|
2608
|
+
sessionsCmd.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) => {
|
|
1955
2609
|
cmdActive(getDb(), opts);
|
|
1956
2610
|
});
|
|
1957
|
-
|
|
2611
|
+
sessionsCmd.command("auto-route").description("Auto-detect which project this session belongs to.\nTries: (1) path match in registry, (2) Notes/PAI.md marker walk, (3) topic detection.").option("--cwd <path>", "Working directory to detect from (default: process.cwd())").option("--context <text>", "Conversation context for topic-based fallback routing").option("--json", "Output raw JSON instead of formatted display").action(async (opts) => {
|
|
1958
2612
|
await cmdAutoRoute(opts);
|
|
1959
2613
|
});
|
|
1960
2614
|
}
|
|
@@ -2302,8 +2956,8 @@ function analyzeProject(db, project) {
|
|
|
2302
2956
|
async function countVectorDbPaths(oldPaths) {
|
|
2303
2957
|
if (oldPaths.length === 0) return 0;
|
|
2304
2958
|
try {
|
|
2305
|
-
const { loadConfig } = await import("../config-
|
|
2306
|
-
const { PostgresBackend } = await import("../postgres-
|
|
2959
|
+
const { loadConfig } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
|
|
2960
|
+
const { PostgresBackend } = await import("../postgres-Cp25xZes.mjs");
|
|
2307
2961
|
const config = loadConfig();
|
|
2308
2962
|
if (config.storageBackend !== "postgres") return 0;
|
|
2309
2963
|
const pgBackend = new PostgresBackend(config.postgres ?? {});
|
|
@@ -2323,8 +2977,8 @@ async function countVectorDbPaths(oldPaths) {
|
|
|
2323
2977
|
async function updateVectorDbPaths(moves) {
|
|
2324
2978
|
if (moves.length === 0) return 0;
|
|
2325
2979
|
try {
|
|
2326
|
-
const { loadConfig } = await import("../config-
|
|
2327
|
-
const { PostgresBackend } = await import("../postgres-
|
|
2980
|
+
const { loadConfig } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
|
|
2981
|
+
const { PostgresBackend } = await import("../postgres-Cp25xZes.mjs");
|
|
2328
2982
|
const config = loadConfig();
|
|
2329
2983
|
if (config.storageBackend !== "postgres") return 0;
|
|
2330
2984
|
const pgBackend = new PostgresBackend(config.postgres ?? {});
|
|
@@ -2647,269 +3301,6 @@ function registerSessionCleanupCommand(sessionCmd, getDb) {
|
|
|
2647
3301
|
});
|
|
2648
3302
|
}
|
|
2649
3303
|
|
|
2650
|
-
//#endregion
|
|
2651
|
-
//#region src/cli/commands/registry/utils.ts
|
|
2652
|
-
/**
|
|
2653
|
-
* Upsert a project row. Returns { id, isNew }.
|
|
2654
|
-
*
|
|
2655
|
-
* Matching priority:
|
|
2656
|
-
* 1. root_path — most reliable; handles slug collisions
|
|
2657
|
-
* 2. encoded_dir — Claude project dirs are canonical
|
|
2658
|
-
* 3. Insert with suffix-deduplication on slug collision
|
|
2659
|
-
*/
|
|
2660
|
-
function upsertProject(db, slug, rootPath, encodedDir) {
|
|
2661
|
-
const ts = now();
|
|
2662
|
-
const byPath = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath);
|
|
2663
|
-
if (byPath) {
|
|
2664
|
-
const encodedOwner = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(encodedDir);
|
|
2665
|
-
if (!encodedOwner || encodedOwner.id === byPath.id) db.prepare("UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?").run(encodedDir, ts, byPath.id);
|
|
2666
|
-
return {
|
|
2667
|
-
id: byPath.id,
|
|
2668
|
-
isNew: false
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
const byEncoded = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(encodedDir);
|
|
2672
|
-
if (byEncoded) {
|
|
2673
|
-
const pathOwner = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath);
|
|
2674
|
-
if (!pathOwner || pathOwner.id === byEncoded.id) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(rootPath, ts, byEncoded.id);
|
|
2675
|
-
return {
|
|
2676
|
-
id: byEncoded.id,
|
|
2677
|
-
isNew: false
|
|
2678
|
-
};
|
|
2679
|
-
}
|
|
2680
|
-
let finalSlug = slug;
|
|
2681
|
-
let attempt = 0;
|
|
2682
|
-
while (true) {
|
|
2683
|
-
if (!db.prepare("SELECT id FROM projects WHERE slug = ?").get(finalSlug)) break;
|
|
2684
|
-
attempt++;
|
|
2685
|
-
finalSlug = `${slug}-${attempt}`;
|
|
2686
|
-
}
|
|
2687
|
-
const result = db.prepare(`INSERT OR IGNORE INTO projects
|
|
2688
|
-
(slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)
|
|
2689
|
-
VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`).run(finalSlug, finalSlug, rootPath, encodedDir, ts, ts);
|
|
2690
|
-
if (result.changes === 0) {
|
|
2691
|
-
const fallback = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(encodedDir) ?? db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath);
|
|
2692
|
-
if (fallback) return {
|
|
2693
|
-
id: fallback.id,
|
|
2694
|
-
isNew: false
|
|
2695
|
-
};
|
|
2696
|
-
throw new Error(`upsertProject: INSERT OR IGNORE was suppressed but no matching row found for root_path=${rootPath} encoded_dir=${encodedDir}`);
|
|
2697
|
-
}
|
|
2698
|
-
return {
|
|
2699
|
-
id: result.lastInsertRowid,
|
|
2700
|
-
isNew: true
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
/** Upsert a session note. Returns true if newly inserted. */
|
|
2704
|
-
function upsertSession(db, projectId, number, date, slug, title, filename) {
|
|
2705
|
-
if (db.prepare("SELECT id FROM sessions WHERE project_id = ? AND number = ?").get(projectId, number)) return false;
|
|
2706
|
-
const ts = now();
|
|
2707
|
-
db.prepare(`INSERT INTO sessions
|
|
2708
|
-
(project_id, number, date, slug, title, filename, status, created_at)
|
|
2709
|
-
VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`).run(projectId, number, date, slug, title, filename, ts);
|
|
2710
|
-
return true;
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
//#endregion
|
|
2714
|
-
//#region src/cli/commands/registry/scan.ts
|
|
2715
|
-
/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */
|
|
2716
|
-
const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
2717
|
-
const PAI_CONFIG_DIR = join(homedir(), ".pai");
|
|
2718
|
-
const PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, "config.json");
|
|
2719
|
-
function loadScanConfig() {
|
|
2720
|
-
if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };
|
|
2721
|
-
try {
|
|
2722
|
-
return JSON.parse(readFileSync(PAI_CONFIG_FILE, "utf8"));
|
|
2723
|
-
} catch {
|
|
2724
|
-
return { scan_dirs: [] };
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
function saveScanConfig(config) {
|
|
2728
|
-
mkdirSync(PAI_CONFIG_DIR, { recursive: true });
|
|
2729
|
-
writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
2730
|
-
}
|
|
2731
|
-
function resolveHome(p) {
|
|
2732
|
-
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
2733
|
-
return resolve(p);
|
|
2734
|
-
}
|
|
2735
|
-
/**
|
|
2736
|
-
* Recursively find all .md files in a directory, including YYYY/MM subdirectories.
|
|
2737
|
-
* Returns filenames (basename only).
|
|
2738
|
-
*/
|
|
2739
|
-
function findNoteFiles(dir) {
|
|
2740
|
-
const results = [];
|
|
2741
|
-
if (!existsSync(dir)) return results;
|
|
2742
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isFile() && entry.name.endsWith(".md")) results.push(entry.name);
|
|
2743
|
-
else if (entry.isDirectory() && /^\d{4}$/.test(entry.name)) {
|
|
2744
|
-
const yearDir = join(dir, entry.name);
|
|
2745
|
-
for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) if (monthEntry.isDirectory() && /^\d{2}$/.test(monthEntry.name)) {
|
|
2746
|
-
const monthDir = join(yearDir, monthEntry.name);
|
|
2747
|
-
for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) if (noteEntry.isFile() && noteEntry.name.endsWith(".md")) results.push(noteEntry.name);
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
return results;
|
|
2751
|
-
}
|
|
2752
|
-
function performScan(db) {
|
|
2753
|
-
const result = {
|
|
2754
|
-
projectsScanned: 0,
|
|
2755
|
-
projectsNew: 0,
|
|
2756
|
-
projectsUpdated: 0,
|
|
2757
|
-
sessionsScanned: 0,
|
|
2758
|
-
sessionsNew: 0,
|
|
2759
|
-
skipped: []
|
|
2760
|
-
};
|
|
2761
|
-
if (!existsSync(CLAUDE_PROJECTS_DIR)) throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);
|
|
2762
|
-
const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {
|
|
2763
|
-
return statSync(join(CLAUDE_PROJECTS_DIR, name)).isDirectory();
|
|
2764
|
-
});
|
|
2765
|
-
const lookupMap = buildEncodedDirMap();
|
|
2766
|
-
for (const encodedDir of entries) {
|
|
2767
|
-
const rootPath = decodeEncodedDir(encodedDir, lookupMap);
|
|
2768
|
-
if (!existsSync(rootPath)) {
|
|
2769
|
-
result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);
|
|
2770
|
-
result.projectsScanned++;
|
|
2771
|
-
continue;
|
|
2772
|
-
}
|
|
2773
|
-
const slug = slugify$1(basename(rootPath) || encodedDir);
|
|
2774
|
-
const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);
|
|
2775
|
-
result.projectsScanned++;
|
|
2776
|
-
if (isNew) result.projectsNew++;
|
|
2777
|
-
else result.projectsUpdated++;
|
|
2778
|
-
try {
|
|
2779
|
-
ensurePaiMarker(rootPath, slug);
|
|
2780
|
-
} catch {}
|
|
2781
|
-
const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, "Notes");
|
|
2782
|
-
if (existsSync(claudeNotesDir)) {
|
|
2783
|
-
if (claudeNotesDir !== join(rootPath, "Notes")) db.prepare("UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?").run(claudeNotesDir, Date.now(), id);
|
|
2784
|
-
}
|
|
2785
|
-
if (!existsSync(claudeNotesDir)) continue;
|
|
2786
|
-
const noteFiles = findNoteFiles(claudeNotesDir);
|
|
2787
|
-
for (const filename of noteFiles) {
|
|
2788
|
-
const parsed = parseSessionFilename(filename);
|
|
2789
|
-
if (!parsed) continue;
|
|
2790
|
-
result.sessionsScanned++;
|
|
2791
|
-
if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
{
|
|
2795
|
-
const activeProjects = db.prepare("SELECT id, slug, root_path FROM projects WHERE status = 'active'").all();
|
|
2796
|
-
for (const project of activeProjects) {
|
|
2797
|
-
const notesDir = join(project.root_path, "Notes");
|
|
2798
|
-
if (!existsSync(notesDir)) continue;
|
|
2799
|
-
let files;
|
|
2800
|
-
try {
|
|
2801
|
-
files = findNoteFiles(notesDir);
|
|
2802
|
-
} catch {
|
|
2803
|
-
continue;
|
|
2804
|
-
}
|
|
2805
|
-
for (const filename of files) {
|
|
2806
|
-
const parsed = parseSessionFilename(filename);
|
|
2807
|
-
if (!parsed) continue;
|
|
2808
|
-
result.sessionsScanned++;
|
|
2809
|
-
if (upsertSession(db, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
const config = loadScanConfig();
|
|
2814
|
-
if (config.scan_dirs.length) for (const rawDir of config.scan_dirs) {
|
|
2815
|
-
const scanDir = resolveHome(rawDir);
|
|
2816
|
-
if (!existsSync(scanDir)) {
|
|
2817
|
-
result.skipped.push(`${rawDir} (configured scan_dir not found)`);
|
|
2818
|
-
continue;
|
|
2819
|
-
}
|
|
2820
|
-
const children = readdirSync(scanDir).filter((name) => {
|
|
2821
|
-
if (name.startsWith(".")) return false;
|
|
2822
|
-
const full = join(scanDir, name);
|
|
2823
|
-
try {
|
|
2824
|
-
return statSync(full).isDirectory();
|
|
2825
|
-
} catch {
|
|
2826
|
-
return false;
|
|
2827
|
-
}
|
|
2828
|
-
});
|
|
2829
|
-
for (const child of children) {
|
|
2830
|
-
const childPath = join(scanDir, child);
|
|
2831
|
-
const childSlug = slugify$1(child);
|
|
2832
|
-
const childEncoded = encodeDir(childPath);
|
|
2833
|
-
const existing = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(childPath);
|
|
2834
|
-
if (existing) {
|
|
2835
|
-
result.projectsScanned++;
|
|
2836
|
-
result.projectsUpdated++;
|
|
2837
|
-
try {
|
|
2838
|
-
ensurePaiMarker(childPath, childSlug);
|
|
2839
|
-
} catch {}
|
|
2840
|
-
const notesDir = join(childPath, "Notes");
|
|
2841
|
-
if (existsSync(notesDir)) {
|
|
2842
|
-
const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(".md"));
|
|
2843
|
-
for (const filename of noteFiles) {
|
|
2844
|
-
const parsed = parseSessionFilename(filename);
|
|
2845
|
-
if (!parsed) continue;
|
|
2846
|
-
result.sessionsScanned++;
|
|
2847
|
-
if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
continue;
|
|
2851
|
-
}
|
|
2852
|
-
const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);
|
|
2853
|
-
result.projectsScanned++;
|
|
2854
|
-
if (isNew) result.projectsNew++;
|
|
2855
|
-
else result.projectsUpdated++;
|
|
2856
|
-
try {
|
|
2857
|
-
ensurePaiMarker(childPath, childSlug);
|
|
2858
|
-
} catch {}
|
|
2859
|
-
const notesDir = join(childPath, "Notes");
|
|
2860
|
-
if (existsSync(notesDir)) {
|
|
2861
|
-
const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(".md"));
|
|
2862
|
-
for (const filename of noteFiles) {
|
|
2863
|
-
const parsed = parseSessionFilename(filename);
|
|
2864
|
-
if (!parsed) continue;
|
|
2865
|
-
result.sessionsScanned++;
|
|
2866
|
-
if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
if (config.scan_dirs.length) {
|
|
2872
|
-
const markers = discoverPaiMarkers(config.scan_dirs.map(resolveHome).filter(existsSync));
|
|
2873
|
-
for (const marker of markers) {
|
|
2874
|
-
const registeredRow = db.prepare("SELECT id, root_path, slug FROM projects WHERE slug = ?").get(marker.slug);
|
|
2875
|
-
if (!registeredRow) continue;
|
|
2876
|
-
if (registeredRow.root_path !== marker.projectRoot) {
|
|
2877
|
-
const newEncoded = encodeDir(marker.projectRoot);
|
|
2878
|
-
const now4 = Date.now();
|
|
2879
|
-
const encodedOwner = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(newEncoded);
|
|
2880
|
-
const pathOwner = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(marker.projectRoot);
|
|
2881
|
-
const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;
|
|
2882
|
-
const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;
|
|
2883
|
-
if (encodedSafe && pathSafe) db.prepare("UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?").run(marker.projectRoot, newEncoded, now4, registeredRow.id);
|
|
2884
|
-
else if (pathSafe) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(marker.projectRoot, now4, registeredRow.id);
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
return result;
|
|
2889
|
-
}
|
|
2890
|
-
function cmdScan(db) {
|
|
2891
|
-
const config = loadScanConfig();
|
|
2892
|
-
console.log(dim("Scanning ~/.claude/projects/ ..."));
|
|
2893
|
-
if (config.scan_dirs.length) console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(", ")}`));
|
|
2894
|
-
console.log(dim("Scanning project-root Notes/ directories ..."));
|
|
2895
|
-
let result;
|
|
2896
|
-
try {
|
|
2897
|
-
result = performScan(db);
|
|
2898
|
-
} catch (e) {
|
|
2899
|
-
console.error(err(String(e)));
|
|
2900
|
-
process.exit(1);
|
|
2901
|
-
}
|
|
2902
|
-
console.log(ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`));
|
|
2903
|
-
console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));
|
|
2904
|
-
console.log(dim(` Sessions: ${result.sessionsNew} new`));
|
|
2905
|
-
if (result.skipped.length) {
|
|
2906
|
-
console.log();
|
|
2907
|
-
console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));
|
|
2908
|
-
for (const s of result.skipped.slice(0, 10)) console.log(dim(` ${s}`));
|
|
2909
|
-
if (result.skipped.length > 10) console.log(dim(` ... and ${result.skipped.length - 10} more`));
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
3304
|
//#endregion
|
|
2914
3305
|
//#region src/cli/commands/registry/migrate.ts
|
|
2915
3306
|
/** Registry migrate command: import data from ~/.claude/session-registry.json. */
|
|
@@ -3015,7 +3406,7 @@ function cmdLookup(db, fsPath) {
|
|
|
3015
3406
|
process.stdout.write(row.slug + "\n");
|
|
3016
3407
|
}
|
|
3017
3408
|
function registerRegistryCommands(registryCmd, getDb) {
|
|
3018
|
-
registryCmd.command("scan").description("Walk ~/.claude/projects/ and configured scan_dirs, upsert all projects").option("--add-dir <path>", "Add a directory to scan_dirs config").option("--remove-dir <path>", "Remove a directory from scan_dirs config").option("--show-dirs", "Show currently configured scan directories").action((opts) => {
|
|
3409
|
+
registryCmd.command("scan").description("Walk ~/.claude/projects/ and configured scan_dirs, upsert all projects").option("--add-dir <path>", "Add a directory to scan_dirs config").option("--remove-dir <path>", "Remove a directory from scan_dirs config").option("--show-dirs", "Show currently configured scan directories").option("--quick", "Minimal output mode (used by hooks and daemon)").action((opts) => {
|
|
3019
3410
|
if (opts.showDirs) {
|
|
3020
3411
|
const config = loadScanConfig();
|
|
3021
3412
|
if (!config.scan_dirs.length) {
|
|
@@ -3053,7 +3444,7 @@ function registerRegistryCommands(registryCmd, getDb) {
|
|
|
3053
3444
|
console.log(ok(`Removed scan directory: ${bold(display)}`));
|
|
3054
3445
|
} else console.log(warn(`Not found in config: ${display}`));
|
|
3055
3446
|
}
|
|
3056
|
-
if (!opts.addDir && !opts.removeDir) cmdScan(getDb());
|
|
3447
|
+
if (!opts.addDir && !opts.removeDir) cmdScan(getDb(), { quick: opts.quick });
|
|
3057
3448
|
});
|
|
3058
3449
|
registryCmd.command("migrate").description("Import data from ~/.claude/session-registry.json").action(() => {
|
|
3059
3450
|
cmdMigrate$1(getDb());
|
|
@@ -3794,8 +4185,8 @@ function cmdLogs(opts) {
|
|
|
3794
4185
|
}
|
|
3795
4186
|
function registerDaemonCommands(daemonCmd) {
|
|
3796
4187
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
3797
|
-
const { serve } = await import("../daemon-
|
|
3798
|
-
const { loadConfig: lc, ensureConfigDir } = await import("../config-
|
|
4188
|
+
const { serve } = await import("../daemon-BryEC3as.mjs").then((n) => n.t);
|
|
4189
|
+
const { loadConfig: lc, ensureConfigDir } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
|
|
3799
4190
|
ensureConfigDir();
|
|
3800
4191
|
await serve(lc());
|
|
3801
4192
|
});
|
|
@@ -6416,7 +6807,7 @@ async function cmdExplore(note, opts) {
|
|
|
6416
6807
|
const depth = parseInt(opts.depth ?? "3", 10);
|
|
6417
6808
|
const direction = opts.direction ?? "both";
|
|
6418
6809
|
const mode = opts.mode ?? "all";
|
|
6419
|
-
const { zettelExplore } = await import("../zettelkasten-
|
|
6810
|
+
const { zettelExplore } = await import("../zettelkasten-B-94vghH.mjs");
|
|
6420
6811
|
const result = zettelExplore(getFedDb(), {
|
|
6421
6812
|
startNote: note,
|
|
6422
6813
|
depth,
|
|
@@ -6480,7 +6871,7 @@ async function cmdHealth(opts) {
|
|
|
6480
6871
|
const projectPath = opts.project;
|
|
6481
6872
|
const recentDays = parseInt(opts.days ?? "30", 10);
|
|
6482
6873
|
const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
|
|
6483
|
-
const { zettelHealth } = await import("../zettelkasten-
|
|
6874
|
+
const { zettelHealth } = await import("../zettelkasten-B-94vghH.mjs");
|
|
6484
6875
|
const result = zettelHealth(getFedDb(), {
|
|
6485
6876
|
scope,
|
|
6486
6877
|
projectPath,
|
|
@@ -6549,7 +6940,7 @@ async function cmdSurprise(note, opts) {
|
|
|
6549
6940
|
const limit = parseInt(opts.limit ?? "10", 10);
|
|
6550
6941
|
const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
|
|
6551
6942
|
const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
|
|
6552
|
-
const { zettelSurprise } = await import("../zettelkasten-
|
|
6943
|
+
const { zettelSurprise } = await import("../zettelkasten-B-94vghH.mjs");
|
|
6553
6944
|
const db = getFedDb();
|
|
6554
6945
|
console.log();
|
|
6555
6946
|
console.log(header(" PAI Zettel Surprise"));
|
|
@@ -6602,7 +6993,7 @@ async function cmdSuggest(note, opts) {
|
|
|
6602
6993
|
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
6603
6994
|
const limit = parseInt(opts.limit ?? "5", 10);
|
|
6604
6995
|
const excludeLinked = opts.excludeLinked !== false;
|
|
6605
|
-
const { zettelSuggest } = await import("../zettelkasten-
|
|
6996
|
+
const { zettelSuggest } = await import("../zettelkasten-B-94vghH.mjs");
|
|
6606
6997
|
const db = getFedDb();
|
|
6607
6998
|
console.log();
|
|
6608
6999
|
console.log(header(" PAI Zettel Suggest"));
|
|
@@ -6653,7 +7044,7 @@ async function cmdConverse(question, opts) {
|
|
|
6653
7044
|
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
6654
7045
|
const depth = parseInt(opts.depth ?? "2", 10);
|
|
6655
7046
|
const limit = parseInt(opts.limit ?? "15", 10);
|
|
6656
|
-
const { zettelConverse } = await import("../zettelkasten-
|
|
7047
|
+
const { zettelConverse } = await import("../zettelkasten-B-94vghH.mjs");
|
|
6657
7048
|
const db = getFedDb();
|
|
6658
7049
|
console.log();
|
|
6659
7050
|
console.log(header(" PAI Zettel Converse"));
|
|
@@ -6714,7 +7105,7 @@ async function cmdThemes(opts) {
|
|
|
6714
7105
|
const minClusterSize = parseInt(opts.minSize ?? "3", 10);
|
|
6715
7106
|
const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
|
|
6716
7107
|
const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
|
|
6717
|
-
const { zettelThemes } = await import("../zettelkasten-
|
|
7108
|
+
const { zettelThemes } = await import("../zettelkasten-B-94vghH.mjs");
|
|
6718
7109
|
const db = getFedDb();
|
|
6719
7110
|
console.log();
|
|
6720
7111
|
console.log(header(" PAI Zettel Themes"));
|
|
@@ -8129,13 +8520,17 @@ function registerDbCommands(dbCmd) {
|
|
|
8129
8520
|
* PAI Knowledge OS — CLI entry point
|
|
8130
8521
|
*
|
|
8131
8522
|
* Command tree:
|
|
8132
|
-
* pai
|
|
8133
|
-
* pai
|
|
8523
|
+
* pai sessions list|goto|pause|info|rename|... (canonical plural namespace)
|
|
8524
|
+
* pai projects list|cd|add|info|... (canonical plural namespace)
|
|
8134
8525
|
* pai registry scan|migrate|stats|rebuild
|
|
8135
8526
|
* pai memory index|search|status
|
|
8136
8527
|
* pai search <query> (placeholder — Phase 3)
|
|
8137
8528
|
* pai update
|
|
8138
8529
|
* pai version
|
|
8530
|
+
*
|
|
8531
|
+
* Daily verb shortcuts (top-level):
|
|
8532
|
+
* pai pause / pai resume / pai cd
|
|
8533
|
+
* pai sessions / pai projects / pai notes
|
|
8139
8534
|
*/
|
|
8140
8535
|
function getVersion() {
|
|
8141
8536
|
try {
|
|
@@ -8156,11 +8551,29 @@ function getDb() {
|
|
|
8156
8551
|
return _db;
|
|
8157
8552
|
}
|
|
8158
8553
|
const program = new Command();
|
|
8159
|
-
program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit")
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8554
|
+
program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit").addHelpText("after", `
|
|
8555
|
+
Daily verbs (short forms):
|
|
8556
|
+
pai pause Save state + display safe-exit reminder
|
|
8557
|
+
pai resume <name> Go to a session (resume or start fresh)
|
|
8558
|
+
pai cd <name> cd to a project directory
|
|
8559
|
+
|
|
8560
|
+
Listings:
|
|
8561
|
+
pai sessions Resumable sessions catalog
|
|
8562
|
+
pai projects Known project directories
|
|
8563
|
+
pai notes Markdown session notes
|
|
8564
|
+
|
|
8565
|
+
Subcommands (power users):
|
|
8566
|
+
pai sessions ... Session management (list, goto, pause, ...)
|
|
8567
|
+
pai projects ... Project management (cd, list, ...)
|
|
8568
|
+
pai registry ... Registry maintenance (scan, ...)
|
|
8569
|
+
pai memory ... Memory engine (index, search, ...)
|
|
8570
|
+
|
|
8571
|
+
Shell integration:
|
|
8572
|
+
eval "$(pai shell-init)" # Add to ~/.zshrc for pai cd to work`);
|
|
8573
|
+
registerProjectsCommands(program.command("projects").description("Manage registered projects (list, cd, add, info, ...)"), getDb);
|
|
8574
|
+
const sessionsCmd = program.command("sessions").description("Session management (list, goto, pause, ...)");
|
|
8575
|
+
registerSessionsCommands(sessionsCmd, getDb);
|
|
8576
|
+
registerSessionCleanupCommand(sessionsCmd, getDb);
|
|
8164
8577
|
registerRegistryCommands(program.command("registry").description("Registry maintenance: scan, migrate, stats, rebuild"), getDb);
|
|
8165
8578
|
registerMemoryCommands(program.command("memory").description("Memory engine: index, search, and status"), getDb);
|
|
8166
8579
|
registerMcpCommands(program.command("mcp").description("MCP server management: install and status"));
|
|
@@ -8179,6 +8592,56 @@ registerObservationCommands(program.command("observation").description("Observat
|
|
|
8179
8592
|
program.command("go <query>").description("Jump to a project directory by slug or partial name.\nPrints the root path to stdout — use with: cd $(pai go <query>)\nExample shell function in ~/.zshrc:\n pcd() { cd \"$(pai go \"$@\")\" }").action((query) => {
|
|
8180
8593
|
cmdGo(getDb(), query);
|
|
8181
8594
|
});
|
|
8595
|
+
program.command("pause").description("Save state and display safe-exit instructions for the current session.\nWrites a ## Continue checkpoint to the project's TODO.md.\nLong form: pai sessions pause").option("--dry-run", "Preview the ## Continue block without writing it").action((opts) => {
|
|
8596
|
+
cmdPause(getDb(), opts);
|
|
8597
|
+
});
|
|
8598
|
+
program.command("resume <name>").description("Go to a session by name: resume if resumable, start fresh otherwise.\nLong form: pai sessions goto <name>").option("--skip-name", "Do not prepend /Name to restore the session name").option("--skip-go", "Do not append \\ngo to trigger PAI auto-resume").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((name, opts) => {
|
|
8599
|
+
cmdGoto(getDb(), name, {
|
|
8600
|
+
noName: opts.skipName,
|
|
8601
|
+
noGo: opts.skipGo,
|
|
8602
|
+
dryRun: opts.dryRun
|
|
8603
|
+
});
|
|
8604
|
+
});
|
|
8605
|
+
program.command("cd <identifier>").description("cd to a project directory (shell wrapper handles the actual cd).\nLong form: pai projects cd <identifier>\nThe shell function installed by pai shell-init intercepts this command\nand calls builtin cd with the resolved path.").action((identifier) => {
|
|
8606
|
+
const project = resolveIdentifier(getDb(), identifier);
|
|
8607
|
+
if (!project) {
|
|
8608
|
+
console.error(`Project not found: ${identifier}`);
|
|
8609
|
+
process.exit(1);
|
|
8610
|
+
}
|
|
8611
|
+
process.stdout.write(project.root_path + "\n");
|
|
8612
|
+
});
|
|
8613
|
+
program.command("notes [project-slug]").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) => {
|
|
8614
|
+
cmdList$2(getDb(), projectSlug, opts);
|
|
8615
|
+
}).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) => {
|
|
8616
|
+
cmdList$2(getDb(), projectSlug, opts);
|
|
8617
|
+
});
|
|
8618
|
+
program.command("shell-init").description("Emit shell integration code. Add to ~/.zshrc:\n eval \"$(pai shell-init)\"").action(() => {
|
|
8619
|
+
process.stdout.write(`# PAI shell integration — generated by: pai shell-init
|
|
8620
|
+
pai() {
|
|
8621
|
+
if [[ "$1" == "cd" ]]; then
|
|
8622
|
+
local dir=$(command pai cd "$2" 2>/dev/null)
|
|
8623
|
+
if [[ -n "$dir" && -d "$dir" ]]; then
|
|
8624
|
+
builtin cd "$dir"
|
|
8625
|
+
echo "-> $dir"
|
|
8626
|
+
else
|
|
8627
|
+
echo "Project not found: $2" >&2
|
|
8628
|
+
return 1
|
|
8629
|
+
fi
|
|
8630
|
+
elif [[ "$1" == "projects" && "$2" == "cd" ]]; then
|
|
8631
|
+
local dir=$(command pai projects cd "$3" 2>/dev/null)
|
|
8632
|
+
if [[ -n "$dir" && -d "$dir" ]]; then
|
|
8633
|
+
builtin cd "$dir"
|
|
8634
|
+
echo "-> $dir"
|
|
8635
|
+
else
|
|
8636
|
+
echo "Project not found: $3" >&2
|
|
8637
|
+
return 1
|
|
8638
|
+
fi
|
|
8639
|
+
else
|
|
8640
|
+
command pai "$@"
|
|
8641
|
+
fi
|
|
8642
|
+
}
|
|
8643
|
+
`);
|
|
8644
|
+
});
|
|
8182
8645
|
program.command("search <query>").description("Full-text search across sessions and notes (Phase 3)").option("--projects <p1,p2>", "Restrict search to these project slugs (comma-separated)").option("--limit <n>", "Maximum results to return", "10").action((query, opts) => {
|
|
8183
8646
|
console.log(`\n Search is coming in Phase 3.\n\n Query: ${query}\n Projects: ${opts.projects ?? "(all)"}\n Limit: ${opts.limit ?? 10}\n`);
|
|
8184
8647
|
});
|