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,792 @@
1
+ import {
2
+ type ConversationMessageSelector,
3
+ type ConversationSource,
4
+ type GetConversationOptions,
5
+ getConversation,
6
+ isConversationSource,
7
+ type ListConversationsForPathOptions,
8
+ listConversationSources,
9
+ listConversationsForPath,
10
+ renderConversationMarkdown,
11
+ resolveConversationRef,
12
+ } from './conversation-data';
13
+
14
+ type ConversationApiDependencies = {
15
+ getConversation?: typeof getConversation;
16
+ listConversationSources?: typeof listConversationSources;
17
+ listConversationsForPath?: typeof listConversationsForPath;
18
+ renderConversationMarkdown?: typeof renderConversationMarkdown;
19
+ resolveConversationRef?: typeof resolveConversationRef;
20
+ };
21
+
22
+ type ApiErrorCode = 'conversation_not_found' | 'method_not_allowed' | 'not_found' | 'validation_error';
23
+ type ParseResult<T> = { error: Response } | { value: T };
24
+
25
+ const MAX_CURSOR_OFFSET = 1_000_000;
26
+ const MAX_ID_LENGTH = 2048;
27
+ const MAX_LIMIT = 200;
28
+ const MAX_PATH_LENGTH = 4096;
29
+ const MAX_TIMESTAMP_MS = 9_999_999_999_999;
30
+
31
+ const jsonResponse = (body: unknown, status = 200, headers: HeadersInit = {}) =>
32
+ Response.json(body, {
33
+ headers: {
34
+ 'Cache-Control': 'no-store',
35
+ 'X-Content-Type-Options': 'nosniff',
36
+ ...headers,
37
+ },
38
+ status,
39
+ });
40
+
41
+ const errorResponse = (
42
+ code: ApiErrorCode,
43
+ message: string,
44
+ status: number,
45
+ details?: unknown,
46
+ headers: HeadersInit = {},
47
+ ) =>
48
+ jsonResponse(
49
+ {
50
+ error: {
51
+ code,
52
+ details,
53
+ message,
54
+ },
55
+ },
56
+ status,
57
+ headers,
58
+ );
59
+
60
+ const parseBoolean = (value: string | null) => value === 'true' || value === '1';
61
+
62
+ const isMessageSelector = (value: unknown): value is ConversationMessageSelector => {
63
+ return value === 'all' || value === 'last_assistant' || value === 'last_final_answer';
64
+ };
65
+
66
+ const invalidSourceResponse = (source: unknown) =>
67
+ errorResponse('validation_error', `Unsupported conversation source: ${String(source)}`, 400, {
68
+ field: 'source',
69
+ source,
70
+ });
71
+
72
+ const invalidMessageSelectorResponse = (messageSelector: unknown) =>
73
+ errorResponse('validation_error', `Unsupported message selector: ${String(messageSelector)}`, 400, {
74
+ field: 'message_selector',
75
+ message_selector: messageSelector,
76
+ });
77
+
78
+ const parseSources = (value: string | null): ParseResult<ConversationSource[] | 'all'> => {
79
+ if (!value) {
80
+ return { value: 'all' };
81
+ }
82
+
83
+ const sources = value
84
+ .split(',')
85
+ .map((source) => source.trim())
86
+ .filter(Boolean);
87
+ const invalidSource = sources.find((source) => !isConversationSource(source));
88
+
89
+ return invalidSource ? { error: invalidSourceResponse(invalidSource) } : { value: sources as ConversationSource[] };
90
+ };
91
+
92
+ const parseMessageSelector = (
93
+ value: string | null,
94
+ fallback: ConversationMessageSelector,
95
+ ): ParseResult<ConversationMessageSelector> => {
96
+ if (!value) {
97
+ return { value: fallback };
98
+ }
99
+
100
+ return isMessageSelector(value) ? { value } : { error: invalidMessageSelectorResponse(value) };
101
+ };
102
+
103
+ const decodeCursorOffset = (cursor: string) => {
104
+ try {
105
+ const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
106
+ if (!/^\d+$/u.test(decoded)) {
107
+ return null;
108
+ }
109
+
110
+ const offset = Number(decoded);
111
+ return Number.isSafeInteger(offset) && offset >= 0 && offset <= MAX_CURSOR_OFFSET ? offset : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ };
116
+
117
+ const invalidFieldResponse = (field: string, value: unknown, message: string) =>
118
+ errorResponse('validation_error', message, 400, { field, value });
119
+
120
+ const normalizeLimit = (value: number | undefined): number | undefined => {
121
+ if (value === undefined) {
122
+ return undefined;
123
+ }
124
+
125
+ return Math.min(value, MAX_LIMIT);
126
+ };
127
+
128
+ const validateCursor = (cursor: string | null | undefined): Response | null => {
129
+ if (!cursor) {
130
+ return null;
131
+ }
132
+
133
+ return decodeCursorOffset(cursor) === null
134
+ ? invalidFieldResponse('cursor', cursor, '`cursor` must be a valid pagination cursor.')
135
+ : null;
136
+ };
137
+
138
+ const validateLimit = (limit: number | undefined): Response | null => {
139
+ if (limit === undefined) {
140
+ return null;
141
+ }
142
+
143
+ return Number.isSafeInteger(limit) && limit > 0
144
+ ? null
145
+ : invalidFieldResponse('limit', limit, '`limit` must be a positive integer.');
146
+ };
147
+
148
+ const validatePathLength = (field: string, value: string): Response | null => {
149
+ return value.length <= MAX_PATH_LENGTH
150
+ ? null
151
+ : invalidFieldResponse(field, value.length, `\`${field}\` is too long.`);
152
+ };
153
+
154
+ const validateTimestamp = (field: string, value: number | undefined): Response | null => {
155
+ if (value === undefined) {
156
+ return null;
157
+ }
158
+
159
+ return Number.isSafeInteger(value) && value >= 0 && value <= MAX_TIMESTAMP_MS
160
+ ? null
161
+ : invalidFieldResponse(field, value, `\`${field}\` must be a non-negative epoch millisecond timestamp.`);
162
+ };
163
+
164
+ const parseListLimitParam = (value: string | null): ParseResult<number | undefined> => {
165
+ if (!value) {
166
+ return { value: undefined };
167
+ }
168
+
169
+ const parsed = Number(value);
170
+ if (!Number.isFinite(parsed)) {
171
+ return { error: invalidFieldResponse('limit', value, '`limit` must be a positive integer.') };
172
+ }
173
+
174
+ const validationError = validateLimit(parsed);
175
+ return validationError ? { error: validationError } : { value: normalizeLimit(parsed) };
176
+ };
177
+
178
+ const parseTimestampParam = (field: string, value: string | null): ParseResult<number | undefined> => {
179
+ if (!value) {
180
+ return { value: undefined };
181
+ }
182
+
183
+ const parsed = Number(value);
184
+ if (!Number.isFinite(parsed)) {
185
+ return {
186
+ error: invalidFieldResponse(
187
+ field,
188
+ value,
189
+ `\`${field}\` must be a non-negative epoch millisecond timestamp.`,
190
+ ),
191
+ };
192
+ }
193
+
194
+ const validationError = validateTimestamp(field, parsed);
195
+ return validationError ? { error: validationError } : { value: parsed };
196
+ };
197
+
198
+ const buildListOptions = (url: URL): ParseResult<ListConversationsForPathOptions> => {
199
+ const cwd = url.searchParams.get('cwd')?.trim();
200
+ if (!cwd) {
201
+ return { error: errorResponse('validation_error', '`cwd` is required.', 400, { field: 'cwd' }) };
202
+ }
203
+ const cwdLengthError = validatePathLength('cwd', cwd);
204
+ if (cwdLengthError) {
205
+ return { error: cwdLengthError };
206
+ }
207
+
208
+ const cursor = url.searchParams.get('cursor');
209
+ const cursorError = validateCursor(cursor);
210
+ if (cursorError) {
211
+ return { error: cursorError };
212
+ }
213
+
214
+ const sources = parseSources(url.searchParams.get('source'));
215
+ if ('error' in sources) {
216
+ return sources;
217
+ }
218
+
219
+ const messageSelector = parseMessageSelector(url.searchParams.get('message_selector'), 'last_final_answer');
220
+ if ('error' in messageSelector) {
221
+ return messageSelector;
222
+ }
223
+
224
+ const limit = parseListLimitParam(url.searchParams.get('limit'));
225
+ if ('error' in limit) {
226
+ return limit;
227
+ }
228
+
229
+ const updatedAfterMs = parseTimestampParam('updated_after_ms', url.searchParams.get('updated_after_ms'));
230
+ if ('error' in updatedAfterMs) {
231
+ return updatedAfterMs;
232
+ }
233
+
234
+ const updatedBeforeMs = parseTimestampParam('updated_before_ms', url.searchParams.get('updated_before_ms'));
235
+ if ('error' in updatedBeforeMs) {
236
+ return updatedBeforeMs;
237
+ }
238
+
239
+ return {
240
+ value: {
241
+ cursor,
242
+ cwd,
243
+ includeMessages: parseBoolean(url.searchParams.get('include_messages')),
244
+ limit: limit.value,
245
+ messageSelector: messageSelector.value,
246
+ sources: sources.value,
247
+ updatedAfterMs: updatedAfterMs.value,
248
+ updatedBeforeMs: updatedBeforeMs.value,
249
+ },
250
+ };
251
+ };
252
+
253
+ const normalizeMeta = (meta: { hasNext: boolean; nextCursor: string | null }) => ({
254
+ hasNext: meta.hasNext,
255
+ next_cursor: meta.nextCursor,
256
+ });
257
+
258
+ const getDeps = (dependencies: ConversationApiDependencies) => ({
259
+ getConversation: dependencies.getConversation ?? getConversation,
260
+ listConversationSources: dependencies.listConversationSources ?? listConversationSources,
261
+ listConversationsForPath: dependencies.listConversationsForPath ?? listConversationsForPath,
262
+ renderConversationMarkdown: dependencies.renderConversationMarkdown ?? renderConversationMarkdown,
263
+ resolveConversationRef: dependencies.resolveConversationRef ?? resolveConversationRef,
264
+ });
265
+
266
+ const handleSources = async (dependencies: ReturnType<typeof getDeps>) => {
267
+ return jsonResponse({
268
+ data: await dependencies.listConversationSources(),
269
+ });
270
+ };
271
+
272
+ const handleListConversations = async (url: URL, dependencies: ReturnType<typeof getDeps>) => {
273
+ const result = buildListOptions(url);
274
+ if ('error' in result) {
275
+ return result.error;
276
+ }
277
+
278
+ const page = await dependencies.listConversationsForPath(result.value);
279
+ return jsonResponse({
280
+ data: page.data,
281
+ meta: normalizeMeta(page.meta),
282
+ });
283
+ };
284
+
285
+ const buildGetConversationOptions = (
286
+ source: string | undefined,
287
+ id: string | undefined,
288
+ url: URL,
289
+ ): ParseResult<GetConversationOptions> => {
290
+ if (!source || !id) {
291
+ return { error: errorResponse('validation_error', 'Conversation source and id are required.', 400) };
292
+ }
293
+
294
+ if (!isConversationSource(source)) {
295
+ return { error: invalidSourceResponse(source) };
296
+ }
297
+
298
+ let decodedId: string;
299
+ try {
300
+ decodedId = decodeURIComponent(id);
301
+ } catch {
302
+ return { error: invalidFieldResponse('id', id, 'Conversation id must be URL encoded.') };
303
+ }
304
+ if (!decodedId.trim() || decodedId.length > MAX_ID_LENGTH) {
305
+ return { error: invalidFieldResponse('id', decodedId.length, 'Conversation id is invalid.') };
306
+ }
307
+
308
+ const messageSelector = parseMessageSelector(url.searchParams.get('message_selector'), 'all');
309
+ if ('error' in messageSelector) {
310
+ return messageSelector;
311
+ }
312
+
313
+ return {
314
+ value: {
315
+ id: decodedId,
316
+ messageSelector: messageSelector.value,
317
+ source,
318
+ },
319
+ };
320
+ };
321
+
322
+ const handleGetConversation = async (
323
+ source: string | undefined,
324
+ id: string | undefined,
325
+ url: URL,
326
+ dependencies: ReturnType<typeof getDeps>,
327
+ ) => {
328
+ const result = buildGetConversationOptions(source, id, url);
329
+ if ('error' in result) {
330
+ return result.error;
331
+ }
332
+
333
+ const conversation = await dependencies.getConversation(result.value);
334
+ if (!conversation) {
335
+ return errorResponse('conversation_not_found', 'No conversation exists for that source and id.', 404, {
336
+ id: result.value.id,
337
+ source: result.value.source,
338
+ });
339
+ }
340
+
341
+ return jsonResponse({ data: conversation });
342
+ };
343
+
344
+ const handleExportConversation = async (
345
+ source: string | undefined,
346
+ id: string | undefined,
347
+ url: URL,
348
+ dependencies: ReturnType<typeof getDeps>,
349
+ ) => {
350
+ const result = buildGetConversationOptions(source, id, url);
351
+ if ('error' in result) {
352
+ return result.error;
353
+ }
354
+
355
+ const conversation = await dependencies.getConversation(result.value);
356
+ if (!conversation) {
357
+ return errorResponse('conversation_not_found', 'No conversation exists for that source and id.', 404, {
358
+ id: result.value.id,
359
+ source: result.value.source,
360
+ });
361
+ }
362
+
363
+ return new Response(dependencies.renderConversationMarkdown(conversation), {
364
+ headers: {
365
+ 'Cache-Control': 'no-store',
366
+ 'Content-Type': 'text/markdown; charset=utf-8',
367
+ 'X-Content-Type-Options': 'nosniff',
368
+ },
369
+ });
370
+ };
371
+
372
+ const handleResolve = async (url: URL, dependencies: ReturnType<typeof getDeps>) => {
373
+ const ref = url.searchParams.get('ref')?.trim();
374
+ if (!ref) {
375
+ return errorResponse('validation_error', '`ref` is required.', 400, { field: 'ref' });
376
+ }
377
+
378
+ const resolved = await dependencies.resolveConversationRef(ref);
379
+ if (!resolved) {
380
+ return errorResponse('conversation_not_found', 'Unable to resolve conversation reference.', 404, { ref });
381
+ }
382
+
383
+ return jsonResponse({ data: resolved });
384
+ };
385
+
386
+ const validateSourceOption = (sources: unknown): Response | null => {
387
+ if (sources === undefined || sources === 'all') {
388
+ return null;
389
+ }
390
+
391
+ if (!Array.isArray(sources)) {
392
+ return invalidSourceResponse(sources);
393
+ }
394
+
395
+ if (sources.length === 0) {
396
+ return invalidFieldResponse('source', sources, '`source` must include at least one source.');
397
+ }
398
+
399
+ const invalidSource = sources.find((source) => !isConversationSource(source));
400
+ return invalidSource ? invalidSourceResponse(invalidSource) : null;
401
+ };
402
+
403
+ const validateMessageSelectorOption = (messageSelector: unknown): Response | null => {
404
+ if (messageSelector === undefined || isMessageSelector(messageSelector)) {
405
+ return null;
406
+ }
407
+
408
+ return invalidMessageSelectorResponse(messageSelector);
409
+ };
410
+
411
+ const isRecord = (value: unknown): value is Record<string, unknown> => {
412
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
413
+ };
414
+
415
+ const getOption = (options: Record<string, unknown>, camelKey: string, snakeKey: string) => {
416
+ if (camelKey in options) {
417
+ return options[camelKey];
418
+ }
419
+
420
+ return snakeKey in options ? options[snakeKey] : undefined;
421
+ };
422
+
423
+ const getStringOption = (
424
+ options: Record<string, unknown>,
425
+ camelKey: string,
426
+ snakeKey: string,
427
+ ): ParseResult<string | undefined> => {
428
+ const value = getOption(options, camelKey, snakeKey);
429
+ if (value === undefined) {
430
+ return { value: undefined };
431
+ }
432
+
433
+ return typeof value === 'string'
434
+ ? { value }
435
+ : { error: invalidFieldResponse(snakeKey, value, `\`${snakeKey}\` must be a string.`) };
436
+ };
437
+
438
+ const getNumberOption = (
439
+ options: Record<string, unknown>,
440
+ camelKey: string,
441
+ snakeKey: string,
442
+ ): ParseResult<number | undefined> => {
443
+ const value = getOption(options, camelKey, snakeKey);
444
+ if (value === undefined) {
445
+ return { value: undefined };
446
+ }
447
+
448
+ return typeof value === 'number' && Number.isFinite(value)
449
+ ? { value }
450
+ : { error: invalidFieldResponse(snakeKey, value, `\`${snakeKey}\` must be a finite number.`) };
451
+ };
452
+
453
+ const getBooleanOption = (
454
+ options: Record<string, unknown>,
455
+ camelKey: string,
456
+ snakeKey: string,
457
+ ): ParseResult<boolean | undefined> => {
458
+ const value = getOption(options, camelKey, snakeKey);
459
+ if (value === undefined) {
460
+ return { value: undefined };
461
+ }
462
+
463
+ return typeof value === 'boolean'
464
+ ? { value }
465
+ : { error: invalidFieldResponse(snakeKey, value, `\`${snakeKey}\` must be a boolean.`) };
466
+ };
467
+
468
+ const parseJsonSources = (value: unknown): ParseResult<ConversationSource[] | 'all'> => {
469
+ if (value === undefined || value === 'all') {
470
+ return { value: 'all' };
471
+ }
472
+
473
+ if (typeof value === 'string') {
474
+ return parseSources(value);
475
+ }
476
+
477
+ if (!Array.isArray(value)) {
478
+ return { error: invalidSourceResponse(value) };
479
+ }
480
+
481
+ if (value.length === 0) {
482
+ return { error: invalidFieldResponse('source', value, '`source` must include at least one source.') };
483
+ }
484
+
485
+ const invalidSource = value.find((source) => !isConversationSource(source));
486
+ return invalidSource ? { error: invalidSourceResponse(invalidSource) } : { value: value as ConversationSource[] };
487
+ };
488
+
489
+ const parseJsonCwd = (body: Record<string, unknown>): ParseResult<string> => {
490
+ const cwdOption = getStringOption(body, 'cwd', 'cwd');
491
+ if ('error' in cwdOption) {
492
+ return cwdOption;
493
+ }
494
+
495
+ const cwd = cwdOption.value?.trim();
496
+ if (!cwd) {
497
+ return { error: errorResponse('validation_error', '`cwd` is required.', 400, { field: 'cwd' }) };
498
+ }
499
+
500
+ const cwdLengthError = validatePathLength('cwd', cwd);
501
+ return cwdLengthError ? { error: cwdLengthError } : { value: cwd };
502
+ };
503
+
504
+ const parseJsonCursor = (body: Record<string, unknown>): ParseResult<string | null> => {
505
+ const cursorValue = body.cursor;
506
+ const cursor = typeof cursorValue === 'string' ? cursorValue : null;
507
+ if (cursorValue !== undefined && cursorValue !== null && typeof cursorValue !== 'string') {
508
+ return { error: invalidFieldResponse('cursor', cursorValue, '`cursor` must be a string.') };
509
+ }
510
+
511
+ const cursorError = validateCursor(cursor);
512
+ return cursorError ? { error: cursorError } : { value: cursor };
513
+ };
514
+
515
+ const parseJsonMessageSelector = (body: Record<string, unknown>): ParseResult<ConversationMessageSelector> => {
516
+ const messageSelectorValue = getStringOption(body, 'messageSelector', 'message_selector');
517
+ if ('error' in messageSelectorValue) {
518
+ return messageSelectorValue;
519
+ }
520
+
521
+ return parseMessageSelector(messageSelectorValue.value ?? null, 'last_final_answer');
522
+ };
523
+
524
+ const parseJsonNumberOption = (
525
+ body: Record<string, unknown>,
526
+ camelKey: string,
527
+ snakeKey: string,
528
+ validate: (value: number | undefined) => Response | null,
529
+ ): ParseResult<number | undefined> => {
530
+ const value = getNumberOption(body, camelKey, snakeKey);
531
+ if ('error' in value) {
532
+ return value;
533
+ }
534
+
535
+ const validationError = validate(value.value);
536
+ return validationError ? { error: validationError } : value;
537
+ };
538
+
539
+ const normalizeJsonListOptions = (body: unknown): ParseResult<ListConversationsForPathOptions> => {
540
+ if (!isRecord(body)) {
541
+ return { error: errorResponse('validation_error', 'Request body must be a JSON object.', 400) };
542
+ }
543
+
544
+ const cwd = parseJsonCwd(body);
545
+ if ('error' in cwd) {
546
+ return cwd;
547
+ }
548
+
549
+ const cursor = parseJsonCursor(body);
550
+ if ('error' in cursor) {
551
+ return cursor;
552
+ }
553
+
554
+ const sources = parseJsonSources(getOption(body, 'sources', 'source'));
555
+ if ('error' in sources) {
556
+ return sources;
557
+ }
558
+
559
+ const messageSelector = parseJsonMessageSelector(body);
560
+ if ('error' in messageSelector) {
561
+ return messageSelector;
562
+ }
563
+
564
+ const limit = parseJsonNumberOption(body, 'limit', 'limit', validateLimit);
565
+ if ('error' in limit) {
566
+ return limit;
567
+ }
568
+
569
+ const updatedAfterMs = parseJsonNumberOption(body, 'updatedAfterMs', 'updated_after_ms', (value) =>
570
+ validateTimestamp('updated_after_ms', value),
571
+ );
572
+ if ('error' in updatedAfterMs) {
573
+ return updatedAfterMs;
574
+ }
575
+
576
+ const updatedBeforeMs = parseJsonNumberOption(body, 'updatedBeforeMs', 'updated_before_ms', (value) =>
577
+ validateTimestamp('updated_before_ms', value),
578
+ );
579
+ if ('error' in updatedBeforeMs) {
580
+ return updatedBeforeMs;
581
+ }
582
+
583
+ const includeMessages = getBooleanOption(body, 'includeMessages', 'include_messages');
584
+ if ('error' in includeMessages) {
585
+ return includeMessages;
586
+ }
587
+
588
+ return {
589
+ value: {
590
+ cursor: cursor.value,
591
+ cwd: cwd.value,
592
+ includeMessages: includeMessages.value,
593
+ limit: normalizeLimit(limit.value),
594
+ messageSelector: messageSelector.value,
595
+ sources: sources.value,
596
+ updatedAfterMs: updatedAfterMs.value,
597
+ updatedBeforeMs: updatedBeforeMs.value,
598
+ },
599
+ };
600
+ };
601
+
602
+ const validateListQueryOptions = (options: ListConversationsForPathOptions): Response | null => {
603
+ if (typeof options.cwd !== 'string' || !options.cwd.trim()) {
604
+ return errorResponse('validation_error', '`cwd` is required.', 400, { field: 'cwd' });
605
+ }
606
+
607
+ return (
608
+ validatePathLength('cwd', options.cwd) ??
609
+ validateCursor(options.cursor) ??
610
+ validateLimit(options.limit) ??
611
+ validateTimestamp('updated_after_ms', options.updatedAfterMs) ??
612
+ validateTimestamp('updated_before_ms', options.updatedBeforeMs) ??
613
+ validateSourceOption(options.sources) ??
614
+ validateMessageSelectorOption(options.messageSelector)
615
+ );
616
+ };
617
+
618
+ const handleConversationQuery = async (request: Request, dependencies: ReturnType<typeof getDeps>) => {
619
+ let body: unknown;
620
+ try {
621
+ body = await request.json();
622
+ } catch {
623
+ return errorResponse('validation_error', 'Request body must be JSON.', 400);
624
+ }
625
+
626
+ const normalized = normalizeJsonListOptions(body);
627
+ if ('error' in normalized) {
628
+ return normalized.error;
629
+ }
630
+
631
+ const options = normalized.value;
632
+ const validationError = validateListQueryOptions(options);
633
+ if (validationError) {
634
+ return validationError;
635
+ }
636
+
637
+ const page = await dependencies.listConversationsForPath(options);
638
+ return jsonResponse({
639
+ data: page.data,
640
+ meta: normalizeMeta(page.meta),
641
+ });
642
+ };
643
+
644
+ type ApiRouteContext = {
645
+ action: string | undefined;
646
+ dependencies: ReturnType<typeof getDeps>;
647
+ id: string | undefined;
648
+ method: string;
649
+ request: Request;
650
+ resource: string | undefined;
651
+ segments: string[];
652
+ source: string | undefined;
653
+ url: URL;
654
+ };
655
+
656
+ type ApiRoute = {
657
+ handle: (context: ApiRouteContext) => Promise<Response> | Response;
658
+ matches: (context: ApiRouteContext) => boolean;
659
+ method: string;
660
+ resource: string;
661
+ };
662
+
663
+ const API_ROUTES: ApiRoute[] = [
664
+ {
665
+ handle: ({ dependencies }) => handleSources(dependencies),
666
+ matches: ({ source }) => !source,
667
+ method: 'GET',
668
+ resource: 'sources',
669
+ },
670
+ {
671
+ handle: ({ dependencies, url }) => handleListConversations(url, dependencies),
672
+ matches: ({ source }) => !source,
673
+ method: 'GET',
674
+ resource: 'conversations',
675
+ },
676
+ {
677
+ handle: ({ dependencies, id, source, url }) => handleGetConversation(source, id, url, dependencies),
678
+ matches: ({ action, id, source }) => Boolean(source && id && !action),
679
+ method: 'GET',
680
+ resource: 'conversations',
681
+ },
682
+ {
683
+ handle: ({ dependencies, id, source, url }) => handleExportConversation(source, id, url, dependencies),
684
+ matches: ({ action, id, source }) => Boolean(source && id && action === 'export'),
685
+ method: 'GET',
686
+ resource: 'conversations',
687
+ },
688
+ {
689
+ handle: ({ dependencies, url }) => handleResolve(url, dependencies),
690
+ matches: ({ source }) => !source,
691
+ method: 'GET',
692
+ resource: 'resolve',
693
+ },
694
+ {
695
+ handle: ({ dependencies, request }) => handleConversationQuery(request, dependencies),
696
+ matches: ({ source }) => !source,
697
+ method: 'POST',
698
+ resource: 'conversation-query',
699
+ },
700
+ ];
701
+
702
+ const API_RESOURCES = new Set(API_ROUTES.map((route) => route.resource));
703
+
704
+ const findRoute = (context: ApiRouteContext) =>
705
+ API_ROUTES.find(
706
+ (route) => route.resource === context.resource && route.method === context.method && route.matches(context),
707
+ ) ?? null;
708
+
709
+ const allowedMethodsForContext = (context: ApiRouteContext) => {
710
+ return [
711
+ ...new Set(
712
+ API_ROUTES.filter((route) => route.resource === context.resource && route.matches(context)).map(
713
+ (route) => route.method,
714
+ ),
715
+ ),
716
+ ].sort();
717
+ };
718
+
719
+ const parseApiSegments = (segments: string[]) => {
720
+ if (segments.length < 3 || segments[0] !== 'api' || segments[1] !== 'v1') {
721
+ return null;
722
+ }
723
+
724
+ const resource = segments[2];
725
+ if (!resource) {
726
+ return null;
727
+ }
728
+
729
+ if (resource === 'conversations') {
730
+ if (segments.length === 3) {
731
+ return { action: undefined, id: undefined, resource, source: undefined };
732
+ }
733
+ if (segments.length === 5) {
734
+ return { action: undefined, id: segments[4], resource, source: segments[3] };
735
+ }
736
+ if (segments.length === 6 && segments[5] === 'export') {
737
+ return { action: 'export', id: segments[4], resource, source: segments[3] };
738
+ }
739
+ return { action: '__invalid__', id: undefined, resource, source: undefined };
740
+ }
741
+
742
+ if (segments.length === 3) {
743
+ return { action: undefined, id: undefined, resource, source: undefined };
744
+ }
745
+
746
+ return { action: '__invalid__', id: undefined, resource, source: undefined };
747
+ };
748
+
749
+ export const handleConversationApiRequest = async (
750
+ request: Request,
751
+ dependencies: ConversationApiDependencies = {},
752
+ ): Promise<Response> => {
753
+ const url = new URL(request.url);
754
+ const segments = url.pathname.split('/').filter(Boolean);
755
+ const parsed = parseApiSegments(segments);
756
+
757
+ if (!parsed) {
758
+ return errorResponse('not_found', 'API route not found.', 404);
759
+ }
760
+
761
+ if (parsed.action === '__invalid__') {
762
+ return errorResponse('not_found', 'API route not found.', 404);
763
+ }
764
+
765
+ const context = {
766
+ action: parsed.action,
767
+ dependencies: getDeps(dependencies),
768
+ id: parsed.id,
769
+ method: request.method,
770
+ request,
771
+ resource: parsed.resource,
772
+ segments,
773
+ source: parsed.source,
774
+ url,
775
+ };
776
+ const route = findRoute(context);
777
+
778
+ if (route) {
779
+ return route.handle(context);
780
+ }
781
+
782
+ if (parsed.resource && API_RESOURCES.has(parsed.resource)) {
783
+ const allowedMethods = allowedMethodsForContext(context);
784
+ if (allowedMethods.length > 0) {
785
+ return errorResponse('method_not_allowed', 'Method not allowed.', 405, undefined, {
786
+ Allow: allowedMethods.join(', '),
787
+ });
788
+ }
789
+ }
790
+
791
+ return errorResponse('not_found', 'API route not found.', 404);
792
+ };