spiracha 1.4.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 (319) hide show
  1. package/AGENTS.md +116 -164
  2. package/README.md +92 -237
  3. package/apps/ui/AGENTS.md +36 -7
  4. package/apps/ui/README.md +79 -6
  5. package/apps/ui/components.json +21 -0
  6. package/apps/ui/package.json +65 -0
  7. package/apps/ui/{dist/client → public}/icon.svg +2 -1
  8. package/apps/ui/src/components/antigravity-conversations-table.tsx +206 -0
  9. package/apps/ui/src/components/antigravity-keychain-panel.tsx +76 -0
  10. package/apps/ui/src/components/antigravity-workspaces-table.tsx +63 -0
  11. package/apps/ui/src/components/app-shell.tsx +124 -0
  12. package/apps/ui/src/components/breadcrumbs.tsx +53 -0
  13. package/apps/ui/src/components/claude-code-sessions-table.tsx +114 -0
  14. package/apps/ui/src/components/claude-code-workspaces-table.tsx +55 -0
  15. package/apps/ui/src/components/cursor-threads-table.tsx +189 -0
  16. package/apps/ui/src/components/cursor-workspaces-table.tsx +140 -0
  17. package/apps/ui/src/components/data-table.tsx +241 -0
  18. package/apps/ui/src/components/delete-confirm-dialog.tsx +88 -0
  19. package/apps/ui/src/components/export-dialog.tsx +184 -0
  20. package/apps/ui/src/components/json-panel.tsx +17 -0
  21. package/apps/ui/src/components/kiro-sessions-table.tsx +112 -0
  22. package/apps/ui/src/components/kiro-workspaces-table.tsx +53 -0
  23. package/apps/ui/src/components/list-search-input.tsx +18 -0
  24. package/apps/ui/src/components/loading-panel.tsx +23 -0
  25. package/apps/ui/src/components/metadata-section.tsx +31 -0
  26. package/apps/ui/src/components/metric-card.tsx +19 -0
  27. package/apps/ui/src/components/opencode-sessions-table.tsx +128 -0
  28. package/apps/ui/src/components/opencode-workspaces-table.tsx +57 -0
  29. package/apps/ui/src/components/page-header.tsx +33 -0
  30. package/apps/ui/src/components/projects-loading-state.tsx +74 -0
  31. package/apps/ui/src/components/projects-table.tsx +108 -0
  32. package/apps/ui/src/components/qoder-sessions-table.tsx +112 -0
  33. package/apps/ui/src/components/qoder-workspaces-table.tsx +53 -0
  34. package/apps/ui/src/components/recent-threads-list.tsx +52 -0
  35. package/apps/ui/src/components/reload-error-panel.tsx +20 -0
  36. package/apps/ui/src/components/text-document-panel.tsx +19 -0
  37. package/apps/ui/src/components/theme-toggle.tsx +48 -0
  38. package/apps/ui/src/components/threads-table.tsx +202 -0
  39. package/apps/ui/src/components/transcript-view.tsx +552 -0
  40. package/apps/ui/src/components/ui/alert-dialog.tsx +160 -0
  41. package/apps/ui/src/components/ui/badge.tsx +45 -0
  42. package/apps/ui/src/components/ui/button.tsx +62 -0
  43. package/apps/ui/src/components/ui/checkbox.tsx +29 -0
  44. package/apps/ui/src/components/ui/dialog.tsx +137 -0
  45. package/apps/ui/src/components/ui/dropdown-menu.tsx +217 -0
  46. package/apps/ui/src/components/ui/input.tsx +21 -0
  47. package/apps/ui/src/components/ui/scroll-area.tsx +46 -0
  48. package/apps/ui/src/components/ui/select.tsx +163 -0
  49. package/apps/ui/src/components/ui/separator.tsx +28 -0
  50. package/apps/ui/src/components/ui/sheet.tsx +107 -0
  51. package/apps/ui/src/components/ui/skeleton.tsx +7 -0
  52. package/apps/ui/src/components/ui/table.tsx +76 -0
  53. package/apps/ui/src/components/ui/tabs.tsx +71 -0
  54. package/apps/ui/src/components/ui/tooltip.tsx +44 -0
  55. package/apps/ui/src/integrations/tanstack-query/devtools.tsx +6 -0
  56. package/apps/ui/src/integrations/tanstack-query/root-provider.tsx +10 -0
  57. package/apps/ui/src/lib/antigravity-conversation-state.ts +28 -0
  58. package/apps/ui/src/lib/antigravity-queries.ts +33 -0
  59. package/apps/ui/src/lib/antigravity-server.ts +128 -0
  60. package/apps/ui/src/lib/antigravity-transcript-events.ts +335 -0
  61. package/apps/ui/src/lib/claude-code-queries.ts +26 -0
  62. package/apps/ui/src/lib/claude-code-server.ts +75 -0
  63. package/apps/ui/src/lib/claude-code-transcript-events.ts +154 -0
  64. package/apps/ui/src/lib/codex-queries.ts +66 -0
  65. package/apps/ui/src/lib/codex-server.ts +209 -0
  66. package/apps/ui/src/lib/cursor-queries.ts +37 -0
  67. package/apps/ui/src/lib/cursor-server.ts +272 -0
  68. package/apps/ui/src/lib/cursor-transcript-events.ts +229 -0
  69. package/apps/ui/src/lib/download.ts +166 -0
  70. package/apps/ui/src/lib/formatters.ts +149 -0
  71. package/apps/ui/src/lib/kiro-queries.ts +22 -0
  72. package/apps/ui/src/lib/kiro-server.ts +73 -0
  73. package/apps/ui/src/lib/kiro-transcript-events.ts +113 -0
  74. package/apps/ui/src/lib/opencode-queries.ts +37 -0
  75. package/apps/ui/src/lib/opencode-server.ts +73 -0
  76. package/apps/ui/src/lib/opencode-transcript-events.ts +221 -0
  77. package/apps/ui/src/lib/package-metadata.ts +16 -0
  78. package/apps/ui/src/lib/path-utils.ts +13 -0
  79. package/apps/ui/src/lib/qoder-queries.ts +22 -0
  80. package/apps/ui/src/lib/qoder-server.ts +79 -0
  81. package/apps/ui/src/lib/qoder-transcript-events.ts +139 -0
  82. package/apps/ui/src/lib/route-search.ts +68 -0
  83. package/apps/ui/src/lib/settings-store.tsx +59 -0
  84. package/apps/ui/src/lib/source-session-export-server.ts +56 -0
  85. package/apps/ui/src/lib/text-filter.ts +22 -0
  86. package/apps/ui/src/lib/thread-id.ts +3 -0
  87. package/apps/ui/src/lib/thread-transcript-stats.ts +57 -0
  88. package/apps/ui/src/lib/utils.ts +7 -0
  89. package/apps/ui/src/routeTree.gen.ts +730 -0
  90. package/apps/ui/src/router.tsx +20 -0
  91. package/apps/ui/src/routes/$threadId.tsx +22 -0
  92. package/apps/ui/src/routes/__root.tsx +127 -0
  93. package/apps/ui/src/routes/analytics.tsx +143 -0
  94. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +438 -0
  95. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +127 -0
  96. package/apps/ui/src/routes/antigravity.index.tsx +51 -0
  97. package/apps/ui/src/routes/api.v1.conversation-query.ts +12 -0
  98. package/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts +12 -0
  99. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +12 -0
  100. package/apps/ui/src/routes/api.v1.conversations.ts +12 -0
  101. package/apps/ui/src/routes/api.v1.resolve.ts +12 -0
  102. package/apps/ui/src/routes/api.v1.sources.ts +12 -0
  103. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +342 -0
  104. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +135 -0
  105. package/apps/ui/src/routes/claude-code.index.tsx +48 -0
  106. package/apps/ui/src/routes/codex.$project.tsx +390 -0
  107. package/apps/ui/src/routes/codex.index.tsx +116 -0
  108. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +435 -0
  109. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +416 -0
  110. package/apps/ui/src/routes/cursor.index.tsx +120 -0
  111. package/apps/ui/src/routes/index.tsx +124 -0
  112. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +345 -0
  113. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +136 -0
  114. package/apps/ui/src/routes/kiro.index.tsx +48 -0
  115. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +334 -0
  116. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +135 -0
  117. package/apps/ui/src/routes/opencode.index.tsx +48 -0
  118. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +341 -0
  119. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +139 -0
  120. package/apps/ui/src/routes/qoder.index.tsx +48 -0
  121. package/apps/ui/src/routes/settings.tsx +91 -0
  122. package/apps/ui/src/routes/threads.$threadId.tsx +619 -0
  123. package/apps/ui/src/styles.css +122 -0
  124. package/apps/ui/tsconfig.json +29 -0
  125. package/apps/ui/vite.config.ts +84 -0
  126. package/bin/spiracha.ts +32 -0
  127. package/package.json +50 -60
  128. package/src/client.ts +290 -0
  129. package/src/lib/antigravity-db.ts +213 -19
  130. package/src/lib/antigravity-exporter-types.ts +1 -0
  131. package/src/lib/claude-code-db.ts +655 -0
  132. package/src/lib/claude-code-exporter-types.ts +110 -0
  133. package/src/lib/claude-code-transcript-phase.ts +25 -0
  134. package/src/lib/claude-code-transcript.ts +180 -0
  135. package/src/lib/codex-analytics.ts +1 -1
  136. package/src/lib/codex-browser-db.ts +856 -61
  137. package/src/lib/codex-browser-export.ts +24 -16
  138. package/src/lib/codex-browser-types.ts +1 -1
  139. package/src/lib/codex-thread-cache.ts +17 -1
  140. package/src/lib/{codex-exporter-types.ts → codex-thread-types.ts} +2 -30
  141. package/src/lib/{codex-exporter-transcript.ts → codex-transcript-renderer.ts} +64 -28
  142. package/src/lib/concurrency.ts +25 -0
  143. package/src/lib/conversation-api.ts +792 -0
  144. package/src/lib/conversation-data/adapter-helpers.ts +110 -0
  145. package/src/lib/conversation-data/antigravity-adapter.ts +148 -0
  146. package/src/lib/conversation-data/claude-code-adapter.ts +175 -0
  147. package/src/lib/conversation-data/codex-adapter.ts +245 -0
  148. package/src/lib/conversation-data/cursor-adapter.ts +171 -0
  149. package/src/lib/conversation-data/index.ts +311 -0
  150. package/src/lib/conversation-data/kiro-adapter.ts +140 -0
  151. package/src/lib/conversation-data/message-selector.ts +37 -0
  152. package/src/lib/conversation-data/opencode-adapter.ts +180 -0
  153. package/src/lib/conversation-data/path-match.ts +79 -0
  154. package/src/lib/conversation-data/qoder-adapter.ts +230 -0
  155. package/src/lib/conversation-data/types.ts +116 -0
  156. package/src/lib/cursor-db.ts +67 -21
  157. package/src/lib/cursor-exporter-types.ts +0 -19
  158. package/src/lib/kiro-db.ts +949 -0
  159. package/src/lib/kiro-exporter-types.ts +100 -0
  160. package/src/lib/kiro-transcript-phase.ts +41 -0
  161. package/src/lib/kiro-transcript.ts +115 -0
  162. package/src/lib/opencode-db.ts +610 -0
  163. package/src/lib/opencode-exporter-types.ts +129 -0
  164. package/src/lib/opencode-think-tags.ts +126 -0
  165. package/src/lib/opencode-transcript-phase.ts +50 -0
  166. package/src/lib/opencode-transcript.ts +194 -0
  167. package/src/lib/qoder-acp-client.ts +268 -0
  168. package/src/lib/qoder-db.ts +1601 -0
  169. package/src/lib/qoder-exporter-types.ts +114 -0
  170. package/src/lib/qoder-transcript-phase.ts +41 -0
  171. package/src/lib/qoder-transcript.ts +112 -0
  172. package/src/lib/shared.ts +25 -0
  173. package/src/lib/ui-export-archive.ts +61 -0
  174. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
  175. package/apps/ui/dist/client/assets/analytics-DK2jdvmI.js +0 -1
  176. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DmD0sN8X.js +0 -7
  177. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +0 -1
  178. package/apps/ui/dist/client/assets/antigravity-keychain-panel-BRHVjFAM.js +0 -1
  179. package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
  180. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CGV7amas.js +0 -1
  181. package/apps/ui/dist/client/assets/antigravity.index-DEEkjlZa.js +0 -1
  182. package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
  183. package/apps/ui/dist/client/assets/badge-DfWvPfKF.js +0 -1
  184. package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
  185. package/apps/ui/dist/client/assets/createServerFn-CgRRVpBH.js +0 -3
  186. package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +0 -1
  187. package/apps/ui/dist/client/assets/cursor-threads._composerId-Ti2gTm63.js +0 -1
  188. package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
  189. package/apps/ui/dist/client/assets/cursor._workspaceKey-DzD_c6j9.js +0 -1
  190. package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
  191. package/apps/ui/dist/client/assets/cursor.index-PkCFVcl4.js +0 -2
  192. package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
  193. package/apps/ui/dist/client/assets/delete-confirm-dialog-BHRmyLHr.js +0 -11
  194. package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
  195. package/apps/ui/dist/client/assets/dist-BhZOxAPP.js +0 -1
  196. package/apps/ui/dist/client/assets/dist-DvMS2965.js +0 -1
  197. package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +0 -1
  198. package/apps/ui/dist/client/assets/dropdown-menu-Dy5cLzTy.js +0 -1
  199. package/apps/ui/dist/client/assets/es2015-BkCWttyM.js +0 -41
  200. package/apps/ui/dist/client/assets/export-dialog-C86SYHJn.js +0 -1
  201. package/apps/ui/dist/client/assets/formatters-BM9kB7ed.js +0 -1
  202. package/apps/ui/dist/client/assets/index-CPvAP-jk.js +0 -149
  203. package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
  204. package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
  205. package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
  206. package/apps/ui/dist/client/assets/projects._project-Ckf6muWZ.js +0 -1
  207. package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
  208. package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
  209. package/apps/ui/dist/client/assets/projects.index-V5ibNZII.js +0 -3
  210. package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +0 -1
  211. package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +0 -1
  212. package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +0 -1
  213. package/apps/ui/dist/client/assets/routes-Df2tgpyd.js +0 -1
  214. package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
  215. package/apps/ui/dist/client/assets/select-Bf2GwfPR.js +0 -1
  216. package/apps/ui/dist/client/assets/settings-5VQIzME0.js +0 -1
  217. package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
  218. package/apps/ui/dist/client/assets/styles-BhRkXgwB.css +0 -1
  219. package/apps/ui/dist/client/assets/tabs-OO4VU5KR.js +0 -7
  220. package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
  221. package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
  222. package/apps/ui/dist/client/assets/threads._threadId-BgDhFj2I.js +0 -1
  223. package/apps/ui/dist/client/assets/useMutation-CC_B7uy5.js +0 -1
  224. package/apps/ui/dist/client/assets/useQuery-C5By6WKU.js +0 -1
  225. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CRW2kqj0.js +0 -232
  226. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +0 -6
  227. package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +0 -16
  228. package/apps/ui/dist/server/assets/analytics-CtpOMVMO.js +0 -146
  229. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +0 -16
  230. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BtexWY-K.js +0 -613
  231. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +0 -11
  232. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-zMrR6v6R.js +0 -20
  233. package/apps/ui/dist/server/assets/antigravity-db-Bh8_U9uw.js +0 -580
  234. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +0 -126
  235. package/apps/ui/dist/server/assets/antigravity-keychain-panel-CC7FLmAK.js +0 -55
  236. package/apps/ui/dist/server/assets/antigravity-queries-CGrJO9Vr.js +0 -37
  237. package/apps/ui/dist/server/assets/antigravity-server-CHRjVFqN.js +0 -114
  238. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +0 -11
  239. package/apps/ui/dist/server/assets/antigravity._workspaceKey-B0QeYUOh.js +0 -28
  240. package/apps/ui/dist/server/assets/antigravity._workspaceKey-C3kP-qyN.js +0 -210
  241. package/apps/ui/dist/server/assets/antigravity.index-CPCYkqCi.js +0 -104
  242. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +0 -11
  243. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +0 -26
  244. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +0 -46
  245. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +0 -19
  246. package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
  247. package/apps/ui/dist/server/assets/codex-server-CNXSJuc2.js +0 -2004
  248. package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +0 -18
  249. package/apps/ui/dist/server/assets/createServerRpc-CF_DEwnm.js +0 -12
  250. package/apps/ui/dist/server/assets/createSsrRpc-C754NPki.js +0 -16
  251. package/apps/ui/dist/server/assets/cursor-db-cYZEU3WQ.js +0 -830
  252. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +0 -34
  253. package/apps/ui/dist/server/assets/cursor-queries-NCIM0Nat.js +0 -67
  254. package/apps/ui/dist/server/assets/cursor-recovery-nq-kR62j.js +0 -361
  255. package/apps/ui/dist/server/assets/cursor-server-C3q7hrp-.js +0 -213
  256. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +0 -11
  257. package/apps/ui/dist/server/assets/cursor-threads._composerId-BPJFWfJj.js +0 -18
  258. package/apps/ui/dist/server/assets/cursor-threads._composerId-BkMMTQ3v.js +0 -582
  259. package/apps/ui/dist/server/assets/cursor-transcript-2iL3KFSK.js +0 -125
  260. package/apps/ui/dist/server/assets/cursor._workspaceKey-C5PdTaKB.js +0 -28
  261. package/apps/ui/dist/server/assets/cursor._workspaceKey-Ckj7apKI.js +0 -401
  262. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +0 -11
  263. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +0 -11
  264. package/apps/ui/dist/server/assets/cursor.index-DkWT-mxw.js +0 -189
  265. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +0 -189
  266. package/apps/ui/dist/server/assets/delete-confirm-dialog-BcBNCxWB.js +0 -144
  267. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +0 -92
  268. package/apps/ui/dist/server/assets/dropdown-menu-Dy_9t6TN.js +0 -36
  269. package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +0 -5
  270. package/apps/ui/dist/server/assets/export-dialog-BItjWgkZ.js +0 -240
  271. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
  272. package/apps/ui/dist/server/assets/loading-panel-BGFnWseS.js +0 -27
  273. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +0 -23
  274. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +0 -13
  275. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +0 -29
  276. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +0 -31
  277. package/apps/ui/dist/server/assets/projects._project-Bpbc3C-L.js +0 -20
  278. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
  279. package/apps/ui/dist/server/assets/projects._project-CvZho6EQ.js +0 -395
  280. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +0 -12
  281. package/apps/ui/dist/server/assets/projects.index-BkLiF2FF.js +0 -182
  282. package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +0 -14
  283. package/apps/ui/dist/server/assets/reload-error-panel-BJMxY3U1.js +0 -25
  284. package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +0 -38
  285. package/apps/ui/dist/server/assets/router-6m-ihwqA.js +0 -410
  286. package/apps/ui/dist/server/assets/routes-Bsact1uB.js +0 -184
  287. package/apps/ui/dist/server/assets/routes-BzkJgq4W.js +0 -34
  288. package/apps/ui/dist/server/assets/select-GW76p-ld.js +0 -76
  289. package/apps/ui/dist/server/assets/settings-OayxIYQQ.js +0 -100
  290. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +0 -52
  291. package/apps/ui/dist/server/assets/shared-CPRNYIql.js +0 -134
  292. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
  293. package/apps/ui/dist/server/assets/start-CI_0bSiY.js +0 -4
  294. package/apps/ui/dist/server/assets/tabs-CGA13IZM.js +0 -502
  295. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
  296. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
  297. package/apps/ui/dist/server/assets/threads._threadId-Bs0yuqO9.js +0 -18
  298. package/apps/ui/dist/server/assets/threads._threadId-D2GYkVn6.js +0 -639
  299. package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +0 -83
  300. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +0 -8
  301. package/apps/ui/dist/server/server.js +0 -5847
  302. package/bin/codex-chats-claude.js +0 -5
  303. package/bin/codex-chats.js +0 -5
  304. package/bin/spiracha.js +0 -5
  305. package/src/export-chats.ts +0 -120
  306. package/src/export-claude.ts +0 -36
  307. package/src/export-cursor.ts +0 -244
  308. package/src/lib/claude-exporter.ts +0 -864
  309. package/src/lib/codex-exporter-cli.ts +0 -277
  310. package/src/lib/codex-exporter-db.ts +0 -319
  311. package/src/lib/codex-exporter.ts +0 -115
  312. package/src/lib/cursor-exporter.ts +0 -266
  313. package/src/lib/interactive-cli.ts +0 -433
  314. package/src/lib/native-open.ts +0 -54
  315. package/src/mcp-server.ts +0 -137
  316. package/src/spiracha.ts +0 -116
  317. package/src/ui-cli.ts +0 -310
  318. /package/apps/ui/{dist/client → public}/manifest.json +0 -0
  319. /package/apps/ui/{dist/client → public}/robots.txt +0 -0
@@ -0,0 +1,655 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ type ClaudeCodeSessionSummary,
5
+ type ClaudeCodeSessionTranscript,
6
+ type ClaudeCodeTranscriptEntry,
7
+ type ClaudeCodeTranscriptPart,
8
+ type ClaudeCodeWorkspaceGroup,
9
+ getDefaultClaudeCodeDataDir,
10
+ resolveClaudeCodeProjectsDir,
11
+ } from './claude-code-exporter-types';
12
+ import { mapWithConcurrency } from './concurrency';
13
+ import {
14
+ asBoolean,
15
+ asNumber,
16
+ asObject,
17
+ asString,
18
+ cleanExtractedText,
19
+ cleanInlineTitle,
20
+ getPortablePathBasename,
21
+ isWorkspacePathQuery,
22
+ type JsonValue,
23
+ readJsonlObjects,
24
+ workspacePathMatchesQuery,
25
+ } from './shared';
26
+
27
+ export { getDefaultClaudeCodeDataDir, resolveClaudeCodeProjectsDir };
28
+
29
+ const READ_CONCURRENCY = 8;
30
+ const WORKSPACE_KEY_PREFIX = 'project:';
31
+
32
+ type TranscriptFile = {
33
+ directoryName: string;
34
+ filePath: string;
35
+ };
36
+
37
+ type ParsedTranscriptFile = {
38
+ transcript: ClaudeCodeSessionTranscript;
39
+ };
40
+
41
+ type SessionStats = {
42
+ assistantMessageCount: number;
43
+ attachmentCount: number;
44
+ cacheCreationInputTokens: number;
45
+ cacheReadInputTokens: number;
46
+ inputTokens: number;
47
+ messageCount: number;
48
+ outputTokens: number;
49
+ renderablePartCount: number;
50
+ toolCallCount: number;
51
+ toolResultCount: number;
52
+ userMessageCount: number;
53
+ };
54
+
55
+ type SessionIdentity = {
56
+ cwd: string | null;
57
+ firstUserText: string | null;
58
+ gitBranch: string | null;
59
+ model: string | null;
60
+ sessionId: string;
61
+ title: string | null;
62
+ version: string | null;
63
+ };
64
+
65
+ type SessionTimeline = {
66
+ createdAtIso: string | null;
67
+ createdAtMs: number | null;
68
+ lastActiveAtIso: string | null;
69
+ lastActiveAtMs: number | null;
70
+ };
71
+
72
+ const pathExists = async (target: string): Promise<boolean> => {
73
+ return await stat(target)
74
+ .then(() => true)
75
+ .catch(() => false);
76
+ };
77
+
78
+ const cleanLabel = (value: string | null | undefined): string | null => {
79
+ const cleaned = value?.replace(/\s+/g, ' ').trim();
80
+ return cleaned ? cleaned : null;
81
+ };
82
+
83
+ const getWorkspaceKey = (directoryName: string): string => `${WORKSPACE_KEY_PREFIX}${directoryName}`;
84
+
85
+ const getDirectoryNameFromWorkspaceKey = (workspaceKey: string): string | null => {
86
+ return workspaceKey.startsWith(WORKSPACE_KEY_PREFIX) ? workspaceKey.slice(WORKSPACE_KEY_PREFIX.length) : null;
87
+ };
88
+
89
+ const decodeWorktreeFromDirectoryName = (directoryName: string): string => {
90
+ if (!directoryName.startsWith('-')) {
91
+ return directoryName.replace(/-/g, path.sep);
92
+ }
93
+
94
+ return path.join(path.sep, ...directoryName.slice(1).split('-').filter(Boolean));
95
+ };
96
+
97
+ const getWorkspaceLabel = (worktree: string): string => {
98
+ if (worktree === path.sep) {
99
+ return '(global)';
100
+ }
101
+
102
+ return getPortablePathBasename(worktree) || worktree;
103
+ };
104
+
105
+ const getWorkspaceUri = (worktree: string): string => {
106
+ return worktree.startsWith(path.sep) ? `file://${worktree}` : worktree;
107
+ };
108
+
109
+ const toIso = (value: number | null): string | null => {
110
+ return value === null ? null : new Date(value).toISOString();
111
+ };
112
+
113
+ const parseTimestampMs = (value: JsonValue | undefined): number | null => {
114
+ if (typeof value === 'number' && Number.isFinite(value)) {
115
+ return value;
116
+ }
117
+
118
+ if (typeof value !== 'string' || !value.trim()) {
119
+ return null;
120
+ }
121
+
122
+ const numeric = Number(value);
123
+ if (Number.isFinite(numeric)) {
124
+ return numeric;
125
+ }
126
+
127
+ const parsed = Date.parse(value);
128
+ return Number.isFinite(parsed) ? parsed : null;
129
+ };
130
+
131
+ const formatJsonLike = (value: JsonValue | undefined): string | null => {
132
+ if (value === undefined || value === null) {
133
+ return null;
134
+ }
135
+
136
+ if (typeof value === 'string') {
137
+ return value;
138
+ }
139
+
140
+ return JSON.stringify(value, null, 2);
141
+ };
142
+
143
+ const textFromContentValue = (value: JsonValue | undefined): string => {
144
+ if (typeof value === 'string') {
145
+ return value;
146
+ }
147
+
148
+ if (Array.isArray(value)) {
149
+ return value
150
+ .map((item) => {
151
+ if (typeof item === 'string') {
152
+ return item;
153
+ }
154
+
155
+ const object = asObject(item);
156
+ return asString(object?.text ?? null) ?? '';
157
+ })
158
+ .filter(Boolean)
159
+ .join('\n\n');
160
+ }
161
+
162
+ const object = asObject(value ?? null);
163
+ return asString(object?.text ?? null) ?? '';
164
+ };
165
+
166
+ const parseTextContentPart = (item: Record<string, JsonValue>): ClaudeCodeTranscriptPart => ({
167
+ raw: item,
168
+ text: asString(item.text ?? null) ?? '',
169
+ type: 'text',
170
+ });
171
+
172
+ const parseThinkingContentPart = (item: Record<string, JsonValue>): ClaudeCodeTranscriptPart => ({
173
+ raw: item,
174
+ text: asString(item.thinking ?? null) ?? '',
175
+ type: 'thinking',
176
+ });
177
+
178
+ const parseToolUseContentPart = (item: Record<string, JsonValue>): ClaudeCodeTranscriptPart => ({
179
+ argumentsText: formatJsonLike(item.input),
180
+ raw: item,
181
+ toolName: asString(item.name ?? null) ?? 'unknown',
182
+ toolUseId: asString(item.id ?? null),
183
+ type: 'tool_use',
184
+ });
185
+
186
+ const parseToolResultContentPart = (item: Record<string, JsonValue>): ClaudeCodeTranscriptPart => ({
187
+ isError: asBoolean(item.is_error ?? null),
188
+ outputText: cleanExtractedText(textFromContentValue(item.content)).trim(),
189
+ raw: item,
190
+ toolUseId: asString(item.tool_use_id ?? null),
191
+ type: 'tool_result',
192
+ });
193
+
194
+ const parseFallbackContentPart = (item: Record<string, JsonValue>): ClaudeCodeTranscriptPart => {
195
+ const fallbackText = asString(item.text ?? null);
196
+ return {
197
+ raw: item,
198
+ text: fallbackText ?? undefined,
199
+ type: fallbackText ? 'text' : 'unknown',
200
+ };
201
+ };
202
+
203
+ const structuredContentParsers: Record<string, (item: Record<string, JsonValue>) => ClaudeCodeTranscriptPart> = {
204
+ input_text: parseTextContentPart,
205
+ output_text: parseTextContentPart,
206
+ text: parseTextContentPart,
207
+ thinking: parseThinkingContentPart,
208
+ tool_result: parseToolResultContentPart,
209
+ tool_use: parseToolUseContentPart,
210
+ };
211
+
212
+ const parseStructuredContentPart = (item: Record<string, JsonValue>): ClaudeCodeTranscriptPart => {
213
+ const type = asString(item.type ?? null);
214
+ return type
215
+ ? (structuredContentParsers[type]?.(item) ?? parseFallbackContentPart(item))
216
+ : parseFallbackContentPart(item);
217
+ };
218
+
219
+ const parseMessageContentPart = (item: JsonValue): ClaudeCodeTranscriptPart | null => {
220
+ if (typeof item === 'string') {
221
+ return {
222
+ raw: { text: item },
223
+ text: item,
224
+ type: 'text',
225
+ };
226
+ }
227
+
228
+ const object = asObject(item);
229
+ return object ? parseStructuredContentPart(object) : null;
230
+ };
231
+
232
+ const parseMessageParts = (message: Record<string, JsonValue>): ClaudeCodeTranscriptPart[] => {
233
+ const content = message.content;
234
+ const items = Array.isArray(content) ? content : [content];
235
+ return items
236
+ .map(parseMessageContentPart)
237
+ .filter((part): part is ClaudeCodeTranscriptPart => part !== null && part.type !== 'unknown');
238
+ };
239
+
240
+ const parseAttachmentParts = (raw: Record<string, JsonValue>): ClaudeCodeTranscriptPart[] => {
241
+ const attachment = asObject(raw.attachment ?? null);
242
+ if (!attachment) {
243
+ return [];
244
+ }
245
+
246
+ return [
247
+ {
248
+ attachmentType: asString(attachment.type ?? null),
249
+ raw: attachment,
250
+ text: textFromContentValue(attachment.content),
251
+ type: 'attachment',
252
+ },
253
+ ];
254
+ };
255
+
256
+ const getTranscriptEntryParts = (
257
+ type: string,
258
+ message: Record<string, JsonValue> | null,
259
+ raw: Record<string, JsonValue>,
260
+ ): ClaudeCodeTranscriptPart[] => {
261
+ if (message) {
262
+ return parseMessageParts(message);
263
+ }
264
+
265
+ if (type === 'attachment') {
266
+ return parseAttachmentParts(raw);
267
+ }
268
+
269
+ return [];
270
+ };
271
+
272
+ const getTranscriptEntryRole = (type: string, message: Record<string, JsonValue> | null): string => {
273
+ return asString(message?.role ?? null) ?? (type === 'attachment' ? 'attachment' : type);
274
+ };
275
+
276
+ const getTranscriptEntryId = (type: string, raw: Record<string, JsonValue>): string => {
277
+ return asString(raw.uuid ?? null) ?? `${type}:${asString(raw.sessionId ?? null) ?? 'unknown'}`;
278
+ };
279
+
280
+ const parseTranscriptEntry = (raw: Record<string, JsonValue>): ClaudeCodeTranscriptEntry | null => {
281
+ const type = asString(raw.type ?? null) ?? 'unknown';
282
+ const message = asObject(raw.message ?? null);
283
+ const parts = getTranscriptEntryParts(type, message, raw);
284
+
285
+ if (parts.length === 0) {
286
+ return null;
287
+ }
288
+
289
+ return {
290
+ cwd: asString(raw.cwd ?? null),
291
+ entryId: getTranscriptEntryId(type, raw),
292
+ model: asString(message?.model ?? null),
293
+ parentEntryId: asString(raw.parentUuid ?? null),
294
+ parts,
295
+ raw,
296
+ role: getTranscriptEntryRole(type, message),
297
+ timestamp: asString(raw.timestamp ?? null),
298
+ type,
299
+ };
300
+ };
301
+
302
+ const createEmptyStats = (): SessionStats => ({
303
+ assistantMessageCount: 0,
304
+ attachmentCount: 0,
305
+ cacheCreationInputTokens: 0,
306
+ cacheReadInputTokens: 0,
307
+ inputTokens: 0,
308
+ messageCount: 0,
309
+ outputTokens: 0,
310
+ renderablePartCount: 0,
311
+ toolCallCount: 0,
312
+ toolResultCount: 0,
313
+ userMessageCount: 0,
314
+ });
315
+
316
+ const addUsageStats = (stats: SessionStats, message: Record<string, JsonValue> | null) => {
317
+ const usage = asObject(message?.usage ?? null);
318
+ if (!usage) {
319
+ return;
320
+ }
321
+
322
+ stats.inputTokens += asNumber(usage.input_tokens ?? null) ?? 0;
323
+ stats.outputTokens += asNumber(usage.output_tokens ?? null) ?? 0;
324
+ stats.cacheCreationInputTokens += asNumber(usage.cache_creation_input_tokens ?? null) ?? 0;
325
+ stats.cacheReadInputTokens += asNumber(usage.cache_read_input_tokens ?? null) ?? 0;
326
+ };
327
+
328
+ const updateStatsFromEntry = (stats: SessionStats, entry: ClaudeCodeTranscriptEntry) => {
329
+ if (entry.type === 'user' || entry.type === 'assistant') {
330
+ stats.messageCount += 1;
331
+ }
332
+
333
+ if (entry.role === 'user') {
334
+ stats.userMessageCount += 1;
335
+ }
336
+
337
+ if (entry.role === 'assistant') {
338
+ stats.assistantMessageCount += 1;
339
+ }
340
+
341
+ if (entry.type === 'attachment') {
342
+ stats.attachmentCount += 1;
343
+ }
344
+
345
+ stats.toolCallCount += entry.parts.filter((part) => part.type === 'tool_use').length;
346
+ stats.toolResultCount += entry.parts.filter((part) => part.type === 'tool_result').length;
347
+ stats.renderablePartCount += entry.parts.filter(isRenderablePart).length;
348
+ addUsageStats(stats, asObject(entry.raw.message ?? null));
349
+ };
350
+
351
+ const updateTimeline = (
352
+ timeline: { firstMs: number | null; lastMs: number | null },
353
+ raw: Record<string, JsonValue>,
354
+ ) => {
355
+ const timestampMs = parseTimestampMs(raw.timestamp);
356
+ if (timestampMs === null) {
357
+ return;
358
+ }
359
+
360
+ timeline.firstMs = timeline.firstMs === null ? timestampMs : Math.min(timeline.firstMs, timestampMs);
361
+ timeline.lastMs = timeline.lastMs === null ? timestampMs : Math.max(timeline.lastMs, timestampMs);
362
+ };
363
+
364
+ const readTitleCandidate = (raw: Record<string, JsonValue>): string | null => {
365
+ const type = asString(raw.type ?? null);
366
+ if (type !== 'ai-title' && type !== 'custom-title') {
367
+ return null;
368
+ }
369
+
370
+ return cleanLabel(asString(raw.title ?? null) ?? asString(raw.content ?? null));
371
+ };
372
+
373
+ const updateIdentityFromRaw = (identity: SessionIdentity, raw: Record<string, JsonValue>) => {
374
+ identity.sessionId = asString(raw.sessionId ?? null) ?? identity.sessionId;
375
+ identity.cwd = asString(raw.cwd ?? null) ?? identity.cwd;
376
+ identity.version = asString(raw.version ?? null) ?? identity.version;
377
+ identity.gitBranch = asString(raw.gitBranch ?? null) ?? identity.gitBranch;
378
+
379
+ const message = asObject(raw.message ?? null);
380
+ identity.model = asString(message?.model ?? null) ?? identity.model;
381
+ identity.title = readTitleCandidate(raw) ?? identity.title;
382
+ };
383
+
384
+ const updateIdentityFromEntry = (identity: SessionIdentity, entry: ClaudeCodeTranscriptEntry) => {
385
+ if (entry.role === 'user' && !identity.firstUserText) {
386
+ const textPart = entry.parts.find((part) => part.type === 'text' && part.text?.trim());
387
+ identity.firstUserText = cleanExtractedText(textPart?.text ?? '').trim() || null;
388
+ }
389
+ };
390
+
391
+ const toTimeline = (
392
+ timeline: { firstMs: number | null; lastMs: number | null },
393
+ fallbackMs: number | null,
394
+ ): SessionTimeline => {
395
+ const createdAtMs = timeline.firstMs ?? fallbackMs;
396
+ const lastActiveAtMs = timeline.lastMs ?? fallbackMs;
397
+ return {
398
+ createdAtIso: toIso(createdAtMs),
399
+ createdAtMs,
400
+ lastActiveAtIso: toIso(lastActiveAtMs),
401
+ lastActiveAtMs,
402
+ };
403
+ };
404
+
405
+ const getTitle = (identity: SessionIdentity): string => {
406
+ return cleanInlineTitle(identity.title ?? identity.firstUserText ?? identity.sessionId);
407
+ };
408
+
409
+ const toSessionSummary = (
410
+ file: TranscriptFile,
411
+ identity: SessionIdentity,
412
+ stats: SessionStats,
413
+ timeline: SessionTimeline,
414
+ ): ClaudeCodeSessionSummary => {
415
+ const worktree = identity.cwd ?? decodeWorktreeFromDirectoryName(file.directoryName);
416
+ const workspaceLabel = getWorkspaceLabel(worktree);
417
+ const totalTokens =
418
+ stats.inputTokens + stats.outputTokens + stats.cacheCreationInputTokens + stats.cacheReadInputTokens;
419
+
420
+ return {
421
+ ...stats,
422
+ ...timeline,
423
+ cwd: identity.cwd,
424
+ filePath: file.filePath,
425
+ gitBranch: identity.gitBranch,
426
+ model: identity.model,
427
+ sessionId: identity.sessionId,
428
+ title: getTitle(identity),
429
+ totalTokens,
430
+ version: identity.version,
431
+ workspaceKey: getWorkspaceKey(file.directoryName),
432
+ workspaceLabel,
433
+ worktree,
434
+ };
435
+ };
436
+
437
+ const isRenderablePart = (part: ClaudeCodeTranscriptPart): boolean => {
438
+ if (part.type === 'text' || part.type === 'thinking' || part.type === 'attachment') {
439
+ return Boolean(part.text?.trim());
440
+ }
441
+
442
+ if (part.type === 'tool_use') {
443
+ return Boolean(part.toolName || part.argumentsText?.trim());
444
+ }
445
+
446
+ if (part.type === 'tool_result') {
447
+ return Boolean(part.outputText?.trim());
448
+ }
449
+
450
+ return false;
451
+ };
452
+
453
+ const readTranscriptFile = async (file: TranscriptFile): Promise<ParsedTranscriptFile | null> => {
454
+ const fallbackMtimeMs = await stat(file.filePath)
455
+ .then((stats) => stats.mtimeMs)
456
+ .catch(() => null);
457
+ const identity: SessionIdentity = {
458
+ cwd: null,
459
+ firstUserText: null,
460
+ gitBranch: null,
461
+ model: null,
462
+ sessionId: path.basename(file.filePath, '.jsonl'),
463
+ title: null,
464
+ version: null,
465
+ };
466
+ const stats = createEmptyStats();
467
+ const timeline = { firstMs: null as number | null, lastMs: null as number | null };
468
+ const entries: ClaudeCodeTranscriptEntry[] = [];
469
+ const rawEvents: Record<string, JsonValue>[] = [];
470
+
471
+ for await (const raw of readJsonlObjects(file.filePath)) {
472
+ rawEvents.push(raw);
473
+ updateTimeline(timeline, raw);
474
+ updateIdentityFromRaw(identity, raw);
475
+
476
+ const entry = parseTranscriptEntry(raw);
477
+ if (!entry) {
478
+ continue;
479
+ }
480
+
481
+ entries.push(entry);
482
+ updateStatsFromEntry(stats, entry);
483
+ updateIdentityFromEntry(identity, entry);
484
+ }
485
+
486
+ const session = toSessionSummary(file, identity, stats, toTimeline(timeline, fallbackMtimeMs));
487
+ return {
488
+ transcript: {
489
+ entries,
490
+ rawEvents,
491
+ renderablePartCount: stats.renderablePartCount,
492
+ session,
493
+ },
494
+ };
495
+ };
496
+
497
+ const listTranscriptFilesForProject = async (projectsDir: string, directoryName: string): Promise<TranscriptFile[]> => {
498
+ const projectDir = path.join(projectsDir, directoryName);
499
+ const entries = await readdir(projectDir, { withFileTypes: true }).catch(() => []);
500
+ return entries
501
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
502
+ .map((entry) => ({
503
+ directoryName,
504
+ filePath: path.join(projectDir, entry.name),
505
+ }));
506
+ };
507
+
508
+ const listTranscriptFiles = async (projectsDir: string): Promise<TranscriptFile[]> => {
509
+ if (!(await pathExists(projectsDir))) {
510
+ return [];
511
+ }
512
+
513
+ const projectDirs = (await readdir(projectsDir, { withFileTypes: true }))
514
+ .filter((entry) => entry.isDirectory())
515
+ .map((entry) => entry.name)
516
+ .sort();
517
+ const groupedFiles = await mapWithConcurrency(projectDirs, READ_CONCURRENCY, (directoryName) =>
518
+ listTranscriptFilesForProject(projectsDir, directoryName),
519
+ );
520
+ return groupedFiles.flat();
521
+ };
522
+
523
+ const readTranscriptFiles = async (files: TranscriptFile[]): Promise<ClaudeCodeSessionTranscript[]> => {
524
+ const parsed = await mapWithConcurrency(files, READ_CONCURRENCY, readTranscriptFile);
525
+ return parsed.flatMap((item) => (item ? [item.transcript] : []));
526
+ };
527
+
528
+ const compareNullableMsDesc = (left: number | null, right: number | null): number => {
529
+ return (right ?? 0) - (left ?? 0);
530
+ };
531
+
532
+ const toWorkspaceGroup = (directoryName: string, sessions: ClaudeCodeSessionSummary[]): ClaudeCodeWorkspaceGroup => {
533
+ const worktree = sessions[0]?.worktree ?? decodeWorktreeFromDirectoryName(directoryName);
534
+ const lastActiveAtMs = sessions.reduce<number | null>((latest, session) => {
535
+ if (session.lastActiveAtMs === null) {
536
+ return latest;
537
+ }
538
+
539
+ return latest === null ? session.lastActiveAtMs : Math.max(latest, session.lastActiveAtMs);
540
+ }, null);
541
+
542
+ return {
543
+ assistantMessageCount: sessions.reduce((total, session) => total + session.assistantMessageCount, 0),
544
+ directoryName,
545
+ key: getWorkspaceKey(directoryName),
546
+ label: getWorkspaceLabel(worktree),
547
+ lastActiveAtIso: toIso(lastActiveAtMs),
548
+ lastActiveAtMs,
549
+ messageCount: sessions.reduce((total, session) => total + session.messageCount, 0),
550
+ sessionCount: sessions.length,
551
+ toolCallCount: sessions.reduce((total, session) => total + session.toolCallCount, 0),
552
+ toolResultCount: sessions.reduce((total, session) => total + session.toolResultCount, 0),
553
+ uri: getWorkspaceUri(worktree),
554
+ userMessageCount: sessions.reduce((total, session) => total + session.userMessageCount, 0),
555
+ worktree,
556
+ };
557
+ };
558
+
559
+ export const listClaudeCodeWorkspaceGroups = async (
560
+ projectsDir = resolveClaudeCodeProjectsDir(),
561
+ ): Promise<ClaudeCodeWorkspaceGroup[]> => {
562
+ const files = await listTranscriptFiles(projectsDir);
563
+ const transcripts = await readTranscriptFiles(files);
564
+ const sessionsByDirectory = new Map<string, ClaudeCodeSessionSummary[]>();
565
+
566
+ for (const transcript of transcripts) {
567
+ const directoryName = getDirectoryNameFromWorkspaceKey(transcript.session.workspaceKey);
568
+ if (!directoryName) {
569
+ continue;
570
+ }
571
+
572
+ const sessions = sessionsByDirectory.get(directoryName) ?? [];
573
+ sessions.push(transcript.session);
574
+ sessionsByDirectory.set(directoryName, sessions);
575
+ }
576
+
577
+ return [...sessionsByDirectory.entries()]
578
+ .map(([directoryName, sessions]) => toWorkspaceGroup(directoryName, sessions))
579
+ .sort(
580
+ (left, right) =>
581
+ compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) ||
582
+ left.worktree.localeCompare(right.worktree),
583
+ );
584
+ };
585
+
586
+ const claudeCodeWorkspaceMatchesQuery = (workspace: ClaudeCodeWorkspaceGroup, query: string): boolean => {
587
+ const raw = query.trim();
588
+ if (!raw) {
589
+ return true;
590
+ }
591
+
592
+ const lowered = raw.toLowerCase();
593
+ if (
594
+ workspace.key.toLowerCase() === lowered ||
595
+ workspace.directoryName.toLowerCase() === lowered ||
596
+ workspace.label.toLowerCase() === lowered
597
+ ) {
598
+ return true;
599
+ }
600
+
601
+ if (isWorkspacePathQuery(raw)) {
602
+ return workspacePathMatchesQuery(workspace.worktree, raw);
603
+ }
604
+
605
+ return getPortablePathBasename(workspace.worktree).toLowerCase() === lowered;
606
+ };
607
+
608
+ export const findClaudeCodeWorkspaceGroups = (
609
+ groups: ClaudeCodeWorkspaceGroup[],
610
+ query: string,
611
+ ): ClaudeCodeWorkspaceGroup[] => {
612
+ return groups.filter((group) => claudeCodeWorkspaceMatchesQuery(group, query));
613
+ };
614
+
615
+ const sortSessions = (sessions: ClaudeCodeSessionSummary[]): ClaudeCodeSessionSummary[] => {
616
+ return [...sessions].sort(
617
+ (left, right) =>
618
+ compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title),
619
+ );
620
+ };
621
+
622
+ export const listClaudeCodeSessionsForGroup = async (
623
+ workspaceKey: string,
624
+ projectsDir = resolveClaudeCodeProjectsDir(),
625
+ ): Promise<ClaudeCodeSessionSummary[]> => {
626
+ const directoryName = getDirectoryNameFromWorkspaceKey(workspaceKey);
627
+ if (!directoryName || !(await pathExists(projectsDir))) {
628
+ return [];
629
+ }
630
+
631
+ const files = await listTranscriptFilesForProject(projectsDir, directoryName);
632
+ const transcripts = await readTranscriptFiles(files);
633
+ return sortSessions(transcripts.map((transcript) => transcript.session));
634
+ };
635
+
636
+ const locateSessionFile = async (projectsDir: string, sessionId: string): Promise<TranscriptFile | null> => {
637
+ const files = await listTranscriptFiles(projectsDir);
638
+ return files.find((file) => path.basename(file.filePath, '.jsonl') === sessionId) ?? null;
639
+ };
640
+
641
+ export const readClaudeCodeSessionTranscript = async (
642
+ projectsDir: string,
643
+ sessionId: string,
644
+ ): Promise<ClaudeCodeSessionTranscript | null> => {
645
+ if (!(await pathExists(projectsDir))) {
646
+ return null;
647
+ }
648
+
649
+ const file = await locateSessionFile(projectsDir, sessionId);
650
+ if (!file) {
651
+ return null;
652
+ }
653
+
654
+ return (await readTranscriptFile(file))?.transcript ?? null;
655
+ };