@tekmidian/pai 0.9.13 → 0.9.15

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 +1365 -378
  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-B-vBWU1i.mjs +1426 -0
  12. package/dist/daemon-B-vBWU1i.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,17 +10,20 @@ 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";
23
+ import { randomUUID } from "node:crypto";
22
24
  import { Command } from "commander";
23
25
  import { fileURLToPath } from "node:url";
26
+ import { connect } from "node:net";
24
27
  import { execSync, spawnSync } from "node:child_process";
25
28
  import { createInterface } from "node:readline";
26
29
  import { createConnection } from "net";
@@ -1094,68 +1097,68 @@ function cmdHealth$1(db, opts) {
1094
1097
  }
1095
1098
 
1096
1099
  //#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) => {
1100
+ //#region src/cli/commands/project/projects-index.ts
1101
+ function registerProjectsCommands(projectsCmd, getDb) {
1102
+ 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
1103
  cmdList$3(getDb(), opts);
1104
1104
  });
1105
- projectCmd.command("info <slug>").description("Show full details for a project").action((slug) => {
1105
+ 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) => {
1106
+ const project = resolveIdentifier(getDb(), identifier);
1107
+ if (!project) {
1108
+ console.error(`Project not found: ${identifier}`);
1109
+ process.exit(1);
1110
+ }
1111
+ process.stdout.write(project.root_path + "\n");
1112
+ });
1113
+ 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) => {
1114
+ cmdAdd(getDb(), rawPath, opts);
1115
+ });
1116
+ projectsCmd.command("info <slug>").description("Show full details for a project").action((slug) => {
1106
1117
  cmdInfo$1(getDb(), slug);
1107
1118
  });
1108
- projectCmd.command("archive <slug>").description("Archive a project").action((slug) => {
1119
+ projectsCmd.command("archive <slug>").description("Archive a project").action((slug) => {
1109
1120
  cmdArchive(getDb(), slug);
1110
1121
  });
1111
- projectCmd.command("unarchive <slug>").description("Restore an archived project to active status").action((slug) => {
1122
+ projectsCmd.command("unarchive <slug>").description("Restore an archived project to active status").action((slug) => {
1112
1123
  cmdUnarchive(getDb(), slug);
1113
1124
  });
1114
- projectCmd.command("move <slug> <new-path>").description("Update the root path for a project").action((slug, newPath) => {
1125
+ projectsCmd.command("move <slug> <new-path>").description("Update the root path for a project").action((slug, newPath) => {
1115
1126
  cmdMove(getDb(), slug, newPath);
1116
1127
  });
1117
- projectCmd.command("tag <slug> <tags...>").description("Add one or more tags to a project").action((slug, tags) => {
1128
+ projectsCmd.command("tag <slug> <tags...>").description("Add one or more tags to a project").action((slug, tags) => {
1118
1129
  cmdTag$1(getDb(), slug, tags);
1119
1130
  });
1120
- projectCmd.command("alias <slug> <alias>").description("Register an alternative slug for a project").action((slug, alias) => {
1131
+ projectsCmd.command("alias <slug> <alias>").description("Register an alternative slug for a project").action((slug, alias) => {
1121
1132
  cmdAlias(getDb(), slug, alias);
1122
1133
  });
1123
- projectCmd.command("edit <slug>").description("Edit project metadata").option("--display-name <name>", "New display name").option("--type <type>", "New type").action((slug, opts) => {
1134
+ projectsCmd.command("edit <slug>").description("Edit project metadata").option("--display-name <name>", "New display name").option("--type <type>", "New type").action((slug, opts) => {
1124
1135
  cmdEdit(getDb(), slug, opts);
1125
1136
  });
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) => {
1137
+ 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
1138
  cmdDetect(getDb(), pathArg, opts);
1136
1139
  });
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) => {
1140
+ 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
1141
  cmdHealth$1(getDb(), opts);
1139
1142
  });
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) => {
1143
+ 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
1144
  cmdConsolidate(getDb(), identifier, opts);
1142
1145
  });
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) => {
1146
+ 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
1147
  cmdPromote(getDb(), opts);
1145
1148
  });
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) => {
1149
+ 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
1150
  cmdGo(getDb(), query);
1148
1151
  });
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) => {
1152
+ 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
1153
  cmdName(getDb(), identifier, shortname, opts);
1151
1154
  });
1152
- projectCmd.command("unname <shortname>").description("Remove a project's short name").action((shortname) => {
1155
+ projectsCmd.command("unname <shortname>").description("Remove a project's short name").action((shortname) => {
1153
1156
  cmdUnname(getDb(), shortname);
1154
1157
  });
1155
- projectCmd.command("names").description("List named projects (your curated shortlist)").option("--json", "Output JSON for AIBroker consumption").action((opts) => {
1158
+ projectsCmd.command("names").description("List named projects (your curated shortlist)").option("--json", "Output JSON for AIBroker consumption").action((opts) => {
1156
1159
  cmdNames(getDb(), opts);
1157
1160
  });
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) => {
1161
+ 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
1162
  cmdConfig(getDb(), identifier, opts);
1160
1163
  });
1161
1164
  }
@@ -1682,10 +1685,10 @@ function cmdActive(db, opts) {
1682
1685
  ], rows));
1683
1686
  }
1684
1687
  async function cmdAutoRoute(opts) {
1685
- const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-DL4ViDKC.mjs");
1688
+ const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-D54Kv7yI.mjs");
1686
1689
  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);
1690
+ const { createStorageBackend } = await import("../factory-BufouUQ1.mjs").then((n) => n.n);
1691
+ const { loadConfig } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
1689
1692
  const config = loadConfig();
1690
1693
  const registryDb = openRegistry();
1691
1694
  const federation = await createStorageBackend(config);
@@ -1811,12 +1814,913 @@ function cmdCheckpoint(message, opts) {
1811
1814
  } catch {}
1812
1815
  process.exit(0);
1813
1816
  }
1814
- touchCheckpointSentinel(notesDir);
1815
- process.exit(0);
1817
+ touchCheckpointSentinel(notesDir);
1818
+ process.exit(0);
1819
+ }
1820
+
1821
+ //#endregion
1822
+ //#region src/cli/commands/session/handover.ts
1823
+ const HANDOVER_TODO_LOCATIONS$1 = [
1824
+ "Notes/TODO.md",
1825
+ ".claude/Notes/TODO.md",
1826
+ "tasks/todo.md",
1827
+ "TODO.md"
1828
+ ];
1829
+ function findProjectTodo$1(rootPath) {
1830
+ for (const rel of HANDOVER_TODO_LOCATIONS$1) {
1831
+ const full = join(rootPath, rel);
1832
+ if (existsSync(full)) try {
1833
+ return {
1834
+ path: full,
1835
+ content: readFileSync(full, "utf8")
1836
+ };
1837
+ } catch {}
1838
+ }
1839
+ return null;
1840
+ }
1841
+ function stripContinueSection$1(content) {
1842
+ const lines = content.split("\n");
1843
+ const startIdx = lines.findIndex((l) => l.trim() === "## Continue");
1844
+ if (startIdx === -1) return content;
1845
+ let endIdx = lines.length;
1846
+ for (let i = startIdx + 1; i < lines.length; i++) {
1847
+ const trimmed = lines[i].trim();
1848
+ if (trimmed === "---" || trimmed.startsWith("##") && trimmed !== "## Continue") {
1849
+ endIdx = i;
1850
+ break;
1851
+ }
1852
+ }
1853
+ let trailingEnd = endIdx;
1854
+ if (trailingEnd < lines.length && lines[trailingEnd].trim() === "---") trailingEnd += 1;
1855
+ const before = lines.slice(0, startIdx);
1856
+ const after = lines.slice(trailingEnd);
1857
+ while (after.length > 0 && after[0].trim() === "") after.shift();
1858
+ return [...before, ...after].join("\n");
1859
+ }
1860
+ function cmdHandover(db, projectSlug, numberOrLatest) {
1861
+ let project;
1862
+ if (projectSlug) {
1863
+ project = db.prepare("SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?").get(projectSlug);
1864
+ if (!project) process.exit(0);
1865
+ } else {
1866
+ const cwd = process.cwd();
1867
+ const row = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
1868
+ FROM projects
1869
+ WHERE ? LIKE root_path || '%'
1870
+ ORDER BY length(root_path) DESC
1871
+ LIMIT 1`).get(cwd);
1872
+ if (!row) process.exit(0);
1873
+ project = row;
1874
+ }
1875
+ let session;
1876
+ const nol = numberOrLatest ?? "latest";
1877
+ if (nol === "latest") session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
1878
+ else {
1879
+ const num = parseInt(nol, 10);
1880
+ if (!isNaN(num)) session = db.prepare("SELECT * FROM sessions WHERE project_id = ? AND number = ?").get(project.id, num);
1881
+ }
1882
+ const todo = findProjectTodo$1(project.root_path);
1883
+ let todoPath;
1884
+ let existingContent;
1885
+ if (todo) {
1886
+ todoPath = todo.path;
1887
+ existingContent = todo.content;
1888
+ } else {
1889
+ const notesDir = join(project.root_path, "Notes");
1890
+ try {
1891
+ if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });
1892
+ } catch {
1893
+ process.exit(0);
1894
+ }
1895
+ todoPath = join(notesDir, "TODO.md");
1896
+ existingContent = "";
1897
+ }
1898
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1899
+ const cwd = process.cwd();
1900
+ let sessionLine;
1901
+ if (session) {
1902
+ const num = String(session.number).padStart(4, "0");
1903
+ const titlePart = session.title || session.slug || "Session";
1904
+ sessionLine = `${num} - ${session.date} - ${titlePart}`;
1905
+ } else sessionLine = "Unknown session";
1906
+ const newContent = [
1907
+ "## Continue",
1908
+ "",
1909
+ `> **Last session:** ${sessionLine}`,
1910
+ `> **Paused at:** ${timestamp}`,
1911
+ ">",
1912
+ `> Working directory: ${cwd}. Check the latest session note for details.`,
1913
+ "",
1914
+ "---",
1915
+ ""
1916
+ ].join("\n") + stripContinueSection$1(existingContent).trimStart();
1917
+ const tmpPath = `${todoPath}.handover.tmp`;
1918
+ try {
1919
+ writeFileSync(tmpPath, newContent, "utf8");
1920
+ renameSync(tmpPath, todoPath);
1921
+ } catch {
1922
+ try {
1923
+ if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
1924
+ } catch {}
1925
+ process.exit(0);
1926
+ }
1927
+ process.exit(0);
1928
+ }
1929
+
1930
+ //#endregion
1931
+ //#region src/cli/lib/session-scan.ts
1932
+ /**
1933
+ * Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .
1934
+ *
1935
+ * Storage layout (Claude Code v2.1.143+):
1936
+ *
1937
+ * <project>/<uuid>.jsonl — top-level: metadata, system snapshots, hook events.
1938
+ * REQUIRED for claude --resume to work.
1939
+ * <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.
1940
+ * 3000+ files, almost none resumable on their own.
1941
+ *
1942
+ * Resumability rule (empirically verified):
1943
+ * A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one
1944
+ * line of type "system". Sessions that only have a sessions/ counterpart cannot be
1945
+ * resumed by Claude Code regardless of how much transcript content they have.
1946
+ *
1947
+ * Stale-UUID problem (fixed in this version):
1948
+ * The clc session.json registry stores ONE uuid per named session — the uuid Claude Code
1949
+ * had when the user last named the session. If the user resumes and the session gets a
1950
+ * new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD
1951
+ * uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project
1952
+ * directory, not the clc-cached uuid.
1953
+ *
1954
+ * Resolution strategy:
1955
+ * 1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).
1956
+ * 2. For each, attach the clc name if the uuid matches (exact hit).
1957
+ * 3. After Pass 1, for every clc registry entry:
1958
+ * a. If the cached uuid was found → already handled.
1959
+ * b. Find the encodedDir for this entry's directory.
1960
+ * c. Check ALL sessions in that encodedDir (already in our Pass-1 results).
1961
+ * d. Pick the MOST RECENT resumable session in that dir and attach the name to it.
1962
+ * If no resumable session, fall through to transcript-only pass.
1963
+ * 4. This means the displayed uuid for "Jobs Matthias" is always today's active session,
1964
+ * not the stale cached one.
1965
+ *
1966
+ * Used by: pai sessions, pai resume
1967
+ */
1968
+ const CLC_SESSIONS_FILE = join(homedir(), ".claude", "session.json");
1969
+ /** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */
1970
+ function buildClcInfoMap() {
1971
+ const map = /* @__PURE__ */ new Map();
1972
+ try {
1973
+ const raw = readFileSync(CLC_SESSIONS_FILE, "utf8");
1974
+ const data = JSON.parse(raw);
1975
+ for (const entry of data.sessions ?? []) {
1976
+ const name = entry.name?.trim();
1977
+ const uuid = (entry.resume ?? entry.session ?? "").trim();
1978
+ if (name && uuid) map.set(uuid, {
1979
+ name,
1980
+ directory: entry.directory?.trim() || void 0
1981
+ });
1982
+ }
1983
+ } catch {}
1984
+ return map;
1985
+ }
1986
+ /** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */
1987
+ function buildRegistryRootPathMap(db) {
1988
+ try {
1989
+ const rows = db.prepare(`SELECT root_path, encoded_dir FROM projects
1990
+ WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`).all();
1991
+ const map = /* @__PURE__ */ new Map();
1992
+ for (const row of rows) map.set(row.encoded_dir, row.root_path);
1993
+ return map;
1994
+ } catch {
1995
+ return /* @__PURE__ */ new Map();
1996
+ }
1997
+ }
1998
+ function parseTopLevel(filePath) {
1999
+ let systemLines = 0;
2000
+ let size = 0;
2001
+ let mtime = 0;
2002
+ try {
2003
+ const st = statSync(filePath);
2004
+ size = st.size;
2005
+ mtime = st.mtimeMs;
2006
+ const content = readFileSync(filePath, "utf8");
2007
+ for (const line of content.split("\n")) {
2008
+ const t = line.trim();
2009
+ if (!t) continue;
2010
+ if (t.includes("\"type\":\"system\"") || t.includes("\"type\": \"system\"")) systemLines++;
2011
+ }
2012
+ } catch {}
2013
+ return {
2014
+ systemLines,
2015
+ size,
2016
+ mtime
2017
+ };
2018
+ }
2019
+ function parseTranscript(filePath) {
2020
+ let userLines = 0;
2021
+ let lastUserPrompt = "";
2022
+ let msgCount = 0;
2023
+ let aiTitle;
2024
+ let mtime = 0;
2025
+ try {
2026
+ mtime = statSync(filePath).mtimeMs;
2027
+ } catch {
2028
+ return {
2029
+ userLines: 0,
2030
+ lastUserPrompt: "",
2031
+ msgCount: 0,
2032
+ mtime: 0
2033
+ };
2034
+ }
2035
+ let content;
2036
+ try {
2037
+ content = readFileSync(filePath, "utf8");
2038
+ } catch {
2039
+ return {
2040
+ userLines: 0,
2041
+ lastUserPrompt: "",
2042
+ msgCount: 0,
2043
+ mtime
2044
+ };
2045
+ }
2046
+ for (const line of content.split("\n")) {
2047
+ const t = line.trim();
2048
+ if (!t) continue;
2049
+ msgCount++;
2050
+ const hasUser = t.includes("\"type\":\"user\"") || t.includes("\"type\": \"user\"");
2051
+ const hasTitle = t.includes("\"type\":\"ai-title\"") || t.includes("\"type\": \"ai-title\"");
2052
+ if (!hasUser && !hasTitle) continue;
2053
+ let parsed;
2054
+ try {
2055
+ parsed = JSON.parse(t);
2056
+ } catch {
2057
+ continue;
2058
+ }
2059
+ if (parsed.type === "ai-title") {
2060
+ const v = parsed.title ?? parsed.name;
2061
+ if (typeof v === "string" && v.trim()) aiTitle = v.trim();
2062
+ continue;
2063
+ }
2064
+ if (parsed.type !== "user") continue;
2065
+ userLines++;
2066
+ const msg = parsed.message;
2067
+ if (!msg) continue;
2068
+ const c = msg.content;
2069
+ let text = "";
2070
+ if (typeof c === "string") text = c;
2071
+ else if (Array.isArray(c) && c.length > 0) {
2072
+ const first = c[0];
2073
+ if (typeof first.text === "string") text = first.text;
2074
+ }
2075
+ if (text) lastUserPrompt = text.slice(0, 80).replace(/\n/g, " ");
2076
+ }
2077
+ return {
2078
+ userLines,
2079
+ lastUserPrompt,
2080
+ msgCount,
2081
+ aiTitle,
2082
+ mtime
2083
+ };
2084
+ }
2085
+ const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
2086
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2087
+ /**
2088
+ * Compute the effective filter mode, resolving the legacy resumableOnly flag.
2089
+ */
2090
+ function resolveFilter(opts) {
2091
+ if (opts.filter) return opts.filter;
2092
+ if (opts.resumableOnly === true) return "resumable";
2093
+ if (opts.resumableOnly === false) return "all";
2094
+ return "named";
2095
+ }
2096
+ /**
2097
+ * Scan ~/.claude/projects/ for all Claude Code sessions.
2098
+ *
2099
+ * Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).
2100
+ * Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.
2101
+ * For each such entry, scan the entry's project dir for the FRESHEST session
2102
+ * and attach the registry name to it. This fixes the stale-UUID bug where
2103
+ * clc's session.json points to an old uuid after a fresh start.
2104
+ * Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.
2105
+ *
2106
+ * Filter modes:
2107
+ * "named" — resumable + registry-known stubs + transcript-only (default)
2108
+ * "all" — all of the above + unnamed orphans from sessions/ subdirs
2109
+ * "resumable" — only sessions claude --resume accepts
2110
+ *
2111
+ * Returns results sorted by mtime descending.
2112
+ */
2113
+ function scanSessions(db, opts = {}) {
2114
+ const limit = opts.limit ?? 200;
2115
+ const filterMode = resolveFilter(opts);
2116
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];
2117
+ const clcInfoMap = buildClcInfoMap();
2118
+ const rootPathMap = buildRegistryRootPathMap(db);
2119
+ const results = [];
2120
+ const seenUuids = /* @__PURE__ */ new Set();
2121
+ const attachedClcUuids = /* @__PURE__ */ new Set();
2122
+ let encodedDirs;
2123
+ try {
2124
+ encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);
2125
+ } catch {
2126
+ return [];
2127
+ }
2128
+ const sessionsByEncodedDir = /* @__PURE__ */ new Map();
2129
+ for (const encodedDir of encodedDirs) {
2130
+ const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);
2131
+ try {
2132
+ if (!statSync(projectDir).isDirectory()) continue;
2133
+ } catch {
2134
+ continue;
2135
+ }
2136
+ let files;
2137
+ try {
2138
+ files = readdirSync(projectDir);
2139
+ } catch {
2140
+ continue;
2141
+ }
2142
+ const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, "/");
2143
+ const registryRootPath = rootPathMap.get(encodedDir);
2144
+ const projectBasename = basename(decodedPath);
2145
+ for (const file of files) {
2146
+ if (!file.endsWith(".jsonl")) continue;
2147
+ const uuid = file.slice(0, -6);
2148
+ if (!UUID_RE.test(uuid)) continue;
2149
+ const topLevelPath = join(projectDir, file);
2150
+ const topInfo = parseTopLevel(topLevelPath);
2151
+ const resumable = topInfo.systemLines > 0;
2152
+ const clcInfo = clcInfoMap.get(uuid);
2153
+ const inRegistry = !!clcInfo;
2154
+ const sessionStatus = resumable ? "resumable" : inRegistry ? "stub" : "orphan";
2155
+ const passesFilter = filterMode === "resumable" ? resumable : filterMode === "named" ? resumable || inRegistry : true;
2156
+ const sessionJsonlPath = join(projectDir, "sessions", `${uuid}.jsonl`);
2157
+ const hasTranscript = existsSync(sessionJsonlPath);
2158
+ const transcript = hasTranscript ? parseTranscript(sessionJsonlPath) : {
2159
+ userLines: 0,
2160
+ lastUserPrompt: "",
2161
+ msgCount: 0,
2162
+ mtime: 0
2163
+ };
2164
+ const mtime = topInfo.mtime || transcript.mtime;
2165
+ const friendlyName = clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? void 0;
2166
+ const session = {
2167
+ uuid,
2168
+ shortId: uuid.slice(0, 8),
2169
+ encodedDir,
2170
+ decodedPath,
2171
+ topLevelPath,
2172
+ topLevelSystemLines: topInfo.systemLines,
2173
+ topLevelSize: topInfo.size,
2174
+ resumable,
2175
+ sessionStatus,
2176
+ sessionJsonlPath: hasTranscript ? sessionJsonlPath : void 0,
2177
+ userLines: transcript.userLines,
2178
+ lastUserPrompt: transcript.lastUserPrompt,
2179
+ msgCount: transcript.msgCount,
2180
+ aiTitle: transcript.aiTitle,
2181
+ mtime,
2182
+ friendlyName,
2183
+ clcDirectory: clcInfo?.directory,
2184
+ registryRootPath
2185
+ };
2186
+ if (!sessionsByEncodedDir.has(encodedDir)) sessionsByEncodedDir.set(encodedDir, []);
2187
+ sessionsByEncodedDir.get(encodedDir).push(session);
2188
+ seenUuids.add(uuid);
2189
+ if (inRegistry) attachedClcUuids.add(uuid);
2190
+ if (passesFilter) results.push(session);
2191
+ }
2192
+ }
2193
+ if (filterMode !== "resumable") for (const [cachedUuid, clcInfo] of clcInfoMap) {
2194
+ if (attachedClcUuids.has(cachedUuid)) continue;
2195
+ let foundEncodedDir;
2196
+ if (clcInfo.directory) {
2197
+ const real = realpathSyncSafe(clcInfo.directory);
2198
+ if (real) {
2199
+ const encoded = encodeProjectDir(real);
2200
+ if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) foundEncodedDir = encoded;
2201
+ }
2202
+ }
2203
+ if (!foundEncodedDir) {
2204
+ for (const encodedDir of encodedDirs) if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, "sessions", `${cachedUuid}.jsonl`))) {
2205
+ foundEncodedDir = encodedDir;
2206
+ break;
2207
+ }
2208
+ }
2209
+ if (foundEncodedDir) {
2210
+ const freshestResumable = (sessionsByEncodedDir.get(foundEncodedDir) ?? []).filter((s) => s.resumable && !s.friendlyName).sort((a, b) => b.mtime - a.mtime)[0];
2211
+ if (freshestResumable) {
2212
+ freshestResumable.friendlyName = clcInfo.name;
2213
+ freshestResumable.clcDirectory = freshestResumable.clcDirectory ?? clcInfo.directory;
2214
+ freshestResumable.sessionStatus = "resumable";
2215
+ attachedClcUuids.add(freshestResumable.uuid);
2216
+ if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {
2217
+ if (!results.includes(freshestResumable)) results.push(freshestResumable);
2218
+ }
2219
+ continue;
2220
+ }
2221
+ }
2222
+ if (!foundEncodedDir) {
2223
+ const encodedDir = "";
2224
+ const decodedPath = clcInfo.directory ?? cachedUuid;
2225
+ const registryRootPath = void 0;
2226
+ const mtime = {
2227
+ userLines: 0,
2228
+ lastUserPrompt: "",
2229
+ msgCount: 0,
2230
+ mtime: 0
2231
+ }.mtime;
2232
+ seenUuids.add(cachedUuid);
2233
+ results.push({
2234
+ uuid: cachedUuid,
2235
+ shortId: cachedUuid.slice(0, 8),
2236
+ encodedDir,
2237
+ decodedPath,
2238
+ topLevelPath: "",
2239
+ topLevelSystemLines: 0,
2240
+ topLevelSize: 0,
2241
+ resumable: false,
2242
+ sessionStatus: "transcript-only",
2243
+ sessionJsonlPath: void 0,
2244
+ userLines: 0,
2245
+ lastUserPrompt: "",
2246
+ msgCount: 0,
2247
+ mtime,
2248
+ friendlyName: clcInfo.name,
2249
+ clcDirectory: clcInfo.directory,
2250
+ registryRootPath
2251
+ });
2252
+ continue;
2253
+ }
2254
+ const foundTranscriptPath = existsSync(join(CLAUDE_PROJECTS_DIR, foundEncodedDir, "sessions", `${cachedUuid}.jsonl`)) ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, "sessions", `${cachedUuid}.jsonl`) : void 0;
2255
+ const decodedPath = clcInfo.directory ?? smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, "/");
2256
+ const registryRootPath = rootPathMap.get(foundEncodedDir);
2257
+ const topLevelPath = join(CLAUDE_PROJECTS_DIR, foundEncodedDir, `${cachedUuid}.jsonl`);
2258
+ const transcript = foundTranscriptPath ? parseTranscript(foundTranscriptPath) : {
2259
+ userLines: 0,
2260
+ lastUserPrompt: "",
2261
+ msgCount: 0,
2262
+ mtime: 0
2263
+ };
2264
+ seenUuids.add(cachedUuid);
2265
+ results.push({
2266
+ uuid: cachedUuid,
2267
+ shortId: cachedUuid.slice(0, 8),
2268
+ encodedDir: foundEncodedDir,
2269
+ decodedPath,
2270
+ topLevelPath,
2271
+ topLevelSystemLines: 0,
2272
+ topLevelSize: 0,
2273
+ resumable: false,
2274
+ sessionStatus: "transcript-only",
2275
+ sessionJsonlPath: foundTranscriptPath,
2276
+ userLines: transcript.userLines,
2277
+ lastUserPrompt: transcript.lastUserPrompt,
2278
+ msgCount: transcript.msgCount,
2279
+ aiTitle: transcript.aiTitle,
2280
+ mtime: transcript.mtime,
2281
+ friendlyName: clcInfo.name,
2282
+ clcDirectory: clcInfo.directory,
2283
+ registryRootPath
2284
+ });
2285
+ }
2286
+ results.sort((a, b) => b.mtime - a.mtime);
2287
+ return results.slice(0, limit);
2288
+ }
2289
+ /** Claude Code's project-dir encoding: replace / . - (space) with - */
2290
+ function encodeProjectDir(realPath) {
2291
+ return realPath.replace(/[/.\- ]/g, "-");
2292
+ }
2293
+ /** realpathSync that returns null instead of throwing */
2294
+ function realpathSyncSafe(p) {
2295
+ try {
2296
+ return realpathSync(p);
2297
+ } catch {
2298
+ return null;
2299
+ }
2300
+ }
2301
+ function fmtAge(mtime) {
2302
+ const diffMs = Date.now() - mtime;
2303
+ const diffMin = Math.floor(diffMs / 6e4);
2304
+ const diffHr = Math.floor(diffMs / 36e5);
2305
+ const diffDay = Math.floor(diffMs / 864e5);
2306
+ if (diffMin < 60) return `${diffMin}m`;
2307
+ if (diffHr < 24) return `${diffHr}h`;
2308
+ if (diffDay < 30) return `${diffDay}d`;
2309
+ return `${Math.floor(diffDay / 30)}mo`;
2310
+ }
2311
+ /**
2312
+ * Resolve a name-or-id-or-prefix to a single ScannedSession.
2313
+ *
2314
+ * Comparisons are case-insensitive; stored casing is preserved in output.
2315
+ *
2316
+ * Priority:
2317
+ * 1. Exact case-insensitive match on friendlyName
2318
+ * 2. Partial case-insensitive match (contains)
2319
+ * 3. UUID prefix match
2320
+ */
2321
+ function resolveSessionByNameOrId(sessions, query) {
2322
+ const qLower = query.toLowerCase().trim();
2323
+ const byExact = sessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower);
2324
+ if (byExact.length >= 1) return {
2325
+ session: byExact[0],
2326
+ friendlyName: byExact[0].friendlyName
2327
+ };
2328
+ const byPartial = sessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower));
2329
+ if (byPartial.length === 1) return {
2330
+ session: byPartial[0],
2331
+ friendlyName: byPartial[0].friendlyName
2332
+ };
2333
+ if (byPartial.length > 1) {
2334
+ const candidates = byPartial.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
2335
+ throw new Error(`Ambiguous name "${query}" — ${byPartial.length} matches:\n${candidates}\n\nBe more specific or use a UUID prefix.`);
2336
+ }
2337
+ const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));
2338
+ if (byUuid.length === 1) return {
2339
+ session: byUuid[0],
2340
+ friendlyName: byUuid[0].friendlyName
2341
+ };
2342
+ if (byUuid.length > 1) {
2343
+ const candidates = byUuid.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
2344
+ throw new Error(`UUID prefix "${query}" is ambiguous — ${byUuid.length} matches:\n${candidates}\n\nProvide more characters.`);
2345
+ }
2346
+ throw new Error(`No session found matching "${query}".\n\nRun: pai sessions to list sessions.\nRun: pai sessions --all to include transcript-only sessions.`);
2347
+ }
2348
+
2349
+ //#endregion
2350
+ //#region src/cli/lib/aibroker-client.ts
2351
+ /**
2352
+ * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.
2353
+ *
2354
+ * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,
2355
+ * and reads a single newline-terminated JSON response. No class needed —
2356
+ * just a thin async function matching the WatcherClient protocol.
2357
+ *
2358
+ * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).
2359
+ */
2360
+ const DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? "/tmp/aibroker.sock";
2361
+ /**
2362
+ * Call an AIBroker IPC method and return the result.
2363
+ *
2364
+ * Resolves with the `result` field of a successful response.
2365
+ * Rejects if the socket is not available, the call times out, or the
2366
+ * daemon returns an error.
2367
+ *
2368
+ * @param method IPC method name (e.g. "session_content", "send_to_session")
2369
+ * @param params Method parameters object
2370
+ * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)
2371
+ */
2372
+ function callAiBroker(method, params = {}, timeoutMs = 8e3) {
2373
+ return new Promise((resolve, reject) => {
2374
+ const socketPath = DEFAULT_SOCKET;
2375
+ let done = false;
2376
+ let buffer = "";
2377
+ let timer = null;
2378
+ function finish(err, value) {
2379
+ if (done) return;
2380
+ done = true;
2381
+ if (timer !== null) {
2382
+ clearTimeout(timer);
2383
+ timer = null;
2384
+ }
2385
+ try {
2386
+ socket.destroy();
2387
+ } catch {}
2388
+ if (err) reject(err);
2389
+ else resolve(value);
2390
+ }
2391
+ const socket = connect(socketPath, () => {
2392
+ const request = {
2393
+ id: randomUUID(),
2394
+ sessionId: process.env.TERM_SESSION_ID ?? "pai-cli",
2395
+ method,
2396
+ params
2397
+ };
2398
+ const itermId = process.env.ITERM_SESSION_ID;
2399
+ if (itermId) Object.assign(request, { itermSessionId: itermId });
2400
+ socket.write(JSON.stringify(request) + "\n");
2401
+ });
2402
+ socket.on("data", (chunk) => {
2403
+ buffer += chunk.toString("utf8");
2404
+ const nl = buffer.indexOf("\n");
2405
+ if (nl === -1) return;
2406
+ const line = buffer.slice(0, nl);
2407
+ let response;
2408
+ try {
2409
+ response = JSON.parse(line);
2410
+ } catch {
2411
+ finish(/* @__PURE__ */ new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));
2412
+ return;
2413
+ }
2414
+ if (!response.ok) finish(new Error(response.error ?? "AIBroker IPC call failed"));
2415
+ else finish(null, response.result ?? {});
2416
+ });
2417
+ socket.on("error", (e) => {
2418
+ if (e.code === "ENOENT" || e.code === "ECONNREFUSED") finish(/* @__PURE__ */ new Error("AIBroker not running (socket not found)."));
2419
+ else finish(e);
2420
+ });
2421
+ socket.on("end", () => {
2422
+ if (!done) finish(/* @__PURE__ */ new Error("AIBroker IPC connection closed before response."));
2423
+ });
2424
+ timer = setTimeout(() => finish(/* @__PURE__ */ new Error("AIBroker IPC call timed out.")), timeoutMs);
2425
+ });
2426
+ }
2427
+ /**
2428
+ * Fetch all live iTerm2 sessions from AIBroker.
2429
+ * Returns an empty array if AIBroker is not running.
2430
+ */
2431
+ async function fetchLiveSessions() {
2432
+ try {
2433
+ const sessions = (await callAiBroker("session_content", { lineCount: 0 })).sessions;
2434
+ if (!Array.isArray(sessions)) return [];
2435
+ return sessions;
2436
+ } catch {
2437
+ return [];
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Send text to a specific AIBroker session by its iTerm2 sessionId.
2442
+ */
2443
+ async function sendToSession(sessionId, text) {
2444
+ try {
2445
+ await callAiBroker("send_to_session", {
2446
+ sessionId,
2447
+ text
2448
+ });
2449
+ return { ok: true };
2450
+ } catch (e) {
2451
+ return {
2452
+ ok: false,
2453
+ error: String(e)
2454
+ };
2455
+ }
2456
+ }
2457
+
2458
+ //#endregion
2459
+ //#region src/cli/commands/session/recent.ts
2460
+ function fmtSize(bytes) {
2461
+ if (bytes < 1024) return `${bytes}B`;
2462
+ if (bytes < 1048576) return `${Math.round(bytes / 1024)}K`;
2463
+ return `${(bytes / 1048576).toFixed(1)}M`;
2464
+ }
2465
+ function fmtStatus(status) {
2466
+ switch (status) {
2467
+ case "resumable": return chalk.green("resumable");
2468
+ case "stub": return chalk.yellow("stub");
2469
+ case "transcript-only": return chalk.yellow("transcript-only");
2470
+ case "orphan": return chalk.dim("orphan");
2471
+ }
2472
+ }
2473
+ function renderLiveSessions(liveSessions) {
2474
+ if (liveSessions.length === 0) return;
2475
+ console.log("\n" + header("Live Sessions") + "\n");
2476
+ const liveHeaders = [
2477
+ "#",
2478
+ "iTerm2 id",
2479
+ "name",
2480
+ "at prompt",
2481
+ "paiName"
2482
+ ];
2483
+ const liveRows = liveSessions.map((s, idx) => {
2484
+ const shortId = s.sessionId.slice(0, 8);
2485
+ const name = s.name.length > 32 ? s.name.slice(0, 31) + "…" : s.name;
2486
+ const paiName = s.paiName ? s.paiName.length > 20 ? s.paiName.slice(0, 19) + "…" : s.paiName : dim("—");
2487
+ const atPrompt = s.atPrompt ? chalk.green("yes") : chalk.yellow("busy");
2488
+ return [
2489
+ chalk.dim(String(idx + 1)),
2490
+ chalk.cyan(shortId),
2491
+ name,
2492
+ atPrompt,
2493
+ paiName
2494
+ ];
2495
+ });
2496
+ console.log(renderTable(liveHeaders, liveRows));
2497
+ }
2498
+ async function cmdRecent(db, opts) {
2499
+ const limit = parseInt(opts.n ?? "20", 10);
2500
+ const includeAll = opts.all === true;
2501
+ const liveSessions = await fetchLiveSessions();
2502
+ const sessions = scanSessions(db, {
2503
+ limit,
2504
+ filter: includeAll ? "all" : "named"
2505
+ });
2506
+ if (opts.json) {
2507
+ const output = {
2508
+ live: liveSessions.map((s) => ({
2509
+ sessionId: s.sessionId,
2510
+ name: s.name,
2511
+ paiName: s.paiName ?? null,
2512
+ atPrompt: s.atPrompt
2513
+ })),
2514
+ paused: sessions.map((s, idx) => ({
2515
+ idx: idx + 1,
2516
+ uuid: s.uuid,
2517
+ shortId: s.shortId,
2518
+ resumable: s.resumable,
2519
+ sessionStatus: s.sessionStatus,
2520
+ age: fmtAge(s.mtime),
2521
+ mtime: s.mtime,
2522
+ name: s.friendlyName,
2523
+ lastUserPrompt: s.lastUserPrompt,
2524
+ userLines: s.userLines,
2525
+ msgCount: s.msgCount,
2526
+ topLevelSize: s.topLevelSize,
2527
+ decodedPath: s.decodedPath
2528
+ }))
2529
+ };
2530
+ console.log(JSON.stringify(output, null, 2));
2531
+ return;
2532
+ }
2533
+ if (liveSessions.length > 0) renderLiveSessions(liveSessions);
2534
+ if (sessions.length === 0) {
2535
+ if (liveSessions.length === 0) if (includeAll) console.log(err("No sessions found in ~/.claude/projects/."));
2536
+ 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."));
2537
+ return;
2538
+ }
2539
+ const title = includeAll ? "Paused / All Sessions (named + orphans)" : "Paused / Resumable Sessions";
2540
+ console.log("\n" + header(title) + "\n");
2541
+ const headers = [
2542
+ "#",
2543
+ "id",
2544
+ "age",
2545
+ "name / project",
2546
+ "last prompt",
2547
+ "msgs",
2548
+ "size",
2549
+ "status"
2550
+ ];
2551
+ const rows = sessions.map((s, idx) => {
2552
+ const name = s.friendlyName ?? s.decodedPath;
2553
+ const prompt = s.lastUserPrompt ? chalk.dim(s.lastUserPrompt.length > 40 ? s.lastUserPrompt.slice(0, 39) + "…" : s.lastUserPrompt) : dim("—");
2554
+ return [
2555
+ chalk.dim(String(idx + 1)),
2556
+ chalk.cyan(s.shortId),
2557
+ dim(fmtAge(s.mtime)),
2558
+ name.length > 26 ? name.slice(0, 25) + "…" : name,
2559
+ prompt,
2560
+ s.userLines > 0 ? String(s.userLines) : dim("—"),
2561
+ dim(fmtSize(s.topLevelSize)),
2562
+ fmtStatus(s.sessionStatus)
2563
+ ];
2564
+ });
2565
+ console.log(renderTable(headers, rows));
2566
+ console.log("\n" + dim("Go to a session: ") + chalk.white("pai resume <name>") + "\n" + (liveSessions.length > 0 ? dim("Pause all live: ") + chalk.white("pai pause all") + "\n" : "") + (includeAll ? "" : dim("Show all sessions: ") + chalk.white("pai sessions --all") + "\n"));
2567
+ }
2568
+
2569
+ //#endregion
2570
+ //#region src/cli/commands/session/goto.ts
2571
+ /**
2572
+ * Run claude --resume <uuid> --print --output-format=json "_" in the given cwd.
2573
+ * Returns ok=true if the session is resumable (exit 0 and no "No conversation found"
2574
+ * in stderr). Timeout: 5 seconds.
2575
+ */
2576
+ function probeResume(uuid, cwd) {
2577
+ const result = spawnSync("claude", [
2578
+ "--resume",
2579
+ uuid,
2580
+ "--print",
2581
+ "--output-format=json",
2582
+ "_"
2583
+ ], {
2584
+ cwd,
2585
+ timeout: 5e3,
2586
+ env: process.env,
2587
+ stdio: [
2588
+ "ignore",
2589
+ "ignore",
2590
+ "pipe"
2591
+ ]
2592
+ });
2593
+ if (result.error) return {
2594
+ ok: false,
2595
+ reason: `spawn error: ${result.error.message}`
2596
+ };
2597
+ const stderr = result.stderr?.toString("utf8") ?? "";
2598
+ if (stderr.toLowerCase().includes("no conversation found") || stderr.toLowerCase().includes("session not found")) return {
2599
+ ok: false,
2600
+ reason: "No conversation found for this UUID"
2601
+ };
2602
+ if (result.status !== 0) return {
2603
+ ok: false,
2604
+ reason: `claude exited ${result.status ?? "signal"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : ""}`
2605
+ };
2606
+ return { ok: true };
2607
+ }
2608
+ function cmdGoto(db, query, opts) {
2609
+ const allSessions = scanSessions(db, {
2610
+ limit: 500,
2611
+ filter: "named"
2612
+ });
2613
+ let resolved;
2614
+ try {
2615
+ resolved = resolveSessionByNameOrId(allSessions, query);
2616
+ } catch (resolveErr) {
2617
+ console.error(err(String(resolveErr).replace(/^Error: /, "")));
2618
+ process.exit(1);
2619
+ }
2620
+ const { session: matchedSession, friendlyName } = resolved;
2621
+ let resumableUuid;
2622
+ let resumableSession;
2623
+ if (matchedSession.resumable) {
2624
+ resumableUuid = matchedSession.uuid;
2625
+ resumableSession = matchedSession;
2626
+ } else if (matchedSession.encodedDir) {
2627
+ const sameProject = allSessions.filter((s) => s.encodedDir === matchedSession.encodedDir && s.resumable);
2628
+ sameProject.sort((a, b) => b.mtime - a.mtime);
2629
+ if (sameProject.length > 0) {
2630
+ resumableSession = sameProject[0];
2631
+ resumableUuid = resumableSession.uuid;
2632
+ }
2633
+ }
2634
+ const rawDir = matchedSession.clcDirectory ?? matchedSession.registryRootPath ?? matchedSession.decodedPath;
2635
+ let projectDir;
2636
+ try {
2637
+ projectDir = realpathSync(rawDir);
2638
+ } catch {
2639
+ 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.`));
2640
+ process.exit(1);
2641
+ }
2642
+ const name = friendlyName ?? query;
2643
+ const promptArg = `/Name ${name}\ngo`;
2644
+ if (opts.dryRun) {
2645
+ if (resumableUuid) {
2646
+ const argvResume = `claude --resume ${resumableUuid} --name "${name}" "/Name ${name}\\ngo"`;
2647
+ const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
2648
+ console.log("\n" + chalk.bold("Dry run — would probe then exec (RESUME path):") + "\n");
2649
+ console.log(` cwd: ${chalk.cyan(projectDir)}`);
2650
+ console.log(` probe: claude --resume ${resumableUuid} --print --output-format=json "_"`);
2651
+ console.log(` argv: ${chalk.white(argvResume)}`);
2652
+ console.log(` fallback: ${chalk.yellow(argvFresh)}`);
2653
+ if (resumableSession) {
2654
+ console.log(`\n uuid: ${resumableSession.uuid}`);
2655
+ console.log(` age: ${fmtAge(resumableSession.mtime)}`);
2656
+ console.log(` status: ${resumableSession.sessionStatus}`);
2657
+ console.log(` sys: ${resumableSession.topLevelSystemLines} system lines`);
2658
+ }
2659
+ } else {
2660
+ const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
2661
+ console.log("\n" + chalk.bold("Dry run — would exec (FRESH path, no resumable UUID):") + "\n");
2662
+ console.log(` cwd: ${chalk.cyan(projectDir)}`);
2663
+ console.log(` argv: ${chalk.white(argvFresh)}`);
2664
+ }
2665
+ console.log();
2666
+ return;
2667
+ }
2668
+ if (resumableUuid) {
2669
+ const probe = probeResume(resumableUuid, projectDir);
2670
+ if (probe.ok) {
2671
+ const result = spawnSync("claude", [
2672
+ "--resume",
2673
+ resumableUuid,
2674
+ "--name",
2675
+ name,
2676
+ promptArg
2677
+ ], {
2678
+ cwd: projectDir,
2679
+ stdio: "inherit",
2680
+ env: process.env
2681
+ });
2682
+ if (result.error) {
2683
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
2684
+ process.exit(1);
2685
+ }
2686
+ process.exit(result.status ?? 0);
2687
+ } else {
2688
+ process.stderr.write(chalk.yellow(`\n Resume failed for ${resumableUuid.slice(0, 8)}: ${probe.reason ?? "unknown error"}\n Starting fresh session in same directory.\n\n`));
2689
+ const result = spawnSync("claude", [
2690
+ "--name",
2691
+ name,
2692
+ promptArg
2693
+ ], {
2694
+ cwd: projectDir,
2695
+ stdio: "inherit",
2696
+ env: process.env
2697
+ });
2698
+ if (result.error) {
2699
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
2700
+ process.exit(1);
2701
+ }
2702
+ process.exit(result.status ?? 0);
2703
+ }
2704
+ } else {
2705
+ const result = spawnSync("claude", [
2706
+ "--name",
2707
+ name,
2708
+ promptArg
2709
+ ], {
2710
+ cwd: projectDir,
2711
+ stdio: "inherit",
2712
+ env: process.env
2713
+ });
2714
+ if (result.error) {
2715
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
2716
+ process.exit(1);
2717
+ }
2718
+ process.exit(result.status ?? 0);
2719
+ }
1816
2720
  }
1817
2721
 
1818
2722
  //#endregion
1819
- //#region src/cli/commands/session/handover.ts
2723
+ //#region src/cli/commands/session/pause.ts
1820
2724
  const HANDOVER_TODO_LOCATIONS = [
1821
2725
  "Notes/TODO.md",
1822
2726
  ".claude/Notes/TODO.md",
@@ -1854,28 +2758,19 @@ function stripContinueSection(content) {
1854
2758
  while (after.length > 0 && after[0].trim() === "") after.shift();
1855
2759
  return [...before, ...after].join("\n");
1856
2760
  }
1857
- function cmdHandover(db, projectSlug, numberOrLatest) {
1858
- let project;
1859
- if (projectSlug) {
1860
- project = db.prepare("SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?").get(projectSlug);
1861
- if (!project) process.exit(0);
1862
- } else {
1863
- const cwd = process.cwd();
1864
- const row = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
1865
- FROM projects
1866
- WHERE ? LIKE root_path || '%'
1867
- ORDER BY length(root_path) DESC
1868
- LIMIT 1`).get(cwd);
1869
- if (!row) process.exit(0);
1870
- project = row;
1871
- }
1872
- let session;
1873
- const nol = numberOrLatest ?? "latest";
1874
- if (nol === "latest") session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
1875
- else {
1876
- const num = parseInt(nol, 10);
1877
- if (!isNaN(num)) session = db.prepare("SELECT * FROM sessions WHERE project_id = ? AND number = ?").get(project.id, num);
2761
+ function cmdPause(db, opts) {
2762
+ const cwd = process.cwd();
2763
+ const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
2764
+ FROM projects
2765
+ WHERE ? LIKE root_path || '%'
2766
+ ORDER BY length(root_path) DESC
2767
+ LIMIT 1`).get(cwd);
2768
+ if (!project) {
2769
+ console.error(chalk.red("pai session pause: ") + `Current directory is not within a registered PAI project.
2770
+ cwd: ${cwd}`);
2771
+ process.exit(1);
1878
2772
  }
2773
+ const session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
1879
2774
  const todo = findProjectTodo(project.root_path);
1880
2775
  let todoPath;
1881
2776
  let existingContent;
@@ -1884,23 +2779,23 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
1884
2779
  existingContent = todo.content;
1885
2780
  } else {
1886
2781
  const notesDir = join(project.root_path, "Notes");
1887
- try {
2782
+ if (!opts.dryRun) try {
1888
2783
  if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });
1889
- } catch {
1890
- process.exit(0);
2784
+ } catch (mkdirErr) {
2785
+ console.error(chalk.red("pai session pause: ") + `Could not create Notes directory: ${String(mkdirErr)}`);
2786
+ process.exit(1);
1891
2787
  }
1892
2788
  todoPath = join(notesDir, "TODO.md");
1893
2789
  existingContent = "";
1894
2790
  }
1895
2791
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1896
- const cwd = process.cwd();
1897
2792
  let sessionLine;
1898
2793
  if (session) {
1899
2794
  const num = String(session.number).padStart(4, "0");
1900
2795
  const titlePart = session.title || session.slug || "Session";
1901
2796
  sessionLine = `${num} - ${session.date} - ${titlePart}`;
1902
2797
  } else sessionLine = "Unknown session";
1903
- const newContent = [
2798
+ const continueBlock = [
1904
2799
  "## Continue",
1905
2800
  "",
1906
2801
  `> **Last session:** ${sessionLine}`,
@@ -1910,51 +2805,322 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
1910
2805
  "",
1911
2806
  "---",
1912
2807
  ""
1913
- ].join("\n") + stripContinueSection(existingContent).trimStart();
1914
- const tmpPath = `${todoPath}.handover.tmp`;
1915
- try {
1916
- writeFileSync(tmpPath, newContent, "utf8");
1917
- renameSync(tmpPath, todoPath);
1918
- } catch {
2808
+ ].join("\n");
2809
+ const newContent = continueBlock + stripContinueSection(existingContent).trimStart();
2810
+ if (opts.dryRun) {
2811
+ console.log("\n" + chalk.bold("Dry run — would write to:") + " " + chalk.cyan(todoPath));
2812
+ console.log(chalk.dim("─".repeat(60)));
2813
+ console.log(chalk.dim(continueBlock));
2814
+ console.log(chalk.dim("─".repeat(60)));
2815
+ } else {
2816
+ const tmpPath = `${todoPath}.pause.tmp`;
1919
2817
  try {
1920
- if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
1921
- } catch {}
1922
- process.exit(0);
2818
+ writeFileSync(tmpPath, newContent, "utf8");
2819
+ renameSync(tmpPath, todoPath);
2820
+ } catch (writeErr) {
2821
+ try {
2822
+ if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
2823
+ } catch {}
2824
+ console.error(chalk.red("pai session pause: ") + `Failed to write TODO.md: ${String(writeErr)}`);
2825
+ process.exit(1);
2826
+ }
2827
+ console.log(chalk.green(" ## Continue written to: ") + chalk.cyan(todoPath));
1923
2828
  }
1924
- process.exit(0);
2829
+ const box = [
2830
+ "",
2831
+ chalk.bgRed.white.bold(" IMPORTANT: How to exit safely "),
2832
+ "",
2833
+ chalk.yellow(" Inside the Claude Code session, type: " + chalk.white.bold("/exit") + chalk.yellow(" (then press Enter)")),
2834
+ "",
2835
+ chalk.red.bold(" DO NOT press Ctrl+C."),
2836
+ "",
2837
+ chalk.dim(" Ctrl+C bypasses PAI's stop-hook, which means:"),
2838
+ chalk.dim(" - No project snapshot is written to the top-level jsonl"),
2839
+ chalk.dim(" - The session becomes orphaned (cannot --resume next time)"),
2840
+ chalk.dim(" - pai session goto will launch a fresh session instead of resuming"),
2841
+ "",
2842
+ chalk.dim(" The ## Continue checkpoint in TODO.md is already saved."),
2843
+ chalk.dim(" Use /exit to preserve full session resumability."),
2844
+ ""
2845
+ ].join("\n");
2846
+ console.log(box);
2847
+ }
2848
+
2849
+ //#endregion
2850
+ //#region src/cli/commands/session/end.ts
2851
+ /** PAI_DIR — mirrors pai-paths.ts resolution. */
2852
+ function getPaiDir() {
2853
+ const envDir = process.env.PAI_DIR;
2854
+ if (envDir) try {
2855
+ return realpathSync(envDir);
2856
+ } catch {
2857
+ return envDir;
2858
+ }
2859
+ return join(homedir(), ".claude");
2860
+ }
2861
+ /**
2862
+ * Find the notes directory for a project — checks local first, then central.
2863
+ * Returns null if neither exists (dry-run safe: don't create).
2864
+ */
2865
+ function findNotesDir$1(rootPath, encodedDir) {
2866
+ for (const rel of [
2867
+ "Notes",
2868
+ "notes",
2869
+ ".claude/Notes"
2870
+ ]) {
2871
+ const p = join(rootPath, rel);
2872
+ if (existsSync(p)) return p;
2873
+ }
2874
+ const central = join(getPaiDir(), "projects", encodedDir, "Notes");
2875
+ if (existsSync(central)) return central;
2876
+ return null;
2877
+ }
2878
+ /**
2879
+ * Find the current (latest) session note inside notesDir.
2880
+ * Searches YYYY/MM subdirectory (current month, then previous month),
2881
+ * then flat notesDir as legacy fallback.
2882
+ */
2883
+ function findLatestNote(notesDir) {
2884
+ const findIn = (dir) => {
2885
+ if (!existsSync(dir)) return null;
2886
+ const files = readdirSync(dir).filter((f) => /^\d{3,4}[\s_-].*\.md$/.test(f)).sort((a, b) => {
2887
+ return parseInt(a.match(/^(\d+)/)?.[1] ?? "0", 10) - parseInt(b.match(/^(\d+)/)?.[1] ?? "0", 10);
2888
+ });
2889
+ return files.length > 0 ? join(dir, files[files.length - 1]) : null;
2890
+ };
2891
+ const now = /* @__PURE__ */ new Date();
2892
+ const current = findIn(join(notesDir, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0")));
2893
+ if (current) return current;
2894
+ const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
2895
+ const prevFound = findIn(join(notesDir, String(prev.getFullYear()), String(prev.getMonth() + 1).padStart(2, "0")));
2896
+ if (prevFound) return prevFound;
2897
+ return findIn(notesDir);
2898
+ }
2899
+ function finalizeNote(notePath) {
2900
+ const content = readFileSync(notePath, "utf-8");
2901
+ if (content.includes("**Status:** Completed")) return {
2902
+ finalized: false,
2903
+ path: notePath
2904
+ };
2905
+ let updated = content.replace("**Status:** In Progress", "**Status:** Completed");
2906
+ if (!updated.includes("**Completed:**")) {
2907
+ const completionTime = (/* @__PURE__ */ new Date()).toISOString();
2908
+ updated = updated.replace("---\n\n## Work Done", `**Completed:** ${completionTime}\n\n---\n\n## Work Done`);
2909
+ }
2910
+ const tmp = `${notePath}.end.tmp`;
2911
+ writeFileSync(tmp, updated, "utf-8");
2912
+ renameSync(tmp, notePath);
2913
+ return {
2914
+ finalized: true,
2915
+ path: notePath
2916
+ };
2917
+ }
2918
+ function cmdEnd(db, opts) {
2919
+ cmdPause(db, opts);
2920
+ const cwd = process.cwd();
2921
+ const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
2922
+ FROM projects
2923
+ WHERE ? LIKE root_path || '%'
2924
+ ORDER BY length(root_path) DESC
2925
+ LIMIT 1`).get(cwd);
2926
+ if (!project) return;
2927
+ const notesDir = findNotesDir$1(project.root_path, project.encoded_dir ?? "");
2928
+ if (!notesDir) {
2929
+ console.log(chalk.dim(" (No session note found — notes directory does not exist yet.)"));
2930
+ printEndBox(opts.dryRun);
2931
+ return;
2932
+ }
2933
+ const notePath = findLatestNote(notesDir);
2934
+ if (!notePath) {
2935
+ console.log(chalk.dim(" (No session note found in notes directory.)"));
2936
+ printEndBox(opts.dryRun);
2937
+ return;
2938
+ }
2939
+ if (opts.dryRun) {
2940
+ console.log("\n" + chalk.bold("Dry run — would finalize session note:") + " " + chalk.cyan(notePath));
2941
+ console.log(chalk.dim(" Would replace: **Status:** In Progress\n with: **Status:** Completed"));
2942
+ console.log(chalk.dim(" Would add: **Completed:** <timestamp>"));
2943
+ } else {
2944
+ const { finalized, path } = finalizeNote(notePath);
2945
+ if (finalized) console.log(chalk.green(" Session note finalized: ") + chalk.cyan(basename(path)));
2946
+ else console.log(chalk.dim(` Session note already marked Completed: ${basename(notePath)}`));
2947
+ }
2948
+ printEndBox(opts.dryRun);
2949
+ }
2950
+ function printEndBox(dryRun) {
2951
+ const label = dryRun ? " (dry-run)" : "";
2952
+ const box = [
2953
+ "",
2954
+ chalk.bgRed.white.bold(` SESSION ENDING${label}: How to exit safely `),
2955
+ "",
2956
+ chalk.yellow(" Inside the Claude Code session, type: " + chalk.white.bold("/exit") + chalk.yellow(" (then press Enter)")),
2957
+ "",
2958
+ chalk.red.bold(" DO NOT press Ctrl+C."),
2959
+ "",
2960
+ chalk.dim(" Ctrl+C bypasses PAI's stop-hook, which means:"),
2961
+ chalk.dim(" - The session note is NOT written to by the stop-hook"),
2962
+ chalk.dim(" - The session becomes orphaned (cannot --resume next time)"),
2963
+ chalk.dim(" - The final session summary is never generated"),
2964
+ "",
2965
+ chalk.dim(" ## Continue checkpoint and session note status are already saved."),
2966
+ chalk.dim(" Use /exit to let PAI's stop-hook finalize the session fully."),
2967
+ ""
2968
+ ].join("\n");
2969
+ console.log(box);
2970
+ }
2971
+
2972
+ //#endregion
2973
+ //#region src/cli/commands/session/pause-all.ts
2974
+ /**
2975
+ * pai pause all [--exit] [--dry-run]
2976
+ *
2977
+ * Pause every live Claude Code session that AIBroker knows about.
2978
+ *
2979
+ * For each live session returned by AIBroker's session_content IPC method:
2980
+ * 1. Send "pause session" to the iTerm2 pane via send_to_session.
2981
+ * 2. Optionally send "\n/exit\n" after a short delay (--exit flag).
2982
+ * 3. Print a summary of what was sent and to which sessions.
2983
+ *
2984
+ * If AIBroker is not running, prints a clear error and exits.
2985
+ * If no live sessions are found, reports "nothing to pause".
2986
+ */
2987
+ function sleep(ms) {
2988
+ return new Promise((resolve) => setTimeout(resolve, ms));
2989
+ }
2990
+ function sessionLabel(s) {
2991
+ const name = s.paiName ?? s.name;
2992
+ return `${chalk.cyan(s.sessionId.slice(0, 8))} ${chalk.bold(name)}`;
2993
+ }
2994
+ async function cmdPauseAll(opts) {
2995
+ const waitMs = opts.wait ?? 5e3;
2996
+ let liveSessions;
2997
+ try {
2998
+ liveSessions = await fetchLiveSessions();
2999
+ } catch (e) {
3000
+ console.error(err("AIBroker is not running. Cannot list live sessions."));
3001
+ console.error(dim(" Start AIBroker or run `pai pause` from each session manually."));
3002
+ process.exitCode = 1;
3003
+ return;
3004
+ }
3005
+ const claudeSessions = liveSessions.filter((s) => s.paiName !== null && s.paiName !== void 0 && s.paiName !== "");
3006
+ const skipped = liveSessions.length - claudeSessions.length;
3007
+ if (skipped > 0) process.stderr.write(`Skipping ${skipped} non-Claude tab${skipped === 1 ? "" : "s"} (bare shells).\n`);
3008
+ if (claudeSessions.length === 0) {
3009
+ console.log(warn("No live Claude sessions found via AIBroker. Nothing to pause."));
3010
+ return;
3011
+ }
3012
+ if (opts.dryRun) {
3013
+ console.log("\n" + header("Dry Run — Would Pause These Sessions") + "\n");
3014
+ for (const s of claudeSessions) {
3015
+ console.log(" " + sessionLabel(s));
3016
+ console.log(dim(" → send: \"pause session\""));
3017
+ if (opts.exit) console.log(dim(` → wait ${waitMs}ms then send: "/exit"`));
3018
+ }
3019
+ console.log();
3020
+ return;
3021
+ }
3022
+ console.log("\n" + header("Pausing All Live Sessions") + "\n" + dim(` ${claudeSessions.length} Claude session(s) via AIBroker`) + "\n");
3023
+ const results = [];
3024
+ for (const s of claudeSessions) {
3025
+ process.stdout.write(" " + sessionLabel(s) + " … ");
3026
+ const pauseResult = await sendToSession(s.sessionId, "pause session\n");
3027
+ if (!pauseResult.ok) {
3028
+ console.log(err("FAILED: " + (pauseResult.error ?? "unknown error")));
3029
+ results.push({
3030
+ session: s,
3031
+ pauseOk: false,
3032
+ error: pauseResult.error
3033
+ });
3034
+ continue;
3035
+ }
3036
+ console.log(ok("paused"));
3037
+ results.push({
3038
+ session: s,
3039
+ pauseOk: true
3040
+ });
3041
+ }
3042
+ if (opts.exit) {
3043
+ const pausedSessions = results.filter((r) => r.pauseOk).map((r) => r.session);
3044
+ if (pausedSessions.length > 0) {
3045
+ console.log("\n" + dim(` Waiting ${waitMs / 1e3}s for sessions to save state…`));
3046
+ await sleep(waitMs);
3047
+ console.log();
3048
+ for (const s of pausedSessions) {
3049
+ process.stdout.write(" " + sessionLabel(s) + " exiting … ");
3050
+ const exitResult = await sendToSession(s.sessionId, "/exit\n");
3051
+ const rec = results.find((r) => r.session.sessionId === s.sessionId);
3052
+ rec.exitOk = exitResult.ok;
3053
+ if (!exitResult.ok) console.log(warn("exit failed: " + (exitResult.error ?? "unknown")));
3054
+ else console.log(ok("exited"));
3055
+ }
3056
+ }
3057
+ }
3058
+ const total = results.length;
3059
+ const succeeded = results.filter((r) => r.pauseOk).length;
3060
+ const failed = total - succeeded;
3061
+ console.log();
3062
+ if (failed === 0) console.log(ok(`All ${total} session(s) paused successfully.`));
3063
+ else {
3064
+ console.log(warn(`${succeeded}/${total} session(s) paused. `) + err(`${failed} failed.`));
3065
+ for (const r of results.filter((r) => !r.pauseOk)) {
3066
+ const label = r.session.paiName ?? r.session.name;
3067
+ console.log(err(` ${label}: ${r.error ?? "unknown error"}`));
3068
+ }
3069
+ }
3070
+ if (opts.exit) {
3071
+ const exitOk = results.filter((r) => r.exitOk).length;
3072
+ console.log(dim(` /exit sent to ${exitOk} session(s).`));
3073
+ }
3074
+ console.log();
1925
3075
  }
1926
3076
 
1927
3077
  //#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);
3078
+ //#region src/cli/commands/session/sessions-index.ts
3079
+ function registerSessionsCommands(sessionsCmd, getDb) {
3080
+ 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(async (opts) => {
3081
+ await cmdRecent(getDb(), opts);
1932
3082
  });
1933
- sessionCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
3083
+ 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("--dry-run", "Print the exact argv and cwd, then exit without launching").action((nameOrId, opts) => {
3084
+ cmdGoto(getDb(), nameOrId, { dryRun: opts.dryRun });
3085
+ });
3086
+ 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) => {
3087
+ cmdPause(getDb(), opts);
3088
+ });
3089
+ sessionsCmd.command("pause-all").description("Pause every live Claude Code session via AIBroker.\nRequires AIBroker to be running. Sends 'pause session' to each live iTerm2 pane.\nTop-level short form: pai pause all").option("--exit", "Also send /exit to each session after it has saved state").option("--wait <ms>", "Milliseconds to wait before /exit (default: 5000)", "5000").option("--dry-run", "Show what would be sent without actually sending").action(async (opts) => {
3090
+ await cmdPauseAll({
3091
+ exit: opts.exit,
3092
+ dryRun: opts.dryRun,
3093
+ wait: opts.wait !== void 0 ? parseInt(opts.wait, 10) : void 0
3094
+ });
3095
+ });
3096
+ sessionsCmd.command("end").description("Finalize a session: write ## Continue checkpoint + mark session note Completed.\nRecommended short form: pai end\nUse /exit inside Claude Code after running this command.").option("--dry-run", "Preview all changes without writing them").action((opts) => {
3097
+ cmdEnd(getDb(), opts);
3098
+ });
3099
+ sessionsCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
1934
3100
  cmdInfo(getDb(), projectSlug, number);
1935
3101
  });
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) => {
3102
+ 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
3103
  cmdRename(getDb(), projectSlug, number, newSlug);
1938
3104
  });
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) => {
3105
+ 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
3106
  cmdSlug(getDb(), projectSlug, number, opts);
1941
3107
  });
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) => {
3108
+ sessionsCmd.command("tag <project-slug> <number> [tags...]").description("Set or show tags on a session.").action((projectSlug, number, tags) => {
1943
3109
  cmdTag(getDb(), projectSlug, number, tags);
1944
3110
  });
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) => {
3111
+ 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
3112
  cmdRoute(getDb(), projectSlug, number, targetProject, opts);
1947
3113
  });
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) => {
3114
+ 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
3115
  cmdHandover(getDb(), projectSlug, sessionId);
1950
3116
  });
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) => {
3117
+ 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
3118
  cmdCheckpoint(message, opts);
1953
3119
  });
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) => {
3120
+ 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
3121
  cmdActive(getDb(), opts);
1956
3122
  });
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) => {
3123
+ 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
3124
  await cmdAutoRoute(opts);
1959
3125
  });
1960
3126
  }
@@ -2302,8 +3468,8 @@ function analyzeProject(db, project) {
2302
3468
  async function countVectorDbPaths(oldPaths) {
2303
3469
  if (oldPaths.length === 0) return 0;
2304
3470
  try {
2305
- const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
2306
- const { PostgresBackend } = await import("../postgres-BGERehmX.mjs");
3471
+ const { loadConfig } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
3472
+ const { PostgresBackend } = await import("../postgres-Cp25xZes.mjs");
2307
3473
  const config = loadConfig();
2308
3474
  if (config.storageBackend !== "postgres") return 0;
2309
3475
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -2323,8 +3489,8 @@ async function countVectorDbPaths(oldPaths) {
2323
3489
  async function updateVectorDbPaths(moves) {
2324
3490
  if (moves.length === 0) return 0;
2325
3491
  try {
2326
- const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
2327
- const { PostgresBackend } = await import("../postgres-BGERehmX.mjs");
3492
+ const { loadConfig } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
3493
+ const { PostgresBackend } = await import("../postgres-Cp25xZes.mjs");
2328
3494
  const config = loadConfig();
2329
3495
  if (config.storageBackend !== "postgres") return 0;
2330
3496
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -2647,269 +3813,6 @@ function registerSessionCleanupCommand(sessionCmd, getDb) {
2647
3813
  });
2648
3814
  }
2649
3815
 
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
3816
  //#endregion
2914
3817
  //#region src/cli/commands/registry/migrate.ts
2915
3818
  /** Registry migrate command: import data from ~/.claude/session-registry.json. */
@@ -3015,7 +3918,7 @@ function cmdLookup(db, fsPath) {
3015
3918
  process.stdout.write(row.slug + "\n");
3016
3919
  }
3017
3920
  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) => {
3921
+ 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
3922
  if (opts.showDirs) {
3020
3923
  const config = loadScanConfig();
3021
3924
  if (!config.scan_dirs.length) {
@@ -3053,7 +3956,7 @@ function registerRegistryCommands(registryCmd, getDb) {
3053
3956
  console.log(ok(`Removed scan directory: ${bold(display)}`));
3054
3957
  } else console.log(warn(`Not found in config: ${display}`));
3055
3958
  }
3056
- if (!opts.addDir && !opts.removeDir) cmdScan(getDb());
3959
+ if (!opts.addDir && !opts.removeDir) cmdScan(getDb(), { quick: opts.quick });
3057
3960
  });
3058
3961
  registryCmd.command("migrate").description("Import data from ~/.claude/session-registry.json").action(() => {
3059
3962
  cmdMigrate$1(getDb());
@@ -3794,8 +4697,8 @@ function cmdLogs(opts) {
3794
4697
  }
3795
4698
  function registerDaemonCommands(daemonCmd) {
3796
4699
  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);
4700
+ const { serve } = await import("../daemon-B-vBWU1i.mjs").then((n) => n.t);
4701
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
3799
4702
  ensureConfigDir();
3800
4703
  await serve(lc());
3801
4704
  });
@@ -6416,7 +7319,7 @@ async function cmdExplore(note, opts) {
6416
7319
  const depth = parseInt(opts.depth ?? "3", 10);
6417
7320
  const direction = opts.direction ?? "both";
6418
7321
  const mode = opts.mode ?? "all";
6419
- const { zettelExplore } = await import("../zettelkasten-BdaMzTGQ.mjs");
7322
+ const { zettelExplore } = await import("../zettelkasten-B-94vghH.mjs");
6420
7323
  const result = zettelExplore(getFedDb(), {
6421
7324
  startNote: note,
6422
7325
  depth,
@@ -6480,7 +7383,7 @@ async function cmdHealth(opts) {
6480
7383
  const projectPath = opts.project;
6481
7384
  const recentDays = parseInt(opts.days ?? "30", 10);
6482
7385
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
6483
- const { zettelHealth } = await import("../zettelkasten-BdaMzTGQ.mjs");
7386
+ const { zettelHealth } = await import("../zettelkasten-B-94vghH.mjs");
6484
7387
  const result = zettelHealth(getFedDb(), {
6485
7388
  scope,
6486
7389
  projectPath,
@@ -6549,7 +7452,7 @@ async function cmdSurprise(note, opts) {
6549
7452
  const limit = parseInt(opts.limit ?? "10", 10);
6550
7453
  const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
6551
7454
  const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
6552
- const { zettelSurprise } = await import("../zettelkasten-BdaMzTGQ.mjs");
7455
+ const { zettelSurprise } = await import("../zettelkasten-B-94vghH.mjs");
6553
7456
  const db = getFedDb();
6554
7457
  console.log();
6555
7458
  console.log(header(" PAI Zettel Surprise"));
@@ -6602,7 +7505,7 @@ async function cmdSuggest(note, opts) {
6602
7505
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6603
7506
  const limit = parseInt(opts.limit ?? "5", 10);
6604
7507
  const excludeLinked = opts.excludeLinked !== false;
6605
- const { zettelSuggest } = await import("../zettelkasten-BdaMzTGQ.mjs");
7508
+ const { zettelSuggest } = await import("../zettelkasten-B-94vghH.mjs");
6606
7509
  const db = getFedDb();
6607
7510
  console.log();
6608
7511
  console.log(header(" PAI Zettel Suggest"));
@@ -6653,7 +7556,7 @@ async function cmdConverse(question, opts) {
6653
7556
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6654
7557
  const depth = parseInt(opts.depth ?? "2", 10);
6655
7558
  const limit = parseInt(opts.limit ?? "15", 10);
6656
- const { zettelConverse } = await import("../zettelkasten-BdaMzTGQ.mjs");
7559
+ const { zettelConverse } = await import("../zettelkasten-B-94vghH.mjs");
6657
7560
  const db = getFedDb();
6658
7561
  console.log();
6659
7562
  console.log(header(" PAI Zettel Converse"));
@@ -6714,7 +7617,7 @@ async function cmdThemes(opts) {
6714
7617
  const minClusterSize = parseInt(opts.minSize ?? "3", 10);
6715
7618
  const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
6716
7619
  const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
6717
- const { zettelThemes } = await import("../zettelkasten-BdaMzTGQ.mjs");
7620
+ const { zettelThemes } = await import("../zettelkasten-B-94vghH.mjs");
6718
7621
  const db = getFedDb();
6719
7622
  console.log();
6720
7623
  console.log(header(" PAI Zettel Themes"));
@@ -8129,13 +9032,17 @@ function registerDbCommands(dbCmd) {
8129
9032
  * PAI Knowledge OS — CLI entry point
8130
9033
  *
8131
9034
  * Command tree:
8132
- * pai project add|list|info|archive|unarchive|move|tag|alias|edit
8133
- * pai session list|info
9035
+ * pai sessions list|goto|pause|info|rename|... (canonical plural namespace)
9036
+ * pai projects list|cd|add|info|... (canonical plural namespace)
8134
9037
  * pai registry scan|migrate|stats|rebuild
8135
9038
  * pai memory index|search|status
8136
9039
  * pai search <query> (placeholder — Phase 3)
8137
9040
  * pai update
8138
9041
  * pai version
9042
+ *
9043
+ * Daily verb shortcuts (top-level):
9044
+ * pai pause / pai end / pai resume / pai cd
9045
+ * pai sessions / pai projects / pai notes
8139
9046
  */
8140
9047
  function getVersion() {
8141
9048
  try {
@@ -8156,11 +9063,30 @@ function getDb() {
8156
9063
  return _db;
8157
9064
  }
8158
9065
  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);
9066
+ program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit").addHelpText("after", `
9067
+ Daily verbs (short forms):
9068
+ pai pause Save state + display safe-exit reminder
9069
+ pai end Finalize session: pause + mark note Completed
9070
+ pai resume <name> Go to a session (resume or start fresh)
9071
+ pai cd <name> cd to a project directory
9072
+
9073
+ Listings:
9074
+ pai sessions Resumable sessions catalog
9075
+ pai projects Known project directories
9076
+ pai notes Markdown session notes
9077
+
9078
+ Subcommands (power users):
9079
+ pai sessions ... Session management (list, goto, pause, end, ...)
9080
+ pai projects ... Project management (cd, list, ...)
9081
+ pai registry ... Registry maintenance (scan, ...)
9082
+ pai memory ... Memory engine (index, search, ...)
9083
+
9084
+ Shell integration:
9085
+ eval "$(pai shell-init)" # Add to ~/.zshrc for pai cd to work`);
9086
+ registerProjectsCommands(program.command("projects").description("Manage registered projects (list, cd, add, info, ...)"), getDb);
9087
+ const sessionsCmd = program.command("sessions").description("Session management (list, goto, pause, ...)");
9088
+ registerSessionsCommands(sessionsCmd, getDb);
9089
+ registerSessionCleanupCommand(sessionsCmd, getDb);
8164
9090
  registerRegistryCommands(program.command("registry").description("Registry maintenance: scan, migrate, stats, rebuild"), getDb);
8165
9091
  registerMemoryCommands(program.command("memory").description("Memory engine: index, search, and status"), getDb);
8166
9092
  registerMcpCommands(program.command("mcp").description("MCP server management: install and status"));
@@ -8179,6 +9105,67 @@ registerObservationCommands(program.command("observation").description("Observat
8179
9105
  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
9106
  cmdGo(getDb(), query);
8181
9107
  });
9108
+ program.command("pause [target]").description("Save state and display safe-exit instructions for the current session.\nWrites a ## Continue checkpoint to the project's TODO.md.\nUse `pai pause all` to pause every live session via AIBroker.\nLong form: pai sessions pause").option("--dry-run", "Preview changes without writing them").option("--exit", "(pause all only) Also send /exit to each session after pausing").option("--wait <ms>", "(pause all only) Milliseconds to wait before /exit (default: 5000)", "5000").action(async (target, opts) => {
9109
+ if (target === "all") await cmdPauseAll({
9110
+ exit: opts.exit,
9111
+ dryRun: opts.dryRun,
9112
+ wait: opts.wait !== void 0 ? parseInt(opts.wait, 10) : void 0
9113
+ });
9114
+ else {
9115
+ if (target !== void 0) {
9116
+ console.error(`Unknown target: ${target}. Did you mean 'pai pause all'?`);
9117
+ process.exitCode = 1;
9118
+ return;
9119
+ }
9120
+ cmdPause(getDb(), { dryRun: opts.dryRun });
9121
+ }
9122
+ });
9123
+ program.command("end").description("Finalize a session: save state, mark note Completed, display safe-exit instructions.\nLong form: pai sessions end").option("--dry-run", "Preview all changes without writing them").action((opts) => {
9124
+ cmdEnd(getDb(), opts);
9125
+ });
9126
+ program.command("resume <name>").description("Go to a session by name: resume if resumable, start fresh otherwise.\nLong form: pai sessions goto <name>").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((name, opts) => {
9127
+ cmdGoto(getDb(), name, { dryRun: opts.dryRun });
9128
+ });
9129
+ 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) => {
9130
+ const project = resolveIdentifier(getDb(), identifier);
9131
+ if (!project) {
9132
+ console.error(`Project not found: ${identifier}`);
9133
+ process.exit(1);
9134
+ }
9135
+ process.stdout.write(project.root_path + "\n");
9136
+ });
9137
+ 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) => {
9138
+ cmdList$2(getDb(), projectSlug, opts);
9139
+ }).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) => {
9140
+ cmdList$2(getDb(), projectSlug, opts);
9141
+ });
9142
+ program.command("shell-init").description("Emit shell integration code. Add to ~/.zshrc:\n eval \"$(pai shell-init)\"").action(() => {
9143
+ process.stdout.write(`# PAI shell integration — generated by: pai shell-init
9144
+ pai() {
9145
+ if [[ "$1" == "cd" ]]; then
9146
+ local dir=$(command pai cd "$2" 2>/dev/null)
9147
+ if [[ -n "$dir" && -d "$dir" ]]; then
9148
+ builtin cd "$dir"
9149
+ echo "-> $dir"
9150
+ else
9151
+ echo "Project not found: $2" >&2
9152
+ return 1
9153
+ fi
9154
+ elif [[ "$1" == "projects" && "$2" == "cd" ]]; then
9155
+ local dir=$(command pai projects cd "$3" 2>/dev/null)
9156
+ if [[ -n "$dir" && -d "$dir" ]]; then
9157
+ builtin cd "$dir"
9158
+ echo "-> $dir"
9159
+ else
9160
+ echo "Project not found: $3" >&2
9161
+ return 1
9162
+ fi
9163
+ else
9164
+ command pai "$@"
9165
+ fi
9166
+ }
9167
+ `);
9168
+ });
8182
9169
  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
9170
  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
9171
  });