@tekmidian/pai 0.14.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/{auto-route-CLYcToZ5.mjs → auto-route-CdvjEWs5.mjs} +4 -4
  2. package/dist/{auto-route-CLYcToZ5.mjs.map → auto-route-CdvjEWs5.mjs.map} +1 -1
  3. package/dist/{kg-extraction-C8DEUHTS.mjs → checkpoint-block-Bl0HD3U2.mjs} +442 -13
  4. package/dist/checkpoint-block-Bl0HD3U2.mjs.map +1 -0
  5. package/dist/cli/index.mjs +14 -12
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/cli/program.d.mts.map +1 -1
  8. package/dist/cli/program.mjs +31 -16
  9. package/dist/cli/program.mjs.map +1 -1
  10. package/dist/{clusters-BYPw7vfW.mjs → clusters-CKWDwcMX.mjs} +2 -2
  11. package/dist/{clusters-BYPw7vfW.mjs.map → clusters-CKWDwcMX.mjs.map} +1 -1
  12. package/dist/{config-C8m-tPhP.mjs → config-DGOfoGfm.mjs} +1 -1
  13. package/dist/config-DGOfoGfm.mjs.map +1 -0
  14. package/dist/daemon/index.mjs +18 -16
  15. package/dist/daemon/index.mjs.map +1 -1
  16. package/dist/{daemon-COSvHTNu.mjs → daemon-1rKbN-fd.mjs} +31 -30
  17. package/dist/{daemon-COSvHTNu.mjs.map → daemon-1rKbN-fd.mjs.map} +1 -1
  18. package/dist/daemon-mcp/index.mjs +24 -2
  19. package/dist/daemon-mcp/index.mjs.map +1 -1
  20. package/dist/db-BtuN768f.mjs +206 -0
  21. package/dist/db-BtuN768f.mjs.map +1 -0
  22. package/dist/{db-CmYbAVCD.mjs → db-CYmBWcjh.mjs} +1 -1
  23. package/dist/{db-CmYbAVCD.mjs.map → db-CYmBWcjh.mjs.map} +1 -1
  24. package/dist/{detect-KjycLtXM.mjs → detect-Bf2z-oKB.mjs} +1 -1
  25. package/dist/{detect-KjycLtXM.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
  26. package/dist/{detector-ZiOhHszd.mjs → detector-rq9JGTmS.mjs} +2 -2
  27. package/dist/{detector-ZiOhHszd.mjs.map → detector-rq9JGTmS.mjs.map} +1 -1
  28. package/dist/{embeddings-BJPOcbik.mjs → embeddings-DGRAPAYb.mjs} +1 -1
  29. package/dist/{embeddings-BJPOcbik.mjs.map → embeddings-DGRAPAYb.mjs.map} +1 -1
  30. package/dist/{factory-Q88X1bAN.mjs → factory-HcQsQqh0.mjs} +8 -5
  31. package/dist/{factory-Q88X1bAN.mjs.map → factory-HcQsQqh0.mjs.map} +1 -1
  32. package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
  33. package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
  34. package/dist/hooks/capture-all-events.mjs.map +1 -1
  35. package/dist/hooks/capture-session-summary.mjs.map +1 -1
  36. package/dist/hooks/capture-tool-output.mjs.map +1 -1
  37. package/dist/hooks/cleanup-session-files.mjs.map +1 -1
  38. package/dist/hooks/context-compression-hook.mjs +304 -48
  39. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  40. package/dist/hooks/initialize-session.mjs.map +1 -1
  41. package/dist/hooks/inject-observations.mjs.map +1 -1
  42. package/dist/hooks/load-core-context.mjs.map +1 -1
  43. package/dist/hooks/load-project-context.mjs.map +1 -1
  44. package/dist/hooks/observe.mjs.map +1 -1
  45. package/dist/hooks/post-compact-inject.mjs.map +1 -1
  46. package/dist/hooks/security-validator.mjs.map +1 -1
  47. package/dist/hooks/session-commands.mjs.map +1 -1
  48. package/dist/hooks/stop-hook.mjs +299 -43
  49. package/dist/hooks/stop-hook.mjs.map +4 -4
  50. package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
  51. package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
  52. package/dist/hooks/update-tab-on-action.mjs.map +1 -1
  53. package/dist/hooks/update-tab-titles.mjs.map +1 -1
  54. package/dist/hooks/whisper-rules.mjs.map +1 -1
  55. package/dist/index.d.mts +0 -9
  56. package/dist/index.d.mts.map +1 -1
  57. package/dist/index.mjs +10 -9
  58. package/dist/indexer-AEcT8wHf.mjs +1 -0
  59. package/dist/{indexer-backend-Cm5RS7y0.mjs → indexer-backend-BQZXoarl.mjs} +3 -3
  60. package/dist/{indexer-backend-Cm5RS7y0.mjs.map → indexer-backend-BQZXoarl.mjs.map} +1 -1
  61. package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-vO2GV325.mjs} +1 -1
  62. package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-vO2GV325.mjs.map} +1 -1
  63. package/dist/{kg-entity-LXblD7LZ.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  64. package/dist/{kg-entity-LXblD7LZ.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  65. package/dist/{latent-ideas-RyC6eyqI.mjs → latent-ideas-C6IFFT0-.mjs} +4 -4
  66. package/dist/{latent-ideas-RyC6eyqI.mjs.map → latent-ideas-C6IFFT0-.mjs.map} +1 -1
  67. package/dist/{main-resolver-uFxNiDy7.mjs → main-resolver-DimnS2qP.mjs} +22 -22
  68. package/dist/{main-resolver-uFxNiDy7.mjs.map → main-resolver-DimnS2qP.mjs.map} +1 -1
  69. package/dist/{migrate-B02wDDgS.mjs → migrate-fLD6rAdO.mjs} +2 -2
  70. package/dist/{migrate-B02wDDgS.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
  71. package/dist/{neighborhood-CklGIB8r.mjs → neighborhood-CzRNG2Oy.mjs} +2 -2
  72. package/dist/{neighborhood-CklGIB8r.mjs.map → neighborhood-CzRNG2Oy.mjs.map} +1 -1
  73. package/dist/{note-context-qvZlVXVO.mjs → note-context-CrfMbr4R.mjs} +1 -1
  74. package/dist/{note-context-qvZlVXVO.mjs.map → note-context-CrfMbr4R.mjs.map} +1 -1
  75. package/dist/{pai-marker-HVBwwBW-.mjs → pai-marker-B20KqhA8.mjs} +44 -2
  76. package/dist/pai-marker-B20KqhA8.mjs.map +1 -0
  77. package/dist/{pick-gne3kOJG.mjs → pick-B3zEYjin.mjs} +2316 -483
  78. package/dist/pick-B3zEYjin.mjs.map +1 -0
  79. package/dist/{postgres-B451Hnkm.mjs → postgres-zjKqPl4X.mjs} +2 -2
  80. package/dist/{postgres-B451Hnkm.mjs.map → postgres-zjKqPl4X.mjs.map} +1 -1
  81. package/dist/{query-feedback-fapbqRgh.mjs → query-feedback-BoY8_Dbb.mjs} +1 -1
  82. package/dist/{query-feedback-fapbqRgh.mjs.map → query-feedback-BoY8_Dbb.mjs.map} +1 -1
  83. package/dist/{reranker-C08R99zn.mjs → reranker-CMNZcfVx.mjs} +1 -1
  84. package/dist/{reranker-C08R99zn.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  85. package/dist/{search-HcdKtMla.mjs → search-i2nlQ-JM.mjs} +3 -3
  86. package/dist/{search-HcdKtMla.mjs.map → search-i2nlQ-JM.mjs.map} +1 -1
  87. package/dist/skills/End/SKILL.md +78 -0
  88. package/dist/skills/Pause/SKILL.md +77 -0
  89. package/dist/skills/Tasks/SKILL.md +22 -0
  90. package/dist/{sqlite-DQpY1Esi.mjs → sqlite-ChSCYwiC.mjs} +3 -3
  91. package/dist/{sqlite-DQpY1Esi.mjs.map → sqlite-ChSCYwiC.mjs.map} +1 -1
  92. package/dist/{state-BIlxNRUn.mjs → state-BXIdbxDs.mjs} +1 -1
  93. package/dist/{state-BIlxNRUn.mjs.map → state-BXIdbxDs.mjs.map} +1 -1
  94. package/dist/{stop-words-BwplsQ3z.mjs → stop-words-BaMEGVeY.mjs} +1 -1
  95. package/dist/{stop-words-BwplsQ3z.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
  96. package/dist/{sync-uPR4g438.mjs → sync-CmBKOL3K.mjs} +6 -205
  97. package/dist/sync-CmBKOL3K.mjs.map +1 -0
  98. package/dist/{themes-Cg8oG9Ra.mjs → themes-nvRM8v84.mjs} +3 -3
  99. package/dist/{themes-Cg8oG9Ra.mjs.map → themes-nvRM8v84.mjs.map} +1 -1
  100. package/dist/{tools-Op3C2Nm6.mjs → tools-CznjzfYz.mjs} +26 -26
  101. package/dist/{tools-Op3C2Nm6.mjs.map → tools-CznjzfYz.mjs.map} +1 -1
  102. package/dist/{trace-CLK-NPkb.mjs → trace-B8oz1ok0.mjs} +1 -1
  103. package/dist/{trace-CLK-NPkb.mjs.map → trace-B8oz1ok0.mjs.map} +1 -1
  104. package/dist/{utils-CqqgB0dH.mjs → utils-BAxjW3j8.mjs} +1 -1
  105. package/dist/{utils-CqqgB0dH.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
  106. package/dist/{vault-indexer-Dn_BccVK.mjs → vault-indexer-DrttVeEk.mjs} +2 -2
  107. package/dist/{vault-indexer-Dn_BccVK.mjs.map → vault-indexer-DrttVeEk.mjs.map} +1 -1
  108. package/dist/{work-queue-worker-BwouhVkb.mjs → work-queue-worker-BPXLkEik.mjs} +47 -30
  109. package/dist/work-queue-worker-BPXLkEik.mjs.map +1 -0
  110. package/dist/{zettelkasten-NwBD1NsT.mjs → zettelkasten-CTlt1dGv.mjs} +4 -4
  111. package/dist/{zettelkasten-NwBD1NsT.mjs.map → zettelkasten-CTlt1dGv.mjs.map} +1 -1
  112. package/docs/commands/README.md +17 -0
  113. package/docs/commands/backup.md +1 -1
  114. package/docs/commands/clear-names.md +1 -1
  115. package/docs/commands/daemon.md +1 -1
  116. package/docs/commands/db.md +1 -1
  117. package/docs/commands/end.md +4 -1
  118. package/docs/commands/help.md +1 -1
  119. package/docs/commands/kg.md +1 -1
  120. package/docs/commands/mcp.md +1 -1
  121. package/docs/commands/memory.md +1 -1
  122. package/docs/commands/notify.md +1 -1
  123. package/docs/commands/observation.md +1 -1
  124. package/docs/commands/obsidian.md +1 -1
  125. package/docs/commands/pause.md +4 -1
  126. package/docs/commands/project.md +1 -1
  127. package/docs/commands/projects.md +1 -1
  128. package/docs/commands/registry.md +18 -1
  129. package/docs/commands/restore.md +1 -1
  130. package/docs/commands/session.md +271 -0
  131. package/docs/commands/sessions.md +1 -1
  132. package/docs/commands/setup.md +1 -1
  133. package/docs/commands/shell-init.md +1 -1
  134. package/docs/commands/skill.md +1 -1
  135. package/docs/commands/task.md +2 -1
  136. package/docs/commands/topic.md +1 -1
  137. package/docs/commands/update.md +1 -1
  138. package/docs/commands/zettel.md +1 -1
  139. package/package.json +1 -1
  140. package/scripts/build-hooks.mjs +32 -3
  141. package/src/hooks/ts/lib/project-utils/todo.test.ts +132 -0
  142. package/src/hooks/ts/lib/project-utils/todo.ts +60 -34
  143. package/dist/config-C8m-tPhP.mjs.map +0 -1
  144. package/dist/kg-extraction-C8DEUHTS.mjs.map +0 -1
  145. package/dist/pai-marker-HVBwwBW-.mjs.map +0 -1
  146. package/dist/pick-gne3kOJG.mjs.map +0 -1
  147. package/dist/sync-uPR4g438.mjs.map +0 -1
  148. package/dist/work-queue-worker-BwouhVkb.mjs.map +0 -1
@@ -1,18 +1,20 @@
1
- import { a as openRegistry, i as indexProject, n as indexAll, t as embedChunks } from "./sync-uPR4g438.mjs";
2
- 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$1, o as header, p as slugFromPath, r as encodeDir, s as now, t as bold$1, u as resolvePath } from "./utils-CqqgB0dH.mjs";
3
- import { a as slugify$1, i as parseSessionFilename } from "./migrate-B02wDDgS.mjs";
4
- import { n as ensurePaiMarker } from "./pai-marker-HVBwwBW-.mjs";
5
- import { n as openFederation } from "./db-CmYbAVCD.mjs";
6
- import { n as populateSlugs, r as searchMemory } from "./search-HcdKtMla.mjs";
7
- import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "./detect-KjycLtXM.mjs";
8
- 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-C8DEUHTS.mjs";
9
- import { t as createStorageBackend } from "./factory-Q88X1bAN.mjs";
10
- import { t as PaiClient } from "./ipc-client-CoyUHPod.mjs";
11
- import { a as expandHome, c as UNROUTED, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, s as OWNER_LABEL_PREFIX, t as CONFIG_DIR } from "./config-C8m-tPhP.mjs";
12
- import { s as kgQuery } from "./kg-entity-LXblD7LZ.mjs";
13
- import { a as renderDedupedSessions, c as revealItermSession, d as fmtAge, f as resolveSessionByNameOrId, i as normalizeName, l as sendToSession, o as callAiBroker, p as scanSessions, r as buildDeduped, s as fetchLiveSessions, u as printExitDir } from "./main-resolver-uFxNiDy7.mjs";
1
+ import { n as openRegistry } from "./db-BtuN768f.mjs";
2
+ 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$1, o as header, p as slugFromPath, r as encodeDir, s as now, t as bold$1, u as resolvePath } from "./utils-BAxjW3j8.mjs";
3
+ import { a as slugify$1, i as parseSessionFilename } from "./migrate-fLD6rAdO.mjs";
4
+ import { n as ensurePaiMarker } from "./pai-marker-B20KqhA8.mjs";
5
+ import { n as openFederation } from "./db-CYmBWcjh.mjs";
6
+ import { i as indexProject, n as indexAll, t as embedChunks } from "./sync-CmBKOL3K.mjs";
7
+ import { t as STOP_WORDS } from "./stop-words-BaMEGVeY.mjs";
8
+ import { n as populateSlugs, r as searchMemory } from "./search-i2nlQ-JM.mjs";
9
+ import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "./detect-Bf2z-oKB.mjs";
10
+ import { a as readBodyFile, c as loadScanConfig, d as saveScanConfig, f as upsertProject, i as findNotesDir$1, n as applyContinue, o as extractAndStoreTriples, p as upsertSession, r as findLatestNote, s as cmdScan, t as appendCheckpointToNote, u as resolveHome } from "./checkpoint-block-Bl0HD3U2.mjs";
11
+ import { t as PaiClient } from "./ipc-client-vO2GV325.mjs";
12
+ import { a as expandHome, c as UNROUTED, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, s as OWNER_LABEL_PREFIX, t as CONFIG_DIR } from "./config-DGOfoGfm.mjs";
13
+ import { t as createStorageBackend } from "./factory-HcQsQqh0.mjs";
14
+ import { s as kgQuery } from "./kg-entity-DbOMPdF9.mjs";
15
+ import { a as renderDedupedSessions, c as fetchLiveSessions, d as fmtAge, f as resolveSessionByNameOrId, i as normalizeName$1, l as revealItermSession, o as printExitDir, p as scanSessions, r as buildDeduped, s as callAiBroker, u as sendToSession } from "./main-resolver-DimnS2qP.mjs";
14
16
  import { appendFileSync, chmodSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
15
- import { homedir, platform } from "node:os";
17
+ import { homedir, platform, tmpdir } from "node:os";
16
18
  import { basename, dirname, join, relative, resolve } from "node:path";
17
19
  import chalk from "chalk";
18
20
  import { fileURLToPath } from "node:url";
@@ -120,7 +122,7 @@ function cmdPromote(db, opts) {
120
122
 
121
123
  //#endregion
122
124
  //#region src/cli/commands/project/helpers.ts
123
- function getProject$1(db, slug) {
125
+ function getProject$2(db, slug) {
124
126
  const direct = db.prepare("SELECT * FROM projects WHERE slug = ?").get(slug);
125
127
  if (direct) return direct;
126
128
  return db.prepare(`SELECT p.* FROM projects p
@@ -128,7 +130,7 @@ function getProject$1(db, slug) {
128
130
  WHERE a.alias = ?`).get(slug);
129
131
  }
130
132
  function requireProject(db, slug) {
131
- const project = getProject$1(db, slug);
133
+ const project = getProject$2(db, slug);
132
134
  if (!project) {
133
135
  console.error(err(`Project not found: ${slug}`));
134
136
  process.exit(1);
@@ -144,7 +146,7 @@ function resolveIdentifier(db, identifier) {
144
146
  const rows = db.prepare("SELECT * FROM projects ORDER BY status ASC, updated_at DESC").all();
145
147
  if (num <= rows.length) return rows[num - 1];
146
148
  }
147
- return getProject$1(db, identifier);
149
+ return getProject$2(db, identifier);
148
150
  }
149
151
  function getProjectTags(db, projectId) {
150
152
  return db.prepare(`SELECT t.name FROM tags t
@@ -163,7 +165,7 @@ function getLastSessionDate(db, projectId) {
163
165
  ORDER BY created_at DESC LIMIT 1`).get(projectId);
164
166
  return row ? row.created_at : null;
165
167
  }
166
- function upsertTag(db, tagName) {
168
+ function upsertTag$1(db, tagName) {
167
169
  db.prepare("INSERT OR IGNORE INTO tags (name) VALUES (?)").run(tagName);
168
170
  return db.prepare("SELECT id FROM tags WHERE name = ?").get(tagName).id;
169
171
  }
@@ -419,7 +421,7 @@ function cmdList$3(db, opts) {
419
421
  if (!opts.all && !opts.status && hiddenCount > 0) console.log(dim$1(` ${rows.length} active project(s) (${hiddenCount} archived — use --all to show)`));
420
422
  else console.log(dim$1(` ${rows.length} project(s)`));
421
423
  }
422
- function cmdInfo(db, identifier) {
424
+ function cmdInfo$1(db, identifier) {
423
425
  const project = resolveIdentifier(db, identifier) ?? requireProject(db, identifier);
424
426
  const tags = getProjectTags(db, project.id);
425
427
  const aliases = getProjectAliases(db, project.id);
@@ -489,12 +491,12 @@ function cmdMove(db, slug, newPath) {
489
491
  console.log(dim$1(` Old path: ${project.root_path}`));
490
492
  console.log(dim$1(` New path: ${resolvedNew}`));
491
493
  }
492
- function cmdTag(db, slug, tags) {
494
+ function cmdTag$1(db, slug, tags) {
493
495
  const project = requireProject(db, slug);
494
496
  const added = [];
495
497
  const skipped = [];
496
498
  for (const tagName of tags) {
497
- const tagId = upsertTag(db, tagName);
499
+ const tagId = upsertTag$1(db, tagName);
498
500
  if (db.prepare("SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ?").get(project.id, tagId)) skipped.push(tagName);
499
501
  else {
500
502
  db.prepare("INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)").run(project.id, tagId);
@@ -510,7 +512,7 @@ function cmdAlias(db, slug, alias) {
510
512
  console.error(err(`"${alias}" is already a project slug — cannot use as alias.`));
511
513
  process.exit(1);
512
514
  }
513
- const project = getProject$1(db, slug);
515
+ const project = getProject$2(db, slug);
514
516
  try {
515
517
  db.prepare("INSERT INTO aliases (alias, project_id) VALUES (?, ?)").run(alias, project.id);
516
518
  console.log(ok(`Alias added: ${bold$1(alias)} → ${slug}`));
@@ -658,7 +660,7 @@ function cmdGo(db, query) {
658
660
  process.exit(1);
659
661
  }
660
662
  const q = query.trim().toLowerCase();
661
- const exact = getProject$1(db, query);
663
+ const exact = getProject$2(db, query);
662
664
  if (exact) {
663
665
  if (!existsSync(exact.root_path)) {
664
666
  const recovered = tryRecoverMovedProject(db, exact);
@@ -1280,7 +1282,7 @@ function cmdHealth$1(db, opts) {
1280
1282
  //#region src/cli/commands/project/projects-index.ts
1281
1283
  function registerProjectsCommands(projectsCmd, getDb) {
1282
1284
  projectsCmd.command("list", { isDefault: true }).description("Show the unified deduped listing (same as `pai`).\nUse --all to include cold / zero-session / archived projects.").option("--all", "Show all entries including cold / zero-session / archived projects").option("-n, --n <count>", "Max rows to show", "20").action(async (opts) => {
1283
- const { cmdMain } = await import("./main-resolver-uFxNiDy7.mjs").then((n) => n.n);
1285
+ const { cmdMain } = await import("./main-resolver-DimnS2qP.mjs").then((n) => n.n);
1284
1286
  await cmdMain(getDb(), void 0, void 0, opts);
1285
1287
  });
1286
1288
  projectsCmd.command("list-raw").description("Raw registry listing with slug, path, status, last_active columns").option("--all", "Include archived projects (default: active only)").option("--status <status>", "Filter by status: active | archived").option("--tag <tag>", "Filter by tag").option("--type <type>", "Filter by type").action((opts) => {
@@ -1323,7 +1325,7 @@ function registerProjectsCommands(projectsCmd, getDb) {
1323
1325
  cmdAdd(getDb(), rawPath, opts);
1324
1326
  });
1325
1327
  projectsCmd.command("info <slug>").description("Show full details for a project").action((slug) => {
1326
- cmdInfo(getDb(), slug);
1328
+ cmdInfo$1(getDb(), slug);
1327
1329
  });
1328
1330
  projectsCmd.command("archive <slug>").description("Archive a project").action((slug) => {
1329
1331
  cmdArchive(getDb(), slug);
@@ -1338,7 +1340,7 @@ function registerProjectsCommands(projectsCmd, getDb) {
1338
1340
  cmdRebind(getDb(), slug, newPath);
1339
1341
  });
1340
1342
  projectsCmd.command("tag <slug> <tags...>").description("Add one or more tags to a project").action((slug, tags) => {
1341
- cmdTag(getDb(), slug, tags);
1343
+ cmdTag$1(getDb(), slug, tags);
1342
1344
  });
1343
1345
  projectsCmd.command("alias <slug> <alias>").description("Register an alternative slug for a project").action((slug, alias) => {
1344
1346
  cmdAlias(getDb(), slug, alias);
@@ -1435,6 +1437,360 @@ function cmdMigrate$1(db) {
1435
1437
  }
1436
1438
  }
1437
1439
 
1440
+ //#endregion
1441
+ //#region src/cli/commands/registry/dedupe.ts
1442
+ /** Resolve a path to its canonical form, or null when it no longer exists. */
1443
+ function safeRealPath(p) {
1444
+ try {
1445
+ return realpathSync(p);
1446
+ } catch {
1447
+ return null;
1448
+ }
1449
+ }
1450
+ function countRefs(db, projectId) {
1451
+ const one = (sql) => db.prepare(sql).get(projectId).n;
1452
+ return {
1453
+ sessions: one("SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?"),
1454
+ compaction: one("SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?"),
1455
+ aliases: one("SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?"),
1456
+ links: one("SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?"),
1457
+ tags: one("SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?"),
1458
+ children: one("SELECT COUNT(*) AS n FROM projects WHERE parent_id = ?")
1459
+ };
1460
+ }
1461
+ /**
1462
+ * Choose which row survives.
1463
+ *
1464
+ * The row whose stored path already IS the canonical filesystem path wins:
1465
+ * keeping it means `root_path` ends up correct without rewriting it, and any
1466
+ * external reference to that path keeps resolving. Failing that, the row with
1467
+ * the most sessions wins, then the oldest.
1468
+ */
1469
+ function pickCanonical(rows, realPath, db) {
1470
+ const exact = rows.filter((r) => r.root_path === realPath);
1471
+ return [...exact.length > 0 ? exact : rows].sort((a, b) => {
1472
+ const sa = countRefs(db, a.id).sessions;
1473
+ const sb = countRefs(db, b.id).sessions;
1474
+ if (sa !== sb) return sb - sa;
1475
+ return a.created_at - b.created_at;
1476
+ })[0];
1477
+ }
1478
+ function analyzeDuplicates(db) {
1479
+ const rows = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir, status,
1480
+ claude_notes_dir, session_config, created_at
1481
+ FROM projects ORDER BY id`).all();
1482
+ const byRealPath = /* @__PURE__ */ new Map();
1483
+ const stalePaths = [];
1484
+ for (const row of rows) {
1485
+ const rp = safeRealPath(row.root_path);
1486
+ if (!rp) {
1487
+ stalePaths.push(row);
1488
+ continue;
1489
+ }
1490
+ const bucket = byRealPath.get(rp);
1491
+ if (bucket) bucket.push(row);
1492
+ else byRealPath.set(rp, [row]);
1493
+ }
1494
+ const groups = [];
1495
+ for (const [realPath, members] of byRealPath) {
1496
+ if (members.length < 2) continue;
1497
+ const canonical = pickCanonical(members, realPath, db);
1498
+ groups.push({
1499
+ realPath,
1500
+ canonical,
1501
+ duplicates: members.filter((m) => m.id !== canonical.id)
1502
+ });
1503
+ }
1504
+ return {
1505
+ groups,
1506
+ stalePaths
1507
+ };
1508
+ }
1509
+ /**
1510
+ * Fold a duplicate session row into the one the canonical project already has
1511
+ * for the same note file.
1512
+ *
1513
+ * Both rows describe one file on disk — the double registration produced two
1514
+ * registry rows for it, often identical and created milliseconds apart. Moving
1515
+ * both would leave the merged project with two entries per note and force one
1516
+ * of them to a number that no longer matches its own filename.
1517
+ *
1518
+ * Nothing is discarded: tags and links are repointed at the surviving row, and
1519
+ * any field the survivor is missing is backfilled from the duplicate before it
1520
+ * is deleted.
1521
+ */
1522
+ function foldSession(db, keepId, dropId) {
1523
+ const tags = db.prepare("SELECT tag_id FROM session_tags WHERE session_id = ?").all(dropId);
1524
+ for (const t of tags) if (db.prepare("SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ? LIMIT 1").get(keepId, t.tag_id)) db.prepare("DELETE FROM session_tags WHERE session_id = ? AND tag_id = ?").run(dropId, t.tag_id);
1525
+ else db.prepare("UPDATE session_tags SET session_id = ? WHERE session_id = ? AND tag_id = ?").run(keepId, dropId, t.tag_id);
1526
+ const links = db.prepare("SELECT id, target_project_id FROM links WHERE session_id = ?").all(dropId);
1527
+ for (const l of links) if (db.prepare("SELECT 1 FROM links WHERE session_id = ? AND target_project_id = ? LIMIT 1").get(keepId, l.target_project_id)) db.prepare("DELETE FROM links WHERE id = ?").run(l.id);
1528
+ else db.prepare("UPDATE links SET session_id = ? WHERE id = ?").run(keepId, l.id);
1529
+ db.prepare("UPDATE compaction_log SET session_id = ? WHERE session_id = ?").run(keepId, dropId);
1530
+ db.prepare(`UPDATE sessions SET
1531
+ claude_session_id = COALESCE(claude_session_id,
1532
+ (SELECT claude_session_id FROM sessions WHERE id = ?)),
1533
+ token_count = COALESCE(token_count,
1534
+ (SELECT token_count FROM sessions WHERE id = ?)),
1535
+ closed_at = COALESCE(closed_at,
1536
+ (SELECT closed_at FROM sessions WHERE id = ?)),
1537
+ status = CASE WHEN status = 'open'
1538
+ AND (SELECT status FROM sessions WHERE id = ?) != 'open'
1539
+ THEN (SELECT status FROM sessions WHERE id = ?)
1540
+ ELSE status END
1541
+ WHERE id = ?`).run(dropId, dropId, dropId, dropId, dropId, keepId);
1542
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(dropId);
1543
+ }
1544
+ function mergeGroup(db, group) {
1545
+ const report = {
1546
+ movedSessions: 0,
1547
+ foldedSessions: 0,
1548
+ renumbered: [],
1549
+ movedCompaction: 0,
1550
+ movedAliases: 0,
1551
+ movedLinks: 0,
1552
+ movedTags: 0,
1553
+ movedChildren: 0,
1554
+ deletedRows: 0,
1555
+ reclaimedSlugs: []
1556
+ };
1557
+ const canonicalId = group.canonical.id;
1558
+ let nextNumber = (db.prepare("SELECT MAX(number) AS n FROM sessions WHERE project_id = ?").get(canonicalId).n ?? 0) + 1;
1559
+ for (const dup of group.duplicates) {
1560
+ const sessions = db.prepare("SELECT id, number, title, filename FROM sessions WHERE project_id = ? ORDER BY number").all(dup.id);
1561
+ for (const s of sessions) {
1562
+ const twin = db.prepare("SELECT id FROM sessions WHERE project_id = ? AND filename = ? LIMIT 1").get(canonicalId, s.filename);
1563
+ if (twin) {
1564
+ foldSession(db, twin.id, s.id);
1565
+ report.foldedSessions++;
1566
+ continue;
1567
+ }
1568
+ if (db.prepare("SELECT 1 FROM sessions WHERE project_id = ? AND number = ? LIMIT 1").get(canonicalId, s.number)) {
1569
+ const assigned = nextNumber++;
1570
+ db.prepare("UPDATE sessions SET project_id = ?, number = ? WHERE id = ?").run(canonicalId, assigned, s.id);
1571
+ report.renumbered.push({
1572
+ from: s.number,
1573
+ to: assigned,
1574
+ title: s.title
1575
+ });
1576
+ } else {
1577
+ db.prepare("UPDATE sessions SET project_id = ? WHERE id = ?").run(canonicalId, s.id);
1578
+ if (s.number >= nextNumber) nextNumber = s.number + 1;
1579
+ }
1580
+ report.movedSessions++;
1581
+ }
1582
+ report.movedCompaction += db.prepare("UPDATE compaction_log SET project_id = ? WHERE project_id = ?").run(canonicalId, dup.id).changes;
1583
+ const dupAliases = db.prepare("SELECT alias FROM aliases WHERE project_id = ?").all(dup.id);
1584
+ for (const a of dupAliases) {
1585
+ const existing = db.prepare("SELECT project_id FROM aliases WHERE alias = ?").get(a.alias);
1586
+ if (existing && existing.project_id === canonicalId) continue;
1587
+ db.prepare("UPDATE aliases SET project_id = ? WHERE alias = ?").run(canonicalId, a.alias);
1588
+ report.movedAliases++;
1589
+ }
1590
+ const dupLinks = db.prepare("SELECT id, session_id FROM links WHERE target_project_id = ?").all(dup.id);
1591
+ for (const l of dupLinks) {
1592
+ if (db.prepare("SELECT 1 FROM links WHERE session_id = ? AND target_project_id = ? LIMIT 1").get(l.session_id, canonicalId)) {
1593
+ db.prepare("DELETE FROM links WHERE id = ?").run(l.id);
1594
+ continue;
1595
+ }
1596
+ db.prepare("UPDATE links SET target_project_id = ? WHERE id = ?").run(canonicalId, l.id);
1597
+ report.movedLinks++;
1598
+ }
1599
+ const dupTags = db.prepare("SELECT tag_id FROM project_tags WHERE project_id = ?").all(dup.id);
1600
+ for (const t of dupTags) {
1601
+ if (db.prepare("SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ? LIMIT 1").get(canonicalId, t.tag_id)) {
1602
+ db.prepare("DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?").run(dup.id, t.tag_id);
1603
+ continue;
1604
+ }
1605
+ db.prepare("UPDATE project_tags SET project_id = ? WHERE project_id = ? AND tag_id = ?").run(canonicalId, dup.id, t.tag_id);
1606
+ report.movedTags++;
1607
+ }
1608
+ report.movedChildren += db.prepare("UPDATE projects SET parent_id = ? WHERE parent_id = ?").run(canonicalId, dup.id).changes;
1609
+ if (!group.canonical.claude_notes_dir && dup.claude_notes_dir) db.prepare("UPDATE projects SET claude_notes_dir = ? WHERE id = ?").run(dup.claude_notes_dir, canonicalId);
1610
+ if (!group.canonical.session_config && dup.session_config) db.prepare("UPDATE projects SET session_config = ? WHERE id = ?").run(dup.session_config, canonicalId);
1611
+ db.prepare("DELETE FROM projects WHERE id = ?").run(dup.id);
1612
+ report.deletedRows++;
1613
+ }
1614
+ if ((group.canonical.status === "active" || group.duplicates.some((d) => d.status === "active")) && group.canonical.status !== "active") db.prepare("UPDATE projects SET status = 'active' WHERE id = ?").run(canonicalId);
1615
+ const suffixed = /^(.*)-\d+$/.exec(group.canonical.slug);
1616
+ if (suffixed) {
1617
+ const candidates = [suffixed[1], ...group.duplicates.map((d) => d.slug).filter((s) => !/-\d+$/.test(s))];
1618
+ for (const candidate of candidates) {
1619
+ if (db.prepare("SELECT 1 FROM projects WHERE slug = ? LIMIT 1").get(candidate)) continue;
1620
+ db.prepare("UPDATE projects SET slug = ?, updated_at = ? WHERE id = ?").run(candidate, Date.now(), canonicalId);
1621
+ report.reclaimedSlugs.push({
1622
+ from: group.canonical.slug,
1623
+ to: candidate
1624
+ });
1625
+ break;
1626
+ }
1627
+ }
1628
+ if (group.canonical.root_path !== group.realPath) {
1629
+ if (!db.prepare("SELECT id FROM projects WHERE root_path = ? AND id != ?").get(group.realPath, canonicalId)) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(group.realPath, Date.now(), canonicalId);
1630
+ }
1631
+ return report;
1632
+ }
1633
+ /**
1634
+ * Rewrite any remaining root_path that is not already its canonical form.
1635
+ *
1636
+ * Merging fixes the rows that already split. This stops the rest from
1637
+ * splitting later: a project registered only as `~/dev/ai/PAILot` still has a
1638
+ * non-canonical path, so the first session started from the other spelling
1639
+ * would create a second row and the whole problem recurs.
1640
+ *
1641
+ * Only rows whose canonical path is free are touched — a collision would mean
1642
+ * a duplicate, and those have already been merged.
1643
+ */
1644
+ function canonicalizePaths(db, dryRun) {
1645
+ const rows = db.prepare("SELECT id, slug, root_path FROM projects").all();
1646
+ const changed = [];
1647
+ for (const row of rows) {
1648
+ const rp = safeRealPath(row.root_path);
1649
+ if (!rp || rp === row.root_path) continue;
1650
+ if (db.prepare("SELECT 1 FROM projects WHERE root_path = ? AND id != ? LIMIT 1").get(rp, row.id)) continue;
1651
+ changed.push({
1652
+ slug: row.slug,
1653
+ from: row.root_path,
1654
+ to: rp
1655
+ });
1656
+ if (!dryRun) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(rp, Date.now(), row.id);
1657
+ }
1658
+ return changed;
1659
+ }
1660
+ function backupDb(dbPath) {
1661
+ if (!existsSync(dbPath)) return null;
1662
+ const dest = `${dbPath}.bak-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1663
+ try {
1664
+ copyFileSync(dbPath, dest);
1665
+ for (const suffix of ["-wal", "-shm"]) if (existsSync(dbPath + suffix)) copyFileSync(dbPath + suffix, dest + suffix);
1666
+ return dest;
1667
+ } catch {
1668
+ return null;
1669
+ }
1670
+ }
1671
+ function cmdDedupe(db, opts) {
1672
+ const { groups, stalePaths } = analyzeDuplicates(db);
1673
+ console.log();
1674
+ console.log(bold$1(opts.execute ? " pai registry dedupe" : " pai registry dedupe — DRY RUN (no changes will be made)"));
1675
+ console.log();
1676
+ if (groups.length === 0) console.log(ok("No duplicate projects found — every root_path resolves uniquely."));
1677
+ for (const g of groups) {
1678
+ console.log(` ${bold$1(g.realPath)}`);
1679
+ const cCounts = countRefs(db, g.canonical.id);
1680
+ console.log(` ${ok("keep")} #${g.canonical.id} ${bold$1(g.canonical.slug)} ` + dim$1(`(${g.canonical.root_path}) — ${cCounts.sessions} sessions, ${g.canonical.status}`));
1681
+ for (const d of g.duplicates) {
1682
+ const dc = countRefs(db, d.id);
1683
+ const moving = [
1684
+ dc.sessions ? `${dc.sessions} sessions` : null,
1685
+ dc.compaction ? `${dc.compaction} compaction rows` : null,
1686
+ dc.aliases ? `${dc.aliases} aliases` : null,
1687
+ dc.links ? `${dc.links} links` : null,
1688
+ dc.tags ? `${dc.tags} tags` : null,
1689
+ dc.children ? `${dc.children} child projects` : null
1690
+ ].filter(Boolean).join(", ");
1691
+ console.log(` ${warn("merge")} #${d.id} ${bold$1(d.slug)} ` + dim$1(`(${d.root_path}) — ${moving || "nothing to move"}`));
1692
+ }
1693
+ console.log();
1694
+ }
1695
+ if (stalePaths.length > 0) {
1696
+ console.log(dim$1(` ${stalePaths.length} project(s) point at a path that no longer exists.`));
1697
+ console.log(dim$1(" These are never merged automatically — a missing directory cannot be"));
1698
+ console.log(dim$1(" proven identical to anything else. Listed for review:"));
1699
+ for (const s of stalePaths) {
1700
+ const sc = countRefs(db, s.id);
1701
+ if (sc.sessions === 0 && sc.compaction === 0) continue;
1702
+ console.log(dim$1(` #${s.id} ${s.slug} (${s.root_path}) — ${sc.sessions} sessions, ${sc.compaction} compaction rows`));
1703
+ }
1704
+ console.log();
1705
+ }
1706
+ const pathFixes = canonicalizePaths(db, true);
1707
+ if (pathFixes.length > 0) {
1708
+ console.log(dim$1(` ${pathFixes.length} project(s) stored under a non-canonical path — these would`));
1709
+ console.log(dim$1(" split into a second row the first time a session runs from the other"));
1710
+ console.log(dim$1(" spelling. They will be rewritten to their resolved path:"));
1711
+ for (const p of pathFixes.slice(0, 8)) console.log(dim$1(` ${p.slug}: ${p.from} -> ${p.to}`));
1712
+ if (pathFixes.length > 8) console.log(dim$1(` ... and ${pathFixes.length - 8} more`));
1713
+ console.log();
1714
+ }
1715
+ if (!opts.execute) {
1716
+ if (groups.length > 0 || pathFixes.length > 0) {
1717
+ console.log(dim$1(" Re-run with --execute to apply. The registry is backed up first."));
1718
+ console.log();
1719
+ }
1720
+ return;
1721
+ }
1722
+ if (groups.length === 0 && pathFixes.length === 0) return;
1723
+ const dbPath = opts.dbPath;
1724
+ if (dbPath) {
1725
+ const backup = backupDb(dbPath);
1726
+ if (backup) console.log(ok(`Registry backed up to ${bold$1(backup)}`));
1727
+ else {
1728
+ console.error(err("Could not back up the registry — refusing to merge without one."));
1729
+ process.exit(1);
1730
+ }
1731
+ }
1732
+ const totals = {
1733
+ movedSessions: 0,
1734
+ foldedSessions: 0,
1735
+ renumbered: [],
1736
+ movedCompaction: 0,
1737
+ movedAliases: 0,
1738
+ movedLinks: 0,
1739
+ movedTags: 0,
1740
+ movedChildren: 0,
1741
+ deletedRows: 0,
1742
+ reclaimedSlugs: []
1743
+ };
1744
+ let canonicalized = 0;
1745
+ const run = db.transaction(() => {
1746
+ for (const g of groups) {
1747
+ const r = mergeGroup(db, g);
1748
+ totals.movedSessions += r.movedSessions;
1749
+ totals.foldedSessions += r.foldedSessions;
1750
+ totals.renumbered.push(...r.renumbered);
1751
+ totals.movedCompaction += r.movedCompaction;
1752
+ totals.movedAliases += r.movedAliases;
1753
+ totals.movedLinks += r.movedLinks;
1754
+ totals.movedTags += r.movedTags;
1755
+ totals.movedChildren += r.movedChildren;
1756
+ totals.deletedRows += r.deletedRows;
1757
+ totals.reclaimedSlugs.push(...r.reclaimedSlugs);
1758
+ }
1759
+ canonicalized = canonicalizePaths(db, false).length;
1760
+ });
1761
+ try {
1762
+ run();
1763
+ } catch (e) {
1764
+ console.error(err(`Merge failed and was rolled back: ${String(e)}`));
1765
+ process.exit(1);
1766
+ }
1767
+ console.log();
1768
+ console.log(ok(`Merged ${totals.deletedRows} duplicate row(s).`));
1769
+ console.log(` sessions moved: ${totals.movedSessions}`);
1770
+ console.log(` sessions folded: ${totals.foldedSessions} ` + dim$1("(same note file registered twice)"));
1771
+ console.log(` compaction rows moved: ${totals.movedCompaction}`);
1772
+ console.log(` aliases moved: ${totals.movedAliases}`);
1773
+ console.log(` links moved: ${totals.movedLinks}`);
1774
+ console.log(` tags moved: ${totals.movedTags}`);
1775
+ console.log(` child projects moved: ${totals.movedChildren}`);
1776
+ console.log(` paths canonicalized: ${canonicalized}`);
1777
+ if (totals.reclaimedSlugs.length > 0) {
1778
+ console.log();
1779
+ console.log(ok("Slugs reclaimed from the merged rows:"));
1780
+ for (const s of totals.reclaimedSlugs) console.log(dim$1(` ${s.from} -> ${s.to}`));
1781
+ }
1782
+ if (totals.renumbered.length > 0) {
1783
+ console.log();
1784
+ console.log(warn(`${totals.renumbered.length} session(s) renumbered to avoid collisions:`));
1785
+ for (const r of totals.renumbered) console.log(dim$1(` ${r.from} -> ${r.to} ${r.title}`));
1786
+ console.log(dim$1(" Session note filenames on disk are unchanged — only the registry number moved."));
1787
+ console.log();
1788
+ console.log(warn(" A renumbered session's registry number no longer matches the number in its"));
1789
+ console.log(dim$1(" filename. The scanner keys sessions by (project, number), so a note whose\n number is now occupied cannot be re-registered until numbering is reconciled.\n Run `pai session cleanup <slug> --execute` on the affected projects to renumber\n notes and registry together."));
1790
+ }
1791
+ console.log();
1792
+ }
1793
+
1438
1794
  //#endregion
1439
1795
  //#region src/cli/commands/registry/index.ts
1440
1796
  function cmdStats$2(db) {
@@ -1529,6 +1885,12 @@ function registerRegistryCommands(registryCmd, getDb) {
1529
1885
  registryCmd.command("rebuild").description("Erase all registry data and rebuild from the filesystem (destructive)").action(() => {
1530
1886
  cmdRebuild(getDb());
1531
1887
  });
1888
+ registryCmd.command("dedupe").description("Merge registry rows that describe the same project.\nTwo spellings of one directory (e.g. via a symlinked path prefix) register\nas separate projects and split session history between them. Rows are grouped\nby resolved path, so a merge only happens when the paths are provably identical.\nDry-run by default; --execute backs up the registry first and merges in one transaction.").option("--execute", "Actually perform the merge (default is dry-run)").action((opts) => {
1889
+ cmdDedupe(getDb(), {
1890
+ execute: opts.execute,
1891
+ dbPath: join(homedir(), ".pai", "registry.db")
1892
+ });
1893
+ });
1532
1894
  registryCmd.command("lookup").description("Find the project slug for a filesystem path (for use in scripts)").requiredOption("--path <path>", "Filesystem path to look up").action((opts) => {
1533
1895
  cmdLookup(getDb(), opts.path);
1534
1896
  });
@@ -1655,7 +2017,7 @@ function registerSearchCommand(memoryCmd, getDb) {
1655
2017
  else if (mode === "semantic" || mode === "hybrid") {
1656
2018
  const backend = await createStorageBackend(config);
1657
2019
  try {
1658
- const { generateEmbedding } = await import("./embeddings-BJPOcbik.mjs").then((n) => n.i);
2020
+ const { generateEmbedding } = await import("./embeddings-DGRAPAYb.mjs").then((n) => n.i);
1659
2021
  console.log(dim$1("Generating query embedding..."));
1660
2022
  const queryEmbedding = await generateEmbedding(query, true);
1661
2023
  if (mode === "semantic") results = await backend.searchSemantic(queryEmbedding, searchOpts);
@@ -1704,13 +2066,13 @@ function registerSearchCommand(memoryCmd, getDb) {
1704
2066
  return;
1705
2067
  }
1706
2068
  if (opts.rerank !== false) {
1707
- const { rerankResults } = await import("./reranker-C08R99zn.mjs").then((n) => n.r);
2069
+ const { rerankResults } = await import("./reranker-CMNZcfVx.mjs").then((n) => n.r);
1708
2070
  console.log(dim$1("Reranking with cross-encoder..."));
1709
2071
  results = await rerankResults(query, results, { topK: maxResults });
1710
2072
  }
1711
2073
  const recencyDays = parseInt(opts.recency ?? String(searchConfig.recencyBoostDays), 10);
1712
2074
  if (recencyDays > 0) {
1713
- const { applyRecencyBoost } = await import("./search-HcdKtMla.mjs").then((n) => n.o);
2075
+ const { applyRecencyBoost } = await import("./search-i2nlQ-JM.mjs").then((n) => n.o);
1714
2076
  console.log(dim$1(`Applying recency boost (half-life: ${recencyDays} days)...`));
1715
2077
  results = applyRecencyBoost(results, recencyDays);
1716
2078
  }
@@ -2305,8 +2667,8 @@ function cmdLogs(opts) {
2305
2667
  }
2306
2668
  function registerDaemonCommands(daemonCmd) {
2307
2669
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
2308
- const { serve } = await import("./daemon-COSvHTNu.mjs").then((n) => n.t);
2309
- const { loadConfig: lc, ensureConfigDir } = await import("./config-C8m-tPhP.mjs").then((n) => n.r);
2670
+ const { serve } = await import("./daemon-1rKbN-fd.mjs").then((n) => n.t);
2671
+ const { loadConfig: lc, ensureConfigDir } = await import("./config-DGOfoGfm.mjs").then((n) => n.r);
2310
2672
  ensureConfigDir();
2311
2673
  await serve(lc());
2312
2674
  });
@@ -4219,6 +4581,10 @@ async function listProjects(token) {
4219
4581
  name: p.name
4220
4582
  }));
4221
4583
  }
4584
+ /** Strip emoji and case so "Whazaa 🐝" and "whazaa" compare equal. */
4585
+ function normalizeName(raw) {
4586
+ return raw.replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}\u{200D}]/gu, "").trim().toLowerCase();
4587
+ }
4222
4588
  /** Todoist priority is inverted: 4 is urgent, 1 is lowest. */
4223
4589
  function toPriority(wire) {
4224
4590
  switch (wire) {
@@ -4301,13 +4667,14 @@ var TodoistProvider = class {
4301
4667
  if (!token || !this.config.rootProjectId) throw new Error("Todoist provider is not configured — run `pai setup`.");
4302
4668
  const labels = [...task.labels ?? []];
4303
4669
  if (task.owner && !labels.some((l) => l.toLowerCase() === `pai:${task.owner.toLowerCase()}`)) labels.push(`pai:${task.owner}`);
4304
- const sectionId = task.owner ? void 0 : this.config.findingsSectionId;
4670
+ const sectionId = task.owner || task.into ? void 0 : this.config.findingsSectionId;
4671
+ const projectId = task.into ? (await this.findOrCreateSubProject(task.into)).id : this.config.rootProjectId;
4305
4672
  const created = await call(token, "/tasks", {
4306
4673
  method: "POST",
4307
4674
  body: {
4308
4675
  content: task.title,
4309
4676
  description: task.body,
4310
- project_id: this.config.rootProjectId,
4677
+ project_id: projectId,
4311
4678
  ...sectionId ? { section_id: sectionId } : {},
4312
4679
  labels,
4313
4680
  priority: fromPriority(task.priority),
@@ -4343,6 +4710,42 @@ var TodoistProvider = class {
4343
4710
  body: { labels }
4344
4711
  });
4345
4712
  }
4713
+ /**
4714
+ * Find the sub-project named `name` under the bus root, creating it if absent.
4715
+ *
4716
+ * Per-project sub-projects are the filing convention: a flat pile in the root
4717
+ * buries findings across projects. This exists so the convention can be
4718
+ * executed rather than re-derived — a session that has to infer structure
4719
+ * from the existing project list will sometimes infer cautiously and file
4720
+ * flat, which is the mess the convention prevents.
4721
+ *
4722
+ * Matching is case-insensitive and ignores decoration, so "Whazaa" finds an
4723
+ * existing "Whazaa 🐝" rather than creating a near-duplicate.
4724
+ */
4725
+ async findOrCreateSubProject(name) {
4726
+ const token = this.token();
4727
+ if (!token || !this.config.rootProjectId) throw new Error("Todoist provider is not configured — run `pai task config`.");
4728
+ const root = this.config.rootProjectId;
4729
+ const projects = await collect(token, "/projects");
4730
+ const want = normalizeName(name);
4731
+ for (const p of projects) {
4732
+ if (p.is_archived || p.is_deleted) continue;
4733
+ if (p.parent_id === root && normalizeName(p.name) === want) return {
4734
+ id: p.id,
4735
+ created: false
4736
+ };
4737
+ }
4738
+ return {
4739
+ id: (await call(token, "/projects", {
4740
+ method: "POST",
4741
+ body: {
4742
+ name,
4743
+ parent_id: root
4744
+ }
4745
+ })).id,
4746
+ created: true
4747
+ };
4748
+ }
4346
4749
  /** Append a comment — used to keep run history on the task itself. */
4347
4750
  async comment(id, content) {
4348
4751
  const token = this.token();
@@ -4505,7 +4908,7 @@ function registerSetupCommand(program) {
4505
4908
  //#region src/obsidian/sync/symlinks.ts
4506
4909
  /** Symlink management: create, validate, migrate, and clean vault project symlinks. */
4507
4910
  /** Find the project-root Notes directory for a project. */
4508
- function findNotesDir$1(rootPath) {
4911
+ function findNotesDir(rootPath) {
4509
4912
  const canonical = join(rootPath, "Notes");
4510
4913
  if (existsSync(canonical)) return canonical;
4511
4914
  const alt = join(rootPath, ".claude", "Notes");
@@ -4516,7 +4919,7 @@ function findNotesDir$1(rootPath) {
4516
4919
  * Find the Claude Code session notes directory from the registry-stored value.
4517
4920
  * Returns null if not set, missing on disk, or identical to notesDir.
4518
4921
  */
4519
- function findClaudeNotesDir(claudeNotesDirFromRegistry, notesDir) {
4922
+ function findClaudeNotesDir$1(claudeNotesDirFromRegistry, notesDir) {
4520
4923
  if (!claudeNotesDirFromRegistry) return null;
4521
4924
  if (!existsSync(claudeNotesDirFromRegistry)) return null;
4522
4925
  if (notesDir && claudeNotesDirFromRegistry === notesDir) return null;
@@ -4629,8 +5032,8 @@ function syncVault(vaultPath, db) {
4629
5032
  ORDER BY status ASC, slug ASC`).all();
4630
5033
  const takenSlugs = /* @__PURE__ */ new Set();
4631
5034
  for (const project of projects) if (project.status === "active") {
4632
- const notesDir = findNotesDir$1(project.root_path);
4633
- const claudeNotesDir = findClaudeNotesDir(project.claude_notes_dir, notesDir);
5035
+ const notesDir = findNotesDir(project.root_path);
5036
+ const claudeNotesDir = findClaudeNotesDir$1(project.claude_notes_dir, notesDir);
4634
5037
  if (!notesDir && !claudeNotesDir) continue;
4635
5038
  const slug = uniqueSlug(project.slug, takenSlugs);
4636
5039
  takenSlugs.add(slug);
@@ -4708,8 +5111,8 @@ function generateIndex(vaultPath, db) {
4708
5111
  for (const row of active) {
4709
5112
  const tags = getTagsForProject.all(row.id).map((t) => `\`${t.name}\``).join(" ");
4710
5113
  const lastActive = fmtDate(row.last_active);
4711
- const notesDir = findNotesDir$1(row.root_path);
4712
- const claudeNotesDir = findClaudeNotesDir(row.claude_notes_dir, notesDir);
5114
+ const notesDir = findNotesDir(row.root_path);
5115
+ const claudeNotesDir = findClaudeNotesDir$1(row.claude_notes_dir, notesDir);
4713
5116
  const sources = [];
4714
5117
  if (notesDir) sources.push("notes");
4715
5118
  if (claudeNotesDir) sources.push("sessions");
@@ -5393,7 +5796,7 @@ async function cmdExplore(note, opts) {
5393
5796
  const depth = parseInt(opts.depth ?? "3", 10);
5394
5797
  const direction = opts.direction ?? "both";
5395
5798
  const mode = opts.mode ?? "all";
5396
- const { zettelExplore } = await import("./zettelkasten-NwBD1NsT.mjs");
5799
+ const { zettelExplore } = await import("./zettelkasten-CTlt1dGv.mjs");
5397
5800
  const result = zettelExplore(getFedDb(), {
5398
5801
  startNote: note,
5399
5802
  depth,
@@ -5457,7 +5860,7 @@ async function cmdHealth(opts) {
5457
5860
  const projectPath = opts.project;
5458
5861
  const recentDays = parseInt(opts.days ?? "30", 10);
5459
5862
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
5460
- const { zettelHealth } = await import("./zettelkasten-NwBD1NsT.mjs");
5863
+ const { zettelHealth } = await import("./zettelkasten-CTlt1dGv.mjs");
5461
5864
  const result = zettelHealth(getFedDb(), {
5462
5865
  scope,
5463
5866
  projectPath,
@@ -5526,7 +5929,7 @@ async function cmdSurprise(note, opts) {
5526
5929
  const limit = parseInt(opts.limit ?? "10", 10);
5527
5930
  const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
5528
5931
  const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
5529
- const { zettelSurprise } = await import("./zettelkasten-NwBD1NsT.mjs");
5932
+ const { zettelSurprise } = await import("./zettelkasten-CTlt1dGv.mjs");
5530
5933
  const db = getFedDb();
5531
5934
  console.log();
5532
5935
  console.log(header(" PAI Zettel Surprise"));
@@ -5579,7 +5982,7 @@ async function cmdSuggest(note, opts) {
5579
5982
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
5580
5983
  const limit = parseInt(opts.limit ?? "5", 10);
5581
5984
  const excludeLinked = opts.excludeLinked !== false;
5582
- const { zettelSuggest } = await import("./zettelkasten-NwBD1NsT.mjs");
5985
+ const { zettelSuggest } = await import("./zettelkasten-CTlt1dGv.mjs");
5583
5986
  const db = getFedDb();
5584
5987
  console.log();
5585
5988
  console.log(header(" PAI Zettel Suggest"));
@@ -5630,7 +6033,7 @@ async function cmdConverse(question, opts) {
5630
6033
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
5631
6034
  const depth = parseInt(opts.depth ?? "2", 10);
5632
6035
  const limit = parseInt(opts.limit ?? "15", 10);
5633
- const { zettelConverse } = await import("./zettelkasten-NwBD1NsT.mjs");
6036
+ const { zettelConverse } = await import("./zettelkasten-CTlt1dGv.mjs");
5634
6037
  const db = getFedDb();
5635
6038
  console.log();
5636
6039
  console.log(header(" PAI Zettel Converse"));
@@ -5691,7 +6094,7 @@ async function cmdThemes(opts) {
5691
6094
  const minClusterSize = parseInt(opts.minSize ?? "3", 10);
5692
6095
  const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
5693
6096
  const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
5694
- const { zettelThemes } = await import("./zettelkasten-NwBD1NsT.mjs");
6097
+ const { zettelThemes } = await import("./zettelkasten-CTlt1dGv.mjs");
5695
6098
  const db = getFedDb();
5696
6099
  console.log();
5697
6100
  console.log(header(" PAI Zettel Themes"));
@@ -7387,7 +7790,7 @@ function registerTaskCommands(taskCmd) {
7387
7790
  limit: opts.limit
7388
7791
  }));
7389
7792
  });
7390
- taskCmd.command("add <title>").description("File a task onto the bus").option("--owner <project>", "PAI project that owns this (adds a pai: label)").option("--body <text>", "Full procedure and reasoning — not just a restatement of the title").option("--due <date>", "Due date (ISO or natural language)").option("--priority <p>", "p1 (highest) … p4 (default)").option("--url <url>", "Reference — prefer a hook:// URL over a file path").action(async (title, opts) => {
7793
+ taskCmd.command("add <title>").description("File a task onto the bus").option("--owner <project>", "PAI project that owns this (adds a pai: label)").option("--body <text>", "Full procedure and reasoning — not just a restatement of the title").option("--due <date>", "Due date (ISO or natural language)").option("--priority <p>", "p1 (highest) … p4 (default)").option("--url <url>", "Reference — prefer a hook:// URL over a file path").option("--into <sub-project>", "File into this sub-project under the bus root, creating it if absent").action(async (title, opts) => {
7391
7794
  const provider = buildProvider();
7392
7795
  if (!provider) return reportUnconfigured();
7393
7796
  if (!opts.body) {
@@ -7400,7 +7803,8 @@ function registerTaskCommands(taskCmd) {
7400
7803
  owner: opts.owner ?? null,
7401
7804
  due: opts.due,
7402
7805
  priority: opts.priority,
7403
- sourceUrl: opts.url
7806
+ sourceUrl: opts.url,
7807
+ into: opts.into
7404
7808
  });
7405
7809
  console.log(chalk.green(` Filed: ${created.title}`));
7406
7810
  console.log(` ${renderOwner(created)} ${dim("· " + created.id)}`);
@@ -8277,211 +8681,1888 @@ function registerHelpCommand(program) {
8277
8681
  }
8278
8682
 
8279
8683
  //#endregion
8280
- //#region src/cli/commands/session/pause.ts
8281
- const HANDOVER_TODO_LOCATIONS = [
8282
- "Notes/TODO.md",
8283
- ".claude/Notes/TODO.md",
8284
- "tasks/todo.md",
8285
- "TODO.md"
8286
- ];
8287
- function findProjectTodo(rootPath) {
8288
- for (const rel of HANDOVER_TODO_LOCATIONS) {
8289
- const full = join(rootPath, rel);
8290
- if (existsSync(full)) try {
8291
- return {
8292
- path: full,
8293
- content: readFileSync(full, "utf8")
8294
- };
8295
- } catch {}
8296
- }
8684
+ //#region src/session/slug-generator.ts
8685
+ /**
8686
+ * Slug generator for PAI session notes.
8687
+ *
8688
+ * Extracts a 1-3 word descriptive slug from Claude Code JSONL transcripts
8689
+ * using keyword frequency analysis — no LLM required.
8690
+ *
8691
+ * JSONL format (one JSON object per line):
8692
+ * - type "user": { type: "user", message: { role: "user", content: string | [{ type, text }] } }
8693
+ * - type "assistant": { type: "assistant", message: { role: "assistant", content: [{ type, text }] } }
8694
+ */
8695
+ /**
8696
+ * Extract plain text from a parsed JSONL message object.
8697
+ */
8698
+ function extractText(obj) {
8699
+ const msg = obj.message;
8700
+ if (!msg) return null;
8701
+ const content = msg.content;
8702
+ if (Array.isArray(content)) {
8703
+ const texts = [];
8704
+ for (const block of content) {
8705
+ const b = block;
8706
+ if (b.type === "text" && typeof b.text === "string") texts.push(b.text);
8707
+ }
8708
+ return texts.join(" ") || null;
8709
+ }
8710
+ if (typeof content === "string") return content || null;
8297
8711
  return null;
8298
8712
  }
8299
- function stripContinueSection(content) {
8300
- const lines = content.split("\n");
8301
- const startIdx = lines.findIndex((l) => l.trim() === "## Continue");
8302
- if (startIdx === -1) return content;
8303
- let endIdx = lines.length;
8304
- for (let i = startIdx + 1; i < lines.length; i++) {
8305
- const trimmed = lines[i].trim();
8306
- if (trimmed === "---" || trimmed.startsWith("##") && trimmed !== "## Continue") {
8307
- endIdx = i;
8308
- break;
8309
- }
8310
- }
8311
- let trailingEnd = endIdx;
8312
- if (trailingEnd < lines.length && lines[trailingEnd].trim() === "---") trailingEnd += 1;
8313
- const before = lines.slice(0, startIdx);
8314
- const after = lines.slice(trailingEnd);
8315
- while (after.length > 0 && after[0].trim() === "") after.shift();
8316
- return [...before, ...after].join("\n");
8713
+ /**
8714
+ * Tokenize a string into lowercase words, filtering stop words and short tokens.
8715
+ */
8716
+ function tokenize$1(text) {
8717
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, " ").split(/\s+/).filter((w) => {
8718
+ if (w.length < 3) return false;
8719
+ if (/^\d+$/.test(w)) return false;
8720
+ if (STOP_WORDS.has(w)) return false;
8721
+ return true;
8722
+ });
8317
8723
  }
8318
- function cmdPause(db, opts) {
8319
- const cwd = process.cwd();
8320
- const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
8321
- FROM projects
8322
- WHERE ? LIKE root_path || '%'
8323
- ORDER BY length(root_path) DESC
8324
- LIMIT 1`).get(cwd);
8325
- if (!project) {
8326
- console.error(chalk.red("pai session pause: ") + `Current directory is not within a registered PAI project.
8327
- cwd: ${cwd}`);
8328
- process.exit(1);
8724
+ /**
8725
+ * Find the latest JSONL transcript for a project's Claude encoded directory.
8726
+ *
8727
+ * @param encodedDir Claude-encoded project directory name,
8728
+ * e.g. "-Users-alice-dev-ai-PAI"
8729
+ * @returns Absolute path to the most recently modified .jsonl file, or null.
8730
+ */
8731
+ function findLatestTranscript(encodedDir) {
8732
+ const sessionsDir = join(homedir(), ".claude", "projects", encodedDir, "sessions");
8733
+ if (!existsSync(sessionsDir)) return null;
8734
+ let entries;
8735
+ try {
8736
+ entries = readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
8737
+ } catch {
8738
+ return null;
8329
8739
  }
8330
- const session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
8331
- const todo = findProjectTodo(project.root_path);
8332
- let todoPath;
8333
- let existingContent;
8334
- if (todo) {
8335
- todoPath = todo.path;
8336
- existingContent = todo.content;
8337
- } else {
8338
- const notesDir = join(project.root_path, "Notes");
8339
- if (!opts.dryRun) try {
8340
- if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });
8341
- } catch (mkdirErr) {
8342
- console.error(chalk.red("pai session pause: ") + `Could not create Notes directory: ${String(mkdirErr)}`);
8343
- process.exit(1);
8740
+ if (!entries.length) return null;
8741
+ return entries.map((f) => {
8742
+ const fullPath = join(sessionsDir, f);
8743
+ try {
8744
+ return {
8745
+ path: fullPath,
8746
+ mtime: statSync(fullPath).mtimeMs
8747
+ };
8748
+ } catch {
8749
+ return {
8750
+ path: fullPath,
8751
+ mtime: 0
8752
+ };
8344
8753
  }
8345
- todoPath = join(notesDir, "TODO.md");
8346
- existingContent = "";
8754
+ }).sort((a, b) => b.mtime - a.mtime)[0].path;
8755
+ }
8756
+ /**
8757
+ * Read the last `count` human/assistant message pairs from a JSONL file.
8758
+ *
8759
+ * Synchronous — reads the whole file in memory. JSONL transcripts are
8760
+ * bounded by context window size so this is safe even for large sessions.
8761
+ *
8762
+ * @param jsonlPath Absolute path to a Claude Code .jsonl transcript file.
8763
+ * @param count Number of conversation pairs to read (default: 15).
8764
+ * @returns Array of plain-text message strings (up to count*2 entries).
8765
+ */
8766
+ function readLastMessages(jsonlPath, count = 15) {
8767
+ if (!existsSync(jsonlPath)) return [];
8768
+ let raw;
8769
+ try {
8770
+ raw = readFileSync(jsonlPath, "utf8");
8771
+ } catch {
8772
+ return [];
8347
8773
  }
8348
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
8349
- let sessionLine;
8350
- if (session) {
8351
- const num = String(session.number).padStart(4, "0");
8352
- const titlePart = session.title || session.slug || "Session";
8353
- sessionLine = `${num} - ${session.date} - ${titlePart}`;
8354
- } else sessionLine = "Unknown session";
8355
- const continueBlock = [
8356
- "## Continue",
8357
- "",
8358
- `> **Last session:** ${sessionLine}`,
8359
- `> **Paused at:** ${timestamp}`,
8360
- ">",
8361
- `> Working directory: ${cwd}. Check the latest session note for details.`,
8362
- "",
8363
- "---",
8364
- ""
8365
- ].join("\n");
8366
- const newContent = continueBlock + stripContinueSection(existingContent).trimStart();
8367
- if (opts.dryRun) {
8368
- console.log("\n" + chalk.bold("Dry run — would write to:") + " " + chalk.cyan(todoPath));
8369
- console.log(chalk.dim("─".repeat(60)));
8370
- console.log(chalk.dim(continueBlock));
8371
- console.log(chalk.dim("─".repeat(60)));
8372
- } else {
8373
- const tmpPath = `${todoPath}.pause.tmp`;
8774
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
8775
+ const messages = [];
8776
+ const limit = count * 2;
8777
+ for (let i = lines.length - 1; i >= 0 && messages.length < limit; i--) {
8778
+ let obj;
8374
8779
  try {
8375
- writeFileSync(tmpPath, newContent, "utf8");
8376
- renameSync(tmpPath, todoPath);
8377
- } catch (writeErr) {
8378
- try {
8379
- if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
8380
- } catch {}
8381
- console.error(chalk.red("pai session pause: ") + `Failed to write TODO.md: ${String(writeErr)}`);
8382
- process.exit(1);
8780
+ obj = JSON.parse(lines[i]);
8781
+ } catch {
8782
+ continue;
8383
8783
  }
8384
- console.log(chalk.green(" ## Continue written to: ") + chalk.cyan(todoPath));
8784
+ const type = obj.type;
8785
+ if (type !== "assistant" && type !== "user") continue;
8786
+ const text = extractText(obj);
8787
+ if (text && text.trim().length > 0) messages.unshift(text);
8385
8788
  }
8386
- const box = [
8387
- "",
8388
- chalk.bgRed.white.bold(" IMPORTANT: How to exit safely "),
8389
- "",
8390
- chalk.yellow(" Inside the Claude Code session, type: " + chalk.white.bold("/exit") + chalk.yellow(" (then press Enter)")),
8391
- "",
8392
- chalk.red.bold(" DO NOT press Ctrl+C."),
8393
- "",
8394
- chalk.dim(" Ctrl+C bypasses PAI's stop-hook, which means:"),
8395
- chalk.dim(" - No project snapshot is written to the top-level jsonl"),
8396
- chalk.dim(" - The session becomes orphaned (cannot --resume next time)"),
8397
- chalk.dim(" - pai session goto will launch a fresh session instead of resuming"),
8398
- "",
8399
- chalk.dim(" The ## Continue checkpoint in TODO.md is already saved."),
8400
- chalk.dim(" Use /exit to preserve full session resumability."),
8401
- ""
8402
- ].join("\n");
8403
- console.log(box);
8789
+ return messages;
8790
+ }
8791
+ /**
8792
+ * Generate a descriptive 1-3 word slug from conversation message strings.
8793
+ *
8794
+ * Algorithm:
8795
+ * 1. Tokenize all messages, remove stop words and short tokens
8796
+ * 2. Count word frequency
8797
+ * 3. Pick the top `maxWords` most frequent tokens
8798
+ * 4. Join with hyphen, lowercase
8799
+ *
8800
+ * @param messages Array of plain-text conversation messages.
8801
+ * @param opts Optional slug configuration.
8802
+ * @returns A lowercase hyphen-joined slug, e.g. "memory-engine-refactor",
8803
+ * or "unnamed-session" if no meaningful words are found.
8804
+ */
8805
+ function generateSlug(messages, opts) {
8806
+ const maxWords = opts?.maxWords ?? 3;
8807
+ const maxLength = opts?.maxLength ?? 30;
8808
+ if (!messages.length) return "unnamed-session";
8809
+ const freq = /* @__PURE__ */ new Map();
8810
+ for (const msg of messages) for (const token of tokenize$1(msg)) freq.set(token, (freq.get(token) ?? 0) + 1);
8811
+ if (!freq.size) return "unnamed-session";
8812
+ const topWords = Array.from(freq.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxWords).map(([word]) => word);
8813
+ if (!topWords.length) return "unnamed-session";
8814
+ let slug = topWords.join("-");
8815
+ if (slug.length > maxLength) slug = slug.slice(0, maxLength).replace(/-[^-]*$/, "");
8816
+ return slug || "unnamed-session";
8404
8817
  }
8405
8818
 
8406
8819
  //#endregion
8407
- //#region src/cli/commands/session/end.ts
8408
- /** PAI_DIR mirrors pai-paths.ts resolution. */
8409
- function getPaiDir() {
8410
- const envDir = process.env.PAI_DIR;
8411
- if (envDir) try {
8412
- return realpathSync(envDir);
8413
- } catch {
8414
- return envDir;
8820
+ //#region src/cli/commands/session/helpers.ts
8821
+ function getProject$1(db, slug) {
8822
+ return db.prepare("SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?").get(slug);
8823
+ }
8824
+ function statusColor(status) {
8825
+ switch (status) {
8826
+ case "completed": return chalk.green(status);
8827
+ case "compacted": return chalk.blue(status);
8828
+ default: return chalk.yellow(status);
8415
8829
  }
8416
- return join(homedir(), ".claude");
8417
8830
  }
8418
- /**
8419
- * Find the notes directory for a project — checks local first, then central.
8420
- * Returns null if neither exists (dry-run safe: don't create).
8421
- */
8422
- function findNotesDir(rootPath, encodedDir) {
8423
- for (const rel of [
8424
- "Notes",
8425
- "notes",
8426
- ".claude/Notes"
8427
- ]) {
8428
- const p = join(rootPath, rel);
8429
- if (existsSync(p)) return p;
8430
- }
8431
- const central = join(getPaiDir(), "projects", encodedDir, "Notes");
8432
- if (existsSync(central)) return central;
8433
- return null;
8831
+ /** Convert a slug to title-cased display name: "memory-engine" → "Memory Engine" */
8832
+ function toTitleCase$1(slug) {
8833
+ return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
8434
8834
  }
8435
- /**
8436
- * Find the current (latest) session note inside notesDir.
8437
- * Searches YYYY/MM subdirectory (current month, then previous month),
8438
- * then flat notesDir as legacy fallback.
8439
- */
8440
- function findLatestNote(notesDir) {
8441
- const findIn = (dir) => {
8442
- if (!existsSync(dir)) return null;
8443
- const files = readdirSync(dir).filter((f) => /^\d{3,4}[\s_-].*\.md$/.test(f)).sort((a, b) => {
8444
- return parseInt(a.match(/^(\d+)/)?.[1] ?? "0", 10) - parseInt(b.match(/^(\d+)/)?.[1] ?? "0", 10);
8445
- });
8446
- return files.length > 0 ? join(dir, files[files.length - 1]) : null;
8447
- };
8448
- const now = /* @__PURE__ */ new Date();
8449
- const current = findIn(join(notesDir, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0")));
8450
- if (current) return current;
8451
- const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
8452
- const prevFound = findIn(join(notesDir, String(prev.getFullYear()), String(prev.getMonth() + 1).padStart(2, "0")));
8453
- if (prevFound) return prevFound;
8454
- return findIn(notesDir);
8835
+ /** Notes directory for a project: ~/.claude/projects/<encoded_dir>/Notes/ */
8836
+ function getNotesDir(project) {
8837
+ return join(homedir(), ".claude", "projects", project.encoded_dir, "Notes");
8455
8838
  }
8456
- function finalizeNote(notePath) {
8457
- const content = readFileSync(notePath, "utf-8");
8458
- if (content.includes("**Status:** Completed")) return {
8459
- finalized: false,
8460
- path: notePath
8461
- };
8462
- let updated = content.replace("**Status:** In Progress", "**Status:** Completed");
8463
- if (!updated.includes("**Completed:**")) {
8464
- const completionTime = (/* @__PURE__ */ new Date()).toISOString();
8465
- updated = updated.replace("---\n\n## Work Done", `**Completed:** ${completionTime}\n\n---\n\n## Work Done`);
8839
+ /** Format a session filename: number=27, date="2026-02-23" → "0027 - 2026-02-23 - Title.md" */
8840
+ function formatFilename(number, date, titleSlug) {
8841
+ return `${String(number).padStart(4, "0")} - ${date} - ${titleSlug}.md`;
8842
+ }
8843
+ /** Resolve a session by project + number or "latest". Exits on failure. */
8844
+ function resolveSession(db, project, numberOrLatest) {
8845
+ let session;
8846
+ if (numberOrLatest === "latest") session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
8847
+ else {
8848
+ const num = parseInt(numberOrLatest, 10);
8849
+ if (isNaN(num)) {
8850
+ console.error(err(`Invalid session number: ${numberOrLatest}`));
8851
+ process.exit(1);
8852
+ }
8853
+ session = db.prepare("SELECT * FROM sessions WHERE project_id = ? AND number = ?").get(project.id, num);
8466
8854
  }
8467
- const tmp = `${notePath}.end.tmp`;
8468
- writeFileSync(tmp, updated, "utf-8");
8469
- renameSync(tmp, notePath);
8470
- return {
8471
- finalized: true,
8472
- path: notePath
8473
- };
8855
+ if (!session) {
8856
+ console.error(err(`Session ${numberOrLatest} not found in project ${project.slug}`));
8857
+ process.exit(1);
8858
+ }
8859
+ return session;
8474
8860
  }
8475
- function cmdEnd(db, opts) {
8476
- cmdPause(db, opts);
8477
- const cwd = process.cwd();
8478
- const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
8479
- FROM projects
8480
- WHERE ? LIKE root_path || '%'
8481
- ORDER BY length(root_path) DESC
8861
+ function upsertTag(db, tagName) {
8862
+ db.prepare("INSERT OR IGNORE INTO tags (name) VALUES (?)").run(tagName);
8863
+ return db.prepare("SELECT id FROM tags WHERE name = ?").get(tagName).id;
8864
+ }
8865
+ function getSessionTags(db, sessionId) {
8866
+ return db.prepare(`SELECT t.name FROM tags t
8867
+ JOIN session_tags st ON st.tag_id = t.id
8868
+ WHERE st.session_id = ?
8869
+ ORDER BY t.name`).all(sessionId).map((r) => r.name);
8870
+ }
8871
+
8872
+ //#endregion
8873
+ //#region src/cli/commands/session/commands.ts
8874
+ function cmdList(db, projectSlug, opts) {
8875
+ const limit = parseInt(opts.limit ?? "20", 10);
8876
+ const params = [];
8877
+ let query = `
8878
+ SELECT s.*, p.slug AS project_slug, p.display_name AS project_name
8879
+ FROM sessions s
8880
+ JOIN projects p ON p.id = s.project_id
8881
+ `;
8882
+ const where = [];
8883
+ if (projectSlug) {
8884
+ const project = getProject$1(db, projectSlug);
8885
+ if (!project) {
8886
+ console.error(err(`Project not found: ${projectSlug}`));
8887
+ process.exit(1);
8888
+ }
8889
+ where.push("s.project_id = ?");
8890
+ params.push(project.id);
8891
+ }
8892
+ if (opts.status) {
8893
+ where.push("s.status = ?");
8894
+ params.push(opts.status);
8895
+ }
8896
+ if (where.length) query += " WHERE " + where.join(" AND ");
8897
+ query += " ORDER BY s.date DESC, s.number DESC";
8898
+ query += ` LIMIT ${limit}`;
8899
+ const rows = db.prepare(query).all(...params);
8900
+ if (!rows.length) {
8901
+ console.log(warn("No sessions found."));
8902
+ return;
8903
+ }
8904
+ const showProject = !projectSlug;
8905
+ const headers = showProject ? [
8906
+ "#",
8907
+ "Project",
8908
+ "Date",
8909
+ "Title",
8910
+ "Status",
8911
+ "Tokens"
8912
+ ] : [
8913
+ "#",
8914
+ "Date",
8915
+ "Title",
8916
+ "Status",
8917
+ "Tokens"
8918
+ ];
8919
+ const tableRows = rows.map((r) => {
8920
+ const title = r.title.length > 45 ? r.title.slice(0, 42) + "..." : r.title;
8921
+ const tokens = r.token_count != null ? dim$1(r.token_count.toLocaleString()) : dim$1("—");
8922
+ if (showProject) return [
8923
+ dim$1(`#${r.number}`),
8924
+ r.project_slug,
8925
+ r.date,
8926
+ title,
8927
+ statusColor(r.status),
8928
+ tokens
8929
+ ];
8930
+ return [
8931
+ dim$1(`#${r.number}`),
8932
+ r.date,
8933
+ title,
8934
+ statusColor(r.status),
8935
+ tokens
8936
+ ];
8937
+ });
8938
+ console.log();
8939
+ if (projectSlug) {
8940
+ const project = getProject$1(db, projectSlug);
8941
+ console.log(` ${bold$1(project.display_name)} sessions:`);
8942
+ console.log();
8943
+ }
8944
+ console.log(renderTable(headers, tableRows));
8945
+ console.log();
8946
+ console.log(dim$1(` ${rows.length} session(s) shown (limit: ${limit})`));
8947
+ }
8948
+ function cmdInfo(db, projectSlug, sessionNumber) {
8949
+ const project = getProject$1(db, projectSlug);
8950
+ if (!project) {
8951
+ console.error(err(`Project not found: ${projectSlug}`));
8952
+ process.exit(1);
8953
+ }
8954
+ const session = resolveSession(db, project, sessionNumber);
8955
+ console.log();
8956
+ console.log(header(` Session #${session.number}: ${session.title}`));
8957
+ console.log();
8958
+ console.log(` ${bold$1("Project:")} ${project.display_name} (${project.slug})`);
8959
+ console.log(` ${bold$1("Date:")} ${session.date}`);
8960
+ console.log(` ${bold$1("Status:")} ${statusColor(session.status)}`);
8961
+ console.log(` ${bold$1("Filename:")} ${session.filename}`);
8962
+ console.log(` ${bold$1("Slug:")} ${session.slug}`);
8963
+ if (session.claude_session_id) console.log(` ${bold$1("Claude ID:")} ${dim$1(session.claude_session_id)}`);
8964
+ if (session.token_count != null) console.log(` ${bold$1("Tokens:")} ${session.token_count.toLocaleString()}`);
8965
+ console.log(` ${bold$1("Created:")} ${fmtDate(session.created_at)}`);
8966
+ if (session.closed_at) console.log(` ${bold$1("Closed:")} ${fmtDate(session.closed_at)}`);
8967
+ console.log();
8968
+ }
8969
+ function cmdRename(db, projectSlug, numberOrLatest, newSlug) {
8970
+ const project = getProject$1(db, projectSlug);
8971
+ if (!project) {
8972
+ console.error(err(`Project not found: ${projectSlug}`));
8973
+ process.exit(1);
8974
+ }
8975
+ const session = resolveSession(db, project, numberOrLatest);
8976
+ const notesDir = getNotesDir(project);
8977
+ if (!existsSync(notesDir)) {
8978
+ console.error(err(`Notes directory not found: ${notesDir}`));
8979
+ process.exit(1);
8980
+ }
8981
+ const titleSlug = toTitleCase$1(newSlug);
8982
+ const newFilename = formatFilename(session.number, session.date, titleSlug);
8983
+ const oldPath = join(notesDir, session.filename);
8984
+ const newPath = join(notesDir, newFilename);
8985
+ if (existsSync(oldPath)) {
8986
+ if (oldPath !== newPath) try {
8987
+ renameSync(oldPath, newPath);
8988
+ } catch (e) {
8989
+ console.error(err(`Failed to rename file: ${e}`));
8990
+ process.exit(1);
8991
+ }
8992
+ } else {
8993
+ console.log(warn(` Note: file not found at expected path: ${session.filename}`));
8994
+ console.log(warn(` Skipping disk rename. Database will still be updated.`));
8995
+ }
8996
+ if (existsSync(newPath)) try {
8997
+ const lines = readFileSync(newPath, "utf8").split("\n");
8998
+ let h1Updated = false;
8999
+ const updated = lines.map((line) => {
9000
+ if (!h1Updated && line.startsWith("# ")) {
9001
+ h1Updated = true;
9002
+ return `# ${titleSlug}`;
9003
+ }
9004
+ return line;
9005
+ });
9006
+ if (!h1Updated) updated.unshift(`# ${titleSlug}`, "");
9007
+ writeFileSync(newPath, updated.join("\n"), "utf8");
9008
+ } catch (e) {
9009
+ console.error(err(`Failed to update H1 in file: ${e}`));
9010
+ }
9011
+ const normalizedSlug = newSlug.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9012
+ db.prepare("UPDATE sessions SET slug = ?, title = ?, filename = ? WHERE id = ?").run(normalizedSlug, titleSlug, newFilename, session.id);
9013
+ console.log();
9014
+ console.log(ok(` Session #${session.number} renamed.`));
9015
+ console.log(` ${bold$1("Old:")} ${session.filename}`);
9016
+ console.log(` ${bold$1("New:")} ${newFilename}`);
9017
+ console.log(` ${bold$1("Slug:")} ${normalizedSlug}`);
9018
+ console.log(` ${bold$1("Title:")} ${titleSlug}`);
9019
+ console.log();
9020
+ }
9021
+ function cmdSlug(db, projectSlug, numberOrLatest, opts) {
9022
+ const project = getProject$1(db, projectSlug);
9023
+ if (!project) {
9024
+ console.error(err(`Project not found: ${projectSlug}`));
9025
+ process.exit(1);
9026
+ }
9027
+ const session = resolveSession(db, project, numberOrLatest);
9028
+ const transcriptPath = findLatestTranscript(project.encoded_dir);
9029
+ if (!transcriptPath) {
9030
+ console.log(warn(` No JSONL transcripts found for project ${projectSlug}`));
9031
+ console.log("unnamed-session");
9032
+ return;
9033
+ }
9034
+ const messages = readLastMessages(transcriptPath);
9035
+ if (messages.length < 2) {
9036
+ console.log(warn(` Too few messages found (${messages.length}) in transcript`));
9037
+ console.log("unnamed-session");
9038
+ return;
9039
+ }
9040
+ const generatedSlug = generateSlug(messages);
9041
+ console.log(generatedSlug);
9042
+ if (opts.apply) {
9043
+ console.log();
9044
+ console.log(dim$1(` Applying slug to session #${session.number}...`));
9045
+ cmdRename(db, projectSlug, String(session.number), generatedSlug);
9046
+ }
9047
+ }
9048
+ function cmdTag(db, projectSlug, sessionNumber, rawTags) {
9049
+ const project = getProject$1(db, projectSlug);
9050
+ if (!project) {
9051
+ console.error(err(`Project not found: ${projectSlug}`));
9052
+ process.exit(1);
9053
+ }
9054
+ const session = resolveSession(db, project, sessionNumber);
9055
+ if (rawTags.length === 0) {
9056
+ const current = getSessionTags(db, session.id);
9057
+ console.log();
9058
+ if (current.length === 0) console.log(dim$1(` Session #${session.number} has no tags.`));
9059
+ else console.log(` ${bold$1(`Session #${session.number}`)} tags: ${current.map((t) => chalk.cyan(t)).join(", ")}`);
9060
+ console.log();
9061
+ return;
9062
+ }
9063
+ const tags = rawTags.flatMap((t) => t.split(",")).map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
9064
+ if (tags.length === 0) {
9065
+ console.log(warn("No valid tags provided."));
9066
+ return;
9067
+ }
9068
+ const added = [];
9069
+ const skipped = [];
9070
+ for (const tagName of tags) {
9071
+ const tagId = upsertTag(db, tagName);
9072
+ if (db.prepare("SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ?").get(session.id, tagId)) skipped.push(tagName);
9073
+ else {
9074
+ db.prepare("INSERT INTO session_tags (session_id, tag_id) VALUES (?, ?)").run(session.id, tagId);
9075
+ added.push(tagName);
9076
+ }
9077
+ }
9078
+ console.log();
9079
+ if (added.length) console.log(ok(` Tagged session #${session.number}: ${added.map((t) => chalk.cyan(t)).join(", ")}`));
9080
+ if (skipped.length) console.log(dim$1(` Already present: ${skipped.join(", ")}`));
9081
+ const allTags = getSessionTags(db, session.id);
9082
+ console.log(` ${bold$1("All tags:")} ${allTags.map((t) => chalk.cyan(t)).join(", ")}`);
9083
+ console.log();
9084
+ }
9085
+ function cmdRoute(db, projectSlug, sessionNumber, targetProjectSlug, opts) {
9086
+ const project = getProject$1(db, projectSlug);
9087
+ if (!project) {
9088
+ console.error(err(`Project not found: ${projectSlug}`));
9089
+ process.exit(1);
9090
+ }
9091
+ const session = resolveSession(db, project, sessionNumber);
9092
+ const targetProject = db.prepare("SELECT id, slug, display_name FROM projects WHERE slug = ?").get(targetProjectSlug);
9093
+ if (!targetProject) {
9094
+ console.error(err(`Target project not found: ${targetProjectSlug}`));
9095
+ process.exit(1);
9096
+ }
9097
+ const validTypes = [
9098
+ "related",
9099
+ "follow-up",
9100
+ "reference"
9101
+ ];
9102
+ const linkType = opts.type ?? "related";
9103
+ if (!validTypes.includes(linkType)) {
9104
+ console.error(err(`Invalid link type "${linkType}". Valid: ${validTypes.join(", ")}`));
9105
+ process.exit(1);
9106
+ }
9107
+ try {
9108
+ db.prepare(`INSERT INTO links (session_id, target_project_id, link_type, created_at)
9109
+ VALUES (?, ?, ?, ?)`).run(session.id, targetProject.id, linkType, Date.now());
9110
+ } catch {
9111
+ console.log(warn(` Link already exists: session #${session.number} → ${targetProjectSlug}`));
9112
+ return;
9113
+ }
9114
+ console.log();
9115
+ console.log(ok(` Linked session #${session.number} (${project.slug}) → ${targetProject.display_name} (${targetProjectSlug})`));
9116
+ console.log(dim$1(` Link type: ${linkType}`));
9117
+ console.log();
9118
+ }
9119
+ function cmdActive(db, opts) {
9120
+ const minutes = parseInt(opts.minutes ?? "60", 10);
9121
+ const cutoff = Date.now() - minutes * 60 * 1e3;
9122
+ const claudeProjectsDir = join(homedir(), ".claude", "projects");
9123
+ if (!existsSync(claudeProjectsDir)) {
9124
+ console.log(err("Claude projects directory not found."));
9125
+ return;
9126
+ }
9127
+ const active = [];
9128
+ const entries = readdirSync(claudeProjectsDir);
9129
+ for (const entry of entries) {
9130
+ const projectDir = join(claudeProjectsDir, entry);
9131
+ try {
9132
+ if (!statSync(projectDir).isDirectory()) continue;
9133
+ } catch {
9134
+ continue;
9135
+ }
9136
+ let latestJsonl = null;
9137
+ let latestMtime = 0;
9138
+ try {
9139
+ for (const file of readdirSync(projectDir)) {
9140
+ if (!file.endsWith(".jsonl")) continue;
9141
+ const filePath = join(projectDir, file);
9142
+ try {
9143
+ const mtime = statSync(filePath).mtimeMs;
9144
+ if (mtime > latestMtime) {
9145
+ latestMtime = mtime;
9146
+ latestJsonl = filePath;
9147
+ }
9148
+ } catch {
9149
+ continue;
9150
+ }
9151
+ }
9152
+ } catch {
9153
+ continue;
9154
+ }
9155
+ if (!latestJsonl || latestMtime < cutoff) continue;
9156
+ const project = db.prepare("SELECT slug, display_name, root_path FROM projects WHERE encoded_dir = ?").get(entry);
9157
+ active.push({
9158
+ slug: project?.slug ?? entry,
9159
+ displayName: project?.display_name ?? project?.slug ?? entry,
9160
+ rootPath: project?.root_path ?? "",
9161
+ encodedDir: entry,
9162
+ lastModified: new Date(latestMtime),
9163
+ jsonlFile: latestJsonl
9164
+ });
9165
+ }
9166
+ active.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
9167
+ const seen = /* @__PURE__ */ new Set();
9168
+ const deduped = active.filter((a) => {
9169
+ const key = a.slug.replace(/-\d+$/, "");
9170
+ if (seen.has(key)) return false;
9171
+ seen.add(key);
9172
+ return true;
9173
+ });
9174
+ if (opts.json) {
9175
+ console.log(JSON.stringify(deduped.map((a) => ({
9176
+ slug: a.slug,
9177
+ display_name: a.displayName,
9178
+ root_path: a.rootPath,
9179
+ last_modified: a.lastModified.toISOString()
9180
+ })), null, 2));
9181
+ return;
9182
+ }
9183
+ if (deduped.length === 0) {
9184
+ console.log(dim$1(`No active sessions in the last ${minutes} minutes.`));
9185
+ return;
9186
+ }
9187
+ console.log(header(`Currently Active Sessions`) + dim$1(` (modified in last ${minutes}min)`));
9188
+ console.log();
9189
+ const rows = deduped.map((a) => {
9190
+ const time = a.lastModified.toTimeString().slice(0, 5);
9191
+ const dirName = a.rootPath ? a.rootPath.replace(homedir(), "~").split("/").pop() ?? a.slug : a.slug;
9192
+ return [
9193
+ chalk.cyan(dirName),
9194
+ dim$1(a.slug),
9195
+ chalk.green(time)
9196
+ ];
9197
+ });
9198
+ console.log(renderTable([
9199
+ "Directory",
9200
+ "Project",
9201
+ "Last Active"
9202
+ ], rows));
9203
+ }
9204
+ async function cmdAutoRoute(opts) {
9205
+ const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("./auto-route-CdvjEWs5.mjs");
9206
+ const { openRegistry } = await import("./db-BtuN768f.mjs").then((n) => n.t);
9207
+ const { createStorageBackend } = await import("./factory-HcQsQqh0.mjs").then((n) => n.n);
9208
+ const { loadConfig } = await import("./config-DGOfoGfm.mjs").then((n) => n.r);
9209
+ const config = loadConfig();
9210
+ const registryDb = openRegistry();
9211
+ const federation = await createStorageBackend(config);
9212
+ const targetCwd = opts.cwd ?? process.cwd();
9213
+ const result = await autoRoute(registryDb, federation, targetCwd, opts.context);
9214
+ if (!result) {
9215
+ console.log();
9216
+ console.log(warn(" No project match found for: " + targetCwd));
9217
+ console.log();
9218
+ console.log(dim$1(" Tried: path match, PAI.md marker walk") + (opts.context ? dim$1(", topic detection") : ""));
9219
+ console.log();
9220
+ console.log(dim$1(" Run 'pai project add .' to register this directory."));
9221
+ console.log();
9222
+ return;
9223
+ }
9224
+ if (opts.json) {
9225
+ console.log(formatAutoRouteJson(result));
9226
+ return;
9227
+ }
9228
+ console.log();
9229
+ console.log(header(" PAI Auto-Route"));
9230
+ console.log();
9231
+ console.log(` ${bold$1("Project:")} ${result.display_name}`);
9232
+ console.log(` ${bold$1("Slug:")} ${result.slug}`);
9233
+ console.log(` ${bold$1("Root path:")} ${result.root_path}`);
9234
+ console.log(` ${bold$1("Method:")} ${result.method}`);
9235
+ console.log(` ${bold$1("Confidence:")} ${(result.confidence * 100).toFixed(0)}%`);
9236
+ console.log();
9237
+ console.log(ok(" Routed to: ") + bold$1(result.slug));
9238
+ console.log();
9239
+ }
9240
+
9241
+ //#endregion
9242
+ //#region src/cli/commands/session/checkpoint.ts
9243
+ /**
9244
+ * Session checkpoint command — appends a timestamped block to the active
9245
+ * session note. Designed for use in hooks; fast, silent, rate-limited.
9246
+ */
9247
+ function findNotesDirForCwd() {
9248
+ const cwd = process.cwd();
9249
+ const claudeProjectsDir = join(homedir(), ".claude", "projects");
9250
+ if (!existsSync(claudeProjectsDir)) return null;
9251
+ const expectedEncoded = cwd.replace(/[/\s.\-]/g, "-");
9252
+ let encodedDir = null;
9253
+ try {
9254
+ const entries = readdirSync(claudeProjectsDir);
9255
+ if (entries.includes(expectedEncoded)) encodedDir = expectedEncoded;
9256
+ else for (const entry of entries) {
9257
+ const full = join(claudeProjectsDir, entry);
9258
+ try {
9259
+ if (!statSync(full).isDirectory()) continue;
9260
+ } catch {
9261
+ continue;
9262
+ }
9263
+ if (entry.replace(/-+$/, "") === expectedEncoded.replace(/-+$/, "")) {
9264
+ encodedDir = entry;
9265
+ break;
9266
+ }
9267
+ }
9268
+ } catch {
9269
+ return null;
9270
+ }
9271
+ if (!encodedDir) return null;
9272
+ const notesDir = join(claudeProjectsDir, encodedDir, "Notes");
9273
+ return existsSync(notesDir) ? notesDir : null;
9274
+ }
9275
+ function findLatestNoteFile(notesDir) {
9276
+ let entries;
9277
+ try {
9278
+ entries = readdirSync(notesDir);
9279
+ } catch {
9280
+ return null;
9281
+ }
9282
+ const mdFiles = entries.filter((e) => e.endsWith(".md"));
9283
+ if (mdFiles.length === 0) return null;
9284
+ let latestPath = null;
9285
+ let latestMtime = 0;
9286
+ for (const file of mdFiles) {
9287
+ const full = join(notesDir, file);
9288
+ try {
9289
+ const { mtimeMs } = statSync(full);
9290
+ if (mtimeMs > latestMtime) {
9291
+ latestMtime = mtimeMs;
9292
+ latestPath = full;
9293
+ }
9294
+ } catch {}
9295
+ }
9296
+ return latestPath;
9297
+ }
9298
+ function checkpointTooRecent(notesDir, minGapSeconds) {
9299
+ const safeKey = notesDir.replace(/[^a-zA-Z0-9]/g, "-").slice(-80);
9300
+ const tmpFile = join(tmpdir(), `pai-checkpoint-${safeKey}`);
9301
+ if (!existsSync(tmpFile)) return false;
9302
+ try {
9303
+ const { mtimeMs } = statSync(tmpFile);
9304
+ return Date.now() - mtimeMs < minGapSeconds * 1e3;
9305
+ } catch {
9306
+ return false;
9307
+ }
9308
+ }
9309
+ function touchCheckpointSentinel(notesDir) {
9310
+ const safeKey = notesDir.replace(/[^a-zA-Z0-9]/g, "-").slice(-80);
9311
+ const tmpFile = join(tmpdir(), `pai-checkpoint-${safeKey}`);
9312
+ try {
9313
+ writeFileSync(tmpFile, String(Date.now()), "utf8");
9314
+ } catch {}
9315
+ }
9316
+ function cmdCheckpoint(message, opts) {
9317
+ const minGapSeconds = parseInt(opts.minGap ?? "300", 10);
9318
+ const notesDir = findNotesDirForCwd();
9319
+ if (!notesDir) process.exit(0);
9320
+ if (checkpointTooRecent(notesDir, minGapSeconds)) process.exit(0);
9321
+ const notePath = findLatestNoteFile(notesDir);
9322
+ if (!notePath) process.exit(0);
9323
+ const block = `\n## Checkpoint — ${(/* @__PURE__ */ new Date()).toISOString()}\n${message}\n`;
9324
+ const tmpPath = `${notePath}.checkpoint.tmp`;
9325
+ try {
9326
+ writeFileSync(tmpPath, readFileSync(notePath, "utf8") + block, "utf8");
9327
+ renameSync(tmpPath, notePath);
9328
+ } catch {
9329
+ try {
9330
+ if (existsSync(tmpPath)) renameSync(tmpPath, tmpPath + ".dead");
9331
+ } catch {}
9332
+ process.exit(0);
9333
+ }
9334
+ touchCheckpointSentinel(notesDir);
9335
+ process.exit(0);
9336
+ }
9337
+
9338
+ //#endregion
9339
+ //#region src/cli/commands/session/pause.ts
9340
+ /** Format a session row as "0003 - 2026-08-01 - Title". */
9341
+ function formatSessionLine(session) {
9342
+ if (!session) return "Unknown session";
9343
+ const num = String(session.number).padStart(4, "0");
9344
+ const titlePart = session.title || session.slug || "Session";
9345
+ return `${num} - ${session.date} - ${titlePart}`;
9346
+ }
9347
+ /**
9348
+ * Resolve the string that identifies a session in a ## Continue block.
9349
+ *
9350
+ * THIS IS THE PRESERVATION KEY. It decides whether an authored checkpoint
9351
+ * belongs to the current session and must be left alone, so every writer must
9352
+ * derive it identically — `pai pause`, `pai session handover`, and the hooks'
9353
+ * updateTodoContinue. Deriving it in more than one place is how a checkpoint
9354
+ * gets clobbered: one writer says "0006 - …" and the other says
9355
+ * "Unknown session", the keys do not match, and the unattended writer replaces
9356
+ * work it should have preserved.
9357
+ *
9358
+ * The registry row is preferred. When the project has no session rows — which
9359
+ * happens whenever one codebase is registered under several paths — it falls
9360
+ * back to the session note's filename, which is what the hooks use.
9361
+ */
9362
+ function resolveSessionLine(project, session, notePath) {
9363
+ if (session) return formatSessionLine(session);
9364
+ if (notePath) return basename(notePath).replace(/\.md$/, "");
9365
+ return "Unknown session";
9366
+ }
9367
+ /** Locate the latest session note for a project, or null. */
9368
+ function resolveNotePath(project) {
9369
+ const notesDir = findNotesDir$1(project.root_path, project.encoded_dir ?? "");
9370
+ return notesDir ? findLatestNote(notesDir) : null;
9371
+ }
9372
+ /**
9373
+ * Resolve the project containing cwd, or undefined.
9374
+ *
9375
+ * Tries the literal path first, then its resolved form. A symlinked path
9376
+ * prefix (`~/dev -> Cloud/Development`) makes one directory reachable under
9377
+ * two spellings; matching only the literal string means a session started from
9378
+ * the other spelling finds nothing, and the scanner then registers a second
9379
+ * project for the same directory — which splits session history in half.
9380
+ */
9381
+ function resolveProjectByCwd(db, cwd) {
9382
+ const stmt = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
9383
+ FROM projects
9384
+ WHERE ? LIKE root_path || '%'
9385
+ ORDER BY length(root_path) DESC
9386
+ LIMIT 1`);
9387
+ const direct = stmt.get(cwd);
9388
+ if (direct) return direct;
9389
+ let resolved;
9390
+ try {
9391
+ resolved = realpathSync(cwd);
9392
+ } catch {
9393
+ return;
9394
+ }
9395
+ if (resolved === cwd) return void 0;
9396
+ return stmt.get(resolved);
9397
+ }
9398
+ function printMissingBodyError() {
9399
+ console.error("\n" + chalk.bgRed.white.bold(" CHECKPOINT BODY MISSING — nothing was written ") + "\n\n" + chalk.yellow(" A checkpoint without a body is just a timestamp. The next session\n would learn nothing about what was decided or what is blocked.\n") + "\n" + chalk.white(" Write your checkpoint markdown to a file, then re-run:\n") + "\n" + chalk.cyan(" pai pause --body-file /path/to/checkpoint.md \\\n") + chalk.cyan(" --session-id <claude-session-uuid>\n") + "\n" + chalk.dim(" Or pipe it: ") + chalk.cyan("cat checkpoint.md | pai pause --body-file -\n") + "\n" + chalk.dim(" If you genuinely want a metadata-only checkpoint, pass --no-body.\n"));
9400
+ }
9401
+ function cmdPause(db, opts) {
9402
+ let body = "";
9403
+ if (opts.bodyFile) {
9404
+ try {
9405
+ body = readBodyFile(opts.bodyFile).trim();
9406
+ } catch (err) {
9407
+ console.error(chalk.red("pai pause: ") + `Could not read checkpoint body from ${opts.bodyFile}: ${String(err)}`);
9408
+ process.exit(1);
9409
+ }
9410
+ if (!body) {
9411
+ console.error(chalk.red("pai pause: ") + `Checkpoint body at ${opts.bodyFile} is empty.`);
9412
+ process.exit(1);
9413
+ }
9414
+ } else if (!opts.noBody) {
9415
+ printMissingBodyError();
9416
+ process.exit(1);
9417
+ }
9418
+ const cwd = process.cwd();
9419
+ const project = resolveProjectByCwd(db, cwd);
9420
+ if (!project) {
9421
+ console.error(chalk.red("pai pause: ") + `Current directory is not within a registered PAI project.
9422
+ cwd: ${cwd}`);
9423
+ process.exit(1);
9424
+ }
9425
+ const session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
9426
+ const notePath = resolveNotePath(project);
9427
+ const sessionLine = resolveSessionLine(project, session, notePath);
9428
+ if (!session) {
9429
+ const siblings = db.prepare(`SELECT p.slug, p.root_path, COUNT(s.id) AS n
9430
+ FROM projects p LEFT JOIN sessions s ON s.project_id = p.id
9431
+ WHERE p.id != ? AND (p.slug = ? OR p.slug LIKE ? || '-%' OR ? LIKE p.slug || '-%')
9432
+ GROUP BY p.id HAVING n > 0
9433
+ ORDER BY n DESC LIMIT 3`).all(project.id, project.slug, project.slug, project.slug);
9434
+ console.log(chalk.yellow(` Note: project '${project.slug}' has no registered sessions; the checkpoint is labelled from the session note instead ("${sessionLine}").`));
9435
+ for (const s of siblings) console.log(chalk.dim(` '${s.slug}' (${s.root_path}) has ${s.n} — likely the same project under another path.`));
9436
+ }
9437
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9438
+ const result = applyContinue({
9439
+ rootPath: project.root_path,
9440
+ authored: body ? "model" : "auto",
9441
+ sessionLine,
9442
+ sessionId: opts.sessionId,
9443
+ cwd,
9444
+ body,
9445
+ timestamp,
9446
+ dryRun: opts.dryRun
9447
+ });
9448
+ if (result.action === "failed") {
9449
+ console.error(chalk.red("pai pause: ") + `Failed to write TODO.md: ${result.error}`);
9450
+ process.exit(1);
9451
+ }
9452
+ if (opts.dryRun) {
9453
+ console.log("\n" + chalk.bold("Dry run — would write to:") + " " + chalk.cyan(result.path ?? "?"));
9454
+ console.log(chalk.dim("─".repeat(60)));
9455
+ console.log(chalk.dim(result.block));
9456
+ console.log(chalk.dim("─".repeat(60)));
9457
+ } else console.log(chalk.green(" ## Continue written to: ") + chalk.cyan(result.path ?? "?"));
9458
+ if (body) if (!notePath) console.log(chalk.yellow(" (No session note found — checkpoint saved to TODO.md only.)"));
9459
+ else if (opts.dryRun) console.log(chalk.dim(` Would append checkpoint to: ${basename(notePath)}`));
9460
+ else {
9461
+ const { appended, error } = appendCheckpointToNote(notePath, body, timestamp);
9462
+ if (appended) console.log(chalk.green(" Checkpoint appended to note: ") + chalk.cyan(basename(notePath)));
9463
+ else if (error) console.log(chalk.yellow(` (Could not append to session note: ${error})`));
9464
+ }
9465
+ const box = [
9466
+ "",
9467
+ chalk.bgRed.white.bold(" IMPORTANT: How to exit safely "),
9468
+ "",
9469
+ chalk.yellow(" Inside the Claude Code session, type: " + chalk.white.bold("/exit") + chalk.yellow(" (then press Enter)")),
9470
+ "",
9471
+ chalk.red.bold(" DO NOT press Ctrl+C."),
9472
+ "",
9473
+ chalk.dim(" Ctrl+C bypasses PAI's stop-hook, which means:"),
9474
+ chalk.dim(" - No project snapshot is written to the top-level jsonl"),
9475
+ chalk.dim(" - The session becomes orphaned (cannot --resume next time)"),
9476
+ chalk.dim(" - pai session goto will launch a fresh session instead of resuming"),
9477
+ "",
9478
+ chalk.dim(" The ## Continue checkpoint in TODO.md is already saved."),
9479
+ chalk.dim(" Use /exit to preserve full session resumability."),
9480
+ ""
9481
+ ].join("\n");
9482
+ console.log(box);
9483
+ }
9484
+
9485
+ //#endregion
9486
+ //#region src/cli/commands/session/handover.ts
9487
+ function cmdHandover(db, projectSlug, numberOrLatest) {
9488
+ let project;
9489
+ if (projectSlug) {
9490
+ project = db.prepare("SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?").get(projectSlug);
9491
+ if (!project) process.exit(0);
9492
+ } else {
9493
+ const row = resolveProjectByCwd(db, process.cwd());
9494
+ if (!row) process.exit(0);
9495
+ project = row;
9496
+ }
9497
+ let session;
9498
+ const nol = numberOrLatest ?? "latest";
9499
+ if (nol === "latest") session = db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(project.id);
9500
+ else {
9501
+ const num = parseInt(nol, 10);
9502
+ if (!isNaN(num)) session = db.prepare("SELECT * FROM sessions WHERE project_id = ? AND number = ?").get(project.id, num);
9503
+ }
9504
+ applyContinue({
9505
+ rootPath: project.root_path,
9506
+ authored: "auto",
9507
+ sessionLine: resolveSessionLine(project, session, resolveNotePath(project)),
9508
+ cwd: process.cwd()
9509
+ });
9510
+ process.exit(0);
9511
+ }
9512
+
9513
+ //#endregion
9514
+ //#region src/cli/commands/session/recent.ts
9515
+ function fmtSize(bytes) {
9516
+ if (bytes < 1024) return `${bytes}B`;
9517
+ if (bytes < 1048576) return `${Math.round(bytes / 1024)}K`;
9518
+ return `${(bytes / 1048576).toFixed(1)}M`;
9519
+ }
9520
+ function fmtStatus(status) {
9521
+ switch (status) {
9522
+ case "resumable": return chalk.green("resumable");
9523
+ case "stub": return chalk.yellow("stub");
9524
+ case "transcript-only": return chalk.yellow("transcript-only");
9525
+ case "orphan": return chalk.dim("orphan");
9526
+ }
9527
+ }
9528
+ function renderLiveSessions(allLiveSessions, allTabs) {
9529
+ const displayed = allTabs ? allLiveSessions : allLiveSessions.filter((s) => s.kind !== "shell");
9530
+ const skippedCount = allLiveSessions.length - displayed.length;
9531
+ if (displayed.length === 0) return;
9532
+ console.log("\n" + header("Live Sessions") + "\n");
9533
+ const liveHeaders = [
9534
+ "#",
9535
+ "id",
9536
+ "name",
9537
+ "at prompt",
9538
+ "kind"
9539
+ ];
9540
+ const liveRows = displayed.map((s, idx) => {
9541
+ const shortId = s.sessionId.slice(0, 8);
9542
+ const rawName = s.paiName ?? s.name;
9543
+ const name = rawName.length > 36 ? rawName.slice(0, 35) + "…" : rawName;
9544
+ const atPrompt = s.atPrompt ? chalk.green("yes") : chalk.yellow("busy");
9545
+ const kind = s.kind === "claude" ? chalk.cyan(s.kind) : chalk.dim(s.kind);
9546
+ return [
9547
+ chalk.dim(String(idx + 1)),
9548
+ chalk.cyan(shortId),
9549
+ name,
9550
+ atPrompt,
9551
+ kind
9552
+ ];
9553
+ });
9554
+ console.log(renderTable(liveHeaders, liveRows));
9555
+ if (skippedCount > 0) console.log(dim$1(` (${skippedCount} shell tab${skippedCount === 1 ? "" : "s"} hidden — use --all-tabs to show)`));
9556
+ }
9557
+ async function cmdRecent(db, opts) {
9558
+ const limit = parseInt(opts.n ?? "20", 10);
9559
+ const includeAll = opts.all === true;
9560
+ const allTabs = opts.allTabs === true;
9561
+ const liveSessions = await fetchLiveSessions();
9562
+ const sessions = scanSessions(db, {
9563
+ limit,
9564
+ filter: includeAll ? "all" : "named"
9565
+ });
9566
+ if (opts.json) {
9567
+ const output = {
9568
+ live: liveSessions.map((s) => ({
9569
+ index: s.index,
9570
+ sessionId: s.sessionId,
9571
+ name: s.name,
9572
+ paiName: s.paiName,
9573
+ atPrompt: s.atPrompt,
9574
+ kind: s.kind,
9575
+ active: s.active
9576
+ })),
9577
+ paused: sessions.map((s, idx) => ({
9578
+ idx: idx + 1,
9579
+ uuid: s.uuid,
9580
+ shortId: s.shortId,
9581
+ resumable: s.resumable,
9582
+ sessionStatus: s.sessionStatus,
9583
+ age: fmtAge(s.mtime),
9584
+ mtime: s.mtime,
9585
+ name: s.friendlyName,
9586
+ lastUserPrompt: s.lastUserPrompt,
9587
+ userLines: s.userLines,
9588
+ msgCount: s.msgCount,
9589
+ topLevelSize: s.topLevelSize,
9590
+ decodedPath: s.decodedPath
9591
+ }))
9592
+ };
9593
+ console.log(JSON.stringify(output, null, 2));
9594
+ return;
9595
+ }
9596
+ const hasClaudeLive = liveSessions.some((s) => s.kind === "claude");
9597
+ if (liveSessions.length > 0) renderLiveSessions(liveSessions, allTabs);
9598
+ if (sessions.length === 0) {
9599
+ if (!hasClaudeLive) if (includeAll) console.log(err("No sessions found in ~/.claude/projects/."));
9600
+ 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."));
9601
+ return;
9602
+ }
9603
+ const title = includeAll ? "Paused / All Sessions (named + orphans)" : "Paused / Resumable Sessions";
9604
+ console.log("\n" + header(title) + "\n");
9605
+ const headers = [
9606
+ "#",
9607
+ "id",
9608
+ "age",
9609
+ "name / project",
9610
+ "last prompt",
9611
+ "msgs",
9612
+ "size",
9613
+ "status"
9614
+ ];
9615
+ const rows = sessions.map((s, idx) => {
9616
+ const name = s.friendlyName ?? s.decodedPath;
9617
+ const prompt = s.lastUserPrompt ? chalk.dim(s.lastUserPrompt.length > 40 ? s.lastUserPrompt.slice(0, 39) + "…" : s.lastUserPrompt) : dim$1("—");
9618
+ return [
9619
+ chalk.dim(String(idx + 1)),
9620
+ chalk.cyan(s.shortId),
9621
+ dim$1(fmtAge(s.mtime)),
9622
+ name.length > 26 ? name.slice(0, 25) + "…" : name,
9623
+ prompt,
9624
+ s.userLines > 0 ? String(s.userLines) : dim$1("—"),
9625
+ dim$1(fmtSize(s.topLevelSize)),
9626
+ fmtStatus(s.sessionStatus)
9627
+ ];
9628
+ });
9629
+ console.log(renderTable(headers, rows));
9630
+ console.log("\n" + dim$1("Go to a session: ") + chalk.white("pai resume <name>") + "\n" + (hasClaudeLive ? dim$1("Pause all live: ") + chalk.white("pai pause all") + "\n" : "") + (includeAll ? "" : dim$1("Show all sessions: ") + chalk.white("pai sessions --all") + "\n"));
9631
+ }
9632
+
9633
+ //#endregion
9634
+ //#region src/cli/commands/session/goto.ts
9635
+ /**
9636
+ * Run claude --resume <uuid> --print --output-format=json "_" in the given cwd.
9637
+ * Returns ok=true if the session is resumable (exit 0 and no "No conversation found"
9638
+ * in stderr). Timeout: 5 seconds.
9639
+ */
9640
+ function probeResume$1(uuid, cwd) {
9641
+ const result = spawnSync("claude", [
9642
+ "--resume",
9643
+ uuid,
9644
+ "--print",
9645
+ "--output-format=json",
9646
+ "_"
9647
+ ], {
9648
+ cwd,
9649
+ timeout: 5e3,
9650
+ env: process.env,
9651
+ stdio: [
9652
+ "ignore",
9653
+ "ignore",
9654
+ "pipe"
9655
+ ]
9656
+ });
9657
+ if (result.error) return {
9658
+ ok: false,
9659
+ reason: `spawn error: ${result.error.message}`
9660
+ };
9661
+ const stderr = result.stderr?.toString("utf8") ?? "";
9662
+ if (stderr.toLowerCase().includes("no conversation found") || stderr.toLowerCase().includes("session not found")) return {
9663
+ ok: false,
9664
+ reason: "No conversation found for this UUID"
9665
+ };
9666
+ if (result.status !== 0) return {
9667
+ ok: false,
9668
+ reason: `claude exited ${result.status ?? "signal"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : ""}`
9669
+ };
9670
+ return { ok: true };
9671
+ }
9672
+ function cmdGoto(db, query, opts) {
9673
+ const allSessions = scanSessions(db, {
9674
+ limit: 500,
9675
+ filter: "named"
9676
+ });
9677
+ let resolved;
9678
+ try {
9679
+ resolved = resolveSessionByNameOrId(allSessions, query);
9680
+ } catch (resolveErr) {
9681
+ let msg = String(resolveErr).replace(/^Error: /, "");
9682
+ if (msg.includes("No session found matching")) msg += `\n\nTip: pai find "${query}" — search prompt history by keywords`;
9683
+ console.error(err(msg));
9684
+ process.exit(1);
9685
+ }
9686
+ const { session: matchedSession, friendlyName } = resolved;
9687
+ let resumableUuid;
9688
+ let resumableSession;
9689
+ if (matchedSession.resumable) {
9690
+ resumableUuid = matchedSession.uuid;
9691
+ resumableSession = matchedSession;
9692
+ } else if (matchedSession.encodedDir) {
9693
+ const sameProject = allSessions.filter((s) => s.encodedDir === matchedSession.encodedDir && s.resumable);
9694
+ sameProject.sort((a, b) => b.mtime - a.mtime);
9695
+ if (sameProject.length > 0) {
9696
+ resumableSession = sameProject[0];
9697
+ resumableUuid = resumableSession.uuid;
9698
+ }
9699
+ }
9700
+ const rawDir = matchedSession.clcDirectory ?? matchedSession.registryRootPath ?? matchedSession.decodedPath;
9701
+ let projectDir;
9702
+ try {
9703
+ projectDir = realpathSync(rawDir);
9704
+ } catch {
9705
+ 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.`));
9706
+ process.exit(1);
9707
+ }
9708
+ const name = friendlyName ?? query;
9709
+ const promptArg = `/Name ${name}\ngo`;
9710
+ if (opts.dryRun) {
9711
+ if (resumableUuid) {
9712
+ const argvResume = `claude --resume ${resumableUuid} --name "${name}" "/Name ${name}\\ngo"`;
9713
+ const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
9714
+ console.log("\n" + chalk.bold("Dry run — would probe then exec (RESUME path):") + "\n");
9715
+ console.log(` cwd: ${chalk.cyan(projectDir)}`);
9716
+ console.log(` probe: claude --resume ${resumableUuid} --print --output-format=json "_"`);
9717
+ console.log(` argv: ${chalk.white(argvResume)}`);
9718
+ console.log(` fallback: ${chalk.yellow(argvFresh)}`);
9719
+ if (resumableSession) {
9720
+ console.log(`\n uuid: ${resumableSession.uuid}`);
9721
+ console.log(` age: ${fmtAge(resumableSession.mtime)}`);
9722
+ console.log(` status: ${resumableSession.sessionStatus}`);
9723
+ console.log(` sys: ${resumableSession.topLevelSystemLines} system lines`);
9724
+ }
9725
+ } else {
9726
+ const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
9727
+ console.log("\n" + chalk.bold("Dry run — would exec (FRESH path, no resumable UUID):") + "\n");
9728
+ console.log(` cwd: ${chalk.cyan(projectDir)}`);
9729
+ console.log(` argv: ${chalk.white(argvFresh)}`);
9730
+ }
9731
+ console.log();
9732
+ return;
9733
+ }
9734
+ if (resumableUuid) {
9735
+ const probe = probeResume$1(resumableUuid, projectDir);
9736
+ if (probe.ok) {
9737
+ const result = spawnSync("claude", [
9738
+ "--resume",
9739
+ resumableUuid,
9740
+ "--name",
9741
+ name,
9742
+ promptArg
9743
+ ], {
9744
+ cwd: projectDir,
9745
+ stdio: "inherit",
9746
+ env: process.env
9747
+ });
9748
+ if (result.error) {
9749
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
9750
+ process.exit(1);
9751
+ }
9752
+ printExitDir(projectDir);
9753
+ process.exit(result.status ?? 0);
9754
+ } else {
9755
+ 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`));
9756
+ const result = spawnSync("claude", [
9757
+ "--name",
9758
+ name,
9759
+ promptArg
9760
+ ], {
9761
+ cwd: projectDir,
9762
+ stdio: "inherit",
9763
+ env: process.env
9764
+ });
9765
+ if (result.error) {
9766
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
9767
+ process.exit(1);
9768
+ }
9769
+ printExitDir(projectDir);
9770
+ process.exit(result.status ?? 0);
9771
+ }
9772
+ } else {
9773
+ const result = spawnSync("claude", [
9774
+ "--name",
9775
+ name,
9776
+ promptArg
9777
+ ], {
9778
+ cwd: projectDir,
9779
+ stdio: "inherit",
9780
+ env: process.env
9781
+ });
9782
+ if (result.error) {
9783
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
9784
+ process.exit(1);
9785
+ }
9786
+ printExitDir(projectDir);
9787
+ process.exit(result.status ?? 0);
9788
+ }
9789
+ }
9790
+
9791
+ //#endregion
9792
+ //#region src/cli/commands/session/index.ts
9793
+ function registerSessionCommands(sessionCmd, getDb) {
9794
+ sessionCmd.command("list").description("Resumable sessions catalog — named sessions with resume status.\nShort form: pai sessions\nDefault shows all sessions you've named or that are resumable.\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) => {
9795
+ await cmdRecent(getDb(), opts);
9796
+ });
9797
+ sessionCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
9798
+ cmdInfo(getDb(), projectSlug, number);
9799
+ });
9800
+ 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) => {
9801
+ cmdRename(getDb(), projectSlug, number, newSlug);
9802
+ });
9803
+ 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) => {
9804
+ cmdSlug(getDb(), projectSlug, number, opts);
9805
+ });
9806
+ 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) => {
9807
+ cmdTag(getDb(), projectSlug, number, tags);
9808
+ });
9809
+ 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) => {
9810
+ cmdRoute(getDb(), projectSlug, number, targetProject, opts);
9811
+ });
9812
+ 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) => {
9813
+ cmdHandover(getDb(), projectSlug, sessionId);
9814
+ });
9815
+ 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) => {
9816
+ cmdCheckpoint(message, opts);
9817
+ });
9818
+ 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) => {
9819
+ cmdActive(getDb(), opts);
9820
+ });
9821
+ sessionCmd.command("recent").description("[Deprecated] Use `pai session list` or `pai sessions` instead.").option("-n <count>", "Maximum sessions to show (default: 20)", "20").option("--all", "Include unnamed orphan sessions (not in clc registry)").option("--json", "Output raw JSON instead of formatted table").action(async (opts) => {
9822
+ process.stderr.write("Deprecated: `pai session recent` — use `pai session list` or `pai sessions` instead.\n");
9823
+ await cmdRecent(getDb(), opts);
9824
+ });
9825
+ sessionCmd.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.\nSends '/Name <name>\\ngo' as the initial prompt so the PAI ## Continue hook fires.").option("--skip-name", "Do not prepend /Name to set the session name in chrome").option("--skip-go", "Do not append \\ngo to trigger PAI auto-resume (## Continue)").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((nameOrId, opts) => {
9826
+ process.stderr.write("Note: `pai session goto` works but `pai resume` is the new shorter form.\n");
9827
+ cmdGoto(getDb(), nameOrId, {
9828
+ noName: opts.skipName,
9829
+ noGo: opts.skipGo,
9830
+ dryRun: opts.dryRun
9831
+ });
9832
+ });
9833
+ sessionCmd.command("pause").description("Write a ## Continue checkpoint to the project's TODO.md.\nRecommended short form: pai pause\nCtrl+C bypasses the stop-hook, orphaning the session (cannot --resume).\nUse /exit inside Claude Code to preserve full session resumability.").option("--dry-run", "Preview the ## Continue block without writing it").option("--body-file <path>", "File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.").option("--session-id <uuid>", "Claude Code session UUID — recorded as the `claude --resume` handle").option("--no-body", "Deliberately write a metadata-only checkpoint (no content)").action((opts) => {
9834
+ process.stderr.write("Note: `pai session pause` works but `pai pause` is the new shorter form.\n");
9835
+ cmdPause(getDb(), {
9836
+ dryRun: opts.dryRun,
9837
+ bodyFile: opts.bodyFile,
9838
+ sessionId: opts.sessionId,
9839
+ noBody: opts.body === false
9840
+ });
9841
+ });
9842
+ 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) => {
9843
+ await cmdAutoRoute(opts);
9844
+ });
9845
+ }
9846
+
9847
+ //#endregion
9848
+ //#region src/cli/commands/session-cleanup/types.ts
9849
+ const TEMPLATE_INDICATORS = [
9850
+ "<!-- PAI will add completed work here during session -->",
9851
+ "<!-- PAI will add completed work here -->",
9852
+ "Session completed.",
9853
+ "Session started and ready for your instructions"
9854
+ ];
9855
+ const MODERN_PATTERN = /^(\d{4}) - (\d{4}-\d{2}-\d{2}) - (.+)\.md$/;
9856
+ const LEGACY_PATTERN = /^(\d{4})_(\d{4}-\d{2}-\d{2})_(.+)\.md$/;
9857
+
9858
+ //#endregion
9859
+ //#region src/cli/commands/session-cleanup/rename.ts
9860
+ /**
9861
+ * Auto-name extraction and string helpers for session-cleanup.
9862
+ * Derives a meaningful session title from Markdown content.
9863
+ */
9864
+ const META_PHRASE_PATTERNS = [
9865
+ /session initialized and ready for your instructions/i,
9866
+ /fresh session with no pending tasks/i,
9867
+ /starting new session.*checking for pending work/i,
9868
+ /fresh session with empty todo/i,
9869
+ /session started and ready/i,
9870
+ /^session\b.*\bready\b/i,
9871
+ /^session\b.*\binitialized\b/i,
9872
+ /^session\b.*\bno pending\b/i,
9873
+ /^session\b.*\bno prior work\b/i,
9874
+ /^no pending tasks/i,
9875
+ /^no prior work/i
9876
+ ];
9877
+ const TITLE_CASE_MINOR_WORDS = new Set([
9878
+ "a",
9879
+ "an",
9880
+ "the",
9881
+ "in",
9882
+ "on",
9883
+ "at",
9884
+ "for",
9885
+ "to",
9886
+ "of",
9887
+ "and",
9888
+ "or",
9889
+ "but",
9890
+ "via",
9891
+ "with",
9892
+ "from",
9893
+ "by",
9894
+ "as",
9895
+ "nor"
9896
+ ]);
9897
+ function cleanMarkdownLine(raw) {
9898
+ let s = raw.trim();
9899
+ s = s.replace(/^[-*+]\s+\[[ xX]\]\s*/, "");
9900
+ s = s.replace(/^[-*+]\s+/, "");
9901
+ s = s.replace(/^\[[ xX]\]\s*/, "");
9902
+ s = s.replace(/\*\*\s*([^*]+?)\s*\*\*/g, "$1");
9903
+ s = s.replace(/\*\s*([^*]+?)\s*\*/g, "$1");
9904
+ s = s.replace(/`([^`]+)`/g, "$1");
9905
+ s = s.replace(/^\*+\s*/, "");
9906
+ s = s.replace(/^[.,;:]+/, "").replace(/[.,;:]+$/, "");
9907
+ s = s.replace(/\s+/g, " ").trim();
9908
+ return s.length >= 4 ? s : null;
9909
+ }
9910
+ function isMetaPhrase(text) {
9911
+ return META_PHRASE_PATTERNS.some((re) => re.test(text));
9912
+ }
9913
+ function toTitleCase(text) {
9914
+ return text.split(" ").map((word, i) => {
9915
+ const lower = word.toLowerCase();
9916
+ if (i !== 0 && TITLE_CASE_MINOR_WORDS.has(lower)) return lower;
9917
+ return word.charAt(0).toUpperCase() + word.slice(1);
9918
+ }).join(" ");
9919
+ }
9920
+ function sanitizeName(raw) {
9921
+ let s = raw.replace(/[\/\\:*?"<>|#`]/g, "");
9922
+ s = s.replace(/\s+/g, " ").trim();
9923
+ if (s.length > 60) {
9924
+ const truncated = s.slice(0, 60);
9925
+ const lastSpace = truncated.lastIndexOf(" ");
9926
+ s = lastSpace > 20 ? truncated.slice(0, lastSpace) : truncated;
9927
+ }
9928
+ s = s.trim();
9929
+ return toTitleCase(s);
9930
+ }
9931
+ function extractAutoName(content) {
9932
+ const lines = content.split("\n");
9933
+ const CONTENT_SECTION_HEADINGS = new Set([
9934
+ "Work Done",
9935
+ "Summary",
9936
+ "Completed",
9937
+ "What Was Done",
9938
+ "Results",
9939
+ "Outcomes",
9940
+ "Changes",
9941
+ "Progress"
9942
+ ]);
9943
+ const SKIP_SECTION_HEADINGS = new Set([
9944
+ "Next Steps",
9945
+ "Tags",
9946
+ "TODO",
9947
+ "Blockers",
9948
+ "Notes",
9949
+ "Metadata",
9950
+ "Context",
9951
+ "Background"
9952
+ ]);
9953
+ let pastH1 = false;
9954
+ let pastFirstHr = false;
9955
+ let currentSection = null;
9956
+ const contentSectionLines = [];
9957
+ const otherH2Headings = [];
9958
+ for (const line of lines) {
9959
+ const trimmed = line.trim();
9960
+ if (trimmed.startsWith("# ")) {
9961
+ pastH1 = true;
9962
+ continue;
9963
+ }
9964
+ if (!pastH1) continue;
9965
+ if (!pastFirstHr) {
9966
+ if (trimmed === "---") pastFirstHr = true;
9967
+ continue;
9968
+ }
9969
+ if (trimmed.startsWith("## ")) {
9970
+ const headingText = trimmed.slice(3).trim();
9971
+ if (CONTENT_SECTION_HEADINGS.has(headingText)) currentSection = "content";
9972
+ else if (SKIP_SECTION_HEADINGS.has(headingText)) currentSection = "skip";
9973
+ else {
9974
+ currentSection = null;
9975
+ otherH2Headings.push(headingText);
9976
+ }
9977
+ continue;
9978
+ }
9979
+ if (trimmed.startsWith("#")) continue;
9980
+ if (trimmed === "-->") continue;
9981
+ if (trimmed.startsWith("<!--") && /^<!--.*-->$/.test(trimmed)) continue;
9982
+ if (currentSection === "content" && trimmed.length > 0) {
9983
+ const withoutComment = trimmed.replace(/^<!--.*?-->\s*/, "");
9984
+ const effective = withoutComment.length > 0 ? withoutComment : trimmed;
9985
+ if (effective.length === 0) continue;
9986
+ contentSectionLines.push(effective);
9987
+ }
9988
+ }
9989
+ for (const raw of contentSectionLines) {
9990
+ const cleaned = cleanMarkdownLine(raw);
9991
+ if (!cleaned) continue;
9992
+ if (isMetaPhrase(cleaned)) continue;
9993
+ if (cleaned.startsWith("<!--") || cleaned.includes("PAI will add")) continue;
9994
+ if (cleaned.length < 5) continue;
9995
+ return sanitizeName(cleaned);
9996
+ }
9997
+ for (const heading of otherH2Headings) {
9998
+ const cleaned = cleanMarkdownLine(heading);
9999
+ if (!cleaned) continue;
10000
+ if (isMetaPhrase(cleaned)) continue;
10001
+ if (cleaned.length > 3 && cleaned.length < 80) return sanitizeName(cleaned);
10002
+ }
10003
+ for (const line of lines) {
10004
+ const trimmed = line.trim();
10005
+ if (trimmed.startsWith("# ")) {
10006
+ const title = trimmed.slice(2).trim();
10007
+ if (/^Session \d{4}/i.test(title)) continue;
10008
+ const cleaned = cleanMarkdownLine(title);
10009
+ if (cleaned && !isMetaPhrase(cleaned) && cleaned.length > 3) return sanitizeName(cleaned);
10010
+ }
10011
+ }
10012
+ return "Unnamed Session";
10013
+ }
10014
+ /** Format a 4-digit padded session number. */
10015
+ function padNum(n) {
10016
+ return String(n).padStart(4, "0");
10017
+ }
10018
+
10019
+ //#endregion
10020
+ //#region src/cli/commands/session-cleanup/scanner.ts
10021
+ function getAllProjects(db) {
10022
+ return db.prepare("SELECT id, slug, display_name, root_path, encoded_dir, claude_notes_dir FROM projects WHERE status = 'active' ORDER BY slug").all();
10023
+ }
10024
+ function getProject(db, slug) {
10025
+ return db.prepare("SELECT id, slug, display_name, root_path, encoded_dir, claude_notes_dir FROM projects WHERE slug = ?").get(slug);
10026
+ }
10027
+ function getProjectSessions(db, projectId) {
10028
+ return db.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY number ASC").all(projectId);
10029
+ }
10030
+ function findRootNotesDir(rootPath) {
10031
+ const canonical = join(rootPath, "Notes");
10032
+ if (existsSync(canonical)) return canonical;
10033
+ const alt = join(rootPath, ".claude", "Notes");
10034
+ if (existsSync(alt)) return alt;
10035
+ return null;
10036
+ }
10037
+ function findClaudeNotesDir(project, rootNotesDir) {
10038
+ const candidate = project.claude_notes_dir ?? join(homedir(), ".claude", "projects", project.encoded_dir, "Notes");
10039
+ if (!existsSync(candidate)) return null;
10040
+ if (rootNotesDir && candidate === rootNotesDir) return null;
10041
+ return candidate;
10042
+ }
10043
+ function findAllNotesDirs(project) {
10044
+ const rootDir = findRootNotesDir(project.root_path);
10045
+ const claudeDir = findClaudeNotesDir(project, rootDir);
10046
+ const dirs = [];
10047
+ if (rootDir) dirs.push(rootDir);
10048
+ if (claudeDir) dirs.push(claudeDir);
10049
+ return dirs;
10050
+ }
10051
+ function isTemplateOnly(content) {
10052
+ if (!TEMPLATE_INDICATORS.some((ind) => content.includes(ind))) return false;
10053
+ const lines = content.split("\n");
10054
+ let inWorkDone = false;
10055
+ for (const line of lines) {
10056
+ const trimmed = line.trim();
10057
+ if (trimmed === "## Work Done") {
10058
+ inWorkDone = true;
10059
+ continue;
10060
+ }
10061
+ if (trimmed.startsWith("## ") && inWorkDone) break;
10062
+ if (!inWorkDone) continue;
10063
+ if (!trimmed) continue;
10064
+ if (trimmed.startsWith("<!--") && trimmed.endsWith("-->")) continue;
10065
+ if (trimmed.startsWith("<!--")) continue;
10066
+ if (trimmed === "-->") continue;
10067
+ if (trimmed === "Session completed.") continue;
10068
+ if (trimmed === "#Session" || trimmed === "**Tags:** #Session") continue;
10069
+ return false;
10070
+ }
10071
+ return true;
10072
+ }
10073
+ function scanNotesDir(notesDir, dbByFilename) {
10074
+ const candidates = [];
10075
+ let flatFiles = [];
10076
+ try {
10077
+ flatFiles = readdirSync(notesDir, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md")).map((d) => d.name);
10078
+ } catch {}
10079
+ const subDirFiles = [];
10080
+ try {
10081
+ const topEntries = readdirSync(notesDir, { withFileTypes: true });
10082
+ for (const entry of topEntries) {
10083
+ if (!entry.isDirectory()) continue;
10084
+ if (!/^\d{4}$/.test(entry.name)) continue;
10085
+ const yearDir = join(notesDir, entry.name);
10086
+ const monthDirs = readdirSync(yearDir, { withFileTypes: true });
10087
+ for (const mEntry of monthDirs) {
10088
+ if (!mEntry.isDirectory()) continue;
10089
+ const monthDir = join(yearDir, mEntry.name);
10090
+ const files = readdirSync(monthDir).filter((f) => f.endsWith(".md"));
10091
+ for (const f of files) subDirFiles.push({
10092
+ filename: f,
10093
+ filepath: join(monthDir, f)
10094
+ });
10095
+ }
10096
+ }
10097
+ } catch {}
10098
+ const allFiles = [...flatFiles.map((f) => ({
10099
+ filename: f,
10100
+ filepath: join(notesDir, f)
10101
+ })), ...subDirFiles];
10102
+ for (const { filename, filepath } of allFiles) {
10103
+ let num;
10104
+ let date;
10105
+ let namepart;
10106
+ let classification;
10107
+ const modernMatch = MODERN_PATTERN.exec(filename);
10108
+ const legacyMatch = LEGACY_PATTERN.exec(filename);
10109
+ if (modernMatch) {
10110
+ num = parseInt(modernMatch[1], 10);
10111
+ date = modernMatch[2];
10112
+ namepart = modernMatch[3];
10113
+ classification = "NAMED";
10114
+ } else if (legacyMatch) {
10115
+ num = parseInt(legacyMatch[1], 10);
10116
+ date = legacyMatch[2];
10117
+ namepart = legacyMatch[3];
10118
+ classification = "LEGACY_FORMAT";
10119
+ } else continue;
10120
+ let sizeBytes = 0;
10121
+ let content = "";
10122
+ try {
10123
+ sizeBytes = statSync(filepath).size;
10124
+ content = readFileSync(filepath, "utf8");
10125
+ } catch {
10126
+ continue;
10127
+ }
10128
+ const dbSession = dbByFilename.get(filename) ?? dbByFilename.get(filepath.split(`${notesDir}/`)[1] ?? "") ?? null;
10129
+ if (classification !== "LEGACY_FORMAT") {
10130
+ if (sizeBytes < 400 || isTemplateOnly(content)) classification = "EMPTY";
10131
+ else if (namepart === "New Session" || namepart === (process.env.USER ?? "") || namepart === "session-started-and-ready-for-your-instructions") classification = "UNNAMED";
10132
+ }
10133
+ const candidate = {
10134
+ session: dbSession,
10135
+ filename,
10136
+ filepath,
10137
+ sizeBytes,
10138
+ classification,
10139
+ date,
10140
+ number: num
10141
+ };
10142
+ if (classification === "UNNAMED" || classification === "LEGACY_FORMAT") candidate.autoName = extractAutoName(content);
10143
+ candidates.push(candidate);
10144
+ }
10145
+ return candidates;
10146
+ }
10147
+ function buildRenumberMap(survivors) {
10148
+ const map = /* @__PURE__ */ new Map();
10149
+ [...survivors].sort((a, b) => a.number - b.number).forEach((s, idx) => {
10150
+ const newNum = idx + 1;
10151
+ if (s.number !== newNum) map.set(s.number, newNum);
10152
+ });
10153
+ return map;
10154
+ }
10155
+ function analyzeProject(db, project) {
10156
+ const notesDirPaths = findAllNotesDirs(project);
10157
+ if (notesDirPaths.length === 0) return null;
10158
+ const dbSessions = getProjectSessions(db, project.id);
10159
+ const dbByFilename = /* @__PURE__ */ new Map();
10160
+ for (const s of dbSessions) dbByFilename.set(s.filename, s);
10161
+ const notesDirPlans = [];
10162
+ const allSurvivors = [];
10163
+ for (const notesDir of notesDirPaths) {
10164
+ const candidates = scanNotesDir(notesDir, dbByFilename);
10165
+ if (candidates.length === 0) continue;
10166
+ const toDelete = candidates.filter((c) => c.classification === "EMPTY");
10167
+ const toRename = candidates.filter((c) => c.classification === "UNNAMED" || c.classification === "LEGACY_FORMAT");
10168
+ const survivors = candidates.filter((c) => c.classification !== "EMPTY");
10169
+ notesDirPlans.push({
10170
+ notesDir,
10171
+ toDelete,
10172
+ toRename,
10173
+ toMove: survivors
10174
+ });
10175
+ allSurvivors.push(...survivors);
10176
+ }
10177
+ if (notesDirPlans.length === 0) return null;
10178
+ return {
10179
+ project,
10180
+ notesDirs: notesDirPlans,
10181
+ renumberMap: buildRenumberMap(allSurvivors)
10182
+ };
10183
+ }
10184
+
10185
+ //#endregion
10186
+ //#region src/cli/commands/session-cleanup/executor.ts
10187
+ async function countVectorDbPaths(oldPaths) {
10188
+ if (oldPaths.length === 0) return 0;
10189
+ try {
10190
+ const { loadConfig } = await import("./config-DGOfoGfm.mjs").then((n) => n.r);
10191
+ const { PostgresBackend } = await import("./postgres-zjKqPl4X.mjs");
10192
+ const config = loadConfig();
10193
+ if (config.storageBackend !== "postgres") return 0;
10194
+ const pgBackend = new PostgresBackend(config.postgres ?? {});
10195
+ if (await pgBackend.testConnection()) {
10196
+ await pgBackend.close();
10197
+ return 0;
10198
+ }
10199
+ const pool = pgBackend.pool;
10200
+ const placeholders = oldPaths.map((_, i) => `$${i + 1}`).join(", ");
10201
+ const result = await pool.query(`SELECT COUNT(*)::text AS n FROM pai_files WHERE path IN (${placeholders})`, oldPaths);
10202
+ await pgBackend.close();
10203
+ return parseInt(result.rows[0]?.n ?? "0", 10);
10204
+ } catch {
10205
+ return 0;
10206
+ }
10207
+ }
10208
+ async function updateVectorDbPaths(moves) {
10209
+ if (moves.length === 0) return 0;
10210
+ try {
10211
+ const { loadConfig } = await import("./config-DGOfoGfm.mjs").then((n) => n.r);
10212
+ const { PostgresBackend } = await import("./postgres-zjKqPl4X.mjs");
10213
+ const config = loadConfig();
10214
+ if (config.storageBackend !== "postgres") return 0;
10215
+ const pgBackend = new PostgresBackend(config.postgres ?? {});
10216
+ const connErr = await pgBackend.testConnection();
10217
+ if (connErr) {
10218
+ process.stderr.write(`[session-cleanup] Postgres unavailable (${connErr}). Skipping vector DB path update.\n`);
10219
+ await pgBackend.close();
10220
+ return 0;
10221
+ }
10222
+ const client = await pgBackend.pool.connect();
10223
+ let filesUpdated = 0;
10224
+ try {
10225
+ await client.query("BEGIN", []);
10226
+ for (const { oldPath, newPath } of moves) {
10227
+ const r = await client.query("UPDATE pai_files SET path = $1 WHERE path = $2", [newPath, oldPath]);
10228
+ filesUpdated += r.rowCount ?? 0;
10229
+ await client.query("UPDATE pai_chunks SET path = $1 WHERE path = $2", [newPath, oldPath]);
10230
+ }
10231
+ await client.query("COMMIT", []);
10232
+ } catch (e) {
10233
+ await client.query("ROLLBACK", []);
10234
+ throw e;
10235
+ } finally {
10236
+ client.release();
10237
+ }
10238
+ await pgBackend.close();
10239
+ return filesUpdated;
10240
+ } catch (e) {
10241
+ process.stderr.write(`[session-cleanup] Failed to update vector DB paths: ${e}\n`);
10242
+ return -1;
10243
+ }
10244
+ }
10245
+ async function displayDryRun(plans) {
10246
+ let totalDelete = 0;
10247
+ let totalRename = 0;
10248
+ let totalMove = 0;
10249
+ let totalRenumber = 0;
10250
+ for (const plan of plans) {
10251
+ if (!(plan.notesDirs.some((d) => d.toDelete.length > 0 || d.toRename.length > 0 || d.toMove.length > 0) || plan.renumberMap.size > 0)) continue;
10252
+ console.log();
10253
+ console.log(header(` Project: ${plan.project.display_name} (${plan.project.slug})`));
10254
+ for (const dirPlan of plan.notesDirs) {
10255
+ console.log(dim$1(` Notes: ${dirPlan.notesDir}`));
10256
+ if (dirPlan.toDelete.length > 0) {
10257
+ console.log(bold$1(" DELETE (empty/template-only sessions):"));
10258
+ for (const c of dirPlan.toDelete) {
10259
+ console.log(` ${chalk.red("DEL")} ${dim$1(padNum(c.number))} - ${c.date} - ${c.filename.split(" - ").slice(2).join(" - ")} ${dim$1(`(${c.sizeBytes}b)`)}`);
10260
+ totalDelete++;
10261
+ }
10262
+ console.log();
10263
+ }
10264
+ if (dirPlan.toRename.length > 0) {
10265
+ console.log(bold$1(" RENAME (unnamed or legacy-format sessions):"));
10266
+ for (const c of dirPlan.toRename) {
10267
+ const autoName = c.autoName ?? "Unnamed Session";
10268
+ console.log(` ${chalk.yellow("REN")} ${c.filename}`);
10269
+ console.log(` → ${padNum(c.number)} - ${c.date} - ${autoName}.md`);
10270
+ totalRename++;
10271
+ }
10272
+ console.log();
10273
+ }
10274
+ if (dirPlan.toMove.length > 0) {
10275
+ console.log(bold$1(" MOVE TO YYYY/MM/ hierarchy:"));
10276
+ for (const c of dirPlan.toMove) {
10277
+ const [year, month] = c.date.split("-");
10278
+ console.log(` ${chalk.cyan("MOV")} ${c.filename}`);
10279
+ console.log(` → ${year}/${month}/${c.filename}`);
10280
+ totalMove++;
10281
+ }
10282
+ console.log();
10283
+ }
10284
+ }
10285
+ if (plan.renumberMap.size > 0) {
10286
+ console.log(bold$1(" RENUMBER (after deletions, global across all Notes/ dirs):"));
10287
+ for (const [oldN, newN] of plan.renumberMap) {
10288
+ console.log(` ${chalk.blue("NUM")} #${padNum(oldN)} → #${padNum(newN)}`);
10289
+ totalRenumber++;
10290
+ }
10291
+ console.log();
10292
+ }
10293
+ }
10294
+ const wouldMovePaths = [];
10295
+ for (const plan of plans) for (const dirPlan of plan.notesDirs) for (const c of dirPlan.toMove) {
10296
+ const [year, month] = c.date.split("-");
10297
+ const targetPath = join(dirPlan.notesDir, year, month, c.filename);
10298
+ if (c.filepath !== targetPath) wouldMovePaths.push(c.filepath);
10299
+ }
10300
+ const vectorDbCount = await countVectorDbPaths(wouldMovePaths);
10301
+ console.log();
10302
+ console.log(bold$1(" Summary (dry-run):"));
10303
+ console.log(` ${chalk.red("DEL")} ${totalDelete} empty sessions to delete`);
10304
+ console.log(` ${chalk.yellow("REN")} ${totalRename} unnamed sessions to rename`);
10305
+ console.log(` ${chalk.blue("NUM")} ${totalRenumber} sessions to renumber`);
10306
+ console.log(` ${chalk.cyan("MOV")} ${totalMove} sessions to move into YYYY/MM/ dirs`);
10307
+ if (vectorDbCount > 0) console.log(` ${chalk.magenta("VEC")} ${vectorDbCount} file path(s) will be updated in the vector DB (embeddings preserved)`);
10308
+ else if (wouldMovePaths.length > 0) console.log(` ${chalk.magenta("VEC")} 0 file path(s) found in vector DB for moved files (no embeddings to preserve)`);
10309
+ console.log();
10310
+ console.log(warn(" This is a dry-run. Add --execute to apply changes."));
10311
+ console.log();
10312
+ }
10313
+ async function executeCleanup(db, plans, skipReindex) {
10314
+ let deleted = 0;
10315
+ let renamed = 0;
10316
+ let moved = 0;
10317
+ let renumbered = 0;
10318
+ let dbUpdated = 0;
10319
+ const vectorDbMoves = [];
10320
+ for (const plan of plans) {
10321
+ console.log();
10322
+ console.log(header(` Project: ${plan.project.display_name} (${plan.project.slug})`));
10323
+ for (const dirPlan of plan.notesDirs) {
10324
+ const { notesDir } = dirPlan;
10325
+ if (plan.notesDirs.length > 1) console.log(dim$1(` Directory: ${notesDir}`));
10326
+ for (const c of dirPlan.toDelete) {
10327
+ try {
10328
+ unlinkSync(c.filepath);
10329
+ console.log(ok(` DEL ${c.filename}`));
10330
+ deleted++;
10331
+ } catch (e) {
10332
+ console.log(err(` FAIL to delete ${c.filename}: ${e}`));
10333
+ }
10334
+ if (c.session) try {
10335
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(c.session.id);
10336
+ dbUpdated++;
10337
+ } catch (e) {
10338
+ console.log(err(` FAIL to remove session #${c.number} from DB: ${e}`));
10339
+ }
10340
+ }
10341
+ for (const c of dirPlan.toRename) {
10342
+ const autoName = c.autoName ?? "Unnamed Session";
10343
+ const newFilename = `${padNum(c.number)} - ${c.date} - ${autoName}.md`;
10344
+ const newPath = join(notesDir, newFilename);
10345
+ if (c.filepath !== newPath) try {
10346
+ renameSync(c.filepath, newPath);
10347
+ console.log(ok(` REN ${c.filename}`));
10348
+ console.log(dim$1(` → ${newFilename}`));
10349
+ renamed++;
10350
+ c.filename = newFilename;
10351
+ c.filepath = newPath;
10352
+ } catch (e) {
10353
+ console.log(err(` FAIL rename ${c.filename}: ${e}`));
10354
+ continue;
10355
+ }
10356
+ try {
10357
+ const lines = readFileSync(newPath, "utf8").split("\n");
10358
+ let h1Updated = false;
10359
+ const updated = lines.map((line) => {
10360
+ if (!h1Updated && line.startsWith("# ")) {
10361
+ h1Updated = true;
10362
+ return `# ${autoName}`;
10363
+ }
10364
+ return line;
10365
+ });
10366
+ if (!h1Updated) updated.unshift(`# ${autoName}`, "");
10367
+ writeFileSync(newPath, updated.join("\n"), "utf8");
10368
+ } catch {}
10369
+ if (c.session) {
10370
+ const normalizedSlug = autoName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10371
+ try {
10372
+ db.prepare("UPDATE sessions SET slug = ?, title = ?, filename = ? WHERE id = ?").run(normalizedSlug, autoName, newFilename, c.session.id);
10373
+ dbUpdated++;
10374
+ } catch (e) {
10375
+ console.log(err(` FAIL DB update for session #${c.number}: ${e}`));
10376
+ }
10377
+ }
10378
+ }
10379
+ if (plan.renumberMap.size > 0) {
10380
+ const toRenumber = dirPlan.toMove.filter((c) => plan.renumberMap.has(c.number));
10381
+ const tempFiles = [];
10382
+ for (const c of toRenumber) {
10383
+ const newNum = plan.renumberMap.get(c.number);
10384
+ const tempPath = join(notesDir, `__tmp_${padNum(c.number)}_${c.filename}`);
10385
+ try {
10386
+ if (existsSync(c.filepath)) {
10387
+ renameSync(c.filepath, tempPath);
10388
+ tempFiles.push({
10389
+ candidate: c,
10390
+ tempPath,
10391
+ newNum
10392
+ });
10393
+ }
10394
+ } catch (e) {
10395
+ console.log(err(` FAIL temp-rename #${c.number}: ${e}`));
10396
+ }
10397
+ }
10398
+ for (const { candidate: c, tempPath, newNum } of tempFiles) {
10399
+ const newFilename = c.filename.replace(/^\d{4}/, padNum(newNum));
10400
+ const newPath = join(notesDir, newFilename);
10401
+ try {
10402
+ renameSync(tempPath, newPath);
10403
+ console.log(ok(` NUM #${padNum(c.number)} → #${padNum(newNum)}: ${newFilename}`));
10404
+ renumbered++;
10405
+ c.filename = newFilename;
10406
+ c.filepath = newPath;
10407
+ c.number = newNum;
10408
+ } catch (e) {
10409
+ console.log(err(` FAIL final-rename #${newNum}: ${e}`));
10410
+ }
10411
+ if (existsSync(newPath)) try {
10412
+ writeFileSync(newPath, readFileSync(newPath, "utf8").split("\n").map((line) => {
10413
+ if (line.match(/^# Session \d{4}:/)) return line.replace(/^# Session \d{4}:/, `# Session ${padNum(newNum)}:`);
10414
+ return line;
10415
+ }).join("\n"), "utf8");
10416
+ } catch {}
10417
+ }
10418
+ const dbRenumbers = tempFiles.filter(({ candidate: c }) => c.session != null).map(({ candidate: c, newNum }) => ({
10419
+ session: c.session,
10420
+ newNum,
10421
+ newFilename: c.filename
10422
+ }));
10423
+ if (dbRenumbers.length > 0) {
10424
+ const renumberDb = db.transaction(() => {
10425
+ for (const { session, newNum } of dbRenumbers) db.prepare("UPDATE sessions SET number = ? WHERE id = ?").run(-newNum, session.id);
10426
+ for (const { session, newNum, newFilename } of dbRenumbers) db.prepare("UPDATE sessions SET number = ?, filename = ? WHERE id = ?").run(newNum, newFilename, session.id);
10427
+ });
10428
+ try {
10429
+ renumberDb();
10430
+ dbUpdated += dbRenumbers.length;
10431
+ } catch (e) {
10432
+ console.log(err(` FAIL DB renumber transaction: ${e}`));
10433
+ }
10434
+ }
10435
+ }
10436
+ for (const c of dirPlan.toMove) {
10437
+ const [year, month] = c.date.split("-");
10438
+ const targetDir = join(notesDir, year, month);
10439
+ const targetPath = join(targetDir, c.filename);
10440
+ if (c.filepath === targetPath) continue;
10441
+ try {
10442
+ mkdirSync(targetDir, { recursive: true });
10443
+ } catch (e) {
10444
+ console.log(err(` FAIL mkdir ${targetDir}: ${e}`));
10445
+ continue;
10446
+ }
10447
+ const oldAbsPath = c.filepath;
10448
+ try {
10449
+ if (existsSync(c.filepath)) {
10450
+ renameSync(c.filepath, targetPath);
10451
+ console.log(ok(` MOV ${c.filename}`));
10452
+ console.log(dim$1(` → ${year}/${month}/${c.filename}`));
10453
+ moved++;
10454
+ vectorDbMoves.push({
10455
+ oldPath: oldAbsPath,
10456
+ newPath: targetPath
10457
+ });
10458
+ }
10459
+ } catch (e) {
10460
+ console.log(err(` FAIL move ${c.filename}: ${e}`));
10461
+ continue;
10462
+ }
10463
+ const newFilenameInDb = `${year}/${month}/${c.filename}`;
10464
+ if (c.session) try {
10465
+ db.prepare("UPDATE sessions SET filename = ? WHERE id = ?").run(newFilenameInDb, c.session.id);
10466
+ dbUpdated++;
10467
+ } catch (e) {
10468
+ console.log(err(` FAIL DB update path for ${c.filename}: ${e}`));
10469
+ }
10470
+ }
10471
+ }
10472
+ }
10473
+ let vectorDbUpdated = 0;
10474
+ if (vectorDbMoves.length > 0) {
10475
+ console.log();
10476
+ console.log(dim$1(` Updating ${vectorDbMoves.length} file path(s) in vector DB to preserve embeddings...`));
10477
+ const result = await updateVectorDbPaths(vectorDbMoves);
10478
+ if (result >= 0) {
10479
+ vectorDbUpdated = result;
10480
+ console.log(ok(` Updated ${vectorDbUpdated} file path(s) in Postgres (embeddings preserved)`));
10481
+ } else console.log(warn(" Vector DB path update failed — embeddings may be orphaned (check logs)"));
10482
+ }
10483
+ console.log();
10484
+ console.log(bold$1(" Cleanup complete:"));
10485
+ console.log(ok(` ${deleted} session(s) deleted`));
10486
+ console.log(ok(` ${renamed} session(s) renamed`));
10487
+ console.log(ok(` ${renumbered} session(s) renumbered`));
10488
+ console.log(ok(` ${moved} session(s) moved to YYYY/MM/ hierarchy`));
10489
+ console.log(ok(` ${dbUpdated} registry DB record(s) updated`));
10490
+ if (vectorDbMoves.length > 0) console.log(ok(` ${vectorDbUpdated} vector DB file path(s) updated (embeddings preserved)`));
10491
+ if (!skipReindex) {
10492
+ console.log();
10493
+ console.log(dim$1(" Memory re-index: the PAI daemon will pick up changes within 5 minutes."));
10494
+ console.log(dim$1(" To force immediate re-index: pai memory index --all"));
10495
+ }
10496
+ console.log();
10497
+ }
10498
+
10499
+ //#endregion
10500
+ //#region src/cli/commands/session-cleanup/index.ts
10501
+ function registerSessionCleanupCommand(sessionCmd, getDb) {
10502
+ sessionCmd.command("cleanup [project-slug]").description("Clean up session notes: delete empties, auto-name unnamed, move into YYYY/MM/ hierarchy, renumber").option("--execute", "Actually perform the cleanup (default is dry-run)").option("--no-renumber", "Skip renumbering sessions after deletions").option("--no-reindex", "Skip triggering memory re-index after moves").action(async (projectSlug, opts) => {
10503
+ const db = getDb();
10504
+ const dryRun = !opts.execute;
10505
+ const skipReindex = opts.reindex === false;
10506
+ let projects;
10507
+ if (projectSlug) {
10508
+ const p = getProject(db, projectSlug);
10509
+ if (!p) {
10510
+ console.error(err(`Project not found: ${projectSlug}`));
10511
+ process.exit(1);
10512
+ }
10513
+ projects = [p];
10514
+ } else projects = getAllProjects(db);
10515
+ console.log();
10516
+ console.log(header(dryRun ? " pai session cleanup — DRY RUN (no changes will be made)" : " pai session cleanup — EXECUTING"));
10517
+ console.log(dim$1(` Analyzing ${projects.length} project(s)...`));
10518
+ const plans = [];
10519
+ for (const project of projects) {
10520
+ const plan = analyzeProject(db, project);
10521
+ if (plan) plans.push(plan);
10522
+ }
10523
+ const activePlans = plans.filter((p) => p.notesDirs.some((d) => d.toDelete.length > 0 || d.toRename.length > 0 || d.toMove.length > 0) || p.renumberMap.size > 0);
10524
+ if (activePlans.length === 0) {
10525
+ console.log();
10526
+ console.log(ok(" Nothing to do — all session notes are clean!"));
10527
+ console.log();
10528
+ return;
10529
+ }
10530
+ if (dryRun) await displayDryRun(activePlans);
10531
+ else await executeCleanup(db, activePlans, skipReindex);
10532
+ });
10533
+ }
10534
+
10535
+ //#endregion
10536
+ //#region src/cli/commands/session/end.ts
10537
+ function finalizeNote(notePath) {
10538
+ const content = readFileSync(notePath, "utf-8");
10539
+ if (content.includes("**Status:** Completed")) return {
10540
+ finalized: false,
10541
+ path: notePath
10542
+ };
10543
+ let updated = content.replace("**Status:** In Progress", "**Status:** Completed");
10544
+ if (!updated.includes("**Completed:**")) {
10545
+ const completionTime = (/* @__PURE__ */ new Date()).toISOString();
10546
+ updated = updated.replace("---\n\n## Work Done", `**Completed:** ${completionTime}\n\n---\n\n## Work Done`);
10547
+ }
10548
+ const tmp = `${notePath}.end.tmp`;
10549
+ writeFileSync(tmp, updated, "utf-8");
10550
+ renameSync(tmp, notePath);
10551
+ return {
10552
+ finalized: true,
10553
+ path: notePath
10554
+ };
10555
+ }
10556
+ function cmdEnd(db, opts) {
10557
+ cmdPause(db, opts);
10558
+ const cwd = process.cwd();
10559
+ const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
10560
+ FROM projects
10561
+ WHERE ? LIKE root_path || '%'
10562
+ ORDER BY length(root_path) DESC
8482
10563
  LIMIT 1`).get(cwd);
8483
10564
  if (!project) return;
8484
- const notesDir = findNotesDir(project.root_path, project.encoded_dir ?? "");
10565
+ const notesDir = findNotesDir$1(project.root_path, project.encoded_dir ?? "");
8485
10566
  if (!notesDir) {
8486
10567
  console.log(chalk.dim(" (No session note found — notes directory does not exist yet.)"));
8487
10568
  printEndBox(opts.dryRun);
@@ -8526,164 +10607,6 @@ function printEndBox(dryRun) {
8526
10607
  console.log(box);
8527
10608
  }
8528
10609
 
8529
- //#endregion
8530
- //#region src/cli/commands/session/goto.ts
8531
- /**
8532
- * Run claude --resume <uuid> --print --output-format=json "_" in the given cwd.
8533
- * Returns ok=true if the session is resumable (exit 0 and no "No conversation found"
8534
- * in stderr). Timeout: 5 seconds.
8535
- */
8536
- function probeResume$1(uuid, cwd) {
8537
- const result = spawnSync("claude", [
8538
- "--resume",
8539
- uuid,
8540
- "--print",
8541
- "--output-format=json",
8542
- "_"
8543
- ], {
8544
- cwd,
8545
- timeout: 5e3,
8546
- env: process.env,
8547
- stdio: [
8548
- "ignore",
8549
- "ignore",
8550
- "pipe"
8551
- ]
8552
- });
8553
- if (result.error) return {
8554
- ok: false,
8555
- reason: `spawn error: ${result.error.message}`
8556
- };
8557
- const stderr = result.stderr?.toString("utf8") ?? "";
8558
- if (stderr.toLowerCase().includes("no conversation found") || stderr.toLowerCase().includes("session not found")) return {
8559
- ok: false,
8560
- reason: "No conversation found for this UUID"
8561
- };
8562
- if (result.status !== 0) return {
8563
- ok: false,
8564
- reason: `claude exited ${result.status ?? "signal"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : ""}`
8565
- };
8566
- return { ok: true };
8567
- }
8568
- function cmdGoto(db, query, opts) {
8569
- const allSessions = scanSessions(db, {
8570
- limit: 500,
8571
- filter: "named"
8572
- });
8573
- let resolved;
8574
- try {
8575
- resolved = resolveSessionByNameOrId(allSessions, query);
8576
- } catch (resolveErr) {
8577
- let msg = String(resolveErr).replace(/^Error: /, "");
8578
- if (msg.includes("No session found matching")) msg += `\n\nTip: pai find "${query}" — search prompt history by keywords`;
8579
- console.error(err(msg));
8580
- process.exit(1);
8581
- }
8582
- const { session: matchedSession, friendlyName } = resolved;
8583
- let resumableUuid;
8584
- let resumableSession;
8585
- if (matchedSession.resumable) {
8586
- resumableUuid = matchedSession.uuid;
8587
- resumableSession = matchedSession;
8588
- } else if (matchedSession.encodedDir) {
8589
- const sameProject = allSessions.filter((s) => s.encodedDir === matchedSession.encodedDir && s.resumable);
8590
- sameProject.sort((a, b) => b.mtime - a.mtime);
8591
- if (sameProject.length > 0) {
8592
- resumableSession = sameProject[0];
8593
- resumableUuid = resumableSession.uuid;
8594
- }
8595
- }
8596
- const rawDir = matchedSession.clcDirectory ?? matchedSession.registryRootPath ?? matchedSession.decodedPath;
8597
- let projectDir;
8598
- try {
8599
- projectDir = realpathSync(rawDir);
8600
- } catch {
8601
- 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.`));
8602
- process.exit(1);
8603
- }
8604
- const name = friendlyName ?? query;
8605
- const promptArg = `/Name ${name}\ngo`;
8606
- if (opts.dryRun) {
8607
- if (resumableUuid) {
8608
- const argvResume = `claude --resume ${resumableUuid} --name "${name}" "/Name ${name}\\ngo"`;
8609
- const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
8610
- console.log("\n" + chalk.bold("Dry run — would probe then exec (RESUME path):") + "\n");
8611
- console.log(` cwd: ${chalk.cyan(projectDir)}`);
8612
- console.log(` probe: claude --resume ${resumableUuid} --print --output-format=json "_"`);
8613
- console.log(` argv: ${chalk.white(argvResume)}`);
8614
- console.log(` fallback: ${chalk.yellow(argvFresh)}`);
8615
- if (resumableSession) {
8616
- console.log(`\n uuid: ${resumableSession.uuid}`);
8617
- console.log(` age: ${fmtAge(resumableSession.mtime)}`);
8618
- console.log(` status: ${resumableSession.sessionStatus}`);
8619
- console.log(` sys: ${resumableSession.topLevelSystemLines} system lines`);
8620
- }
8621
- } else {
8622
- const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
8623
- console.log("\n" + chalk.bold("Dry run — would exec (FRESH path, no resumable UUID):") + "\n");
8624
- console.log(` cwd: ${chalk.cyan(projectDir)}`);
8625
- console.log(` argv: ${chalk.white(argvFresh)}`);
8626
- }
8627
- console.log();
8628
- return;
8629
- }
8630
- if (resumableUuid) {
8631
- const probe = probeResume$1(resumableUuid, projectDir);
8632
- if (probe.ok) {
8633
- const result = spawnSync("claude", [
8634
- "--resume",
8635
- resumableUuid,
8636
- "--name",
8637
- name,
8638
- promptArg
8639
- ], {
8640
- cwd: projectDir,
8641
- stdio: "inherit",
8642
- env: process.env
8643
- });
8644
- if (result.error) {
8645
- console.error(err(`Failed to launch claude: ${result.error.message}`));
8646
- process.exit(1);
8647
- }
8648
- printExitDir(projectDir);
8649
- process.exit(result.status ?? 0);
8650
- } else {
8651
- 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`));
8652
- const result = spawnSync("claude", [
8653
- "--name",
8654
- name,
8655
- promptArg
8656
- ], {
8657
- cwd: projectDir,
8658
- stdio: "inherit",
8659
- env: process.env
8660
- });
8661
- if (result.error) {
8662
- console.error(err(`Failed to launch claude: ${result.error.message}`));
8663
- process.exit(1);
8664
- }
8665
- printExitDir(projectDir);
8666
- process.exit(result.status ?? 0);
8667
- }
8668
- } else {
8669
- const result = spawnSync("claude", [
8670
- "--name",
8671
- name,
8672
- promptArg
8673
- ], {
8674
- cwd: projectDir,
8675
- stdio: "inherit",
8676
- env: process.env
8677
- });
8678
- if (result.error) {
8679
- console.error(err(`Failed to launch claude: ${result.error.message}`));
8680
- process.exit(1);
8681
- }
8682
- printExitDir(projectDir);
8683
- process.exit(result.status ?? 0);
8684
- }
8685
- }
8686
-
8687
10610
  //#endregion
8688
10611
  //#region src/cli/commands/session/pause-all.ts
8689
10612
  /**
@@ -8845,96 +10768,6 @@ async function cmdClearNames(opts) {
8845
10768
  } else console.log(ok("\nDone. Use /Name to re-label each tab."));
8846
10769
  }
8847
10770
 
8848
- //#endregion
8849
- //#region src/cli/commands/session/helpers.ts
8850
- function getProject(db, slug) {
8851
- return db.prepare("SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?").get(slug);
8852
- }
8853
- function statusColor(status) {
8854
- switch (status) {
8855
- case "completed": return chalk.green(status);
8856
- case "compacted": return chalk.blue(status);
8857
- default: return chalk.yellow(status);
8858
- }
8859
- }
8860
-
8861
- //#endregion
8862
- //#region src/cli/commands/session/commands.ts
8863
- function cmdList(db, projectSlug, opts) {
8864
- const limit = parseInt(opts.limit ?? "20", 10);
8865
- const params = [];
8866
- let query = `
8867
- SELECT s.*, p.slug AS project_slug, p.display_name AS project_name
8868
- FROM sessions s
8869
- JOIN projects p ON p.id = s.project_id
8870
- `;
8871
- const where = [];
8872
- if (projectSlug) {
8873
- const project = getProject(db, projectSlug);
8874
- if (!project) {
8875
- console.error(err(`Project not found: ${projectSlug}`));
8876
- process.exit(1);
8877
- }
8878
- where.push("s.project_id = ?");
8879
- params.push(project.id);
8880
- }
8881
- if (opts.status) {
8882
- where.push("s.status = ?");
8883
- params.push(opts.status);
8884
- }
8885
- if (where.length) query += " WHERE " + where.join(" AND ");
8886
- query += " ORDER BY s.date DESC, s.number DESC";
8887
- query += ` LIMIT ${limit}`;
8888
- const rows = db.prepare(query).all(...params);
8889
- if (!rows.length) {
8890
- console.log(warn("No sessions found."));
8891
- return;
8892
- }
8893
- const showProject = !projectSlug;
8894
- const headers = showProject ? [
8895
- "#",
8896
- "Project",
8897
- "Date",
8898
- "Title",
8899
- "Status",
8900
- "Tokens"
8901
- ] : [
8902
- "#",
8903
- "Date",
8904
- "Title",
8905
- "Status",
8906
- "Tokens"
8907
- ];
8908
- const tableRows = rows.map((r) => {
8909
- const title = r.title.length > 45 ? r.title.slice(0, 42) + "..." : r.title;
8910
- const tokens = r.token_count != null ? dim$1(r.token_count.toLocaleString()) : dim$1("—");
8911
- if (showProject) return [
8912
- dim$1(`#${r.number}`),
8913
- r.project_slug,
8914
- r.date,
8915
- title,
8916
- statusColor(r.status),
8917
- tokens
8918
- ];
8919
- return [
8920
- dim$1(`#${r.number}`),
8921
- r.date,
8922
- title,
8923
- statusColor(r.status),
8924
- tokens
8925
- ];
8926
- });
8927
- console.log();
8928
- if (projectSlug) {
8929
- const project = getProject(db, projectSlug);
8930
- console.log(` ${bold$1(project.display_name)} sessions:`);
8931
- console.log();
8932
- }
8933
- console.log(renderTable(headers, tableRows));
8934
- console.log();
8935
- console.log(dim$1(` ${rows.length} session(s) shown (limit: ${limit})`));
8936
- }
8937
-
8938
10771
  //#endregion
8939
10772
  //#region src/cli/commands/find.ts
8940
10773
  /**
@@ -9304,7 +11137,7 @@ function buildFeedFrom(allSessions, projects, liveSessions, allProjects) {
9304
11137
  const nameRoot = /* @__PURE__ */ new Map();
9305
11138
  for (const p of allProjects) {
9306
11139
  slugRoot.set(p.slug, p.root_path);
9307
- nameRoot.set(normalizeName(p.display_name ?? p.slug).toLowerCase(), p.root_path);
11140
+ nameRoot.set(normalizeName$1(p.display_name ?? p.slug).toLowerCase(), p.root_path);
9308
11141
  }
9309
11142
  const bestResumable = /* @__PURE__ */ new Map();
9310
11143
  for (const s of allSessions) {
@@ -9706,5 +11539,5 @@ async function cmdPick(db, opts = {}) {
9706
11539
  }
9707
11540
 
9708
11541
  //#endregion
9709
- export { registerDaemonCommands as C, registerProjectsCommands as D, registerRegistryCommands as E, findMovedPath as O, registerBackupCommands as S, registerMemoryCommands as T, registerObservationCommands as _, cmdPauseAll as a, registerSetupCommand as b, cmdPause as c, registerKgCommands as d, registerTopicCommands as f, registerSkillCommands as g, registerUpdateCommand as h, cmdClearNames as i, resolveIdentifier as k, registerHelpCommand as l, registerNotifyCommands as m, cmdFind as n, cmdGoto as o, registerTaskCommands as p, cmdList as r, cmdEnd as s, cmdPick as t, registerDbCommands as u, registerZettelCommands as v, registerMcpCommands as w, registerRestoreCommands as x, registerObsidianCommands as y };
9710
- //# sourceMappingURL=pick-gne3kOJG.mjs.map
11542
+ export { findMovedPath as A, registerRestoreCommands as C, registerMemoryCommands as D, registerMcpCommands as E, registerRegistryCommands as O, registerSetupCommand as S, registerDaemonCommands as T, registerUpdateCommand as _, cmdEnd as a, registerZettelCommands as b, cmdGoto as c, registerHelpCommand as d, registerDbCommands as f, registerNotifyCommands as g, registerTaskCommands as h, cmdPauseAll as i, resolveIdentifier as j, registerProjectsCommands as k, cmdPause as l, registerTopicCommands as m, cmdFind as n, registerSessionCleanupCommand as o, registerKgCommands as p, cmdClearNames as r, registerSessionCommands as s, cmdPick as t, cmdList as u, registerSkillCommands as v, registerBackupCommands as w, registerObsidianCommands as x, registerObservationCommands as y };
11543
+ //# sourceMappingURL=pick-B3zEYjin.mjs.map