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
@@ -31,7 +31,8 @@ import crypto from 'crypto';
31
31
  import jwt from 'jsonwebtoken';
32
32
  import { userDb, apiKeysDb, relayTokensDb } from './database/db.js';
33
33
 
34
- const JWT_SECRET = process.env.JWT_SECRET?.trim() || (() => { throw new Error('JWT_SECRET required'); })();
34
+ import { IS_PLATFORM } from './constants/config.js';
35
+ const JWT_SECRET = process.env.JWT_SECRET?.trim() || (IS_PLATFORM ? crypto.randomBytes(32).toString('hex') : (() => { throw new Error('JWT_SECRET required'); })());
35
36
 
36
37
  // In-memory canvas state (Excalidraw elements, synced via WebSocket with browser clients)
37
38
  let canvasElements = [];
@@ -1,11 +1,17 @@
1
1
  import jwt from 'jsonwebtoken';
2
+ import crypto from 'crypto';
2
3
  import { userDb, relayTokensDb } from '../database/db.js';
3
4
  import { IS_PLATFORM } from '../constants/config.js';
4
5
 
5
- const JWT_SECRET = process.env.JWT_SECRET?.trim();
6
+ let JWT_SECRET = process.env.JWT_SECRET?.trim();
6
7
  if (!JWT_SECRET) {
7
- console.error('[SECURITY] JWT_SECRET environment variable is required. Server cannot start without it.');
8
- process.exit(1);
8
+ if (IS_PLATFORM) {
9
+ // In local/self-hosted mode, generate a random secret (auth is bypassed anyway)
10
+ JWT_SECRET = crypto.randomBytes(32).toString('hex');
11
+ } else {
12
+ console.error('[SECURITY] JWT_SECRET environment variable is required. Server cannot start without it.');
13
+ process.exit(1);
14
+ }
9
15
  }
10
16
 
11
17
  // Optional static API key middleware
@@ -18,17 +24,21 @@ const validateApiKey = (req, res, next) => {
18
24
  next();
19
25
  };
20
26
 
21
- // Extract JWT from request: cookie → Bearer header → query param
27
+ // Extract JWT from request: cookie → Bearer header → query param (SSE only)
22
28
  const extractToken = (req) => {
23
- // 1. httpOnly cookie (browser sessions)
29
+ // 1. httpOnly cookie (browser sessions — primary auth method)
24
30
  if (req.cookies?.session) return req.cookies.session;
25
31
 
26
- // 2. Bearer header (API clients, MCP, backward compat)
32
+ // 2. Bearer header (API clients, MCP)
27
33
  const authHeader = req.headers['authorization'];
28
34
  if (authHeader?.startsWith('Bearer ')) return authHeader.slice(7);
29
35
 
30
- // 3. Query param (SSE EventSource fallback)
31
- if (req.query?.token) return req.query.token;
36
+ // 3. Query param — ONLY for SSE EventSource (which cannot set custom headers)
37
+ // Restricted to GET requests with Accept: text/event-stream to minimize exposure
38
+ if (req.query?.token && req.method === 'GET' &&
39
+ (req.headers.accept || '').includes('text/event-stream')) {
40
+ return req.query.token;
41
+ }
32
42
 
33
43
  return null;
34
44
  };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * OpenRouter Integration
3
+ *
4
+ * Lightweight wrapper for OpenRouter API, which provides access to
5
+ * hundreds of AI models (GPT-4, Claude, Gemini, Llama, Mistral, etc.)
6
+ * through a single API key and endpoint.
7
+ *
8
+ * Users provide their own OpenRouter API key via BYOK settings.
9
+ */
10
+
11
+ const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
12
+
13
+ // Popular models available on OpenRouter
14
+ export const OPENROUTER_MODELS = {
15
+ OPTIONS: [
16
+ { value: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4' },
17
+ { value: 'anthropic/claude-opus-4', label: 'Claude Opus 4' },
18
+ { value: 'openai/gpt-4o', label: 'GPT-4o' },
19
+ { value: 'openai/o3', label: 'O3' },
20
+ { value: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
21
+ { value: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick' },
22
+ { value: 'mistralai/mistral-large', label: 'Mistral Large' },
23
+ { value: 'deepseek/deepseek-r1', label: 'DeepSeek R1' },
24
+ ],
25
+ DEFAULT: 'anthropic/claude-sonnet-4',
26
+ };
27
+
28
+ /**
29
+ * Query OpenRouter API with streaming support
30
+ * @param {string} message - User message
31
+ * @param {Object} options - { model, apiKey, systemPrompt }
32
+ * @param {Object} writer - WebSocketWriter or SSEStreamWriter
33
+ */
34
+ export async function queryOpenRouter(message, options = {}, writer) {
35
+ const {
36
+ model = OPENROUTER_MODELS.DEFAULT,
37
+ apiKey,
38
+ systemPrompt,
39
+ sessionId,
40
+ } = options;
41
+
42
+ if (!apiKey) {
43
+ writer.send({
44
+ type: 'error',
45
+ error: 'OpenRouter API key required. Add your key in Settings > AI Providers.',
46
+ });
47
+ return;
48
+ }
49
+
50
+ // Generate session ID
51
+ const sid = sessionId || `or-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
52
+ writer.send({ type: 'session-created', sessionId: sid });
53
+
54
+ const messages = [];
55
+ if (systemPrompt) {
56
+ messages.push({ role: 'system', content: systemPrompt });
57
+ }
58
+ messages.push({ role: 'user', content: message });
59
+
60
+ try {
61
+ const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Authorization': `Bearer ${apiKey}`,
65
+ 'Content-Type': 'application/json',
66
+ 'HTTP-Referer': 'https://cli.upfyn.com',
67
+ 'X-Title': 'Upfyn-Code',
68
+ },
69
+ body: JSON.stringify({
70
+ model,
71
+ messages,
72
+ stream: true,
73
+ }),
74
+ });
75
+
76
+ if (!response.ok) {
77
+ const errBody = await response.text();
78
+ let errMsg = `OpenRouter API error (${response.status})`;
79
+ try {
80
+ const parsed = JSON.parse(errBody);
81
+ errMsg = parsed.error?.message || errMsg;
82
+ } catch {}
83
+ writer.send({ type: 'error', error: errMsg, sessionId: sid });
84
+ return;
85
+ }
86
+
87
+ // Stream SSE response
88
+ const reader = response.body.getReader();
89
+ const decoder = new TextDecoder();
90
+ let buffer = '';
91
+ let fullContent = '';
92
+
93
+ while (true) {
94
+ const { done, value } = await reader.read();
95
+ if (done) break;
96
+
97
+ buffer += decoder.decode(value, { stream: true });
98
+ const lines = buffer.split('\n');
99
+ buffer = lines.pop() || '';
100
+
101
+ for (const line of lines) {
102
+ if (!line.startsWith('data: ')) continue;
103
+ const data = line.slice(6).trim();
104
+ if (data === '[DONE]') break;
105
+
106
+ try {
107
+ const parsed = JSON.parse(data);
108
+ const delta = parsed.choices?.[0]?.delta?.content;
109
+ if (delta) {
110
+ fullContent += delta;
111
+ writer.send({
112
+ type: 'assistant',
113
+ content: delta,
114
+ sessionId: sid,
115
+ });
116
+ }
117
+ } catch {}
118
+ }
119
+ }
120
+
121
+ // Send completion
122
+ writer.send({
123
+ type: 'result',
124
+ subtype: 'success',
125
+ sessionId: sid,
126
+ content: fullContent,
127
+ model,
128
+ provider: 'openrouter',
129
+ });
130
+ } catch (error) {
131
+ writer.send({
132
+ type: 'error',
133
+ error: `OpenRouter request failed: ${error.message}`,
134
+ sessionId: sid,
135
+ });
136
+ }
137
+ }
@@ -14,7 +14,7 @@ import WebSocket from 'ws';
14
14
  import os from 'os';
15
15
  import fs from 'fs';
16
16
  import path from 'path';
17
- import { spawn } from 'child_process';
17
+ import { spawn, execSync } from 'child_process';
18
18
  import { promises as fsPromises } from 'fs';
19
19
  import crypto from 'crypto';
20
20
  import {
@@ -99,7 +99,7 @@ async function handleRelayCommand(data, ws) {
99
99
  switch (action) {
100
100
  case 'claude-query': {
101
101
  const { command, options } = data;
102
- logRelayEvent('🤖', `Claude query: ${command?.slice(0, 60)}...`, 'cyan');
102
+ logRelayEvent('>', `Claude query: ${command?.slice(0, 60)}...`, 'cyan');
103
103
 
104
104
  const args = ['--print'];
105
105
  if (options?.projectPath) args.push('--cwd', options.projectPath);
@@ -137,9 +137,107 @@ async function handleRelayCommand(data, ws) {
137
137
  break;
138
138
  }
139
139
 
140
+ case 'codex-query': {
141
+ const { command, options } = data;
142
+ logRelayEvent('>', `Codex query: ${command?.slice(0, 60)}...`, 'cyan');
143
+
144
+ const codexArgs = ['--quiet'];
145
+ if (options?.projectPath || options?.cwd) {
146
+ codexArgs.push('--cwd', options.projectPath || options.cwd);
147
+ }
148
+ if (options?.model) codexArgs.push('--model', options.model);
149
+
150
+ const codexProc = spawn('codex', [...codexArgs, command || ''], {
151
+ shell: true,
152
+ cwd: options?.projectPath || options?.cwd || os.homedir(),
153
+ env: process.env,
154
+ });
155
+
156
+ codexProc.stdout.on('data', (chunk) => {
157
+ ws.send(JSON.stringify({
158
+ type: 'relay-stream',
159
+ requestId,
160
+ data: { type: 'codex-response', content: chunk.toString() }
161
+ }));
162
+ });
163
+
164
+ codexProc.stderr.on('data', (chunk) => {
165
+ ws.send(JSON.stringify({
166
+ type: 'relay-stream',
167
+ requestId,
168
+ data: { type: 'codex-error', content: chunk.toString() }
169
+ }));
170
+ });
171
+
172
+ codexProc.on('close', (code) => {
173
+ ws.send(JSON.stringify({
174
+ type: 'relay-complete',
175
+ requestId,
176
+ exitCode: code
177
+ }));
178
+ });
179
+ break;
180
+ }
181
+
182
+ case 'cursor-query': {
183
+ const { command, options } = data;
184
+ logRelayEvent('>', `Cursor query: ${command?.slice(0, 60)}...`, 'cyan');
185
+
186
+ const cursorArgs = [];
187
+ if (options?.projectPath || options?.cwd) {
188
+ cursorArgs.push('--cwd', options.projectPath || options.cwd);
189
+ }
190
+ if (options?.model) cursorArgs.push('--model', options.model);
191
+
192
+ const cursorProc = spawn('cursor-agent', [...cursorArgs, command || ''], {
193
+ shell: true,
194
+ cwd: options?.projectPath || options?.cwd || os.homedir(),
195
+ env: process.env,
196
+ });
197
+
198
+ cursorProc.stdout.on('data', (chunk) => {
199
+ ws.send(JSON.stringify({
200
+ type: 'relay-stream',
201
+ requestId,
202
+ data: { type: 'cursor-response', content: chunk.toString() }
203
+ }));
204
+ });
205
+
206
+ cursorProc.stderr.on('data', (chunk) => {
207
+ ws.send(JSON.stringify({
208
+ type: 'relay-stream',
209
+ requestId,
210
+ data: { type: 'cursor-error', content: chunk.toString() }
211
+ }));
212
+ });
213
+
214
+ cursorProc.on('close', (code) => {
215
+ ws.send(JSON.stringify({
216
+ type: 'relay-complete',
217
+ requestId,
218
+ exitCode: code
219
+ }));
220
+ });
221
+ break;
222
+ }
223
+
224
+ case 'detect-agents': {
225
+ const agents = detectInstalledAgents();
226
+ ws.send(JSON.stringify({
227
+ type: 'relay-response',
228
+ requestId,
229
+ data: { agents }
230
+ }));
231
+ break;
232
+ }
233
+
140
234
  case 'shell-command': {
141
235
  const { command: cmd, cwd } = data;
142
- logRelayEvent('⚡', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
236
+ // Block dangerous shell patterns
237
+ if (!cmd || typeof cmd !== 'string') throw new Error('Invalid command');
238
+ const dangerous = ['rm -rf /', 'mkfs', 'dd if=', ':(){', 'fork bomb', '> /dev/sd'];
239
+ if (dangerous.some(d => cmd.includes(d))) throw new Error('Command blocked for safety');
240
+ logRelayEvent('$', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
143
241
  const result = await execCommand(cmd, [], { cwd: cwd || os.homedir(), timeout: 60000 });
144
242
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
145
243
  break;
@@ -147,16 +245,26 @@ async function handleRelayCommand(data, ws) {
147
245
 
148
246
  case 'file-read': {
149
247
  const { filePath } = data;
150
- logRelayEvent('📄', `Read: ${filePath}`, 'dim');
151
- const content = await fsPromises.readFile(filePath, 'utf8');
248
+ if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
249
+ // Block reading sensitive system files
250
+ const normalizedPath = path.resolve(filePath);
251
+ const blocked = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.env'];
252
+ if (blocked.some(b => normalizedPath.includes(b))) throw new Error('Access denied');
253
+ logRelayEvent('R', `Read: ${filePath}`, 'dim');
254
+ const content = await fsPromises.readFile(normalizedPath, 'utf8');
152
255
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
153
256
  break;
154
257
  }
155
258
 
156
259
  case 'file-write': {
157
260
  const { filePath: fp, content: fileContent } = data;
158
- logRelayEvent('💾', `Write: ${fp}`, 'dim');
159
- await fsPromises.writeFile(fp, fileContent, 'utf8');
261
+ if (!fp || typeof fp !== 'string') throw new Error('Invalid file path');
262
+ // Block writing to sensitive locations
263
+ const normalizedFp = path.resolve(fp);
264
+ const blockedDirs = ['/etc/', '/usr/bin/', '/usr/sbin/', 'System32', '.ssh/'];
265
+ if (blockedDirs.some(d => normalizedFp.includes(d))) throw new Error('Access denied');
266
+ logRelayEvent('W', `Write: ${fp}`, 'dim');
267
+ await fsPromises.writeFile(normalizedFp, fileContent, 'utf8');
160
268
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { success: true } }));
161
269
  break;
162
270
  }
@@ -170,7 +278,7 @@ async function handleRelayCommand(data, ws) {
170
278
 
171
279
  case 'git-operation': {
172
280
  const { gitCommand, cwd: gitCwd } = data;
173
- logRelayEvent('🔀', `Git: ${gitCommand}`, 'dim');
281
+ logRelayEvent('G', `Git: ${gitCommand}`, 'dim');
174
282
  const result = await execCommand('git', [gitCommand], { cwd: gitCwd });
175
283
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
176
284
  break;
@@ -215,7 +323,57 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
215
323
  }
216
324
 
217
325
  /**
218
- * Main connect function
326
+ * Detect which AI CLI agents are installed on this machine
327
+ * Returns an object with agent names and their availability
328
+ */
329
+ function detectInstalledAgents() {
330
+ const isWindows = process.platform === 'win32';
331
+ const whichCmd = isWindows ? 'where' : 'which';
332
+
333
+ const agents = [
334
+ { name: 'claude', binary: 'claude', label: 'Claude Code' },
335
+ { name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
336
+ { name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
337
+ ];
338
+
339
+ const detected = {};
340
+ for (const agent of agents) {
341
+ try {
342
+ const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
343
+ detected[agent.name] = {
344
+ installed: true,
345
+ path: result.split('\n')[0].trim(),
346
+ label: agent.label,
347
+ };
348
+ } catch {
349
+ detected[agent.name] = {
350
+ installed: false,
351
+ label: agent.label,
352
+ };
353
+ }
354
+ }
355
+ return detected;
356
+ }
357
+
358
+ /**
359
+ * Create WebSocket connection with optional API key in handshake
360
+ */
361
+ function createRelayConnection(wsUrl, config = {}) {
362
+ const headers = {};
363
+ // Send API key in WebSocket headers if available
364
+ if (config.anthropicApiKey) {
365
+ headers['x-anthropic-api-key'] = config.anthropicApiKey;
366
+ }
367
+ headers['x-upfyn-version'] = VERSION;
368
+ headers['x-upfyn-machine'] = os.hostname();
369
+ headers['x-upfyn-platform'] = process.platform;
370
+ headers['x-upfyn-cwd'] = process.cwd();
371
+
372
+ return new WebSocket(wsUrl, { headers });
373
+ }
374
+
375
+ /**
376
+ * Main connect function (interactive — with animation and logging)
219
377
  */
220
378
  export async function connectToServer(options = {}) {
221
379
  const config = loadConfig();
@@ -224,9 +382,9 @@ export async function connectToServer(options = {}) {
224
382
 
225
383
  if (!relayKey) {
226
384
  console.log('');
227
- console.log(` ${c.red('')} No relay key provided.`);
385
+ console.log(` ${c.red('FAIL')} No relay key provided.`);
228
386
  console.log('');
229
- console.log(` ${c.gray('Get your relay token from the web UI:')} `);
387
+ console.log(` ${c.gray('Get your relay token from the web UI:')}`);
230
388
  console.log(` ${c.dim('1.')} Sign in at ${c.cyan('https://cli.upfyn.com')}`);
231
389
  console.log(` ${c.dim('2.')} Click ${c.bright('Connect')} button`);
232
390
  console.log(` ${c.dim('3.')} Copy the command and run it here`);
@@ -256,7 +414,7 @@ export async function connectToServer(options = {}) {
256
414
  const MAX_RECONNECT = 10;
257
415
 
258
416
  function connect() {
259
- const ws = new WebSocket(wsUrl);
417
+ const ws = createRelayConnection(wsUrl, config);
260
418
 
261
419
  ws.on('open', () => {
262
420
  reconnectAttempts = 0;
@@ -273,7 +431,35 @@ export async function connectToServer(options = {}) {
273
431
  const nameMatch = data.message?.match(/Connected as (.+?)\./);
274
432
  const username = nameMatch ? nameMatch[1] : 'Unknown';
275
433
  showConnectionBanner(username, serverUrl);
276
- logRelayEvent('🟢', 'Relay active — waiting for commands...', 'green');
434
+
435
+ // Detect and report installed agents
436
+ const agents = detectInstalledAgents();
437
+ const installed = Object.entries(agents)
438
+ .filter(([, info]) => info.installed)
439
+ .map(([name, info]) => info.label);
440
+ const missing = Object.entries(agents)
441
+ .filter(([, info]) => !info.installed)
442
+ .map(([name, info]) => info.label);
443
+
444
+ if (installed.length > 0) {
445
+ logRelayEvent('+', `Agents found: ${installed.join(', ')}`, 'green');
446
+ }
447
+ if (missing.length > 0) {
448
+ logRelayEvent('~', `Not found: ${missing.join(', ')} (install to enable)`, 'yellow');
449
+ }
450
+
451
+ // Send agent capabilities to server
452
+ ws.send(JSON.stringify({
453
+ type: 'agent-capabilities',
454
+ agents,
455
+ machine: {
456
+ hostname: os.hostname(),
457
+ platform: process.platform,
458
+ cwd: process.cwd(),
459
+ }
460
+ }));
461
+
462
+ logRelayEvent('*', 'Relay active -- waiting for commands...', 'green');
277
463
  return;
278
464
  }
279
465
 
@@ -289,23 +475,23 @@ export async function connectToServer(options = {}) {
289
475
  return;
290
476
  }
291
477
  } catch (e) {
292
- logRelayEvent('', `Parse error: ${e.message}`, 'red');
478
+ logRelayEvent('!', `Parse error: ${e.message}`, 'red');
293
479
  }
294
480
  });
295
481
 
296
482
  ws.on('close', (code) => {
297
483
  if (code === 1000) {
298
- logRelayEvent('', 'Disconnected gracefully.', 'dim');
484
+ logRelayEvent('-', 'Disconnected gracefully.', 'dim');
299
485
  process.exit(0);
300
486
  }
301
487
 
302
488
  reconnectAttempts++;
303
489
  if (reconnectAttempts <= MAX_RECONNECT) {
304
490
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
305
- logRelayEvent('🔄', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
491
+ logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
306
492
  setTimeout(connect, delay);
307
493
  } else {
308
- logRelayEvent('🔴', 'Max reconnection attempts reached. Exiting.', 'red');
494
+ logRelayEvent('X', 'Max reconnection attempts reached. Exiting.', 'red');
309
495
  process.exit(1);
310
496
  }
311
497
  });
@@ -332,7 +518,65 @@ export async function connectToServer(options = {}) {
332
518
  // Graceful shutdown
333
519
  process.on('SIGINT', () => {
334
520
  console.log('');
335
- logRelayEvent('', 'Disconnecting...', 'dim');
521
+ logRelayEvent('-', 'Disconnecting...', 'dim');
336
522
  process.exit(0);
337
523
  });
338
524
  }
525
+
526
+ /**
527
+ * Background connect function (silent — used when uc launches Claude Code)
528
+ * Runs relay in the background without animation or user-facing output.
529
+ */
530
+ export function connectToServerBackground(options = {}) {
531
+ const config = loadConfig();
532
+ const serverUrl = options.server || config.server;
533
+ const relayKey = options.key || config.relayKey;
534
+
535
+ if (!serverUrl || !relayKey) return;
536
+
537
+ const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
538
+
539
+ let reconnectAttempts = 0;
540
+ const MAX_RECONNECT = 5;
541
+
542
+ function connect() {
543
+ const ws = createRelayConnection(wsUrl, config);
544
+
545
+ ws.on('message', (rawMessage) => {
546
+ try {
547
+ const data = JSON.parse(rawMessage);
548
+ if (data.type === 'relay-command') {
549
+ handleRelayCommand(data, ws);
550
+ }
551
+ } catch { /* ignore */ }
552
+ });
553
+
554
+ ws.on('open', () => {
555
+ reconnectAttempts = 0;
556
+ });
557
+
558
+ ws.on('close', (code) => {
559
+ if (code === 1000) return;
560
+ reconnectAttempts++;
561
+ if (reconnectAttempts <= MAX_RECONNECT) {
562
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
563
+ setTimeout(connect, delay);
564
+ }
565
+ });
566
+
567
+ ws.on('error', () => {
568
+ // silent — close handler will reconnect
569
+ });
570
+
571
+ // Heartbeat
572
+ const heartbeat = setInterval(() => {
573
+ if (ws.readyState === 1) {
574
+ ws.send(JSON.stringify({ type: 'ping' }));
575
+ } else {
576
+ clearInterval(heartbeat);
577
+ }
578
+ }, 30000);
579
+ }
580
+
581
+ connect();
582
+ }
@@ -4,15 +4,34 @@ import path from 'path';
4
4
  import os from 'os';
5
5
  import { promises as fs } from 'fs';
6
6
  import crypto from 'crypto';
7
- import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js';
7
+ import { userDb, apiKeysDb, githubTokensDb, credentialsDb } from '../database/db.js';
8
8
  import { addProjectManually } from '../projects.js';
9
9
  import { queryClaudeSDK } from '../claude-sdk.js';
10
10
  import { spawnCursor } from '../cursor-cli.js';
11
11
  import { queryCodex } from '../openai-codex.js';
12
12
  import { Octokit } from '@octokit/rest';
13
13
  import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
14
+ import { queryOpenRouter, OPENROUTER_MODELS } from '../openrouter.js';
14
15
  import { IS_PLATFORM } from '../constants/config.js';
15
16
 
17
+ // BYOK helper: get user's stored API key for a provider
18
+ async function getUserProviderKey(userId, providerType) {
19
+ if (!userId) return null;
20
+ try {
21
+ const creds = await credentialsDb.getCredentials(userId, providerType);
22
+ const active = creds.find(c => c.is_active);
23
+ return active?.credential_value || null;
24
+ } catch { return null; }
25
+ }
26
+
27
+ async function withUserApiKey(envKey, userKey, fn) {
28
+ if (!userKey) return fn();
29
+ const prev = process.env[envKey];
30
+ process.env[envKey] = userKey;
31
+ try { return await fn(); }
32
+ finally { if (prev !== undefined) process.env[envKey] = prev; else delete process.env[envKey]; }
33
+ }
34
+
16
35
  const router = express.Router();
17
36
 
18
37
  /**
@@ -939,17 +958,22 @@ router.post('/', validateExternalApiKey, async (req, res) => {
939
958
  });
940
959
  }
941
960
 
942
- // Start the appropriate session
961
+ // Start the appropriate session (with BYOK key injection where applicable)
962
+ const userId = req.user?.id;
963
+
943
964
  if (provider === 'claude') {
944
965
  console.log('🤖 Starting Claude SDK session');
945
-
946
- await queryClaudeSDK(message.trim(), {
947
- projectPath: finalProjectPath,
948
- cwd: finalProjectPath,
949
- sessionId: null, // New session
950
- model: model,
951
- permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
952
- }, writer);
966
+ const userKey = await getUserProviderKey(userId, 'anthropic_key');
967
+
968
+ await withUserApiKey('ANTHROPIC_API_KEY', userKey, () =>
969
+ queryClaudeSDK(message.trim(), {
970
+ projectPath: finalProjectPath,
971
+ cwd: finalProjectPath,
972
+ sessionId: null,
973
+ model: model,
974
+ permissionMode: 'bypassPermissions'
975
+ }, writer)
976
+ );
953
977
 
954
978
  } else if (provider === 'cursor') {
955
979
  console.log('🖱️ Starting Cursor CLI session');
@@ -957,19 +981,30 @@ router.post('/', validateExternalApiKey, async (req, res) => {
957
981
  await spawnCursor(message.trim(), {
958
982
  projectPath: finalProjectPath,
959
983
  cwd: finalProjectPath,
960
- sessionId: null, // New session
984
+ sessionId: null,
961
985
  model: model || undefined,
962
- skipPermissions: true // Bypass permissions for Cursor
986
+ skipPermissions: true
963
987
  }, writer);
964
988
  } else if (provider === 'codex') {
965
989
  console.log('🤖 Starting Codex SDK session');
966
-
967
- await queryCodex(message.trim(), {
968
- projectPath: finalProjectPath,
969
- cwd: finalProjectPath,
970
- sessionId: null,
971
- model: model || CODEX_MODELS.DEFAULT,
972
- permissionMode: 'bypassPermissions'
990
+ const userKey = await getUserProviderKey(userId, 'openai_key');
991
+
992
+ await withUserApiKey('OPENAI_API_KEY', userKey, () =>
993
+ queryCodex(message.trim(), {
994
+ projectPath: finalProjectPath,
995
+ cwd: finalProjectPath,
996
+ sessionId: null,
997
+ model: model || CODEX_MODELS.DEFAULT,
998
+ permissionMode: 'bypassPermissions'
999
+ }, writer)
1000
+ );
1001
+ } else if (provider === 'openrouter') {
1002
+ console.log('🌐 Starting OpenRouter session');
1003
+ const userKey = await getUserProviderKey(userId, 'openrouter_key');
1004
+
1005
+ await queryOpenRouter(message.trim(), {
1006
+ model: model || OPENROUTER_MODELS.DEFAULT,
1007
+ apiKey: userKey,
973
1008
  }, writer);
974
1009
  }
975
1010