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,429 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getThreadBrowseData } from './codex-browser-db';
|
|
5
|
+
import { convertSessionFile, writeSessionFileExport } from './codex-exporter-transcript';
|
|
6
|
+
import type { CodexCliOptions } from './codex-exporter-types';
|
|
7
|
+
import { applyPathTransforms, type PathDisplaySettings } from './path-transforms';
|
|
8
|
+
import { type ExportFormat, getPortablePathBasename } from './shared';
|
|
9
|
+
import { buildUiExportDownloadUrl, ensureUiExportDir } from './ui-export-files';
|
|
10
|
+
|
|
11
|
+
type RenderCodexThreadDownloadInput = {
|
|
12
|
+
dbPath: string;
|
|
13
|
+
includeCommentary: boolean;
|
|
14
|
+
includeTools: boolean;
|
|
15
|
+
largeExportThresholdBytes?: number;
|
|
16
|
+
optimized: boolean;
|
|
17
|
+
outputFormat: ExportFormat;
|
|
18
|
+
pathDisplaySettings?: Pick<PathDisplaySettings, 'convertToProjectRoot' | 'redactUsername'>;
|
|
19
|
+
publicExportDir?: string;
|
|
20
|
+
threadId: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RenderCodexThreadsDownloadInput = Omit<RenderCodexThreadDownloadInput, 'threadId'> & {
|
|
24
|
+
threadIds: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CodexThreadDownload =
|
|
28
|
+
| {
|
|
29
|
+
content: string;
|
|
30
|
+
fileName: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
mode: 'download';
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
downloadUrl: string;
|
|
36
|
+
fileName: string;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
mode: 'download_url';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const LARGE_BROWSER_EXPORT_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
const toSanitizedFileName = (value: string) => {
|
|
44
|
+
return value
|
|
45
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/gu, ' ')
|
|
46
|
+
.replace(/\.\.+/gu, ' ')
|
|
47
|
+
.replace(/\s+/gu, ' ')
|
|
48
|
+
.trim();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const formatReadableExportDate = (value: number) => {
|
|
52
|
+
const date = new Date(value);
|
|
53
|
+
const year = date.getUTCFullYear();
|
|
54
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
55
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
56
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
57
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
58
|
+
return `${year}-${month}-${day}-${hours}${minutes}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const buildExportBaseName = (thread: ReturnType<typeof getThreadBrowseData>['thread']) => {
|
|
62
|
+
const projectName = toSanitizedFileName(getPortablePathBasename(thread.cwd) || 'thread') || 'thread';
|
|
63
|
+
const timestamp = thread.updated_at_ms ?? thread.updated_at * 1000;
|
|
64
|
+
return `${projectName}-${formatReadableExportDate(timestamp)}-${thread.id.slice(0, 8)}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const buildBatchExportBaseName = (threads: Array<ReturnType<typeof getThreadBrowseData>['thread']>) => {
|
|
68
|
+
const firstThread = threads[0];
|
|
69
|
+
if (!firstThread) {
|
|
70
|
+
throw new Error('No threads selected for export');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const projectName = toSanitizedFileName(getPortablePathBasename(firstThread.cwd) || 'threads') || 'threads';
|
|
74
|
+
const latestTimestamp = Math.max(...threads.map((thread) => thread.updated_at_ms ?? thread.updated_at * 1000));
|
|
75
|
+
return `${projectName}-${formatReadableExportDate(latestTimestamp)}-threads-${threads.length}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const buildUniqueArchivePath = (exportDir: string, exportBaseName: string) => {
|
|
79
|
+
return path.join(exportDir, `${exportBaseName}-${randomUUID()}.zip`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type RolloutSnapshot = {
|
|
83
|
+
mtimeMs: number;
|
|
84
|
+
sizeBytes: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const toDownloadOptions = (input: RenderCodexThreadDownloadInput): CodexCliOptions => {
|
|
88
|
+
return {
|
|
89
|
+
cwdFilter: null,
|
|
90
|
+
dbPath: input.dbPath,
|
|
91
|
+
flat: false,
|
|
92
|
+
includeCommentary: input.includeCommentary,
|
|
93
|
+
includeTools: input.includeTools,
|
|
94
|
+
inputDir: '',
|
|
95
|
+
optimized: input.optimized,
|
|
96
|
+
outputDir: '',
|
|
97
|
+
outputFormat: input.outputFormat,
|
|
98
|
+
projectFilter: null,
|
|
99
|
+
threadIds: [input.threadId],
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getMimeType = (outputFormat: ExportFormat) => {
|
|
104
|
+
return outputFormat === 'md' ? 'text/markdown; charset=utf-8' : 'text/plain; charset=utf-8';
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const resolvePublicExportDir = async (publicExportDir?: string) => {
|
|
108
|
+
if (publicExportDir) {
|
|
109
|
+
await ensureDirectory(publicExportDir);
|
|
110
|
+
return publicExportDir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return ensureUiExportDir();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const ensureDirectory = async (directoryPath: string) => {
|
|
117
|
+
await mkdir(directoryPath, { recursive: true });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const createExportWorkspace = async (exportDir: string, exportBaseName: string) => {
|
|
121
|
+
return mkdtemp(path.join(exportDir, `${exportBaseName}-`));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const getRolloutSnapshot = async (rolloutPath: string): Promise<RolloutSnapshot> => {
|
|
125
|
+
const metadata = await stat(rolloutPath);
|
|
126
|
+
return {
|
|
127
|
+
mtimeMs: metadata.mtimeMs,
|
|
128
|
+
sizeBytes: metadata.size,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const logExportEvent = (level: 'error' | 'info' | 'warn', event: string, details: Record<string, unknown>) => {
|
|
133
|
+
console[level](`[spiracha:export] ${event}`, details);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const logRolloutChangeIfDetected = (
|
|
137
|
+
threadId: string,
|
|
138
|
+
rolloutPath: string,
|
|
139
|
+
beforeSnapshot: RolloutSnapshot,
|
|
140
|
+
afterSnapshot: RolloutSnapshot,
|
|
141
|
+
) => {
|
|
142
|
+
if (beforeSnapshot.mtimeMs === afterSnapshot.mtimeMs && beforeSnapshot.sizeBytes === afterSnapshot.sizeBytes) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
logExportEvent('warn', 'rollout_changed_during_export', {
|
|
147
|
+
afterMtimeMs: afterSnapshot.mtimeMs,
|
|
148
|
+
afterSizeBytes: afterSnapshot.sizeBytes,
|
|
149
|
+
beforeMtimeMs: beforeSnapshot.mtimeMs,
|
|
150
|
+
beforeSizeBytes: beforeSnapshot.sizeBytes,
|
|
151
|
+
rolloutPath,
|
|
152
|
+
threadId,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const cleanupExportWorkspace = async (workspacePath: string) => {
|
|
157
|
+
try {
|
|
158
|
+
await rm(workspacePath, { force: true, recursive: true });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logExportEvent('warn', 'workspace_cleanup_failed', {
|
|
161
|
+
error: error instanceof Error ? error.message : String(error),
|
|
162
|
+
workspacePath,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const zipExportFile = async (sourcePath: string, zipPath: string) => {
|
|
168
|
+
const proc = Bun.spawn(['zip', '-9', '-j', zipPath, sourcePath], {
|
|
169
|
+
stderr: 'pipe',
|
|
170
|
+
stdout: 'pipe',
|
|
171
|
+
});
|
|
172
|
+
const [stdoutText, stderrText, exitCode] = await Promise.all([
|
|
173
|
+
new Response(proc.stdout).text(),
|
|
174
|
+
new Response(proc.stderr).text(),
|
|
175
|
+
proc.exited,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
if (exitCode !== 0) {
|
|
179
|
+
throw new Error(`zip failed (${exitCode}): ${(stderrText || stdoutText).trim()}`);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const zipExportDirectory = async (sourceDirectory: string, zipPath: string) => {
|
|
184
|
+
const proc = Bun.spawn(['zip', '-9', '-r', zipPath, '.'], {
|
|
185
|
+
cwd: sourceDirectory,
|
|
186
|
+
stderr: 'pipe',
|
|
187
|
+
stdout: 'pipe',
|
|
188
|
+
});
|
|
189
|
+
const [stdoutText, stderrText, exitCode] = await Promise.all([
|
|
190
|
+
new Response(proc.stdout).text(),
|
|
191
|
+
new Response(proc.stderr).text(),
|
|
192
|
+
proc.exited,
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
if (exitCode !== 0) {
|
|
196
|
+
throw new Error(`zip failed (${exitCode}): ${(stderrText || stdoutText).trim()}`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const renderCodexThreadDownload = async (
|
|
201
|
+
input: RenderCodexThreadDownloadInput,
|
|
202
|
+
): Promise<CodexThreadDownload> => {
|
|
203
|
+
const startedAt = Date.now();
|
|
204
|
+
const browseData = getThreadBrowseData(input.dbPath, input.threadId);
|
|
205
|
+
const extension = input.outputFormat === 'md' ? 'md' : 'txt';
|
|
206
|
+
const fileBaseName = buildExportBaseName(browseData.thread);
|
|
207
|
+
const fileName = `${fileBaseName}.${extension}`;
|
|
208
|
+
const mimeType = getMimeType(input.outputFormat);
|
|
209
|
+
const transform = (text: string) =>
|
|
210
|
+
input.pathDisplaySettings
|
|
211
|
+
? applyPathTransforms(text, {
|
|
212
|
+
...input.pathDisplaySettings,
|
|
213
|
+
projectPath: browseData.thread.cwd,
|
|
214
|
+
})
|
|
215
|
+
: text;
|
|
216
|
+
const rolloutSnapshotBefore = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
217
|
+
|
|
218
|
+
logExportEvent('info', 'single_start', {
|
|
219
|
+
fileName,
|
|
220
|
+
rolloutPath: browseData.thread.rollout_path,
|
|
221
|
+
sizeBytes: rolloutSnapshotBefore.sizeBytes,
|
|
222
|
+
threadId: input.threadId,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
if (
|
|
227
|
+
rolloutSnapshotBefore.sizeBytes > (input.largeExportThresholdBytes ?? LARGE_BROWSER_EXPORT_THRESHOLD_BYTES)
|
|
228
|
+
) {
|
|
229
|
+
const exportBaseName = fileBaseName;
|
|
230
|
+
const exportDir = await resolvePublicExportDir(input.publicExportDir);
|
|
231
|
+
const workspaceDir = await createExportWorkspace(exportDir, exportBaseName);
|
|
232
|
+
const savedPath = path.join(workspaceDir, `${exportBaseName}.${extension}`);
|
|
233
|
+
const zipPath = buildUniqueArchivePath(exportDir, exportBaseName);
|
|
234
|
+
try {
|
|
235
|
+
const saved = await writeSessionFileExport(
|
|
236
|
+
{
|
|
237
|
+
fallbackReason: null,
|
|
238
|
+
outputRelativePath: fileName,
|
|
239
|
+
relations: browseData.relations,
|
|
240
|
+
sessionFile: browseData.thread.rollout_path,
|
|
241
|
+
thread: browseData.thread,
|
|
242
|
+
},
|
|
243
|
+
toDownloadOptions(input),
|
|
244
|
+
savedPath,
|
|
245
|
+
transform,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (!saved) {
|
|
249
|
+
throw new Error(`Thread ${input.threadId} produced no exportable content`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await zipExportFile(savedPath, zipPath);
|
|
253
|
+
} finally {
|
|
254
|
+
await cleanupExportWorkspace(workspaceDir);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
258
|
+
logRolloutChangeIfDetected(
|
|
259
|
+
input.threadId,
|
|
260
|
+
browseData.thread.rollout_path,
|
|
261
|
+
rolloutSnapshotBefore,
|
|
262
|
+
rolloutSnapshotAfter,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const zipStat = await Bun.file(zipPath).stat();
|
|
266
|
+
logExportEvent('info', 'single_zip_ready', {
|
|
267
|
+
downloadUrl: buildUiExportDownloadUrl(zipPath),
|
|
268
|
+
durationMs: Date.now() - startedAt,
|
|
269
|
+
fileName: `${exportBaseName}.zip`,
|
|
270
|
+
sizeBytes: zipStat.size,
|
|
271
|
+
threadId: input.threadId,
|
|
272
|
+
zipPath,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
downloadUrl: buildUiExportDownloadUrl(zipPath),
|
|
277
|
+
fileName: `${exportBaseName}.zip`,
|
|
278
|
+
mimeType: 'application/zip',
|
|
279
|
+
mode: 'download_url',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const content = await convertSessionFile(
|
|
284
|
+
{
|
|
285
|
+
fallbackReason: null,
|
|
286
|
+
outputRelativePath: fileName,
|
|
287
|
+
relations: browseData.relations,
|
|
288
|
+
sessionFile: browseData.thread.rollout_path,
|
|
289
|
+
thread: browseData.thread,
|
|
290
|
+
},
|
|
291
|
+
toDownloadOptions(input),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (!content) {
|
|
295
|
+
throw new Error(`Thread ${input.threadId} produced no exportable content`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
299
|
+
logRolloutChangeIfDetected(
|
|
300
|
+
input.threadId,
|
|
301
|
+
browseData.thread.rollout_path,
|
|
302
|
+
rolloutSnapshotBefore,
|
|
303
|
+
rolloutSnapshotAfter,
|
|
304
|
+
);
|
|
305
|
+
logExportEvent('info', 'single_inline_ready', {
|
|
306
|
+
durationMs: Date.now() - startedAt,
|
|
307
|
+
fileName,
|
|
308
|
+
sizeBytes: content.length,
|
|
309
|
+
threadId: input.threadId,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
content: transform(content),
|
|
314
|
+
fileName,
|
|
315
|
+
mimeType,
|
|
316
|
+
mode: 'download',
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
logExportEvent('error', 'single_error', {
|
|
320
|
+
error: error instanceof Error ? error.message : String(error),
|
|
321
|
+
fileName,
|
|
322
|
+
threadId: input.threadId,
|
|
323
|
+
});
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const renderCodexThreadsDownload = async (
|
|
329
|
+
input: RenderCodexThreadsDownloadInput,
|
|
330
|
+
): Promise<CodexThreadDownload> => {
|
|
331
|
+
const startedAt = Date.now();
|
|
332
|
+
const threadIds = [...new Set(input.threadIds)];
|
|
333
|
+
if (threadIds.length === 0) {
|
|
334
|
+
throw new Error('No threads selected for export');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const browseEntries = threadIds.map((threadId) => getThreadBrowseData(input.dbPath, threadId));
|
|
338
|
+
const threads = browseEntries.map((entry) => entry.thread);
|
|
339
|
+
const exportDir = await resolvePublicExportDir(input.publicExportDir);
|
|
340
|
+
const exportBaseName = buildBatchExportBaseName(threads);
|
|
341
|
+
const bundleDirectory = await createExportWorkspace(exportDir, exportBaseName);
|
|
342
|
+
const zipPath = buildUniqueArchivePath(exportDir, exportBaseName);
|
|
343
|
+
|
|
344
|
+
logExportEvent('info', 'batch_start', {
|
|
345
|
+
exportBaseName,
|
|
346
|
+
selectedThreadCount: threadIds.length,
|
|
347
|
+
selectedThreadIds: threadIds,
|
|
348
|
+
zipPath,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
for (const entry of browseEntries) {
|
|
353
|
+
const rolloutSnapshotBefore = await getRolloutSnapshot(entry.thread.rollout_path);
|
|
354
|
+
const singleBaseName = buildExportBaseName(entry.thread);
|
|
355
|
+
const extension = input.outputFormat === 'md' ? 'md' : 'txt';
|
|
356
|
+
const relativeFileName = `${singleBaseName}.${extension}`;
|
|
357
|
+
const savedPath = path.join(bundleDirectory, relativeFileName);
|
|
358
|
+
const transform = (text: string) =>
|
|
359
|
+
input.pathDisplaySettings
|
|
360
|
+
? applyPathTransforms(text, {
|
|
361
|
+
...input.pathDisplaySettings,
|
|
362
|
+
projectPath: entry.thread.cwd,
|
|
363
|
+
})
|
|
364
|
+
: text;
|
|
365
|
+
|
|
366
|
+
const saved = await writeSessionFileExport(
|
|
367
|
+
{
|
|
368
|
+
fallbackReason: null,
|
|
369
|
+
outputRelativePath: relativeFileName,
|
|
370
|
+
relations: entry.relations,
|
|
371
|
+
sessionFile: entry.thread.rollout_path,
|
|
372
|
+
thread: entry.thread,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
...toDownloadOptions({
|
|
376
|
+
...input,
|
|
377
|
+
threadId: entry.thread.id,
|
|
378
|
+
}),
|
|
379
|
+
threadIds: [entry.thread.id],
|
|
380
|
+
},
|
|
381
|
+
savedPath,
|
|
382
|
+
transform,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
if (!saved) {
|
|
386
|
+
throw new Error(`Thread ${entry.thread.id} produced no exportable content`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const rolloutSnapshotAfter = await getRolloutSnapshot(entry.thread.rollout_path);
|
|
390
|
+
logRolloutChangeIfDetected(
|
|
391
|
+
entry.thread.id,
|
|
392
|
+
entry.thread.rollout_path,
|
|
393
|
+
rolloutSnapshotBefore,
|
|
394
|
+
rolloutSnapshotAfter,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await zipExportDirectory(bundleDirectory, zipPath);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
logExportEvent('error', 'batch_error', {
|
|
401
|
+
error: error instanceof Error ? error.message : String(error),
|
|
402
|
+
exportBaseName,
|
|
403
|
+
selectedThreadCount: threadIds.length,
|
|
404
|
+
selectedThreadIds: threadIds,
|
|
405
|
+
zipPath,
|
|
406
|
+
});
|
|
407
|
+
throw error;
|
|
408
|
+
} finally {
|
|
409
|
+
await cleanupExportWorkspace(bundleDirectory);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const zipStat = await Bun.file(zipPath).stat();
|
|
413
|
+
logExportEvent('info', 'batch_ready', {
|
|
414
|
+
downloadUrl: buildUiExportDownloadUrl(zipPath),
|
|
415
|
+
durationMs: Date.now() - startedAt,
|
|
416
|
+
fileName: `${exportBaseName}.zip`,
|
|
417
|
+
selectedThreadCount: threadIds.length,
|
|
418
|
+
selectedThreadIds: threadIds,
|
|
419
|
+
sizeBytes: zipStat.size,
|
|
420
|
+
zipPath,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
downloadUrl: buildUiExportDownloadUrl(zipPath),
|
|
425
|
+
fileName: `${exportBaseName}.zip`,
|
|
426
|
+
mimeType: 'application/zip',
|
|
427
|
+
mode: 'download_url',
|
|
428
|
+
};
|
|
429
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { SessionMeta, ThreadRelations, ThreadRow } from './codex-exporter-types';
|
|
2
|
+
import type { JsonValue } from './shared';
|
|
3
|
+
|
|
4
|
+
export type DynamicToolDefinition = {
|
|
5
|
+
deferLoading: boolean;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: JsonValue | null;
|
|
8
|
+
name: string;
|
|
9
|
+
namespace: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type DynamicToolRow = DynamicToolDefinition & {
|
|
13
|
+
position: number;
|
|
14
|
+
threadId: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SessionMetaExtended = SessionMeta & {
|
|
18
|
+
baseInstructions: JsonValue | null;
|
|
19
|
+
dynamicTools: DynamicToolDefinition[];
|
|
20
|
+
git: Record<string, JsonValue> | null;
|
|
21
|
+
modelProvider: string | null;
|
|
22
|
+
threadSource: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type TurnContextRecord = {
|
|
26
|
+
payload: Record<string, JsonValue>;
|
|
27
|
+
timestamp: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type BaseThreadEvent = {
|
|
31
|
+
kind:
|
|
32
|
+
| 'message'
|
|
33
|
+
| 'reasoning'
|
|
34
|
+
| 'task_complete'
|
|
35
|
+
| 'task_started'
|
|
36
|
+
| 'token_count'
|
|
37
|
+
| 'tool_call'
|
|
38
|
+
| 'tool_output'
|
|
39
|
+
| 'web_search';
|
|
40
|
+
raw: Record<string, JsonValue>;
|
|
41
|
+
sequence: number;
|
|
42
|
+
timestamp: string | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type MessageEvent = BaseThreadEvent & {
|
|
46
|
+
kind: 'message';
|
|
47
|
+
isHiddenByDefault: boolean;
|
|
48
|
+
memoryCitation: JsonValue | null;
|
|
49
|
+
model: string | null;
|
|
50
|
+
phase: string | null;
|
|
51
|
+
role: string;
|
|
52
|
+
text: string;
|
|
53
|
+
variant: 'agent_message' | 'message' | 'user_message';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type ToolCallEvent = BaseThreadEvent & {
|
|
57
|
+
argumentsText: string | null;
|
|
58
|
+
argumentsParseFailed: boolean;
|
|
59
|
+
callId: string | null;
|
|
60
|
+
command: string | null;
|
|
61
|
+
kind: 'tool_call';
|
|
62
|
+
name: string;
|
|
63
|
+
workdir: string | null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ToolOutputEvent = BaseThreadEvent & {
|
|
67
|
+
callId: string | null;
|
|
68
|
+
exitCode: number | null;
|
|
69
|
+
kind: 'tool_output';
|
|
70
|
+
outputText: string;
|
|
71
|
+
summary: string;
|
|
72
|
+
wallTime: string | null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ReasoningEvent = BaseThreadEvent & {
|
|
76
|
+
content: JsonValue | null;
|
|
77
|
+
hasEncryptedContent: boolean;
|
|
78
|
+
kind: 'reasoning';
|
|
79
|
+
summary: string[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type TokenCountEvent = BaseThreadEvent & {
|
|
83
|
+
info: JsonValue | null;
|
|
84
|
+
kind: 'token_count';
|
|
85
|
+
rateLimits: JsonValue | null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type TaskStartedEvent = BaseThreadEvent & {
|
|
89
|
+
collaborationModeKind: string | null;
|
|
90
|
+
kind: 'task_started';
|
|
91
|
+
modelContextWindow: number | null;
|
|
92
|
+
startedAt: number | null;
|
|
93
|
+
turnId: string | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type TaskCompleteEvent = BaseThreadEvent & {
|
|
97
|
+
completedAt: number | null;
|
|
98
|
+
durationMs: number | null;
|
|
99
|
+
kind: 'task_complete';
|
|
100
|
+
lastAgentMessage: string | null;
|
|
101
|
+
timeToFirstTokenMs: number | null;
|
|
102
|
+
turnId: string | null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type WebSearchEvent = BaseThreadEvent & {
|
|
106
|
+
action: JsonValue | null;
|
|
107
|
+
callId: string | null;
|
|
108
|
+
kind: 'web_search';
|
|
109
|
+
phase: 'call' | 'end';
|
|
110
|
+
query: string | null;
|
|
111
|
+
status: string | null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type ThreadEvent =
|
|
115
|
+
| MessageEvent
|
|
116
|
+
| ReasoningEvent
|
|
117
|
+
| TaskCompleteEvent
|
|
118
|
+
| TaskStartedEvent
|
|
119
|
+
| TokenCountEvent
|
|
120
|
+
| ToolCallEvent
|
|
121
|
+
| ToolOutputEvent
|
|
122
|
+
| WebSearchEvent;
|
|
123
|
+
|
|
124
|
+
export type ThreadTranscriptStats = {
|
|
125
|
+
assistantMessageCount: number;
|
|
126
|
+
commentaryCount: number;
|
|
127
|
+
execCommandCount: number;
|
|
128
|
+
finalAnswerCount: number;
|
|
129
|
+
messageCount: number;
|
|
130
|
+
toolCallCount: number;
|
|
131
|
+
toolOutputCount: number;
|
|
132
|
+
userMessageCount: number;
|
|
133
|
+
webSearchEventCount: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type ParsedCodexTranscript = {
|
|
137
|
+
events: ThreadEvent[];
|
|
138
|
+
isPartial: boolean;
|
|
139
|
+
rawIncluded: boolean;
|
|
140
|
+
sessionMeta: SessionMetaExtended;
|
|
141
|
+
sourceFileSizeBytes: number | null;
|
|
142
|
+
stats: ThreadTranscriptStats;
|
|
143
|
+
statsArePartial: boolean;
|
|
144
|
+
turnContexts: TurnContextRecord[];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type ProjectSummary = {
|
|
148
|
+
archivedThreadCount: number;
|
|
149
|
+
cwdPaths: string[];
|
|
150
|
+
lastUpdatedAtMs: number | null;
|
|
151
|
+
modelNames: string[];
|
|
152
|
+
name: string;
|
|
153
|
+
threadCount: number;
|
|
154
|
+
totalTokens: number;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ThreadListEntry = {
|
|
158
|
+
project: string;
|
|
159
|
+
rolloutSizeBytes: number | null;
|
|
160
|
+
stats: Pick<ThreadTranscriptStats, 'execCommandCount' | 'toolCallCount' | 'webSearchEventCount'> & {
|
|
161
|
+
deferred: boolean;
|
|
162
|
+
};
|
|
163
|
+
thread: ThreadRow;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type ThreadBrowseData = {
|
|
167
|
+
dynamicTools: DynamicToolRow[];
|
|
168
|
+
project: string;
|
|
169
|
+
relations: ThreadRelations;
|
|
170
|
+
thread: ThreadRow;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type DashboardSummary = {
|
|
174
|
+
activeThreads: number;
|
|
175
|
+
archivedThreads: number;
|
|
176
|
+
recentThreads: ThreadRow[];
|
|
177
|
+
threadsWithRelations: number;
|
|
178
|
+
topProjectsByThreadCount: ProjectSummary[];
|
|
179
|
+
topProjectsByTokens: ProjectSummary[];
|
|
180
|
+
totalProjects: number;
|
|
181
|
+
totalThreads: number;
|
|
182
|
+
totalTokens: number;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export type DeleteThreadsResult = {
|
|
186
|
+
deletedSessionFiles: string[];
|
|
187
|
+
deletedThreadIds: string[];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type DeleteProjectResult = DeleteThreadsResult & {
|
|
191
|
+
projectName: string;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type ToolUsageSummary = {
|
|
195
|
+
count: number;
|
|
196
|
+
name: string;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export type ModelTokenSummary = {
|
|
200
|
+
model: string;
|
|
201
|
+
threadCount: number;
|
|
202
|
+
totalTokens: number;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type DistributionItem = {
|
|
206
|
+
count: number;
|
|
207
|
+
label: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export type CodexAnalyticsSummary = {
|
|
211
|
+
archivedThreads: number;
|
|
212
|
+
averageTokensPerThread: number;
|
|
213
|
+
distinctToolNames: number;
|
|
214
|
+
threadsWithWebSearch: number;
|
|
215
|
+
totalProjects: number;
|
|
216
|
+
totalThreads: number;
|
|
217
|
+
totalTokens: number;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type CodexAnalytics = {
|
|
221
|
+
modelsByTokens: ModelTokenSummary[];
|
|
222
|
+
summary: CodexAnalyticsSummary;
|
|
223
|
+
toolUsage: ToolUsageSummary[];
|
|
224
|
+
};
|
|
@@ -11,6 +11,7 @@ export const parseCodexCliArgs = (argv: string[]): CodexCliOptions => {
|
|
|
11
11
|
let threadIds: string[] = [];
|
|
12
12
|
let outputProvided = false;
|
|
13
13
|
let optimized = false;
|
|
14
|
+
let includeCommentary = true;
|
|
14
15
|
let includeTools = false;
|
|
15
16
|
let outputFormat: ExportFormat = 'md';
|
|
16
17
|
let flat = false;
|
|
@@ -20,6 +21,7 @@ export const parseCodexCliArgs = (argv: string[]): CodexCliOptions => {
|
|
|
20
21
|
cwdFilter,
|
|
21
22
|
dbPath,
|
|
22
23
|
flat,
|
|
24
|
+
includeCommentary,
|
|
23
25
|
includeTools,
|
|
24
26
|
inputDir,
|
|
25
27
|
optimized,
|
|
@@ -34,6 +36,7 @@ export const parseCodexCliArgs = (argv: string[]): CodexCliOptions => {
|
|
|
34
36
|
cwdFilter,
|
|
35
37
|
dbPath,
|
|
36
38
|
flat,
|
|
39
|
+
includeCommentary,
|
|
37
40
|
includeTools,
|
|
38
41
|
inputDir,
|
|
39
42
|
optimized,
|
|
@@ -54,6 +57,7 @@ export const parseCodexCliArgs = (argv: string[]): CodexCliOptions => {
|
|
|
54
57
|
cwdFilter,
|
|
55
58
|
dbPath,
|
|
56
59
|
flat,
|
|
60
|
+
includeCommentary,
|
|
57
61
|
includeTools,
|
|
58
62
|
inputDir,
|
|
59
63
|
optimized,
|
|
@@ -68,6 +72,7 @@ type CodexCliState = {
|
|
|
68
72
|
cwdFilter: string | null;
|
|
69
73
|
dbPath: string;
|
|
70
74
|
flat: boolean;
|
|
75
|
+
includeCommentary: boolean;
|
|
71
76
|
includeTools: boolean;
|
|
72
77
|
inputDir: string;
|
|
73
78
|
optimized: boolean;
|
|
@@ -256,7 +261,7 @@ export const resolveDefaultOutputDir = (cwdFilter: string | null): string => {
|
|
|
256
261
|
};
|
|
257
262
|
|
|
258
263
|
const requireValue = (value: string | undefined, flag: string): string => {
|
|
259
|
-
if (!value || value.startsWith('
|
|
264
|
+
if (!value || (value.startsWith('-') && value !== '-')) {
|
|
260
265
|
throw new CliUsageError(`Missing value for ${flag}`);
|
|
261
266
|
}
|
|
262
267
|
|