upfynai-code 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/client/dist/assets/AppContent-DTZ2FbvM.js +513 -0
  2. package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +6 -0
  3. package/client/dist/assets/LoginModal-CWoFm0au.js +19 -0
  4. package/client/dist/assets/MarkdownPreview-CYdvwJaV.js +1 -0
  5. package/client/dist/assets/{Onboarding-Coxo6mFA.js → Onboarding-CtIoXiTp.js} +1 -1
  6. package/client/dist/assets/{SetupForm-BzYOsbji.js → SetupForm-B4p8im5O.js} +1 -1
  7. package/client/dist/assets/{ar-SA-G6X2FPQ2-Bmw2-hDt.js → ar-SA-G6X2FPQ2-2gfmdvHk.js} +1 -1
  8. package/client/dist/assets/{arc-BMqY7_Ci.js → arc-DCZSHhoJ.js} +1 -1
  9. package/client/dist/assets/{az-AZ-76LH7QW2-Dh1le_qs.js → az-AZ-76LH7QW2-CDdeucRZ.js} +1 -1
  10. package/client/dist/assets/{bg-BG-XCXSNQG7-Cbav8Z9z.js → bg-BG-XCXSNQG7-D6__XtOK.js} +1 -1
  11. package/client/dist/assets/{blockDiagram-38ab4fdb-ChHJxsXw.js → blockDiagram-38ab4fdb-Cfbaeyp6.js} +3 -3
  12. package/client/dist/assets/{bn-BD-2XOGV67Q-DCNjOaWz.js → bn-BD-2XOGV67Q-DHNJw3OG.js} +1 -1
  13. package/client/dist/assets/{c4Diagram-3d4e48cf-b8Xue4Z6.js → c4Diagram-3d4e48cf-BBCnjOTy.js} +1 -1
  14. package/client/dist/assets/{ca-ES-6MX7JW3Y-Dl_vM7NS.js → ca-ES-6MX7JW3Y-r5g4o3zQ.js} +1 -1
  15. package/client/dist/assets/channel-O3ovC0x9.js +1 -0
  16. package/client/dist/assets/{classDiagram-70f12bd4-BheP7Ggo.js → classDiagram-70f12bd4-D0lhAcxU.js} +1 -1
  17. package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +2 -0
  18. package/client/dist/assets/clone-BG9u7vLi.js +1 -0
  19. package/client/dist/assets/{createText-2e5e7dd3-_n4jI_fO.js → createText-2e5e7dd3-B8jCDmF_.js} +1 -1
  20. package/client/dist/assets/{cs-CZ-2BRQDIVT-ftsKDdz4.js → cs-CZ-2BRQDIVT-p08jRLRC.js} +1 -1
  21. package/client/dist/assets/{da-DK-5WZEPLOC-DAjdwGRO.js → da-DK-5WZEPLOC-CnhOImFf.js} +1 -1
  22. package/client/dist/assets/{de-DE-XR44H4JA-BJXczHGT.js → de-DE-XR44H4JA-BunSXZ-Y.js} +1 -1
  23. package/client/dist/assets/{edges-e0da2a9e-CfPZr4YM.js → edges-e0da2a9e-CGBBhG8k.js} +2 -2
  24. package/client/dist/assets/{el-GR-BZB4AONW-DW2p_uy7.js → el-GR-BZB4AONW-D4wv1oIz.js} +1 -1
  25. package/client/dist/assets/{erDiagram-9861fffd-CF33V-Of.js → erDiagram-9861fffd-CYaF3q1I.js} +1 -1
  26. package/client/dist/assets/{es-ES-U4NZUMDT-DLOIGnrl.js → es-ES-U4NZUMDT-CGeTKXgd.js} +1 -1
  27. package/client/dist/assets/{eu-ES-A7QVB2H4-LJXbf89m.js → eu-ES-A7QVB2H4-Cayx1TxR.js} +1 -1
  28. package/client/dist/assets/{fa-IR-HGAKTJCU-Dvx65fgW.js → fa-IR-HGAKTJCU-CmUg8pmw.js} +1 -1
  29. package/client/dist/assets/{fi-FI-Z5N7JZ37-EoL65BQh.js → fi-FI-Z5N7JZ37-xvHcPhsU.js} +1 -1
  30. package/client/dist/assets/{flowDb-956e92f1-HgoXVy2H.js → flowDb-956e92f1-C-_LFz70.js} +3 -3
  31. package/client/dist/assets/flowDiagram-66a62f08-C1sHdSjn.js +4 -0
  32. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +1 -0
  33. package/client/dist/assets/{flowchart-elk-definition-4a651766-DJbI2dpv.js → flowchart-elk-definition-4a651766-CNGfpudb.js} +7 -7
  34. package/client/dist/assets/{fr-FR-RHASNOE6-DNk_jdDs.js → fr-FR-RHASNOE6-DBoHEcNj.js} +1 -1
  35. package/client/dist/assets/{ganttDiagram-c361ad54-2XX670FU.js → ganttDiagram-c361ad54-B8HJQqjt.js} +1 -1
  36. package/client/dist/assets/{gitGraphDiagram-72cf32ee-CcUfruAo.js → gitGraphDiagram-72cf32ee-DojCDvlS.js} +1 -1
  37. package/client/dist/assets/{gl-ES-HMX3MZ6V-dxzFjZlG.js → gl-ES-HMX3MZ6V-p6hrn2cN.js} +1 -1
  38. package/client/dist/assets/{graph-BSbiMSBC.js → graph-DXM7lcy1.js} +1 -1
  39. package/client/dist/assets/{he-IL-6SHJWFNN-Cogsfdt1.js → he-IL-6SHJWFNN-y2jEX6-0.js} +1 -1
  40. package/client/dist/assets/{hi-IN-IWLTKZ5I-L6wbgi4F.js → hi-IN-IWLTKZ5I-99pNfyWr.js} +1 -1
  41. package/client/dist/assets/{hu-HU-A5ZG7DT2-DSA6ZDsH.js → hu-HU-A5ZG7DT2-hygceGMS.js} +1 -1
  42. package/client/dist/assets/{id-ID-SAP4L64H-BK_vGGS6.js → id-ID-SAP4L64H-CyIqi1hv.js} +1 -1
  43. package/client/dist/assets/{image-blob-reduce.esm-BLtmMM_J.js → image-blob-reduce.esm-D6s-rqMO.js} +6 -1
  44. package/client/dist/assets/{index-3862675e-Bv32HUgT.js → index-3862675e-4idOQN2N.js} +1 -1
  45. package/client/dist/assets/{index-BPwf8Fw3.js → index-BGmwbRlb.js} +6 -6
  46. package/client/dist/assets/index-BHZfFT_V.js +97 -0
  47. package/client/dist/assets/{infoDiagram-f8f76790-w4mR4pxn.js → infoDiagram-f8f76790-CFLrHqtc.js} +1 -1
  48. package/client/dist/assets/{it-IT-JPQ66NNP-BLdHYMhn.js → it-IT-JPQ66NNP-DzVvVdQI.js} +1 -1
  49. package/client/dist/assets/{ja-JP-DBVTYXUO-B_vmexl_.js → ja-JP-DBVTYXUO-BI4fPexV.js} +1 -1
  50. package/client/dist/assets/{journeyDiagram-49397b02-D9nmO17e.js → journeyDiagram-49397b02-C3CFDo8z.js} +1 -1
  51. package/client/dist/assets/{kaa-6HZHGXH3-5s-3jl6F.js → kaa-6HZHGXH3-fwOleoQB.js} +1 -1
  52. package/client/dist/assets/{kab-KAB-ZGHBKWFO-2QaVDuSf.js → kab-KAB-ZGHBKWFO-DBI_ri48.js} +1 -1
  53. package/client/dist/assets/{kk-KZ-P5N5QNE5-CTC52Vbi.js → kk-KZ-P5N5QNE5-zpl7uvyF.js} +1 -1
  54. package/client/dist/assets/{km-KH-HSX4SM5Z-DxawH8UZ.js → km-KH-HSX4SM5Z-DOMFSres.js} +1 -1
  55. package/client/dist/assets/{ko-KR-MTYHY66A-CmosEM8_.js → ko-KR-MTYHY66A-tb08hXzd.js} +1 -1
  56. package/client/dist/assets/{ku-TR-6OUDTVRD-DbiLen4y.js → ku-TR-6OUDTVRD-DlIQCCY4.js} +1 -1
  57. package/client/dist/assets/{layout-jmt3H9tA.js → layout-B_11mCXA.js} +1 -1
  58. package/client/dist/assets/{line-JTlRayUJ.js → line-B-qmK_vI.js} +1 -1
  59. package/client/dist/assets/{linear-DJeB5p7x.js → linear-Ph6uuYcX.js} +1 -1
  60. package/client/dist/assets/{lt-LT-XHIRWOB4-CH15wrjA.js → lt-LT-XHIRWOB4--qWy24_Z.js} +1 -1
  61. package/client/dist/assets/{lv-LV-5QDEKY6T-dhgfPuCQ.js → lv-LV-5QDEKY6T-Bnd_1GDb.js} +1 -1
  62. package/client/dist/assets/mindmap-definition-fc14e90a-Do79tIc0.js +425 -0
  63. package/client/dist/assets/{mr-IN-CRQNXWMA-3Gi6iq7A.js → mr-IN-CRQNXWMA-BsV6HaD9.js} +1 -1
  64. package/client/dist/assets/{my-MM-5M5IBNSE-CpH4rdJj.js → my-MM-5M5IBNSE-kZQURVIi.js} +1 -1
  65. package/client/dist/assets/{nb-NO-T6EIAALU-Du6iiGql.js → nb-NO-T6EIAALU-Cvf9FdSF.js} +1 -1
  66. package/client/dist/assets/{nl-NL-IS3SIHDZ-BGvsd1MT.js → nl-NL-IS3SIHDZ-DA1yqpXw.js} +1 -1
  67. package/client/dist/assets/{nn-NO-6E72VCQL-B-odvJZW.js → nn-NO-6E72VCQL-89lm3vku.js} +1 -1
  68. package/client/dist/assets/{oc-FR-POXYY2M6-COC8xNjo.js → oc-FR-POXYY2M6-BsrjTJQh.js} +1 -1
  69. package/client/dist/assets/{pa-IN-N4M65BXN-CE21PUQH.js → pa-IN-N4M65BXN-CczefYaj.js} +1 -1
  70. package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
  71. package/client/dist/assets/percentages-BXMCSKIN-Be6p9phi.js +207 -0
  72. package/client/dist/assets/pica-CQIY57Tf.js +7 -0
  73. package/client/dist/assets/{pieDiagram-8a3498a8-Cvfh7Qr5.js → pieDiagram-8a3498a8-CfblQHdm.js} +2 -2
  74. package/client/dist/assets/{pl-PL-T2D74RX3-D4xFVSoT.js → pl-PL-T2D74RX3-DdhH-zcK.js} +1 -1
  75. package/client/dist/assets/{pt-BR-5N22H2LF-CCq257gA.js → pt-BR-5N22H2LF-gpwlheL6.js} +1 -1
  76. package/client/dist/assets/{pt-PT-UZXXM6DQ-1l8gt5vA.js → pt-PT-UZXXM6DQ-Cs87vICi.js} +1 -1
  77. package/client/dist/assets/{quadrantDiagram-120e2f19-BA0js1aD.js → quadrantDiagram-120e2f19-CRMSamSP.js} +1 -1
  78. package/client/dist/assets/{requirementDiagram-deff3bca-B0QNFfIn.js → requirementDiagram-deff3bca-D3LBN016.js} +1 -1
  79. package/client/dist/assets/{ro-RO-JPDTUUEW-yosBW01E.js → ro-RO-JPDTUUEW-CWTSJ1Dt.js} +1 -1
  80. package/client/dist/assets/roundRect-0PYZxl1G.js +1 -0
  81. package/client/dist/assets/{ru-RU-B4JR7IUQ-8LkEJUix.js → ru-RU-B4JR7IUQ-Bq7aN2ep.js} +1 -1
  82. package/client/dist/assets/{sankeyDiagram-04a897e0-D4T9eCXn.js → sankeyDiagram-04a897e0-CsFqOQZN.js} +3 -3
  83. package/client/dist/assets/{sequenceDiagram-704730f1-CfBUTCrO.js → sequenceDiagram-704730f1-BRYXVDGX.js} +1 -1
  84. package/client/dist/assets/{si-LK-N5RQ5JYF-D8rjbqtd.js → si-LK-N5RQ5JYF-BBjcNYQh.js} +1 -1
  85. package/client/dist/assets/{sk-SK-C5VTKIMK-Bg14sAzN.js → sk-SK-C5VTKIMK-ByjKQzUb.js} +1 -1
  86. package/client/dist/assets/{sl-SI-NN7IZMDC-CMTib6Zs.js → sl-SI-NN7IZMDC-B8WCyMBU.js} +1 -1
  87. package/client/dist/assets/{stateDiagram-587899a1-BGgvmVSZ.js → stateDiagram-587899a1-BHoy9LtD.js} +1 -1
  88. package/client/dist/assets/{stateDiagram-v2-d93cdb3a-Qn3DpYuO.js → stateDiagram-v2-d93cdb3a-BvMUA6bS.js} +1 -1
  89. package/client/dist/assets/{styles-6aaf32cf-IdVZLPrD.js → styles-6aaf32cf-Dr-lfIOW.js} +2 -2
  90. package/client/dist/assets/{styles-9a916d00-BAC3L45X.js → styles-9a916d00-DS4wRpL7.js} +1 -1
  91. package/client/dist/assets/{styles-c10674c1-COhXxX8c.js → styles-c10674c1-nKRF6NrH.js} +1 -1
  92. package/client/dist/assets/{subset-shared.chunk-BWHnFai4.js → subset-shared.chunk-KT79s7KG.js} +64 -2
  93. package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +1 -0
  94. package/client/dist/assets/{sv-SE-XGPEYMSR-C1425rOF.js → sv-SE-XGPEYMSR-BiIPUVbv.js} +1 -1
  95. package/client/dist/assets/{svgDrawCommon-08f97a94-Cfk-fgnN.js → svgDrawCommon-08f97a94-C3uP9PYr.js} +1 -1
  96. package/client/dist/assets/{ta-IN-2NMHFXQM-BHHo1zpF.js → ta-IN-2NMHFXQM-Cidadso2.js} +1 -1
  97. package/client/dist/assets/{th-TH-HPSO5L25-CZVzm_WT.js → th-TH-HPSO5L25-CFNnJwSv.js} +1 -1
  98. package/client/dist/assets/{timeline-definition-85554ec2-VAvuJith.js → timeline-definition-85554ec2-BSsLsIgF.js} +1 -1
  99. package/client/dist/assets/{tr-TR-DEFEU3FU-DE1lclCq.js → tr-TR-DEFEU3FU-DaFcI-KL.js} +1 -1
  100. package/client/dist/assets/{uk-UA-QMV73CPH-D4lJZ85O.js → uk-UA-QMV73CPH-DkBW36St.js} +1 -1
  101. package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +20 -0
  102. package/client/dist/assets/vendor-codemirror-rix45NST.js +16 -0
  103. package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
  104. package/client/dist/assets/vendor-icons-Dh9m_Ydt.js +596 -0
  105. package/client/dist/assets/{vendor-markdown-CIVH08vJ.js → vendor-markdown-BXEi_H3G.js} +3 -3
  106. package/client/dist/assets/vendor-react-9mUTKBHH.js +67 -0
  107. package/client/dist/assets/{vendor-syntax-Djb62v3a.js → vendor-syntax-DnmwQQJF.js} +14 -7
  108. package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
  109. package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
  110. package/client/dist/assets/{vi-VN-M7AON7JQ-Dgc_SShk.js → vi-VN-M7AON7JQ-KrtfxOzl.js} +1 -1
  111. package/client/dist/assets/{xychartDiagram-e933f94c-BeyVBJhb.js → xychartDiagram-e933f94c-CgNgZ4pp.js} +1 -1
  112. package/client/dist/assets/{zh-CN-LNUGB5OW-MH4Yh8in.js → zh-CN-LNUGB5OW-BQu12RoD.js} +1 -1
  113. package/client/dist/assets/{zh-HK-E62DVLB3-D4XHehjx.js → zh-HK-E62DVLB3-zx9CvERq.js} +1 -1
  114. package/client/dist/assets/{zh-TW-RAJ6MFWO--efj3evj.js → zh-TW-RAJ6MFWO-ffJWgVxn.js} +1 -1
  115. package/client/dist/index.html +128 -66
  116. package/client/dist/manifest.json +61 -15
  117. package/client/dist/sw.js +19 -55
  118. package/commands/upfynai-connect.md +31 -18
  119. package/commands/upfynai.md +45 -26
  120. package/package.json +1 -1
  121. package/server/cli-ui.js +320 -169
  122. package/server/cli.js +255 -23
  123. package/server/constants/config.js +29 -3
  124. package/server/database/auth.db +0 -0
  125. package/server/index.js +380 -121
  126. package/server/mcp-server.js +2 -1
  127. package/server/middleware/auth.js +18 -8
  128. package/server/openrouter.js +137 -0
  129. package/server/relay-client.js +262 -18
  130. package/server/routes/agent.js +54 -19
  131. package/server/routes/auth.js +23 -12
  132. package/server/routes/commands.js +1 -1
  133. package/server/routes/settings.js +91 -0
  134. package/shared/modelConstants.js +29 -0
  135. package/client/dist/assets/AppContent-CTSHQdyq.js +0 -513
  136. package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +0 -6
  137. package/client/dist/assets/LoginModal-silya-zP.js +0 -11
  138. package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +0 -1
  139. package/client/dist/assets/channel-CSnvHe_M.js +0 -1
  140. package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +0 -2
  141. package/client/dist/assets/clone-B75abXxS.js +0 -1
  142. package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +0 -4
  143. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +0 -1
  144. package/client/dist/assets/index-BnXuHrpJ.js +0 -523
  145. package/client/dist/assets/index-BwxNox94.css +0 -1
  146. package/client/dist/assets/index-D1urGMYu.js +0 -95
  147. package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +0 -415
  148. package/client/dist/assets/pdf-TYrZqVzP.js +0 -12
  149. package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +0 -199
  150. package/client/dist/assets/pica-VkdyTzi8.js +0 -2
  151. package/client/dist/assets/roundRect-mAH3dD0p.js +0 -1
  152. package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +0 -1
  153. package/client/dist/assets/vendor-codemirror-BARtJV1V.js +0 -16
  154. package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +0 -20
  155. package/client/dist/assets/vendor-i18n-ByAl-gdx.js +0 -1
  156. package/client/dist/assets/vendor-icons-D33IkSIf.js +0 -1
  157. package/client/dist/assets/vendor-react-CHoMc7ka.js +0 -8
  158. package/client/dist/assets/vendor-xterm-DBb3RXlu.js +0 -66
  159. package/client/dist/assets/vendor-xterm-DrlLKa8f.css +0 -1
  160. package/client/dist/llms.txt +0 -40
  161. package/client/dist/robots.txt +0 -11
  162. package/client/dist/sitemap.xml +0 -45
package/server/index.js CHANGED
@@ -54,6 +54,7 @@ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSess
54
54
  import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
55
55
  import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
56
56
  import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
57
+ import { queryOpenRouter, OPENROUTER_MODELS } from './openrouter.js';
57
58
  import { createMcpServer, mountMcpServer } from './mcp-server.js';
58
59
  import gitRoutes from './routes/git.js';
59
60
  import authRoutes from './routes/auth.js';
@@ -69,9 +70,10 @@ import cliAuthRoutes from './routes/cli-auth.js';
69
70
  import userRoutes from './routes/user.js';
70
71
  import codexRoutes from './routes/codex.js';
71
72
  import paymentRoutes from './routes/payments.js';
72
- import { initializeDatabase, relayTokensDb, subscriptionDb } from './database/db.js';
73
+ import { initializeDatabase, relayTokensDb, subscriptionDb, credentialsDb, userDb } from './database/db.js';
73
74
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
74
- import { IS_PLATFORM } from './constants/config.js';
75
+ import { IS_PLATFORM, IS_LOCAL } from './constants/config.js';
76
+ import { execSync } from 'child_process';
75
77
 
76
78
  // File system watchers for provider project/session folders
77
79
  const PROVIDER_WATCH_PATHS = [
@@ -348,6 +350,18 @@ if (server) {
348
350
  // Make WebSocket server available to routes
349
351
  app.locals.wss = wss;
350
352
 
353
+ // Security headers — protect against common web attacks
354
+ app.use((req, res, next) => {
355
+ res.setHeader('X-Content-Type-Options', 'nosniff');
356
+ res.setHeader('X-Frame-Options', 'DENY');
357
+ res.setHeader('X-XSS-Protection', '1; mode=block');
358
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
359
+ if (process.env.NODE_ENV === 'production') {
360
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
361
+ }
362
+ next();
363
+ });
364
+
351
365
  // CORS: require explicit CORS_ORIGINS in production, restrict to same-origin otherwise
352
366
  const CORS_ORIGINS = process.env.CORS_ORIGINS
353
367
  ? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
@@ -367,6 +381,44 @@ app.use(express.json({
367
381
  }));
368
382
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
369
383
 
384
+ // Rate limiting for auth endpoints — prevent brute force attacks
385
+ const authRateLimitMap = new Map();
386
+ const AUTH_RATE_WINDOW = 15 * 60 * 1000; // 15 minutes
387
+ const AUTH_RATE_MAX = 10; // max attempts per window
388
+
389
+ function authRateLimit(req, res, next) {
390
+ const key = req.ip || req.connection.remoteAddress || 'unknown';
391
+ const now = Date.now();
392
+ const entry = authRateLimitMap.get(key);
393
+
394
+ if (entry) {
395
+ // Clean expired entries
396
+ if (now - entry.windowStart > AUTH_RATE_WINDOW) {
397
+ authRateLimitMap.set(key, { windowStart: now, count: 1 });
398
+ return next();
399
+ }
400
+ entry.count++;
401
+ if (entry.count > AUTH_RATE_MAX) {
402
+ return res.status(429).json({ error: 'Too many attempts. Please try again later.' });
403
+ }
404
+ } else {
405
+ authRateLimitMap.set(key, { windowStart: now, count: 1 });
406
+ }
407
+ next();
408
+ }
409
+
410
+ // Clean up stale rate limit entries every 30 minutes
411
+ setInterval(() => {
412
+ const now = Date.now();
413
+ for (const [key, entry] of authRateLimitMap) {
414
+ if (now - entry.windowStart > AUTH_RATE_WINDOW) {
415
+ authRateLimitMap.delete(key);
416
+ }
417
+ }
418
+ }, 30 * 60 * 1000);
419
+
420
+ app.locals.authRateLimit = authRateLimit;
421
+
370
422
  // Vercel serverless: lazy DB initialization on first request
371
423
  let dbInitialized = false;
372
424
  if (process.env.VERCEL) {
@@ -488,12 +540,70 @@ app.get('/api/relay/status', authenticateToken, (req, res) => {
488
540
  });
489
541
  });
490
542
 
543
+ /**
544
+ * Detect installed AI CLI agents on the local machine (server-side).
545
+ * Used in self-hosted/local mode where no relay is needed.
546
+ */
547
+ let cachedLocalAgents = null;
548
+ let localAgentsCacheTime = 0;
549
+ function detectLocalAgents() {
550
+ // Cache for 60 seconds
551
+ if (cachedLocalAgents && Date.now() - localAgentsCacheTime < 60000) {
552
+ return cachedLocalAgents;
553
+ }
554
+ const isWindows = process.platform === 'win32';
555
+ const whichCmd = isWindows ? 'where' : 'which';
556
+ const agents = [
557
+ { name: 'claude', binary: 'claude', label: 'Claude Code' },
558
+ { name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
559
+ { name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
560
+ ];
561
+ const detected = {};
562
+ for (const agent of agents) {
563
+ try {
564
+ const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
565
+ detected[agent.name] = { installed: true, path: result.split('\n')[0].trim(), label: agent.label };
566
+ } catch {
567
+ detected[agent.name] = { installed: false, label: agent.label };
568
+ }
569
+ }
570
+ cachedLocalAgents = detected;
571
+ localAgentsCacheTime = Date.now();
572
+ return detected;
573
+ }
574
+
491
575
  // Connection status — alias at path the frontend expects
492
576
  app.get('/api/auth/connection-status', authenticateToken, (req, res) => {
493
577
  const relay = relayConnections.get(Number(req.user.id));
578
+ const connected = !!(relay && relay.ws.readyState === 1);
579
+
580
+ // In local mode, always "connected" — SDK runs directly on this machine
581
+ if (IS_LOCAL) {
582
+ const agents = detectLocalAgents();
583
+ return res.json({
584
+ connected: true,
585
+ local: true,
586
+ connectedAt: Date.now(),
587
+ agents,
588
+ machine: {
589
+ hostname: os.hostname(),
590
+ platform: process.platform,
591
+ cwd: process.cwd(),
592
+ }
593
+ });
594
+ }
595
+
494
596
  res.json({
495
- connected: !!(relay && relay.ws.readyState === 1),
496
- connectedAt: relay?.connectedAt || null
597
+ connected,
598
+ local: false,
599
+ connectedAt: relay?.connectedAt || null,
600
+ agents: connected ? (relay.agents || null) : null,
601
+ machine: connected ? {
602
+ hostname: relay.machine,
603
+ platform: relay.platform,
604
+ cwd: relay.cwd,
605
+ version: relay.version,
606
+ } : null
497
607
  });
498
608
  });
499
609
 
@@ -520,77 +630,15 @@ app.use(express.static(path.join(__dirname, '../client/dist'), {
520
630
  // /api/config endpoint removed - no longer needed
521
631
  // Frontend now uses window.location for WebSocket URLs
522
632
 
523
- // System update endpoint
524
- app.post('/api/system/update', authenticateToken, async (req, res) => {
525
- try {
526
- // Get the project root directory (parent of server directory)
527
- const projectRoot = path.join(__dirname, '..');
528
-
529
- console.log('Starting system update from directory:', projectRoot);
530
-
531
- // Run the update command
532
- const updateCommand = 'git checkout main && git pull && npm install';
533
-
534
- const child = spawn('sh', ['-c', updateCommand], {
535
- cwd: projectRoot,
536
- env: process.env
537
- });
538
-
539
- let output = '';
540
- let errorOutput = '';
541
-
542
- child.stdout.on('data', (data) => {
543
- const text = data.toString();
544
- output += text;
545
- console.log('Update output:', text);
546
- });
547
-
548
- child.stderr.on('data', (data) => {
549
- const text = data.toString();
550
- errorOutput += text;
551
- console.error('Update error:', text);
552
- });
553
-
554
- child.on('close', (code) => {
555
- if (code === 0) {
556
- res.json({
557
- success: true,
558
- output: output || 'Update completed successfully',
559
- message: 'Update completed. Please restart the server to apply changes.'
560
- });
561
- } else {
562
- res.status(500).json({
563
- success: false,
564
- error: 'Update command failed',
565
- output: output,
566
- errorOutput: errorOutput
567
- });
568
- }
569
- });
570
-
571
- child.on('error', (error) => {
572
- console.error('Update process error:', error);
573
- res.status(500).json({
574
- success: false,
575
- error: error.message
576
- });
577
- });
578
-
579
- } catch (error) {
580
- console.error('System update error:', error);
581
- res.status(500).json({
582
- success: false,
583
- error: error.message
584
- });
585
- }
586
- });
633
+ // System update endpoint — REMOVED for security (shell command execution risk)
634
+ // Use `uc update` from CLI instead
587
635
 
588
636
  app.get('/api/projects', authenticateToken, async (req, res) => {
589
637
  try {
590
638
  const projects = await getProjects(broadcastProgress);
591
639
  res.json(projects);
592
640
  } catch (error) {
593
- res.status(500).json({ error: error.message });
641
+ res.status(500).json({ error: 'Internal server error' });
594
642
  }
595
643
  });
596
644
 
@@ -600,7 +648,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
600
648
  const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
601
649
  res.json(result);
602
650
  } catch (error) {
603
- res.status(500).json({ error: error.message });
651
+ res.status(500).json({ error: 'Internal server error' });
604
652
  }
605
653
  });
606
654
 
@@ -625,7 +673,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateT
625
673
  res.json(result);
626
674
  }
627
675
  } catch (error) {
628
- res.status(500).json({ error: error.message });
676
+ res.status(500).json({ error: 'Internal server error' });
629
677
  }
630
678
  });
631
679
 
@@ -636,7 +684,7 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
636
684
  await renameProject(req.params.projectName, displayName);
637
685
  res.json({ success: true });
638
686
  } catch (error) {
639
- res.status(500).json({ error: error.message });
687
+ res.status(500).json({ error: 'Internal server error' });
640
688
  }
641
689
  });
642
690
 
@@ -650,7 +698,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
650
698
  res.json({ success: true });
651
699
  } catch (error) {
652
700
  // session delete error
653
- res.status(500).json({ error: error.message });
701
+ res.status(500).json({ error: 'Internal server error' });
654
702
  }
655
703
  });
656
704
 
@@ -662,7 +710,7 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) =>
662
710
  await deleteProject(projectName, force);
663
711
  res.json({ success: true });
664
712
  } catch (error) {
665
- res.status(500).json({ error: error.message });
713
+ res.status(500).json({ error: 'Internal server error' });
666
714
  }
667
715
  });
668
716
 
@@ -679,7 +727,7 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => {
679
727
  res.json({ success: true, project });
680
728
  } catch (error) {
681
729
  console.error('Error creating project:', error);
682
- res.status(500).json({ error: error.message });
730
+ res.status(500).json({ error: 'Internal server error' });
683
731
  }
684
732
  });
685
733
 
@@ -832,13 +880,11 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
832
880
  return res.status(404).json({ error: 'Project not found' });
833
881
  }
834
882
 
835
- // Handle both absolute and relative paths
836
- const resolved = path.isAbsolute(filePath)
837
- ? path.resolve(filePath)
838
- : path.resolve(projectRoot, filePath);
883
+ // Always resolve relative to project root to prevent path traversal
884
+ const resolved = path.resolve(projectRoot, filePath);
839
885
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
840
- if (!resolved.startsWith(normalizedRoot)) {
841
- return res.status(403).json({ error: 'Path must be under project root' });
886
+ if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
887
+ return res.status(403).json({ error: 'Access denied' });
842
888
  }
843
889
 
844
890
  const content = await fsPromises.readFile(resolved, 'utf8');
@@ -850,7 +896,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
850
896
  } else if (error.code === 'EACCES') {
851
897
  res.status(403).json({ error: 'Permission denied' });
852
898
  } else {
853
- res.status(500).json({ error: error.message });
899
+ res.status(500).json({ error: 'Internal server error' });
854
900
  }
855
901
  }
856
902
  });
@@ -872,10 +918,11 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
872
918
  return res.status(404).json({ error: 'Project not found' });
873
919
  }
874
920
 
875
- const resolved = path.resolve(filePath);
921
+ // Resolve path relative to project root to prevent path traversal
922
+ const resolved = path.resolve(projectRoot, filePath);
876
923
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
877
- if (!resolved.startsWith(normalizedRoot)) {
878
- return res.status(403).json({ error: 'Path must be under project root' });
924
+ if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
925
+ return res.status(403).json({ error: 'Access denied' });
879
926
  }
880
927
 
881
928
  // Check if file exists
@@ -903,7 +950,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
903
950
  } catch (error) {
904
951
  console.error('Error serving binary file:', error);
905
952
  if (!res.headersSent) {
906
- res.status(500).json({ error: error.message });
953
+ res.status(500).json({ error: 'Internal server error' });
907
954
  }
908
955
  }
909
956
  });
@@ -929,13 +976,11 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
929
976
  return res.status(404).json({ error: 'Project not found' });
930
977
  }
931
978
 
932
- // Handle both absolute and relative paths
933
- const resolved = path.isAbsolute(filePath)
934
- ? path.resolve(filePath)
935
- : path.resolve(projectRoot, filePath);
979
+ // Always resolve relative to project root to prevent path traversal
980
+ const resolved = path.resolve(projectRoot, filePath);
936
981
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
937
- if (!resolved.startsWith(normalizedRoot)) {
938
- return res.status(403).json({ error: 'Path must be under project root' });
982
+ if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
983
+ return res.status(403).json({ error: 'Access denied' });
939
984
  }
940
985
 
941
986
  // Write the new content
@@ -953,7 +998,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
953
998
  } else if (error.code === 'EACCES') {
954
999
  res.status(403).json({ error: 'Permission denied' });
955
1000
  } else {
956
- res.status(500).json({ error: error.message });
1001
+ res.status(500).json({ error: 'Internal server error' });
957
1002
  }
958
1003
  }
959
1004
  });
@@ -985,7 +1030,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
985
1030
  res.json(files);
986
1031
  } catch (error) {
987
1032
  console.error('[ERROR] File tree error:', error.message);
988
- res.status(500).json({ error: error.message });
1033
+ res.status(500).json({ error: 'Internal server error' });
989
1034
  }
990
1035
  });
991
1036
 
@@ -1001,9 +1046,9 @@ if (wss) wss.on('connection', (ws, request) => {
1001
1046
  if (pathname === '/shell') {
1002
1047
  handleShellConnection(ws);
1003
1048
  } else if (pathname === '/ws') {
1004
- handleChatConnection(ws);
1049
+ handleChatConnection(ws, request);
1005
1050
  } else if (pathname === '/relay') {
1006
- handleRelayConnection(ws, urlObj.searchParams.get('token'));
1051
+ handleRelayConnection(ws, urlObj.searchParams.get('token'), request);
1007
1052
  } else {
1008
1053
  // unknown WebSocket path
1009
1054
  ws.close();
@@ -1036,9 +1081,44 @@ class WebSocketWriter {
1036
1081
  }
1037
1082
  }
1038
1083
 
1084
+ /**
1085
+ * Look up a user's stored API key for a given provider.
1086
+ * Falls back to server env vars if user has none stored.
1087
+ * @param {number} userId
1088
+ * @param {string} providerType - e.g. 'anthropic_key', 'openai_key', 'openrouter_key', 'google_key'
1089
+ * @returns {Promise<string|null>}
1090
+ */
1091
+ async function getUserProviderKey(userId, providerType) {
1092
+ if (!userId) return null;
1093
+ try {
1094
+ const creds = await credentialsDb.getCredentials(userId, providerType);
1095
+ const active = creds.find(c => c.is_active);
1096
+ return active?.credential_value || null;
1097
+ } catch { return null; }
1098
+ }
1099
+
1100
+ /**
1101
+ * Temporarily set environment variable for an AI SDK call, then restore.
1102
+ * @param {string} envKey - e.g. 'ANTHROPIC_API_KEY'
1103
+ * @param {string|null} userKey - user's BYOK key, null to skip
1104
+ * @param {Function} fn - async function to execute with the key set
1105
+ */
1106
+ async function withUserApiKey(envKey, userKey, fn) {
1107
+ if (!userKey) return fn();
1108
+ const prev = process.env[envKey];
1109
+ process.env[envKey] = userKey;
1110
+ try {
1111
+ return await fn();
1112
+ } finally {
1113
+ if (prev !== undefined) process.env[envKey] = prev;
1114
+ else delete process.env[envKey];
1115
+ }
1116
+ }
1117
+
1039
1118
  // Handle chat WebSocket connections
1040
- function handleChatConnection(ws) {
1119
+ function handleChatConnection(ws, request) {
1041
1120
  // chat WebSocket connected
1121
+ const wsUser = request?.user || null;
1042
1122
 
1043
1123
  // Add to connected clients for project updates
1044
1124
  connectedClients.add(ws);
@@ -1107,24 +1187,64 @@ function handleChatConnection(ws) {
1107
1187
  }
1108
1188
  if (sid) lockedSessionsForThisWs.add(sid);
1109
1189
 
1110
- console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1111
- console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1112
- // session message received
1113
-
1114
- // Use Claude Agents SDK
1115
- await queryClaudeSDK(data.command, data.options, writer);
1190
+ // Check if user has active relay → route to local machine
1191
+ if (hasActiveRelay(wsUser?.userId)) {
1192
+ await routeViaRelay(wsUser.userId, 'claude-query', data, writer, {
1193
+ response: 'claude-response',
1194
+ complete: 'claude-complete',
1195
+ error: 'claude-error'
1196
+ });
1197
+ } else {
1198
+ // Fall back to server-side SDK
1199
+ const userAnthropicKey = wsUser?.userId
1200
+ ? await getUserProviderKey(wsUser.userId, 'anthropic_key')
1201
+ : null;
1202
+
1203
+ await withUserApiKey('ANTHROPIC_API_KEY', userAnthropicKey, () =>
1204
+ queryClaudeSDK(data.command, data.options, writer)
1205
+ );
1206
+ }
1116
1207
  } else if (data.type === 'cursor-command') {
1117
- console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
1118
- console.log('📁 Project:', data.options?.cwd || 'Unknown');
1119
- // session message received
1120
- console.log('🤖 Model:', data.options?.model || 'default');
1121
- await spawnCursor(data.command, data.options, writer);
1208
+ // Check if user has active relay → route to local machine
1209
+ if (hasActiveRelay(wsUser?.userId)) {
1210
+ await routeViaRelay(wsUser.userId, 'cursor-query', data, writer, {
1211
+ response: 'cursor-response',
1212
+ complete: 'cursor-complete',
1213
+ error: 'cursor-error'
1214
+ });
1215
+ } else {
1216
+ await spawnCursor(data.command, data.options, writer);
1217
+ }
1122
1218
  } else if (data.type === 'codex-command') {
1123
- console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1124
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1125
- // session message received
1126
- console.log('🤖 Model:', data.options?.model || 'default');
1127
- await queryCodex(data.command, data.options, writer);
1219
+ // Check if user has active relay → route to local machine
1220
+ if (hasActiveRelay(wsUser?.userId)) {
1221
+ await routeViaRelay(wsUser.userId, 'codex-query', data, writer, {
1222
+ response: 'codex-response',
1223
+ complete: 'codex-complete',
1224
+ error: 'codex-error'
1225
+ });
1226
+ } else {
1227
+ const userOpenaiKey = wsUser?.userId
1228
+ ? await getUserProviderKey(wsUser.userId, 'openai_key')
1229
+ : null;
1230
+
1231
+ await withUserApiKey('OPENAI_API_KEY', userOpenaiKey, () =>
1232
+ queryCodex(data.command, data.options, writer)
1233
+ );
1234
+ }
1235
+ } else if (data.type === 'openrouter-command') {
1236
+ console.log('[DEBUG] OpenRouter message:', data.command?.slice(0, 60) || '[empty]');
1237
+ console.log('🤖 Model:', data.options?.model || OPENROUTER_MODELS.DEFAULT);
1238
+
1239
+ // BYOK: OpenRouter requires user's own API key
1240
+ const userOrKey = wsUser?.userId
1241
+ ? await getUserProviderKey(wsUser.userId, 'openrouter_key')
1242
+ : null;
1243
+
1244
+ await queryOpenRouter(data.command, {
1245
+ ...data.options,
1246
+ apiKey: userOrKey,
1247
+ }, writer);
1128
1248
  } else if (data.type === 'cursor-resume') {
1129
1249
  // Backward compatibility: treat as cursor-command with resume and no prompt
1130
1250
  // cursor resume session
@@ -1226,7 +1346,7 @@ function handleChatConnection(ws) {
1226
1346
  }
1227
1347
 
1228
1348
  // Handle relay WebSocket connections (local machine ↔ server bridge)
1229
- async function handleRelayConnection(ws, token) {
1349
+ async function handleRelayConnection(ws, token, request) {
1230
1350
  if (!token) {
1231
1351
  ws.send(JSON.stringify({ type: 'error', error: 'Relay token required. Use ?token=upfyn_xxx' }));
1232
1352
  ws.close();
@@ -1243,9 +1363,20 @@ async function handleRelayConnection(ws, token) {
1243
1363
  const userId = Number(tokenData.user_id);
1244
1364
  const username = tokenData.username;
1245
1365
 
1246
- // Store relay connection (use Number() to ensure consistent Map key type)
1247
- relayConnections.set(userId, { ws, user: tokenData, connectedAt: Date.now() });
1248
- // relay connection established
1366
+ // Extract optional headers from relay handshake
1367
+ const anthropicApiKey = request?.headers?.['x-anthropic-api-key'] || null;
1368
+ const relayVersion = request?.headers?.['x-upfyn-version'] || null;
1369
+ const relayMachine = request?.headers?.['x-upfyn-machine'] || null;
1370
+ const relayPlatform = request?.headers?.['x-upfyn-platform'] || null;
1371
+ const relayCwd = request?.headers?.['x-upfyn-cwd'] || null;
1372
+
1373
+ // Store relay connection with API key in memory only (use Number() for consistent Map key type)
1374
+ // API key is held per-user in the relay connection, NOT in process.env
1375
+ relayConnections.set(userId, {
1376
+ ws, user: tokenData, connectedAt: Date.now(), anthropicApiKey,
1377
+ version: relayVersion, machine: relayMachine, platform: relayPlatform, cwd: relayCwd,
1378
+ agents: null // populated when client sends agent-capabilities
1379
+ });
1249
1380
 
1250
1381
  ws.send(JSON.stringify({
1251
1382
  type: 'relay-connected',
@@ -1296,6 +1427,29 @@ async function handleRelayConnection(ws, token) {
1296
1427
  return;
1297
1428
  }
1298
1429
 
1430
+ // Agent capabilities report from relay client
1431
+ if (data.type === 'agent-capabilities') {
1432
+ const relay = relayConnections.get(userId);
1433
+ if (relay) {
1434
+ relay.agents = data.agents || {};
1435
+ relay.machine = data.machine || relay.machine;
1436
+ }
1437
+ // Broadcast agent info to browser clients
1438
+ for (const client of connectedClients) {
1439
+ try {
1440
+ if (client.readyState === 1) {
1441
+ client.send(JSON.stringify({
1442
+ type: 'relay-agents',
1443
+ userId,
1444
+ agents: data.agents || {},
1445
+ machine: data.machine || {}
1446
+ }));
1447
+ }
1448
+ } catch (e) { /* ignore */ }
1449
+ }
1450
+ return;
1451
+ }
1452
+
1299
1453
  // Heartbeat
1300
1454
  if (data.type === 'ping') {
1301
1455
  ws.send(JSON.stringify({ type: 'pong' }));
@@ -1346,7 +1500,7 @@ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs =
1346
1500
  return new Promise((resolve, reject) => {
1347
1501
  const relay = relayConnections.get(userId);
1348
1502
  if (!relay || relay.ws.readyState !== 1) {
1349
- reject(new Error('No relay connection. Run "upfynai-code connect" on your local machine.'));
1503
+ reject(new Error('No relay connection. Run "uc connect" on your local machine.'));
1350
1504
  return;
1351
1505
  }
1352
1506
 
@@ -1367,6 +1521,95 @@ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs =
1367
1521
  });
1368
1522
  }
1369
1523
 
1524
+ /**
1525
+ * Check if a user has an active relay connection
1526
+ */
1527
+ function hasActiveRelay(userId) {
1528
+ if (!userId) return false;
1529
+ const relay = relayConnections.get(Number(userId));
1530
+ return relay && relay.ws.readyState === 1;
1531
+ }
1532
+
1533
+ /**
1534
+ * Route a chat command through the user's relay connection to their local machine.
1535
+ * Translates relay-stream/relay-complete events into the format the frontend expects.
1536
+ *
1537
+ * @param {number} userId - User ID
1538
+ * @param {string} action - Relay action (claude-query, codex-query, cursor-query)
1539
+ * @param {object} data - Original command data from the browser
1540
+ * @param {object} writer - WebSocket writer to send events to browser
1541
+ * @param {object} eventMap - Maps relay stream data types to chat event types
1542
+ */
1543
+ async function routeViaRelay(userId, action, data, writer, eventMap = {}) {
1544
+ const sessionId = data.options?.sessionId || `relay-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1545
+
1546
+ // Send session-created so the frontend can track this query
1547
+ writer.send({ type: 'session-created', sessionId });
1548
+
1549
+ // Determine event types from the provider
1550
+ const responseType = eventMap.response || 'claude-response';
1551
+ const completeType = eventMap.complete || 'claude-complete';
1552
+ const errorType = eventMap.error || 'claude-error';
1553
+
1554
+ let fullContent = '';
1555
+
1556
+ try {
1557
+ const result = await sendRelayCommand(
1558
+ Number(userId),
1559
+ action,
1560
+ {
1561
+ command: data.command,
1562
+ options: data.options || {}
1563
+ },
1564
+ // onStream callback — translates relay events to chat events
1565
+ (streamData) => {
1566
+ if (streamData.type === 'claude-response' || streamData.type === 'codex-response' || streamData.type === 'cursor-response') {
1567
+ fullContent += streamData.content || '';
1568
+ writer.send({
1569
+ type: responseType,
1570
+ data: {
1571
+ type: 'assistant',
1572
+ message: {
1573
+ type: 'text',
1574
+ text: streamData.content || ''
1575
+ }
1576
+ },
1577
+ sessionId
1578
+ });
1579
+ } else if (streamData.type === 'claude-error' || streamData.type === 'codex-error' || streamData.type === 'cursor-error') {
1580
+ writer.send({
1581
+ type: responseType,
1582
+ data: {
1583
+ type: 'assistant',
1584
+ message: {
1585
+ type: 'text',
1586
+ text: streamData.content || ''
1587
+ }
1588
+ },
1589
+ sessionId
1590
+ });
1591
+ }
1592
+ },
1593
+ 600000 // 10 minute timeout for AI queries
1594
+ );
1595
+
1596
+ // Send completion event
1597
+ writer.send({
1598
+ type: completeType,
1599
+ sessionId,
1600
+ exitCode: result?.exitCode ?? 0,
1601
+ isNewSession: !data.options?.sessionId,
1602
+ viaRelay: true
1603
+ });
1604
+ } catch (error) {
1605
+ writer.send({
1606
+ type: errorType,
1607
+ error: error.message,
1608
+ sessionId
1609
+ });
1610
+ }
1611
+ }
1612
+
1370
1613
  // Handle shell WebSocket connections
1371
1614
  function handleShellConnection(ws) {
1372
1615
  if (!pty) {
@@ -1707,9 +1950,13 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
1707
1950
  return res.status(400).json({ error: 'No audio file provided' });
1708
1951
  }
1709
1952
 
1710
- const apiKey = process.env.OPENAI_API_KEY;
1953
+ // BYOK: check user's stored OpenAI key first, fall back to server env
1954
+ const userOpenaiKey = req.user?.id
1955
+ ? await getUserProviderKey(req.user.id, 'openai_key')
1956
+ : null;
1957
+ const apiKey = userOpenaiKey || process.env.OPENAI_API_KEY;
1711
1958
  if (!apiKey) {
1712
- return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' });
1959
+ return res.status(500).json({ error: 'OpenAI API key not configured. Add your key in Settings > AI Providers, or ask the admin to set OPENAI_API_KEY.' });
1713
1960
  }
1714
1961
 
1715
1962
  try {
@@ -1831,7 +2078,7 @@ Agent instructions:`;
1831
2078
 
1832
2079
  } catch (error) {
1833
2080
  console.error('Transcription error:', error);
1834
- res.status(500).json({ error: error.message });
2081
+ res.status(500).json({ error: 'Internal server error' });
1835
2082
  }
1836
2083
  });
1837
2084
  } catch (error) {
@@ -2222,6 +2469,18 @@ async function startServer() {
2222
2469
  // Initialize authentication database
2223
2470
  await initializeDatabase();
2224
2471
 
2472
+ // In local mode, ensure a default user exists (no signup needed)
2473
+ if (IS_LOCAL) {
2474
+ const hasUsers = await userDb.hasUsers();
2475
+ if (!hasUsers) {
2476
+ const localUsername = os.userInfo().username || 'local';
2477
+ const dummyHash = crypto.randomBytes(32).toString('hex');
2478
+ await userDb.createUser(localUsername, dummyHash);
2479
+ console.log(`${c.ok('[LOCAL]')} Created local user: ${c.bright(localUsername)}`);
2480
+ }
2481
+ console.log(`${c.info('[MODE]')} Running in ${c.bright('LOCAL')} mode (no login required)`);
2482
+ }
2483
+
2225
2484
  // Check if running in production mode (dist folder exists OR NODE_ENV/RAILWAY set)
2226
2485
  const distIndexPath = path.join(__dirname, '../dist/index.html');
2227
2486
  const isProduction = fs.existsSync(distIndexPath) || process.env.NODE_ENV === 'production' || !!process.env.RAILWAY_ENVIRONMENT;