@ccpocket/bridge 0.1.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 (77) hide show
  1. package/README.md +54 -0
  2. package/dist/claude-process.d.ts +108 -0
  3. package/dist/claude-process.js +471 -0
  4. package/dist/claude-process.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +42 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/codex-process.d.ts +46 -0
  9. package/dist/codex-process.js +420 -0
  10. package/dist/codex-process.js.map +1 -0
  11. package/dist/debug-trace-store.d.ts +15 -0
  12. package/dist/debug-trace-store.js +78 -0
  13. package/dist/debug-trace-store.js.map +1 -0
  14. package/dist/firebase-auth.d.ts +35 -0
  15. package/dist/firebase-auth.js +132 -0
  16. package/dist/firebase-auth.js.map +1 -0
  17. package/dist/gallery-store.d.ts +66 -0
  18. package/dist/gallery-store.js +310 -0
  19. package/dist/gallery-store.js.map +1 -0
  20. package/dist/image-store.d.ts +22 -0
  21. package/dist/image-store.js +113 -0
  22. package/dist/image-store.js.map +1 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +153 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/mdns.d.ts +6 -0
  27. package/dist/mdns.js +42 -0
  28. package/dist/mdns.js.map +1 -0
  29. package/dist/parser.d.ts +381 -0
  30. package/dist/parser.js +218 -0
  31. package/dist/parser.js.map +1 -0
  32. package/dist/project-history.d.ts +10 -0
  33. package/dist/project-history.js +73 -0
  34. package/dist/project-history.js.map +1 -0
  35. package/dist/prompt-history-backup.d.ts +15 -0
  36. package/dist/prompt-history-backup.js +46 -0
  37. package/dist/prompt-history-backup.js.map +1 -0
  38. package/dist/push-relay.d.ts +27 -0
  39. package/dist/push-relay.js +69 -0
  40. package/dist/push-relay.js.map +1 -0
  41. package/dist/recording-store.d.ts +51 -0
  42. package/dist/recording-store.js +158 -0
  43. package/dist/recording-store.js.map +1 -0
  44. package/dist/screenshot.d.ts +28 -0
  45. package/dist/screenshot.js +98 -0
  46. package/dist/screenshot.js.map +1 -0
  47. package/dist/sdk-process.d.ts +151 -0
  48. package/dist/sdk-process.js +740 -0
  49. package/dist/sdk-process.js.map +1 -0
  50. package/dist/session.d.ts +126 -0
  51. package/dist/session.js +550 -0
  52. package/dist/session.js.map +1 -0
  53. package/dist/sessions-index.d.ts +86 -0
  54. package/dist/sessions-index.js +1027 -0
  55. package/dist/sessions-index.js.map +1 -0
  56. package/dist/setup-launchd.d.ts +8 -0
  57. package/dist/setup-launchd.js +109 -0
  58. package/dist/setup-launchd.js.map +1 -0
  59. package/dist/startup-info.d.ts +8 -0
  60. package/dist/startup-info.js +78 -0
  61. package/dist/startup-info.js.map +1 -0
  62. package/dist/usage.d.ts +17 -0
  63. package/dist/usage.js +236 -0
  64. package/dist/usage.js.map +1 -0
  65. package/dist/version.d.ts +11 -0
  66. package/dist/version.js +39 -0
  67. package/dist/version.js.map +1 -0
  68. package/dist/websocket.d.ts +71 -0
  69. package/dist/websocket.js +1487 -0
  70. package/dist/websocket.js.map +1 -0
  71. package/dist/worktree-store.d.ts +25 -0
  72. package/dist/worktree-store.js +59 -0
  73. package/dist/worktree-store.js.map +1 -0
  74. package/dist/worktree.d.ts +43 -0
  75. package/dist/worktree.js +295 -0
  76. package/dist/worktree.js.map +1 -0
  77. package/package.json +63 -0
@@ -0,0 +1,1027 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ /** Convert a filesystem path to Claude's project directory slug (e.g. /foo/bar → -foo-bar). */
5
+ export function pathToSlug(p) {
6
+ return p.replaceAll("/", "-").replaceAll("_", "-");
7
+ }
8
+ /**
9
+ * Normalize a worktree cwd back to the main project path.
10
+ * e.g. /path/to/project-worktrees/branch → /path/to/project
11
+ */
12
+ export function normalizeWorktreePath(p) {
13
+ const match = p.match(/^(.+)-worktrees\/[^/]+$/);
14
+ return match?.[1] ?? p;
15
+ }
16
+ /**
17
+ * Check if a directory slug represents a worktree directory for a given project slug.
18
+ * e.g. "-Users-x-proj-worktrees-branch" is a worktree dir for "-Users-x-proj".
19
+ */
20
+ export function isWorktreeSlug(dirSlug, projectSlug) {
21
+ return dirSlug.startsWith(projectSlug + "-worktrees-");
22
+ }
23
+ /**
24
+ * Scan a directory for JSONL session files and create SessionIndexEntry objects.
25
+ * Used as a fallback when sessions-index.json is missing (common for worktree sessions).
26
+ */
27
+ export async function scanJsonlDir(dirPath) {
28
+ const entries = [];
29
+ let files;
30
+ try {
31
+ files = await readdir(dirPath);
32
+ }
33
+ catch {
34
+ return entries;
35
+ }
36
+ for (const file of files) {
37
+ if (!file.endsWith(".jsonl"))
38
+ continue;
39
+ const sessionId = basename(file, ".jsonl");
40
+ const filePath = join(dirPath, file);
41
+ let raw;
42
+ try {
43
+ raw = await readFile(filePath, "utf-8");
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ const lines = raw.split("\n");
49
+ let firstPrompt = "";
50
+ let lastPrompt = "";
51
+ let messageCount = 0;
52
+ let created = "";
53
+ let modified = "";
54
+ let gitBranch = "";
55
+ let projectPath = "";
56
+ let isSidechain = false;
57
+ let summary;
58
+ for (const line of lines) {
59
+ if (!line.trim())
60
+ continue;
61
+ let entry;
62
+ try {
63
+ entry = JSON.parse(line);
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ const type = entry.type;
69
+ if (type === "summary" && entry.summary) {
70
+ summary = entry.summary;
71
+ }
72
+ if (type !== "user" && type !== "assistant")
73
+ continue;
74
+ messageCount++;
75
+ const timestamp = entry.timestamp;
76
+ if (timestamp) {
77
+ if (!created)
78
+ created = timestamp;
79
+ modified = timestamp;
80
+ }
81
+ if (!gitBranch && entry.gitBranch) {
82
+ gitBranch = entry.gitBranch;
83
+ }
84
+ if (!projectPath && entry.cwd) {
85
+ projectPath = normalizeWorktreePath(entry.cwd);
86
+ }
87
+ if (type === "user") {
88
+ const message = entry.message;
89
+ if (message?.content) {
90
+ let text = "";
91
+ if (typeof message.content === "string") {
92
+ text = message.content;
93
+ }
94
+ else if (Array.isArray(message.content)) {
95
+ const textBlock = message.content.find((c) => c.type === "text" && c.text);
96
+ if (textBlock?.text) {
97
+ text = textBlock.text;
98
+ }
99
+ }
100
+ if (text) {
101
+ if (!firstPrompt)
102
+ firstPrompt = text;
103
+ lastPrompt = text;
104
+ }
105
+ }
106
+ }
107
+ if (entry.isSidechain) {
108
+ isSidechain = true;
109
+ }
110
+ }
111
+ if (messageCount > 0) {
112
+ entries.push({
113
+ sessionId,
114
+ provider: "claude",
115
+ summary,
116
+ firstPrompt,
117
+ ...(lastPrompt && lastPrompt !== firstPrompt ? { lastPrompt } : {}),
118
+ messageCount,
119
+ created,
120
+ modified,
121
+ gitBranch,
122
+ projectPath,
123
+ isSidechain,
124
+ });
125
+ }
126
+ }
127
+ return entries;
128
+ }
129
+ export async function getAllRecentSessions(options = {}) {
130
+ const limit = options.limit ?? 20;
131
+ const offset = options.offset ?? 0;
132
+ const filterProjectPath = options.projectPath;
133
+ const projectsDir = join(homedir(), ".claude", "projects");
134
+ const entries = [];
135
+ let projectDirs;
136
+ try {
137
+ projectDirs = await readdir(projectsDir);
138
+ }
139
+ catch {
140
+ // ~/.claude/projects doesn't exist
141
+ projectDirs = [];
142
+ }
143
+ // Compute worktree slug prefix for projectPath filtering
144
+ const projectSlug = filterProjectPath
145
+ ? pathToSlug(filterProjectPath)
146
+ : null;
147
+ for (const dirName of projectDirs) {
148
+ // Skip hidden directories
149
+ if (dirName.startsWith("."))
150
+ continue;
151
+ // When filtering by project, skip unrelated directories early
152
+ const isProjectDir = projectSlug ? dirName === projectSlug : false;
153
+ const isWorktreeDir = projectSlug
154
+ ? isWorktreeSlug(dirName, projectSlug)
155
+ : false;
156
+ if (filterProjectPath && !isProjectDir && !isWorktreeDir)
157
+ continue;
158
+ const dirPath = join(projectsDir, dirName);
159
+ const indexPath = join(dirPath, "sessions-index.json");
160
+ let raw = null;
161
+ try {
162
+ raw = await readFile(indexPath, "utf-8");
163
+ }
164
+ catch {
165
+ // No sessions-index.json — will try JSONL scan for worktree dirs
166
+ }
167
+ if (raw !== null) {
168
+ // Parse sessions-index.json
169
+ let index;
170
+ try {
171
+ index = JSON.parse(raw);
172
+ }
173
+ catch {
174
+ console.error(`[sessions-index] Failed to parse ${indexPath}`);
175
+ continue;
176
+ }
177
+ if (!Array.isArray(index.entries))
178
+ continue;
179
+ const indexedIds = new Set();
180
+ for (const entry of index.entries) {
181
+ indexedIds.add(entry.sessionId);
182
+ const mapped = {
183
+ sessionId: entry.sessionId,
184
+ provider: "claude",
185
+ summary: entry.summary,
186
+ firstPrompt: entry.firstPrompt ?? "",
187
+ messageCount: entry.messageCount ?? 0,
188
+ created: entry.created ?? "",
189
+ modified: entry.modified ?? "",
190
+ gitBranch: entry.gitBranch ?? "",
191
+ projectPath: normalizeWorktreePath(entry.projectPath ?? ""),
192
+ isSidechain: entry.isSidechain ?? false,
193
+ };
194
+ entries.push(mapped);
195
+ }
196
+ // Supplement: scan JSONL files not covered by the index.
197
+ // Claude CLI may not register every session (e.g. `claude -r` resumes)
198
+ // into sessions-index.json, so we pick up any orphaned JSONL files here.
199
+ const scanned = await scanJsonlDir(dirPath);
200
+ for (const s of scanned) {
201
+ if (!indexedIds.has(s.sessionId)) {
202
+ entries.push(s);
203
+ }
204
+ }
205
+ }
206
+ else {
207
+ // No sessions-index.json: scan JSONL files directly.
208
+ // Directories are already filtered above, so all remaining dirs are relevant.
209
+ const scanned = await scanJsonlDir(dirPath);
210
+ entries.push(...scanned);
211
+ }
212
+ }
213
+ const codexEntries = await getAllRecentCodexSessions({
214
+ projectPath: filterProjectPath,
215
+ });
216
+ entries.push(...codexEntries);
217
+ // Sort by modified descending
218
+ entries.sort((a, b) => {
219
+ const ta = new Date(a.modified).getTime();
220
+ const tb = new Date(b.modified).getTime();
221
+ return tb - ta;
222
+ });
223
+ const sliced = entries.slice(offset, offset + limit);
224
+ const hasMore = offset + limit < entries.length;
225
+ return { sessions: sliced, hasMore };
226
+ }
227
+ async function listCodexSessionFiles() {
228
+ const root = join(homedir(), ".codex", "sessions");
229
+ const files = [];
230
+ const stack = [root];
231
+ while (stack.length > 0) {
232
+ const dir = stack.pop();
233
+ let children;
234
+ try {
235
+ children = await readdir(dir);
236
+ }
237
+ catch {
238
+ continue;
239
+ }
240
+ for (const child of children) {
241
+ const p = join(dir, child);
242
+ let st;
243
+ try {
244
+ st = await stat(p);
245
+ }
246
+ catch {
247
+ continue;
248
+ }
249
+ if (st.isDirectory()) {
250
+ stack.push(p);
251
+ }
252
+ else if (st.isFile() && p.endsWith(".jsonl")) {
253
+ files.push(p);
254
+ }
255
+ }
256
+ }
257
+ return files;
258
+ }
259
+ function parseCodexSessionJsonl(raw, fallbackSessionId) {
260
+ const lines = raw.split("\n");
261
+ let threadId = fallbackSessionId;
262
+ let projectPath = "";
263
+ let resumeCwd = "";
264
+ let gitBranch = "";
265
+ let created = "";
266
+ let modified = "";
267
+ let firstPrompt = "";
268
+ let lastPrompt = "";
269
+ let summary = "";
270
+ let messageCount = 0;
271
+ let lastAssistantText = "";
272
+ // Settings extracted from the first turn_context entry
273
+ let approvalPolicy;
274
+ let sandboxMode;
275
+ let model;
276
+ let modelReasoningEffort;
277
+ let networkAccessEnabled;
278
+ let webSearchMode;
279
+ for (const line of lines) {
280
+ if (!line.trim())
281
+ continue;
282
+ let entry;
283
+ try {
284
+ entry = JSON.parse(line);
285
+ }
286
+ catch {
287
+ continue;
288
+ }
289
+ const timestamp = entry.timestamp;
290
+ if (timestamp) {
291
+ if (!created)
292
+ created = timestamp;
293
+ modified = timestamp;
294
+ }
295
+ if (entry.type === "session_meta") {
296
+ const payload = entry.payload;
297
+ if (payload) {
298
+ if (typeof payload.id === "string" && payload.id.length > 0) {
299
+ threadId = payload.id;
300
+ }
301
+ if (typeof payload.cwd === "string" && payload.cwd.length > 0) {
302
+ resumeCwd = payload.cwd;
303
+ projectPath = normalizeWorktreePath(payload.cwd);
304
+ }
305
+ const git = payload.git;
306
+ if (git && typeof git.branch === "string") {
307
+ gitBranch = git.branch;
308
+ }
309
+ }
310
+ continue;
311
+ }
312
+ // Extract codex settings from turn_context
313
+ if (entry.type === "turn_context" && !approvalPolicy) {
314
+ const payload = entry.payload;
315
+ if (payload) {
316
+ if (typeof payload.approval_policy === "string") {
317
+ approvalPolicy = payload.approval_policy;
318
+ }
319
+ const sp = payload.sandbox_policy;
320
+ if (sp && typeof sp.type === "string") {
321
+ sandboxMode = sp.type;
322
+ }
323
+ if (typeof payload.model === "string") {
324
+ model = payload.model;
325
+ }
326
+ const collaborationMode = payload.collaboration_mode;
327
+ const collaborationSettings = collaborationMode?.settings;
328
+ if (typeof collaborationSettings?.reasoning_effort === "string") {
329
+ modelReasoningEffort = collaborationSettings.reasoning_effort;
330
+ }
331
+ if (typeof sp?.network_access === "boolean") {
332
+ networkAccessEnabled = sp.network_access;
333
+ }
334
+ if (typeof payload.web_search === "string") {
335
+ webSearchMode = payload.web_search;
336
+ }
337
+ }
338
+ continue;
339
+ }
340
+ if (entry.type === "event_msg") {
341
+ const payload = entry.payload;
342
+ if (payload?.type === "user_message" && typeof payload.message === "string") {
343
+ messageCount += 1;
344
+ if (!firstPrompt)
345
+ firstPrompt = payload.message;
346
+ lastPrompt = payload.message;
347
+ }
348
+ continue;
349
+ }
350
+ if (entry.type === "response_item") {
351
+ const payload = entry.payload;
352
+ if (!payload || payload.type !== "message" || payload.role !== "assistant") {
353
+ continue;
354
+ }
355
+ const content = payload.content;
356
+ if (!Array.isArray(content))
357
+ continue;
358
+ const text = content
359
+ .filter((item) => item.type === "output_text" && typeof item.text === "string")
360
+ .map((item) => item.text)
361
+ .join("\n")
362
+ .trim();
363
+ if (text.length > 0) {
364
+ messageCount += 1;
365
+ lastAssistantText = text;
366
+ }
367
+ }
368
+ }
369
+ if (!projectPath || messageCount === 0)
370
+ return null;
371
+ summary = lastAssistantText || summary;
372
+ const codexSettings = (approvalPolicy
373
+ || sandboxMode
374
+ || model
375
+ || modelReasoningEffort
376
+ || networkAccessEnabled !== undefined
377
+ || webSearchMode)
378
+ ? {
379
+ approvalPolicy,
380
+ sandboxMode,
381
+ model,
382
+ modelReasoningEffort,
383
+ networkAccessEnabled,
384
+ webSearchMode,
385
+ }
386
+ : undefined;
387
+ return {
388
+ threadId,
389
+ entry: {
390
+ sessionId: threadId,
391
+ provider: "codex",
392
+ summary: summary || undefined,
393
+ firstPrompt,
394
+ ...(lastPrompt && lastPrompt !== firstPrompt ? { lastPrompt } : {}),
395
+ messageCount,
396
+ created,
397
+ modified,
398
+ gitBranch,
399
+ projectPath,
400
+ ...(resumeCwd && resumeCwd !== projectPath ? { resumeCwd } : {}),
401
+ isSidechain: false,
402
+ codexSettings,
403
+ },
404
+ };
405
+ }
406
+ async function getAllRecentCodexSessions(options = {}) {
407
+ const files = await listCodexSessionFiles();
408
+ const entries = [];
409
+ const normalizedProjectPath = options.projectPath
410
+ ? normalizeWorktreePath(options.projectPath)
411
+ : null;
412
+ for (const filePath of files) {
413
+ let raw;
414
+ try {
415
+ raw = await readFile(filePath, "utf-8");
416
+ }
417
+ catch {
418
+ continue;
419
+ }
420
+ const fallbackSessionId = basename(filePath, ".jsonl");
421
+ const parsed = parseCodexSessionJsonl(raw, fallbackSessionId);
422
+ if (!parsed)
423
+ continue;
424
+ if (normalizedProjectPath && parsed.entry.projectPath !== normalizedProjectPath) {
425
+ continue;
426
+ }
427
+ entries.push(parsed.entry);
428
+ }
429
+ return entries;
430
+ }
431
+ function asObject(value) {
432
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
433
+ return null;
434
+ }
435
+ return value;
436
+ }
437
+ function parseObjectLike(value) {
438
+ if (typeof value === "string") {
439
+ try {
440
+ const parsed = JSON.parse(value);
441
+ return asObject(parsed) ?? { value: parsed };
442
+ }
443
+ catch {
444
+ return { value };
445
+ }
446
+ }
447
+ return asObject(value) ?? {};
448
+ }
449
+ function appendTextMessage(messages, role, text, timestamp) {
450
+ const normalized = text.trim();
451
+ if (!normalized)
452
+ return;
453
+ const last = messages.at(-1);
454
+ if (last
455
+ && last.role === role
456
+ && last.content.length === 1
457
+ && last.content[0].type === "text"
458
+ && typeof last.content[0].text === "string"
459
+ && last.content[0].text.trim() === normalized) {
460
+ return;
461
+ }
462
+ messages.push({
463
+ role,
464
+ content: [{ type: "text", text }],
465
+ ...(timestamp ? { timestamp } : {}),
466
+ });
467
+ }
468
+ function appendToolUseMessage(messages, id, name, input) {
469
+ const normalizedName = name.trim();
470
+ if (!normalizedName)
471
+ return;
472
+ const last = messages.at(-1);
473
+ if (last
474
+ && last.role === "assistant"
475
+ && last.content.length === 1
476
+ && last.content[0].type === "tool_use"
477
+ && last.content[0].id === id
478
+ && last.content[0].name === normalizedName) {
479
+ return;
480
+ }
481
+ messages.push({
482
+ role: "assistant",
483
+ content: [
484
+ {
485
+ type: "tool_use",
486
+ id,
487
+ name: normalizedName,
488
+ input,
489
+ },
490
+ ],
491
+ });
492
+ }
493
+ function normalizeCodexToolName(name) {
494
+ if (name === "exec_command" || name === "write_stdin") {
495
+ return "Bash";
496
+ }
497
+ // Codex function names for MCP tools look like: mcp__server__tool_name
498
+ if (name.startsWith("mcp__")) {
499
+ const [server, ...toolParts] = name.slice("mcp__".length).split("__");
500
+ if (server && toolParts.length > 0) {
501
+ return `mcp:${server}/${toolParts.join("__")}`;
502
+ }
503
+ }
504
+ return name;
505
+ }
506
+ function isCodexInjectedUserContext(text) {
507
+ const normalized = text.trimStart();
508
+ return (normalized.startsWith("# AGENTS.md instructions for ")
509
+ || normalized.startsWith("<environment_context>")
510
+ || normalized.startsWith("<permissions instructions>"));
511
+ }
512
+ function getCodexSearchInput(payload) {
513
+ const action = asObject(payload.action);
514
+ const input = {};
515
+ if (typeof action?.query === "string") {
516
+ input.query = action.query;
517
+ }
518
+ if (Array.isArray(action?.queries)) {
519
+ const queries = action.queries.filter((q) => typeof q === "string" && q.length > 0);
520
+ if (queries.length > 0) {
521
+ input.queries = queries;
522
+ }
523
+ }
524
+ return input;
525
+ }
526
+ /**
527
+ * Find the JSONL file path for a given sessionId by searching sessions-index.json files,
528
+ * then falling back to scanning directories for the JSONL file directly.
529
+ */
530
+ async function findSessionJsonlPath(sessionId) {
531
+ const projectsDir = join(homedir(), ".claude", "projects");
532
+ let projectDirs;
533
+ try {
534
+ projectDirs = await readdir(projectsDir);
535
+ }
536
+ catch {
537
+ return null;
538
+ }
539
+ // First pass: check sessions-index.json files
540
+ for (const dirName of projectDirs) {
541
+ if (dirName.startsWith("."))
542
+ continue;
543
+ const indexPath = join(projectsDir, dirName, "sessions-index.json");
544
+ let raw;
545
+ try {
546
+ raw = await readFile(indexPath, "utf-8");
547
+ }
548
+ catch {
549
+ continue;
550
+ }
551
+ let index;
552
+ try {
553
+ index = JSON.parse(raw);
554
+ }
555
+ catch {
556
+ continue;
557
+ }
558
+ if (!Array.isArray(index.entries))
559
+ continue;
560
+ const entry = index.entries.find((e) => e.sessionId === sessionId);
561
+ if (entry?.fullPath) {
562
+ return entry.fullPath;
563
+ }
564
+ }
565
+ // Fallback: scan directories for the JSONL file directly
566
+ // This handles worktree sessions without sessions-index.json
567
+ const jsonlFileName = `${sessionId}.jsonl`;
568
+ for (const dirName of projectDirs) {
569
+ if (dirName.startsWith("."))
570
+ continue;
571
+ const candidatePath = join(projectsDir, dirName, jsonlFileName);
572
+ try {
573
+ await stat(candidatePath);
574
+ return candidatePath;
575
+ }
576
+ catch {
577
+ continue;
578
+ }
579
+ }
580
+ return null;
581
+ }
582
+ async function findCodexSessionJsonlPath(threadId) {
583
+ const files = await listCodexSessionFiles();
584
+ for (const filePath of files) {
585
+ const fallbackSessionId = basename(filePath, ".jsonl");
586
+ if (fallbackSessionId === threadId) {
587
+ return filePath;
588
+ }
589
+ let raw;
590
+ try {
591
+ raw = await readFile(filePath, "utf-8");
592
+ }
593
+ catch {
594
+ continue;
595
+ }
596
+ const parsed = parseCodexSessionJsonl(raw, fallbackSessionId);
597
+ if (parsed?.threadId === threadId) {
598
+ return filePath;
599
+ }
600
+ }
601
+ return null;
602
+ }
603
+ /**
604
+ * Read past conversation messages from a session's JSONL file.
605
+ * Returns user and assistant messages suitable for display.
606
+ */
607
+ export async function getSessionHistory(sessionId) {
608
+ const jsonlPath = await findSessionJsonlPath(sessionId);
609
+ if (!jsonlPath)
610
+ return [];
611
+ let raw;
612
+ try {
613
+ raw = await readFile(jsonlPath, "utf-8");
614
+ }
615
+ catch {
616
+ return [];
617
+ }
618
+ const messages = [];
619
+ const lines = raw.split("\n");
620
+ for (const line of lines) {
621
+ if (!line.trim())
622
+ continue;
623
+ let entry;
624
+ try {
625
+ entry = JSON.parse(line);
626
+ }
627
+ catch {
628
+ continue;
629
+ }
630
+ const type = entry.type;
631
+ if (type !== "user" && type !== "assistant")
632
+ continue;
633
+ // Skip context compaction and transcript-only messages (not real user input)
634
+ if (type === "user") {
635
+ if (entry.isCompactSummary === true || entry.isVisibleInTranscriptOnly === true) {
636
+ continue;
637
+ }
638
+ }
639
+ const message = entry.message;
640
+ if (!message?.content)
641
+ continue;
642
+ const role = message.role;
643
+ const isMeta = role === "user" && entry.isMeta === true ? true : undefined;
644
+ // Handle string content (e.g. user message after interrupt)
645
+ if (typeof message.content === "string") {
646
+ if (message.content) {
647
+ const uuid = entry.uuid;
648
+ const ts = entry.timestamp;
649
+ messages.push({
650
+ role,
651
+ content: [{ type: "text", text: message.content }],
652
+ ...(uuid ? { uuid } : {}),
653
+ ...(ts ? { timestamp: ts } : {}),
654
+ ...(isMeta ? { isMeta } : {}),
655
+ });
656
+ }
657
+ continue;
658
+ }
659
+ if (!Array.isArray(message.content))
660
+ continue;
661
+ // Filter content to only text and tool_use (skip tool_result for cleaner display)
662
+ const content = [];
663
+ let imageCount = 0;
664
+ for (const c of message.content) {
665
+ if (typeof c !== "object" || c === null)
666
+ continue;
667
+ const item = c;
668
+ const contentType = item.type;
669
+ if (contentType === "text" && item.text) {
670
+ content.push({ type: "text", text: item.text });
671
+ }
672
+ else if (contentType === "tool_use") {
673
+ content.push({
674
+ type: "tool_use",
675
+ id: item.id,
676
+ name: item.name,
677
+ input: item.input ?? {},
678
+ });
679
+ }
680
+ else if (contentType === "image") {
681
+ imageCount++;
682
+ }
683
+ }
684
+ if (content.length > 0 || imageCount > 0) {
685
+ const uuid = entry.uuid;
686
+ const ts = entry.timestamp;
687
+ // If there are only images and no text, add a placeholder
688
+ if (content.length === 0 && imageCount > 0) {
689
+ content.push({
690
+ type: "text",
691
+ text: `[Image attached${imageCount > 1 ? ` x${imageCount}` : ""}]`,
692
+ });
693
+ }
694
+ messages.push({
695
+ role,
696
+ content,
697
+ ...(uuid ? { uuid } : {}),
698
+ ...(ts ? { timestamp: ts } : {}),
699
+ ...(isMeta ? { isMeta } : {}),
700
+ ...(imageCount > 0 ? { imageCount } : {}),
701
+ });
702
+ }
703
+ }
704
+ return messages;
705
+ }
706
+ /**
707
+ * Extract image base64 data from a Claude Code session JSONL for a specific message UUID.
708
+ */
709
+ export async function extractMessageImages(sessionId, messageUuid) {
710
+ // Try Claude Code first, then Codex
711
+ const claudeImages = await extractClaudeMessageImages(sessionId, messageUuid);
712
+ if (claudeImages.length > 0)
713
+ return claudeImages;
714
+ return extractCodexMessageImages(sessionId, messageUuid);
715
+ }
716
+ async function extractClaudeMessageImages(sessionId, messageUuid) {
717
+ const jsonlPath = await findSessionJsonlPath(sessionId);
718
+ if (!jsonlPath)
719
+ return [];
720
+ let raw;
721
+ try {
722
+ raw = await readFile(jsonlPath, "utf-8");
723
+ }
724
+ catch {
725
+ return [];
726
+ }
727
+ const lines = raw.split("\n");
728
+ for (const line of lines) {
729
+ if (!line.trim())
730
+ continue;
731
+ let entry;
732
+ try {
733
+ entry = JSON.parse(line);
734
+ }
735
+ catch {
736
+ continue;
737
+ }
738
+ if (entry.type !== "user")
739
+ continue;
740
+ if (entry.uuid !== messageUuid)
741
+ continue;
742
+ const message = entry.message;
743
+ if (!message?.content || !Array.isArray(message.content))
744
+ continue;
745
+ const images = [];
746
+ for (const c of message.content) {
747
+ if (typeof c !== "object" || c === null)
748
+ continue;
749
+ const item = c;
750
+ if (item.type !== "image")
751
+ continue;
752
+ const source = item.source;
753
+ if (!source || source.type !== "base64")
754
+ continue;
755
+ const data = source.data;
756
+ const mediaType = source.media_type;
757
+ if (data && mediaType) {
758
+ images.push({ base64: data, mimeType: mediaType });
759
+ }
760
+ }
761
+ return images;
762
+ }
763
+ return [];
764
+ }
765
+ async function extractCodexMessageImages(sessionId, messageUuid) {
766
+ const jsonlPath = await findCodexSessionJsonlPath(sessionId);
767
+ if (!jsonlPath)
768
+ return [];
769
+ let raw;
770
+ try {
771
+ raw = await readFile(jsonlPath, "utf-8");
772
+ }
773
+ catch {
774
+ return [];
775
+ }
776
+ // Codex doesn't have per-message UUIDs in the same way.
777
+ // We scan for event_msg with user_message that has images and match by line index
778
+ // encoded in the UUID (format: "codex-line-{index}").
779
+ const lineIndex = messageUuid.startsWith("codex-line-")
780
+ ? parseInt(messageUuid.slice("codex-line-".length), 10)
781
+ : -1;
782
+ if (lineIndex < 0)
783
+ return [];
784
+ const lines = raw.split("\n");
785
+ if (lineIndex >= lines.length)
786
+ return [];
787
+ const line = lines[lineIndex];
788
+ if (!line?.trim())
789
+ return [];
790
+ let entry;
791
+ try {
792
+ entry = JSON.parse(line);
793
+ }
794
+ catch {
795
+ return [];
796
+ }
797
+ if (entry.type !== "event_msg")
798
+ return [];
799
+ const payload = asObject(entry.payload);
800
+ if (!payload || payload.type !== "user_message")
801
+ return [];
802
+ const images = [];
803
+ // Parse payload.images (Data URI format: "data:image/png;base64,...")
804
+ if (Array.isArray(payload.images)) {
805
+ for (const img of payload.images) {
806
+ if (typeof img !== "string")
807
+ continue;
808
+ const match = img.match(/^data:(image\/[^;]+);base64,(.+)$/);
809
+ if (match) {
810
+ images.push({ base64: match[2], mimeType: match[1] });
811
+ }
812
+ }
813
+ }
814
+ return images;
815
+ }
816
+ export async function getCodexSessionHistory(threadId) {
817
+ const jsonlPath = await findCodexSessionJsonlPath(threadId);
818
+ if (!jsonlPath)
819
+ return [];
820
+ let raw;
821
+ try {
822
+ raw = await readFile(jsonlPath, "utf-8");
823
+ }
824
+ catch {
825
+ return [];
826
+ }
827
+ const messages = [];
828
+ const lines = raw.split("\n");
829
+ for (const [index, line] of lines.entries()) {
830
+ if (!line.trim())
831
+ continue;
832
+ let entry;
833
+ try {
834
+ entry = JSON.parse(line);
835
+ }
836
+ catch {
837
+ continue;
838
+ }
839
+ const entryTimestamp = entry.timestamp;
840
+ if (entry.type === "event_msg") {
841
+ const payload = asObject(entry.payload);
842
+ if (!payload)
843
+ continue;
844
+ if (payload.type === "user_message") {
845
+ const rawMessage = typeof payload.message === "string" ? payload.message : "";
846
+ const images = Array.isArray(payload.images) ? payload.images.length : 0;
847
+ const localImages = Array.isArray(payload.local_images)
848
+ ? payload.local_images.length
849
+ : 0;
850
+ const imageCount = images + localImages;
851
+ const text = rawMessage.trim().length > 0
852
+ ? rawMessage
853
+ : imageCount > 0
854
+ ? `[Image attached${imageCount > 1 ? ` x${imageCount}` : ""}]`
855
+ : "";
856
+ if (imageCount > 0) {
857
+ // Push directly to include imageCount metadata
858
+ const normalized = text.trim();
859
+ if (normalized) {
860
+ messages.push({
861
+ role: "user",
862
+ content: [{ type: "text", text }],
863
+ imageCount,
864
+ ...(entryTimestamp ? { timestamp: entryTimestamp } : {}),
865
+ });
866
+ }
867
+ }
868
+ else {
869
+ appendTextMessage(messages, "user", text, entryTimestamp);
870
+ }
871
+ continue;
872
+ }
873
+ if (payload.type === "agent_message" && typeof payload.message === "string") {
874
+ appendTextMessage(messages, "assistant", payload.message, entryTimestamp);
875
+ }
876
+ continue;
877
+ }
878
+ if (entry.type === "response_item") {
879
+ const payload = asObject(entry.payload);
880
+ if (!payload)
881
+ continue;
882
+ if (payload.type === "message") {
883
+ const content = Array.isArray(payload.content)
884
+ ? payload.content
885
+ : [];
886
+ if (payload.role === "assistant") {
887
+ const text = content
888
+ .filter((item) => item.type === "output_text" && typeof item.text === "string")
889
+ .map((item) => item.text)
890
+ .join("\n");
891
+ appendTextMessage(messages, "assistant", text, entryTimestamp);
892
+ continue;
893
+ }
894
+ if (payload.role === "user") {
895
+ const text = content
896
+ .filter((item) => item.type === "input_text" && typeof item.text === "string")
897
+ .map((item) => item.text)
898
+ .join("\n");
899
+ if (!isCodexInjectedUserContext(text)) {
900
+ appendTextMessage(messages, "user", text, entryTimestamp);
901
+ }
902
+ continue;
903
+ }
904
+ }
905
+ if (payload.type === "function_call") {
906
+ const id = typeof payload.call_id === "string" ? payload.call_id : `tool-${index}`;
907
+ const rawName = typeof payload.name === "string" ? payload.name : "tool";
908
+ appendToolUseMessage(messages, id, normalizeCodexToolName(rawName), parseObjectLike(payload.arguments));
909
+ continue;
910
+ }
911
+ if (payload.type === "custom_tool_call") {
912
+ const id = typeof payload.call_id === "string" ? payload.call_id : `tool-${index}`;
913
+ const rawName = typeof payload.name === "string" ? payload.name : "custom_tool";
914
+ appendToolUseMessage(messages, id, normalizeCodexToolName(rawName), parseObjectLike(payload.input));
915
+ continue;
916
+ }
917
+ if (payload.type === "web_search_call") {
918
+ appendToolUseMessage(messages, typeof payload.call_id === "string" ? payload.call_id : `web-search-${index}`, "WebSearch", getCodexSearchInput(payload));
919
+ continue;
920
+ }
921
+ // Backward/forward compatibility with older/newer Codex JSONL schemas.
922
+ if (payload.type === "command_execution") {
923
+ const id = typeof payload.id === "string"
924
+ ? payload.id
925
+ : typeof payload.call_id === "string"
926
+ ? payload.call_id
927
+ : `cmd-${index}`;
928
+ const input = typeof payload.command === "string"
929
+ ? { command: payload.command }
930
+ : parseObjectLike(payload);
931
+ appendToolUseMessage(messages, id, "Bash", input);
932
+ continue;
933
+ }
934
+ if (payload.type === "mcp_tool_call") {
935
+ const id = typeof payload.id === "string"
936
+ ? payload.id
937
+ : typeof payload.call_id === "string"
938
+ ? payload.call_id
939
+ : `mcp-${index}`;
940
+ const server = typeof payload.server === "string" ? payload.server : "unknown";
941
+ const tool = typeof payload.tool === "string" ? payload.tool : "tool";
942
+ appendToolUseMessage(messages, id, `mcp:${server}/${tool}`, parseObjectLike(payload.arguments));
943
+ continue;
944
+ }
945
+ if (payload.type === "file_change") {
946
+ const id = typeof payload.id === "string"
947
+ ? payload.id
948
+ : typeof payload.call_id === "string"
949
+ ? payload.call_id
950
+ : `file-change-${index}`;
951
+ const input = Array.isArray(payload.changes)
952
+ ? { changes: payload.changes }
953
+ : parseObjectLike(payload.changes);
954
+ appendToolUseMessage(messages, id, "FileChange", input);
955
+ continue;
956
+ }
957
+ if (payload.type === "web_search") {
958
+ const id = typeof payload.id === "string"
959
+ ? payload.id
960
+ : typeof payload.call_id === "string"
961
+ ? payload.call_id
962
+ : `web-search-${index}`;
963
+ const input = typeof payload.query === "string"
964
+ ? { query: payload.query }
965
+ : getCodexSearchInput(payload);
966
+ appendToolUseMessage(messages, id, "WebSearch", input);
967
+ }
968
+ }
969
+ }
970
+ return messages;
971
+ }
972
+ /**
973
+ * Look up session metadata for a set of Claude CLI sessionIds.
974
+ * Returns a map from sessionId to a subset of session metadata.
975
+ * More efficient than getAllRecentSessions when you only need a few entries.
976
+ */
977
+ export async function findSessionsByClaudeIds(ids) {
978
+ if (ids.size === 0)
979
+ return new Map();
980
+ const result = new Map();
981
+ const remaining = new Set(ids);
982
+ const projectsDir = join(homedir(), ".claude", "projects");
983
+ let projectDirs;
984
+ try {
985
+ projectDirs = await readdir(projectsDir);
986
+ }
987
+ catch {
988
+ return result;
989
+ }
990
+ for (const dirName of projectDirs) {
991
+ if (remaining.size === 0)
992
+ break;
993
+ if (dirName.startsWith("."))
994
+ continue;
995
+ const indexPath = join(projectsDir, dirName, "sessions-index.json");
996
+ let raw;
997
+ try {
998
+ raw = await readFile(indexPath, "utf-8");
999
+ }
1000
+ catch {
1001
+ continue;
1002
+ }
1003
+ let index;
1004
+ try {
1005
+ index = JSON.parse(raw);
1006
+ }
1007
+ catch {
1008
+ continue;
1009
+ }
1010
+ if (!Array.isArray(index.entries))
1011
+ continue;
1012
+ for (const entry of index.entries) {
1013
+ const sid = entry.sessionId;
1014
+ if (!sid || !remaining.has(sid))
1015
+ continue;
1016
+ result.set(sid, {
1017
+ summary: entry.summary,
1018
+ firstPrompt: entry.firstPrompt ?? "",
1019
+ lastPrompt: entry.lastPrompt,
1020
+ projectPath: normalizeWorktreePath(entry.projectPath ?? ""),
1021
+ });
1022
+ remaining.delete(sid);
1023
+ }
1024
+ }
1025
+ return result;
1026
+ }
1027
+ //# sourceMappingURL=sessions-index.js.map