orbitchat 3.1.1 → 3.1.3
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 +79 -245
- package/bin/orbitchat.js +284 -729
- package/dist/assets/{ChartRenderer-CapBCpbV.js → ChartRenderer-Rhc3nvY3.js} +1 -1
- package/dist/assets/{MermaidRenderer-DQJRirnm.js → MermaidRenderer-BzYGDbGL.js} +5 -5
- package/dist/assets/{MusicRenderer-9vnA54gg.js → MusicRenderer-NnEIYAVw.js} +2 -2
- package/dist/assets/{SVGRenderer-By7CKtqU.js → SVGRenderer-_sr2u3jg.js} +1 -1
- package/dist/assets/{_basePickBy-Dm-zx8im.js → _basePickBy-gYhf5rnT.js} +1 -1
- package/dist/assets/{_baseUniq-DLPvoy-d.js → _baseUniq-Cbl9K6Wy.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-B1kMlTMW.js → architectureDiagram-VXUJARFQ-PClktC-4.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-BlglDb-U.js → blockDiagram-VD42YOAC-C0SW8BqM.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-CaZm--Yy.js → c4Diagram-YG6GDRKO-DSxXGReX.js} +1 -1
- package/dist/assets/channel-BqlT4tcZ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-4Kq-2AyN.js → chunk-4BX2VUAB-BGhKp2Fa.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-FApueVFk.js → chunk-55IACEB6-lipYmTAW.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-CHox7be9.js → chunk-B4BG7PRW-BbomVOnh.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-N5sePLFo.js → chunk-DI55MBZ5-Dc5SGEEJ.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BSbBs0xy.js → chunk-FMBD7UC4-8br3AiC5.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-ByurI9-W.js → chunk-QN33PNHL-DVUU2Ki_.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Yy8VVut9.js → chunk-QZHKN3VN-DeCce7KS.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-B5oe8pT4.js → chunk-TZMSLE5B-DGJHN-_V.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-D4Qktu8o.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-D4Qktu8o.js +1 -0
- package/dist/assets/clone-Dr4xHo_5.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DS_-k6qR.js → cose-bilkent-S5V4N54A-B2ykdhqW.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-DgkwJjVY.js → dagre-6UL2VRFP-UORxvWZD.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-C9su64f9.js → diagram-PSM6KHXK-BQFC4vRN.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-DY6-9Dbc.js → diagram-QEK2KX5R-CCSOfl0X.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-_Wub-Eik.js → diagram-S2PKOQOG-D3PUtgH9.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-Gwx9nSAM.js → erDiagram-Q2GNP2WA-Bq9ZcVy_.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-DmrXTF_S.js → flowDiagram-NV44I4VS-BBgWHY4c.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-DymVUgsd.js → ganttDiagram-JELNMOA3-CkPkSvB6.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-D3VSQFT4.js → gitGraphDiagram-V2S2FVAM-hYLGJpbm.js} +1 -1
- package/dist/assets/{graph-BIe-N9yO.js → graph-DJgI4-QL.js} +1 -1
- package/dist/assets/{index-B-NnBj1Y.js → index-CkCAzH0b.js} +116 -116
- package/dist/assets/{index-BiM6VNSE.js → index-CtSNEVGd.js} +37 -37
- package/dist/assets/index-w5RjGGjS.css +1 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-D2DeUrgk.js → infoDiagram-HS3SLOUP-DqPal58O.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-GxFhv1XB.js → journeyDiagram-XKPGCS4Q-ckP7tQ6L.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-BhT-ga9I.js → kanban-definition-3W4ZIXB7-D5coRbbB.js} +1 -1
- package/dist/assets/{layout-38O87Ivt.js → layout-Dbu3AECQ.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-DfKjmcaV.js → mindmap-definition-VGOIOE7T-Rh2iyVPx.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-D7lDWRGN.js → pieDiagram-ADFJNKIX-jhHLFKwA.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CPtZ8LgW.js → quadrantDiagram-AYHSOK5B-_KQn5Onj.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-h-H7R8sR.js → requirementDiagram-UZGBJVZJ-BGcyxuJP.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-C6cn5g4m.js → sankeyDiagram-TZEHDZUN-HpFhSy4a.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-B6AwcQ4F.js → sequenceDiagram-WL72ISMW-CRcnJnhH.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-B4ne7z3I.js → stateDiagram-FKZM4ZOC-CzYGK2Iy.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-C-DbD7yi.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-BmjJnJ-r.js → timeline-definition-IT6M3QCI-DtJxDVMX.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-CBUbyJIX.js → treemap-GDKQZRPO-DC-6lh4E.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-C3lVgboM.js → xychartDiagram-PRI3JC2R-DJ3uOpiG.js} +1 -1
- package/dist/index.html +2 -2
- package/orbitchat.sh +103 -35
- package/package.json +1 -1
- package/dist/assets/channel-BjUDQMqf.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-BhHCvLfM.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-BhHCvLfM.js +0 -1
- package/dist/assets/clone-oQOn4Sap.js +0 -1
- package/dist/assets/index-BA5Mw76S.css +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-C_qp8U90.js +0 -1
package/bin/orbitchat.js
CHANGED
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
* ORBIT Chat CLI
|
|
4
4
|
*
|
|
5
5
|
* Serves the chat-app as a standalone application with runtime configuration.
|
|
6
|
-
* Configuration is read from orbitchat.yaml
|
|
7
|
-
* Secrets
|
|
8
|
-
*
|
|
9
|
-
* The server acts as a proxy to hide API keys from the client by mapping
|
|
10
|
-
* adapter names to actual API keys.
|
|
6
|
+
* Configuration is read from orbitchat.yaml.
|
|
7
|
+
* Secrets come from VITE_ADAPTER_KEYS / ORBIT_ADAPTER_KEYS env var.
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
import express from 'express';
|
|
@@ -23,19 +20,38 @@ import rateLimit from 'express-rate-limit';
|
|
|
23
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
21
|
const __dirname = dirname(__filename);
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
function getCliVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
26
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
return pkg.version || 'unknown';
|
|
28
|
+
} catch {
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(`orbitchat [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--port PORT Server port (default: 5173)
|
|
38
|
+
--host HOST Server host (default: localhost)
|
|
39
|
+
--open Open browser automatically
|
|
40
|
+
--config PATH Path to orbitchat.yaml (default: ./orbitchat.yaml)
|
|
41
|
+
--api-only Run API proxy only (no UI serving)
|
|
42
|
+
--cors-origin URL Allowed CORS origin in api-only mode (default: *)
|
|
43
|
+
--help, -h Show help message
|
|
44
|
+
--version, -v Show version number`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- Minimal .env loader ----
|
|
27
48
|
|
|
28
49
|
function parseDotEnvValue(raw) {
|
|
29
50
|
const trimmed = raw.trim();
|
|
30
51
|
if (!trimmed) return '';
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
34
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
35
|
-
) {
|
|
52
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
36
53
|
return trimmed.slice(1, -1);
|
|
37
54
|
}
|
|
38
|
-
|
|
39
55
|
return trimmed;
|
|
40
56
|
}
|
|
41
57
|
|
|
@@ -43,217 +59,106 @@ function loadDotEnvFromFile(filePath) {
|
|
|
43
59
|
if (!fs.existsSync(filePath)) return;
|
|
44
60
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
61
|
const lines = content.split(/\r?\n/);
|
|
46
|
-
|
|
47
62
|
for (let i = 0; i < lines.length; i++) {
|
|
48
63
|
const trimmed = lines[i].trim();
|
|
49
64
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
50
|
-
|
|
51
65
|
const equalsIndex = trimmed.indexOf('=');
|
|
52
66
|
if (equalsIndex <= 0) continue;
|
|
53
|
-
|
|
54
67
|
const key = trimmed.slice(0, equalsIndex).trim();
|
|
55
|
-
if (!key) continue;
|
|
56
|
-
|
|
57
68
|
if (process.env[key] !== undefined) continue;
|
|
58
|
-
|
|
59
69
|
let valueRaw = trimmed.slice(equalsIndex + 1);
|
|
60
70
|
const startsWithDouble = valueRaw.startsWith('"');
|
|
61
71
|
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
|
-
) {
|
|
72
|
+
if ((startsWithDouble && !valueRaw.endsWith('"')) || (startsWithSingle && !valueRaw.endsWith("'"))) {
|
|
68
73
|
const quote = startsWithDouble ? '"' : "'";
|
|
69
74
|
while (i + 1 < lines.length) {
|
|
70
75
|
i += 1;
|
|
71
76
|
valueRaw += `\n${lines[i]}`;
|
|
72
|
-
if (lines[i].trim().endsWith(quote))
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
77
|
+
if (lines[i].trim().endsWith(quote)) break;
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
|
-
|
|
78
|
-
const value = parseDotEnvValue(valueRaw);
|
|
79
|
-
process.env[key] = value;
|
|
80
|
+
process.env[key] = parseDotEnvValue(valueRaw);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
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
85
|
loadDotEnvFromFile(path.join(baseDir, '.env.local'));
|
|
86
|
+
loadDotEnvFromFile(path.join(baseDir, '.env'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- Deep Merge ----
|
|
90
|
+
|
|
91
|
+
function isObject(item) {
|
|
92
|
+
return typeof item === 'object' && item !== null && !Array.isArray(item);
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
function deepMerge(target, source) {
|
|
96
|
+
if (!isObject(target) || !isObject(source)) return source;
|
|
97
|
+
const output = { ...target };
|
|
98
|
+
Object.keys(source).forEach(key => {
|
|
99
|
+
if (isObject(target[key]) && isObject(source[key])) {
|
|
100
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
101
|
+
} else if (source[key] !== undefined) {
|
|
102
|
+
output[key] = source[key];
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- Defaults ----
|
|
90
109
|
|
|
91
110
|
const DEFAULTS = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
application: {
|
|
112
|
+
name: 'ORBIT Chat',
|
|
113
|
+
description: "Explore ideas with ORBIT's AI copilots, share context, and build together.",
|
|
114
|
+
inputPlaceholder: 'Message ORBIT...',
|
|
115
|
+
settingsAboutMsg: 'ORBIT Chat',
|
|
116
|
+
locale: 'en-US',
|
|
117
|
+
},
|
|
118
|
+
debug: {
|
|
119
|
+
consoleDebug: false,
|
|
120
|
+
},
|
|
121
|
+
features: {
|
|
122
|
+
enableUpload: false,
|
|
123
|
+
enableAudioOutput: false,
|
|
124
|
+
enableAudioInput: false,
|
|
125
|
+
enableFeedbackButtons: false,
|
|
126
|
+
enableConversationThreads: true,
|
|
127
|
+
enableAutocomplete: false,
|
|
128
|
+
},
|
|
129
|
+
voice: {
|
|
130
|
+
silenceTimeoutMs: 4000,
|
|
131
|
+
recognitionLanguage: '',
|
|
132
|
+
},
|
|
133
|
+
github: {
|
|
134
|
+
showStats: true,
|
|
135
|
+
owner: 'schmitech',
|
|
136
|
+
repo: 'orbit',
|
|
137
|
+
},
|
|
108
138
|
outOfServiceMessage: null,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
guestMaxFilesPerConversation: 1,
|
|
124
|
-
guestMaxTotalFiles: 2,
|
|
125
|
-
guestMaxMessageLength: 500,
|
|
126
|
-
guestMaxFileSizeMB: 10,
|
|
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: [],
|
|
139
|
+
limits: {
|
|
140
|
+
files: { perConversation: 5, maxSizeMB: 50, totalFiles: 100 },
|
|
141
|
+
conversations: { maxConversations: 10, messagesPerConversation: 1000, messagesPerThread: 1000, totalMessages: 10000 },
|
|
142
|
+
messages: { maxLength: 1000 },
|
|
143
|
+
},
|
|
144
|
+
guestLimits: {
|
|
145
|
+
files: { perConversation: 1, maxSizeMB: 10, totalFiles: 2 },
|
|
146
|
+
conversations: { maxConversations: 1, messagesPerConversation: 10, messagesPerThread: 10, totalMessages: 10 },
|
|
147
|
+
messages: { maxLength: 500 },
|
|
148
|
+
},
|
|
149
|
+
auth: { enabled: false, domain: '', clientId: '', audience: '' },
|
|
150
|
+
header: { enabled: false, logoUrl: '', logoUrlLight: '', logoUrlDark: '', brandName: '', bgColor: '', textColor: '', showBorder: true, navLinks: [] },
|
|
151
|
+
footer: { enabled: false, text: '', bgColor: '', textColor: '', showBorder: false, layout: 'stacked', align: 'center', topPadding: 'large', navLinks: [] },
|
|
152
|
+
adapters: [],
|
|
143
153
|
};
|
|
144
154
|
|
|
145
155
|
// ---- YAML config loading ----
|
|
146
156
|
|
|
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;
|
|
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
157
|
function loadYamlConfig(configPath) {
|
|
250
158
|
try {
|
|
251
159
|
if (fs.existsSync(configPath)) {
|
|
252
160
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
253
|
-
|
|
254
|
-
if (parsed && typeof parsed === 'object') {
|
|
255
|
-
return parsed;
|
|
256
|
-
}
|
|
161
|
+
return yaml.load(content);
|
|
257
162
|
}
|
|
258
163
|
} catch (error) {
|
|
259
164
|
console.error(`Error: Failed to parse ${configPath}: ${error.message}`);
|
|
@@ -262,245 +167,100 @@ function loadYamlConfig(configPath) {
|
|
|
262
167
|
return null;
|
|
263
168
|
}
|
|
264
169
|
|
|
265
|
-
// ---- Local asset handling
|
|
266
|
-
|
|
267
|
-
function isUrlLike(value) {
|
|
268
|
-
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) || value.startsWith('//');
|
|
269
|
-
}
|
|
170
|
+
// ---- Local asset handling ----
|
|
270
171
|
|
|
271
172
|
function resolveLocalAssetPath(rawValue, yamlPath) {
|
|
272
173
|
if (!rawValue || typeof rawValue !== 'string') return null;
|
|
273
174
|
const value = rawValue.trim();
|
|
274
|
-
if (!value ||
|
|
275
|
-
|
|
276
|
-
const expandedValue = value.startsWith('~/')
|
|
277
|
-
? path.join(process.env.HOME || '', value.slice(2))
|
|
278
|
-
: value;
|
|
279
|
-
|
|
175
|
+
if (!value || /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) || value.startsWith('//')) return null;
|
|
176
|
+
const expandedValue = value.startsWith('~/') ? path.join(process.env.HOME || '', value.slice(2)) : value;
|
|
280
177
|
const yamlDir = path.dirname(yamlPath);
|
|
281
|
-
const candidates = path.isAbsolute(expandedValue)
|
|
282
|
-
? [expandedValue]
|
|
283
|
-
: [path.resolve(yamlDir, expandedValue), path.resolve(process.cwd(), expandedValue)];
|
|
284
|
-
|
|
178
|
+
const candidates = path.isAbsolute(expandedValue) ? [expandedValue] : [path.resolve(yamlDir, expandedValue), path.resolve(process.cwd(), expandedValue)];
|
|
285
179
|
for (const candidate of candidates) {
|
|
286
180
|
try {
|
|
287
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
} catch {
|
|
291
|
-
// ignore invalid candidate and continue
|
|
292
|
-
}
|
|
181
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return fs.realpathSync(candidate);
|
|
182
|
+
} catch { /* ignore */ }
|
|
293
183
|
}
|
|
294
|
-
|
|
295
184
|
return null;
|
|
296
185
|
}
|
|
297
186
|
|
|
298
187
|
// ---- Adapter loading (env secrets + YAML metadata) ----
|
|
299
188
|
|
|
300
|
-
function
|
|
301
|
-
const envValue = process.env.ORBIT_ADAPTERS || process.env.VITE_ADAPTERS;
|
|
302
|
-
if (!envValue) return [];
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
const parsed = JSON.parse(envValue);
|
|
306
|
-
if (!Array.isArray(parsed)) {
|
|
307
|
-
console.warn('Warning: ORBIT_ADAPTERS/VITE_ADAPTERS must be a JSON array');
|
|
308
|
-
return [];
|
|
309
|
-
}
|
|
310
|
-
return parsed.filter(a => a.name).map(a => ({
|
|
311
|
-
name: a.name,
|
|
312
|
-
apiKey: a.apiKey || DEFAULTS.defaultKey,
|
|
313
|
-
apiUrl: a.apiUrl || DEFAULTS.apiUrl,
|
|
314
|
-
description: a.description || a.summary,
|
|
315
|
-
notes: a.notes,
|
|
316
|
-
}));
|
|
317
|
-
} catch (error) {
|
|
318
|
-
console.warn('Warning: Could not parse ORBIT_ADAPTERS/VITE_ADAPTERS:', error.message);
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function loadAdaptersConfig() {
|
|
324
|
-
const adapterList = parseAdaptersFromEnv();
|
|
325
|
-
if (adapterList.length === 0) return null;
|
|
326
|
-
|
|
189
|
+
function loadAdaptersForProxy(yamlAdapters) {
|
|
327
190
|
const adapters = {};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
191
|
+
const fallbackApiUrl = 'http://localhost:3000';
|
|
192
|
+
|
|
193
|
+
if (Array.isArray(yamlAdapters)) {
|
|
194
|
+
for (const ya of yamlAdapters) {
|
|
195
|
+
if (!ya.name) continue;
|
|
196
|
+
adapters[ya.name] = {
|
|
197
|
+
apiKey: '',
|
|
198
|
+
apiUrl: ya.apiUrl || fallbackApiUrl,
|
|
199
|
+
description: ya.description,
|
|
200
|
+
notes: ya.notes,
|
|
201
|
+
model: ya.model
|
|
202
|
+
};
|
|
203
|
+
}
|
|
335
204
|
}
|
|
336
205
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
serverConfig.host = args[++i];
|
|
366
|
-
break;
|
|
367
|
-
case '--open':
|
|
368
|
-
serverConfig.open = true;
|
|
369
|
-
break;
|
|
370
|
-
case '--config':
|
|
371
|
-
serverConfig.configFile = args[++i];
|
|
372
|
-
break;
|
|
373
|
-
case '--api-only':
|
|
374
|
-
serverConfig.apiOnly = true;
|
|
375
|
-
break;
|
|
376
|
-
case '--cors-origin':
|
|
377
|
-
serverConfig.corsOrigin = args[++i];
|
|
378
|
-
break;
|
|
379
|
-
case '--help':
|
|
380
|
-
case '-h':
|
|
381
|
-
case '--version':
|
|
382
|
-
case '-v':
|
|
383
|
-
break;
|
|
384
|
-
default:
|
|
385
|
-
if (arg.startsWith('--')) {
|
|
386
|
-
console.error(`Unknown option: ${arg}`);
|
|
387
|
-
console.error('Use --help for usage information');
|
|
388
|
-
process.exit(1);
|
|
206
|
+
const envKeysRaw = process.env.VITE_ADAPTER_KEYS || process.env.ORBIT_ADAPTER_KEYS;
|
|
207
|
+
if (envKeysRaw) {
|
|
208
|
+
try {
|
|
209
|
+
const keys = JSON.parse(envKeysRaw);
|
|
210
|
+
for (const [name, value] of Object.entries(keys)) {
|
|
211
|
+
const isObjectValue = typeof value === 'object' && value !== null;
|
|
212
|
+
const apiKey = isObjectValue
|
|
213
|
+
? String(value.apiKey || value.key || '')
|
|
214
|
+
: String(value);
|
|
215
|
+
const apiUrl = isObjectValue && value.apiUrl ? String(value.apiUrl) : undefined;
|
|
216
|
+
const description = isObjectValue && value.description ? String(value.description) : undefined;
|
|
217
|
+
const notes = isObjectValue && value.notes ? String(value.notes) : undefined;
|
|
218
|
+
const model = isObjectValue && value.model ? String(value.model) : undefined;
|
|
219
|
+
|
|
220
|
+
if (!adapters[name]) {
|
|
221
|
+
adapters[name] = {
|
|
222
|
+
apiKey,
|
|
223
|
+
apiUrl: apiUrl || fallbackApiUrl,
|
|
224
|
+
description,
|
|
225
|
+
notes,
|
|
226
|
+
model
|
|
227
|
+
};
|
|
228
|
+
} else {
|
|
229
|
+
adapters[name].apiKey = apiKey;
|
|
230
|
+
if (apiUrl) adapters[name].apiUrl = apiUrl;
|
|
231
|
+
if (description !== undefined) adapters[name].description = description;
|
|
232
|
+
if (notes !== undefined) adapters[name].notes = notes;
|
|
233
|
+
if (model !== undefined) adapters[name].model = model;
|
|
389
234
|
}
|
|
390
|
-
|
|
235
|
+
}
|
|
236
|
+
} catch { /* ignore */ }
|
|
391
237
|
}
|
|
392
238
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// ---- HTML injection ----
|
|
397
|
-
|
|
398
|
-
function injectConfig(html, config) {
|
|
399
|
-
const configScript = `<script>window.ORBIT_CHAT_CONFIG = ${JSON.stringify(config)};</script>`;
|
|
400
|
-
|
|
401
|
-
// Remove the placeholder script tag
|
|
402
|
-
html = html.replace(
|
|
403
|
-
/<script id="orbit-chat-config" type="application\/json">[\s\S]*?<\/script>/,
|
|
404
|
-
'<!-- Config injected in head -->'
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
// Replace the title tag with the configured application name
|
|
408
|
-
if (config.applicationName) {
|
|
409
|
-
html = html.replace(
|
|
410
|
-
/<title>.*?<\/title>/i,
|
|
411
|
-
`<title>${config.applicationName}</title>`
|
|
412
|
-
);
|
|
413
|
-
html = html.replace(
|
|
414
|
-
/<meta name="apple-mobile-web-app-title" content="[^"]*" \/>/i,
|
|
415
|
-
`<meta name="apple-mobile-web-app-title" content="${config.applicationName}" />`
|
|
416
|
-
);
|
|
239
|
+
const finalAdapters = {};
|
|
240
|
+
for (const [name, config] of Object.entries(adapters)) {
|
|
241
|
+
if (config.apiKey) finalAdapters[name] = config;
|
|
417
242
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
'<head>\n ' + configScript
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ---- Rate limiting ----
|
|
427
|
-
|
|
428
|
-
function createRateLimiters(rateLimitConfig) {
|
|
429
|
-
if (!rateLimitConfig || rateLimitConfig.enabled === false) return null;
|
|
430
|
-
|
|
431
|
-
const windowMs = rateLimitConfig.windowMs || 60000;
|
|
432
|
-
const maxRequests = rateLimitConfig.maxRequests || 30;
|
|
433
|
-
const chatWindowMs = rateLimitConfig.chat?.windowMs || 60000;
|
|
434
|
-
const chatMaxRequests = rateLimitConfig.chat?.maxRequests || 10;
|
|
435
|
-
|
|
436
|
-
const keyGenerator = (req) =>
|
|
437
|
-
req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
438
|
-
|
|
439
|
-
const api = rateLimit({
|
|
440
|
-
windowMs,
|
|
441
|
-
max: maxRequests,
|
|
442
|
-
keyGenerator,
|
|
443
|
-
standardHeaders: 'draft-7',
|
|
444
|
-
legacyHeaders: false,
|
|
445
|
-
validate: { default: true, keyGeneratorIpFallback: false },
|
|
446
|
-
skip: (req) => req.method === 'OPTIONS' || req.path === '/adapters',
|
|
447
|
-
handler: (req, res) => {
|
|
448
|
-
const retryAfterMs = res.getHeader('RateLimit-Reset')
|
|
449
|
-
? Number(res.getHeader('RateLimit-Reset')) * 1000
|
|
450
|
-
: windowMs;
|
|
451
|
-
res.status(429).json({
|
|
452
|
-
error: 'Too many requests',
|
|
453
|
-
message: `Rate limit exceeded. Try again in ${Math.ceil(retryAfterMs / 1000)} seconds.`,
|
|
454
|
-
retryAfterMs,
|
|
455
|
-
});
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
const chat = rateLimit({
|
|
460
|
-
windowMs: chatWindowMs,
|
|
461
|
-
max: chatMaxRequests,
|
|
462
|
-
keyGenerator,
|
|
463
|
-
standardHeaders: 'draft-7',
|
|
464
|
-
legacyHeaders: false,
|
|
465
|
-
validate: { default: true, keyGeneratorIpFallback: false },
|
|
466
|
-
handler: (req, res) => {
|
|
467
|
-
const retryAfterMs = res.getHeader('RateLimit-Reset')
|
|
468
|
-
? Number(res.getHeader('RateLimit-Reset')) * 1000
|
|
469
|
-
: chatWindowMs;
|
|
470
|
-
res.status(429).json({
|
|
471
|
-
error: 'Too many requests',
|
|
472
|
-
message: `Chat rate limit exceeded. Try again in ${Math.ceil(retryAfterMs / 1000)} seconds.`,
|
|
473
|
-
retryAfterMs,
|
|
474
|
-
});
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
return { api, chat };
|
|
243
|
+
if (Object.keys(finalAdapters).length > 0) {
|
|
244
|
+
console.debug(`Loaded ${Object.keys(finalAdapters).length} adapters with API keys from environment.`);
|
|
245
|
+
}
|
|
246
|
+
return Object.keys(finalAdapters).length > 0 ? finalAdapters : null;
|
|
479
247
|
}
|
|
480
248
|
|
|
481
249
|
// ---- Express server ----
|
|
482
250
|
|
|
483
251
|
function createServer(distPath, config, serverConfig = {}) {
|
|
484
252
|
const app = express();
|
|
485
|
-
const adapters =
|
|
253
|
+
const adapters = loadAdaptersForProxy(config.adapters);
|
|
486
254
|
const apiOnly = serverConfig.apiOnly || false;
|
|
487
255
|
const localAssets = serverConfig.localAssets || {};
|
|
488
|
-
const yamlAdapterMetadata = new Map(
|
|
489
|
-
Array.isArray(config.adapters)
|
|
490
|
-
? config.adapters.filter(a => a && a.name).map(a => [a.name, a])
|
|
491
|
-
: []
|
|
492
|
-
);
|
|
493
256
|
|
|
494
257
|
if (apiOnly) {
|
|
495
258
|
const allowedOrigin = serverConfig.corsOrigin || '*';
|
|
496
259
|
app.use((req, res, next) => {
|
|
497
260
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
498
261
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
499
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, X-Session-ID, X-Thread-ID, X-Adapter-Name, Accept');
|
|
500
|
-
|
|
501
|
-
if (req.method === 'OPTIONS') {
|
|
502
|
-
return res.sendStatus(204);
|
|
503
|
-
}
|
|
262
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, X-Session-ID, X-Thread-ID, X-Adapter-Name, Accept, Authorization');
|
|
263
|
+
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
|
504
264
|
next();
|
|
505
265
|
});
|
|
506
266
|
}
|
|
@@ -508,144 +268,59 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
508
268
|
if (Object.keys(localAssets).length > 0) {
|
|
509
269
|
app.get('/__orbitchat_assets/:assetId', (req, res) => {
|
|
510
270
|
const assetPath = localAssets[req.params.assetId];
|
|
511
|
-
if (!assetPath)
|
|
512
|
-
return res.status(404).send('Asset not found');
|
|
513
|
-
}
|
|
514
|
-
if (!fs.existsSync(assetPath)) {
|
|
515
|
-
return res.status(404).send('Asset not found');
|
|
516
|
-
}
|
|
271
|
+
if (!assetPath || !fs.existsSync(assetPath)) return res.status(404).send('Asset not found');
|
|
517
272
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
518
|
-
res.sendFile(assetPath
|
|
519
|
-
if (error && !res.headersSent) {
|
|
520
|
-
res.status(500).send('Failed to serve asset');
|
|
521
|
-
}
|
|
522
|
-
});
|
|
273
|
+
res.sendFile(assetPath);
|
|
523
274
|
});
|
|
524
275
|
}
|
|
525
276
|
|
|
526
|
-
// Guest rate limiting
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
277
|
+
// Guest rate limiting
|
|
278
|
+
if (serverConfig.rateLimit?.enabled !== false) {
|
|
279
|
+
const rl = serverConfig.rateLimit || {};
|
|
280
|
+
const apiLimiter = rateLimit({
|
|
281
|
+
windowMs: rl.windowMs || 60000, max: rl.maxRequests || 30,
|
|
282
|
+
skip: (req) => req.method === 'OPTIONS' || req.path === '/adapters',
|
|
283
|
+
handler: (req, res) => res.status(429).json({ error: 'Too many requests' }),
|
|
284
|
+
});
|
|
285
|
+
const chatLimiter = rateLimit({
|
|
286
|
+
windowMs: rl.chat?.windowMs || 60000, max: rl.chat?.maxRequests || 10,
|
|
287
|
+
handler: (req, res) => res.status(429).json({ error: 'Chat rate limit exceeded' }),
|
|
532
288
|
});
|
|
289
|
+
app.use('/api', (req, res, next) => { if (req.headers.authorization) return next(); apiLimiter(req, res, next); });
|
|
533
290
|
app.use('/api', (req, res, next) => {
|
|
534
291
|
if (req.headers.authorization) return next();
|
|
535
|
-
if (req.method === 'POST' && (/\/chat/i.test(req.path) || /\/stream/i.test(req.path)))
|
|
536
|
-
return limiters.chat(req, res, next);
|
|
537
|
-
}
|
|
292
|
+
if (req.method === 'POST' && (/\/chat/i.test(req.path) || /\/stream/i.test(req.path))) return chatLimiter(req, res, next);
|
|
538
293
|
next();
|
|
539
294
|
});
|
|
540
295
|
}
|
|
541
296
|
|
|
542
|
-
// API proxy endpoints - MUST be before body parsers
|
|
543
297
|
if (adapters) {
|
|
544
|
-
//
|
|
545
|
-
for (const [adapterName, adapter] of Object.entries(adapters)) {
|
|
546
|
-
const metadata = yamlAdapterMetadata.get(adapterName);
|
|
547
|
-
if (!metadata) continue;
|
|
548
|
-
if (!adapter.description && metadata.description) {
|
|
549
|
-
adapter.description = metadata.description;
|
|
550
|
-
}
|
|
551
|
-
if (!adapter.notes && metadata.notes) {
|
|
552
|
-
adapter.notes = metadata.notes;
|
|
553
|
-
}
|
|
554
|
-
if (!adapter.apiUrl && metadata.apiUrl) {
|
|
555
|
-
adapter.apiUrl = metadata.apiUrl;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const proxyInstances = {};
|
|
560
|
-
for (const [adapterName, adapter] of Object.entries(adapters)) {
|
|
561
|
-
if (!adapter.apiKey || !adapter.apiUrl) {
|
|
562
|
-
console.warn(`[Proxy] Skipping adapter '${adapterName}': missing apiKey or apiUrl`);
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
proxyInstances[adapterName] = createProxyMiddleware({
|
|
566
|
-
target: adapter.apiUrl,
|
|
567
|
-
changeOrigin: true,
|
|
568
|
-
pathRewrite: (path) => {
|
|
569
|
-
if (path.startsWith('/files') || path.startsWith('/threads')) {
|
|
570
|
-
return '/api' + path;
|
|
571
|
-
}
|
|
572
|
-
return path;
|
|
573
|
-
},
|
|
574
|
-
headers: {
|
|
575
|
-
'X-API-Key': adapter.apiKey,
|
|
576
|
-
},
|
|
577
|
-
selfHandleResponse: false,
|
|
578
|
-
onProxyReq: (proxyReq, req) => {
|
|
579
|
-
proxyReq.removeHeader('x-adapter-name');
|
|
580
|
-
proxyReq.setHeader('X-API-Key', adapter.apiKey);
|
|
581
|
-
const headersToPreserve = ['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length', 'authorization'];
|
|
582
|
-
headersToPreserve.forEach(header => {
|
|
583
|
-
const value = req.headers[header];
|
|
584
|
-
if (value) {
|
|
585
|
-
proxyReq.setHeader(header, value);
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
Object.keys(req.headers).forEach(key => {
|
|
589
|
-
const lowerKey = key.toLowerCase();
|
|
590
|
-
if (!['x-adapter-name', 'host', 'connection', 'transfer-encoding'].includes(lowerKey)) {
|
|
591
|
-
const value = req.headers[key];
|
|
592
|
-
if (value && !headersToPreserve.includes(lowerKey)) {
|
|
593
|
-
proxyReq.setHeader(key, value);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
},
|
|
598
|
-
onProxyRes: (proxyRes, req, res) => {
|
|
599
|
-
proxyRes.headers['access-control-allow-origin'] = '*';
|
|
600
|
-
proxyRes.headers['access-control-allow-methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
|
|
601
|
-
proxyRes.headers['access-control-allow-headers'] = 'Content-Type, X-API-Key, X-Session-ID, X-Thread-ID, X-Adapter-Name';
|
|
602
|
-
|
|
603
|
-
const contentType = proxyRes.headers['content-type'] || '';
|
|
604
|
-
if (contentType.includes('text/event-stream')) {
|
|
605
|
-
proxyRes.headers['cache-control'] = 'no-cache';
|
|
606
|
-
proxyRes.headers['x-accel-buffering'] = 'no';
|
|
607
|
-
if (res.flushHeaders) {
|
|
608
|
-
res.flushHeaders();
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
onError: (err, req, res) => {
|
|
613
|
-
console.error('Proxy error:', err);
|
|
614
|
-
if (!res.headersSent) {
|
|
615
|
-
res.status(500).json({ error: 'Proxy error', message: err.message });
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
ws: false,
|
|
619
|
-
logLevel: 'silent',
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Fetch model info from ORBIT backend for each adapter (lazy, short-lived cache).
|
|
298
|
+
// Lazy model hydration for adapter cards (mirrors vite dev behavior).
|
|
624
299
|
let modelsLastFetchedAt = 0;
|
|
625
300
|
let modelsFetchInFlight = null;
|
|
626
301
|
const MODELS_CACHE_TTL_MS = 30000;
|
|
627
|
-
|
|
302
|
+
|
|
303
|
+
async function fetchAdapterModels(adapterMap, force = false) {
|
|
628
304
|
const now = Date.now();
|
|
629
305
|
if (!force && (now - modelsLastFetchedAt) < MODELS_CACHE_TTL_MS) return;
|
|
630
306
|
if (modelsFetchInFlight) return modelsFetchInFlight;
|
|
631
307
|
|
|
632
308
|
modelsFetchInFlight = (async () => {
|
|
633
|
-
const fetches = Object.entries(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
309
|
+
const fetches = Object.entries(adapterMap).map(async ([, adapter]) => {
|
|
310
|
+
if (!adapter.apiUrl || !adapter.apiKey) return;
|
|
311
|
+
try {
|
|
312
|
+
const url = `${String(adapter.apiUrl).replace(/\/+$/, '')}/admin/adapters/info`;
|
|
313
|
+
const resp = await fetch(url, {
|
|
314
|
+
headers: { 'X-API-Key': adapter.apiKey },
|
|
315
|
+
signal: AbortSignal.timeout(5000),
|
|
316
|
+
});
|
|
317
|
+
if (resp.ok) {
|
|
318
|
+
const info = await resp.json();
|
|
319
|
+
adapter.model = typeof info?.model === 'string' ? info.model.trim() || undefined : undefined;
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
// Best-effort only; cards can render without model metadata.
|
|
645
323
|
}
|
|
646
|
-
} catch {
|
|
647
|
-
// Silently ignore — model will be omitted
|
|
648
|
-
}
|
|
649
324
|
});
|
|
650
325
|
await Promise.all(fetches);
|
|
651
326
|
modelsLastFetchedAt = Date.now();
|
|
@@ -656,256 +331,136 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
656
331
|
return modelsFetchInFlight;
|
|
657
332
|
}
|
|
658
333
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const forceRefresh = req.query?.refresh === '1' || cacheControl.includes('no-cache');
|
|
662
|
-
await fetchAdapterModels(forceRefresh);
|
|
663
|
-
const adapterList = Object.keys(adapters).map(name => ({
|
|
334
|
+
const buildAdapterList = (adapterMap) =>
|
|
335
|
+
Object.keys(adapterMap).map(name => ({
|
|
664
336
|
name,
|
|
665
|
-
description:
|
|
666
|
-
notes:
|
|
667
|
-
model:
|
|
337
|
+
description: adapterMap[name].description,
|
|
338
|
+
notes: adapterMap[name].notes,
|
|
339
|
+
model: adapterMap[name].model || null
|
|
668
340
|
}));
|
|
669
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
670
|
-
res.json({ adapters: adapterList });
|
|
671
|
-
});
|
|
672
341
|
|
|
673
|
-
app.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
const adapterName = req.headers['x-adapter-name'];
|
|
342
|
+
app.get('/api/adapters', (req, res) => {
|
|
343
|
+
const cacheControlHeader = typeof req.headers['cache-control'] === 'string' ? req.headers['cache-control'] : '';
|
|
344
|
+
const forceRefresh = req.url?.includes('refresh=1') || cacheControlHeader.includes('no-cache');
|
|
678
345
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
346
|
+
fetchAdapterModels(adapters, forceRefresh).then(() => {
|
|
347
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
348
|
+
res.json({ adapters: buildAdapterList(adapters) });
|
|
349
|
+
}).catch(() => {
|
|
350
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
351
|
+
res.json({ adapters: buildAdapterList(adapters) });
|
|
352
|
+
});
|
|
353
|
+
});
|
|
682
354
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
355
|
+
const dynamicProxy = createProxyMiddleware({
|
|
356
|
+
target: 'http://localhost:3000', // Default fallback
|
|
357
|
+
router: (req) => {
|
|
358
|
+
const adapterName = req.headers['x-adapter-name'];
|
|
359
|
+
return adapters[adapterName]?.apiUrl;
|
|
360
|
+
},
|
|
361
|
+
changeOrigin: true,
|
|
362
|
+
pathRewrite: (p) => p.startsWith('/files') || p.startsWith('/threads') ? '/api' + p : p,
|
|
363
|
+
on: {
|
|
364
|
+
proxyReq: (proxyReq, reqIncoming) => {
|
|
365
|
+
const adapterName = reqIncoming.headers['x-adapter-name'];
|
|
366
|
+
const adapter = adapters[adapterName];
|
|
367
|
+
if (adapter) {
|
|
368
|
+
proxyReq.setHeader('X-API-Key', adapter.apiKey);
|
|
369
|
+
}
|
|
370
|
+
proxyReq.removeHeader('x-adapter-name');
|
|
371
|
+
['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length', 'authorization'].forEach(h => {
|
|
372
|
+
if (reqIncoming.headers[h]) proxyReq.setHeader(h, reqIncoming.headers[h]);
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
error: (err, _req, resProxy) => {
|
|
376
|
+
console.error('[Proxy] Proxy error:', err);
|
|
377
|
+
if (!resProxy.headersSent) {
|
|
378
|
+
resProxy.status(500).json({ error: 'Proxy error', message: err.message });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
logLevel: 'silent',
|
|
383
|
+
});
|
|
688
384
|
|
|
689
|
-
|
|
385
|
+
app.use('/api', (req, res, next) => {
|
|
386
|
+
if (req.path === '/adapters') return next('route');
|
|
387
|
+
const adapterName = req.headers['x-adapter-name'];
|
|
388
|
+
if (!adapterName) return res.status(400).json({ error: 'X-Adapter-Name header is required' });
|
|
389
|
+
if (!adapters[adapterName]) return res.status(404).json({ error: `Adapter '${adapterName}' not found` });
|
|
390
|
+
dynamicProxy(req, res, next);
|
|
690
391
|
});
|
|
691
392
|
}
|
|
692
393
|
|
|
693
394
|
app.use(express.json());
|
|
694
|
-
app.use(express.urlencoded({ extended: true }));
|
|
695
|
-
|
|
696
395
|
if (!apiOnly && distPath) {
|
|
697
|
-
app.get(['/', '/index.html'], (req, res) => {
|
|
698
|
-
try {
|
|
699
|
-
const indexPath = path.join(distPath, 'index.html');
|
|
700
|
-
let content = fs.readFileSync(indexPath, 'utf8');
|
|
701
|
-
content = injectConfig(content, config);
|
|
702
|
-
res.setHeader('Content-Type', 'text/html');
|
|
703
|
-
res.send(content);
|
|
704
|
-
} catch (error) {
|
|
705
|
-
console.error('Error serving index.html:', error);
|
|
706
|
-
res.status(500).send('Internal Server Error');
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
396
|
app.use(express.static(distPath, { index: false }));
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
let content = fs.readFileSync(indexPath, 'utf8');
|
|
722
|
-
content = injectConfig(content, config);
|
|
723
|
-
res.setHeader('Content-Type', 'text/html');
|
|
724
|
-
res.send(content);
|
|
725
|
-
} catch (error) {
|
|
726
|
-
console.error('Error serving index.html:', error);
|
|
727
|
-
res.status(500).send('Internal Server Error');
|
|
728
|
-
}
|
|
397
|
+
app.get(/(.*)/, (req, res) => {
|
|
398
|
+
if (req.path.startsWith('/api/')) return res.status(404).json({ error: 'Not found' });
|
|
399
|
+
const indexPath = path.join(distPath, 'index.html');
|
|
400
|
+
let content = fs.readFileSync(indexPath, 'utf8');
|
|
401
|
+
content = content.replace(/<script id="orbit-chat-config" type="application\/json">[\s\S]*?<\/script>/, '<!-- Config injected -->');
|
|
402
|
+
const configScript = `<script>window.ORBIT_CHAT_CONFIG = ${JSON.stringify(config)};</script>`;
|
|
403
|
+
content = content.replace(/<head>/i, '<head>\n ' + configScript);
|
|
404
|
+
if (config.application?.name) content = content.replace(/<title>.*?<\/title>/i, `<title>${config.application.name}</title>`);
|
|
405
|
+
res.setHeader('Content-Type', 'text/html');
|
|
406
|
+
res.send(content);
|
|
729
407
|
});
|
|
730
408
|
}
|
|
731
|
-
|
|
732
409
|
return app;
|
|
733
410
|
}
|
|
734
411
|
|
|
735
|
-
// ---- Utilities ----
|
|
736
|
-
|
|
737
|
-
function openBrowser(url) {
|
|
738
|
-
const platform = process.platform;
|
|
739
|
-
let command;
|
|
740
|
-
if (platform === 'darwin') command = `open "${url}"`;
|
|
741
|
-
else if (platform === 'linux') command = `xdg-open "${url}"`;
|
|
742
|
-
else if (platform === 'win32') command = `start "${url}"`;
|
|
743
|
-
else return;
|
|
744
|
-
|
|
745
|
-
try { execSync(command, { stdio: 'ignore' }); } catch { /* ignore */ }
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function getVersion() {
|
|
749
|
-
try {
|
|
750
|
-
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
751
|
-
const packageContent = fs.readFileSync(packagePath, 'utf8');
|
|
752
|
-
return JSON.parse(packageContent).version || 'unknown';
|
|
753
|
-
} catch { return 'unknown'; }
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function printHelp() {
|
|
757
|
-
console.log(`
|
|
758
|
-
ORBIT Chat CLI
|
|
759
|
-
|
|
760
|
-
Usage: orbitchat [options]
|
|
761
|
-
|
|
762
|
-
All application settings are configured in orbitchat.yaml (see orbitchat.yaml.example).
|
|
763
|
-
Secrets (adapter API keys) go in VITE_ADAPTERS / ORBIT_ADAPTERS env var.
|
|
764
|
-
|
|
765
|
-
Options:
|
|
766
|
-
--port PORT Server port (default: 5173)
|
|
767
|
-
--host HOST Server host (default: localhost)
|
|
768
|
-
--open Open browser automatically
|
|
769
|
-
--config PATH Path to orbitchat.yaml (default: ./orbitchat.yaml)
|
|
770
|
-
--api-only Run API proxy only (no UI serving)
|
|
771
|
-
--cors-origin URL Allowed CORS origin in api-only mode (default: *)
|
|
772
|
-
--help, -h Show this help message
|
|
773
|
-
--version, -v Show version number
|
|
774
|
-
|
|
775
|
-
Environment Variables:
|
|
776
|
-
ORBIT_ADAPTERS or VITE_ADAPTERS JSON array of adapter configurations (secrets)
|
|
777
|
-
Example: '[{"name":"Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
778
|
-
|
|
779
|
-
Examples:
|
|
780
|
-
orbitchat --port 8080
|
|
781
|
-
orbitchat --config /path/to/orbitchat.yaml --open
|
|
782
|
-
orbitchat --api-only --cors-origin http://localhost:3001
|
|
783
|
-
`);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
412
|
// ---- Main ----
|
|
787
413
|
|
|
788
414
|
function main() {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
415
|
+
const args = process.argv.slice(2);
|
|
416
|
+
const serverConfig = { port: 5173, host: 'localhost', open: false, configFile: null, apiOnly: false, corsOrigin: '*' };
|
|
417
|
+
for (let i = 0; i < args.length; i++) {
|
|
418
|
+
if (args[i] === '--port') serverConfig.port = parseInt(args[++i], 10);
|
|
419
|
+
else if (args[i] === '--host') serverConfig.host = args[++i];
|
|
420
|
+
else if (args[i] === '--open') serverConfig.open = true;
|
|
421
|
+
else if (args[i] === '--config') serverConfig.configFile = args[++i];
|
|
422
|
+
else if (args[i] === '--api-only') serverConfig.apiOnly = true;
|
|
423
|
+
else if (args[i] === '--cors-origin') serverConfig.corsOrigin = args[++i];
|
|
424
|
+
else if (args[i] === '--help' || args[i] === '-h') { printHelp(); return; }
|
|
425
|
+
else if (args[i] === '--version' || args[i] === '-v') { console.log(getCliVersion()); return; }
|
|
796
426
|
}
|
|
797
427
|
|
|
798
|
-
const serverConfig = parseArgs();
|
|
799
428
|
loadDotEnv(process.cwd());
|
|
800
|
-
|
|
801
|
-
// Load YAML config
|
|
802
429
|
const yamlPath = serverConfig.configFile || path.join(process.cwd(), 'orbitchat.yaml');
|
|
803
430
|
const yamlObj = loadYamlConfig(yamlPath);
|
|
804
|
-
|
|
431
|
+
let config = deepMerge(DEFAULTS, yamlObj || {});
|
|
805
432
|
|
|
806
|
-
if
|
|
807
|
-
|
|
808
|
-
|
|
433
|
+
// Overlay secret env vars if they exist
|
|
434
|
+
if (process.env.VITE_AUTH_DOMAIN) config.auth.domain = process.env.VITE_AUTH_DOMAIN;
|
|
435
|
+
if (process.env.VITE_AUTH_CLIENT_ID) config.auth.clientId = process.env.VITE_AUTH_CLIENT_ID;
|
|
436
|
+
if (process.env.VITE_AUTH_AUDIENCE) config.auth.audience = process.env.VITE_AUTH_AUDIENCE;
|
|
809
437
|
|
|
810
|
-
// Merge: DEFAULTS < YAML config < auth secrets from env
|
|
811
|
-
const config = { ...DEFAULTS, ...yamlFlat };
|
|
812
438
|
const localAssets = {};
|
|
439
|
+
const mapHeaderLogoAsset = (fieldName, assetId) => {
|
|
440
|
+
const resPath = resolveLocalAssetPath(config.header?.[fieldName], yamlPath);
|
|
441
|
+
if (!resPath) return;
|
|
442
|
+
localAssets[assetId] = resPath;
|
|
443
|
+
config.header[fieldName] = `/__orbitchat_assets/${assetId}?v=${Date.now()}`;
|
|
444
|
+
};
|
|
813
445
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const mtimeMs = fs.statSync(resolvedHeaderLogoPath).mtimeMs || Date.now();
|
|
818
|
-
localAssets.header_logo = resolvedHeaderLogoPath;
|
|
819
|
-
config.headerLogoUrl = `/__orbitchat_assets/header_logo?v=${Math.floor(mtimeMs)}`;
|
|
820
|
-
}
|
|
446
|
+
mapHeaderLogoAsset('logoUrl', 'header_logo');
|
|
447
|
+
mapHeaderLogoAsset('logoUrlLight', 'header_logo_light');
|
|
448
|
+
mapHeaderLogoAsset('logoUrlDark', 'header_logo_dark');
|
|
821
449
|
|
|
822
|
-
// Auth secrets from env
|
|
823
|
-
if (process.env.VITE_AUTH_DOMAIN) config.authDomain = process.env.VITE_AUTH_DOMAIN;
|
|
824
|
-
if (process.env.VITE_AUTH_CLIENT_ID) config.authClientId = process.env.VITE_AUTH_CLIENT_ID;
|
|
825
|
-
if (process.env.VITE_AUTH_AUDIENCE) config.authAudience = process.env.VITE_AUTH_AUDIENCE;
|
|
826
|
-
|
|
827
|
-
// Default adapter fallback
|
|
828
|
-
const trimmedDefaultKey = (config.defaultKey || '').trim();
|
|
829
|
-
if (!trimmedDefaultKey || trimmedDefaultKey === DEFAULTS.defaultKey) {
|
|
830
|
-
const fallbackAdapter = getDefaultAdapterFromEnv();
|
|
831
|
-
if (fallbackAdapter) {
|
|
832
|
-
config.defaultKey = fallbackAdapter;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Guest rate limiting (server-only, never sent to browser)
|
|
837
|
-
if (yamlObj && yamlObj.guestLimits?.rateLimit) {
|
|
838
|
-
serverConfig.rateLimit = yamlObj.guestLimits.rateLimit;
|
|
839
|
-
}
|
|
840
|
-
if (Object.keys(localAssets).length > 0) {
|
|
841
|
-
serverConfig.localAssets = localAssets;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Find dist directory
|
|
845
450
|
const distPath = path.join(__dirname, '..', 'dist');
|
|
451
|
+
const app = createServer(distPath, config, { ...serverConfig, rateLimit: yamlObj?.guestLimits?.rateLimit, localAssets });
|
|
846
452
|
|
|
847
|
-
|
|
848
|
-
console.
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const app = createServer(
|
|
853
|
-
serverConfig.apiOnly ? null : distPath,
|
|
854
|
-
config,
|
|
855
|
-
serverConfig
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
app.listen(serverConfig.port, serverConfig.host, () => {
|
|
859
|
-
const url = `http://${serverConfig.host}:${serverConfig.port}`;
|
|
860
|
-
if (serverConfig.apiOnly) {
|
|
861
|
-
console.debug(`\n🚀 ORBIT API Proxy is running at ${url}\n`);
|
|
862
|
-
} else {
|
|
863
|
-
console.debug(`\n🚀 ORBIT Chat App is running at ${url}\n`);
|
|
864
|
-
}
|
|
865
|
-
console.debug('Configuration:');
|
|
866
|
-
console.debug(` Mode: ${serverConfig.apiOnly ? 'API-only (no UI)' : 'Full (API + UI)'}`);
|
|
867
|
-
console.debug(` API URL: ${config.apiUrl}`);
|
|
868
|
-
console.debug(` Default Adapter: ${config.defaultKey || '(not set)'}`);
|
|
869
|
-
console.debug(` Port: ${serverConfig.port}`);
|
|
870
|
-
console.debug(` Host: ${serverConfig.host}`);
|
|
871
|
-
if (yamlObj) {
|
|
872
|
-
console.debug(` Config: ${yamlPath}`);
|
|
873
|
-
}
|
|
874
|
-
if (resolvedHeaderLogoPath) {
|
|
875
|
-
console.debug(` Header logo file: ${resolvedHeaderLogoPath}`);
|
|
876
|
-
}
|
|
877
|
-
if (serverConfig.rateLimit && serverConfig.rateLimit.enabled !== false) {
|
|
878
|
-
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)`);
|
|
879
|
-
}
|
|
880
|
-
const startupAdapters = loadAdaptersConfig();
|
|
881
|
-
if (startupAdapters) {
|
|
882
|
-
console.debug(` Available Adapters: ${Object.keys(startupAdapters).join(', ')}`);
|
|
883
|
-
} else {
|
|
884
|
-
console.debug(` Warning: No adapters configured. Set ORBIT_ADAPTERS or VITE_ADAPTERS environment variable.`);
|
|
885
|
-
}
|
|
886
|
-
console.debug('');
|
|
887
|
-
|
|
888
|
-
if (serverConfig.open) {
|
|
889
|
-
openBrowser(url);
|
|
890
|
-
}
|
|
453
|
+
const server = app.listen(serverConfig.port, serverConfig.host, () => {
|
|
454
|
+
console.debug(`🚀 ORBIT Chat is running at http://${serverConfig.host}:${serverConfig.port}`);
|
|
455
|
+
if (serverConfig.open) execSync(`open http://${serverConfig.host}:${serverConfig.port}`);
|
|
891
456
|
});
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Run if called directly
|
|
900
|
-
const isMainModule = process.argv[1] && (
|
|
901
|
-
import.meta.url === `file://${process.argv[1]}` ||
|
|
902
|
-
import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) ||
|
|
903
|
-
path.basename(process.argv[1]) === 'orbitchat' ||
|
|
904
|
-
path.basename(process.argv[1]) === 'orbitchat.js'
|
|
905
|
-
);
|
|
906
|
-
|
|
907
|
-
if (isMainModule) {
|
|
908
|
-
main();
|
|
457
|
+
// http-proxy may register multiple close listeners when routing across many adapters.
|
|
458
|
+
// Raise the listener cap to avoid noisy false-positive MaxListeners warnings.
|
|
459
|
+
if (typeof server.setMaxListeners === 'function') {
|
|
460
|
+
server.setMaxListeners(0);
|
|
461
|
+
}
|
|
909
462
|
}
|
|
910
463
|
|
|
911
|
-
|
|
464
|
+
const isMainModule = process.argv[1] && (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) || path.basename(process.argv[1]) === 'orbitchat');
|
|
465
|
+
if (isMainModule) main();
|
|
466
|
+
export { main, createServer };
|