spiracha 1.1.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 +3 -0
- package/README.md +14 -0
- package/apps/ui/dist/client/assets/{analytics-BjYaHqXk.js → analytics-CqWZmyV6.js} +1 -1
- package/apps/ui/dist/client/assets/{checkbox-wPoGG3of.js → checkbox-DXM4lkJq.js} +1 -1
- package/apps/ui/dist/client/assets/{data-table-6yDgAdtf.js → data-table-DnPYMPCD.js} +1 -1
- package/apps/ui/dist/client/assets/{delete-confirm-dialog-DJUAk7ha.js → delete-confirm-dialog-CcZaRX33.js} +1 -1
- package/apps/ui/dist/client/assets/{download-BhWd-Pm5.js → download-DOwxk-cG.js} +1 -1
- package/apps/ui/dist/client/assets/{es2015-BlyMI4CF.js → es2015-Bm0kEzx2.js} +1 -1
- package/apps/ui/dist/client/assets/{formatters-BxjZwWSE.js → formatters-C12LmYaa.js} +1 -1
- package/apps/ui/dist/client/assets/{index-T01rPkb4.js → index-DdJ7ahIt.js} +3 -3
- package/apps/ui/dist/client/assets/{input-B3YN8gzg.js → input-CEsI7EpI.js} +1 -1
- package/apps/ui/dist/client/assets/{metric-card-BWW7TWER.js → metric-card-9jwBF7rG.js} +1 -1
- package/apps/ui/dist/client/assets/{page-header-BZ8Gnxgs.js → page-header-Dr_h1CVv.js} +1 -1
- package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
- package/apps/ui/dist/client/assets/{projects._project-EfBhCHPY.js → projects._project-zoM8d2nH.js} +1 -1
- package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
- package/apps/ui/dist/client/assets/{projects.index-DzEZ4pAJ.js → projects.index-DukMuny6.js} +1 -1
- package/apps/ui/dist/client/assets/{routes-CWCCZykE.js → routes-Gr2Wwh83.js} +1 -1
- package/apps/ui/dist/client/assets/{select-DLXGsyZ4.js → select-CFim44gT.js} +1 -1
- package/apps/ui/dist/client/assets/{settings-b0Xthfae.js → settings-DqhyDxo2.js} +1 -1
- package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
- package/apps/ui/dist/client/assets/{threads._threadId-CgtoCqTb.js → threads._threadId-DT75NiBa.js} +1 -1
- package/apps/ui/dist/client/assets/{threads._threadId-DBiDb38K.js → threads._threadId-Df5VXIuZ.js} +3 -3
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
- package/apps/ui/dist/server/assets/{analytics-Br_fZB6a.js → analytics-BMxW_bZL.js} +1 -1
- package/apps/ui/dist/server/assets/{codex-server-Cqh0hb93.js → codex-server-BFZq2Y2O.js} +129 -62
- package/apps/ui/dist/server/assets/{download-CzHmFWGk.js → download-C5rkk_Bo.js} +3 -0
- package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
- package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
- package/apps/ui/dist/server/assets/{path-transforms-DD1e7rhY.js → path-transforms-DL2IwtYd.js} +1 -1
- package/apps/ui/dist/server/assets/{projects._project-DdVSdfPe.js → projects._project-CJ7l0ynC.js} +1 -1
- package/apps/ui/dist/server/assets/{projects._project-Bwf6iJC-.js → projects._project-CcJLp_A8.js} +5 -3
- package/apps/ui/dist/server/assets/{projects.index-DKeVeqUZ.js → projects.index-srtogpuF.js} +2 -1
- package/apps/ui/dist/server/assets/{router-ve2Hrl2Y.js → router-C_w-haH6.js} +6 -6
- package/apps/ui/dist/server/assets/{routes-BJyx5OmO.js → routes-BhbxvJE7.js} +1 -1
- package/apps/ui/dist/server/assets/{routes-pkOwjjYc.js → routes-CPe-ppmC.js} +3 -2
- package/apps/ui/dist/server/assets/{start-BAvbjjfs.js → start-HeKLHD9b.js} +1 -1
- package/apps/ui/dist/server/assets/{threads._threadId-D3PYZIwl.js → threads._threadId-Ba7vv6-K.js} +1 -1
- package/apps/ui/dist/server/assets/{threads._threadId-D3xaWM86.js → threads._threadId-euyNckhj.js} +31 -9
- package/apps/ui/dist/server/server.js +15 -15
- package/package.json +7 -1
- package/src/export-chats.ts +3 -4
- package/src/lib/claude-exporter.ts +1 -1
- package/src/lib/codex-browser-db.ts +146 -59
- package/src/lib/codex-browser-export.ts +13 -2
- package/src/lib/codex-exporter-cli.ts +1 -1
- package/src/lib/codex-exporter-db.ts +19 -20
- package/src/lib/codex-exporter-transcript.ts +38 -25
- package/src/lib/interactive-cli.ts +6 -13
- package/src/lib/model-label.ts +24 -0
- package/src/lib/path-transforms.ts +1 -0
- package/src/lib/shared.ts +2 -24
- package/src/lib/sqlite-retry.ts +15 -1
- package/src/spiracha.ts +3 -4
- package/apps/ui/dist/client/assets/projects._project-B7XcpoLt.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-4vfIwLjw.js +0 -1
- package/apps/ui/dist/client/assets/styles-8Wtc8YJw.css +0 -1
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-BjsXNYgm.js +0 -99
- package/apps/ui/dist/server/assets/formatters-B6o5pTY9.js +0 -72
package/apps/ui/dist/server/assets/{threads._threadId-D3xaWM86.js → threads._threadId-euyNckhj.js}
RENAMED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { t as applyPathTransforms$1 } from "./path-transforms-
|
|
1
|
+
import { t as applyPathTransforms$1 } from "./path-transforms-DL2IwtYd.js";
|
|
2
2
|
import { t as cn } from "./utils-C_uf36nf.js";
|
|
3
3
|
import { t as Button } from "./button-CmTDnzOn.js";
|
|
4
4
|
import { n as useSettings } from "./settings-store-DpEJEQ7M.js";
|
|
5
5
|
import { a as threadSnapshotQueryOptions, c as deleteThreadFn, o as threadTranscriptQueryOptions, u as exportThreadFn } from "./codex-queries-CAF6HYiG.js";
|
|
6
|
-
import { t as Route } from "./threads._threadId-
|
|
6
|
+
import { t as Route } from "./threads._threadId-Ba7vv6-K.js";
|
|
7
7
|
import { t as Checkbox$1 } from "./checkbox-C0hovF41.js";
|
|
8
8
|
import { t as MetricCard } from "./metric-card-ByEeLu0r.js";
|
|
9
9
|
import { t as PageHeader } from "./page-header-CxdZM86z.js";
|
|
10
|
-
import { a as formatModelLabel, i as formatList, n as formatBytes, r as formatDateTime, s as formatTokens, t as formatBooleanLabel } from "./formatters-
|
|
10
|
+
import { a as formatModelLabel, i as formatList, n as formatBytes, r as formatDateTime, s as formatTokens, t as formatBooleanLabel } from "./formatters-FJaGZgJk.js";
|
|
11
11
|
import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-CWqcTXTF.js";
|
|
12
|
-
import { n as downloadUrlFile, r as ExportDialog, t as downloadTextFile } from "./download-
|
|
12
|
+
import { n as downloadUrlFile, r as ExportDialog, t as downloadTextFile } from "./download-C5rkk_Bo.js";
|
|
13
13
|
import { useEffect, useRef, useState } from "react";
|
|
14
14
|
import { Link, useNavigate } from "@tanstack/react-router";
|
|
15
15
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -235,6 +235,7 @@ function TranscriptEventCard({ assistantModel, copied, event, isSelected, showRa
|
|
|
235
235
|
]
|
|
236
236
|
}), event.timestamp ? /* @__PURE__ */ jsx("p", {
|
|
237
237
|
className: "shrink-0 text-[var(--muted-foreground)] text-xs",
|
|
238
|
+
suppressHydrationWarning: true,
|
|
238
239
|
children: formatDateTime(event.timestamp)
|
|
239
240
|
}) : null]
|
|
240
241
|
}),
|
|
@@ -294,6 +295,12 @@ function TranscriptView({ assistantModel, events, projectPath, showCommentary, s
|
|
|
294
295
|
timeoutIdsRef.current = [];
|
|
295
296
|
};
|
|
296
297
|
}, []);
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
setSelectedEventKeys((current) => {
|
|
300
|
+
const next = current.filter((key) => visibleEventKeySet.has(key));
|
|
301
|
+
return next.length === current.length ? current : next;
|
|
302
|
+
});
|
|
303
|
+
}, [visibleEventKeySet]);
|
|
297
304
|
const scheduleTimeout = (callback, delayMs) => {
|
|
298
305
|
const timeoutId = window.setTimeout(() => {
|
|
299
306
|
timeoutIdsRef.current = timeoutIdsRef.current.filter((entry) => entry !== timeoutId);
|
|
@@ -497,15 +504,24 @@ var buildThreadItems = (snapshot) => {
|
|
|
497
504
|
},
|
|
498
505
|
{
|
|
499
506
|
label: "Created",
|
|
500
|
-
value:
|
|
507
|
+
value: /* @__PURE__ */ jsx("span", {
|
|
508
|
+
suppressHydrationWarning: true,
|
|
509
|
+
children: formatDateTime(snapshot.thread.created_at_ms ?? snapshot.thread.created_at * 1e3)
|
|
510
|
+
})
|
|
501
511
|
},
|
|
502
512
|
{
|
|
503
513
|
label: "Updated",
|
|
504
|
-
value:
|
|
514
|
+
value: /* @__PURE__ */ jsx("span", {
|
|
515
|
+
suppressHydrationWarning: true,
|
|
516
|
+
children: formatDateTime(snapshot.thread.updated_at_ms ?? snapshot.thread.updated_at * 1e3)
|
|
517
|
+
})
|
|
505
518
|
},
|
|
506
519
|
{
|
|
507
520
|
label: "Session started",
|
|
508
|
-
value:
|
|
521
|
+
value: /* @__PURE__ */ jsx("span", {
|
|
522
|
+
suppressHydrationWarning: true,
|
|
523
|
+
children: formatDateTime(snapshot.transcript?.sessionMeta.timestamp ?? null)
|
|
524
|
+
})
|
|
509
525
|
},
|
|
510
526
|
{
|
|
511
527
|
label: "Rollout size",
|
|
@@ -517,7 +533,10 @@ var buildThreadItems = (snapshot) => {
|
|
|
517
533
|
},
|
|
518
534
|
{
|
|
519
535
|
label: "Archived at",
|
|
520
|
-
value:
|
|
536
|
+
value: /* @__PURE__ */ jsx("span", {
|
|
537
|
+
suppressHydrationWarning: true,
|
|
538
|
+
children: formatDateTime(snapshot.thread.archived_at ? snapshot.thread.archived_at * 1e3 : null)
|
|
539
|
+
})
|
|
521
540
|
}
|
|
522
541
|
];
|
|
523
542
|
};
|
|
@@ -934,7 +953,10 @@ function ThreadDetailPage() {
|
|
|
934
953
|
}),
|
|
935
954
|
/* @__PURE__ */ jsx(MetricCard, {
|
|
936
955
|
label: "Updated",
|
|
937
|
-
value:
|
|
956
|
+
value: /* @__PURE__ */ jsx("span", {
|
|
957
|
+
suppressHydrationWarning: true,
|
|
958
|
+
children: formatDateTime(snapshot.thread.updated_at_ms ?? snapshot.thread.updated_at * 1e3)
|
|
959
|
+
})
|
|
938
960
|
}),
|
|
939
961
|
/* @__PURE__ */ jsx(MetricCard, {
|
|
940
962
|
label: "Thread source",
|
|
@@ -3471,7 +3471,7 @@ var defaultSerovalPlugins = [
|
|
|
3471
3471
|
* the dev styles URL for route-scoped CSS collection.
|
|
3472
3472
|
*/
|
|
3473
3473
|
async function getStartManifest(matchedRoutes) {
|
|
3474
|
-
const { tsrStartManifest } = await import("./assets/_tanstack-start-manifest_v-
|
|
3474
|
+
const { tsrStartManifest } = await import("./assets/_tanstack-start-manifest_v-C0V305Nt.js");
|
|
3475
3475
|
const startManifest = tsrStartManifest();
|
|
3476
3476
|
let routes = startManifest.routes;
|
|
3477
3477
|
routes[rootRouteId];
|
|
@@ -3498,47 +3498,47 @@ async function getStartManifest(matchedRoutes) {
|
|
|
3498
3498
|
var manifest = {
|
|
3499
3499
|
"0814663c3bdc58135f97d210d145ef0be5ca54ef9a5f1e3030be9b1bfc901e30": {
|
|
3500
3500
|
functionName: "exportThreadFn_createServerFn_handler",
|
|
3501
|
-
importer: () => import("./assets/codex-server-
|
|
3501
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3502
3502
|
},
|
|
3503
3503
|
"164ee82cdd565ed96591a64312f0f7bd961040baf066a89d9f5636330d11360b": {
|
|
3504
3504
|
functionName: "deleteProjectFn_createServerFn_handler",
|
|
3505
|
-
importer: () => import("./assets/codex-server-
|
|
3505
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3506
3506
|
},
|
|
3507
3507
|
"29727b7ad5b8fe42e83817376653e064d9fe8888799f056b2e59296b3396568b": {
|
|
3508
3508
|
functionName: "deleteThreadFn_createServerFn_handler",
|
|
3509
|
-
importer: () => import("./assets/codex-server-
|
|
3509
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3510
3510
|
},
|
|
3511
3511
|
"4712520da0f07bbd1f0907e5a162fe518516ff4caca3fd23876cc65539d87d7a": {
|
|
3512
3512
|
functionName: "getAnalyticsFn_createServerFn_handler",
|
|
3513
|
-
importer: () => import("./assets/codex-server-
|
|
3513
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3514
3514
|
},
|
|
3515
3515
|
"59fb2cb4d60c8e7d47e0afcc914ee6f9d9f4bf076c8e66eab1693066753655b3": {
|
|
3516
3516
|
functionName: "listProjectThreadsFn_createServerFn_handler",
|
|
3517
|
-
importer: () => import("./assets/codex-server-
|
|
3517
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3518
3518
|
},
|
|
3519
3519
|
"5da27045f7e28ded6353bc16aace284af7ef1b4010ef04d0186a6feadb466497": {
|
|
3520
3520
|
functionName: "getThreadTranscriptFn_createServerFn_handler",
|
|
3521
|
-
importer: () => import("./assets/codex-server-
|
|
3521
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3522
3522
|
},
|
|
3523
3523
|
"72991e2b6e0adbf8d63bb8b139dad88a00b77b7030ec28ceac36c3cce7846b4c": {
|
|
3524
3524
|
functionName: "getThreadSnapshotFn_createServerFn_handler",
|
|
3525
|
-
importer: () => import("./assets/codex-server-
|
|
3525
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3526
3526
|
},
|
|
3527
3527
|
"792690638a3b10035a5b7368c3d98bdc70cbfe1e36a4aa5f45b1c49b8b1025b0": {
|
|
3528
3528
|
functionName: "getDashboardSummaryFn_createServerFn_handler",
|
|
3529
|
-
importer: () => import("./assets/codex-server-
|
|
3529
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3530
3530
|
},
|
|
3531
3531
|
"96aa60bf7dd9b5bde415bcf3ad6f6955a975eecd9aa0516cf401cc39bebebe6c": {
|
|
3532
3532
|
functionName: "deleteThreadsFn_createServerFn_handler",
|
|
3533
|
-
importer: () => import("./assets/codex-server-
|
|
3533
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3534
3534
|
},
|
|
3535
3535
|
"b4e15c006e9a277470958bb008f89b5b0acc7256109581de44cf17d587d174a5": {
|
|
3536
3536
|
functionName: "exportThreadsFn_createServerFn_handler",
|
|
3537
|
-
importer: () => import("./assets/codex-server-
|
|
3537
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3538
3538
|
},
|
|
3539
3539
|
"ccefccb816ba13508f23db4e31067b3403e750225257592d3ae11071ffc3fd6f": {
|
|
3540
3540
|
functionName: "listProjectsFn_createServerFn_handler",
|
|
3541
|
-
importer: () => import("./assets/codex-server-
|
|
3541
|
+
importer: () => import("./assets/codex-server-BFZq2Y2O.js")
|
|
3542
3542
|
}
|
|
3543
3543
|
};
|
|
3544
3544
|
async function getServerFnById(id, access) {
|
|
@@ -5328,8 +5328,8 @@ var getBaseManifest = getProdBaseManifest;
|
|
|
5328
5328
|
var createEarlyHintsForRequest = createEarlyHintsCollector;
|
|
5329
5329
|
async function loadEntries() {
|
|
5330
5330
|
const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
|
|
5331
|
-
import("./assets/router-
|
|
5332
|
-
import("./assets/start-
|
|
5331
|
+
import("./assets/router-C_w-haH6.js"),
|
|
5332
|
+
import("./assets/start-HeKLHD9b.js"),
|
|
5333
5333
|
import("./assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js")
|
|
5334
5334
|
]);
|
|
5335
5335
|
return {
|
|
@@ -5666,7 +5666,7 @@ async function handleServerRoutes({ getRouter, request, url, executeRouter, cont
|
|
|
5666
5666
|
return ctx.response;
|
|
5667
5667
|
}
|
|
5668
5668
|
//#endregion
|
|
5669
|
-
//#region ../../node_modules/.bun/@tanstack+react-start@1.168.11+
|
|
5669
|
+
//#region ../../node_modules/.bun/@tanstack+react-start@1.168.11+5cefe02f681c7619/node_modules/@tanstack/react-start/dist/plugin/default-entry/server.ts
|
|
5670
5670
|
var fetch$1 = createStartHandler(defaultStreamHandler);
|
|
5671
5671
|
function createServerEntry(entry) {
|
|
5672
5672
|
return { async fetch(...args) {
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@tanstack/react-virtual": "3.13.25",
|
|
22
22
|
"class-variance-authority": "0.7.1",
|
|
23
23
|
"clsx": "2.1.1",
|
|
24
|
+
"iconv-lite": "^0.7.2",
|
|
24
25
|
"lucide-react": "1.16.0",
|
|
25
26
|
"radix-ui": "1.4.3",
|
|
26
27
|
"react": "19.2.6",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"src/lib/ui-export-files.ts",
|
|
62
63
|
"src/lib/ui-cache.ts",
|
|
63
64
|
"src/lib/interactive-cli.ts",
|
|
65
|
+
"src/lib/model-label.ts",
|
|
64
66
|
"src/lib/native-open.ts",
|
|
65
67
|
"src/lib/shared.ts",
|
|
66
68
|
"apps/ui/dist/client/assets/**/*",
|
|
@@ -91,10 +93,14 @@
|
|
|
91
93
|
},
|
|
92
94
|
"scripts": {
|
|
93
95
|
"build": "bun run typecheck && bun run --cwd apps/ui build",
|
|
96
|
+
"coverage": "bun run coverage:root && bun run coverage:ui",
|
|
97
|
+
"coverage:root": "bun test --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run ./src/coverage-check.ts root",
|
|
98
|
+
"coverage:ui": "bun run --cwd apps/ui coverage && bun run ./src/coverage-check.ts ui",
|
|
94
99
|
"export:claude": "bun run ./src/export-claude.ts",
|
|
95
100
|
"format": "biome check . --write && biome lint . --write",
|
|
96
101
|
"lint": "biome check .",
|
|
97
102
|
"mcp": "bun run ./src/mcp-server.ts",
|
|
103
|
+
"smoke:package-ui": "bun run ./src/package-ui-smoke.ts",
|
|
98
104
|
"start": "bun run ./src/export-chats.ts",
|
|
99
105
|
"typecheck": "bun run typecheck:root && bun run typecheck:ui",
|
|
100
106
|
"typecheck:root": "bunx tsc --noEmit",
|
|
@@ -103,7 +109,7 @@
|
|
|
103
109
|
"ui:preview": "bun run --cwd apps/ui preview"
|
|
104
110
|
},
|
|
105
111
|
"type": "module",
|
|
106
|
-
"version": "1.1.
|
|
112
|
+
"version": "1.1.1",
|
|
107
113
|
"workspaces": [
|
|
108
114
|
"apps/*"
|
|
109
115
|
]
|
package/src/export-chats.ts
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
6
|
import { createInterface } from 'node:readline/promises';
|
|
7
7
|
import { getCodexHelpText, parseCodexCliArgs, runCodexExport } from './lib/codex-exporter';
|
|
8
|
-
import {
|
|
8
|
+
import type { InteractiveExportResult } from './lib/interactive-cli';
|
|
9
9
|
import { openPathNatively } from './lib/native-open';
|
|
10
10
|
import { CliUsageError } from './lib/shared';
|
|
11
11
|
|
|
@@ -44,6 +44,7 @@ const shouldRunInteractive = (argv: string[]): boolean => {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const runInteractiveCliFlow = async (): Promise<void> => {
|
|
47
|
+
const { runInteractiveExport } = await import('./lib/interactive-cli');
|
|
47
48
|
const result = await runInteractiveExport();
|
|
48
49
|
const targetFolder = await printInteractiveExportResult(result);
|
|
49
50
|
await maybeOpenExportFolder(targetFolder);
|
|
@@ -55,9 +56,7 @@ const runCodexCliFlow = async (argv: string[]): Promise<void> => {
|
|
|
55
56
|
printCodexExportResult(result);
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const printInteractiveExportResult = async (
|
|
59
|
-
result: Awaited<ReturnType<typeof runInteractiveExport>>,
|
|
60
|
-
): Promise<string> => {
|
|
59
|
+
const printInteractiveExportResult = async (result: InteractiveExportResult): Promise<string> => {
|
|
61
60
|
if (result.mode === 'claude') {
|
|
62
61
|
console.log(`Exported ${result.sourcePath} -> ${result.outputPath}`);
|
|
63
62
|
return resolveExportFolder(result.outputPath);
|
|
@@ -848,7 +848,7 @@ const fileExists = async (targetPath: string): Promise<boolean> => {
|
|
|
848
848
|
};
|
|
849
849
|
|
|
850
850
|
const requireValue = (value: string | undefined, flag: string): string => {
|
|
851
|
-
if (!value || value.startsWith('
|
|
851
|
+
if (!value || (value.startsWith('-') && value !== '-')) {
|
|
852
852
|
throw new CliUsageError(`Missing value for ${flag}`);
|
|
853
853
|
}
|
|
854
854
|
|
|
@@ -26,6 +26,75 @@ type DeleteProjectOptions = {
|
|
|
26
26
|
deleteSessionFiles?: boolean;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const SQLITE_DELETE_BATCH_SIZE = 400;
|
|
30
|
+
const SESSION_FILE_DELETE_CONCURRENCY = 16;
|
|
31
|
+
const THREAD_LIST_IO_CONCURRENCY = 8;
|
|
32
|
+
|
|
33
|
+
const chunkValues = <T>(values: T[], chunkSize: number) => {
|
|
34
|
+
const chunks: T[][] = [];
|
|
35
|
+
|
|
36
|
+
for (let index = 0; index < values.length; index += chunkSize) {
|
|
37
|
+
chunks.push(values.slice(index, index + chunkSize));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return chunks;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isPromiseLike = (value: unknown): value is PromiseLike<unknown> => {
|
|
44
|
+
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return 'then' in value && typeof value.then === 'function';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const mapWithConcurrency = async <T, TResult>(
|
|
52
|
+
values: T[],
|
|
53
|
+
limit: number,
|
|
54
|
+
mapper: (value: T, index: number) => Promise<TResult>,
|
|
55
|
+
) => {
|
|
56
|
+
const results = new Array<TResult>(values.length);
|
|
57
|
+
let nextIndex = 0;
|
|
58
|
+
|
|
59
|
+
const worker = async () => {
|
|
60
|
+
while (true) {
|
|
61
|
+
const currentIndex = nextIndex;
|
|
62
|
+
nextIndex += 1;
|
|
63
|
+
|
|
64
|
+
if (currentIndex >= values.length) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
results[currentIndex] = await mapper(values[currentIndex]!, currentIndex);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, () => worker()));
|
|
73
|
+
return results;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const openReadonlyDb = (dbPath: string, busyTimeoutMs: number) => {
|
|
77
|
+
const db = new Database(dbPath, { readonly: true });
|
|
78
|
+
try {
|
|
79
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
80
|
+
return db;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
db.close();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const openWritableDb = (dbPath: string, busyTimeoutMs: number) => {
|
|
88
|
+
const db = new Database(dbPath);
|
|
89
|
+
try {
|
|
90
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
91
|
+
return db;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
db.close();
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
29
98
|
const toTimestampMs = (thread: ThreadRow) => {
|
|
30
99
|
return thread.updated_at_ms ?? thread.updated_at * 1000;
|
|
31
100
|
};
|
|
@@ -54,13 +123,17 @@ const parseJsonSafely = (value: string | null) => {
|
|
|
54
123
|
}
|
|
55
124
|
};
|
|
56
125
|
|
|
57
|
-
const withReadonlyDb = <T>(dbPath: string, callback: (db: Database) => T): T => {
|
|
126
|
+
export const withReadonlyDb = <T>(dbPath: string, callback: (db: Database) => T): T => {
|
|
58
127
|
return runWithSqliteRetry({
|
|
59
128
|
action: () => {
|
|
60
|
-
const db =
|
|
61
|
-
db.exec('PRAGMA busy_timeout = 5000');
|
|
129
|
+
const db = openReadonlyDb(dbPath, 5000);
|
|
62
130
|
try {
|
|
63
|
-
|
|
131
|
+
const result = callback(db);
|
|
132
|
+
if (isPromiseLike(result)) {
|
|
133
|
+
throw new Error('Database callbacks must be synchronous');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
64
137
|
} finally {
|
|
65
138
|
db.close();
|
|
66
139
|
}
|
|
@@ -71,13 +144,16 @@ const withReadonlyDb = <T>(dbPath: string, callback: (db: Database) => T): T =>
|
|
|
71
144
|
const withWritableDb = <T>(dbPath: string, callback: (db: Database) => T): T => {
|
|
72
145
|
const db = runWithSqliteRetry({
|
|
73
146
|
action: () => {
|
|
74
|
-
|
|
75
|
-
connection.exec('PRAGMA busy_timeout = 5000');
|
|
76
|
-
return connection;
|
|
147
|
+
return openWritableDb(dbPath, 5000);
|
|
77
148
|
},
|
|
78
149
|
});
|
|
79
150
|
try {
|
|
80
|
-
|
|
151
|
+
const result = callback(db);
|
|
152
|
+
if (isPromiseLike(result)) {
|
|
153
|
+
throw new Error('Database callbacks must be synchronous');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
81
157
|
} finally {
|
|
82
158
|
db.close();
|
|
83
159
|
}
|
|
@@ -99,9 +175,7 @@ export const resolveCodexThreadDbPath = () => {
|
|
|
99
175
|
try {
|
|
100
176
|
const db = runWithSqliteRetry({
|
|
101
177
|
action: () => {
|
|
102
|
-
|
|
103
|
-
connection.exec('PRAGMA busy_timeout = 1500');
|
|
104
|
-
return connection;
|
|
178
|
+
return openReadonlyDb(candidate, 1500);
|
|
105
179
|
},
|
|
106
180
|
});
|
|
107
181
|
db.close();
|
|
@@ -236,11 +310,21 @@ const getThreadDeleteTargets = (db: Database, threadIds: string[]) => {
|
|
|
236
310
|
return [];
|
|
237
311
|
}
|
|
238
312
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
313
|
+
const targets: Array<{ id: string; rollout_path: string }> = [];
|
|
314
|
+
|
|
315
|
+
for (const threadIdChunk of chunkValues(threadIds, SQLITE_DELETE_BATCH_SIZE)) {
|
|
316
|
+
const placeholders = threadIdChunk.map(() => '?').join(', ');
|
|
317
|
+
targets.push(
|
|
318
|
+
...(db
|
|
319
|
+
.query(`SELECT id, rollout_path FROM threads WHERE id IN (${placeholders})`)
|
|
320
|
+
.all(...threadIdChunk) as Array<{
|
|
321
|
+
id: string;
|
|
322
|
+
rollout_path: string;
|
|
323
|
+
}>),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return targets;
|
|
244
328
|
};
|
|
245
329
|
|
|
246
330
|
const deleteThreadIds = (db: Database, threadIds: string[]): DeleteThreadsResult => {
|
|
@@ -263,28 +347,30 @@ const deleteThreadIds = (db: Database, threadIds: string[]): DeleteThreadsResult
|
|
|
263
347
|
}
|
|
264
348
|
|
|
265
349
|
const deleteMany = db.transaction((ids: string[]) => {
|
|
266
|
-
const
|
|
350
|
+
for (const threadIdChunk of chunkValues(ids, SQLITE_DELETE_BATCH_SIZE)) {
|
|
351
|
+
const placeholders = threadIdChunk.map(() => '?').join(', ');
|
|
267
352
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
353
|
+
// Codex schema differs across versions, so only touch dependent tables that actually exist.
|
|
354
|
+
if (existingTableNames.has('thread_dynamic_tools')) {
|
|
355
|
+
db.query(`DELETE FROM thread_dynamic_tools WHERE thread_id IN (${placeholders})`).run(...threadIdChunk);
|
|
356
|
+
}
|
|
272
357
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
358
|
+
if (existingTableNames.has('thread_goals')) {
|
|
359
|
+
db.query(`DELETE FROM thread_goals WHERE thread_id IN (${placeholders})`).run(...threadIdChunk);
|
|
360
|
+
}
|
|
276
361
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
362
|
+
if (existingTableNames.has('stage1_outputs')) {
|
|
363
|
+
db.query(`DELETE FROM stage1_outputs WHERE thread_id IN (${placeholders})`).run(...threadIdChunk);
|
|
364
|
+
}
|
|
280
365
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
366
|
+
if (existingTableNames.has('thread_spawn_edges')) {
|
|
367
|
+
db.query(
|
|
368
|
+
`DELETE FROM thread_spawn_edges WHERE parent_thread_id IN (${placeholders}) OR child_thread_id IN (${placeholders})`,
|
|
369
|
+
).run(...threadIdChunk, ...threadIdChunk);
|
|
370
|
+
}
|
|
286
371
|
|
|
287
|
-
|
|
372
|
+
db.query(`DELETE FROM threads WHERE id IN (${placeholders})`).run(...threadIdChunk);
|
|
373
|
+
}
|
|
288
374
|
});
|
|
289
375
|
|
|
290
376
|
deleteMany(existingIds);
|
|
@@ -297,7 +383,10 @@ const deleteThreadIds = (db: Database, threadIds: string[]): DeleteThreadsResult
|
|
|
297
383
|
|
|
298
384
|
const deleteThreadSessionFiles = async (sessionFiles: string[]) => {
|
|
299
385
|
const uniqueSessionFiles = [...new Set(sessionFiles)];
|
|
300
|
-
await
|
|
386
|
+
await mapWithConcurrency(uniqueSessionFiles, SESSION_FILE_DELETE_CONCURRENCY, async (sessionFile) => {
|
|
387
|
+
await rm(sessionFile, { force: true });
|
|
388
|
+
return sessionFile;
|
|
389
|
+
});
|
|
301
390
|
return uniqueSessionFiles;
|
|
302
391
|
};
|
|
303
392
|
|
|
@@ -323,39 +412,37 @@ export const listProjectThreads = async (
|
|
|
323
412
|
options: ListProjectThreadsOptions = {},
|
|
324
413
|
): Promise<ThreadListEntry[]> => {
|
|
325
414
|
const threads = filterThreadsByProject(readAllThreads(dbPath), projectName);
|
|
326
|
-
const entries = await
|
|
327
|
-
|
|
328
|
-
const rollout = await getThreadRolloutLoadState(thread.rollout_path, options.largeTranscriptThresholdBytes);
|
|
329
|
-
|
|
330
|
-
if (rollout.shouldDeferTranscriptLoad) {
|
|
331
|
-
return {
|
|
332
|
-
project: projectName,
|
|
333
|
-
rolloutSizeBytes: rollout.fileSizeBytes,
|
|
334
|
-
stats: {
|
|
335
|
-
deferred: true,
|
|
336
|
-
execCommandCount: 0,
|
|
337
|
-
toolCallCount: 0,
|
|
338
|
-
webSearchEventCount: 0,
|
|
339
|
-
},
|
|
340
|
-
thread: compactThreadListRow(thread),
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const transcript = await getCachedParsedCodexTranscript(thread.rollout_path);
|
|
415
|
+
const entries = await mapWithConcurrency(threads, THREAD_LIST_IO_CONCURRENCY, async (thread) => {
|
|
416
|
+
const rollout = await getThreadRolloutLoadState(thread.rollout_path, options.largeTranscriptThresholdBytes);
|
|
345
417
|
|
|
418
|
+
if (rollout.shouldDeferTranscriptLoad) {
|
|
346
419
|
return {
|
|
347
420
|
project: projectName,
|
|
348
421
|
rolloutSizeBytes: rollout.fileSizeBytes,
|
|
349
422
|
stats: {
|
|
350
|
-
deferred:
|
|
351
|
-
execCommandCount:
|
|
352
|
-
toolCallCount:
|
|
353
|
-
webSearchEventCount:
|
|
423
|
+
deferred: true,
|
|
424
|
+
execCommandCount: 0,
|
|
425
|
+
toolCallCount: 0,
|
|
426
|
+
webSearchEventCount: 0,
|
|
354
427
|
},
|
|
355
428
|
thread: compactThreadListRow(thread),
|
|
356
429
|
};
|
|
357
|
-
}
|
|
358
|
-
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const transcript = await getCachedParsedCodexTranscript(thread.rollout_path);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
project: projectName,
|
|
436
|
+
rolloutSizeBytes: rollout.fileSizeBytes,
|
|
437
|
+
stats: {
|
|
438
|
+
deferred: false,
|
|
439
|
+
execCommandCount: transcript.stats.execCommandCount,
|
|
440
|
+
toolCallCount: transcript.stats.toolCallCount,
|
|
441
|
+
webSearchEventCount: transcript.stats.webSearchEventCount,
|
|
442
|
+
},
|
|
443
|
+
thread: compactThreadListRow(thread),
|
|
444
|
+
};
|
|
445
|
+
});
|
|
359
446
|
|
|
360
447
|
return entries.sort((left, right) => toTimestampMs(right.thread) - toTimestampMs(left.thread));
|
|
361
448
|
};
|
|
@@ -153,6 +153,17 @@ const logRolloutChangeIfDetected = (
|
|
|
153
153
|
});
|
|
154
154
|
};
|
|
155
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
|
+
|
|
156
167
|
const zipExportFile = async (sourcePath: string, zipPath: string) => {
|
|
157
168
|
const proc = Bun.spawn(['zip', '-9', '-j', zipPath, sourcePath], {
|
|
158
169
|
stderr: 'pipe',
|
|
@@ -240,7 +251,7 @@ export const renderCodexThreadDownload = async (
|
|
|
240
251
|
|
|
241
252
|
await zipExportFile(savedPath, zipPath);
|
|
242
253
|
} finally {
|
|
243
|
-
await
|
|
254
|
+
await cleanupExportWorkspace(workspaceDir);
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
@@ -395,7 +406,7 @@ export const renderCodexThreadsDownload = async (
|
|
|
395
406
|
});
|
|
396
407
|
throw error;
|
|
397
408
|
} finally {
|
|
398
|
-
await
|
|
409
|
+
await cleanupExportWorkspace(bundleDirectory);
|
|
399
410
|
}
|
|
400
411
|
|
|
401
412
|
const zipStat = await Bun.file(zipPath).stat();
|
|
@@ -261,7 +261,7 @@ export const resolveDefaultOutputDir = (cwdFilter: string | null): string => {
|
|
|
261
261
|
};
|
|
262
262
|
|
|
263
263
|
const requireValue = (value: string | undefined, flag: string): string => {
|
|
264
|
-
if (!value || value.startsWith('
|
|
264
|
+
if (!value || (value.startsWith('-') && value !== '-')) {
|
|
265
265
|
throw new CliUsageError(`Missing value for ${flag}`);
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite';
|
|
2
1
|
import { readdir } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { withReadonlyDb } from './codex-browser-db';
|
|
4
4
|
import {
|
|
5
5
|
type CodexCliOptions,
|
|
6
6
|
DEFAULT_CODEX_DIR,
|
|
@@ -17,33 +17,29 @@ export const loadThreadData = (dbPath: string, options: CodexCliOptions): Thread
|
|
|
17
17
|
const parentByChildId = new Map<string, SpawnEdgeRow>();
|
|
18
18
|
const childEdgesByParentId = new Map<string, SpawnEdgeRow[]>();
|
|
19
19
|
|
|
20
|
-
let db: Database | null = null;
|
|
21
|
-
|
|
22
20
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const threadRows = db.query(threadQuery.sql).all(...threadQuery.params) as ThreadRow[];
|
|
21
|
+
withReadonlyDb(dbPath, (db) => {
|
|
22
|
+
const threadQuery = buildThreadQuery(options);
|
|
23
|
+
const threadRows = db.query(threadQuery.sql).all(...threadQuery.params) as ThreadRow[];
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
for (const row of threadRows) {
|
|
26
|
+
threadsById.set(row.id, row);
|
|
27
|
+
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const edgeQuery = buildSpawnEdgeQuery([...threadsById.keys()], options);
|
|
30
|
+
const edgeRows = db.query(edgeQuery.sql).all(...edgeQuery.params) as SpawnEdgeRow[];
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
for (const row of edgeRows) {
|
|
33
|
+
parentByChildId.set(row.child_thread_id, row);
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const existing = childEdgesByParentId.get(row.parent_thread_id) ?? [];
|
|
36
|
+
existing.push(row);
|
|
37
|
+
childEdgesByParentId.set(row.parent_thread_id, existing);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
42
40
|
} catch (error) {
|
|
43
41
|
const message = error instanceof Error ? error.message : String(error);
|
|
44
42
|
throw new Error(`Failed to read thread database at ${dbPath}: ${message}`);
|
|
45
|
-
} finally {
|
|
46
|
-
db?.close();
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
return {
|
|
@@ -210,14 +206,17 @@ export const toOutputRelativePath = (
|
|
|
210
206
|
return flatName;
|
|
211
207
|
}
|
|
212
208
|
|
|
209
|
+
// Prefer preserving the input sessions tree when the rollout lives under the configured input root.
|
|
213
210
|
if (normalized.startsWith(`${inputRoot}${path.sep}`)) {
|
|
214
211
|
return path.relative(inputRoot, normalized).replace(/\.jsonl$/i, extension);
|
|
215
212
|
}
|
|
216
213
|
|
|
214
|
+
// Fall back to a stable Codex-relative path when the file is under ~/.codex.
|
|
217
215
|
if (normalized.startsWith(`${codexRoot}${path.sep}`)) {
|
|
218
216
|
return path.relative(codexRoot, normalized).replace(/\.jsonl$/i, extension);
|
|
219
217
|
}
|
|
220
218
|
|
|
219
|
+
// Otherwise collapse to the basename so ad hoc session files cannot escape the output directory.
|
|
221
220
|
return path.basename(normalized).replace(/\.jsonl$/i, extension);
|
|
222
221
|
};
|
|
223
222
|
|