upfynai-code 0.1.0 → 2.2.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 (340) hide show
  1. package/client/dist/api-docs.html +879 -0
  2. package/client/dist/assets/AppContent-CTSHQdyq.js +513 -0
  3. package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +6 -0
  4. package/client/dist/assets/CanvasPanel-q4HEqNtV.css +1 -0
  5. package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/client/dist/assets/LoginModal-silya-zP.js +11 -0
  65. package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +1 -0
  66. package/client/dist/assets/Onboarding-Coxo6mFA.js +1 -0
  67. package/client/dist/assets/SetupForm-BzYOsbji.js +1 -0
  68. package/client/dist/assets/Tableau10-B-NsZVaP.js +1 -0
  69. package/client/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  70. package/client/dist/assets/ar-SA-G6X2FPQ2-Bmw2-hDt.js +10 -0
  71. package/client/dist/assets/arc-BMqY7_Ci.js +1 -0
  72. package/client/dist/assets/array-BKyUJesY.js +1 -0
  73. package/client/dist/assets/az-AZ-76LH7QW2-Dh1le_qs.js +1 -0
  74. package/client/dist/assets/bg-BG-XCXSNQG7-Cbav8Z9z.js +5 -0
  75. package/client/dist/assets/blockDiagram-38ab4fdb-ChHJxsXw.js +118 -0
  76. package/client/dist/assets/bn-BD-2XOGV67Q-DCNjOaWz.js +5 -0
  77. package/client/dist/assets/c4Diagram-3d4e48cf-b8Xue4Z6.js +10 -0
  78. package/client/dist/assets/ca-ES-6MX7JW3Y-Dl_vM7NS.js +8 -0
  79. package/client/dist/assets/channel-CSnvHe_M.js +1 -0
  80. package/client/dist/assets/classDiagram-70f12bd4-BheP7Ggo.js +2 -0
  81. package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +2 -0
  82. package/client/dist/assets/clone-B75abXxS.js +1 -0
  83. package/client/dist/assets/createText-2e5e7dd3-_n4jI_fO.js +5 -0
  84. package/client/dist/assets/cs-CZ-2BRQDIVT-ftsKDdz4.js +11 -0
  85. package/client/dist/assets/da-DK-5WZEPLOC-DAjdwGRO.js +5 -0
  86. package/client/dist/assets/de-DE-XR44H4JA-BJXczHGT.js +8 -0
  87. package/client/dist/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
  88. package/client/dist/assets/directory-open-4ed118d0-CunoC1EB.js +1 -0
  89. package/client/dist/assets/edges-e0da2a9e-CfPZr4YM.js +4 -0
  90. package/client/dist/assets/el-GR-BZB4AONW-DW2p_uy7.js +10 -0
  91. package/client/dist/assets/erDiagram-9861fffd-CF33V-Of.js +51 -0
  92. package/client/dist/assets/es-ES-U4NZUMDT-DLOIGnrl.js +9 -0
  93. package/client/dist/assets/eu-ES-A7QVB2H4-LJXbf89m.js +11 -0
  94. package/client/dist/assets/fa-IR-HGAKTJCU-Dvx65fgW.js +8 -0
  95. package/client/dist/assets/fi-FI-Z5N7JZ37-EoL65BQh.js +6 -0
  96. package/client/dist/assets/file-open-002ab408-DIuFHtCF.js +1 -0
  97. package/client/dist/assets/file-open-7c801643-684qeFg4.js +1 -0
  98. package/client/dist/assets/file-save-3189631c-C1wFhQhH.js +1 -0
  99. package/client/dist/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
  100. package/client/dist/assets/flowDb-956e92f1-HgoXVy2H.js +10 -0
  101. package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +4 -0
  102. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +1 -0
  103. package/client/dist/assets/flowchart-elk-definition-4a651766-DJbI2dpv.js +139 -0
  104. package/client/dist/assets/fr-FR-RHASNOE6-DNk_jdDs.js +9 -0
  105. package/client/dist/assets/ganttDiagram-c361ad54-2XX670FU.js +257 -0
  106. package/client/dist/assets/gitGraphDiagram-72cf32ee-CcUfruAo.js +70 -0
  107. package/client/dist/assets/gl-ES-HMX3MZ6V-dxzFjZlG.js +10 -0
  108. package/client/dist/assets/graph-BSbiMSBC.js +1 -0
  109. package/client/dist/assets/he-IL-6SHJWFNN-Cogsfdt1.js +10 -0
  110. package/client/dist/assets/hi-IN-IWLTKZ5I-L6wbgi4F.js +4 -0
  111. package/client/dist/assets/hu-HU-A5ZG7DT2-DSA6ZDsH.js +7 -0
  112. package/client/dist/assets/id-ID-SAP4L64H-BK_vGGS6.js +10 -0
  113. package/client/dist/assets/image-blob-reduce.esm-BLtmMM_J.js +2 -0
  114. package/client/dist/assets/index-3862675e-Bv32HUgT.js +1 -0
  115. package/client/dist/assets/index-B8wwD_Xo.css +1 -0
  116. package/client/dist/assets/index-BPwf8Fw3.js +27 -0
  117. package/client/dist/assets/index-D1urGMYu.js +95 -0
  118. package/client/dist/assets/infoDiagram-f8f76790-w4mR4pxn.js +7 -0
  119. package/client/dist/assets/init-Gi6I4Gst.js +1 -0
  120. package/client/dist/assets/it-IT-JPQ66NNP-BLdHYMhn.js +11 -0
  121. package/client/dist/assets/ja-JP-DBVTYXUO-B_vmexl_.js +8 -0
  122. package/client/dist/assets/journeyDiagram-49397b02-D9nmO17e.js +139 -0
  123. package/client/dist/assets/kaa-6HZHGXH3-5s-3jl6F.js +1 -0
  124. package/client/dist/assets/kab-KAB-ZGHBKWFO-2QaVDuSf.js +8 -0
  125. package/client/dist/assets/kk-KZ-P5N5QNE5-CTC52Vbi.js +1 -0
  126. package/client/dist/assets/km-KH-HSX4SM5Z-DxawH8UZ.js +11 -0
  127. package/client/dist/assets/ko-KR-MTYHY66A-CmosEM8_.js +9 -0
  128. package/client/dist/assets/ku-TR-6OUDTVRD-DbiLen4y.js +9 -0
  129. package/client/dist/assets/layout-jmt3H9tA.js +1 -0
  130. package/client/dist/assets/line-JTlRayUJ.js +1 -0
  131. package/client/dist/assets/linear-DJeB5p7x.js +1 -0
  132. package/client/dist/assets/lt-LT-XHIRWOB4-CH15wrjA.js +3 -0
  133. package/client/dist/assets/lv-LV-5QDEKY6T-dhgfPuCQ.js +7 -0
  134. package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +415 -0
  135. package/client/dist/assets/mr-IN-CRQNXWMA-3Gi6iq7A.js +13 -0
  136. package/client/dist/assets/my-MM-5M5IBNSE-CpH4rdJj.js +1 -0
  137. package/client/dist/assets/nb-NO-T6EIAALU-Du6iiGql.js +10 -0
  138. package/client/dist/assets/nl-NL-IS3SIHDZ-BGvsd1MT.js +8 -0
  139. package/client/dist/assets/nn-NO-6E72VCQL-B-odvJZW.js +8 -0
  140. package/client/dist/assets/oc-FR-POXYY2M6-COC8xNjo.js +8 -0
  141. package/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  142. package/client/dist/assets/pa-IN-N4M65BXN-CE21PUQH.js +4 -0
  143. package/client/dist/assets/path-CbwjOpE9.js +1 -0
  144. package/client/dist/assets/pdf-TYrZqVzP.js +12 -0
  145. package/client/dist/assets/pdf.worker-BA9kU3Pw.mjs +61080 -0
  146. package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +199 -0
  147. package/client/dist/assets/pica-VkdyTzi8.js +2 -0
  148. package/client/dist/assets/pieDiagram-8a3498a8-Cvfh7Qr5.js +35 -0
  149. package/client/dist/assets/pl-PL-T2D74RX3-D4xFVSoT.js +9 -0
  150. package/client/dist/assets/pt-BR-5N22H2LF-CCq257gA.js +9 -0
  151. package/client/dist/assets/pt-PT-UZXXM6DQ-1l8gt5vA.js +9 -0
  152. package/client/dist/assets/quadrantDiagram-120e2f19-BA0js1aD.js +7 -0
  153. package/client/dist/assets/requirementDiagram-deff3bca-B0QNFfIn.js +52 -0
  154. package/client/dist/assets/ro-RO-JPDTUUEW-yosBW01E.js +11 -0
  155. package/client/dist/assets/roundRect-mAH3dD0p.js +1 -0
  156. package/client/dist/assets/ru-RU-B4JR7IUQ-8LkEJUix.js +9 -0
  157. package/client/dist/assets/sankeyDiagram-04a897e0-D4T9eCXn.js +8 -0
  158. package/client/dist/assets/sequenceDiagram-704730f1-CfBUTCrO.js +122 -0
  159. package/client/dist/assets/si-LK-N5RQ5JYF-D8rjbqtd.js +1 -0
  160. package/client/dist/assets/sk-SK-C5VTKIMK-Bg14sAzN.js +6 -0
  161. package/client/dist/assets/sl-SI-NN7IZMDC-CMTib6Zs.js +6 -0
  162. package/client/dist/assets/stateDiagram-587899a1-BGgvmVSZ.js +1 -0
  163. package/client/dist/assets/stateDiagram-v2-d93cdb3a-Qn3DpYuO.js +1 -0
  164. package/client/dist/assets/styles-6aaf32cf-IdVZLPrD.js +207 -0
  165. package/client/dist/assets/styles-9a916d00-BAC3L45X.js +160 -0
  166. package/client/dist/assets/styles-c10674c1-COhXxX8c.js +116 -0
  167. package/client/dist/assets/subset-shared.chunk-BWHnFai4.js +22 -0
  168. package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +1 -0
  169. package/client/dist/assets/sv-SE-XGPEYMSR-C1425rOF.js +10 -0
  170. package/client/dist/assets/svgDrawCommon-08f97a94-Cfk-fgnN.js +1 -0
  171. package/client/dist/assets/ta-IN-2NMHFXQM-BHHo1zpF.js +9 -0
  172. package/client/dist/assets/th-TH-HPSO5L25-CZVzm_WT.js +2 -0
  173. package/client/dist/assets/timeline-definition-85554ec2-VAvuJith.js +61 -0
  174. package/client/dist/assets/tr-TR-DEFEU3FU-DE1lclCq.js +7 -0
  175. package/client/dist/assets/uk-UA-QMV73CPH-D4lJZ85O.js +6 -0
  176. package/client/dist/assets/vendor-codemirror-BARtJV1V.js +16 -0
  177. package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +20 -0
  178. package/client/dist/assets/vendor-i18n-ByAl-gdx.js +1 -0
  179. package/client/dist/assets/vendor-icons-D33IkSIf.js +1 -0
  180. package/client/dist/assets/vendor-markdown-CIVH08vJ.js +298 -0
  181. package/client/dist/assets/vendor-react-CHoMc7ka.js +8 -0
  182. package/client/dist/assets/vendor-syntax-Djb62v3a.js +9 -0
  183. package/client/dist/assets/vendor-xterm-DBb3RXlu.js +66 -0
  184. package/client/dist/assets/vendor-xterm-DrlLKa8f.css +1 -0
  185. package/client/dist/assets/vi-VN-M7AON7JQ-Dgc_SShk.js +5 -0
  186. package/client/dist/assets/xychartDiagram-e933f94c-BeyVBJhb.js +7 -0
  187. package/client/dist/assets/zh-CN-LNUGB5OW-MH4Yh8in.js +10 -0
  188. package/client/dist/assets/zh-HK-E62DVLB3-D4XHehjx.js +1 -0
  189. package/client/dist/assets/zh-TW-RAJ6MFWO--efj3evj.js +9 -0
  190. package/client/dist/clear-cache.html +85 -0
  191. package/client/dist/convert-icons.md +53 -0
  192. package/client/dist/favicon.png +0 -0
  193. package/client/dist/favicon.svg +9 -0
  194. package/client/dist/generate-icons.js +49 -0
  195. package/client/dist/icons/claude-ai-icon.svg +1 -0
  196. package/client/dist/icons/codex-white.svg +3 -0
  197. package/client/dist/icons/codex.svg +3 -0
  198. package/client/dist/icons/cursor-white.svg +12 -0
  199. package/client/dist/icons/cursor.svg +1 -0
  200. package/client/dist/icons/icon-128x128.png +0 -0
  201. package/client/dist/icons/icon-128x128.svg +12 -0
  202. package/client/dist/icons/icon-144x144.png +0 -0
  203. package/client/dist/icons/icon-144x144.svg +12 -0
  204. package/client/dist/icons/icon-152x152.png +0 -0
  205. package/client/dist/icons/icon-152x152.svg +12 -0
  206. package/client/dist/icons/icon-192x192.png +0 -0
  207. package/client/dist/icons/icon-192x192.svg +12 -0
  208. package/client/dist/icons/icon-384x384.png +0 -0
  209. package/client/dist/icons/icon-384x384.svg +12 -0
  210. package/client/dist/icons/icon-512x512.png +0 -0
  211. package/client/dist/icons/icon-512x512.svg +12 -0
  212. package/client/dist/icons/icon-72x72.png +0 -0
  213. package/client/dist/icons/icon-72x72.svg +12 -0
  214. package/client/dist/icons/icon-96x96.png +0 -0
  215. package/client/dist/icons/icon-96x96.svg +12 -0
  216. package/client/dist/icons/icon-template.svg +12 -0
  217. package/client/dist/index.html +128 -0
  218. package/client/dist/logo-128.png +0 -0
  219. package/client/dist/logo-256.png +0 -0
  220. package/client/dist/logo-32.png +0 -0
  221. package/client/dist/logo-512.png +0 -0
  222. package/client/dist/logo-64.png +0 -0
  223. package/client/dist/logo.svg +17 -0
  224. package/client/dist/manifest.json +61 -0
  225. package/client/dist/mcp-docs.html +119 -0
  226. package/client/dist/screenshots/cli-selection.png +0 -0
  227. package/client/dist/screenshots/desktop-main.png +0 -0
  228. package/client/dist/screenshots/mobile-chat.png +0 -0
  229. package/client/dist/screenshots/tools-modal.png +0 -0
  230. package/client/dist/sw.js +19 -0
  231. package/commands/upfynai-connect.md +46 -0
  232. package/commands/upfynai-disconnect.md +31 -0
  233. package/commands/upfynai-doctor.md +99 -0
  234. package/commands/upfynai-export.md +49 -0
  235. package/commands/upfynai-local.md +82 -0
  236. package/commands/upfynai-status.md +75 -0
  237. package/commands/upfynai-stop.md +49 -0
  238. package/commands/upfynai-uninstall.md +58 -0
  239. package/commands/upfynai.md +50 -0
  240. package/package.json +106 -47
  241. package/scripts/build-client.js +17 -0
  242. package/scripts/fix-node-pty.js +67 -0
  243. package/scripts/install-commands.js +78 -0
  244. package/server/claude-sdk.js +714 -0
  245. package/server/cli.js +419 -0
  246. package/server/constants/config.js +5 -0
  247. package/server/cursor-cli.js +270 -0
  248. package/server/database/auth.db +0 -0
  249. package/server/database/db.js +606 -0
  250. package/server/database/init.sql +70 -0
  251. package/server/index.js +2269 -0
  252. package/server/load-env.js +26 -0
  253. package/server/mcp-server.js +620 -0
  254. package/server/middleware/auth.js +158 -0
  255. package/server/openai-codex.js +403 -0
  256. package/server/projects.js +1849 -0
  257. package/server/relay-client.js +314 -0
  258. package/server/routes/agent.js +1231 -0
  259. package/server/routes/auth.js +220 -0
  260. package/server/routes/cli-auth.js +263 -0
  261. package/server/routes/codex.js +344 -0
  262. package/server/routes/commands.js +601 -0
  263. package/server/routes/cursor.js +808 -0
  264. package/server/routes/git.js +1165 -0
  265. package/server/routes/mcp-utils.js +48 -0
  266. package/server/routes/mcp.js +552 -0
  267. package/server/routes/payments.js +172 -0
  268. package/server/routes/projects.js +549 -0
  269. package/server/routes/settings.js +178 -0
  270. package/server/routes/taskmaster.js +1964 -0
  271. package/server/routes/user.js +106 -0
  272. package/server/utils/commandParser.js +303 -0
  273. package/server/utils/gitConfig.js +24 -0
  274. package/server/utils/mcp-detector.js +198 -0
  275. package/server/utils/taskmaster-websocket.js +129 -0
  276. package/shared/modelConstants.js +67 -0
  277. package/LICENSE +0 -22
  278. package/bin/cli.js +0 -86
  279. package/dist/assets/CanvasPanel-B48gAKVY.js +0 -538
  280. package/dist/assets/CanvasPanel-B48gAKVY.js.map +0 -1
  281. package/dist/assets/CanvasPanel-BsOG3EVs.css +0 -1
  282. package/dist/assets/index-CEhTwG68.css +0 -1
  283. package/dist/assets/index-GqAGWpJI.js +0 -70
  284. package/dist/assets/index-GqAGWpJI.js.map +0 -1
  285. package/dist/index.html +0 -18
  286. package/index.html +0 -17
  287. package/src/App.tsx +0 -226
  288. package/src/components/canvas/CanvasPanel.tsx +0 -62
  289. package/src/components/canvas/layout/graph-builder.ts +0 -136
  290. package/src/components/canvas/shapes/CompactionNodeShape.tsx +0 -76
  291. package/src/components/canvas/shapes/SessionNodeShape.tsx +0 -93
  292. package/src/components/canvas/shapes/StatuslineWidgetShape.tsx +0 -125
  293. package/src/components/canvas/shapes/TextResponseNodeShape.tsx +0 -86
  294. package/src/components/canvas/shapes/ToolCallNodeShape.tsx +0 -107
  295. package/src/components/canvas/shapes/ToolResultNodeShape.tsx +0 -87
  296. package/src/components/canvas/shapes/shared-styles.ts +0 -35
  297. package/src/components/chat/ChatPanel.tsx +0 -96
  298. package/src/components/chat/InputBar.tsx +0 -81
  299. package/src/components/chat/MessageList.tsx +0 -130
  300. package/src/components/chat/PermissionDialog.tsx +0 -70
  301. package/src/components/layout/FolderSelector.tsx +0 -152
  302. package/src/components/layout/ModelSelector.tsx +0 -65
  303. package/src/components/layout/SessionManager.tsx +0 -115
  304. package/src/components/statusline/StatuslineBar.tsx +0 -114
  305. package/src/main.tsx +0 -10
  306. package/src/server/claude-session.ts +0 -156
  307. package/src/server/index.ts +0 -149
  308. package/src/services/stream-consumer.ts +0 -330
  309. package/src/statusline-core/bin/statusline.sh +0 -121
  310. package/src/statusline-core/commands/sls-config.md +0 -42
  311. package/src/statusline-core/commands/sls-doctor.md +0 -35
  312. package/src/statusline-core/commands/sls-help.md +0 -48
  313. package/src/statusline-core/commands/sls-layout.md +0 -38
  314. package/src/statusline-core/commands/sls-preview.md +0 -34
  315. package/src/statusline-core/commands/sls-theme.md +0 -40
  316. package/src/statusline-core/installer.js +0 -228
  317. package/src/statusline-core/layouts/compact.sh +0 -21
  318. package/src/statusline-core/layouts/full.sh +0 -62
  319. package/src/statusline-core/layouts/standard.sh +0 -39
  320. package/src/statusline-core/lib/core.sh +0 -389
  321. package/src/statusline-core/lib/helpers.sh +0 -81
  322. package/src/statusline-core/lib/json-parser.sh +0 -71
  323. package/src/statusline-core/themes/catppuccin.sh +0 -32
  324. package/src/statusline-core/themes/default.sh +0 -37
  325. package/src/statusline-core/themes/gruvbox.sh +0 -32
  326. package/src/statusline-core/themes/nord.sh +0 -32
  327. package/src/statusline-core/themes/tokyo-night.sh +0 -32
  328. package/src/store/canvas-store.ts +0 -50
  329. package/src/store/chat-store.ts +0 -60
  330. package/src/store/permission-store.ts +0 -29
  331. package/src/store/session-store.ts +0 -52
  332. package/src/store/statusline-store.ts +0 -160
  333. package/src/styles/global.css +0 -117
  334. package/src/themes/index.ts +0 -149
  335. package/src/types/canvas-graph.ts +0 -24
  336. package/src/types/sdk-messages.ts +0 -156
  337. package/src/types/statusline-fields.ts +0 -67
  338. package/src/vite-env.d.ts +0 -1
  339. package/tsconfig.json +0 -26
  340. package/vite.config.ts +0 -24
@@ -0,0 +1,2269 @@
1
+ #!/usr/bin/env node
2
+ // Load environment variables before other imports execute
3
+ import './load-env.js';
4
+ import crypto from 'crypto';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname } from 'path';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ // ANSI color codes for terminal output
14
+ const colors = {
15
+ reset: '\x1b[0m',
16
+ bright: '\x1b[1m',
17
+ cyan: '\x1b[36m',
18
+ green: '\x1b[32m',
19
+ yellow: '\x1b[33m',
20
+ blue: '\x1b[34m',
21
+ dim: '\x1b[2m',
22
+ };
23
+
24
+ const c = {
25
+ info: (text) => `${colors.cyan}${text}${colors.reset}`,
26
+ ok: (text) => `${colors.green}${text}${colors.reset}`,
27
+ warn: (text) => `${colors.yellow}${text}${colors.reset}`,
28
+ tip: (text) => `${colors.blue}${text}${colors.reset}`,
29
+ bright: (text) => `${colors.bright}${text}${colors.reset}`,
30
+ dim: (text) => `${colors.dim}${text}${colors.reset}`,
31
+ };
32
+
33
+ if (!process.env.VERCEL) console.log('PORT from env:', process.env.PORT);
34
+
35
+ import express from 'express';
36
+ import { WebSocketServer, WebSocket } from 'ws';
37
+ import os from 'os';
38
+ import http from 'http';
39
+ import cors from 'cors';
40
+ import cookieParser from 'cookie-parser';
41
+ import { promises as fsPromises } from 'fs';
42
+ import { spawn } from 'child_process';
43
+ // node-pty: conditionally imported (not available on Vercel serverless)
44
+ let pty = null;
45
+ try {
46
+ pty = (await import('node-pty')).default;
47
+ } catch (e) {
48
+ console.warn('[WARN] node-pty not available. Shell tab requires relay connection.');
49
+ }
50
+ import fetch from 'node-fetch';
51
+ import mime from 'mime-types';
52
+
53
+ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
54
+ import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
55
+ import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
56
+ import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
57
+ import { createMcpServer, mountMcpServer } from './mcp-server.js';
58
+ import gitRoutes from './routes/git.js';
59
+ import authRoutes from './routes/auth.js';
60
+ import mcpRoutes from './routes/mcp.js';
61
+ import cursorRoutes from './routes/cursor.js';
62
+ import taskmasterRoutes from './routes/taskmaster.js';
63
+ import mcpUtilsRoutes from './routes/mcp-utils.js';
64
+ import commandsRoutes from './routes/commands.js';
65
+ import settingsRoutes from './routes/settings.js';
66
+ import agentRoutes from './routes/agent.js';
67
+ import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js';
68
+ import cliAuthRoutes from './routes/cli-auth.js';
69
+ import userRoutes from './routes/user.js';
70
+ import codexRoutes from './routes/codex.js';
71
+ import paymentRoutes from './routes/payments.js';
72
+ import { initializeDatabase, relayTokensDb, subscriptionDb } from './database/db.js';
73
+ import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
74
+ import { IS_PLATFORM } from './constants/config.js';
75
+
76
+ // File system watchers for provider project/session folders
77
+ const PROVIDER_WATCH_PATHS = [
78
+ { provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
79
+ { provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
80
+ { provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') }
81
+ ];
82
+ const WATCHER_IGNORED_PATTERNS = [
83
+ '**/node_modules/**',
84
+ '**/.git/**',
85
+ '**/dist/**',
86
+ '**/build/**',
87
+ '**/*.tmp',
88
+ '**/*.swp',
89
+ '**/.DS_Store'
90
+ ];
91
+ const WATCHER_DEBOUNCE_MS = 300;
92
+ let projectsWatchers = [];
93
+ let projectsWatcherDebounceTimer = null;
94
+ const connectedClients = new Set();
95
+ let isGetProjectsRunning = false; // Flag to prevent reentrant calls
96
+
97
+ // Relay connections: Maps userId → { ws, capabilities, user }
98
+ // Connects user's local machine to the hosted server
99
+ const relayConnections = new Map();
100
+ // Pending relay requests: Maps requestId → { resolve, reject, timeout }
101
+ const pendingRelayRequests = new Map();
102
+
103
+ // Session-tab locking: Maps sessionId → WebSocket connection
104
+ // Prevents the same session from being active in multiple tabs
105
+ const sessionLocks = new Map();
106
+
107
+ function acquireSessionLock(sessionId, ws) {
108
+ if (!sessionId) return true; // New sessions don't need locks yet
109
+ const existingWs = sessionLocks.get(sessionId);
110
+ if (existingWs && existingWs !== ws && existingWs.readyState === 1) {
111
+ return false; // Another tab has this session locked
112
+ }
113
+ sessionLocks.set(sessionId, ws);
114
+ return true;
115
+ }
116
+
117
+ function releaseSessionLock(sessionId, ws) {
118
+ const lockedWs = sessionLocks.get(sessionId);
119
+ if (lockedWs === ws) {
120
+ sessionLocks.delete(sessionId);
121
+ }
122
+ }
123
+
124
+ function releaseAllLocksForWs(ws) {
125
+ for (const [sessionId, lockedWs] of sessionLocks.entries()) {
126
+ if (lockedWs === ws) {
127
+ sessionLocks.delete(sessionId);
128
+ }
129
+ }
130
+ }
131
+
132
+ // Broadcast progress to all connected WebSocket clients
133
+ function broadcastProgress(progress) {
134
+ const message = JSON.stringify({
135
+ type: 'loading_progress',
136
+ ...progress
137
+ });
138
+ connectedClients.forEach(client => {
139
+ if (client.readyState === WebSocket.OPEN) {
140
+ client.send(message);
141
+ }
142
+ });
143
+ }
144
+
145
+ // Setup file system watchers for Claude, Cursor, and Codex project/session folders
146
+ async function setupProjectsWatcher() {
147
+ const chokidar = (await import('chokidar')).default;
148
+
149
+ if (projectsWatcherDebounceTimer) {
150
+ clearTimeout(projectsWatcherDebounceTimer);
151
+ projectsWatcherDebounceTimer = null;
152
+ }
153
+
154
+ await Promise.all(
155
+ projectsWatchers.map(async (watcher) => {
156
+ try {
157
+ await watcher.close();
158
+ } catch (error) {
159
+ console.error('[WARN] Failed to close watcher:', error);
160
+ }
161
+ })
162
+ );
163
+ projectsWatchers = [];
164
+
165
+ const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
166
+ if (projectsWatcherDebounceTimer) {
167
+ clearTimeout(projectsWatcherDebounceTimer);
168
+ }
169
+
170
+ projectsWatcherDebounceTimer = setTimeout(async () => {
171
+ // Prevent reentrant calls
172
+ if (isGetProjectsRunning) {
173
+ return;
174
+ }
175
+
176
+ try {
177
+ isGetProjectsRunning = true;
178
+
179
+ // Clear project directory cache when files change
180
+ clearProjectDirectoryCache();
181
+
182
+ // Get updated projects list
183
+ const updatedProjects = await getProjects(broadcastProgress);
184
+
185
+ // Notify all connected clients about the project changes
186
+ const updateMessage = JSON.stringify({
187
+ type: 'projects_updated',
188
+ projects: updatedProjects,
189
+ timestamp: new Date().toISOString(),
190
+ changeType: eventType,
191
+ changedFile: path.relative(rootPath, filePath),
192
+ watchProvider: provider
193
+ });
194
+
195
+ connectedClients.forEach(client => {
196
+ if (client.readyState === WebSocket.OPEN) {
197
+ client.send(updateMessage);
198
+ }
199
+ });
200
+
201
+ } catch (error) {
202
+ console.error('[ERROR] Error handling project changes:', error);
203
+ } finally {
204
+ isGetProjectsRunning = false;
205
+ }
206
+ }, WATCHER_DEBOUNCE_MS);
207
+ };
208
+
209
+ for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
210
+ try {
211
+ // chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
212
+ // Ensure provider folders exist before creating the watcher so watching stays active.
213
+ await fsPromises.mkdir(rootPath, { recursive: true });
214
+
215
+ // Initialize chokidar watcher with optimized settings
216
+ const watcher = chokidar.watch(rootPath, {
217
+ ignored: WATCHER_IGNORED_PATTERNS,
218
+ persistent: true,
219
+ ignoreInitial: true, // Don't fire events for existing files on startup
220
+ followSymlinks: false,
221
+ depth: 10, // Reasonable depth limit
222
+ awaitWriteFinish: {
223
+ stabilityThreshold: 100, // Wait 100ms for file to stabilize
224
+ pollInterval: 50
225
+ }
226
+ });
227
+
228
+ // Set up event listeners
229
+ watcher
230
+ .on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
231
+ .on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
232
+ .on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
233
+ .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
234
+ .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
235
+ .on('error', (error) => {
236
+ console.error(`[ERROR] ${provider} watcher error:`, error);
237
+ })
238
+ .on('ready', () => {
239
+ });
240
+
241
+ projectsWatchers.push(watcher);
242
+ } catch (error) {
243
+ console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
244
+ }
245
+ }
246
+
247
+ if (projectsWatchers.length === 0) {
248
+ console.error('[ERROR] Failed to setup any provider watchers');
249
+ }
250
+ }
251
+
252
+
253
+ const app = express();
254
+
255
+ // On Vercel serverless, we don't need an HTTP server or WebSocket server
256
+ const server = process.env.VERCEL ? null : http.createServer(app);
257
+
258
+ const ptySessionsMap = new Map();
259
+ const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
260
+ const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
261
+ const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
262
+ const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
263
+
264
+ function stripAnsiSequences(value = '') {
265
+ return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
266
+ }
267
+
268
+ function normalizeDetectedUrl(url) {
269
+ if (!url || typeof url !== 'string') return null;
270
+
271
+ const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
272
+ if (!cleaned) return null;
273
+
274
+ try {
275
+ const parsed = new URL(cleaned);
276
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
277
+ return null;
278
+ }
279
+ return parsed.toString();
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+
285
+ function extractUrlsFromText(value = '') {
286
+ const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
287
+
288
+ // Handle wrapped terminal URLs split across lines by terminal width.
289
+ const wrappedMatches = [];
290
+ const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
291
+ const lines = value.split(/\r?\n/);
292
+ for (let i = 0; i < lines.length; i++) {
293
+ const line = lines[i].trim();
294
+ const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
295
+ if (!startMatch) continue;
296
+
297
+ let combined = startMatch[0];
298
+ let j = i + 1;
299
+ while (j < lines.length) {
300
+ const continuation = lines[j].trim();
301
+ if (!continuation) break;
302
+ if (!continuationRegex.test(continuation)) break;
303
+ combined += continuation;
304
+ j++;
305
+ }
306
+
307
+ wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
308
+ }
309
+
310
+ return Array.from(new Set([...directMatches, ...wrappedMatches]));
311
+ }
312
+
313
+ function shouldAutoOpenUrlFromOutput(value = '') {
314
+ const normalized = value.toLowerCase();
315
+ return (
316
+ normalized.includes('browser didn\'t open') ||
317
+ normalized.includes('open this url') ||
318
+ normalized.includes('continue in your browser') ||
319
+ normalized.includes('press enter to open') ||
320
+ normalized.includes('open_url:')
321
+ );
322
+ }
323
+
324
+ // Single WebSocket server that handles both paths (skip on Vercel serverless)
325
+ let wss = null;
326
+ if (server) {
327
+ wss = new WebSocketServer({
328
+ server,
329
+ verifyClient: (info, done) => {
330
+ // authenticateWebSocket now parses cookies from the upgrade request
331
+ authenticateWebSocket(info.req).then(user => {
332
+ if (!user) {
333
+ // WebSocket authentication failed
334
+ done(false, 401, 'Unauthorized');
335
+ return;
336
+ }
337
+ info.req.user = user;
338
+ // WebSocket authenticated
339
+ done(true);
340
+ }).catch(err => {
341
+ // WebSocket auth error
342
+ done(false, 500, 'Auth error');
343
+ });
344
+ }
345
+ });
346
+ }
347
+
348
+ // Make WebSocket server available to routes
349
+ app.locals.wss = wss;
350
+
351
+ // CORS: require explicit CORS_ORIGINS in production, restrict to same-origin otherwise
352
+ const CORS_ORIGINS = process.env.CORS_ORIGINS
353
+ ? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
354
+ : (process.env.NODE_ENV === 'production' ? ['https://cli.upfyn.com'] : true);
355
+ app.use(cors({ origin: CORS_ORIGINS, credentials: true }));
356
+ app.use(cookieParser());
357
+ app.use(express.json({
358
+ limit: '50mb',
359
+ type: (req) => {
360
+ // Skip multipart/form-data requests (for file uploads like images)
361
+ const contentType = req.headers['content-type'] || '';
362
+ if (contentType.includes('multipart/form-data')) {
363
+ return false;
364
+ }
365
+ return contentType.includes('json');
366
+ }
367
+ }));
368
+ app.use(express.urlencoded({ limit: '50mb', extended: true }));
369
+
370
+ // Vercel serverless: lazy DB initialization on first request
371
+ let dbInitialized = false;
372
+ if (process.env.VERCEL) {
373
+ app.use(async (req, res, next) => {
374
+ if (!dbInitialized) {
375
+ try {
376
+ await initializeDatabase();
377
+ dbInitialized = true;
378
+ } catch (err) {
379
+ console.error('[VERCEL] DB init failed:', err);
380
+ return res.status(500).json({ error: 'Service temporarily unavailable' });
381
+ }
382
+ }
383
+ next();
384
+ });
385
+ }
386
+
387
+ // Public health check endpoint (no authentication required)
388
+ app.get('/health', (req, res) => {
389
+ res.json({
390
+ status: 'ok',
391
+ timestamp: new Date().toISOString()
392
+ });
393
+ });
394
+
395
+ // Optional API key validation (if configured)
396
+ app.use('/api', validateApiKey);
397
+
398
+ // Authentication routes (public)
399
+ app.use('/api/auth', authRoutes);
400
+
401
+ // Projects API Routes (protected)
402
+ app.use('/api/projects', authenticateToken, projectsRoutes);
403
+
404
+ // Git API Routes (protected)
405
+ app.use('/api/git', authenticateToken, gitRoutes);
406
+
407
+ // MCP API Routes (protected)
408
+ app.use('/api/mcp', authenticateToken, mcpRoutes);
409
+
410
+ // Cursor API Routes (protected)
411
+ app.use('/api/cursor', authenticateToken, cursorRoutes);
412
+
413
+ // TaskMaster API Routes (protected)
414
+ app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
415
+
416
+ // MCP utilities
417
+ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
418
+
419
+ // Upfyn-Code MCP Server — exposes app capabilities to any MCP client (ChatGPT, Claude Desktop, Cursor, etc.)
420
+ const mcpDeps = {
421
+ getProjects,
422
+ getSessions,
423
+ getSessionMessages,
424
+ queryClaudeSDK,
425
+ abortClaudeSDKSession,
426
+ getActiveClaudeSDKSessions,
427
+ connectedClients,
428
+ };
429
+ const mcpAppServer = createMcpServer(mcpDeps);
430
+ const mcpServerFactory = () => createMcpServer(mcpDeps);
431
+ mountMcpServer(app, mcpAppServer, mcpServerFactory).catch(err => console.error('[MCP] Failed to mount:', err.message));
432
+
433
+ // Commands API Routes (protected)
434
+ app.use('/api/commands', authenticateToken, commandsRoutes);
435
+
436
+ // Settings API Routes (protected)
437
+ app.use('/api/settings', authenticateToken, settingsRoutes);
438
+
439
+ // CLI Authentication API Routes (protected)
440
+ app.use('/api/cli', authenticateToken, cliAuthRoutes);
441
+
442
+ // User API Routes (protected)
443
+ app.use('/api/user', authenticateToken, userRoutes);
444
+
445
+ // Codex API Routes (protected)
446
+ app.use('/api/codex', authenticateToken, codexRoutes);
447
+
448
+ // Payment & Subscription Routes (protected)
449
+ app.use('/api/payments', authenticateToken, paymentRoutes);
450
+
451
+ // Agent API Routes (uses API key authentication)
452
+ app.use('/api/agent', agentRoutes);
453
+
454
+ // Relay token management routes
455
+ app.get('/api/relay/tokens', authenticateToken, async (req, res) => {
456
+ try {
457
+ const tokens = await relayTokensDb.getTokens(req.user.id);
458
+ res.json(tokens.map(t => ({ ...t, token: t.token.slice(0, 10) + '...' }))); // mask tokens
459
+ } catch (err) {
460
+ res.status(500).json({ error: err.message });
461
+ }
462
+ });
463
+
464
+ app.post('/api/relay/tokens', authenticateToken, async (req, res) => {
465
+ try {
466
+ const name = req.body.name || 'default';
467
+ const result = await relayTokensDb.createToken(req.user.id, name);
468
+ res.json(result); // returns full token only on creation
469
+ } catch (err) {
470
+ res.status(500).json({ error: err.message });
471
+ }
472
+ });
473
+
474
+ app.delete('/api/relay/tokens/:id', authenticateToken, async (req, res) => {
475
+ try {
476
+ await relayTokensDb.deleteToken(req.user.id, req.params.id);
477
+ res.json({ success: true });
478
+ } catch (err) {
479
+ res.status(500).json({ error: err.message });
480
+ }
481
+ });
482
+
483
+ app.get('/api/relay/status', authenticateToken, (req, res) => {
484
+ const relay = relayConnections.get(Number(req.user.id));
485
+ res.json({
486
+ connected: !!(relay && relay.ws.readyState === 1),
487
+ connectedAt: relay?.connectedAt || null
488
+ });
489
+ });
490
+
491
+ // Connection status — alias at path the frontend expects
492
+ app.get('/api/auth/connection-status', authenticateToken, (req, res) => {
493
+ const relay = relayConnections.get(Number(req.user.id));
494
+ res.json({
495
+ connected: !!(relay && relay.ws.readyState === 1),
496
+ connectedAt: relay?.connectedAt || null
497
+ });
498
+ });
499
+
500
+ // Serve public files (like api-docs.html)
501
+ app.use(express.static(path.join(__dirname, '../client/public')));
502
+
503
+ // Static files served after API routes
504
+ // Add cache control: HTML files should not be cached, but assets can be cached
505
+ app.use(express.static(path.join(__dirname, '../client/dist'), {
506
+ setHeaders: (res, filePath) => {
507
+ if (filePath.endsWith('.html')) {
508
+ // Prevent HTML caching to avoid service worker issues after builds
509
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
510
+ res.setHeader('Pragma', 'no-cache');
511
+ res.setHeader('Expires', '0');
512
+ } else if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
513
+ // Cache static assets for 1 year (they have hashed names)
514
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
515
+ }
516
+ }
517
+ }));
518
+
519
+ // API Routes (protected)
520
+ // /api/config endpoint removed - no longer needed
521
+ // Frontend now uses window.location for WebSocket URLs
522
+
523
+ // System update endpoint
524
+ app.post('/api/system/update', authenticateToken, async (req, res) => {
525
+ try {
526
+ // Get the project root directory (parent of server directory)
527
+ const projectRoot = path.join(__dirname, '..');
528
+
529
+ console.log('Starting system update from directory:', projectRoot);
530
+
531
+ // Run the update command
532
+ const updateCommand = 'git checkout main && git pull && npm install';
533
+
534
+ const child = spawn('sh', ['-c', updateCommand], {
535
+ cwd: projectRoot,
536
+ env: process.env
537
+ });
538
+
539
+ let output = '';
540
+ let errorOutput = '';
541
+
542
+ child.stdout.on('data', (data) => {
543
+ const text = data.toString();
544
+ output += text;
545
+ console.log('Update output:', text);
546
+ });
547
+
548
+ child.stderr.on('data', (data) => {
549
+ const text = data.toString();
550
+ errorOutput += text;
551
+ console.error('Update error:', text);
552
+ });
553
+
554
+ child.on('close', (code) => {
555
+ if (code === 0) {
556
+ res.json({
557
+ success: true,
558
+ output: output || 'Update completed successfully',
559
+ message: 'Update completed. Please restart the server to apply changes.'
560
+ });
561
+ } else {
562
+ res.status(500).json({
563
+ success: false,
564
+ error: 'Update command failed',
565
+ output: output,
566
+ errorOutput: errorOutput
567
+ });
568
+ }
569
+ });
570
+
571
+ child.on('error', (error) => {
572
+ console.error('Update process error:', error);
573
+ res.status(500).json({
574
+ success: false,
575
+ error: error.message
576
+ });
577
+ });
578
+
579
+ } catch (error) {
580
+ console.error('System update error:', error);
581
+ res.status(500).json({
582
+ success: false,
583
+ error: error.message
584
+ });
585
+ }
586
+ });
587
+
588
+ app.get('/api/projects', authenticateToken, async (req, res) => {
589
+ try {
590
+ const projects = await getProjects(broadcastProgress);
591
+ res.json(projects);
592
+ } catch (error) {
593
+ res.status(500).json({ error: error.message });
594
+ }
595
+ });
596
+
597
+ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
598
+ try {
599
+ const { limit = 5, offset = 0 } = req.query;
600
+ const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
601
+ res.json(result);
602
+ } catch (error) {
603
+ res.status(500).json({ error: error.message });
604
+ }
605
+ });
606
+
607
+ // Get messages for a specific session
608
+ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
609
+ try {
610
+ const { projectName, sessionId } = req.params;
611
+ const { limit, offset } = req.query;
612
+
613
+ // Parse limit and offset if provided
614
+ const parsedLimit = limit ? parseInt(limit, 10) : null;
615
+ const parsedOffset = offset ? parseInt(offset, 10) : 0;
616
+
617
+ const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset);
618
+
619
+ // Handle both old and new response formats
620
+ if (Array.isArray(result)) {
621
+ // Backward compatibility: no pagination parameters were provided
622
+ res.json({ messages: result });
623
+ } else {
624
+ // New format with pagination info
625
+ res.json(result);
626
+ }
627
+ } catch (error) {
628
+ res.status(500).json({ error: error.message });
629
+ }
630
+ });
631
+
632
+ // Rename project endpoint
633
+ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
634
+ try {
635
+ const { displayName } = req.body;
636
+ await renameProject(req.params.projectName, displayName);
637
+ res.json({ success: true });
638
+ } catch (error) {
639
+ res.status(500).json({ error: error.message });
640
+ }
641
+ });
642
+
643
+ // Delete session endpoint
644
+ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
645
+ try {
646
+ const { projectName, sessionId } = req.params;
647
+ // deleting session
648
+ await deleteSession(projectName, sessionId);
649
+ // session deleted
650
+ res.json({ success: true });
651
+ } catch (error) {
652
+ // session delete error
653
+ res.status(500).json({ error: error.message });
654
+ }
655
+ });
656
+
657
+ // Delete project endpoint (force=true to delete with sessions)
658
+ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
659
+ try {
660
+ const { projectName } = req.params;
661
+ const force = req.query.force === 'true';
662
+ await deleteProject(projectName, force);
663
+ res.json({ success: true });
664
+ } catch (error) {
665
+ res.status(500).json({ error: error.message });
666
+ }
667
+ });
668
+
669
+ // Create project endpoint
670
+ app.post('/api/projects/create', authenticateToken, async (req, res) => {
671
+ try {
672
+ const { path: projectPath } = req.body;
673
+
674
+ if (!projectPath || !projectPath.trim()) {
675
+ return res.status(400).json({ error: 'Project path is required' });
676
+ }
677
+
678
+ const project = await addProjectManually(projectPath.trim());
679
+ res.json({ success: true, project });
680
+ } catch (error) {
681
+ console.error('Error creating project:', error);
682
+ res.status(500).json({ error: error.message });
683
+ }
684
+ });
685
+
686
+ const expandWorkspacePath = (inputPath) => {
687
+ if (!inputPath) return inputPath;
688
+ if (inputPath === '~') {
689
+ return WORKSPACES_ROOT;
690
+ }
691
+ if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
692
+ return path.join(WORKSPACES_ROOT, inputPath.slice(2));
693
+ }
694
+ return inputPath;
695
+ };
696
+
697
+ // Browse filesystem endpoint for project suggestions - uses existing getFileTree
698
+ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
699
+ try {
700
+ const { path: dirPath } = req.query;
701
+
702
+ console.log('[API] Browse filesystem request for path:', dirPath);
703
+ console.log('[API] WORKSPACES_ROOT is:', WORKSPACES_ROOT);
704
+ // Default to home directory if no path provided
705
+ const defaultRoot = WORKSPACES_ROOT;
706
+ let targetPath = dirPath ? expandWorkspacePath(dirPath) : defaultRoot;
707
+
708
+ // Resolve and normalize the path
709
+ targetPath = path.resolve(targetPath);
710
+
711
+ // Security check - ensure path is within allowed workspace root
712
+ const validation = await validateWorkspacePath(targetPath);
713
+ if (!validation.valid) {
714
+ return res.status(403).json({ error: validation.error });
715
+ }
716
+ const resolvedPath = validation.resolvedPath || targetPath;
717
+
718
+ // Security check - ensure path is accessible
719
+ try {
720
+ await fs.promises.access(resolvedPath);
721
+ const stats = await fs.promises.stat(resolvedPath);
722
+
723
+ if (!stats.isDirectory()) {
724
+ return res.status(400).json({ error: 'Path is not a directory' });
725
+ }
726
+ } catch (err) {
727
+ return res.status(404).json({ error: 'Directory not accessible' });
728
+ }
729
+
730
+ // Use existing getFileTree function with shallow depth (only direct children)
731
+ const fileTree = await getFileTree(resolvedPath, 1, 0, false); // maxDepth=1, showHidden=false
732
+
733
+ // Filter only directories and format for suggestions
734
+ const directories = fileTree
735
+ .filter(item => item.type === 'directory')
736
+ .map(item => ({
737
+ path: item.path,
738
+ name: item.name,
739
+ type: 'directory'
740
+ }))
741
+ .sort((a, b) => {
742
+ const aHidden = a.name.startsWith('.');
743
+ const bHidden = b.name.startsWith('.');
744
+ if (aHidden && !bHidden) return 1;
745
+ if (!aHidden && bHidden) return -1;
746
+ return a.name.localeCompare(b.name);
747
+ });
748
+
749
+ // Add common directories if browsing home directory
750
+ const suggestions = [];
751
+ let resolvedWorkspaceRoot = defaultRoot;
752
+ try {
753
+ resolvedWorkspaceRoot = await fsPromises.realpath(defaultRoot);
754
+ } catch (error) {
755
+ // Use default root as-is if realpath fails
756
+ }
757
+ if (resolvedPath === resolvedWorkspaceRoot) {
758
+ const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace'];
759
+ const existingCommon = directories.filter(dir => commonDirs.includes(dir.name));
760
+ const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name));
761
+
762
+ suggestions.push(...existingCommon, ...otherDirs);
763
+ } else {
764
+ suggestions.push(...directories);
765
+ }
766
+
767
+ res.json({
768
+ path: resolvedPath,
769
+ suggestions: suggestions
770
+ });
771
+
772
+ } catch (error) {
773
+ console.error('Error browsing filesystem:', error);
774
+ res.status(500).json({ error: 'Failed to browse filesystem' });
775
+ }
776
+ });
777
+
778
+ app.post('/api/create-folder', authenticateToken, async (req, res) => {
779
+ try {
780
+ const { path: folderPath } = req.body;
781
+ if (!folderPath) {
782
+ return res.status(400).json({ error: 'Path is required' });
783
+ }
784
+ const expandedPath = expandWorkspacePath(folderPath);
785
+ const resolvedInput = path.resolve(expandedPath);
786
+ const validation = await validateWorkspacePath(resolvedInput);
787
+ if (!validation.valid) {
788
+ return res.status(403).json({ error: validation.error });
789
+ }
790
+ const targetPath = validation.resolvedPath || resolvedInput;
791
+ const parentDir = path.dirname(targetPath);
792
+ try {
793
+ await fs.promises.access(parentDir);
794
+ } catch (err) {
795
+ return res.status(404).json({ error: 'Parent directory does not exist' });
796
+ }
797
+ try {
798
+ await fs.promises.access(targetPath);
799
+ return res.status(409).json({ error: 'Folder already exists' });
800
+ } catch (err) {
801
+ // Folder doesn't exist, which is what we want
802
+ }
803
+ try {
804
+ await fs.promises.mkdir(targetPath, { recursive: false });
805
+ res.json({ success: true, path: targetPath });
806
+ } catch (mkdirError) {
807
+ if (mkdirError.code === 'EEXIST') {
808
+ return res.status(409).json({ error: 'Folder already exists' });
809
+ }
810
+ throw mkdirError;
811
+ }
812
+ } catch (error) {
813
+ console.error('Error creating folder:', error);
814
+ res.status(500).json({ error: 'Failed to create folder' });
815
+ }
816
+ });
817
+
818
+ // Read file content endpoint
819
+ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
820
+ try {
821
+ const { projectName } = req.params;
822
+ const { filePath } = req.query;
823
+
824
+
825
+ // Security: ensure the requested path is inside the project root
826
+ if (!filePath) {
827
+ return res.status(400).json({ error: 'Invalid file path' });
828
+ }
829
+
830
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
831
+ if (!projectRoot) {
832
+ return res.status(404).json({ error: 'Project not found' });
833
+ }
834
+
835
+ // Handle both absolute and relative paths
836
+ const resolved = path.isAbsolute(filePath)
837
+ ? path.resolve(filePath)
838
+ : path.resolve(projectRoot, filePath);
839
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
840
+ if (!resolved.startsWith(normalizedRoot)) {
841
+ return res.status(403).json({ error: 'Path must be under project root' });
842
+ }
843
+
844
+ const content = await fsPromises.readFile(resolved, 'utf8');
845
+ res.json({ content, path: resolved });
846
+ } catch (error) {
847
+ console.error('Error reading file:', error);
848
+ if (error.code === 'ENOENT') {
849
+ res.status(404).json({ error: 'File not found' });
850
+ } else if (error.code === 'EACCES') {
851
+ res.status(403).json({ error: 'Permission denied' });
852
+ } else {
853
+ res.status(500).json({ error: error.message });
854
+ }
855
+ }
856
+ });
857
+
858
+ // Serve binary file content endpoint (for images, etc.)
859
+ app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
860
+ try {
861
+ const { projectName } = req.params;
862
+ const { path: filePath } = req.query;
863
+
864
+
865
+ // Security: ensure the requested path is inside the project root
866
+ if (!filePath) {
867
+ return res.status(400).json({ error: 'Invalid file path' });
868
+ }
869
+
870
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
871
+ if (!projectRoot) {
872
+ return res.status(404).json({ error: 'Project not found' });
873
+ }
874
+
875
+ const resolved = path.resolve(filePath);
876
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
877
+ if (!resolved.startsWith(normalizedRoot)) {
878
+ return res.status(403).json({ error: 'Path must be under project root' });
879
+ }
880
+
881
+ // Check if file exists
882
+ try {
883
+ await fsPromises.access(resolved);
884
+ } catch (error) {
885
+ return res.status(404).json({ error: 'File not found' });
886
+ }
887
+
888
+ // Get file extension and set appropriate content type
889
+ const mimeType = mime.lookup(resolved) || 'application/octet-stream';
890
+ res.setHeader('Content-Type', mimeType);
891
+
892
+ // Stream the file
893
+ const fileStream = fs.createReadStream(resolved);
894
+ fileStream.pipe(res);
895
+
896
+ fileStream.on('error', (error) => {
897
+ console.error('Error streaming file:', error);
898
+ if (!res.headersSent) {
899
+ res.status(500).json({ error: 'Error reading file' });
900
+ }
901
+ });
902
+
903
+ } catch (error) {
904
+ console.error('Error serving binary file:', error);
905
+ if (!res.headersSent) {
906
+ res.status(500).json({ error: error.message });
907
+ }
908
+ }
909
+ });
910
+
911
+ // Save file content endpoint
912
+ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
913
+ try {
914
+ const { projectName } = req.params;
915
+ const { filePath, content } = req.body;
916
+
917
+
918
+ // Security: ensure the requested path is inside the project root
919
+ if (!filePath) {
920
+ return res.status(400).json({ error: 'Invalid file path' });
921
+ }
922
+
923
+ if (content === undefined) {
924
+ return res.status(400).json({ error: 'Content is required' });
925
+ }
926
+
927
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
928
+ if (!projectRoot) {
929
+ return res.status(404).json({ error: 'Project not found' });
930
+ }
931
+
932
+ // Handle both absolute and relative paths
933
+ const resolved = path.isAbsolute(filePath)
934
+ ? path.resolve(filePath)
935
+ : path.resolve(projectRoot, filePath);
936
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
937
+ if (!resolved.startsWith(normalizedRoot)) {
938
+ return res.status(403).json({ error: 'Path must be under project root' });
939
+ }
940
+
941
+ // Write the new content
942
+ await fsPromises.writeFile(resolved, content, 'utf8');
943
+
944
+ res.json({
945
+ success: true,
946
+ path: resolved,
947
+ message: 'File saved successfully'
948
+ });
949
+ } catch (error) {
950
+ console.error('Error saving file:', error);
951
+ if (error.code === 'ENOENT') {
952
+ res.status(404).json({ error: 'File or directory not found' });
953
+ } else if (error.code === 'EACCES') {
954
+ res.status(403).json({ error: 'Permission denied' });
955
+ } else {
956
+ res.status(500).json({ error: error.message });
957
+ }
958
+ }
959
+ });
960
+
961
+ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
962
+ try {
963
+
964
+ // Using fsPromises from import
965
+
966
+ // Use extractProjectDirectory to get the actual project path
967
+ let actualPath;
968
+ try {
969
+ actualPath = await extractProjectDirectory(req.params.projectName);
970
+ } catch (error) {
971
+ console.error('Error extracting project directory:', error);
972
+ // Fallback to simple dash replacement
973
+ actualPath = req.params.projectName.replace(/-/g, '/');
974
+ }
975
+
976
+ // Check if path exists
977
+ try {
978
+ await fsPromises.access(actualPath);
979
+ } catch (e) {
980
+ return res.status(404).json({ error: `Project path not found: ${actualPath}` });
981
+ }
982
+
983
+ const files = await getFileTree(actualPath, 10, 0, true);
984
+ const hiddenFiles = files.filter(f => f.name.startsWith('.'));
985
+ res.json(files);
986
+ } catch (error) {
987
+ console.error('[ERROR] File tree error:', error.message);
988
+ res.status(500).json({ error: error.message });
989
+ }
990
+ });
991
+
992
+ // WebSocket connection handler that routes based on URL path (skip on Vercel)
993
+ if (wss) wss.on('connection', (ws, request) => {
994
+ const url = request.url;
995
+ console.log('[INFO] Client connected to:', url);
996
+
997
+ // Parse URL to get pathname without query parameters
998
+ const urlObj = new URL(url, 'http://localhost');
999
+ const pathname = urlObj.pathname;
1000
+
1001
+ if (pathname === '/shell') {
1002
+ handleShellConnection(ws);
1003
+ } else if (pathname === '/ws') {
1004
+ handleChatConnection(ws);
1005
+ } else if (pathname === '/relay') {
1006
+ handleRelayConnection(ws, urlObj.searchParams.get('token'));
1007
+ } else {
1008
+ // unknown WebSocket path
1009
+ ws.close();
1010
+ }
1011
+ });
1012
+
1013
+ /**
1014
+ * WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
1015
+ */
1016
+ class WebSocketWriter {
1017
+ constructor(ws) {
1018
+ this.ws = ws;
1019
+ this.sessionId = null;
1020
+ this.isWebSocketWriter = true; // Marker for transport detection
1021
+ }
1022
+
1023
+ send(data) {
1024
+ if (this.ws.readyState === 1) { // WebSocket.OPEN
1025
+ // Providers send raw objects, we stringify for WebSocket
1026
+ this.ws.send(JSON.stringify(data));
1027
+ }
1028
+ }
1029
+
1030
+ setSessionId(sessionId) {
1031
+ this.sessionId = sessionId;
1032
+ }
1033
+
1034
+ getSessionId() {
1035
+ return this.sessionId;
1036
+ }
1037
+ }
1038
+
1039
+ // Handle chat WebSocket connections
1040
+ function handleChatConnection(ws) {
1041
+ // chat WebSocket connected
1042
+
1043
+ // Add to connected clients for project updates
1044
+ connectedClients.add(ws);
1045
+
1046
+ // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
1047
+ const writer = new WebSocketWriter(ws);
1048
+
1049
+ // Track which sessions this WebSocket has locked
1050
+ const lockedSessionsForThisWs = new Set();
1051
+
1052
+ // Wrap the original writer.send to capture session-created events for auto-locking
1053
+ const originalSend = writer.send.bind(writer);
1054
+ writer.send = (data) => {
1055
+ // When a new session is created, auto-lock it to this WebSocket
1056
+ if (data.type === 'session-created' && data.sessionId) {
1057
+ sessionLocks.set(data.sessionId, ws);
1058
+ lockedSessionsForThisWs.add(data.sessionId);
1059
+ // session locked
1060
+ }
1061
+ originalSend(data);
1062
+ };
1063
+
1064
+ ws.on('message', async (message) => {
1065
+ try {
1066
+ const data = JSON.parse(message);
1067
+
1068
+ // Handle session lock request (tab claiming a session)
1069
+ if (data.type === 'lock-session') {
1070
+ const sid = data.sessionId;
1071
+ if (!sid) return;
1072
+ if (acquireSessionLock(sid, ws)) {
1073
+ lockedSessionsForThisWs.add(sid);
1074
+ writer.send({ type: 'session-locked', sessionId: sid, success: true });
1075
+ // session locked to tab
1076
+ } else {
1077
+ writer.send({ type: 'session-locked', sessionId: sid, success: false,
1078
+ error: 'Session is already open in another tab' });
1079
+ // session lock denied
1080
+ }
1081
+ return;
1082
+ }
1083
+
1084
+ // Handle session unlock request
1085
+ if (data.type === 'unlock-session') {
1086
+ const sid = data.sessionId;
1087
+ if (sid) {
1088
+ releaseSessionLock(sid, ws);
1089
+ lockedSessionsForThisWs.delete(sid);
1090
+ writer.send({ type: 'session-unlocked', sessionId: sid });
1091
+ // session unlocked
1092
+ }
1093
+ return;
1094
+ }
1095
+
1096
+ if (data.type === 'claude-command') {
1097
+ const sid = data.options?.sessionId;
1098
+
1099
+ // Session-tab locking: check if this session is locked by another tab
1100
+ if (sid && !acquireSessionLock(sid, ws)) {
1101
+ writer.send({
1102
+ type: 'session-lock-denied',
1103
+ sessionId: sid,
1104
+ error: 'This session is already active in another tab. Close it there first.'
1105
+ });
1106
+ return;
1107
+ }
1108
+ if (sid) lockedSessionsForThisWs.add(sid);
1109
+
1110
+ console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1111
+ console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1112
+ // session message received
1113
+
1114
+ // Use Claude Agents SDK
1115
+ await queryClaudeSDK(data.command, data.options, writer);
1116
+ } else if (data.type === 'cursor-command') {
1117
+ console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
1118
+ console.log('📁 Project:', data.options?.cwd || 'Unknown');
1119
+ // session message received
1120
+ console.log('🤖 Model:', data.options?.model || 'default');
1121
+ await spawnCursor(data.command, data.options, writer);
1122
+ } else if (data.type === 'codex-command') {
1123
+ console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1124
+ console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1125
+ // session message received
1126
+ console.log('🤖 Model:', data.options?.model || 'default');
1127
+ await queryCodex(data.command, data.options, writer);
1128
+ } else if (data.type === 'cursor-resume') {
1129
+ // Backward compatibility: treat as cursor-command with resume and no prompt
1130
+ // cursor resume session
1131
+ await spawnCursor('', {
1132
+ sessionId: data.sessionId,
1133
+ resume: true,
1134
+ cwd: data.options?.cwd
1135
+ }, writer);
1136
+ } else if (data.type === 'abort-session') {
1137
+ // abort session request
1138
+ const provider = data.provider || 'claude';
1139
+ let success;
1140
+
1141
+ if (provider === 'cursor') {
1142
+ success = abortCursorSession(data.sessionId);
1143
+ } else if (provider === 'codex') {
1144
+ success = abortCodexSession(data.sessionId);
1145
+ } else {
1146
+ // Use Claude Agents SDK
1147
+ success = await abortClaudeSDKSession(data.sessionId);
1148
+ }
1149
+
1150
+ writer.send({
1151
+ type: 'session-aborted',
1152
+ sessionId: data.sessionId,
1153
+ provider,
1154
+ success
1155
+ });
1156
+ } else if (data.type === 'claude-permission-response') {
1157
+ // Relay UI approval decisions back into the SDK control flow.
1158
+ // This does not persist permissions; it only resolves the in-flight request,
1159
+ // introduced so the SDK can resume once the user clicks Allow/Deny.
1160
+ if (data.requestId) {
1161
+ resolveToolApproval(data.requestId, {
1162
+ allow: Boolean(data.allow),
1163
+ updatedInput: data.updatedInput,
1164
+ message: data.message,
1165
+ rememberEntry: data.rememberEntry
1166
+ });
1167
+ }
1168
+ } else if (data.type === 'cursor-abort') {
1169
+ // abort cursor session
1170
+ const success = abortCursorSession(data.sessionId);
1171
+ writer.send({
1172
+ type: 'session-aborted',
1173
+ sessionId: data.sessionId,
1174
+ provider: 'cursor',
1175
+ success
1176
+ });
1177
+ } else if (data.type === 'check-session-status') {
1178
+ // Check if a specific session is currently processing
1179
+ const provider = data.provider || 'claude';
1180
+ const sessionId = data.sessionId;
1181
+ let isActive;
1182
+
1183
+ if (provider === 'cursor') {
1184
+ isActive = isCursorSessionActive(sessionId);
1185
+ } else if (provider === 'codex') {
1186
+ isActive = isCodexSessionActive(sessionId);
1187
+ } else {
1188
+ // Use Claude Agents SDK
1189
+ isActive = isClaudeSDKSessionActive(sessionId);
1190
+ }
1191
+
1192
+ writer.send({
1193
+ type: 'session-status',
1194
+ sessionId,
1195
+ provider,
1196
+ isProcessing: isActive
1197
+ });
1198
+ } else if (data.type === 'get-active-sessions') {
1199
+ // Get all currently active sessions
1200
+ const activeSessions = {
1201
+ claude: getActiveClaudeSDKSessions(),
1202
+ cursor: getActiveCursorSessions(),
1203
+ codex: getActiveCodexSessions()
1204
+ };
1205
+ writer.send({
1206
+ type: 'active-sessions',
1207
+ sessions: activeSessions
1208
+ });
1209
+ }
1210
+ } catch (error) {
1211
+ // chat WebSocket error
1212
+ writer.send({
1213
+ type: 'error',
1214
+ error: error.message
1215
+ });
1216
+ }
1217
+ });
1218
+
1219
+ ws.on('close', () => {
1220
+ console.log('🔌 Chat client disconnected');
1221
+ // Release all session locks held by this WebSocket
1222
+ releaseAllLocksForWs(ws);
1223
+ // Remove from connected clients
1224
+ connectedClients.delete(ws);
1225
+ });
1226
+ }
1227
+
1228
+ // Handle relay WebSocket connections (local machine ↔ server bridge)
1229
+ async function handleRelayConnection(ws, token) {
1230
+ if (!token) {
1231
+ ws.send(JSON.stringify({ type: 'error', error: 'Relay token required. Use ?token=upfyn_xxx' }));
1232
+ ws.close();
1233
+ return;
1234
+ }
1235
+
1236
+ const tokenData = await relayTokensDb.validateToken(token);
1237
+ if (!tokenData) {
1238
+ ws.send(JSON.stringify({ type: 'error', error: 'Invalid or expired relay token' }));
1239
+ ws.close();
1240
+ return;
1241
+ }
1242
+
1243
+ const userId = Number(tokenData.user_id);
1244
+ const username = tokenData.username;
1245
+
1246
+ // Store relay connection (use Number() to ensure consistent Map key type)
1247
+ relayConnections.set(userId, { ws, user: tokenData, connectedAt: Date.now() });
1248
+ // relay connection established
1249
+
1250
+ ws.send(JSON.stringify({
1251
+ type: 'relay-connected',
1252
+ message: `Connected as ${username}. Your local machine is now bridged to the server.`
1253
+ }));
1254
+
1255
+ // Broadcast relay status to browser clients
1256
+ for (const client of connectedClients) {
1257
+ try {
1258
+ if (client.readyState === 1) {
1259
+ client.send(JSON.stringify({ type: 'relay-status', userId, connected: true }));
1260
+ }
1261
+ } catch (e) { /* ignore */ }
1262
+ }
1263
+
1264
+ ws.on('message', (message) => {
1265
+ try {
1266
+ const data = JSON.parse(message);
1267
+
1268
+ // Relay response from local machine → resolve pending request
1269
+ if (data.type === 'relay-response' && data.requestId) {
1270
+ const pending = pendingRelayRequests.get(data.requestId);
1271
+ if (pending) {
1272
+ clearTimeout(pending.timeout);
1273
+ pendingRelayRequests.delete(data.requestId);
1274
+ pending.resolve(data);
1275
+ }
1276
+ return;
1277
+ }
1278
+
1279
+ // Relay stream chunk from local machine → forward to browser WebSocket
1280
+ if (data.type === 'relay-stream' && data.requestId) {
1281
+ const pending = pendingRelayRequests.get(data.requestId);
1282
+ if (pending && pending.onStream) {
1283
+ pending.onStream(data.data);
1284
+ }
1285
+ return;
1286
+ }
1287
+
1288
+ // Relay complete signal
1289
+ if (data.type === 'relay-complete' && data.requestId) {
1290
+ const pending = pendingRelayRequests.get(data.requestId);
1291
+ if (pending) {
1292
+ clearTimeout(pending.timeout);
1293
+ pendingRelayRequests.delete(data.requestId);
1294
+ pending.resolve(data);
1295
+ }
1296
+ return;
1297
+ }
1298
+
1299
+ // Heartbeat
1300
+ if (data.type === 'ping') {
1301
+ ws.send(JSON.stringify({ type: 'pong' }));
1302
+ return;
1303
+ }
1304
+ } catch (e) {
1305
+ // relay message processing error
1306
+ }
1307
+ });
1308
+
1309
+ ws.on('close', () => {
1310
+ relayConnections.delete(userId);
1311
+ // Clean up pending requests for this user
1312
+ for (const [reqId, pending] of pendingRelayRequests) {
1313
+ if (pending.userId === userId) {
1314
+ clearTimeout(pending.timeout);
1315
+ pending.reject(new Error('Relay disconnected'));
1316
+ pendingRelayRequests.delete(reqId);
1317
+ }
1318
+ }
1319
+ // relay disconnected
1320
+
1321
+ // Broadcast relay status
1322
+ for (const client of connectedClients) {
1323
+ try {
1324
+ if (client.readyState === 1) {
1325
+ client.send(JSON.stringify({ type: 'relay-status', userId, connected: false }));
1326
+ }
1327
+ } catch (e) { /* ignore */ }
1328
+ }
1329
+ });
1330
+
1331
+ ws.on('error', (err) => {
1332
+ // relay error
1333
+ });
1334
+ }
1335
+
1336
+ /**
1337
+ * Send a command to a user's relay and wait for response
1338
+ * @param {number} userId - User ID
1339
+ * @param {string} action - Action type (claude-query, shell-command, file-read, etc.)
1340
+ * @param {object} payload - Action payload
1341
+ * @param {function} onStream - Optional callback for streaming chunks
1342
+ * @param {number} timeoutMs - Timeout in milliseconds (default 5 min)
1343
+ * @returns {Promise<object>} Relay response
1344
+ */
1345
+ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs = 300000) {
1346
+ return new Promise((resolve, reject) => {
1347
+ const relay = relayConnections.get(userId);
1348
+ if (!relay || relay.ws.readyState !== 1) {
1349
+ reject(new Error('No relay connection. Run "upfynai-code connect" on your local machine.'));
1350
+ return;
1351
+ }
1352
+
1353
+ const requestId = crypto.randomUUID();
1354
+ const timeout = setTimeout(() => {
1355
+ pendingRelayRequests.delete(requestId);
1356
+ reject(new Error('Relay request timed out'));
1357
+ }, timeoutMs);
1358
+
1359
+ pendingRelayRequests.set(requestId, { resolve, reject, timeout, userId, onStream });
1360
+
1361
+ relay.ws.send(JSON.stringify({
1362
+ type: 'relay-command',
1363
+ requestId,
1364
+ action,
1365
+ ...payload
1366
+ }));
1367
+ });
1368
+ }
1369
+
1370
+ // Handle shell WebSocket connections
1371
+ function handleShellConnection(ws) {
1372
+ if (!pty) {
1373
+ ws.send(JSON.stringify({ type: 'output', data: '\r\n[Shell unavailable] node-pty not installed. Use relay connection for shell access.\r\n' }));
1374
+ ws.close();
1375
+ return;
1376
+ }
1377
+ console.log('🐚 Shell client connected');
1378
+ let shellProcess = null;
1379
+ let ptySessionKey = null;
1380
+ let urlDetectionBuffer = '';
1381
+ const announcedAuthUrls = new Set();
1382
+
1383
+ ws.on('message', async (message) => {
1384
+ try {
1385
+ const data = JSON.parse(message);
1386
+ console.log('📨 Shell message received:', data.type);
1387
+
1388
+ if (data.type === 'init') {
1389
+ const projectPath = data.projectPath || process.cwd();
1390
+ const sessionId = data.sessionId;
1391
+ const hasSession = data.hasSession;
1392
+ const provider = data.provider || 'claude';
1393
+ const initialCommand = data.initialCommand;
1394
+ const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
1395
+ urlDetectionBuffer = '';
1396
+ announcedAuthUrls.clear();
1397
+
1398
+ // Login commands (Claude/Cursor auth) should never reuse cached sessions
1399
+ const isLoginCommand = initialCommand && (
1400
+ initialCommand.includes('setup-token') ||
1401
+ initialCommand.includes('cursor-agent login') ||
1402
+ initialCommand.includes('auth login')
1403
+ );
1404
+
1405
+ // Include command hash in session key so different commands get separate sessions
1406
+ const commandSuffix = isPlainShell && initialCommand
1407
+ ? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
1408
+ : '';
1409
+ ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
1410
+
1411
+ // Kill any existing login session before starting fresh
1412
+ if (isLoginCommand) {
1413
+ const oldSession = ptySessionsMap.get(ptySessionKey);
1414
+ if (oldSession) {
1415
+ // cleaning up existing session
1416
+ if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
1417
+ if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
1418
+ ptySessionsMap.delete(ptySessionKey);
1419
+ }
1420
+ }
1421
+
1422
+ const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
1423
+ if (existingSession) {
1424
+ // reconnecting to existing PTY session
1425
+ shellProcess = existingSession.pty;
1426
+
1427
+ clearTimeout(existingSession.timeoutId);
1428
+
1429
+ ws.send(JSON.stringify({
1430
+ type: 'output',
1431
+ data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
1432
+ }));
1433
+
1434
+ if (existingSession.buffer && existingSession.buffer.length > 0) {
1435
+ // sending buffered messages
1436
+ existingSession.buffer.forEach(bufferedData => {
1437
+ ws.send(JSON.stringify({
1438
+ type: 'output',
1439
+ data: bufferedData
1440
+ }));
1441
+ });
1442
+ }
1443
+
1444
+ existingSession.ws = ws;
1445
+
1446
+ return;
1447
+ }
1448
+
1449
+ console.log('[INFO] Starting shell in:', projectPath);
1450
+ // shell session started
1451
+ console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
1452
+ if (initialCommand) {
1453
+ console.log('⚡ Initial command:', initialCommand);
1454
+ }
1455
+
1456
+ // First send a welcome message
1457
+ let welcomeMsg;
1458
+ if (isPlainShell) {
1459
+ welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
1460
+ } else {
1461
+ const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
1462
+ welcomeMsg = hasSession ?
1463
+ `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
1464
+ `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
1465
+ }
1466
+
1467
+ ws.send(JSON.stringify({
1468
+ type: 'output',
1469
+ data: welcomeMsg
1470
+ }));
1471
+
1472
+ try {
1473
+ // Prepare the shell command adapted to the platform and provider
1474
+ let shellCommand;
1475
+ if (isPlainShell) {
1476
+ // Plain shell mode - just run the initial command in the project directory
1477
+ if (os.platform() === 'win32') {
1478
+ shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
1479
+ } else {
1480
+ shellCommand = `cd "${projectPath}" && ${initialCommand}`;
1481
+ }
1482
+ } else if (provider === 'cursor') {
1483
+ // Use cursor-agent command
1484
+ if (os.platform() === 'win32') {
1485
+ if (hasSession && sessionId) {
1486
+ shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent --resume="${sessionId}"`;
1487
+ } else {
1488
+ shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent`;
1489
+ }
1490
+ } else {
1491
+ if (hasSession && sessionId) {
1492
+ shellCommand = `cd "${projectPath}" && cursor-agent --resume="${sessionId}"`;
1493
+ } else {
1494
+ shellCommand = `cd "${projectPath}" && cursor-agent`;
1495
+ }
1496
+ }
1497
+ } else {
1498
+ // Use claude command (default) or initialCommand if provided
1499
+ const command = initialCommand || 'claude';
1500
+ if (os.platform() === 'win32') {
1501
+ if (hasSession && sessionId) {
1502
+ // Try to resume session, but with fallback to new session if it fails
1503
+ shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`;
1504
+ } else {
1505
+ shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
1506
+ }
1507
+ } else {
1508
+ if (hasSession && sessionId) {
1509
+ shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`;
1510
+ } else {
1511
+ shellCommand = `cd "${projectPath}" && ${command}`;
1512
+ }
1513
+ }
1514
+ }
1515
+
1516
+ console.log('🔧 Executing shell command:', shellCommand);
1517
+
1518
+ // Use appropriate shell based on platform
1519
+ const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
1520
+ const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
1521
+
1522
+ // Use terminal dimensions from client if provided, otherwise use defaults
1523
+ const termCols = data.cols || 80;
1524
+ const termRows = data.rows || 24;
1525
+ console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
1526
+
1527
+ shellProcess = pty.spawn(shell, shellArgs, {
1528
+ name: 'xterm-256color',
1529
+ cols: termCols,
1530
+ rows: termRows,
1531
+ cwd: os.homedir(),
1532
+ env: {
1533
+ ...process.env,
1534
+ TERM: 'xterm-256color',
1535
+ COLORTERM: 'truecolor',
1536
+ FORCE_COLOR: '3'
1537
+ }
1538
+ });
1539
+
1540
+ console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
1541
+
1542
+ ptySessionsMap.set(ptySessionKey, {
1543
+ pty: shellProcess,
1544
+ ws: ws,
1545
+ buffer: [],
1546
+ timeoutId: null,
1547
+ projectPath,
1548
+ sessionId
1549
+ });
1550
+
1551
+ // Handle data output
1552
+ shellProcess.onData((data) => {
1553
+ const session = ptySessionsMap.get(ptySessionKey);
1554
+ if (!session) return;
1555
+
1556
+ if (session.buffer.length < 5000) {
1557
+ session.buffer.push(data);
1558
+ } else {
1559
+ session.buffer.shift();
1560
+ session.buffer.push(data);
1561
+ }
1562
+
1563
+ if (session.ws && session.ws.readyState === WebSocket.OPEN) {
1564
+ let outputData = data;
1565
+
1566
+ const cleanChunk = stripAnsiSequences(data);
1567
+ urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
1568
+
1569
+ outputData = outputData.replace(
1570
+ /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
1571
+ '[INFO] Opening in browser: $1'
1572
+ );
1573
+
1574
+ const emitAuthUrl = (detectedUrl, autoOpen = false) => {
1575
+ const normalizedUrl = normalizeDetectedUrl(detectedUrl);
1576
+ if (!normalizedUrl) return;
1577
+
1578
+ const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
1579
+ if (isNewUrl) {
1580
+ announcedAuthUrls.add(normalizedUrl);
1581
+ session.ws.send(JSON.stringify({
1582
+ type: 'auth_url',
1583
+ url: normalizedUrl,
1584
+ autoOpen
1585
+ }));
1586
+ }
1587
+
1588
+ };
1589
+
1590
+ const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
1591
+ .map((url) => normalizeDetectedUrl(url))
1592
+ .filter(Boolean);
1593
+
1594
+ // Prefer the most complete URL if shorter prefix variants are also present.
1595
+ const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) =>
1596
+ !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
1597
+ );
1598
+
1599
+ dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
1600
+
1601
+ if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
1602
+ const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
1603
+ current.length > longest.length ? current : longest
1604
+ );
1605
+ emitAuthUrl(bestUrl, true);
1606
+ }
1607
+
1608
+ // Send regular output
1609
+ session.ws.send(JSON.stringify({
1610
+ type: 'output',
1611
+ data: outputData
1612
+ }));
1613
+ }
1614
+ });
1615
+
1616
+ // Handle process exit
1617
+ shellProcess.onExit((exitCode) => {
1618
+ console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
1619
+ const session = ptySessionsMap.get(ptySessionKey);
1620
+ if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
1621
+ session.ws.send(JSON.stringify({
1622
+ type: 'output',
1623
+ data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
1624
+ }));
1625
+ }
1626
+ if (session && session.timeoutId) {
1627
+ clearTimeout(session.timeoutId);
1628
+ }
1629
+ ptySessionsMap.delete(ptySessionKey);
1630
+ shellProcess = null;
1631
+ });
1632
+
1633
+ } catch (spawnError) {
1634
+ console.error('[ERROR] Error spawning process:', spawnError);
1635
+ ws.send(JSON.stringify({
1636
+ type: 'output',
1637
+ data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
1638
+ }));
1639
+ }
1640
+
1641
+ } else if (data.type === 'input') {
1642
+ // Send input to shell process
1643
+ if (shellProcess && shellProcess.write) {
1644
+ try {
1645
+ shellProcess.write(data.data);
1646
+ } catch (error) {
1647
+ console.error('Error writing to shell:', error);
1648
+ }
1649
+ } else {
1650
+ console.warn('No active shell process to send input to');
1651
+ }
1652
+ } else if (data.type === 'resize') {
1653
+ // Handle terminal resize
1654
+ if (shellProcess && shellProcess.resize) {
1655
+ console.log('Terminal resize requested:', data.cols, 'x', data.rows);
1656
+ shellProcess.resize(data.cols, data.rows);
1657
+ }
1658
+ }
1659
+ } catch (error) {
1660
+ // shell WebSocket error
1661
+ if (ws.readyState === WebSocket.OPEN) {
1662
+ ws.send(JSON.stringify({
1663
+ type: 'output',
1664
+ data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
1665
+ }));
1666
+ }
1667
+ }
1668
+ });
1669
+
1670
+ ws.on('close', () => {
1671
+ console.log('🔌 Shell client disconnected');
1672
+
1673
+ if (ptySessionKey) {
1674
+ const session = ptySessionsMap.get(ptySessionKey);
1675
+ if (session) {
1676
+ // PTY session kept alive
1677
+ session.ws = null;
1678
+
1679
+ session.timeoutId = setTimeout(() => {
1680
+ // PTY session timeout
1681
+ if (session.pty && session.pty.kill) {
1682
+ session.pty.kill();
1683
+ }
1684
+ ptySessionsMap.delete(ptySessionKey);
1685
+ }, PTY_SESSION_TIMEOUT);
1686
+ }
1687
+ }
1688
+ });
1689
+
1690
+ ws.on('error', (error) => {
1691
+ // shell error
1692
+ });
1693
+ }
1694
+ // Audio transcription endpoint
1695
+ app.post('/api/transcribe', authenticateToken, async (req, res) => {
1696
+ try {
1697
+ const multer = (await import('multer')).default;
1698
+ const upload = multer({ storage: multer.memoryStorage() });
1699
+
1700
+ // Handle multipart form data
1701
+ upload.single('audio')(req, res, async (err) => {
1702
+ if (err) {
1703
+ return res.status(400).json({ error: 'Failed to process audio file' });
1704
+ }
1705
+
1706
+ if (!req.file) {
1707
+ return res.status(400).json({ error: 'No audio file provided' });
1708
+ }
1709
+
1710
+ const apiKey = process.env.OPENAI_API_KEY;
1711
+ if (!apiKey) {
1712
+ return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' });
1713
+ }
1714
+
1715
+ try {
1716
+ // Create form data for OpenAI
1717
+ const FormData = (await import('form-data')).default;
1718
+ const formData = new FormData();
1719
+ formData.append('file', req.file.buffer, {
1720
+ filename: req.file.originalname,
1721
+ contentType: req.file.mimetype
1722
+ });
1723
+ formData.append('model', 'whisper-1');
1724
+ formData.append('response_format', 'json');
1725
+ formData.append('language', 'en');
1726
+
1727
+ // Make request to OpenAI
1728
+ const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
1729
+ method: 'POST',
1730
+ headers: {
1731
+ 'Authorization': `Bearer ${apiKey}`,
1732
+ ...formData.getHeaders()
1733
+ },
1734
+ body: formData
1735
+ });
1736
+
1737
+ if (!response.ok) {
1738
+ const errorData = await response.json().catch(() => ({}));
1739
+ throw new Error(errorData.error?.message || `Whisper API error: ${response.status}`);
1740
+ }
1741
+
1742
+ const data = await response.json();
1743
+ let transcribedText = data.text || '';
1744
+
1745
+ // Check if enhancement mode is enabled
1746
+ const mode = req.body.mode || 'default';
1747
+
1748
+ // If no transcribed text, return empty
1749
+ if (!transcribedText) {
1750
+ return res.json({ text: '' });
1751
+ }
1752
+
1753
+ // If default mode, return transcribed text without enhancement
1754
+ if (mode === 'default') {
1755
+ return res.json({ text: transcribedText });
1756
+ }
1757
+
1758
+ // Handle different enhancement modes
1759
+ try {
1760
+ const OpenAI = (await import('openai')).default;
1761
+ const openai = new OpenAI({ apiKey });
1762
+
1763
+ let prompt, systemMessage, temperature = 0.7, maxTokens = 800;
1764
+
1765
+ switch (mode) {
1766
+ case 'prompt':
1767
+ systemMessage = 'You are an expert prompt engineer who creates clear, detailed, and effective prompts.';
1768
+ prompt = `You are an expert prompt engineer. Transform the following rough instruction into a clear, detailed, and context-aware AI prompt.
1769
+
1770
+ Your enhanced prompt should:
1771
+ 1. Be specific and unambiguous
1772
+ 2. Include relevant context and constraints
1773
+ 3. Specify the desired output format
1774
+ 4. Use clear, actionable language
1775
+ 5. Include examples where helpful
1776
+ 6. Consider edge cases and potential ambiguities
1777
+
1778
+ Transform this rough instruction into a well-crafted prompt:
1779
+ "${transcribedText}"
1780
+
1781
+ Enhanced prompt:`;
1782
+ break;
1783
+
1784
+ case 'vibe':
1785
+ case 'instructions':
1786
+ case 'architect':
1787
+ systemMessage = 'You are a helpful assistant that formats ideas into clear, actionable instructions for AI agents.';
1788
+ temperature = 0.5; // Lower temperature for more controlled output
1789
+ prompt = `Transform the following idea into clear, well-structured instructions that an AI agent can easily understand and execute.
1790
+
1791
+ IMPORTANT RULES:
1792
+ - Format as clear, step-by-step instructions
1793
+ - Add reasonable implementation details based on common patterns
1794
+ - Only include details directly related to what was asked
1795
+ - Do NOT add features or functionality not mentioned
1796
+ - Keep the original intent and scope intact
1797
+ - Use clear, actionable language an agent can follow
1798
+
1799
+ Transform this idea into agent-friendly instructions:
1800
+ "${transcribedText}"
1801
+
1802
+ Agent instructions:`;
1803
+ break;
1804
+
1805
+ default:
1806
+ // No enhancement needed
1807
+ break;
1808
+ }
1809
+
1810
+ // Only make GPT call if we have a prompt
1811
+ if (prompt) {
1812
+ const completion = await openai.chat.completions.create({
1813
+ model: 'gpt-4o-mini',
1814
+ messages: [
1815
+ { role: 'system', content: systemMessage },
1816
+ { role: 'user', content: prompt }
1817
+ ],
1818
+ temperature: temperature,
1819
+ max_tokens: maxTokens
1820
+ });
1821
+
1822
+ transcribedText = completion.choices[0].message.content || transcribedText;
1823
+ }
1824
+
1825
+ } catch (gptError) {
1826
+ console.error('GPT processing error:', gptError);
1827
+ // Fall back to original transcription if GPT fails
1828
+ }
1829
+
1830
+ res.json({ text: transcribedText });
1831
+
1832
+ } catch (error) {
1833
+ console.error('Transcription error:', error);
1834
+ res.status(500).json({ error: error.message });
1835
+ }
1836
+ });
1837
+ } catch (error) {
1838
+ console.error('Endpoint error:', error);
1839
+ res.status(500).json({ error: 'Internal server error' });
1840
+ }
1841
+ });
1842
+
1843
+ // Image upload endpoint
1844
+ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
1845
+ try {
1846
+ const multer = (await import('multer')).default;
1847
+ const path = (await import('path')).default;
1848
+ const fs = (await import('fs')).promises;
1849
+ const os = (await import('os')).default;
1850
+
1851
+ // Configure multer for image uploads
1852
+ const storage = multer.diskStorage({
1853
+ destination: async (req, file, cb) => {
1854
+ const uploadDir = path.join(os.tmpdir(), 'claude-ui-uploads', String(req.user.id));
1855
+ await fs.mkdir(uploadDir, { recursive: true });
1856
+ cb(null, uploadDir);
1857
+ },
1858
+ filename: (req, file, cb) => {
1859
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
1860
+ const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
1861
+ cb(null, uniqueSuffix + '-' + sanitizedName);
1862
+ }
1863
+ });
1864
+
1865
+ const fileFilter = (req, file, cb) => {
1866
+ const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
1867
+ if (allowedMimes.includes(file.mimetype)) {
1868
+ cb(null, true);
1869
+ } else {
1870
+ cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
1871
+ }
1872
+ };
1873
+
1874
+ const upload = multer({
1875
+ storage,
1876
+ fileFilter,
1877
+ limits: {
1878
+ fileSize: 5 * 1024 * 1024, // 5MB
1879
+ files: 5
1880
+ }
1881
+ });
1882
+
1883
+ // Handle multipart form data
1884
+ upload.array('images', 5)(req, res, async (err) => {
1885
+ if (err) {
1886
+ return res.status(400).json({ error: err.message });
1887
+ }
1888
+
1889
+ if (!req.files || req.files.length === 0) {
1890
+ return res.status(400).json({ error: 'No image files provided' });
1891
+ }
1892
+
1893
+ try {
1894
+ // Process uploaded images
1895
+ const processedImages = await Promise.all(
1896
+ req.files.map(async (file) => {
1897
+ // Read file and convert to base64
1898
+ const buffer = await fs.readFile(file.path);
1899
+ const base64 = buffer.toString('base64');
1900
+ const mimeType = file.mimetype;
1901
+
1902
+ // Clean up temp file immediately
1903
+ await fs.unlink(file.path);
1904
+
1905
+ return {
1906
+ name: file.originalname,
1907
+ data: `data:${mimeType};base64,${base64}`,
1908
+ size: file.size,
1909
+ mimeType: mimeType
1910
+ };
1911
+ })
1912
+ );
1913
+
1914
+ res.json({ images: processedImages });
1915
+ } catch (error) {
1916
+ console.error('Error processing images:', error);
1917
+ // Clean up any remaining files
1918
+ await Promise.all(req.files.map(f => fs.unlink(f.path).catch(() => { })));
1919
+ res.status(500).json({ error: 'Failed to process images' });
1920
+ }
1921
+ });
1922
+ } catch (error) {
1923
+ console.error('Error in image upload endpoint:', error);
1924
+ res.status(500).json({ error: 'Internal server error' });
1925
+ }
1926
+ });
1927
+
1928
+ // Get token usage for a specific session
1929
+ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1930
+ try {
1931
+ const { projectName, sessionId } = req.params;
1932
+ const { provider = 'claude' } = req.query;
1933
+ const homeDir = os.homedir();
1934
+
1935
+ // Allow only safe characters in sessionId
1936
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
1937
+ if (!safeSessionId) {
1938
+ return res.status(400).json({ error: 'Invalid sessionId' });
1939
+ }
1940
+
1941
+ // Handle Cursor sessions - they use SQLite and don't have token usage info
1942
+ if (provider === 'cursor') {
1943
+ return res.json({
1944
+ used: 0,
1945
+ total: 0,
1946
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1947
+ unsupported: true,
1948
+ message: 'Token usage tracking not available for Cursor sessions'
1949
+ });
1950
+ }
1951
+
1952
+ // Handle Codex sessions
1953
+ if (provider === 'codex') {
1954
+ const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
1955
+
1956
+ // Find the session file by searching for the session ID
1957
+ const findSessionFile = async (dir) => {
1958
+ try {
1959
+ const entries = await fsPromises.readdir(dir, { withFileTypes: true });
1960
+ for (const entry of entries) {
1961
+ const fullPath = path.join(dir, entry.name);
1962
+ if (entry.isDirectory()) {
1963
+ const found = await findSessionFile(fullPath);
1964
+ if (found) return found;
1965
+ } else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
1966
+ return fullPath;
1967
+ }
1968
+ }
1969
+ } catch (error) {
1970
+ // Skip directories we can't read
1971
+ }
1972
+ return null;
1973
+ };
1974
+
1975
+ const sessionFilePath = await findSessionFile(codexSessionsDir);
1976
+
1977
+ if (!sessionFilePath) {
1978
+ return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
1979
+ }
1980
+
1981
+ // Read and parse the Codex JSONL file
1982
+ let fileContent;
1983
+ try {
1984
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1985
+ } catch (error) {
1986
+ if (error.code === 'ENOENT') {
1987
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1988
+ }
1989
+ throw error;
1990
+ }
1991
+ const lines = fileContent.trim().split('\n');
1992
+ let totalTokens = 0;
1993
+ let contextWindow = 200000; // Default for Codex/OpenAI
1994
+
1995
+ // Find the latest token_count event with info (scan from end)
1996
+ for (let i = lines.length - 1; i >= 0; i--) {
1997
+ try {
1998
+ const entry = JSON.parse(lines[i]);
1999
+
2000
+ // Codex stores token info in event_msg with type: "token_count"
2001
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
2002
+ const tokenInfo = entry.payload.info;
2003
+ if (tokenInfo.total_token_usage) {
2004
+ totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
2005
+ }
2006
+ if (tokenInfo.model_context_window) {
2007
+ contextWindow = tokenInfo.model_context_window;
2008
+ }
2009
+ break; // Stop after finding the latest token count
2010
+ }
2011
+ } catch (parseError) {
2012
+ // Skip lines that can't be parsed
2013
+ continue;
2014
+ }
2015
+ }
2016
+
2017
+ return res.json({
2018
+ used: totalTokens,
2019
+ total: contextWindow
2020
+ });
2021
+ }
2022
+
2023
+ // Handle Claude sessions (default)
2024
+ // Extract actual project path
2025
+ let projectPath;
2026
+ try {
2027
+ projectPath = await extractProjectDirectory(projectName);
2028
+ } catch (error) {
2029
+ console.error('Error extracting project directory:', error);
2030
+ return res.status(500).json({ error: 'Failed to determine project path' });
2031
+ }
2032
+
2033
+ // Construct the JSONL file path
2034
+ // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
2035
+ // The encoding replaces /, spaces, ~, and _ with -
2036
+ const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
2037
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
2038
+
2039
+ const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
2040
+
2041
+ // Constrain to projectDir
2042
+ const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
2043
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
2044
+ return res.status(400).json({ error: 'Invalid path' });
2045
+ }
2046
+
2047
+ // Read and parse the JSONL file
2048
+ let fileContent;
2049
+ try {
2050
+ fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
2051
+ } catch (error) {
2052
+ if (error.code === 'ENOENT') {
2053
+ return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
2054
+ }
2055
+ throw error; // Re-throw other errors to be caught by outer try-catch
2056
+ }
2057
+ const lines = fileContent.trim().split('\n');
2058
+
2059
+ const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
2060
+ const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
2061
+ let inputTokens = 0;
2062
+ let cacheCreationTokens = 0;
2063
+ let cacheReadTokens = 0;
2064
+
2065
+ // Find the latest assistant message with usage data (scan from end)
2066
+ for (let i = lines.length - 1; i >= 0; i--) {
2067
+ try {
2068
+ const entry = JSON.parse(lines[i]);
2069
+
2070
+ // Only count assistant messages which have usage data
2071
+ if (entry.type === 'assistant' && entry.message?.usage) {
2072
+ const usage = entry.message.usage;
2073
+
2074
+ // Use token counts from latest assistant message only
2075
+ inputTokens = usage.input_tokens || 0;
2076
+ cacheCreationTokens = usage.cache_creation_input_tokens || 0;
2077
+ cacheReadTokens = usage.cache_read_input_tokens || 0;
2078
+
2079
+ break; // Stop after finding the latest assistant message
2080
+ }
2081
+ } catch (parseError) {
2082
+ // Skip lines that can't be parsed
2083
+ continue;
2084
+ }
2085
+ }
2086
+
2087
+ // Calculate total context usage (excluding output_tokens, as per ccusage)
2088
+ const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
2089
+
2090
+ res.json({
2091
+ used: totalUsed,
2092
+ total: contextWindow,
2093
+ breakdown: {
2094
+ input: inputTokens,
2095
+ cacheCreation: cacheCreationTokens,
2096
+ cacheRead: cacheReadTokens
2097
+ }
2098
+ });
2099
+ } catch (error) {
2100
+ // token usage read error
2101
+ res.status(500).json({ error: 'Failed to read session token usage' });
2102
+ }
2103
+ });
2104
+
2105
+ // Serve React app for all other routes (excluding static files and API routes)
2106
+ app.get('*', (req, res) => {
2107
+ // Skip API routes — they should be handled by their own routers
2108
+ if (req.path.startsWith('/api/') || req.path === '/mcp' || req.path === '/relay' || req.path === '/health') {
2109
+ return res.status(404).json({ error: 'Not found' });
2110
+ }
2111
+ // Skip requests for static assets (files with extensions)
2112
+ if (path.extname(req.path)) {
2113
+ return res.status(404).send('Not found');
2114
+ }
2115
+
2116
+ // Only serve index.html for HTML routes, not for static assets
2117
+ // Static assets should already be handled by express.static middleware above
2118
+ const indexPath = path.join(__dirname, '../dist/index.html');
2119
+
2120
+ // Check if dist/index.html exists (production build available)
2121
+ if (fs.existsSync(indexPath)) {
2122
+ // Set no-cache headers for HTML to prevent service worker issues
2123
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
2124
+ res.setHeader('Pragma', 'no-cache');
2125
+ res.setHeader('Expires', '0');
2126
+ res.sendFile(indexPath);
2127
+ } else {
2128
+ // In development, redirect to Vite dev server only if dist doesn't exist
2129
+ res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
2130
+ }
2131
+ });
2132
+
2133
+ // Helper function to convert permissions to rwx format
2134
+ function permToRwx(perm) {
2135
+ const r = perm & 4 ? 'r' : '-';
2136
+ const w = perm & 2 ? 'w' : '-';
2137
+ const x = perm & 1 ? 'x' : '-';
2138
+ return r + w + x;
2139
+ }
2140
+
2141
+ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
2142
+ // Using fsPromises from import
2143
+ const items = [];
2144
+
2145
+ try {
2146
+ const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
2147
+
2148
+ for (const entry of entries) {
2149
+ // Debug: log all entries including hidden files
2150
+
2151
+
2152
+ // Skip heavy build directories and VCS directories
2153
+ if (entry.name === 'node_modules' ||
2154
+ entry.name === 'dist' ||
2155
+ entry.name === 'build' ||
2156
+ entry.name === '.git' ||
2157
+ entry.name === '.svn' ||
2158
+ entry.name === '.hg') continue;
2159
+
2160
+ const itemPath = path.join(dirPath, entry.name);
2161
+ const item = {
2162
+ name: entry.name,
2163
+ path: itemPath,
2164
+ type: entry.isDirectory() ? 'directory' : 'file'
2165
+ };
2166
+
2167
+ // Get file stats for additional metadata
2168
+ try {
2169
+ const stats = await fsPromises.stat(itemPath);
2170
+ item.size = stats.size;
2171
+ item.modified = stats.mtime.toISOString();
2172
+
2173
+ // Convert permissions to rwx format
2174
+ const mode = stats.mode;
2175
+ const ownerPerm = (mode >> 6) & 7;
2176
+ const groupPerm = (mode >> 3) & 7;
2177
+ const otherPerm = mode & 7;
2178
+ item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
2179
+ item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
2180
+ } catch (statError) {
2181
+ // If stat fails, provide default values
2182
+ item.size = 0;
2183
+ item.modified = null;
2184
+ item.permissions = '000';
2185
+ item.permissionsRwx = '---------';
2186
+ }
2187
+
2188
+ if (entry.isDirectory() && currentDepth < maxDepth) {
2189
+ // Recursively get subdirectories but limit depth
2190
+ try {
2191
+ // Check if we can access the directory before trying to read it
2192
+ await fsPromises.access(item.path, fs.constants.R_OK);
2193
+ item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
2194
+ } catch (e) {
2195
+ // Silently skip directories we can't access (permission denied, etc.)
2196
+ item.children = [];
2197
+ }
2198
+ }
2199
+
2200
+ items.push(item);
2201
+ }
2202
+ } catch (error) {
2203
+ // Only log non-permission errors to avoid spam
2204
+ if (error.code !== 'EACCES' && error.code !== 'EPERM') {
2205
+ console.error('Error reading directory:', error);
2206
+ }
2207
+ }
2208
+
2209
+ return items.sort((a, b) => {
2210
+ if (a.type !== b.type) {
2211
+ return a.type === 'directory' ? -1 : 1;
2212
+ }
2213
+ return a.name.localeCompare(b.name);
2214
+ });
2215
+ }
2216
+
2217
+ const PORT = process.env.PORT || 3001;
2218
+
2219
+ // Initialize database and start server
2220
+ async function startServer() {
2221
+ try {
2222
+ // Initialize authentication database
2223
+ await initializeDatabase();
2224
+
2225
+ // Check if running in production mode (dist folder exists OR NODE_ENV/RAILWAY set)
2226
+ const distIndexPath = path.join(__dirname, '../dist/index.html');
2227
+ const isProduction = fs.existsSync(distIndexPath) || process.env.NODE_ENV === 'production' || !!process.env.RAILWAY_ENVIRONMENT;
2228
+
2229
+ // Log Claude implementation mode
2230
+ console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
2231
+ console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);
2232
+
2233
+ if (!isProduction) {
2234
+ console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
2235
+ }
2236
+
2237
+ server.listen(PORT, '0.0.0.0', async () => {
2238
+ const appInstallPath = path.join(__dirname, '..');
2239
+
2240
+ console.log('');
2241
+ console.log(c.dim('═'.repeat(63)));
2242
+ console.log(` ${c.bright('Upfyn-Code Server - Ready')}`);
2243
+ console.log(c.dim('═'.repeat(63)));
2244
+ console.log('');
2245
+ console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://0.0.0.0:' + PORT)}`);
2246
+ console.log(`${c.info('[INFO]')} MCP Server: ${c.bright('http://0.0.0.0:' + PORT + '/mcp')}`);
2247
+ console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
2248
+ console.log(`${c.tip('[TIP]')} Run "uc status" for full configuration details`);
2249
+ console.log('');
2250
+
2251
+ // Start watching the projects folder for changes (skip on Vercel)
2252
+ if (!process.env.VERCEL) {
2253
+ await setupProjectsWatcher();
2254
+ }
2255
+ });
2256
+ } catch (error) {
2257
+ console.error('[ERROR] Failed to start server:', error);
2258
+ process.exit(1);
2259
+ }
2260
+ }
2261
+
2262
+ // Only start server when not running on Vercel (Vercel uses the exported app)
2263
+ if (!process.env.VERCEL) {
2264
+ startServer();
2265
+ }
2266
+
2267
+ // Export for Vercel serverless and testing
2268
+ export default app;
2269
+ export { app, server, relayConnections, sendRelayCommand };