orbitchat 3.1.1 → 3.1.2
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 +258 -729
- package/dist/assets/{ChartRenderer-CapBCpbV.js → ChartRenderer-C6fHQxUM.js} +1 -1
- package/dist/assets/{MermaidRenderer-DQJRirnm.js → MermaidRenderer-uOsrV112.js} +5 -5
- package/dist/assets/{MusicRenderer-9vnA54gg.js → MusicRenderer-C-8fXPzl.js} +2 -2
- package/dist/assets/{SVGRenderer-By7CKtqU.js → SVGRenderer-7yLFncSt.js} +1 -1
- package/dist/assets/{_basePickBy-Dm-zx8im.js → _basePickBy-BaAS07_g.js} +1 -1
- package/dist/assets/{_baseUniq-DLPvoy-d.js → _baseUniq-C0rd4rnF.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-B1kMlTMW.js → architectureDiagram-VXUJARFQ-DB0b91Sa.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-BlglDb-U.js → blockDiagram-VD42YOAC-BQ4Z_gCK.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-CaZm--Yy.js → c4Diagram-YG6GDRKO-Dtix_O1L.js} +1 -1
- package/dist/assets/channel-CuLY6R17.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-4Kq-2AyN.js → chunk-4BX2VUAB-BMK9KFRb.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-FApueVFk.js → chunk-55IACEB6-DireVK0h.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-CHox7be9.js → chunk-B4BG7PRW-BRwwDrXp.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-N5sePLFo.js → chunk-DI55MBZ5-DeD4e72t.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BSbBs0xy.js → chunk-FMBD7UC4-D1-BcrE_.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-ByurI9-W.js → chunk-QN33PNHL-5dHN5THo.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Yy8VVut9.js → chunk-QZHKN3VN-BFj7kkUd.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-B5oe8pT4.js → chunk-TZMSLE5B-B5JCOX6Y.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-CCd0SByT.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-CCd0SByT.js +1 -0
- package/dist/assets/clone-DRrrmN93.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DS_-k6qR.js → cose-bilkent-S5V4N54A-CmgTSX_t.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-DgkwJjVY.js → dagre-6UL2VRFP-C3rzyEFR.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-C9su64f9.js → diagram-PSM6KHXK-CyHeoxQN.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-DY6-9Dbc.js → diagram-QEK2KX5R-2gCRyxH5.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-_Wub-Eik.js → diagram-S2PKOQOG-BV4hBni4.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-Gwx9nSAM.js → erDiagram-Q2GNP2WA-DLJ0STwx.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-DmrXTF_S.js → flowDiagram-NV44I4VS-DaJev1Dk.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-DymVUgsd.js → ganttDiagram-JELNMOA3-BVqSlqWd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-D3VSQFT4.js → gitGraphDiagram-V2S2FVAM-fILg4kGK.js} +1 -1
- package/dist/assets/{graph-BIe-N9yO.js → graph-CMQ9In6q.js} +1 -1
- package/dist/assets/{index-BiM6VNSE.js → index-Bp6Dtnjv.js} +37 -37
- package/dist/assets/{index-B-NnBj1Y.js → index-CeOmPokb.js} +116 -116
- package/dist/assets/index-w5RjGGjS.css +1 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-D2DeUrgk.js → infoDiagram-HS3SLOUP-D7dyBxUc.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-GxFhv1XB.js → journeyDiagram-XKPGCS4Q-B5mS1ZV0.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-BhT-ga9I.js → kanban-definition-3W4ZIXB7-BiWSAAjN.js} +1 -1
- package/dist/assets/{layout-38O87Ivt.js → layout-BME0mjSq.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-DfKjmcaV.js → mindmap-definition-VGOIOE7T-CoH0z1ra.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-D7lDWRGN.js → pieDiagram-ADFJNKIX-Dq0SKRlZ.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CPtZ8LgW.js → quadrantDiagram-AYHSOK5B-DwutakEK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-h-H7R8sR.js → requirementDiagram-UZGBJVZJ-BzFetBkD.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-C6cn5g4m.js → sankeyDiagram-TZEHDZUN-BbOUy78i.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-B6AwcQ4F.js → sequenceDiagram-WL72ISMW-1ZNpnZKs.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-B4ne7z3I.js → stateDiagram-FKZM4ZOC-BAP0pbLV.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-HGuFIOmd.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-BmjJnJ-r.js → timeline-definition-IT6M3QCI-CuyYlb9E.js} +1 -1
- package/dist/assets/treemap-GDKQZRPO-CUELWxdP.js +160 -0
- package/dist/assets/{xychartDiagram-PRI3JC2R-C3lVgboM.js → xychartDiagram-PRI3JC2R-CaZOodKt.js} +1 -1
- package/dist/index.html +2 -2
- package/orbitchat.sh +94 -36
- 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/dist/assets/treemap-GDKQZRPO-CBUbyJIX.js +0 -160
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,14 @@ import rateLimit from 'express-rate-limit';
|
|
|
23
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
21
|
const __dirname = dirname(__filename);
|
|
25
22
|
|
|
26
|
-
// ---- Minimal .env loader
|
|
23
|
+
// ---- Minimal .env loader ----
|
|
27
24
|
|
|
28
25
|
function parseDotEnvValue(raw) {
|
|
29
26
|
const trimmed = raw.trim();
|
|
30
27
|
if (!trimmed) return '';
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
34
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
35
|
-
) {
|
|
28
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
36
29
|
return trimmed.slice(1, -1);
|
|
37
30
|
}
|
|
38
|
-
|
|
39
31
|
return trimmed;
|
|
40
32
|
}
|
|
41
33
|
|
|
@@ -43,217 +35,106 @@ function loadDotEnvFromFile(filePath) {
|
|
|
43
35
|
if (!fs.existsSync(filePath)) return;
|
|
44
36
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
37
|
const lines = content.split(/\r?\n/);
|
|
46
|
-
|
|
47
38
|
for (let i = 0; i < lines.length; i++) {
|
|
48
39
|
const trimmed = lines[i].trim();
|
|
49
40
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
50
|
-
|
|
51
41
|
const equalsIndex = trimmed.indexOf('=');
|
|
52
42
|
if (equalsIndex <= 0) continue;
|
|
53
|
-
|
|
54
43
|
const key = trimmed.slice(0, equalsIndex).trim();
|
|
55
|
-
if (!key) continue;
|
|
56
|
-
|
|
57
44
|
if (process.env[key] !== undefined) continue;
|
|
58
|
-
|
|
59
45
|
let valueRaw = trimmed.slice(equalsIndex + 1);
|
|
60
46
|
const startsWithDouble = valueRaw.startsWith('"');
|
|
61
47
|
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
|
-
) {
|
|
48
|
+
if ((startsWithDouble && !valueRaw.endsWith('"')) || (startsWithSingle && !valueRaw.endsWith("'"))) {
|
|
68
49
|
const quote = startsWithDouble ? '"' : "'";
|
|
69
50
|
while (i + 1 < lines.length) {
|
|
70
51
|
i += 1;
|
|
71
52
|
valueRaw += `\n${lines[i]}`;
|
|
72
|
-
if (lines[i].trim().endsWith(quote))
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
53
|
+
if (lines[i].trim().endsWith(quote)) break;
|
|
75
54
|
}
|
|
76
55
|
}
|
|
77
|
-
|
|
78
|
-
const value = parseDotEnvValue(valueRaw);
|
|
79
|
-
process.env[key] = value;
|
|
56
|
+
process.env[key] = parseDotEnvValue(valueRaw);
|
|
80
57
|
}
|
|
81
58
|
}
|
|
82
59
|
|
|
83
60
|
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
61
|
loadDotEnvFromFile(path.join(baseDir, '.env.local'));
|
|
62
|
+
loadDotEnvFromFile(path.join(baseDir, '.env'));
|
|
87
63
|
}
|
|
88
64
|
|
|
89
|
-
// ----
|
|
65
|
+
// ---- Deep Merge ----
|
|
66
|
+
|
|
67
|
+
function isObject(item) {
|
|
68
|
+
return typeof item === 'object' && item !== null && !Array.isArray(item);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deepMerge(target, source) {
|
|
72
|
+
if (!isObject(target) || !isObject(source)) return source;
|
|
73
|
+
const output = { ...target };
|
|
74
|
+
Object.keys(source).forEach(key => {
|
|
75
|
+
if (isObject(target[key]) && isObject(source[key])) {
|
|
76
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
77
|
+
} else if (source[key] !== undefined) {
|
|
78
|
+
output[key] = source[key];
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- Defaults ----
|
|
90
85
|
|
|
91
86
|
const DEFAULTS = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
87
|
+
application: {
|
|
88
|
+
name: 'ORBIT Chat',
|
|
89
|
+
description: "Explore ideas with ORBIT's AI copilots, share context, and build together.",
|
|
90
|
+
inputPlaceholder: 'Message ORBIT...',
|
|
91
|
+
settingsAboutMsg: 'ORBIT Chat',
|
|
92
|
+
locale: 'en-US',
|
|
93
|
+
},
|
|
94
|
+
debug: {
|
|
95
|
+
consoleDebug: false,
|
|
96
|
+
},
|
|
97
|
+
features: {
|
|
98
|
+
enableUpload: false,
|
|
99
|
+
enableAudioOutput: false,
|
|
100
|
+
enableAudioInput: false,
|
|
101
|
+
enableFeedbackButtons: false,
|
|
102
|
+
enableConversationThreads: true,
|
|
103
|
+
enableAutocomplete: false,
|
|
104
|
+
},
|
|
105
|
+
voice: {
|
|
106
|
+
silenceTimeoutMs: 4000,
|
|
107
|
+
recognitionLanguage: '',
|
|
108
|
+
},
|
|
109
|
+
github: {
|
|
110
|
+
showStats: true,
|
|
111
|
+
owner: 'schmitech',
|
|
112
|
+
repo: 'orbit',
|
|
113
|
+
},
|
|
108
114
|
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: [],
|
|
115
|
+
limits: {
|
|
116
|
+
files: { perConversation: 5, maxSizeMB: 50, totalFiles: 100 },
|
|
117
|
+
conversations: { maxConversations: 10, messagesPerConversation: 1000, messagesPerThread: 1000, totalMessages: 10000 },
|
|
118
|
+
messages: { maxLength: 1000 },
|
|
119
|
+
},
|
|
120
|
+
guestLimits: {
|
|
121
|
+
files: { perConversation: 1, maxSizeMB: 10, totalFiles: 2 },
|
|
122
|
+
conversations: { maxConversations: 1, messagesPerConversation: 10, messagesPerThread: 10, totalMessages: 10 },
|
|
123
|
+
messages: { maxLength: 500 },
|
|
124
|
+
},
|
|
125
|
+
auth: { enabled: false, domain: '', clientId: '', audience: '' },
|
|
126
|
+
header: { enabled: false, logoUrl: '', logoUrlLight: '', logoUrlDark: '', brandName: '', bgColor: '', textColor: '', showBorder: true, navLinks: [] },
|
|
127
|
+
footer: { enabled: false, text: '', bgColor: '', textColor: '', showBorder: false, layout: 'stacked', align: 'center', topPadding: 'large', navLinks: [] },
|
|
128
|
+
adapters: [],
|
|
143
129
|
};
|
|
144
130
|
|
|
145
131
|
// ---- YAML config loading ----
|
|
146
132
|
|
|
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
133
|
function loadYamlConfig(configPath) {
|
|
250
134
|
try {
|
|
251
135
|
if (fs.existsSync(configPath)) {
|
|
252
136
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
253
|
-
|
|
254
|
-
if (parsed && typeof parsed === 'object') {
|
|
255
|
-
return parsed;
|
|
256
|
-
}
|
|
137
|
+
return yaml.load(content);
|
|
257
138
|
}
|
|
258
139
|
} catch (error) {
|
|
259
140
|
console.error(`Error: Failed to parse ${configPath}: ${error.message}`);
|
|
@@ -262,245 +143,100 @@ function loadYamlConfig(configPath) {
|
|
|
262
143
|
return null;
|
|
263
144
|
}
|
|
264
145
|
|
|
265
|
-
// ---- Local asset handling
|
|
266
|
-
|
|
267
|
-
function isUrlLike(value) {
|
|
268
|
-
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) || value.startsWith('//');
|
|
269
|
-
}
|
|
146
|
+
// ---- Local asset handling ----
|
|
270
147
|
|
|
271
148
|
function resolveLocalAssetPath(rawValue, yamlPath) {
|
|
272
149
|
if (!rawValue || typeof rawValue !== 'string') return null;
|
|
273
150
|
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
|
-
|
|
151
|
+
if (!value || /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) || value.startsWith('//')) return null;
|
|
152
|
+
const expandedValue = value.startsWith('~/') ? path.join(process.env.HOME || '', value.slice(2)) : value;
|
|
280
153
|
const yamlDir = path.dirname(yamlPath);
|
|
281
|
-
const candidates = path.isAbsolute(expandedValue)
|
|
282
|
-
? [expandedValue]
|
|
283
|
-
: [path.resolve(yamlDir, expandedValue), path.resolve(process.cwd(), expandedValue)];
|
|
284
|
-
|
|
154
|
+
const candidates = path.isAbsolute(expandedValue) ? [expandedValue] : [path.resolve(yamlDir, expandedValue), path.resolve(process.cwd(), expandedValue)];
|
|
285
155
|
for (const candidate of candidates) {
|
|
286
156
|
try {
|
|
287
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
} catch {
|
|
291
|
-
// ignore invalid candidate and continue
|
|
292
|
-
}
|
|
157
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return fs.realpathSync(candidate);
|
|
158
|
+
} catch { /* ignore */ }
|
|
293
159
|
}
|
|
294
|
-
|
|
295
160
|
return null;
|
|
296
161
|
}
|
|
297
162
|
|
|
298
163
|
// ---- Adapter loading (env secrets + YAML metadata) ----
|
|
299
164
|
|
|
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
|
-
|
|
165
|
+
function loadAdaptersForProxy(yamlAdapters) {
|
|
327
166
|
const adapters = {};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
167
|
+
const fallbackApiUrl = 'http://localhost:3000';
|
|
168
|
+
|
|
169
|
+
if (Array.isArray(yamlAdapters)) {
|
|
170
|
+
for (const ya of yamlAdapters) {
|
|
171
|
+
if (!ya.name) continue;
|
|
172
|
+
adapters[ya.name] = {
|
|
173
|
+
apiKey: '',
|
|
174
|
+
apiUrl: ya.apiUrl || fallbackApiUrl,
|
|
175
|
+
description: ya.description,
|
|
176
|
+
notes: ya.notes,
|
|
177
|
+
model: ya.model
|
|
178
|
+
};
|
|
179
|
+
}
|
|
335
180
|
}
|
|
336
181
|
|
|
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);
|
|
182
|
+
const envKeysRaw = process.env.VITE_ADAPTER_KEYS || process.env.ORBIT_ADAPTER_KEYS;
|
|
183
|
+
if (envKeysRaw) {
|
|
184
|
+
try {
|
|
185
|
+
const keys = JSON.parse(envKeysRaw);
|
|
186
|
+
for (const [name, value] of Object.entries(keys)) {
|
|
187
|
+
const isObjectValue = typeof value === 'object' && value !== null;
|
|
188
|
+
const apiKey = isObjectValue
|
|
189
|
+
? String(value.apiKey || value.key || '')
|
|
190
|
+
: String(value);
|
|
191
|
+
const apiUrl = isObjectValue && value.apiUrl ? String(value.apiUrl) : undefined;
|
|
192
|
+
const description = isObjectValue && value.description ? String(value.description) : undefined;
|
|
193
|
+
const notes = isObjectValue && value.notes ? String(value.notes) : undefined;
|
|
194
|
+
const model = isObjectValue && value.model ? String(value.model) : undefined;
|
|
195
|
+
|
|
196
|
+
if (!adapters[name]) {
|
|
197
|
+
adapters[name] = {
|
|
198
|
+
apiKey,
|
|
199
|
+
apiUrl: apiUrl || fallbackApiUrl,
|
|
200
|
+
description,
|
|
201
|
+
notes,
|
|
202
|
+
model
|
|
203
|
+
};
|
|
204
|
+
} else {
|
|
205
|
+
adapters[name].apiKey = apiKey;
|
|
206
|
+
if (apiUrl) adapters[name].apiUrl = apiUrl;
|
|
207
|
+
if (description !== undefined) adapters[name].description = description;
|
|
208
|
+
if (notes !== undefined) adapters[name].notes = notes;
|
|
209
|
+
if (model !== undefined) adapters[name].model = model;
|
|
389
210
|
}
|
|
390
|
-
|
|
211
|
+
}
|
|
212
|
+
} catch { /* ignore */ }
|
|
391
213
|
}
|
|
392
214
|
|
|
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
|
-
);
|
|
215
|
+
const finalAdapters = {};
|
|
216
|
+
for (const [name, config] of Object.entries(adapters)) {
|
|
217
|
+
if (config.apiKey) finalAdapters[name] = config;
|
|
417
218
|
}
|
|
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 };
|
|
219
|
+
if (Object.keys(finalAdapters).length > 0) {
|
|
220
|
+
console.debug(`Loaded ${Object.keys(finalAdapters).length} adapters with API keys from environment.`);
|
|
221
|
+
}
|
|
222
|
+
return Object.keys(finalAdapters).length > 0 ? finalAdapters : null;
|
|
479
223
|
}
|
|
480
224
|
|
|
481
225
|
// ---- Express server ----
|
|
482
226
|
|
|
483
227
|
function createServer(distPath, config, serverConfig = {}) {
|
|
484
228
|
const app = express();
|
|
485
|
-
const adapters =
|
|
229
|
+
const adapters = loadAdaptersForProxy(config.adapters);
|
|
486
230
|
const apiOnly = serverConfig.apiOnly || false;
|
|
487
231
|
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
232
|
|
|
494
233
|
if (apiOnly) {
|
|
495
234
|
const allowedOrigin = serverConfig.corsOrigin || '*';
|
|
496
235
|
app.use((req, res, next) => {
|
|
497
236
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
498
237
|
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
|
-
}
|
|
238
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, X-Session-ID, X-Thread-ID, X-Adapter-Name, Accept, Authorization');
|
|
239
|
+
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
|
504
240
|
next();
|
|
505
241
|
});
|
|
506
242
|
}
|
|
@@ -508,144 +244,59 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
508
244
|
if (Object.keys(localAssets).length > 0) {
|
|
509
245
|
app.get('/__orbitchat_assets/:assetId', (req, res) => {
|
|
510
246
|
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
|
-
}
|
|
247
|
+
if (!assetPath || !fs.existsSync(assetPath)) return res.status(404).send('Asset not found');
|
|
517
248
|
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
|
-
});
|
|
249
|
+
res.sendFile(assetPath);
|
|
523
250
|
});
|
|
524
251
|
}
|
|
525
252
|
|
|
526
|
-
// Guest rate limiting
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
253
|
+
// Guest rate limiting
|
|
254
|
+
if (serverConfig.rateLimit?.enabled !== false) {
|
|
255
|
+
const rl = serverConfig.rateLimit || {};
|
|
256
|
+
const apiLimiter = rateLimit({
|
|
257
|
+
windowMs: rl.windowMs || 60000, max: rl.maxRequests || 30,
|
|
258
|
+
skip: (req) => req.method === 'OPTIONS' || req.path === '/adapters',
|
|
259
|
+
handler: (req, res) => res.status(429).json({ error: 'Too many requests' }),
|
|
260
|
+
});
|
|
261
|
+
const chatLimiter = rateLimit({
|
|
262
|
+
windowMs: rl.chat?.windowMs || 60000, max: rl.chat?.maxRequests || 10,
|
|
263
|
+
handler: (req, res) => res.status(429).json({ error: 'Chat rate limit exceeded' }),
|
|
532
264
|
});
|
|
265
|
+
app.use('/api', (req, res, next) => { if (req.headers.authorization) return next(); apiLimiter(req, res, next); });
|
|
533
266
|
app.use('/api', (req, res, next) => {
|
|
534
267
|
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
|
-
}
|
|
268
|
+
if (req.method === 'POST' && (/\/chat/i.test(req.path) || /\/stream/i.test(req.path))) return chatLimiter(req, res, next);
|
|
538
269
|
next();
|
|
539
270
|
});
|
|
540
271
|
}
|
|
541
272
|
|
|
542
|
-
// API proxy endpoints - MUST be before body parsers
|
|
543
273
|
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).
|
|
274
|
+
// Lazy model hydration for adapter cards (mirrors vite dev behavior).
|
|
624
275
|
let modelsLastFetchedAt = 0;
|
|
625
276
|
let modelsFetchInFlight = null;
|
|
626
277
|
const MODELS_CACHE_TTL_MS = 30000;
|
|
627
|
-
|
|
278
|
+
|
|
279
|
+
async function fetchAdapterModels(adapterMap, force = false) {
|
|
628
280
|
const now = Date.now();
|
|
629
281
|
if (!force && (now - modelsLastFetchedAt) < MODELS_CACHE_TTL_MS) return;
|
|
630
282
|
if (modelsFetchInFlight) return modelsFetchInFlight;
|
|
631
283
|
|
|
632
284
|
modelsFetchInFlight = (async () => {
|
|
633
|
-
const fetches = Object.entries(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
285
|
+
const fetches = Object.entries(adapterMap).map(async ([, adapter]) => {
|
|
286
|
+
if (!adapter.apiUrl || !adapter.apiKey) return;
|
|
287
|
+
try {
|
|
288
|
+
const url = `${String(adapter.apiUrl).replace(/\/+$/, '')}/admin/adapters/info`;
|
|
289
|
+
const resp = await fetch(url, {
|
|
290
|
+
headers: { 'X-API-Key': adapter.apiKey },
|
|
291
|
+
signal: AbortSignal.timeout(5000),
|
|
292
|
+
});
|
|
293
|
+
if (resp.ok) {
|
|
294
|
+
const info = await resp.json();
|
|
295
|
+
adapter.model = typeof info?.model === 'string' ? info.model.trim() || undefined : undefined;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Best-effort only; cards can render without model metadata.
|
|
645
299
|
}
|
|
646
|
-
} catch {
|
|
647
|
-
// Silently ignore — model will be omitted
|
|
648
|
-
}
|
|
649
300
|
});
|
|
650
301
|
await Promise.all(fetches);
|
|
651
302
|
modelsLastFetchedAt = Date.now();
|
|
@@ -656,256 +307,134 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
656
307
|
return modelsFetchInFlight;
|
|
657
308
|
}
|
|
658
309
|
|
|
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 => ({
|
|
310
|
+
const buildAdapterList = (adapterMap) =>
|
|
311
|
+
Object.keys(adapterMap).map(name => ({
|
|
664
312
|
name,
|
|
665
|
-
description:
|
|
666
|
-
notes:
|
|
667
|
-
model:
|
|
313
|
+
description: adapterMap[name].description,
|
|
314
|
+
notes: adapterMap[name].notes,
|
|
315
|
+
model: adapterMap[name].model || null
|
|
668
316
|
}));
|
|
669
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
670
|
-
res.json({ adapters: adapterList });
|
|
671
|
-
});
|
|
672
317
|
|
|
673
|
-
app.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
const adapterName = req.headers['x-adapter-name'];
|
|
318
|
+
app.get('/api/adapters', (req, res) => {
|
|
319
|
+
const cacheControlHeader = typeof req.headers['cache-control'] === 'string' ? req.headers['cache-control'] : '';
|
|
320
|
+
const forceRefresh = req.url?.includes('refresh=1') || cacheControlHeader.includes('no-cache');
|
|
678
321
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
322
|
+
fetchAdapterModels(adapters, forceRefresh).then(() => {
|
|
323
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
324
|
+
res.json({ adapters: buildAdapterList(adapters) });
|
|
325
|
+
}).catch(() => {
|
|
326
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
327
|
+
res.json({ adapters: buildAdapterList(adapters) });
|
|
328
|
+
});
|
|
329
|
+
});
|
|
682
330
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
331
|
+
const dynamicProxy = createProxyMiddleware({
|
|
332
|
+
target: 'http://localhost:3000', // Default fallback
|
|
333
|
+
router: (req) => {
|
|
334
|
+
const adapterName = req.headers['x-adapter-name'];
|
|
335
|
+
return adapters[adapterName]?.apiUrl;
|
|
336
|
+
},
|
|
337
|
+
changeOrigin: true,
|
|
338
|
+
pathRewrite: (p) => p.startsWith('/files') || p.startsWith('/threads') ? '/api' + p : p,
|
|
339
|
+
on: {
|
|
340
|
+
proxyReq: (proxyReq, reqIncoming) => {
|
|
341
|
+
const adapterName = reqIncoming.headers['x-adapter-name'];
|
|
342
|
+
const adapter = adapters[adapterName];
|
|
343
|
+
if (adapter) {
|
|
344
|
+
proxyReq.setHeader('X-API-Key', adapter.apiKey);
|
|
345
|
+
}
|
|
346
|
+
proxyReq.removeHeader('x-adapter-name');
|
|
347
|
+
['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length', 'authorization'].forEach(h => {
|
|
348
|
+
if (reqIncoming.headers[h]) proxyReq.setHeader(h, reqIncoming.headers[h]);
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
error: (err, _req, resProxy) => {
|
|
352
|
+
console.error('[Proxy] Proxy error:', err);
|
|
353
|
+
if (!resProxy.headersSent) {
|
|
354
|
+
resProxy.status(500).json({ error: 'Proxy error', message: err.message });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
logLevel: 'silent',
|
|
359
|
+
});
|
|
688
360
|
|
|
689
|
-
|
|
361
|
+
app.use('/api', (req, res, next) => {
|
|
362
|
+
if (req.path === '/adapters') return next('route');
|
|
363
|
+
const adapterName = req.headers['x-adapter-name'];
|
|
364
|
+
if (!adapterName) return res.status(400).json({ error: 'X-Adapter-Name header is required' });
|
|
365
|
+
if (!adapters[adapterName]) return res.status(404).json({ error: `Adapter '${adapterName}' not found` });
|
|
366
|
+
dynamicProxy(req, res, next);
|
|
690
367
|
});
|
|
691
368
|
}
|
|
692
369
|
|
|
693
370
|
app.use(express.json());
|
|
694
|
-
app.use(express.urlencoded({ extended: true }));
|
|
695
|
-
|
|
696
371
|
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
372
|
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
|
-
}
|
|
373
|
+
app.get(/(.*)/, (req, res) => {
|
|
374
|
+
if (req.path.startsWith('/api/')) return res.status(404).json({ error: 'Not found' });
|
|
375
|
+
const indexPath = path.join(distPath, 'index.html');
|
|
376
|
+
let content = fs.readFileSync(indexPath, 'utf8');
|
|
377
|
+
content = content.replace(/<script id="orbit-chat-config" type="application\/json">[\s\S]*?<\/script>/, '<!-- Config injected -->');
|
|
378
|
+
const configScript = `<script>window.ORBIT_CHAT_CONFIG = ${JSON.stringify(config)};</script>`;
|
|
379
|
+
content = content.replace(/<head>/i, '<head>\n ' + configScript);
|
|
380
|
+
if (config.application?.name) content = content.replace(/<title>.*?<\/title>/i, `<title>${config.application.name}</title>`);
|
|
381
|
+
res.setHeader('Content-Type', 'text/html');
|
|
382
|
+
res.send(content);
|
|
729
383
|
});
|
|
730
384
|
}
|
|
731
|
-
|
|
732
385
|
return app;
|
|
733
386
|
}
|
|
734
387
|
|
|
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
388
|
// ---- Main ----
|
|
787
389
|
|
|
788
390
|
function main() {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
391
|
+
const args = process.argv.slice(2);
|
|
392
|
+
const serverConfig = { port: 5173, host: 'localhost', open: false, configFile: null, apiOnly: false };
|
|
393
|
+
for (let i = 0; i < args.length; i++) {
|
|
394
|
+
if (args[i] === '--port') serverConfig.port = parseInt(args[++i], 10);
|
|
395
|
+
else if (args[i] === '--host') serverConfig.host = args[++i];
|
|
396
|
+
else if (args[i] === '--open') serverConfig.open = true;
|
|
397
|
+
else if (args[i] === '--config') serverConfig.configFile = args[++i];
|
|
398
|
+
else if (args[i] === '--api-only') serverConfig.apiOnly = true;
|
|
399
|
+
else if (args[i] === '--help' || args[i] === '-h') { console.log('ORBIT Chat CLI Help...'); return; }
|
|
796
400
|
}
|
|
797
401
|
|
|
798
|
-
const serverConfig = parseArgs();
|
|
799
402
|
loadDotEnv(process.cwd());
|
|
800
|
-
|
|
801
|
-
// Load YAML config
|
|
802
403
|
const yamlPath = serverConfig.configFile || path.join(process.cwd(), 'orbitchat.yaml');
|
|
803
404
|
const yamlObj = loadYamlConfig(yamlPath);
|
|
804
|
-
|
|
405
|
+
let config = deepMerge(DEFAULTS, yamlObj || {});
|
|
805
406
|
|
|
806
|
-
if
|
|
807
|
-
|
|
808
|
-
|
|
407
|
+
// Overlay secret env vars if they exist
|
|
408
|
+
if (process.env.VITE_AUTH_DOMAIN) config.auth.domain = process.env.VITE_AUTH_DOMAIN;
|
|
409
|
+
if (process.env.VITE_AUTH_CLIENT_ID) config.auth.clientId = process.env.VITE_AUTH_CLIENT_ID;
|
|
410
|
+
if (process.env.VITE_AUTH_AUDIENCE) config.auth.audience = process.env.VITE_AUTH_AUDIENCE;
|
|
809
411
|
|
|
810
|
-
// Merge: DEFAULTS < YAML config < auth secrets from env
|
|
811
|
-
const config = { ...DEFAULTS, ...yamlFlat };
|
|
812
412
|
const localAssets = {};
|
|
413
|
+
const mapHeaderLogoAsset = (fieldName, assetId) => {
|
|
414
|
+
const resPath = resolveLocalAssetPath(config.header?.[fieldName], yamlPath);
|
|
415
|
+
if (!resPath) return;
|
|
416
|
+
localAssets[assetId] = resPath;
|
|
417
|
+
config.header[fieldName] = `/__orbitchat_assets/${assetId}?v=${Date.now()}`;
|
|
418
|
+
};
|
|
813
419
|
|
|
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
|
-
}
|
|
821
|
-
|
|
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
|
-
}
|
|
420
|
+
mapHeaderLogoAsset('logoUrl', 'header_logo');
|
|
421
|
+
mapHeaderLogoAsset('logoUrlLight', 'header_logo_light');
|
|
422
|
+
mapHeaderLogoAsset('logoUrlDark', 'header_logo_dark');
|
|
843
423
|
|
|
844
|
-
// Find dist directory
|
|
845
424
|
const distPath = path.join(__dirname, '..', 'dist');
|
|
425
|
+
const app = createServer(distPath, config, { ...serverConfig, rateLimit: yamlObj?.guestLimits?.rateLimit, localAssets });
|
|
846
426
|
|
|
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
|
-
}
|
|
427
|
+
const server = app.listen(serverConfig.port, serverConfig.host, () => {
|
|
428
|
+
console.debug(`🚀 ORBIT Chat is running at http://${serverConfig.host}:${serverConfig.port}`);
|
|
429
|
+
if (serverConfig.open) execSync(`open http://${serverConfig.host}:${serverConfig.port}`);
|
|
891
430
|
});
|
|
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();
|
|
431
|
+
// http-proxy may register multiple close listeners when routing across many adapters.
|
|
432
|
+
// Raise the listener cap to avoid noisy false-positive MaxListeners warnings.
|
|
433
|
+
if (typeof server.setMaxListeners === 'function') {
|
|
434
|
+
server.setMaxListeners(0);
|
|
435
|
+
}
|
|
909
436
|
}
|
|
910
437
|
|
|
911
|
-
|
|
438
|
+
const isMainModule = process.argv[1] && (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) || path.basename(process.argv[1]) === 'orbitchat');
|
|
439
|
+
if (isMainModule) main();
|
|
440
|
+
export { main, createServer };
|