@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.
Files changed (67) hide show
  1. package/dist/{auto-route-DL4ViDKC.mjs → auto-route-D54Kv7yI.mjs} +2 -2
  2. package/dist/{auto-route-DL4ViDKC.mjs.map → auto-route-D54Kv7yI.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +808 -345
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/{clusters-CRlPBpq8.mjs → clusters-BwmdKB-V.mjs} +1 -1
  6. package/dist/{clusters-CRlPBpq8.mjs.map → clusters-BwmdKB-V.mjs.map} +1 -1
  7. package/dist/{config-BuhHWyOK.mjs → config-DqBY3aT0.mjs} +1 -1
  8. package/dist/{config-BuhHWyOK.mjs.map → config-DqBY3aT0.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +12 -8
  10. package/dist/daemon/index.mjs.map +1 -1
  11. package/dist/daemon-BryEC3as.mjs +1426 -0
  12. package/dist/daemon-BryEC3as.mjs.map +1 -0
  13. package/dist/daemon-mcp/index.mjs +2 -2
  14. package/dist/{detector-AzVtGLtL.mjs → detector-Q_lVXtj-.mjs} +1 -1
  15. package/dist/{detector-AzVtGLtL.mjs.map → detector-Q_lVXtj-.mjs.map} +1 -1
  16. package/dist/{factory-BDBIfTDC.mjs → factory-BufouUQ1.mjs} +3 -3
  17. package/dist/{factory-BDBIfTDC.mjs.map → factory-BufouUQ1.mjs.map} +1 -1
  18. package/dist/hooks/context-compression-hook.mjs +39 -0
  19. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  20. package/dist/hooks/stop-hook.mjs +36 -0
  21. package/dist/hooks/stop-hook.mjs.map +2 -2
  22. package/dist/index.mjs +2 -2
  23. package/dist/indexer-D7MvSQPY.mjs +1 -0
  24. package/dist/{indexer-backend-DXkDh-kc.mjs → indexer-backend-BrrCnasS.mjs} +1 -1
  25. package/dist/{indexer-backend-DXkDh-kc.mjs.map → indexer-backend-BrrCnasS.mjs.map} +1 -1
  26. package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-C3pjwy2m.mjs} +1 -1
  27. package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-C3pjwy2m.mjs.map} +1 -1
  28. package/dist/{kg-entity-D5v7RCDi.mjs → kg-entity-DVzTsy6G.mjs} +1 -1
  29. package/dist/{kg-entity-D5v7RCDi.mjs.map → kg-entity-DVzTsy6G.mjs.map} +1 -1
  30. package/dist/kg-extraction-r5KkTmWY.mjs +524 -0
  31. package/dist/kg-extraction-r5KkTmWY.mjs.map +1 -0
  32. package/dist/{latent-ideas-DvWBRHsy.mjs → latent-ideas-dWVuI_dg.mjs} +2 -2
  33. package/dist/{latent-ideas-DvWBRHsy.mjs.map → latent-ideas-dWVuI_dg.mjs.map} +1 -1
  34. package/dist/{migrate-jokLenje.mjs → migrate-fLD6rAdO.mjs} +2 -2
  35. package/dist/{migrate-jokLenje.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
  36. package/dist/{neighborhood-u8ytjmWq.mjs → neighborhood-DixmpfPT.mjs} +1 -1
  37. package/dist/{neighborhood-u8ytjmWq.mjs.map → neighborhood-DixmpfPT.mjs.map} +1 -1
  38. package/dist/{note-context-CG2_e-0W.mjs → note-context-BY7rfGdl.mjs} +1 -1
  39. package/dist/{note-context-CG2_e-0W.mjs.map → note-context-BY7rfGdl.mjs.map} +1 -1
  40. package/dist/{postgres-BGERehmX.mjs → postgres-Cp25xZes.mjs} +1 -1
  41. package/dist/{postgres-BGERehmX.mjs.map → postgres-Cp25xZes.mjs.map} +1 -1
  42. package/dist/{query-feedback-CQSumXDy.mjs → query-feedback-C0hKUHVz.mjs} +1 -1
  43. package/dist/{query-feedback-CQSumXDy.mjs.map → query-feedback-C0hKUHVz.mjs.map} +1 -1
  44. package/dist/{sqlite-xUe94oUZ.mjs → sqlite-B3JHfJc2.mjs} +1 -1
  45. package/dist/{sqlite-xUe94oUZ.mjs.map → sqlite-B3JHfJc2.mjs.map} +1 -1
  46. package/dist/{state-BIlxNRUn.mjs → state-BHZAhLou.mjs} +1 -1
  47. package/dist/{state-BIlxNRUn.mjs.map → state-BHZAhLou.mjs.map} +1 -1
  48. package/dist/{themes-9jxFn3Rf.mjs → themes-DO8mUVwD.mjs} +1 -1
  49. package/dist/{themes-9jxFn3Rf.mjs.map → themes-DO8mUVwD.mjs.map} +1 -1
  50. package/dist/{tools-CF0lB5lW.mjs → tools-C1JNgya7.mjs} +14 -14
  51. package/dist/{tools-CF0lB5lW.mjs.map → tools-C1JNgya7.mjs.map} +1 -1
  52. package/dist/{trace-C2XrzssW.mjs → trace-TYCLMCkY.mjs} +1 -1
  53. package/dist/{trace-C2XrzssW.mjs.map → trace-TYCLMCkY.mjs.map} +1 -1
  54. package/dist/{utils-QSfKagcj.mjs → utils-BAxjW3j8.mjs} +2 -2
  55. package/dist/{utils-QSfKagcj.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
  56. package/dist/{vault-indexer-iHRjcy6e.mjs → vault-indexer-DsPC6wQf.mjs} +1 -1
  57. package/dist/{vault-indexer-iHRjcy6e.mjs.map → vault-indexer-DsPC6wQf.mjs.map} +1 -1
  58. package/dist/{daemon-w2fE3UMz.mjs → work-queue-worker-94INtUiz.mjs} +54 -1398
  59. package/dist/work-queue-worker-94INtUiz.mjs.map +1 -0
  60. package/dist/{zettelkasten-BdaMzTGQ.mjs → zettelkasten-B-94vghH.mjs} +3 -3
  61. package/dist/{zettelkasten-BdaMzTGQ.mjs.map → zettelkasten-B-94vghH.mjs.map} +1 -1
  62. package/package.json +1 -1
  63. package/src/hooks/ts/pre-compact/context-compression-hook.ts +48 -0
  64. package/src/hooks/ts/stop/stop-hook.ts +40 -0
  65. package/dist/daemon-w2fE3UMz.mjs.map +0 -1
  66. package/dist/kg-extraction-Uvr9fTlz.mjs +0 -246
  67. package/dist/kg-extraction-Uvr9fTlz.mjs.map +0 -1
@@ -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-QSfKagcj.mjs";
4
- import { a as slugify$1, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "../migrate-jokLenje.mjs";
5
- import { n as ensurePaiMarker, t as discoverPaiMarkers } from "../pai-marker-CXQPX2P6.mjs";
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-Uvr9fTlz.mjs";
14
- import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
15
- import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, t as CONFIG_DIR } from "../config-BuhHWyOK.mjs";
16
- import { t as createStorageBackend } from "../factory-BDBIfTDC.mjs";
17
- import { s as kgQuery } from "../kg-entity-D5v7RCDi.mjs";
18
- import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
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 registerProjectCommands(projectCmd, getDb) {
1099
- projectCmd.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) => {
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
- projectCmd.command("info <slug>").description("Show full details for a project").action((slug) => {
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
- projectCmd.command("archive <slug>").description("Archive a project").action((slug) => {
1117
+ projectsCmd.command("archive <slug>").description("Archive a project").action((slug) => {
1109
1118
  cmdArchive(getDb(), slug);
1110
1119
  });
1111
- projectCmd.command("unarchive <slug>").description("Restore an archived project to active status").action((slug) => {
1120
+ projectsCmd.command("unarchive <slug>").description("Restore an archived project to active status").action((slug) => {
1112
1121
  cmdUnarchive(getDb(), slug);
1113
1122
  });
1114
- projectCmd.command("move <slug> <new-path>").description("Update the root path for a project").action((slug, newPath) => {
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
- projectCmd.command("tag <slug> <tags...>").description("Add one or more tags to a project").action((slug, tags) => {
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
- projectCmd.command("alias <slug> <alias>").description("Register an alternative slug for a project").action((slug, alias) => {
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
- projectCmd.command("edit <slug>").description("Edit project metadata").option("--display-name <name>", "New display name").option("--type <type>", "New type").action((slug, opts) => {
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
- projectCmd.command("cd <identifier>").description("Print the root path for a project (use with: cd $(pai project cd <id>))").action((identifier) => {
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
- projectCmd.command("health").description("Audit all registered projects: check which paths still exist, find moved/dead projects").option("--fix", "Auto-remediate where possible (update moved paths, archive dead zero-session projects)").option("--json", "Output raw JSON report").option("--status <category>", "Filter output to: active | stale | dead").action((opts) => {
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
- projectCmd.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
+ 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
- projectCmd.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
+ 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
- projectCmd.command("go <query>").description("Print the root path for a project by slug, partial name, or fuzzy match.\nDesigned for shell integration: cd $(pai project go <query>)\nOr set a shell alias: alias pcd='cd $(pai project go)'").action((query) => {
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
- projectCmd.command("name <identifier> <shortname>").description("Give a project a short name for quick access (used by AIBroker to launch sessions)").option("--permission <level>", "Permission level: full | trusted | default (or raw CLI flags)").action((identifier, shortname, opts) => {
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
- projectCmd.command("unname <shortname>").description("Remove a project's short name").action((shortname) => {
1153
+ projectsCmd.command("unname <shortname>").description("Remove a project's short name").action((shortname) => {
1153
1154
  cmdUnname(getDb(), shortname);
1154
1155
  });
1155
- projectCmd.command("names").description("List named projects (your curated shortlist)").option("--json", "Output JSON for AIBroker consumption").action((opts) => {
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
- projectCmd.command("config [identifier]").description("View or modify session launch config for a project.\nUse --options to discover available keys and presets.\nUse --defaults to manage global defaults for new sessions.").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
+ 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-DL4ViDKC.mjs");
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-BDBIfTDC.mjs").then((n) => n.n);
1688
- const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
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/commands/session/index.ts
1929
- function registerSessionCommands(sessionCmd, getDb) {
1930
- sessionCmd.command("list [project-slug]").description("List sessions, optionally filtered to a single project").option("--limit <n>", "Maximum number of sessions to show", "20").option("--status <status>", "Filter by status: open | completed | compacted").action((projectSlug, opts) => {
1931
- cmdList$2(getDb(), projectSlug, opts);
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
- sessionCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
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
- sessionCmd.command("rename <project-slug> <number> <new-slug>").description("Rename a session note — updates file on disk, H1 title, and registry").action((projectSlug, number, newSlug) => {
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
- sessionCmd.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) => {
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
- sessionCmd.command("tag <project-slug> <number> [tags...]").description("Set or show tags on a session. Tags can be space-separated or comma-separated.").action((projectSlug, number, tags) => {
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
- sessionCmd.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) => {
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
- sessionCmd.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.\nRecords the last session identifier, timestamp, and working directory\nso the next session can resume from the correct context.").action((projectSlug, sessionId) => {
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
- 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) => {
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
- 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) => {
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
- sessionCmd.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.\nDesigned for use in CLAUDE.md session-start hooks.").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) => {
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-BuhHWyOK.mjs").then((n) => n.r);
2306
- const { PostgresBackend } = await import("../postgres-BGERehmX.mjs");
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-BuhHWyOK.mjs").then((n) => n.r);
2327
- const { PostgresBackend } = await import("../postgres-BGERehmX.mjs");
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-w2fE3UMz.mjs").then((n) => n.t);
3798
- const { loadConfig: lc, ensureConfigDir } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
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-BdaMzTGQ.mjs");
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-BdaMzTGQ.mjs");
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-BdaMzTGQ.mjs");
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-BdaMzTGQ.mjs");
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-BdaMzTGQ.mjs");
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-BdaMzTGQ.mjs");
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 project add|list|info|archive|unarchive|move|tag|alias|edit
8133
- * pai session list|info
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
- registerProjectCommands(program.command("project").description("Manage registered projects"), getDb);
8161
- const sessionCmd = program.command("session").description("Browse session notes");
8162
- registerSessionCommands(sessionCmd, getDb);
8163
- registerSessionCleanupCommand(sessionCmd, getDb);
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
  });