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,1231 @@
1
+ import express from 'express';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { promises as fs } from 'fs';
6
+ import crypto from 'crypto';
7
+ import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js';
8
+ import { addProjectManually } from '../projects.js';
9
+ import { queryClaudeSDK } from '../claude-sdk.js';
10
+ import { spawnCursor } from '../cursor-cli.js';
11
+ import { queryCodex } from '../openai-codex.js';
12
+ import { Octokit } from '@octokit/rest';
13
+ import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
14
+ import { IS_PLATFORM } from '../constants/config.js';
15
+
16
+ const router = express.Router();
17
+
18
+ /**
19
+ * Middleware to authenticate agent API requests.
20
+ *
21
+ * Supports two authentication modes:
22
+ * 1. Platform mode (IS_PLATFORM=true): For managed/hosted deployments where
23
+ * authentication is handled by an external proxy. Requests are trusted and
24
+ * the default user context is used.
25
+ *
26
+ * 2. API key mode (default): For self-hosted deployments where users authenticate
27
+ * via API keys created in the UI. Keys are validated against the local database.
28
+ */
29
+ const validateExternalApiKey = async (req, res, next) => {
30
+ // Platform mode: Authentication is handled externally (e.g., by a proxy layer).
31
+ // Trust the request and use the default user context.
32
+ if (IS_PLATFORM) {
33
+ try {
34
+ const user = await userDb.getFirstUser();
35
+ if (!user) {
36
+ return res.status(500).json({ error: 'Platform mode: No user found in database' });
37
+ }
38
+ req.user = user;
39
+ return next();
40
+ } catch (error) {
41
+ console.error('Platform mode error:', error);
42
+ return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
43
+ }
44
+ }
45
+
46
+ // Self-hosted mode: Validate API key from header or query parameter
47
+ const apiKey = req.headers['x-api-key'] || req.query.apiKey;
48
+
49
+ if (!apiKey) {
50
+ return res.status(401).json({ error: 'API key required' });
51
+ }
52
+
53
+ const user = await apiKeysDb.validateApiKey(apiKey);
54
+
55
+ if (!user) {
56
+ return res.status(401).json({ error: 'Invalid or inactive API key' });
57
+ }
58
+
59
+ req.user = user;
60
+ next();
61
+ };
62
+
63
+ /**
64
+ * Get the remote URL of a git repository
65
+ * @param {string} repoPath - Path to the git repository
66
+ * @returns {Promise<string>} - Remote URL of the repository
67
+ */
68
+ async function getGitRemoteUrl(repoPath) {
69
+ return new Promise((resolve, reject) => {
70
+ const gitProcess = spawn('git', ['config', '--get', 'remote.origin.url'], {
71
+ cwd: repoPath,
72
+ stdio: ['pipe', 'pipe', 'pipe']
73
+ });
74
+
75
+ let stdout = '';
76
+ let stderr = '';
77
+
78
+ gitProcess.stdout.on('data', (data) => {
79
+ stdout += data.toString();
80
+ });
81
+
82
+ gitProcess.stderr.on('data', (data) => {
83
+ stderr += data.toString();
84
+ });
85
+
86
+ gitProcess.on('close', (code) => {
87
+ if (code === 0) {
88
+ resolve(stdout.trim());
89
+ } else {
90
+ reject(new Error(`Failed to get git remote: ${stderr}`));
91
+ }
92
+ });
93
+
94
+ gitProcess.on('error', (error) => {
95
+ reject(new Error(`Failed to execute git: ${error.message}`));
96
+ });
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Normalize GitHub URLs for comparison
102
+ * @param {string} url - GitHub URL
103
+ * @returns {string} - Normalized URL
104
+ */
105
+ function normalizeGitHubUrl(url) {
106
+ // Remove .git suffix
107
+ let normalized = url.replace(/\.git$/, '');
108
+ // Convert SSH to HTTPS format for comparison
109
+ normalized = normalized.replace(/^git@github\.com:/, 'https://github.com/');
110
+ // Remove trailing slash
111
+ normalized = normalized.replace(/\/$/, '');
112
+ return normalized.toLowerCase();
113
+ }
114
+
115
+ /**
116
+ * Parse GitHub URL to extract owner and repo
117
+ * @param {string} url - GitHub URL (HTTPS or SSH)
118
+ * @returns {{owner: string, repo: string}} - Parsed owner and repo
119
+ */
120
+ function parseGitHubUrl(url) {
121
+ // Handle HTTPS URLs: https://github.com/owner/repo or https://github.com/owner/repo.git
122
+ // Handle SSH URLs: git@github.com:owner/repo or git@github.com:owner/repo.git
123
+ const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
124
+ if (!match) {
125
+ throw new Error('Invalid GitHub URL format');
126
+ }
127
+ return {
128
+ owner: match[1],
129
+ repo: match[2].replace(/\.git$/, '')
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Auto-generate a branch name from a message
135
+ * @param {string} message - The agent message
136
+ * @returns {string} - Generated branch name
137
+ */
138
+ function autogenerateBranchName(message) {
139
+ // Convert to lowercase, replace spaces/special chars with hyphens
140
+ let branchName = message
141
+ .toLowerCase()
142
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
143
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
144
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
145
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
146
+
147
+ // Ensure non-empty fallback
148
+ if (!branchName) {
149
+ branchName = 'task';
150
+ }
151
+
152
+ // Generate timestamp suffix (last 6 chars of base36 timestamp)
153
+ const timestamp = Date.now().toString(36).slice(-6);
154
+ const suffix = `-${timestamp}`;
155
+
156
+ // Limit length to ensure total length including suffix fits within 50 characters
157
+ const maxBaseLength = 50 - suffix.length;
158
+ if (branchName.length > maxBaseLength) {
159
+ branchName = branchName.substring(0, maxBaseLength);
160
+ }
161
+
162
+ // Remove any trailing hyphen after truncation and ensure no leading hyphen
163
+ branchName = branchName.replace(/-$/, '').replace(/^-+/, '');
164
+
165
+ // If still empty or starts with hyphen after cleanup, use fallback
166
+ if (!branchName || branchName.startsWith('-')) {
167
+ branchName = 'task';
168
+ }
169
+
170
+ // Combine base name with timestamp suffix
171
+ branchName = `${branchName}${suffix}`;
172
+
173
+ // Final validation: ensure it matches safe pattern
174
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(branchName)) {
175
+ // Fallback to deterministic safe name
176
+ return `branch-${timestamp}`;
177
+ }
178
+
179
+ return branchName;
180
+ }
181
+
182
+ /**
183
+ * Validate a Git branch name
184
+ * @param {string} branchName - Branch name to validate
185
+ * @returns {{valid: boolean, error?: string}} - Validation result
186
+ */
187
+ function validateBranchName(branchName) {
188
+ if (!branchName || branchName.trim() === '') {
189
+ return { valid: false, error: 'Branch name cannot be empty' };
190
+ }
191
+
192
+ // Git branch name rules
193
+ const invalidPatterns = [
194
+ { pattern: /^\./, message: 'Branch name cannot start with a dot' },
195
+ { pattern: /\.$/, message: 'Branch name cannot end with a dot' },
196
+ { pattern: /\.\./, message: 'Branch name cannot contain consecutive dots (..)' },
197
+ { pattern: /\s/, message: 'Branch name cannot contain spaces' },
198
+ { pattern: /[~^:?*\[\\]/, message: 'Branch name cannot contain special characters: ~ ^ : ? * [ \\' },
199
+ { pattern: /@{/, message: 'Branch name cannot contain @{' },
200
+ { pattern: /\/$/, message: 'Branch name cannot end with a slash' },
201
+ { pattern: /^\//, message: 'Branch name cannot start with a slash' },
202
+ { pattern: /\/\//, message: 'Branch name cannot contain consecutive slashes' },
203
+ { pattern: /\.lock$/, message: 'Branch name cannot end with .lock' }
204
+ ];
205
+
206
+ for (const { pattern, message } of invalidPatterns) {
207
+ if (pattern.test(branchName)) {
208
+ return { valid: false, error: message };
209
+ }
210
+ }
211
+
212
+ // Check for ASCII control characters
213
+ if (/[\x00-\x1F\x7F]/.test(branchName)) {
214
+ return { valid: false, error: 'Branch name cannot contain control characters' };
215
+ }
216
+
217
+ return { valid: true };
218
+ }
219
+
220
+ /**
221
+ * Get recent commit messages from a repository
222
+ * @param {string} projectPath - Path to the git repository
223
+ * @param {number} limit - Number of commits to retrieve (default: 5)
224
+ * @returns {Promise<string[]>} - Array of commit messages
225
+ */
226
+ async function getCommitMessages(projectPath, limit = 5) {
227
+ return new Promise((resolve, reject) => {
228
+ const gitProcess = spawn('git', ['log', `-${limit}`, '--pretty=format:%s'], {
229
+ cwd: projectPath,
230
+ stdio: ['pipe', 'pipe', 'pipe']
231
+ });
232
+
233
+ let stdout = '';
234
+ let stderr = '';
235
+
236
+ gitProcess.stdout.on('data', (data) => {
237
+ stdout += data.toString();
238
+ });
239
+
240
+ gitProcess.stderr.on('data', (data) => {
241
+ stderr += data.toString();
242
+ });
243
+
244
+ gitProcess.on('close', (code) => {
245
+ if (code === 0) {
246
+ const messages = stdout.trim().split('\n').filter(msg => msg.length > 0);
247
+ resolve(messages);
248
+ } else {
249
+ reject(new Error(`Failed to get commit messages: ${stderr}`));
250
+ }
251
+ });
252
+
253
+ gitProcess.on('error', (error) => {
254
+ reject(new Error(`Failed to execute git: ${error.message}`));
255
+ });
256
+ });
257
+ }
258
+
259
+ /**
260
+ * Create a new branch on GitHub using the API
261
+ * @param {Octokit} octokit - Octokit instance
262
+ * @param {string} owner - Repository owner
263
+ * @param {string} repo - Repository name
264
+ * @param {string} branchName - Name of the new branch
265
+ * @param {string} baseBranch - Base branch to branch from (default: 'main')
266
+ * @returns {Promise<void>}
267
+ */
268
+ async function createGitHubBranch(octokit, owner, repo, branchName, baseBranch = 'main') {
269
+ try {
270
+ // Get the SHA of the base branch
271
+ const { data: ref } = await octokit.git.getRef({
272
+ owner,
273
+ repo,
274
+ ref: `heads/${baseBranch}`
275
+ });
276
+
277
+ const baseSha = ref.object.sha;
278
+
279
+ // Create the new branch
280
+ await octokit.git.createRef({
281
+ owner,
282
+ repo,
283
+ ref: `refs/heads/${branchName}`,
284
+ sha: baseSha
285
+ });
286
+
287
+ console.log(`✅ Created branch '${branchName}' on GitHub`);
288
+ } catch (error) {
289
+ if (error.status === 422 && error.message.includes('Reference already exists')) {
290
+ console.log(`ℹ️ Branch '${branchName}' already exists on GitHub`);
291
+ } else {
292
+ throw error;
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Create a pull request on GitHub
299
+ * @param {Octokit} octokit - Octokit instance
300
+ * @param {string} owner - Repository owner
301
+ * @param {string} repo - Repository name
302
+ * @param {string} branchName - Head branch name
303
+ * @param {string} title - PR title
304
+ * @param {string} body - PR body/description
305
+ * @param {string} baseBranch - Base branch (default: 'main')
306
+ * @returns {Promise<{number: number, url: string}>} - PR number and URL
307
+ */
308
+ async function createGitHubPR(octokit, owner, repo, branchName, title, body, baseBranch = 'main') {
309
+ const { data: pr } = await octokit.pulls.create({
310
+ owner,
311
+ repo,
312
+ title,
313
+ head: branchName,
314
+ base: baseBranch,
315
+ body
316
+ });
317
+
318
+ console.log(`✅ Created pull request #${pr.number}: ${pr.html_url}`);
319
+
320
+ return {
321
+ number: pr.number,
322
+ url: pr.html_url
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Clone a GitHub repository to a directory
328
+ * @param {string} githubUrl - GitHub repository URL
329
+ * @param {string} githubToken - Optional GitHub token for private repos
330
+ * @param {string} projectPath - Path for cloning the repository
331
+ * @returns {Promise<string>} - Path to the cloned repository
332
+ */
333
+ async function cloneGitHubRepo(githubUrl, githubToken = null, projectPath) {
334
+ return new Promise(async (resolve, reject) => {
335
+ try {
336
+ // Validate GitHub URL
337
+ if (!githubUrl || !githubUrl.includes('github.com')) {
338
+ throw new Error('Invalid GitHub URL');
339
+ }
340
+
341
+ const cloneDir = path.resolve(projectPath);
342
+
343
+ // Check if directory already exists
344
+ try {
345
+ await fs.access(cloneDir);
346
+ // Directory exists - check if it's a git repo with the same URL
347
+ try {
348
+ const existingUrl = await getGitRemoteUrl(cloneDir);
349
+ const normalizedExisting = normalizeGitHubUrl(existingUrl);
350
+ const normalizedRequested = normalizeGitHubUrl(githubUrl);
351
+
352
+ if (normalizedExisting === normalizedRequested) {
353
+ console.log('✅ Repository already exists at path with correct URL');
354
+ return resolve(cloneDir);
355
+ } else {
356
+ throw new Error(`Directory ${cloneDir} already exists with a different repository (${existingUrl}). Expected: ${githubUrl}`);
357
+ }
358
+ } catch (gitError) {
359
+ throw new Error(`Directory ${cloneDir} already exists but is not a valid git repository or git command failed`);
360
+ }
361
+ } catch (accessError) {
362
+ // Directory doesn't exist - proceed with clone
363
+ }
364
+
365
+ // Ensure parent directory exists
366
+ await fs.mkdir(path.dirname(cloneDir), { recursive: true });
367
+
368
+ // Prepare the git clone URL with authentication if token is provided
369
+ let cloneUrl = githubUrl;
370
+ if (githubToken) {
371
+ // Convert HTTPS URL to authenticated URL
372
+ // Example: https://github.com/user/repo -> https://token@github.com/user/repo
373
+ cloneUrl = githubUrl.replace('https://github.com', `https://${githubToken}@github.com`);
374
+ }
375
+
376
+ console.log('🔄 Cloning repository:', githubUrl);
377
+ console.log('📁 Destination:', cloneDir);
378
+
379
+ // Execute git clone
380
+ const gitProcess = spawn('git', ['clone', '--depth', '1', cloneUrl, cloneDir], {
381
+ stdio: ['pipe', 'pipe', 'pipe']
382
+ });
383
+
384
+ let stdout = '';
385
+ let stderr = '';
386
+
387
+ gitProcess.stdout.on('data', (data) => {
388
+ stdout += data.toString();
389
+ });
390
+
391
+ gitProcess.stderr.on('data', (data) => {
392
+ stderr += data.toString();
393
+ console.log('Git stderr:', data.toString());
394
+ });
395
+
396
+ gitProcess.on('close', (code) => {
397
+ if (code === 0) {
398
+ console.log('✅ Repository cloned successfully');
399
+ resolve(cloneDir);
400
+ } else {
401
+ console.error('❌ Git clone failed:', stderr);
402
+ reject(new Error(`Git clone failed: ${stderr}`));
403
+ }
404
+ });
405
+
406
+ gitProcess.on('error', (error) => {
407
+ reject(new Error(`Failed to execute git: ${error.message}`));
408
+ });
409
+ } catch (error) {
410
+ reject(error);
411
+ }
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Clean up a temporary project directory and its Claude session
417
+ * @param {string} projectPath - Path to the project directory
418
+ * @param {string} sessionId - Session ID to clean up
419
+ */
420
+ async function cleanupProject(projectPath, sessionId = null) {
421
+ try {
422
+ // Only clean up projects in the external-projects directory
423
+ if (!projectPath.includes('.claude/external-projects')) {
424
+ console.warn('⚠️ Refusing to clean up non-external project:', projectPath);
425
+ return;
426
+ }
427
+
428
+ console.log('🧹 Cleaning up project:', projectPath);
429
+ await fs.rm(projectPath, { recursive: true, force: true });
430
+ console.log('✅ Project cleaned up');
431
+
432
+ // Also clean up the Claude session directory if sessionId provided
433
+ if (sessionId) {
434
+ try {
435
+ const sessionPath = path.join(os.homedir(), '.claude', 'sessions', sessionId);
436
+ console.log('🧹 Cleaning up session directory:', sessionPath);
437
+ await fs.rm(sessionPath, { recursive: true, force: true });
438
+ console.log('✅ Session directory cleaned up');
439
+ } catch (error) {
440
+ // session cleanup error
441
+ }
442
+ }
443
+ } catch (error) {
444
+ console.error('❌ Failed to clean up project:', error);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * SSE Stream Writer - Adapts SDK/CLI output to Server-Sent Events
450
+ */
451
+ class SSEStreamWriter {
452
+ constructor(res) {
453
+ this.res = res;
454
+ this.sessionId = null;
455
+ this.isSSEStreamWriter = true; // Marker for transport detection
456
+ }
457
+
458
+ send(data) {
459
+ if (this.res.writableEnded) {
460
+ return;
461
+ }
462
+
463
+ // Format as SSE - providers send raw objects, we stringify
464
+ this.res.write(`data: ${JSON.stringify(data)}\n\n`);
465
+ }
466
+
467
+ end() {
468
+ if (!this.res.writableEnded) {
469
+ this.res.write('data: {"type":"done"}\n\n');
470
+ this.res.end();
471
+ }
472
+ }
473
+
474
+ setSessionId(sessionId) {
475
+ this.sessionId = sessionId;
476
+ }
477
+
478
+ getSessionId() {
479
+ return this.sessionId;
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Non-streaming response collector
485
+ */
486
+ class ResponseCollector {
487
+ constructor() {
488
+ this.messages = [];
489
+ this.sessionId = null;
490
+ }
491
+
492
+ send(data) {
493
+ // Store ALL messages for now - we'll filter when returning
494
+ this.messages.push(data);
495
+
496
+ // Extract sessionId if present
497
+ if (typeof data === 'string') {
498
+ try {
499
+ const parsed = JSON.parse(data);
500
+ if (parsed.sessionId) {
501
+ this.sessionId = parsed.sessionId;
502
+ }
503
+ } catch (e) {
504
+ // Not JSON, ignore
505
+ }
506
+ } else if (data && data.sessionId) {
507
+ this.sessionId = data.sessionId;
508
+ }
509
+ }
510
+
511
+ end() {
512
+ // Do nothing - we'll collect all messages
513
+ }
514
+
515
+ setSessionId(sessionId) {
516
+ this.sessionId = sessionId;
517
+ }
518
+
519
+ getSessionId() {
520
+ return this.sessionId;
521
+ }
522
+
523
+ getMessages() {
524
+ return this.messages;
525
+ }
526
+
527
+ /**
528
+ * Get filtered assistant messages only
529
+ */
530
+ getAssistantMessages() {
531
+ const assistantMessages = [];
532
+
533
+ for (const msg of this.messages) {
534
+ // Skip initial status message
535
+ if (msg && msg.type === 'status') {
536
+ continue;
537
+ }
538
+
539
+ // Handle JSON strings
540
+ if (typeof msg === 'string') {
541
+ try {
542
+ const parsed = JSON.parse(msg);
543
+ // Only include claude-response messages with assistant type
544
+ if (parsed.type === 'claude-response' && parsed.data && parsed.data.type === 'assistant') {
545
+ assistantMessages.push(parsed.data);
546
+ }
547
+ } catch (e) {
548
+ // Not JSON, skip
549
+ }
550
+ }
551
+ }
552
+
553
+ return assistantMessages;
554
+ }
555
+
556
+ /**
557
+ * Calculate total tokens from all messages
558
+ */
559
+ getTotalTokens() {
560
+ let totalInput = 0;
561
+ let totalOutput = 0;
562
+ let totalCacheRead = 0;
563
+ let totalCacheCreation = 0;
564
+
565
+ for (const msg of this.messages) {
566
+ let data = msg;
567
+
568
+ // Parse if string
569
+ if (typeof msg === 'string') {
570
+ try {
571
+ data = JSON.parse(msg);
572
+ } catch (e) {
573
+ continue;
574
+ }
575
+ }
576
+
577
+ // Extract usage from claude-response messages
578
+ if (data && data.type === 'claude-response' && data.data) {
579
+ const msgData = data.data;
580
+ if (msgData.message && msgData.message.usage) {
581
+ const usage = msgData.message.usage;
582
+ totalInput += usage.input_tokens || 0;
583
+ totalOutput += usage.output_tokens || 0;
584
+ totalCacheRead += usage.cache_read_input_tokens || 0;
585
+ totalCacheCreation += usage.cache_creation_input_tokens || 0;
586
+ }
587
+ }
588
+ }
589
+
590
+ return {
591
+ inputTokens: totalInput,
592
+ outputTokens: totalOutput,
593
+ cacheReadTokens: totalCacheRead,
594
+ cacheCreationTokens: totalCacheCreation,
595
+ totalTokens: totalInput + totalOutput + totalCacheRead + totalCacheCreation
596
+ };
597
+ }
598
+ }
599
+
600
+ // ===============================
601
+ // External API Endpoint
602
+ // ===============================
603
+
604
+ /**
605
+ * POST /api/agent
606
+ *
607
+ * Trigger an AI agent (Claude or Cursor) to work on a project.
608
+ * Supports automatic GitHub branch and pull request creation after successful completion.
609
+ *
610
+ * ================================================================================================
611
+ * REQUEST BODY PARAMETERS
612
+ * ================================================================================================
613
+ *
614
+ * @param {string} githubUrl - (Conditionally Required) GitHub repository URL to clone.
615
+ * Supported formats:
616
+ * - HTTPS: https://github.com/owner/repo
617
+ * - HTTPS with .git: https://github.com/owner/repo.git
618
+ * - SSH: git@github.com:owner/repo
619
+ * - SSH with .git: git@github.com:owner/repo.git
620
+ *
621
+ * @param {string} projectPath - (Conditionally Required) Path to existing project OR destination for cloning.
622
+ * Behavior depends on usage:
623
+ * - If used alone: Must point to existing project directory
624
+ * - If used with githubUrl: Target location for cloning
625
+ * - If omitted with githubUrl: Auto-generates temporary path in ~/.claude/external-projects/
626
+ *
627
+ * @param {string} message - (Required) Task description for the AI agent. Used as:
628
+ * - Instructions for the agent
629
+ * - Source for auto-generated branch names (if createBranch=true and no branchName)
630
+ * - Fallback for PR title if no commits are made
631
+ *
632
+ * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor'
633
+ * Default: 'claude'
634
+ *
635
+ * @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
636
+ * Default: true
637
+ * - true: Returns text/event-stream with incremental updates
638
+ * - false: Returns complete JSON response after completion
639
+ *
640
+ * @param {string} model - (Optional) Model identifier for providers.
641
+ *
642
+ * Claude models: 'sonnet' (default), 'opus', 'haiku', 'opusplan', 'sonnet[1m]'
643
+ * Cursor models: 'gpt-5' (default), 'gpt-5.2', 'gpt-5.2-high', 'sonnet-4.5', 'opus-4.5',
644
+ * 'gemini-3-pro', 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high',
645
+ * 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max',
646
+ * 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants
647
+ * Codex models: 'gpt-5.2' (default), 'gpt-5.1-codex-max', 'o3', 'o4-mini'
648
+ *
649
+ * @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
650
+ * Default: true
651
+ * Behavior:
652
+ * - Only applies when cloning via githubUrl (not for existing projectPath)
653
+ * - Deletes cloned repository after 5 seconds
654
+ * - Also deletes associated Claude session directory
655
+ * - Remote branch and PR remain on GitHub if created
656
+ *
657
+ * @param {string} githubToken - (Optional) GitHub Personal Access Token for authentication.
658
+ * Overrides stored token from user settings.
659
+ * Required for:
660
+ * - Private repositories
661
+ * - Branch/PR creation features
662
+ * Token must have 'repo' scope for full functionality.
663
+ *
664
+ * @param {string} branchName - (Optional) Custom name for the Git branch.
665
+ * If provided, createBranch is automatically set to true.
666
+ * Validation rules (errors returned if violated):
667
+ * - Cannot be empty or whitespace only
668
+ * - Cannot start or end with dot (.)
669
+ * - Cannot contain consecutive dots (..)
670
+ * - Cannot contain spaces
671
+ * - Cannot contain special characters: ~ ^ : ? * [ \
672
+ * - Cannot contain @{
673
+ * - Cannot start or end with forward slash (/)
674
+ * - Cannot contain consecutive slashes (//)
675
+ * - Cannot end with .lock
676
+ * - Cannot contain ASCII control characters
677
+ * Examples: 'feature/user-auth', 'bugfix/login-error', 'refactor/db-optimization'
678
+ *
679
+ * @param {boolean} createBranch - (Optional) Create a new Git branch after successful agent completion.
680
+ * Default: false (or true if branchName is provided)
681
+ * Behavior:
682
+ * - Creates branch locally and pushes to remote
683
+ * - If branch exists locally: Checks out existing branch (no error)
684
+ * - If branch exists on remote: Uses existing branch (no error)
685
+ * - Branch name: Custom (if branchName provided) or auto-generated from message
686
+ * - Requires either githubUrl OR projectPath with GitHub remote
687
+ *
688
+ * @param {boolean} createPR - (Optional) Create a GitHub Pull Request after successful completion.
689
+ * Default: false
690
+ * Behavior:
691
+ * - PR title: First commit message (or fallback to message parameter)
692
+ * - PR description: Auto-generated from all commit messages
693
+ * - Base branch: Always 'main' (currently hardcoded)
694
+ * - If PR already exists: GitHub returns error with details
695
+ * - Requires either githubUrl OR projectPath with GitHub remote
696
+ *
697
+ * ================================================================================================
698
+ * PATH HANDLING BEHAVIOR
699
+ * ================================================================================================
700
+ *
701
+ * Scenario 1: Only githubUrl provided
702
+ * Input: { githubUrl: "https://github.com/owner/repo" }
703
+ * Action: Clones to auto-generated temporary path: ~/.claude/external-projects/<hash>/
704
+ * Cleanup: Yes (if cleanup=true)
705
+ *
706
+ * Scenario 2: Only projectPath provided
707
+ * Input: { projectPath: "/home/user/my-project" }
708
+ * Action: Uses existing project at specified path
709
+ * Validation: Path must exist and be accessible
710
+ * Cleanup: No (never cleanup existing projects)
711
+ *
712
+ * Scenario 3: Both githubUrl and projectPath provided
713
+ * Input: { githubUrl: "https://github.com/owner/repo", projectPath: "/custom/path" }
714
+ * Action: Clones githubUrl to projectPath location
715
+ * Validation:
716
+ * - If projectPath exists with git repo:
717
+ * - Compares remote URL with githubUrl
718
+ * - If URLs match: Reuses existing repo
719
+ * - If URLs differ: Returns error
720
+ * Cleanup: Yes (if cleanup=true)
721
+ *
722
+ * ================================================================================================
723
+ * GITHUB BRANCH/PR CREATION REQUIREMENTS
724
+ * ================================================================================================
725
+ *
726
+ * For createBranch or createPR to work, one of the following must be true:
727
+ *
728
+ * Option A: githubUrl provided
729
+ * - Repository URL directly specified
730
+ * - Works with both cloning and existing paths
731
+ *
732
+ * Option B: projectPath with GitHub remote
733
+ * - Project must be a Git repository
734
+ * - Must have 'origin' remote configured
735
+ * - Remote URL must point to github.com
736
+ * - System auto-detects GitHub URL via: git remote get-url origin
737
+ *
738
+ * Additional Requirements:
739
+ * - Valid GitHub token (from settings or githubToken parameter)
740
+ * - Token must have 'repo' scope for private repos
741
+ * - Project must have commits (for PR creation)
742
+ *
743
+ * ================================================================================================
744
+ * VALIDATION & ERROR HANDLING
745
+ * ================================================================================================
746
+ *
747
+ * Input Validations (400 Bad Request):
748
+ * - Either githubUrl OR projectPath must be provided (not neither)
749
+ * - message must be non-empty string
750
+ * - provider must be 'claude' or 'cursor'
751
+ * - createBranch/createPR requires githubUrl OR projectPath (not neither)
752
+ * - branchName must pass Git naming rules (if provided)
753
+ *
754
+ * Runtime Validations (500 Internal Server Error or specific error in response):
755
+ * - projectPath must exist (if used alone)
756
+ * - GitHub URL format must be valid
757
+ * - Git remote URL must include github.com (for projectPath + branch/PR)
758
+ * - GitHub token must be available (for private repos and branch/PR)
759
+ * - Directory conflicts handled (existing path with different repo)
760
+ *
761
+ * Branch Name Validation Errors (returned in response, not HTTP error):
762
+ * Invalid names return: { branch: { error: "Invalid branch name: <reason>" } }
763
+ * Examples:
764
+ * - "my branch" → "Branch name cannot contain spaces"
765
+ * - ".feature" → "Branch name cannot start with a dot"
766
+ * - "feature.lock" → "Branch name cannot end with .lock"
767
+ *
768
+ * ================================================================================================
769
+ * RESPONSE FORMATS
770
+ * ================================================================================================
771
+ *
772
+ * Streaming Response (stream=true):
773
+ * Content-Type: text/event-stream
774
+ * Events:
775
+ * - { type: "status", message: "...", projectPath: "..." }
776
+ * - { type: "claude-response", data: {...} }
777
+ * - { type: "github-branch", branch: { name: "...", url: "..." } }
778
+ * - { type: "github-pr", pullRequest: { number: 42, url: "..." } }
779
+ * - { type: "github-error", error: "..." }
780
+ * - { type: "done" }
781
+ *
782
+ * Non-Streaming Response (stream=false):
783
+ * Content-Type: application/json
784
+ * {
785
+ * success: true,
786
+ * sessionId: "session-123",
787
+ * messages: [...], // Assistant messages only (filtered)
788
+ * tokens: {
789
+ * inputTokens: 150,
790
+ * outputTokens: 50,
791
+ * cacheReadTokens: 0,
792
+ * cacheCreationTokens: 0,
793
+ * totalTokens: 200
794
+ * },
795
+ * projectPath: "/path/to/project",
796
+ * branch: { // Only if createBranch=true
797
+ * name: "feature/xyz",
798
+ * url: "https://github.com/owner/repo/tree/feature/xyz"
799
+ * } | { error: "..." },
800
+ * pullRequest: { // Only if createPR=true
801
+ * number: 42,
802
+ * url: "https://github.com/owner/repo/pull/42"
803
+ * } | { error: "..." }
804
+ * }
805
+ *
806
+ * Error Response:
807
+ * HTTP Status: 400, 401, 500
808
+ * Content-Type: application/json
809
+ * { success: false, error: "Error description" }
810
+ *
811
+ * ================================================================================================
812
+ * EXAMPLES
813
+ * ================================================================================================
814
+ *
815
+ * Example 1: Clone and process with auto-cleanup
816
+ * POST /api/agent
817
+ * { "githubUrl": "https://github.com/user/repo", "message": "Fix bug" }
818
+ *
819
+ * Example 2: Use existing project with custom branch and PR
820
+ * POST /api/agent
821
+ * {
822
+ * "projectPath": "/home/user/project",
823
+ * "message": "Add feature",
824
+ * "branchName": "feature/new-feature",
825
+ * "createPR": true
826
+ * }
827
+ *
828
+ * Example 3: Clone to specific path with auto-generated branch
829
+ * POST /api/agent
830
+ * {
831
+ * "githubUrl": "https://github.com/user/repo",
832
+ * "projectPath": "/tmp/work",
833
+ * "message": "Refactor code",
834
+ * "createBranch": true,
835
+ * "cleanup": false
836
+ * }
837
+ */
838
+ router.post('/', validateExternalApiKey, async (req, res) => {
839
+ const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName } = req.body;
840
+
841
+ // Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
842
+ const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
843
+ const cleanup = req.body.cleanup === undefined ? true : (req.body.cleanup === true || req.body.cleanup === 'true');
844
+
845
+ // If branchName is provided, automatically enable createBranch
846
+ const createBranch = branchName ? true : (req.body.createBranch === true || req.body.createBranch === 'true');
847
+ const createPR = req.body.createPR === true || req.body.createPR === 'true';
848
+
849
+ // Validate inputs
850
+ if (!githubUrl && !projectPath) {
851
+ return res.status(400).json({ error: 'Either githubUrl or projectPath is required' });
852
+ }
853
+
854
+ if (!message || !message.trim()) {
855
+ return res.status(400).json({ error: 'message is required' });
856
+ }
857
+
858
+ if (!['claude', 'cursor', 'codex'].includes(provider)) {
859
+ return res.status(400).json({ error: 'provider must be "claude", "cursor", or "codex"' });
860
+ }
861
+
862
+ // Validate GitHub branch/PR creation requirements
863
+ // Allow branch/PR creation with projectPath as long as it has a GitHub remote
864
+ if ((createBranch || createPR) && !githubUrl && !projectPath) {
865
+ return res.status(400).json({ error: 'createBranch and createPR require either githubUrl or projectPath with a GitHub remote' });
866
+ }
867
+
868
+ let finalProjectPath = null;
869
+ let writer = null;
870
+
871
+ try {
872
+ // Determine the final project path
873
+ if (githubUrl) {
874
+ // Clone repository (to projectPath if provided, otherwise generate path)
875
+ const tokenToUse = githubToken || githubTokensDb.getActiveGithubToken(req.user.id);
876
+
877
+ let targetPath;
878
+ if (projectPath) {
879
+ targetPath = projectPath;
880
+ } else {
881
+ // Generate a unique path for cloning
882
+ const repoHash = crypto.createHash('md5').update(githubUrl + Date.now()).digest('hex');
883
+ targetPath = path.join(os.homedir(), '.claude', 'external-projects', repoHash);
884
+ }
885
+
886
+ finalProjectPath = await cloneGitHubRepo(githubUrl.trim(), tokenToUse, targetPath);
887
+ } else {
888
+ // Use existing project path
889
+ finalProjectPath = path.resolve(projectPath);
890
+
891
+ // Verify the path exists
892
+ try {
893
+ await fs.access(finalProjectPath);
894
+ } catch (error) {
895
+ throw new Error(`Project path does not exist: ${finalProjectPath}`);
896
+ }
897
+ }
898
+
899
+ // Register the project (or use existing registration)
900
+ let project;
901
+ try {
902
+ project = await addProjectManually(finalProjectPath);
903
+ console.log('📦 Project registered:', project);
904
+ } catch (error) {
905
+ // If project already exists, that's fine - continue with the existing registration
906
+ if (error.message && error.message.includes('Project already configured')) {
907
+ console.log('📦 Using existing project registration for:', finalProjectPath);
908
+ project = { path: finalProjectPath };
909
+ } else {
910
+ throw error;
911
+ }
912
+ }
913
+
914
+ // Set up writer based on streaming mode
915
+ if (stream) {
916
+ // Set up SSE headers for streaming
917
+ res.setHeader('Content-Type', 'text/event-stream');
918
+ res.setHeader('Cache-Control', 'no-cache');
919
+ res.setHeader('Connection', 'keep-alive');
920
+ res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
921
+
922
+ writer = new SSEStreamWriter(res);
923
+
924
+ // Send initial status
925
+ writer.send({
926
+ type: 'status',
927
+ message: githubUrl ? 'Repository cloned and session started' : 'Session started',
928
+ projectPath: finalProjectPath
929
+ });
930
+ } else {
931
+ // Non-streaming mode: collect messages
932
+ writer = new ResponseCollector();
933
+
934
+ // Collect initial status message
935
+ writer.send({
936
+ type: 'status',
937
+ message: githubUrl ? 'Repository cloned and session started' : 'Session started',
938
+ projectPath: finalProjectPath
939
+ });
940
+ }
941
+
942
+ // Start the appropriate session
943
+ if (provider === 'claude') {
944
+ console.log('🤖 Starting Claude SDK session');
945
+
946
+ await queryClaudeSDK(message.trim(), {
947
+ projectPath: finalProjectPath,
948
+ cwd: finalProjectPath,
949
+ sessionId: null, // New session
950
+ model: model,
951
+ permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
952
+ }, writer);
953
+
954
+ } else if (provider === 'cursor') {
955
+ console.log('🖱️ Starting Cursor CLI session');
956
+
957
+ await spawnCursor(message.trim(), {
958
+ projectPath: finalProjectPath,
959
+ cwd: finalProjectPath,
960
+ sessionId: null, // New session
961
+ model: model || undefined,
962
+ skipPermissions: true // Bypass permissions for Cursor
963
+ }, writer);
964
+ } else if (provider === 'codex') {
965
+ console.log('🤖 Starting Codex SDK session');
966
+
967
+ await queryCodex(message.trim(), {
968
+ projectPath: finalProjectPath,
969
+ cwd: finalProjectPath,
970
+ sessionId: null,
971
+ model: model || CODEX_MODELS.DEFAULT,
972
+ permissionMode: 'bypassPermissions'
973
+ }, writer);
974
+ }
975
+
976
+ // Handle GitHub branch and PR creation after successful agent completion
977
+ let branchInfo = null;
978
+ let prInfo = null;
979
+
980
+ if (createBranch || createPR) {
981
+ try {
982
+ console.log('🔄 Starting GitHub branch/PR creation workflow...');
983
+
984
+ // Get GitHub token
985
+ const tokenToUse = githubToken || githubTokensDb.getActiveGithubToken(req.user.id);
986
+
987
+ if (!tokenToUse) {
988
+ throw new Error('GitHub token required for branch/PR creation. Please configure a GitHub token in settings.');
989
+ }
990
+
991
+ // Initialize Octokit
992
+ const octokit = new Octokit({ auth: tokenToUse });
993
+
994
+ // Get GitHub URL - either from parameter or from git remote
995
+ let repoUrl = githubUrl;
996
+ if (!repoUrl) {
997
+ console.log('🔍 Getting GitHub URL from git remote...');
998
+ try {
999
+ repoUrl = await getGitRemoteUrl(finalProjectPath);
1000
+ if (!repoUrl.includes('github.com')) {
1001
+ throw new Error('Project does not have a GitHub remote configured');
1002
+ }
1003
+ console.log(`✅ Found GitHub remote: ${repoUrl}`);
1004
+ } catch (error) {
1005
+ throw new Error(`Failed to get GitHub remote URL: ${error.message}`);
1006
+ }
1007
+ }
1008
+
1009
+ // Parse GitHub URL to get owner and repo
1010
+ const { owner, repo } = parseGitHubUrl(repoUrl);
1011
+ console.log(`📦 Repository: ${owner}/${repo}`);
1012
+
1013
+ // Use provided branch name or auto-generate from message
1014
+ const finalBranchName = branchName || autogenerateBranchName(message);
1015
+ if (branchName) {
1016
+ console.log(`🌿 Using provided branch name: ${finalBranchName}`);
1017
+
1018
+ // Validate custom branch name
1019
+ const validation = validateBranchName(finalBranchName);
1020
+ if (!validation.valid) {
1021
+ throw new Error(`Invalid branch name: ${validation.error}`);
1022
+ }
1023
+ } else {
1024
+ console.log(`🌿 Auto-generated branch name: ${finalBranchName}`);
1025
+ }
1026
+
1027
+ if (createBranch) {
1028
+ // Create and checkout the new branch locally
1029
+ console.log('🔄 Creating local branch...');
1030
+ const checkoutProcess = spawn('git', ['checkout', '-b', finalBranchName], {
1031
+ cwd: finalProjectPath,
1032
+ stdio: 'pipe'
1033
+ });
1034
+
1035
+ await new Promise((resolve, reject) => {
1036
+ let stderr = '';
1037
+ checkoutProcess.stderr.on('data', (data) => { stderr += data.toString(); });
1038
+ checkoutProcess.on('close', (code) => {
1039
+ if (code === 0) {
1040
+ console.log(`✅ Created and checked out local branch '${finalBranchName}'`);
1041
+ resolve();
1042
+ } else {
1043
+ // Branch might already exist locally, try to checkout
1044
+ if (stderr.includes('already exists')) {
1045
+ console.log(`ℹ️ Branch '${finalBranchName}' already exists locally, checking out...`);
1046
+ const checkoutExisting = spawn('git', ['checkout', finalBranchName], {
1047
+ cwd: finalProjectPath,
1048
+ stdio: 'pipe'
1049
+ });
1050
+ checkoutExisting.on('close', (checkoutCode) => {
1051
+ if (checkoutCode === 0) {
1052
+ console.log(`✅ Checked out existing branch '${finalBranchName}'`);
1053
+ resolve();
1054
+ } else {
1055
+ reject(new Error(`Failed to checkout existing branch: ${stderr}`));
1056
+ }
1057
+ });
1058
+ } else {
1059
+ reject(new Error(`Failed to create branch: ${stderr}`));
1060
+ }
1061
+ }
1062
+ });
1063
+ });
1064
+
1065
+ // Push the branch to remote
1066
+ console.log('🔄 Pushing branch to remote...');
1067
+ const pushProcess = spawn('git', ['push', '-u', 'origin', finalBranchName], {
1068
+ cwd: finalProjectPath,
1069
+ stdio: 'pipe'
1070
+ });
1071
+
1072
+ await new Promise((resolve, reject) => {
1073
+ let stderr = '';
1074
+ let stdout = '';
1075
+ pushProcess.stdout.on('data', (data) => { stdout += data.toString(); });
1076
+ pushProcess.stderr.on('data', (data) => { stderr += data.toString(); });
1077
+ pushProcess.on('close', (code) => {
1078
+ if (code === 0) {
1079
+ console.log(`✅ Pushed branch '${finalBranchName}' to remote`);
1080
+ resolve();
1081
+ } else {
1082
+ // Check if branch exists on remote but has different commits
1083
+ if (stderr.includes('already exists') || stderr.includes('up-to-date')) {
1084
+ console.log(`ℹ️ Branch '${finalBranchName}' already exists on remote, using existing branch`);
1085
+ resolve();
1086
+ } else {
1087
+ reject(new Error(`Failed to push branch: ${stderr}`));
1088
+ }
1089
+ }
1090
+ });
1091
+ });
1092
+
1093
+ branchInfo = {
1094
+ name: finalBranchName,
1095
+ url: `https://github.com/${owner}/${repo}/tree/${finalBranchName}`
1096
+ };
1097
+ }
1098
+
1099
+ if (createPR) {
1100
+ // Get commit messages to generate PR description
1101
+ console.log('🔄 Generating PR title and description...');
1102
+ const commitMessages = await getCommitMessages(finalProjectPath, 5);
1103
+
1104
+ // Use the first commit message as the PR title, or fallback to the agent message
1105
+ const prTitle = commitMessages.length > 0 ? commitMessages[0] : message;
1106
+
1107
+ // Generate PR body from commit messages
1108
+ let prBody = '## Changes\n\n';
1109
+ if (commitMessages.length > 0) {
1110
+ prBody += commitMessages.map(msg => `- ${msg}`).join('\n');
1111
+ } else {
1112
+ prBody += `Agent task: ${message}`;
1113
+ }
1114
+ prBody += '\n\n---\n*This pull request was automatically created by Upfyn-Code Agent.*';
1115
+
1116
+ console.log(`📝 PR Title: ${prTitle}`);
1117
+
1118
+ // Create the pull request
1119
+ console.log('🔄 Creating pull request...');
1120
+ prInfo = await createGitHubPR(octokit, owner, repo, finalBranchName, prTitle, prBody, 'main');
1121
+ }
1122
+
1123
+ // Send branch/PR info in response
1124
+ if (stream) {
1125
+ if (branchInfo) {
1126
+ writer.send({
1127
+ type: 'github-branch',
1128
+ branch: branchInfo
1129
+ });
1130
+ }
1131
+ if (prInfo) {
1132
+ writer.send({
1133
+ type: 'github-pr',
1134
+ pullRequest: prInfo
1135
+ });
1136
+ }
1137
+ }
1138
+
1139
+ } catch (error) {
1140
+ console.error('❌ GitHub branch/PR creation error:', error);
1141
+
1142
+ // Send error but don't fail the entire request
1143
+ if (stream) {
1144
+ writer.send({
1145
+ type: 'github-error',
1146
+ error: error.message
1147
+ });
1148
+ }
1149
+ // Store error info for non-streaming response
1150
+ if (!stream) {
1151
+ branchInfo = { error: error.message };
1152
+ prInfo = { error: error.message };
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // Handle response based on streaming mode
1158
+ if (stream) {
1159
+ // Streaming mode: end the SSE stream
1160
+ writer.end();
1161
+ } else {
1162
+ // Non-streaming mode: send filtered messages and token summary as JSON
1163
+ const assistantMessages = writer.getAssistantMessages();
1164
+ const tokenSummary = writer.getTotalTokens();
1165
+
1166
+ const response = {
1167
+ success: true,
1168
+ sessionId: writer.getSessionId(),
1169
+ messages: assistantMessages,
1170
+ tokens: tokenSummary,
1171
+ projectPath: finalProjectPath
1172
+ };
1173
+
1174
+ // Add branch/PR info if created
1175
+ if (branchInfo) {
1176
+ response.branch = branchInfo;
1177
+ }
1178
+ if (prInfo) {
1179
+ response.pullRequest = prInfo;
1180
+ }
1181
+
1182
+ res.json(response);
1183
+ }
1184
+
1185
+ // Clean up if requested
1186
+ if (cleanup && githubUrl) {
1187
+ // Only cleanup if we cloned a repo (not for existing project paths)
1188
+ const sessionIdForCleanup = writer.getSessionId();
1189
+ setTimeout(() => {
1190
+ cleanupProject(finalProjectPath, sessionIdForCleanup);
1191
+ }, 5000);
1192
+ }
1193
+
1194
+ } catch (error) {
1195
+ console.error('❌ External session error:', error);
1196
+
1197
+ // Clean up on error
1198
+ if (finalProjectPath && cleanup && githubUrl) {
1199
+ const sessionIdForCleanup = writer ? writer.getSessionId() : null;
1200
+ cleanupProject(finalProjectPath, sessionIdForCleanup);
1201
+ }
1202
+
1203
+ if (stream) {
1204
+ // For streaming, send error event and stop
1205
+ if (!writer) {
1206
+ // Set up SSE headers if not already done
1207
+ res.setHeader('Content-Type', 'text/event-stream');
1208
+ res.setHeader('Cache-Control', 'no-cache');
1209
+ res.setHeader('Connection', 'keep-alive');
1210
+ res.setHeader('X-Accel-Buffering', 'no');
1211
+ writer = new SSEStreamWriter(res);
1212
+ }
1213
+
1214
+ if (!res.writableEnded) {
1215
+ writer.send({
1216
+ type: 'error',
1217
+ error: error.message,
1218
+ message: `Failed: ${error.message}`
1219
+ });
1220
+ writer.end();
1221
+ }
1222
+ } else if (!res.headersSent) {
1223
+ res.status(500).json({
1224
+ success: false,
1225
+ error: error.message
1226
+ });
1227
+ }
1228
+ }
1229
+ });
1230
+
1231
+ export default router;