spiracha 1.0.0 → 1.1.1

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 (92) hide show
  1. package/AGENTS.md +31 -1
  2. package/README.md +61 -7
  3. package/apps/ui/AGENTS.md +70 -0
  4. package/apps/ui/README.md +72 -0
  5. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
  6. package/apps/ui/dist/client/assets/analytics-CqWZmyV6.js +1 -0
  7. package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +1 -0
  8. package/apps/ui/dist/client/assets/data-table-DnPYMPCD.js +4 -0
  9. package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +11 -0
  10. package/apps/ui/dist/client/assets/download-DOwxk-cG.js +1 -0
  11. package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +41 -0
  12. package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +1 -0
  13. package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +22 -0
  14. package/apps/ui/dist/client/assets/input-CEsI7EpI.js +1 -0
  15. package/apps/ui/dist/client/assets/metric-card-9jwBF7rG.js +1 -0
  16. package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +1 -0
  17. package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
  18. package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +1 -0
  19. package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
  20. package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +1 -0
  21. package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +1 -0
  22. package/apps/ui/dist/client/assets/select-CFim44gT.js +1 -0
  23. package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +1 -0
  24. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
  25. package/apps/ui/dist/client/assets/threads._threadId-DT75NiBa.js +1 -0
  26. package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +7 -0
  27. package/apps/ui/dist/client/favicon.ico +0 -0
  28. package/apps/ui/dist/client/logo192.png +0 -0
  29. package/apps/ui/dist/client/logo512.png +0 -0
  30. package/apps/ui/dist/client/manifest.json +25 -0
  31. package/apps/ui/dist/client/robots.txt +3 -0
  32. package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
  33. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
  34. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
  35. package/apps/ui/dist/server/assets/analytics-BMxW_bZL.js +139 -0
  36. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
  37. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
  38. package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
  39. package/apps/ui/dist/server/assets/codex-server-BFZq2Y2O.js +2062 -0
  40. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
  41. package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
  42. package/apps/ui/dist/server/assets/download-C5rkk_Bo.js +289 -0
  43. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
  44. package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
  45. package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
  46. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
  47. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
  48. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
  49. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +31 -0
  50. package/apps/ui/dist/server/assets/projects._project-CJ7l0ynC.js +18 -0
  51. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
  52. package/apps/ui/dist/server/assets/projects._project-CcJLp_A8.js +337 -0
  53. package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
  54. package/apps/ui/dist/server/assets/projects.index-srtogpuF.js +172 -0
  55. package/apps/ui/dist/server/assets/router-C_w-haH6.js +307 -0
  56. package/apps/ui/dist/server/assets/routes-BhbxvJE7.js +34 -0
  57. package/apps/ui/dist/server/assets/routes-CPe-ppmC.js +169 -0
  58. package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
  59. package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
  60. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
  61. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
  62. package/apps/ui/dist/server/assets/start-HeKLHD9b.js +4 -0
  63. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
  64. package/apps/ui/dist/server/assets/threads._threadId-Ba7vv6-K.js +18 -0
  65. package/apps/ui/dist/server/assets/threads._threadId-euyNckhj.js +1059 -0
  66. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
  67. package/apps/ui/dist/server/server.js +5678 -0
  68. package/package.json +53 -7
  69. package/src/export-chats.ts +4 -18
  70. package/src/lib/claude-exporter.ts +1 -1
  71. package/src/lib/codex-analytics.ts +100 -0
  72. package/src/lib/codex-browser-db.ts +605 -0
  73. package/src/lib/codex-browser-export.ts +429 -0
  74. package/src/lib/codex-browser-types.ts +224 -0
  75. package/src/lib/codex-exporter-cli.ts +6 -1
  76. package/src/lib/codex-exporter-db.ts +19 -20
  77. package/src/lib/codex-exporter-transcript.ts +158 -34
  78. package/src/lib/codex-exporter-types.ts +8 -0
  79. package/src/lib/codex-thread-cache.ts +58 -0
  80. package/src/lib/codex-thread-parser.ts +604 -0
  81. package/src/lib/interactive-cli.ts +10 -25
  82. package/src/lib/model-label.ts +24 -0
  83. package/src/lib/native-open.ts +54 -0
  84. package/src/lib/path-transforms.ts +46 -0
  85. package/src/lib/shared.ts +15 -1
  86. package/src/lib/sqlite-error.ts +14 -0
  87. package/src/lib/sqlite-retry.ts +53 -0
  88. package/src/lib/ui-cache.ts +96 -0
  89. package/src/lib/ui-export-files.ts +77 -0
  90. package/src/mcp-server.ts +1 -0
  91. package/src/spiracha.ts +16 -4
  92. package/src/ui-cli.ts +310 -0
@@ -0,0 +1,604 @@
1
+ import type {
2
+ DynamicToolDefinition,
3
+ MessageEvent,
4
+ ParsedCodexTranscript,
5
+ ReasoningEvent,
6
+ SessionMetaExtended,
7
+ TaskCompleteEvent,
8
+ TaskStartedEvent,
9
+ ThreadEvent,
10
+ ThreadTranscriptStats,
11
+ TokenCountEvent,
12
+ ToolCallEvent,
13
+ ToolOutputEvent,
14
+ TurnContextRecord,
15
+ WebSearchEvent,
16
+ } from './codex-browser-types';
17
+ import { asNumber, asObject, asString, type JsonValue, readJsonlObjects } from './shared';
18
+
19
+ type ParseCodexTranscriptOptions = {
20
+ includeRaw?: boolean;
21
+ maxEvents?: number;
22
+ maxTurnContexts?: number;
23
+ sourceFileSizeBytes?: number | null;
24
+ };
25
+
26
+ const createEmptyStats = (): ThreadTranscriptStats => {
27
+ return {
28
+ assistantMessageCount: 0,
29
+ commentaryCount: 0,
30
+ execCommandCount: 0,
31
+ finalAnswerCount: 0,
32
+ messageCount: 0,
33
+ toolCallCount: 0,
34
+ toolOutputCount: 0,
35
+ userMessageCount: 0,
36
+ webSearchEventCount: 0,
37
+ };
38
+ };
39
+
40
+ const createEmptySessionMeta = (): SessionMetaExtended => {
41
+ return {
42
+ baseInstructions: null,
43
+ cli_version: undefined,
44
+ cwd: undefined,
45
+ dynamicTools: [],
46
+ git: null,
47
+ id: undefined,
48
+ modelProvider: null,
49
+ originator: undefined,
50
+ source: undefined,
51
+ threadSource: null,
52
+ timestamp: undefined,
53
+ };
54
+ };
55
+
56
+ export const parseCodexTranscriptFile = async (
57
+ sessionFile: string,
58
+ options: ParseCodexTranscriptOptions = {},
59
+ ): Promise<ParsedCodexTranscript> => {
60
+ const sessionMeta = createEmptySessionMeta();
61
+ const turnContexts: TurnContextRecord[] = [];
62
+ const events: ThreadEvent[] = [];
63
+ const stats = createEmptyStats();
64
+ const includeRaw = options.includeRaw ?? true;
65
+ const maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
66
+ const maxTurnContexts = options.maxTurnContexts ?? Number.POSITIVE_INFINITY;
67
+ let sequence = 0;
68
+
69
+ for await (const parsed of readJsonlObjects(sessionFile)) {
70
+ captureSessionMeta(parsed, sessionMeta);
71
+ const topLevelType = asString(parsed.type);
72
+
73
+ if (topLevelType === 'turn_context') {
74
+ if (turnContexts.length < maxTurnContexts) {
75
+ captureTurnContext(parsed, turnContexts);
76
+ }
77
+ continue;
78
+ }
79
+
80
+ const event = toThreadEvent(parsed, sequence, includeRaw);
81
+ if (!event) {
82
+ continue;
83
+ }
84
+
85
+ events.push(event);
86
+ updateTranscriptStats(stats, event);
87
+ sequence += 1;
88
+
89
+ if (events.length >= maxEvents) {
90
+ break;
91
+ }
92
+ }
93
+
94
+ return {
95
+ events,
96
+ isPartial: Number.isFinite(maxEvents) || Number.isFinite(maxTurnContexts),
97
+ rawIncluded: includeRaw,
98
+ sessionMeta,
99
+ sourceFileSizeBytes: options.sourceFileSizeBytes ?? null,
100
+ stats,
101
+ statsArePartial: Number.isFinite(maxEvents),
102
+ turnContexts,
103
+ };
104
+ };
105
+
106
+ const captureSessionMeta = (parsed: Record<string, JsonValue>, sessionMeta: SessionMetaExtended) => {
107
+ if (parsed.type !== 'session_meta') {
108
+ return;
109
+ }
110
+
111
+ const payload = asObject(parsed.payload);
112
+ if (!payload) {
113
+ return;
114
+ }
115
+
116
+ sessionMeta.baseInstructions = payload.base_instructions ?? sessionMeta.baseInstructions;
117
+ sessionMeta.cli_version = asString(payload.cli_version) ?? sessionMeta.cli_version;
118
+ sessionMeta.cwd = asString(payload.cwd) ?? sessionMeta.cwd;
119
+ sessionMeta.dynamicTools = parseDynamicTools(payload.dynamic_tools) ?? sessionMeta.dynamicTools;
120
+ sessionMeta.git = asObject(payload.git) ?? sessionMeta.git;
121
+ sessionMeta.id = asString(payload.id) ?? sessionMeta.id;
122
+ sessionMeta.modelProvider = asString(payload.model_provider) ?? sessionMeta.modelProvider;
123
+ sessionMeta.originator = asString(payload.originator) ?? sessionMeta.originator;
124
+ sessionMeta.source = asString(payload.source) ?? sessionMeta.source;
125
+ sessionMeta.threadSource = asString(payload.thread_source) ?? sessionMeta.threadSource;
126
+ sessionMeta.timestamp = asString(payload.timestamp) ?? sessionMeta.timestamp;
127
+ };
128
+
129
+ const parseDynamicTools = (value: JsonValue | undefined): DynamicToolDefinition[] | null => {
130
+ if (!Array.isArray(value)) {
131
+ return null;
132
+ }
133
+
134
+ return value.flatMap((entry) => {
135
+ const tool = asObject(entry);
136
+ if (!tool) {
137
+ return [];
138
+ }
139
+
140
+ return [
141
+ {
142
+ deferLoading: tool.deferLoading === true || tool.defer_loading === true,
143
+ description: asString(tool.description) ?? '',
144
+ inputSchema: asObject(tool.inputSchema) ?? asObject(tool.input_schema) ?? null,
145
+ name: asString(tool.name) ?? 'unknown',
146
+ namespace: asString(tool.namespace),
147
+ },
148
+ ];
149
+ });
150
+ };
151
+
152
+ const captureTurnContext = (parsed: Record<string, JsonValue>, turnContexts: TurnContextRecord[]) => {
153
+ const payload = asObject(parsed.payload);
154
+ if (!payload) {
155
+ return;
156
+ }
157
+
158
+ turnContexts.push({
159
+ payload,
160
+ timestamp: asString(parsed.timestamp),
161
+ });
162
+ };
163
+
164
+ const toThreadEvent = (
165
+ parsed: Record<string, JsonValue>,
166
+ sequence: number,
167
+ includeRaw: boolean,
168
+ ): ThreadEvent | null => {
169
+ const payload = asObject(parsed.payload);
170
+ if (!payload) {
171
+ return null;
172
+ }
173
+
174
+ const payloadType = asString(payload.type);
175
+ const timestamp = asString(parsed.timestamp);
176
+
177
+ if (parsed.type === 'event_msg') {
178
+ return buildEventMessage(payload, payloadType, includeRaw ? parsed : {}, sequence, timestamp);
179
+ }
180
+
181
+ if (parsed.type !== 'response_item') {
182
+ return null;
183
+ }
184
+
185
+ return buildResponseItemEvent(payload, payloadType, includeRaw ? parsed : {}, sequence, timestamp);
186
+ };
187
+
188
+ const buildEventMessage = (
189
+ payload: Record<string, JsonValue>,
190
+ payloadType: string | null,
191
+ raw: Record<string, JsonValue>,
192
+ sequence: number,
193
+ timestamp: string | null,
194
+ ) => {
195
+ if (payloadType === 'task_started') {
196
+ return createTaskStartedEvent(payload, raw, sequence, timestamp);
197
+ }
198
+
199
+ if (payloadType === 'task_complete') {
200
+ return createTaskCompleteEvent(payload, raw, sequence, timestamp);
201
+ }
202
+
203
+ return null;
204
+ };
205
+
206
+ const buildResponseItemEvent = (
207
+ payload: Record<string, JsonValue>,
208
+ payloadType: string | null,
209
+ raw: Record<string, JsonValue>,
210
+ sequence: number,
211
+ timestamp: string | null,
212
+ ) => {
213
+ if (payloadType === 'message') {
214
+ return createMessageEvent(payload, raw, sequence, timestamp);
215
+ }
216
+
217
+ if (payloadType === 'user_message') {
218
+ return createUserMessageEvent(payload, raw, sequence, timestamp);
219
+ }
220
+
221
+ if (payloadType === 'agent_message') {
222
+ return createAgentMessageEvent(payload, raw, sequence, timestamp);
223
+ }
224
+
225
+ if (payloadType === 'function_call') {
226
+ return createToolCallEvent(payload, raw, sequence, timestamp);
227
+ }
228
+
229
+ if (payloadType === 'function_call_output') {
230
+ return createToolOutputEvent(payload, raw, sequence, timestamp);
231
+ }
232
+
233
+ if (payloadType === 'reasoning') {
234
+ return createReasoningEvent(payload, raw, sequence, timestamp);
235
+ }
236
+
237
+ if (payloadType === 'token_count') {
238
+ return createTokenCountEvent(payload, raw, sequence, timestamp);
239
+ }
240
+
241
+ if (payloadType === 'web_search_call' || payloadType === 'web_search_end') {
242
+ return createWebSearchEvent(payload, raw, sequence, timestamp);
243
+ }
244
+
245
+ if (payloadType === 'task_started') {
246
+ return createTaskStartedEvent(payload, raw, sequence, timestamp);
247
+ }
248
+
249
+ if (payloadType === 'task_complete') {
250
+ return createTaskCompleteEvent(payload, raw, sequence, timestamp);
251
+ }
252
+
253
+ return null;
254
+ };
255
+
256
+ const createMessageEvent = (
257
+ payload: Record<string, JsonValue>,
258
+ raw: Record<string, JsonValue>,
259
+ sequence: number,
260
+ timestamp: string | null,
261
+ ): MessageEvent | null => {
262
+ const role = asString(payload.role);
263
+ const content = payload.content;
264
+ if (!role || content === undefined) {
265
+ return null;
266
+ }
267
+
268
+ return {
269
+ isHiddenByDefault: shouldHideTranscriptText(role, extractText(content)),
270
+ kind: 'message',
271
+ memoryCitation: null,
272
+ model: asString(payload.model),
273
+ phase: asString(payload.phase),
274
+ raw,
275
+ role,
276
+ sequence,
277
+ text: extractText(content),
278
+ timestamp,
279
+ variant: 'message',
280
+ };
281
+ };
282
+
283
+ const createUserMessageEvent = (
284
+ payload: Record<string, JsonValue>,
285
+ raw: Record<string, JsonValue>,
286
+ sequence: number,
287
+ timestamp: string | null,
288
+ ): MessageEvent => {
289
+ return {
290
+ isHiddenByDefault: shouldHideTranscriptText('user', asString(payload.message)?.trim() ?? ''),
291
+ kind: 'message',
292
+ memoryCitation: null,
293
+ model: null,
294
+ phase: null,
295
+ raw,
296
+ role: 'user',
297
+ sequence,
298
+ text: asString(payload.message)?.trim() ?? '',
299
+ timestamp,
300
+ variant: 'user_message',
301
+ };
302
+ };
303
+
304
+ const createAgentMessageEvent = (
305
+ payload: Record<string, JsonValue>,
306
+ raw: Record<string, JsonValue>,
307
+ sequence: number,
308
+ timestamp: string | null,
309
+ ): MessageEvent => {
310
+ return {
311
+ isHiddenByDefault: false,
312
+ kind: 'message',
313
+ memoryCitation: payload.memory_citation ?? null,
314
+ model: asString(payload.model),
315
+ phase: asString(payload.phase),
316
+ raw,
317
+ role: 'assistant',
318
+ sequence,
319
+ text: asString(payload.message)?.trim() ?? '',
320
+ timestamp,
321
+ variant: 'agent_message',
322
+ };
323
+ };
324
+
325
+ const createToolCallEvent = (
326
+ payload: Record<string, JsonValue>,
327
+ raw: Record<string, JsonValue>,
328
+ sequence: number,
329
+ timestamp: string | null,
330
+ ): ToolCallEvent => {
331
+ const name = asString(payload.name) ?? 'unknown';
332
+ const argumentsText = asString(payload.arguments);
333
+ const parsedArguments = parseExecCommandArguments(argumentsText);
334
+
335
+ return {
336
+ argumentsParseFailed: parsedArguments.argumentsParseFailed,
337
+ argumentsText,
338
+ callId: asString(payload.call_id),
339
+ command: parsedArguments.cmd,
340
+ kind: 'tool_call',
341
+ name,
342
+ raw,
343
+ sequence,
344
+ timestamp,
345
+ workdir: parsedArguments.workdir,
346
+ };
347
+ };
348
+
349
+ const createToolOutputEvent = (
350
+ payload: Record<string, JsonValue>,
351
+ raw: Record<string, JsonValue>,
352
+ sequence: number,
353
+ timestamp: string | null,
354
+ ): ToolOutputEvent => {
355
+ const outputText = asString(payload.output) ?? '';
356
+
357
+ return {
358
+ callId: asString(payload.call_id),
359
+ exitCode: parseExitCode(outputText),
360
+ kind: 'tool_output',
361
+ outputText,
362
+ raw,
363
+ sequence,
364
+ summary: formatToolOutputSummary(outputText),
365
+ timestamp,
366
+ wallTime: parseWallTime(outputText),
367
+ };
368
+ };
369
+
370
+ const createReasoningEvent = (
371
+ payload: Record<string, JsonValue>,
372
+ raw: Record<string, JsonValue>,
373
+ sequence: number,
374
+ timestamp: string | null,
375
+ ): ReasoningEvent => {
376
+ return {
377
+ content: payload.content ?? null,
378
+ hasEncryptedContent: Boolean(asString(payload.encrypted_content)),
379
+ kind: 'reasoning',
380
+ raw,
381
+ sequence,
382
+ summary: toStringArray(payload.summary),
383
+ timestamp,
384
+ };
385
+ };
386
+
387
+ const createTokenCountEvent = (
388
+ payload: Record<string, JsonValue>,
389
+ raw: Record<string, JsonValue>,
390
+ sequence: number,
391
+ timestamp: string | null,
392
+ ): TokenCountEvent => {
393
+ return {
394
+ info: payload.info ?? null,
395
+ kind: 'token_count',
396
+ rateLimits: payload.rate_limits ?? null,
397
+ raw,
398
+ sequence,
399
+ timestamp,
400
+ };
401
+ };
402
+
403
+ const createTaskStartedEvent = (
404
+ payload: Record<string, JsonValue>,
405
+ raw: Record<string, JsonValue>,
406
+ sequence: number,
407
+ timestamp: string | null,
408
+ ): TaskStartedEvent => {
409
+ return {
410
+ collaborationModeKind: asString(payload.collaboration_mode_kind),
411
+ kind: 'task_started',
412
+ modelContextWindow: asNumber(payload.model_context_window),
413
+ raw,
414
+ sequence,
415
+ startedAt: asNumber(payload.started_at),
416
+ timestamp,
417
+ turnId: asString(payload.turn_id),
418
+ };
419
+ };
420
+
421
+ const createTaskCompleteEvent = (
422
+ payload: Record<string, JsonValue>,
423
+ raw: Record<string, JsonValue>,
424
+ sequence: number,
425
+ timestamp: string | null,
426
+ ): TaskCompleteEvent => {
427
+ return {
428
+ completedAt: asNumber(payload.completed_at),
429
+ durationMs: asNumber(payload.duration_ms),
430
+ kind: 'task_complete',
431
+ lastAgentMessage: asString(payload.last_agent_message),
432
+ raw,
433
+ sequence,
434
+ timestamp,
435
+ timeToFirstTokenMs: asNumber(payload.time_to_first_token_ms),
436
+ turnId: asString(payload.turn_id),
437
+ };
438
+ };
439
+
440
+ const createWebSearchEvent = (
441
+ payload: Record<string, JsonValue>,
442
+ raw: Record<string, JsonValue>,
443
+ sequence: number,
444
+ timestamp: string | null,
445
+ ): WebSearchEvent => {
446
+ const payloadType = asString(payload.type);
447
+
448
+ return {
449
+ action: payload.action ?? null,
450
+ callId: asString(payload.call_id),
451
+ kind: 'web_search',
452
+ phase: payloadType === 'web_search_end' ? 'end' : 'call',
453
+ query: asString(payload.query),
454
+ raw,
455
+ sequence,
456
+ status: asString(payload.status),
457
+ timestamp,
458
+ };
459
+ };
460
+
461
+ const updateTranscriptStats = (stats: ThreadTranscriptStats, event: ThreadEvent) => {
462
+ if (event.kind === 'message') {
463
+ stats.messageCount += 1;
464
+ if (event.role === 'assistant') {
465
+ stats.assistantMessageCount += 1;
466
+ }
467
+ if (event.role === 'user') {
468
+ stats.userMessageCount += 1;
469
+ }
470
+ if (event.phase === 'commentary') {
471
+ stats.commentaryCount += 1;
472
+ }
473
+ if (event.phase === 'final_answer') {
474
+ stats.finalAnswerCount += 1;
475
+ }
476
+ return;
477
+ }
478
+
479
+ if (event.kind === 'tool_call') {
480
+ stats.toolCallCount += 1;
481
+ if (event.name === 'exec_command') {
482
+ stats.execCommandCount += 1;
483
+ }
484
+ return;
485
+ }
486
+
487
+ if (event.kind === 'tool_output') {
488
+ stats.toolOutputCount += 1;
489
+ return;
490
+ }
491
+
492
+ if (event.kind === 'web_search') {
493
+ stats.webSearchEventCount += 1;
494
+ }
495
+ };
496
+
497
+ const toStringArray = (value: JsonValue | undefined): string[] => {
498
+ if (!Array.isArray(value)) {
499
+ return [];
500
+ }
501
+
502
+ return value.map((entry) => asString(entry)).filter((entry): entry is string => Boolean(entry));
503
+ };
504
+
505
+ const parseExitCode = (outputText: string): number | null => {
506
+ const match = /Process exited with code (\d+)/u.exec(outputText);
507
+ return match ? Number(match[1]) : null;
508
+ };
509
+
510
+ const parseWallTime = (outputText: string): string | null => {
511
+ const match = /Wall time: ([^\n]+)/u.exec(outputText);
512
+ return match?.[1] ?? null;
513
+ };
514
+
515
+ const formatToolOutputSummary = (outputText: string): string => {
516
+ const lines = outputText
517
+ .split('\n')
518
+ .map((line) => line.trim())
519
+ .filter(Boolean);
520
+
521
+ return lines
522
+ .filter((line) => {
523
+ return (
524
+ line.startsWith('Command: ') ||
525
+ line.startsWith('Process exited with code ') ||
526
+ line.startsWith('Wall time: ')
527
+ );
528
+ })
529
+ .join('\n');
530
+ };
531
+
532
+ const parseExecCommandArguments = (argumentsText: string | null) => {
533
+ if (!argumentsText) {
534
+ return { argumentsParseFailed: false, cmd: null as string | null, workdir: null as string | null };
535
+ }
536
+
537
+ try {
538
+ const parsed = JSON.parse(argumentsText) as Record<string, unknown>;
539
+ return {
540
+ argumentsParseFailed: false,
541
+ cmd: typeof parsed.cmd === 'string' ? parsed.cmd : null,
542
+ workdir: typeof parsed.workdir === 'string' ? parsed.workdir : null,
543
+ };
544
+ } catch {
545
+ return { argumentsParseFailed: true, cmd: null as string | null, workdir: null as string | null };
546
+ }
547
+ };
548
+
549
+ const extractText = (content: JsonValue): string => {
550
+ if (typeof content === 'string') {
551
+ return content.trim();
552
+ }
553
+
554
+ if (Array.isArray(content)) {
555
+ return content
556
+ .map((entry) => extractTextPart(entry))
557
+ .filter(Boolean)
558
+ .join('\n\n')
559
+ .trim();
560
+ }
561
+
562
+ if (content && typeof content === 'object') {
563
+ return asString((content as Record<string, JsonValue>).text)?.trim() ?? '';
564
+ }
565
+
566
+ return '';
567
+ };
568
+
569
+ const extractTextPart = (entry: JsonValue): string => {
570
+ const objectValue = asObject(entry);
571
+ if (!objectValue) {
572
+ return '';
573
+ }
574
+
575
+ const type = asString(objectValue.type);
576
+ const text = asString(objectValue.text);
577
+
578
+ if (type === 'input_image') {
579
+ return '[Image attached]';
580
+ }
581
+
582
+ return text ?? '';
583
+ };
584
+
585
+ const shouldHideTranscriptText = (role: string, text: string) => {
586
+ if (!text) {
587
+ return true;
588
+ }
589
+
590
+ if (role === 'developer') {
591
+ return true;
592
+ }
593
+
594
+ return (
595
+ text.startsWith('# AGENTS.md instructions for ') ||
596
+ text.startsWith('<permissions instructions>') ||
597
+ text.startsWith('<app-context>') ||
598
+ text.startsWith('<environment_context>') ||
599
+ text.startsWith('<collaboration_mode>') ||
600
+ text.startsWith('<skills_instructions>') ||
601
+ text.startsWith('<plugins_instructions>') ||
602
+ text.includes('Filesystem sandboxing defines which files can be read or written.')
603
+ );
604
+ };
@@ -1,13 +1,12 @@
1
- import { Database } from 'bun:sqlite';
2
1
  import { access, lstat } from 'node:fs/promises';
3
- import os from 'node:os';
4
2
  import path from 'node:path';
5
3
  import { stdin as input, stdout as output } from 'node:process';
6
4
  import { createInterface, type Interface } from 'node:readline/promises';
7
5
  import { checkbox } from '@inquirer/prompts';
8
6
  import { type ClaudeCliOptions, runClaudeExport } from './claude-exporter';
7
+ import { resolveCodexThreadDbPath, withReadonlyDb } from './codex-browser-db';
9
8
  import { type CodexCliOptions, runCodexExport } from './codex-exporter';
10
- import { DEFAULT_DB_PATH, DEFAULT_INPUT_DIR } from './codex-exporter-types';
9
+ import { DEFAULT_INPUT_DIR } from './codex-exporter-types';
11
10
  import { type ExportFormat, expandHome, getPortablePathBasename } from './shared';
12
11
 
13
12
  type InteractiveTargetKind =
@@ -23,7 +22,7 @@ type InteractiveInference = {
23
22
  value: string | null;
24
23
  };
25
24
 
26
- type InteractiveExportResult =
25
+ export type InteractiveExportResult =
27
26
  | {
28
27
  mode: 'codex';
29
28
  outputDir: string;
@@ -309,6 +308,7 @@ const promptForCommonCodexOptions = async (
309
308
  ): Promise<CodexCliOptions> => {
310
309
  const outputFormat = await promptForOutputFormat(rl);
311
310
  const optimized = await promptYesNo(rl, 'Use optimized output? [y/N]: ', false);
311
+ const includeCommentary = await promptYesNo(rl, 'Include commentary messages? [y/N]: ', false);
312
312
  const includeTools = await promptYesNo(rl, 'Include tool logs? [y/N]: ', false);
313
313
  const flat = await promptYesNo(rl, 'Write to a flat output folder? [y/N]: ', false);
314
314
  const outputDir = await promptOptionalPath(rl, 'Optional output directory (leave blank for default):\n> ');
@@ -317,6 +317,7 @@ const promptForCommonCodexOptions = async (
317
317
  cwdFilter: target.cwdFilter,
318
318
  dbPath,
319
319
  flat,
320
+ includeCommentary,
320
321
  includeTools,
321
322
  inputDir: DEFAULT_INPUT_DIR,
322
323
  optimized,
@@ -397,29 +398,16 @@ const normalizeInteractiveThreadSelections = (value: string): string[] => {
397
398
  };
398
399
 
399
400
  const listCodexProjects = (dbPath: string): string[] => {
400
- const db = new Database(dbPath, { readonly: true });
401
- try {
401
+ return withReadonlyDb(dbPath, (db) => {
402
402
  const rows = db.query("SELECT DISTINCT cwd FROM threads WHERE cwd IS NOT NULL AND cwd != ''").all() as Array<{
403
403
  cwd: string;
404
404
  }>;
405
405
  return [...new Set(rows.map((row) => getPortablePathBasename(row.cwd)).filter(Boolean))].sort();
406
- } finally {
407
- db.close();
408
- }
406
+ });
409
407
  };
410
408
 
411
409
  const resolveInteractiveDbPath = (): string => {
412
- const candidates = [DEFAULT_DB_PATH, path.join(os.homedir(), '.codex', 'sqlite', 'state_5.sqlite')];
413
-
414
- for (const candidate of candidates) {
415
- try {
416
- const db = new Database(candidate, { readonly: true });
417
- db.close();
418
- return candidate;
419
- } catch {}
420
- }
421
-
422
- throw new Error(`Unable to open Codex thread database. Tried: ${candidates.join(', ')}`);
410
+ return resolveCodexThreadDbPath();
423
411
  };
424
412
 
425
413
  const listThreadIdsForProjects = (dbPath: string, projectNames: string[]): string[] => {
@@ -427,16 +415,13 @@ const listThreadIdsForProjects = (dbPath: string, projectNames: string[]): strin
427
415
  return [];
428
416
  }
429
417
 
430
- const db = new Database(dbPath, { readonly: true });
431
- try {
418
+ return withReadonlyDb(dbPath, (db) => {
432
419
  const projectNameSet = new Set(projectNames);
433
420
  const rows = db
434
421
  .query("SELECT id, cwd FROM threads WHERE cwd IS NOT NULL AND cwd != '' ORDER BY updated_at DESC")
435
422
  .all() as Array<{ id: string; cwd: string }>;
436
423
  return rows.filter((row) => projectNameSet.has(getPortablePathBasename(row.cwd))).map((row) => row.id);
437
- } finally {
438
- db.close();
439
- }
424
+ });
440
425
  };
441
426
 
442
427
  const createPromptInterface = (): Interface => {