spiracha 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -1
- package/README.md +6 -0
- package/apps/ui/AGENTS.md +8 -0
- package/apps/ui/README.md +10 -3
- package/apps/ui/dist/client/assets/analytics-DK2jdvmI.js +1 -0
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DmD0sN8X.js +7 -0
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +1 -0
- package/apps/ui/dist/client/assets/antigravity-keychain-panel-BRHVjFAM.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CGV7amas.js +1 -0
- package/apps/ui/dist/client/assets/antigravity.index-DEEkjlZa.js +1 -0
- package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +1 -0
- package/apps/ui/dist/client/assets/badge-DfWvPfKF.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +1 -0
- package/apps/ui/dist/client/assets/createServerFn-CgRRVpBH.js +3 -0
- package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +1 -0
- package/apps/ui/dist/client/assets/cursor-threads._composerId-Ti2gTm63.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-DzD_c6j9.js +1 -0
- package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +1 -0
- package/apps/ui/dist/client/assets/cursor.index-PkCFVcl4.js +2 -0
- package/apps/ui/dist/client/assets/{data-table-Cj-v-uyB.js → data-table-Br78_dk3.js} +2 -2
- package/apps/ui/dist/client/assets/delete-confirm-dialog-BHRmyLHr.js +11 -0
- package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +1 -0
- package/apps/ui/dist/client/assets/dist-BhZOxAPP.js +1 -0
- package/apps/ui/dist/client/assets/dist-DvMS2965.js +1 -0
- package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +1 -0
- package/apps/ui/dist/client/assets/dropdown-menu-Dy5cLzTy.js +1 -0
- package/apps/ui/dist/client/assets/es2015-BkCWttyM.js +41 -0
- package/apps/ui/dist/client/assets/export-dialog-C86SYHJn.js +1 -0
- package/apps/ui/dist/client/assets/formatters-BM9kB7ed.js +1 -0
- package/apps/ui/dist/client/assets/index-CPvAP-jk.js +149 -0
- package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +1 -0
- package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +1 -0
- package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-Ckf6muWZ.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-V5ibNZII.js +3 -0
- package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +1 -0
- package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +1 -0
- package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +1 -0
- package/apps/ui/dist/client/assets/routes-Df2tgpyd.js +1 -0
- package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +1 -0
- package/apps/ui/dist/client/assets/select-Bf2GwfPR.js +1 -0
- package/apps/ui/dist/client/assets/settings-5VQIzME0.js +1 -0
- package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +1 -0
- package/apps/ui/dist/client/assets/styles-BhRkXgwB.css +1 -0
- package/apps/ui/dist/client/assets/tabs-OO4VU5KR.js +7 -0
- package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +2 -0
- package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-BgDhFj2I.js +1 -0
- package/apps/ui/dist/client/assets/useMutation-CC_B7uy5.js +1 -0
- package/apps/ui/dist/client/assets/useQuery-C5By6WKU.js +1 -0
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CRW2kqj0.js +232 -0
- package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +16 -0
- package/apps/ui/dist/server/assets/{analytics-CBNOYZwJ.js → analytics-CtpOMVMO.js} +13 -6
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BtexWY-K.js +613 -0
- package/apps/ui/dist/server/assets/{antigravity-conversations._conversationId-BIdYNy68.js → antigravity-conversations._conversationId-zMrR6v6R.js} +2 -2
- package/apps/ui/dist/server/assets/{antigravity-db-D9gW1D8G.js → antigravity-db-Bh8_U9uw.js} +11 -7
- package/apps/ui/dist/server/assets/{antigravity-keychain-panel-DcLyBBwd.js → antigravity-keychain-panel-CC7FLmAK.js} +1 -1
- package/apps/ui/dist/server/assets/{antigravity-queries-CgQhlQ7J.js → antigravity-queries-CGrJO9Vr.js} +6 -6
- package/apps/ui/dist/server/assets/{antigravity-server-DFUx4Khk.js → antigravity-server-CHRjVFqN.js} +11 -11
- package/apps/ui/dist/server/assets/{antigravity._workspaceKey-DnSlSC-C.js → antigravity._workspaceKey-B0QeYUOh.js} +2 -2
- package/apps/ui/dist/server/assets/{antigravity._workspaceKey-D42ixtzp.js → antigravity._workspaceKey-C3kP-qyN.js} +5 -5
- package/apps/ui/dist/server/assets/{antigravity.index-DZVT-cac.js → antigravity.index-CPCYkqCi.js} +2 -2
- package/apps/ui/dist/server/assets/{codex-queries-eOJGfHQj.js → codex-queries-CtgeZ7VL.js} +16 -14
- package/apps/ui/dist/server/assets/{codex-server-nrETIF--.js → codex-server-CNXSJuc2.js} +105 -120
- package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +18 -0
- package/apps/ui/dist/server/assets/{createServerRpc-BtXIw2iP.js → createServerRpc-CF_DEwnm.js} +1 -1
- package/apps/ui/dist/server/assets/{createSsrRpc-COf5Zuye.js → createSsrRpc-C754NPki.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor-db-B7agkAvM.js → cursor-db-cYZEU3WQ.js} +194 -7
- package/apps/ui/dist/server/assets/{cursor-queries-BMhuJeUO.js → cursor-queries-NCIM0Nat.js} +13 -11
- package/apps/ui/dist/server/assets/{cursor-recovery-9bJLs7vG.js → cursor-recovery-nq-kR62j.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor-server-BgylIFgn.js → cursor-server-C3q7hrp-.js} +70 -41
- package/apps/ui/dist/server/assets/{cursor-threads._composerId-DXffY_CK.js → cursor-threads._composerId-BPJFWfJj.js} +2 -2
- package/apps/ui/dist/server/assets/cursor-threads._composerId-BkMMTQ3v.js +582 -0
- package/apps/ui/dist/server/assets/{cursor._workspaceKey-BP2J1x_x.js → cursor._workspaceKey-C5PdTaKB.js} +2 -2
- package/apps/ui/dist/server/assets/{cursor._workspaceKey-BQd0e-Pd.js → cursor._workspaceKey-Ckj7apKI.js} +11 -9
- package/apps/ui/dist/server/assets/{cursor.index-CQVxtCm8.js → cursor.index-DkWT-mxw.js} +2 -2
- package/apps/ui/dist/server/assets/{delete-confirm-dialog-PCD7S0_M.js → delete-confirm-dialog-BcBNCxWB.js} +5 -1
- package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +5 -0
- package/apps/ui/dist/server/assets/{export-dialog-DaPlOGFT.js → export-dialog-BItjWgkZ.js} +53 -11
- package/apps/ui/dist/server/assets/{projects._project-DUN3iWfg.js → projects._project-Bpbc3C-L.js} +5 -3
- package/apps/ui/dist/server/assets/{projects._project-Dim9Y0kD.js → projects._project-CvZho6EQ.js} +42 -12
- package/apps/ui/dist/server/assets/{projects.index-DjSQK5dm.js → projects.index-BkLiF2FF.js} +21 -7
- package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +14 -0
- package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +38 -0
- package/apps/ui/dist/server/assets/{router-DrDgc-LD.js → router-6m-ihwqA.js} +78 -62
- package/apps/ui/dist/server/assets/{routes-B-GlEe2C.js → routes-Bsact1uB.js} +2 -2
- package/apps/ui/dist/server/assets/{routes-CNHAUMwo.js → routes-BzkJgq4W.js} +2 -2
- package/apps/ui/dist/server/assets/start-CI_0bSiY.js +4 -0
- package/apps/ui/dist/server/assets/tabs-CGA13IZM.js +502 -0
- package/apps/ui/dist/server/assets/{threads._threadId-CJzm4KrZ.js → threads._threadId-Bs0yuqO9.js} +2 -2
- package/apps/ui/dist/server/assets/threads._threadId-D2GYkVn6.js +639 -0
- package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +83 -0
- package/apps/ui/dist/server/server.js +340 -218
- package/package.json +9 -9
- package/src/lib/antigravity-db.ts +20 -13
- package/src/lib/codex-analytics.ts +118 -18
- package/src/lib/codex-browser-db.ts +1 -25
- package/src/lib/codex-browser-export.ts +9 -69
- package/src/lib/cursor-db.ts +318 -7
- package/src/lib/cursor-exporter.ts +2 -2
- package/src/lib/ui-cache.ts +14 -1
- package/src/spiracha.ts +20 -1
- package/apps/ui/dist/client/assets/analytics-B_hYz65v.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-qiyygB7e.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-z1SQC2Kg.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-keychain-panel-dYuRWtCf.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CliqUr7o.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CnoBzyX6.js +0 -1
- package/apps/ui/dist/client/assets/antigravity.index-CakfZz_E.js +0 -1
- package/apps/ui/dist/client/assets/antigravity.index-DY7M1KhG.js +0 -1
- package/apps/ui/dist/client/assets/badge-aHE9ETVe.js +0 -1
- package/apps/ui/dist/client/assets/checkbox-DN3XnJaA.js +0 -1
- package/apps/ui/dist/client/assets/cursor-threads._composerId-BMQyx8qG.js +0 -1
- package/apps/ui/dist/client/assets/cursor-threads._composerId-BTlaA-tV.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-CrgrfevV.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-bYS2syGL.js +0 -1
- package/apps/ui/dist/client/assets/cursor.index-CTqZMPYU.js +0 -1
- package/apps/ui/dist/client/assets/cursor.index-Clsz4E_e.js +0 -2
- package/apps/ui/dist/client/assets/delete-confirm-dialog-DTpzBiNK.js +0 -11
- package/apps/ui/dist/client/assets/dist-BNAn99Pu.js +0 -1
- package/apps/ui/dist/client/assets/download-P3Rp23Ad.js +0 -1
- package/apps/ui/dist/client/assets/dropdown-menu-3qB5j9nt.js +0 -1
- package/apps/ui/dist/client/assets/es2015-Dwm_turD.js +0 -41
- package/apps/ui/dist/client/assets/export-dialog-CazdrASq.js +0 -1
- package/apps/ui/dist/client/assets/formatters-BdnWuM1z.js +0 -1
- package/apps/ui/dist/client/assets/index-BVFnfS78.js +0 -22
- package/apps/ui/dist/client/assets/json-panel-DLkS30sQ.js +0 -1
- package/apps/ui/dist/client/assets/metadata-section-jnIkB7dB.js +0 -1
- package/apps/ui/dist/client/assets/metric-card-CBZuWLzQ.js +0 -1
- package/apps/ui/dist/client/assets/page-header-CnD21cPn.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-BLszwvYL.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-DvLxYbvk.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-COn8woBR.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-DYs98skV.js +0 -3
- package/apps/ui/dist/client/assets/refresh-ccw-BDrYXjtD.js +0 -1
- package/apps/ui/dist/client/assets/reload-error-panel-DLAg0AW2.js +0 -1
- package/apps/ui/dist/client/assets/routes-BtF5-coe.js +0 -1
- package/apps/ui/dist/client/assets/scroll-text-CqaFm9by.js +0 -1
- package/apps/ui/dist/client/assets/select-DbnpwqL6.js +0 -1
- package/apps/ui/dist/client/assets/settings-CGX3VleN.js +0 -1
- package/apps/ui/dist/client/assets/styles-Ch0r3kMZ.css +0 -1
- package/apps/ui/dist/client/assets/text-document-panel-DPleOmmq.js +0 -1
- package/apps/ui/dist/client/assets/text-filter-7M6wRo-t.js +0 -2
- package/apps/ui/dist/client/assets/threads._threadId-D5w76IB-.js +0 -7
- package/apps/ui/dist/client/assets/threads._threadId-Dx85sI9P.js +0 -1
- package/apps/ui/dist/client/assets/useMutation-MZ3Hr9h9.js +0 -1
- package/apps/ui/dist/client/assets/useQuery-Cb4V0AT0.js +0 -1
- package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +0 -5
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CBbkUXw6.js +0 -227
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-B9Rm4EXh.js +0 -212
- package/apps/ui/dist/server/assets/cursor-threads._composerId-BsxFKzoJ.js +0 -218
- package/apps/ui/dist/server/assets/json-panel-RYsxWFae.js +0 -16
- package/apps/ui/dist/server/assets/metadata-section-D6Lbc7D6.js +0 -54
- package/apps/ui/dist/server/assets/start-HeKLHD9b.js +0 -4
- package/apps/ui/dist/server/assets/text-document-panel-D8JbQWAn.js +0 -23
- package/apps/ui/dist/server/assets/threads._threadId-DODTYddm.js +0 -1052
package/package.json
CHANGED
|
@@ -14,25 +14,25 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@inquirer/prompts": "^8.5.2",
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
17
|
-
"@tanstack/react-query": "5.
|
|
18
|
-
"@tanstack/react-router": "1.170.
|
|
17
|
+
"@tanstack/react-query": "5.101.0",
|
|
18
|
+
"@tanstack/react-router": "1.170.15",
|
|
19
19
|
"@tanstack/react-router-ssr-query": "1.167.1",
|
|
20
20
|
"@tanstack/react-table": "8.21.3",
|
|
21
|
-
"@tanstack/react-virtual": "3.
|
|
21
|
+
"@tanstack/react-virtual": "3.14.2",
|
|
22
22
|
"class-variance-authority": "0.7.1",
|
|
23
23
|
"clsx": "2.1.1",
|
|
24
24
|
"iconv-lite": "^0.7.2",
|
|
25
25
|
"lucide-react": "1.17.0",
|
|
26
|
-
"radix-ui": "1.
|
|
27
|
-
"react": "19.2.
|
|
28
|
-
"react-dom": "19.2.
|
|
26
|
+
"radix-ui": "1.5.0",
|
|
27
|
+
"react": "19.2.7",
|
|
28
|
+
"react-dom": "19.2.7",
|
|
29
29
|
"tailwind-merge": "3.6.0",
|
|
30
30
|
"zod": "4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"description": "Export local Codex, Claude Code, and Cursor transcripts, and browse Codex, Cursor, and Antigravity history through a local UI.",
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/bun": "^1.3.14",
|
|
35
|
-
"@types/node": "^25.9.
|
|
35
|
+
"@types/node": "^25.9.2",
|
|
36
36
|
"typescript": "^6.0.3"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"README.md",
|
|
87
87
|
"AGENTS.md"
|
|
88
88
|
],
|
|
89
|
-
"homepage": "https://github.com/ragaeeb/spiracha
|
|
89
|
+
"homepage": "https://github.com/ragaeeb/spiracha",
|
|
90
90
|
"keywords": [
|
|
91
91
|
"codex",
|
|
92
92
|
"claude",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"ui:preview": "bun run --cwd apps/ui preview"
|
|
123
123
|
},
|
|
124
124
|
"type": "module",
|
|
125
|
-
"version": "1.
|
|
125
|
+
"version": "1.4.0",
|
|
126
126
|
"workspaces": [
|
|
127
127
|
"apps/*"
|
|
128
128
|
]
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveAntigravityRoots,
|
|
12
12
|
} from './antigravity-exporter-types';
|
|
13
13
|
import { decryptAntigravitySafeStoragePayload } from './antigravity-keychain';
|
|
14
|
+
import { mapWithConcurrency } from './concurrency';
|
|
14
15
|
|
|
15
16
|
type ProtoField = {
|
|
16
17
|
bytes?: Uint8Array;
|
|
@@ -39,6 +40,11 @@ type ConversationFile = {
|
|
|
39
40
|
root: string;
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
const ANTIGRAVITY_ARTIFACT_READ_CONCURRENCY = 16;
|
|
44
|
+
const SAFE_CONVERSATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
45
|
+
|
|
46
|
+
const isSafeConversationId = (value: string) => SAFE_CONVERSATION_ID_PATTERN.test(value);
|
|
47
|
+
|
|
42
48
|
type TranscriptFile = {
|
|
43
49
|
bytes: number;
|
|
44
50
|
entryCount: number;
|
|
@@ -428,25 +434,22 @@ const readArtifactsForRoot = async (root: string): Promise<Map<string, Antigravi
|
|
|
428
434
|
|
|
429
435
|
const artifactsByConversation = new Map<string, AntigravityArtifact[]>();
|
|
430
436
|
for (const entry of entries) {
|
|
431
|
-
if (!entry.isDirectory()) {
|
|
437
|
+
if (!entry.isDirectory() || !isSafeConversationId(entry.name)) {
|
|
432
438
|
continue;
|
|
433
439
|
}
|
|
434
440
|
|
|
435
441
|
const artifactDir = path.join(brainDir, entry.name);
|
|
436
442
|
const files = await readdir(artifactDir, { withFileTypes: true }).catch(() => []);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
443
|
+
const markdownFiles = files.filter((file) => file.isFile() && file.name.endsWith('.md'));
|
|
444
|
+
const artifacts = await mapWithConcurrency(markdownFiles, ANTIGRAVITY_ARTIFACT_READ_CONCURRENCY, (file) =>
|
|
445
|
+
readArtifactCandidate(root, path.join(artifactDir, file.name), file.name),
|
|
446
|
+
);
|
|
447
|
+
for (const artifact of artifacts) {
|
|
448
|
+
if (artifact) {
|
|
449
|
+
const list = artifactsByConversation.get(entry.name) ?? [];
|
|
450
|
+
list.push(artifact);
|
|
451
|
+
artifactsByConversation.set(entry.name, list);
|
|
440
452
|
}
|
|
441
|
-
|
|
442
|
-
const artifactPath = path.join(artifactDir, file.name);
|
|
443
|
-
const artifact = await readArtifactCandidate(root, artifactPath, file.name);
|
|
444
|
-
if (!artifact) {
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
const list = artifactsByConversation.get(entry.name) ?? [];
|
|
448
|
-
list.push(artifact);
|
|
449
|
-
artifactsByConversation.set(entry.name, list);
|
|
450
453
|
}
|
|
451
454
|
}
|
|
452
455
|
|
|
@@ -811,6 +814,10 @@ const logEntryHeading = (entry: AntigravityLogEntry): string => {
|
|
|
811
814
|
}
|
|
812
815
|
|
|
813
816
|
if (source === 'MODEL') {
|
|
817
|
+
if (type && type !== 'PLANNER_RESPONSE') {
|
|
818
|
+
return `Tool: ${type}`;
|
|
819
|
+
}
|
|
820
|
+
|
|
814
821
|
return 'Assistant';
|
|
815
822
|
}
|
|
816
823
|
|
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import { listScopedThreads } from './codex-browser-db';
|
|
2
2
|
import type { CodexAnalytics, DistributionItem, ModelTokenSummary } from './codex-browser-types';
|
|
3
3
|
import type { ThreadRow } from './codex-exporter-types';
|
|
4
|
-
import {
|
|
5
|
-
import { getPortablePathBasename } from './shared';
|
|
6
|
-
import {
|
|
4
|
+
import { mapWithConcurrency } from './concurrency';
|
|
5
|
+
import { asObject, asString, getPortablePathBasename, readJsonlObjects } from './shared';
|
|
6
|
+
import { hashCacheKeyParts, hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
|
|
7
7
|
|
|
8
8
|
export type CodexAnalyticsInput = {
|
|
9
9
|
dbPath: string;
|
|
10
10
|
project: string | null;
|
|
11
|
+
transcriptConcurrency?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ThreadAnalyticsSummary = {
|
|
15
|
+
hasWebSearch: boolean;
|
|
16
|
+
toolNames: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ComputeCodexAnalyticsOptions = {
|
|
20
|
+
loadThreadAnalytics?: (thread: ThreadRow) => Promise<ThreadAnalyticsSummary>;
|
|
21
|
+
transcriptConcurrency?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_ANALYTICS_TRANSCRIPT_CONCURRENCY = 8;
|
|
25
|
+
|
|
26
|
+
export const resolveAnalyticsTranscriptConcurrency = (
|
|
27
|
+
configuredValue = process.env.SPIRACHA_ANALYTICS_TRANSCRIPT_CONCURRENCY,
|
|
28
|
+
) => {
|
|
29
|
+
const parsed = Number(configuredValue);
|
|
30
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
31
|
+
return DEFAULT_ANALYTICS_TRANSCRIPT_CONCURRENCY;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return parsed;
|
|
11
35
|
};
|
|
12
36
|
|
|
13
37
|
const roundToTwoDecimals = (value: number) => {
|
|
@@ -52,28 +76,100 @@ const buildModelsByTokens = (threads: ThreadRow[]): ModelTokenSummary[] => {
|
|
|
52
76
|
});
|
|
53
77
|
};
|
|
54
78
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const rolloutFingerprints = await Promise.all(threads.map((thread) => getFileFingerprint(thread.rollout_path)));
|
|
58
|
-
return `analytics-${hashCacheKeyParts(dbFingerprint, project ?? 'all', ...rolloutFingerprints)}`;
|
|
79
|
+
const timestampSignature = (thread: ThreadRow) => {
|
|
80
|
+
return String(thread.updated_at_ms ?? thread.updated_at * 1000);
|
|
59
81
|
};
|
|
60
82
|
|
|
61
|
-
const
|
|
83
|
+
const threadMetadataCacheKeyParts = (thread: ThreadRow) => [
|
|
84
|
+
thread.id,
|
|
85
|
+
thread.rollout_path,
|
|
86
|
+
timestampSignature(thread),
|
|
87
|
+
String(thread.created_at_ms ?? thread.created_at * 1000),
|
|
88
|
+
String(thread.tokens_used),
|
|
89
|
+
String(thread.archived),
|
|
90
|
+
String(thread.archived_at ?? ''),
|
|
91
|
+
thread.cwd,
|
|
92
|
+
thread.model ?? '',
|
|
93
|
+
thread.model_provider,
|
|
94
|
+
thread.cli_version,
|
|
95
|
+
thread.title,
|
|
96
|
+
thread.preview,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export const buildCodexAnalyticsCacheKey = (dbPath: string, threads: ThreadRow[], project: string | null) => {
|
|
100
|
+
const parts = (function* () {
|
|
101
|
+
yield 'v2';
|
|
102
|
+
yield dbPath;
|
|
103
|
+
yield project ?? 'all';
|
|
104
|
+
yield String(threads.length);
|
|
105
|
+
for (const thread of threads) {
|
|
106
|
+
yield* threadMetadataCacheKeyParts(thread);
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
return `analytics-${hashCacheKeyPartsIterable(parts)}`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const buildThreadAnalyticsCacheKey = (thread: ThreadRow) => {
|
|
114
|
+
return `thread-analytics-${hashCacheKeyParts('v1', ...threadMetadataCacheKeyParts(thread))}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const parseThreadAnalyticsFile = async (sessionFile: string): Promise<ThreadAnalyticsSummary> => {
|
|
118
|
+
const toolNames: string[] = [];
|
|
119
|
+
let hasWebSearch = false;
|
|
120
|
+
|
|
121
|
+
for await (const parsed of readJsonlObjects(sessionFile)) {
|
|
122
|
+
if (parsed.type !== 'response_item') {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const payload = asObject(parsed.payload);
|
|
127
|
+
if (!payload) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const payloadType = asString(payload.type);
|
|
132
|
+
if (payloadType === 'function_call') {
|
|
133
|
+
toolNames.push(asString(payload.name) ?? 'unknown');
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (payloadType === 'web_search_call' || payloadType === 'web_search_end') {
|
|
138
|
+
hasWebSearch = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
hasWebSearch,
|
|
144
|
+
toolNames,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getCachedThreadAnalytics = async (thread: ThreadRow): Promise<ThreadAnalyticsSummary> => {
|
|
149
|
+
return withCachedJson(buildThreadAnalyticsCacheKey(thread), () => parseThreadAnalyticsFile(thread.rollout_path));
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const computeCodexAnalyticsFromThreads = async (
|
|
153
|
+
threads: ThreadRow[],
|
|
154
|
+
options: ComputeCodexAnalyticsOptions = {},
|
|
155
|
+
): Promise<CodexAnalytics> => {
|
|
62
156
|
const totalTokens = threads.reduce((sum, thread) => sum + thread.tokens_used, 0);
|
|
63
157
|
const projectNames = new Set(threads.map((thread) => getPortablePathBasename(thread.cwd)).filter(Boolean));
|
|
64
158
|
const toolUsage = new Map<string, number>();
|
|
65
|
-
const transcripts = await Promise.all(threads.map((thread) => getCachedParsedCodexTranscript(thread.rollout_path)));
|
|
66
159
|
let threadsWithWebSearch = 0;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
160
|
+
const loadThreadAnalytics = options.loadThreadAnalytics ?? getCachedThreadAnalytics;
|
|
161
|
+
const transcriptConcurrency = options.transcriptConcurrency ?? resolveAnalyticsTranscriptConcurrency();
|
|
162
|
+
const threadAnalytics = await mapWithConcurrency(threads, transcriptConcurrency, (thread) =>
|
|
163
|
+
loadThreadAnalytics(thread),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
for (const analytics of threadAnalytics) {
|
|
167
|
+
if (analytics.hasWebSearch) {
|
|
70
168
|
threadsWithWebSearch += 1;
|
|
71
169
|
}
|
|
72
170
|
|
|
73
|
-
for (const
|
|
74
|
-
|
|
75
|
-
incrementCount(toolUsage, event.name);
|
|
76
|
-
}
|
|
171
|
+
for (const toolName of analytics.toolNames) {
|
|
172
|
+
incrementCount(toolUsage, toolName);
|
|
77
173
|
}
|
|
78
174
|
}
|
|
79
175
|
|
|
@@ -94,7 +190,11 @@ const computeCodexAnalytics = async (threads: ThreadRow[]): Promise<CodexAnalyti
|
|
|
94
190
|
|
|
95
191
|
export const getCodexAnalytics = async (input: CodexAnalyticsInput): Promise<CodexAnalytics> => {
|
|
96
192
|
const threads = listScopedThreads(input.dbPath, input.project);
|
|
97
|
-
const key =
|
|
193
|
+
const key = buildCodexAnalyticsCacheKey(input.dbPath, threads, input.project);
|
|
98
194
|
|
|
99
|
-
return withCachedJson(key, async () =>
|
|
195
|
+
return withCachedJson(key, async () =>
|
|
196
|
+
computeCodexAnalyticsFromThreads(threads, {
|
|
197
|
+
transcriptConcurrency: input.transcriptConcurrency,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
100
200
|
};
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import type { ThreadRelations, ThreadRow } from './codex-exporter-types';
|
|
15
15
|
import { DEFAULT_CODEX_DIR, DEFAULT_DB_PATH } from './codex-exporter-types';
|
|
16
16
|
import { getCachedParsedCodexTranscript, getThreadRolloutLoadState } from './codex-thread-cache';
|
|
17
|
+
import { mapWithConcurrency } from './concurrency';
|
|
17
18
|
import { cleanInlineTitle, getPortablePathBasename } from './shared';
|
|
18
19
|
import { runWithSqliteRetry } from './sqlite-retry';
|
|
19
20
|
import { invalidateCacheByPrefix } from './ui-cache';
|
|
@@ -48,31 +49,6 @@ const isPromiseLike = (value: unknown): value is PromiseLike<unknown> => {
|
|
|
48
49
|
return 'then' in value && typeof value.then === 'function';
|
|
49
50
|
};
|
|
50
51
|
|
|
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
52
|
const openReadonlyDb = (dbPath: string, busyTimeoutMs: number) => {
|
|
77
53
|
const db = new Database(dbPath, { readonly: true });
|
|
78
54
|
try {
|
|
@@ -6,6 +6,7 @@ import { convertSessionFile, writeSessionFileExport } from './codex-exporter-tra
|
|
|
6
6
|
import type { CodexCliOptions } from './codex-exporter-types';
|
|
7
7
|
import { applyPathTransforms, type PathDisplaySettings } from './path-transforms';
|
|
8
8
|
import { type ExportFormat, getPortablePathBasename } from './shared';
|
|
9
|
+
import { getExportMimeType, sanitizeExportFileName, zipExportDirectory, zipExportFile } from './ui-export-archive';
|
|
9
10
|
import { buildUiExportDownloadUrl, ensureUiExportDir } from './ui-export-files';
|
|
10
11
|
|
|
11
12
|
type RenderCodexThreadDownloadInput = {
|
|
@@ -18,6 +19,7 @@ type RenderCodexThreadDownloadInput = {
|
|
|
18
19
|
pathDisplaySettings?: Pick<PathDisplaySettings, 'convertToProjectRoot' | 'redactUsername'>;
|
|
19
20
|
publicExportDir?: string;
|
|
20
21
|
threadId: string;
|
|
22
|
+
zipArchive?: boolean;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
type RenderCodexThreadsDownloadInput = Omit<RenderCodexThreadDownloadInput, 'threadId'> & {
|
|
@@ -40,14 +42,6 @@ export type CodexThreadDownload =
|
|
|
40
42
|
|
|
41
43
|
const LARGE_BROWSER_EXPORT_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
42
44
|
|
|
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
45
|
const formatReadableExportDate = (value: number) => {
|
|
52
46
|
const date = new Date(value);
|
|
53
47
|
const year = date.getUTCFullYear();
|
|
@@ -59,7 +53,7 @@ const formatReadableExportDate = (value: number) => {
|
|
|
59
53
|
};
|
|
60
54
|
|
|
61
55
|
const buildExportBaseName = (thread: ReturnType<typeof getThreadBrowseData>['thread']) => {
|
|
62
|
-
const projectName =
|
|
56
|
+
const projectName = sanitizeExportFileName(getPortablePathBasename(thread.cwd) || 'thread') || 'thread';
|
|
63
57
|
const timestamp = thread.updated_at_ms ?? thread.updated_at * 1000;
|
|
64
58
|
return `${projectName}-${formatReadableExportDate(timestamp)}-${thread.id.slice(0, 8)}`;
|
|
65
59
|
};
|
|
@@ -70,7 +64,7 @@ const buildBatchExportBaseName = (threads: Array<ReturnType<typeof getThreadBrow
|
|
|
70
64
|
throw new Error('No threads selected for export');
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
const projectName =
|
|
67
|
+
const projectName = sanitizeExportFileName(getPortablePathBasename(firstThread.cwd) || 'threads') || 'threads';
|
|
74
68
|
const latestTimestamp = Math.max(...threads.map((thread) => thread.updated_at_ms ?? thread.updated_at * 1000));
|
|
75
69
|
return `${projectName}-${formatReadableExportDate(latestTimestamp)}-threads-${threads.length}`;
|
|
76
70
|
};
|
|
@@ -111,10 +105,6 @@ const toDownloadOptions = (input: RenderCodexThreadDownloadInput): CodexCliOptio
|
|
|
111
105
|
};
|
|
112
106
|
};
|
|
113
107
|
|
|
114
|
-
const getMimeType = (outputFormat: ExportFormat) => {
|
|
115
|
-
return outputFormat === 'md' ? 'text/markdown; charset=utf-8' : 'text/plain; charset=utf-8';
|
|
116
|
-
};
|
|
117
|
-
|
|
118
108
|
const resolvePublicExportDir = async (publicExportDir?: string) => {
|
|
119
109
|
if (publicExportDir) {
|
|
120
110
|
await ensureDirectory(publicExportDir);
|
|
@@ -146,7 +136,6 @@ const logExportEvent = (level: 'error' | 'info' | 'warn', event: string, details
|
|
|
146
136
|
|
|
147
137
|
const logRolloutChangeIfDetected = (
|
|
148
138
|
threadId: string,
|
|
149
|
-
rolloutPath: string,
|
|
150
139
|
beforeSnapshot: RolloutSnapshot,
|
|
151
140
|
afterSnapshot: RolloutSnapshot,
|
|
152
141
|
) => {
|
|
@@ -159,7 +148,6 @@ const logRolloutChangeIfDetected = (
|
|
|
159
148
|
afterSizeBytes: afterSnapshot.sizeBytes,
|
|
160
149
|
beforeMtimeMs: beforeSnapshot.mtimeMs,
|
|
161
150
|
beforeSizeBytes: beforeSnapshot.sizeBytes,
|
|
162
|
-
rolloutPath,
|
|
163
151
|
threadId,
|
|
164
152
|
});
|
|
165
153
|
};
|
|
@@ -175,39 +163,6 @@ const cleanupExportWorkspace = async (workspacePath: string) => {
|
|
|
175
163
|
}
|
|
176
164
|
};
|
|
177
165
|
|
|
178
|
-
const zipExportFile = async (sourcePath: string, zipPath: string) => {
|
|
179
|
-
const proc = Bun.spawn(['zip', '-9', '-j', zipPath, sourcePath], {
|
|
180
|
-
stderr: 'pipe',
|
|
181
|
-
stdout: 'pipe',
|
|
182
|
-
});
|
|
183
|
-
const [stdoutText, stderrText, exitCode] = await Promise.all([
|
|
184
|
-
new Response(proc.stdout).text(),
|
|
185
|
-
new Response(proc.stderr).text(),
|
|
186
|
-
proc.exited,
|
|
187
|
-
]);
|
|
188
|
-
|
|
189
|
-
if (exitCode !== 0) {
|
|
190
|
-
throw new Error(`zip failed (${exitCode}): ${(stderrText || stdoutText).trim()}`);
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const zipExportDirectory = async (sourceDirectory: string, zipPath: string) => {
|
|
195
|
-
const proc = Bun.spawn(['zip', '-9', '-r', zipPath, '.'], {
|
|
196
|
-
cwd: sourceDirectory,
|
|
197
|
-
stderr: 'pipe',
|
|
198
|
-
stdout: 'pipe',
|
|
199
|
-
});
|
|
200
|
-
const [stdoutText, stderrText, exitCode] = await Promise.all([
|
|
201
|
-
new Response(proc.stdout).text(),
|
|
202
|
-
new Response(proc.stderr).text(),
|
|
203
|
-
proc.exited,
|
|
204
|
-
]);
|
|
205
|
-
|
|
206
|
-
if (exitCode !== 0) {
|
|
207
|
-
throw new Error(`zip failed (${exitCode}): ${(stderrText || stdoutText).trim()}`);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
166
|
export const renderCodexThreadDownload = async (
|
|
212
167
|
input: RenderCodexThreadDownloadInput,
|
|
213
168
|
): Promise<CodexThreadDownload> => {
|
|
@@ -216,7 +171,7 @@ export const renderCodexThreadDownload = async (
|
|
|
216
171
|
const extension = input.outputFormat === 'md' ? 'md' : 'txt';
|
|
217
172
|
const fileBaseName = buildExportBaseName(browseData.thread);
|
|
218
173
|
const fileName = `${fileBaseName}.${extension}`;
|
|
219
|
-
const mimeType =
|
|
174
|
+
const mimeType = getExportMimeType(input.outputFormat);
|
|
220
175
|
const transform = (text: string) =>
|
|
221
176
|
input.pathDisplaySettings
|
|
222
177
|
? applyPathTransforms(text, {
|
|
@@ -228,13 +183,13 @@ export const renderCodexThreadDownload = async (
|
|
|
228
183
|
|
|
229
184
|
logExportEvent('info', 'single_start', {
|
|
230
185
|
fileName,
|
|
231
|
-
rolloutPath: browseData.thread.rollout_path,
|
|
232
186
|
sizeBytes: rolloutSnapshotBefore.sizeBytes,
|
|
233
187
|
threadId: input.threadId,
|
|
234
188
|
});
|
|
235
189
|
|
|
236
190
|
try {
|
|
237
191
|
if (
|
|
192
|
+
input.zipArchive ||
|
|
238
193
|
rolloutSnapshotBefore.sizeBytes > (input.largeExportThresholdBytes ?? LARGE_BROWSER_EXPORT_THRESHOLD_BYTES)
|
|
239
194
|
) {
|
|
240
195
|
const exportBaseName = fileBaseName;
|
|
@@ -266,12 +221,7 @@ export const renderCodexThreadDownload = async (
|
|
|
266
221
|
}
|
|
267
222
|
|
|
268
223
|
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
269
|
-
logRolloutChangeIfDetected(
|
|
270
|
-
input.threadId,
|
|
271
|
-
browseData.thread.rollout_path,
|
|
272
|
-
rolloutSnapshotBefore,
|
|
273
|
-
rolloutSnapshotAfter,
|
|
274
|
-
);
|
|
224
|
+
logRolloutChangeIfDetected(input.threadId, rolloutSnapshotBefore, rolloutSnapshotAfter);
|
|
275
225
|
|
|
276
226
|
const zipStat = await Bun.file(zipPath).stat();
|
|
277
227
|
logExportEvent('info', 'single_zip_ready', {
|
|
@@ -307,12 +257,7 @@ export const renderCodexThreadDownload = async (
|
|
|
307
257
|
}
|
|
308
258
|
|
|
309
259
|
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
310
|
-
logRolloutChangeIfDetected(
|
|
311
|
-
input.threadId,
|
|
312
|
-
browseData.thread.rollout_path,
|
|
313
|
-
rolloutSnapshotBefore,
|
|
314
|
-
rolloutSnapshotAfter,
|
|
315
|
-
);
|
|
260
|
+
logRolloutChangeIfDetected(input.threadId, rolloutSnapshotBefore, rolloutSnapshotAfter);
|
|
316
261
|
logExportEvent('info', 'single_inline_ready', {
|
|
317
262
|
durationMs: Date.now() - startedAt,
|
|
318
263
|
fileName,
|
|
@@ -412,12 +357,7 @@ export const renderCodexThreadsDownload = async (
|
|
|
412
357
|
}
|
|
413
358
|
|
|
414
359
|
const rolloutSnapshotAfter = await getRolloutSnapshot(entry.thread.rollout_path);
|
|
415
|
-
logRolloutChangeIfDetected(
|
|
416
|
-
entry.thread.id,
|
|
417
|
-
entry.thread.rollout_path,
|
|
418
|
-
rolloutSnapshotBefore,
|
|
419
|
-
rolloutSnapshotAfter,
|
|
420
|
-
);
|
|
360
|
+
logRolloutChangeIfDetected(entry.thread.id, rolloutSnapshotBefore, rolloutSnapshotAfter);
|
|
421
361
|
}
|
|
422
362
|
|
|
423
363
|
await zipExportDirectory(bundleDirectory, zipPath);
|