@tekmidian/pai 0.36.2 → 0.38.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 (157) hide show
  1. package/dist/{auto-route-Byf8ENXj.mjs → auto-route-DM7GhJ8y.mjs} +2 -2
  2. package/dist/{auto-route-Byf8ENXj.mjs.map → auto-route-DM7GhJ8y.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +5 -4
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/cli/program.d.mts.map +1 -1
  6. package/dist/cli/program.mjs +5 -4
  7. package/dist/{clusters-wZgTCYCB.mjs → clusters-Do4tEGyc.mjs} +1 -1
  8. package/dist/{clusters-wZgTCYCB.mjs.map → clusters-Do4tEGyc.mjs.map} +1 -1
  9. package/dist/{context-handover-cache-PtNvj_8D.mjs → context-handover-cache-BpUojjsi.mjs} +2 -2
  10. package/dist/{context-handover-cache-PtNvj_8D.mjs.map → context-handover-cache-BpUojjsi.mjs.map} +1 -1
  11. package/dist/daemon/index.mjs +8 -8
  12. package/dist/{daemon-BZ93KRBo.mjs → daemon-CGg1VCbA.mjs} +25 -25
  13. package/dist/{daemon-BZ93KRBo.mjs.map → daemon-CGg1VCbA.mjs.map} +1 -1
  14. package/dist/daemon-DsGGiIJM.mjs +20 -0
  15. package/dist/daemon-mcp/index.mjs +549 -1
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/{detector--Gg5JRN5.mjs → detector-CMap-9vw.mjs} +1 -1
  18. package/dist/{detector--Gg5JRN5.mjs.map → detector-CMap-9vw.mjs.map} +1 -1
  19. package/dist/detector-DtLExmHN.mjs +5 -0
  20. package/dist/factory-BXzqRYVZ.mjs +3 -0
  21. package/dist/{factory-Bsp7xOpO.mjs → factory-DD2T33C9.mjs} +5 -5
  22. package/dist/{factory-Bsp7xOpO.mjs.map → factory-DD2T33C9.mjs.map} +1 -1
  23. package/dist/hooks/context-compression-hook.mjs +164 -14
  24. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  25. package/dist/hooks/initialize-session.mjs +6 -0
  26. package/dist/hooks/initialize-session.mjs.map +3 -3
  27. package/dist/hooks/load-core-context.mjs +6 -0
  28. package/dist/hooks/load-core-context.mjs.map +3 -3
  29. package/dist/hooks/load-project-context.mjs +6 -0
  30. package/dist/hooks/load-project-context.mjs.map +3 -3
  31. package/dist/hooks/post-compact-inject.mjs +6 -0
  32. package/dist/hooks/post-compact-inject.mjs.map +3 -3
  33. package/dist/hooks/route-agents-to-worker.mjs +518 -0
  34. package/dist/hooks/route-agents-to-worker.mjs.map +7 -0
  35. package/dist/hooks/status-line.mjs +400 -0
  36. package/dist/hooks/status-line.mjs.map +7 -0
  37. package/dist/hooks/stop-hook.mjs +6 -0
  38. package/dist/hooks/stop-hook.mjs.map +3 -3
  39. package/dist/hooks/subagent-stop-hook.mjs +6 -0
  40. package/dist/hooks/subagent-stop-hook.mjs.map +3 -3
  41. package/dist/hooks/sync-todo-to-md.mjs +6 -0
  42. package/dist/hooks/sync-todo-to-md.mjs.map +3 -3
  43. package/dist/hooks/update-tab-on-action.mjs +6 -0
  44. package/dist/hooks/update-tab-on-action.mjs.map +3 -3
  45. package/dist/hooks/update-tab-titles.mjs +6 -0
  46. package/dist/hooks/update-tab-titles.mjs.map +3 -3
  47. package/dist/hooks/worker-proxy.mjs +858 -0
  48. package/dist/hooks/worker-proxy.mjs.map +7 -0
  49. package/dist/hooks/worker-status-line.mjs +545 -0
  50. package/dist/hooks/worker-status-line.mjs.map +7 -0
  51. package/dist/{indexer-backend-nQZuEx6N.mjs → indexer-backend-Cox9BCo-.mjs} +1 -1
  52. package/dist/{indexer-backend-nQZuEx6N.mjs.map → indexer-backend-Cox9BCo-.mjs.map} +1 -1
  53. package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
  54. package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
  55. package/dist/{latent-ideas-Bn6A5-5P.mjs → latent-ideas-BC1oINZ-.mjs} +2 -2
  56. package/dist/{latent-ideas-Bn6A5-5P.mjs.map → latent-ideas-BC1oINZ-.mjs.map} +1 -1
  57. package/dist/{link-boost-fYjUnxCN.mjs → link-boost-QFLrJwD6.mjs} +1 -1
  58. package/dist/{link-boost-fYjUnxCN.mjs.map → link-boost-QFLrJwD6.mjs.map} +1 -1
  59. package/dist/{main-resolver-BAbhKpeX.mjs → main-resolver-DlaLOFBA.mjs} +1 -1
  60. package/dist/{main-resolver-BAbhKpeX.mjs.map → main-resolver-DlaLOFBA.mjs.map} +1 -1
  61. package/dist/{main-resolver-Dxh444GO.mjs → main-resolver-IhZo4pI0.mjs} +1 -1
  62. package/dist/{neighborhood-DpaEM991.mjs → neighborhood-D9MJ1c8f.mjs} +1 -1
  63. package/dist/{neighborhood-DpaEM991.mjs.map → neighborhood-D9MJ1c8f.mjs.map} +1 -1
  64. package/dist/{note-context-DrcY4cWm.mjs → note-context-d1wT_-GA.mjs} +1 -1
  65. package/dist/{note-context-DrcY4cWm.mjs.map → note-context-d1wT_-GA.mjs.map} +1 -1
  66. package/dist/{postgres-BVme6qX0.mjs → postgres--BjPtLa0.mjs} +1 -1
  67. package/dist/{postgres-BVme6qX0.mjs.map → postgres--BjPtLa0.mjs.map} +1 -1
  68. package/dist/{program-C-fUghPv.mjs → program-Y0hAiVy8.mjs} +851 -101
  69. package/dist/program-Y0hAiVy8.mjs.map +1 -0
  70. package/dist/providers-DUshcB-d.mjs +4031 -0
  71. package/dist/providers-DUshcB-d.mjs.map +1 -0
  72. package/dist/{query-feedback-C1T6kS18.mjs → query-feedback-BUJxgw5B.mjs} +1 -1
  73. package/dist/{query-feedback-C1T6kS18.mjs.map → query-feedback-BUJxgw5B.mjs.map} +1 -1
  74. package/dist/query-feedback-DhyLOe5S.mjs +3 -0
  75. package/dist/router-1zi8jiNF.mjs +3 -0
  76. package/dist/{router-CsDm7HvK.mjs → router-DK_sLsUL.mjs} +1 -1
  77. package/dist/{router-CsDm7HvK.mjs.map → router-DK_sLsUL.mjs.map} +1 -1
  78. package/dist/skills/Worker/SKILL.md +63 -0
  79. package/dist/{sources-D8ZdNfvK.mjs → sources-Bi7--33T.mjs} +1 -1
  80. package/dist/{sources-D8ZdNfvK.mjs.map → sources-Bi7--33T.mjs.map} +1 -1
  81. package/dist/{sqlite-D1IaR8Am.mjs → sqlite-DtaL1glm.mjs} +1 -1
  82. package/dist/{sqlite-D1IaR8Am.mjs.map → sqlite-DtaL1glm.mjs.map} +1 -1
  83. package/dist/{state-qtmrBWCm.mjs → state-8Hm9E4tW.mjs} +1 -1
  84. package/dist/{state-WaXhLr6R.mjs → state-BY2L6-vX.mjs} +1 -1
  85. package/dist/{state-WaXhLr6R.mjs.map → state-BY2L6-vX.mjs.map} +1 -1
  86. package/dist/{themes-XPkj_bfP.mjs → themes-BN0a2duq.mjs} +1 -1
  87. package/dist/{themes-XPkj_bfP.mjs.map → themes-BN0a2duq.mjs.map} +1 -1
  88. package/dist/{tools-DEt6YPfc.mjs → tools-CGPqpU3A.mjs} +1 -1
  89. package/dist/{tools-ceiy7ANX.mjs → tools-y2bJpKom.mjs} +14 -14
  90. package/dist/{tools-ceiy7ANX.mjs.map → tools-y2bJpKom.mjs.map} +1 -1
  91. package/dist/{trace-DfyGmMG_.mjs → trace-h23JCcFD.mjs} +1 -1
  92. package/dist/{trace-DfyGmMG_.mjs.map → trace-h23JCcFD.mjs.map} +1 -1
  93. package/dist/{vault-indexer-CFvlPUMB.mjs → vault-indexer-DgsPjMgs.mjs} +1 -1
  94. package/dist/{vault-indexer-CFvlPUMB.mjs.map → vault-indexer-DgsPjMgs.mjs.map} +1 -1
  95. package/dist/{work-queue-worker-B8W8_3Rn.mjs → work-queue-worker-Dva_v_pI.mjs} +4 -4
  96. package/dist/{work-queue-worker-B8W8_3Rn.mjs.map → work-queue-worker-Dva_v_pI.mjs.map} +1 -1
  97. package/dist/{work-queue-worker-HN2Ufg-L.mjs → work-queue-worker-gsKd2LJa.mjs} +4 -4
  98. package/dist/{zettelkasten-CvjmMghT.mjs → zettelkasten-vo7psPdT.mjs} +3 -3
  99. package/dist/{zettelkasten-CvjmMghT.mjs.map → zettelkasten-vo7psPdT.mjs.map} +1 -1
  100. package/docs/commands/README.md +29 -0
  101. package/docs/commands/backup.md +1 -1
  102. package/docs/commands/clear-names.md +1 -1
  103. package/docs/commands/daemon.md +1 -1
  104. package/docs/commands/db.md +1 -1
  105. package/docs/commands/end.md +1 -1
  106. package/docs/commands/help.md +1 -1
  107. package/docs/commands/identity.md +1 -1
  108. package/docs/commands/kg.md +1 -1
  109. package/docs/commands/mcp.md +1 -1
  110. package/docs/commands/memory.md +1 -1
  111. package/docs/commands/notify.md +1 -1
  112. package/docs/commands/observation.md +1 -1
  113. package/docs/commands/obsidian.md +1 -1
  114. package/docs/commands/pause.md +1 -1
  115. package/docs/commands/project.md +1 -1
  116. package/docs/commands/projects.md +1 -1
  117. package/docs/commands/registry.md +1 -1
  118. package/docs/commands/restore.md +1 -1
  119. package/docs/commands/session.md +1 -1
  120. package/docs/commands/sessions.md +1 -1
  121. package/docs/commands/setup.md +1 -1
  122. package/docs/commands/shell-init.md +1 -1
  123. package/docs/commands/skill.md +1 -1
  124. package/docs/commands/task.md +1 -1
  125. package/docs/commands/topic.md +1 -1
  126. package/docs/commands/update.md +1 -1
  127. package/docs/commands/worker.md +398 -0
  128. package/docs/commands/zettel.md +1 -1
  129. package/docs/worker.md +434 -0
  130. package/package.json +1 -1
  131. package/scripts/build-hooks.mjs +46 -1
  132. package/src/hooks/pre-compact.sh +1 -0
  133. package/src/hooks/session-autosave.sh +1 -0
  134. package/src/hooks/session-stop.sh +1 -0
  135. package/src/hooks/ts/lib/context-fill.test.ts +104 -0
  136. package/src/hooks/ts/lib/context-fill.ts +73 -0
  137. package/src/hooks/ts/lib/handover-evidence.ts +227 -0
  138. package/src/hooks/ts/lib/worker-session.test.ts +24 -0
  139. package/src/hooks/ts/lib/worker-session.ts +23 -0
  140. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +2 -0
  141. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +2 -0
  142. package/src/hooks/ts/pre-compact/context-compression-hook.ts +17 -0
  143. package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +128 -0
  144. package/src/hooks/ts/session-start/initialize-session.ts +2 -0
  145. package/src/hooks/ts/session-start/load-core-context.ts +2 -0
  146. package/src/hooks/ts/session-start/load-project-context.ts +2 -0
  147. package/src/hooks/ts/session-start/post-compact-inject.ts +2 -0
  148. package/src/hooks/ts/stop/stop-hook.ts +2 -0
  149. package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +2 -0
  150. package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -0
  151. package/statusline-command.sh +17 -0
  152. package/dist/daemon-DRdoA489.mjs +0 -20
  153. package/dist/detector-DGAk1iBR.mjs +0 -5
  154. package/dist/factory-CrokPMk2.mjs +0 -3
  155. package/dist/program-C-fUghPv.mjs.map +0 -1
  156. package/dist/query-feedback-BBMBp96K.mjs +0 -3
  157. package/dist/router-BMkOb62X.mjs +0 -3
@@ -1 +1 @@
1
- {"version":3,"file":"main-resolver-BAbhKpeX.mjs","names":["shortenProject"],"sources":["../src/cli/lib/session-scan.ts","../src/cli/lib/aibroker-client.ts","../src/cli/lib/exit-dir.ts","../src/cli/lib/launch.ts","../src/cli/lib/history-search.ts","../src/cli/lib/dedup-sessions.ts","../src/cli/commands/main-resolver.ts"],"sourcesContent":["/**\n * Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .\n *\n * Storage layout (Claude Code v2.1.143+):\n *\n * <project>/<uuid>.jsonl — top-level: metadata, system snapshots, hook events.\n * REQUIRED for claude --resume to work.\n * <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.\n * 3000+ files, almost none resumable on their own.\n *\n * Resumability rule (empirically verified):\n * A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one\n * line of type \"system\". Sessions that only have a sessions/ counterpart cannot be\n * resumed by Claude Code regardless of how much transcript content they have.\n *\n * Stale-UUID problem (fixed in this version):\n * The clc session.json registry stores ONE uuid per named session — the uuid Claude Code\n * had when the user last named the session. If the user resumes and the session gets a\n * new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD\n * uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project\n * directory, not the clc-cached uuid.\n *\n * Resolution strategy:\n * 1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).\n * 2. For each, attach the clc name if the uuid matches (exact hit).\n * 3. After Pass 1, for every clc registry entry:\n * a. If the cached uuid was found → already handled.\n * b. Find the encodedDir for this entry's directory.\n * c. Check ALL sessions in that encodedDir (already in our Pass-1 results).\n * d. Pick the MOST RECENT resumable session in that dir and attach the name to it.\n * If no resumable session, fall through to transcript-only pass.\n * 4. This means the displayed uuid for \"Jobs Alpha\" is always today's active session,\n * not the stale cached one.\n *\n * Used by: pai sessions, pai resume\n */\n\nimport { readdirSync, statSync, readFileSync, existsSync, realpathSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Database } from \"better-sqlite3\";\nimport { smartDecodeDir } from \"../utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Display status for a session:\n * resumable — top-level jsonl exists with ≥1 system line (claude --resume works)\n * stub — in clc registry, top-level jsonl exists but 0 system lines (Ctrl+C exit)\n * transcript-only — in clc registry but no top-level jsonl at all\n * orphan — not in clc registry, only sessions/ transcript exists\n */\nexport type SessionStatus = \"resumable\" | \"stub\" | \"transcript-only\" | \"orphan\";\n\nexport interface ScannedSession {\n /** Full UUID from the filename. */\n uuid: string;\n /** Short 8-char prefix for display. */\n shortId: string;\n /** Encoded project directory name (e.g. \"-Users-foo-myproject\"). */\n encodedDir: string;\n /** Decoded project root path (via smartDecodeDir). */\n decodedPath: string;\n\n // Top-level jsonl — the resumability source\n /** Full path to the top-level <project>/<uuid>.jsonl (may not exist). */\n topLevelPath: string;\n /** Number of \"system\" lines in the top-level jsonl. */\n topLevelSystemLines: number;\n /** Size of the top-level jsonl in bytes. */\n topLevelSize: number;\n /**\n * True iff the top-level jsonl has at least one \"system\" line.\n * This is the canonical resumability gate — matches Claude Code's --resume behaviour.\n */\n resumable: boolean;\n /** Categorised display status. */\n sessionStatus: SessionStatus;\n\n // Sessions/ transcript data (best-effort, may not exist)\n /** Full path to sessions/<uuid>.jsonl, if present. */\n sessionJsonlPath?: string;\n /** Number of \"user\" type lines in the transcript. */\n userLines: number;\n /** Last user message text (truncated to 80 chars). */\n lastUserPrompt: string;\n /** Total line count in the transcript file. */\n msgCount: number;\n /** Auto-generated title from the last ai-title line in the transcript. */\n aiTitle?: string;\n\n /** mtime in epoch ms — top-level file mtime (primary), transcript mtime (fallback). */\n mtime: number;\n\n /**\n * Human-readable friendly name (for display and /Name restoration).\n * Priority: clc session.json name → ai-title → project dir basename.\n * NEVER slugified — preserves original spaces and capitalisation.\n */\n friendlyName?: string;\n /**\n * Literal directory from the clc session registry for this UUID.\n * May contain symlinks and emoji (e.g. /Users/foo/dev/... or .../🧠 Vault/...).\n * When present, realpathSync(clcDirectory) gives the correct cwd for --resume.\n */\n clcDirectory?: string;\n /** Project root path from PAI registry (authoritative cwd for resume). */\n registryRootPath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// clc session registry (~/.claude/session.json)\n// ---------------------------------------------------------------------------\n\nconst CLC_SESSIONS_FILE = join(homedir(), \".claude\", \"session.json\");\n\ninterface ClcEntry {\n name?: string;\n resume?: string;\n session?: string;\n directory?: string;\n}\n\ninterface ClcRegistry {\n sessions?: ClcEntry[];\n}\n\nexport interface ClcInfo {\n name: string;\n /** Literal directory from clc registry (may contain symlinks, emoji, spaces). */\n directory?: string;\n}\n\n/** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */\nfunction buildClcInfoMap(): Map<string, ClcInfo> {\n const map = new Map<string, ClcInfo>();\n try {\n const raw = readFileSync(CLC_SESSIONS_FILE, \"utf8\");\n const data = JSON.parse(raw) as ClcRegistry;\n for (const entry of data.sessions ?? []) {\n const name = entry.name?.trim();\n const uuid = (entry.resume ?? entry.session ?? \"\").trim();\n if (name && uuid) {\n map.set(uuid, { name, directory: entry.directory?.trim() || undefined });\n }\n }\n } catch {\n // Missing or malformed — not fatal\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// PAI registry\n// ---------------------------------------------------------------------------\n\ninterface RegistryProject {\n root_path: string;\n encoded_dir: string;\n}\n\n/** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */\nfunction buildRegistryRootPathMap(db: Database): Map<string, string> {\n try {\n const rows = db\n .prepare(\n `SELECT root_path, encoded_dir FROM projects\n WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`\n )\n .all() as RegistryProject[];\n const map = new Map<string, string>();\n for (const row of rows) {\n map.set(row.encoded_dir, row.root_path);\n }\n return map;\n } catch {\n return new Map();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Top-level jsonl parser\n// ---------------------------------------------------------------------------\n\ninterface TopLevelInfo {\n systemLines: number;\n size: number;\n mtime: number;\n}\n\nfunction parseTopLevel(filePath: string): TopLevelInfo {\n let systemLines = 0;\n let size = 0;\n let mtime = 0;\n try {\n const st = statSync(filePath);\n size = st.size;\n mtime = st.mtimeMs;\n const content = readFileSync(filePath, \"utf8\");\n for (const line of content.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n if (t.includes('\"type\":\"system\"') || t.includes('\"type\": \"system\"')) {\n systemLines++;\n }\n }\n } catch {\n /* ignore */\n }\n return { systemLines, size, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Sessions/ transcript parser\n// ---------------------------------------------------------------------------\n\ninterface TranscriptInfo {\n userLines: number;\n lastUserPrompt: string;\n msgCount: number;\n aiTitle?: string;\n mtime: number;\n}\n\nfunction parseTranscript(filePath: string): TranscriptInfo {\n let userLines = 0;\n let lastUserPrompt = \"\";\n let msgCount = 0;\n let aiTitle: string | undefined;\n let mtime = 0;\n\n try {\n mtime = statSync(filePath).mtimeMs;\n } catch {\n return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n }\n\n let content: string;\n try {\n content = readFileSync(filePath, \"utf8\");\n } catch {\n return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime };\n }\n\n for (const line of content.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n msgCount++;\n\n const hasUser = t.includes('\"type\":\"user\"') || t.includes('\"type\": \"user\"');\n const hasTitle =\n t.includes('\"type\":\"ai-title\"') || t.includes('\"type\": \"ai-title\"');\n if (!hasUser && !hasTitle) continue;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(t) as Record<string, unknown>;\n } catch {\n continue;\n }\n\n if (parsed.type === \"ai-title\") {\n const v = parsed.title ?? parsed.name;\n if (typeof v === \"string\" && v.trim()) aiTitle = v.trim();\n continue;\n }\n\n if (parsed.type !== \"user\") continue;\n userLines++;\n\n const msg = parsed.message as Record<string, unknown> | undefined;\n if (!msg) continue;\n const c = msg.content;\n let text = \"\";\n if (typeof c === \"string\") {\n text = c;\n } else if (Array.isArray(c) && c.length > 0) {\n const first = c[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") text = first.text;\n }\n if (text) lastUserPrompt = text.slice(0, 80).replace(/\\n/g, \" \");\n }\n\n return { userLines, lastUserPrompt, msgCount, aiTitle, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Main scanner\n// ---------------------------------------------------------------------------\n\nexport interface ScanOptions {\n /** Maximum results to return after sorting (default: 200). */\n limit?: number;\n /**\n * Filter mode:\n * \"named\" (default) — resumable OR in clc registry (named sessions the user cares about)\n * \"all\" — everything including unnamed orphans\n * \"resumable\" — only sessions claude --resume accepts (top-level + system lines)\n */\n filter?: \"named\" | \"all\" | \"resumable\";\n /**\n * @deprecated Use filter instead. When true → filter:\"resumable\", false → filter:\"named\".\n * Kept for backwards compatibility with callers that predate the filter field.\n */\n resumableOnly?: boolean;\n}\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// UUID format: 8-4-4-4-12 hex digits\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Compute the effective filter mode, resolving the legacy resumableOnly flag.\n */\nfunction resolveFilter(opts: ScanOptions): \"named\" | \"all\" | \"resumable\" {\n if (opts.filter) return opts.filter;\n if (opts.resumableOnly === true) return \"resumable\";\n if (opts.resumableOnly === false) return \"all\";\n return \"named\"; // default\n}\n\n/**\n * Scan ~/.claude/projects/ for all Claude Code sessions.\n *\n * Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).\n * Pass 1b: walk <project>/sessions/<uuid>.jsonl — where the stop hook moves a\n * session's transcript once it ends. These are resumable too, and\n * skipping them made every cleanly-stopped session unfindable by name.\n * Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.\n * For each such entry, scan the entry's project dir for the FRESHEST session\n * and attach the registry name to it. This fixes the stale-UUID bug where\n * clc's session.json points to an old uuid after a fresh start.\n * Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.\n *\n * Filter modes:\n * \"named\" — resumable + registry-known stubs + transcript-only (default)\n * \"all\" — all of the above + unnamed orphans from sessions/ subdirs\n * \"resumable\" — only sessions claude --resume accepts\n *\n * Returns results sorted by mtime descending.\n */\nexport function scanSessions(\n db: Database,\n opts: ScanOptions = {}\n): ScannedSession[] {\n const limit = opts.limit ?? 200;\n const filterMode = resolveFilter(opts);\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n const clcInfoMap = buildClcInfoMap();\n const rootPathMap = buildRegistryRootPathMap(db);\n const results: ScannedSession[] = [];\n // Track which UUIDs we've already added (from the jsonl walk)\n const seenUuids = new Set<string>();\n // Track which clc names we've already attached to a session\n const attachedClcUuids = new Set<string>();\n\n let encodedDirs: string[];\n try {\n encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n } catch {\n return [];\n }\n\n // Build a map: encodedDir → all top-level sessions found in Pass 1\n // This is used in Pass 2 to find the freshest session for a clc registry entry.\n const sessionsByEncodedDir = new Map<string, ScannedSession[]>();\n\n // ---- Pass 1: walk top-level <project>/<uuid>.jsonl files ----\n for (const encodedDir of encodedDirs) {\n const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n try {\n if (!statSync(projectDir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(projectDir);\n } catch {\n continue;\n }\n\n const decodedPath =\n smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n const registryRootPath = rootPathMap.get(encodedDir);\n const projectBasename = basename(decodedPath);\n\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n\n const topLevelPath = join(projectDir, file);\n const topInfo = parseTopLevel(topLevelPath);\n const resumable = topInfo.systemLines > 0;\n const clcInfo = clcInfoMap.get(uuid);\n const inRegistry = !!clcInfo;\n\n // Determine status\n const sessionStatus: SessionStatus = resumable\n ? \"resumable\"\n : inRegistry\n ? \"stub\"\n : \"orphan\";\n\n // Apply filter — but include ALL in this pass so Pass 2 can use them\n // We'll filter again when deciding what to add to `results`.\n const passesFilter =\n filterMode === \"resumable\"\n ? resumable\n : filterMode === \"named\"\n ? resumable || inRegistry\n : true;\n\n // Join with sessions/ transcript (best-effort)\n const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n const hasTranscript = existsSync(sessionJsonlPath);\n const transcript: TranscriptInfo = hasTranscript\n ? parseTranscript(sessionJsonlPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n // Use top-level mtime as primary\n const mtime = topInfo.mtime || transcript.mtime;\n\n // Name resolution: clc registry (verbatim) → ai-title → project basename\n const friendlyName =\n clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined;\n\n const session: ScannedSession = {\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: topInfo.systemLines,\n topLevelSize: topInfo.size,\n resumable,\n sessionStatus,\n sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime,\n friendlyName,\n clcDirectory: clcInfo?.directory,\n registryRootPath,\n };\n\n // Track in per-dir map for Pass 2 lookups (all sessions, regardless of filter)\n if (!sessionsByEncodedDir.has(encodedDir)) {\n sessionsByEncodedDir.set(encodedDir, []);\n }\n sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n seenUuids.add(uuid);\n if (inRegistry) attachedClcUuids.add(uuid);\n\n if (passesFilter) {\n results.push(session);\n }\n }\n\n // ---- Pass 1b: finished sessions, which live only in sessions/ ----\n //\n // The stop hook MOVES a session's transcript from <project>/<uuid>.jsonl to\n // <project>/sessions/<uuid>.jsonl. Pass 1 walks the top level only, so a\n // session becomes invisible to this scan the moment it is cleanly stopped —\n // and name resolution is built on this scan. That is why `pai Paperfull`\n // stopped finding Paperfull: three resumable transcripts sat in sessions/\n // while the scan reported zero sessions for the project, so `pai <name>`\n // fell through to a prompt-history search over old messages.\n //\n // These are reported resumable, which needs justifying, because\n // `claude --resume` does NOT read sessions/ — it reads the top-level path\n // and only that. What makes the label true is that nothing resumes without\n // going through probeResume first, and probeResume links the transcript back\n // to the top level before answering (lib/launch.ts, restoreTopLevel). PAI's\n // SessionStart hook displaced these files in the first place; it now links\n // rather than moves, so this is a claim about repairable state, not a hope.\n //\n // Do not weaken that chain without demoting these to transcript-only. An\n // earlier revision asserted plain \"claude --resume accepts these\", which is\n // false, and it fed unresumable ids to launch with full confidence.\n const sessionsDir = join(projectDir, \"sessions\");\n let finished: string[] = [];\n try {\n finished = readdirSync(sessionsDir);\n } catch {\n finished = []; // no sessions/ subdir — nothing was ever stopped here\n }\n\n for (const file of finished) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n if (seenUuids.has(uuid)) continue; // Pass 1 already has it\n\n const sessionJsonlPath = join(sessionsDir, file);\n const transcript = parseTranscript(sessionJsonlPath);\n const clcInfo = clcInfoMap.get(uuid);\n\n const session: ScannedSession = {\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n // No top-level file — that is the whole point of this pass.\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: true,\n sessionStatus: \"resumable\",\n sessionJsonlPath,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: transcript.mtime,\n friendlyName:\n clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined,\n clcDirectory: clcInfo?.directory,\n registryRootPath,\n };\n\n if (!sessionsByEncodedDir.has(encodedDir)) {\n sessionsByEncodedDir.set(encodedDir, []);\n }\n sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n seenUuids.add(uuid);\n if (clcInfo) attachedClcUuids.add(uuid);\n\n results.push(session); // resumable — passes every filter mode\n }\n }\n\n // ---- Pass 2: clc registry entries whose cached UUID wasn't found as a top-level file ----\n // For each such entry, find the project's encodedDir and look for the FRESHEST\n // resumable session there. Attach the clc name to it.\n // This fixes the stale-UUID bug: clc may store a uuid that's no longer valid,\n // but the project dir has a new active session the user is actually in.\n if (filterMode !== \"resumable\") {\n for (const [cachedUuid, clcInfo] of clcInfoMap) {\n if (attachedClcUuids.has(cachedUuid)) continue; // already named in Pass 1\n\n // Derive the encodedDir from clcInfo.directory\n let foundEncodedDir: string | undefined;\n if (clcInfo.directory) {\n const real = realpathSyncSafe(clcInfo.directory);\n if (real) {\n const encoded = encodeProjectDir(real);\n if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) {\n foundEncodedDir = encoded;\n }\n }\n }\n\n // Fallback: check if the cached uuid exists as a sessions/ transcript somewhere\n if (!foundEncodedDir) {\n for (const encodedDir of encodedDirs) {\n if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, \"sessions\", `${cachedUuid}.jsonl`))) {\n foundEncodedDir = encodedDir;\n break;\n }\n }\n }\n\n if (foundEncodedDir) {\n const dirSessions = sessionsByEncodedDir.get(foundEncodedDir) ?? [];\n\n // Find the freshest RESUMABLE session in this dir that doesn't yet have a name\n const freshestResumable = dirSessions\n .filter((s) => s.resumable && !s.friendlyName)\n .sort((a, b) => b.mtime - a.mtime)[0];\n\n if (freshestResumable) {\n // Attach the clc name to this fresher session\n freshestResumable.friendlyName = clcInfo.name;\n freshestResumable.clcDirectory =\n freshestResumable.clcDirectory ?? clcInfo.directory;\n freshestResumable.sessionStatus = \"resumable\";\n attachedClcUuids.add(freshestResumable.uuid);\n\n // Add to results if not already there (it might have been filtered out as orphan)\n if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {\n // It was added to sessionsByEncodedDir but might have been filtered from results\n if (!results.includes(freshestResumable)) {\n results.push(freshestResumable);\n }\n }\n continue; // Done with this clc entry\n }\n\n // No unnamed resumable session found. The project might have resumable sessions\n // that already have a name (different name). Or only stubs/orphans.\n // Fall through to transcript-only handling below.\n }\n\n // ---- Pass 3: truly no top-level jsonl → transcript-only ----\n if (!foundEncodedDir) {\n // We couldn't find the project dir at all\n const encodedDir = \"\";\n const decodedPath = clcInfo.directory ?? cachedUuid;\n const registryRootPath = undefined;\n\n const transcript: TranscriptInfo = { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n const mtime = transcript.mtime;\n\n seenUuids.add(cachedUuid);\n results.push({\n uuid: cachedUuid,\n shortId: cachedUuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: undefined,\n userLines: 0,\n lastUserPrompt: \"\",\n msgCount: 0,\n mtime,\n friendlyName: clcInfo.name,\n clcDirectory: clcInfo.directory,\n registryRootPath,\n });\n continue;\n }\n\n // Project dir was found but no unnamed resumable session.\n // Check sessions/ for the cached uuid transcript.\n const foundTranscriptPath = existsSync(\n join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n )\n ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n : undefined;\n\n const decodedPath =\n clcInfo.directory ??\n (smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, \"/\"));\n const registryRootPath = rootPathMap.get(foundEncodedDir);\n const topLevelPath = join(\n CLAUDE_PROJECTS_DIR,\n foundEncodedDir,\n `${cachedUuid}.jsonl`\n );\n\n const transcript: TranscriptInfo = foundTranscriptPath\n ? parseTranscript(foundTranscriptPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n seenUuids.add(cachedUuid);\n results.push({\n uuid: cachedUuid,\n shortId: cachedUuid.slice(0, 8),\n encodedDir: foundEncodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: foundTranscriptPath,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: transcript.mtime,\n friendlyName: clcInfo.name,\n clcDirectory: clcInfo.directory,\n registryRootPath,\n });\n }\n }\n\n results.sort((a, b) => b.mtime - a.mtime);\n return results.slice(0, limit);\n}\n\n// ---------------------------------------------------------------------------\n// Path encoding helpers\n// ---------------------------------------------------------------------------\n\n/** Claude Code's project-dir encoding: replace / . - (space) with - */\nfunction encodeProjectDir(realPath: string): string {\n // Leading slash becomes leading -; all / . - and space → -\n return realPath.replace(/[/.\\- ]/g, \"-\");\n}\n\n/** realpathSync that returns null instead of throwing */\nfunction realpathSyncSafe(p: string): string | null {\n try {\n return realpathSync(p);\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Age formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtAge(mtime: number): string {\n const diffMs = Date.now() - mtime;\n const diffMin = Math.floor(diffMs / 60_000);\n const diffHr = Math.floor(diffMs / 3_600_000);\n const diffDay = Math.floor(diffMs / 86_400_000);\n\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay < 30) return `${diffDay}d`;\n return `${Math.floor(diffDay / 30)}mo`;\n}\n\n// ---------------------------------------------------------------------------\n// Resolve a name/id/prefix to a session entry\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedSession {\n session: ScannedSession;\n /** The friendly name to use verbatim in the /Name command. Never slugified. */\n friendlyName?: string;\n}\n\n/**\n * Filesystem-level UUID scan: walk ALL ~/.claude/projects/<encoded-dir>/<uuid>.jsonl\n * looking for top-level files whose UUID starts with `prefix`.\n *\n * Returns a ScannedSession-like object (minimal fields) for each match, or an empty\n * array if nothing is found. This is used as a fallback when the regular catalog\n * (limited to named/recent sessions) doesn't contain the requested UUID.\n *\n * Complexity: O(number of project directories + files per dir). Typically <200 dirs\n * with a handful of top-level jsonl each — fast enough for an interactive CLI.\n */\nfunction scanFilesystemForUuidPrefix(prefix: string): ScannedSession[] {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n const prefixLower = prefix.toLowerCase();\n const matches: ScannedSession[] = [];\n\n let encodedDirs: string[];\n try {\n encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n } catch {\n return [];\n }\n\n for (const encodedDir of encodedDirs) {\n const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n try {\n if (!statSync(projectDir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(projectDir);\n } catch {\n continue;\n }\n\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n if (!uuid.toLowerCase().startsWith(prefixLower)) continue;\n\n const topLevelPath = join(projectDir, file);\n const topInfo = parseTopLevel(topLevelPath);\n const resumable = topInfo.systemLines > 0;\n const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n\n // Check sessions/ transcript for prompt and title\n const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n const hasTranscript = existsSync(sessionJsonlPath);\n const transcript: TranscriptInfo = hasTranscript\n ? parseTranscript(sessionJsonlPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n matches.push({\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: topInfo.systemLines,\n topLevelSize: topInfo.size,\n resumable,\n sessionStatus: resumable ? \"resumable\" : \"stub\",\n sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: topInfo.mtime || transcript.mtime,\n friendlyName: transcript.aiTitle ?? basename(decodedPath),\n clcDirectory: undefined,\n registryRootPath: undefined,\n });\n }\n }\n\n return matches.sort((a, b) => b.mtime - a.mtime);\n}\n\n/**\n * Resolve a name-or-id-or-prefix to a single ScannedSession.\n *\n * Comparisons are case-insensitive; stored casing is preserved in output.\n *\n * Priority:\n * 1. Exact case-insensitive match on friendlyName\n * 2. Partial case-insensitive match (contains)\n * 3. UUID prefix match against the in-memory catalog\n * 4. UUID prefix match against the full filesystem (fallback for any session)\n */\nexport function resolveSessionByNameOrId(\n sessions: ScannedSession[],\n query: string\n): ResolvedSession {\n const qLower = query.toLowerCase().trim();\n\n // 1. Exact match (case-insensitive, preserve stored casing)\n const byExact = sessions.filter(\n (s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower\n );\n if (byExact.length >= 1) {\n return { session: byExact[0], friendlyName: byExact[0].friendlyName };\n }\n\n // 2. Partial match\n const byPartial = sessions.filter(\n (s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower)\n );\n if (byPartial.length === 1) {\n return { session: byPartial[0], friendlyName: byPartial[0].friendlyName };\n }\n if (byPartial.length > 1) {\n const candidates = byPartial\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `Ambiguous name \"${query}\" — ${byPartial.length} matches:\\n${candidates}\\n\\nBe more specific or use a UUID prefix.`\n );\n }\n\n // 3. UUID prefix against catalog\n const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));\n if (byUuid.length === 1) {\n return { session: byUuid[0], friendlyName: byUuid[0].friendlyName };\n }\n if (byUuid.length > 1) {\n const candidates = byUuid\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `UUID prefix \"${query}\" is ambiguous — ${byUuid.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n );\n }\n\n // 4. Filesystem fallback — scan ALL ~/.claude/projects/*/<uuid>.jsonl files\n // This handles any session UUID, even if not in the named/recent catalog.\n // Only attempted when the query looks like a UUID prefix (hex + dashes).\n const UUID_PREFIX_RE = /^[0-9a-f-]{4,36}$/i;\n if (UUID_PREFIX_RE.test(qLower)) {\n const fsSessions = scanFilesystemForUuidPrefix(qLower);\n if (fsSessions.length === 1) {\n return { session: fsSessions[0], friendlyName: fsSessions[0].friendlyName };\n }\n if (fsSessions.length > 1) {\n const candidates = fsSessions\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `UUID prefix \"${query}\" is ambiguous — ${fsSessions.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n );\n }\n }\n\n throw new Error(\n `No session found matching \"${query}\".\\n\\nRun: pai sessions to list sessions.\\nRun: pai sessions --all to include transcript-only sessions.\\nRun: pai find <words> to search prompt history.`\n );\n}\n","/**\n * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.\n *\n * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,\n * and reads a single newline-terminated JSON response. No class needed —\n * just a thin async function matching the WatcherClient protocol.\n *\n * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport { spawnSync } from \"node:child_process\";\nimport { aibrokerSocketPath } from \"../../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Lightweight session metadata returned by the AIBroker `sessions` IPC method.\n * Does NOT include scrollback content — use `session_content` for that.\n */\nexport interface AiBrokerSessionMeta {\n index: number;\n sessionId: string;\n /** iTerm2 tab title or profile name */\n name: string;\n /** PAI session name set via /Name; null for bare shells */\n paiName: string | null;\n atPrompt: boolean;\n /** \"claude\" for Claude Code panes, \"shell\" for bare terminals */\n kind: \"claude\" | \"shell\";\n /** Whether this is the currently focused pane */\n active: boolean;\n /** Last user prompt seen in scrollback (only populated by fetchLiveSessionsWithPrompts) */\n lastPrompt?: string;\n}\n\ninterface AiBrokerSessionsResult {\n sessions: AiBrokerSessionMeta[];\n}\n\n// ---------------------------------------------------------------------------\n// Core call\n// ---------------------------------------------------------------------------\n\n// AIBROKER_SOCKET stays first: it is AIBroker's own variable and a user who has\n// set it means it. The fallback goes through runtime-paths so the test guard\n// can redirect it — writing to a live socket from a test is the same hazard as\n// writing to live user state, and this one is not even ours.\nconst DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? aibrokerSocketPath();\n\n/**\n * How long to wait for `send_to_session`, which must exceed the server's own\n * ack window or the caller gives up while the callee is still working.\n *\n * AIBroker waits up to SEND_ACK_TIMEOUT_MS (15s) for the submit confirmation —\n * the text leaving the input line — before answering. The generic client\n * timeout is 8s. So every send needing 8-15s to confirm reported\n * \"AIBroker IPC call timed out\" while succeeding, and the handler's eventual\n * `delivered: true` was written into a socket nobody was reading any more.\n *\n * Measured on `pai pause all` across 15 sessions: 9 reported failed, 8 of them\n * verifiably paused. A caller's deadline shorter than the callee's is not a\n * tuning question, it is a guaranteed false negative on every slow success.\n */\nconst SEND_TIMEOUT_MS = 30_000;\n\n/**\n * Call an AIBroker IPC method and return the result.\n *\n * Resolves with the `result` field of a successful response.\n * Rejects if the socket is not available, the call times out, or the\n * daemon returns an error.\n *\n * @param method IPC method name (e.g. \"session_content\", \"send_to_session\")\n * @param params Method parameters object\n * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)\n */\nexport function callAiBroker(\n method: string,\n params: Record<string, unknown> = {},\n timeoutMs = 8_000\n): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const socketPath = DEFAULT_SOCKET;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(err: Error | null, value?: Record<string, unknown>): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket.destroy();\n } catch {\n /* ignore */\n }\n if (err) reject(err);\n else resolve(value!);\n }\n\n const socket = connect(socketPath, () => {\n const request = {\n id: randomUUID(),\n sessionId: process.env.TERM_SESSION_ID ?? \"pai-cli\",\n method,\n params,\n };\n const itermId = process.env.ITERM_SESSION_ID;\n if (itermId) Object.assign(request, { itermSessionId: itermId });\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n\n let response: { ok: boolean; result?: Record<string, unknown>; error?: string };\n try {\n response = JSON.parse(line);\n } catch {\n finish(new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"AIBroker IPC call failed\"));\n } else {\n finish(null, response.result ?? {});\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(new Error(\"AIBroker not running (socket not found).\"));\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) finish(new Error(\"AIBroker IPC connection closed before response.\"));\n });\n\n timer = setTimeout(\n () => finish(new Error(\"AIBroker IPC call timed out.\")),\n timeoutMs\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Typed helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch all live iTerm2 session metadata from AIBroker via the `sessions` method.\n * Returns an empty array if AIBroker is not running.\n *\n * This is metadata-only (no scrollback). It is faster than `session_content`\n * and the correct source for listing/routing purposes.\n */\nexport async function fetchLiveSessions(): Promise<AiBrokerSessionMeta[]> {\n try {\n const result = await callAiBroker(\"sessions\", {});\n const sessions = (result as unknown as AiBrokerSessionsResult).sessions;\n if (!Array.isArray(sessions)) return [];\n return sessions;\n } catch {\n return [];\n }\n}\n\n/**\n * Fetch live sessions WITH the last user prompt extracted from terminal scrollback.\n *\n * Heavier than `fetchLiveSessions` (one IPC call returning full content for\n * every session), but yields a `lastPrompt` field useful for the unified\n * listing.\n */\nexport async function fetchLiveSessionsWithPrompts(): Promise<\n (AiBrokerSessionMeta & { lastPrompt?: string })[]\n> {\n // Get the basic metadata first (always works, fast).\n const metas = await fetchLiveSessions();\n if (metas.length === 0) return [];\n\n // Then enrich with last prompts via session_content (single call, all sessions).\n try {\n const contentResult = (await callAiBroker(\"session_content\", { lines: 60 })) as {\n sessions?: Array<{ sessionId: string; content?: string }>;\n };\n const contentMap = new Map<string, string>();\n for (const s of contentResult.sessions ?? []) {\n if (s.sessionId && typeof s.content === \"string\") {\n contentMap.set(s.sessionId, s.content);\n }\n }\n return metas.map((m) => {\n const content = contentMap.get(m.sessionId);\n const lastPrompt = content ? extractLastUserPrompt(content) : undefined;\n return { ...m, lastPrompt };\n });\n } catch {\n return metas.map((m) => ({ ...m }));\n }\n}\n\n/**\n * Extract the most-recent user prompt from a terminal scrollback string.\n *\n * Claude Code's TUI shows user input lines prefixed with `❯ `. We scan from\n * the bottom up, skipping the active input box (between the two horizontal\n * rule lines) and the statusline footer.\n */\nfunction extractLastUserPrompt(content: string): string | undefined {\n const lines = content.split(\"\\n\");\n // Walk bottom-up looking for the most recent user-typed line.\n // Claude Code marks user prompts with one of these prefixes:\n // ❯ <text> (current/recent prompt in the input box or scrollback)\n // > <text> (older variant)\n // The line MUST have non-empty content after the prompt symbol.\n // Skip the active input box (often empty `❯`) and statusline (👋 PAI CC...).\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n // Match ❯ or > (followed by space) then content\n const m = line.match(/^[❯>]\\s+(.+?)\\s*$/);\n if (!m) continue;\n let text = m[1].trim();\n if (!text) continue;\n // Skip lines that are just box-drawing or known UI noise\n if (/^[─━═]+/.test(text)) continue;\n if (text.startsWith(\"👋\")) continue;\n // Strip ANSI escape sequences that may leak in\n text = text.replace(/\\x1B\\[[0-9;]*[A-Za-z]/g, \"\").trim();\n if (text) return text;\n }\n return undefined;\n}\n\n/**\n * Send text to a specific AIBroker session by its iTerm2 sessionId.\n *\n * The wire keys are `target` and `message`, and neither is negotiable — the\n * handler rejects anything else with \"target is required\" before it looks at\n * the rest. This was written as `{ target: sessionId, message: text }`, which is the shape of\n * THIS function's own parameters rather than the shape of the IPC, so every\n * call failed identically. `pai pause all` was the only caller, so the live\n * path had never once executed: its --dry-run branch returns before sending,\n * and that was the only branch anyone had exercised.\n *\n * The caller must NOT terminate `text` with a newline. The transport sends with\n * `enter: true` and appends the Enter itself, so a trailing \\n submits twice —\n * once for the text and once for an empty prompt.\n */\nexport async function sendToSession(\n sessionId: string,\n text: string,\n timeoutMs = SEND_TIMEOUT_MS\n): Promise<{ ok: boolean; error?: string; timedOut?: boolean }> {\n try {\n await callAiBroker(\"send_to_session\", { target: sessionId, message: text }, timeoutMs);\n return { ok: true };\n } catch (e) {\n const error = String(e);\n // A timeout is NOT a delivery failure, and conflating the two is expensive.\n // The handler deposits into the target's mailbox BEFORE it waits for the\n // submit ack, so a send that times out has still been delivered — only the\n // confirmation is missing. Reported as a plain failure, the natural response\n // is to send again, which is how `pai pause all` produced nested\n // \"carried forward\" blocks in checkpoints that had already been written.\n return { ok: false, error, timedOut: /timed out/i.test(error) };\n }\n}\n\n/**\n * Switch iTerm2 focus to the session identified by `target`.\n *\n * `target` can be a sessionId, paiName, or tab index number (as string).\n * After switching, activates the iTerm2 application itself so the window\n * comes to the foreground.\n *\n * Returns { ok: true } if AIBroker confirmed the switch, or\n * { ok: false, error } if AIBroker is not running or the session was not found.\n */\nexport async function switchToSession(target: string): Promise<{ ok: boolean; error?: string }> {\n try {\n await callAiBroker(\"switch\", { target });\n // Bring iTerm2 itself to the foreground (the IPC only selects the tab)\n const { spawnSync } = await import(\"node:child_process\");\n spawnSync(\"osascript\", [\"-e\", 'tell application \"iTerm\" to activate'], {\n stdio: \"ignore\",\n });\n return { ok: true };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n\n/**\n * Bring the iTerm2 tab containing a specific session to the front.\n *\n * This is the mechanism AIBroker's screenshot path uses: match the session by\n * its iTerm2 session id (the `sessionId` returned by `fetchLiveSessions`, which\n * is iTerm's own `id of session`), then `select` its window, tab, and session.\n * Unlike the `switch` IPC (which only flips an internal index and never touches\n * iTerm), this actually reveals the tab.\n */\nexport function revealItermSession(itermSessionId: string): { ok: boolean; error?: string } {\n // Strip any \"iterm:\" style prefix, matching AIBroker's stripItermPrefix.\n const id = itermSessionId.replace(/^iterm:/i, \"\").trim();\n const script = `tell application \"iTerm2\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if id of s is \"${id}\" then\n select w\n select t\n select s\n return \"ok\"\n end if\n end repeat\n end repeat\n end repeat\n return \"not-found\"\nend tell`;\n try {\n const r = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n if (r.status !== 0) {\n return { ok: false, error: (r.stderr || \"osascript failed\").trim() };\n }\n if ((r.stdout ?? \"\").trim() === \"ok\") return { ok: true };\n return { ok: false, error: \"session not found in any iTerm2 window\" };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n","/**\n * Print the working directory of a `claude` session that pai launched, right\n * after it exits. This lets the user cd back into the same directory and\n * restart claude to continue — Claude Code's own `--resume` hint is unreliable\n * for this workflow.\n *\n * Why here and not a SessionEnd hook: a hook inside Claude Code is cancelled\n * during teardown (anthropics/claude-code#41577) and races the terminal\n * restore, so it can't reliably print anything. pai spawns the `claude` binary\n * directly (no shell), so the `claude()` shell wrapper never sees pai-launched\n * sessions either. Printing here — after spawnSync has returned and claude has\n * fully exited — is the one place that reliably reaches the terminal.\n */\nexport function printExitDir(dir: string): void {\n process.stdout.write(\n `\\n\\x1b[2m📂 Working directory:\\x1b[0m ${dir}\\n` +\n `\\x1b[2m cd \"${dir}\"\\x1b[0m\\n`,\n );\n}\n","/**\n * launch.ts — Launch Claude Code in a directory, in the CURRENT terminal.\n *\n * Shared by the interactive picker (pick.ts). Deliberately does NOT switch\n * iTerm tabs (aibroker_switch): switching jumps the user to a different — and\n * sometimes wrong — terminal, which is confusing. Picking a place should start\n * a session right here, in the chosen directory.\n *\n * Behaviour:\n * resume-or-fresh → if a resumable UUID is given, probe it; on success\n * `claude --resume`, otherwise fall back to a fresh session\n * in the same dir. With no UUID, start fresh.\n *\n * The `claude` child inherits the tty (stdio: \"inherit\"), so the session runs\n * in the terminal that launched `pai`. On exit we print the working directory.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport {\n realpathSync,\n existsSync,\n linkSync,\n copyFileSync,\n openSync,\n readSync,\n closeSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport { err } from \"../utils.js\";\nimport { printExitDir } from \"./exit-dir.js\";\n\nexport interface ProbeResult {\n ok: boolean;\n reason?: string;\n}\n\n/**\n * Put a transcript back where `claude --resume` looks for it.\n *\n * `claude --resume <uuid>` reads ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl\n * and ONLY that path. A copy under `sessions/` is invisible to it — measured\n * 2026-08-04, both directions:\n *\n * b3462801 867 KB, sessions/ only → \"No conversation found with session ID\"\n * a9ecdc1c top level → found\n *\n * Location is one of two factors, and this function addresses that one. PAI\n * displaced these files itself, from FOUR movers — a SessionStart hook, a\n * UserPromptSubmit hook, the stop hook, and the work-queue worker — of which\n * the UserPromptSubmit one did most of the damage, because it ran on every\n * prompt of every session and excluded only the caller's own transcript. All\n * four now hardlink (project-utils/paths.ts). So this restores a file PAI\n * displaced rather than inventing a layout Claude Code does not use.\n *\n * The other factor is content, and no amount of relinking helps there — see\n * `hasConversation`.\n *\n * A hard link is preferred over a copy: same inode, no second megabyte on disk,\n * and the archive under `sessions/` keeps working for everything that reads it.\n * Returns whether the top-level path exists afterwards.\n */\nexport function restoreTopLevel(uuid: string, dir: string): boolean {\n const topLevel = join(dir, `${uuid}.jsonl`);\n if (existsSync(topLevel)) return true;\n\n const archived = join(dir, \"sessions\", `${uuid}.jsonl`);\n if (!existsSync(archived)) return false;\n\n try {\n linkSync(archived, topLevel);\n return true;\n } catch {\n try {\n copyFileSync(archived, topLevel);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Does this transcript hold an exchange, or only session metadata?\n *\n * The second reason `claude --resume` says \"No conversation found\": the file is\n * there and readable and still holds no conversation. Measured 2026-08-04 on\n * 046bb712 — 537 bytes of last-prompt, custom-title, agent-name, mode and\n * permission-mode, no user line, no assistant line. It was restored to the top\n * level, verified same-inode, and still refused. It was never resumable, and\n * relinking cannot make it so. 30 of PAI's own 50 displaced transcripts are\n * this shape, one of them 745 KB — size proves nothing, because hook context\n * attachments are large.\n *\n * This scans the WHOLE file, and a bounded head will not do. It is tempting —\n * \"a real session's first assistant line lands within the first few KB\" — and\n * it is false. Measured on b3462801, a session `claude --resume` accepts:\n *\n * file size 866953\n * length of LINE 1 762977 <- one hook context attachment\n * first \"type\":\"user\" 766830\n *\n * The first exchange sits past 766 KB because line 1 is a single enormous\n * attachment blob. Any head shorter than that reports a 867 KB working session\n * as an empty stub, which is the exact false negative this function exists to\n * prevent — and it is the shape that produced today's whole incident.\n *\n * Chunked so that a large transcript costs a scan rather than a resident copy,\n * with an overlap so the marker cannot hide across a chunk boundary. Reading a\n * few MB to answer a question about resumability is cheap; being wrong is not.\n *\n * Unreadable counts as real. Claiming a session is empty when it cannot be\n * inspected would talk the caller out of a resume that might have worked.\n */\nconst CONVERSATION_MARKERS = ['\"type\":\"assistant\"', '\"type\": \"assistant\"', '\"type\":\"user\"', '\"type\": \"user\"'];\nconst OVERLAP = 32; // > longest marker, so none can straddle two chunks\n\nexport function hasConversation(path: string, chunkBytes = 1 << 20): boolean {\n let fd: number | undefined;\n try {\n fd = openSync(path, \"r\");\n const buf = Buffer.alloc(chunkBytes);\n let carry = \"\";\n let pos = 0;\n\n for (;;) {\n const read = readSync(fd, buf, 0, chunkBytes, pos);\n if (read <= 0) return false;\n pos += read;\n\n const text = carry + buf.subarray(0, read).toString(\"utf8\");\n if (CONVERSATION_MARKERS.some((m) => text.includes(m))) return true;\n carry = text.slice(-OVERLAP);\n }\n } catch {\n return true;\n } finally {\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n /* nothing useful to do */\n }\n }\n }\n}\n\n/**\n * Can this session be resumed — and if the only thing standing in the way is\n * where its transcript sits, put it back so that the answer is yes.\n *\n * Returns true/false when it can tell, and null when it cannot — the caller\n * must treat null as \"ask something else\", never as \"no\". A path this function\n * fails to recognise would otherwise silently veto a perfectly good resume.\n *\n * Claude Code stores transcripts at ~/.claude/projects/<encoded-cwd>/, where the\n * encoding replaces every non-alphanumeric character with `-`.\n */\nfunction transcriptOnDisk(\n uuid: string,\n cwd: string,\n home = homedir()\n): true | \"missing\" | \"stub\" | null {\n try {\n const encoded = cwd.replace(/[^a-zA-Z0-9]/g, \"-\");\n const dir = join(home, \".claude\", \"projects\", encoded);\n if (!existsSync(dir)) return null; // unknown layout — not evidence of absence\n if (!restoreTopLevel(uuid, dir)) return \"missing\";\n // Present is not the same as resumable. A metadata stub survives every\n // location check and still fails at `claude --resume`, and this probe is the\n // only thing standing between that and a caller that exits on the failure\n // instead of falling back to a fresh session.\n return hasConversation(join(dir, `${uuid}.jsonl`)) ? true : \"stub\";\n } catch {\n return null;\n }\n}\n\n/**\n * Probe whether a session UUID is resumable from `cwd`.\n *\n * The filesystem is asked first, and usually answers.\n *\n * This used to run `claude --resume <uuid> --print --output-format=json \"_\"`\n * with a 5s timeout, which is not a probe: it resumes the session AND sends a\n * prompt to the model, then waits for a complete JSON reply. That costs a model\n * round-trip per probe, and 5s is not enough time for one — for a LARGE session\n * least of all, because the transcript has to be loaded first.\n *\n * So the check failed precisely for the sessions most worth resuming, and the\n * caller's fallback quietly started a fresh session in their place. Observed\n * 2026-08-04: `pai Paperfull` reported `spawn error: spawnSync claude ETIMEDOUT`\n * for fb76a6c3 and started over, while\n * `~/.claude/projects/…-Paperfull/sessions/fb76a6c3-….jsonl` sat on disk the\n * whole time.\n *\n * A transcript on disk is what resumable MEANS, and reading a directory entry is\n * free. The spawn remains only for the case the filesystem cannot answer, and\n * now gets a timeout that a real answer can fit inside.\n *\n * \"On disk\" had to be tightened once. It first accepted a transcript sitting\n * only under `sessions/`, which claude --resume does not read — so the probe\n * swapped a 5s false negative for a confident false positive, and the caller\n * spawned a resume that died with \"No conversation found\" instead of falling\n * back to a fresh session. `restoreTopLevel` is what makes the permissive\n * reading true rather than merely optimistic.\n */\nexport function probeResume(uuid: string, cwd: string, home?: string): ProbeResult {\n const onDisk = transcriptOnDisk(uuid, cwd, home);\n if (onDisk === true) return { ok: true };\n if (onDisk === \"missing\") {\n return { ok: false, reason: \"No transcript on disk for this UUID\" };\n }\n if (onDisk === \"stub\") {\n // Say which of the two it is. \"No conversation found\" from claude tells the\n // user nothing about whether a repair might help; this does.\n return { ok: false, reason: \"Transcript holds only session metadata — no conversation to resume\" };\n }\n\n const result = spawnSync(\n \"claude\",\n [\"--resume\", uuid, \"--print\", \"--output-format=json\", \"_\"],\n { cwd, timeout: 30_000, env: process.env, stdio: [\"ignore\", \"ignore\", \"pipe\"] }\n );\n\n if (result.error) return { ok: false, reason: `spawn error: ${result.error.message}` };\n\n const stderr = result.stderr?.toString(\"utf8\") ?? \"\";\n if (\n stderr.toLowerCase().includes(\"no conversation found\") ||\n stderr.toLowerCase().includes(\"session not found\")\n ) {\n return { ok: false, reason: \"No conversation found for this UUID\" };\n }\n if (result.status !== 0) {\n return {\n ok: false,\n reason: `claude exited ${result.status ?? \"signal\"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : \"\"}`,\n };\n }\n return { ok: true };\n}\n\nexport interface LaunchOpts {\n /** If set, try to resume this session before falling back to fresh. */\n resumableUuid?: string;\n /** Skip the resume probe and start a brand-new session in the dir. */\n forceFresh?: boolean;\n /** Print what would happen, then return without launching. */\n dryRun?: boolean;\n}\n\n/**\n * Launch `claude` in `dir` in the current terminal. `name` is used for both the\n * Claude session label (--name) and the /Name slash command (tab/statusline).\n * Never returns on the live path — it exits the process after claude exits.\n */\nexport function launchInDir(dir: string, name: string, opts: LaunchOpts = {}): void {\n let cwd: string;\n try {\n cwd = realpathSync(dir);\n } catch {\n console.error(\n err(\n `Directory does not exist or cannot be resolved:\\n ${dir}\\n` +\n ` The folder may have moved or been deleted.`\n )\n );\n process.exitCode = 1;\n return;\n }\n\n const promptArg = `/Name ${name}\\ngo`;\n const wantResume = !opts.forceFresh && !!opts.resumableUuid;\n\n if (opts.dryRun) {\n if (wantResume) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would probe then exec (RESUME path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` probe: transcript on disk for ${opts.resumableUuid!.slice(0, 8)}?`);\n console.log(` argv: claude --resume ${opts.resumableUuid} --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n console.log(` fallback: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n } else {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (FRESH path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` argv: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n }\n console.log();\n return;\n }\n\n const fresh = () => {\n const result = spawnSync(\"claude\", [\"--name\", name, promptArg], {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n };\n\n if (wantResume) {\n const probe = probeResume(opts.resumableUuid!, cwd);\n if (probe.ok) {\n const result = spawnSync(\n \"claude\",\n [\"--resume\", opts.resumableUuid!, \"--name\", name, promptArg],\n { cwd, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n return;\n }\n process.stderr.write(\n chalk.yellow(\n `\\n Resume failed for ${opts.resumableUuid!.slice(0, 8)}: ${probe.reason ?? \"unknown error\"}\\n` +\n ` Starting fresh session in same directory.\\n\\n`\n )\n );\n fresh();\n return;\n }\n\n fresh();\n}\n","/**\n * history-search.ts\n *\n * Content-based search across ~/.claude/history.jsonl.\n * Streams the file (avoids loading 26k+ lines into memory), groups matching\n * lines by sessionId, and returns results sorted by most-recent match.\n *\n * Used by: pai <query> (main resolver) and pai find (compat alias)\n */\n\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HistoryEntry {\n display?: string;\n timestamp?: number;\n project?: string;\n sessionId?: string;\n pastedContents?: unknown;\n}\n\nexport interface SessionMatch {\n sessionId: string | null;\n /** Most-recent timestamp among matching lines */\n lastMatchTs: number;\n /** Most-recent matching prompt text */\n lastMatchDisplay: string;\n /** Project path (from the most-recent match) */\n project: string;\n /** Number of matching lines in this session */\n matchCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// History file location\n// ---------------------------------------------------------------------------\n\nexport const HISTORY_FILE = join(homedir(), \".claude\", \"history.jsonl\");\n\n// ---------------------------------------------------------------------------\n// Core search function (streaming)\n// ---------------------------------------------------------------------------\n\n/**\n * Search ~/.claude/history.jsonl for prompts matching the query (case-insensitive\n * substring). Results are grouped by sessionId and sorted by most-recent match.\n *\n * Entries without a sessionId (old Claude Code versions) are excluded from\n * results since they can't be resumed.\n */\nexport async function searchHistory(\n query: string,\n maxResults: number\n): Promise<SessionMatch[]> {\n if (!existsSync(HISTORY_FILE)) return [];\n\n const queryLower = query.toLowerCase();\n const bySession = new Map<string, SessionMatch>();\n\n const rl = createInterface({\n input: createReadStream(HISTORY_FILE, { encoding: \"utf8\" }),\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n const t = line.trim();\n if (!t) continue;\n\n let entry: HistoryEntry;\n try {\n entry = JSON.parse(t) as HistoryEntry;\n } catch {\n continue;\n }\n\n const display = entry.display ?? \"\";\n if (!display.toLowerCase().includes(queryLower)) continue;\n\n const ts = entry.timestamp ?? 0;\n const project = entry.project ?? \"\";\n const sessionId = entry.sessionId ?? null;\n\n // Skip entries with no sessionId — can't resume them\n if (!sessionId) continue;\n\n const existing = bySession.get(sessionId);\n if (!existing) {\n bySession.set(sessionId, {\n sessionId,\n lastMatchTs: ts,\n lastMatchDisplay: display,\n project,\n matchCount: 1,\n });\n } else {\n existing.matchCount++;\n if (ts > existing.lastMatchTs) {\n existing.lastMatchTs = ts;\n existing.lastMatchDisplay = display;\n existing.project = project;\n }\n }\n }\n\n // Sort by most-recent match descending\n const sorted = [...bySession.values()].sort(\n (a, b) => b.lastMatchTs - a.lastMatchTs\n );\n\n return sorted.slice(0, maxResults);\n}\n","/**\n * dedup-sessions.ts — Shared dedup catalog for `pai` and listing commands.\n *\n * Both `pai` (no args) and any listing command must show IDENTICAL output.\n * This module is the single source of truth for:\n * - Name normalization (strips spinner prefixes and \" (node)\" suffixes)\n * - Dedup grouping (one row per normalized name)\n * - Priority ranking (live > resumable > transcript-only > stub > project > orphan)\n * - Registered projects (shown as status \"idle\" when no live/disk session matches)\n * - Filtering (hide 0-session / cold / archived projects by default)\n * - Table rendering (shared columns and formatting)\n *\n * Default visibility rules (overridden by showAll=true):\n * SHOW: live sessions (always)\n * SHOW: resumable sessions (always)\n * SHOW: registered projects with session_count > 0 AND last_active within 90 days\n * OR session_count >= 3 (i.e. well-used projects regardless of age)\n * HIDE: projects with session_count = 0 (never used)\n * HIDE: projects with last_active > 90 days AND session_count < 3 (cold)\n * HIDE: archived projects (status != 'active')\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport chalk from \"chalk\";\nimport { renderTable, dim, header, warn } from \"../utils.js\";\nimport { fmtAge, type ScannedSession } from \"./session-scan.js\";\nimport { type AiBrokerSessionMeta } from \"./aibroker-client.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type UnifiedStatus =\n | \"live\"\n | \"resumable\"\n | \"transcript-only\"\n | \"stub\"\n | \"project\"\n | \"orphan\";\n\nexport const STATUS_PRIORITY: Record<UnifiedStatus, number> = {\n live: 0,\n resumable: 1,\n \"transcript-only\": 2,\n stub: 3,\n project: 4,\n orphan: 5,\n};\n\nexport interface UnifiedSession {\n /** Clean display name (normalized — no spinners, no \"(node)\") */\n name: string;\n /** Slug from the project registry (for slug-based matching in `pai <name>`) */\n slug?: string;\n status: UnifiedStatus;\n /** Only present for live sessions */\n liveSessionId?: string;\n /** Only present for disk sessions */\n diskSession?: ScannedSession;\n lastActivity: number;\n project: string;\n lastPrompt: string;\n /** For project-source entries: number of historical sessions ever */\n sessionCount?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Name normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Strip Claude Code spinner characters, decoration prefixes, and \" (node)\" suffixes\n * from a session name so that \"✳ Chenarlier (node)\" and \"Chenarlier\" group together.\n *\n * Handles:\n * - Unicode braille spinner frames: ⠐ ⠂ ⠁ ⠈ ⠘ ⠙ ⠚ ⠛ ⠗ ⠝\n * - Decoration characters: ✻ ✳ ✲ ✼ ✦ * ★ ☆ • ●\n * - Any leading non-letter/digit characters followed by whitespace\n * - Trailing \" (node)\" suffix (Claude Code appends this on some platforms)\n */\nexport function normalizeName(s: string): string {\n return s\n // Strip leading spinner / decoration characters (braille + symbols)\n .replace(/^[⠀-⣿✀-➿✻✳✲✼✦*★☆•●⠐⠂⠁⠈⠘⠙⠚⠛⠗⠝]+\\s*/u, \"\")\n // Strip any remaining leading non-alphanumeric prefix before whitespace\n .replace(/^[^a-zA-Z0-9À-ÿ -鿿]+\\s*/u, \"\")\n // Strip trailing \" (node)\" suffix\n .replace(/\\s*\\(node\\)\\s*$/i, \"\")\n .trim();\n}\n\n/**\n * Normalize a slug (kebab-case) to a human-readable form for matching.\n * \"jobs-beta\" → \"jobs beta\"\n */\nfunction slugToWords(slug: string): string {\n return slug.replace(/-/g, \" \");\n}\n\n// ---------------------------------------------------------------------------\n// Dedup builder\n// ---------------------------------------------------------------------------\n\nexport interface RegisteredProject {\n slug: string;\n display_name: string;\n root_path: string;\n session_count: number;\n last_active: number | null;\n status: string;\n}\n\n/**\n * Merge live (AIBroker) + disk (session-scan) + registry (projects DB) sessions\n * into a deduped catalog.\n *\n * Algorithm:\n * 1. Normalize names before grouping\n * 2. Group by normalized name (case-insensitive)\n * 3. Within each group, rank by STATUS_PRIORITY (live wins)\n * 4. Within same priority, keep the most recent (highest mtime)\n * 5. Sort output: by priority, then by lastActivity desc\n * 6. Filter idle project entries (default) or show all (showAll=true)\n *\n * @param showAll When true, include cold / zero-session / archived projects.\n */\n/**\n * Which of two sessions of equal status should represent a name.\n *\n * Recency alone was the rule, and recency is exactly the wrong instinct here.\n * A resume that FAILS still creates a session file, and that file is newer than\n * the transcript it failed to open. So the tie-break reliably handed back the\n * artefact of a bug in preference to the work.\n *\n * Measured on Paperfull, 2026-08-04:\n *\n * b3462801 867 KB 32 lines 3 user 15:41:57 <- the actual work\n * 7fdbb9a8 82 KB 25 lines 2 user 15:41:59 <- empty, Ctrl-C'd, WON by 2s\n * a9ecdc1c 82 KB 25 lines 2 user 15:49:40 <- a second failed resume\n *\n * `pai Paperfull` therefore resolved to 7fdbb9a8 — 82 KB of hook context in\n * `attachment` lines, with no assistant turn anywhere in it — while 867 KB of\n * real work sat one second older and unchosen.\n *\n * WHAT THIS DOES NOT FIX, measured before claiming otherwise: the reported\n * failure was `No conversation found with session ID: 7fdbb9a8`, and that error\n * is about WHERE the transcript is, not what is in it. Both transcripts live\n * only in sessions/, and `claude --resume b3462801` — the good one, the one this\n * change now selects — fails with the identical message. Verified directly, and\n * again on a second project. So this stops dedup preferring the artefact of a\n * bug, which is worth doing on its own, and `pai <Name>` still cannot resume a\n * cleanly-stopped session until the sessions/ relocation is addressed. That part\n * is tracked in Notes/TODO.md and belongs to session-scan/the stop hook.\n *\n * Conversation volume comes first because an empty artefact is small BY\n * CONSTRUCTION — nobody spoke into it — whereas an artefact can be newer by\n * mere milliseconds. Recency only decides between transcripts that recorded the\n * same amount, where it is a reasonable guess again.\n *\n * HONEST LIMIT: the exact signal is \"did the assistant ever reply\", and this is\n * not that — it is a proxy that happens to separate the measured case by one\n * user line and seven total lines, which is a thin margin. The real predicate\n * belongs in `parseTranscript` (session-scan.ts) as an `assistantLines` count,\n * which is free there since it already reads every line of the file. That file\n * was mid-edit in another session when this landed, so this stays out of it.\n * Move to the exact signal when it exists; the tests below should keep passing\n * unchanged, since they assert the outcome and not the proxy.\n */\nfunction winsTiebreak(candidate: UnifiedSession, incumbent: UnifiedSession): boolean {\n const c = candidate.diskSession;\n const i = incumbent.diskSession;\n\n // A live entry has no transcript to weigh. It also never reaches here except\n // against another live entry, where recency is all there is.\n if (c && i) {\n if (c.userLines !== i.userLines) return c.userLines > i.userLines;\n if (c.msgCount !== i.msgCount) return c.msgCount > i.msgCount;\n }\n\n return candidate.lastActivity > incumbent.lastActivity;\n}\n\nexport function buildDeduped(\n liveSessions: AiBrokerSessionMeta[],\n diskSessions: ScannedSession[],\n registeredProjects?: RegisteredProject[],\n showAll = false\n): UnifiedSession[] {\n const byName = new Map<string, UnifiedSession>();\n\n // Process live Claude sessions\n for (const s of liveSessions) {\n if (s.kind === \"shell\") continue;\n // NOT the iTerm tab title.\n //\n // `s.name` is whatever iTerm is calling the tab right now, which tracks the\n // foreground process and the window title escape codes. Using it as a\n // session IDENTITY means any tab that happens to be titled like a project\n // becomes a \"live session\" of that name — and because live entries win over\n // project entries in the merge below, `pai <Project>` then SWITCHES to that\n // tab instead of launching the project. Reported 2026-08-04: `pai PAI`\n // printing \"Switched to live session: pai\" and opening nothing.\n //\n // A session's identity is the name PAI gave it. A session that has none is\n // listed by its id and matches nothing by name, which is the safe answer:\n // failing to find it launches a fresh one, whereas matching it wrongly\n // hands the user someone else's terminal.\n const raw = s.paiName ?? s.sessionId.slice(0, 8);\n const normalized = normalizeName(raw);\n const key = normalized.toLowerCase();\n\n const entry: UnifiedSession = {\n name: normalized,\n status: \"live\",\n liveSessionId: s.sessionId,\n lastActivity: Date.now(),\n project: \"\",\n lastPrompt: s.lastPrompt ?? \"\",\n };\n\n const existing = byName.get(key);\n if (!existing || STATUS_PRIORITY[\"live\"] < STATUS_PRIORITY[existing.status]) {\n byName.set(key, entry);\n }\n }\n\n // Process disk sessions\n for (const s of diskSessions) {\n const raw = s.friendlyName ?? s.shortId;\n const normalized = normalizeName(raw);\n const key = normalized.toLowerCase();\n\n let status: UnifiedStatus;\n if (s.resumable) {\n status = \"resumable\";\n } else if (s.sessionStatus === \"transcript-only\") {\n status = \"transcript-only\";\n } else if (s.sessionStatus === \"stub\") {\n status = \"stub\";\n } else {\n status = \"orphan\";\n }\n\n const entry: UnifiedSession = {\n name: normalized,\n status,\n diskSession: s,\n lastActivity: s.mtime,\n project: s.decodedPath ?? \"\",\n lastPrompt: s.lastUserPrompt ?? \"\",\n };\n\n const existing = byName.get(key);\n if (!existing) {\n byName.set(key, entry);\n } else {\n const ep = STATUS_PRIORITY[existing.status];\n const np = STATUS_PRIORITY[status];\n if (np < ep || (np === ep && winsTiebreak(entry, existing))) {\n byName.set(key, entry);\n }\n }\n }\n\n // Constants for idle-project filtering\n const COLD_DAYS = 90;\n const COLD_THRESHOLD_MS = COLD_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n\n // Process registered projects — add any not already covered by live/disk entries\n if (registeredProjects) {\n for (const p of registeredProjects) {\n // Skip archived projects unless showAll\n if (!showAll && p.status !== \"active\") continue;\n\n const displayName = p.display_name ?? p.slug;\n const normalized = normalizeName(displayName);\n const key = normalized.toLowerCase();\n\n // Also check if slug matches (via slug→words form)\n const slugKey = slugToWords(p.slug).toLowerCase();\n\n if (!byName.has(key) && !byName.has(slugKey)) {\n // Apply idle-project filter unless showAll:\n // - Hide projects with 0 sessions (never used)\n // - Hide projects cold AND low usage: last_active > 90 days AND sessions < 3\n if (!showAll) {\n if (p.session_count === 0) continue;\n if (p.session_count < 3) {\n const lastActive = p.last_active ?? 0;\n if (now - lastActive > COLD_THRESHOLD_MS) continue;\n }\n }\n\n byName.set(key, {\n name: normalized,\n slug: p.slug,\n status: \"project\",\n lastActivity: p.last_active ?? 0,\n project: p.root_path,\n lastPrompt: \"\",\n sessionCount: p.session_count,\n });\n } else {\n // Already have an entry — enrich it with the slug for matching\n const existing = byName.get(key) ?? byName.get(slugKey);\n if (existing && !existing.slug) {\n existing.slug = p.slug;\n }\n if (existing && existing.sessionCount === undefined) {\n existing.sessionCount = p.session_count;\n }\n }\n }\n }\n\n // Sort: by priority first, then by lastActivity desc\n const entries = Array.from(byName.values());\n entries.sort((a, b) => {\n const pa = STATUS_PRIORITY[a.status];\n const pb = STATUS_PRIORITY[b.status];\n if (pa !== pb) return pa - pb;\n return b.lastActivity - a.lastActivity;\n });\n\n return entries;\n}\n\n// ---------------------------------------------------------------------------\n// Status formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtUnifiedStatus(s: UnifiedStatus): string {\n switch (s) {\n case \"live\": return chalk.green(\"live\");\n case \"resumable\": return chalk.cyan(\"resumable\");\n case \"transcript-only\": return chalk.dim(\"transcript\");\n case \"stub\": return chalk.dim(\"stub\");\n case \"project\": return chalk.dim(\"idle\");\n case \"orphan\": return chalk.dim(\"orphan\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared table renderer\n// ---------------------------------------------------------------------------\n\nfunction shortenProject(p: string, maxLen = 28): string {\n if (!p || p.length <= maxLen) return p || dim(\"—\");\n return \"…\" + p.slice(-(maxLen - 1));\n}\n\n/**\n * Render the deduped session catalog to stdout.\n *\n * @param entries Output of buildDeduped()\n * @param maxRows Maximum rows to display. undefined = no limit (for --all).\n */\nexport function renderDedupedSessions(entries: UnifiedSession[], maxRows?: number): void {\n if (entries.length === 0) {\n console.log(warn(\"No sessions found. Start Claude Code in a project directory first.\"));\n return;\n }\n\n console.log(\"\\n\" + header(\"Sessions\") + \"\\n\");\n\n const visible = maxRows !== undefined ? entries.slice(0, maxRows) : entries;\n const tableHeaders = [\"#\", \"name\", \"status\", \"age\", \"project\", \"last prompt\"];\n const tableRows = visible.map((entry, i) => {\n const age =\n entry.status === \"live\"\n ? chalk.green(\"now\")\n : entry.lastActivity > 0\n ? dim(fmtAge(entry.lastActivity))\n : dim(\"—\");\n const snippet = entry.lastPrompt.replace(/\\n+/g, \" \").trim().slice(0, 36);\n const projectPath =\n entry.diskSession\n ? entry.diskSession.friendlyName ?? entry.diskSession.decodedPath\n : entry.project;\n const project = dim(shortenProject(projectPath, 28));\n return [\n dim(String(i + 1)),\n chalk.white(entry.name),\n fmtUnifiedStatus(entry.status),\n age,\n project,\n chalk.dim(snippet ? `\"${snippet}\"` : \"—\"),\n ];\n });\n\n console.log(renderTable(tableHeaders, tableRows));\n\n if (maxRows !== undefined && entries.length > maxRows) {\n console.log(dim(` … ${entries.length - maxRows} more — use --all to show everything`));\n }\n\n console.log();\n console.log(\n dim(\" Switch/resume/start: \") +\n chalk.white(\"pai <name>\") +\n dim(\" or \") +\n chalk.white(\"pai <uuid-prefix>\")\n );\n console.log();\n}\n","/**\n * main-resolver.ts\n *\n * `pai [<query>]` — topic-first session discovery and launcher (v0.11.1)\n *\n * Decision tree:\n * 1. No arg → show deduped session listing (one row per name)\n * 2. UUID prefix → universal filesystem scan; auto-launch the match\n * 3. Any string:\n * a. Live match (by normalized paiName) → aibroker_switch → iTerm tab to front. Done.\n * b. Resumable match → probe + claude --resume <uuid>\n * c. Transcript/stub match → fresh claude in same project dir\n * d. No name match → free-text history search → picker\n *\n * Dedup + name normalization logic: src/cli/lib/dedup-sessions.ts (shared with listing).\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { createInterface } from \"node:readline\";\nimport { existsSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\nimport chalk from \"chalk\";\nimport { err, dim, warn, ok, header, renderTable } from \"../utils.js\";\nimport {\n scanSessions,\n fmtAge,\n type ScannedSession,\n} from \"../lib/session-scan.js\";\nimport { searchHistory, HISTORY_FILE, type SessionMatch } from \"../lib/history-search.js\";\nimport { fetchLiveSessions, fetchLiveSessionsWithPrompts, switchToSession } from \"../lib/aibroker-client.js\";\nimport { basename } from \"node:path\";\nimport { printExitDir } from \"../lib/exit-dir.js\";\nimport { probeResume, launchInDir } from \"../lib/launch.js\";\nimport {\n buildDeduped,\n normalizeName,\n STATUS_PRIORITY,\n fmtUnifiedStatus,\n renderDedupedSessions,\n type UnifiedSession,\n type RegisteredProject,\n} from \"../lib/dedup-sessions.js\";\n\n// ---------------------------------------------------------------------------\n// Launch session (disk-based)\n// ---------------------------------------------------------------------------\n\n/**\n * Which directory should we open for this session?\n *\n * A session carries up to three ideas of where it lives, they disagree in\n * practice, and the FIRST is the least trustworthy. `clcDirectory` and\n * `decodedPath` are recorded per session and go stale the moment a project\n * directory is renamed; `registryRootPath` is kept current by the registry.\n *\n * This used to take the preferred value and, if it did not resolve, print an\n * error and exit — so a live project became unopenable by its own name. The\n * session had been started when its directory had a different name, so the\n * stale per-session value pointed at a path that no longer existed while the\n * registry held the right one, untried.\n *\n * The order is unchanged, because the per-session value IS more specific when\n * valid — it can name a subdirectory the session actually ran in. What changed\n * is that a candidate which fails no longer ends the attempt.\n *\n * realpath, not resolve: a directory reached through a symlinked parent has\n * several valid spellings, and comparing or storing the unresolved one is how\n * the same directory ends up with two identities.\n *\n * Returns the resolved directory plus every candidate tried, so a total failure\n * can report what it looked at instead of naming one path and leaving the user\n * to guess whether the others were even considered.\n */\nexport function resolveSessionDir(session: {\n clcDirectory?: string;\n registryRootPath?: string;\n decodedPath?: string;\n}): { dir: string | undefined; tried: string[] } {\n const tried = [\n session.clcDirectory,\n session.registryRootPath,\n session.decodedPath,\n ].filter((d): d is string => typeof d === \"string\" && d.length > 0);\n\n for (const candidate of tried) {\n try {\n return { dir: realpathSync(candidate), tried };\n } catch {\n // Stale, renamed or deleted — try the next one.\n }\n }\n return { dir: undefined, tried };\n}\n\nfunction launchSession(\n session: ScannedSession,\n allSessions: ScannedSession[],\n dryRun: boolean\n): boolean {\n let resumableUuid: string | undefined;\n\n if (session.resumable) {\n resumableUuid = session.uuid;\n } else if (session.encodedDir) {\n const sameProject = allSessions.filter(\n (s) => s.encodedDir === session.encodedDir && s.resumable\n );\n sameProject.sort((a, b) => b.mtime - a.mtime);\n if (sameProject.length > 0) {\n resumableUuid = sameProject[0].uuid;\n }\n }\n\n const { dir: projectDir } = resolveSessionDir(session);\n\n // A dead session must not end the whole attempt.\n //\n // This used to print an error and process.exit(1). But the caller has more\n // candidates after this one — most importantly the registered PROJECT, whose\n // path the registry keeps current — and `openMatch` already knows how to open\n // a directory directly. Exiting here meant one stale transcript made a live\n // project unreachable by name: the user asks for a name they can see in the\n // listing, and gets a dead path instead of a session in the directory that\n // plainly exists.\n //\n // Returning false lets resolution continue. If nothing at all can be opened,\n // the caller reports that once, with everything it tried.\n if (projectDir === undefined) return false;\n\n // A session is named for a human to recognise, so a UUID prefix is never an\n // acceptable answer. `friendlyName` is undefined for the sessions synthesized\n // from a prompt-history match, and picking one of those used to open a tab\n // called \"7fdbb9a8\" — for a project plainly called Paperfull, sitting right\n // there in the path we already resolved. The directory is the better fallback\n // in every case, so shortId is now only reached for a path with no basename.\n const name = session.friendlyName ?? (basename(projectDir) || session.shortId);\n const promptArg = `/Name ${name}\\ngo`;\n\n if (dryRun) {\n if (resumableUuid) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would probe then exec (RESUME path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(projectDir)}`);\n console.log(` probe: transcript on disk for ${resumableUuid.slice(0, 8)}?`);\n console.log(` argv: claude --resume ${resumableUuid} --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n console.log(` fallback: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n } else {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (FRESH path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(projectDir)}`);\n console.log(` argv: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n }\n console.log();\n return true;\n }\n\n if (resumableUuid) {\n const probe = probeResume(resumableUuid, projectDir);\n if (probe.ok) {\n const result = spawnSync(\n \"claude\",\n [\"--resume\", resumableUuid, \"--name\", name, promptArg],\n { cwd: projectDir, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exit(1);\n }\n printExitDir(projectDir);\n process.exit(result.status ?? 0);\n } else {\n process.stderr.write(\n chalk.yellow(\n `\\n Resume failed for ${resumableUuid.slice(0, 8)}: ${probe.reason ?? \"unknown error\"}\\n` +\n ` Starting fresh session in same directory.\\n\\n`\n )\n );\n const result = spawnSync(\n \"claude\",\n [\"--name\", name, promptArg],\n { cwd: projectDir, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exit(1);\n }\n printExitDir(projectDir);\n process.exit(result.status ?? 0);\n }\n } else {\n const result = spawnSync(\n \"claude\",\n [\"--name\", name, promptArg],\n { cwd: projectDir, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exit(1);\n }\n printExitDir(projectDir);\n process.exit(result.status ?? 0);\n }\n}\n\n// ---------------------------------------------------------------------------\n// History match → ScannedSession bridge\n// ---------------------------------------------------------------------------\n\nfunction matchToSession(\n match: SessionMatch,\n allSessions: ScannedSession[]\n): ScannedSession | null {\n if (!match.sessionId) return null;\n\n const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);\n if (catalogMatch) return catalogMatch;\n\n if (!match.project) return null;\n\n return {\n uuid: match.sessionId,\n shortId: match.sessionId.slice(0, 8),\n encodedDir: \"\",\n decodedPath: match.project,\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: undefined,\n userLines: 0,\n lastUserPrompt: match.lastMatchDisplay.slice(0, 80),\n msgCount: 0,\n mtime: match.lastMatchTs,\n friendlyName: undefined,\n clcDirectory: undefined,\n registryRootPath: match.project,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatters\n// ---------------------------------------------------------------------------\n\nfunction fmtTs(ts: number): string {\n const d = new Date(ts);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +\n `${pad(d.getHours())}:${pad(d.getMinutes())}`\n );\n}\n\nfunction shortenProject(p: string, maxLen = 44): string {\n if (!p || p.length <= maxLen) return p || dim(\"—\");\n return \"…\" + p.slice(-(maxLen - 1));\n}\n\n// ---------------------------------------------------------------------------\n// Interactive picker prompt\n// ---------------------------------------------------------------------------\n\nasync function askForChoice(max: number): Promise<number | null> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(\n dim(`\\n Enter # to launch (1-${max}), or press Enter to cancel: `),\n (answer) => {\n rl.close();\n const n = parseInt(answer.trim(), 10);\n if (!isNaN(n) && n >= 1 && n <= max) {\n resolve(n);\n } else {\n resolve(null);\n }\n }\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Shared switch helper (live session → iTerm tab)\n// ---------------------------------------------------------------------------\n\nasync function doSwitch(\n entry: UnifiedSession,\n dryRun: boolean\n): Promise<boolean> {\n if (!entry.liveSessionId) return false;\n if (dryRun) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would switch iTerm tab:\") + \"\\n\");\n console.log(` target: ${entry.name} (${entry.liveSessionId.slice(0, 8)})`);\n console.log(` action: aibroker_switch + osascript iTerm activate`);\n console.log();\n return true;\n }\n const result = await switchToSession(entry.liveSessionId);\n if (result.ok) {\n // Name AND id. \"Switched to live session: pai\" was reported three times on\n // 2026-08-04 by a user watching nothing happen, and the message gave no way\n // to tell WHICH session had been revealed — so there was nothing to check\n // against the tabs actually on screen. The id makes the claim falsifiable.\n console.log(\n ok(\n `Switched to live session: ${chalk.white(entry.name)} ` +\n chalk.dim(`(${entry.liveSessionId.slice(0, 8)})`),\n ),\n );\n return true;\n }\n console.error(warn(`Could not switch via AIBroker: ${result.error ?? \"unknown error\"}`));\n return false;\n}\n\n/**\n * Act on a matched catalog entry: switch to it, resume it, or start it.\n *\n * The third case is the one that was missing. A registered project whose\n * sessions have all ended matches by name perfectly well, but carries no\n * `diskSession`, and the old code simply ran off the end of both the exact and\n * the partial branch — past the picker, into a free-text search of prompt\n * history. So `pai Paperfull` answered a request to open a project with a list\n * of five old conversations that merely mentioned the word, while the project's\n * own directory was sitting in `entry.project` the whole time.\n *\n * Returns false only when there is genuinely nothing to open, which is the one\n * case where falling through to a history search is the right thing to do.\n */\nasync function openMatch(\n entry: UnifiedSession,\n allSessions: ScannedSession[],\n dryRun: boolean\n): Promise<boolean> {\n if (entry.status === \"live\") {\n if (await doSwitch(entry, dryRun)) return true;\n // Switch failed — fall through and try to open it from disk instead.\n }\n // A transcript whose directory is gone is not an answer. Fall through to the\n // registered project below, whose path the registry keeps current — that is\n // the difference between \"your project moved\" and \"cannot open anything\".\n if (entry.diskSession && launchSession(entry.diskSession, allSessions, dryRun)) {\n return true;\n }\n if (entry.project && existsSync(entry.project)) {\n launchInDir(entry.project, entry.name, { dryRun });\n return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Main command\n// ---------------------------------------------------------------------------\n\nexport interface MainResolverOpts {\n auto?: boolean; // -y / --auto: pick #1 without prompting\n dryRun?: boolean; // --dry-run: show what would happen\n n?: string; // --n <count>: max candidates for history search\n all?: boolean; // --all: show cold / 0-session / archived projects too\n}\n\nfunction getRegisteredProjects(db: Database, all = false): RegisteredProject[] {\n try {\n const statusClause = all ? \"\" : \"WHERE p.status = 'active'\";\n return db\n .prepare(`\n SELECT\n p.slug,\n p.display_name,\n p.root_path,\n p.status,\n COUNT(s.id) AS session_count,\n MAX(s.created_at) AS last_active\n FROM projects p\n LEFT JOIN sessions s ON s.project_id = p.id\n ${statusClause}\n GROUP BY p.id\n ORDER BY last_active DESC NULLS LAST, p.updated_at DESC\n `)\n .all() as RegisteredProject[];\n } catch {\n return [];\n }\n}\n\nexport async function cmdMain(\n db: Database,\n query: string | undefined,\n pickN: number | undefined,\n opts: MainResolverOpts\n): Promise<void> {\n const maxResults = parseInt(opts.n ?? \"20\", 10);\n const showAll = opts.all ?? false;\n // Live sessions: metadata-only fetch (1s) — last-prompt fetch is too slow\n // (~8s for AppleScript per-session scrollback). For live entries, last-prompt\n // stays empty; user can `pai <name>` to switch into the tab and see context.\n const livePromise = !query\n ? fetchLiveSessions().catch(() => [] as Awaited<ReturnType<typeof fetchLiveSessions>>)\n : Promise.resolve([]);\n const allSessions = scanSessions(db, { limit: 500, filter: \"named\" });\n const registeredProjects = getRegisteredProjects(db, showAll);\n\n // -----------------------------------------------------------------------\n // Case 1: No query → deduped session listing (shared renderer)\n // -----------------------------------------------------------------------\n if (!query) {\n const liveSessions = await livePromise;\n const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n renderDedupedSessions(deduped, showAll ? undefined : maxResults);\n return;\n }\n\n // -----------------------------------------------------------------------\n // Case 2: UUID prefix (8+ hex chars) → universal filesystem scan\n // -----------------------------------------------------------------------\n const UUID_PREFIX_RE = /^[0-9a-f-]{8,36}$/i;\n if (UUID_PREFIX_RE.test(query)) {\n const byUuid = allSessions.filter((s) => s.uuid.startsWith(query.toLowerCase()));\n if (byUuid.length === 1) {\n launchSession(byUuid[0], allSessions, opts.dryRun ?? false);\n return;\n }\n if (byUuid.length > 1) {\n console.error(err(`UUID prefix \"${query}\" is ambiguous — ${byUuid.length} catalog matches.`));\n process.exitCode = 1;\n return;\n }\n // Fall through to name match / history search\n }\n\n // -----------------------------------------------------------------------\n // Case 3: Name match against deduped catalog (normalized + slug matching)\n // -----------------------------------------------------------------------\n {\n let liveSessions: Awaited<ReturnType<typeof fetchLiveSessions>> = [];\n try {\n liveSessions = await fetchLiveSessions();\n } catch {\n // AIBroker not running\n }\n\n const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n // Normalize the query the same way we normalize session names\n // Also support slug form: \"jobs-beta\" → \"jobs beta\" for matching\n const qNorm = normalizeName(query).toLowerCase();\n const qSlug = query.toLowerCase().replace(/\\s+/g, \"-\"); // words → slug form for slug lookup\n\n // Match helper: checks normalized display name AND slug\n const nameMatches = (e: UnifiedSession, q: string) =>\n e.name.toLowerCase() === q ||\n (e.slug !== undefined && e.slug.toLowerCase() === qSlug);\n const nameIncludes = (e: UnifiedSession, q: string) =>\n e.name.toLowerCase().includes(q) ||\n (e.slug !== undefined && e.slug.toLowerCase().includes(qSlug));\n\n // Among equally-good matches, the most recently active one is what the user\n // means. This used `deduped.find()`, which took whichever entry happened to\n // come first — so a two-month-old note whose title matched EXACTLY beat the\n // live project the user was actually working in, whose sessions carried an\n // extra word in their names. Typing the project's own name opened its oldest\n // session.\n const newestFirst = (a: UnifiedSession, b: UnifiedSession) =>\n b.lastActivity - a.lastActivity;\n\n // Exact normalized-name match first (display_name or slug), newest wins.\n //\n // Try EVERY exact match, not just the best one. They are ranked by recency,\n // but the top-ranked entry can be unopenable — a transcript pointing at a\n // directory that has since been renamed — while a lower-ranked entry for the\n // same name is the live project sitting right there. Stopping at the first\n // candidate turned \"this name is ambiguous\" into \"this name is broken\".\n const exactMatches = deduped.filter((e) => nameMatches(e, qNorm)).sort(newestFirst);\n for (const match of exactMatches) {\n if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n }\n\n // Partial normalized-name match (display_name or slug)\n let partialMatches = deduped.filter((e) => nameIncludes(e, qNorm));\n\n // Substring matching misses the common case where every query word is\n // present but not adjacent: \"foo baz\" is not a substring of \"foo bar baz\",\n // so a project whose directory gained a word became unreachable by the name\n // its owner thinks of it by. Fall back to requiring every query WORD to\n // appear somewhere in the name or slug. That stays narrow — a sibling\n // project sharing only the first word still will not match, because it\n // lacks the second.\n if (partialMatches.length === 0) {\n const qWords = qNorm.split(/\\s+/).filter(Boolean);\n if (qWords.length > 1) {\n partialMatches = deduped.filter((e) => {\n const haystack = `${e.name} ${e.slug ?? \"\"}`.toLowerCase();\n return qWords.every((w) => haystack.includes(w));\n });\n }\n }\n\n partialMatches.sort(newestFirst);\n\n if (partialMatches.length === 1) {\n if (await openMatch(partialMatches[0], allSessions, opts.dryRun ?? false)) return;\n }\n\n if (partialMatches.length > 1) {\n // Multiple matches — show picker\n console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n const headers = [\"#\", \"name\", \"status\", \"age\", \"project\"];\n const rows = partialMatches.slice(0, maxResults).map((entry, i) => {\n const age =\n entry.status === \"live\"\n ? chalk.green(\"now\")\n : dim(fmtAge(entry.lastActivity));\n const project = entry.diskSession\n ? dim(shortenProject(entry.diskSession.decodedPath, 36))\n : dim(\"—\");\n return [\n dim(String(i + 1)),\n chalk.white(entry.name),\n fmtUnifiedStatus(entry.status),\n age,\n project,\n ];\n });\n console.log(renderTable(headers, rows));\n console.log();\n\n const pickMatch = async (match: UnifiedSession) => {\n if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n console.error(err(`Nothing to open for \"${match.name}\" — no live session, no transcript, no directory.`));\n process.exitCode = 1;\n };\n\n if (pickN !== undefined) {\n const idx = pickN - 1;\n if (idx >= 0 && idx < partialMatches.length) {\n await pickMatch(partialMatches[idx]);\n return;\n }\n console.error(err(`Invalid choice: ${pickN}`));\n process.exitCode = 1;\n return;\n }\n\n if (opts.auto) {\n await pickMatch(partialMatches[0]);\n return;\n }\n\n const choice = await askForChoice(Math.min(partialMatches.length, maxResults));\n if (choice !== null) {\n await pickMatch(partialMatches[choice - 1]);\n }\n return;\n }\n }\n\n // -----------------------------------------------------------------------\n // Case 3b: Registered project by name → open its directory\n // -----------------------------------------------------------------------\n // If a project is registered under this name and its directory is there, that\n // is the answer, full stop. Naming a project should start a session in it.\n //\n // The deduped catalog cannot be relied on for this: it keeps ONE entry per\n // project and prefers a transcript over the registry row, so when the\n // transcript's recorded directory has gone stale the surviving entry carries a\n // dead path and the live registry value is never consulted. The catalog is\n // built for listing, where showing the session is right; it is the wrong\n // source for \"where do I open this\".\n //\n // So ask the registry directly, before falling back to scraping prompt\n // history — which is a search feature, not a way to open a known project.\n {\n const q = query.toLowerCase().replace(/[-_]+/g, \" \").replace(/\\s+/g, \" \").trim();\n const byName = registeredProjects.filter((p) => {\n const name = (p.display_name ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n const slug = (p.slug ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n return name === q || slug === q;\n });\n for (const p of byName) {\n if (p.root_path && existsSync(p.root_path)) {\n launchInDir(p.root_path, p.display_name ?? query, { dryRun: opts.dryRun ?? false });\n return;\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Case 4: Free-text history search\n // -----------------------------------------------------------------------\n if (!existsSync(HISTORY_FILE)) {\n console.error(err(\"~/.claude/history.jsonl not found.\"));\n console.error(dim(\" No prompt history available for search.\"));\n console.error(dim(\" Try: pai (no args) to see all sessions.\"));\n process.exitCode = 1;\n return;\n }\n\n process.stderr.write(dim(` Searching prompt history for \"${query}\"...\\n`));\n const matches = await searchHistory(query, maxResults);\n\n if (matches.length === 0) {\n console.log(warn(`No sessions found matching \"${query}\".`));\n console.log(dim(\" Try a shorter or different search term.\"));\n console.log(dim(\" Or run: \") + chalk.white(\"pai\") + dim(\" (no args) to see all sessions.\"));\n return;\n }\n\n console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n const headers = [\"#\", \"id\", \"when\", \"project\", \"last matching prompt\"];\n const rows = matches.map((m, idx) => {\n const shortId = (m.sessionId ?? \"—\").slice(0, 8);\n const when = m.lastMatchTs > 0 ? fmtTs(m.lastMatchTs) : dim(\"—\");\n const project = shortenProject(m.project || \"—\");\n const snippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim().slice(0, 48);\n const fullSnippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim();\n const display = snippet.length < fullSnippet.length ? `\"${snippet}…\"` : `\"${snippet}\"`;\n return [\n dim(String(idx + 1)),\n chalk.cyan(shortId),\n when,\n dim(project),\n chalk.dim(display),\n ];\n });\n\n console.log(renderTable(headers, rows));\n console.log();\n\n const launchHistoryMatch = (match: SessionMatch) => {\n const session = matchToSession(match, allSessions);\n if (!session) {\n console.error(err(\"Could not resolve session for launch (no project path).\"));\n process.exitCode = 1;\n return;\n }\n launchSession(session, allSessions, opts.dryRun ?? false);\n };\n\n if (pickN !== undefined) {\n const idx = pickN - 1;\n if (idx >= 0 && idx < matches.length) {\n launchHistoryMatch(matches[idx]);\n return;\n }\n console.error(err(`Invalid choice: ${pickN}`));\n process.exitCode = 1;\n return;\n }\n\n if (opts.auto) {\n launchHistoryMatch(matches[0]);\n return;\n }\n\n const choice = await askForChoice(matches.length);\n if (choice !== null) {\n launchHistoryMatch(matches[choice - 1]);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main-resolver-DlaLOFBA.mjs","names":["shortenProject"],"sources":["../src/cli/lib/session-scan.ts","../src/cli/lib/aibroker-client.ts","../src/cli/lib/exit-dir.ts","../src/cli/lib/launch.ts","../src/cli/lib/history-search.ts","../src/cli/lib/dedup-sessions.ts","../src/cli/commands/main-resolver.ts"],"sourcesContent":["/**\n * Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .\n *\n * Storage layout (Claude Code v2.1.143+):\n *\n * <project>/<uuid>.jsonl — top-level: metadata, system snapshots, hook events.\n * REQUIRED for claude --resume to work.\n * <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.\n * 3000+ files, almost none resumable on their own.\n *\n * Resumability rule (empirically verified):\n * A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one\n * line of type \"system\". Sessions that only have a sessions/ counterpart cannot be\n * resumed by Claude Code regardless of how much transcript content they have.\n *\n * Stale-UUID problem (fixed in this version):\n * The clc session.json registry stores ONE uuid per named session — the uuid Claude Code\n * had when the user last named the session. If the user resumes and the session gets a\n * new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD\n * uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project\n * directory, not the clc-cached uuid.\n *\n * Resolution strategy:\n * 1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).\n * 2. For each, attach the clc name if the uuid matches (exact hit).\n * 3. After Pass 1, for every clc registry entry:\n * a. If the cached uuid was found → already handled.\n * b. Find the encodedDir for this entry's directory.\n * c. Check ALL sessions in that encodedDir (already in our Pass-1 results).\n * d. Pick the MOST RECENT resumable session in that dir and attach the name to it.\n * If no resumable session, fall through to transcript-only pass.\n * 4. This means the displayed uuid for \"Jobs Alpha\" is always today's active session,\n * not the stale cached one.\n *\n * Used by: pai sessions, pai resume\n */\n\nimport { readdirSync, statSync, readFileSync, existsSync, realpathSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Database } from \"better-sqlite3\";\nimport { smartDecodeDir } from \"../utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Display status for a session:\n * resumable — top-level jsonl exists with ≥1 system line (claude --resume works)\n * stub — in clc registry, top-level jsonl exists but 0 system lines (Ctrl+C exit)\n * transcript-only — in clc registry but no top-level jsonl at all\n * orphan — not in clc registry, only sessions/ transcript exists\n */\nexport type SessionStatus = \"resumable\" | \"stub\" | \"transcript-only\" | \"orphan\";\n\nexport interface ScannedSession {\n /** Full UUID from the filename. */\n uuid: string;\n /** Short 8-char prefix for display. */\n shortId: string;\n /** Encoded project directory name (e.g. \"-Users-foo-myproject\"). */\n encodedDir: string;\n /** Decoded project root path (via smartDecodeDir). */\n decodedPath: string;\n\n // Top-level jsonl — the resumability source\n /** Full path to the top-level <project>/<uuid>.jsonl (may not exist). */\n topLevelPath: string;\n /** Number of \"system\" lines in the top-level jsonl. */\n topLevelSystemLines: number;\n /** Size of the top-level jsonl in bytes. */\n topLevelSize: number;\n /**\n * True iff the top-level jsonl has at least one \"system\" line.\n * This is the canonical resumability gate — matches Claude Code's --resume behaviour.\n */\n resumable: boolean;\n /** Categorised display status. */\n sessionStatus: SessionStatus;\n\n // Sessions/ transcript data (best-effort, may not exist)\n /** Full path to sessions/<uuid>.jsonl, if present. */\n sessionJsonlPath?: string;\n /** Number of \"user\" type lines in the transcript. */\n userLines: number;\n /** Last user message text (truncated to 80 chars). */\n lastUserPrompt: string;\n /** Total line count in the transcript file. */\n msgCount: number;\n /** Auto-generated title from the last ai-title line in the transcript. */\n aiTitle?: string;\n\n /** mtime in epoch ms — top-level file mtime (primary), transcript mtime (fallback). */\n mtime: number;\n\n /**\n * Human-readable friendly name (for display and /Name restoration).\n * Priority: clc session.json name → ai-title → project dir basename.\n * NEVER slugified — preserves original spaces and capitalisation.\n */\n friendlyName?: string;\n /**\n * Literal directory from the clc session registry for this UUID.\n * May contain symlinks and emoji (e.g. /Users/foo/dev/... or .../🧠 Vault/...).\n * When present, realpathSync(clcDirectory) gives the correct cwd for --resume.\n */\n clcDirectory?: string;\n /** Project root path from PAI registry (authoritative cwd for resume). */\n registryRootPath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// clc session registry (~/.claude/session.json)\n// ---------------------------------------------------------------------------\n\nconst CLC_SESSIONS_FILE = join(homedir(), \".claude\", \"session.json\");\n\ninterface ClcEntry {\n name?: string;\n resume?: string;\n session?: string;\n directory?: string;\n}\n\ninterface ClcRegistry {\n sessions?: ClcEntry[];\n}\n\nexport interface ClcInfo {\n name: string;\n /** Literal directory from clc registry (may contain symlinks, emoji, spaces). */\n directory?: string;\n}\n\n/** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */\nfunction buildClcInfoMap(): Map<string, ClcInfo> {\n const map = new Map<string, ClcInfo>();\n try {\n const raw = readFileSync(CLC_SESSIONS_FILE, \"utf8\");\n const data = JSON.parse(raw) as ClcRegistry;\n for (const entry of data.sessions ?? []) {\n const name = entry.name?.trim();\n const uuid = (entry.resume ?? entry.session ?? \"\").trim();\n if (name && uuid) {\n map.set(uuid, { name, directory: entry.directory?.trim() || undefined });\n }\n }\n } catch {\n // Missing or malformed — not fatal\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// PAI registry\n// ---------------------------------------------------------------------------\n\ninterface RegistryProject {\n root_path: string;\n encoded_dir: string;\n}\n\n/** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */\nfunction buildRegistryRootPathMap(db: Database): Map<string, string> {\n try {\n const rows = db\n .prepare(\n `SELECT root_path, encoded_dir FROM projects\n WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`\n )\n .all() as RegistryProject[];\n const map = new Map<string, string>();\n for (const row of rows) {\n map.set(row.encoded_dir, row.root_path);\n }\n return map;\n } catch {\n return new Map();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Top-level jsonl parser\n// ---------------------------------------------------------------------------\n\ninterface TopLevelInfo {\n systemLines: number;\n size: number;\n mtime: number;\n}\n\nfunction parseTopLevel(filePath: string): TopLevelInfo {\n let systemLines = 0;\n let size = 0;\n let mtime = 0;\n try {\n const st = statSync(filePath);\n size = st.size;\n mtime = st.mtimeMs;\n const content = readFileSync(filePath, \"utf8\");\n for (const line of content.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n if (t.includes('\"type\":\"system\"') || t.includes('\"type\": \"system\"')) {\n systemLines++;\n }\n }\n } catch {\n /* ignore */\n }\n return { systemLines, size, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Sessions/ transcript parser\n// ---------------------------------------------------------------------------\n\ninterface TranscriptInfo {\n userLines: number;\n lastUserPrompt: string;\n msgCount: number;\n aiTitle?: string;\n mtime: number;\n}\n\nfunction parseTranscript(filePath: string): TranscriptInfo {\n let userLines = 0;\n let lastUserPrompt = \"\";\n let msgCount = 0;\n let aiTitle: string | undefined;\n let mtime = 0;\n\n try {\n mtime = statSync(filePath).mtimeMs;\n } catch {\n return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n }\n\n let content: string;\n try {\n content = readFileSync(filePath, \"utf8\");\n } catch {\n return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime };\n }\n\n for (const line of content.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n msgCount++;\n\n const hasUser = t.includes('\"type\":\"user\"') || t.includes('\"type\": \"user\"');\n const hasTitle =\n t.includes('\"type\":\"ai-title\"') || t.includes('\"type\": \"ai-title\"');\n if (!hasUser && !hasTitle) continue;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(t) as Record<string, unknown>;\n } catch {\n continue;\n }\n\n if (parsed.type === \"ai-title\") {\n const v = parsed.title ?? parsed.name;\n if (typeof v === \"string\" && v.trim()) aiTitle = v.trim();\n continue;\n }\n\n if (parsed.type !== \"user\") continue;\n userLines++;\n\n const msg = parsed.message as Record<string, unknown> | undefined;\n if (!msg) continue;\n const c = msg.content;\n let text = \"\";\n if (typeof c === \"string\") {\n text = c;\n } else if (Array.isArray(c) && c.length > 0) {\n const first = c[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") text = first.text;\n }\n if (text) lastUserPrompt = text.slice(0, 80).replace(/\\n/g, \" \");\n }\n\n return { userLines, lastUserPrompt, msgCount, aiTitle, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Main scanner\n// ---------------------------------------------------------------------------\n\nexport interface ScanOptions {\n /** Maximum results to return after sorting (default: 200). */\n limit?: number;\n /**\n * Filter mode:\n * \"named\" (default) — resumable OR in clc registry (named sessions the user cares about)\n * \"all\" — everything including unnamed orphans\n * \"resumable\" — only sessions claude --resume accepts (top-level + system lines)\n */\n filter?: \"named\" | \"all\" | \"resumable\";\n /**\n * @deprecated Use filter instead. When true → filter:\"resumable\", false → filter:\"named\".\n * Kept for backwards compatibility with callers that predate the filter field.\n */\n resumableOnly?: boolean;\n}\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// UUID format: 8-4-4-4-12 hex digits\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Compute the effective filter mode, resolving the legacy resumableOnly flag.\n */\nfunction resolveFilter(opts: ScanOptions): \"named\" | \"all\" | \"resumable\" {\n if (opts.filter) return opts.filter;\n if (opts.resumableOnly === true) return \"resumable\";\n if (opts.resumableOnly === false) return \"all\";\n return \"named\"; // default\n}\n\n/**\n * Scan ~/.claude/projects/ for all Claude Code sessions.\n *\n * Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).\n * Pass 1b: walk <project>/sessions/<uuid>.jsonl — where the stop hook moves a\n * session's transcript once it ends. These are resumable too, and\n * skipping them made every cleanly-stopped session unfindable by name.\n * Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.\n * For each such entry, scan the entry's project dir for the FRESHEST session\n * and attach the registry name to it. This fixes the stale-UUID bug where\n * clc's session.json points to an old uuid after a fresh start.\n * Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.\n *\n * Filter modes:\n * \"named\" — resumable + registry-known stubs + transcript-only (default)\n * \"all\" — all of the above + unnamed orphans from sessions/ subdirs\n * \"resumable\" — only sessions claude --resume accepts\n *\n * Returns results sorted by mtime descending.\n */\nexport function scanSessions(\n db: Database,\n opts: ScanOptions = {}\n): ScannedSession[] {\n const limit = opts.limit ?? 200;\n const filterMode = resolveFilter(opts);\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n const clcInfoMap = buildClcInfoMap();\n const rootPathMap = buildRegistryRootPathMap(db);\n const results: ScannedSession[] = [];\n // Track which UUIDs we've already added (from the jsonl walk)\n const seenUuids = new Set<string>();\n // Track which clc names we've already attached to a session\n const attachedClcUuids = new Set<string>();\n\n let encodedDirs: string[];\n try {\n encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n } catch {\n return [];\n }\n\n // Build a map: encodedDir → all top-level sessions found in Pass 1\n // This is used in Pass 2 to find the freshest session for a clc registry entry.\n const sessionsByEncodedDir = new Map<string, ScannedSession[]>();\n\n // ---- Pass 1: walk top-level <project>/<uuid>.jsonl files ----\n for (const encodedDir of encodedDirs) {\n const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n try {\n if (!statSync(projectDir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(projectDir);\n } catch {\n continue;\n }\n\n const decodedPath =\n smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n const registryRootPath = rootPathMap.get(encodedDir);\n const projectBasename = basename(decodedPath);\n\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n\n const topLevelPath = join(projectDir, file);\n const topInfo = parseTopLevel(topLevelPath);\n const resumable = topInfo.systemLines > 0;\n const clcInfo = clcInfoMap.get(uuid);\n const inRegistry = !!clcInfo;\n\n // Determine status\n const sessionStatus: SessionStatus = resumable\n ? \"resumable\"\n : inRegistry\n ? \"stub\"\n : \"orphan\";\n\n // Apply filter — but include ALL in this pass so Pass 2 can use them\n // We'll filter again when deciding what to add to `results`.\n const passesFilter =\n filterMode === \"resumable\"\n ? resumable\n : filterMode === \"named\"\n ? resumable || inRegistry\n : true;\n\n // Join with sessions/ transcript (best-effort)\n const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n const hasTranscript = existsSync(sessionJsonlPath);\n const transcript: TranscriptInfo = hasTranscript\n ? parseTranscript(sessionJsonlPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n // Use top-level mtime as primary\n const mtime = topInfo.mtime || transcript.mtime;\n\n // Name resolution: clc registry (verbatim) → ai-title → project basename\n const friendlyName =\n clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined;\n\n const session: ScannedSession = {\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: topInfo.systemLines,\n topLevelSize: topInfo.size,\n resumable,\n sessionStatus,\n sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime,\n friendlyName,\n clcDirectory: clcInfo?.directory,\n registryRootPath,\n };\n\n // Track in per-dir map for Pass 2 lookups (all sessions, regardless of filter)\n if (!sessionsByEncodedDir.has(encodedDir)) {\n sessionsByEncodedDir.set(encodedDir, []);\n }\n sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n seenUuids.add(uuid);\n if (inRegistry) attachedClcUuids.add(uuid);\n\n if (passesFilter) {\n results.push(session);\n }\n }\n\n // ---- Pass 1b: finished sessions, which live only in sessions/ ----\n //\n // The stop hook MOVES a session's transcript from <project>/<uuid>.jsonl to\n // <project>/sessions/<uuid>.jsonl. Pass 1 walks the top level only, so a\n // session becomes invisible to this scan the moment it is cleanly stopped —\n // and name resolution is built on this scan. That is why `pai Paperfull`\n // stopped finding Paperfull: three resumable transcripts sat in sessions/\n // while the scan reported zero sessions for the project, so `pai <name>`\n // fell through to a prompt-history search over old messages.\n //\n // These are reported resumable, which needs justifying, because\n // `claude --resume` does NOT read sessions/ — it reads the top-level path\n // and only that. What makes the label true is that nothing resumes without\n // going through probeResume first, and probeResume links the transcript back\n // to the top level before answering (lib/launch.ts, restoreTopLevel). PAI's\n // SessionStart hook displaced these files in the first place; it now links\n // rather than moves, so this is a claim about repairable state, not a hope.\n //\n // Do not weaken that chain without demoting these to transcript-only. An\n // earlier revision asserted plain \"claude --resume accepts these\", which is\n // false, and it fed unresumable ids to launch with full confidence.\n const sessionsDir = join(projectDir, \"sessions\");\n let finished: string[] = [];\n try {\n finished = readdirSync(sessionsDir);\n } catch {\n finished = []; // no sessions/ subdir — nothing was ever stopped here\n }\n\n for (const file of finished) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n if (seenUuids.has(uuid)) continue; // Pass 1 already has it\n\n const sessionJsonlPath = join(sessionsDir, file);\n const transcript = parseTranscript(sessionJsonlPath);\n const clcInfo = clcInfoMap.get(uuid);\n\n const session: ScannedSession = {\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n // No top-level file — that is the whole point of this pass.\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: true,\n sessionStatus: \"resumable\",\n sessionJsonlPath,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: transcript.mtime,\n friendlyName:\n clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined,\n clcDirectory: clcInfo?.directory,\n registryRootPath,\n };\n\n if (!sessionsByEncodedDir.has(encodedDir)) {\n sessionsByEncodedDir.set(encodedDir, []);\n }\n sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n seenUuids.add(uuid);\n if (clcInfo) attachedClcUuids.add(uuid);\n\n results.push(session); // resumable — passes every filter mode\n }\n }\n\n // ---- Pass 2: clc registry entries whose cached UUID wasn't found as a top-level file ----\n // For each such entry, find the project's encodedDir and look for the FRESHEST\n // resumable session there. Attach the clc name to it.\n // This fixes the stale-UUID bug: clc may store a uuid that's no longer valid,\n // but the project dir has a new active session the user is actually in.\n if (filterMode !== \"resumable\") {\n for (const [cachedUuid, clcInfo] of clcInfoMap) {\n if (attachedClcUuids.has(cachedUuid)) continue; // already named in Pass 1\n\n // Derive the encodedDir from clcInfo.directory\n let foundEncodedDir: string | undefined;\n if (clcInfo.directory) {\n const real = realpathSyncSafe(clcInfo.directory);\n if (real) {\n const encoded = encodeProjectDir(real);\n if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) {\n foundEncodedDir = encoded;\n }\n }\n }\n\n // Fallback: check if the cached uuid exists as a sessions/ transcript somewhere\n if (!foundEncodedDir) {\n for (const encodedDir of encodedDirs) {\n if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, \"sessions\", `${cachedUuid}.jsonl`))) {\n foundEncodedDir = encodedDir;\n break;\n }\n }\n }\n\n if (foundEncodedDir) {\n const dirSessions = sessionsByEncodedDir.get(foundEncodedDir) ?? [];\n\n // Find the freshest RESUMABLE session in this dir that doesn't yet have a name\n const freshestResumable = dirSessions\n .filter((s) => s.resumable && !s.friendlyName)\n .sort((a, b) => b.mtime - a.mtime)[0];\n\n if (freshestResumable) {\n // Attach the clc name to this fresher session\n freshestResumable.friendlyName = clcInfo.name;\n freshestResumable.clcDirectory =\n freshestResumable.clcDirectory ?? clcInfo.directory;\n freshestResumable.sessionStatus = \"resumable\";\n attachedClcUuids.add(freshestResumable.uuid);\n\n // Add to results if not already there (it might have been filtered out as orphan)\n if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {\n // It was added to sessionsByEncodedDir but might have been filtered from results\n if (!results.includes(freshestResumable)) {\n results.push(freshestResumable);\n }\n }\n continue; // Done with this clc entry\n }\n\n // No unnamed resumable session found. The project might have resumable sessions\n // that already have a name (different name). Or only stubs/orphans.\n // Fall through to transcript-only handling below.\n }\n\n // ---- Pass 3: truly no top-level jsonl → transcript-only ----\n if (!foundEncodedDir) {\n // We couldn't find the project dir at all\n const encodedDir = \"\";\n const decodedPath = clcInfo.directory ?? cachedUuid;\n const registryRootPath = undefined;\n\n const transcript: TranscriptInfo = { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n const mtime = transcript.mtime;\n\n seenUuids.add(cachedUuid);\n results.push({\n uuid: cachedUuid,\n shortId: cachedUuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: undefined,\n userLines: 0,\n lastUserPrompt: \"\",\n msgCount: 0,\n mtime,\n friendlyName: clcInfo.name,\n clcDirectory: clcInfo.directory,\n registryRootPath,\n });\n continue;\n }\n\n // Project dir was found but no unnamed resumable session.\n // Check sessions/ for the cached uuid transcript.\n const foundTranscriptPath = existsSync(\n join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n )\n ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n : undefined;\n\n const decodedPath =\n clcInfo.directory ??\n (smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, \"/\"));\n const registryRootPath = rootPathMap.get(foundEncodedDir);\n const topLevelPath = join(\n CLAUDE_PROJECTS_DIR,\n foundEncodedDir,\n `${cachedUuid}.jsonl`\n );\n\n const transcript: TranscriptInfo = foundTranscriptPath\n ? parseTranscript(foundTranscriptPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n seenUuids.add(cachedUuid);\n results.push({\n uuid: cachedUuid,\n shortId: cachedUuid.slice(0, 8),\n encodedDir: foundEncodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: foundTranscriptPath,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: transcript.mtime,\n friendlyName: clcInfo.name,\n clcDirectory: clcInfo.directory,\n registryRootPath,\n });\n }\n }\n\n results.sort((a, b) => b.mtime - a.mtime);\n return results.slice(0, limit);\n}\n\n// ---------------------------------------------------------------------------\n// Path encoding helpers\n// ---------------------------------------------------------------------------\n\n/** Claude Code's project-dir encoding: replace / . - (space) with - */\nfunction encodeProjectDir(realPath: string): string {\n // Leading slash becomes leading -; all / . - and space → -\n return realPath.replace(/[/.\\- ]/g, \"-\");\n}\n\n/** realpathSync that returns null instead of throwing */\nfunction realpathSyncSafe(p: string): string | null {\n try {\n return realpathSync(p);\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Age formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtAge(mtime: number): string {\n const diffMs = Date.now() - mtime;\n const diffMin = Math.floor(diffMs / 60_000);\n const diffHr = Math.floor(diffMs / 3_600_000);\n const diffDay = Math.floor(diffMs / 86_400_000);\n\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay < 30) return `${diffDay}d`;\n return `${Math.floor(diffDay / 30)}mo`;\n}\n\n// ---------------------------------------------------------------------------\n// Resolve a name/id/prefix to a session entry\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedSession {\n session: ScannedSession;\n /** The friendly name to use verbatim in the /Name command. Never slugified. */\n friendlyName?: string;\n}\n\n/**\n * Filesystem-level UUID scan: walk ALL ~/.claude/projects/<encoded-dir>/<uuid>.jsonl\n * looking for top-level files whose UUID starts with `prefix`.\n *\n * Returns a ScannedSession-like object (minimal fields) for each match, or an empty\n * array if nothing is found. This is used as a fallback when the regular catalog\n * (limited to named/recent sessions) doesn't contain the requested UUID.\n *\n * Complexity: O(number of project directories + files per dir). Typically <200 dirs\n * with a handful of top-level jsonl each — fast enough for an interactive CLI.\n */\nfunction scanFilesystemForUuidPrefix(prefix: string): ScannedSession[] {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n const prefixLower = prefix.toLowerCase();\n const matches: ScannedSession[] = [];\n\n let encodedDirs: string[];\n try {\n encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n } catch {\n return [];\n }\n\n for (const encodedDir of encodedDirs) {\n const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n try {\n if (!statSync(projectDir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(projectDir);\n } catch {\n continue;\n }\n\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n if (!uuid.toLowerCase().startsWith(prefixLower)) continue;\n\n const topLevelPath = join(projectDir, file);\n const topInfo = parseTopLevel(topLevelPath);\n const resumable = topInfo.systemLines > 0;\n const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n\n // Check sessions/ transcript for prompt and title\n const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n const hasTranscript = existsSync(sessionJsonlPath);\n const transcript: TranscriptInfo = hasTranscript\n ? parseTranscript(sessionJsonlPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n matches.push({\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: topInfo.systemLines,\n topLevelSize: topInfo.size,\n resumable,\n sessionStatus: resumable ? \"resumable\" : \"stub\",\n sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: topInfo.mtime || transcript.mtime,\n friendlyName: transcript.aiTitle ?? basename(decodedPath),\n clcDirectory: undefined,\n registryRootPath: undefined,\n });\n }\n }\n\n return matches.sort((a, b) => b.mtime - a.mtime);\n}\n\n/**\n * Resolve a name-or-id-or-prefix to a single ScannedSession.\n *\n * Comparisons are case-insensitive; stored casing is preserved in output.\n *\n * Priority:\n * 1. Exact case-insensitive match on friendlyName\n * 2. Partial case-insensitive match (contains)\n * 3. UUID prefix match against the in-memory catalog\n * 4. UUID prefix match against the full filesystem (fallback for any session)\n */\nexport function resolveSessionByNameOrId(\n sessions: ScannedSession[],\n query: string\n): ResolvedSession {\n const qLower = query.toLowerCase().trim();\n\n // 1. Exact match (case-insensitive, preserve stored casing)\n const byExact = sessions.filter(\n (s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower\n );\n if (byExact.length >= 1) {\n return { session: byExact[0], friendlyName: byExact[0].friendlyName };\n }\n\n // 2. Partial match\n const byPartial = sessions.filter(\n (s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower)\n );\n if (byPartial.length === 1) {\n return { session: byPartial[0], friendlyName: byPartial[0].friendlyName };\n }\n if (byPartial.length > 1) {\n const candidates = byPartial\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `Ambiguous name \"${query}\" — ${byPartial.length} matches:\\n${candidates}\\n\\nBe more specific or use a UUID prefix.`\n );\n }\n\n // 3. UUID prefix against catalog\n const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));\n if (byUuid.length === 1) {\n return { session: byUuid[0], friendlyName: byUuid[0].friendlyName };\n }\n if (byUuid.length > 1) {\n const candidates = byUuid\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `UUID prefix \"${query}\" is ambiguous — ${byUuid.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n );\n }\n\n // 4. Filesystem fallback — scan ALL ~/.claude/projects/*/<uuid>.jsonl files\n // This handles any session UUID, even if not in the named/recent catalog.\n // Only attempted when the query looks like a UUID prefix (hex + dashes).\n const UUID_PREFIX_RE = /^[0-9a-f-]{4,36}$/i;\n if (UUID_PREFIX_RE.test(qLower)) {\n const fsSessions = scanFilesystemForUuidPrefix(qLower);\n if (fsSessions.length === 1) {\n return { session: fsSessions[0], friendlyName: fsSessions[0].friendlyName };\n }\n if (fsSessions.length > 1) {\n const candidates = fsSessions\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `UUID prefix \"${query}\" is ambiguous — ${fsSessions.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n );\n }\n }\n\n throw new Error(\n `No session found matching \"${query}\".\\n\\nRun: pai sessions to list sessions.\\nRun: pai sessions --all to include transcript-only sessions.\\nRun: pai find <words> to search prompt history.`\n );\n}\n","/**\n * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.\n *\n * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,\n * and reads a single newline-terminated JSON response. No class needed —\n * just a thin async function matching the WatcherClient protocol.\n *\n * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport { spawnSync } from \"node:child_process\";\nimport { aibrokerSocketPath } from \"../../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Lightweight session metadata returned by the AIBroker `sessions` IPC method.\n * Does NOT include scrollback content — use `session_content` for that.\n */\nexport interface AiBrokerSessionMeta {\n index: number;\n sessionId: string;\n /** iTerm2 tab title or profile name */\n name: string;\n /** PAI session name set via /Name; null for bare shells */\n paiName: string | null;\n atPrompt: boolean;\n /** \"claude\" for Claude Code panes, \"shell\" for bare terminals */\n kind: \"claude\" | \"shell\";\n /** Whether this is the currently focused pane */\n active: boolean;\n /** Last user prompt seen in scrollback (only populated by fetchLiveSessionsWithPrompts) */\n lastPrompt?: string;\n}\n\ninterface AiBrokerSessionsResult {\n sessions: AiBrokerSessionMeta[];\n}\n\n// ---------------------------------------------------------------------------\n// Core call\n// ---------------------------------------------------------------------------\n\n// AIBROKER_SOCKET stays first: it is AIBroker's own variable and a user who has\n// set it means it. The fallback goes through runtime-paths so the test guard\n// can redirect it — writing to a live socket from a test is the same hazard as\n// writing to live user state, and this one is not even ours.\nconst DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? aibrokerSocketPath();\n\n/**\n * How long to wait for `send_to_session`, which must exceed the server's own\n * ack window or the caller gives up while the callee is still working.\n *\n * AIBroker waits up to SEND_ACK_TIMEOUT_MS (15s) for the submit confirmation —\n * the text leaving the input line — before answering. The generic client\n * timeout is 8s. So every send needing 8-15s to confirm reported\n * \"AIBroker IPC call timed out\" while succeeding, and the handler's eventual\n * `delivered: true` was written into a socket nobody was reading any more.\n *\n * Measured on `pai pause all` across 15 sessions: 9 reported failed, 8 of them\n * verifiably paused. A caller's deadline shorter than the callee's is not a\n * tuning question, it is a guaranteed false negative on every slow success.\n */\nconst SEND_TIMEOUT_MS = 30_000;\n\n/**\n * Call an AIBroker IPC method and return the result.\n *\n * Resolves with the `result` field of a successful response.\n * Rejects if the socket is not available, the call times out, or the\n * daemon returns an error.\n *\n * @param method IPC method name (e.g. \"session_content\", \"send_to_session\")\n * @param params Method parameters object\n * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)\n */\nexport function callAiBroker(\n method: string,\n params: Record<string, unknown> = {},\n timeoutMs = 8_000\n): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const socketPath = DEFAULT_SOCKET;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(err: Error | null, value?: Record<string, unknown>): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket.destroy();\n } catch {\n /* ignore */\n }\n if (err) reject(err);\n else resolve(value!);\n }\n\n const socket = connect(socketPath, () => {\n const request = {\n id: randomUUID(),\n sessionId: process.env.TERM_SESSION_ID ?? \"pai-cli\",\n method,\n params,\n };\n const itermId = process.env.ITERM_SESSION_ID;\n if (itermId) Object.assign(request, { itermSessionId: itermId });\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n\n let response: { ok: boolean; result?: Record<string, unknown>; error?: string };\n try {\n response = JSON.parse(line);\n } catch {\n finish(new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"AIBroker IPC call failed\"));\n } else {\n finish(null, response.result ?? {});\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(new Error(\"AIBroker not running (socket not found).\"));\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) finish(new Error(\"AIBroker IPC connection closed before response.\"));\n });\n\n timer = setTimeout(\n () => finish(new Error(\"AIBroker IPC call timed out.\")),\n timeoutMs\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Typed helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch all live iTerm2 session metadata from AIBroker via the `sessions` method.\n * Returns an empty array if AIBroker is not running.\n *\n * This is metadata-only (no scrollback). It is faster than `session_content`\n * and the correct source for listing/routing purposes.\n */\nexport async function fetchLiveSessions(): Promise<AiBrokerSessionMeta[]> {\n try {\n const result = await callAiBroker(\"sessions\", {});\n const sessions = (result as unknown as AiBrokerSessionsResult).sessions;\n if (!Array.isArray(sessions)) return [];\n return sessions;\n } catch {\n return [];\n }\n}\n\n/**\n * Fetch live sessions WITH the last user prompt extracted from terminal scrollback.\n *\n * Heavier than `fetchLiveSessions` (one IPC call returning full content for\n * every session), but yields a `lastPrompt` field useful for the unified\n * listing.\n */\nexport async function fetchLiveSessionsWithPrompts(): Promise<\n (AiBrokerSessionMeta & { lastPrompt?: string })[]\n> {\n // Get the basic metadata first (always works, fast).\n const metas = await fetchLiveSessions();\n if (metas.length === 0) return [];\n\n // Then enrich with last prompts via session_content (single call, all sessions).\n try {\n const contentResult = (await callAiBroker(\"session_content\", { lines: 60 })) as {\n sessions?: Array<{ sessionId: string; content?: string }>;\n };\n const contentMap = new Map<string, string>();\n for (const s of contentResult.sessions ?? []) {\n if (s.sessionId && typeof s.content === \"string\") {\n contentMap.set(s.sessionId, s.content);\n }\n }\n return metas.map((m) => {\n const content = contentMap.get(m.sessionId);\n const lastPrompt = content ? extractLastUserPrompt(content) : undefined;\n return { ...m, lastPrompt };\n });\n } catch {\n return metas.map((m) => ({ ...m }));\n }\n}\n\n/**\n * Extract the most-recent user prompt from a terminal scrollback string.\n *\n * Claude Code's TUI shows user input lines prefixed with `❯ `. We scan from\n * the bottom up, skipping the active input box (between the two horizontal\n * rule lines) and the statusline footer.\n */\nfunction extractLastUserPrompt(content: string): string | undefined {\n const lines = content.split(\"\\n\");\n // Walk bottom-up looking for the most recent user-typed line.\n // Claude Code marks user prompts with one of these prefixes:\n // ❯ <text> (current/recent prompt in the input box or scrollback)\n // > <text> (older variant)\n // The line MUST have non-empty content after the prompt symbol.\n // Skip the active input box (often empty `❯`) and statusline (👋 PAI CC...).\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n // Match ❯ or > (followed by space) then content\n const m = line.match(/^[❯>]\\s+(.+?)\\s*$/);\n if (!m) continue;\n let text = m[1].trim();\n if (!text) continue;\n // Skip lines that are just box-drawing or known UI noise\n if (/^[─━═]+/.test(text)) continue;\n if (text.startsWith(\"👋\")) continue;\n // Strip ANSI escape sequences that may leak in\n text = text.replace(/\\x1B\\[[0-9;]*[A-Za-z]/g, \"\").trim();\n if (text) return text;\n }\n return undefined;\n}\n\n/**\n * Send text to a specific AIBroker session by its iTerm2 sessionId.\n *\n * The wire keys are `target` and `message`, and neither is negotiable — the\n * handler rejects anything else with \"target is required\" before it looks at\n * the rest. This was written as `{ target: sessionId, message: text }`, which is the shape of\n * THIS function's own parameters rather than the shape of the IPC, so every\n * call failed identically. `pai pause all` was the only caller, so the live\n * path had never once executed: its --dry-run branch returns before sending,\n * and that was the only branch anyone had exercised.\n *\n * The caller must NOT terminate `text` with a newline. The transport sends with\n * `enter: true` and appends the Enter itself, so a trailing \\n submits twice —\n * once for the text and once for an empty prompt.\n */\nexport async function sendToSession(\n sessionId: string,\n text: string,\n timeoutMs = SEND_TIMEOUT_MS\n): Promise<{ ok: boolean; error?: string; timedOut?: boolean }> {\n try {\n await callAiBroker(\"send_to_session\", { target: sessionId, message: text }, timeoutMs);\n return { ok: true };\n } catch (e) {\n const error = String(e);\n // A timeout is NOT a delivery failure, and conflating the two is expensive.\n // The handler deposits into the target's mailbox BEFORE it waits for the\n // submit ack, so a send that times out has still been delivered — only the\n // confirmation is missing. Reported as a plain failure, the natural response\n // is to send again, which is how `pai pause all` produced nested\n // \"carried forward\" blocks in checkpoints that had already been written.\n return { ok: false, error, timedOut: /timed out/i.test(error) };\n }\n}\n\n/**\n * Switch iTerm2 focus to the session identified by `target`.\n *\n * `target` can be a sessionId, paiName, or tab index number (as string).\n * After switching, activates the iTerm2 application itself so the window\n * comes to the foreground.\n *\n * Returns { ok: true } if AIBroker confirmed the switch, or\n * { ok: false, error } if AIBroker is not running or the session was not found.\n */\nexport async function switchToSession(target: string): Promise<{ ok: boolean; error?: string }> {\n try {\n await callAiBroker(\"switch\", { target });\n // Bring iTerm2 itself to the foreground (the IPC only selects the tab)\n const { spawnSync } = await import(\"node:child_process\");\n spawnSync(\"osascript\", [\"-e\", 'tell application \"iTerm\" to activate'], {\n stdio: \"ignore\",\n });\n return { ok: true };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n\n/**\n * Bring the iTerm2 tab containing a specific session to the front.\n *\n * This is the mechanism AIBroker's screenshot path uses: match the session by\n * its iTerm2 session id (the `sessionId` returned by `fetchLiveSessions`, which\n * is iTerm's own `id of session`), then `select` its window, tab, and session.\n * Unlike the `switch` IPC (which only flips an internal index and never touches\n * iTerm), this actually reveals the tab.\n */\nexport function revealItermSession(itermSessionId: string): { ok: boolean; error?: string } {\n // Strip any \"iterm:\" style prefix, matching AIBroker's stripItermPrefix.\n const id = itermSessionId.replace(/^iterm:/i, \"\").trim();\n const script = `tell application \"iTerm2\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if id of s is \"${id}\" then\n select w\n select t\n select s\n return \"ok\"\n end if\n end repeat\n end repeat\n end repeat\n return \"not-found\"\nend tell`;\n try {\n const r = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n if (r.status !== 0) {\n return { ok: false, error: (r.stderr || \"osascript failed\").trim() };\n }\n if ((r.stdout ?? \"\").trim() === \"ok\") return { ok: true };\n return { ok: false, error: \"session not found in any iTerm2 window\" };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n","/**\n * Print the working directory of a `claude` session that pai launched, right\n * after it exits. This lets the user cd back into the same directory and\n * restart claude to continue — Claude Code's own `--resume` hint is unreliable\n * for this workflow.\n *\n * Why here and not a SessionEnd hook: a hook inside Claude Code is cancelled\n * during teardown (anthropics/claude-code#41577) and races the terminal\n * restore, so it can't reliably print anything. pai spawns the `claude` binary\n * directly (no shell), so the `claude()` shell wrapper never sees pai-launched\n * sessions either. Printing here — after spawnSync has returned and claude has\n * fully exited — is the one place that reliably reaches the terminal.\n */\nexport function printExitDir(dir: string): void {\n process.stdout.write(\n `\\n\\x1b[2m📂 Working directory:\\x1b[0m ${dir}\\n` +\n `\\x1b[2m cd \"${dir}\"\\x1b[0m\\n`,\n );\n}\n","/**\n * launch.ts — Launch Claude Code in a directory, in the CURRENT terminal.\n *\n * Shared by the interactive picker (pick.ts). Deliberately does NOT switch\n * iTerm tabs (aibroker_switch): switching jumps the user to a different — and\n * sometimes wrong — terminal, which is confusing. Picking a place should start\n * a session right here, in the chosen directory.\n *\n * Behaviour:\n * resume-or-fresh → if a resumable UUID is given, probe it; on success\n * `claude --resume`, otherwise fall back to a fresh session\n * in the same dir. With no UUID, start fresh.\n *\n * The `claude` child inherits the tty (stdio: \"inherit\"), so the session runs\n * in the terminal that launched `pai`. On exit we print the working directory.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport {\n realpathSync,\n existsSync,\n linkSync,\n copyFileSync,\n openSync,\n readSync,\n closeSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport { err } from \"../utils.js\";\nimport { printExitDir } from \"./exit-dir.js\";\n\nexport interface ProbeResult {\n ok: boolean;\n reason?: string;\n}\n\n/**\n * Put a transcript back where `claude --resume` looks for it.\n *\n * `claude --resume <uuid>` reads ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl\n * and ONLY that path. A copy under `sessions/` is invisible to it — measured\n * 2026-08-04, both directions:\n *\n * b3462801 867 KB, sessions/ only → \"No conversation found with session ID\"\n * a9ecdc1c top level → found\n *\n * Location is one of two factors, and this function addresses that one. PAI\n * displaced these files itself, from FOUR movers — a SessionStart hook, a\n * UserPromptSubmit hook, the stop hook, and the work-queue worker — of which\n * the UserPromptSubmit one did most of the damage, because it ran on every\n * prompt of every session and excluded only the caller's own transcript. All\n * four now hardlink (project-utils/paths.ts). So this restores a file PAI\n * displaced rather than inventing a layout Claude Code does not use.\n *\n * The other factor is content, and no amount of relinking helps there — see\n * `hasConversation`.\n *\n * A hard link is preferred over a copy: same inode, no second megabyte on disk,\n * and the archive under `sessions/` keeps working for everything that reads it.\n * Returns whether the top-level path exists afterwards.\n */\nexport function restoreTopLevel(uuid: string, dir: string): boolean {\n const topLevel = join(dir, `${uuid}.jsonl`);\n if (existsSync(topLevel)) return true;\n\n const archived = join(dir, \"sessions\", `${uuid}.jsonl`);\n if (!existsSync(archived)) return false;\n\n try {\n linkSync(archived, topLevel);\n return true;\n } catch {\n try {\n copyFileSync(archived, topLevel);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Does this transcript hold an exchange, or only session metadata?\n *\n * The second reason `claude --resume` says \"No conversation found\": the file is\n * there and readable and still holds no conversation. Measured 2026-08-04 on\n * 046bb712 — 537 bytes of last-prompt, custom-title, agent-name, mode and\n * permission-mode, no user line, no assistant line. It was restored to the top\n * level, verified same-inode, and still refused. It was never resumable, and\n * relinking cannot make it so. 30 of PAI's own 50 displaced transcripts are\n * this shape, one of them 745 KB — size proves nothing, because hook context\n * attachments are large.\n *\n * This scans the WHOLE file, and a bounded head will not do. It is tempting —\n * \"a real session's first assistant line lands within the first few KB\" — and\n * it is false. Measured on b3462801, a session `claude --resume` accepts:\n *\n * file size 866953\n * length of LINE 1 762977 <- one hook context attachment\n * first \"type\":\"user\" 766830\n *\n * The first exchange sits past 766 KB because line 1 is a single enormous\n * attachment blob. Any head shorter than that reports a 867 KB working session\n * as an empty stub, which is the exact false negative this function exists to\n * prevent — and it is the shape that produced today's whole incident.\n *\n * Chunked so that a large transcript costs a scan rather than a resident copy,\n * with an overlap so the marker cannot hide across a chunk boundary. Reading a\n * few MB to answer a question about resumability is cheap; being wrong is not.\n *\n * Unreadable counts as real. Claiming a session is empty when it cannot be\n * inspected would talk the caller out of a resume that might have worked.\n */\nconst CONVERSATION_MARKERS = ['\"type\":\"assistant\"', '\"type\": \"assistant\"', '\"type\":\"user\"', '\"type\": \"user\"'];\nconst OVERLAP = 32; // > longest marker, so none can straddle two chunks\n\nexport function hasConversation(path: string, chunkBytes = 1 << 20): boolean {\n let fd: number | undefined;\n try {\n fd = openSync(path, \"r\");\n const buf = Buffer.alloc(chunkBytes);\n let carry = \"\";\n let pos = 0;\n\n for (;;) {\n const read = readSync(fd, buf, 0, chunkBytes, pos);\n if (read <= 0) return false;\n pos += read;\n\n const text = carry + buf.subarray(0, read).toString(\"utf8\");\n if (CONVERSATION_MARKERS.some((m) => text.includes(m))) return true;\n carry = text.slice(-OVERLAP);\n }\n } catch {\n return true;\n } finally {\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n /* nothing useful to do */\n }\n }\n }\n}\n\n/**\n * Can this session be resumed — and if the only thing standing in the way is\n * where its transcript sits, put it back so that the answer is yes.\n *\n * Returns true/false when it can tell, and null when it cannot — the caller\n * must treat null as \"ask something else\", never as \"no\". A path this function\n * fails to recognise would otherwise silently veto a perfectly good resume.\n *\n * Claude Code stores transcripts at ~/.claude/projects/<encoded-cwd>/, where the\n * encoding replaces every non-alphanumeric character with `-`.\n */\nfunction transcriptOnDisk(\n uuid: string,\n cwd: string,\n home = homedir()\n): true | \"missing\" | \"stub\" | null {\n try {\n const encoded = cwd.replace(/[^a-zA-Z0-9]/g, \"-\");\n const dir = join(home, \".claude\", \"projects\", encoded);\n if (!existsSync(dir)) return null; // unknown layout — not evidence of absence\n if (!restoreTopLevel(uuid, dir)) return \"missing\";\n // Present is not the same as resumable. A metadata stub survives every\n // location check and still fails at `claude --resume`, and this probe is the\n // only thing standing between that and a caller that exits on the failure\n // instead of falling back to a fresh session.\n return hasConversation(join(dir, `${uuid}.jsonl`)) ? true : \"stub\";\n } catch {\n return null;\n }\n}\n\n/**\n * Probe whether a session UUID is resumable from `cwd`.\n *\n * The filesystem is asked first, and usually answers.\n *\n * This used to run `claude --resume <uuid> --print --output-format=json \"_\"`\n * with a 5s timeout, which is not a probe: it resumes the session AND sends a\n * prompt to the model, then waits for a complete JSON reply. That costs a model\n * round-trip per probe, and 5s is not enough time for one — for a LARGE session\n * least of all, because the transcript has to be loaded first.\n *\n * So the check failed precisely for the sessions most worth resuming, and the\n * caller's fallback quietly started a fresh session in their place. Observed\n * 2026-08-04: `pai Paperfull` reported `spawn error: spawnSync claude ETIMEDOUT`\n * for fb76a6c3 and started over, while\n * `~/.claude/projects/…-Paperfull/sessions/fb76a6c3-….jsonl` sat on disk the\n * whole time.\n *\n * A transcript on disk is what resumable MEANS, and reading a directory entry is\n * free. The spawn remains only for the case the filesystem cannot answer, and\n * now gets a timeout that a real answer can fit inside.\n *\n * \"On disk\" had to be tightened once. It first accepted a transcript sitting\n * only under `sessions/`, which claude --resume does not read — so the probe\n * swapped a 5s false negative for a confident false positive, and the caller\n * spawned a resume that died with \"No conversation found\" instead of falling\n * back to a fresh session. `restoreTopLevel` is what makes the permissive\n * reading true rather than merely optimistic.\n */\nexport function probeResume(uuid: string, cwd: string, home?: string): ProbeResult {\n const onDisk = transcriptOnDisk(uuid, cwd, home);\n if (onDisk === true) return { ok: true };\n if (onDisk === \"missing\") {\n return { ok: false, reason: \"No transcript on disk for this UUID\" };\n }\n if (onDisk === \"stub\") {\n // Say which of the two it is. \"No conversation found\" from claude tells the\n // user nothing about whether a repair might help; this does.\n return { ok: false, reason: \"Transcript holds only session metadata — no conversation to resume\" };\n }\n\n const result = spawnSync(\n \"claude\",\n [\"--resume\", uuid, \"--print\", \"--output-format=json\", \"_\"],\n { cwd, timeout: 30_000, env: process.env, stdio: [\"ignore\", \"ignore\", \"pipe\"] }\n );\n\n if (result.error) return { ok: false, reason: `spawn error: ${result.error.message}` };\n\n const stderr = result.stderr?.toString(\"utf8\") ?? \"\";\n if (\n stderr.toLowerCase().includes(\"no conversation found\") ||\n stderr.toLowerCase().includes(\"session not found\")\n ) {\n return { ok: false, reason: \"No conversation found for this UUID\" };\n }\n if (result.status !== 0) {\n return {\n ok: false,\n reason: `claude exited ${result.status ?? \"signal\"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : \"\"}`,\n };\n }\n return { ok: true };\n}\n\nexport interface LaunchOpts {\n /** If set, try to resume this session before falling back to fresh. */\n resumableUuid?: string;\n /** Skip the resume probe and start a brand-new session in the dir. */\n forceFresh?: boolean;\n /** Print what would happen, then return without launching. */\n dryRun?: boolean;\n}\n\n/**\n * Launch `claude` in `dir` in the current terminal. `name` is used for both the\n * Claude session label (--name) and the /Name slash command (tab/statusline).\n * Never returns on the live path — it exits the process after claude exits.\n */\nexport function launchInDir(dir: string, name: string, opts: LaunchOpts = {}): void {\n let cwd: string;\n try {\n cwd = realpathSync(dir);\n } catch {\n console.error(\n err(\n `Directory does not exist or cannot be resolved:\\n ${dir}\\n` +\n ` The folder may have moved or been deleted.`\n )\n );\n process.exitCode = 1;\n return;\n }\n\n const promptArg = `/Name ${name}\\ngo`;\n const wantResume = !opts.forceFresh && !!opts.resumableUuid;\n\n if (opts.dryRun) {\n if (wantResume) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would probe then exec (RESUME path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` probe: transcript on disk for ${opts.resumableUuid!.slice(0, 8)}?`);\n console.log(` argv: claude --resume ${opts.resumableUuid} --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n console.log(` fallback: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n } else {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (FRESH path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` argv: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n }\n console.log();\n return;\n }\n\n const fresh = () => {\n const result = spawnSync(\"claude\", [\"--name\", name, promptArg], {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n };\n\n if (wantResume) {\n const probe = probeResume(opts.resumableUuid!, cwd);\n if (probe.ok) {\n const result = spawnSync(\n \"claude\",\n [\"--resume\", opts.resumableUuid!, \"--name\", name, promptArg],\n { cwd, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n return;\n }\n process.stderr.write(\n chalk.yellow(\n `\\n Resume failed for ${opts.resumableUuid!.slice(0, 8)}: ${probe.reason ?? \"unknown error\"}\\n` +\n ` Starting fresh session in same directory.\\n\\n`\n )\n );\n fresh();\n return;\n }\n\n fresh();\n}\n","/**\n * history-search.ts\n *\n * Content-based search across ~/.claude/history.jsonl.\n * Streams the file (avoids loading 26k+ lines into memory), groups matching\n * lines by sessionId, and returns results sorted by most-recent match.\n *\n * Used by: pai <query> (main resolver) and pai find (compat alias)\n */\n\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HistoryEntry {\n display?: string;\n timestamp?: number;\n project?: string;\n sessionId?: string;\n pastedContents?: unknown;\n}\n\nexport interface SessionMatch {\n sessionId: string | null;\n /** Most-recent timestamp among matching lines */\n lastMatchTs: number;\n /** Most-recent matching prompt text */\n lastMatchDisplay: string;\n /** Project path (from the most-recent match) */\n project: string;\n /** Number of matching lines in this session */\n matchCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// History file location\n// ---------------------------------------------------------------------------\n\nexport const HISTORY_FILE = join(homedir(), \".claude\", \"history.jsonl\");\n\n// ---------------------------------------------------------------------------\n// Core search function (streaming)\n// ---------------------------------------------------------------------------\n\n/**\n * Search ~/.claude/history.jsonl for prompts matching the query (case-insensitive\n * substring). Results are grouped by sessionId and sorted by most-recent match.\n *\n * Entries without a sessionId (old Claude Code versions) are excluded from\n * results since they can't be resumed.\n */\nexport async function searchHistory(\n query: string,\n maxResults: number\n): Promise<SessionMatch[]> {\n if (!existsSync(HISTORY_FILE)) return [];\n\n const queryLower = query.toLowerCase();\n const bySession = new Map<string, SessionMatch>();\n\n const rl = createInterface({\n input: createReadStream(HISTORY_FILE, { encoding: \"utf8\" }),\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n const t = line.trim();\n if (!t) continue;\n\n let entry: HistoryEntry;\n try {\n entry = JSON.parse(t) as HistoryEntry;\n } catch {\n continue;\n }\n\n const display = entry.display ?? \"\";\n if (!display.toLowerCase().includes(queryLower)) continue;\n\n const ts = entry.timestamp ?? 0;\n const project = entry.project ?? \"\";\n const sessionId = entry.sessionId ?? null;\n\n // Skip entries with no sessionId — can't resume them\n if (!sessionId) continue;\n\n const existing = bySession.get(sessionId);\n if (!existing) {\n bySession.set(sessionId, {\n sessionId,\n lastMatchTs: ts,\n lastMatchDisplay: display,\n project,\n matchCount: 1,\n });\n } else {\n existing.matchCount++;\n if (ts > existing.lastMatchTs) {\n existing.lastMatchTs = ts;\n existing.lastMatchDisplay = display;\n existing.project = project;\n }\n }\n }\n\n // Sort by most-recent match descending\n const sorted = [...bySession.values()].sort(\n (a, b) => b.lastMatchTs - a.lastMatchTs\n );\n\n return sorted.slice(0, maxResults);\n}\n","/**\n * dedup-sessions.ts — Shared dedup catalog for `pai` and listing commands.\n *\n * Both `pai` (no args) and any listing command must show IDENTICAL output.\n * This module is the single source of truth for:\n * - Name normalization (strips spinner prefixes and \" (node)\" suffixes)\n * - Dedup grouping (one row per normalized name)\n * - Priority ranking (live > resumable > transcript-only > stub > project > orphan)\n * - Registered projects (shown as status \"idle\" when no live/disk session matches)\n * - Filtering (hide 0-session / cold / archived projects by default)\n * - Table rendering (shared columns and formatting)\n *\n * Default visibility rules (overridden by showAll=true):\n * SHOW: live sessions (always)\n * SHOW: resumable sessions (always)\n * SHOW: registered projects with session_count > 0 AND last_active within 90 days\n * OR session_count >= 3 (i.e. well-used projects regardless of age)\n * HIDE: projects with session_count = 0 (never used)\n * HIDE: projects with last_active > 90 days AND session_count < 3 (cold)\n * HIDE: archived projects (status != 'active')\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport chalk from \"chalk\";\nimport { renderTable, dim, header, warn } from \"../utils.js\";\nimport { fmtAge, type ScannedSession } from \"./session-scan.js\";\nimport { type AiBrokerSessionMeta } from \"./aibroker-client.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type UnifiedStatus =\n | \"live\"\n | \"resumable\"\n | \"transcript-only\"\n | \"stub\"\n | \"project\"\n | \"orphan\";\n\nexport const STATUS_PRIORITY: Record<UnifiedStatus, number> = {\n live: 0,\n resumable: 1,\n \"transcript-only\": 2,\n stub: 3,\n project: 4,\n orphan: 5,\n};\n\nexport interface UnifiedSession {\n /** Clean display name (normalized — no spinners, no \"(node)\") */\n name: string;\n /** Slug from the project registry (for slug-based matching in `pai <name>`) */\n slug?: string;\n status: UnifiedStatus;\n /** Only present for live sessions */\n liveSessionId?: string;\n /** Only present for disk sessions */\n diskSession?: ScannedSession;\n lastActivity: number;\n project: string;\n lastPrompt: string;\n /** For project-source entries: number of historical sessions ever */\n sessionCount?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Name normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Strip Claude Code spinner characters, decoration prefixes, and \" (node)\" suffixes\n * from a session name so that \"✳ Chenarlier (node)\" and \"Chenarlier\" group together.\n *\n * Handles:\n * - Unicode braille spinner frames: ⠐ ⠂ ⠁ ⠈ ⠘ ⠙ ⠚ ⠛ ⠗ ⠝\n * - Decoration characters: ✻ ✳ ✲ ✼ ✦ * ★ ☆ • ●\n * - Any leading non-letter/digit characters followed by whitespace\n * - Trailing \" (node)\" suffix (Claude Code appends this on some platforms)\n */\nexport function normalizeName(s: string): string {\n return s\n // Strip leading spinner / decoration characters (braille + symbols)\n .replace(/^[⠀-⣿✀-➿✻✳✲✼✦*★☆•●⠐⠂⠁⠈⠘⠙⠚⠛⠗⠝]+\\s*/u, \"\")\n // Strip any remaining leading non-alphanumeric prefix before whitespace\n .replace(/^[^a-zA-Z0-9À-ÿ -鿿]+\\s*/u, \"\")\n // Strip trailing \" (node)\" suffix\n .replace(/\\s*\\(node\\)\\s*$/i, \"\")\n .trim();\n}\n\n/**\n * Normalize a slug (kebab-case) to a human-readable form for matching.\n * \"jobs-beta\" → \"jobs beta\"\n */\nfunction slugToWords(slug: string): string {\n return slug.replace(/-/g, \" \");\n}\n\n// ---------------------------------------------------------------------------\n// Dedup builder\n// ---------------------------------------------------------------------------\n\nexport interface RegisteredProject {\n slug: string;\n display_name: string;\n root_path: string;\n session_count: number;\n last_active: number | null;\n status: string;\n}\n\n/**\n * Merge live (AIBroker) + disk (session-scan) + registry (projects DB) sessions\n * into a deduped catalog.\n *\n * Algorithm:\n * 1. Normalize names before grouping\n * 2. Group by normalized name (case-insensitive)\n * 3. Within each group, rank by STATUS_PRIORITY (live wins)\n * 4. Within same priority, keep the most recent (highest mtime)\n * 5. Sort output: by priority, then by lastActivity desc\n * 6. Filter idle project entries (default) or show all (showAll=true)\n *\n * @param showAll When true, include cold / zero-session / archived projects.\n */\n/**\n * Which of two sessions of equal status should represent a name.\n *\n * Recency alone was the rule, and recency is exactly the wrong instinct here.\n * A resume that FAILS still creates a session file, and that file is newer than\n * the transcript it failed to open. So the tie-break reliably handed back the\n * artefact of a bug in preference to the work.\n *\n * Measured on Paperfull, 2026-08-04:\n *\n * b3462801 867 KB 32 lines 3 user 15:41:57 <- the actual work\n * 7fdbb9a8 82 KB 25 lines 2 user 15:41:59 <- empty, Ctrl-C'd, WON by 2s\n * a9ecdc1c 82 KB 25 lines 2 user 15:49:40 <- a second failed resume\n *\n * `pai Paperfull` therefore resolved to 7fdbb9a8 — 82 KB of hook context in\n * `attachment` lines, with no assistant turn anywhere in it — while 867 KB of\n * real work sat one second older and unchosen.\n *\n * WHAT THIS DOES NOT FIX, measured before claiming otherwise: the reported\n * failure was `No conversation found with session ID: 7fdbb9a8`, and that error\n * is about WHERE the transcript is, not what is in it. Both transcripts live\n * only in sessions/, and `claude --resume b3462801` — the good one, the one this\n * change now selects — fails with the identical message. Verified directly, and\n * again on a second project. So this stops dedup preferring the artefact of a\n * bug, which is worth doing on its own, and `pai <Name>` still cannot resume a\n * cleanly-stopped session until the sessions/ relocation is addressed. That part\n * is tracked in Notes/TODO.md and belongs to session-scan/the stop hook.\n *\n * Conversation volume comes first because an empty artefact is small BY\n * CONSTRUCTION — nobody spoke into it — whereas an artefact can be newer by\n * mere milliseconds. Recency only decides between transcripts that recorded the\n * same amount, where it is a reasonable guess again.\n *\n * HONEST LIMIT: the exact signal is \"did the assistant ever reply\", and this is\n * not that — it is a proxy that happens to separate the measured case by one\n * user line and seven total lines, which is a thin margin. The real predicate\n * belongs in `parseTranscript` (session-scan.ts) as an `assistantLines` count,\n * which is free there since it already reads every line of the file. That file\n * was mid-edit in another session when this landed, so this stays out of it.\n * Move to the exact signal when it exists; the tests below should keep passing\n * unchanged, since they assert the outcome and not the proxy.\n */\nfunction winsTiebreak(candidate: UnifiedSession, incumbent: UnifiedSession): boolean {\n const c = candidate.diskSession;\n const i = incumbent.diskSession;\n\n // A live entry has no transcript to weigh. It also never reaches here except\n // against another live entry, where recency is all there is.\n if (c && i) {\n if (c.userLines !== i.userLines) return c.userLines > i.userLines;\n if (c.msgCount !== i.msgCount) return c.msgCount > i.msgCount;\n }\n\n return candidate.lastActivity > incumbent.lastActivity;\n}\n\nexport function buildDeduped(\n liveSessions: AiBrokerSessionMeta[],\n diskSessions: ScannedSession[],\n registeredProjects?: RegisteredProject[],\n showAll = false\n): UnifiedSession[] {\n const byName = new Map<string, UnifiedSession>();\n\n // Process live Claude sessions\n for (const s of liveSessions) {\n if (s.kind === \"shell\") continue;\n // NOT the iTerm tab title.\n //\n // `s.name` is whatever iTerm is calling the tab right now, which tracks the\n // foreground process and the window title escape codes. Using it as a\n // session IDENTITY means any tab that happens to be titled like a project\n // becomes a \"live session\" of that name — and because live entries win over\n // project entries in the merge below, `pai <Project>` then SWITCHES to that\n // tab instead of launching the project. Reported 2026-08-04: `pai PAI`\n // printing \"Switched to live session: pai\" and opening nothing.\n //\n // A session's identity is the name PAI gave it. A session that has none is\n // listed by its id and matches nothing by name, which is the safe answer:\n // failing to find it launches a fresh one, whereas matching it wrongly\n // hands the user someone else's terminal.\n const raw = s.paiName ?? s.sessionId.slice(0, 8);\n const normalized = normalizeName(raw);\n const key = normalized.toLowerCase();\n\n const entry: UnifiedSession = {\n name: normalized,\n status: \"live\",\n liveSessionId: s.sessionId,\n lastActivity: Date.now(),\n project: \"\",\n lastPrompt: s.lastPrompt ?? \"\",\n };\n\n const existing = byName.get(key);\n if (!existing || STATUS_PRIORITY[\"live\"] < STATUS_PRIORITY[existing.status]) {\n byName.set(key, entry);\n }\n }\n\n // Process disk sessions\n for (const s of diskSessions) {\n const raw = s.friendlyName ?? s.shortId;\n const normalized = normalizeName(raw);\n const key = normalized.toLowerCase();\n\n let status: UnifiedStatus;\n if (s.resumable) {\n status = \"resumable\";\n } else if (s.sessionStatus === \"transcript-only\") {\n status = \"transcript-only\";\n } else if (s.sessionStatus === \"stub\") {\n status = \"stub\";\n } else {\n status = \"orphan\";\n }\n\n const entry: UnifiedSession = {\n name: normalized,\n status,\n diskSession: s,\n lastActivity: s.mtime,\n project: s.decodedPath ?? \"\",\n lastPrompt: s.lastUserPrompt ?? \"\",\n };\n\n const existing = byName.get(key);\n if (!existing) {\n byName.set(key, entry);\n } else {\n const ep = STATUS_PRIORITY[existing.status];\n const np = STATUS_PRIORITY[status];\n if (np < ep || (np === ep && winsTiebreak(entry, existing))) {\n byName.set(key, entry);\n }\n }\n }\n\n // Constants for idle-project filtering\n const COLD_DAYS = 90;\n const COLD_THRESHOLD_MS = COLD_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n\n // Process registered projects — add any not already covered by live/disk entries\n if (registeredProjects) {\n for (const p of registeredProjects) {\n // Skip archived projects unless showAll\n if (!showAll && p.status !== \"active\") continue;\n\n const displayName = p.display_name ?? p.slug;\n const normalized = normalizeName(displayName);\n const key = normalized.toLowerCase();\n\n // Also check if slug matches (via slug→words form)\n const slugKey = slugToWords(p.slug).toLowerCase();\n\n if (!byName.has(key) && !byName.has(slugKey)) {\n // Apply idle-project filter unless showAll:\n // - Hide projects with 0 sessions (never used)\n // - Hide projects cold AND low usage: last_active > 90 days AND sessions < 3\n if (!showAll) {\n if (p.session_count === 0) continue;\n if (p.session_count < 3) {\n const lastActive = p.last_active ?? 0;\n if (now - lastActive > COLD_THRESHOLD_MS) continue;\n }\n }\n\n byName.set(key, {\n name: normalized,\n slug: p.slug,\n status: \"project\",\n lastActivity: p.last_active ?? 0,\n project: p.root_path,\n lastPrompt: \"\",\n sessionCount: p.session_count,\n });\n } else {\n // Already have an entry — enrich it with the slug for matching\n const existing = byName.get(key) ?? byName.get(slugKey);\n if (existing && !existing.slug) {\n existing.slug = p.slug;\n }\n if (existing && existing.sessionCount === undefined) {\n existing.sessionCount = p.session_count;\n }\n }\n }\n }\n\n // Sort: by priority first, then by lastActivity desc\n const entries = Array.from(byName.values());\n entries.sort((a, b) => {\n const pa = STATUS_PRIORITY[a.status];\n const pb = STATUS_PRIORITY[b.status];\n if (pa !== pb) return pa - pb;\n return b.lastActivity - a.lastActivity;\n });\n\n return entries;\n}\n\n// ---------------------------------------------------------------------------\n// Status formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtUnifiedStatus(s: UnifiedStatus): string {\n switch (s) {\n case \"live\": return chalk.green(\"live\");\n case \"resumable\": return chalk.cyan(\"resumable\");\n case \"transcript-only\": return chalk.dim(\"transcript\");\n case \"stub\": return chalk.dim(\"stub\");\n case \"project\": return chalk.dim(\"idle\");\n case \"orphan\": return chalk.dim(\"orphan\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared table renderer\n// ---------------------------------------------------------------------------\n\nfunction shortenProject(p: string, maxLen = 28): string {\n if (!p || p.length <= maxLen) return p || dim(\"—\");\n return \"…\" + p.slice(-(maxLen - 1));\n}\n\n/**\n * Render the deduped session catalog to stdout.\n *\n * @param entries Output of buildDeduped()\n * @param maxRows Maximum rows to display. undefined = no limit (for --all).\n */\nexport function renderDedupedSessions(entries: UnifiedSession[], maxRows?: number): void {\n if (entries.length === 0) {\n console.log(warn(\"No sessions found. Start Claude Code in a project directory first.\"));\n return;\n }\n\n console.log(\"\\n\" + header(\"Sessions\") + \"\\n\");\n\n const visible = maxRows !== undefined ? entries.slice(0, maxRows) : entries;\n const tableHeaders = [\"#\", \"name\", \"status\", \"age\", \"project\", \"last prompt\"];\n const tableRows = visible.map((entry, i) => {\n const age =\n entry.status === \"live\"\n ? chalk.green(\"now\")\n : entry.lastActivity > 0\n ? dim(fmtAge(entry.lastActivity))\n : dim(\"—\");\n const snippet = entry.lastPrompt.replace(/\\n+/g, \" \").trim().slice(0, 36);\n const projectPath =\n entry.diskSession\n ? entry.diskSession.friendlyName ?? entry.diskSession.decodedPath\n : entry.project;\n const project = dim(shortenProject(projectPath, 28));\n return [\n dim(String(i + 1)),\n chalk.white(entry.name),\n fmtUnifiedStatus(entry.status),\n age,\n project,\n chalk.dim(snippet ? `\"${snippet}\"` : \"—\"),\n ];\n });\n\n console.log(renderTable(tableHeaders, tableRows));\n\n if (maxRows !== undefined && entries.length > maxRows) {\n console.log(dim(` … ${entries.length - maxRows} more — use --all to show everything`));\n }\n\n console.log();\n console.log(\n dim(\" Switch/resume/start: \") +\n chalk.white(\"pai <name>\") +\n dim(\" or \") +\n chalk.white(\"pai <uuid-prefix>\")\n );\n console.log();\n}\n","/**\n * main-resolver.ts\n *\n * `pai [<query>]` — topic-first session discovery and launcher (v0.11.1)\n *\n * Decision tree:\n * 1. No arg → show deduped session listing (one row per name)\n * 2. UUID prefix → universal filesystem scan; auto-launch the match\n * 3. Any string:\n * a. Live match (by normalized paiName) → aibroker_switch → iTerm tab to front. Done.\n * b. Resumable match → probe + claude --resume <uuid>\n * c. Transcript/stub match → fresh claude in same project dir\n * d. No name match → free-text history search → picker\n *\n * Dedup + name normalization logic: src/cli/lib/dedup-sessions.ts (shared with listing).\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { createInterface } from \"node:readline\";\nimport { existsSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\nimport chalk from \"chalk\";\nimport { err, dim, warn, ok, header, renderTable } from \"../utils.js\";\nimport {\n scanSessions,\n fmtAge,\n type ScannedSession,\n} from \"../lib/session-scan.js\";\nimport { searchHistory, HISTORY_FILE, type SessionMatch } from \"../lib/history-search.js\";\nimport { fetchLiveSessions, fetchLiveSessionsWithPrompts, switchToSession } from \"../lib/aibroker-client.js\";\nimport { basename } from \"node:path\";\nimport { printExitDir } from \"../lib/exit-dir.js\";\nimport { probeResume, launchInDir } from \"../lib/launch.js\";\nimport {\n buildDeduped,\n normalizeName,\n STATUS_PRIORITY,\n fmtUnifiedStatus,\n renderDedupedSessions,\n type UnifiedSession,\n type RegisteredProject,\n} from \"../lib/dedup-sessions.js\";\n\n// ---------------------------------------------------------------------------\n// Launch session (disk-based)\n// ---------------------------------------------------------------------------\n\n/**\n * Which directory should we open for this session?\n *\n * A session carries up to three ideas of where it lives, they disagree in\n * practice, and the FIRST is the least trustworthy. `clcDirectory` and\n * `decodedPath` are recorded per session and go stale the moment a project\n * directory is renamed; `registryRootPath` is kept current by the registry.\n *\n * This used to take the preferred value and, if it did not resolve, print an\n * error and exit — so a live project became unopenable by its own name. The\n * session had been started when its directory had a different name, so the\n * stale per-session value pointed at a path that no longer existed while the\n * registry held the right one, untried.\n *\n * The order is unchanged, because the per-session value IS more specific when\n * valid — it can name a subdirectory the session actually ran in. What changed\n * is that a candidate which fails no longer ends the attempt.\n *\n * realpath, not resolve: a directory reached through a symlinked parent has\n * several valid spellings, and comparing or storing the unresolved one is how\n * the same directory ends up with two identities.\n *\n * Returns the resolved directory plus every candidate tried, so a total failure\n * can report what it looked at instead of naming one path and leaving the user\n * to guess whether the others were even considered.\n */\nexport function resolveSessionDir(session: {\n clcDirectory?: string;\n registryRootPath?: string;\n decodedPath?: string;\n}): { dir: string | undefined; tried: string[] } {\n const tried = [\n session.clcDirectory,\n session.registryRootPath,\n session.decodedPath,\n ].filter((d): d is string => typeof d === \"string\" && d.length > 0);\n\n for (const candidate of tried) {\n try {\n return { dir: realpathSync(candidate), tried };\n } catch {\n // Stale, renamed or deleted — try the next one.\n }\n }\n return { dir: undefined, tried };\n}\n\nfunction launchSession(\n session: ScannedSession,\n allSessions: ScannedSession[],\n dryRun: boolean\n): boolean {\n let resumableUuid: string | undefined;\n\n if (session.resumable) {\n resumableUuid = session.uuid;\n } else if (session.encodedDir) {\n const sameProject = allSessions.filter(\n (s) => s.encodedDir === session.encodedDir && s.resumable\n );\n sameProject.sort((a, b) => b.mtime - a.mtime);\n if (sameProject.length > 0) {\n resumableUuid = sameProject[0].uuid;\n }\n }\n\n const { dir: projectDir } = resolveSessionDir(session);\n\n // A dead session must not end the whole attempt.\n //\n // This used to print an error and process.exit(1). But the caller has more\n // candidates after this one — most importantly the registered PROJECT, whose\n // path the registry keeps current — and `openMatch` already knows how to open\n // a directory directly. Exiting here meant one stale transcript made a live\n // project unreachable by name: the user asks for a name they can see in the\n // listing, and gets a dead path instead of a session in the directory that\n // plainly exists.\n //\n // Returning false lets resolution continue. If nothing at all can be opened,\n // the caller reports that once, with everything it tried.\n if (projectDir === undefined) return false;\n\n // A session is named for a human to recognise, so a UUID prefix is never an\n // acceptable answer. `friendlyName` is undefined for the sessions synthesized\n // from a prompt-history match, and picking one of those used to open a tab\n // called \"7fdbb9a8\" — for a project plainly called Paperfull, sitting right\n // there in the path we already resolved. The directory is the better fallback\n // in every case, so shortId is now only reached for a path with no basename.\n const name = session.friendlyName ?? (basename(projectDir) || session.shortId);\n const promptArg = `/Name ${name}\\ngo`;\n\n if (dryRun) {\n if (resumableUuid) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would probe then exec (RESUME path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(projectDir)}`);\n console.log(` probe: transcript on disk for ${resumableUuid.slice(0, 8)}?`);\n console.log(` argv: claude --resume ${resumableUuid} --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n console.log(` fallback: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n } else {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (FRESH path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(projectDir)}`);\n console.log(` argv: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n }\n console.log();\n return true;\n }\n\n if (resumableUuid) {\n const probe = probeResume(resumableUuid, projectDir);\n if (probe.ok) {\n const result = spawnSync(\n \"claude\",\n [\"--resume\", resumableUuid, \"--name\", name, promptArg],\n { cwd: projectDir, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exit(1);\n }\n printExitDir(projectDir);\n process.exit(result.status ?? 0);\n } else {\n process.stderr.write(\n chalk.yellow(\n `\\n Resume failed for ${resumableUuid.slice(0, 8)}: ${probe.reason ?? \"unknown error\"}\\n` +\n ` Starting fresh session in same directory.\\n\\n`\n )\n );\n const result = spawnSync(\n \"claude\",\n [\"--name\", name, promptArg],\n { cwd: projectDir, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exit(1);\n }\n printExitDir(projectDir);\n process.exit(result.status ?? 0);\n }\n } else {\n const result = spawnSync(\n \"claude\",\n [\"--name\", name, promptArg],\n { cwd: projectDir, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exit(1);\n }\n printExitDir(projectDir);\n process.exit(result.status ?? 0);\n }\n}\n\n// ---------------------------------------------------------------------------\n// History match → ScannedSession bridge\n// ---------------------------------------------------------------------------\n\nfunction matchToSession(\n match: SessionMatch,\n allSessions: ScannedSession[]\n): ScannedSession | null {\n if (!match.sessionId) return null;\n\n const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);\n if (catalogMatch) return catalogMatch;\n\n if (!match.project) return null;\n\n return {\n uuid: match.sessionId,\n shortId: match.sessionId.slice(0, 8),\n encodedDir: \"\",\n decodedPath: match.project,\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: undefined,\n userLines: 0,\n lastUserPrompt: match.lastMatchDisplay.slice(0, 80),\n msgCount: 0,\n mtime: match.lastMatchTs,\n friendlyName: undefined,\n clcDirectory: undefined,\n registryRootPath: match.project,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatters\n// ---------------------------------------------------------------------------\n\nfunction fmtTs(ts: number): string {\n const d = new Date(ts);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +\n `${pad(d.getHours())}:${pad(d.getMinutes())}`\n );\n}\n\nfunction shortenProject(p: string, maxLen = 44): string {\n if (!p || p.length <= maxLen) return p || dim(\"—\");\n return \"…\" + p.slice(-(maxLen - 1));\n}\n\n// ---------------------------------------------------------------------------\n// Interactive picker prompt\n// ---------------------------------------------------------------------------\n\nasync function askForChoice(max: number): Promise<number | null> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(\n dim(`\\n Enter # to launch (1-${max}), or press Enter to cancel: `),\n (answer) => {\n rl.close();\n const n = parseInt(answer.trim(), 10);\n if (!isNaN(n) && n >= 1 && n <= max) {\n resolve(n);\n } else {\n resolve(null);\n }\n }\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Shared switch helper (live session → iTerm tab)\n// ---------------------------------------------------------------------------\n\nasync function doSwitch(\n entry: UnifiedSession,\n dryRun: boolean\n): Promise<boolean> {\n if (!entry.liveSessionId) return false;\n if (dryRun) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would switch iTerm tab:\") + \"\\n\");\n console.log(` target: ${entry.name} (${entry.liveSessionId.slice(0, 8)})`);\n console.log(` action: aibroker_switch + osascript iTerm activate`);\n console.log();\n return true;\n }\n const result = await switchToSession(entry.liveSessionId);\n if (result.ok) {\n // Name AND id. \"Switched to live session: pai\" was reported three times on\n // 2026-08-04 by a user watching nothing happen, and the message gave no way\n // to tell WHICH session had been revealed — so there was nothing to check\n // against the tabs actually on screen. The id makes the claim falsifiable.\n console.log(\n ok(\n `Switched to live session: ${chalk.white(entry.name)} ` +\n chalk.dim(`(${entry.liveSessionId.slice(0, 8)})`),\n ),\n );\n return true;\n }\n console.error(warn(`Could not switch via AIBroker: ${result.error ?? \"unknown error\"}`));\n return false;\n}\n\n/**\n * Act on a matched catalog entry: switch to it, resume it, or start it.\n *\n * The third case is the one that was missing. A registered project whose\n * sessions have all ended matches by name perfectly well, but carries no\n * `diskSession`, and the old code simply ran off the end of both the exact and\n * the partial branch — past the picker, into a free-text search of prompt\n * history. So `pai Paperfull` answered a request to open a project with a list\n * of five old conversations that merely mentioned the word, while the project's\n * own directory was sitting in `entry.project` the whole time.\n *\n * Returns false only when there is genuinely nothing to open, which is the one\n * case where falling through to a history search is the right thing to do.\n */\nasync function openMatch(\n entry: UnifiedSession,\n allSessions: ScannedSession[],\n dryRun: boolean\n): Promise<boolean> {\n if (entry.status === \"live\") {\n if (await doSwitch(entry, dryRun)) return true;\n // Switch failed — fall through and try to open it from disk instead.\n }\n // A transcript whose directory is gone is not an answer. Fall through to the\n // registered project below, whose path the registry keeps current — that is\n // the difference between \"your project moved\" and \"cannot open anything\".\n if (entry.diskSession && launchSession(entry.diskSession, allSessions, dryRun)) {\n return true;\n }\n if (entry.project && existsSync(entry.project)) {\n launchInDir(entry.project, entry.name, { dryRun });\n return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Main command\n// ---------------------------------------------------------------------------\n\nexport interface MainResolverOpts {\n auto?: boolean; // -y / --auto: pick #1 without prompting\n dryRun?: boolean; // --dry-run: show what would happen\n n?: string; // --n <count>: max candidates for history search\n all?: boolean; // --all: show cold / 0-session / archived projects too\n}\n\nfunction getRegisteredProjects(db: Database, all = false): RegisteredProject[] {\n try {\n const statusClause = all ? \"\" : \"WHERE p.status = 'active'\";\n return db\n .prepare(`\n SELECT\n p.slug,\n p.display_name,\n p.root_path,\n p.status,\n COUNT(s.id) AS session_count,\n MAX(s.created_at) AS last_active\n FROM projects p\n LEFT JOIN sessions s ON s.project_id = p.id\n ${statusClause}\n GROUP BY p.id\n ORDER BY last_active DESC NULLS LAST, p.updated_at DESC\n `)\n .all() as RegisteredProject[];\n } catch {\n return [];\n }\n}\n\nexport async function cmdMain(\n db: Database,\n query: string | undefined,\n pickN: number | undefined,\n opts: MainResolverOpts\n): Promise<void> {\n const maxResults = parseInt(opts.n ?? \"20\", 10);\n const showAll = opts.all ?? false;\n // Live sessions: metadata-only fetch (1s) — last-prompt fetch is too slow\n // (~8s for AppleScript per-session scrollback). For live entries, last-prompt\n // stays empty; user can `pai <name>` to switch into the tab and see context.\n const livePromise = !query\n ? fetchLiveSessions().catch(() => [] as Awaited<ReturnType<typeof fetchLiveSessions>>)\n : Promise.resolve([]);\n const allSessions = scanSessions(db, { limit: 500, filter: \"named\" });\n const registeredProjects = getRegisteredProjects(db, showAll);\n\n // -----------------------------------------------------------------------\n // Case 1: No query → deduped session listing (shared renderer)\n // -----------------------------------------------------------------------\n if (!query) {\n const liveSessions = await livePromise;\n const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n renderDedupedSessions(deduped, showAll ? undefined : maxResults);\n return;\n }\n\n // -----------------------------------------------------------------------\n // Case 2: UUID prefix (8+ hex chars) → universal filesystem scan\n // -----------------------------------------------------------------------\n const UUID_PREFIX_RE = /^[0-9a-f-]{8,36}$/i;\n if (UUID_PREFIX_RE.test(query)) {\n const byUuid = allSessions.filter((s) => s.uuid.startsWith(query.toLowerCase()));\n if (byUuid.length === 1) {\n launchSession(byUuid[0], allSessions, opts.dryRun ?? false);\n return;\n }\n if (byUuid.length > 1) {\n console.error(err(`UUID prefix \"${query}\" is ambiguous — ${byUuid.length} catalog matches.`));\n process.exitCode = 1;\n return;\n }\n // Fall through to name match / history search\n }\n\n // -----------------------------------------------------------------------\n // Case 3: Name match against deduped catalog (normalized + slug matching)\n // -----------------------------------------------------------------------\n {\n let liveSessions: Awaited<ReturnType<typeof fetchLiveSessions>> = [];\n try {\n liveSessions = await fetchLiveSessions();\n } catch {\n // AIBroker not running\n }\n\n const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n // Normalize the query the same way we normalize session names\n // Also support slug form: \"jobs-beta\" → \"jobs beta\" for matching\n const qNorm = normalizeName(query).toLowerCase();\n const qSlug = query.toLowerCase().replace(/\\s+/g, \"-\"); // words → slug form for slug lookup\n\n // Match helper: checks normalized display name AND slug\n const nameMatches = (e: UnifiedSession, q: string) =>\n e.name.toLowerCase() === q ||\n (e.slug !== undefined && e.slug.toLowerCase() === qSlug);\n const nameIncludes = (e: UnifiedSession, q: string) =>\n e.name.toLowerCase().includes(q) ||\n (e.slug !== undefined && e.slug.toLowerCase().includes(qSlug));\n\n // Among equally-good matches, the most recently active one is what the user\n // means. This used `deduped.find()`, which took whichever entry happened to\n // come first — so a two-month-old note whose title matched EXACTLY beat the\n // live project the user was actually working in, whose sessions carried an\n // extra word in their names. Typing the project's own name opened its oldest\n // session.\n const newestFirst = (a: UnifiedSession, b: UnifiedSession) =>\n b.lastActivity - a.lastActivity;\n\n // Exact normalized-name match first (display_name or slug), newest wins.\n //\n // Try EVERY exact match, not just the best one. They are ranked by recency,\n // but the top-ranked entry can be unopenable — a transcript pointing at a\n // directory that has since been renamed — while a lower-ranked entry for the\n // same name is the live project sitting right there. Stopping at the first\n // candidate turned \"this name is ambiguous\" into \"this name is broken\".\n const exactMatches = deduped.filter((e) => nameMatches(e, qNorm)).sort(newestFirst);\n for (const match of exactMatches) {\n if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n }\n\n // Partial normalized-name match (display_name or slug)\n let partialMatches = deduped.filter((e) => nameIncludes(e, qNorm));\n\n // Substring matching misses the common case where every query word is\n // present but not adjacent: \"foo baz\" is not a substring of \"foo bar baz\",\n // so a project whose directory gained a word became unreachable by the name\n // its owner thinks of it by. Fall back to requiring every query WORD to\n // appear somewhere in the name or slug. That stays narrow — a sibling\n // project sharing only the first word still will not match, because it\n // lacks the second.\n if (partialMatches.length === 0) {\n const qWords = qNorm.split(/\\s+/).filter(Boolean);\n if (qWords.length > 1) {\n partialMatches = deduped.filter((e) => {\n const haystack = `${e.name} ${e.slug ?? \"\"}`.toLowerCase();\n return qWords.every((w) => haystack.includes(w));\n });\n }\n }\n\n partialMatches.sort(newestFirst);\n\n if (partialMatches.length === 1) {\n if (await openMatch(partialMatches[0], allSessions, opts.dryRun ?? false)) return;\n }\n\n if (partialMatches.length > 1) {\n // Multiple matches — show picker\n console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n const headers = [\"#\", \"name\", \"status\", \"age\", \"project\"];\n const rows = partialMatches.slice(0, maxResults).map((entry, i) => {\n const age =\n entry.status === \"live\"\n ? chalk.green(\"now\")\n : dim(fmtAge(entry.lastActivity));\n const project = entry.diskSession\n ? dim(shortenProject(entry.diskSession.decodedPath, 36))\n : dim(\"—\");\n return [\n dim(String(i + 1)),\n chalk.white(entry.name),\n fmtUnifiedStatus(entry.status),\n age,\n project,\n ];\n });\n console.log(renderTable(headers, rows));\n console.log();\n\n const pickMatch = async (match: UnifiedSession) => {\n if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n console.error(err(`Nothing to open for \"${match.name}\" — no live session, no transcript, no directory.`));\n process.exitCode = 1;\n };\n\n if (pickN !== undefined) {\n const idx = pickN - 1;\n if (idx >= 0 && idx < partialMatches.length) {\n await pickMatch(partialMatches[idx]);\n return;\n }\n console.error(err(`Invalid choice: ${pickN}`));\n process.exitCode = 1;\n return;\n }\n\n if (opts.auto) {\n await pickMatch(partialMatches[0]);\n return;\n }\n\n const choice = await askForChoice(Math.min(partialMatches.length, maxResults));\n if (choice !== null) {\n await pickMatch(partialMatches[choice - 1]);\n }\n return;\n }\n }\n\n // -----------------------------------------------------------------------\n // Case 3b: Registered project by name → open its directory\n // -----------------------------------------------------------------------\n // If a project is registered under this name and its directory is there, that\n // is the answer, full stop. Naming a project should start a session in it.\n //\n // The deduped catalog cannot be relied on for this: it keeps ONE entry per\n // project and prefers a transcript over the registry row, so when the\n // transcript's recorded directory has gone stale the surviving entry carries a\n // dead path and the live registry value is never consulted. The catalog is\n // built for listing, where showing the session is right; it is the wrong\n // source for \"where do I open this\".\n //\n // So ask the registry directly, before falling back to scraping prompt\n // history — which is a search feature, not a way to open a known project.\n {\n const q = query.toLowerCase().replace(/[-_]+/g, \" \").replace(/\\s+/g, \" \").trim();\n const byName = registeredProjects.filter((p) => {\n const name = (p.display_name ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n const slug = (p.slug ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n return name === q || slug === q;\n });\n for (const p of byName) {\n if (p.root_path && existsSync(p.root_path)) {\n launchInDir(p.root_path, p.display_name ?? query, { dryRun: opts.dryRun ?? false });\n return;\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Case 4: Free-text history search\n // -----------------------------------------------------------------------\n if (!existsSync(HISTORY_FILE)) {\n console.error(err(\"~/.claude/history.jsonl not found.\"));\n console.error(dim(\" No prompt history available for search.\"));\n console.error(dim(\" Try: pai (no args) to see all sessions.\"));\n process.exitCode = 1;\n return;\n }\n\n process.stderr.write(dim(` Searching prompt history for \"${query}\"...\\n`));\n const matches = await searchHistory(query, maxResults);\n\n if (matches.length === 0) {\n console.log(warn(`No sessions found matching \"${query}\".`));\n console.log(dim(\" Try a shorter or different search term.\"));\n console.log(dim(\" Or run: \") + chalk.white(\"pai\") + dim(\" (no args) to see all sessions.\"));\n return;\n }\n\n console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n const headers = [\"#\", \"id\", \"when\", \"project\", \"last matching prompt\"];\n const rows = matches.map((m, idx) => {\n const shortId = (m.sessionId ?? \"—\").slice(0, 8);\n const when = m.lastMatchTs > 0 ? fmtTs(m.lastMatchTs) : dim(\"—\");\n const project = shortenProject(m.project || \"—\");\n const snippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim().slice(0, 48);\n const fullSnippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim();\n const display = snippet.length < fullSnippet.length ? `\"${snippet}…\"` : `\"${snippet}\"`;\n return [\n dim(String(idx + 1)),\n chalk.cyan(shortId),\n when,\n dim(project),\n chalk.dim(display),\n ];\n });\n\n console.log(renderTable(headers, rows));\n console.log();\n\n const launchHistoryMatch = (match: SessionMatch) => {\n const session = matchToSession(match, allSessions);\n if (!session) {\n console.error(err(\"Could not resolve session for launch (no project path).\"));\n process.exitCode = 1;\n return;\n }\n launchSession(session, allSessions, opts.dryRun ?? false);\n };\n\n if (pickN !== undefined) {\n const idx = pickN - 1;\n if (idx >= 0 && idx < matches.length) {\n launchHistoryMatch(matches[idx]);\n return;\n }\n console.error(err(`Invalid choice: ${pickN}`));\n process.exitCode = 1;\n return;\n }\n\n if (opts.auto) {\n launchHistoryMatch(matches[0]);\n return;\n }\n\n const choice = await askForChoice(matches.length);\n if (choice !== null) {\n launchHistoryMatch(matches[choice - 1]);\n }\n}\n"],"mappings":"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"}