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.
- package/client/dist/assets/AppContent-DTZ2FbvM.js +513 -0
- package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +6 -0
- package/client/dist/assets/LoginModal-CWoFm0au.js +19 -0
- package/client/dist/assets/MarkdownPreview-CYdvwJaV.js +1 -0
- package/client/dist/assets/{Onboarding-Coxo6mFA.js → Onboarding-CtIoXiTp.js} +1 -1
- package/client/dist/assets/{SetupForm-BzYOsbji.js → SetupForm-B4p8im5O.js} +1 -1
- package/client/dist/assets/{ar-SA-G6X2FPQ2-Bmw2-hDt.js → ar-SA-G6X2FPQ2-2gfmdvHk.js} +1 -1
- package/client/dist/assets/{arc-BMqY7_Ci.js → arc-DCZSHhoJ.js} +1 -1
- package/client/dist/assets/{az-AZ-76LH7QW2-Dh1le_qs.js → az-AZ-76LH7QW2-CDdeucRZ.js} +1 -1
- package/client/dist/assets/{bg-BG-XCXSNQG7-Cbav8Z9z.js → bg-BG-XCXSNQG7-D6__XtOK.js} +1 -1
- package/client/dist/assets/{blockDiagram-38ab4fdb-ChHJxsXw.js → blockDiagram-38ab4fdb-Cfbaeyp6.js} +3 -3
- package/client/dist/assets/{bn-BD-2XOGV67Q-DCNjOaWz.js → bn-BD-2XOGV67Q-DHNJw3OG.js} +1 -1
- package/client/dist/assets/{c4Diagram-3d4e48cf-b8Xue4Z6.js → c4Diagram-3d4e48cf-BBCnjOTy.js} +1 -1
- package/client/dist/assets/{ca-ES-6MX7JW3Y-Dl_vM7NS.js → ca-ES-6MX7JW3Y-r5g4o3zQ.js} +1 -1
- package/client/dist/assets/channel-O3ovC0x9.js +1 -0
- package/client/dist/assets/{classDiagram-70f12bd4-BheP7Ggo.js → classDiagram-70f12bd4-D0lhAcxU.js} +1 -1
- package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +2 -0
- package/client/dist/assets/clone-BG9u7vLi.js +1 -0
- package/client/dist/assets/{createText-2e5e7dd3-_n4jI_fO.js → createText-2e5e7dd3-B8jCDmF_.js} +1 -1
- package/client/dist/assets/{cs-CZ-2BRQDIVT-ftsKDdz4.js → cs-CZ-2BRQDIVT-p08jRLRC.js} +1 -1
- package/client/dist/assets/{da-DK-5WZEPLOC-DAjdwGRO.js → da-DK-5WZEPLOC-CnhOImFf.js} +1 -1
- package/client/dist/assets/{de-DE-XR44H4JA-BJXczHGT.js → de-DE-XR44H4JA-BunSXZ-Y.js} +1 -1
- package/client/dist/assets/{edges-e0da2a9e-CfPZr4YM.js → edges-e0da2a9e-CGBBhG8k.js} +2 -2
- package/client/dist/assets/{el-GR-BZB4AONW-DW2p_uy7.js → el-GR-BZB4AONW-D4wv1oIz.js} +1 -1
- package/client/dist/assets/{erDiagram-9861fffd-CF33V-Of.js → erDiagram-9861fffd-CYaF3q1I.js} +1 -1
- package/client/dist/assets/{es-ES-U4NZUMDT-DLOIGnrl.js → es-ES-U4NZUMDT-CGeTKXgd.js} +1 -1
- package/client/dist/assets/{eu-ES-A7QVB2H4-LJXbf89m.js → eu-ES-A7QVB2H4-Cayx1TxR.js} +1 -1
- package/client/dist/assets/{fa-IR-HGAKTJCU-Dvx65fgW.js → fa-IR-HGAKTJCU-CmUg8pmw.js} +1 -1
- package/client/dist/assets/{fi-FI-Z5N7JZ37-EoL65BQh.js → fi-FI-Z5N7JZ37-xvHcPhsU.js} +1 -1
- package/client/dist/assets/{flowDb-956e92f1-HgoXVy2H.js → flowDb-956e92f1-C-_LFz70.js} +3 -3
- package/client/dist/assets/flowDiagram-66a62f08-C1sHdSjn.js +4 -0
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +1 -0
- package/client/dist/assets/{flowchart-elk-definition-4a651766-DJbI2dpv.js → flowchart-elk-definition-4a651766-CNGfpudb.js} +7 -7
- package/client/dist/assets/{fr-FR-RHASNOE6-DNk_jdDs.js → fr-FR-RHASNOE6-DBoHEcNj.js} +1 -1
- package/client/dist/assets/{ganttDiagram-c361ad54-2XX670FU.js → ganttDiagram-c361ad54-B8HJQqjt.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-72cf32ee-CcUfruAo.js → gitGraphDiagram-72cf32ee-DojCDvlS.js} +1 -1
- package/client/dist/assets/{gl-ES-HMX3MZ6V-dxzFjZlG.js → gl-ES-HMX3MZ6V-p6hrn2cN.js} +1 -1
- package/client/dist/assets/{graph-BSbiMSBC.js → graph-DXM7lcy1.js} +1 -1
- package/client/dist/assets/{he-IL-6SHJWFNN-Cogsfdt1.js → he-IL-6SHJWFNN-y2jEX6-0.js} +1 -1
- package/client/dist/assets/{hi-IN-IWLTKZ5I-L6wbgi4F.js → hi-IN-IWLTKZ5I-99pNfyWr.js} +1 -1
- package/client/dist/assets/{hu-HU-A5ZG7DT2-DSA6ZDsH.js → hu-HU-A5ZG7DT2-hygceGMS.js} +1 -1
- package/client/dist/assets/{id-ID-SAP4L64H-BK_vGGS6.js → id-ID-SAP4L64H-CyIqi1hv.js} +1 -1
- package/client/dist/assets/{image-blob-reduce.esm-BLtmMM_J.js → image-blob-reduce.esm-D6s-rqMO.js} +6 -1
- package/client/dist/assets/{index-3862675e-Bv32HUgT.js → index-3862675e-4idOQN2N.js} +1 -1
- package/client/dist/assets/{index-BPwf8Fw3.js → index-BGmwbRlb.js} +6 -6
- package/client/dist/assets/index-BHZfFT_V.js +97 -0
- package/client/dist/assets/{infoDiagram-f8f76790-w4mR4pxn.js → infoDiagram-f8f76790-CFLrHqtc.js} +1 -1
- package/client/dist/assets/{it-IT-JPQ66NNP-BLdHYMhn.js → it-IT-JPQ66NNP-DzVvVdQI.js} +1 -1
- package/client/dist/assets/{ja-JP-DBVTYXUO-B_vmexl_.js → ja-JP-DBVTYXUO-BI4fPexV.js} +1 -1
- package/client/dist/assets/{journeyDiagram-49397b02-D9nmO17e.js → journeyDiagram-49397b02-C3CFDo8z.js} +1 -1
- package/client/dist/assets/{kaa-6HZHGXH3-5s-3jl6F.js → kaa-6HZHGXH3-fwOleoQB.js} +1 -1
- package/client/dist/assets/{kab-KAB-ZGHBKWFO-2QaVDuSf.js → kab-KAB-ZGHBKWFO-DBI_ri48.js} +1 -1
- package/client/dist/assets/{kk-KZ-P5N5QNE5-CTC52Vbi.js → kk-KZ-P5N5QNE5-zpl7uvyF.js} +1 -1
- package/client/dist/assets/{km-KH-HSX4SM5Z-DxawH8UZ.js → km-KH-HSX4SM5Z-DOMFSres.js} +1 -1
- package/client/dist/assets/{ko-KR-MTYHY66A-CmosEM8_.js → ko-KR-MTYHY66A-tb08hXzd.js} +1 -1
- package/client/dist/assets/{ku-TR-6OUDTVRD-DbiLen4y.js → ku-TR-6OUDTVRD-DlIQCCY4.js} +1 -1
- package/client/dist/assets/{layout-jmt3H9tA.js → layout-B_11mCXA.js} +1 -1
- package/client/dist/assets/{line-JTlRayUJ.js → line-B-qmK_vI.js} +1 -1
- package/client/dist/assets/{linear-DJeB5p7x.js → linear-Ph6uuYcX.js} +1 -1
- package/client/dist/assets/{lt-LT-XHIRWOB4-CH15wrjA.js → lt-LT-XHIRWOB4--qWy24_Z.js} +1 -1
- package/client/dist/assets/{lv-LV-5QDEKY6T-dhgfPuCQ.js → lv-LV-5QDEKY6T-Bnd_1GDb.js} +1 -1
- package/client/dist/assets/mindmap-definition-fc14e90a-Do79tIc0.js +425 -0
- package/client/dist/assets/{mr-IN-CRQNXWMA-3Gi6iq7A.js → mr-IN-CRQNXWMA-BsV6HaD9.js} +1 -1
- package/client/dist/assets/{my-MM-5M5IBNSE-CpH4rdJj.js → my-MM-5M5IBNSE-kZQURVIi.js} +1 -1
- package/client/dist/assets/{nb-NO-T6EIAALU-Du6iiGql.js → nb-NO-T6EIAALU-Cvf9FdSF.js} +1 -1
- package/client/dist/assets/{nl-NL-IS3SIHDZ-BGvsd1MT.js → nl-NL-IS3SIHDZ-DA1yqpXw.js} +1 -1
- package/client/dist/assets/{nn-NO-6E72VCQL-B-odvJZW.js → nn-NO-6E72VCQL-89lm3vku.js} +1 -1
- package/client/dist/assets/{oc-FR-POXYY2M6-COC8xNjo.js → oc-FR-POXYY2M6-BsrjTJQh.js} +1 -1
- package/client/dist/assets/{pa-IN-N4M65BXN-CE21PUQH.js → pa-IN-N4M65BXN-CczefYaj.js} +1 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
- package/client/dist/assets/percentages-BXMCSKIN-Be6p9phi.js +207 -0
- package/client/dist/assets/pica-CQIY57Tf.js +7 -0
- package/client/dist/assets/{pieDiagram-8a3498a8-Cvfh7Qr5.js → pieDiagram-8a3498a8-CfblQHdm.js} +2 -2
- package/client/dist/assets/{pl-PL-T2D74RX3-D4xFVSoT.js → pl-PL-T2D74RX3-DdhH-zcK.js} +1 -1
- package/client/dist/assets/{pt-BR-5N22H2LF-CCq257gA.js → pt-BR-5N22H2LF-gpwlheL6.js} +1 -1
- package/client/dist/assets/{pt-PT-UZXXM6DQ-1l8gt5vA.js → pt-PT-UZXXM6DQ-Cs87vICi.js} +1 -1
- package/client/dist/assets/{quadrantDiagram-120e2f19-BA0js1aD.js → quadrantDiagram-120e2f19-CRMSamSP.js} +1 -1
- package/client/dist/assets/{requirementDiagram-deff3bca-B0QNFfIn.js → requirementDiagram-deff3bca-D3LBN016.js} +1 -1
- package/client/dist/assets/{ro-RO-JPDTUUEW-yosBW01E.js → ro-RO-JPDTUUEW-CWTSJ1Dt.js} +1 -1
- package/client/dist/assets/roundRect-0PYZxl1G.js +1 -0
- package/client/dist/assets/{ru-RU-B4JR7IUQ-8LkEJUix.js → ru-RU-B4JR7IUQ-Bq7aN2ep.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-04a897e0-D4T9eCXn.js → sankeyDiagram-04a897e0-CsFqOQZN.js} +3 -3
- package/client/dist/assets/{sequenceDiagram-704730f1-CfBUTCrO.js → sequenceDiagram-704730f1-BRYXVDGX.js} +1 -1
- package/client/dist/assets/{si-LK-N5RQ5JYF-D8rjbqtd.js → si-LK-N5RQ5JYF-BBjcNYQh.js} +1 -1
- package/client/dist/assets/{sk-SK-C5VTKIMK-Bg14sAzN.js → sk-SK-C5VTKIMK-ByjKQzUb.js} +1 -1
- package/client/dist/assets/{sl-SI-NN7IZMDC-CMTib6Zs.js → sl-SI-NN7IZMDC-B8WCyMBU.js} +1 -1
- package/client/dist/assets/{stateDiagram-587899a1-BGgvmVSZ.js → stateDiagram-587899a1-BHoy9LtD.js} +1 -1
- package/client/dist/assets/{stateDiagram-v2-d93cdb3a-Qn3DpYuO.js → stateDiagram-v2-d93cdb3a-BvMUA6bS.js} +1 -1
- package/client/dist/assets/{styles-6aaf32cf-IdVZLPrD.js → styles-6aaf32cf-Dr-lfIOW.js} +2 -2
- package/client/dist/assets/{styles-9a916d00-BAC3L45X.js → styles-9a916d00-DS4wRpL7.js} +1 -1
- package/client/dist/assets/{styles-c10674c1-COhXxX8c.js → styles-c10674c1-nKRF6NrH.js} +1 -1
- package/client/dist/assets/{subset-shared.chunk-BWHnFai4.js → subset-shared.chunk-KT79s7KG.js} +64 -2
- package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +1 -0
- package/client/dist/assets/{sv-SE-XGPEYMSR-C1425rOF.js → sv-SE-XGPEYMSR-BiIPUVbv.js} +1 -1
- package/client/dist/assets/{svgDrawCommon-08f97a94-Cfk-fgnN.js → svgDrawCommon-08f97a94-C3uP9PYr.js} +1 -1
- package/client/dist/assets/{ta-IN-2NMHFXQM-BHHo1zpF.js → ta-IN-2NMHFXQM-Cidadso2.js} +1 -1
- package/client/dist/assets/{th-TH-HPSO5L25-CZVzm_WT.js → th-TH-HPSO5L25-CFNnJwSv.js} +1 -1
- package/client/dist/assets/{timeline-definition-85554ec2-VAvuJith.js → timeline-definition-85554ec2-BSsLsIgF.js} +1 -1
- package/client/dist/assets/{tr-TR-DEFEU3FU-DE1lclCq.js → tr-TR-DEFEU3FU-DaFcI-KL.js} +1 -1
- package/client/dist/assets/{uk-UA-QMV73CPH-D4lJZ85O.js → uk-UA-QMV73CPH-DkBW36St.js} +1 -1
- package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +20 -0
- package/client/dist/assets/vendor-codemirror-rix45NST.js +16 -0
- package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
- package/client/dist/assets/vendor-icons-Dh9m_Ydt.js +596 -0
- package/client/dist/assets/{vendor-markdown-CIVH08vJ.js → vendor-markdown-BXEi_H3G.js} +3 -3
- package/client/dist/assets/vendor-react-9mUTKBHH.js +67 -0
- package/client/dist/assets/{vendor-syntax-Djb62v3a.js → vendor-syntax-DnmwQQJF.js} +14 -7
- package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
- package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
- package/client/dist/assets/{vi-VN-M7AON7JQ-Dgc_SShk.js → vi-VN-M7AON7JQ-KrtfxOzl.js} +1 -1
- package/client/dist/assets/{xychartDiagram-e933f94c-BeyVBJhb.js → xychartDiagram-e933f94c-CgNgZ4pp.js} +1 -1
- package/client/dist/assets/{zh-CN-LNUGB5OW-MH4Yh8in.js → zh-CN-LNUGB5OW-BQu12RoD.js} +1 -1
- package/client/dist/assets/{zh-HK-E62DVLB3-D4XHehjx.js → zh-HK-E62DVLB3-zx9CvERq.js} +1 -1
- package/client/dist/assets/{zh-TW-RAJ6MFWO--efj3evj.js → zh-TW-RAJ6MFWO-ffJWgVxn.js} +1 -1
- package/client/dist/index.html +128 -66
- package/client/dist/manifest.json +61 -15
- package/client/dist/sw.js +19 -55
- package/commands/upfynai-connect.md +31 -18
- package/commands/upfynai.md +45 -26
- package/package.json +1 -1
- package/server/cli-ui.js +320 -169
- package/server/cli.js +194 -21
- package/server/index.js +159 -107
- package/server/middleware/auth.js +9 -5
- package/server/openrouter.js +137 -0
- package/server/relay-client.js +104 -17
- package/server/routes/agent.js +54 -19
- package/server/routes/auth.js +23 -12
- package/server/routes/settings.js +91 -0
- package/shared/modelConstants.js +29 -0
- package/client/dist/assets/AppContent-CTSHQdyq.js +0 -513
- package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +0 -6
- package/client/dist/assets/LoginModal-silya-zP.js +0 -11
- package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +0 -1
- package/client/dist/assets/channel-CSnvHe_M.js +0 -1
- package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +0 -2
- package/client/dist/assets/clone-B75abXxS.js +0 -1
- package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +0 -4
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +0 -1
- package/client/dist/assets/index-BnXuHrpJ.js +0 -523
- package/client/dist/assets/index-BwxNox94.css +0 -1
- package/client/dist/assets/index-D1urGMYu.js +0 -95
- package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +0 -415
- package/client/dist/assets/pdf-TYrZqVzP.js +0 -12
- package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +0 -199
- package/client/dist/assets/pica-VkdyTzi8.js +0 -2
- package/client/dist/assets/roundRect-mAH3dD0p.js +0 -1
- package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +0 -1
- package/client/dist/assets/vendor-codemirror-BARtJV1V.js +0 -16
- package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +0 -20
- package/client/dist/assets/vendor-i18n-ByAl-gdx.js +0 -1
- package/client/dist/assets/vendor-icons-D33IkSIf.js +0 -1
- package/client/dist/assets/vendor-react-CHoMc7ka.js +0 -8
- package/client/dist/assets/vendor-xterm-DBb3RXlu.js +0 -66
- package/client/dist/assets/vendor-xterm-DrlLKa8f.css +0 -1
- package/client/dist/llms.txt +0 -40
- package/client/dist/robots.txt +0 -11
- package/client/dist/sitemap.xml +0 -45
package/server/relay-client.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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('
|
|
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
|
-
*
|
|
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('
|
|
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 =
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
334
|
+
logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
|
|
306
335
|
setTimeout(connect, delay);
|
|
307
336
|
} else {
|
|
308
|
-
logRelayEvent('
|
|
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('
|
|
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
|
+
}
|
package/server/routes/agent.js
CHANGED
|
@@ -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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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,
|
|
984
|
+
sessionId: null,
|
|
961
985
|
model: model || undefined,
|
|
962
|
-
skipPermissions: true
|
|
986
|
+
skipPermissions: true
|
|
963
987
|
}, writer);
|
|
964
988
|
} else if (provider === 'codex') {
|
|
965
989
|
console.log('🤖 Starting Codex SDK session');
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
|
package/server/routes/auth.js
CHANGED
|
@@ -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',
|
|
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 <
|
|
32
|
-
return res.status(400).json({ error: 'Password must be at least
|
|
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
|
-
//
|
|
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',
|
|
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;
|
package/shared/modelConstants.js
CHANGED
|
@@ -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
|
+
];
|