orbitchat 2.14.1 → 3.1.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/README.md +60 -90
- package/bin/orbitchat.js +408 -429
- package/dist/assets/ChartRenderer-BEwTOgnq.js +80 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MermaidRenderer-Cwj5RQ3g.js +260 -0
- package/dist/assets/MusicRenderer-jiCOczly.js +18 -0
- package/dist/assets/SVGRenderer-B7MdKpL3.js +6 -0
- package/dist/assets/_basePickBy-CYc6FJVo.js +1 -0
- package/dist/assets/_baseUniq-Db9U29CG.js +1 -0
- package/dist/assets/arc-BSnyvXPh.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-ByDEpAtB.js +36 -0
- package/dist/assets/band-CquvqAHh.js +1 -0
- package/dist/assets/blockDiagram-VD42YOAC-DeU2cMg9.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-C9ykT6JL.js +10 -0
- package/dist/assets/channel-C4M311uf.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-DSGrQP0T.js +1 -0
- package/dist/assets/chunk-55IACEB6-BqJf9Ja5.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-CjLmShZ1.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-DpYiJhBX.js +220 -0
- package/dist/assets/{chunk-FMBD7UC4-dPK7Boav-tO950uDG.js → chunk-FMBD7UC4-B_-ci5rY.js} +2 -2
- package/dist/assets/chunk-QN33PNHL-PupYKHzM.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-Ddt7KdjH.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-DFxHvBUh.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-Brx1b2Nn.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-Brx1b2Nn.js +1 -0
- package/dist/assets/clone-C6___fhg.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CoAwh2dl.js +1 -0
- package/dist/assets/cytoscape.esm-CyJtwmzi.js +331 -0
- package/dist/assets/dagre-6UL2VRFP-wqnQLI1r.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-BThDYp3k.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-QO0jbKYo.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-DqXZQ_2w.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-BbHMUMHv.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-BcZTJeWn.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-n27iVieA.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-DaO03D0c.js +65 -0
- package/dist/assets/graph-CqgsEnyP.js +1 -0
- package/dist/assets/index-BLVU3y3C.js +624 -0
- package/dist/assets/index-DEMLXEhu.css +1 -0
- package/dist/assets/index-pVqH5W3l.js +134 -0
- package/dist/assets/infoDiagram-HS3SLOUP-DA9UPsCv.js +2 -0
- package/dist/assets/init-Dmth1JHB.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-DOnOrs8o.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-CeCakzDJ.js +89 -0
- package/dist/assets/layout-D9Y_lOuO.js +1 -0
- package/dist/assets/linear-CKzVTQ0r.js +1 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-DBP7KZgR.js +68 -0
- package/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-DSpFXjVs.js +30 -0
- package/dist/assets/purify.es-A66Cw1IH.js +2 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-1w3noe3A.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-B9YzwP8J.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-FLiuEFVI.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-BSJt3OHN.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-Ch3MODOU.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-CT2uJHYZ.js +1 -0
- package/dist/assets/step-EjIQ8UIn.js +1 -0
- package/dist/assets/time-h5EapSZu.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-C_qTQlCc.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-yzuGqs4o.js +160 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-G6i_3Vla.js +7 -0
- package/dist/favicon.svg +3 -1
- package/dist/index.html +2 -2
- package/dist/logo.png +0 -0
- package/markdown-renderer/LICENSE +201 -0
- package/markdown-renderer/src/CodeBlock.tsx +332 -0
- package/markdown-renderer/src/MarkdownComponents.tsx +233 -0
- package/markdown-renderer/src/MarkdownStyles.css +668 -0
- package/markdown-renderer/src/css.d.ts +4 -0
- package/markdown-renderer/src/index.ts +32 -0
- package/markdown-renderer/src/preprocessing.ts +519 -0
- package/markdown-renderer/src/renderers/ChartRenderer.tsx +1434 -0
- package/markdown-renderer/src/renderers/MermaidRenderer.tsx +474 -0
- package/markdown-renderer/src/renderers/MusicRenderer.tsx +394 -0
- package/markdown-renderer/src/renderers/SVGRenderer.tsx +307 -0
- package/markdown-renderer/src/types.ts +174 -0
- package/orbitchat.sh +282 -0
- package/package.json +30 -4
- package/dist/assets/_baseUniq-BRKsqoH--CzpIYnbF.js +0 -1
- package/dist/assets/arc-pab_su9s-DhurQhL4.js +0 -1
- package/dist/assets/architectureDiagram-VXUJARFQ-DqQ8r_6g-eqA8fOch.js +0 -36
- package/dist/assets/blockDiagram-VD42YOAC-B-dKfcH3-DT3R176S.js +0 -122
- package/dist/assets/c4Diagram-YG6GDRKO-DMUPaBEl-q6bAZUce.js +0 -10
- package/dist/assets/channel-HKsfPa5q-DshlUHWu.js +0 -1
- package/dist/assets/chunk-4BX2VUAB-CX67kh_B-gkWPpLiy.js +0 -1
- package/dist/assets/chunk-55IACEB6-BocSyyvr-9ksZ29Ab.js +0 -1
- package/dist/assets/chunk-B4BG7PRW-CO8QAyfE-CNBeArXO.js +0 -165
- package/dist/assets/chunk-DI55MBZ5-Dw1L6Eos-BVbGVMNG.js +0 -220
- package/dist/assets/chunk-QN33PNHL-vP2PqfVG-Y3PYDsm9.js +0 -1
- package/dist/assets/chunk-QZHKN3VN-Bcidzu63-BAQ3PYro.js +0 -1
- package/dist/assets/chunk-TZMSLE5B-BtljMjlg-CwIvybWR.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-C7cYN9hv-CGN1Rq_o.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-C7cYN9hv-CGN1Rq_o.js +0 -1
- package/dist/assets/clone-DoPb9X13-C3rfr14F.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-BGzO4EsH-DtYXpCOp.js +0 -1
- package/dist/assets/cytoscape.esm-CjI2IsL8-Da6dFVsf.js +0 -331
- package/dist/assets/dagre-6UL2VRFP-TzNvXCds-xMthYtzg.js +0 -4
- package/dist/assets/diagram-PSM6KHXK-BqY4RpUg-CtQy_-7V.js +0 -24
- package/dist/assets/diagram-QEK2KX5R-CTjgBsne-DROkeGJL.js +0 -43
- package/dist/assets/diagram-S2PKOQOG-BqrhTIpA-Bv0e-7TM.js +0 -24
- package/dist/assets/erDiagram-Q2GNP2WA-B2hsi_Tl-BbvH-Jb1.js +0 -60
- package/dist/assets/flowDiagram-NV44I4VS-C03vtt_F-DT9Y-btb.js +0 -162
- package/dist/assets/ganttDiagram-JELNMOA3-B3hAg964-CnqUJtIB.js +0 -267
- package/dist/assets/gitGraphDiagram-NY62KEGX-ByhMH0yZ-CdQlDU7A.js +0 -65
- package/dist/assets/graph-BmNkcFEM-mCcAnvHY.js +0 -1
- package/dist/assets/index-BXexqYFc-D_cjFmh8.js +0 -134
- package/dist/assets/index-DBOy9259.css +0 -1
- package/dist/assets/index-zlswAhjt.js +0 -1010
- package/dist/assets/infoDiagram-WHAUD3N6-is6Ho4-T-BHS1wv7y.js +0 -2
- package/dist/assets/journeyDiagram-XKPGCS4Q-CRTOL26C-DkQ68DJ6.js +0 -139
- package/dist/assets/kanban-definition-3W4ZIXB7-CNnO_t6O-DWZm5JX4.js +0 -89
- package/dist/assets/layout-C0kZPebx-CygTh9Uo.js +0 -1
- package/dist/assets/min-7Gb0pNxh-BfuBtSqq.js +0 -1
- package/dist/assets/mindmap-definition-VGOIOE7T-CJZ2wTTa-B7rvUdG3.js +0 -68
- package/dist/assets/pieDiagram-ADFJNKIX-C9OSknjr-eiZAefZN.js +0 -30
- package/dist/assets/quadrantDiagram-AYHSOK5B-CW8yuAqv-DzBEpWQk.js +0 -7
- package/dist/assets/requirementDiagram-UZGBJVZJ-nGPhruO1-z5Z4Fwg9.js +0 -64
- package/dist/assets/sankeyDiagram-TZEHDZUN-CmL90u-m-Dk028WXo.js +0 -10
- package/dist/assets/sequenceDiagram-WL72ISMW-B02VRcnM-Cz8sPx_Q.js +0 -145
- package/dist/assets/stateDiagram-FKZM4ZOC-DjoyLUdz-BxsZBi1n.js +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Bq76BTB7-BTybFIdr.js +0 -1
- package/dist/assets/timeline-definition-IT6M3QCI-CTn0Gm3T-CI07_-Ei.js +0 -61
- package/dist/assets/treemap-KMMF4GRG-BjgLKKyi-Dg9N7uMT.js +0 -128
- package/dist/assets/xychartDiagram-PRI3JC2R-Cgg6Uija-BDR2Lfu_.js +0 -7
package/bin/orbitchat.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* ORBIT Chat CLI
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Serves the chat-app as a standalone application with runtime configuration.
|
|
6
|
-
* Configuration
|
|
7
|
-
*
|
|
6
|
+
* Configuration is read from orbitchat.yaml (CWD by default, overridable via --config).
|
|
7
|
+
* Secrets (adapter API keys) come from VITE_ADAPTERS / ORBIT_ADAPTERS env var.
|
|
8
|
+
*
|
|
8
9
|
* The server acts as a proxy to hide API keys from the client by mapping
|
|
9
10
|
* adapter names to actual API keys.
|
|
10
11
|
*/
|
|
@@ -12,34 +13,101 @@
|
|
|
12
13
|
import express from 'express';
|
|
13
14
|
import fs from 'fs';
|
|
14
15
|
import path from 'path';
|
|
15
|
-
import { homedir } from 'os';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
18
18
|
import { dirname } from 'path';
|
|
19
19
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
20
|
+
import yaml from 'js-yaml';
|
|
21
|
+
import rateLimit from 'express-rate-limit';
|
|
20
22
|
|
|
21
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
24
|
const __dirname = dirname(__filename);
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// ---- Minimal .env loader (CLI mode) ----
|
|
27
|
+
|
|
28
|
+
function parseDotEnvValue(raw) {
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed) return '';
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
34
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
35
|
+
) {
|
|
36
|
+
return trimmed.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadDotEnvFromFile(filePath) {
|
|
43
|
+
if (!fs.existsSync(filePath)) return;
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const trimmed = lines[i].trim();
|
|
49
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
50
|
+
|
|
51
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
52
|
+
if (equalsIndex <= 0) continue;
|
|
53
|
+
|
|
54
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
55
|
+
if (!key) continue;
|
|
56
|
+
|
|
57
|
+
if (process.env[key] !== undefined) continue;
|
|
58
|
+
|
|
59
|
+
let valueRaw = trimmed.slice(equalsIndex + 1);
|
|
60
|
+
const startsWithDouble = valueRaw.startsWith('"');
|
|
61
|
+
const startsWithSingle = valueRaw.startsWith("'");
|
|
62
|
+
|
|
63
|
+
// Support simple multiline quoted values, useful for formatted JSON values.
|
|
64
|
+
if (
|
|
65
|
+
(startsWithDouble && !valueRaw.endsWith('"')) ||
|
|
66
|
+
(startsWithSingle && !valueRaw.endsWith("'"))
|
|
67
|
+
) {
|
|
68
|
+
const quote = startsWithDouble ? '"' : "'";
|
|
69
|
+
while (i + 1 < lines.length) {
|
|
70
|
+
i += 1;
|
|
71
|
+
valueRaw += `\n${lines[i]}`;
|
|
72
|
+
if (lines[i].trim().endsWith(quote)) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const value = parseDotEnvValue(valueRaw);
|
|
79
|
+
process.env[key] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadDotEnv(baseDir) {
|
|
84
|
+
// Same precedence idea as Vite: .env then .env.local; do not override exported vars.
|
|
85
|
+
loadDotEnvFromFile(path.join(baseDir, '.env'));
|
|
86
|
+
loadDotEnvFromFile(path.join(baseDir, '.env.local'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- Defaults (must match DEFAULTS in src/utils/runtimeConfig.ts) ----
|
|
90
|
+
|
|
91
|
+
const DEFAULTS = {
|
|
27
92
|
apiUrl: 'http://localhost:3000',
|
|
28
93
|
defaultKey: 'default-key',
|
|
29
94
|
applicationName: 'ORBIT Chat',
|
|
30
95
|
applicationDescription: "Explore ideas with ORBIT's AI copilots, share context, and build together.",
|
|
31
96
|
defaultInputPlaceholder: 'Message ORBIT...',
|
|
32
|
-
useLocalApi: false,
|
|
33
|
-
localApiPath: undefined,
|
|
34
97
|
consoleDebug: false,
|
|
98
|
+
locale: 'en-US',
|
|
35
99
|
enableUploadButton: false,
|
|
36
100
|
enableAudioOutput: false,
|
|
37
101
|
enableAudioInput: false,
|
|
38
102
|
enableFeedbackButtons: false,
|
|
103
|
+
enableConversationThreads: true,
|
|
39
104
|
enableAutocomplete: false,
|
|
40
105
|
voiceSilenceTimeoutMs: 4000,
|
|
41
106
|
voiceRecognitionLanguage: '',
|
|
107
|
+
showGitHubStats: true,
|
|
42
108
|
outOfServiceMessage: null,
|
|
109
|
+
githubOwner: 'schmitech',
|
|
110
|
+
githubRepo: 'orbit',
|
|
43
111
|
maxFilesPerConversation: 5,
|
|
44
112
|
maxFileSizeMB: 50,
|
|
45
113
|
maxTotalFiles: 100,
|
|
@@ -48,14 +116,157 @@ const DEFAULT_CONFIG = {
|
|
|
48
116
|
maxMessagesPerThread: 1000,
|
|
49
117
|
maxTotalMessages: 10000,
|
|
50
118
|
maxMessageLength: 1000,
|
|
119
|
+
guestMaxConversations: 1,
|
|
120
|
+
guestMaxMessagesPerConversation: 10,
|
|
121
|
+
guestMaxTotalMessages: 10,
|
|
122
|
+
guestMaxMessagesPerThread: 10,
|
|
123
|
+
guestMaxFilesPerConversation: 1,
|
|
124
|
+
guestMaxTotalFiles: 2,
|
|
125
|
+
guestMaxMessageLength: 500,
|
|
126
|
+
guestMaxFileSizeMB: 10,
|
|
51
127
|
settingsAboutMsg: 'ORBIT Chat',
|
|
128
|
+
enableAuth: false,
|
|
129
|
+
authDomain: '',
|
|
130
|
+
authClientId: '',
|
|
131
|
+
authAudience: '',
|
|
132
|
+
enableHeader: false,
|
|
133
|
+
headerLogoUrl: '',
|
|
134
|
+
headerBrandName: '',
|
|
135
|
+
headerBgColor: '',
|
|
136
|
+
headerTextColor: '',
|
|
137
|
+
headerNavLinks: [],
|
|
138
|
+
enableFooter: false,
|
|
139
|
+
footerText: '',
|
|
140
|
+
footerBgColor: '',
|
|
141
|
+
footerTextColor: '',
|
|
142
|
+
footerNavLinks: [],
|
|
52
143
|
};
|
|
53
144
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
145
|
+
// ---- YAML config loading ----
|
|
146
|
+
|
|
147
|
+
function flattenYamlConfig(y) {
|
|
148
|
+
const f = {};
|
|
149
|
+
if (y.application) {
|
|
150
|
+
const a = y.application;
|
|
151
|
+
if (a.name !== undefined) f.applicationName = a.name;
|
|
152
|
+
if (a.description !== undefined) f.applicationDescription = a.description;
|
|
153
|
+
if (a.inputPlaceholder !== undefined) f.defaultInputPlaceholder = a.inputPlaceholder;
|
|
154
|
+
if (a.settingsAboutMsg !== undefined) f.settingsAboutMsg = a.settingsAboutMsg;
|
|
155
|
+
if (a.locale !== undefined) f.locale = a.locale;
|
|
156
|
+
}
|
|
157
|
+
if (y.api) {
|
|
158
|
+
if (y.api.url !== undefined) f.apiUrl = y.api.url;
|
|
159
|
+
if (y.api.defaultAdapter !== undefined) f.defaultKey = y.api.defaultAdapter;
|
|
160
|
+
}
|
|
161
|
+
if (y.debug) {
|
|
162
|
+
if (y.debug.consoleDebug !== undefined) f.consoleDebug = y.debug.consoleDebug;
|
|
163
|
+
}
|
|
164
|
+
if (y.features) {
|
|
165
|
+
const fe = y.features;
|
|
166
|
+
if (fe.enableUpload !== undefined) f.enableUploadButton = fe.enableUpload;
|
|
167
|
+
if (fe.enableAudioOutput !== undefined) f.enableAudioOutput = fe.enableAudioOutput;
|
|
168
|
+
if (fe.enableAudioInput !== undefined) f.enableAudioInput = fe.enableAudioInput;
|
|
169
|
+
if (fe.enableFeedbackButtons !== undefined) f.enableFeedbackButtons = fe.enableFeedbackButtons;
|
|
170
|
+
if (fe.enableConversationThreads !== undefined) f.enableConversationThreads = fe.enableConversationThreads;
|
|
171
|
+
if (fe.enableAutocomplete !== undefined) f.enableAutocomplete = fe.enableAutocomplete;
|
|
172
|
+
}
|
|
173
|
+
if (y.voice) {
|
|
174
|
+
if (y.voice.silenceTimeoutMs !== undefined) f.voiceSilenceTimeoutMs = y.voice.silenceTimeoutMs;
|
|
175
|
+
if (y.voice.recognitionLanguage !== undefined) f.voiceRecognitionLanguage = y.voice.recognitionLanguage;
|
|
176
|
+
}
|
|
177
|
+
if (y.github) {
|
|
178
|
+
if (y.github.showStats !== undefined) f.showGitHubStats = y.github.showStats;
|
|
179
|
+
if (y.github.owner !== undefined) f.githubOwner = y.github.owner;
|
|
180
|
+
if (y.github.repo !== undefined) f.githubRepo = y.github.repo;
|
|
58
181
|
}
|
|
182
|
+
if (y.outOfServiceMessage !== undefined) f.outOfServiceMessage = y.outOfServiceMessage;
|
|
183
|
+
if (y.limits) {
|
|
184
|
+
const l = y.limits;
|
|
185
|
+
if (l.files) {
|
|
186
|
+
if (l.files.perConversation !== undefined) f.maxFilesPerConversation = l.files.perConversation;
|
|
187
|
+
if (l.files.maxSizeMB !== undefined) f.maxFileSizeMB = l.files.maxSizeMB;
|
|
188
|
+
if (l.files.totalFiles !== undefined) f.maxTotalFiles = l.files.totalFiles;
|
|
189
|
+
}
|
|
190
|
+
if (l.conversations) {
|
|
191
|
+
if (l.conversations.maxConversations !== undefined) f.maxConversations = l.conversations.maxConversations;
|
|
192
|
+
if (l.conversations.messagesPerConversation !== undefined) f.maxMessagesPerConversation = l.conversations.messagesPerConversation;
|
|
193
|
+
if (l.conversations.messagesPerThread !== undefined) f.maxMessagesPerThread = l.conversations.messagesPerThread;
|
|
194
|
+
if (l.conversations.totalMessages !== undefined) f.maxTotalMessages = l.conversations.totalMessages;
|
|
195
|
+
}
|
|
196
|
+
if (l.messages) {
|
|
197
|
+
if (l.messages.maxLength !== undefined) f.maxMessageLength = l.messages.maxLength;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (y.guestLimits) {
|
|
201
|
+
const g = y.guestLimits;
|
|
202
|
+
if (g.files) {
|
|
203
|
+
if (g.files.perConversation !== undefined) f.guestMaxFilesPerConversation = g.files.perConversation;
|
|
204
|
+
if (g.files.maxSizeMB !== undefined) f.guestMaxFileSizeMB = g.files.maxSizeMB;
|
|
205
|
+
if (g.files.totalFiles !== undefined) f.guestMaxTotalFiles = g.files.totalFiles;
|
|
206
|
+
}
|
|
207
|
+
if (g.conversations) {
|
|
208
|
+
if (g.conversations.maxConversations !== undefined) f.guestMaxConversations = g.conversations.maxConversations;
|
|
209
|
+
if (g.conversations.messagesPerConversation !== undefined) f.guestMaxMessagesPerConversation = g.conversations.messagesPerConversation;
|
|
210
|
+
if (g.conversations.messagesPerThread !== undefined) f.guestMaxMessagesPerThread = g.conversations.messagesPerThread;
|
|
211
|
+
if (g.conversations.totalMessages !== undefined) f.guestMaxTotalMessages = g.conversations.totalMessages;
|
|
212
|
+
}
|
|
213
|
+
if (g.messages) {
|
|
214
|
+
if (g.messages.maxLength !== undefined) f.guestMaxMessageLength = g.messages.maxLength;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (y.auth) {
|
|
218
|
+
if (y.auth.enabled !== undefined) f.enableAuth = y.auth.enabled;
|
|
219
|
+
}
|
|
220
|
+
if (y.header) {
|
|
221
|
+
const h = y.header;
|
|
222
|
+
if (h.enabled !== undefined) f.enableHeader = h.enabled;
|
|
223
|
+
if (h.logoUrl !== undefined) f.headerLogoUrl = h.logoUrl;
|
|
224
|
+
if (h.brandName !== undefined) f.headerBrandName = h.brandName;
|
|
225
|
+
if (h.bgColor !== undefined) f.headerBgColor = h.bgColor;
|
|
226
|
+
if (h.textColor !== undefined) f.headerTextColor = h.textColor;
|
|
227
|
+
if (h.navLinks !== undefined) f.headerNavLinks = h.navLinks;
|
|
228
|
+
}
|
|
229
|
+
if (y.footer) {
|
|
230
|
+
const ft = y.footer;
|
|
231
|
+
if (ft.enabled !== undefined) f.enableFooter = ft.enabled;
|
|
232
|
+
if (ft.text !== undefined) f.footerText = ft.text;
|
|
233
|
+
if (ft.bgColor !== undefined) f.footerBgColor = ft.bgColor;
|
|
234
|
+
if (ft.textColor !== undefined) f.footerTextColor = ft.textColor;
|
|
235
|
+
if (ft.navLinks !== undefined) f.footerNavLinks = ft.navLinks;
|
|
236
|
+
}
|
|
237
|
+
// Adapters from YAML: include metadata (name, description, notes, apiUrl) but NOT apiKey
|
|
238
|
+
if (y.adapters !== undefined) {
|
|
239
|
+
f.adapters = y.adapters.map(a => ({
|
|
240
|
+
name: a.name,
|
|
241
|
+
...(a.apiUrl ? { apiUrl: a.apiUrl } : {}),
|
|
242
|
+
...(a.description ? { description: a.description } : {}),
|
|
243
|
+
...(a.notes ? { notes: a.notes } : {}),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
return f;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function loadYamlConfig(configPath) {
|
|
250
|
+
try {
|
|
251
|
+
if (fs.existsSync(configPath)) {
|
|
252
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
253
|
+
const parsed = yaml.load(content);
|
|
254
|
+
if (parsed && typeof parsed === 'object') {
|
|
255
|
+
return parsed;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(`Error: Failed to parse ${configPath}: ${error.message}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- Adapter loading (env secrets + YAML metadata) ----
|
|
266
|
+
|
|
267
|
+
function parseAdaptersFromEnv() {
|
|
268
|
+
const envValue = process.env.ORBIT_ADAPTERS || process.env.VITE_ADAPTERS;
|
|
269
|
+
if (!envValue) return [];
|
|
59
270
|
|
|
60
271
|
try {
|
|
61
272
|
const parsed = JSON.parse(envValue);
|
|
@@ -63,46 +274,28 @@ function parseAdaptersListFromEnv() {
|
|
|
63
274
|
console.warn('Warning: ORBIT_ADAPTERS/VITE_ADAPTERS must be a JSON array');
|
|
64
275
|
return [];
|
|
65
276
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
adapters.push({
|
|
74
|
-
name: adapter.name,
|
|
75
|
-
apiKey: adapter.apiKey || DEFAULT_CONFIG.defaultKey,
|
|
76
|
-
apiUrl: adapter.apiUrl || DEFAULT_CONFIG.apiUrl,
|
|
77
|
-
description: adapter.description || adapter.summary,
|
|
78
|
-
notes: adapter.notes,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return adapters;
|
|
277
|
+
return parsed.filter(a => a.name).map(a => ({
|
|
278
|
+
name: a.name,
|
|
279
|
+
apiKey: a.apiKey || DEFAULTS.defaultKey,
|
|
280
|
+
apiUrl: a.apiUrl || DEFAULTS.apiUrl,
|
|
281
|
+
description: a.description || a.summary,
|
|
282
|
+
notes: a.notes,
|
|
283
|
+
}));
|
|
83
284
|
} catch (error) {
|
|
84
285
|
console.warn('Warning: Could not parse ORBIT_ADAPTERS/VITE_ADAPTERS:', error.message);
|
|
85
286
|
return [];
|
|
86
287
|
}
|
|
87
288
|
}
|
|
88
289
|
|
|
89
|
-
/**
|
|
90
|
-
* Load adapter mappings from environment variable
|
|
91
|
-
* Format: JSON array of adapter objects
|
|
92
|
-
* Example: ORBIT_ADAPTERS='[{"name":"Simple Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
93
|
-
* @returns {object|null} - Adapters object or null if not found/invalid
|
|
94
|
-
*/
|
|
95
290
|
function loadAdaptersConfig() {
|
|
96
|
-
const adapterList =
|
|
97
|
-
if (adapterList.length === 0)
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
291
|
+
const adapterList = parseAdaptersFromEnv();
|
|
292
|
+
if (adapterList.length === 0) return null;
|
|
100
293
|
|
|
101
294
|
const adapters = {};
|
|
102
295
|
for (const adapter of adapterList) {
|
|
103
296
|
adapters[adapter.name] = {
|
|
104
|
-
apiKey: adapter.apiKey ||
|
|
105
|
-
apiUrl: adapter.apiUrl ||
|
|
297
|
+
apiKey: adapter.apiKey || DEFAULTS.defaultKey,
|
|
298
|
+
apiUrl: adapter.apiUrl || DEFAULTS.apiUrl,
|
|
106
299
|
description: adapter.description,
|
|
107
300
|
notes: adapter.notes,
|
|
108
301
|
};
|
|
@@ -112,113 +305,26 @@ function loadAdaptersConfig() {
|
|
|
112
305
|
}
|
|
113
306
|
|
|
114
307
|
function getDefaultAdapterFromEnv() {
|
|
115
|
-
const adapterList =
|
|
308
|
+
const adapterList = parseAdaptersFromEnv();
|
|
116
309
|
return adapterList.length > 0 ? adapterList[0].name : null;
|
|
117
310
|
}
|
|
118
311
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*/
|
|
312
|
+
// ---- CLI arg parsing (server flags only) ----
|
|
313
|
+
|
|
122
314
|
function parseArgs() {
|
|
123
315
|
const args = process.argv.slice(2);
|
|
124
|
-
const config = { ...DEFAULT_CONFIG };
|
|
125
316
|
const serverConfig = {
|
|
126
317
|
port: 5173,
|
|
127
318
|
host: 'localhost',
|
|
128
319
|
open: false,
|
|
129
320
|
configFile: null,
|
|
130
321
|
apiOnly: false,
|
|
322
|
+
corsOrigin: undefined,
|
|
131
323
|
};
|
|
132
324
|
|
|
133
325
|
for (let i = 0; i < args.length; i++) {
|
|
134
326
|
const arg = args[i];
|
|
135
|
-
|
|
136
327
|
switch (arg) {
|
|
137
|
-
case '--api-url':
|
|
138
|
-
config.apiUrl = args[++i];
|
|
139
|
-
break;
|
|
140
|
-
case '--api-key':
|
|
141
|
-
config.defaultKey = args[++i];
|
|
142
|
-
break;
|
|
143
|
-
case '--default-adapter':
|
|
144
|
-
config.defaultKey = args[++i];
|
|
145
|
-
break;
|
|
146
|
-
case '--default-key':
|
|
147
|
-
console.warn('Warning: --default-key is deprecated. Use --default-adapter instead.');
|
|
148
|
-
config.defaultKey = args[++i];
|
|
149
|
-
break;
|
|
150
|
-
case '--application-name':
|
|
151
|
-
config.applicationName = args[++i];
|
|
152
|
-
break;
|
|
153
|
-
case '--application-description':
|
|
154
|
-
config.applicationDescription = args[++i];
|
|
155
|
-
break;
|
|
156
|
-
case '--default-input-placeholder':
|
|
157
|
-
config.defaultInputPlaceholder = args[++i];
|
|
158
|
-
break;
|
|
159
|
-
case '--use-local-api':
|
|
160
|
-
config.useLocalApi = true;
|
|
161
|
-
break;
|
|
162
|
-
case '--local-api-path':
|
|
163
|
-
config.localApiPath = args[++i];
|
|
164
|
-
break;
|
|
165
|
-
case '--console-debug':
|
|
166
|
-
config.consoleDebug = true;
|
|
167
|
-
break;
|
|
168
|
-
case '--enable-upload':
|
|
169
|
-
config.enableUploadButton = true;
|
|
170
|
-
break;
|
|
171
|
-
case '--enable-audio':
|
|
172
|
-
config.enableAudioOutput = true;
|
|
173
|
-
break;
|
|
174
|
-
case '--enable-audio-input':
|
|
175
|
-
config.enableAudioInput = true;
|
|
176
|
-
break;
|
|
177
|
-
case '--enable-feedback':
|
|
178
|
-
config.enableFeedbackButtons = true;
|
|
179
|
-
break;
|
|
180
|
-
case '--enable-autocomplete':
|
|
181
|
-
config.enableAutocomplete = true;
|
|
182
|
-
break;
|
|
183
|
-
case '--voice-silence-timeout-ms':
|
|
184
|
-
config.voiceSilenceTimeoutMs = parseInt(args[++i], 10);
|
|
185
|
-
break;
|
|
186
|
-
case '--voice-recognition-lang':
|
|
187
|
-
config.voiceRecognitionLanguage = args[++i];
|
|
188
|
-
break;
|
|
189
|
-
case '--enable-api-middleware':
|
|
190
|
-
// Ignored — middleware mode is now always enabled
|
|
191
|
-
break;
|
|
192
|
-
case '--out-of-service-message':
|
|
193
|
-
config.outOfServiceMessage = args[++i];
|
|
194
|
-
break;
|
|
195
|
-
case '--max-files-per-conversation':
|
|
196
|
-
config.maxFilesPerConversation = parseInt(args[++i], 10);
|
|
197
|
-
break;
|
|
198
|
-
case '--max-file-size-mb':
|
|
199
|
-
config.maxFileSizeMB = parseInt(args[++i], 10);
|
|
200
|
-
break;
|
|
201
|
-
case '--max-total-files':
|
|
202
|
-
config.maxTotalFiles = parseInt(args[++i], 10);
|
|
203
|
-
break;
|
|
204
|
-
case '--max-conversations':
|
|
205
|
-
config.maxConversations = parseInt(args[++i], 10);
|
|
206
|
-
break;
|
|
207
|
-
case '--max-messages-per-conversation':
|
|
208
|
-
config.maxMessagesPerConversation = parseInt(args[++i], 10);
|
|
209
|
-
break;
|
|
210
|
-
case '--max-messages-per-thread':
|
|
211
|
-
config.maxMessagesPerThread = parseInt(args[++i], 10);
|
|
212
|
-
break;
|
|
213
|
-
case '--max-total-messages':
|
|
214
|
-
config.maxTotalMessages = parseInt(args[++i], 10);
|
|
215
|
-
break;
|
|
216
|
-
case '--max-message-length':
|
|
217
|
-
config.maxMessageLength = parseInt(args[++i], 10);
|
|
218
|
-
break;
|
|
219
|
-
case '--settings-about-msg':
|
|
220
|
-
config.settingsAboutMsg = args[++i];
|
|
221
|
-
break;
|
|
222
328
|
case '--port':
|
|
223
329
|
serverConfig.port = parseInt(args[++i], 10);
|
|
224
330
|
break;
|
|
@@ -228,22 +334,19 @@ function parseArgs() {
|
|
|
228
334
|
case '--open':
|
|
229
335
|
serverConfig.open = true;
|
|
230
336
|
break;
|
|
337
|
+
case '--config':
|
|
338
|
+
serverConfig.configFile = args[++i];
|
|
339
|
+
break;
|
|
231
340
|
case '--api-only':
|
|
232
341
|
serverConfig.apiOnly = true;
|
|
233
342
|
break;
|
|
234
343
|
case '--cors-origin':
|
|
235
344
|
serverConfig.corsOrigin = args[++i];
|
|
236
345
|
break;
|
|
237
|
-
case '--config':
|
|
238
|
-
serverConfig.configFile = args[++i];
|
|
239
|
-
break;
|
|
240
346
|
case '--help':
|
|
241
347
|
case '-h':
|
|
242
|
-
// Handled in main() function
|
|
243
|
-
break;
|
|
244
348
|
case '--version':
|
|
245
349
|
case '-v':
|
|
246
|
-
// Handled in main() function
|
|
247
350
|
break;
|
|
248
351
|
default:
|
|
249
352
|
if (arg.startsWith('--')) {
|
|
@@ -254,126 +357,15 @@ function parseArgs() {
|
|
|
254
357
|
}
|
|
255
358
|
}
|
|
256
359
|
|
|
257
|
-
return
|
|
360
|
+
return serverConfig;
|
|
258
361
|
}
|
|
259
362
|
|
|
260
|
-
|
|
261
|
-
* Load configuration from file
|
|
262
|
-
*/
|
|
263
|
-
function loadConfigFile(filePath) {
|
|
264
|
-
try {
|
|
265
|
-
if (fs.existsSync(filePath)) {
|
|
266
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
267
|
-
return JSON.parse(content);
|
|
268
|
-
}
|
|
269
|
-
} catch (error) {
|
|
270
|
-
console.warn(`Warning: Could not load config file ${filePath}:`, error.message);
|
|
271
|
-
}
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
363
|
+
// ---- HTML injection ----
|
|
274
364
|
|
|
275
|
-
/**
|
|
276
|
-
* Load configuration from environment variables
|
|
277
|
-
*/
|
|
278
|
-
function loadConfigFromEnv() {
|
|
279
|
-
const envConfig = {};
|
|
280
|
-
|
|
281
|
-
// Map VITE_* environment variables to config keys
|
|
282
|
-
const envMap = {
|
|
283
|
-
VITE_API_URL: 'apiUrl',
|
|
284
|
-
VITE_DEFAULT_KEY: 'defaultKey',
|
|
285
|
-
VITE_APPLICATION_NAME: 'applicationName',
|
|
286
|
-
VITE_APPLICATION_DESCRIPTION: 'applicationDescription',
|
|
287
|
-
VITE_DEFAULT_INPUT_PLACEHOLDER: 'defaultInputPlaceholder',
|
|
288
|
-
VITE_USE_LOCAL_API: 'useLocalApi',
|
|
289
|
-
VITE_LOCAL_API_PATH: 'localApiPath',
|
|
290
|
-
VITE_CONSOLE_DEBUG: 'consoleDebug',
|
|
291
|
-
VITE_ENABLE_UPLOAD: 'enableUploadButton',
|
|
292
|
-
VITE_ENABLE_AUDIO_OUTPUT: 'enableAudioOutput',
|
|
293
|
-
VITE_ENABLE_AUDIO_INPUT: 'enableAudioInput',
|
|
294
|
-
VITE_ENABLE_FEEDBACK: 'enableFeedbackButtons',
|
|
295
|
-
VITE_ENABLE_AUTOCOMPLETE: 'enableAutocomplete',
|
|
296
|
-
VITE_VOICE_SILENCE_TIMEOUT_MS: 'voiceSilenceTimeoutMs',
|
|
297
|
-
VITE_VOICE_RECOGNITION_LANG: 'voiceRecognitionLanguage',
|
|
298
|
-
VITE_OUT_OF_SERVICE_MESSAGE: 'outOfServiceMessage',
|
|
299
|
-
VITE_MAX_FILES_PER_CONVERSATION: 'maxFilesPerConversation',
|
|
300
|
-
VITE_MAX_FILE_SIZE_MB: 'maxFileSizeMB',
|
|
301
|
-
VITE_MAX_TOTAL_FILES: 'maxTotalFiles',
|
|
302
|
-
VITE_MAX_CONVERSATIONS: 'maxConversations',
|
|
303
|
-
VITE_MAX_MESSAGES_PER_CONVERSATION: 'maxMessagesPerConversation',
|
|
304
|
-
VITE_MAX_MESSAGES_PER_THREAD: 'maxMessagesPerThread',
|
|
305
|
-
VITE_MAX_TOTAL_MESSAGES: 'maxTotalMessages',
|
|
306
|
-
VITE_MAX_MESSAGE_LENGTH: 'maxMessageLength',
|
|
307
|
-
VITE_SETTINGS_ABOUT_MSG: 'settingsAboutMsg',
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
for (const [envKey, configKey] of Object.entries(envMap)) {
|
|
311
|
-
const value = process.env[envKey];
|
|
312
|
-
if (value !== undefined) {
|
|
313
|
-
if (configKey === 'useLocalApi' || configKey === 'consoleDebug' ||
|
|
314
|
-
configKey === 'enableUploadButton' || configKey === 'enableAudioOutput' ||
|
|
315
|
-
configKey === 'enableAudioInput' || configKey === 'enableFeedbackButtons' || configKey === 'enableAutocomplete') {
|
|
316
|
-
envConfig[configKey] = value === 'true';
|
|
317
|
-
} else if (configKey === 'voiceSilenceTimeoutMs') {
|
|
318
|
-
const parsed = parseInt(value, 10);
|
|
319
|
-
if (!isNaN(parsed)) {
|
|
320
|
-
envConfig[configKey] = parsed;
|
|
321
|
-
}
|
|
322
|
-
} else if (configKey.includes('max') && configKey !== 'maxFileSizeMB') {
|
|
323
|
-
const parsed = parseInt(value, 10);
|
|
324
|
-
if (!isNaN(parsed)) {
|
|
325
|
-
envConfig[configKey] = parsed;
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
envConfig[configKey] = value;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return envConfig;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Merge configurations in priority order: CLI args > config file > env vars > defaults
|
|
338
|
-
* Note: GitHub stats/owner/repo are not included in runtime config - they're hardcoded
|
|
339
|
-
* and only configurable via build-time env vars for developers who fork.
|
|
340
|
-
*/
|
|
341
|
-
function mergeConfig(cliConfig, serverConfig) {
|
|
342
|
-
// Start with defaults
|
|
343
|
-
let config = { ...DEFAULT_CONFIG };
|
|
344
|
-
|
|
345
|
-
// Load from environment variables (excluding GitHub config)
|
|
346
|
-
const envConfig = loadConfigFromEnv();
|
|
347
|
-
config = { ...config, ...envConfig };
|
|
348
|
-
|
|
349
|
-
// Load from config file (excluding GitHub config)
|
|
350
|
-
const configDir = path.join(homedir(), '.orbit-chat-app');
|
|
351
|
-
const defaultConfigFile = path.join(configDir, 'config.json');
|
|
352
|
-
const configFile = serverConfig.configFile || defaultConfigFile;
|
|
353
|
-
|
|
354
|
-
const fileConfig = loadConfigFile(configFile);
|
|
355
|
-
if (fileConfig) {
|
|
356
|
-
// Remove GitHub config from file config if present
|
|
357
|
-
const { showGitHubStats, githubOwner, githubRepo, ...fileConfigWithoutGitHub } = fileConfig;
|
|
358
|
-
config = { ...config, ...fileConfigWithoutGitHub };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// CLI arguments override everything (excluding GitHub config)
|
|
362
|
-
const { showGitHubStats, githubOwner, githubRepo, ...cliConfigWithoutGitHub } = cliConfig;
|
|
363
|
-
config = { ...config, ...cliConfigWithoutGitHub };
|
|
364
|
-
|
|
365
|
-
return config;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Inject configuration into HTML
|
|
370
|
-
* The config script MUST be placed in <head> BEFORE the main JS module loads,
|
|
371
|
-
* otherwise window.ORBIT_CHAT_CONFIG won't be available when the app initializes.
|
|
372
|
-
*/
|
|
373
365
|
function injectConfig(html, config) {
|
|
374
366
|
const configScript = `<script>window.ORBIT_CHAT_CONFIG = ${JSON.stringify(config)};</script>`;
|
|
375
367
|
|
|
376
|
-
// Remove the placeholder script tag
|
|
368
|
+
// Remove the placeholder script tag
|
|
377
369
|
html = html.replace(
|
|
378
370
|
/<script id="orbit-chat-config" type="application\/json">[\s\S]*?<\/script>/,
|
|
379
371
|
'<!-- Config injected in head -->'
|
|
@@ -385,34 +377,86 @@ function injectConfig(html, config) {
|
|
|
385
377
|
/<title>.*?<\/title>/i,
|
|
386
378
|
`<title>${config.applicationName}</title>`
|
|
387
379
|
);
|
|
388
|
-
// Also update apple-mobile-web-app-title meta tag
|
|
389
380
|
html = html.replace(
|
|
390
381
|
/<meta name="apple-mobile-web-app-title" content="[^"]*" \/>/i,
|
|
391
382
|
`<meta name="apple-mobile-web-app-title" content="${config.applicationName}" />`
|
|
392
383
|
);
|
|
393
384
|
}
|
|
394
385
|
|
|
395
|
-
// Inject the config script at the START of <head
|
|
396
|
-
// This ensures window.ORBIT_CHAT_CONFIG is available when the main JS bundle loads
|
|
386
|
+
// Inject the config script at the START of <head>
|
|
397
387
|
return html.replace(
|
|
398
388
|
/<head>/i,
|
|
399
389
|
'<head>\n ' + configScript
|
|
400
390
|
);
|
|
401
391
|
}
|
|
402
392
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
393
|
+
// ---- Rate limiting ----
|
|
394
|
+
|
|
395
|
+
function createRateLimiters(rateLimitConfig) {
|
|
396
|
+
if (!rateLimitConfig || rateLimitConfig.enabled === false) return null;
|
|
397
|
+
|
|
398
|
+
const windowMs = rateLimitConfig.windowMs || 60000;
|
|
399
|
+
const maxRequests = rateLimitConfig.maxRequests || 30;
|
|
400
|
+
const chatWindowMs = rateLimitConfig.chat?.windowMs || 60000;
|
|
401
|
+
const chatMaxRequests = rateLimitConfig.chat?.maxRequests || 10;
|
|
402
|
+
|
|
403
|
+
const keyGenerator = (req) =>
|
|
404
|
+
req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
405
|
+
|
|
406
|
+
const api = rateLimit({
|
|
407
|
+
windowMs,
|
|
408
|
+
max: maxRequests,
|
|
409
|
+
keyGenerator,
|
|
410
|
+
standardHeaders: 'draft-7',
|
|
411
|
+
legacyHeaders: false,
|
|
412
|
+
validate: { default: true, keyGeneratorIpFallback: false },
|
|
413
|
+
skip: (req) => req.method === 'OPTIONS' || req.path === '/adapters',
|
|
414
|
+
handler: (req, res) => {
|
|
415
|
+
const retryAfterMs = res.getHeader('RateLimit-Reset')
|
|
416
|
+
? Number(res.getHeader('RateLimit-Reset')) * 1000
|
|
417
|
+
: windowMs;
|
|
418
|
+
res.status(429).json({
|
|
419
|
+
error: 'Too many requests',
|
|
420
|
+
message: `Rate limit exceeded. Try again in ${Math.ceil(retryAfterMs / 1000)} seconds.`,
|
|
421
|
+
retryAfterMs,
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const chat = rateLimit({
|
|
427
|
+
windowMs: chatWindowMs,
|
|
428
|
+
max: chatMaxRequests,
|
|
429
|
+
keyGenerator,
|
|
430
|
+
standardHeaders: 'draft-7',
|
|
431
|
+
legacyHeaders: false,
|
|
432
|
+
validate: { default: true, keyGeneratorIpFallback: false },
|
|
433
|
+
handler: (req, res) => {
|
|
434
|
+
const retryAfterMs = res.getHeader('RateLimit-Reset')
|
|
435
|
+
? Number(res.getHeader('RateLimit-Reset')) * 1000
|
|
436
|
+
: chatWindowMs;
|
|
437
|
+
res.status(429).json({
|
|
438
|
+
error: 'Too many requests',
|
|
439
|
+
message: `Chat rate limit exceeded. Try again in ${Math.ceil(retryAfterMs / 1000)} seconds.`,
|
|
440
|
+
retryAfterMs,
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return { api, chat };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- Express server ----
|
|
449
|
+
|
|
410
450
|
function createServer(distPath, config, serverConfig = {}) {
|
|
411
451
|
const app = express();
|
|
412
452
|
const adapters = loadAdaptersConfig();
|
|
413
453
|
const apiOnly = serverConfig.apiOnly || false;
|
|
454
|
+
const yamlAdapterMetadata = new Map(
|
|
455
|
+
Array.isArray(config.adapters)
|
|
456
|
+
? config.adapters.filter(a => a && a.name).map(a => [a.name, a])
|
|
457
|
+
: []
|
|
458
|
+
);
|
|
414
459
|
|
|
415
|
-
// In api-only mode, add CORS middleware so external UIs on other origins can call the API
|
|
416
460
|
if (apiOnly) {
|
|
417
461
|
const allowedOrigin = serverConfig.corsOrigin || '*';
|
|
418
462
|
app.use((req, res, next) => {
|
|
@@ -427,10 +471,39 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
427
471
|
});
|
|
428
472
|
}
|
|
429
473
|
|
|
474
|
+
// Guest rate limiting — after CORS, before proxy. Skips authenticated requests.
|
|
475
|
+
const limiters = createRateLimiters(serverConfig.rateLimit);
|
|
476
|
+
if (limiters) {
|
|
477
|
+
app.use('/api', (req, res, next) => {
|
|
478
|
+
if (req.headers.authorization) return next();
|
|
479
|
+
limiters.api(req, res, next);
|
|
480
|
+
});
|
|
481
|
+
app.use('/api', (req, res, next) => {
|
|
482
|
+
if (req.headers.authorization) return next();
|
|
483
|
+
if (req.method === 'POST' && (/\/chat/i.test(req.path) || /\/stream/i.test(req.path))) {
|
|
484
|
+
return limiters.chat(req, res, next);
|
|
485
|
+
}
|
|
486
|
+
next();
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
430
490
|
// API proxy endpoints - MUST be before body parsers
|
|
431
491
|
if (adapters) {
|
|
432
|
-
//
|
|
433
|
-
|
|
492
|
+
// Merge adapter metadata from YAML so UI labels/notes are consistent with dev mode.
|
|
493
|
+
for (const [adapterName, adapter] of Object.entries(adapters)) {
|
|
494
|
+
const metadata = yamlAdapterMetadata.get(adapterName);
|
|
495
|
+
if (!metadata) continue;
|
|
496
|
+
if (!adapter.description && metadata.description) {
|
|
497
|
+
adapter.description = metadata.description;
|
|
498
|
+
}
|
|
499
|
+
if (!adapter.notes && metadata.notes) {
|
|
500
|
+
adapter.notes = metadata.notes;
|
|
501
|
+
}
|
|
502
|
+
if (!adapter.apiUrl && metadata.apiUrl) {
|
|
503
|
+
adapter.apiUrl = metadata.apiUrl;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
434
507
|
const proxyInstances = {};
|
|
435
508
|
for (const [adapterName, adapter] of Object.entries(adapters)) {
|
|
436
509
|
if (!adapter.apiKey || !adapter.apiUrl) {
|
|
@@ -440,33 +513,26 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
440
513
|
proxyInstances[adapterName] = createProxyMiddleware({
|
|
441
514
|
target: adapter.apiUrl,
|
|
442
515
|
changeOrigin: true,
|
|
443
|
-
// Restore /api prefix for backend paths that need it (files, threads)
|
|
444
516
|
pathRewrite: (path) => {
|
|
445
517
|
if (path.startsWith('/files') || path.startsWith('/threads')) {
|
|
446
518
|
return '/api' + path;
|
|
447
519
|
}
|
|
448
520
|
return path;
|
|
449
521
|
},
|
|
450
|
-
// Set headers directly - this is more reliable than onProxyReq for some cases
|
|
451
522
|
headers: {
|
|
452
523
|
'X-API-Key': adapter.apiKey,
|
|
453
524
|
},
|
|
454
|
-
// Critical for SSE streaming - disable response buffering
|
|
455
525
|
selfHandleResponse: false,
|
|
456
526
|
onProxyReq: (proxyReq, req) => {
|
|
457
|
-
// Remove adapter name header
|
|
458
527
|
proxyReq.removeHeader('x-adapter-name');
|
|
459
|
-
// Ensure API key is set (backup to headers option above)
|
|
460
528
|
proxyReq.setHeader('X-API-Key', adapter.apiKey);
|
|
461
|
-
|
|
462
|
-
const headersToPreserve = ['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length'];
|
|
529
|
+
const headersToPreserve = ['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length', 'authorization'];
|
|
463
530
|
headersToPreserve.forEach(header => {
|
|
464
531
|
const value = req.headers[header];
|
|
465
532
|
if (value) {
|
|
466
533
|
proxyReq.setHeader(header, value);
|
|
467
534
|
}
|
|
468
535
|
});
|
|
469
|
-
// Copy all other headers
|
|
470
536
|
Object.keys(req.headers).forEach(key => {
|
|
471
537
|
const lowerKey = key.toLowerCase();
|
|
472
538
|
if (!['x-adapter-name', 'host', 'connection', 'transfer-encoding'].includes(lowerKey)) {
|
|
@@ -478,18 +544,14 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
478
544
|
});
|
|
479
545
|
},
|
|
480
546
|
onProxyRes: (proxyRes, req, res) => {
|
|
481
|
-
// Handle CORS if needed
|
|
482
547
|
proxyRes.headers['access-control-allow-origin'] = '*';
|
|
483
548
|
proxyRes.headers['access-control-allow-methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
|
|
484
549
|
proxyRes.headers['access-control-allow-headers'] = 'Content-Type, X-API-Key, X-Session-ID, X-Thread-ID, X-Adapter-Name';
|
|
485
550
|
|
|
486
|
-
// Critical for SSE streaming - disable buffering
|
|
487
551
|
const contentType = proxyRes.headers['content-type'] || '';
|
|
488
552
|
if (contentType.includes('text/event-stream')) {
|
|
489
|
-
// Disable caching and buffering for SSE
|
|
490
553
|
proxyRes.headers['cache-control'] = 'no-cache';
|
|
491
554
|
proxyRes.headers['x-accel-buffering'] = 'no';
|
|
492
|
-
// Flush response immediately
|
|
493
555
|
if (res.flushHeaders) {
|
|
494
556
|
res.flushHeaders();
|
|
495
557
|
}
|
|
@@ -501,12 +563,11 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
501
563
|
res.status(500).json({ error: 'Proxy error', message: err.message });
|
|
502
564
|
}
|
|
503
565
|
},
|
|
504
|
-
ws: false,
|
|
505
|
-
logLevel: 'silent',
|
|
566
|
+
ws: false,
|
|
567
|
+
logLevel: 'silent',
|
|
506
568
|
});
|
|
507
569
|
}
|
|
508
570
|
|
|
509
|
-
// Endpoint to list available adapters (only expose names, not URLs or keys)
|
|
510
571
|
app.get('/api/adapters', (req, res) => {
|
|
511
572
|
const adapterList = Object.keys(adapters).map(name => ({
|
|
512
573
|
name,
|
|
@@ -516,10 +577,7 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
516
577
|
res.json({ adapters: adapterList });
|
|
517
578
|
});
|
|
518
579
|
|
|
519
|
-
// Proxy middleware for API requests - must be before body parsers to preserve request stream
|
|
520
|
-
// Note: Uses /api path instead of /api/proxy for security (hides proxy nature)
|
|
521
580
|
app.use('/api', (req, res, next) => {
|
|
522
|
-
// Skip the /api/adapters route - it's handled separately above
|
|
523
581
|
if (req.path === '/adapters') {
|
|
524
582
|
return next('route');
|
|
525
583
|
}
|
|
@@ -539,14 +597,10 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
539
597
|
});
|
|
540
598
|
}
|
|
541
599
|
|
|
542
|
-
// Middleware for parsing JSON - after proxy routes to preserve request body stream
|
|
543
600
|
app.use(express.json());
|
|
544
601
|
app.use(express.urlencoded({ extended: true }));
|
|
545
602
|
|
|
546
|
-
// --- UI serving (skipped in api-only mode) ---
|
|
547
603
|
if (!apiOnly && distPath) {
|
|
548
|
-
// IMPORTANT: Handle index.html BEFORE express.static to inject runtime config
|
|
549
|
-
// express.static would serve the file without config injection otherwise
|
|
550
604
|
app.get(['/', '/index.html'], (req, res) => {
|
|
551
605
|
try {
|
|
552
606
|
const indexPath = path.join(distPath, 'index.html');
|
|
@@ -560,24 +614,15 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
560
614
|
}
|
|
561
615
|
});
|
|
562
616
|
|
|
563
|
-
|
|
564
|
-
app.use(express.static(distPath, {
|
|
565
|
-
index: false, // Don't serve index.html automatically - we handle it above
|
|
566
|
-
}));
|
|
617
|
+
app.use(express.static(distPath, { index: false }));
|
|
567
618
|
|
|
568
|
-
// SPA fallback - serve index.html for all non-file routes (client-side routing)
|
|
569
619
|
app.get('/{*splat}', (req, res, next) => {
|
|
570
|
-
// Skip API routes
|
|
571
620
|
if (req.path.startsWith('/api/')) {
|
|
572
621
|
return next();
|
|
573
622
|
}
|
|
574
|
-
|
|
575
|
-
// Skip requests for files with extensions (let them 404)
|
|
576
623
|
if (path.extname(req.path)) {
|
|
577
624
|
return res.status(404).send('Not Found');
|
|
578
625
|
}
|
|
579
|
-
|
|
580
|
-
// Serve index.html for SPA routes
|
|
581
626
|
try {
|
|
582
627
|
const indexPath = path.join(distPath, 'index.html');
|
|
583
628
|
let content = fs.readFileSync(indexPath, 'utf8');
|
|
@@ -594,173 +639,104 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
594
639
|
return app;
|
|
595
640
|
}
|
|
596
641
|
|
|
597
|
-
|
|
598
|
-
* Get MIME type for file extension
|
|
599
|
-
*/
|
|
600
|
-
function getMimeType(filePath) {
|
|
601
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
602
|
-
const mimeTypes = {
|
|
603
|
-
'.html': 'text/html',
|
|
604
|
-
'.js': 'application/javascript',
|
|
605
|
-
'.mjs': 'application/javascript',
|
|
606
|
-
'.json': 'application/json',
|
|
607
|
-
'.css': 'text/css',
|
|
608
|
-
'.png': 'image/png',
|
|
609
|
-
'.jpg': 'image/jpeg',
|
|
610
|
-
'.jpeg': 'image/jpeg',
|
|
611
|
-
'.gif': 'image/gif',
|
|
612
|
-
'.svg': 'image/svg+xml',
|
|
613
|
-
'.ico': 'image/x-icon',
|
|
614
|
-
'.woff': 'font/woff',
|
|
615
|
-
'.woff2': 'font/woff2',
|
|
616
|
-
'.ttf': 'font/ttf',
|
|
617
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
618
|
-
};
|
|
619
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
620
|
-
}
|
|
642
|
+
// ---- Utilities ----
|
|
621
643
|
|
|
622
|
-
/**
|
|
623
|
-
* Open browser
|
|
624
|
-
*/
|
|
625
644
|
function openBrowser(url) {
|
|
626
645
|
const platform = process.platform;
|
|
627
646
|
let command;
|
|
628
|
-
|
|
629
|
-
if (platform === '
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
command = `xdg-open "${url}"`;
|
|
633
|
-
} else if (platform === 'win32') {
|
|
634
|
-
command = `start "${url}"`;
|
|
635
|
-
} else {
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
647
|
+
if (platform === 'darwin') command = `open "${url}"`;
|
|
648
|
+
else if (platform === 'linux') command = `xdg-open "${url}"`;
|
|
649
|
+
else if (platform === 'win32') command = `start "${url}"`;
|
|
650
|
+
else return;
|
|
638
651
|
|
|
639
|
-
try {
|
|
640
|
-
execSync(command, { stdio: 'ignore' });
|
|
641
|
-
} catch (error) {
|
|
642
|
-
// Ignore errors
|
|
643
|
-
}
|
|
652
|
+
try { execSync(command, { stdio: 'ignore' }); } catch { /* ignore */ }
|
|
644
653
|
}
|
|
645
654
|
|
|
646
|
-
/**
|
|
647
|
-
* Get version from package.json
|
|
648
|
-
*/
|
|
649
655
|
function getVersion() {
|
|
650
656
|
try {
|
|
651
657
|
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
652
658
|
const packageContent = fs.readFileSync(packagePath, 'utf8');
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
} catch (error) {
|
|
656
|
-
return 'unknown';
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Print version
|
|
662
|
-
*/
|
|
663
|
-
function printVersion() {
|
|
664
|
-
console.log(getVersion());
|
|
659
|
+
return JSON.parse(packageContent).version || 'unknown';
|
|
660
|
+
} catch { return 'unknown'; }
|
|
665
661
|
}
|
|
666
662
|
|
|
667
|
-
/**
|
|
668
|
-
* Print help message
|
|
669
|
-
*/
|
|
670
663
|
function printHelp() {
|
|
671
664
|
console.log(`
|
|
672
665
|
ORBIT Chat CLI
|
|
673
666
|
|
|
674
667
|
Usage: orbitchat [options]
|
|
675
668
|
|
|
669
|
+
All application settings are configured in orbitchat.yaml (see orbitchat.yaml.example).
|
|
670
|
+
Secrets (adapter API keys) go in VITE_ADAPTERS / ORBIT_ADAPTERS env var.
|
|
671
|
+
|
|
676
672
|
Options:
|
|
677
|
-
--
|
|
678
|
-
--
|
|
679
|
-
--
|
|
680
|
-
--
|
|
681
|
-
--
|
|
682
|
-
--
|
|
683
|
-
--
|
|
684
|
-
--
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
--enable-feedback Enable feedback buttons (default: false)
|
|
690
|
-
--enable-autocomplete Enable autocomplete suggestions (default: false)
|
|
691
|
-
--voice-silence-timeout-ms N Auto-stop voice capture after N ms of silence (default: 4000)
|
|
692
|
-
--voice-recognition-lang LANG BCP-47 language code for speech recognition (default: browser locale)
|
|
693
|
-
--enable-api-middleware Ignored (middleware mode is always enabled)
|
|
694
|
-
--out-of-service-message TEXT Show maintenance screen blocking access
|
|
695
|
-
--max-files-per-conversation N Max files per conversation (default: 5)
|
|
696
|
-
--max-file-size-mb N Max file size in MB (default: 50)
|
|
697
|
-
--max-total-files N Max total files (default: 100, 0 = unlimited)
|
|
698
|
-
--max-conversations N Max conversations (default: 10, 0 = unlimited)
|
|
699
|
-
--max-messages-per-conversation N Max messages per conversation (default: 1000, 0 = unlimited)
|
|
700
|
-
--max-messages-per-thread N Max messages per thread (default: 1000, 0 = unlimited)
|
|
701
|
-
--max-total-messages N Max total messages (default: 10000, 0 = unlimited)
|
|
702
|
-
--max-message-length N Max message length (default: 1000)
|
|
703
|
-
--settings-about-msg TEXT About message in settings page (default: ORBIT Chat)
|
|
704
|
-
--port PORT Server port (default: 5173)
|
|
705
|
-
--host HOST Server host (default: localhost)
|
|
706
|
-
--open Open browser automatically
|
|
707
|
-
--api-only Run API proxy only (no UI serving, no build required)
|
|
708
|
-
--cors-origin ORIGIN Allowed CORS origin in api-only mode (default: *)
|
|
709
|
-
--config PATH Path to config file (default: ~/.orbit-chat-app/config.json)
|
|
710
|
-
--help, -h Show this help message
|
|
711
|
-
--version, -v Show version number
|
|
712
|
-
|
|
713
|
-
Configuration Priority:
|
|
714
|
-
1. CLI arguments
|
|
715
|
-
2. Config file (~/.orbit-chat-app/config.json)
|
|
716
|
-
3. Environment variables (VITE_*)
|
|
717
|
-
4. Default values
|
|
718
|
-
|
|
719
|
-
Environment Variables for Middleware Mode:
|
|
720
|
-
ORBIT_ADAPTERS or VITE_ADAPTERS JSON array of adapter configurations
|
|
721
|
-
Example: '[{"name":"Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
673
|
+
--port PORT Server port (default: 5173)
|
|
674
|
+
--host HOST Server host (default: localhost)
|
|
675
|
+
--open Open browser automatically
|
|
676
|
+
--config PATH Path to orbitchat.yaml (default: ./orbitchat.yaml)
|
|
677
|
+
--api-only Run API proxy only (no UI serving)
|
|
678
|
+
--cors-origin URL Allowed CORS origin in api-only mode (default: *)
|
|
679
|
+
--help, -h Show this help message
|
|
680
|
+
--version, -v Show version number
|
|
681
|
+
|
|
682
|
+
Environment Variables:
|
|
683
|
+
ORBIT_ADAPTERS or VITE_ADAPTERS JSON array of adapter configurations (secrets)
|
|
684
|
+
Example: '[{"name":"Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
722
685
|
|
|
723
686
|
Examples:
|
|
724
|
-
orbitchat --
|
|
725
|
-
orbitchat --
|
|
726
|
-
orbitchat --enable-audio --enable-audio-input --enable-upload --console-debug
|
|
727
|
-
orbitchat --config /path/to/config.json
|
|
728
|
-
ORBIT_ADAPTERS='[{"name":"Chat","apiKey":"mykey","apiUrl":"https://api.example.com"}]' orbitchat
|
|
729
|
-
orbitchat --api-only --port 5174
|
|
687
|
+
orbitchat --port 8080
|
|
688
|
+
orbitchat --config /path/to/orbitchat.yaml --open
|
|
730
689
|
orbitchat --api-only --cors-origin http://localhost:3001
|
|
731
690
|
`);
|
|
732
|
-
|
|
733
691
|
}
|
|
734
692
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
*/
|
|
693
|
+
// ---- Main ----
|
|
694
|
+
|
|
738
695
|
function main() {
|
|
739
|
-
// Check for version flag first
|
|
740
696
|
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
741
|
-
|
|
697
|
+
console.log(getVersion());
|
|
742
698
|
return;
|
|
743
699
|
}
|
|
744
|
-
|
|
745
|
-
// Check for help flag
|
|
746
700
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
747
701
|
printHelp();
|
|
748
702
|
return;
|
|
749
703
|
}
|
|
750
704
|
|
|
751
|
-
const
|
|
752
|
-
|
|
705
|
+
const serverConfig = parseArgs();
|
|
706
|
+
loadDotEnv(process.cwd());
|
|
707
|
+
|
|
708
|
+
// Load YAML config
|
|
709
|
+
const yamlPath = serverConfig.configFile || path.join(process.cwd(), 'orbitchat.yaml');
|
|
710
|
+
const yamlObj = loadYamlConfig(yamlPath);
|
|
711
|
+
const yamlFlat = yamlObj ? flattenYamlConfig(yamlObj) : {};
|
|
712
|
+
|
|
713
|
+
if (yamlObj) {
|
|
714
|
+
console.debug(`Loaded config from ${yamlPath}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Merge: DEFAULTS < YAML config < auth secrets from env
|
|
718
|
+
const config = { ...DEFAULTS, ...yamlFlat };
|
|
753
719
|
|
|
720
|
+
// Auth secrets from env
|
|
721
|
+
if (process.env.VITE_AUTH_DOMAIN) config.authDomain = process.env.VITE_AUTH_DOMAIN;
|
|
722
|
+
if (process.env.VITE_AUTH_CLIENT_ID) config.authClientId = process.env.VITE_AUTH_CLIENT_ID;
|
|
723
|
+
if (process.env.VITE_AUTH_AUDIENCE) config.authAudience = process.env.VITE_AUTH_AUDIENCE;
|
|
724
|
+
|
|
725
|
+
// Default adapter fallback
|
|
754
726
|
const trimmedDefaultKey = (config.defaultKey || '').trim();
|
|
755
|
-
if (!trimmedDefaultKey || trimmedDefaultKey ===
|
|
727
|
+
if (!trimmedDefaultKey || trimmedDefaultKey === DEFAULTS.defaultKey) {
|
|
756
728
|
const fallbackAdapter = getDefaultAdapterFromEnv();
|
|
757
729
|
if (fallbackAdapter) {
|
|
758
730
|
config.defaultKey = fallbackAdapter;
|
|
759
|
-
console.debug(`ℹ️ Using '${fallbackAdapter}' as the default adapter (first entry from VITE_ADAPTERS).`);
|
|
760
731
|
}
|
|
761
732
|
}
|
|
762
733
|
|
|
763
|
-
//
|
|
734
|
+
// Guest rate limiting (server-only, never sent to browser)
|
|
735
|
+
if (yamlObj && yamlObj.guestLimits?.rateLimit) {
|
|
736
|
+
serverConfig.rateLimit = yamlObj.guestLimits.rateLimit;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Find dist directory
|
|
764
740
|
const distPath = path.join(__dirname, '..', 'dist');
|
|
765
741
|
|
|
766
742
|
if (!serverConfig.apiOnly && !fs.existsSync(distPath)) {
|
|
@@ -768,7 +744,6 @@ function main() {
|
|
|
768
744
|
process.exit(1);
|
|
769
745
|
}
|
|
770
746
|
|
|
771
|
-
// Create and start server
|
|
772
747
|
const app = createServer(
|
|
773
748
|
serverConfig.apiOnly ? null : distPath,
|
|
774
749
|
config,
|
|
@@ -785,9 +760,15 @@ function main() {
|
|
|
785
760
|
console.debug('Configuration:');
|
|
786
761
|
console.debug(` Mode: ${serverConfig.apiOnly ? 'API-only (no UI)' : 'Full (API + UI)'}`);
|
|
787
762
|
console.debug(` API URL: ${config.apiUrl}`);
|
|
788
|
-
console.debug(` Default
|
|
763
|
+
console.debug(` Default Adapter: ${config.defaultKey || '(not set)'}`);
|
|
789
764
|
console.debug(` Port: ${serverConfig.port}`);
|
|
790
765
|
console.debug(` Host: ${serverConfig.host}`);
|
|
766
|
+
if (yamlObj) {
|
|
767
|
+
console.debug(` Config: ${yamlPath}`);
|
|
768
|
+
}
|
|
769
|
+
if (serverConfig.rateLimit && serverConfig.rateLimit.enabled !== false) {
|
|
770
|
+
console.debug(` Guest Rate Limiting: enabled (${serverConfig.rateLimit.maxRequests || 30} req/${(serverConfig.rateLimit.windowMs || 60000) / 1000}s, chat: ${serverConfig.rateLimit.chat?.maxRequests || 10} req/${(serverConfig.rateLimit.chat?.windowMs || 60000) / 1000}s)`);
|
|
771
|
+
}
|
|
791
772
|
const startupAdapters = loadAdaptersConfig();
|
|
792
773
|
if (startupAdapters) {
|
|
793
774
|
console.debug(` Available Adapters: ${Object.keys(startupAdapters).join(', ')}`);
|
|
@@ -795,21 +776,19 @@ function main() {
|
|
|
795
776
|
console.debug(` Warning: No adapters configured. Set ORBIT_ADAPTERS or VITE_ADAPTERS environment variable.`);
|
|
796
777
|
}
|
|
797
778
|
console.debug('');
|
|
798
|
-
|
|
779
|
+
|
|
799
780
|
if (serverConfig.open) {
|
|
800
781
|
openBrowser(url);
|
|
801
782
|
}
|
|
802
783
|
});
|
|
803
784
|
|
|
804
|
-
// Handle graceful shutdown
|
|
805
785
|
process.on('SIGINT', () => {
|
|
806
786
|
console.debug('\n\nShutting down server...');
|
|
807
787
|
process.exit(0);
|
|
808
788
|
});
|
|
809
789
|
}
|
|
810
790
|
|
|
811
|
-
// Run if called directly
|
|
812
|
-
// For ES modules, we check if this file is being executed directly
|
|
791
|
+
// Run if called directly
|
|
813
792
|
const isMainModule = process.argv[1] && (
|
|
814
793
|
import.meta.url === `file://${process.argv[1]}` ||
|
|
815
794
|
import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) ||
|
|
@@ -821,4 +800,4 @@ if (isMainModule) {
|
|
|
821
800
|
main();
|
|
822
801
|
}
|
|
823
802
|
|
|
824
|
-
export { main,
|
|
803
|
+
export { main, createServer, loadAdaptersConfig };
|