upfynai-code 2.3.0 → 2.4.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 (158) 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 +194 -21
  123. package/server/index.js +159 -107
  124. package/server/middleware/auth.js +9 -5
  125. package/server/openrouter.js +137 -0
  126. package/server/relay-client.js +104 -17
  127. package/server/routes/agent.js +54 -19
  128. package/server/routes/auth.js +23 -12
  129. package/server/routes/settings.js +91 -0
  130. package/shared/modelConstants.js +29 -0
  131. package/client/dist/assets/AppContent-CTSHQdyq.js +0 -513
  132. package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +0 -6
  133. package/client/dist/assets/LoginModal-silya-zP.js +0 -11
  134. package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +0 -1
  135. package/client/dist/assets/channel-CSnvHe_M.js +0 -1
  136. package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +0 -2
  137. package/client/dist/assets/clone-B75abXxS.js +0 -1
  138. package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +0 -4
  139. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +0 -1
  140. package/client/dist/assets/index-BnXuHrpJ.js +0 -523
  141. package/client/dist/assets/index-BwxNox94.css +0 -1
  142. package/client/dist/assets/index-D1urGMYu.js +0 -95
  143. package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +0 -415
  144. package/client/dist/assets/pdf-TYrZqVzP.js +0 -12
  145. package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +0 -199
  146. package/client/dist/assets/pica-VkdyTzi8.js +0 -2
  147. package/client/dist/assets/roundRect-mAH3dD0p.js +0 -1
  148. package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +0 -1
  149. package/client/dist/assets/vendor-codemirror-BARtJV1V.js +0 -16
  150. package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +0 -20
  151. package/client/dist/assets/vendor-i18n-ByAl-gdx.js +0 -1
  152. package/client/dist/assets/vendor-icons-D33IkSIf.js +0 -1
  153. package/client/dist/assets/vendor-react-CHoMc7ka.js +0 -8
  154. package/client/dist/assets/vendor-xterm-DBb3RXlu.js +0 -66
  155. package/client/dist/assets/vendor-xterm-DrlLKa8f.css +0 -1
  156. package/client/dist/llms.txt +0 -40
  157. package/client/dist/robots.txt +0 -11
  158. package/client/dist/sitemap.xml +0 -45
@@ -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);
@@ -139,7 +139,11 @@ async function handleRelayCommand(data, ws) {
139
139
 
140
140
  case 'shell-command': {
141
141
  const { command: cmd, cwd } = data;
142
- logRelayEvent('⚡', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
142
+ // Block dangerous shell patterns
143
+ if (!cmd || typeof cmd !== 'string') throw new Error('Invalid command');
144
+ const dangerous = ['rm -rf /', 'mkfs', 'dd if=', ':(){', 'fork bomb', '> /dev/sd'];
145
+ if (dangerous.some(d => cmd.includes(d))) throw new Error('Command blocked for safety');
146
+ logRelayEvent('$', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
143
147
  const result = await execCommand(cmd, [], { cwd: cwd || os.homedir(), timeout: 60000 });
144
148
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
145
149
  break;
@@ -147,16 +151,26 @@ async function handleRelayCommand(data, ws) {
147
151
 
148
152
  case 'file-read': {
149
153
  const { filePath } = data;
150
- logRelayEvent('📄', `Read: ${filePath}`, 'dim');
151
- const content = await fsPromises.readFile(filePath, 'utf8');
154
+ if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
155
+ // Block reading sensitive system files
156
+ const normalizedPath = path.resolve(filePath);
157
+ const blocked = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.env'];
158
+ if (blocked.some(b => normalizedPath.includes(b))) throw new Error('Access denied');
159
+ logRelayEvent('R', `Read: ${filePath}`, 'dim');
160
+ const content = await fsPromises.readFile(normalizedPath, 'utf8');
152
161
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
153
162
  break;
154
163
  }
155
164
 
156
165
  case 'file-write': {
157
166
  const { filePath: fp, content: fileContent } = data;
158
- logRelayEvent('💾', `Write: ${fp}`, 'dim');
159
- await fsPromises.writeFile(fp, fileContent, 'utf8');
167
+ if (!fp || typeof fp !== 'string') throw new Error('Invalid file path');
168
+ // Block writing to sensitive locations
169
+ const normalizedFp = path.resolve(fp);
170
+ const blockedDirs = ['/etc/', '/usr/bin/', '/usr/sbin/', 'System32', '.ssh/'];
171
+ if (blockedDirs.some(d => normalizedFp.includes(d))) throw new Error('Access denied');
172
+ logRelayEvent('W', `Write: ${fp}`, 'dim');
173
+ await fsPromises.writeFile(normalizedFp, fileContent, 'utf8');
160
174
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { success: true } }));
161
175
  break;
162
176
  }
@@ -170,7 +184,7 @@ async function handleRelayCommand(data, ws) {
170
184
 
171
185
  case 'git-operation': {
172
186
  const { gitCommand, cwd: gitCwd } = data;
173
- logRelayEvent('🔀', `Git: ${gitCommand}`, 'dim');
187
+ logRelayEvent('G', `Git: ${gitCommand}`, 'dim');
174
188
  const result = await execCommand('git', [gitCommand], { cwd: gitCwd });
175
189
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
176
190
  break;
@@ -215,7 +229,22 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
215
229
  }
216
230
 
217
231
  /**
218
- * Main connect function
232
+ * Create WebSocket connection with optional API key in handshake
233
+ */
234
+ function createRelayConnection(wsUrl, config = {}) {
235
+ const headers = {};
236
+ // Send API key in WebSocket headers if available
237
+ if (config.anthropicApiKey) {
238
+ headers['x-anthropic-api-key'] = config.anthropicApiKey;
239
+ }
240
+ headers['x-upfyn-version'] = VERSION;
241
+ headers['x-upfyn-machine'] = os.hostname();
242
+
243
+ return new WebSocket(wsUrl, { headers });
244
+ }
245
+
246
+ /**
247
+ * Main connect function (interactive — with animation and logging)
219
248
  */
220
249
  export async function connectToServer(options = {}) {
221
250
  const config = loadConfig();
@@ -224,9 +253,9 @@ export async function connectToServer(options = {}) {
224
253
 
225
254
  if (!relayKey) {
226
255
  console.log('');
227
- console.log(` ${c.red('')} No relay key provided.`);
256
+ console.log(` ${c.red('FAIL')} No relay key provided.`);
228
257
  console.log('');
229
- console.log(` ${c.gray('Get your relay token from the web UI:')} `);
258
+ console.log(` ${c.gray('Get your relay token from the web UI:')}`);
230
259
  console.log(` ${c.dim('1.')} Sign in at ${c.cyan('https://cli.upfyn.com')}`);
231
260
  console.log(` ${c.dim('2.')} Click ${c.bright('Connect')} button`);
232
261
  console.log(` ${c.dim('3.')} Copy the command and run it here`);
@@ -256,7 +285,7 @@ export async function connectToServer(options = {}) {
256
285
  const MAX_RECONNECT = 10;
257
286
 
258
287
  function connect() {
259
- const ws = new WebSocket(wsUrl);
288
+ const ws = createRelayConnection(wsUrl, config);
260
289
 
261
290
  ws.on('open', () => {
262
291
  reconnectAttempts = 0;
@@ -273,7 +302,7 @@ export async function connectToServer(options = {}) {
273
302
  const nameMatch = data.message?.match(/Connected as (.+?)\./);
274
303
  const username = nameMatch ? nameMatch[1] : 'Unknown';
275
304
  showConnectionBanner(username, serverUrl);
276
- logRelayEvent('🟢', 'Relay active waiting for commands...', 'green');
305
+ logRelayEvent('*', 'Relay active -- waiting for commands...', 'green');
277
306
  return;
278
307
  }
279
308
 
@@ -289,23 +318,23 @@ export async function connectToServer(options = {}) {
289
318
  return;
290
319
  }
291
320
  } catch (e) {
292
- logRelayEvent('', `Parse error: ${e.message}`, 'red');
321
+ logRelayEvent('!', `Parse error: ${e.message}`, 'red');
293
322
  }
294
323
  });
295
324
 
296
325
  ws.on('close', (code) => {
297
326
  if (code === 1000) {
298
- logRelayEvent('', 'Disconnected gracefully.', 'dim');
327
+ logRelayEvent('-', 'Disconnected gracefully.', 'dim');
299
328
  process.exit(0);
300
329
  }
301
330
 
302
331
  reconnectAttempts++;
303
332
  if (reconnectAttempts <= MAX_RECONNECT) {
304
333
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
305
- logRelayEvent('🔄', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
334
+ logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
306
335
  setTimeout(connect, delay);
307
336
  } else {
308
- logRelayEvent('🔴', 'Max reconnection attempts reached. Exiting.', 'red');
337
+ logRelayEvent('X', 'Max reconnection attempts reached. Exiting.', 'red');
309
338
  process.exit(1);
310
339
  }
311
340
  });
@@ -332,7 +361,65 @@ export async function connectToServer(options = {}) {
332
361
  // Graceful shutdown
333
362
  process.on('SIGINT', () => {
334
363
  console.log('');
335
- logRelayEvent('', 'Disconnecting...', 'dim');
364
+ logRelayEvent('-', 'Disconnecting...', 'dim');
336
365
  process.exit(0);
337
366
  });
338
367
  }
368
+
369
+ /**
370
+ * Background connect function (silent — used when uc launches Claude Code)
371
+ * Runs relay in the background without animation or user-facing output.
372
+ */
373
+ export function connectToServerBackground(options = {}) {
374
+ const config = loadConfig();
375
+ const serverUrl = options.server || config.server;
376
+ const relayKey = options.key || config.relayKey;
377
+
378
+ if (!serverUrl || !relayKey) return;
379
+
380
+ const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
381
+
382
+ let reconnectAttempts = 0;
383
+ const MAX_RECONNECT = 5;
384
+
385
+ function connect() {
386
+ const ws = createRelayConnection(wsUrl, config);
387
+
388
+ ws.on('message', (rawMessage) => {
389
+ try {
390
+ const data = JSON.parse(rawMessage);
391
+ if (data.type === 'relay-command') {
392
+ handleRelayCommand(data, ws);
393
+ }
394
+ } catch { /* ignore */ }
395
+ });
396
+
397
+ ws.on('open', () => {
398
+ reconnectAttempts = 0;
399
+ });
400
+
401
+ ws.on('close', (code) => {
402
+ if (code === 1000) return;
403
+ reconnectAttempts++;
404
+ if (reconnectAttempts <= MAX_RECONNECT) {
405
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
406
+ setTimeout(connect, delay);
407
+ }
408
+ });
409
+
410
+ ws.on('error', () => {
411
+ // silent — close handler will reconnect
412
+ });
413
+
414
+ // Heartbeat
415
+ const heartbeat = setInterval(() => {
416
+ if (ws.readyState === 1) {
417
+ ws.send(JSON.stringify({ type: 'ping' }));
418
+ } else {
419
+ clearInterval(heartbeat);
420
+ }
421
+ }, 30000);
422
+ }
423
+
424
+ connect();
425
+ }
@@ -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
 
@@ -19,8 +19,12 @@ router.get('/status', async (req, res) => {
19
19
  }
20
20
  });
21
21
 
22
- // User registration — allows multiple users
23
- router.post('/register', async (req, res) => {
22
+ // User registration — allows multiple users (rate limited)
23
+ router.post('/register', (req, res, next) => {
24
+ const rl = req.app.locals.authRateLimit;
25
+ if (rl) return rl(req, res, next);
26
+ next();
27
+ }, async (req, res) => {
24
28
  try {
25
29
  const { username, password, email, phone, firstName, lastName } = req.body;
26
30
 
@@ -28,13 +32,16 @@ router.post('/register', async (req, res) => {
28
32
  if (!password) {
29
33
  return res.status(400).json({ error: 'Password is required' });
30
34
  }
31
- if (password.length < 6) {
32
- return res.status(400).json({ error: 'Password must be at least 6 characters' });
35
+ if (password.length < 8) {
36
+ return res.status(400).json({ error: 'Password must be at least 8 characters' });
37
+ }
38
+ if (password.length > 128) {
39
+ return res.status(400).json({ error: 'Password is too long' });
33
40
  }
34
41
 
35
- // Derive display username from firstName + lastName if no username provided
36
- const fName = (firstName || '').trim();
37
- const lName = (lastName || '').trim();
42
+ // Sanitize and validate name/phone inputs
43
+ const fName = (firstName || '').trim().slice(0, 50);
44
+ const lName = (lastName || '').trim().slice(0, 50);
38
45
  const displayName = username || [fName, lName].filter(Boolean).join(' ') || 'User';
39
46
 
40
47
  if (displayName.length < 2) {
@@ -86,8 +93,12 @@ router.post('/register', async (req, res) => {
86
93
  }
87
94
  });
88
95
 
89
- // User login
90
- router.post('/login', async (req, res) => {
96
+ // User login (rate limited)
97
+ router.post('/login', (req, res, next) => {
98
+ const rl = req.app.locals.authRateLimit;
99
+ if (rl) return rl(req, res, next);
100
+ next();
101
+ }, async (req, res) => {
91
102
  try {
92
103
  const { username, password, firstName, lastName, phone } = req.body;
93
104
 
@@ -106,9 +117,9 @@ router.post('/login', async (req, res) => {
106
117
  }
107
118
 
108
119
  // Update name/phone if provided and different from stored values
109
- const fName = (firstName || '').trim();
110
- const lName = (lastName || '').trim();
111
- const ph = (phone || '').trim();
120
+ const fName = (firstName || '').trim().slice(0, 50);
121
+ const lName = (lastName || '').trim().slice(0, 50);
122
+ const ph = (phone || '').trim().slice(0, 20);
112
123
  const updates = [];
113
124
  const args = [];
114
125
  if (fName && fName !== user.first_name) { updates.push('first_name = ?'); args.push(fName); }
@@ -175,4 +175,95 @@ router.patch('/credentials/:credentialId/toggle', async (req, res) => {
175
175
  }
176
176
  });
177
177
 
178
+ // ===============================
179
+ // AI Provider Keys (BYOK)
180
+ // ===============================
181
+
182
+ const AI_PROVIDER_TYPES = [
183
+ 'anthropic_key',
184
+ 'openai_key',
185
+ 'openrouter_key',
186
+ 'google_key',
187
+ ];
188
+
189
+ // Get all AI provider keys for the authenticated user (masked values)
190
+ router.get('/ai-providers', async (req, res) => {
191
+ try {
192
+ const allCreds = [];
193
+ for (const type of AI_PROVIDER_TYPES) {
194
+ const creds = await credentialsDb.getCredentials(req.user.id, type);
195
+ allCreds.push(...creds.map(c => ({
196
+ id: c.id,
197
+ credential_name: c.credential_name,
198
+ credential_type: c.credential_type,
199
+ description: c.description,
200
+ is_active: c.is_active,
201
+ created_at: c.created_at,
202
+ // Mask the key — show first 8 and last 4 chars
203
+ masked_value: c.credential_value
204
+ ? c.credential_value.slice(0, 8) + '...' + c.credential_value.slice(-4)
205
+ : '***',
206
+ })));
207
+ }
208
+ res.json({ providers: allCreds });
209
+ } catch (error) {
210
+ res.status(500).json({ error: 'Failed to fetch AI provider keys' });
211
+ }
212
+ });
213
+
214
+ // Save an AI provider key
215
+ router.post('/ai-providers', async (req, res) => {
216
+ try {
217
+ const { providerType, apiKey, name } = req.body;
218
+
219
+ if (!providerType || !AI_PROVIDER_TYPES.includes(providerType)) {
220
+ return res.status(400).json({ error: `Invalid provider type. Supported: ${AI_PROVIDER_TYPES.join(', ')}` });
221
+ }
222
+ if (!apiKey || !apiKey.trim()) {
223
+ return res.status(400).json({ error: 'API key is required' });
224
+ }
225
+ if (apiKey.trim().length < 10 || apiKey.trim().length > 256) {
226
+ return res.status(400).json({ error: 'Invalid API key length' });
227
+ }
228
+
229
+ const label = providerType.replace('_key', '').replace('_', ' ');
230
+ const credName = name?.trim() || `${label} API key`;
231
+
232
+ // Deactivate existing keys of same type (user should only have one active per provider)
233
+ const existing = await credentialsDb.getCredentials(req.user.id, providerType);
234
+ for (const cred of existing) {
235
+ if (cred.is_active) {
236
+ await credentialsDb.toggleCredential(req.user.id, cred.id, false);
237
+ }
238
+ }
239
+
240
+ const result = await credentialsDb.createCredential(
241
+ req.user.id,
242
+ credName,
243
+ providerType,
244
+ apiKey.trim(),
245
+ `User-provided ${label} API key`
246
+ );
247
+
248
+ res.json({ success: true, credential: { id: result.id, credential_type: providerType, credential_name: credName } });
249
+ } catch (error) {
250
+ res.status(500).json({ error: 'Failed to save AI provider key' });
251
+ }
252
+ });
253
+
254
+ // Delete an AI provider key
255
+ router.delete('/ai-providers/:credentialId', async (req, res) => {
256
+ try {
257
+ const { credentialId } = req.params;
258
+ const success = await credentialsDb.deleteCredential(req.user.id, parseInt(credentialId));
259
+ if (success) {
260
+ res.json({ success: true });
261
+ } else {
262
+ res.status(404).json({ error: 'Provider key not found' });
263
+ }
264
+ } catch (error) {
265
+ res.status(500).json({ error: 'Failed to delete provider key' });
266
+ }
267
+ });
268
+
178
269
  export default router;
@@ -65,3 +65,32 @@ export const CODEX_MODELS = {
65
65
 
66
66
  DEFAULT: 'gpt-5.2'
67
67
  };
68
+
69
+ /**
70
+ * OpenRouter Models (BYOK — user brings their own API key)
71
+ * Access 200+ models from all major providers through a single API.
72
+ */
73
+ export const OPENROUTER_MODELS = {
74
+ OPTIONS: [
75
+ { value: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4' },
76
+ { value: 'anthropic/claude-opus-4', label: 'Claude Opus 4' },
77
+ { value: 'openai/gpt-4o', label: 'GPT-4o' },
78
+ { value: 'openai/o3', label: 'O3' },
79
+ { value: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
80
+ { value: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick' },
81
+ { value: 'mistralai/mistral-large', label: 'Mistral Large' },
82
+ { value: 'deepseek/deepseek-r1', label: 'DeepSeek R1' },
83
+ ],
84
+
85
+ DEFAULT: 'anthropic/claude-sonnet-4'
86
+ };
87
+
88
+ /**
89
+ * AI Provider types for BYOK (Bring Your Own Key)
90
+ */
91
+ export const AI_PROVIDER_TYPES = [
92
+ { type: 'anthropic_key', label: 'Anthropic', prefix: 'sk-ant-', placeholder: 'sk-ant-api03-...' },
93
+ { type: 'openai_key', label: 'OpenAI', prefix: 'sk-', placeholder: 'sk-...' },
94
+ { type: 'openrouter_key', label: 'OpenRouter', prefix: 'sk-or-', placeholder: 'sk-or-v1-...' },
95
+ { type: 'google_key', label: 'Google AI', prefix: 'AI', placeholder: 'AIza...' },
96
+ ];