spiracha 1.0.0 → 1.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 (88) hide show
  1. package/AGENTS.md +28 -1
  2. package/README.md +47 -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-BjYaHqXk.js +1 -0
  7. package/apps/ui/dist/client/assets/checkbox-wPoGG3of.js +1 -0
  8. package/apps/ui/dist/client/assets/data-table-6yDgAdtf.js +4 -0
  9. package/apps/ui/dist/client/assets/delete-confirm-dialog-DJUAk7ha.js +11 -0
  10. package/apps/ui/dist/client/assets/download-BhWd-Pm5.js +1 -0
  11. package/apps/ui/dist/client/assets/es2015-BlyMI4CF.js +41 -0
  12. package/apps/ui/dist/client/assets/formatters-BxjZwWSE.js +1 -0
  13. package/apps/ui/dist/client/assets/index-T01rPkb4.js +22 -0
  14. package/apps/ui/dist/client/assets/input-B3YN8gzg.js +1 -0
  15. package/apps/ui/dist/client/assets/metric-card-BWW7TWER.js +1 -0
  16. package/apps/ui/dist/client/assets/page-header-BZ8Gnxgs.js +1 -0
  17. package/apps/ui/dist/client/assets/projects._project-B7XcpoLt.js +1 -0
  18. package/apps/ui/dist/client/assets/projects._project-EfBhCHPY.js +1 -0
  19. package/apps/ui/dist/client/assets/projects.index-4vfIwLjw.js +1 -0
  20. package/apps/ui/dist/client/assets/projects.index-DzEZ4pAJ.js +1 -0
  21. package/apps/ui/dist/client/assets/routes-CWCCZykE.js +1 -0
  22. package/apps/ui/dist/client/assets/select-DLXGsyZ4.js +1 -0
  23. package/apps/ui/dist/client/assets/settings-b0Xthfae.js +1 -0
  24. package/apps/ui/dist/client/assets/styles-8Wtc8YJw.css +1 -0
  25. package/apps/ui/dist/client/assets/threads._threadId-CgtoCqTb.js +1 -0
  26. package/apps/ui/dist/client/assets/threads._threadId-DBiDb38K.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-BjsXNYgm.js +99 -0
  34. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
  35. package/apps/ui/dist/server/assets/analytics-Br_fZB6a.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-Cqh0hb93.js +1995 -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-CzHmFWGk.js +286 -0
  43. package/apps/ui/dist/server/assets/formatters-B6o5pTY9.js +72 -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/page-header-CxdZM86z.js +25 -0
  48. package/apps/ui/dist/server/assets/path-transforms-DD1e7rhY.js +31 -0
  49. package/apps/ui/dist/server/assets/projects._project-Bwf6iJC-.js +335 -0
  50. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
  51. package/apps/ui/dist/server/assets/projects._project-DdVSdfPe.js +18 -0
  52. package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
  53. package/apps/ui/dist/server/assets/projects.index-DKeVeqUZ.js +171 -0
  54. package/apps/ui/dist/server/assets/router-ve2Hrl2Y.js +307 -0
  55. package/apps/ui/dist/server/assets/routes-BJyx5OmO.js +34 -0
  56. package/apps/ui/dist/server/assets/routes-pkOwjjYc.js +168 -0
  57. package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
  58. package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
  59. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
  60. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
  61. package/apps/ui/dist/server/assets/start-BAvbjjfs.js +4 -0
  62. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
  63. package/apps/ui/dist/server/assets/threads._threadId-D3PYZIwl.js +18 -0
  64. package/apps/ui/dist/server/assets/threads._threadId-D3xaWM86.js +1037 -0
  65. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
  66. package/apps/ui/dist/server/server.js +5678 -0
  67. package/package.json +47 -7
  68. package/src/export-chats.ts +1 -14
  69. package/src/lib/codex-analytics.ts +100 -0
  70. package/src/lib/codex-browser-db.ts +518 -0
  71. package/src/lib/codex-browser-export.ts +418 -0
  72. package/src/lib/codex-browser-types.ts +224 -0
  73. package/src/lib/codex-exporter-cli.ts +5 -0
  74. package/src/lib/codex-exporter-transcript.ts +143 -32
  75. package/src/lib/codex-exporter-types.ts +8 -0
  76. package/src/lib/codex-thread-cache.ts +58 -0
  77. package/src/lib/codex-thread-parser.ts +604 -0
  78. package/src/lib/interactive-cli.ts +5 -13
  79. package/src/lib/native-open.ts +54 -0
  80. package/src/lib/path-transforms.ts +45 -0
  81. package/src/lib/shared.ts +37 -1
  82. package/src/lib/sqlite-error.ts +14 -0
  83. package/src/lib/sqlite-retry.ts +39 -0
  84. package/src/lib/ui-cache.ts +96 -0
  85. package/src/lib/ui-export-files.ts +77 -0
  86. package/src/mcp-server.ts +1 -0
  87. package/src/spiracha.ts +14 -1
  88. 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,13 @@
1
1
  import { Database } from 'bun:sqlite';
2
2
  import { access, lstat } from 'node:fs/promises';
3
- import os from 'node:os';
4
3
  import path from 'node:path';
5
4
  import { stdin as input, stdout as output } from 'node:process';
6
5
  import { createInterface, type Interface } from 'node:readline/promises';
7
6
  import { checkbox } from '@inquirer/prompts';
8
7
  import { type ClaudeCliOptions, runClaudeExport } from './claude-exporter';
8
+ import { resolveCodexThreadDbPath } from './codex-browser-db';
9
9
  import { type CodexCliOptions, runCodexExport } from './codex-exporter';
10
- import { DEFAULT_DB_PATH, DEFAULT_INPUT_DIR } from './codex-exporter-types';
10
+ import { DEFAULT_INPUT_DIR } from './codex-exporter-types';
11
11
  import { type ExportFormat, expandHome, getPortablePathBasename } from './shared';
12
12
 
13
13
  type InteractiveTargetKind =
@@ -309,6 +309,7 @@ const promptForCommonCodexOptions = async (
309
309
  ): Promise<CodexCliOptions> => {
310
310
  const outputFormat = await promptForOutputFormat(rl);
311
311
  const optimized = await promptYesNo(rl, 'Use optimized output? [y/N]: ', false);
312
+ const includeCommentary = await promptYesNo(rl, 'Include commentary messages? [y/N]: ', false);
312
313
  const includeTools = await promptYesNo(rl, 'Include tool logs? [y/N]: ', false);
313
314
  const flat = await promptYesNo(rl, 'Write to a flat output folder? [y/N]: ', false);
314
315
  const outputDir = await promptOptionalPath(rl, 'Optional output directory (leave blank for default):\n> ');
@@ -317,6 +318,7 @@ const promptForCommonCodexOptions = async (
317
318
  cwdFilter: target.cwdFilter,
318
319
  dbPath,
319
320
  flat,
321
+ includeCommentary,
320
322
  includeTools,
321
323
  inputDir: DEFAULT_INPUT_DIR,
322
324
  optimized,
@@ -409,17 +411,7 @@ const listCodexProjects = (dbPath: string): string[] => {
409
411
  };
410
412
 
411
413
  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(', ')}`);
414
+ return resolveCodexThreadDbPath();
423
415
  };
424
416
 
425
417
  const listThreadIdsForProjects = (dbPath: string, projectNames: string[]): string[] => {
@@ -0,0 +1,54 @@
1
+ type NativeOpenTarget = {
2
+ kind: 'path' | 'url';
3
+ value: string;
4
+ };
5
+
6
+ const resolveNativeOpenCommand = () => {
7
+ if (process.platform === 'darwin') {
8
+ return {
9
+ argv: (target: NativeOpenTarget) => ['open', target.value],
10
+ label: 'open',
11
+ };
12
+ }
13
+
14
+ if (process.platform === 'win32') {
15
+ return {
16
+ argv: (target: NativeOpenTarget) => ['cmd', '/c', 'start', '', target.value],
17
+ label: 'start',
18
+ };
19
+ }
20
+
21
+ return {
22
+ argv: (target: NativeOpenTarget) => ['xdg-open', target.value],
23
+ label: 'xdg-open',
24
+ };
25
+ };
26
+
27
+ const openNatively = async (target: NativeOpenTarget) => {
28
+ const command = resolveNativeOpenCommand();
29
+ const proc = Bun.spawn(command.argv(target), {
30
+ stderr: 'pipe',
31
+ stdout: 'ignore',
32
+ });
33
+ const exitCode = await proc.exited;
34
+ if (exitCode !== 0) {
35
+ const errorText = await new Response(proc.stderr).text();
36
+ throw new Error(
37
+ `Failed to open ${target.value} with ${command.label}: ${errorText.trim() || `exit code ${exitCode}`}`,
38
+ );
39
+ }
40
+ };
41
+
42
+ export const openPathNatively = async (targetPath: string) => {
43
+ await openNatively({
44
+ kind: 'path',
45
+ value: targetPath,
46
+ });
47
+ };
48
+
49
+ export const openUrlNatively = async (url: string) => {
50
+ await openNatively({
51
+ kind: 'url',
52
+ value: url,
53
+ });
54
+ };