spiracha 1.3.3 → 1.5.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 +55 -6
- package/README.md +46 -5
- package/apps/ui/AGENTS.md +31 -4
- package/apps/ui/README.md +59 -5
- package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +1 -0
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DqySVO2K.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-B77qJz6f.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +1 -0
- package/apps/ui/dist/client/assets/antigravity.index-DP9JC2P9.js +1 -0
- package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +1 -0
- package/apps/ui/dist/client/assets/badge-C3i4OKs8.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +1 -0
- package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +1 -0
- package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +1 -0
- package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +1 -0
- package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +1 -0
- package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +1 -0
- package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +1 -0
- package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.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-hBWlP1D-.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +1 -0
- package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +2 -0
- package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +1 -0
- package/apps/ui/dist/client/assets/{data-table-DeQq8xdt.js → data-table-Br78_dk3.js} +2 -2
- package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +7 -0
- package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +1 -0
- package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +5 -0
- package/apps/ui/dist/client/assets/dist-Cmd3AIfD.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-D5cvPaZt.js +1 -0
- package/apps/ui/dist/client/assets/es2015-ad6040dw.js +41 -0
- package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +1 -0
- package/apps/ui/dist/client/assets/formatters-BtqyrX_c.js +1 -0
- package/apps/ui/dist/client/assets/index-C-STgIH4.js +166 -0
- package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +1 -0
- package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +1 -0
- package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +1 -0
- package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +1 -0
- package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +1 -0
- package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +1 -0
- package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +1 -0
- package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +1 -0
- package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +1 -0
- package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +8 -0
- package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +1 -0
- package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +1 -0
- package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +1 -0
- package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +1 -0
- package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +1 -0
- package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +3 -0
- package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +1 -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-DnzF_9CX.js +1 -0
- package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +1 -0
- package/apps/ui/dist/client/assets/select-CYF6pF37.js +1 -0
- package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +1 -0
- package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +1 -0
- package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +1 -0
- package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +7 -0
- package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +2 -0
- package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +1 -0
- package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +1 -0
- package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +1 -0
- package/apps/ui/dist/client/icon.svg +2 -1
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +387 -0
- package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +16 -0
- package/apps/ui/dist/server/assets/{analytics-Dc_QDZZC.js → analytics-CtpOMVMO.js} +13 -6
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BKOyL2T6.js +613 -0
- package/apps/ui/dist/server/assets/{antigravity-conversations._conversationId-C9z4AeN-.js → antigravity-conversations._conversationId-eS87Dx7g.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-bpPoxhyp.js → antigravity-keychain-panel-CC7FLmAK.js} +1 -1
- package/apps/ui/dist/server/assets/{antigravity-queries-DVIKyOXi.js → antigravity-queries-CGrJO9Vr.js} +5 -5
- package/apps/ui/dist/server/assets/{antigravity-server-Cp1lc8fp.js → antigravity-server-CHRjVFqN.js} +11 -11
- package/apps/ui/dist/server/assets/{antigravity._workspaceKey-Dl8o1ptg.js → antigravity._workspaceKey-B0QeYUOh.js} +2 -2
- package/apps/ui/dist/server/assets/{antigravity._workspaceKey-BAoa4izU.js → antigravity._workspaceKey-C3kP-qyN.js} +4 -4
- package/apps/ui/dist/server/assets/{antigravity.index-BOlrJasZ.js → antigravity.index-CPCYkqCi.js} +2 -2
- package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +361 -0
- package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +37 -0
- package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +72 -0
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +18 -0
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +473 -0
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +11 -0
- package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +139 -0
- package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +10 -0
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +28 -0
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +11 -0
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +191 -0
- package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +92 -0
- package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +11 -0
- package/apps/ui/dist/server/assets/{codex-queries-ev_mRUzr.js → codex-queries-CtgeZ7VL.js} +15 -13
- package/apps/ui/dist/server/assets/{codex-server-DIYFLozB.js → codex-server-CGXU4H8-.js} +421 -146
- package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +18 -0
- package/apps/ui/dist/server/assets/{createServerRpc-Cg-Zr1rq.js → createServerRpc-CF_DEwnm.js} +1 -1
- package/apps/ui/dist/server/assets/{createSsrRpc-DVnVQhGx.js → createSsrRpc-C754NPki.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor-db-B7agkAvM.js → cursor-db-CZnYy7aT.js} +196 -9
- package/apps/ui/dist/server/assets/{cursor-queries-f7DjBKSD.js → cursor-queries-NCIM0Nat.js} +12 -10
- package/apps/ui/dist/server/assets/{cursor-recovery-9bJLs7vG.js → cursor-recovery-CXpO8KcD.js} +2 -2
- package/apps/ui/dist/server/assets/{cursor-server-nPzojHN3.js → cursor-server-DNu52A3K.js} +71 -42
- package/apps/ui/dist/server/assets/cursor-threads._composerId-Ckn6ItHI.js +582 -0
- package/apps/ui/dist/server/assets/{cursor-threads._composerId-B3qcsh9C.js → cursor-threads._composerId-p9-ARAzc.js} +2 -2
- package/apps/ui/dist/server/assets/{cursor-transcript-2iL3KFSK.js → cursor-transcript-B7tGnqtn.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor._workspaceKey-BL_CQyHQ.js → cursor._workspaceKey-CZuohIUL.js} +2 -2
- package/apps/ui/dist/server/assets/{cursor._workspaceKey-BhQeepXB.js → cursor._workspaceKey-DPp5JSjy.js} +11 -9
- package/apps/ui/dist/server/assets/{cursor.index-CsrdckZD.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-D3qLMPfC.js → empty-plugin-adapters-B3lHh1La.js} +1 -1
- package/apps/ui/dist/server/assets/{export-dialog-DaPlOGFT.js → export-dialog-D88ze9Gy.js} +54 -12
- package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +552 -0
- package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +37 -0
- package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +72 -0
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +11 -0
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +449 -0
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +18 -0
- package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +112 -0
- package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +27 -0
- package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +11 -0
- package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +28 -0
- package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +193 -0
- package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +11 -0
- package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +93 -0
- package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +397 -0
- package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +50 -0
- package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +72 -0
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +518 -0
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +18 -0
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +11 -0
- package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +87 -0
- package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +141 -0
- package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +32 -0
- package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +11 -0
- package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +28 -0
- package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +207 -0
- package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +11 -0
- package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +93 -0
- package/apps/ui/dist/server/assets/{projects._project-CqqUFr4l.js → projects._project-4io5LO0E.js} +5 -3
- package/apps/ui/dist/server/assets/{projects._project-DahGqNYS.js → projects._project-D6ZoGhJ8.js} +42 -12
- package/apps/ui/dist/server/assets/{projects.index-CkTeFWNU.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-Sac2DGk6.js +518 -0
- package/apps/ui/dist/server/assets/{routes-Xix3ZeSo.js → routes-Bsact1uB.js} +2 -2
- package/apps/ui/dist/server/assets/{routes-0x1U0IHq.js → routes-BzkJgq4W.js} +2 -2
- package/apps/ui/dist/server/assets/{shared-CPRNYIql.js → shared-DyhChtHf.js} +4 -1
- package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +38 -0
- package/apps/ui/dist/server/assets/start-B_HFwkkk.js +4 -0
- package/apps/ui/dist/server/assets/tabs-DkaChNVg.js +501 -0
- package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +45 -0
- package/apps/ui/dist/server/assets/threads._threadId-BeXEc4Lo.js +648 -0
- package/apps/ui/dist/server/assets/{threads._threadId-BLCkGW--.js → threads._threadId-Ci0S4mKU.js} +2 -2
- package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +83 -0
- package/apps/ui/dist/server/server.js +141 -83
- package/package.json +28 -11
- package/src/lib/antigravity-db.ts +20 -13
- package/src/lib/claude-code-db.ts +655 -0
- package/src/lib/claude-code-exporter-types.ts +110 -0
- package/src/lib/claude-code-transcript-phase.ts +25 -0
- package/src/lib/claude-code-transcript.ts +180 -0
- package/src/lib/codex-analytics.ts +118 -18
- package/src/lib/codex-browser-db.ts +491 -56
- package/src/lib/codex-browser-export.ts +27 -71
- package/src/lib/codex-thread-cache.ts +17 -1
- package/src/lib/concurrency.ts +25 -0
- package/src/lib/cursor-db.ts +318 -7
- package/src/lib/cursor-exporter.ts +2 -2
- package/src/lib/kiro-db.ts +949 -0
- package/src/lib/kiro-exporter-types.ts +100 -0
- package/src/lib/kiro-transcript-phase.ts +41 -0
- package/src/lib/kiro-transcript.ts +115 -0
- package/src/lib/opencode-db.ts +610 -0
- package/src/lib/opencode-exporter-types.ts +129 -0
- package/src/lib/opencode-think-tags.ts +126 -0
- package/src/lib/opencode-transcript-phase.ts +50 -0
- package/src/lib/opencode-transcript.ts +194 -0
- package/src/lib/shared.ts +25 -0
- package/src/lib/ui-cache.ts +14 -1
- package/src/lib/ui-export-archive.ts +61 -0
- package/apps/ui/dist/client/assets/analytics-C83LcWNb.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DkY8EEpK.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-OraHuz8j.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-keychain-panel-DnDcq1ge.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CQaZscA5.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-DTs99obU.js +0 -1
- package/apps/ui/dist/client/assets/antigravity.index-DbJa715k.js +0 -1
- package/apps/ui/dist/client/assets/antigravity.index-P-rs6w5X.js +0 -1
- package/apps/ui/dist/client/assets/badge-wgTVp-aq.js +0 -1
- package/apps/ui/dist/client/assets/checkbox-BUx0Gkuf.js +0 -1
- package/apps/ui/dist/client/assets/cursor-threads._composerId-D0DFXt8t.js +0 -1
- package/apps/ui/dist/client/assets/cursor-threads._composerId-tFa6Cf6A.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-Df4PfA2k.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-DwrzEtnV.js +0 -1
- package/apps/ui/dist/client/assets/cursor.index-CNBf4ZiR.js +0 -1
- package/apps/ui/dist/client/assets/cursor.index-DP9Hy8X6.js +0 -2
- package/apps/ui/dist/client/assets/delete-confirm-dialog-CPlNsLhM.js +0 -11
- package/apps/ui/dist/client/assets/dist-Ds3kS91l.js +0 -1
- package/apps/ui/dist/client/assets/download-DLuPnxpV.js +0 -1
- package/apps/ui/dist/client/assets/dropdown-menu-lFk6zjoL.js +0 -1
- package/apps/ui/dist/client/assets/es2015-D-H_iZF6.js +0 -41
- package/apps/ui/dist/client/assets/export-dialog-CyqyvwiP.js +0 -1
- package/apps/ui/dist/client/assets/formatters-DbCKP02D.js +0 -1
- package/apps/ui/dist/client/assets/index-C-ZPpKJ0.js +0 -22
- package/apps/ui/dist/client/assets/json-panel-BsngvucB.js +0 -1
- package/apps/ui/dist/client/assets/metadata-section-Dtl8dNag.js +0 -1
- package/apps/ui/dist/client/assets/metric-card-DNzR1Rne.js +0 -1
- package/apps/ui/dist/client/assets/page-header-dha_O7ZL.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-B7HBhbXv.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-DHVvuT7A.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-BUUtx2AZ.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-BojAe1Y4.js +0 -3
- package/apps/ui/dist/client/assets/refresh-ccw-B-ASKHtx.js +0 -1
- package/apps/ui/dist/client/assets/reload-error-panel-DD8M1BIY.js +0 -1
- package/apps/ui/dist/client/assets/routes-q7bzFp35.js +0 -1
- package/apps/ui/dist/client/assets/scroll-text-TvHyiJ_N.js +0 -1
- package/apps/ui/dist/client/assets/select-BFv1HMiT.js +0 -1
- package/apps/ui/dist/client/assets/settings-CvegTo4F.js +0 -1
- package/apps/ui/dist/client/assets/styles-Ch0r3kMZ.css +0 -1
- package/apps/ui/dist/client/assets/text-document-panel-D7Rws41o.js +0 -1
- package/apps/ui/dist/client/assets/text-filter-2q5pEoZh.js +0 -2
- package/apps/ui/dist/client/assets/threads._threadId-LpZVUlPq.js +0 -7
- package/apps/ui/dist/client/assets/threads._threadId-MSmNIBkq.js +0 -1
- package/apps/ui/dist/client/assets/useMutation-BOPJU9e0.js +0 -1
- package/apps/ui/dist/client/assets/useQuery-CbxZiVGF.js +0 -1
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-zvGzK-Yx.js +0 -229
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-JEhx-f2R.js +0 -212
- package/apps/ui/dist/server/assets/cursor-threads._composerId-BK3iW-3e.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/router-K0m7dK4T.js +0 -394
- package/apps/ui/dist/server/assets/start-bX7bXjzw.js +0 -4
- package/apps/ui/dist/server/assets/text-document-panel-D8JbQWAn.js +0 -23
- package/apps/ui/dist/server/assets/threads._threadId-LjgsGUh-.js +0 -1052
|
@@ -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);
|
|
@@ -140,13 +130,28 @@ const getRolloutSnapshot = async (rolloutPath: string): Promise<RolloutSnapshot>
|
|
|
140
130
|
};
|
|
141
131
|
};
|
|
142
132
|
|
|
133
|
+
const isMissingFileError = (error: unknown) => {
|
|
134
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const getExistingRolloutSnapshot = async (threadId: string, rolloutPath: string): Promise<RolloutSnapshot> => {
|
|
138
|
+
try {
|
|
139
|
+
return await getRolloutSnapshot(rolloutPath);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (isMissingFileError(error)) {
|
|
142
|
+
throw new Error(`Thread ${threadId} rollout file is missing: ${rolloutPath}`, { cause: error });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
143
149
|
const logExportEvent = (level: 'error' | 'info' | 'warn', event: string, details: Record<string, unknown>) => {
|
|
144
150
|
console[level](`[spiracha:export] ${event}`, details);
|
|
145
151
|
};
|
|
146
152
|
|
|
147
153
|
const logRolloutChangeIfDetected = (
|
|
148
154
|
threadId: string,
|
|
149
|
-
rolloutPath: string,
|
|
150
155
|
beforeSnapshot: RolloutSnapshot,
|
|
151
156
|
afterSnapshot: RolloutSnapshot,
|
|
152
157
|
) => {
|
|
@@ -159,7 +164,6 @@ const logRolloutChangeIfDetected = (
|
|
|
159
164
|
afterSizeBytes: afterSnapshot.sizeBytes,
|
|
160
165
|
beforeMtimeMs: beforeSnapshot.mtimeMs,
|
|
161
166
|
beforeSizeBytes: beforeSnapshot.sizeBytes,
|
|
162
|
-
rolloutPath,
|
|
163
167
|
threadId,
|
|
164
168
|
});
|
|
165
169
|
};
|
|
@@ -175,39 +179,6 @@ const cleanupExportWorkspace = async (workspacePath: string) => {
|
|
|
175
179
|
}
|
|
176
180
|
};
|
|
177
181
|
|
|
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
182
|
export const renderCodexThreadDownload = async (
|
|
212
183
|
input: RenderCodexThreadDownloadInput,
|
|
213
184
|
): Promise<CodexThreadDownload> => {
|
|
@@ -216,7 +187,7 @@ export const renderCodexThreadDownload = async (
|
|
|
216
187
|
const extension = input.outputFormat === 'md' ? 'md' : 'txt';
|
|
217
188
|
const fileBaseName = buildExportBaseName(browseData.thread);
|
|
218
189
|
const fileName = `${fileBaseName}.${extension}`;
|
|
219
|
-
const mimeType =
|
|
190
|
+
const mimeType = getExportMimeType(input.outputFormat);
|
|
220
191
|
const transform = (text: string) =>
|
|
221
192
|
input.pathDisplaySettings
|
|
222
193
|
? applyPathTransforms(text, {
|
|
@@ -224,17 +195,17 @@ export const renderCodexThreadDownload = async (
|
|
|
224
195
|
projectPath: browseData.thread.cwd,
|
|
225
196
|
})
|
|
226
197
|
: text;
|
|
227
|
-
const rolloutSnapshotBefore = await
|
|
198
|
+
const rolloutSnapshotBefore = await getExistingRolloutSnapshot(input.threadId, browseData.thread.rollout_path);
|
|
228
199
|
|
|
229
200
|
logExportEvent('info', 'single_start', {
|
|
230
201
|
fileName,
|
|
231
|
-
rolloutPath: browseData.thread.rollout_path,
|
|
232
202
|
sizeBytes: rolloutSnapshotBefore.sizeBytes,
|
|
233
203
|
threadId: input.threadId,
|
|
234
204
|
});
|
|
235
205
|
|
|
236
206
|
try {
|
|
237
207
|
if (
|
|
208
|
+
input.zipArchive ||
|
|
238
209
|
rolloutSnapshotBefore.sizeBytes > (input.largeExportThresholdBytes ?? LARGE_BROWSER_EXPORT_THRESHOLD_BYTES)
|
|
239
210
|
) {
|
|
240
211
|
const exportBaseName = fileBaseName;
|
|
@@ -266,12 +237,7 @@ export const renderCodexThreadDownload = async (
|
|
|
266
237
|
}
|
|
267
238
|
|
|
268
239
|
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
269
|
-
logRolloutChangeIfDetected(
|
|
270
|
-
input.threadId,
|
|
271
|
-
browseData.thread.rollout_path,
|
|
272
|
-
rolloutSnapshotBefore,
|
|
273
|
-
rolloutSnapshotAfter,
|
|
274
|
-
);
|
|
240
|
+
logRolloutChangeIfDetected(input.threadId, rolloutSnapshotBefore, rolloutSnapshotAfter);
|
|
275
241
|
|
|
276
242
|
const zipStat = await Bun.file(zipPath).stat();
|
|
277
243
|
logExportEvent('info', 'single_zip_ready', {
|
|
@@ -307,12 +273,7 @@ export const renderCodexThreadDownload = async (
|
|
|
307
273
|
}
|
|
308
274
|
|
|
309
275
|
const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
|
|
310
|
-
logRolloutChangeIfDetected(
|
|
311
|
-
input.threadId,
|
|
312
|
-
browseData.thread.rollout_path,
|
|
313
|
-
rolloutSnapshotBefore,
|
|
314
|
-
rolloutSnapshotAfter,
|
|
315
|
-
);
|
|
276
|
+
logRolloutChangeIfDetected(input.threadId, rolloutSnapshotBefore, rolloutSnapshotAfter);
|
|
316
277
|
logExportEvent('info', 'single_inline_ready', {
|
|
317
278
|
durationMs: Date.now() - startedAt,
|
|
318
279
|
fileName,
|
|
@@ -362,7 +323,7 @@ export const renderCodexThreadsDownload = async (
|
|
|
362
323
|
|
|
363
324
|
try {
|
|
364
325
|
for (const entry of browseEntries) {
|
|
365
|
-
const rolloutSnapshotBefore = await
|
|
326
|
+
const rolloutSnapshotBefore = await getExistingRolloutSnapshot(entry.thread.id, entry.thread.rollout_path);
|
|
366
327
|
const singleBaseName = buildExportBaseName(entry.thread);
|
|
367
328
|
const uniqueBaseName = buildUniqueBatchEntryBaseName(
|
|
368
329
|
singleBaseName,
|
|
@@ -412,12 +373,7 @@ export const renderCodexThreadsDownload = async (
|
|
|
412
373
|
}
|
|
413
374
|
|
|
414
375
|
const rolloutSnapshotAfter = await getRolloutSnapshot(entry.thread.rollout_path);
|
|
415
|
-
logRolloutChangeIfDetected(
|
|
416
|
-
entry.thread.id,
|
|
417
|
-
entry.thread.rollout_path,
|
|
418
|
-
rolloutSnapshotBefore,
|
|
419
|
-
rolloutSnapshotAfter,
|
|
420
|
-
);
|
|
376
|
+
logRolloutChangeIfDetected(entry.thread.id, rolloutSnapshotBefore, rolloutSnapshotAfter);
|
|
421
377
|
}
|
|
422
378
|
|
|
423
379
|
await zipExportDirectory(bundleDirectory, zipPath);
|
|
@@ -7,6 +7,10 @@ import { getFileFingerprint, hashCacheKeyParts, withCachedJson } from './ui-cach
|
|
|
7
7
|
export const LARGE_THREAD_SIZE_BYTES = 100 * 1024 * 1024;
|
|
8
8
|
export const LARGE_THREAD_PREVIEW_EVENT_LIMIT = 200;
|
|
9
9
|
|
|
10
|
+
const isMissingFileError = (error: unknown) => {
|
|
11
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
12
|
+
};
|
|
13
|
+
|
|
10
14
|
export const getCachedParsedCodexTranscript = async (sessionFile: string): Promise<ParsedCodexTranscript> => {
|
|
11
15
|
const fingerprint = await getFileFingerprint(sessionFile);
|
|
12
16
|
const key = `thread-${hashCacheKeyParts(path.basename(sessionFile), fingerprint)}`;
|
|
@@ -23,7 +27,19 @@ export const getThreadRolloutLoadState = async (
|
|
|
23
27
|
sessionFile: string,
|
|
24
28
|
largeTranscriptThresholdBytes = LARGE_THREAD_SIZE_BYTES,
|
|
25
29
|
) => {
|
|
26
|
-
|
|
30
|
+
let metadata: Awaited<ReturnType<typeof stat>>;
|
|
31
|
+
try {
|
|
32
|
+
metadata = await stat(sessionFile);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (isMissingFileError(error)) {
|
|
35
|
+
return {
|
|
36
|
+
fileSizeBytes: null,
|
|
37
|
+
shouldDeferTranscriptLoad: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
27
43
|
|
|
28
44
|
return {
|
|
29
45
|
fileSizeBytes: metadata.size,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const mapWithConcurrency = async <T, TResult>(
|
|
2
|
+
values: T[],
|
|
3
|
+
limit: number,
|
|
4
|
+
mapper: (value: T, index: number) => Promise<TResult>,
|
|
5
|
+
) => {
|
|
6
|
+
if (values.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const requestedLimit = Number.isFinite(limit) ? Math.floor(limit) : 1;
|
|
11
|
+
const workerLimit = Math.max(1, requestedLimit);
|
|
12
|
+
const results = new Array<TResult>(values.length);
|
|
13
|
+
let nextIndex = 0;
|
|
14
|
+
|
|
15
|
+
const worker = async () => {
|
|
16
|
+
while (nextIndex < values.length) {
|
|
17
|
+
const currentIndex = nextIndex;
|
|
18
|
+
nextIndex += 1;
|
|
19
|
+
results[currentIndex] = await mapper(values[currentIndex]!, currentIndex);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await Promise.all(Array.from({ length: Math.min(workerLimit, values.length) }, () => worker()));
|
|
24
|
+
return results;
|
|
25
|
+
};
|
package/src/lib/cursor-db.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { constants, Database } from 'bun:sqlite';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import {
|
|
@@ -109,13 +110,29 @@ const normalizeCursorPath = (value: string): string => {
|
|
|
109
110
|
return decoded.replace(/\/+$/u, '') || decoded;
|
|
110
111
|
};
|
|
111
112
|
|
|
113
|
+
const warnCursorDataIssue = (event: string, details: Record<string, unknown>) => {
|
|
114
|
+
console.warn(`[spiracha:cursor] ${event}`, details);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const stripJsonComments = (value: string): string => {
|
|
118
|
+
return value.replace(/\/\*[\s\S]*?\*\//gu, '').replace(/(^|\s)\/\/.*$/gmu, '$1');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const parseCodeWorkspaceJson = (text: string): { folders?: Array<{ path?: string }> } => {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(text) as { folders?: Array<{ path?: string }> };
|
|
124
|
+
} catch {
|
|
125
|
+
return JSON.parse(stripJsonComments(text)) as { folders?: Array<{ path?: string }> };
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
112
129
|
const parseCodeWorkspaceFolders = async (workspaceFilePath: string): Promise<string[]> => {
|
|
113
130
|
if (!workspaceFilePath.endsWith('.code-workspace')) {
|
|
114
131
|
return [];
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
try {
|
|
118
|
-
const data = (await Bun.file(workspaceFilePath).
|
|
135
|
+
const data = parseCodeWorkspaceJson(await Bun.file(workspaceFilePath).text());
|
|
119
136
|
const folders: string[] = [];
|
|
120
137
|
for (const entry of data.folders ?? []) {
|
|
121
138
|
const folderPath = entry.path;
|
|
@@ -131,18 +148,30 @@ const parseCodeWorkspaceFolders = async (workspaceFilePath: string): Promise<str
|
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
return folders;
|
|
134
|
-
} catch {
|
|
151
|
+
} catch (error) {
|
|
152
|
+
warnCursorDataIssue('invalid_code_workspace_json', {
|
|
153
|
+
error: error instanceof Error ? error.message : String(error),
|
|
154
|
+
workspaceFilePath,
|
|
155
|
+
});
|
|
135
156
|
return [];
|
|
136
157
|
}
|
|
137
158
|
};
|
|
138
159
|
|
|
139
160
|
export const loadGlobalComposerHeaders = (globalDbPath: string): ComposerEntry[] => {
|
|
140
|
-
const db = openCursorReadonlyDb(globalDbPath);
|
|
141
161
|
try {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
const db = openCursorReadonlyDb(globalDbPath);
|
|
163
|
+
try {
|
|
164
|
+
const data = readItemValue<{ allComposers?: ComposerEntry[] }>(db, COMPOSER_HEADERS_KEY);
|
|
165
|
+
return data?.allComposers ?? [];
|
|
166
|
+
} finally {
|
|
167
|
+
db.close();
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
warnCursorDataIssue('global_composer_headers_unavailable', {
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
globalDbPath,
|
|
173
|
+
});
|
|
174
|
+
return [];
|
|
146
175
|
}
|
|
147
176
|
};
|
|
148
177
|
|
|
@@ -1055,6 +1084,274 @@ const isRenderableBubble = (bubble: CursorBubble): boolean => {
|
|
|
1055
1084
|
return Boolean(bubble.text.trim() || bubble.thinking?.trim() || bubble.toolCall);
|
|
1056
1085
|
};
|
|
1057
1086
|
|
|
1087
|
+
const normalizeBubbleText = (value: string | null): string => {
|
|
1088
|
+
return (value ?? '').replace(/\s+/gu, ' ').trim();
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
const hashText = (value: string) => createHash('sha1').update(value).digest('hex').slice(0, 12);
|
|
1092
|
+
|
|
1093
|
+
const stableStringifyJson = (value: JsonValue): string => {
|
|
1094
|
+
if (Array.isArray(value)) {
|
|
1095
|
+
return `[${value.map(stableStringifyJson).join(',')}]`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (value && typeof value === 'object') {
|
|
1099
|
+
return `{${Object.keys(value)
|
|
1100
|
+
.sort()
|
|
1101
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringifyJson((value as Record<string, JsonValue>)[key]!)}`)
|
|
1102
|
+
.join(',')}}`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return JSON.stringify(value);
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const normalizeToolArgumentsText = (value: string | null): string => {
|
|
1109
|
+
if (!value?.trim()) {
|
|
1110
|
+
return '';
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
return stableStringifyJson(JSON.parse(value) as JsonValue);
|
|
1115
|
+
} catch {
|
|
1116
|
+
return normalizeBubbleText(value);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
const hasRenderableTextSuperset = (left: string | null, right: string | null): boolean => {
|
|
1121
|
+
const normalizedLeft = normalizeBubbleText(left);
|
|
1122
|
+
const normalizedRight = normalizeBubbleText(right);
|
|
1123
|
+
if (!normalizedLeft && !normalizedRight) {
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return (
|
|
1128
|
+
normalizedLeft === normalizedRight ||
|
|
1129
|
+
normalizedLeft.startsWith(normalizedRight) ||
|
|
1130
|
+
normalizedRight.startsWith(normalizedLeft)
|
|
1131
|
+
);
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const haveSameToolIdentity = (left: CursorToolCall | null, right: CursorToolCall | null): boolean => {
|
|
1135
|
+
if (!left && !right) {
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (!left || !right) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return (
|
|
1144
|
+
left.name === right.name &&
|
|
1145
|
+
normalizeToolArgumentsText(left.argumentsText) === normalizeToolArgumentsText(right.argumentsText)
|
|
1146
|
+
);
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const areEquivalentBubbles = (left: CursorBubble, right: CursorBubble): boolean => {
|
|
1150
|
+
return (
|
|
1151
|
+
left.kind === right.kind &&
|
|
1152
|
+
hasRenderableTextSuperset(left.text, right.text) &&
|
|
1153
|
+
Boolean(normalizeBubbleText(left.thinking)) === Boolean(normalizeBubbleText(right.thinking)) &&
|
|
1154
|
+
haveSameToolIdentity(left.toolCall, right.toolCall)
|
|
1155
|
+
);
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const findAgentTailStartIndex = (existingBubbles: CursorBubble[], agentBubbles: CursorBubble[]): number => {
|
|
1159
|
+
const maxOverlap = Math.min(existingBubbles.length, agentBubbles.length);
|
|
1160
|
+
// Agent transcript files contain the complete run while SQLite may lag or truncate the tail.
|
|
1161
|
+
// Match the longest SQLite suffix to the agent prefix, then append only the remaining agent tail.
|
|
1162
|
+
for (let overlapLength = maxOverlap; overlapLength > 0; overlapLength -= 1) {
|
|
1163
|
+
const existingStart = existingBubbles.length - overlapLength;
|
|
1164
|
+
const matches = agentBubbles
|
|
1165
|
+
.slice(0, overlapLength)
|
|
1166
|
+
.every((bubble, index) => areEquivalentBubbles(existingBubbles[existingStart + index]!, bubble));
|
|
1167
|
+
if (matches) {
|
|
1168
|
+
return overlapLength;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return 0;
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const hasEquivalentBubble = (bubbles: CursorBubble[], candidate: CursorBubble): boolean => {
|
|
1176
|
+
return bubbles.some((bubble) => areEquivalentBubbles(bubble, candidate));
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
const getAgentTranscriptContentParts = (entry: Record<string, JsonValue>): Record<string, JsonValue>[] => {
|
|
1180
|
+
const message = asObject(entry.message ?? null);
|
|
1181
|
+
const content = message?.content ?? entry.content ?? null;
|
|
1182
|
+
if (Array.isArray(content)) {
|
|
1183
|
+
return content.map((part) => asObject(part)).filter((part): part is Record<string, JsonValue> => Boolean(part));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (typeof content === 'string') {
|
|
1187
|
+
return [{ text: content, type: 'text' }];
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return [];
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const parseAgentTranscriptToolCall = (parts: Record<string, JsonValue>[]): CursorToolCall | null => {
|
|
1194
|
+
const toolUse = parts.find((part) => asString(part.type ?? null) === 'tool_use');
|
|
1195
|
+
if (!toolUse) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const name = asString(toolUse.name ?? null);
|
|
1200
|
+
if (!name) {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return {
|
|
1205
|
+
argumentsText: toolUse.input === undefined ? null : JSON.stringify(toolUse.input),
|
|
1206
|
+
callId: asString(toolUse.id ?? null),
|
|
1207
|
+
name,
|
|
1208
|
+
resultText: null,
|
|
1209
|
+
status: null,
|
|
1210
|
+
};
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
const parseAgentTranscriptBubble = (
|
|
1214
|
+
filePath: string,
|
|
1215
|
+
lineNumber: number,
|
|
1216
|
+
raw: Record<string, JsonValue>,
|
|
1217
|
+
): CursorBubble | null => {
|
|
1218
|
+
const message = asObject(raw.message ?? null);
|
|
1219
|
+
const role = asString(raw.role ?? message?.role ?? null);
|
|
1220
|
+
const kind = role === 'user' || role === 'assistant' ? role : 'unknown';
|
|
1221
|
+
const parts = getAgentTranscriptContentParts(raw);
|
|
1222
|
+
const text = parts
|
|
1223
|
+
.filter((part) => asString(part.type ?? null) === 'text')
|
|
1224
|
+
.map((part) => asString(part.text ?? null))
|
|
1225
|
+
.filter((value): value is string => Boolean(value?.trim()))
|
|
1226
|
+
.join('\n\n');
|
|
1227
|
+
const toolCall = parseAgentTranscriptToolCall(parts);
|
|
1228
|
+
const bubble: CursorBubble = {
|
|
1229
|
+
bubbleId: `agent-transcript:${hashText(path.resolve(filePath))}:${path.basename(filePath)}:${lineNumber}`,
|
|
1230
|
+
createdAtMs: null,
|
|
1231
|
+
kind,
|
|
1232
|
+
text,
|
|
1233
|
+
thinking: null,
|
|
1234
|
+
toolCall,
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
return isRenderableBubble(bubble) ? bubble : null;
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
const readCursorAgentTranscriptFile = async (filePath: string): Promise<CursorBubble[]> => {
|
|
1241
|
+
let text = '';
|
|
1242
|
+
try {
|
|
1243
|
+
text = await Bun.file(filePath).text();
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
warnCursorDataIssue('agent_transcript_unreadable', {
|
|
1246
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1247
|
+
filePath,
|
|
1248
|
+
});
|
|
1249
|
+
return [];
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const bubbles: CursorBubble[] = [];
|
|
1253
|
+
for (const [index, line] of text.split(/\n/u).entries()) {
|
|
1254
|
+
if (!line.trim()) {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
let raw: JsonValue;
|
|
1259
|
+
try {
|
|
1260
|
+
raw = JSON.parse(line) as JsonValue;
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
warnCursorDataIssue('invalid_agent_transcript_jsonl', {
|
|
1263
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1264
|
+
filePath,
|
|
1265
|
+
lineNumber: index + 1,
|
|
1266
|
+
});
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const entry = asObject(raw);
|
|
1271
|
+
if (!entry) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const bubble = parseAgentTranscriptBubble(filePath, index + 1, entry);
|
|
1276
|
+
if (bubble) {
|
|
1277
|
+
bubbles.push(bubble);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return bubbles;
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const listCursorAgentTranscriptFiles = async (transcriptDir: string, composerId: string): Promise<string[]> => {
|
|
1285
|
+
const preferred = path.join(transcriptDir, `${composerId}.jsonl`);
|
|
1286
|
+
const files = new Set<string>();
|
|
1287
|
+
if (await pathExists(preferred)) {
|
|
1288
|
+
files.add(preferred);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
let entries: string[] = [];
|
|
1292
|
+
try {
|
|
1293
|
+
entries = await readdir(transcriptDir);
|
|
1294
|
+
} catch {
|
|
1295
|
+
return [...files];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
for (const entry of entries) {
|
|
1299
|
+
if (entry.endsWith('.jsonl')) {
|
|
1300
|
+
files.add(path.join(transcriptDir, entry));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return [...files].sort();
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const readCursorAgentTranscriptBubbles = async (composerId: string, userDir: string): Promise<CursorBubble[]> => {
|
|
1308
|
+
const transcriptDirs = await findCursorTranscriptDirs(composerId, userDir);
|
|
1309
|
+
const bubbles: CursorBubble[] = [];
|
|
1310
|
+
for (const transcriptDir of transcriptDirs.sort()) {
|
|
1311
|
+
const files = await listCursorAgentTranscriptFiles(transcriptDir, composerId);
|
|
1312
|
+
for (const file of files) {
|
|
1313
|
+
bubbles.push(...(await readCursorAgentTranscriptFile(file)));
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return bubbles;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
const mergeAgentTranscriptTail = (
|
|
1321
|
+
transcript: CursorThreadTranscript,
|
|
1322
|
+
agentBubbles: CursorBubble[],
|
|
1323
|
+
): CursorThreadTranscript => {
|
|
1324
|
+
const tailStartIndex = findAgentTailStartIndex(transcript.bubbles, agentBubbles);
|
|
1325
|
+
const seen = [...transcript.bubbles];
|
|
1326
|
+
const appended: CursorBubble[] = [];
|
|
1327
|
+
const candidates = agentBubbles.slice(tailStartIndex);
|
|
1328
|
+
for (const bubble of candidates) {
|
|
1329
|
+
if (hasEquivalentBubble(seen, bubble)) {
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
seen.push(bubble);
|
|
1334
|
+
appended.push(bubble);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (appended.length === 0) {
|
|
1338
|
+
return transcript;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return {
|
|
1342
|
+
...transcript,
|
|
1343
|
+
bubbles: [...transcript.bubbles, ...appended],
|
|
1344
|
+
renderableBubbleCount: transcript.renderableBubbleCount + appended.length,
|
|
1345
|
+
};
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const inferCursorUserDirFromGlobalDbPath = (globalDbPath: string): string => {
|
|
1349
|
+
const globalStorageDir = path.dirname(globalDbPath);
|
|
1350
|
+
return path.basename(globalStorageDir) === 'globalStorage'
|
|
1351
|
+
? path.dirname(globalStorageDir)
|
|
1352
|
+
: resolveCursorUserDir();
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1058
1355
|
export const readCursorThreadTranscript = (globalDbPath: string, composerId: string): CursorThreadTranscript | null => {
|
|
1059
1356
|
const head = readCursorThreadHead(globalDbPath, composerId);
|
|
1060
1357
|
if (!head) {
|
|
@@ -1087,6 +1384,20 @@ export const readCursorThreadTranscript = (globalDbPath: string, composerId: str
|
|
|
1087
1384
|
}
|
|
1088
1385
|
};
|
|
1089
1386
|
|
|
1387
|
+
export const readCursorThreadTranscriptWithAgentFiles = async (
|
|
1388
|
+
globalDbPath: string,
|
|
1389
|
+
composerId: string,
|
|
1390
|
+
userDir = inferCursorUserDirFromGlobalDbPath(globalDbPath),
|
|
1391
|
+
): Promise<CursorThreadTranscript | null> => {
|
|
1392
|
+
const transcript = readCursorThreadTranscript(globalDbPath, composerId);
|
|
1393
|
+
if (!transcript) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const agentBubbles = await readCursorAgentTranscriptBubbles(composerId, userDir);
|
|
1398
|
+
return mergeAgentTranscriptTail(transcript, agentBubbles);
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1090
1401
|
const readAllBubbleIds = (db: Database, composerId: string): string[] => {
|
|
1091
1402
|
const prefix = `bubbleId:${composerId}:`;
|
|
1092
1403
|
const rows = db.query('SELECT key FROM cursorDiskKV WHERE key LIKE ? ORDER BY key ASC').all(`${prefix}%`) as Array<{
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
listCursorThreadsForGroup,
|
|
5
5
|
listCursorWorkspaceGroups,
|
|
6
6
|
readCursorThreadHead,
|
|
7
|
-
|
|
7
|
+
readCursorThreadTranscriptWithAgentFiles,
|
|
8
8
|
} from './cursor-db';
|
|
9
9
|
import {
|
|
10
10
|
type CursorCliOptions,
|
|
@@ -115,7 +115,7 @@ const exportSingleThread = async (
|
|
|
115
115
|
outputDir: string,
|
|
116
116
|
options: CursorCliOptions,
|
|
117
117
|
): Promise<CursorExportedFile | null> => {
|
|
118
|
-
const transcript =
|
|
118
|
+
const transcript = await readCursorThreadTranscriptWithAgentFiles(globalDbPath, thread.composerId, options.userDir);
|
|
119
119
|
if (!transcript) {
|
|
120
120
|
return null;
|
|
121
121
|
}
|