@glwhappen/web-code 1.32.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 (479) hide show
  1. package/LICENSE +718 -0
  2. package/README.de.md +250 -0
  3. package/README.ja.md +242 -0
  4. package/README.ko.md +242 -0
  5. package/README.md +252 -0
  6. package/README.ru.md +250 -0
  7. package/README.tr.md +252 -0
  8. package/README.zh-CN.md +242 -0
  9. package/dist/api-docs.html +879 -0
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/index-Ct6oPUQk.css +32 -0
  70. package/dist/assets/index-u6XmIqLb.js +1346 -0
  71. package/dist/assets/vendor-codemirror-OwyKSvPE.js +41 -0
  72. package/dist/assets/vendor-react-BGZc9oRE.js +59 -0
  73. package/dist/assets/vendor-xterm-CJZjLICi.js +66 -0
  74. package/dist/clear-cache.html +85 -0
  75. package/dist/convert-icons.md +53 -0
  76. package/dist/favicon.png +0 -0
  77. package/dist/favicon.svg +9 -0
  78. package/dist/generate-icons.js +49 -0
  79. package/dist/icons/claude-ai-icon.svg +1 -0
  80. package/dist/icons/codex-white.svg +3 -0
  81. package/dist/icons/codex.svg +3 -0
  82. package/dist/icons/cursor-white.svg +12 -0
  83. package/dist/icons/cursor.svg +1 -0
  84. package/dist/icons/gemini-ai-icon.svg +1 -0
  85. package/dist/icons/icon-128x128.png +0 -0
  86. package/dist/icons/icon-128x128.svg +12 -0
  87. package/dist/icons/icon-144x144.png +0 -0
  88. package/dist/icons/icon-144x144.svg +12 -0
  89. package/dist/icons/icon-152x152.png +0 -0
  90. package/dist/icons/icon-152x152.svg +12 -0
  91. package/dist/icons/icon-192x192.png +0 -0
  92. package/dist/icons/icon-192x192.svg +12 -0
  93. package/dist/icons/icon-384x384.png +0 -0
  94. package/dist/icons/icon-384x384.svg +12 -0
  95. package/dist/icons/icon-512x512.png +0 -0
  96. package/dist/icons/icon-512x512.svg +12 -0
  97. package/dist/icons/icon-72x72.png +0 -0
  98. package/dist/icons/icon-72x72.svg +12 -0
  99. package/dist/icons/icon-96x96.png +0 -0
  100. package/dist/icons/icon-96x96.svg +12 -0
  101. package/dist/icons/icon-template.svg +12 -0
  102. package/dist/index.html +52 -0
  103. package/dist/logo-128.png +0 -0
  104. package/dist/logo-256.png +0 -0
  105. package/dist/logo-32.png +0 -0
  106. package/dist/logo-512.png +0 -0
  107. package/dist/logo-64.png +0 -0
  108. package/dist/logo.svg +17 -0
  109. package/dist/manifest.json +61 -0
  110. package/dist/screenshots/cli-selection.png +0 -0
  111. package/dist/screenshots/desktop-main.png +0 -0
  112. package/dist/screenshots/mobile-chat.png +0 -0
  113. package/dist/screenshots/tools-modal.png +0 -0
  114. package/dist/sw.js +124 -0
  115. package/dist-server/server/claude-sdk.js +738 -0
  116. package/dist-server/server/claude-sdk.js.map +1 -0
  117. package/dist-server/server/cli.js +641 -0
  118. package/dist-server/server/cli.js.map +1 -0
  119. package/dist-server/server/constants/config.js +6 -0
  120. package/dist-server/server/constants/config.js.map +1 -0
  121. package/dist-server/server/cursor-cli.js +271 -0
  122. package/dist-server/server/cursor-cli.js.map +1 -0
  123. package/dist-server/server/gemini-cli.js +539 -0
  124. package/dist-server/server/gemini-cli.js.map +1 -0
  125. package/dist-server/server/gemini-response-handler.js +72 -0
  126. package/dist-server/server/gemini-response-handler.js.map +1 -0
  127. package/dist-server/server/index.js +1340 -0
  128. package/dist-server/server/index.js.map +1 -0
  129. package/dist-server/server/load-env.js +32 -0
  130. package/dist-server/server/load-env.js.map +1 -0
  131. package/dist-server/server/middleware/auth.js +117 -0
  132. package/dist-server/server/middleware/auth.js.map +1 -0
  133. package/dist-server/server/modules/database/connection.js +125 -0
  134. package/dist-server/server/modules/database/connection.js.map +1 -0
  135. package/dist-server/server/modules/database/index.js +13 -0
  136. package/dist-server/server/modules/database/index.js.map +1 -0
  137. package/dist-server/server/modules/database/init-db.js +18 -0
  138. package/dist-server/server/modules/database/init-db.js.map +1 -0
  139. package/dist-server/server/modules/database/migrations.js +419 -0
  140. package/dist-server/server/modules/database/migrations.js.map +1 -0
  141. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  142. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  143. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  144. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  145. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  146. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  147. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  148. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  149. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  150. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  151. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +67 -0
  152. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  153. package/dist-server/server/modules/database/repositories/projects.db.js +185 -0
  154. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  155. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  156. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  157. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  158. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  159. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +64 -0
  160. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
  161. package/dist-server/server/modules/database/repositories/sessions.db.js +150 -0
  162. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  163. package/dist-server/server/modules/database/repositories/users.js +116 -0
  164. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  165. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  166. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  167. package/dist-server/server/modules/database/schema.js +150 -0
  168. package/dist-server/server/modules/database/schema.js.map +1 -0
  169. package/dist-server/server/modules/projects/index.js +4 -0
  170. package/dist-server/server/modules/projects/index.js.map +1 -0
  171. package/dist-server/server/modules/projects/projects.routes.js +225 -0
  172. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  173. package/dist-server/server/modules/projects/services/project-clone.service.js +220 -0
  174. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  175. package/dist-server/server/modules/projects/services/project-delete.service.js +83 -0
  176. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  177. package/dist-server/server/modules/projects/services/project-management.service.js +99 -0
  178. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  179. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  180. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  181. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +171 -0
  182. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  183. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +213 -0
  184. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  185. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +129 -0
  186. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  187. package/dist-server/server/modules/projects/tests/project-management.service.test.js +89 -0
  188. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  189. package/dist-server/server/modules/projects/tests/project-star.service.test.js +99 -0
  190. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  191. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +88 -0
  192. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  193. package/dist-server/server/modules/providers/index.js +5 -0
  194. package/dist-server/server/modules/providers/index.js.map +1 -0
  195. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +104 -0
  196. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -0
  197. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js +103 -0
  198. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js.map +1 -0
  199. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +116 -0
  200. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  201. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +546 -0
  202. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -0
  203. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
  204. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
  205. package/dist-server/server/modules/providers/list/claude/claude.provider.js +17 -0
  206. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -0
  207. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +84 -0
  208. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -0
  209. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js +107 -0
  210. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js.map +1 -0
  211. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +123 -0
  212. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  213. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +513 -0
  214. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -0
  215. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +82 -0
  216. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
  217. package/dist-server/server/modules/providers/list/codex/codex.provider.js +17 -0
  218. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -0
  219. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +118 -0
  220. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -0
  221. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js +80 -0
  222. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js.map +1 -0
  223. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +105 -0
  224. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  225. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +545 -0
  226. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -0
  227. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
  228. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
  229. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +17 -0
  230. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -0
  231. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +254 -0
  232. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -0
  233. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +82 -0
  234. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +1 -0
  235. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +312 -0
  236. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  237. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +484 -0
  238. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -0
  239. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
  240. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
  241. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +17 -0
  242. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -0
  243. package/dist-server/server/modules/providers/provider.registry.js +31 -0
  244. package/dist-server/server/modules/providers/provider.registry.js.map +1 -0
  245. package/dist-server/server/modules/providers/provider.routes.js +377 -0
  246. package/dist-server/server/modules/providers/provider.routes.js.map +1 -0
  247. package/dist-server/server/modules/providers/services/mcp.service.js +69 -0
  248. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -0
  249. package/dist-server/server/modules/providers/services/provider-auth.service.js +25 -0
  250. package/dist-server/server/modules/providers/services/provider-auth.service.js.map +1 -0
  251. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +984 -0
  252. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  253. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  254. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  255. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +269 -0
  256. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  257. package/dist-server/server/modules/providers/services/sessions.service.js +179 -0
  258. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -0
  259. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  260. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
  261. package/dist-server/server/modules/providers/shared/base/abstract.provider.js +14 -0
  262. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -0
  263. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js +102 -0
  264. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js.map +1 -0
  265. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
  266. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
  267. package/dist-server/server/modules/providers/tests/mcp.test.js +250 -0
  268. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -0
  269. package/dist-server/server/modules/providers/tests/skills.test.js +226 -0
  270. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
  271. package/dist-server/server/modules/websocket/index.js +3 -0
  272. package/dist-server/server/modules/websocket/index.js.map +1 -0
  273. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +192 -0
  274. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  275. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  276. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  277. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +360 -0
  278. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  279. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  280. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  281. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  282. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  283. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  284. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  285. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  286. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  287. package/dist-server/server/openai-codex.js +418 -0
  288. package/dist-server/server/openai-codex.js.map +1 -0
  289. package/dist-server/server/routes/admin.js +109 -0
  290. package/dist-server/server/routes/admin.js.map +1 -0
  291. package/dist-server/server/routes/agent.js +1145 -0
  292. package/dist-server/server/routes/agent.js.map +1 -0
  293. package/dist-server/server/routes/auth.js +123 -0
  294. package/dist-server/server/routes/auth.js.map +1 -0
  295. package/dist-server/server/routes/commands.js +487 -0
  296. package/dist-server/server/routes/commands.js.map +1 -0
  297. package/dist-server/server/routes/cursor.js +49 -0
  298. package/dist-server/server/routes/cursor.js.map +1 -0
  299. package/dist-server/server/routes/gemini.js +25 -0
  300. package/dist-server/server/routes/gemini.js.map +1 -0
  301. package/dist-server/server/routes/git.js +1263 -0
  302. package/dist-server/server/routes/git.js.map +1 -0
  303. package/dist-server/server/routes/mcp-utils.js +29 -0
  304. package/dist-server/server/routes/mcp-utils.js.map +1 -0
  305. package/dist-server/server/routes/plugins.js +266 -0
  306. package/dist-server/server/routes/plugins.js.map +1 -0
  307. package/dist-server/server/routes/settings.js +259 -0
  308. package/dist-server/server/routes/settings.js.map +1 -0
  309. package/dist-server/server/routes/taskmaster.js +1360 -0
  310. package/dist-server/server/routes/taskmaster.js.map +1 -0
  311. package/dist-server/server/routes/user.js +115 -0
  312. package/dist-server/server/routes/user.js.map +1 -0
  313. package/dist-server/server/services/notification-orchestrator.js +177 -0
  314. package/dist-server/server/services/notification-orchestrator.js.map +1 -0
  315. package/dist-server/server/services/vapid-keys.js +27 -0
  316. package/dist-server/server/services/vapid-keys.js.map +1 -0
  317. package/dist-server/server/sessionManager.js +215 -0
  318. package/dist-server/server/sessionManager.js.map +1 -0
  319. package/dist-server/server/shared/claude-cli-path.js +103 -0
  320. package/dist-server/server/shared/claude-cli-path.js.map +1 -0
  321. package/dist-server/server/shared/claude-cli-path.test.js +45 -0
  322. package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
  323. package/dist-server/server/shared/default-user.js +29 -0
  324. package/dist-server/server/shared/default-user.js.map +1 -0
  325. package/dist-server/server/shared/frontmatter.js +16 -0
  326. package/dist-server/server/shared/frontmatter.js.map +1 -0
  327. package/dist-server/server/shared/interfaces.js +2 -0
  328. package/dist-server/server/shared/interfaces.js.map +1 -0
  329. package/dist-server/server/shared/types.js +2 -0
  330. package/dist-server/server/shared/types.js.map +1 -0
  331. package/dist-server/server/shared/utils.js +633 -0
  332. package/dist-server/server/shared/utils.js.map +1 -0
  333. package/dist-server/server/utils/colors.js +20 -0
  334. package/dist-server/server/utils/colors.js.map +1 -0
  335. package/dist-server/server/utils/commandParser.js +255 -0
  336. package/dist-server/server/utils/commandParser.js.map +1 -0
  337. package/dist-server/server/utils/gitConfig.js +36 -0
  338. package/dist-server/server/utils/gitConfig.js.map +1 -0
  339. package/dist-server/server/utils/mcp-detector.js +134 -0
  340. package/dist-server/server/utils/mcp-detector.js.map +1 -0
  341. package/dist-server/server/utils/plugin-loader.js +413 -0
  342. package/dist-server/server/utils/plugin-loader.js.map +1 -0
  343. package/dist-server/server/utils/plugin-process-manager.js +163 -0
  344. package/dist-server/server/utils/plugin-process-manager.js.map +1 -0
  345. package/dist-server/server/utils/runtime-paths.js +30 -0
  346. package/dist-server/server/utils/runtime-paths.js.map +1 -0
  347. package/dist-server/server/utils/taskmaster-websocket.js +124 -0
  348. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -0
  349. package/dist-server/server/utils/url-detection.js +58 -0
  350. package/dist-server/server/utils/url-detection.js.map +1 -0
  351. package/dist-server/shared/modelConstants.js +99 -0
  352. package/dist-server/shared/modelConstants.js.map +1 -0
  353. package/dist-server/shared/networkHosts.js +20 -0
  354. package/dist-server/shared/networkHosts.js.map +1 -0
  355. package/package.json +169 -0
  356. package/scripts/fix-node-pty.js +67 -0
  357. package/server/claude-sdk.js +864 -0
  358. package/server/cli.js +688 -0
  359. package/server/constants/config.js +5 -0
  360. package/server/cursor-cli.js +334 -0
  361. package/server/gemini-cli.js +622 -0
  362. package/server/gemini-response-handler.js +79 -0
  363. package/server/index.js +1505 -0
  364. package/server/load-env.js +34 -0
  365. package/server/middleware/auth.js +142 -0
  366. package/server/modules/database/connection.ts +143 -0
  367. package/server/modules/database/index.ts +12 -0
  368. package/server/modules/database/init-db.ts +17 -0
  369. package/server/modules/database/migrations.ts +496 -0
  370. package/server/modules/database/repositories/api-keys.ts +119 -0
  371. package/server/modules/database/repositories/app-config.ts +53 -0
  372. package/server/modules/database/repositories/credentials.ts +106 -0
  373. package/server/modules/database/repositories/github-tokens.ts +100 -0
  374. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  375. package/server/modules/database/repositories/projects.db.integration.test.ts +78 -0
  376. package/server/modules/database/repositories/projects.db.ts +210 -0
  377. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  378. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  379. package/server/modules/database/repositories/sessions.db.integration.test.ts +78 -0
  380. package/server/modules/database/repositories/sessions.db.ts +230 -0
  381. package/server/modules/database/repositories/users.ts +186 -0
  382. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  383. package/server/modules/database/schema.ts +159 -0
  384. package/server/modules/projects/index.ts +6 -0
  385. package/server/modules/projects/projects.routes.ts +292 -0
  386. package/server/modules/projects/services/project-clone.service.ts +327 -0
  387. package/server/modules/projects/services/project-delete.service.ts +95 -0
  388. package/server/modules/projects/services/project-management.service.ts +158 -0
  389. package/server/modules/projects/services/project-star.service.ts +78 -0
  390. package/server/modules/projects/services/projects-has-taskmaster.service.ts +257 -0
  391. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +355 -0
  392. package/server/modules/projects/tests/project-clone.service.test.ts +186 -0
  393. package/server/modules/projects/tests/project-management.service.test.ts +122 -0
  394. package/server/modules/projects/tests/project-star.service.test.ts +128 -0
  395. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +107 -0
  396. package/server/modules/providers/README.md +346 -0
  397. package/server/modules/providers/index.ts +5 -0
  398. package/server/modules/providers/list/claude/claude-auth.provider.ts +124 -0
  399. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -0
  400. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +179 -0
  401. package/server/modules/providers/list/claude/claude-sessions.provider.ts +642 -0
  402. package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
  403. package/server/modules/providers/list/claude/claude.provider.ts +24 -0
  404. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -0
  405. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -0
  406. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +182 -0
  407. package/server/modules/providers/list/codex/codex-sessions.provider.ts +589 -0
  408. package/server/modules/providers/list/codex/codex-skills.provider.ts +100 -0
  409. package/server/modules/providers/list/codex/codex.provider.ts +24 -0
  410. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -0
  411. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -0
  412. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +155 -0
  413. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +624 -0
  414. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
  415. package/server/modules/providers/list/cursor/cursor.provider.ts +24 -0
  416. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +307 -0
  417. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -0
  418. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +407 -0
  419. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +552 -0
  420. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
  421. package/server/modules/providers/list/gemini/gemini.provider.ts +24 -0
  422. package/server/modules/providers/provider.registry.ts +36 -0
  423. package/server/modules/providers/provider.routes.ts +488 -0
  424. package/server/modules/providers/services/mcp.service.ts +94 -0
  425. package/server/modules/providers/services/provider-auth.service.ts +26 -0
  426. package/server/modules/providers/services/session-conversations-search.service.ts +1319 -0
  427. package/server/modules/providers/services/session-synchronizer.service.ts +75 -0
  428. package/server/modules/providers/services/sessions-watcher.service.ts +318 -0
  429. package/server/modules/providers/services/sessions.service.ts +240 -0
  430. package/server/modules/providers/services/skills.service.ts +15 -0
  431. package/server/modules/providers/shared/base/abstract.provider.ts +29 -0
  432. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -0
  433. package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
  434. package/server/modules/providers/tests/mcp.test.ts +293 -0
  435. package/server/modules/providers/tests/skills.test.ts +446 -0
  436. package/server/modules/websocket/README.md +267 -0
  437. package/server/modules/websocket/index.ts +2 -0
  438. package/server/modules/websocket/services/chat-websocket.service.ts +275 -0
  439. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  440. package/server/modules/websocket/services/shell-websocket.service.ts +489 -0
  441. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  442. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  443. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  444. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  445. package/server/openai-codex.js +474 -0
  446. package/server/routes/admin.js +128 -0
  447. package/server/routes/agent.js +1246 -0
  448. package/server/routes/auth.js +144 -0
  449. package/server/routes/commands.js +556 -0
  450. package/server/routes/cursor.js +52 -0
  451. package/server/routes/gemini.js +30 -0
  452. package/server/routes/git.js +1493 -0
  453. package/server/routes/mcp-utils.js +31 -0
  454. package/server/routes/plugins.js +307 -0
  455. package/server/routes/settings.js +286 -0
  456. package/server/routes/taskmaster.js +1468 -0
  457. package/server/routes/user.js +123 -0
  458. package/server/services/notification-orchestrator.js +228 -0
  459. package/server/services/vapid-keys.js +36 -0
  460. package/server/sessionManager.js +248 -0
  461. package/server/shared/claude-cli-path.test.ts +61 -0
  462. package/server/shared/claude-cli-path.ts +139 -0
  463. package/server/shared/default-user.ts +30 -0
  464. package/server/shared/frontmatter.ts +18 -0
  465. package/server/shared/interfaces.ts +111 -0
  466. package/server/shared/types.ts +406 -0
  467. package/server/shared/utils.ts +763 -0
  468. package/server/tsconfig.json +36 -0
  469. package/server/utils/colors.js +21 -0
  470. package/server/utils/commandParser.js +305 -0
  471. package/server/utils/gitConfig.js +34 -0
  472. package/server/utils/mcp-detector.js +147 -0
  473. package/server/utils/plugin-loader.js +457 -0
  474. package/server/utils/plugin-process-manager.js +184 -0
  475. package/server/utils/runtime-paths.js +37 -0
  476. package/server/utils/taskmaster-websocket.js +135 -0
  477. package/server/utils/url-detection.js +71 -0
  478. package/shared/modelConstants.js +107 -0
  479. package/shared/networkHosts.js +22 -0
@@ -0,0 +1,1505 @@
1
+ #!/usr/bin/env node
2
+ // Load environment variables before other imports execute
3
+ import './load-env.js';
4
+ import fs, { promises as fsPromises } from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import http from 'http';
8
+ import { spawn } from 'child_process';
9
+
10
+ import express from 'express';
11
+ import cors from 'cors';
12
+ import mime from 'mime-types';
13
+
14
+ import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
15
+ import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
16
+ import { createWebSocketServer } from '@/modules/websocket/index.js';
17
+
18
+ import { getConnectableHost } from '../shared/networkHosts.js';
19
+
20
+ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
21
+ import {
22
+ queryClaudeSDK,
23
+ abortClaudeSDKSession,
24
+ isClaudeSDKSessionActive,
25
+ getActiveClaudeSDKSessions,
26
+ resolveToolApproval,
27
+ getPendingApprovalsForSession,
28
+ reconnectSessionWriter,
29
+ } from './claude-sdk.js';
30
+ import {
31
+ spawnCursor,
32
+ abortCursorSession,
33
+ isCursorSessionActive,
34
+ getActiveCursorSessions,
35
+ } from './cursor-cli.js';
36
+ import {
37
+ queryCodex,
38
+ abortCodexSession,
39
+ isCodexSessionActive,
40
+ getActiveCodexSessions,
41
+ } from './openai-codex.js';
42
+ import {
43
+ spawnGemini,
44
+ abortGeminiSession,
45
+ isGeminiSessionActive,
46
+ getActiveGeminiSessions,
47
+ } from './gemini-cli.js';
48
+ import sessionManager from './sessionManager.js';
49
+ import {
50
+ stripAnsiSequences,
51
+ normalizeDetectedUrl,
52
+ extractUrlsFromText,
53
+ shouldAutoOpenUrlFromOutput,
54
+ } from './utils/url-detection.js';
55
+ import gitRoutes from './routes/git.js';
56
+ import authRoutes from './routes/auth.js';
57
+ import cursorRoutes from './routes/cursor.js';
58
+ import taskmasterRoutes from './routes/taskmaster.js';
59
+ import mcpUtilsRoutes from './routes/mcp-utils.js';
60
+ import commandsRoutes from './routes/commands.js';
61
+ import settingsRoutes from './routes/settings.js';
62
+ import agentRoutes from './routes/agent.js';
63
+ import adminRoutes from './routes/admin.js';
64
+ import projectModuleRoutes from './modules/projects/projects.routes.js';
65
+ import userRoutes from './routes/user.js';
66
+ import geminiRoutes from './routes/gemini.js';
67
+ import pluginsRoutes from './routes/plugins.js';
68
+ import providerRoutes from './modules/providers/provider.routes.js';
69
+ import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
70
+ import { initializeDatabase, projectsDb } from './modules/database/index.js';
71
+ import { configureWebPush } from './services/vapid-keys.js';
72
+ import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
73
+ import { IS_PLATFORM } from './constants/config.js';
74
+ import { c } from './utils/colors.js';
75
+
76
+ const __dirname = getModuleDir(import.meta.url);
77
+ // The server source runs from /server, while the compiled output runs from /dist-server/server.
78
+ // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
79
+ const APP_ROOT = findAppRoot(__dirname);
80
+ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
81
+
82
+ console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
83
+
84
+ const app = express();
85
+ const server = http.createServer(app);
86
+
87
+ // Single WebSocket server that handles chat, shell, and plugin proxy paths.
88
+ const wss = createWebSocketServer(server, {
89
+ verifyClient: {
90
+ isPlatform: IS_PLATFORM,
91
+ authenticateWebSocket,
92
+ },
93
+ chat: {
94
+ queryClaudeSDK,
95
+ spawnCursor,
96
+ queryCodex,
97
+ spawnGemini,
98
+ abortClaudeSDKSession,
99
+ abortCursorSession,
100
+ abortCodexSession,
101
+ abortGeminiSession,
102
+ resolveToolApproval,
103
+ isClaudeSDKSessionActive,
104
+ isCursorSessionActive,
105
+ isCodexSessionActive,
106
+ isGeminiSessionActive,
107
+ reconnectSessionWriter,
108
+ getPendingApprovalsForSession,
109
+ getActiveClaudeSDKSessions,
110
+ getActiveCursorSessions,
111
+ getActiveCodexSessions,
112
+ getActiveGeminiSessions,
113
+ },
114
+ shell: {
115
+ getSessionById: (userId, sessionId) => sessionManager.getSession(userId, sessionId),
116
+ stripAnsiSequences,
117
+ normalizeDetectedUrl,
118
+ extractUrlsFromText,
119
+ shouldAutoOpenUrlFromOutput,
120
+ },
121
+ getPluginPort,
122
+ });
123
+
124
+ // Make WebSocket server available to routes
125
+ app.locals.wss = wss;
126
+
127
+ app.use(cors({ exposedHeaders: ['X-Refreshed-Token'] }));
128
+ app.use(express.json({
129
+ limit: '50mb',
130
+ type: (req) => {
131
+ // Skip multipart/form-data requests (for file uploads like images)
132
+ const contentType = req.headers['content-type'] || '';
133
+ if (contentType.includes('multipart/form-data')) {
134
+ return false;
135
+ }
136
+ return contentType.includes('json');
137
+ }
138
+ }));
139
+ app.use(express.urlencoded({ limit: '50mb', extended: true }));
140
+
141
+ // Public health check endpoint (no authentication required)
142
+ app.get('/health', (req, res) => {
143
+ res.json({
144
+ status: 'ok',
145
+ timestamp: new Date().toISOString(),
146
+ installMode
147
+ });
148
+ });
149
+
150
+ // Optional API key validation (if configured)
151
+ app.use('/api', validateApiKey);
152
+
153
+ // Authentication routes (public)
154
+ app.use('/api/auth', authRoutes);
155
+
156
+ // Projects API Routes (protected)
157
+ app.use('/api/projects', authenticateToken, projectModuleRoutes);
158
+
159
+ // Git API Routes (protected)
160
+ app.use('/api/git', authenticateToken, gitRoutes);
161
+
162
+ // Cursor API Routes (protected)
163
+ app.use('/api/cursor', authenticateToken, cursorRoutes);
164
+
165
+ // TaskMaster API Routes (protected)
166
+ app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
167
+
168
+ // MCP utilities
169
+ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
170
+
171
+ // Commands API Routes (protected)
172
+ app.use('/api/commands', authenticateToken, commandsRoutes);
173
+
174
+ // Settings API Routes (protected)
175
+ app.use('/api/settings', authenticateToken, settingsRoutes);
176
+
177
+ // User API Routes (protected)
178
+ app.use('/api/user', authenticateToken, userRoutes);
179
+
180
+ // Admin API Routes (protected)
181
+ app.use('/api/admin', adminRoutes);
182
+
183
+ // Gemini API Routes (protected)
184
+ app.use('/api/gemini', authenticateToken, geminiRoutes);
185
+
186
+ // Plugins API Routes (protected)
187
+ app.use('/api/plugins', authenticateToken, pluginsRoutes);
188
+
189
+ // Unified provider MCP routes (protected)
190
+ app.use('/api/providers', authenticateToken, providerRoutes);
191
+
192
+ // Agent API Routes (uses API key authentication)
193
+ app.use('/api/agent', agentRoutes);
194
+
195
+ // Serve public files (like api-docs.html)
196
+ app.use(express.static(path.join(APP_ROOT, 'public')));
197
+
198
+ // Static files served after API routes
199
+ // Add cache control: HTML files should not be cached, but assets can be cached
200
+ app.use(express.static(path.join(APP_ROOT, 'dist'), {
201
+ setHeaders: (res, filePath) => {
202
+ if (filePath.endsWith('.html')) {
203
+ // Prevent HTML caching to avoid service worker issues after builds
204
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
205
+ res.setHeader('Pragma', 'no-cache');
206
+ res.setHeader('Expires', '0');
207
+ } else if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
208
+ // Cache static assets for 1 year (they have hashed names)
209
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
210
+ }
211
+ }
212
+ }));
213
+
214
+ // API Routes (protected)
215
+ // /api/config endpoint removed - no longer needed
216
+ // Frontend now uses window.location for WebSocket URLs
217
+
218
+ // System update endpoint
219
+ app.post('/api/system/update', authenticateToken, async (req, res) => {
220
+ try {
221
+ // Get the project root directory (parent of server directory)
222
+ const projectRoot = APP_ROOT;
223
+
224
+ console.log('Starting system update from directory:', projectRoot);
225
+
226
+ // Platform deployments use their own update workflow from the project root.
227
+ const updateCommand = IS_PLATFORM
228
+ // In platform, husky and dev dependencies are not needed
229
+ ? 'npm run update:platform'
230
+ : installMode === 'git'
231
+ ? 'git checkout main && git pull && npm install'
232
+ : 'npm install -g @glwhappen/web-code@latest';
233
+
234
+ const updateCwd = IS_PLATFORM || installMode === 'git'
235
+ ? projectRoot
236
+ : os.homedir();
237
+
238
+ const child = spawn('sh', ['-c', updateCommand], {
239
+ cwd: updateCwd,
240
+ env: process.env
241
+ });
242
+
243
+ let output = '';
244
+ let errorOutput = '';
245
+
246
+ child.stdout.on('data', (data) => {
247
+ const text = data.toString();
248
+ output += text;
249
+ console.log('Update output:', text);
250
+ });
251
+
252
+ child.stderr.on('data', (data) => {
253
+ const text = data.toString();
254
+ errorOutput += text;
255
+ console.error('Update error:', text);
256
+ });
257
+
258
+ child.on('close', (code) => {
259
+ if (code === 0) {
260
+ res.json({
261
+ success: true,
262
+ output: output || 'Update completed successfully',
263
+ message: 'Update completed. Please restart the server to apply changes.'
264
+ });
265
+ } else {
266
+ res.status(500).json({
267
+ success: false,
268
+ error: 'Update command failed',
269
+ output: output,
270
+ errorOutput: errorOutput
271
+ });
272
+ }
273
+ });
274
+
275
+ child.on('error', (error) => {
276
+ console.error('Update process error:', error);
277
+ res.status(500).json({
278
+ success: false,
279
+ error: error.message
280
+ });
281
+ });
282
+
283
+ } catch (error) {
284
+ console.error('System update error:', error);
285
+ res.status(500).json({
286
+ success: false,
287
+ error: error.message
288
+ });
289
+ }
290
+ });
291
+
292
+ const expandWorkspacePath = (inputPath) => {
293
+ if (!inputPath) return inputPath;
294
+ if (inputPath === '~') {
295
+ return WORKSPACES_ROOT;
296
+ }
297
+ if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
298
+ return path.join(WORKSPACES_ROOT, inputPath.slice(2));
299
+ }
300
+ return inputPath;
301
+ };
302
+
303
+ // Browse filesystem endpoint for project suggestions - uses existing getFileTree
304
+ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
305
+ try {
306
+ const { path: dirPath } = req.query;
307
+
308
+ console.log('[API] Browse filesystem request for path:', dirPath);
309
+ console.log('[API] WORKSPACES_ROOT is:', WORKSPACES_ROOT);
310
+ // Default to home directory if no path provided
311
+ const defaultRoot = WORKSPACES_ROOT;
312
+ let targetPath = dirPath ? expandWorkspacePath(dirPath) : defaultRoot;
313
+
314
+ // Resolve and normalize the path
315
+ targetPath = path.resolve(targetPath);
316
+
317
+ // Security check - ensure path is within allowed workspace root
318
+ const validation = await validateWorkspacePath(targetPath);
319
+ if (!validation.valid) {
320
+ return res.status(403).json({ error: validation.error });
321
+ }
322
+ const resolvedPath = validation.resolvedPath || targetPath;
323
+
324
+ // Security check - ensure path is accessible
325
+ try {
326
+ await fs.promises.access(resolvedPath);
327
+ const stats = await fs.promises.stat(resolvedPath);
328
+
329
+ if (!stats.isDirectory()) {
330
+ return res.status(400).json({ error: 'Path is not a directory' });
331
+ }
332
+ } catch (err) {
333
+ return res.status(404).json({ error: 'Directory not accessible' });
334
+ }
335
+
336
+ // Use existing getFileTree function with shallow depth (only direct children)
337
+ const fileTree = await getFileTree(resolvedPath, 1, 0, false); // maxDepth=1, showHidden=false
338
+
339
+ // Filter only directories and format for suggestions
340
+ const directories = fileTree
341
+ .filter(item => item.type === 'directory')
342
+ .map(item => ({
343
+ path: item.path,
344
+ name: item.name,
345
+ type: 'directory'
346
+ }))
347
+ .sort((a, b) => {
348
+ const aHidden = a.name.startsWith('.');
349
+ const bHidden = b.name.startsWith('.');
350
+ if (aHidden && !bHidden) return 1;
351
+ if (!aHidden && bHidden) return -1;
352
+ return a.name.localeCompare(b.name);
353
+ });
354
+
355
+ // Add common directories if browsing home directory
356
+ const suggestions = [];
357
+ let resolvedWorkspaceRoot = defaultRoot;
358
+ try {
359
+ resolvedWorkspaceRoot = await fsPromises.realpath(defaultRoot);
360
+ } catch (error) {
361
+ // Use default root as-is if realpath fails
362
+ }
363
+ if (resolvedPath === resolvedWorkspaceRoot) {
364
+ const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace'];
365
+ const existingCommon = directories.filter(dir => commonDirs.includes(dir.name));
366
+ const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name));
367
+
368
+ suggestions.push(...existingCommon, ...otherDirs);
369
+ } else {
370
+ suggestions.push(...directories);
371
+ }
372
+
373
+ res.json({
374
+ path: resolvedPath,
375
+ suggestions: suggestions
376
+ });
377
+
378
+ } catch (error) {
379
+ console.error('Error browsing filesystem:', error);
380
+ res.status(500).json({ error: 'Failed to browse filesystem' });
381
+ }
382
+ });
383
+
384
+ app.post('/api/create-folder', authenticateToken, async (req, res) => {
385
+ try {
386
+ const { path: folderPath } = req.body;
387
+ if (!folderPath) {
388
+ return res.status(400).json({ error: 'Path is required' });
389
+ }
390
+ const expandedPath = expandWorkspacePath(folderPath);
391
+ const resolvedInput = path.resolve(expandedPath);
392
+ const validation = await validateWorkspacePath(resolvedInput);
393
+ if (!validation.valid) {
394
+ return res.status(403).json({ error: validation.error });
395
+ }
396
+ const targetPath = validation.resolvedPath || resolvedInput;
397
+ const parentDir = path.dirname(targetPath);
398
+ try {
399
+ await fs.promises.access(parentDir);
400
+ } catch (err) {
401
+ return res.status(404).json({ error: 'Parent directory does not exist' });
402
+ }
403
+ try {
404
+ await fs.promises.access(targetPath);
405
+ return res.status(409).json({ error: 'Folder already exists' });
406
+ } catch (err) {
407
+ // Folder doesn't exist, which is what we want
408
+ }
409
+ try {
410
+ await fs.promises.mkdir(targetPath, { recursive: false });
411
+ res.json({ success: true, path: targetPath });
412
+ } catch (mkdirError) {
413
+ if (mkdirError.code === 'EEXIST') {
414
+ return res.status(409).json({ error: 'Folder already exists' });
415
+ }
416
+ throw mkdirError;
417
+ }
418
+ } catch (error) {
419
+ console.error('Error creating folder:', error);
420
+ res.status(500).json({ error: 'Failed to create folder' });
421
+ }
422
+ });
423
+
424
+ // Read file content endpoint
425
+ app.get('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
426
+ try {
427
+ const { projectId } = req.params;
428
+ const { filePath } = req.query;
429
+
430
+
431
+ // Security: ensure the requested path is inside the project root
432
+ if (!filePath) {
433
+ return res.status(400).json({ error: 'Invalid file path' });
434
+ }
435
+
436
+ // Resolve the absolute project root via the DB-backed helper; the
437
+ // caller passes the DB-assigned `projectId`, not a folder name.
438
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
439
+ if (!projectRoot) {
440
+ return res.status(404).json({ error: 'Project not found' });
441
+ }
442
+
443
+ // Handle both absolute and relative paths
444
+ const resolved = path.isAbsolute(filePath)
445
+ ? path.resolve(filePath)
446
+ : path.resolve(projectRoot, filePath);
447
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
448
+ if (!resolved.startsWith(normalizedRoot)) {
449
+ return res.status(403).json({ error: 'Path must be under project root' });
450
+ }
451
+
452
+ const content = await fsPromises.readFile(resolved, 'utf8');
453
+ res.json({ content, path: resolved });
454
+ } catch (error) {
455
+ console.error('Error reading file:', error);
456
+ if (error.code === 'ENOENT') {
457
+ res.status(404).json({ error: 'File not found' });
458
+ } else if (error.code === 'EACCES') {
459
+ res.status(403).json({ error: 'Permission denied' });
460
+ } else {
461
+ res.status(500).json({ error: error.message });
462
+ }
463
+ }
464
+ });
465
+
466
+ // Serve raw file bytes for previews and downloads.
467
+ app.get('/api/projects/:projectId/files/content', authenticateToken, async (req, res) => {
468
+ try {
469
+ const { projectId } = req.params;
470
+ const { path: filePath } = req.query;
471
+
472
+
473
+ // Security: ensure the requested path is inside the project root
474
+ if (!filePath) {
475
+ return res.status(400).json({ error: 'Invalid file path' });
476
+ }
477
+
478
+ // Projects are now addressed by DB `projectId`, resolved to their path here.
479
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
480
+ if (!projectRoot) {
481
+ return res.status(404).json({ error: 'Project not found' });
482
+ }
483
+
484
+ // Match the text reader endpoint so callers can pass either project-relative
485
+ // or absolute paths without changing how the bytes are served.
486
+ const resolved = path.isAbsolute(filePath)
487
+ ? path.resolve(filePath)
488
+ : path.resolve(projectRoot, filePath);
489
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
490
+ if (!resolved.startsWith(normalizedRoot)) {
491
+ return res.status(403).json({ error: 'Path must be under project root' });
492
+ }
493
+
494
+ // Check if file exists
495
+ try {
496
+ await fsPromises.access(resolved);
497
+ } catch (error) {
498
+ return res.status(404).json({ error: 'File not found' });
499
+ }
500
+
501
+ // Get file extension and set appropriate content type
502
+ const mimeType = mime.lookup(resolved) || 'application/octet-stream';
503
+ res.setHeader('Content-Type', mimeType);
504
+
505
+ // Stream the file
506
+ const fileStream = fs.createReadStream(resolved);
507
+ fileStream.pipe(res);
508
+
509
+ fileStream.on('error', (error) => {
510
+ console.error('Error streaming file:', error);
511
+ if (!res.headersSent) {
512
+ res.status(500).json({ error: 'Error reading file' });
513
+ }
514
+ });
515
+
516
+ } catch (error) {
517
+ console.error('Error serving binary file:', error);
518
+ if (!res.headersSent) {
519
+ res.status(500).json({ error: error.message });
520
+ }
521
+ }
522
+ });
523
+
524
+ // Save file content endpoint
525
+ app.put('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
526
+ try {
527
+ const { projectId } = req.params;
528
+ const { filePath, content } = req.body;
529
+
530
+
531
+ // Security: ensure the requested path is inside the project root
532
+ if (!filePath) {
533
+ return res.status(400).json({ error: 'Invalid file path' });
534
+ }
535
+
536
+ if (content === undefined) {
537
+ return res.status(400).json({ error: 'Content is required' });
538
+ }
539
+
540
+ // Projects are now addressed by DB `projectId`, resolved to their path here.
541
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
542
+ if (!projectRoot) {
543
+ return res.status(404).json({ error: 'Project not found' });
544
+ }
545
+
546
+ // Handle both absolute and relative paths
547
+ const resolved = path.isAbsolute(filePath)
548
+ ? path.resolve(filePath)
549
+ : path.resolve(projectRoot, filePath);
550
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
551
+ if (!resolved.startsWith(normalizedRoot)) {
552
+ return res.status(403).json({ error: 'Path must be under project root' });
553
+ }
554
+
555
+ // Write the new content
556
+ await fsPromises.writeFile(resolved, content, 'utf8');
557
+
558
+ res.json({
559
+ success: true,
560
+ path: resolved,
561
+ message: 'File saved successfully'
562
+ });
563
+ } catch (error) {
564
+ console.error('Error saving file:', error);
565
+ if (error.code === 'ENOENT') {
566
+ res.status(404).json({ error: 'File or directory not found' });
567
+ } else if (error.code === 'EACCES') {
568
+ res.status(403).json({ error: 'Permission denied' });
569
+ } else {
570
+ res.status(500).json({ error: error.message });
571
+ }
572
+ }
573
+ });
574
+
575
+ app.get('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
576
+ try {
577
+
578
+ // Using fsPromises from import
579
+
580
+ // Resolve the project's absolute path through the DB (projectId is the
581
+ // primary key of the `projects` table after the identifier migration).
582
+ const actualPath = await projectsDb.getProjectPathById(req.user.id, req.params.projectId);
583
+ if (!actualPath) {
584
+ return res.status(404).json({ error: 'Project not found' });
585
+ }
586
+
587
+ // Check if path exists
588
+ try {
589
+ await fsPromises.access(actualPath);
590
+ } catch (e) {
591
+ return res.status(404).json({ error: `Project path not found: ${actualPath}` });
592
+ }
593
+
594
+ const files = await getFileTree(actualPath, 10, 0, true);
595
+ res.json(files);
596
+ } catch (error) {
597
+ console.error('[ERROR] File tree error:', error.message);
598
+ res.status(500).json({ error: error.message });
599
+ }
600
+ });
601
+
602
+ // ============================================================================
603
+ // FILE OPERATIONS API ENDPOINTS
604
+ // ============================================================================
605
+
606
+ /**
607
+ * Validate that a path is within the project root
608
+ * @param {string} projectRoot - The project root path
609
+ * @param {string} targetPath - The path to validate
610
+ * @returns {{ valid: boolean, resolved?: string, error?: string }}
611
+ */
612
+ function validatePathInProject(projectRoot, targetPath) {
613
+ const resolved = path.isAbsolute(targetPath)
614
+ ? path.resolve(targetPath)
615
+ : path.resolve(projectRoot, targetPath);
616
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
617
+ if (!resolved.startsWith(normalizedRoot)) {
618
+ return { valid: false, error: 'Path must be under project root' };
619
+ }
620
+ return { valid: true, resolved };
621
+ }
622
+
623
+ /**
624
+ * Validate filename - check for invalid characters
625
+ * @param {string} name - The filename to validate
626
+ * @returns {{ valid: boolean, error?: string }}
627
+ */
628
+ function validateFilename(name) {
629
+ if (!name || !name.trim()) {
630
+ return { valid: false, error: 'Filename cannot be empty' };
631
+ }
632
+ // Check for invalid characters (Windows + Unix)
633
+ const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
634
+ if (invalidChars.test(name)) {
635
+ return { valid: false, error: 'Filename contains invalid characters' };
636
+ }
637
+ // Check for reserved names (Windows)
638
+ const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
639
+ if (reserved.test(name)) {
640
+ return { valid: false, error: 'Filename is a reserved name' };
641
+ }
642
+ // Check for dots only
643
+ if (/^\.+$/.test(name)) {
644
+ return { valid: false, error: 'Filename cannot be only dots' };
645
+ }
646
+ return { valid: true };
647
+ }
648
+
649
+ // POST /api/projects/:projectId/files/create - Create new file or directory
650
+ app.post('/api/projects/:projectId/files/create', authenticateToken, async (req, res) => {
651
+ try {
652
+ const { projectId } = req.params;
653
+ const { path: parentPath, type, name } = req.body;
654
+
655
+ // Validate input
656
+ if (!name || !type) {
657
+ return res.status(400).json({ error: 'Name and type are required' });
658
+ }
659
+
660
+ if (!['file', 'directory'].includes(type)) {
661
+ return res.status(400).json({ error: 'Type must be "file" or "directory"' });
662
+ }
663
+
664
+ const nameValidation = validateFilename(name);
665
+ if (!nameValidation.valid) {
666
+ return res.status(400).json({ error: nameValidation.error });
667
+ }
668
+
669
+ // Resolve the project directory through the DB using the new projectId.
670
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
671
+ if (!projectRoot) {
672
+ return res.status(404).json({ error: 'Project not found' });
673
+ }
674
+
675
+ // Build and validate target path
676
+ const targetDir = parentPath || '';
677
+ const targetPath = targetDir ? path.join(targetDir, name) : name;
678
+ const validation = validatePathInProject(projectRoot, targetPath);
679
+ if (!validation.valid) {
680
+ return res.status(403).json({ error: validation.error });
681
+ }
682
+
683
+ const resolvedPath = validation.resolved;
684
+
685
+ // Check if already exists
686
+ try {
687
+ await fsPromises.access(resolvedPath);
688
+ return res.status(409).json({ error: `${type === 'file' ? 'File' : 'Directory'} already exists` });
689
+ } catch {
690
+ // Doesn't exist, which is what we want
691
+ }
692
+
693
+ // Create file or directory
694
+ if (type === 'directory') {
695
+ await fsPromises.mkdir(resolvedPath, { recursive: false });
696
+ } else {
697
+ // Ensure parent directory exists
698
+ const parentDir = path.dirname(resolvedPath);
699
+ try {
700
+ await fsPromises.access(parentDir);
701
+ } catch {
702
+ await fsPromises.mkdir(parentDir, { recursive: true });
703
+ }
704
+ await fsPromises.writeFile(resolvedPath, '', 'utf8');
705
+ }
706
+
707
+ res.json({
708
+ success: true,
709
+ path: resolvedPath,
710
+ name,
711
+ type,
712
+ message: `${type === 'file' ? 'File' : 'Directory'} created successfully`
713
+ });
714
+ } catch (error) {
715
+ console.error('Error creating file/directory:', error);
716
+ if (error.code === 'EACCES') {
717
+ res.status(403).json({ error: 'Permission denied' });
718
+ } else if (error.code === 'ENOENT') {
719
+ res.status(404).json({ error: 'Parent directory not found' });
720
+ } else {
721
+ res.status(500).json({ error: error.message });
722
+ }
723
+ }
724
+ });
725
+
726
+ // PUT /api/projects/:projectId/files/rename - Rename file or directory
727
+ app.put('/api/projects/:projectId/files/rename', authenticateToken, async (req, res) => {
728
+ try {
729
+ const { projectId } = req.params;
730
+ const { oldPath, newName } = req.body;
731
+
732
+ // Validate input
733
+ if (!oldPath || !newName) {
734
+ return res.status(400).json({ error: 'oldPath and newName are required' });
735
+ }
736
+
737
+ const nameValidation = validateFilename(newName);
738
+ if (!nameValidation.valid) {
739
+ return res.status(400).json({ error: nameValidation.error });
740
+ }
741
+
742
+ // Resolve the project directory through the DB using the new projectId.
743
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
744
+ if (!projectRoot) {
745
+ return res.status(404).json({ error: 'Project not found' });
746
+ }
747
+
748
+ // Validate old path
749
+ const oldValidation = validatePathInProject(projectRoot, oldPath);
750
+ if (!oldValidation.valid) {
751
+ return res.status(403).json({ error: oldValidation.error });
752
+ }
753
+
754
+ const resolvedOldPath = oldValidation.resolved;
755
+
756
+ // Check if old path exists
757
+ try {
758
+ await fsPromises.access(resolvedOldPath);
759
+ } catch {
760
+ return res.status(404).json({ error: 'File or directory not found' });
761
+ }
762
+
763
+ // Build and validate new path
764
+ const parentDir = path.dirname(resolvedOldPath);
765
+ const resolvedNewPath = path.join(parentDir, newName);
766
+ const newValidation = validatePathInProject(projectRoot, resolvedNewPath);
767
+ if (!newValidation.valid) {
768
+ return res.status(403).json({ error: newValidation.error });
769
+ }
770
+
771
+ // Check if new path already exists
772
+ try {
773
+ await fsPromises.access(resolvedNewPath);
774
+ return res.status(409).json({ error: 'A file or directory with this name already exists' });
775
+ } catch {
776
+ // Doesn't exist, which is what we want
777
+ }
778
+
779
+ // Rename
780
+ await fsPromises.rename(resolvedOldPath, resolvedNewPath);
781
+
782
+ res.json({
783
+ success: true,
784
+ oldPath: resolvedOldPath,
785
+ newPath: resolvedNewPath,
786
+ newName,
787
+ message: 'Renamed successfully'
788
+ });
789
+ } catch (error) {
790
+ console.error('Error renaming file/directory:', error);
791
+ if (error.code === 'EACCES') {
792
+ res.status(403).json({ error: 'Permission denied' });
793
+ } else if (error.code === 'ENOENT') {
794
+ res.status(404).json({ error: 'File or directory not found' });
795
+ } else if (error.code === 'EXDEV') {
796
+ res.status(400).json({ error: 'Cannot move across different filesystems' });
797
+ } else {
798
+ res.status(500).json({ error: error.message });
799
+ }
800
+ }
801
+ });
802
+
803
+ // DELETE /api/projects/:projectId/files - Delete file or directory
804
+ app.delete('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
805
+ try {
806
+ const { projectId } = req.params;
807
+ const { path: targetPath, type } = req.body;
808
+
809
+ // Validate input
810
+ if (!targetPath) {
811
+ return res.status(400).json({ error: 'Path is required' });
812
+ }
813
+
814
+ // Resolve the project directory through the DB using the new projectId.
815
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
816
+ if (!projectRoot) {
817
+ return res.status(404).json({ error: 'Project not found' });
818
+ }
819
+
820
+ // Validate path
821
+ const validation = validatePathInProject(projectRoot, targetPath);
822
+ if (!validation.valid) {
823
+ return res.status(403).json({ error: validation.error });
824
+ }
825
+
826
+ const resolvedPath = validation.resolved;
827
+
828
+ // Check if path exists and get stats
829
+ let stats;
830
+ try {
831
+ stats = await fsPromises.stat(resolvedPath);
832
+ } catch {
833
+ return res.status(404).json({ error: 'File or directory not found' });
834
+ }
835
+
836
+ // Prevent deleting the project root itself
837
+ if (resolvedPath === path.resolve(projectRoot)) {
838
+ return res.status(403).json({ error: 'Cannot delete project root directory' });
839
+ }
840
+
841
+ // Delete based on type
842
+ if (stats.isDirectory()) {
843
+ await fsPromises.rm(resolvedPath, { recursive: true, force: true });
844
+ } else {
845
+ await fsPromises.unlink(resolvedPath);
846
+ }
847
+
848
+ res.json({
849
+ success: true,
850
+ path: resolvedPath,
851
+ type: stats.isDirectory() ? 'directory' : 'file',
852
+ message: 'Deleted successfully'
853
+ });
854
+ } catch (error) {
855
+ console.error('Error deleting file/directory:', error);
856
+ if (error.code === 'EACCES') {
857
+ res.status(403).json({ error: 'Permission denied' });
858
+ } else if (error.code === 'ENOENT') {
859
+ res.status(404).json({ error: 'File or directory not found' });
860
+ } else if (error.code === 'ENOTEMPTY') {
861
+ res.status(400).json({ error: 'Directory is not empty' });
862
+ } else {
863
+ res.status(500).json({ error: error.message });
864
+ }
865
+ }
866
+ });
867
+
868
+ // POST /api/projects/:projectId/files/upload - Upload files
869
+ // Dynamic import of multer for file uploads
870
+ const uploadFilesHandler = async (req, res) => {
871
+ // Dynamic import of multer
872
+ const multer = (await import('multer')).default;
873
+
874
+ const uploadMiddleware = multer({
875
+ storage: multer.diskStorage({
876
+ destination: (req, file, cb) => {
877
+ cb(null, os.tmpdir());
878
+ },
879
+ filename: (req, file, cb) => {
880
+ // Use a unique temp name, but preserve original name in file.originalname
881
+ // Note: file.originalname may contain path separators for folder uploads
882
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
883
+ // For temp file, just use a safe unique name without the path
884
+ cb(null, `upload-${uniqueSuffix}`);
885
+ }
886
+ }),
887
+ limits: {
888
+ fileSize: 50 * 1024 * 1024, // 50MB limit
889
+ files: 20 // Max 20 files at once
890
+ }
891
+ });
892
+
893
+ // Use multer middleware
894
+ uploadMiddleware.array('files', 20)(req, res, async (err) => {
895
+ if (err) {
896
+ console.error('Multer error:', err);
897
+ if (err.code === 'LIMIT_FILE_SIZE') {
898
+ return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' });
899
+ }
900
+ if (err.code === 'LIMIT_FILE_COUNT') {
901
+ return res.status(400).json({ error: 'Too many files. Maximum is 20 files.' });
902
+ }
903
+ return res.status(500).json({ error: err.message });
904
+ }
905
+
906
+ try {
907
+ const { projectId } = req.params;
908
+ const { targetPath, relativePaths } = req.body;
909
+
910
+ // Parse relative paths if provided (for folder uploads)
911
+ let filePaths = [];
912
+ if (relativePaths) {
913
+ try {
914
+ filePaths = JSON.parse(relativePaths);
915
+ } catch (e) {
916
+ console.log('[DEBUG] Failed to parse relativePaths:', relativePaths);
917
+ }
918
+ }
919
+
920
+ console.log('[DEBUG] File upload request:', {
921
+ projectId,
922
+ targetPath: JSON.stringify(targetPath),
923
+ targetPathType: typeof targetPath,
924
+ filesCount: req.files?.length,
925
+ relativePaths: filePaths
926
+ });
927
+
928
+ if (!req.files || req.files.length === 0) {
929
+ return res.status(400).json({ error: 'No files provided' });
930
+ }
931
+
932
+ // Resolve the project directory through the DB using the new projectId.
933
+ const projectRoot = await projectsDb.getProjectPathById(req.user.id, projectId);
934
+ if (!projectRoot) {
935
+ return res.status(404).json({ error: 'Project not found' });
936
+ }
937
+
938
+ console.log('[DEBUG] Project root:', projectRoot);
939
+
940
+ // Validate and resolve target path
941
+ // If targetPath is empty or '.', use project root directly
942
+ const targetDir = targetPath || '';
943
+ let resolvedTargetDir;
944
+
945
+ console.log('[DEBUG] Target dir:', JSON.stringify(targetDir));
946
+
947
+ if (!targetDir || targetDir === '.' || targetDir === './') {
948
+ // Empty path means upload to project root
949
+ resolvedTargetDir = path.resolve(projectRoot);
950
+ console.log('[DEBUG] Using project root as target:', resolvedTargetDir);
951
+ } else {
952
+ const validation = validatePathInProject(projectRoot, targetDir);
953
+ if (!validation.valid) {
954
+ console.log('[DEBUG] Path validation failed:', validation.error);
955
+ return res.status(403).json({ error: validation.error });
956
+ }
957
+ resolvedTargetDir = validation.resolved;
958
+ console.log('[DEBUG] Resolved target dir:', resolvedTargetDir);
959
+ }
960
+
961
+ // Ensure target directory exists
962
+ try {
963
+ await fsPromises.access(resolvedTargetDir);
964
+ } catch {
965
+ await fsPromises.mkdir(resolvedTargetDir, { recursive: true });
966
+ }
967
+
968
+ // Move uploaded files from temp to target directory
969
+ const uploadedFiles = [];
970
+ console.log('[DEBUG] Processing files:', req.files.map(f => ({ originalname: f.originalname, path: f.path })));
971
+ for (let i = 0; i < req.files.length; i++) {
972
+ const file = req.files[i];
973
+ // Use relative path if provided (for folder uploads), otherwise use originalname
974
+ const fileName = (filePaths && filePaths[i]) ? filePaths[i] : file.originalname;
975
+ console.log('[DEBUG] Processing file:', fileName, '(originalname:', file.originalname + ')');
976
+ const destPath = path.join(resolvedTargetDir, fileName);
977
+
978
+ // Validate destination path
979
+ const destValidation = validatePathInProject(projectRoot, destPath);
980
+ if (!destValidation.valid) {
981
+ console.log('[DEBUG] Destination validation failed for:', destPath);
982
+ // Clean up temp file
983
+ await fsPromises.unlink(file.path).catch(() => {});
984
+ continue;
985
+ }
986
+
987
+ // Ensure parent directory exists (for nested files from folder upload)
988
+ const parentDir = path.dirname(destPath);
989
+ try {
990
+ await fsPromises.access(parentDir);
991
+ } catch {
992
+ await fsPromises.mkdir(parentDir, { recursive: true });
993
+ }
994
+
995
+ // Move file (copy + unlink to handle cross-device scenarios)
996
+ await fsPromises.copyFile(file.path, destPath);
997
+ await fsPromises.unlink(file.path);
998
+
999
+ uploadedFiles.push({
1000
+ name: fileName,
1001
+ path: destPath,
1002
+ size: file.size,
1003
+ mimeType: file.mimetype
1004
+ });
1005
+ }
1006
+
1007
+ res.json({
1008
+ success: true,
1009
+ files: uploadedFiles,
1010
+ targetPath: resolvedTargetDir,
1011
+ message: `Uploaded ${uploadedFiles.length} file(s) successfully`
1012
+ });
1013
+ } catch (error) {
1014
+ console.error('Error uploading files:', error);
1015
+ // Clean up any remaining temp files
1016
+ if (req.files) {
1017
+ for (const file of req.files) {
1018
+ await fsPromises.unlink(file.path).catch(() => {});
1019
+ }
1020
+ }
1021
+ if (error.code === 'EACCES') {
1022
+ res.status(403).json({ error: 'Permission denied' });
1023
+ } else {
1024
+ res.status(500).json({ error: error.message });
1025
+ }
1026
+ }
1027
+ });
1028
+ };
1029
+
1030
+ app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
1031
+
1032
+ // Image upload endpoint. Accepts the DB-assigned `projectId` (not a folder name)
1033
+ // but the current implementation doesn't need to touch the project directory,
1034
+ // so we just leave the param rename for consistency with the rest of the API.
1035
+ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
1036
+ try {
1037
+ const multer = (await import('multer')).default;
1038
+ const path = (await import('path')).default;
1039
+ const fs = (await import('fs')).promises;
1040
+ const os = (await import('os')).default;
1041
+
1042
+ // Configure multer for image uploads
1043
+ const storage = multer.diskStorage({
1044
+ destination: async (req, file, cb) => {
1045
+ const uploadDir = path.join(os.tmpdir(), 'claude-ui-uploads', String(req.user.id));
1046
+ await fs.mkdir(uploadDir, { recursive: true });
1047
+ cb(null, uploadDir);
1048
+ },
1049
+ filename: (req, file, cb) => {
1050
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
1051
+ const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
1052
+ cb(null, uniqueSuffix + '-' + sanitizedName);
1053
+ }
1054
+ });
1055
+
1056
+ const fileFilter = (req, file, cb) => {
1057
+ const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
1058
+ if (allowedMimes.includes(file.mimetype)) {
1059
+ cb(null, true);
1060
+ } else {
1061
+ cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
1062
+ }
1063
+ };
1064
+
1065
+ const upload = multer({
1066
+ storage,
1067
+ fileFilter,
1068
+ limits: {
1069
+ fileSize: 5 * 1024 * 1024, // 5MB
1070
+ files: 5
1071
+ }
1072
+ });
1073
+
1074
+ // Handle multipart form data
1075
+ upload.array('images', 5)(req, res, async (err) => {
1076
+ if (err) {
1077
+ return res.status(400).json({ error: err.message });
1078
+ }
1079
+
1080
+ if (!req.files || req.files.length === 0) {
1081
+ return res.status(400).json({ error: 'No image files provided' });
1082
+ }
1083
+
1084
+ try {
1085
+ // Process uploaded images
1086
+ const processedImages = await Promise.all(
1087
+ req.files.map(async (file) => {
1088
+ // Read file and convert to base64
1089
+ const buffer = await fs.readFile(file.path);
1090
+ const base64 = buffer.toString('base64');
1091
+ const mimeType = file.mimetype;
1092
+
1093
+ // Clean up temp file immediately
1094
+ await fs.unlink(file.path);
1095
+
1096
+ return {
1097
+ name: file.originalname,
1098
+ data: `data:${mimeType};base64,${base64}`,
1099
+ size: file.size,
1100
+ mimeType: mimeType
1101
+ };
1102
+ })
1103
+ );
1104
+
1105
+ res.json({ images: processedImages });
1106
+ } catch (error) {
1107
+ console.error('Error processing images:', error);
1108
+ // Clean up any remaining files
1109
+ await Promise.all(req.files.map(f => fs.unlink(f.path).catch(() => { })));
1110
+ res.status(500).json({ error: 'Failed to process images' });
1111
+ }
1112
+ });
1113
+ } catch (error) {
1114
+ console.error('Error in image upload endpoint:', error);
1115
+ res.status(500).json({ error: 'Internal server error' });
1116
+ }
1117
+ });
1118
+
1119
+ // Get token usage for a specific session. `projectId` is the DB primary key;
1120
+ // the Claude branch below resolves it to an absolute path via the DB.
1121
+ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1122
+ try {
1123
+ const { projectId, sessionId } = req.params;
1124
+ const { provider = 'claude' } = req.query;
1125
+ const homeDir = os.homedir();
1126
+
1127
+ // Allow only safe characters in sessionId
1128
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
1129
+ if (!safeSessionId || safeSessionId !== String(sessionId)) {
1130
+ return res.status(400).json({ error: 'Invalid sessionId' });
1131
+ }
1132
+
1133
+ // Handle Cursor sessions - they use SQLite and don't have token usage info
1134
+ if (provider === 'cursor') {
1135
+ return res.json({
1136
+ used: 0,
1137
+ total: 0,
1138
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1139
+ unsupported: true,
1140
+ message: 'Token usage tracking not available for Cursor sessions'
1141
+ });
1142
+ }
1143
+
1144
+ // Handle Gemini sessions - they are raw logs in our current setup
1145
+ if (provider === 'gemini') {
1146
+ return res.json({
1147
+ used: 0,
1148
+ total: 0,
1149
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1150
+ unsupported: true,
1151
+ message: 'Token usage tracking not available for Gemini sessions'
1152
+ });
1153
+ }
1154
+
1155
+ // Handle Codex sessions
1156
+ if (provider === 'codex') {
1157
+ const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
1158
+
1159
+ // Find the session file by searching for the session ID
1160
+ const findSessionFile = async (dir) => {
1161
+ try {
1162
+ const entries = await fsPromises.readdir(dir, { withFileTypes: true });
1163
+ for (const entry of entries) {
1164
+ const fullPath = path.join(dir, entry.name);
1165
+ if (entry.isDirectory()) {
1166
+ const found = await findSessionFile(fullPath);
1167
+ if (found) return found;
1168
+ } else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
1169
+ return fullPath;
1170
+ }
1171
+ }
1172
+ } catch (error) {
1173
+ // Skip directories we can't read
1174
+ }
1175
+ return null;
1176
+ };
1177
+
1178
+ const sessionFilePath = await findSessionFile(codexSessionsDir);
1179
+
1180
+ if (!sessionFilePath) {
1181
+ return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
1182
+ }
1183
+
1184
+ // Read and parse the Codex JSONL file
1185
+ let fileContent;
1186
+ try {
1187
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1188
+ } catch (error) {
1189
+ if (error.code === 'ENOENT') {
1190
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1191
+ }
1192
+ throw error;
1193
+ }
1194
+ const lines = fileContent.trim().split('\n');
1195
+ let totalTokens = 0;
1196
+ let contextWindow = 200000; // Default for Codex/OpenAI
1197
+
1198
+ // Find the latest token_count event with info (scan from end)
1199
+ for (let i = lines.length - 1; i >= 0; i--) {
1200
+ try {
1201
+ const entry = JSON.parse(lines[i]);
1202
+
1203
+ // Codex stores token info in event_msg with type: "token_count"
1204
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1205
+ const tokenInfo = entry.payload.info;
1206
+ if (tokenInfo.total_token_usage) {
1207
+ totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
1208
+ }
1209
+ if (tokenInfo.model_context_window) {
1210
+ contextWindow = tokenInfo.model_context_window;
1211
+ }
1212
+ break; // Stop after finding the latest token count
1213
+ }
1214
+ } catch (parseError) {
1215
+ // Skip lines that can't be parsed
1216
+ continue;
1217
+ }
1218
+ }
1219
+
1220
+ return res.json({
1221
+ used: totalTokens,
1222
+ total: contextWindow
1223
+ });
1224
+ }
1225
+
1226
+ // Handle Claude sessions (default)
1227
+ // Resolve the project path through the DB using the caller-supplied
1228
+ // `projectId`. Legacy code here called extractProjectDirectory with a
1229
+ // folder-encoded project name; the migration centralizes that lookup
1230
+ // in the projects table.
1231
+ const projectPath = await projectsDb.getProjectPathById(req.user.id, projectId);
1232
+ if (!projectPath) {
1233
+ return res.status(404).json({ error: 'Project not found' });
1234
+ }
1235
+
1236
+ // Construct the JSONL file path
1237
+ // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
1238
+ // The encoding replaces any non-alphanumeric character (except -) with -
1239
+ const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
1240
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1241
+
1242
+ const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
1243
+
1244
+ // Constrain to projectDir
1245
+ const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
1246
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
1247
+ return res.status(400).json({ error: 'Invalid path' });
1248
+ }
1249
+
1250
+ // Read and parse the JSONL file
1251
+ let fileContent;
1252
+ try {
1253
+ fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
1254
+ } catch (error) {
1255
+ if (error.code === 'ENOENT') {
1256
+ return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
1257
+ }
1258
+ throw error; // Re-throw other errors to be caught by outer try-catch
1259
+ }
1260
+ const lines = fileContent.trim().split('\n');
1261
+
1262
+ const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
1263
+ const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
1264
+ let inputTokens = 0;
1265
+ let cacheCreationTokens = 0;
1266
+ let cacheReadTokens = 0;
1267
+
1268
+ // Find the latest assistant message with usage data (scan from end)
1269
+ for (let i = lines.length - 1; i >= 0; i--) {
1270
+ try {
1271
+ const entry = JSON.parse(lines[i]);
1272
+
1273
+ // Only count assistant messages which have usage data
1274
+ if (entry.type === 'assistant' && entry.message?.usage) {
1275
+ const usage = entry.message.usage;
1276
+
1277
+ // Use token counts from latest assistant message only
1278
+ inputTokens = usage.input_tokens || 0;
1279
+ cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1280
+ cacheReadTokens = usage.cache_read_input_tokens || 0;
1281
+
1282
+ break; // Stop after finding the latest assistant message
1283
+ }
1284
+ } catch (parseError) {
1285
+ // Skip lines that can't be parsed
1286
+ continue;
1287
+ }
1288
+ }
1289
+
1290
+ // Calculate total context usage (excluding output_tokens, as per ccusage)
1291
+ const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
1292
+
1293
+ res.json({
1294
+ used: totalUsed,
1295
+ total: contextWindow,
1296
+ breakdown: {
1297
+ input: inputTokens,
1298
+ cacheCreation: cacheCreationTokens,
1299
+ cacheRead: cacheReadTokens
1300
+ }
1301
+ });
1302
+ } catch (error) {
1303
+ console.error('Error reading session token usage:', error);
1304
+ res.status(500).json({ error: 'Failed to read session token usage' });
1305
+ }
1306
+ });
1307
+
1308
+ // Serve React app for all other routes (excluding static files)
1309
+ app.get('*', (req, res) => {
1310
+ // Skip requests for static assets (files with extensions)
1311
+ if (path.extname(req.path)) {
1312
+ return res.status(404).send('Not found');
1313
+ }
1314
+
1315
+ // Only serve index.html for HTML routes, not for static assets
1316
+ // Static assets should already be handled by express.static middleware above
1317
+ const indexPath = path.join(APP_ROOT, 'dist', 'index.html');
1318
+
1319
+ // Check if dist/index.html exists (production build available)
1320
+ if (fs.existsSync(indexPath)) {
1321
+ // Set no-cache headers for HTML to prevent service worker issues
1322
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1323
+ res.setHeader('Pragma', 'no-cache');
1324
+ res.setHeader('Expires', '0');
1325
+ res.sendFile(indexPath);
1326
+ } else {
1327
+ // In development, redirect to Vite dev server only if dist doesn't exist
1328
+ const redirectHost = getConnectableHost(req.hostname);
1329
+ res.redirect(`${req.protocol}://${redirectHost}:${VITE_PORT}`);
1330
+ }
1331
+ });
1332
+
1333
+ // global error middleware must be last
1334
+ app.use((err, req, res, next) => {
1335
+ if (err instanceof AppError) {
1336
+ return res.status(err.statusCode).json({
1337
+ success: false,
1338
+ error: {
1339
+ code: err.code,
1340
+ message: err.message,
1341
+ details: err.details,
1342
+ },
1343
+ });
1344
+ }
1345
+
1346
+ console.error(err);
1347
+
1348
+ return res.status(500).json({
1349
+ success: false,
1350
+ error: {
1351
+ code: 'INTERNAL_ERROR',
1352
+ message: 'Internal server error',
1353
+ },
1354
+ });
1355
+ });
1356
+
1357
+ // Helper function to convert permissions to rwx format
1358
+ function permToRwx(perm) {
1359
+ const r = perm & 4 ? 'r' : '-';
1360
+ const w = perm & 2 ? 'w' : '-';
1361
+ const x = perm & 1 ? 'x' : '-';
1362
+ return r + w + x;
1363
+ }
1364
+
1365
+ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
1366
+ // Using fsPromises from import
1367
+ const items = [];
1368
+
1369
+ try {
1370
+ const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
1371
+
1372
+ for (const entry of entries) {
1373
+ // Debug: log all entries including hidden files
1374
+
1375
+
1376
+ // Skip heavy build directories and VCS directories
1377
+ if (entry.name === 'node_modules' ||
1378
+ entry.name === 'dist' ||
1379
+ entry.name === 'build' ||
1380
+ entry.name === '.git' ||
1381
+ entry.name === '.svn' ||
1382
+ entry.name === '.hg') continue;
1383
+
1384
+ const itemPath = path.join(dirPath, entry.name);
1385
+ const item = {
1386
+ name: entry.name,
1387
+ path: itemPath,
1388
+ type: entry.isDirectory() ? 'directory' : 'file'
1389
+ };
1390
+
1391
+ // Get file stats for additional metadata
1392
+ try {
1393
+ const stats = await fsPromises.stat(itemPath);
1394
+ item.size = stats.size;
1395
+ item.modified = stats.mtime.toISOString();
1396
+
1397
+ // Convert permissions to rwx format
1398
+ const mode = stats.mode;
1399
+ const ownerPerm = (mode >> 6) & 7;
1400
+ const groupPerm = (mode >> 3) & 7;
1401
+ const otherPerm = mode & 7;
1402
+ item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
1403
+ item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
1404
+ } catch (statError) {
1405
+ // If stat fails, provide default values
1406
+ item.size = 0;
1407
+ item.modified = null;
1408
+ item.permissions = '000';
1409
+ item.permissionsRwx = '---------';
1410
+ }
1411
+
1412
+ if (entry.isDirectory() && currentDepth < maxDepth) {
1413
+ // Recursively get subdirectories but limit depth
1414
+ try {
1415
+ // Check if we can access the directory before trying to read it
1416
+ await fsPromises.access(item.path, fs.constants.R_OK);
1417
+ item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
1418
+ } catch (e) {
1419
+ // Silently skip directories we can't access (permission denied, etc.)
1420
+ item.children = [];
1421
+ }
1422
+ }
1423
+
1424
+ items.push(item);
1425
+ }
1426
+ } catch (error) {
1427
+ // Only log non-permission errors to avoid spam
1428
+ if (error.code !== 'EACCES' && error.code !== 'EPERM') {
1429
+ console.error('Error reading directory:', error);
1430
+ }
1431
+ }
1432
+
1433
+ return items.sort((a, b) => {
1434
+ if (a.type !== b.type) {
1435
+ return a.type === 'directory' ? -1 : 1;
1436
+ }
1437
+ return a.name.localeCompare(b.name);
1438
+ });
1439
+ }
1440
+
1441
+ const SERVER_PORT = process.env.SERVER_PORT || 3001;
1442
+ const HOST = process.env.HOST || '0.0.0.0';
1443
+ const DISPLAY_HOST = getConnectableHost(HOST);
1444
+ const VITE_PORT = process.env.VITE_PORT || 5173;
1445
+
1446
+ // Initialize database and start server
1447
+ async function startServer() {
1448
+ try {
1449
+ // Initialize authentication database
1450
+ await initializeDatabase();
1451
+
1452
+ // Configure Web Push (VAPID keys)
1453
+ configureWebPush();
1454
+
1455
+ // Check if running in production mode (dist folder exists)
1456
+ const distIndexPath = path.join(APP_ROOT, 'dist', 'index.html');
1457
+ const isProduction = fs.existsSync(distIndexPath);
1458
+
1459
+ // Log Claude implementation mode
1460
+ console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
1461
+ console.log('');
1462
+
1463
+ if (isProduction) {
1464
+ console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}`);
1465
+ }
1466
+
1467
+ console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`);
1468
+
1469
+ server.listen(SERVER_PORT, HOST, async () => {
1470
+ const appInstallPath = APP_ROOT;
1471
+
1472
+ console.log('');
1473
+ console.log(c.dim('═'.repeat(63)));
1474
+ console.log(` ${c.bright('CloudCLI Server - Ready')}`);
1475
+ console.log(c.dim('═'.repeat(63)));
1476
+ console.log('');
1477
+ console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
1478
+ console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
1479
+ console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
1480
+ console.log('');
1481
+
1482
+ // Start watching the projects folder for changes
1483
+ await initializeSessionsWatcher();
1484
+
1485
+ // Start server-side plugin processes for enabled plugins
1486
+ startEnabledPluginServers().catch(err => {
1487
+ console.error('[Plugins] Error during startup:', err.message);
1488
+ });
1489
+ });
1490
+
1491
+ await closeSessionsWatcher();
1492
+ // Clean up plugin processes on shutdown
1493
+ const shutdownPlugins = async () => {
1494
+ await stopAllPlugins();
1495
+ process.exit(0);
1496
+ };
1497
+ process.on('SIGTERM', () => void shutdownPlugins());
1498
+ process.on('SIGINT', () => void shutdownPlugins());
1499
+ } catch (error) {
1500
+ console.error('[ERROR] Failed to start server:', error);
1501
+ process.exit(1);
1502
+ }
1503
+ }
1504
+
1505
+ startServer();