@tekmidian/pai 0.27.2 → 0.28.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 (43) hide show
  1. package/dist/checkpoint-block-DYq9RmMU.mjs +1409 -0
  2. package/dist/checkpoint-block-DYq9RmMU.mjs.map +1 -0
  3. package/dist/checkpoint-block-xwQ8Y3LQ.mjs +1309 -0
  4. package/dist/checkpoint-block-xwQ8Y3LQ.mjs.map +1 -0
  5. package/dist/cli/index.mjs +3 -3
  6. package/dist/cli/program.mjs +3 -3
  7. package/dist/daemon/index.mjs +3 -3
  8. package/dist/daemon-B8qoEFRx.mjs +1356 -0
  9. package/dist/daemon-B8qoEFRx.mjs.map +1 -0
  10. package/dist/daemon-BrU1Q8rt.mjs +1356 -0
  11. package/dist/daemon-BrU1Q8rt.mjs.map +1 -0
  12. package/dist/hooks/capture-all-events.mjs.map +2 -2
  13. package/dist/hooks/cleanup-session-files.mjs.map +2 -2
  14. package/dist/hooks/context-compression-hook.mjs +67 -9
  15. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  16. package/dist/hooks/initialize-session.mjs.map +2 -2
  17. package/dist/hooks/inject-observations.mjs.map +2 -2
  18. package/dist/hooks/load-core-context.mjs.map +2 -2
  19. package/dist/hooks/load-project-context.mjs +5 -5
  20. package/dist/hooks/load-project-context.mjs.map +2 -2
  21. package/dist/hooks/observe.mjs.map +2 -2
  22. package/dist/hooks/stop-hook.mjs +84 -22
  23. package/dist/hooks/stop-hook.mjs.map +2 -2
  24. package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
  25. package/dist/main-resolver-BfSL8zio.mjs +1369 -0
  26. package/dist/main-resolver-BfSL8zio.mjs.map +1 -0
  27. package/dist/pick-BApRU7W5.mjs +13422 -0
  28. package/dist/pick-BApRU7W5.mjs.map +1 -0
  29. package/dist/pick-BFYPTFff.mjs +13444 -0
  30. package/dist/pick-BFYPTFff.mjs.map +1 -0
  31. package/dist/pick-CYYAvA-I.mjs +13403 -0
  32. package/dist/pick-CYYAvA-I.mjs.map +1 -0
  33. package/dist/pick-DGWeA1-Y.mjs +13444 -0
  34. package/dist/pick-DGWeA1-Y.mjs.map +1 -0
  35. package/dist/pick-wPMp5DBm.mjs +13444 -0
  36. package/dist/pick-wPMp5DBm.mjs.map +1 -0
  37. package/dist/work-queue-worker-BSryd-On.mjs +1857 -0
  38. package/dist/work-queue-worker-BSryd-On.mjs.map +1 -0
  39. package/dist/work-queue-worker-BsdNuTM2.mjs +1857 -0
  40. package/dist/work-queue-worker-BsdNuTM2.mjs.map +1 -0
  41. package/package.json +1 -1
  42. package/src/hooks/ts/lib/project-utils/todo.test.ts +15 -4
  43. package/src/hooks/ts/stop/stop-hook.ts +25 -13
@@ -0,0 +1,1369 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
+ import { _ as warn, c as ok, h as smartDecodeDir, i as err, l as renderTable, n as dim, o as header } from "./utils-BAxjW3j8.mjs";
3
+ import { t as aibrokerSocketPath } from "./runtime-paths-B0P1TvUr.mjs";
4
+ import { createReadStream, existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { basename, join } from "node:path";
7
+ import chalk from "chalk";
8
+ import { randomUUID } from "node:crypto";
9
+ import { connect } from "node:net";
10
+ import { spawnSync } from "node:child_process";
11
+ import { createInterface } from "node:readline";
12
+
13
+ //#region src/cli/lib/session-scan.ts
14
+ /**
15
+ * Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .
16
+ *
17
+ * Storage layout (Claude Code v2.1.143+):
18
+ *
19
+ * <project>/<uuid>.jsonl — top-level: metadata, system snapshots, hook events.
20
+ * REQUIRED for claude --resume to work.
21
+ * <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.
22
+ * 3000+ files, almost none resumable on their own.
23
+ *
24
+ * Resumability rule (empirically verified):
25
+ * A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one
26
+ * line of type "system". Sessions that only have a sessions/ counterpart cannot be
27
+ * resumed by Claude Code regardless of how much transcript content they have.
28
+ *
29
+ * Stale-UUID problem (fixed in this version):
30
+ * The clc session.json registry stores ONE uuid per named session — the uuid Claude Code
31
+ * had when the user last named the session. If the user resumes and the session gets a
32
+ * new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD
33
+ * uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project
34
+ * directory, not the clc-cached uuid.
35
+ *
36
+ * Resolution strategy:
37
+ * 1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).
38
+ * 2. For each, attach the clc name if the uuid matches (exact hit).
39
+ * 3. After Pass 1, for every clc registry entry:
40
+ * a. If the cached uuid was found → already handled.
41
+ * b. Find the encodedDir for this entry's directory.
42
+ * c. Check ALL sessions in that encodedDir (already in our Pass-1 results).
43
+ * d. Pick the MOST RECENT resumable session in that dir and attach the name to it.
44
+ * If no resumable session, fall through to transcript-only pass.
45
+ * 4. This means the displayed uuid for "Jobs Matthias" is always today's active session,
46
+ * not the stale cached one.
47
+ *
48
+ * Used by: pai sessions, pai resume
49
+ */
50
+ const CLC_SESSIONS_FILE = join(homedir(), ".claude", "session.json");
51
+ /** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */
52
+ function buildClcInfoMap() {
53
+ const map = /* @__PURE__ */ new Map();
54
+ try {
55
+ const raw = readFileSync(CLC_SESSIONS_FILE, "utf8");
56
+ const data = JSON.parse(raw);
57
+ for (const entry of data.sessions ?? []) {
58
+ const name = entry.name?.trim();
59
+ const uuid = (entry.resume ?? entry.session ?? "").trim();
60
+ if (name && uuid) map.set(uuid, {
61
+ name,
62
+ directory: entry.directory?.trim() || void 0
63
+ });
64
+ }
65
+ } catch {}
66
+ return map;
67
+ }
68
+ /** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */
69
+ function buildRegistryRootPathMap(db) {
70
+ try {
71
+ const rows = db.prepare(`SELECT root_path, encoded_dir FROM projects
72
+ WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`).all();
73
+ const map = /* @__PURE__ */ new Map();
74
+ for (const row of rows) map.set(row.encoded_dir, row.root_path);
75
+ return map;
76
+ } catch {
77
+ return /* @__PURE__ */ new Map();
78
+ }
79
+ }
80
+ function parseTopLevel(filePath) {
81
+ let systemLines = 0;
82
+ let size = 0;
83
+ let mtime = 0;
84
+ try {
85
+ const st = statSync(filePath);
86
+ size = st.size;
87
+ mtime = st.mtimeMs;
88
+ const content = readFileSync(filePath, "utf8");
89
+ for (const line of content.split("\n")) {
90
+ const t = line.trim();
91
+ if (!t) continue;
92
+ if (t.includes("\"type\":\"system\"") || t.includes("\"type\": \"system\"")) systemLines++;
93
+ }
94
+ } catch {}
95
+ return {
96
+ systemLines,
97
+ size,
98
+ mtime
99
+ };
100
+ }
101
+ function parseTranscript(filePath) {
102
+ let userLines = 0;
103
+ let lastUserPrompt = "";
104
+ let msgCount = 0;
105
+ let aiTitle;
106
+ let mtime = 0;
107
+ try {
108
+ mtime = statSync(filePath).mtimeMs;
109
+ } catch {
110
+ return {
111
+ userLines: 0,
112
+ lastUserPrompt: "",
113
+ msgCount: 0,
114
+ mtime: 0
115
+ };
116
+ }
117
+ let content;
118
+ try {
119
+ content = readFileSync(filePath, "utf8");
120
+ } catch {
121
+ return {
122
+ userLines: 0,
123
+ lastUserPrompt: "",
124
+ msgCount: 0,
125
+ mtime
126
+ };
127
+ }
128
+ for (const line of content.split("\n")) {
129
+ const t = line.trim();
130
+ if (!t) continue;
131
+ msgCount++;
132
+ const hasUser = t.includes("\"type\":\"user\"") || t.includes("\"type\": \"user\"");
133
+ const hasTitle = t.includes("\"type\":\"ai-title\"") || t.includes("\"type\": \"ai-title\"");
134
+ if (!hasUser && !hasTitle) continue;
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(t);
138
+ } catch {
139
+ continue;
140
+ }
141
+ if (parsed.type === "ai-title") {
142
+ const v = parsed.title ?? parsed.name;
143
+ if (typeof v === "string" && v.trim()) aiTitle = v.trim();
144
+ continue;
145
+ }
146
+ if (parsed.type !== "user") continue;
147
+ userLines++;
148
+ const msg = parsed.message;
149
+ if (!msg) continue;
150
+ const c = msg.content;
151
+ let text = "";
152
+ if (typeof c === "string") text = c;
153
+ else if (Array.isArray(c) && c.length > 0) {
154
+ const first = c[0];
155
+ if (typeof first.text === "string") text = first.text;
156
+ }
157
+ if (text) lastUserPrompt = text.slice(0, 80).replace(/\n/g, " ");
158
+ }
159
+ return {
160
+ userLines,
161
+ lastUserPrompt,
162
+ msgCount,
163
+ aiTitle,
164
+ mtime
165
+ };
166
+ }
167
+ const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
168
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
169
+ /**
170
+ * Compute the effective filter mode, resolving the legacy resumableOnly flag.
171
+ */
172
+ function resolveFilter(opts) {
173
+ if (opts.filter) return opts.filter;
174
+ if (opts.resumableOnly === true) return "resumable";
175
+ if (opts.resumableOnly === false) return "all";
176
+ return "named";
177
+ }
178
+ /**
179
+ * Scan ~/.claude/projects/ for all Claude Code sessions.
180
+ *
181
+ * Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).
182
+ * Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.
183
+ * For each such entry, scan the entry's project dir for the FRESHEST session
184
+ * and attach the registry name to it. This fixes the stale-UUID bug where
185
+ * clc's session.json points to an old uuid after a fresh start.
186
+ * Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.
187
+ *
188
+ * Filter modes:
189
+ * "named" — resumable + registry-known stubs + transcript-only (default)
190
+ * "all" — all of the above + unnamed orphans from sessions/ subdirs
191
+ * "resumable" — only sessions claude --resume accepts
192
+ *
193
+ * Returns results sorted by mtime descending.
194
+ */
195
+ function scanSessions(db, opts = {}) {
196
+ const limit = opts.limit ?? 200;
197
+ const filterMode = resolveFilter(opts);
198
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];
199
+ const clcInfoMap = buildClcInfoMap();
200
+ const rootPathMap = buildRegistryRootPathMap(db);
201
+ const results = [];
202
+ const seenUuids = /* @__PURE__ */ new Set();
203
+ const attachedClcUuids = /* @__PURE__ */ new Set();
204
+ let encodedDirs;
205
+ try {
206
+ encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);
207
+ } catch {
208
+ return [];
209
+ }
210
+ const sessionsByEncodedDir = /* @__PURE__ */ new Map();
211
+ for (const encodedDir of encodedDirs) {
212
+ const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);
213
+ try {
214
+ if (!statSync(projectDir).isDirectory()) continue;
215
+ } catch {
216
+ continue;
217
+ }
218
+ let files;
219
+ try {
220
+ files = readdirSync(projectDir);
221
+ } catch {
222
+ continue;
223
+ }
224
+ const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, "/");
225
+ const registryRootPath = rootPathMap.get(encodedDir);
226
+ const projectBasename = basename(decodedPath);
227
+ for (const file of files) {
228
+ if (!file.endsWith(".jsonl")) continue;
229
+ const uuid = file.slice(0, -6);
230
+ if (!UUID_RE.test(uuid)) continue;
231
+ const topLevelPath = join(projectDir, file);
232
+ const topInfo = parseTopLevel(topLevelPath);
233
+ const resumable = topInfo.systemLines > 0;
234
+ const clcInfo = clcInfoMap.get(uuid);
235
+ const inRegistry = !!clcInfo;
236
+ const sessionStatus = resumable ? "resumable" : inRegistry ? "stub" : "orphan";
237
+ const passesFilter = filterMode === "resumable" ? resumable : filterMode === "named" ? resumable || inRegistry : true;
238
+ const sessionJsonlPath = join(projectDir, "sessions", `${uuid}.jsonl`);
239
+ const hasTranscript = existsSync(sessionJsonlPath);
240
+ const transcript = hasTranscript ? parseTranscript(sessionJsonlPath) : {
241
+ userLines: 0,
242
+ lastUserPrompt: "",
243
+ msgCount: 0,
244
+ mtime: 0
245
+ };
246
+ const mtime = topInfo.mtime || transcript.mtime;
247
+ const friendlyName = clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? void 0;
248
+ const session = {
249
+ uuid,
250
+ shortId: uuid.slice(0, 8),
251
+ encodedDir,
252
+ decodedPath,
253
+ topLevelPath,
254
+ topLevelSystemLines: topInfo.systemLines,
255
+ topLevelSize: topInfo.size,
256
+ resumable,
257
+ sessionStatus,
258
+ sessionJsonlPath: hasTranscript ? sessionJsonlPath : void 0,
259
+ userLines: transcript.userLines,
260
+ lastUserPrompt: transcript.lastUserPrompt,
261
+ msgCount: transcript.msgCount,
262
+ aiTitle: transcript.aiTitle,
263
+ mtime,
264
+ friendlyName,
265
+ clcDirectory: clcInfo?.directory,
266
+ registryRootPath
267
+ };
268
+ if (!sessionsByEncodedDir.has(encodedDir)) sessionsByEncodedDir.set(encodedDir, []);
269
+ sessionsByEncodedDir.get(encodedDir).push(session);
270
+ seenUuids.add(uuid);
271
+ if (inRegistry) attachedClcUuids.add(uuid);
272
+ if (passesFilter) results.push(session);
273
+ }
274
+ }
275
+ if (filterMode !== "resumable") for (const [cachedUuid, clcInfo] of clcInfoMap) {
276
+ if (attachedClcUuids.has(cachedUuid)) continue;
277
+ let foundEncodedDir;
278
+ if (clcInfo.directory) {
279
+ const real = realpathSyncSafe(clcInfo.directory);
280
+ if (real) {
281
+ const encoded = encodeProjectDir(real);
282
+ if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) foundEncodedDir = encoded;
283
+ }
284
+ }
285
+ if (!foundEncodedDir) {
286
+ for (const encodedDir of encodedDirs) if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, "sessions", `${cachedUuid}.jsonl`))) {
287
+ foundEncodedDir = encodedDir;
288
+ break;
289
+ }
290
+ }
291
+ if (foundEncodedDir) {
292
+ const freshestResumable = (sessionsByEncodedDir.get(foundEncodedDir) ?? []).filter((s) => s.resumable && !s.friendlyName).sort((a, b) => b.mtime - a.mtime)[0];
293
+ if (freshestResumable) {
294
+ freshestResumable.friendlyName = clcInfo.name;
295
+ freshestResumable.clcDirectory = freshestResumable.clcDirectory ?? clcInfo.directory;
296
+ freshestResumable.sessionStatus = "resumable";
297
+ attachedClcUuids.add(freshestResumable.uuid);
298
+ if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {
299
+ if (!results.includes(freshestResumable)) results.push(freshestResumable);
300
+ }
301
+ continue;
302
+ }
303
+ }
304
+ if (!foundEncodedDir) {
305
+ const encodedDir = "";
306
+ const decodedPath = clcInfo.directory ?? cachedUuid;
307
+ const registryRootPath = void 0;
308
+ const mtime = {
309
+ userLines: 0,
310
+ lastUserPrompt: "",
311
+ msgCount: 0,
312
+ mtime: 0
313
+ }.mtime;
314
+ seenUuids.add(cachedUuid);
315
+ results.push({
316
+ uuid: cachedUuid,
317
+ shortId: cachedUuid.slice(0, 8),
318
+ encodedDir,
319
+ decodedPath,
320
+ topLevelPath: "",
321
+ topLevelSystemLines: 0,
322
+ topLevelSize: 0,
323
+ resumable: false,
324
+ sessionStatus: "transcript-only",
325
+ sessionJsonlPath: void 0,
326
+ userLines: 0,
327
+ lastUserPrompt: "",
328
+ msgCount: 0,
329
+ mtime,
330
+ friendlyName: clcInfo.name,
331
+ clcDirectory: clcInfo.directory,
332
+ registryRootPath
333
+ });
334
+ continue;
335
+ }
336
+ const foundTranscriptPath = existsSync(join(CLAUDE_PROJECTS_DIR, foundEncodedDir, "sessions", `${cachedUuid}.jsonl`)) ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, "sessions", `${cachedUuid}.jsonl`) : void 0;
337
+ const decodedPath = clcInfo.directory ?? smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, "/");
338
+ const registryRootPath = rootPathMap.get(foundEncodedDir);
339
+ const topLevelPath = join(CLAUDE_PROJECTS_DIR, foundEncodedDir, `${cachedUuid}.jsonl`);
340
+ const transcript = foundTranscriptPath ? parseTranscript(foundTranscriptPath) : {
341
+ userLines: 0,
342
+ lastUserPrompt: "",
343
+ msgCount: 0,
344
+ mtime: 0
345
+ };
346
+ seenUuids.add(cachedUuid);
347
+ results.push({
348
+ uuid: cachedUuid,
349
+ shortId: cachedUuid.slice(0, 8),
350
+ encodedDir: foundEncodedDir,
351
+ decodedPath,
352
+ topLevelPath,
353
+ topLevelSystemLines: 0,
354
+ topLevelSize: 0,
355
+ resumable: false,
356
+ sessionStatus: "transcript-only",
357
+ sessionJsonlPath: foundTranscriptPath,
358
+ userLines: transcript.userLines,
359
+ lastUserPrompt: transcript.lastUserPrompt,
360
+ msgCount: transcript.msgCount,
361
+ aiTitle: transcript.aiTitle,
362
+ mtime: transcript.mtime,
363
+ friendlyName: clcInfo.name,
364
+ clcDirectory: clcInfo.directory,
365
+ registryRootPath
366
+ });
367
+ }
368
+ results.sort((a, b) => b.mtime - a.mtime);
369
+ return results.slice(0, limit);
370
+ }
371
+ /** Claude Code's project-dir encoding: replace / . - (space) with - */
372
+ function encodeProjectDir(realPath) {
373
+ return realPath.replace(/[/.\- ]/g, "-");
374
+ }
375
+ /** realpathSync that returns null instead of throwing */
376
+ function realpathSyncSafe(p) {
377
+ try {
378
+ return realpathSync(p);
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+ function fmtAge(mtime) {
384
+ const diffMs = Date.now() - mtime;
385
+ const diffMin = Math.floor(diffMs / 6e4);
386
+ const diffHr = Math.floor(diffMs / 36e5);
387
+ const diffDay = Math.floor(diffMs / 864e5);
388
+ if (diffMin < 60) return `${diffMin}m`;
389
+ if (diffHr < 24) return `${diffHr}h`;
390
+ if (diffDay < 30) return `${diffDay}d`;
391
+ return `${Math.floor(diffDay / 30)}mo`;
392
+ }
393
+ /**
394
+ * Filesystem-level UUID scan: walk ALL ~/.claude/projects/<encoded-dir>/<uuid>.jsonl
395
+ * looking for top-level files whose UUID starts with `prefix`.
396
+ *
397
+ * Returns a ScannedSession-like object (minimal fields) for each match, or an empty
398
+ * array if nothing is found. This is used as a fallback when the regular catalog
399
+ * (limited to named/recent sessions) doesn't contain the requested UUID.
400
+ *
401
+ * Complexity: O(number of project directories + files per dir). Typically <200 dirs
402
+ * with a handful of top-level jsonl each — fast enough for an interactive CLI.
403
+ */
404
+ function scanFilesystemForUuidPrefix(prefix) {
405
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];
406
+ const prefixLower = prefix.toLowerCase();
407
+ const matches = [];
408
+ let encodedDirs;
409
+ try {
410
+ encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);
411
+ } catch {
412
+ return [];
413
+ }
414
+ for (const encodedDir of encodedDirs) {
415
+ const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);
416
+ try {
417
+ if (!statSync(projectDir).isDirectory()) continue;
418
+ } catch {
419
+ continue;
420
+ }
421
+ let files;
422
+ try {
423
+ files = readdirSync(projectDir);
424
+ } catch {
425
+ continue;
426
+ }
427
+ for (const file of files) {
428
+ if (!file.endsWith(".jsonl")) continue;
429
+ const uuid = file.slice(0, -6);
430
+ if (!UUID_RE.test(uuid)) continue;
431
+ if (!uuid.toLowerCase().startsWith(prefixLower)) continue;
432
+ const topLevelPath = join(projectDir, file);
433
+ const topInfo = parseTopLevel(topLevelPath);
434
+ const resumable = topInfo.systemLines > 0;
435
+ const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, "/");
436
+ const sessionJsonlPath = join(projectDir, "sessions", `${uuid}.jsonl`);
437
+ const hasTranscript = existsSync(sessionJsonlPath);
438
+ const transcript = hasTranscript ? parseTranscript(sessionJsonlPath) : {
439
+ userLines: 0,
440
+ lastUserPrompt: "",
441
+ msgCount: 0,
442
+ mtime: 0
443
+ };
444
+ matches.push({
445
+ uuid,
446
+ shortId: uuid.slice(0, 8),
447
+ encodedDir,
448
+ decodedPath,
449
+ topLevelPath,
450
+ topLevelSystemLines: topInfo.systemLines,
451
+ topLevelSize: topInfo.size,
452
+ resumable,
453
+ sessionStatus: resumable ? "resumable" : "stub",
454
+ sessionJsonlPath: hasTranscript ? sessionJsonlPath : void 0,
455
+ userLines: transcript.userLines,
456
+ lastUserPrompt: transcript.lastUserPrompt,
457
+ msgCount: transcript.msgCount,
458
+ aiTitle: transcript.aiTitle,
459
+ mtime: topInfo.mtime || transcript.mtime,
460
+ friendlyName: transcript.aiTitle ?? basename(decodedPath),
461
+ clcDirectory: void 0,
462
+ registryRootPath: void 0
463
+ });
464
+ }
465
+ }
466
+ return matches.sort((a, b) => b.mtime - a.mtime);
467
+ }
468
+ /**
469
+ * Resolve a name-or-id-or-prefix to a single ScannedSession.
470
+ *
471
+ * Comparisons are case-insensitive; stored casing is preserved in output.
472
+ *
473
+ * Priority:
474
+ * 1. Exact case-insensitive match on friendlyName
475
+ * 2. Partial case-insensitive match (contains)
476
+ * 3. UUID prefix match against the in-memory catalog
477
+ * 4. UUID prefix match against the full filesystem (fallback for any session)
478
+ */
479
+ function resolveSessionByNameOrId(sessions, query) {
480
+ const qLower = query.toLowerCase().trim();
481
+ const byExact = sessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower);
482
+ if (byExact.length >= 1) return {
483
+ session: byExact[0],
484
+ friendlyName: byExact[0].friendlyName
485
+ };
486
+ const byPartial = sessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower));
487
+ if (byPartial.length === 1) return {
488
+ session: byPartial[0],
489
+ friendlyName: byPartial[0].friendlyName
490
+ };
491
+ if (byPartial.length > 1) {
492
+ const candidates = byPartial.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
493
+ throw new Error(`Ambiguous name "${query}" — ${byPartial.length} matches:\n${candidates}\n\nBe more specific or use a UUID prefix.`);
494
+ }
495
+ const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));
496
+ if (byUuid.length === 1) return {
497
+ session: byUuid[0],
498
+ friendlyName: byUuid[0].friendlyName
499
+ };
500
+ if (byUuid.length > 1) {
501
+ const candidates = byUuid.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
502
+ throw new Error(`UUID prefix "${query}" is ambiguous — ${byUuid.length} matches:\n${candidates}\n\nProvide more characters.`);
503
+ }
504
+ if (/^[0-9a-f-]{4,36}$/i.test(qLower)) {
505
+ const fsSessions = scanFilesystemForUuidPrefix(qLower);
506
+ if (fsSessions.length === 1) return {
507
+ session: fsSessions[0],
508
+ friendlyName: fsSessions[0].friendlyName
509
+ };
510
+ if (fsSessions.length > 1) {
511
+ const candidates = fsSessions.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
512
+ throw new Error(`UUID prefix "${query}" is ambiguous — ${fsSessions.length} matches:\n${candidates}\n\nProvide more characters.`);
513
+ }
514
+ }
515
+ throw new Error(`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.`);
516
+ }
517
+
518
+ //#endregion
519
+ //#region src/cli/lib/aibroker-client.ts
520
+ /**
521
+ * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.
522
+ *
523
+ * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,
524
+ * and reads a single newline-terminated JSON response. No class needed —
525
+ * just a thin async function matching the WatcherClient protocol.
526
+ *
527
+ * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).
528
+ */
529
+ const DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? aibrokerSocketPath();
530
+ /**
531
+ * How long to wait for `send_to_session`, which must exceed the server's own
532
+ * ack window or the caller gives up while the callee is still working.
533
+ *
534
+ * AIBroker waits up to SEND_ACK_TIMEOUT_MS (15s) for the submit confirmation —
535
+ * the text leaving the input line — before answering. The generic client
536
+ * timeout is 8s. So every send needing 8-15s to confirm reported
537
+ * "AIBroker IPC call timed out" while succeeding, and the handler's eventual
538
+ * `delivered: true` was written into a socket nobody was reading any more.
539
+ *
540
+ * Measured on `pai pause all` across 15 sessions: 9 reported failed, 8 of them
541
+ * verifiably paused. A caller's deadline shorter than the callee's is not a
542
+ * tuning question, it is a guaranteed false negative on every slow success.
543
+ */
544
+ const SEND_TIMEOUT_MS = 3e4;
545
+ /**
546
+ * Call an AIBroker IPC method and return the result.
547
+ *
548
+ * Resolves with the `result` field of a successful response.
549
+ * Rejects if the socket is not available, the call times out, or the
550
+ * daemon returns an error.
551
+ *
552
+ * @param method IPC method name (e.g. "session_content", "send_to_session")
553
+ * @param params Method parameters object
554
+ * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)
555
+ */
556
+ function callAiBroker(method, params = {}, timeoutMs = 8e3) {
557
+ return new Promise((resolve, reject) => {
558
+ const socketPath = DEFAULT_SOCKET;
559
+ let done = false;
560
+ let buffer = "";
561
+ let timer = null;
562
+ function finish(err, value) {
563
+ if (done) return;
564
+ done = true;
565
+ if (timer !== null) {
566
+ clearTimeout(timer);
567
+ timer = null;
568
+ }
569
+ try {
570
+ socket.destroy();
571
+ } catch {}
572
+ if (err) reject(err);
573
+ else resolve(value);
574
+ }
575
+ const socket = connect(socketPath, () => {
576
+ const request = {
577
+ id: randomUUID(),
578
+ sessionId: process.env.TERM_SESSION_ID ?? "pai-cli",
579
+ method,
580
+ params
581
+ };
582
+ const itermId = process.env.ITERM_SESSION_ID;
583
+ if (itermId) Object.assign(request, { itermSessionId: itermId });
584
+ socket.write(JSON.stringify(request) + "\n");
585
+ });
586
+ socket.on("data", (chunk) => {
587
+ buffer += chunk.toString("utf8");
588
+ const nl = buffer.indexOf("\n");
589
+ if (nl === -1) return;
590
+ const line = buffer.slice(0, nl);
591
+ let response;
592
+ try {
593
+ response = JSON.parse(line);
594
+ } catch {
595
+ finish(/* @__PURE__ */ new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));
596
+ return;
597
+ }
598
+ if (!response.ok) finish(new Error(response.error ?? "AIBroker IPC call failed"));
599
+ else finish(null, response.result ?? {});
600
+ });
601
+ socket.on("error", (e) => {
602
+ if (e.code === "ENOENT" || e.code === "ECONNREFUSED") finish(/* @__PURE__ */ new Error("AIBroker not running (socket not found)."));
603
+ else finish(e);
604
+ });
605
+ socket.on("end", () => {
606
+ if (!done) finish(/* @__PURE__ */ new Error("AIBroker IPC connection closed before response."));
607
+ });
608
+ timer = setTimeout(() => finish(/* @__PURE__ */ new Error("AIBroker IPC call timed out.")), timeoutMs);
609
+ });
610
+ }
611
+ /**
612
+ * Fetch all live iTerm2 session metadata from AIBroker via the `sessions` method.
613
+ * Returns an empty array if AIBroker is not running.
614
+ *
615
+ * This is metadata-only (no scrollback). It is faster than `session_content`
616
+ * and the correct source for listing/routing purposes.
617
+ */
618
+ async function fetchLiveSessions() {
619
+ try {
620
+ const sessions = (await callAiBroker("sessions", {})).sessions;
621
+ if (!Array.isArray(sessions)) return [];
622
+ return sessions;
623
+ } catch {
624
+ return [];
625
+ }
626
+ }
627
+ /**
628
+ * Send text to a specific AIBroker session by its iTerm2 sessionId.
629
+ *
630
+ * The wire keys are `target` and `message`, and neither is negotiable — the
631
+ * handler rejects anything else with "target is required" before it looks at
632
+ * the rest. This was written as `{ target: sessionId, message: text }`, which is the shape of
633
+ * THIS function's own parameters rather than the shape of the IPC, so every
634
+ * call failed identically. `pai pause all` was the only caller, so the live
635
+ * path had never once executed: its --dry-run branch returns before sending,
636
+ * and that was the only branch anyone had exercised.
637
+ *
638
+ * The caller must NOT terminate `text` with a newline. The transport sends with
639
+ * `enter: true` and appends the Enter itself, so a trailing \n submits twice —
640
+ * once for the text and once for an empty prompt.
641
+ */
642
+ async function sendToSession(sessionId, text, timeoutMs = SEND_TIMEOUT_MS) {
643
+ try {
644
+ await callAiBroker("send_to_session", {
645
+ target: sessionId,
646
+ message: text
647
+ }, timeoutMs);
648
+ return { ok: true };
649
+ } catch (e) {
650
+ const error = String(e);
651
+ return {
652
+ ok: false,
653
+ error,
654
+ timedOut: /timed out/i.test(error)
655
+ };
656
+ }
657
+ }
658
+ /**
659
+ * Switch iTerm2 focus to the session identified by `target`.
660
+ *
661
+ * `target` can be a sessionId, paiName, or tab index number (as string).
662
+ * After switching, activates the iTerm2 application itself so the window
663
+ * comes to the foreground.
664
+ *
665
+ * Returns { ok: true } if AIBroker confirmed the switch, or
666
+ * { ok: false, error } if AIBroker is not running or the session was not found.
667
+ */
668
+ async function switchToSession(target) {
669
+ try {
670
+ await callAiBroker("switch", { target });
671
+ const { spawnSync } = await import("node:child_process");
672
+ spawnSync("osascript", ["-e", "tell application \"iTerm\" to activate"], { stdio: "ignore" });
673
+ return { ok: true };
674
+ } catch (e) {
675
+ return {
676
+ ok: false,
677
+ error: String(e)
678
+ };
679
+ }
680
+ }
681
+ /**
682
+ * Bring the iTerm2 tab containing a specific session to the front.
683
+ *
684
+ * This is the mechanism AIBroker's screenshot path uses: match the session by
685
+ * its iTerm2 session id (the `sessionId` returned by `fetchLiveSessions`, which
686
+ * is iTerm's own `id of session`), then `select` its window, tab, and session.
687
+ * Unlike the `switch` IPC (which only flips an internal index and never touches
688
+ * iTerm), this actually reveals the tab.
689
+ */
690
+ function revealItermSession(itermSessionId) {
691
+ const script = `tell application "iTerm2"
692
+ activate
693
+ repeat with w in windows
694
+ repeat with t in tabs of w
695
+ repeat with s in sessions of t
696
+ if id of s is "${itermSessionId.replace(/^iterm:/i, "").trim()}" then
697
+ select w
698
+ select t
699
+ select s
700
+ return "ok"
701
+ end if
702
+ end repeat
703
+ end repeat
704
+ end repeat
705
+ return "not-found"
706
+ end tell`;
707
+ try {
708
+ const r = spawnSync("osascript", ["-e", script], { encoding: "utf8" });
709
+ if (r.status !== 0) return {
710
+ ok: false,
711
+ error: (r.stderr || "osascript failed").trim()
712
+ };
713
+ if ((r.stdout ?? "").trim() === "ok") return { ok: true };
714
+ return {
715
+ ok: false,
716
+ error: "session not found in any iTerm2 window"
717
+ };
718
+ } catch (e) {
719
+ return {
720
+ ok: false,
721
+ error: String(e)
722
+ };
723
+ }
724
+ }
725
+
726
+ //#endregion
727
+ //#region src/cli/lib/exit-dir.ts
728
+ /**
729
+ * Print the working directory of a `claude` session that pai launched, right
730
+ * after it exits. This lets the user cd back into the same directory and
731
+ * restart claude to continue — Claude Code's own `--resume` hint is unreliable
732
+ * for this workflow.
733
+ *
734
+ * Why here and not a SessionEnd hook: a hook inside Claude Code is cancelled
735
+ * during teardown (anthropics/claude-code#41577) and races the terminal
736
+ * restore, so it can't reliably print anything. pai spawns the `claude` binary
737
+ * directly (no shell), so the `claude()` shell wrapper never sees pai-launched
738
+ * sessions either. Printing here — after spawnSync has returned and claude has
739
+ * fully exited — is the one place that reliably reaches the terminal.
740
+ */
741
+ function printExitDir(dir) {
742
+ process.stdout.write(`\n\x1b[2m📂 Working directory:\x1b[0m ${dir}\n\x1b[2m cd "${dir}"\x1b[0m\n`);
743
+ }
744
+
745
+ //#endregion
746
+ //#region src/cli/lib/history-search.ts
747
+ /**
748
+ * history-search.ts
749
+ *
750
+ * Content-based search across ~/.claude/history.jsonl.
751
+ * Streams the file (avoids loading 26k+ lines into memory), groups matching
752
+ * lines by sessionId, and returns results sorted by most-recent match.
753
+ *
754
+ * Used by: pai <query> (main resolver) and pai find (compat alias)
755
+ */
756
+ const HISTORY_FILE = join(homedir(), ".claude", "history.jsonl");
757
+ /**
758
+ * Search ~/.claude/history.jsonl for prompts matching the query (case-insensitive
759
+ * substring). Results are grouped by sessionId and sorted by most-recent match.
760
+ *
761
+ * Entries without a sessionId (old Claude Code versions) are excluded from
762
+ * results since they can't be resumed.
763
+ */
764
+ async function searchHistory(query, maxResults) {
765
+ if (!existsSync(HISTORY_FILE)) return [];
766
+ const queryLower = query.toLowerCase();
767
+ const bySession = /* @__PURE__ */ new Map();
768
+ const rl = createInterface({
769
+ input: createReadStream(HISTORY_FILE, { encoding: "utf8" }),
770
+ crlfDelay: Infinity
771
+ });
772
+ for await (const line of rl) {
773
+ const t = line.trim();
774
+ if (!t) continue;
775
+ let entry;
776
+ try {
777
+ entry = JSON.parse(t);
778
+ } catch {
779
+ continue;
780
+ }
781
+ const display = entry.display ?? "";
782
+ if (!display.toLowerCase().includes(queryLower)) continue;
783
+ const ts = entry.timestamp ?? 0;
784
+ const project = entry.project ?? "";
785
+ const sessionId = entry.sessionId ?? null;
786
+ if (!sessionId) continue;
787
+ const existing = bySession.get(sessionId);
788
+ if (!existing) bySession.set(sessionId, {
789
+ sessionId,
790
+ lastMatchTs: ts,
791
+ lastMatchDisplay: display,
792
+ project,
793
+ matchCount: 1
794
+ });
795
+ else {
796
+ existing.matchCount++;
797
+ if (ts > existing.lastMatchTs) {
798
+ existing.lastMatchTs = ts;
799
+ existing.lastMatchDisplay = display;
800
+ existing.project = project;
801
+ }
802
+ }
803
+ }
804
+ return [...bySession.values()].sort((a, b) => b.lastMatchTs - a.lastMatchTs).slice(0, maxResults);
805
+ }
806
+
807
+ //#endregion
808
+ //#region src/cli/lib/dedup-sessions.ts
809
+ const STATUS_PRIORITY = {
810
+ live: 0,
811
+ resumable: 1,
812
+ "transcript-only": 2,
813
+ stub: 3,
814
+ project: 4,
815
+ orphan: 5
816
+ };
817
+ /**
818
+ * Strip Claude Code spinner characters, decoration prefixes, and " (node)" suffixes
819
+ * from a session name so that "✳ Chenarlier (node)" and "Chenarlier" group together.
820
+ *
821
+ * Handles:
822
+ * - Unicode braille spinner frames: ⠐ ⠂ ⠁ ⠈ ⠘ ⠙ ⠚ ⠛ ⠗ ⠝
823
+ * - Decoration characters: ✻ ✳ ✲ ✼ ✦ * ★ ☆ • ●
824
+ * - Any leading non-letter/digit characters followed by whitespace
825
+ * - Trailing " (node)" suffix (Claude Code appends this on some platforms)
826
+ */
827
+ function normalizeName(s) {
828
+ return s.replace(/^[⠀-⣿✀-➿✻✳✲✼✦*★☆•●⠐⠂⠁⠈⠘⠙⠚⠛⠗⠝]+\s*/u, "").replace(/^[^a-zA-Z0-9À-ÿ -鿿]+\s*/u, "").replace(/\s*\(node\)\s*$/i, "").trim();
829
+ }
830
+ /**
831
+ * Normalize a slug (kebab-case) to a human-readable form for matching.
832
+ * "jobs-grazyna" → "jobs grazyna"
833
+ */
834
+ function slugToWords(slug) {
835
+ return slug.replace(/-/g, " ");
836
+ }
837
+ /**
838
+ * Merge live (AIBroker) + disk (session-scan) + registry (projects DB) sessions
839
+ * into a deduped catalog.
840
+ *
841
+ * Algorithm:
842
+ * 1. Normalize names before grouping
843
+ * 2. Group by normalized name (case-insensitive)
844
+ * 3. Within each group, rank by STATUS_PRIORITY (live wins)
845
+ * 4. Within same priority, keep the most recent (highest mtime)
846
+ * 5. Sort output: by priority, then by lastActivity desc
847
+ * 6. Filter idle project entries (default) or show all (showAll=true)
848
+ *
849
+ * @param showAll When true, include cold / zero-session / archived projects.
850
+ */
851
+ function buildDeduped(liveSessions, diskSessions, registeredProjects, showAll = false) {
852
+ const byName = /* @__PURE__ */ new Map();
853
+ for (const s of liveSessions) {
854
+ if (s.kind === "shell") continue;
855
+ const normalized = normalizeName(s.paiName ?? s.sessionId.slice(0, 8));
856
+ const key = normalized.toLowerCase();
857
+ const entry = {
858
+ name: normalized,
859
+ status: "live",
860
+ liveSessionId: s.sessionId,
861
+ lastActivity: Date.now(),
862
+ project: "",
863
+ lastPrompt: s.lastPrompt ?? ""
864
+ };
865
+ const existing = byName.get(key);
866
+ if (!existing || STATUS_PRIORITY["live"] < STATUS_PRIORITY[existing.status]) byName.set(key, entry);
867
+ }
868
+ for (const s of diskSessions) {
869
+ const normalized = normalizeName(s.friendlyName ?? s.shortId);
870
+ const key = normalized.toLowerCase();
871
+ let status;
872
+ if (s.resumable) status = "resumable";
873
+ else if (s.sessionStatus === "transcript-only") status = "transcript-only";
874
+ else if (s.sessionStatus === "stub") status = "stub";
875
+ else status = "orphan";
876
+ const entry = {
877
+ name: normalized,
878
+ status,
879
+ diskSession: s,
880
+ lastActivity: s.mtime,
881
+ project: s.decodedPath ?? "",
882
+ lastPrompt: s.lastUserPrompt ?? ""
883
+ };
884
+ const existing = byName.get(key);
885
+ if (!existing) byName.set(key, entry);
886
+ else {
887
+ const ep = STATUS_PRIORITY[existing.status];
888
+ const np = STATUS_PRIORITY[status];
889
+ if (np < ep || np === ep && entry.lastActivity > existing.lastActivity) byName.set(key, entry);
890
+ }
891
+ }
892
+ const COLD_THRESHOLD_MS = 2160 * 60 * 60 * 1e3;
893
+ const now = Date.now();
894
+ if (registeredProjects) for (const p of registeredProjects) {
895
+ if (!showAll && p.status !== "active") continue;
896
+ const normalized = normalizeName(p.display_name ?? p.slug);
897
+ const key = normalized.toLowerCase();
898
+ const slugKey = slugToWords(p.slug).toLowerCase();
899
+ if (!byName.has(key) && !byName.has(slugKey)) {
900
+ if (!showAll) {
901
+ if (p.session_count === 0) continue;
902
+ if (p.session_count < 3) {
903
+ if (now - (p.last_active ?? 0) > COLD_THRESHOLD_MS) continue;
904
+ }
905
+ }
906
+ byName.set(key, {
907
+ name: normalized,
908
+ slug: p.slug,
909
+ status: "project",
910
+ lastActivity: p.last_active ?? 0,
911
+ project: p.root_path,
912
+ lastPrompt: "",
913
+ sessionCount: p.session_count
914
+ });
915
+ } else {
916
+ const existing = byName.get(key) ?? byName.get(slugKey);
917
+ if (existing && !existing.slug) existing.slug = p.slug;
918
+ if (existing && existing.sessionCount === void 0) existing.sessionCount = p.session_count;
919
+ }
920
+ }
921
+ const entries = Array.from(byName.values());
922
+ entries.sort((a, b) => {
923
+ const pa = STATUS_PRIORITY[a.status];
924
+ const pb = STATUS_PRIORITY[b.status];
925
+ if (pa !== pb) return pa - pb;
926
+ return b.lastActivity - a.lastActivity;
927
+ });
928
+ return entries;
929
+ }
930
+ function fmtUnifiedStatus(s) {
931
+ switch (s) {
932
+ case "live": return chalk.green("live");
933
+ case "resumable": return chalk.cyan("resumable");
934
+ case "transcript-only": return chalk.dim("transcript");
935
+ case "stub": return chalk.dim("stub");
936
+ case "project": return chalk.dim("idle");
937
+ case "orphan": return chalk.dim("orphan");
938
+ }
939
+ }
940
+ function shortenProject$1(p, maxLen = 28) {
941
+ if (!p || p.length <= maxLen) return p || dim("—");
942
+ return "…" + p.slice(-(maxLen - 1));
943
+ }
944
+ /**
945
+ * Render the deduped session catalog to stdout.
946
+ *
947
+ * @param entries Output of buildDeduped()
948
+ * @param maxRows Maximum rows to display. undefined = no limit (for --all).
949
+ */
950
+ function renderDedupedSessions(entries, maxRows) {
951
+ if (entries.length === 0) {
952
+ console.log(warn("No sessions found. Start Claude Code in a project directory first."));
953
+ return;
954
+ }
955
+ console.log("\n" + header("Sessions") + "\n");
956
+ const visible = maxRows !== void 0 ? entries.slice(0, maxRows) : entries;
957
+ const tableHeaders = [
958
+ "#",
959
+ "name",
960
+ "status",
961
+ "age",
962
+ "project",
963
+ "last prompt"
964
+ ];
965
+ const tableRows = visible.map((entry, i) => {
966
+ const age = entry.status === "live" ? chalk.green("now") : entry.lastActivity > 0 ? dim(fmtAge(entry.lastActivity)) : dim("—");
967
+ const snippet = entry.lastPrompt.replace(/\n+/g, " ").trim().slice(0, 36);
968
+ const project = dim(shortenProject$1(entry.diskSession ? entry.diskSession.friendlyName ?? entry.diskSession.decodedPath : entry.project, 28));
969
+ return [
970
+ dim(String(i + 1)),
971
+ chalk.white(entry.name),
972
+ fmtUnifiedStatus(entry.status),
973
+ age,
974
+ project,
975
+ chalk.dim(snippet ? `"${snippet}"` : "—")
976
+ ];
977
+ });
978
+ console.log(renderTable(tableHeaders, tableRows));
979
+ if (maxRows !== void 0 && entries.length > maxRows) console.log(dim(` … ${entries.length - maxRows} more — use --all to show everything`));
980
+ console.log();
981
+ console.log(dim(" Switch/resume/start: ") + chalk.white("pai <name>") + dim(" or ") + chalk.white("pai <uuid-prefix>"));
982
+ console.log();
983
+ }
984
+
985
+ //#endregion
986
+ //#region src/cli/commands/main-resolver.ts
987
+ var main_resolver_exports = /* @__PURE__ */ __exportAll({ cmdMain: () => cmdMain });
988
+ function probeResume(uuid, cwd) {
989
+ const result = spawnSync("claude", [
990
+ "--resume",
991
+ uuid,
992
+ "--print",
993
+ "--output-format=json",
994
+ "_"
995
+ ], {
996
+ cwd,
997
+ timeout: 5e3,
998
+ env: process.env,
999
+ stdio: [
1000
+ "ignore",
1001
+ "ignore",
1002
+ "pipe"
1003
+ ]
1004
+ });
1005
+ if (result.error) return {
1006
+ ok: false,
1007
+ reason: `spawn error: ${result.error.message}`
1008
+ };
1009
+ const stderr = result.stderr?.toString("utf8") ?? "";
1010
+ if (stderr.toLowerCase().includes("no conversation found") || stderr.toLowerCase().includes("session not found")) return {
1011
+ ok: false,
1012
+ reason: "No conversation found for this UUID"
1013
+ };
1014
+ if (result.status !== 0) return {
1015
+ ok: false,
1016
+ reason: `claude exited ${result.status ?? "signal"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : ""}`
1017
+ };
1018
+ return { ok: true };
1019
+ }
1020
+ function launchSession(session, allSessions, dryRun) {
1021
+ let resumableUuid;
1022
+ if (session.resumable) resumableUuid = session.uuid;
1023
+ else if (session.encodedDir) {
1024
+ const sameProject = allSessions.filter((s) => s.encodedDir === session.encodedDir && s.resumable);
1025
+ sameProject.sort((a, b) => b.mtime - a.mtime);
1026
+ if (sameProject.length > 0) resumableUuid = sameProject[0].uuid;
1027
+ }
1028
+ const rawDir = session.clcDirectory ?? session.registryRootPath ?? session.decodedPath;
1029
+ let projectDir;
1030
+ try {
1031
+ projectDir = realpathSync(rawDir);
1032
+ } catch {
1033
+ console.error(err(`Session directory does not exist or cannot be resolved.\n Path: ${rawDir}\n The directory may have moved or been deleted.`));
1034
+ process.exit(1);
1035
+ return;
1036
+ }
1037
+ const name = session.friendlyName ?? session.shortId;
1038
+ const promptArg = `/Name ${name}\ngo`;
1039
+ if (dryRun) {
1040
+ if (resumableUuid) {
1041
+ console.log("\n" + chalk.bold("Dry run — would probe then exec (RESUME path):") + "\n");
1042
+ console.log(` cwd: ${chalk.cyan(projectDir)}`);
1043
+ console.log(` probe: claude --resume ${resumableUuid} --print --output-format=json "_"`);
1044
+ console.log(` argv: claude --resume ${resumableUuid} --name "${name}" "/Name ${name}\\ngo"`);
1045
+ console.log(` fallback: claude --name "${name}" "/Name ${name}\\ngo"`);
1046
+ } else {
1047
+ console.log("\n" + chalk.bold("Dry run — would exec (FRESH path):") + "\n");
1048
+ console.log(` cwd: ${chalk.cyan(projectDir)}`);
1049
+ console.log(` argv: claude --name "${name}" "/Name ${name}\\ngo"`);
1050
+ }
1051
+ console.log();
1052
+ return;
1053
+ }
1054
+ if (resumableUuid) {
1055
+ const probe = probeResume(resumableUuid, projectDir);
1056
+ if (probe.ok) {
1057
+ const result = spawnSync("claude", [
1058
+ "--resume",
1059
+ resumableUuid,
1060
+ "--name",
1061
+ name,
1062
+ promptArg
1063
+ ], {
1064
+ cwd: projectDir,
1065
+ stdio: "inherit",
1066
+ env: process.env
1067
+ });
1068
+ if (result.error) {
1069
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
1070
+ process.exit(1);
1071
+ }
1072
+ printExitDir(projectDir);
1073
+ process.exit(result.status ?? 0);
1074
+ } else {
1075
+ process.stderr.write(chalk.yellow(`\n Resume failed for ${resumableUuid.slice(0, 8)}: ${probe.reason ?? "unknown error"}\n Starting fresh session in same directory.\n\n`));
1076
+ const result = spawnSync("claude", [
1077
+ "--name",
1078
+ name,
1079
+ promptArg
1080
+ ], {
1081
+ cwd: projectDir,
1082
+ stdio: "inherit",
1083
+ env: process.env
1084
+ });
1085
+ if (result.error) {
1086
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
1087
+ process.exit(1);
1088
+ }
1089
+ printExitDir(projectDir);
1090
+ process.exit(result.status ?? 0);
1091
+ }
1092
+ } else {
1093
+ const result = spawnSync("claude", [
1094
+ "--name",
1095
+ name,
1096
+ promptArg
1097
+ ], {
1098
+ cwd: projectDir,
1099
+ stdio: "inherit",
1100
+ env: process.env
1101
+ });
1102
+ if (result.error) {
1103
+ console.error(err(`Failed to launch claude: ${result.error.message}`));
1104
+ process.exit(1);
1105
+ }
1106
+ printExitDir(projectDir);
1107
+ process.exit(result.status ?? 0);
1108
+ }
1109
+ }
1110
+ function matchToSession(match, allSessions) {
1111
+ if (!match.sessionId) return null;
1112
+ const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);
1113
+ if (catalogMatch) return catalogMatch;
1114
+ if (!match.project) return null;
1115
+ return {
1116
+ uuid: match.sessionId,
1117
+ shortId: match.sessionId.slice(0, 8),
1118
+ encodedDir: "",
1119
+ decodedPath: match.project,
1120
+ topLevelPath: "",
1121
+ topLevelSystemLines: 0,
1122
+ topLevelSize: 0,
1123
+ resumable: false,
1124
+ sessionStatus: "transcript-only",
1125
+ sessionJsonlPath: void 0,
1126
+ userLines: 0,
1127
+ lastUserPrompt: match.lastMatchDisplay.slice(0, 80),
1128
+ msgCount: 0,
1129
+ mtime: match.lastMatchTs,
1130
+ friendlyName: void 0,
1131
+ clcDirectory: void 0,
1132
+ registryRootPath: match.project
1133
+ };
1134
+ }
1135
+ function fmtTs(ts) {
1136
+ const d = new Date(ts);
1137
+ const pad = (n) => String(n).padStart(2, "0");
1138
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1139
+ }
1140
+ function shortenProject(p, maxLen = 44) {
1141
+ if (!p || p.length <= maxLen) return p || dim("—");
1142
+ return "…" + p.slice(-(maxLen - 1));
1143
+ }
1144
+ async function askForChoice(max) {
1145
+ return new Promise((resolve) => {
1146
+ const rl = createInterface({
1147
+ input: process.stdin,
1148
+ output: process.stdout
1149
+ });
1150
+ rl.question(dim(`\n Enter # to launch (1-${max}), or press Enter to cancel: `), (answer) => {
1151
+ rl.close();
1152
+ const n = parseInt(answer.trim(), 10);
1153
+ if (!isNaN(n) && n >= 1 && n <= max) resolve(n);
1154
+ else resolve(null);
1155
+ });
1156
+ });
1157
+ }
1158
+ async function doSwitch(entry, dryRun) {
1159
+ if (!entry.liveSessionId) return false;
1160
+ if (dryRun) {
1161
+ console.log("\n" + chalk.bold("Dry run — would switch iTerm tab:") + "\n");
1162
+ console.log(` target: ${entry.name} (${entry.liveSessionId.slice(0, 8)})`);
1163
+ console.log(` action: aibroker_switch + osascript iTerm activate`);
1164
+ console.log();
1165
+ return true;
1166
+ }
1167
+ const result = await switchToSession(entry.liveSessionId);
1168
+ if (result.ok) {
1169
+ console.log(ok(`Switched to live session: ${chalk.white(entry.name)} ` + chalk.dim(`(${entry.liveSessionId.slice(0, 8)})`)));
1170
+ return true;
1171
+ }
1172
+ console.error(warn(`Could not switch via AIBroker: ${result.error ?? "unknown error"}`));
1173
+ return false;
1174
+ }
1175
+ function getRegisteredProjects(db, all = false) {
1176
+ try {
1177
+ const statusClause = all ? "" : "WHERE p.status = 'active'";
1178
+ return db.prepare(`
1179
+ SELECT
1180
+ p.slug,
1181
+ p.display_name,
1182
+ p.root_path,
1183
+ p.status,
1184
+ COUNT(s.id) AS session_count,
1185
+ MAX(s.created_at) AS last_active
1186
+ FROM projects p
1187
+ LEFT JOIN sessions s ON s.project_id = p.id
1188
+ ${statusClause}
1189
+ GROUP BY p.id
1190
+ ORDER BY last_active DESC NULLS LAST, p.updated_at DESC
1191
+ `).all();
1192
+ } catch {
1193
+ return [];
1194
+ }
1195
+ }
1196
+ async function cmdMain(db, query, pickN, opts) {
1197
+ const maxResults = parseInt(opts.n ?? "20", 10);
1198
+ const showAll = opts.all ?? false;
1199
+ const livePromise = !query ? fetchLiveSessions().catch(() => []) : Promise.resolve([]);
1200
+ const allSessions = scanSessions(db, {
1201
+ limit: 500,
1202
+ filter: "named"
1203
+ });
1204
+ const registeredProjects = getRegisteredProjects(db, showAll);
1205
+ if (!query) {
1206
+ renderDedupedSessions(buildDeduped(await livePromise, allSessions, registeredProjects, showAll), showAll ? void 0 : maxResults);
1207
+ return;
1208
+ }
1209
+ if (/^[0-9a-f-]{8,36}$/i.test(query)) {
1210
+ const byUuid = allSessions.filter((s) => s.uuid.startsWith(query.toLowerCase()));
1211
+ if (byUuid.length === 1) {
1212
+ launchSession(byUuid[0], allSessions, opts.dryRun ?? false);
1213
+ return;
1214
+ }
1215
+ if (byUuid.length > 1) {
1216
+ console.error(err(`UUID prefix "${query}" is ambiguous — ${byUuid.length} catalog matches.`));
1217
+ process.exitCode = 1;
1218
+ return;
1219
+ }
1220
+ }
1221
+ {
1222
+ let liveSessions = [];
1223
+ try {
1224
+ liveSessions = await fetchLiveSessions();
1225
+ } catch {}
1226
+ const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);
1227
+ const qNorm = normalizeName(query).toLowerCase();
1228
+ const qSlug = query.toLowerCase().replace(/\s+/g, "-");
1229
+ const nameMatches = (e, q) => e.name.toLowerCase() === q || e.slug !== void 0 && e.slug.toLowerCase() === qSlug;
1230
+ const nameIncludes = (e, q) => e.name.toLowerCase().includes(q) || e.slug !== void 0 && e.slug.toLowerCase().includes(qSlug);
1231
+ const exactMatch = deduped.find((e) => nameMatches(e, qNorm));
1232
+ if (exactMatch) {
1233
+ if (exactMatch.status === "live") {
1234
+ if (await doSwitch(exactMatch, opts.dryRun ?? false)) return;
1235
+ }
1236
+ if (exactMatch.diskSession) {
1237
+ launchSession(exactMatch.diskSession, allSessions, opts.dryRun ?? false);
1238
+ return;
1239
+ }
1240
+ }
1241
+ const partialMatches = deduped.filter((e) => nameIncludes(e, qNorm));
1242
+ if (partialMatches.length === 1) {
1243
+ const match = partialMatches[0];
1244
+ if (match.status === "live") {
1245
+ if (await doSwitch(match, opts.dryRun ?? false)) return;
1246
+ }
1247
+ if (match.diskSession) {
1248
+ launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
1249
+ return;
1250
+ }
1251
+ }
1252
+ if (partialMatches.length > 1) {
1253
+ console.log("\n" + header(`Sessions matching "${query}"`) + "\n");
1254
+ const headers = [
1255
+ "#",
1256
+ "name",
1257
+ "status",
1258
+ "age",
1259
+ "project"
1260
+ ];
1261
+ const rows = partialMatches.slice(0, maxResults).map((entry, i) => {
1262
+ const age = entry.status === "live" ? chalk.green("now") : dim(fmtAge(entry.lastActivity));
1263
+ const project = entry.diskSession ? dim(shortenProject(entry.diskSession.decodedPath, 36)) : dim("—");
1264
+ return [
1265
+ dim(String(i + 1)),
1266
+ chalk.white(entry.name),
1267
+ fmtUnifiedStatus(entry.status),
1268
+ age,
1269
+ project
1270
+ ];
1271
+ });
1272
+ console.log(renderTable(headers, rows));
1273
+ console.log();
1274
+ const pickMatch = async (match) => {
1275
+ if (match.status === "live") {
1276
+ await doSwitch(match, opts.dryRun ?? false);
1277
+ return;
1278
+ }
1279
+ if (match.diskSession) launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
1280
+ };
1281
+ if (pickN !== void 0) {
1282
+ const idx = pickN - 1;
1283
+ if (idx >= 0 && idx < partialMatches.length) {
1284
+ await pickMatch(partialMatches[idx]);
1285
+ return;
1286
+ }
1287
+ console.error(err(`Invalid choice: ${pickN}`));
1288
+ process.exitCode = 1;
1289
+ return;
1290
+ }
1291
+ if (opts.auto) {
1292
+ await pickMatch(partialMatches[0]);
1293
+ return;
1294
+ }
1295
+ const choice = await askForChoice(Math.min(partialMatches.length, maxResults));
1296
+ if (choice !== null) await pickMatch(partialMatches[choice - 1]);
1297
+ return;
1298
+ }
1299
+ }
1300
+ if (!existsSync(HISTORY_FILE)) {
1301
+ console.error(err("~/.claude/history.jsonl not found."));
1302
+ console.error(dim(" No prompt history available for search."));
1303
+ console.error(dim(" Try: pai (no args) to see all sessions."));
1304
+ process.exitCode = 1;
1305
+ return;
1306
+ }
1307
+ process.stderr.write(dim(` Searching prompt history for "${query}"...\n`));
1308
+ const matches = await searchHistory(query, maxResults);
1309
+ if (matches.length === 0) {
1310
+ console.log(warn(`No sessions found matching "${query}".`));
1311
+ console.log(dim(" Try a shorter or different search term."));
1312
+ console.log(dim(" Or run: ") + chalk.white("pai") + dim(" (no args) to see all sessions."));
1313
+ return;
1314
+ }
1315
+ console.log("\n" + header(`Sessions matching "${query}"`) + "\n");
1316
+ const headers = [
1317
+ "#",
1318
+ "id",
1319
+ "when",
1320
+ "project",
1321
+ "last matching prompt"
1322
+ ];
1323
+ const rows = matches.map((m, idx) => {
1324
+ const shortId = (m.sessionId ?? "—").slice(0, 8);
1325
+ const when = m.lastMatchTs > 0 ? fmtTs(m.lastMatchTs) : dim("—");
1326
+ const project = shortenProject(m.project || "—");
1327
+ const snippet = m.lastMatchDisplay.replace(/\n+/g, " ").trim().slice(0, 48);
1328
+ const fullSnippet = m.lastMatchDisplay.replace(/\n+/g, " ").trim();
1329
+ const display = snippet.length < fullSnippet.length ? `"${snippet}…"` : `"${snippet}"`;
1330
+ return [
1331
+ dim(String(idx + 1)),
1332
+ chalk.cyan(shortId),
1333
+ when,
1334
+ dim(project),
1335
+ chalk.dim(display)
1336
+ ];
1337
+ });
1338
+ console.log(renderTable(headers, rows));
1339
+ console.log();
1340
+ const launchHistoryMatch = (match) => {
1341
+ const session = matchToSession(match, allSessions);
1342
+ if (!session) {
1343
+ console.error(err("Could not resolve session for launch (no project path)."));
1344
+ process.exitCode = 1;
1345
+ return;
1346
+ }
1347
+ launchSession(session, allSessions, opts.dryRun ?? false);
1348
+ };
1349
+ if (pickN !== void 0) {
1350
+ const idx = pickN - 1;
1351
+ if (idx >= 0 && idx < matches.length) {
1352
+ launchHistoryMatch(matches[idx]);
1353
+ return;
1354
+ }
1355
+ console.error(err(`Invalid choice: ${pickN}`));
1356
+ process.exitCode = 1;
1357
+ return;
1358
+ }
1359
+ if (opts.auto) {
1360
+ launchHistoryMatch(matches[0]);
1361
+ return;
1362
+ }
1363
+ const choice = await askForChoice(matches.length);
1364
+ if (choice !== null) launchHistoryMatch(matches[choice - 1]);
1365
+ }
1366
+
1367
+ //#endregion
1368
+ export { renderDedupedSessions as a, fetchLiveSessions as c, fmtAge as d, resolveSessionByNameOrId as f, normalizeName as i, revealItermSession as l, main_resolver_exports as n, printExitDir as o, scanSessions as p, buildDeduped as r, callAiBroker as s, cmdMain as t, sendToSession as u };
1369
+ //# sourceMappingURL=main-resolver-BfSL8zio.mjs.map