@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.
- package/README.md +54 -0
- package/dist/claude-process.d.ts +108 -0
- package/dist/claude-process.js +471 -0
- package/dist/claude-process.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-process.d.ts +46 -0
- package/dist/codex-process.js +420 -0
- package/dist/codex-process.js.map +1 -0
- package/dist/debug-trace-store.d.ts +15 -0
- package/dist/debug-trace-store.js +78 -0
- package/dist/debug-trace-store.js.map +1 -0
- package/dist/firebase-auth.d.ts +35 -0
- package/dist/firebase-auth.js +132 -0
- package/dist/firebase-auth.js.map +1 -0
- package/dist/gallery-store.d.ts +66 -0
- package/dist/gallery-store.js +310 -0
- package/dist/gallery-store.js.map +1 -0
- package/dist/image-store.d.ts +22 -0
- package/dist/image-store.js +113 -0
- package/dist/image-store.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/mdns.d.ts +6 -0
- package/dist/mdns.js +42 -0
- package/dist/mdns.js.map +1 -0
- package/dist/parser.d.ts +381 -0
- package/dist/parser.js +218 -0
- package/dist/parser.js.map +1 -0
- package/dist/project-history.d.ts +10 -0
- package/dist/project-history.js +73 -0
- package/dist/project-history.js.map +1 -0
- package/dist/prompt-history-backup.d.ts +15 -0
- package/dist/prompt-history-backup.js +46 -0
- package/dist/prompt-history-backup.js.map +1 -0
- package/dist/push-relay.d.ts +27 -0
- package/dist/push-relay.js +69 -0
- package/dist/push-relay.js.map +1 -0
- package/dist/recording-store.d.ts +51 -0
- package/dist/recording-store.js +158 -0
- package/dist/recording-store.js.map +1 -0
- package/dist/screenshot.d.ts +28 -0
- package/dist/screenshot.js +98 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/sdk-process.d.ts +151 -0
- package/dist/sdk-process.js +740 -0
- package/dist/sdk-process.js.map +1 -0
- package/dist/session.d.ts +126 -0
- package/dist/session.js +550 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions-index.d.ts +86 -0
- package/dist/sessions-index.js +1027 -0
- package/dist/sessions-index.js.map +1 -0
- package/dist/setup-launchd.d.ts +8 -0
- package/dist/setup-launchd.js +109 -0
- package/dist/setup-launchd.js.map +1 -0
- package/dist/startup-info.d.ts +8 -0
- package/dist/startup-info.js +78 -0
- package/dist/startup-info.js.map +1 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +236 -0
- package/dist/usage.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.js +39 -0
- package/dist/version.js.map +1 -0
- package/dist/websocket.d.ts +71 -0
- package/dist/websocket.js +1487 -0
- package/dist/websocket.js.map +1 -0
- package/dist/worktree-store.d.ts +25 -0
- package/dist/worktree-store.js +59 -0
- package/dist/worktree-store.js.map +1 -0
- package/dist/worktree.d.ts +43 -0
- package/dist/worktree.js +295 -0
- package/dist/worktree.js.map +1 -0
- 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
|