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.
- 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 +255 -23
- package/server/constants/config.js +29 -3
- package/server/database/auth.db +0 -0
- package/server/index.js +380 -121
- package/server/mcp-server.js +2 -1
- package/server/middleware/auth.js +18 -8
- package/server/openrouter.js +137 -0
- package/server/relay-client.js +262 -18
- package/server/routes/agent.js +54 -19
- package/server/routes/auth.js +23 -12
- package/server/routes/commands.js +1 -1
- 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/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
6
|
+
let JWT_SECRET = process.env.JWT_SECRET?.trim();
|
|
6
7
|
if (!JWT_SECRET) {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
|
31
|
-
|
|
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
|
+
}
|
package/server/relay-client.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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('
|
|
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
|
-
*
|
|
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('
|
|
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 =
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
491
|
+
logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
|
|
306
492
|
setTimeout(connect, delay);
|
|
307
493
|
} else {
|
|
308
|
-
logRelayEvent('
|
|
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('
|
|
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
|
+
}
|
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
|
|