spiracha 1.5.0 → 2.0.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.
Files changed (365) hide show
  1. package/AGENTS.md +114 -205
  2. package/README.md +89 -269
  3. package/apps/ui/AGENTS.md +17 -7
  4. package/apps/ui/README.md +34 -8
  5. package/apps/ui/components.json +21 -0
  6. package/apps/ui/package.json +65 -0
  7. package/apps/ui/src/components/antigravity-conversations-table.tsx +206 -0
  8. package/apps/ui/src/components/antigravity-keychain-panel.tsx +76 -0
  9. package/apps/ui/src/components/antigravity-workspaces-table.tsx +63 -0
  10. package/apps/ui/src/components/app-shell.tsx +124 -0
  11. package/apps/ui/src/components/breadcrumbs.tsx +53 -0
  12. package/apps/ui/src/components/claude-code-sessions-table.tsx +114 -0
  13. package/apps/ui/src/components/claude-code-workspaces-table.tsx +55 -0
  14. package/apps/ui/src/components/cursor-threads-table.tsx +189 -0
  15. package/apps/ui/src/components/cursor-workspaces-table.tsx +140 -0
  16. package/apps/ui/src/components/data-table.tsx +241 -0
  17. package/apps/ui/src/components/delete-confirm-dialog.tsx +88 -0
  18. package/apps/ui/src/components/export-dialog.tsx +184 -0
  19. package/apps/ui/src/components/json-panel.tsx +17 -0
  20. package/apps/ui/src/components/kiro-sessions-table.tsx +112 -0
  21. package/apps/ui/src/components/kiro-workspaces-table.tsx +53 -0
  22. package/apps/ui/src/components/list-search-input.tsx +18 -0
  23. package/apps/ui/src/components/loading-panel.tsx +23 -0
  24. package/apps/ui/src/components/metadata-section.tsx +31 -0
  25. package/apps/ui/src/components/metric-card.tsx +19 -0
  26. package/apps/ui/src/components/opencode-sessions-table.tsx +128 -0
  27. package/apps/ui/src/components/opencode-workspaces-table.tsx +57 -0
  28. package/apps/ui/src/components/page-header.tsx +33 -0
  29. package/apps/ui/src/components/projects-loading-state.tsx +74 -0
  30. package/apps/ui/src/components/projects-table.tsx +108 -0
  31. package/apps/ui/src/components/qoder-sessions-table.tsx +112 -0
  32. package/apps/ui/src/components/qoder-workspaces-table.tsx +53 -0
  33. package/apps/ui/src/components/recent-threads-list.tsx +52 -0
  34. package/apps/ui/src/components/reload-error-panel.tsx +20 -0
  35. package/apps/ui/src/components/text-document-panel.tsx +19 -0
  36. package/apps/ui/src/components/theme-toggle.tsx +48 -0
  37. package/apps/ui/src/components/threads-table.tsx +202 -0
  38. package/apps/ui/src/components/transcript-view.tsx +552 -0
  39. package/apps/ui/src/components/ui/alert-dialog.tsx +160 -0
  40. package/apps/ui/src/components/ui/badge.tsx +45 -0
  41. package/apps/ui/src/components/ui/button.tsx +62 -0
  42. package/apps/ui/src/components/ui/checkbox.tsx +29 -0
  43. package/apps/ui/src/components/ui/dialog.tsx +137 -0
  44. package/apps/ui/src/components/ui/dropdown-menu.tsx +217 -0
  45. package/apps/ui/src/components/ui/input.tsx +21 -0
  46. package/apps/ui/src/components/ui/scroll-area.tsx +46 -0
  47. package/apps/ui/src/components/ui/select.tsx +163 -0
  48. package/apps/ui/src/components/ui/separator.tsx +28 -0
  49. package/apps/ui/src/components/ui/sheet.tsx +107 -0
  50. package/apps/ui/src/components/ui/skeleton.tsx +7 -0
  51. package/apps/ui/src/components/ui/table.tsx +76 -0
  52. package/apps/ui/src/components/ui/tabs.tsx +71 -0
  53. package/apps/ui/src/components/ui/tooltip.tsx +44 -0
  54. package/apps/ui/src/integrations/tanstack-query/devtools.tsx +6 -0
  55. package/apps/ui/src/integrations/tanstack-query/root-provider.tsx +10 -0
  56. package/apps/ui/src/lib/antigravity-conversation-state.ts +28 -0
  57. package/apps/ui/src/lib/antigravity-queries.ts +33 -0
  58. package/apps/ui/src/lib/antigravity-server.ts +128 -0
  59. package/apps/ui/src/lib/antigravity-transcript-events.ts +335 -0
  60. package/apps/ui/src/lib/claude-code-queries.ts +26 -0
  61. package/apps/ui/src/lib/claude-code-server.ts +75 -0
  62. package/apps/ui/src/lib/claude-code-transcript-events.ts +154 -0
  63. package/apps/ui/src/lib/codex-queries.ts +66 -0
  64. package/apps/ui/src/lib/codex-server.ts +209 -0
  65. package/apps/ui/src/lib/cursor-queries.ts +37 -0
  66. package/apps/ui/src/lib/cursor-server.ts +272 -0
  67. package/apps/ui/src/lib/cursor-transcript-events.ts +229 -0
  68. package/apps/ui/src/lib/download.ts +166 -0
  69. package/apps/ui/src/lib/formatters.ts +149 -0
  70. package/apps/ui/src/lib/kiro-queries.ts +22 -0
  71. package/apps/ui/src/lib/kiro-server.ts +73 -0
  72. package/apps/ui/src/lib/kiro-transcript-events.ts +113 -0
  73. package/apps/ui/src/lib/opencode-queries.ts +37 -0
  74. package/apps/ui/src/lib/opencode-server.ts +73 -0
  75. package/apps/ui/src/lib/opencode-transcript-events.ts +221 -0
  76. package/apps/ui/src/lib/package-metadata.ts +16 -0
  77. package/apps/ui/src/lib/path-utils.ts +13 -0
  78. package/apps/ui/src/lib/qoder-queries.ts +22 -0
  79. package/apps/ui/src/lib/qoder-server.ts +79 -0
  80. package/apps/ui/src/lib/qoder-transcript-events.ts +139 -0
  81. package/apps/ui/src/lib/route-search.ts +68 -0
  82. package/apps/ui/src/lib/settings-store.tsx +59 -0
  83. package/apps/ui/src/lib/source-session-export-server.ts +56 -0
  84. package/apps/ui/src/lib/text-filter.ts +22 -0
  85. package/apps/ui/src/lib/thread-id.ts +3 -0
  86. package/apps/ui/src/lib/thread-transcript-stats.ts +57 -0
  87. package/apps/ui/src/lib/utils.ts +7 -0
  88. package/apps/ui/src/routeTree.gen.ts +730 -0
  89. package/apps/ui/src/router.tsx +20 -0
  90. package/apps/ui/src/routes/$threadId.tsx +22 -0
  91. package/apps/ui/src/routes/__root.tsx +127 -0
  92. package/apps/ui/src/routes/analytics.tsx +143 -0
  93. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +438 -0
  94. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +127 -0
  95. package/apps/ui/src/routes/antigravity.index.tsx +51 -0
  96. package/apps/ui/src/routes/api.v1.conversation-query.ts +12 -0
  97. package/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts +12 -0
  98. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +12 -0
  99. package/apps/ui/src/routes/api.v1.conversations.ts +12 -0
  100. package/apps/ui/src/routes/api.v1.resolve.ts +12 -0
  101. package/apps/ui/src/routes/api.v1.sources.ts +12 -0
  102. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +342 -0
  103. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +135 -0
  104. package/apps/ui/src/routes/claude-code.index.tsx +48 -0
  105. package/apps/ui/src/routes/codex.$project.tsx +390 -0
  106. package/apps/ui/src/routes/codex.index.tsx +116 -0
  107. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +435 -0
  108. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +416 -0
  109. package/apps/ui/src/routes/cursor.index.tsx +120 -0
  110. package/apps/ui/src/routes/index.tsx +124 -0
  111. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +345 -0
  112. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +136 -0
  113. package/apps/ui/src/routes/kiro.index.tsx +48 -0
  114. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +334 -0
  115. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +135 -0
  116. package/apps/ui/src/routes/opencode.index.tsx +48 -0
  117. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +341 -0
  118. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +139 -0
  119. package/apps/ui/src/routes/qoder.index.tsx +48 -0
  120. package/apps/ui/src/routes/settings.tsx +91 -0
  121. package/apps/ui/src/routes/threads.$threadId.tsx +619 -0
  122. package/apps/ui/src/styles.css +122 -0
  123. package/apps/ui/tsconfig.json +29 -0
  124. package/apps/ui/vite.config.ts +84 -0
  125. package/bin/spiracha.ts +32 -0
  126. package/package.json +46 -73
  127. package/src/client.ts +290 -0
  128. package/src/lib/antigravity-db.ts +213 -19
  129. package/src/lib/antigravity-exporter-types.ts +1 -0
  130. package/src/lib/codex-analytics.ts +1 -1
  131. package/src/lib/codex-browser-db.ts +410 -74
  132. package/src/lib/codex-browser-export.ts +6 -14
  133. package/src/lib/codex-browser-types.ts +1 -1
  134. package/src/lib/{codex-exporter-types.ts → codex-thread-types.ts} +2 -30
  135. package/src/lib/{codex-exporter-transcript.ts → codex-transcript-renderer.ts} +64 -28
  136. package/src/lib/conversation-api.ts +792 -0
  137. package/src/lib/conversation-data/adapter-helpers.ts +110 -0
  138. package/src/lib/conversation-data/antigravity-adapter.ts +148 -0
  139. package/src/lib/conversation-data/claude-code-adapter.ts +175 -0
  140. package/src/lib/conversation-data/codex-adapter.ts +245 -0
  141. package/src/lib/conversation-data/cursor-adapter.ts +171 -0
  142. package/src/lib/conversation-data/index.ts +311 -0
  143. package/src/lib/conversation-data/kiro-adapter.ts +140 -0
  144. package/src/lib/conversation-data/message-selector.ts +37 -0
  145. package/src/lib/conversation-data/opencode-adapter.ts +180 -0
  146. package/src/lib/conversation-data/path-match.ts +79 -0
  147. package/src/lib/conversation-data/qoder-adapter.ts +230 -0
  148. package/src/lib/conversation-data/types.ts +116 -0
  149. package/src/lib/cursor-db.ts +67 -21
  150. package/src/lib/cursor-exporter-types.ts +0 -19
  151. package/src/lib/qoder-acp-client.ts +268 -0
  152. package/src/lib/qoder-db.ts +1601 -0
  153. package/src/lib/qoder-exporter-types.ts +114 -0
  154. package/src/lib/qoder-transcript-phase.ts +41 -0
  155. package/src/lib/qoder-transcript.ts +112 -0
  156. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
  157. package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +0 -1
  158. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DqySVO2K.js +0 -7
  159. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +0 -1
  160. package/apps/ui/dist/client/assets/antigravity-keychain-panel-B77qJz6f.js +0 -1
  161. package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +0 -1
  162. package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
  163. package/apps/ui/dist/client/assets/antigravity.index-DP9JC2P9.js +0 -1
  164. package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
  165. package/apps/ui/dist/client/assets/badge-C3i4OKs8.js +0 -1
  166. package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
  167. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +0 -1
  168. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +0 -1
  169. package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +0 -1
  170. package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +0 -1
  171. package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +0 -1
  172. package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +0 -1
  173. package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.js +0 -3
  174. package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +0 -1
  175. package/apps/ui/dist/client/assets/cursor-threads._composerId-hBWlP1D-.js +0 -1
  176. package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +0 -1
  177. package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
  178. package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +0 -2
  179. package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
  180. package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
  181. package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +0 -7
  182. package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
  183. package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +0 -5
  184. package/apps/ui/dist/client/assets/dist-Cmd3AIfD.js +0 -1
  185. package/apps/ui/dist/client/assets/dist-DvMS2965.js +0 -1
  186. package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +0 -1
  187. package/apps/ui/dist/client/assets/dropdown-menu-D5cvPaZt.js +0 -1
  188. package/apps/ui/dist/client/assets/es2015-ad6040dw.js +0 -41
  189. package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +0 -1
  190. package/apps/ui/dist/client/assets/formatters-BtqyrX_c.js +0 -1
  191. package/apps/ui/dist/client/assets/index-C-STgIH4.js +0 -166
  192. package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
  193. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +0 -1
  194. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +0 -1
  195. package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +0 -1
  196. package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +0 -1
  197. package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +0 -1
  198. package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +0 -1
  199. package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
  200. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +0 -1
  201. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +0 -8
  202. package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +0 -1
  203. package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +0 -1
  204. package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +0 -1
  205. package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +0 -1
  206. package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
  207. package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +0 -1
  208. package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +0 -1
  209. package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
  210. package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +0 -3
  211. package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
  212. package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +0 -1
  213. package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +0 -1
  214. package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +0 -1
  215. package/apps/ui/dist/client/assets/routes-DnzF_9CX.js +0 -1
  216. package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
  217. package/apps/ui/dist/client/assets/select-CYF6pF37.js +0 -1
  218. package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +0 -1
  219. package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
  220. package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +0 -1
  221. package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +0 -7
  222. package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
  223. package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +0 -1
  224. package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
  225. package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +0 -1
  226. package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +0 -1
  227. package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +0 -1
  228. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +0 -387
  229. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +0 -6
  230. package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +0 -16
  231. package/apps/ui/dist/server/assets/analytics-CtpOMVMO.js +0 -146
  232. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +0 -16
  233. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BKOyL2T6.js +0 -613
  234. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +0 -11
  235. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-eS87Dx7g.js +0 -20
  236. package/apps/ui/dist/server/assets/antigravity-db-Bh8_U9uw.js +0 -580
  237. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +0 -126
  238. package/apps/ui/dist/server/assets/antigravity-keychain-panel-CC7FLmAK.js +0 -55
  239. package/apps/ui/dist/server/assets/antigravity-queries-CGrJO9Vr.js +0 -37
  240. package/apps/ui/dist/server/assets/antigravity-server-CHRjVFqN.js +0 -114
  241. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +0 -11
  242. package/apps/ui/dist/server/assets/antigravity._workspaceKey-B0QeYUOh.js +0 -28
  243. package/apps/ui/dist/server/assets/antigravity._workspaceKey-C3kP-qyN.js +0 -210
  244. package/apps/ui/dist/server/assets/antigravity.index-CPCYkqCi.js +0 -104
  245. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +0 -11
  246. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +0 -26
  247. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +0 -46
  248. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +0 -19
  249. package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +0 -361
  250. package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +0 -37
  251. package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +0 -72
  252. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +0 -18
  253. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +0 -473
  254. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +0 -11
  255. package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +0 -139
  256. package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +0 -10
  257. package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +0 -28
  258. package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +0 -11
  259. package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +0 -191
  260. package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +0 -92
  261. package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +0 -11
  262. package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
  263. package/apps/ui/dist/server/assets/codex-server-CGXU4H8-.js +0 -2294
  264. package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +0 -18
  265. package/apps/ui/dist/server/assets/createServerRpc-CF_DEwnm.js +0 -12
  266. package/apps/ui/dist/server/assets/createSsrRpc-C754NPki.js +0 -16
  267. package/apps/ui/dist/server/assets/cursor-db-CZnYy7aT.js +0 -830
  268. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +0 -34
  269. package/apps/ui/dist/server/assets/cursor-queries-NCIM0Nat.js +0 -67
  270. package/apps/ui/dist/server/assets/cursor-recovery-CXpO8KcD.js +0 -361
  271. package/apps/ui/dist/server/assets/cursor-server-DNu52A3K.js +0 -213
  272. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +0 -11
  273. package/apps/ui/dist/server/assets/cursor-threads._composerId-Ckn6ItHI.js +0 -582
  274. package/apps/ui/dist/server/assets/cursor-threads._composerId-p9-ARAzc.js +0 -18
  275. package/apps/ui/dist/server/assets/cursor-transcript-B7tGnqtn.js +0 -125
  276. package/apps/ui/dist/server/assets/cursor._workspaceKey-CZuohIUL.js +0 -28
  277. package/apps/ui/dist/server/assets/cursor._workspaceKey-DPp5JSjy.js +0 -401
  278. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +0 -11
  279. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +0 -11
  280. package/apps/ui/dist/server/assets/cursor.index-DkWT-mxw.js +0 -189
  281. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +0 -189
  282. package/apps/ui/dist/server/assets/delete-confirm-dialog-BcBNCxWB.js +0 -144
  283. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +0 -92
  284. package/apps/ui/dist/server/assets/dropdown-menu-Dy_9t6TN.js +0 -36
  285. package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +0 -5
  286. package/apps/ui/dist/server/assets/export-dialog-D88ze9Gy.js +0 -240
  287. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
  288. package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +0 -552
  289. package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +0 -37
  290. package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +0 -72
  291. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +0 -11
  292. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +0 -449
  293. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +0 -18
  294. package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +0 -112
  295. package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +0 -27
  296. package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +0 -11
  297. package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +0 -28
  298. package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +0 -193
  299. package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +0 -11
  300. package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +0 -93
  301. package/apps/ui/dist/server/assets/loading-panel-BGFnWseS.js +0 -27
  302. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +0 -23
  303. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +0 -13
  304. package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +0 -397
  305. package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +0 -50
  306. package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +0 -72
  307. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +0 -518
  308. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +0 -18
  309. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +0 -11
  310. package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +0 -87
  311. package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +0 -141
  312. package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +0 -32
  313. package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +0 -11
  314. package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +0 -28
  315. package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +0 -207
  316. package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +0 -11
  317. package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +0 -93
  318. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +0 -29
  319. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +0 -31
  320. package/apps/ui/dist/server/assets/projects._project-4io5LO0E.js +0 -20
  321. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
  322. package/apps/ui/dist/server/assets/projects._project-D6ZoGhJ8.js +0 -395
  323. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +0 -12
  324. package/apps/ui/dist/server/assets/projects.index-BkLiF2FF.js +0 -182
  325. package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +0 -14
  326. package/apps/ui/dist/server/assets/reload-error-panel-BJMxY3U1.js +0 -25
  327. package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +0 -38
  328. package/apps/ui/dist/server/assets/router-Sac2DGk6.js +0 -518
  329. package/apps/ui/dist/server/assets/routes-Bsact1uB.js +0 -184
  330. package/apps/ui/dist/server/assets/routes-BzkJgq4W.js +0 -34
  331. package/apps/ui/dist/server/assets/select-GW76p-ld.js +0 -76
  332. package/apps/ui/dist/server/assets/settings-OayxIYQQ.js +0 -100
  333. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +0 -52
  334. package/apps/ui/dist/server/assets/shared-DyhChtHf.js +0 -137
  335. package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +0 -38
  336. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
  337. package/apps/ui/dist/server/assets/start-B_HFwkkk.js +0 -4
  338. package/apps/ui/dist/server/assets/tabs-DkaChNVg.js +0 -501
  339. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
  340. package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +0 -45
  341. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
  342. package/apps/ui/dist/server/assets/threads._threadId-BeXEc4Lo.js +0 -648
  343. package/apps/ui/dist/server/assets/threads._threadId-Ci0S4mKU.js +0 -18
  344. package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +0 -83
  345. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +0 -8
  346. package/apps/ui/dist/server/server.js +0 -5895
  347. package/bin/codex-chats-claude.js +0 -5
  348. package/bin/codex-chats.js +0 -5
  349. package/bin/spiracha.js +0 -5
  350. package/src/export-chats.ts +0 -120
  351. package/src/export-claude.ts +0 -36
  352. package/src/export-cursor.ts +0 -244
  353. package/src/lib/claude-exporter.ts +0 -864
  354. package/src/lib/codex-exporter-cli.ts +0 -277
  355. package/src/lib/codex-exporter-db.ts +0 -319
  356. package/src/lib/codex-exporter.ts +0 -115
  357. package/src/lib/cursor-exporter.ts +0 -266
  358. package/src/lib/interactive-cli.ts +0 -433
  359. package/src/lib/native-open.ts +0 -54
  360. package/src/mcp-server.ts +0 -137
  361. package/src/spiracha.ts +0 -116
  362. package/src/ui-cli.ts +0 -310
  363. /package/apps/ui/{dist/client → public}/icon.svg +0 -0
  364. /package/apps/ui/{dist/client → public}/manifest.json +0 -0
  365. /package/apps/ui/{dist/client → public}/robots.txt +0 -0
@@ -0,0 +1,272 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {
6
+ getExportMimeType,
7
+ resolveUniqueExportFileBaseName,
8
+ sanitizeExportFileName,
9
+ zipExportDirectory,
10
+ } from '@spiracha/lib/ui-export-archive';
11
+ import { buildUiExportDownloadUrl, ensureUiExportDir } from '@spiracha/lib/ui-export-files';
12
+ import { createServerFn } from '@tanstack/react-start';
13
+ import { z } from 'zod';
14
+
15
+ const workspaceSchema = z.object({
16
+ workspaceKey: z.string().min(1),
17
+ });
18
+
19
+ const threadSchema = z.object({
20
+ composerId: z.string().min(1),
21
+ });
22
+
23
+ const recoverSchema = z.object({
24
+ apply: z.boolean().default(false),
25
+ workspaceKey: z.string().min(1),
26
+ });
27
+
28
+ const exportSchema = z.object({
29
+ composerId: z.string().min(1),
30
+ includeCommentary: z.boolean().default(true),
31
+ includeMetadata: z.boolean().default(true),
32
+ includeTools: z.boolean().default(true),
33
+ outputFormat: z.enum(['md', 'txt']).default('md'),
34
+ zipArchive: z.boolean().default(false),
35
+ });
36
+
37
+ const exportThreadsSchema = z.object({
38
+ composerIds: z.array(z.string().min(1)).min(1),
39
+ includeCommentary: z.boolean().default(true),
40
+ includeMetadata: z.boolean().default(true),
41
+ includeTools: z.boolean().default(true),
42
+ outputFormat: z.enum(['md', 'txt']).default('md'),
43
+ zipArchive: z.boolean().default(true),
44
+ });
45
+
46
+ const deleteThreadsSchema = z.object({
47
+ composerIds: z.array(z.string().min(1)).min(1),
48
+ });
49
+
50
+ const ensureCursorClosedForWrite = async () => {
51
+ const { isCursorRunning } = await import('@spiracha/lib/cursor-recovery');
52
+ if (await isCursorRunning()) {
53
+ throw new Error(
54
+ 'Quit Cursor before deleting. It rewrites chat history on exit, which can resurrect deleted threads.',
55
+ );
56
+ }
57
+ };
58
+
59
+ const findGroupByKey = async (workspaceKey: string) => {
60
+ const { listCursorWorkspaceGroups } = await import('@spiracha/lib/cursor-db');
61
+ const groups = await listCursorWorkspaceGroups();
62
+ const group = groups.find((candidate) => candidate.key === workspaceKey);
63
+ if (!group) {
64
+ throw new Error(`Cursor workspace not found: ${workspaceKey}`);
65
+ }
66
+
67
+ return group;
68
+ };
69
+
70
+ const toSafeExportName = (value: string) => {
71
+ return sanitizeExportFileName(value) || 'cursor-thread';
72
+ };
73
+
74
+ const renderCursorZipDownload = async (
75
+ rendered: Array<{ composerId: string; content: string }>,
76
+ outputFormat: 'md' | 'txt',
77
+ ) => {
78
+ const exportDir = await ensureUiExportDir();
79
+ const exportBaseName =
80
+ rendered.length === 1 ? `${toSafeExportName(rendered[0]!.composerId)}` : `cursor-threads-${rendered.length}`;
81
+ const workspaceDir = await mkdtemp(path.join(os.tmpdir(), `${exportBaseName}-`));
82
+ const zipPath = path.join(exportDir, `${exportBaseName}-${randomUUID()}.zip`);
83
+ const usedBaseNames = new Map<string, number>();
84
+
85
+ try {
86
+ for (const entry of rendered) {
87
+ const baseName = toSafeExportName(entry.composerId);
88
+ const fileBaseName = resolveUniqueExportFileBaseName(baseName, usedBaseNames);
89
+ await Bun.write(path.join(workspaceDir, `${fileBaseName}.${outputFormat}`), entry.content);
90
+ }
91
+
92
+ await zipExportDirectory(workspaceDir, zipPath);
93
+ } finally {
94
+ await rm(workspaceDir, { force: true, recursive: true });
95
+ }
96
+
97
+ return {
98
+ downloadUrl: buildUiExportDownloadUrl(zipPath),
99
+ fileName: `${exportBaseName}.zip`,
100
+ mimeType: 'application/zip',
101
+ mode: 'download_url' as const,
102
+ };
103
+ };
104
+
105
+ export const findCursorThreadByComposerId = async (composerId: string) => {
106
+ const { listCursorThreadsForGroup, listCursorWorkspaceGroups } = await import('@spiracha/lib/cursor-db');
107
+ for (const group of await listCursorWorkspaceGroups()) {
108
+ const threads = await listCursorThreadsForGroup(group, undefined, { includeTranscriptDirs: false });
109
+ const thread = threads.find((candidate) => candidate.composerId === composerId);
110
+ if (thread) {
111
+ return thread;
112
+ }
113
+ }
114
+
115
+ return null;
116
+ };
117
+
118
+ const renderCursorDownload = async (input: {
119
+ composerIds: string[];
120
+ includeCommentary: boolean;
121
+ includeMetadata: boolean;
122
+ includeTools: boolean;
123
+ outputFormat: 'md' | 'txt';
124
+ zipArchive: boolean;
125
+ }) => {
126
+ const { readCursorThreadTranscriptWithAgentFiles } = await import('@spiracha/lib/cursor-db');
127
+ const { getCursorGlobalDbPath } = await import('@spiracha/lib/cursor-exporter-types');
128
+ const { renderCursorTranscript } = await import('@spiracha/lib/cursor-transcript');
129
+ const globalDbPath = getCursorGlobalDbPath();
130
+ const rendered = await Promise.all(
131
+ input.composerIds.map(async (composerId) => {
132
+ const transcript = await readCursorThreadTranscriptWithAgentFiles(globalDbPath, composerId);
133
+ if (!transcript) {
134
+ throw new Error(`No transcript found for thread: ${composerId}`);
135
+ }
136
+
137
+ const content = renderCursorTranscript(transcript, {
138
+ includeCommentary: input.includeCommentary,
139
+ includeMetadata: input.includeMetadata,
140
+ includeTools: input.includeTools,
141
+ outputFormat: input.outputFormat,
142
+ });
143
+
144
+ if (!content) {
145
+ throw new Error(`Thread has no exportable content: ${composerId}`);
146
+ }
147
+
148
+ return {
149
+ composerId,
150
+ content,
151
+ };
152
+ }),
153
+ );
154
+
155
+ if (input.zipArchive || rendered.length > 1) {
156
+ return renderCursorZipDownload(rendered, input.outputFormat);
157
+ }
158
+
159
+ if (rendered.length === 1) {
160
+ return {
161
+ content: rendered[0]!.content,
162
+ fileName: `${toSafeExportName(rendered[0]!.composerId)}.${input.outputFormat}`,
163
+ mimeType: getExportMimeType(input.outputFormat),
164
+ mode: 'download' as const,
165
+ };
166
+ }
167
+
168
+ throw new Error('No Cursor threads selected for export');
169
+ };
170
+
171
+ export const listCursorWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
172
+ const { listCursorWorkspaceGroups } = await import('@spiracha/lib/cursor-db');
173
+ return listCursorWorkspaceGroups();
174
+ });
175
+
176
+ export const listCursorThreadsFn = createServerFn({ method: 'GET' })
177
+ .validator(workspaceSchema)
178
+ .handler(async ({ data }) => {
179
+ const { listCursorThreadsForGroup } = await import('@spiracha/lib/cursor-db');
180
+ const group = await findGroupByKey(data.workspaceKey);
181
+ return listCursorThreadsForGroup(group, undefined, { includeTranscriptDirs: false });
182
+ });
183
+
184
+ export const getCursorThreadDetailFn = createServerFn({ method: 'GET' })
185
+ .validator(threadSchema)
186
+ .handler(async ({ data }) => {
187
+ const { readCursorThreadTranscriptWithAgentFiles } = await import('@spiracha/lib/cursor-db');
188
+ const { getCursorGlobalDbPath } = await import('@spiracha/lib/cursor-exporter-types');
189
+ const thread = await findCursorThreadByComposerId(data.composerId);
190
+ if (!thread) {
191
+ throw new Error(`Cursor thread not found: ${data.composerId}`);
192
+ }
193
+
194
+ const transcript = await readCursorThreadTranscriptWithAgentFiles(getCursorGlobalDbPath(), data.composerId);
195
+ return {
196
+ thread,
197
+ transcript,
198
+ };
199
+ });
200
+
201
+ export const exportCursorThreadFn = createServerFn({ method: 'POST' })
202
+ .validator(exportSchema)
203
+ .handler(async ({ data }) => {
204
+ return await renderCursorDownload({
205
+ composerIds: [data.composerId],
206
+ includeCommentary: data.includeCommentary,
207
+ includeMetadata: data.includeMetadata,
208
+ includeTools: data.includeTools,
209
+ outputFormat: data.outputFormat,
210
+ zipArchive: data.zipArchive,
211
+ });
212
+ });
213
+
214
+ export const exportCursorThreadsFn = createServerFn({ method: 'POST' })
215
+ .validator(exportThreadsSchema)
216
+ .handler(async ({ data }) => {
217
+ return await renderCursorDownload({
218
+ composerIds: data.composerIds,
219
+ includeCommentary: data.includeCommentary,
220
+ includeMetadata: data.includeMetadata,
221
+ includeTools: data.includeTools,
222
+ outputFormat: data.outputFormat,
223
+ zipArchive: data.zipArchive,
224
+ });
225
+ });
226
+
227
+ export const recoverCursorWorkspaceFn = createServerFn({ method: 'POST' })
228
+ .validator(recoverSchema)
229
+ .handler(async ({ data }) => {
230
+ const { isCursorRunning, recoverCursorWorkspaceGroup } = await import('@spiracha/lib/cursor-recovery');
231
+ const group = await findGroupByKey(data.workspaceKey);
232
+ // Cursor rewrites composer.composerHeaders on exit, so a write while it is running gets
233
+ // clobbered. Refuse to apply until Cursor is closed.
234
+ if (data.apply && (await isCursorRunning())) {
235
+ throw new Error('Quit Cursor before recovering. It overwrites chat history on exit, undoing the recovery.');
236
+ }
237
+
238
+ return recoverCursorWorkspaceGroup(group, data.apply);
239
+ });
240
+
241
+ export const deleteCursorThreadsFn = createServerFn({ method: 'POST' })
242
+ .validator(deleteThreadsSchema)
243
+ .handler(async ({ data }) => {
244
+ const { collectCursorThreadsForDeletion, pruneCursorThreads } = await import('@spiracha/lib/cursor-recovery');
245
+ await ensureCursorClosedForWrite();
246
+ const threads = await collectCursorThreadsForDeletion(data.composerIds);
247
+ return pruneCursorThreads(threads, true);
248
+ });
249
+
250
+ export const deleteCursorWorkspaceFn = createServerFn({ method: 'POST' })
251
+ .validator(workspaceSchema)
252
+ .handler(async ({ data }) => {
253
+ const { listCursorThreadsForGroup } = await import('@spiracha/lib/cursor-db');
254
+ const { collectCursorThreadsForDeletion, pruneCursorThreads } = await import('@spiracha/lib/cursor-recovery');
255
+ await ensureCursorClosedForWrite();
256
+ const group = await findGroupByKey(data.workspaceKey);
257
+ const threads = await listCursorThreadsForGroup(group, undefined, { includeTranscriptDirs: false });
258
+ const composerIds = threads.map((thread) => thread.composerId);
259
+ if (composerIds.length === 0) {
260
+ return {
261
+ bubblesDeleted: 0,
262
+ composerDataDeleted: 0,
263
+ composerIds: [],
264
+ headersRemoved: 0,
265
+ transcriptDirsRemoved: 0,
266
+ workspaceBucketsUpdated: 0,
267
+ };
268
+ }
269
+
270
+ const deletable = await collectCursorThreadsForDeletion(composerIds);
271
+ return pruneCursorThreads(deletable, true);
272
+ });
@@ -0,0 +1,229 @@
1
+ import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
2
+ import type { CursorBubble, CursorThreadTranscript, CursorToolCall } from '@spiracha/lib/cursor-exporter-types';
3
+ import type { JsonValue } from '@spiracha/lib/shared';
4
+
5
+ const toTimestamp = (value: number | null): string | null => {
6
+ if (value === null || !Number.isFinite(value)) {
7
+ return null;
8
+ }
9
+
10
+ return new Date(value).toISOString();
11
+ };
12
+
13
+ const toolCallToJson = (toolCall: CursorToolCall | null): JsonValue => {
14
+ if (!toolCall) {
15
+ return null;
16
+ }
17
+
18
+ return {
19
+ argumentsText: toolCall.argumentsText,
20
+ callId: toolCall.callId,
21
+ name: toolCall.name,
22
+ resultText: toolCall.resultText,
23
+ status: toolCall.status,
24
+ };
25
+ };
26
+
27
+ const buildBubbleRaw = (
28
+ bubble: CursorBubble,
29
+ eventType: string,
30
+ extra: Record<string, JsonValue> = {},
31
+ ): Record<string, JsonValue> => ({
32
+ bubbleId: bubble.bubbleId,
33
+ createdAtMs: bubble.createdAtMs,
34
+ eventType,
35
+ kind: bubble.kind,
36
+ source: 'cursor_bubble',
37
+ text: bubble.text,
38
+ thinking: bubble.thinking,
39
+ toolCall: toolCallToJson(bubble.toolCall),
40
+ ...extra,
41
+ });
42
+
43
+ const buildMessageEvent = (
44
+ bubble: CursorBubble,
45
+ sequence: number,
46
+ role: 'assistant' | 'user',
47
+ text: string,
48
+ phase: string | null,
49
+ ): ThreadEvent => ({
50
+ isHiddenByDefault: false,
51
+ kind: 'message',
52
+ memoryCitation: null,
53
+ model: null,
54
+ phase,
55
+ raw: buildBubbleRaw(bubble, phase === 'commentary' ? 'thinking' : 'message'),
56
+ role,
57
+ sequence,
58
+ text,
59
+ timestamp: toTimestamp(bubble.createdAtMs),
60
+ variant: role === 'user' ? 'user_message' : 'agent_message',
61
+ });
62
+
63
+ const buildToolCallCommand = (toolCall: CursorToolCall): string => {
64
+ if (!toolCall.argumentsText?.trim()) {
65
+ return toolCall.name;
66
+ }
67
+
68
+ return `${toolCall.name}\n${toolCall.argumentsText}`;
69
+ };
70
+
71
+ const buildToolCallEvent = (bubble: CursorBubble, sequence: number, toolCall: CursorToolCall): ThreadEvent => ({
72
+ argumentsParseFailed: false,
73
+ argumentsText: toolCall.argumentsText,
74
+ callId: toolCall.callId,
75
+ command: buildToolCallCommand(toolCall),
76
+ kind: 'tool_call',
77
+ name: toolCall.name,
78
+ raw: buildBubbleRaw(bubble, 'tool_call', {
79
+ argumentsText: toolCall.argumentsText,
80
+ callId: toolCall.callId,
81
+ name: toolCall.name,
82
+ status: toolCall.status,
83
+ }),
84
+ sequence,
85
+ timestamp: toTimestamp(bubble.createdAtMs),
86
+ workdir: null,
87
+ });
88
+
89
+ const buildToolOutputEvent = (bubble: CursorBubble, sequence: number, toolCall: CursorToolCall): ThreadEvent | null => {
90
+ const outputText = toolCall.resultText?.trim();
91
+ if (!outputText) {
92
+ return null;
93
+ }
94
+
95
+ return {
96
+ callId: toolCall.callId,
97
+ exitCode: null,
98
+ kind: 'tool_output',
99
+ outputText,
100
+ raw: buildBubbleRaw(bubble, 'tool_output', {
101
+ callId: toolCall.callId,
102
+ name: toolCall.name,
103
+ resultText: outputText,
104
+ status: toolCall.status,
105
+ }),
106
+ sequence,
107
+ summary: outputText,
108
+ timestamp: toTimestamp(bubble.createdAtMs),
109
+ wallTime: null,
110
+ };
111
+ };
112
+
113
+ const getAssistantFinalTextIndexes = (bubbles: CursorBubble[]) => {
114
+ const finalTextIndexes = new Set<number>();
115
+ let currentAssistantTextIndex: number | null = null;
116
+
117
+ const flushAssistantRun = () => {
118
+ if (currentAssistantTextIndex !== null) {
119
+ finalTextIndexes.add(currentAssistantTextIndex);
120
+ currentAssistantTextIndex = null;
121
+ }
122
+ };
123
+
124
+ bubbles.forEach((bubble, index) => {
125
+ if (bubble.kind !== 'assistant') {
126
+ flushAssistantRun();
127
+ return;
128
+ }
129
+
130
+ if (bubble.text.trim()) {
131
+ currentAssistantTextIndex = index;
132
+ }
133
+ });
134
+
135
+ flushAssistantRun();
136
+ return finalTextIndexes;
137
+ };
138
+
139
+ const cursorBubbleToThreadEvents = (
140
+ bubble: CursorBubble,
141
+ bubbleIndex: number,
142
+ finalTextIndexes: Set<number>,
143
+ ): ThreadEvent[] => {
144
+ if (bubble.kind !== 'assistant' && bubble.kind !== 'user') {
145
+ return [];
146
+ }
147
+
148
+ const baseSequence = bubbleIndex * 10;
149
+ const events: ThreadEvent[] = [];
150
+ if (bubble.kind === 'assistant' && bubble.thinking?.trim()) {
151
+ events.push(buildMessageEvent(bubble, baseSequence, 'assistant', bubble.thinking.trim(), 'commentary'));
152
+ }
153
+
154
+ if (bubble.text.trim()) {
155
+ const phase =
156
+ bubble.kind === 'assistant' ? (finalTextIndexes.has(bubbleIndex) ? 'final_answer' : 'commentary') : null;
157
+ events.push(buildMessageEvent(bubble, baseSequence + 1, bubble.kind, bubble.text, phase));
158
+ }
159
+
160
+ if (bubble.kind === 'assistant' && bubble.toolCall) {
161
+ events.push(buildToolCallEvent(bubble, baseSequence + 2, bubble.toolCall));
162
+ const output = buildToolOutputEvent(bubble, baseSequence + 3, bubble.toolCall);
163
+ if (output) {
164
+ events.push(output);
165
+ }
166
+ }
167
+
168
+ return events;
169
+ };
170
+
171
+ export const cursorTranscriptToThreadEvents = (transcript: CursorThreadTranscript): ThreadEvent[] => {
172
+ const finalTextIndexes = getAssistantFinalTextIndexes(transcript.bubbles);
173
+ return transcript.bubbles.flatMap((bubble, index) => cursorBubbleToThreadEvents(bubble, index, finalTextIndexes));
174
+ };
175
+
176
+ const updateMessageStats = (stats: ThreadTranscriptStats, event: Extract<ThreadEvent, { kind: 'message' }>) => {
177
+ stats.messageCount += 1;
178
+ if (event.role === 'assistant') {
179
+ stats.assistantMessageCount += 1;
180
+ }
181
+ if (event.role === 'user') {
182
+ stats.userMessageCount += 1;
183
+ }
184
+ if (event.phase === 'commentary') {
185
+ stats.commentaryCount += 1;
186
+ }
187
+ if (event.phase === 'final_answer') {
188
+ stats.finalAnswerCount += 1;
189
+ }
190
+ };
191
+
192
+ const updateCursorStats = (stats: ThreadTranscriptStats, event: ThreadEvent) => {
193
+ switch (event.kind) {
194
+ case 'message':
195
+ updateMessageStats(stats, event);
196
+ break;
197
+ case 'tool_call':
198
+ stats.toolCallCount += 1;
199
+ break;
200
+ case 'tool_output':
201
+ stats.toolOutputCount += 1;
202
+ break;
203
+ case 'web_search':
204
+ stats.webSearchEventCount += 1;
205
+ break;
206
+ default:
207
+ break;
208
+ }
209
+ };
210
+
211
+ export const getCursorThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats => {
212
+ const stats: ThreadTranscriptStats = {
213
+ assistantMessageCount: 0,
214
+ commentaryCount: 0,
215
+ execCommandCount: 0,
216
+ finalAnswerCount: 0,
217
+ messageCount: 0,
218
+ toolCallCount: 0,
219
+ toolOutputCount: 0,
220
+ userMessageCount: 0,
221
+ webSearchEventCount: 0,
222
+ };
223
+
224
+ for (const event of events) {
225
+ updateCursorStats(stats, event);
226
+ }
227
+
228
+ return stats;
229
+ };
@@ -0,0 +1,166 @@
1
+ type DownloadLogger = Pick<Console, 'error' | 'info' | 'warn'>;
2
+
3
+ type DownloadTextOptions = {
4
+ createObjectUrl?: (blob: Blob) => string;
5
+ documentRef?: Document;
6
+ logger?: DownloadLogger;
7
+ revokeDelayMs?: number;
8
+ revokeObjectUrl?: (url: string) => void;
9
+ schedule?: (callback: () => void, delayMs: number) => void;
10
+ };
11
+
12
+ type DownloadUrlOptions = {
13
+ documentRef?: Document;
14
+ fetchImpl?: typeof fetch;
15
+ logger?: DownloadLogger;
16
+ maxAttempts?: number;
17
+ retryDelayMs?: number;
18
+ sleep?: (delayMs: number) => Promise<void>;
19
+ };
20
+
21
+ const DEFAULT_DOWNLOAD_ATTEMPTS = 6;
22
+ const DEFAULT_DOWNLOAD_RETRY_DELAY_MS = 250;
23
+ const DEFAULT_INLINE_REVOKE_DELAY_MS = 30000;
24
+
25
+ const logDownloadEvent = (
26
+ logger: DownloadLogger,
27
+ level: keyof DownloadLogger,
28
+ event: string,
29
+ details: Record<string, unknown>,
30
+ ) => {
31
+ logger[level](`[spiracha:download] ${event}`, details);
32
+ };
33
+
34
+ const delay = (delayMs: number) =>
35
+ new Promise<void>((resolve) => {
36
+ window.setTimeout(resolve, delayMs);
37
+ });
38
+
39
+ const triggerAnchorDownload = (documentRef: Document, href: string, fileName: string) => {
40
+ const link = documentRef.createElement('a');
41
+ link.href = href;
42
+ link.download = fileName;
43
+ documentRef.body.append(link);
44
+ link.click();
45
+ link.remove();
46
+ };
47
+
48
+ const isReadyStatus = (status: number) => {
49
+ return (status >= 200 && status < 400) || status === 405;
50
+ };
51
+
52
+ export const waitForDownloadUrlAvailability = async (
53
+ downloadUrl: string,
54
+ fileName: string,
55
+ {
56
+ fetchImpl = fetch,
57
+ logger = console,
58
+ maxAttempts = DEFAULT_DOWNLOAD_ATTEMPTS,
59
+ retryDelayMs = DEFAULT_DOWNLOAD_RETRY_DELAY_MS,
60
+ sleep = delay,
61
+ }: Omit<DownloadUrlOptions, 'documentRef'> = {},
62
+ ) => {
63
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
64
+ try {
65
+ const response = await fetchImpl(downloadUrl, {
66
+ cache: 'no-store',
67
+ method: 'HEAD',
68
+ });
69
+
70
+ if (isReadyStatus(response.status)) {
71
+ logDownloadEvent(logger, 'info', 'url_ready', {
72
+ attempt,
73
+ downloadUrl,
74
+ fileName,
75
+ status: response.status,
76
+ });
77
+ return;
78
+ }
79
+
80
+ logDownloadEvent(logger, 'warn', 'url_not_ready', {
81
+ attempt,
82
+ downloadUrl,
83
+ fileName,
84
+ status: response.status,
85
+ });
86
+ } catch (error) {
87
+ logDownloadEvent(logger, 'warn', 'url_probe_failed', {
88
+ attempt,
89
+ downloadUrl,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ fileName,
92
+ });
93
+ }
94
+
95
+ if (attempt < maxAttempts) {
96
+ await sleep(retryDelayMs);
97
+ }
98
+ }
99
+
100
+ throw new Error(`Download file was not available after ${maxAttempts} attempts: ${fileName}`);
101
+ };
102
+
103
+ export const downloadUrlFile = async (
104
+ fileName: string,
105
+ downloadUrl: string,
106
+ {
107
+ documentRef = document,
108
+ fetchImpl = fetch,
109
+ logger = console,
110
+ maxAttempts = DEFAULT_DOWNLOAD_ATTEMPTS,
111
+ retryDelayMs = DEFAULT_DOWNLOAD_RETRY_DELAY_MS,
112
+ sleep = delay,
113
+ }: DownloadUrlOptions = {},
114
+ ) => {
115
+ logDownloadEvent(logger, 'info', 'start', {
116
+ downloadUrl,
117
+ fileName,
118
+ });
119
+
120
+ await waitForDownloadUrlAvailability(downloadUrl, fileName, {
121
+ fetchImpl,
122
+ logger,
123
+ maxAttempts,
124
+ retryDelayMs,
125
+ sleep,
126
+ });
127
+
128
+ triggerAnchorDownload(documentRef, downloadUrl, fileName);
129
+ logDownloadEvent(logger, 'info', 'triggered', {
130
+ downloadUrl,
131
+ fileName,
132
+ });
133
+ };
134
+
135
+ export const downloadTextFile = (
136
+ fileName: string,
137
+ content: string,
138
+ mimeType: string,
139
+ {
140
+ createObjectUrl = (blob) => URL.createObjectURL(blob),
141
+ documentRef = document,
142
+ logger = console,
143
+ revokeDelayMs = DEFAULT_INLINE_REVOKE_DELAY_MS,
144
+ revokeObjectUrl = (url) => URL.revokeObjectURL(url),
145
+ schedule = (callback, delayMs) => {
146
+ window.setTimeout(callback, delayMs);
147
+ },
148
+ }: DownloadTextOptions = {},
149
+ ) => {
150
+ logDownloadEvent(logger, 'info', 'inline_start', {
151
+ fileName,
152
+ mimeType,
153
+ sizeBytes: content.length,
154
+ });
155
+
156
+ const blob = new Blob([content], { type: mimeType });
157
+ const url = createObjectUrl(blob);
158
+ triggerAnchorDownload(documentRef, url, fileName);
159
+ schedule(() => revokeObjectUrl(url), revokeDelayMs);
160
+
161
+ logDownloadEvent(logger, 'info', 'inline_triggered', {
162
+ fileName,
163
+ mimeType,
164
+ sizeBytes: content.length,
165
+ });
166
+ };