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.
- package/AGENTS.md +31 -1
- package/README.md +61 -7
- package/apps/ui/AGENTS.md +70 -0
- package/apps/ui/README.md +72 -0
- package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
- package/apps/ui/dist/client/assets/analytics-CqWZmyV6.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +1 -0
- package/apps/ui/dist/client/assets/data-table-DnPYMPCD.js +4 -0
- package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +11 -0
- package/apps/ui/dist/client/assets/download-DOwxk-cG.js +1 -0
- package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +41 -0
- package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +1 -0
- package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +22 -0
- package/apps/ui/dist/client/assets/input-CEsI7EpI.js +1 -0
- package/apps/ui/dist/client/assets/metric-card-9jwBF7rG.js +1 -0
- package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +1 -0
- package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +1 -0
- package/apps/ui/dist/client/assets/select-CFim44gT.js +1 -0
- package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +1 -0
- package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-DT75NiBa.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +7 -0
- package/apps/ui/dist/client/favicon.ico +0 -0
- package/apps/ui/dist/client/logo192.png +0 -0
- package/apps/ui/dist/client/logo512.png +0 -0
- package/apps/ui/dist/client/manifest.json +25 -0
- package/apps/ui/dist/client/robots.txt +3 -0
- package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
- package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
- package/apps/ui/dist/server/assets/analytics-BMxW_bZL.js +139 -0
- package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
- package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
- package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
- package/apps/ui/dist/server/assets/codex-server-BFZq2Y2O.js +2062 -0
- package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
- package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
- package/apps/ui/dist/server/assets/download-C5rkk_Bo.js +289 -0
- package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
- package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
- package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
- package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
- package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
- package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
- package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +31 -0
- package/apps/ui/dist/server/assets/projects._project-CJ7l0ynC.js +18 -0
- package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
- package/apps/ui/dist/server/assets/projects._project-CcJLp_A8.js +337 -0
- package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
- package/apps/ui/dist/server/assets/projects.index-srtogpuF.js +172 -0
- package/apps/ui/dist/server/assets/router-C_w-haH6.js +307 -0
- package/apps/ui/dist/server/assets/routes-BhbxvJE7.js +34 -0
- package/apps/ui/dist/server/assets/routes-CPe-ppmC.js +169 -0
- package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
- package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
- package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
- package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
- package/apps/ui/dist/server/assets/start-HeKLHD9b.js +4 -0
- package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
- package/apps/ui/dist/server/assets/threads._threadId-Ba7vv6-K.js +18 -0
- package/apps/ui/dist/server/assets/threads._threadId-euyNckhj.js +1059 -0
- package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
- package/apps/ui/dist/server/server.js +5678 -0
- package/package.json +53 -7
- package/src/export-chats.ts +4 -18
- package/src/lib/claude-exporter.ts +1 -1
- package/src/lib/codex-analytics.ts +100 -0
- package/src/lib/codex-browser-db.ts +605 -0
- package/src/lib/codex-browser-export.ts +429 -0
- package/src/lib/codex-browser-types.ts +224 -0
- package/src/lib/codex-exporter-cli.ts +6 -1
- package/src/lib/codex-exporter-db.ts +19 -20
- package/src/lib/codex-exporter-transcript.ts +158 -34
- package/src/lib/codex-exporter-types.ts +8 -0
- package/src/lib/codex-thread-cache.ts +58 -0
- package/src/lib/codex-thread-parser.ts +604 -0
- package/src/lib/interactive-cli.ts +10 -25
- package/src/lib/model-label.ts +24 -0
- package/src/lib/native-open.ts +54 -0
- package/src/lib/path-transforms.ts +46 -0
- package/src/lib/shared.ts +15 -1
- package/src/lib/sqlite-error.ts +14 -0
- package/src/lib/sqlite-retry.ts +53 -0
- package/src/lib/ui-cache.ts +96 -0
- package/src/lib/ui-export-files.ts +77 -0
- package/src/mcp-server.ts +1 -0
- package/src/spiracha.ts +16 -4
- 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 {
|
|
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
|
-
|
|
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
|
-
}
|
|
407
|
-
db.close();
|
|
408
|
-
}
|
|
406
|
+
});
|
|
409
407
|
};
|
|
410
408
|
|
|
411
409
|
const resolveInteractiveDbPath = (): string => {
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
438
|
-
db.close();
|
|
439
|
-
}
|
|
424
|
+
});
|
|
440
425
|
};
|
|
441
426
|
|
|
442
427
|
const createPromptInterface = (): Interface => {
|