upfynai-code 2.6.0 → 2.6.1

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 (295) hide show
  1. package/README.md +123 -88
  2. package/bin/cli.js +63 -0
  3. package/package.json +48 -106
  4. package/src/auth.js +115 -0
  5. package/src/config.js +33 -0
  6. package/src/connect.js +314 -0
  7. package/src/launch.js +54 -0
  8. package/src/mcp.js +57 -0
  9. package/src/server.js +54 -0
  10. package/client/dist/api-docs.html +0 -879
  11. package/client/dist/assets/AppContent-C0CyP3g5.js +0 -513
  12. package/client/dist/assets/CanvasPanel-0u9QR7U-.js +0 -34
  13. package/client/dist/assets/CanvasPanel-WhZulBJw.css +0 -1
  14. package/client/dist/assets/DashboardPanel-Dgqw1yZk.js +0 -1
  15. package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  16. package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  17. package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  18. package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  19. package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  20. package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  21. package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  22. package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  23. package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  24. package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  25. package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  26. package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  27. package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  28. package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  29. package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  30. package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  31. package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  32. package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  33. package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  34. package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  35. package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  36. package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  37. package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  38. package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  39. package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  40. package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  41. package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  42. package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  43. package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  44. package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  45. package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  46. package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  47. package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  48. package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  49. package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  50. package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  51. package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  52. package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  53. package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  54. package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  55. package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  56. package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  57. package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  58. package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  59. package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  60. package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  61. package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  62. package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  63. package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  64. package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  65. package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  66. package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  67. package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  68. package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  69. package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  70. package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  71. package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  72. package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  73. package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  74. package/client/dist/assets/LoginModal-CZDEzqjK.js +0 -19
  75. package/client/dist/assets/MarkdownPreview-CYdvwJaV.js +0 -1
  76. package/client/dist/assets/Onboarding-DR6NZ4Vz.js +0 -1
  77. package/client/dist/assets/SetupForm-D49gtWY4.js +0 -1
  78. package/client/dist/assets/Tableau10-B-NsZVaP.js +0 -1
  79. package/client/dist/assets/WorkflowsPanel-CqlbEJA_.js +0 -1
  80. package/client/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +0 -1
  81. package/client/dist/assets/ar-SA-G6X2FPQ2-BWqa1yBH.js +0 -10
  82. package/client/dist/assets/arc-BegSKqEW.js +0 -1
  83. package/client/dist/assets/array-BKyUJesY.js +0 -1
  84. package/client/dist/assets/az-AZ-76LH7QW2-DrVlbZDP.js +0 -1
  85. package/client/dist/assets/bg-BG-XCXSNQG7-DdunjBgT.js +0 -5
  86. package/client/dist/assets/blockDiagram-38ab4fdb-BKMbwGHu.js +0 -118
  87. package/client/dist/assets/bn-BD-2XOGV67Q-_7DtmvwO.js +0 -5
  88. package/client/dist/assets/c4Diagram-3d4e48cf-hJuiHhSn.js +0 -10
  89. package/client/dist/assets/ca-ES-6MX7JW3Y-BFIrmojG.js +0 -8
  90. package/client/dist/assets/channel-Bur-rRTp.js +0 -1
  91. package/client/dist/assets/classDiagram-70f12bd4-BjiAf9cM.js +0 -2
  92. package/client/dist/assets/classDiagram-v2-f2320105-pwBewejc.js +0 -2
  93. package/client/dist/assets/clone-BtqXeoBJ.js +0 -1
  94. package/client/dist/assets/createText-2e5e7dd3-Dq_acOWe.js +0 -5
  95. package/client/dist/assets/cs-CZ-2BRQDIVT-B-x4F6TJ.js +0 -11
  96. package/client/dist/assets/da-DK-5WZEPLOC-Btlc8Dgn.js +0 -5
  97. package/client/dist/assets/de-DE-XR44H4JA-BVu3ZIoD.js +0 -8
  98. package/client/dist/assets/directory-open-01563666-DWU9wJ6I.js +0 -1
  99. package/client/dist/assets/directory-open-4ed118d0-CunoC1EB.js +0 -1
  100. package/client/dist/assets/edges-e0da2a9e-DH0wVTXR.js +0 -4
  101. package/client/dist/assets/el-GR-BZB4AONW-h2ll8_ZC.js +0 -10
  102. package/client/dist/assets/erDiagram-9861fffd-BYezLIR7.js +0 -51
  103. package/client/dist/assets/es-ES-U4NZUMDT-Cveiulwt.js +0 -9
  104. package/client/dist/assets/eu-ES-A7QVB2H4-DQluL2PY.js +0 -11
  105. package/client/dist/assets/fa-IR-HGAKTJCU-BJtcMBSv.js +0 -8
  106. package/client/dist/assets/fi-FI-Z5N7JZ37-D8NfbVXV.js +0 -6
  107. package/client/dist/assets/file-open-002ab408-DIuFHtCF.js +0 -1
  108. package/client/dist/assets/file-open-7c801643-684qeFg4.js +0 -1
  109. package/client/dist/assets/file-save-3189631c-C1wFhQhH.js +0 -1
  110. package/client/dist/assets/file-save-745eba88-Bb9F9Kg7.js +0 -1
  111. package/client/dist/assets/flowDb-956e92f1-scnUykhM.js +0 -10
  112. package/client/dist/assets/flowDiagram-66a62f08-jVyWsfyU.js +0 -4
  113. package/client/dist/assets/flowDiagram-v2-96b9c2cf-N6xgi25h.js +0 -1
  114. package/client/dist/assets/flowchart-elk-definition-4a651766-gKGX3HqR.js +0 -139
  115. package/client/dist/assets/fr-FR-RHASNOE6-vdj42kC6.js +0 -9
  116. package/client/dist/assets/ganttDiagram-c361ad54-C2CiWFUP.js +0 -257
  117. package/client/dist/assets/gitGraphDiagram-72cf32ee-C59Yz2LK.js +0 -70
  118. package/client/dist/assets/gl-ES-HMX3MZ6V-DQo0TzoP.js +0 -10
  119. package/client/dist/assets/graph-Dx_H43Kv.js +0 -1
  120. package/client/dist/assets/he-IL-6SHJWFNN-DKXK5e33.js +0 -10
  121. package/client/dist/assets/hi-IN-IWLTKZ5I-C2Qgqc0R.js +0 -4
  122. package/client/dist/assets/hu-HU-A5ZG7DT2-Ss-6vX0m.js +0 -7
  123. package/client/dist/assets/id-ID-SAP4L64H-D7Wsg1S2.js +0 -10
  124. package/client/dist/assets/image-blob-reduce.esm-D6s-rqMO.js +0 -7
  125. package/client/dist/assets/index-3862675e-u8Nv7hHC.js +0 -1
  126. package/client/dist/assets/index-BVowJdZF.js +0 -97
  127. package/client/dist/assets/index-ce18TYkg.js +0 -27
  128. package/client/dist/assets/index-kQoJx-bc.css +0 -1
  129. package/client/dist/assets/infoDiagram-f8f76790-LmoJYsxo.js +0 -7
  130. package/client/dist/assets/init-Gi6I4Gst.js +0 -1
  131. package/client/dist/assets/it-IT-JPQ66NNP-CAPTVl7M.js +0 -11
  132. package/client/dist/assets/ja-JP-DBVTYXUO-eNVPawR2.js +0 -8
  133. package/client/dist/assets/journeyDiagram-49397b02-BaJqehpR.js +0 -139
  134. package/client/dist/assets/kaa-6HZHGXH3-tpuNkKhS.js +0 -1
  135. package/client/dist/assets/kab-KAB-ZGHBKWFO-Dp83kx4x.js +0 -8
  136. package/client/dist/assets/kk-KZ-P5N5QNE5-B9IlC6YN.js +0 -1
  137. package/client/dist/assets/km-KH-HSX4SM5Z-B_KMYaMj.js +0 -11
  138. package/client/dist/assets/ko-KR-MTYHY66A-yebnUNdb.js +0 -9
  139. package/client/dist/assets/ku-TR-6OUDTVRD-BR6fh6-5.js +0 -9
  140. package/client/dist/assets/layout-DLl5Jwcl.js +0 -1
  141. package/client/dist/assets/line-FpB7omSK.js +0 -1
  142. package/client/dist/assets/linear-CkXqUFJ8.js +0 -1
  143. package/client/dist/assets/lt-LT-XHIRWOB4-SutZSWtR.js +0 -3
  144. package/client/dist/assets/lv-LV-5QDEKY6T-DuAxdcZL.js +0 -7
  145. package/client/dist/assets/mindmap-definition-fc14e90a-DyxXOExh.js +0 -425
  146. package/client/dist/assets/mr-IN-CRQNXWMA-DqDUWM_8.js +0 -13
  147. package/client/dist/assets/my-MM-5M5IBNSE-C40kMFMR.js +0 -1
  148. package/client/dist/assets/nb-NO-T6EIAALU-DVij32Ju.js +0 -10
  149. package/client/dist/assets/nl-NL-IS3SIHDZ-rT84mDYq.js +0 -8
  150. package/client/dist/assets/nn-NO-6E72VCQL-BBZXBW8V.js +0 -8
  151. package/client/dist/assets/oc-FR-POXYY2M6-DzjOugOf.js +0 -8
  152. package/client/dist/assets/ordinal-Cboi1Yqb.js +0 -1
  153. package/client/dist/assets/pa-IN-N4M65BXN-DD1iU8_F.js +0 -4
  154. package/client/dist/assets/path-CbwjOpE9.js +0 -1
  155. package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
  156. package/client/dist/assets/pdf.worker-BA9kU3Pw.mjs +0 -61080
  157. package/client/dist/assets/percentages-BXMCSKIN-WVlHS4wx.js +0 -207
  158. package/client/dist/assets/pica-CQIY57Tf.js +0 -7
  159. package/client/dist/assets/pieDiagram-8a3498a8-Dd_85qBH.js +0 -35
  160. package/client/dist/assets/pl-PL-T2D74RX3-ukVXa48G.js +0 -9
  161. package/client/dist/assets/pt-BR-5N22H2LF-BibawarT.js +0 -9
  162. package/client/dist/assets/pt-PT-UZXXM6DQ-So3i9l9w.js +0 -9
  163. package/client/dist/assets/quadrantDiagram-120e2f19-C4dFVDEx.js +0 -7
  164. package/client/dist/assets/requirementDiagram-deff3bca-DrTO7yFl.js +0 -52
  165. package/client/dist/assets/ro-RO-JPDTUUEW-DY0Xq_Hd.js +0 -11
  166. package/client/dist/assets/roundRect-0PYZxl1G.js +0 -1
  167. package/client/dist/assets/ru-RU-B4JR7IUQ-B7u_Zvkd.js +0 -9
  168. package/client/dist/assets/sankeyDiagram-04a897e0-D24gfzuS.js +0 -8
  169. package/client/dist/assets/sequenceDiagram-704730f1-Dgji2XLQ.js +0 -122
  170. package/client/dist/assets/si-LK-N5RQ5JYF-OejsLzQ_.js +0 -1
  171. package/client/dist/assets/sk-SK-C5VTKIMK-_vy2Bt-M.js +0 -6
  172. package/client/dist/assets/sl-SI-NN7IZMDC-DKOl_u2M.js +0 -6
  173. package/client/dist/assets/stateDiagram-587899a1-CJ8eBaiU.js +0 -1
  174. package/client/dist/assets/stateDiagram-v2-d93cdb3a-C5K3l-Nt.js +0 -1
  175. package/client/dist/assets/styles-6aaf32cf-DAKE0jbx.js +0 -207
  176. package/client/dist/assets/styles-9a916d00-LFAJCgEy.js +0 -160
  177. package/client/dist/assets/styles-c10674c1-CllKO8NG.js +0 -116
  178. package/client/dist/assets/subset-shared.chunk-Uy-J87FQ.js +0 -84
  179. package/client/dist/assets/subset-worker.chunk-dvgDvqt9.js +0 -1
  180. package/client/dist/assets/sv-SE-XGPEYMSR-CDCB2ZV5.js +0 -10
  181. package/client/dist/assets/svgDrawCommon-08f97a94-CObOzbFQ.js +0 -1
  182. package/client/dist/assets/ta-IN-2NMHFXQM-DHUNdO69.js +0 -9
  183. package/client/dist/assets/th-TH-HPSO5L25-zI2hnBq3.js +0 -2
  184. package/client/dist/assets/timeline-definition-85554ec2-C2XHRmxK.js +0 -61
  185. package/client/dist/assets/tr-TR-DEFEU3FU-l-6Hu4-D.js +0 -7
  186. package/client/dist/assets/uk-UA-QMV73CPH-CqSOwrl7.js +0 -6
  187. package/client/dist/assets/vendor-codemirror-D_s0aGBu.js +0 -35
  188. package/client/dist/assets/vendor-i18n-DCFGyhQR.js +0 -1
  189. package/client/dist/assets/vendor-icons-Lb69KSFJ.js +0 -646
  190. package/client/dist/assets/vendor-markdown-BXEi_H3G.js +0 -298
  191. package/client/dist/assets/vendor-react-9mUTKBHH.js +0 -67
  192. package/client/dist/assets/vendor-syntax-DnmwQQJF.js +0 -16
  193. package/client/dist/assets/vendor-xterm-CZq1hqo1.js +0 -66
  194. package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +0 -32
  195. package/client/dist/assets/vi-VN-M7AON7JQ-CUL8-mBZ.js +0 -5
  196. package/client/dist/assets/xychartDiagram-e933f94c-1fmf6slj.js +0 -7
  197. package/client/dist/assets/zh-CN-LNUGB5OW-CB5y5VVU.js +0 -10
  198. package/client/dist/assets/zh-HK-E62DVLB3-BHcrrEeJ.js +0 -1
  199. package/client/dist/assets/zh-TW-RAJ6MFWO-DoDUdkaJ.js +0 -9
  200. package/client/dist/clear-cache.html +0 -85
  201. package/client/dist/convert-icons.md +0 -53
  202. package/client/dist/favicon.png +0 -0
  203. package/client/dist/favicon.svg +0 -9
  204. package/client/dist/generate-icons.js +0 -49
  205. package/client/dist/icons/claude-ai-icon.svg +0 -1
  206. package/client/dist/icons/codex-white.svg +0 -3
  207. package/client/dist/icons/codex.svg +0 -3
  208. package/client/dist/icons/cursor-white.svg +0 -12
  209. package/client/dist/icons/cursor.svg +0 -1
  210. package/client/dist/icons/icon-128x128.png +0 -0
  211. package/client/dist/icons/icon-128x128.svg +0 -12
  212. package/client/dist/icons/icon-144x144.png +0 -0
  213. package/client/dist/icons/icon-144x144.svg +0 -12
  214. package/client/dist/icons/icon-152x152.png +0 -0
  215. package/client/dist/icons/icon-152x152.svg +0 -12
  216. package/client/dist/icons/icon-192x192.png +0 -0
  217. package/client/dist/icons/icon-192x192.svg +0 -12
  218. package/client/dist/icons/icon-384x384.png +0 -0
  219. package/client/dist/icons/icon-384x384.svg +0 -12
  220. package/client/dist/icons/icon-512x512.png +0 -0
  221. package/client/dist/icons/icon-512x512.svg +0 -12
  222. package/client/dist/icons/icon-72x72.png +0 -0
  223. package/client/dist/icons/icon-72x72.svg +0 -12
  224. package/client/dist/icons/icon-96x96.png +0 -0
  225. package/client/dist/icons/icon-96x96.svg +0 -12
  226. package/client/dist/icons/icon-template.svg +0 -12
  227. package/client/dist/index.html +0 -128
  228. package/client/dist/logo-128.png +0 -0
  229. package/client/dist/logo-256.png +0 -0
  230. package/client/dist/logo-32.png +0 -0
  231. package/client/dist/logo-512.png +0 -0
  232. package/client/dist/logo-64.png +0 -0
  233. package/client/dist/logo.svg +0 -17
  234. package/client/dist/manifest.json +0 -61
  235. package/client/dist/mcp-docs.html +0 -119
  236. package/client/dist/screenshots/cli-selection.png +0 -0
  237. package/client/dist/screenshots/desktop-main.png +0 -0
  238. package/client/dist/screenshots/mobile-chat.png +0 -0
  239. package/client/dist/screenshots/tools-modal.png +0 -0
  240. package/client/dist/sw.js +0 -19
  241. package/commands/upfynai-connect.md +0 -59
  242. package/commands/upfynai-disconnect.md +0 -31
  243. package/commands/upfynai-doctor.md +0 -99
  244. package/commands/upfynai-export.md +0 -49
  245. package/commands/upfynai-local.md +0 -82
  246. package/commands/upfynai-status.md +0 -75
  247. package/commands/upfynai-stop.md +0 -49
  248. package/commands/upfynai-uninstall.md +0 -58
  249. package/commands/upfynai.md +0 -69
  250. package/scripts/build-client.js +0 -17
  251. package/scripts/fix-node-pty.js +0 -67
  252. package/scripts/install-commands.js +0 -78
  253. package/server/claude-sdk.js +0 -714
  254. package/server/cli-ui.js +0 -785
  255. package/server/cli.js +0 -596
  256. package/server/constants/config.js +0 -31
  257. package/server/cursor-cli.js +0 -270
  258. package/server/database/auth.db +0 -0
  259. package/server/database/db.js +0 -822
  260. package/server/database/init.sql +0 -70
  261. package/server/index.js +0 -2738
  262. package/server/load-env.js +0 -26
  263. package/server/mcp-server.js +0 -621
  264. package/server/middleware/auth.js +0 -181
  265. package/server/openai-codex.js +0 -403
  266. package/server/openrouter.js +0 -137
  267. package/server/projects.js +0 -1742
  268. package/server/relay-client.js +0 -672
  269. package/server/routes/agent.js +0 -1226
  270. package/server/routes/auth.js +0 -266
  271. package/server/routes/cli-auth.js +0 -263
  272. package/server/routes/codex.js +0 -344
  273. package/server/routes/commands.js +0 -598
  274. package/server/routes/cursor.js +0 -807
  275. package/server/routes/dashboard.js +0 -205
  276. package/server/routes/git.js +0 -1151
  277. package/server/routes/mcp-utils.js +0 -48
  278. package/server/routes/mcp.js +0 -535
  279. package/server/routes/payments.js +0 -172
  280. package/server/routes/projects.js +0 -552
  281. package/server/routes/settings.js +0 -261
  282. package/server/routes/taskmaster.js +0 -1928
  283. package/server/routes/user.js +0 -106
  284. package/server/routes/vapi-chat.js +0 -94
  285. package/server/routes/voice.js +0 -194
  286. package/server/routes/webhooks.js +0 -166
  287. package/server/routes/workflows.js +0 -118
  288. package/server/sandbox.js +0 -120
  289. package/server/services/whisperService.js +0 -84
  290. package/server/services/workflowScheduler.js +0 -186
  291. package/server/utils/commandParser.js +0 -303
  292. package/server/utils/gitConfig.js +0 -24
  293. package/server/utils/mcp-detector.js +0 -198
  294. package/server/utils/taskmaster-websocket.js +0 -129
  295. package/shared/modelConstants.js +0 -96
@@ -1,1742 +0,0 @@
1
- /**
2
- * PROJECT DISCOVERY AND MANAGEMENT SYSTEM
3
- * ========================================
4
- *
5
- * This module manages project discovery for both Claude CLI and Cursor CLI sessions.
6
- *
7
- * ## Architecture Overview
8
- *
9
- * 1. **Claude Projects** (stored in ~/.claude/projects/)
10
- * - Each project is a directory named with the project path encoded (/ replaced with -)
11
- * - Contains .jsonl files with conversation history including 'cwd' field
12
- * - Project metadata stored in ~/.claude/project-config.json
13
- *
14
- * 2. **Cursor Projects** (stored in ~/.cursor/chats/)
15
- * - Each project directory is named with MD5 hash of the absolute project path
16
- * - Example: /Users/john/myproject -> MD5 -> a1b2c3d4e5f6...
17
- * - Contains session directories with SQLite databases (store.db)
18
- * - Project path is NOT stored in the database - only in the MD5 hash
19
- *
20
- * ## Project Discovery Strategy
21
- *
22
- * 1. **Claude Projects Discovery**:
23
- * - Scan ~/.claude/projects/ directory for Claude project folders
24
- * - Extract actual project path from .jsonl files (cwd field)
25
- * - Fall back to decoded directory name if no sessions exist
26
- *
27
- * 2. **Cursor Sessions Discovery**:
28
- * - For each KNOWN project (from Claude or manually added)
29
- * - Compute MD5 hash of the project's absolute path
30
- * - Check if ~/.cursor/chats/{md5_hash}/ directory exists
31
- * - Read session metadata from SQLite store.db files
32
- *
33
- * 3. **Manual Project Addition**:
34
- * - Users can manually add project paths via UI
35
- * - Stored in ~/.claude/project-config.json with 'manuallyAdded' flag
36
- * - Allows discovering Cursor sessions for projects without Claude sessions
37
- *
38
- * ## Critical Limitations
39
- *
40
- * - **CANNOT discover Cursor-only projects**: From a quick check, there was no mention of
41
- * the cwd of each project. if someone has the time, you can try to reverse engineer it.
42
- *
43
- * - **Project relocation breaks history**: If a project directory is moved or renamed,
44
- * the MD5 hash changes, making old Cursor sessions inaccessible unless the old
45
- * path is known and manually added.
46
- *
47
- * ## Error Handling
48
- *
49
- * - Missing ~/.claude directory is handled gracefully with automatic creation
50
- * - ENOENT errors are caught and handled without crashing
51
- * - Empty arrays returned when no projects/sessions exist
52
- *
53
- * ## Caching Strategy
54
- *
55
- * - Project directory extraction is cached to minimize file I/O
56
- * - Cache is cleared when project configuration changes
57
- * - Session data is fetched on-demand, not cached
58
- */
59
-
60
- import { promises as fs } from 'fs';
61
- import fsSync from 'fs';
62
- import path from 'path';
63
- import readline from 'readline';
64
- import crypto from 'crypto';
65
- // sqlite3 is a native module — conditionally imported (not available on Vercel)
66
- let sqlite3 = null;
67
- let sqliteOpen = null;
68
- try {
69
- sqlite3 = (await import('sqlite3')).default;
70
- sqliteOpen = (await import('sqlite')).open;
71
- } catch (e) {
72
- console.warn('[WARN] sqlite3/sqlite not available. Project scanning from SQLite databases disabled.');
73
- }
74
- import os from 'os';
75
-
76
- // Import TaskMaster detection functions
77
- async function detectTaskMasterFolder(projectPath) {
78
- try {
79
- const taskMasterPath = path.join(projectPath, '.taskmaster');
80
-
81
- // Check if .taskmaster directory exists
82
- try {
83
- const stats = await fs.stat(taskMasterPath);
84
- if (!stats.isDirectory()) {
85
- return {
86
- hasTaskmaster: false,
87
- reason: '.taskmaster exists but is not a directory'
88
- };
89
- }
90
- } catch (error) {
91
- if (error.code === 'ENOENT') {
92
- return {
93
- hasTaskmaster: false,
94
- reason: '.taskmaster directory not found'
95
- };
96
- }
97
- throw error;
98
- }
99
-
100
- // Check for key TaskMaster files
101
- const keyFiles = [
102
- 'tasks/tasks.json',
103
- 'config.json'
104
- ];
105
-
106
- const fileStatus = {};
107
- let hasEssentialFiles = true;
108
-
109
- for (const file of keyFiles) {
110
- const filePath = path.join(taskMasterPath, file);
111
- try {
112
- await fs.access(filePath);
113
- fileStatus[file] = true;
114
- } catch (error) {
115
- fileStatus[file] = false;
116
- if (file === 'tasks/tasks.json') {
117
- hasEssentialFiles = false;
118
- }
119
- }
120
- }
121
-
122
- // Parse tasks.json if it exists for metadata
123
- let taskMetadata = null;
124
- if (fileStatus['tasks/tasks.json']) {
125
- try {
126
- const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
127
- const tasksContent = await fs.readFile(tasksPath, 'utf8');
128
- const tasksData = JSON.parse(tasksContent);
129
-
130
- // Handle both tagged and legacy formats
131
- let tasks = [];
132
- if (tasksData.tasks) {
133
- // Legacy format
134
- tasks = tasksData.tasks;
135
- } else {
136
- // Tagged format - get tasks from all tags
137
- Object.values(tasksData).forEach(tagData => {
138
- if (tagData.tasks) {
139
- tasks = tasks.concat(tagData.tasks);
140
- }
141
- });
142
- }
143
-
144
- // Calculate task statistics
145
- const stats = tasks.reduce((acc, task) => {
146
- acc.total++;
147
- acc[task.status] = (acc[task.status] || 0) + 1;
148
-
149
- // Count subtasks
150
- if (task.subtasks) {
151
- task.subtasks.forEach(subtask => {
152
- acc.subtotalTasks++;
153
- acc.subtasks = acc.subtasks || {};
154
- acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
155
- });
156
- }
157
-
158
- return acc;
159
- }, {
160
- total: 0,
161
- subtotalTasks: 0,
162
- pending: 0,
163
- 'in-progress': 0,
164
- done: 0,
165
- review: 0,
166
- deferred: 0,
167
- cancelled: 0,
168
- subtasks: {}
169
- });
170
-
171
- taskMetadata = {
172
- taskCount: stats.total,
173
- subtaskCount: stats.subtotalTasks,
174
- completed: stats.done || 0,
175
- pending: stats.pending || 0,
176
- inProgress: stats['in-progress'] || 0,
177
- review: stats.review || 0,
178
- completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
179
- lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
180
- };
181
- } catch (parseError) {
182
- console.warn('Failed to parse tasks.json:', parseError.message);
183
- taskMetadata = { error: 'Failed to parse tasks.json' };
184
- }
185
- }
186
-
187
- return {
188
- hasTaskmaster: true,
189
- hasEssentialFiles,
190
- files: fileStatus,
191
- metadata: taskMetadata,
192
- path: taskMasterPath
193
- };
194
-
195
- } catch (error) {
196
- console.error('Error detecting TaskMaster folder:', error);
197
- return {
198
- hasTaskmaster: false,
199
- reason: `Error checking directory: ${error.message}`
200
- };
201
- }
202
- }
203
-
204
- // Cache for extracted project directories
205
- const projectDirectoryCache = new Map();
206
-
207
- // Clear cache when needed (called when project files change)
208
- function clearProjectDirectoryCache() {
209
- projectDirectoryCache.clear();
210
- }
211
-
212
- // Load project configuration file
213
- async function loadProjectConfig() {
214
- const configPath = path.join(os.homedir(), '.claude', 'project-config.json');
215
- try {
216
- const configData = await fs.readFile(configPath, 'utf8');
217
- return JSON.parse(configData);
218
- } catch (error) {
219
- // Return empty config if file doesn't exist
220
- return {};
221
- }
222
- }
223
-
224
- // Save project configuration file
225
- async function saveProjectConfig(config) {
226
- const claudeDir = path.join(os.homedir(), '.claude');
227
- const configPath = path.join(claudeDir, 'project-config.json');
228
-
229
- // Ensure the .claude directory exists
230
- try {
231
- await fs.mkdir(claudeDir, { recursive: true });
232
- } catch (error) {
233
- if (error.code !== 'EEXIST') {
234
- throw error;
235
- }
236
- }
237
-
238
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
239
- }
240
-
241
- // Generate better display name from path
242
- async function generateDisplayName(projectName, actualProjectDir = null) {
243
- // Use actual project directory if provided, otherwise decode from project name
244
- let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
245
-
246
- // Try to read package.json from the project path
247
- try {
248
- const packageJsonPath = path.join(projectPath, 'package.json');
249
- const packageData = await fs.readFile(packageJsonPath, 'utf8');
250
- const packageJson = JSON.parse(packageData);
251
-
252
- // Return the name from package.json if it exists
253
- if (packageJson.name) {
254
- return packageJson.name;
255
- }
256
- } catch (error) {
257
- // Fall back to path-based naming if package.json doesn't exist or can't be read
258
- }
259
-
260
- // If it starts with /, it's an absolute path
261
- if (projectPath.startsWith('/')) {
262
- const parts = projectPath.split('/').filter(Boolean);
263
- // Return only the last folder name
264
- return parts[parts.length - 1] || projectPath;
265
- }
266
-
267
- return projectPath;
268
- }
269
-
270
- // Extract the actual project directory from JSONL sessions (with caching)
271
- async function extractProjectDirectory(projectName) {
272
- // Check cache first
273
- if (projectDirectoryCache.has(projectName)) {
274
- return projectDirectoryCache.get(projectName);
275
- }
276
-
277
- // Check project config for originalPath (manually added projects via UI or platform)
278
- // This handles projects with dashes in their directory names correctly
279
- const config = await loadProjectConfig();
280
- if (config[projectName]?.originalPath) {
281
- const originalPath = config[projectName].originalPath;
282
- projectDirectoryCache.set(projectName, originalPath);
283
- return originalPath;
284
- }
285
-
286
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
287
- const cwdCounts = new Map();
288
- let latestTimestamp = 0;
289
- let latestCwd = null;
290
- let extractedPath;
291
-
292
- try {
293
- // Check if the project directory exists
294
- await fs.access(projectDir);
295
-
296
- const files = await fs.readdir(projectDir);
297
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
298
-
299
- if (jsonlFiles.length === 0) {
300
- // Fall back to decoded project name if no sessions
301
- extractedPath = projectName.replace(/-/g, '/');
302
- } else {
303
- // Process all JSONL files to collect cwd values
304
- for (const file of jsonlFiles) {
305
- const jsonlFile = path.join(projectDir, file);
306
- const fileStream = fsSync.createReadStream(jsonlFile);
307
- const rl = readline.createInterface({
308
- input: fileStream,
309
- crlfDelay: Infinity
310
- });
311
-
312
- for await (const line of rl) {
313
- if (line.trim()) {
314
- try {
315
- const entry = JSON.parse(line);
316
-
317
- if (entry.cwd) {
318
- // Count occurrences of each cwd
319
- cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
320
-
321
- // Track the most recent cwd
322
- const timestamp = new Date(entry.timestamp || 0).getTime();
323
- if (timestamp > latestTimestamp) {
324
- latestTimestamp = timestamp;
325
- latestCwd = entry.cwd;
326
- }
327
- }
328
- } catch (parseError) {
329
- // Skip malformed lines
330
- }
331
- }
332
- }
333
- }
334
-
335
- // Determine the best cwd to use
336
- if (cwdCounts.size === 0) {
337
- // No cwd found, fall back to decoded project name
338
- extractedPath = projectName.replace(/-/g, '/');
339
- } else if (cwdCounts.size === 1) {
340
- // Only one cwd, use it
341
- extractedPath = Array.from(cwdCounts.keys())[0];
342
- } else {
343
- // Multiple cwd values - prefer the most recent one if it has reasonable usage
344
- const mostRecentCount = cwdCounts.get(latestCwd) || 0;
345
- const maxCount = Math.max(...cwdCounts.values());
346
-
347
- // Use most recent if it has at least 25% of the max count
348
- if (mostRecentCount >= maxCount * 0.25) {
349
- extractedPath = latestCwd;
350
- } else {
351
- // Otherwise use the most frequently used cwd
352
- for (const [cwd, count] of cwdCounts.entries()) {
353
- if (count === maxCount) {
354
- extractedPath = cwd;
355
- break;
356
- }
357
- }
358
- }
359
-
360
- // Fallback (shouldn't reach here)
361
- if (!extractedPath) {
362
- extractedPath = latestCwd || projectName.replace(/-/g, '/');
363
- }
364
- }
365
- }
366
-
367
- // Cache the result
368
- projectDirectoryCache.set(projectName, extractedPath);
369
-
370
- return extractedPath;
371
-
372
- } catch (error) {
373
- // If the directory doesn't exist, just use the decoded project name
374
- if (error.code === 'ENOENT') {
375
- extractedPath = projectName.replace(/-/g, '/');
376
- } else {
377
- console.error(`Error extracting project directory for ${projectName}:`, error);
378
- // Fall back to decoded project name for other errors
379
- extractedPath = projectName.replace(/-/g, '/');
380
- }
381
-
382
- // Cache the fallback result too
383
- projectDirectoryCache.set(projectName, extractedPath);
384
-
385
- return extractedPath;
386
- }
387
- }
388
-
389
- async function getProjects(progressCallback = null) {
390
- // Wrap with a timeout to prevent hanging on slow filesystems
391
- const timeoutMs = 15000;
392
- const result = await Promise.race([
393
- _getProjectsImpl(progressCallback),
394
- new Promise((_, reject) => setTimeout(() => reject(new Error('Projects scan timed out')), timeoutMs))
395
- ]).catch(err => {
396
- console.warn(`[WARN] getProjects failed: ${err.message}. Returning empty list.`);
397
- return [];
398
- });
399
- return result;
400
- }
401
-
402
- async function _getProjectsImpl(progressCallback = null) {
403
- const claudeDir = path.join(os.homedir(), '.claude', 'projects');
404
- const config = await loadProjectConfig();
405
- const projects = [];
406
- const codexSessionsIndexRef = { sessionsByProject: null };
407
-
408
- // Only load projects that were explicitly added by the user (manuallyAdded).
409
- // No auto-scanning of ~/.claude/projects/ — the user adds projects via the UI.
410
- const manualEntries = Object.entries(config).filter(([, cfg]) => cfg.manuallyAdded);
411
- const totalProjects = manualEntries.length;
412
- let processedProjects = 0;
413
-
414
- for (const [projectName, projectConfig] of manualEntries) {
415
- processedProjects++;
416
-
417
- if (progressCallback) {
418
- progressCallback({
419
- phase: 'loading',
420
- current: processedProjects,
421
- total: totalProjects,
422
- currentProject: projectName
423
- });
424
- }
425
-
426
- // Use the original path if available, otherwise extract from potential sessions
427
- let actualProjectDir = projectConfig.originalPath;
428
-
429
- if (!actualProjectDir) {
430
- try {
431
- actualProjectDir = await extractProjectDirectory(projectName);
432
- } catch (error) {
433
- // Fall back to decoded project name
434
- actualProjectDir = projectName.replace(/-/g, '/');
435
- }
436
- }
437
-
438
- const project = {
439
- name: projectName,
440
- path: actualProjectDir,
441
- displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
442
- fullPath: actualProjectDir,
443
- isCustomName: !!projectConfig.displayName,
444
- isManuallyAdded: true,
445
- sessions: [],
446
- sessionMeta: { hasMore: false, total: 0 },
447
- cursorSessions: [],
448
- codexSessions: []
449
- };
450
-
451
- // Check if a Claude project folder exists for this project (for session history)
452
- const projectDir = path.join(claudeDir, projectName);
453
- try {
454
- await fs.access(projectDir);
455
- const sessionResult = await getSessions(projectName, 5, 0);
456
- project.sessions = sessionResult.sessions || [];
457
- project.sessionMeta = {
458
- hasMore: sessionResult.hasMore,
459
- total: sessionResult.total
460
- };
461
- } catch (e) {
462
- // No Claude sessions — that's fine
463
- }
464
-
465
- // Fetch Cursor sessions
466
- try {
467
- project.cursorSessions = await getCursorSessions(actualProjectDir);
468
- } catch (e) {
469
- // No Cursor sessions
470
- }
471
-
472
- // Fetch Codex sessions
473
- try {
474
- project.codexSessions = await getCodexSessions(actualProjectDir, {
475
- indexRef: codexSessionsIndexRef,
476
- });
477
- } catch (e) {
478
- // No Codex sessions
479
- }
480
-
481
- // TaskMaster detection
482
- try {
483
- const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
484
- project.taskmaster = {
485
- status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles
486
- ? 'configured' : 'not-configured',
487
- hasTaskmaster: taskMasterResult.hasTaskmaster,
488
- hasEssentialFiles: taskMasterResult.hasEssentialFiles,
489
- metadata: taskMasterResult.metadata
490
- };
491
- } catch (error) {
492
- project.taskmaster = {
493
- status: 'error',
494
- hasTaskmaster: false,
495
- hasEssentialFiles: false,
496
- error: error.message
497
- };
498
- }
499
-
500
- projects.push(project);
501
- }
502
-
503
- if (progressCallback) {
504
- progressCallback({
505
- phase: 'complete',
506
- current: totalProjects,
507
- total: totalProjects
508
- });
509
- }
510
-
511
- return projects;
512
- }
513
-
514
- async function getSessions(projectName, limit = 5, offset = 0) {
515
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
516
-
517
- try {
518
- const files = await fs.readdir(projectDir);
519
- // agent-*.jsonl files contain session start data at this point. This needs to be revisited
520
- // periodically to make sure only accurate data is there and no new functionality is added there
521
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
522
-
523
- if (jsonlFiles.length === 0) {
524
- return { sessions: [], hasMore: false, total: 0 };
525
- }
526
-
527
- // Sort files by modification time (newest first)
528
- const filesWithStats = await Promise.all(
529
- jsonlFiles.map(async (file) => {
530
- const filePath = path.join(projectDir, file);
531
- const stats = await fs.stat(filePath);
532
- return { file, mtime: stats.mtime };
533
- })
534
- );
535
- filesWithStats.sort((a, b) => b.mtime - a.mtime);
536
-
537
- const allSessions = new Map();
538
- const allEntries = [];
539
- const uuidToSessionMap = new Map();
540
-
541
- // Collect all sessions and entries from all files
542
- for (const { file } of filesWithStats) {
543
- const jsonlFile = path.join(projectDir, file);
544
- const result = await parseJsonlSessions(jsonlFile);
545
-
546
- result.sessions.forEach(session => {
547
- if (!allSessions.has(session.id)) {
548
- allSessions.set(session.id, session);
549
- }
550
- });
551
-
552
- allEntries.push(...result.entries);
553
-
554
- // Early exit optimization for large projects
555
- if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
556
- break;
557
- }
558
- }
559
-
560
- // Build UUID-to-session mapping for timeline detection
561
- allEntries.forEach(entry => {
562
- if (entry.uuid && entry.sessionId) {
563
- uuidToSessionMap.set(entry.uuid, entry.sessionId);
564
- }
565
- });
566
-
567
- // Group sessions by first user message ID
568
- const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
569
- const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
570
-
571
- // Find the first user message for each session
572
- allEntries.forEach(entry => {
573
- if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
574
- // This is a first user message in a session (parentUuid is null)
575
- const firstUserMsgId = entry.uuid;
576
-
577
- if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
578
- sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
579
-
580
- const session = allSessions.get(entry.sessionId);
581
- if (session) {
582
- if (!sessionGroups.has(firstUserMsgId)) {
583
- sessionGroups.set(firstUserMsgId, {
584
- latestSession: session,
585
- allSessions: [session]
586
- });
587
- } else {
588
- const group = sessionGroups.get(firstUserMsgId);
589
- group.allSessions.push(session);
590
-
591
- // Update latest session if this one is more recent
592
- if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
593
- group.latestSession = session;
594
- }
595
- }
596
- }
597
- }
598
- }
599
- });
600
-
601
- // Collect all sessions that don't belong to any group (standalone sessions)
602
- const groupedSessionIds = new Set();
603
- sessionGroups.forEach(group => {
604
- group.allSessions.forEach(session => groupedSessionIds.add(session.id));
605
- });
606
-
607
- const standaloneSessionsArray = Array.from(allSessions.values())
608
- .filter(session => !groupedSessionIds.has(session.id));
609
-
610
- // Combine grouped sessions (only show latest from each group) + standalone sessions
611
- const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
612
- const session = { ...group.latestSession };
613
- // Add metadata about grouping
614
- if (group.allSessions.length > 1) {
615
- session.isGrouped = true;
616
- session.groupSize = group.allSessions.length;
617
- session.groupSessions = group.allSessions.map(s => s.id);
618
- }
619
- return session;
620
- });
621
- const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
622
- .filter(session => !session.summary.startsWith('{ "'))
623
- .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
624
-
625
- const total = visibleSessions.length;
626
- const paginatedSessions = visibleSessions.slice(offset, offset + limit);
627
- const hasMore = offset + limit < total;
628
-
629
- return {
630
- sessions: paginatedSessions,
631
- hasMore,
632
- total,
633
- offset,
634
- limit
635
- };
636
- } catch (error) {
637
- console.error(`Error reading sessions for project ${projectName}:`, error);
638
- return { sessions: [], hasMore: false, total: 0 };
639
- }
640
- }
641
-
642
- async function parseJsonlSessions(filePath) {
643
- const sessions = new Map();
644
- const entries = [];
645
- const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
646
-
647
- try {
648
- const fileStream = fsSync.createReadStream(filePath);
649
- const rl = readline.createInterface({
650
- input: fileStream,
651
- crlfDelay: Infinity
652
- });
653
-
654
- for await (const line of rl) {
655
- if (line.trim()) {
656
- try {
657
- const entry = JSON.parse(line);
658
- entries.push(entry);
659
-
660
- // Handle summary entries that don't have sessionId yet
661
- if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
662
- pendingSummaries.set(entry.leafUuid, entry.summary);
663
- }
664
-
665
- if (entry.sessionId) {
666
- if (!sessions.has(entry.sessionId)) {
667
- sessions.set(entry.sessionId, {
668
- id: entry.sessionId,
669
- summary: 'New Session',
670
- messageCount: 0,
671
- lastActivity: new Date(),
672
- cwd: entry.cwd || '',
673
- lastUserMessage: null,
674
- lastAssistantMessage: null
675
- });
676
- }
677
-
678
- const session = sessions.get(entry.sessionId);
679
-
680
- // Apply pending summary if this entry has a parentUuid that matches a pending summary
681
- if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
682
- session.summary = pendingSummaries.get(entry.parentUuid);
683
- }
684
-
685
- // Update summary from summary entries with sessionId
686
- if (entry.type === 'summary' && entry.summary) {
687
- session.summary = entry.summary;
688
- }
689
-
690
- // Track last user and assistant messages (skip system messages)
691
- if (entry.message?.role === 'user' && entry.message?.content) {
692
- const content = entry.message.content;
693
-
694
- // Extract text from array format if needed
695
- let textContent = content;
696
- if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
697
- textContent = content[0].text;
698
- }
699
-
700
- const isSystemMessage = typeof textContent === 'string' && (
701
- textContent.startsWith('<command-name>') ||
702
- textContent.startsWith('<command-message>') ||
703
- textContent.startsWith('<command-args>') ||
704
- textContent.startsWith('<local-command-stdout>') ||
705
- textContent.startsWith('<system-reminder>') ||
706
- textContent.startsWith('Caveat:') ||
707
- textContent.startsWith('This session is being continued from a previous') ||
708
- textContent.startsWith('Invalid API key') ||
709
- textContent.includes('{"subtasks":') || // Filter Task Master prompts
710
- textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
711
- textContent === 'Warmup' // Explicitly filter out "Warmup"
712
- );
713
-
714
- if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
715
- session.lastUserMessage = textContent;
716
- }
717
- } else if (entry.message?.role === 'assistant' && entry.message?.content) {
718
- // Skip API error messages using the isApiErrorMessage flag
719
- if (entry.isApiErrorMessage === true) {
720
- // Skip this message entirely
721
- } else {
722
- // Track last assistant text message
723
- let assistantText = null;
724
-
725
- if (Array.isArray(entry.message.content)) {
726
- for (const part of entry.message.content) {
727
- if (part.type === 'text' && part.text) {
728
- assistantText = part.text;
729
- }
730
- }
731
- } else if (typeof entry.message.content === 'string') {
732
- assistantText = entry.message.content;
733
- }
734
-
735
- // Additional filter for assistant messages with system content
736
- const isSystemAssistantMessage = typeof assistantText === 'string' && (
737
- assistantText.startsWith('Invalid API key') ||
738
- assistantText.includes('{"subtasks":') ||
739
- assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
740
- );
741
-
742
- if (assistantText && !isSystemAssistantMessage) {
743
- session.lastAssistantMessage = assistantText;
744
- }
745
- }
746
- }
747
-
748
- session.messageCount++;
749
-
750
- if (entry.timestamp) {
751
- session.lastActivity = new Date(entry.timestamp);
752
- }
753
- }
754
- } catch (parseError) {
755
- // Skip malformed lines silently
756
- }
757
- }
758
- }
759
-
760
- // After processing all entries, set final summary based on last message if no summary exists
761
- for (const session of sessions.values()) {
762
- if (session.summary === 'New Session') {
763
- // Prefer last user message, fall back to last assistant message
764
- const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
765
- if (lastMessage) {
766
- session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
767
- }
768
- }
769
- }
770
-
771
- // Filter out sessions that contain JSON responses (Task Master errors)
772
- const allSessions = Array.from(sessions.values());
773
- const filteredSessions = allSessions.filter(session => {
774
- const shouldFilter = session.summary.startsWith('{ "');
775
- if (shouldFilter) {
776
- }
777
- // Log a sample of summaries to debug
778
- if (Math.random() < 0.01) { // Log 1% of sessions
779
- }
780
- return !shouldFilter;
781
- });
782
-
783
-
784
- return {
785
- sessions: filteredSessions,
786
- entries: entries
787
- };
788
-
789
- } catch (error) {
790
- console.error('Error reading JSONL file:', error);
791
- return { sessions: [], entries: [] };
792
- }
793
- }
794
-
795
- // Parse an agent JSONL file and extract tool uses
796
- async function parseAgentTools(filePath) {
797
- const tools = [];
798
-
799
- try {
800
- const fileStream = fsSync.createReadStream(filePath);
801
- const rl = readline.createInterface({
802
- input: fileStream,
803
- crlfDelay: Infinity
804
- });
805
-
806
- for await (const line of rl) {
807
- if (line.trim()) {
808
- try {
809
- const entry = JSON.parse(line);
810
- // Look for assistant messages with tool_use
811
- if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
812
- for (const part of entry.message.content) {
813
- if (part.type === 'tool_use') {
814
- tools.push({
815
- toolId: part.id,
816
- toolName: part.name,
817
- toolInput: part.input,
818
- timestamp: entry.timestamp
819
- });
820
- }
821
- }
822
- }
823
- // Look for tool results
824
- if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
825
- for (const part of entry.message.content) {
826
- if (part.type === 'tool_result') {
827
- // Find the matching tool and add result
828
- const tool = tools.find(t => t.toolId === part.tool_use_id);
829
- if (tool) {
830
- tool.toolResult = {
831
- content: typeof part.content === 'string' ? part.content :
832
- Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
833
- JSON.stringify(part.content),
834
- isError: Boolean(part.is_error)
835
- };
836
- }
837
- }
838
- }
839
- }
840
- } catch (parseError) {
841
- // Skip malformed lines
842
- }
843
- }
844
- }
845
- } catch (error) {
846
- console.warn(`Error parsing agent file ${filePath}:`, error.message);
847
- }
848
-
849
- return tools;
850
- }
851
-
852
- // Get messages for a specific session with pagination support
853
- async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
854
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
855
-
856
- try {
857
- const files = await fs.readdir(projectDir);
858
- // agent-*.jsonl files contain subagent tool history - we'll process them separately
859
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
860
- const agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
861
-
862
- if (jsonlFiles.length === 0) {
863
- return { messages: [], total: 0, hasMore: false };
864
- }
865
-
866
- const messages = [];
867
- // Map of agentId -> tools for subagent tool grouping
868
- const agentToolsCache = new Map();
869
-
870
- // Process all JSONL files to find messages for this session
871
- for (const file of jsonlFiles) {
872
- const jsonlFile = path.join(projectDir, file);
873
- const fileStream = fsSync.createReadStream(jsonlFile);
874
- const rl = readline.createInterface({
875
- input: fileStream,
876
- crlfDelay: Infinity
877
- });
878
-
879
- for await (const line of rl) {
880
- if (line.trim()) {
881
- try {
882
- const entry = JSON.parse(line);
883
- if (entry.sessionId === sessionId) {
884
- messages.push(entry);
885
- }
886
- } catch (parseError) {
887
- console.warn('Error parsing line:', parseError.message);
888
- }
889
- }
890
- }
891
- }
892
-
893
- // Collect agentIds from Task tool results
894
- const agentIds = new Set();
895
- for (const message of messages) {
896
- if (message.toolUseResult?.agentId) {
897
- agentIds.add(message.toolUseResult.agentId);
898
- }
899
- }
900
-
901
- // Load agent tools for each agentId found
902
- for (const agentId of agentIds) {
903
- const agentFileName = `agent-${agentId}.jsonl`;
904
- if (agentFiles.includes(agentFileName)) {
905
- const agentFilePath = path.join(projectDir, agentFileName);
906
- const tools = await parseAgentTools(agentFilePath);
907
- agentToolsCache.set(agentId, tools);
908
- }
909
- }
910
-
911
- // Attach agent tools to their parent Task messages
912
- for (const message of messages) {
913
- if (message.toolUseResult?.agentId) {
914
- const agentId = message.toolUseResult.agentId;
915
- const agentTools = agentToolsCache.get(agentId);
916
- if (agentTools && agentTools.length > 0) {
917
- message.subagentTools = agentTools;
918
- }
919
- }
920
- }
921
-
922
- // Sort messages by timestamp
923
- const sortedMessages = messages.sort((a, b) =>
924
- new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
925
- );
926
-
927
- const total = sortedMessages.length;
928
-
929
- // If no limit is specified, return all messages (backward compatibility)
930
- if (limit === null) {
931
- return sortedMessages;
932
- }
933
-
934
- // Apply pagination - for recent messages, we need to slice from the end
935
- // offset 0 should give us the most recent messages
936
- const startIndex = Math.max(0, total - offset - limit);
937
- const endIndex = total - offset;
938
- const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
939
- const hasMore = startIndex > 0;
940
-
941
- return {
942
- messages: paginatedMessages,
943
- total,
944
- hasMore,
945
- offset,
946
- limit
947
- };
948
- } catch (error) {
949
- console.error(`Error reading messages for session ${sessionId}:`, error);
950
- return limit === null ? [] : { messages: [], total: 0, hasMore: false };
951
- }
952
- }
953
-
954
- // Rename a project's display name
955
- async function renameProject(projectName, newDisplayName) {
956
- const config = await loadProjectConfig();
957
-
958
- if (!newDisplayName || newDisplayName.trim() === '') {
959
- // Remove custom name if empty, will fall back to auto-generated
960
- delete config[projectName];
961
- } else {
962
- // Set custom display name
963
- config[projectName] = {
964
- displayName: newDisplayName.trim()
965
- };
966
- }
967
-
968
- await saveProjectConfig(config);
969
- return true;
970
- }
971
-
972
- // Delete a session from a project
973
- async function deleteSession(projectName, sessionId) {
974
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
975
-
976
- try {
977
- const files = await fs.readdir(projectDir);
978
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
979
-
980
- if (jsonlFiles.length === 0) {
981
- throw new Error('No session files found for this project');
982
- }
983
-
984
- // Check all JSONL files to find which one contains the session
985
- for (const file of jsonlFiles) {
986
- const jsonlFile = path.join(projectDir, file);
987
- const content = await fs.readFile(jsonlFile, 'utf8');
988
- const lines = content.split('\n').filter(line => line.trim());
989
-
990
- // Check if this file contains the session
991
- const hasSession = lines.some(line => {
992
- try {
993
- const data = JSON.parse(line);
994
- return data.sessionId === sessionId;
995
- } catch {
996
- return false;
997
- }
998
- });
999
-
1000
- if (hasSession) {
1001
- // Filter out all entries for this session
1002
- const filteredLines = lines.filter(line => {
1003
- try {
1004
- const data = JSON.parse(line);
1005
- return data.sessionId !== sessionId;
1006
- } catch {
1007
- return true; // Keep malformed lines
1008
- }
1009
- });
1010
-
1011
- // Write back the filtered content
1012
- await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
1013
- return true;
1014
- }
1015
- }
1016
-
1017
- throw new Error(`Session ${sessionId} not found in any files`);
1018
- } catch (error) {
1019
- console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
1020
- throw error;
1021
- }
1022
- }
1023
-
1024
- // Check if a project is empty (has no sessions)
1025
- async function isProjectEmpty(projectName) {
1026
- try {
1027
- const sessionsResult = await getSessions(projectName, 1, 0);
1028
- return sessionsResult.total === 0;
1029
- } catch (error) {
1030
- console.error(`Error checking if project ${projectName} is empty:`, error);
1031
- return false;
1032
- }
1033
- }
1034
-
1035
- // Delete a project (force=true to delete even with sessions)
1036
- async function deleteProject(projectName, force = false) {
1037
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1038
-
1039
- try {
1040
- const isEmpty = await isProjectEmpty(projectName);
1041
- if (!isEmpty && !force) {
1042
- throw new Error('Cannot delete project with existing sessions');
1043
- }
1044
-
1045
- const config = await loadProjectConfig();
1046
- let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
1047
-
1048
- // Fallback to extractProjectDirectory if projectPath is not in config
1049
- if (!projectPath) {
1050
- projectPath = await extractProjectDirectory(projectName);
1051
- }
1052
-
1053
- // Remove the project directory (includes all Claude sessions)
1054
- await fs.rm(projectDir, { recursive: true, force: true });
1055
-
1056
- // Delete all Codex sessions associated with this project
1057
- if (projectPath) {
1058
- try {
1059
- const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
1060
- for (const session of codexSessions) {
1061
- try {
1062
- await deleteCodexSession(session.id);
1063
- } catch (err) {
1064
- console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
1065
- }
1066
- }
1067
- } catch (err) {
1068
- console.warn('Failed to delete Codex sessions:', err.message);
1069
- }
1070
-
1071
- // Delete Cursor sessions directory if it exists
1072
- try {
1073
- const hash = crypto.createHash('md5').update(projectPath).digest('hex');
1074
- const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
1075
- await fs.rm(cursorProjectDir, { recursive: true, force: true });
1076
- } catch (err) {
1077
- // Cursor dir may not exist, ignore
1078
- }
1079
- }
1080
-
1081
- // Remove from project config
1082
- delete config[projectName];
1083
- await saveProjectConfig(config);
1084
-
1085
- return true;
1086
- } catch (error) {
1087
- console.error(`Error deleting project ${projectName}:`, error);
1088
- throw error;
1089
- }
1090
- }
1091
-
1092
- // Add a project manually to the config (without creating folders)
1093
- async function addProjectManually(projectPath, displayName = null) {
1094
- const absolutePath = path.resolve(projectPath);
1095
-
1096
- try {
1097
- // Check if the path exists
1098
- await fs.access(absolutePath);
1099
- } catch (error) {
1100
- throw new Error(`Path does not exist: ${absolutePath}`);
1101
- }
1102
-
1103
- // Generate project name (encode path for use as directory name)
1104
- const projectName = absolutePath.replace(/[\\/:\s~_]/g, '-');
1105
-
1106
- // Check if project already exists in config
1107
- const config = await loadProjectConfig();
1108
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1109
-
1110
- if (config[projectName]) {
1111
- // Project already exists — return it instead of erroring
1112
- return {
1113
- name: projectName,
1114
- path: absolutePath,
1115
- fullPath: absolutePath,
1116
- displayName: config[projectName].displayName || await generateDisplayName(projectName, absolutePath),
1117
- isManuallyAdded: true,
1118
- alreadyExists: true,
1119
- sessions: [],
1120
- cursorSessions: []
1121
- };
1122
- }
1123
-
1124
- // Allow adding projects even if the directory exists - this enables tracking
1125
- // existing AI assistant projects (Claude, Cursor, Codex, etc.) in the UI
1126
-
1127
- // Add to config as manually added project
1128
- config[projectName] = {
1129
- manuallyAdded: true,
1130
- originalPath: absolutePath
1131
- };
1132
-
1133
- if (displayName) {
1134
- config[projectName].displayName = displayName;
1135
- }
1136
-
1137
- await saveProjectConfig(config);
1138
-
1139
-
1140
- return {
1141
- name: projectName,
1142
- path: absolutePath,
1143
- fullPath: absolutePath,
1144
- displayName: displayName || await generateDisplayName(projectName, absolutePath),
1145
- isManuallyAdded: true,
1146
- sessions: [],
1147
- cursorSessions: []
1148
- };
1149
- }
1150
-
1151
- // Fetch Cursor sessions for a given project path
1152
- async function getCursorSessions(projectPath) {
1153
- try {
1154
- // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
1155
- const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
1156
- const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
1157
-
1158
- // Check if the directory exists
1159
- try {
1160
- await fs.access(cursorChatsPath);
1161
- } catch (error) {
1162
- // No sessions for this project
1163
- return [];
1164
- }
1165
-
1166
- // List all session directories
1167
- const sessionDirs = await fs.readdir(cursorChatsPath);
1168
- const sessions = [];
1169
-
1170
- for (const sessionId of sessionDirs) {
1171
- const sessionPath = path.join(cursorChatsPath, sessionId);
1172
- const storeDbPath = path.join(sessionPath, 'store.db');
1173
-
1174
- try {
1175
- // Check if store.db exists
1176
- await fs.access(storeDbPath);
1177
-
1178
- // Capture store.db mtime as a reliable fallback timestamp
1179
- let dbStatMtimeMs = null;
1180
- try {
1181
- const stat = await fs.stat(storeDbPath);
1182
- dbStatMtimeMs = stat.mtimeMs;
1183
- } catch (_) {}
1184
-
1185
- // Open SQLite database (requires native sqlite3 module)
1186
- if (!sqliteOpen || !sqlite3) {
1187
- continue; // Skip on Vercel where native modules aren't available
1188
- }
1189
- const db = await sqliteOpen({
1190
- filename: storeDbPath,
1191
- driver: sqlite3.Database,
1192
- mode: sqlite3.OPEN_READONLY
1193
- });
1194
-
1195
- // Get metadata from meta table
1196
- const metaRows = await db.all(`
1197
- SELECT key, value FROM meta
1198
- `);
1199
-
1200
- // Parse metadata
1201
- let metadata = {};
1202
- for (const row of metaRows) {
1203
- if (row.value) {
1204
- try {
1205
- // Try to decode as hex-encoded JSON
1206
- const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
1207
- if (hexMatch) {
1208
- const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
1209
- metadata[row.key] = JSON.parse(jsonStr);
1210
- } else {
1211
- metadata[row.key] = row.value.toString();
1212
- }
1213
- } catch (e) {
1214
- metadata[row.key] = row.value.toString();
1215
- }
1216
- }
1217
- }
1218
-
1219
- // Get message count
1220
- const messageCountResult = await db.get(`
1221
- SELECT COUNT(*) as count FROM blobs
1222
- `);
1223
-
1224
- await db.close();
1225
-
1226
- // Extract session info
1227
- const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
1228
-
1229
- // Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
1230
- let createdAt = null;
1231
- if (metadata.createdAt) {
1232
- createdAt = new Date(metadata.createdAt).toISOString();
1233
- } else if (dbStatMtimeMs) {
1234
- createdAt = new Date(dbStatMtimeMs).toISOString();
1235
- } else {
1236
- createdAt = new Date().toISOString();
1237
- }
1238
-
1239
- sessions.push({
1240
- id: sessionId,
1241
- name: sessionName,
1242
- createdAt: createdAt,
1243
- lastActivity: createdAt, // For compatibility with Claude sessions
1244
- messageCount: messageCountResult.count || 0,
1245
- projectPath: projectPath
1246
- });
1247
-
1248
- } catch (error) {
1249
- console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
1250
- }
1251
- }
1252
-
1253
- // Sort sessions by creation time (newest first)
1254
- sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
1255
-
1256
- // Return only the first 5 sessions for performance
1257
- return sessions.slice(0, 5);
1258
-
1259
- } catch (error) {
1260
- console.error('Error fetching Cursor sessions:', error);
1261
- return [];
1262
- }
1263
- }
1264
-
1265
-
1266
- function normalizeComparablePath(inputPath) {
1267
- if (!inputPath || typeof inputPath !== 'string') {
1268
- return '';
1269
- }
1270
-
1271
- const withoutLongPathPrefix = inputPath.startsWith('\\\\?\\')
1272
- ? inputPath.slice(4)
1273
- : inputPath;
1274
- const normalized = path.normalize(withoutLongPathPrefix.trim());
1275
-
1276
- if (!normalized) {
1277
- return '';
1278
- }
1279
-
1280
- const resolved = path.resolve(normalized);
1281
- return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1282
- }
1283
-
1284
- async function findCodexJsonlFiles(dir) {
1285
- const files = [];
1286
-
1287
- try {
1288
- const entries = await fs.readdir(dir, { withFileTypes: true });
1289
- for (const entry of entries) {
1290
- const fullPath = path.join(dir, entry.name);
1291
- if (entry.isDirectory()) {
1292
- files.push(...await findCodexJsonlFiles(fullPath));
1293
- } else if (entry.name.endsWith('.jsonl')) {
1294
- files.push(fullPath);
1295
- }
1296
- }
1297
- } catch (error) {
1298
- // Skip directories we can't read
1299
- }
1300
-
1301
- return files;
1302
- }
1303
-
1304
- async function buildCodexSessionsIndex() {
1305
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1306
- const sessionsByProject = new Map();
1307
-
1308
- try {
1309
- await fs.access(codexSessionsDir);
1310
- } catch (error) {
1311
- return sessionsByProject;
1312
- }
1313
-
1314
- const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
1315
-
1316
- for (const filePath of jsonlFiles) {
1317
- try {
1318
- const sessionData = await parseCodexSessionFile(filePath);
1319
- if (!sessionData || !sessionData.id) {
1320
- continue;
1321
- }
1322
-
1323
- const normalizedProjectPath = normalizeComparablePath(sessionData.cwd);
1324
- if (!normalizedProjectPath) {
1325
- continue;
1326
- }
1327
-
1328
- const session = {
1329
- id: sessionData.id,
1330
- summary: sessionData.summary || 'Codex Session',
1331
- messageCount: sessionData.messageCount || 0,
1332
- lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
1333
- cwd: sessionData.cwd,
1334
- model: sessionData.model,
1335
- filePath,
1336
- provider: 'codex',
1337
- };
1338
-
1339
- if (!sessionsByProject.has(normalizedProjectPath)) {
1340
- sessionsByProject.set(normalizedProjectPath, []);
1341
- }
1342
-
1343
- sessionsByProject.get(normalizedProjectPath).push(session);
1344
- } catch (error) {
1345
- console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
1346
- }
1347
- }
1348
-
1349
- for (const sessions of sessionsByProject.values()) {
1350
- sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1351
- }
1352
-
1353
- return sessionsByProject;
1354
- }
1355
-
1356
- // Fetch Codex sessions for a given project path
1357
- async function getCodexSessions(projectPath, options = {}) {
1358
- const { limit = 5, indexRef = null } = options;
1359
- try {
1360
- const normalizedProjectPath = normalizeComparablePath(projectPath);
1361
- if (!normalizedProjectPath) {
1362
- return [];
1363
- }
1364
-
1365
- if (indexRef && !indexRef.sessionsByProject) {
1366
- indexRef.sessionsByProject = await buildCodexSessionsIndex();
1367
- }
1368
-
1369
- const sessionsByProject = indexRef?.sessionsByProject || await buildCodexSessionsIndex();
1370
- const sessions = sessionsByProject.get(normalizedProjectPath) || [];
1371
-
1372
- // Return limited sessions for performance (0 = unlimited for deletion)
1373
- return limit > 0 ? sessions.slice(0, limit) : [...sessions];
1374
-
1375
- } catch (error) {
1376
- console.error('Error fetching Codex sessions:', error);
1377
- return [];
1378
- }
1379
- }
1380
-
1381
- // Parse a Codex session JSONL file to extract metadata
1382
- async function parseCodexSessionFile(filePath) {
1383
- try {
1384
- const fileStream = fsSync.createReadStream(filePath);
1385
- const rl = readline.createInterface({
1386
- input: fileStream,
1387
- crlfDelay: Infinity
1388
- });
1389
-
1390
- let sessionMeta = null;
1391
- let lastTimestamp = null;
1392
- let lastUserMessage = null;
1393
- let messageCount = 0;
1394
-
1395
- for await (const line of rl) {
1396
- if (line.trim()) {
1397
- try {
1398
- const entry = JSON.parse(line);
1399
-
1400
- // Track timestamp
1401
- if (entry.timestamp) {
1402
- lastTimestamp = entry.timestamp;
1403
- }
1404
-
1405
- // Extract session metadata
1406
- if (entry.type === 'session_meta' && entry.payload) {
1407
- sessionMeta = {
1408
- id: entry.payload.id,
1409
- cwd: entry.payload.cwd,
1410
- model: entry.payload.model || entry.payload.model_provider,
1411
- timestamp: entry.timestamp,
1412
- git: entry.payload.git
1413
- };
1414
- }
1415
-
1416
- // Count messages and extract user messages for summary
1417
- if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') {
1418
- messageCount++;
1419
- if (entry.payload.message) {
1420
- lastUserMessage = entry.payload.message;
1421
- }
1422
- }
1423
-
1424
- if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
1425
- messageCount++;
1426
- }
1427
-
1428
- } catch (parseError) {
1429
- // Skip malformed lines
1430
- }
1431
- }
1432
- }
1433
-
1434
- if (sessionMeta) {
1435
- return {
1436
- ...sessionMeta,
1437
- timestamp: lastTimestamp || sessionMeta.timestamp,
1438
- summary: lastUserMessage ?
1439
- (lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage) :
1440
- 'Codex Session',
1441
- messageCount
1442
- };
1443
- }
1444
-
1445
- return null;
1446
-
1447
- } catch (error) {
1448
- console.error('Error parsing Codex session file:', error);
1449
- return null;
1450
- }
1451
- }
1452
-
1453
- // Get messages for a specific Codex session
1454
- async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1455
- try {
1456
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1457
-
1458
- // Find the session file by searching for the session ID
1459
- const findSessionFile = async (dir) => {
1460
- try {
1461
- const entries = await fs.readdir(dir, { withFileTypes: true });
1462
- for (const entry of entries) {
1463
- const fullPath = path.join(dir, entry.name);
1464
- if (entry.isDirectory()) {
1465
- const found = await findSessionFile(fullPath);
1466
- if (found) return found;
1467
- } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
1468
- return fullPath;
1469
- }
1470
- }
1471
- } catch (error) {
1472
- // Skip directories we can't read
1473
- }
1474
- return null;
1475
- };
1476
-
1477
- const sessionFilePath = await findSessionFile(codexSessionsDir);
1478
-
1479
- if (!sessionFilePath) {
1480
- console.warn(`Codex session file not found for session ${sessionId}`);
1481
- return { messages: [], total: 0, hasMore: false };
1482
- }
1483
-
1484
- const messages = [];
1485
- let tokenUsage = null;
1486
- const fileStream = fsSync.createReadStream(sessionFilePath);
1487
- const rl = readline.createInterface({
1488
- input: fileStream,
1489
- crlfDelay: Infinity
1490
- });
1491
-
1492
- // Helper to extract text from Codex content array
1493
- const extractText = (content) => {
1494
- if (!Array.isArray(content)) return content;
1495
- return content
1496
- .map(item => {
1497
- if (item.type === 'input_text' || item.type === 'output_text') {
1498
- return item.text;
1499
- }
1500
- if (item.type === 'text') {
1501
- return item.text;
1502
- }
1503
- return '';
1504
- })
1505
- .filter(Boolean)
1506
- .join('\n');
1507
- };
1508
-
1509
- for await (const line of rl) {
1510
- if (line.trim()) {
1511
- try {
1512
- const entry = JSON.parse(line);
1513
-
1514
- // Extract token usage from token_count events (keep latest)
1515
- if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1516
- const info = entry.payload.info;
1517
- if (info.total_token_usage) {
1518
- tokenUsage = {
1519
- used: info.total_token_usage.total_tokens || 0,
1520
- total: info.model_context_window || 200000
1521
- };
1522
- }
1523
- }
1524
-
1525
- // Extract messages from response_item
1526
- if (entry.type === 'response_item' && entry.payload?.type === 'message') {
1527
- const content = entry.payload.content;
1528
- const role = entry.payload.role || 'assistant';
1529
- const textContent = extractText(content);
1530
-
1531
- // Skip system context messages (environment_context)
1532
- if (textContent?.includes('<environment_context>')) {
1533
- continue;
1534
- }
1535
-
1536
- // Only add if there's actual content
1537
- if (textContent?.trim()) {
1538
- messages.push({
1539
- type: role === 'user' ? 'user' : 'assistant',
1540
- timestamp: entry.timestamp,
1541
- message: {
1542
- role: role,
1543
- content: textContent
1544
- }
1545
- });
1546
- }
1547
- }
1548
-
1549
- if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
1550
- const summaryText = entry.payload.summary
1551
- ?.map(s => s.text)
1552
- .filter(Boolean)
1553
- .join('\n');
1554
- if (summaryText?.trim()) {
1555
- messages.push({
1556
- type: 'thinking',
1557
- timestamp: entry.timestamp,
1558
- message: {
1559
- role: 'assistant',
1560
- content: summaryText
1561
- }
1562
- });
1563
- }
1564
- }
1565
-
1566
- if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
1567
- let toolName = entry.payload.name;
1568
- let toolInput = entry.payload.arguments;
1569
-
1570
- // Map Codex tool names to Claude equivalents
1571
- if (toolName === 'shell_command') {
1572
- toolName = 'Bash';
1573
- try {
1574
- const args = JSON.parse(entry.payload.arguments);
1575
- toolInput = JSON.stringify({ command: args.command });
1576
- } catch (e) {
1577
- // Keep original if parsing fails
1578
- }
1579
- }
1580
-
1581
- messages.push({
1582
- type: 'tool_use',
1583
- timestamp: entry.timestamp,
1584
- toolName: toolName,
1585
- toolInput: toolInput,
1586
- toolCallId: entry.payload.call_id
1587
- });
1588
- }
1589
-
1590
- if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
1591
- messages.push({
1592
- type: 'tool_result',
1593
- timestamp: entry.timestamp,
1594
- toolCallId: entry.payload.call_id,
1595
- output: entry.payload.output
1596
- });
1597
- }
1598
-
1599
- if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
1600
- const toolName = entry.payload.name || 'custom_tool';
1601
- const input = entry.payload.input || '';
1602
-
1603
- if (toolName === 'apply_patch') {
1604
- // Parse Codex patch format and convert to Claude Edit format
1605
- const fileMatch = input.match(/\*\*\* Update File: (.+)/);
1606
- const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
1607
-
1608
- // Extract old and new content from patch
1609
- const lines = input.split('\n');
1610
- const oldLines = [];
1611
- const newLines = [];
1612
-
1613
- for (const line of lines) {
1614
- if (line.startsWith('-') && !line.startsWith('---')) {
1615
- oldLines.push(line.substring(1));
1616
- } else if (line.startsWith('+') && !line.startsWith('+++')) {
1617
- newLines.push(line.substring(1));
1618
- }
1619
- }
1620
-
1621
- messages.push({
1622
- type: 'tool_use',
1623
- timestamp: entry.timestamp,
1624
- toolName: 'Edit',
1625
- toolInput: JSON.stringify({
1626
- file_path: filePath,
1627
- old_string: oldLines.join('\n'),
1628
- new_string: newLines.join('\n')
1629
- }),
1630
- toolCallId: entry.payload.call_id
1631
- });
1632
- } else {
1633
- messages.push({
1634
- type: 'tool_use',
1635
- timestamp: entry.timestamp,
1636
- toolName: toolName,
1637
- toolInput: input,
1638
- toolCallId: entry.payload.call_id
1639
- });
1640
- }
1641
- }
1642
-
1643
- if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
1644
- messages.push({
1645
- type: 'tool_result',
1646
- timestamp: entry.timestamp,
1647
- toolCallId: entry.payload.call_id,
1648
- output: entry.payload.output || ''
1649
- });
1650
- }
1651
-
1652
- } catch (parseError) {
1653
- // Skip malformed lines
1654
- }
1655
- }
1656
- }
1657
-
1658
- // Sort by timestamp
1659
- messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
1660
-
1661
- const total = messages.length;
1662
-
1663
- // Apply pagination if limit is specified
1664
- if (limit !== null) {
1665
- const startIndex = Math.max(0, total - offset - limit);
1666
- const endIndex = total - offset;
1667
- const paginatedMessages = messages.slice(startIndex, endIndex);
1668
- const hasMore = startIndex > 0;
1669
-
1670
- return {
1671
- messages: paginatedMessages,
1672
- total,
1673
- hasMore,
1674
- offset,
1675
- limit,
1676
- tokenUsage
1677
- };
1678
- }
1679
-
1680
- return { messages, tokenUsage };
1681
-
1682
- } catch (error) {
1683
- console.error(`Error reading Codex session messages for ${sessionId}:`, error);
1684
- return { messages: [], total: 0, hasMore: false };
1685
- }
1686
- }
1687
-
1688
- async function deleteCodexSession(sessionId) {
1689
- try {
1690
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1691
-
1692
- const findJsonlFiles = async (dir) => {
1693
- const files = [];
1694
- try {
1695
- const entries = await fs.readdir(dir, { withFileTypes: true });
1696
- for (const entry of entries) {
1697
- const fullPath = path.join(dir, entry.name);
1698
- if (entry.isDirectory()) {
1699
- files.push(...await findJsonlFiles(fullPath));
1700
- } else if (entry.name.endsWith('.jsonl')) {
1701
- files.push(fullPath);
1702
- }
1703
- }
1704
- } catch (error) {}
1705
- return files;
1706
- };
1707
-
1708
- const jsonlFiles = await findJsonlFiles(codexSessionsDir);
1709
-
1710
- for (const filePath of jsonlFiles) {
1711
- const sessionData = await parseCodexSessionFile(filePath);
1712
- if (sessionData && sessionData.id === sessionId) {
1713
- await fs.unlink(filePath);
1714
- return true;
1715
- }
1716
- }
1717
-
1718
- throw new Error(`Codex session file not found for session ${sessionId}`);
1719
- } catch (error) {
1720
- console.error(`Error deleting Codex session ${sessionId}:`, error);
1721
- throw error;
1722
- }
1723
- }
1724
-
1725
- export {
1726
- getProjects,
1727
- getSessions,
1728
- getSessionMessages,
1729
- parseJsonlSessions,
1730
- renameProject,
1731
- deleteSession,
1732
- isProjectEmpty,
1733
- deleteProject,
1734
- addProjectManually,
1735
- loadProjectConfig,
1736
- saveProjectConfig,
1737
- extractProjectDirectory,
1738
- clearProjectDirectoryCache,
1739
- getCodexSessions,
1740
- getCodexSessionMessages,
1741
- deleteCodexSession
1742
- };