@jacobbd/relay-ai 0.4.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/{chunk-DO5FAMNC.js → chunk-MVBA7ABV.js} +22 -2
- package/dist/chunk-MVBA7ABV.js.map +1 -0
- package/dist/{chunk-XTBJCRT3.js → chunk-SV2Y6OCD.js} +2827 -2586
- package/dist/chunk-SV2Y6OCD.js.map +1 -0
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/{provider-templates-LYGIFRRT.js → provider-templates-6XYKAZB5.js} +2 -2
- package/dist/ui/public/app.js +48 -18
- package/dist/ui/public/index.html +1 -1
- package/dist/ui/public/oauth-device.js +44 -19
- package/dist/ui/public/provider-logo.js +4 -0
- package/dist/ui/public/style.css +8 -0
- package/dist/{ui-command-GATWK5JC.js → ui-command-DJZIIIWC.js} +112 -48
- package/dist/ui-command-DJZIIIWC.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-DO5FAMNC.js.map +0 -1
- package/dist/chunk-XTBJCRT3.js.map +0 -1
- package/dist/ui-command-GATWK5JC.js.map +0 -1
- /package/dist/{provider-templates-LYGIFRRT.js.map → provider-templates-6XYKAZB5.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
listAddableTemplates,
|
|
8
8
|
listSupportedTemplates,
|
|
9
9
|
listVisibleOAuthTemplates
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-MVBA7ABV.js";
|
|
11
11
|
init_provider_templates();
|
|
12
12
|
export {
|
|
13
13
|
PROVIDER_TEMPLATES,
|
|
@@ -17,4 +17,4 @@ export {
|
|
|
17
17
|
listSupportedTemplates,
|
|
18
18
|
listVisibleOAuthTemplates
|
|
19
19
|
};
|
|
20
|
-
//# sourceMappingURL=provider-templates-
|
|
20
|
+
//# sourceMappingURL=provider-templates-6XYKAZB5.js.map
|
package/dist/ui/public/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getProviderModelPage,
|
|
4
4
|
PROVIDER_MODEL_PAGE_SIZE,
|
|
5
5
|
} from './provider-model-browser.js';
|
|
6
|
-
import { copyDeviceCode, oauthConnectionLabel } from './oauth-device.js';
|
|
6
|
+
import { copyDeviceCode, copyTextToClipboard, oauthConnectionLabel } from './oauth-device.js';
|
|
7
7
|
import { providerInitial, providerLogoHtml } from './provider-logo.js';
|
|
8
8
|
|
|
9
9
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
@@ -207,7 +207,7 @@ async function initUpdateIndicator() {
|
|
|
207
207
|
|
|
208
208
|
document.getElementById('update-copy-btn')?.addEventListener('click', async () => {
|
|
209
209
|
try {
|
|
210
|
-
await
|
|
210
|
+
await copyTextToClipboard(UPDATE_COMMAND);
|
|
211
211
|
showToast('Update command copied');
|
|
212
212
|
} catch {
|
|
213
213
|
showToast('Could not copy the update command');
|
|
@@ -1519,8 +1519,15 @@ function updateAgyCounter() {
|
|
|
1519
1519
|
|
|
1520
1520
|
// ─── Sidebar nav highlight ────────────────────────────────────────────────────
|
|
1521
1521
|
|
|
1522
|
+
function isServerAdminUi() {
|
|
1523
|
+
return document.documentElement.dataset.uiMode === 'server';
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1522
1526
|
function initNav() {
|
|
1523
|
-
|
|
1527
|
+
// Host-only sections stay in the DOM but are CSS-hidden in server admin mode.
|
|
1528
|
+
const sectionIds = isServerAdminUi()
|
|
1529
|
+
? ['providers', 'favorites', 'server']
|
|
1530
|
+
: ['providers', 'favorites', 'antigravity', 'apps', 'server'];
|
|
1524
1531
|
const navItems = document.querySelectorAll('.nav-item');
|
|
1525
1532
|
const content = document.getElementById('content');
|
|
1526
1533
|
|
|
@@ -1616,11 +1623,13 @@ async function init() {
|
|
|
1616
1623
|
|
|
1617
1624
|
await loadConfig();
|
|
1618
1625
|
renderFavList();
|
|
1619
|
-
renderAgyList();
|
|
1620
|
-
updateAgyCounter();
|
|
1621
1626
|
updateGeneralCounter();
|
|
1622
|
-
|
|
1623
|
-
|
|
1627
|
+
if (!isServerAdminUi()) {
|
|
1628
|
+
renderAgyList();
|
|
1629
|
+
updateAgyCounter();
|
|
1630
|
+
await loadApps();
|
|
1631
|
+
renderApps();
|
|
1632
|
+
}
|
|
1624
1633
|
renderProviders(); // shows skeletons
|
|
1625
1634
|
|
|
1626
1635
|
initServerPolling();
|
|
@@ -1631,12 +1640,12 @@ async function init() {
|
|
|
1631
1640
|
initModels().then(() => {
|
|
1632
1641
|
renderProviders();
|
|
1633
1642
|
renderProviderStats();
|
|
1634
|
-
renderApps();
|
|
1643
|
+
if (!isServerAdminUi()) renderApps();
|
|
1635
1644
|
// Re-render favorites now that we have full provider names
|
|
1636
1645
|
renderFavList();
|
|
1637
|
-
renderAgyList();
|
|
1646
|
+
if (!isServerAdminUi()) renderAgyList();
|
|
1638
1647
|
if (state.modelFilter) buildModelResults(state.modelFilter, 'general');
|
|
1639
|
-
if (state.agyFilter)
|
|
1648
|
+
if (!isServerAdminUi() && state.agyFilter) buildModelResults(state.agyFilter, 'agy');
|
|
1640
1649
|
syncProviderModelBrowserFromHash();
|
|
1641
1650
|
});
|
|
1642
1651
|
|
|
@@ -2220,15 +2229,28 @@ document.addEventListener('click', (e) => {
|
|
|
2220
2229
|
async function refreshServerStatus() {
|
|
2221
2230
|
try {
|
|
2222
2231
|
const data = await api('GET', '/api/server/status');
|
|
2232
|
+
const wasRunning = Boolean(state.server.status?.running);
|
|
2223
2233
|
state.server.status = data;
|
|
2224
2234
|
if (!data.running) seedServerFormFromSaved(data.saved);
|
|
2235
|
+
const runningChanged = wasRunning !== Boolean(data.running);
|
|
2236
|
+
// Full innerHTML replace steals focus from password / search fields — skip while typing.
|
|
2237
|
+
if (runningChanged || !isEditingInside(document.getElementById('server-panel'))) {
|
|
2238
|
+
renderServerPanel();
|
|
2239
|
+
}
|
|
2225
2240
|
} catch {
|
|
2226
2241
|
// Transient poll failure — keep showing the last known state.
|
|
2227
2242
|
}
|
|
2228
|
-
renderServerPanel();
|
|
2229
2243
|
updateServerNavBadge();
|
|
2230
2244
|
}
|
|
2231
2245
|
|
|
2246
|
+
function isEditingInside(root) {
|
|
2247
|
+
if (!root) return false;
|
|
2248
|
+
const el = document.activeElement;
|
|
2249
|
+
if (!el || !root.contains(el)) return false;
|
|
2250
|
+
const tag = el.tagName;
|
|
2251
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2232
2254
|
function seedServerFormFromSaved(saved) {
|
|
2233
2255
|
const f = state.server.form;
|
|
2234
2256
|
if (f.seeded || !saved) return;
|
|
@@ -2240,7 +2262,8 @@ function seedServerFormFromSaved(saved) {
|
|
|
2240
2262
|
f.freeModelsOnly = Boolean(saved.freeModelsOnly);
|
|
2241
2263
|
f.exposedProviders = saved.exposedProviders ? [...saved.exposedProviders] : [];
|
|
2242
2264
|
f.maskGatewayIds = saved.maskGatewayIds;
|
|
2243
|
-
|
|
2265
|
+
// Published Docker ports only reach 0.0.0.0 inside the container.
|
|
2266
|
+
f.listenMode = isServerAdminUi() || saved.listenMode === 'network' ? 'network' : 'local';
|
|
2244
2267
|
f.passwordMode = saved.hasSavedPassword ? 'saved' : 'new';
|
|
2245
2268
|
if (f.expose === 'specific') loadServerProviders().then(renderServerPanel);
|
|
2246
2269
|
}
|
|
@@ -2360,6 +2383,17 @@ function renderServerNetworkOptions() {
|
|
|
2360
2383
|
`;
|
|
2361
2384
|
}
|
|
2362
2385
|
|
|
2386
|
+
function renderServerListenField(f) {
|
|
2387
|
+
if (isServerAdminUi()) {
|
|
2388
|
+
return `<div class="server-field-hint">Network (required in container — use the published host port from the URL cards)</div>${renderServerNetworkOptions()}`;
|
|
2389
|
+
}
|
|
2390
|
+
const toggle = toggleGroupHtml([
|
|
2391
|
+
{ label: 'Local only', active: f.listenMode === 'local', onClick: "setServerListenMode('local')" },
|
|
2392
|
+
{ label: 'Network', active: f.listenMode === 'network', onClick: "setServerListenMode('network')" },
|
|
2393
|
+
]);
|
|
2394
|
+
return f.listenMode === 'network' ? `${toggle}${renderServerNetworkOptions()}` : toggle;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2363
2397
|
function renderServerSetup(s) {
|
|
2364
2398
|
const f = s.form;
|
|
2365
2399
|
const specific = f.expose === 'specific';
|
|
@@ -2410,11 +2444,7 @@ function renderServerSetup(s) {
|
|
|
2410
2444
|
|
|
2411
2445
|
<div class="server-field">
|
|
2412
2446
|
<div class="server-field-label">Listen</div>
|
|
2413
|
-
${
|
|
2414
|
-
{ label: 'Local only', active: f.listenMode === 'local', onClick: "setServerListenMode('local')" },
|
|
2415
|
-
{ label: 'Network', active: f.listenMode === 'network', onClick: "setServerListenMode('network')" },
|
|
2416
|
-
])}
|
|
2417
|
-
${f.listenMode === 'network' ? renderServerNetworkOptions() : ''}
|
|
2447
|
+
${renderServerListenField(f)}
|
|
2418
2448
|
</div>
|
|
2419
2449
|
|
|
2420
2450
|
${s.error ? `<div class="key-feedback error server-start-error">${escapeHtml(s.error)}</div>` : ''}
|
|
@@ -2618,7 +2648,7 @@ function serverIdCell(id) {
|
|
|
2618
2648
|
|
|
2619
2649
|
async function copyPlainText(text) {
|
|
2620
2650
|
try {
|
|
2621
|
-
await
|
|
2651
|
+
await copyTextToClipboard(text);
|
|
2622
2652
|
showToast('Copied to clipboard');
|
|
2623
2653
|
} catch {
|
|
2624
2654
|
showToast('Could not copy — copy manually');
|
|
@@ -1,30 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function oauthConnectionLabel(provider) {
|
|
3
|
-
if (!provider?.hasKey) return provider?.freeAccess ? 'Free models' : 'Not configured';
|
|
4
|
-
if (provider.authType !== 'oauth') return 'Key stored';
|
|
5
|
-
const planLabel = provider.subscription?.label;
|
|
6
|
-
return typeof planLabel === 'string' && planLabel.trim()
|
|
7
|
-
? `Connected · ${planLabel.trim()}`
|
|
8
|
-
: 'Connected';
|
|
9
|
-
}
|
|
1
|
+
/** Clipboard helpers for the web UI (works on http://LAN as well as https/localhost). */
|
|
10
2
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Copy text to the clipboard. Prefer the async Clipboard API in secure contexts;
|
|
5
|
+
* fall back to a hidden textarea + execCommand for plain HTTP (LAN Docker UI).
|
|
6
|
+
*/
|
|
7
|
+
export async function copyTextToClipboard(
|
|
8
|
+
text,
|
|
9
|
+
clipboard = globalThis.navigator?.clipboard,
|
|
10
|
+
doc = globalThis.document,
|
|
11
|
+
) {
|
|
12
|
+
const secure = typeof globalThis.isSecureContext === 'boolean'
|
|
13
|
+
? globalThis.isSecureContext
|
|
14
|
+
: Boolean(clipboard?.writeText);
|
|
15
|
+
|
|
16
|
+
if (secure && clipboard?.writeText) {
|
|
17
|
+
try {
|
|
18
|
+
await clipboard.writeText(text);
|
|
19
|
+
return;
|
|
20
|
+
} catch {
|
|
21
|
+
// Fall through — common on http://192.168.x.x where the API exists but throws.
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
if (!doc?.body || typeof doc.execCommand !== 'function') {
|
|
18
26
|
throw new Error('Clipboard access is unavailable');
|
|
19
27
|
}
|
|
20
|
-
const input =
|
|
21
|
-
input.value =
|
|
28
|
+
const input = doc.createElement('textarea');
|
|
29
|
+
input.value = text;
|
|
22
30
|
input.setAttribute('readonly', '');
|
|
23
31
|
input.style.position = 'fixed';
|
|
32
|
+
input.style.left = '-9999px';
|
|
24
33
|
input.style.opacity = '0';
|
|
25
|
-
|
|
34
|
+
doc.body.appendChild(input);
|
|
26
35
|
input.select();
|
|
27
|
-
|
|
36
|
+
input.setSelectionRange(0, input.value.length);
|
|
37
|
+
const copied = doc.execCommand('copy');
|
|
28
38
|
input.remove();
|
|
29
39
|
if (!copied) throw new Error('Copy failed');
|
|
30
40
|
}
|
|
41
|
+
|
|
42
|
+
/** @deprecated Prefer copyTextToClipboard — kept for existing call sites / tests. */
|
|
43
|
+
export async function copyDeviceCode(code, clipboard = globalThis.navigator?.clipboard) {
|
|
44
|
+
return copyTextToClipboard(code, clipboard);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Return the short credential label used on a provider card. */
|
|
48
|
+
export function oauthConnectionLabel(provider) {
|
|
49
|
+
if (!provider?.hasKey) return provider?.freeAccess ? 'Free models' : 'Not configured';
|
|
50
|
+
if (provider.authType !== 'oauth') return 'Key stored';
|
|
51
|
+
const planLabel = provider.subscription?.label;
|
|
52
|
+
return typeof planLabel === 'string' && planLabel.trim()
|
|
53
|
+
? `Connected · ${planLabel.trim()}`
|
|
54
|
+
: 'Connected';
|
|
55
|
+
}
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
// Simple Icons URLs were verified when added; inline SVGs cover brands whose
|
|
3
3
|
// official assets cannot be hotlinked reliably.
|
|
4
4
|
|
|
5
|
+
const QWEN_CLOUD_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.38 140" style="width:22px;height:22px"><title>Qwen Cloud</title><path fill="#6D44E8" d="m140.93 85-16.35-28.33-1.93-3.34 8.66-15a3.323 3.323 0 0 0 0-3.34l-9.62-16.67c-.3-.51-.72-.93-1.22-1.22s-1.07-.45-1.67-.45H82.23l-8.66-15a3.33 3.33 0 0 0-2.89-1.67H51.43c-.59 0-1.17.16-1.66.45-.5.29-.92.71-1.22 1.22L32.19 29.98l-1.92 3.33H12.96c-.59 0-1.17.16-1.66.45-.5.29-.93.71-1.22 1.22L.45 51.66a3.323 3.323 0 0 0 0 3.34l18.28 31.67-8.66 15a3.32 3.32 0 0 0 0 3.34l9.62 16.67c.3.51.72.93 1.22 1.22s1.07.45 1.67.45h36.56l8.66 15a3.35 3.35 0 0 0 2.89 1.67h19.25a3.34 3.34 0 0 0 2.89-1.67l18.28-31.67h17.32c.6 0 1.17-.16 1.67-.45s.92-.71 1.22-1.22l9.62-16.67a3.323 3.323 0 0 0 0-3.34ZM51.44 3.33 61.07 20l-9.63 16.66h76.98l-9.62 16.66H45.67l-11.54-20zM57.21 120H22.58l9.63-16.67h19.25l-38.5-66.67h19.25l9.62 16.67L68.78 100l-11.55 20Zm61.59-33.34-9.62-16.67-38.49 66.67-9.63-16.67 9.63-16.66 26.94-46.67h23.1l17.32 30z"/></svg>`;
|
|
6
|
+
|
|
5
7
|
const PROVIDER_INLINE_SVGS = {
|
|
6
8
|
openai: `<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg" style="width:20px;height:20px"><path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.896zm16.597 3.855l-5.843-3.368L15.115 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.403-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/></svg>`,
|
|
7
9
|
kilo: `<svg viewBox="0 0 24 24" fill="white" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" style="width:22px;height:22px"><title>Kilo Code</title><path d="M0 0v24h24V0H0zm22.222 22.222H1.778V1.778h20.444v20.444zm-7.555-4.964h2.222v1.778h-2.794L12.89 17.83v-2.794h1.778v2.222zm4 0h-1.778v-2.222h-2.222v-1.778h2.793l1.207 1.207v2.793zm-7.556-2.591H9.333v-1.778h1.778v1.778zm-5.778-1.778h1.778v4h4v1.778H6.54L5.333 17.46V12.89zm13.334-3.556v1.778h-5.778V9.333h1.987V7.111h-1.987V5.333h2.558l1.206 1.207v2.793h2.014zm-11.556-2h2.222l1.778 1.778v2H9.333v-2H7.111v2H5.333V5.333h1.778v2zm4 0H9.333v-2h1.778v2z"/></svg>`,
|
|
10
|
+
'qwen-cloud-token-plan': QWEN_CLOUD_SVG,
|
|
11
|
+
'qwen-cloud-payg': QWEN_CLOUD_SVG,
|
|
8
12
|
xai: `<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg" style="width:20px;height:20px"><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"/></svg>`,
|
|
9
13
|
};
|
|
10
14
|
|
package/dist/ui/public/style.css
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
/* ─── Tokens ─────────────────────────────────────────────────────────────── */
|
|
2
|
+
/* Server admin UI (Docker): hide host-only sections before JS runs */
|
|
3
|
+
html[data-ui-mode="server"] .nav-item[data-section="apps"],
|
|
4
|
+
html[data-ui-mode="server"] .nav-item[data-section="antigravity"],
|
|
5
|
+
html[data-ui-mode="server"] #apps,
|
|
6
|
+
html[data-ui-mode="server"] #antigravity {
|
|
7
|
+
display: none !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
2
10
|
:root {
|
|
3
11
|
/* Deep navy background — NOT flat black */
|
|
4
12
|
--bg: oklch(12% 0.038 265);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
completeAntigravityExchange,
|
|
12
12
|
copilotPlanTier,
|
|
13
13
|
createGatewayModelCatalog,
|
|
14
|
+
ensureOpencodeCloudProviders,
|
|
14
15
|
favoriteProviderDisplayName,
|
|
15
16
|
fetchProviderCatalog,
|
|
16
17
|
filterServerModelsByFavorites,
|
|
@@ -19,11 +20,11 @@ import {
|
|
|
19
20
|
findBinaryOnPath,
|
|
20
21
|
findClaudeApp,
|
|
21
22
|
findCodexApp,
|
|
23
|
+
formatGatewayUrls,
|
|
22
24
|
freeStatusLabel,
|
|
23
25
|
gatewayProviderLabel,
|
|
24
26
|
getAppHome,
|
|
25
27
|
getAppPathOverride,
|
|
26
|
-
getLocalIps,
|
|
27
28
|
getSavedServerPassword,
|
|
28
29
|
getServerExposedProviders,
|
|
29
30
|
getServerFavoritesOnly,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
getServerMaskGatewayIds,
|
|
33
34
|
getUiDebugLogPath,
|
|
34
35
|
guiCallbackRedirectUri,
|
|
36
|
+
hostFromHeader,
|
|
35
37
|
loadPreferences,
|
|
36
38
|
loadRegistry,
|
|
37
39
|
loadServerModels,
|
|
@@ -49,6 +51,8 @@ import {
|
|
|
49
51
|
requestGithubDeviceCode,
|
|
50
52
|
requestOpenAiDeviceCode,
|
|
51
53
|
requestXaiDeviceCode,
|
|
54
|
+
resolveAdvertiseAddresses,
|
|
55
|
+
resolveAdvertiseGatewayPort,
|
|
52
56
|
resolveProviderCredential,
|
|
53
57
|
resolveServerUpstreamApiKey,
|
|
54
58
|
saveNativeOAuthCredential,
|
|
@@ -67,14 +71,14 @@ import {
|
|
|
67
71
|
supportsClaudeTransparentMode,
|
|
68
72
|
validateCustomEndpointUrl,
|
|
69
73
|
writeSecureLogLine
|
|
70
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-SV2Y6OCD.js";
|
|
71
75
|
import {
|
|
72
76
|
__toCommonJS,
|
|
73
77
|
init_provider_templates,
|
|
74
78
|
listAddableTemplates,
|
|
75
79
|
listVisibleOAuthTemplates,
|
|
76
80
|
provider_templates_exports
|
|
77
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-MVBA7ABV.js";
|
|
78
82
|
|
|
79
83
|
// src/ui-command.ts
|
|
80
84
|
import { createServer } from "http";
|
|
@@ -390,16 +394,18 @@ async function buildSavedConfig() {
|
|
|
390
394
|
hasSavedPassword: await hasSavedPasswordCached()
|
|
391
395
|
};
|
|
392
396
|
}
|
|
393
|
-
async function getServerStatus() {
|
|
397
|
+
async function getServerStatus(opts) {
|
|
394
398
|
const saved = await buildSavedConfig();
|
|
395
399
|
if (!running) return { running: false, saved };
|
|
396
400
|
const { handle, config, serverPassword, providerSummary, modelRows } = running;
|
|
401
|
+
const publicPort = resolveAdvertiseGatewayPort(handle.port);
|
|
402
|
+
const loopback = formatGatewayUrls("127.0.0.1", publicPort);
|
|
397
403
|
const payload = {
|
|
398
404
|
running: true,
|
|
399
405
|
saved,
|
|
400
406
|
listenMode: config.listenMode,
|
|
401
|
-
anthropicUrl:
|
|
402
|
-
openaiUrl:
|
|
407
|
+
anthropicUrl: loopback.anthropicUrl,
|
|
408
|
+
openaiUrl: loopback.openaiUrl,
|
|
403
409
|
exposedProviders: config.exposedProviders,
|
|
404
410
|
favoritesOnly: config.favoritesOnly,
|
|
405
411
|
freeModelsOnly: config.freeModelsOnly,
|
|
@@ -408,26 +414,27 @@ async function getServerStatus() {
|
|
|
408
414
|
models: modelRows
|
|
409
415
|
};
|
|
410
416
|
if (config.listenMode === "network") {
|
|
411
|
-
payload.networkUrls =
|
|
412
|
-
name,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
417
|
+
payload.networkUrls = resolveAdvertiseAddresses({ requestHost: opts?.requestHost }).map(
|
|
418
|
+
({ name, address }) => {
|
|
419
|
+
const urls = formatGatewayUrls(address, publicPort);
|
|
420
|
+
return { name, anthropicUrl: urls.anthropicUrl, openaiUrl: urls.openaiUrl };
|
|
421
|
+
}
|
|
422
|
+
);
|
|
416
423
|
payload.apiKey = serverPassword ?? void 0;
|
|
417
424
|
} else {
|
|
418
425
|
payload.apiKey = "any non-empty value";
|
|
419
426
|
}
|
|
420
427
|
return payload;
|
|
421
428
|
}
|
|
422
|
-
function startGatewayServer(req) {
|
|
429
|
+
function startGatewayServer(req, opts) {
|
|
423
430
|
if (running) return Promise.resolve({ ok: false, error: "Server is already running. Stop it first." });
|
|
424
431
|
if (startInFlight) return startInFlight;
|
|
425
|
-
startInFlight = doStartGatewayServer(req).finally(() => {
|
|
432
|
+
startInFlight = doStartGatewayServer(req, opts).finally(() => {
|
|
426
433
|
startInFlight = null;
|
|
427
434
|
});
|
|
428
435
|
return startInFlight;
|
|
429
436
|
}
|
|
430
|
-
async function doStartGatewayServer(req) {
|
|
437
|
+
async function doStartGatewayServer(req, opts) {
|
|
431
438
|
if (req.listenMode !== "local" && req.listenMode !== "network") {
|
|
432
439
|
return { ok: false, error: "Invalid listen mode." };
|
|
433
440
|
}
|
|
@@ -515,7 +522,7 @@ async function doStartGatewayServer(req) {
|
|
|
515
522
|
providerSummary: summarizeServerProviders(models),
|
|
516
523
|
modelRows: buildModelRows(models, gateway)
|
|
517
524
|
};
|
|
518
|
-
return { ok: true, status: await getServerStatus() };
|
|
525
|
+
return { ok: true, status: await getServerStatus({ requestHost: opts?.requestHost }) };
|
|
519
526
|
}
|
|
520
527
|
async function stopGatewayServer() {
|
|
521
528
|
if (running) {
|
|
@@ -605,16 +612,18 @@ function handleUiApiRequest(req, res, opts = {}) {
|
|
|
605
612
|
handleOAuthStatus(req, res);
|
|
606
613
|
} else if (url.startsWith("/oauth/callback") && req.method === "GET") {
|
|
607
614
|
handleOAuthCallback(req, res);
|
|
608
|
-
} else if (url
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
615
|
+
} else if (url.startsWith("/api/apps")) {
|
|
616
|
+
if (opts.uiMode === "server") {
|
|
617
|
+
sendJson(res, 403, { error: "App launch is unavailable in server admin UI mode." });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (url === "/api/apps" && req.method === "GET") handleGetApps(res);
|
|
621
|
+
else if (url === "/api/apps/path" && req.method === "POST") handleSetAppPath(req, res);
|
|
622
|
+
else if (url === "/api/apps/launch" && req.method === "POST") handleLaunchApp(req, res, opts);
|
|
623
|
+
else if (url === "/api/apps/browse-folder" && req.method === "POST") handleBrowseFolder(res);
|
|
624
|
+
else sendJson(res, 404, { error: "Not found" });
|
|
616
625
|
} else if (url === "/api/server/status" && req.method === "GET") {
|
|
617
|
-
handleGetServerStatus(res);
|
|
626
|
+
handleGetServerStatus(req, res);
|
|
618
627
|
} else if (url === "/api/server/providers" && req.method === "GET") {
|
|
619
628
|
handleGetServerProviders(res);
|
|
620
629
|
} else if (url === "/api/server/start" && req.method === "POST") {
|
|
@@ -723,7 +732,7 @@ async function handlePostKeys(req, res) {
|
|
|
723
732
|
if (saved) {
|
|
724
733
|
sendJson(res, 200, { ok: true });
|
|
725
734
|
} else {
|
|
726
|
-
sendJson(res, 500, { error: "
|
|
735
|
+
sendJson(res, 500, { error: "Credential store unavailable \u2014 key not saved" });
|
|
727
736
|
}
|
|
728
737
|
} catch (err) {
|
|
729
738
|
sendJson(res, 400, { error: String(err) });
|
|
@@ -798,7 +807,7 @@ async function handleAddProvider(req, res) {
|
|
|
798
807
|
sendJson(res, 400, { error: "templateId required" });
|
|
799
808
|
return;
|
|
800
809
|
}
|
|
801
|
-
const { listSupportedTemplates } = await import("./provider-templates-
|
|
810
|
+
const { listSupportedTemplates } = await import("./provider-templates-6XYKAZB5.js");
|
|
802
811
|
const template = listSupportedTemplates().find((t) => t.id === templateId);
|
|
803
812
|
if (!template) {
|
|
804
813
|
sendJson(res, 404, { error: `Template '${templateId}' not found` });
|
|
@@ -1199,9 +1208,9 @@ async function handleSetAppPath(req, res) {
|
|
|
1199
1208
|
sendJson(res, 500, { error: String(err) });
|
|
1200
1209
|
}
|
|
1201
1210
|
}
|
|
1202
|
-
async function handleGetServerStatus(res) {
|
|
1211
|
+
async function handleGetServerStatus(req, res) {
|
|
1203
1212
|
try {
|
|
1204
|
-
sendJson(res, 200, await getServerStatus());
|
|
1213
|
+
sendJson(res, 200, await getServerStatus({ requestHost: hostFromHeader(req.headers.host) }));
|
|
1205
1214
|
} catch (err) {
|
|
1206
1215
|
sendJson(res, 500, { error: String(err) });
|
|
1207
1216
|
}
|
|
@@ -1229,17 +1238,20 @@ async function handleStartServer(req, res, opts) {
|
|
|
1229
1238
|
sendJson(res, 400, { error: 'listenMode must be "local" or "network"' });
|
|
1230
1239
|
return;
|
|
1231
1240
|
}
|
|
1241
|
+
const listenMode = opts.uiMode === "server" ? "network" : body.listenMode;
|
|
1232
1242
|
const request = {
|
|
1233
1243
|
favoritesOnly: body.favoritesOnly,
|
|
1234
1244
|
freeModelsOnly: Boolean(body.freeModelsOnly),
|
|
1235
1245
|
exposedProviders: Array.isArray(body.exposedProviders) ? body.exposedProviders : null,
|
|
1236
1246
|
maskGatewayIds: body.maskGatewayIds,
|
|
1237
|
-
listenMode
|
|
1247
|
+
listenMode,
|
|
1238
1248
|
passwordMode: body.passwordMode === "saved" ? "saved" : "new",
|
|
1239
1249
|
password: typeof body.password === "string" ? body.password : void 0,
|
|
1240
1250
|
savePassword: Boolean(body.savePassword)
|
|
1241
1251
|
};
|
|
1242
|
-
const result = await startGatewayServer(request
|
|
1252
|
+
const result = await startGatewayServer(request, {
|
|
1253
|
+
requestHost: hostFromHeader(req.headers.host)
|
|
1254
|
+
});
|
|
1243
1255
|
if (result.ok) {
|
|
1244
1256
|
notifyServerLifecycle(opts, {
|
|
1245
1257
|
type: "started",
|
|
@@ -1337,6 +1349,7 @@ async function handleBrowseFolder(res) {
|
|
|
1337
1349
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1338
1350
|
var PUBLIC_DIR = join2(__dirname, "ui", "public");
|
|
1339
1351
|
var LOCK_FILE = join2(getAppHome(), "ui.lock");
|
|
1352
|
+
var DEFAULT_SERVER_UI_PORT = 8787;
|
|
1340
1353
|
var MIME = {
|
|
1341
1354
|
".html": "text/html; charset=utf-8",
|
|
1342
1355
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -1349,14 +1362,47 @@ function ext(path) {
|
|
|
1349
1362
|
const i = path.lastIndexOf(".");
|
|
1350
1363
|
return i >= 0 ? path.slice(i) : "";
|
|
1351
1364
|
}
|
|
1352
|
-
function
|
|
1365
|
+
function resolveUiMode(opts = {}, env = process.env) {
|
|
1366
|
+
if (opts.serverMode) return "server";
|
|
1367
|
+
return env.RELAY_AI_UI_MODE === "server" ? "server" : "full";
|
|
1368
|
+
}
|
|
1369
|
+
function resolveServerUiPort(opts, env) {
|
|
1370
|
+
if (opts.port != null) return opts.port;
|
|
1371
|
+
const envPort = Number(env.RELAY_AI_UI_PORT);
|
|
1372
|
+
return Number.isFinite(envPort) && envPort > 0 ? envPort : DEFAULT_SERVER_UI_PORT;
|
|
1373
|
+
}
|
|
1374
|
+
function resolveUiRuntimeConfig(opts = {}, env = process.env) {
|
|
1375
|
+
const mode = resolveUiMode(opts, env);
|
|
1376
|
+
if (mode === "server") {
|
|
1377
|
+
return {
|
|
1378
|
+
mode,
|
|
1379
|
+
host: "0.0.0.0",
|
|
1380
|
+
port: resolveServerUiPort(opts, env),
|
|
1381
|
+
openBrowser: false,
|
|
1382
|
+
confirmShutdownOnSigint: false
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
mode,
|
|
1387
|
+
host: "127.0.0.1",
|
|
1388
|
+
port: opts.port ?? 0,
|
|
1389
|
+
openBrowser: true,
|
|
1390
|
+
confirmShutdownOnSigint: true
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function buildStaticCache(mode) {
|
|
1353
1394
|
const cache = /* @__PURE__ */ new Map();
|
|
1354
1395
|
try {
|
|
1355
1396
|
for (const name of readdirSync(PUBLIC_DIR)) {
|
|
1356
1397
|
const mime = MIME[ext(name)];
|
|
1357
1398
|
if (!mime) continue;
|
|
1358
1399
|
const raw = readFileSync(join2(PUBLIC_DIR, name));
|
|
1359
|
-
|
|
1400
|
+
let content = raw;
|
|
1401
|
+
if (name === "index.html") {
|
|
1402
|
+
content = Buffer.from(
|
|
1403
|
+
raw.toString("utf8").replaceAll("{{VERSION}}", VERSION).replaceAll("{{UI_MODE}}", mode)
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1360
1406
|
cache.set(`/${name}`, { content, mime });
|
|
1361
1407
|
}
|
|
1362
1408
|
} catch {
|
|
@@ -1392,13 +1438,16 @@ function formatUiServerLifecycleMessage(event) {
|
|
|
1392
1438
|
async function resolveUiShutdownDecision(signal, promptClose = () => p.confirm({
|
|
1393
1439
|
message: "Relay-AI UI is still running. Close it?",
|
|
1394
1440
|
initialValue: true
|
|
1395
|
-
})) {
|
|
1396
|
-
|
|
1441
|
+
}), opts) {
|
|
1442
|
+
const confirmOnSigint = opts?.confirmOnSigint !== false;
|
|
1443
|
+
if (signal !== "SIGINT" || !confirmOnSigint) return "close";
|
|
1397
1444
|
const shouldClose = await promptClose();
|
|
1398
1445
|
if (p.isCancel(shouldClose)) return "close";
|
|
1399
1446
|
return shouldClose ? "close" : "keep";
|
|
1400
1447
|
}
|
|
1401
1448
|
async function runUiCommand(opts = {}) {
|
|
1449
|
+
const runtime = resolveUiRuntimeConfig(opts);
|
|
1450
|
+
await ensureOpencodeCloudProviders();
|
|
1402
1451
|
const existing = checkExistingServer();
|
|
1403
1452
|
if (existing) {
|
|
1404
1453
|
console.log(`
|
|
@@ -1409,10 +1458,10 @@ async function runUiCommand(opts = {}) {
|
|
|
1409
1458
|
if (opts.trace) {
|
|
1410
1459
|
process.env.RELAY_AI_TRACE = "1";
|
|
1411
1460
|
}
|
|
1412
|
-
const staticCache = buildStaticCache();
|
|
1461
|
+
const staticCache = buildStaticCache(runtime.mode);
|
|
1413
1462
|
const traceLogPath = opts.trace ? getUiDebugLogPath() : void 0;
|
|
1414
1463
|
const trace = traceLogPath ? makeTraceLogger(traceLogPath) : void 0;
|
|
1415
|
-
trace?.(
|
|
1464
|
+
trace?.(`ui server starting mode=${runtime.mode}`);
|
|
1416
1465
|
const server = createServer((req, res) => {
|
|
1417
1466
|
const url2 = req.url ?? "/";
|
|
1418
1467
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -1420,6 +1469,7 @@ async function runUiCommand(opts = {}) {
|
|
|
1420
1469
|
handleUiApiRequest(req, res, {
|
|
1421
1470
|
trace: opts.trace,
|
|
1422
1471
|
traceLogPath,
|
|
1472
|
+
uiMode: runtime.mode,
|
|
1423
1473
|
onServerLifecycle: (event) => {
|
|
1424
1474
|
console.log(`
|
|
1425
1475
|
${formatUiServerLifecycleMessage(event)}
|
|
@@ -1440,7 +1490,7 @@ async function runUiCommand(opts = {}) {
|
|
|
1440
1490
|
res.end("Not found");
|
|
1441
1491
|
});
|
|
1442
1492
|
await new Promise((resolve, reject) => {
|
|
1443
|
-
server.listen(
|
|
1493
|
+
server.listen(runtime.port, runtime.host, () => resolve());
|
|
1444
1494
|
server.once("error", reject);
|
|
1445
1495
|
});
|
|
1446
1496
|
const addr = server.address();
|
|
@@ -1449,9 +1499,10 @@ async function runUiCommand(opts = {}) {
|
|
|
1449
1499
|
return 1;
|
|
1450
1500
|
}
|
|
1451
1501
|
const port = addr.port;
|
|
1452
|
-
const
|
|
1502
|
+
const displayHost = runtime.host === "0.0.0.0" ? "127.0.0.1" : runtime.host;
|
|
1503
|
+
const url = `http://${displayHost}:${port}`;
|
|
1453
1504
|
mkdirSync(getAppHome(), { recursive: true });
|
|
1454
|
-
writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port }));
|
|
1505
|
+
writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port, mode: runtime.mode }));
|
|
1455
1506
|
const cleanup = () => {
|
|
1456
1507
|
removeLock();
|
|
1457
1508
|
server.close();
|
|
@@ -1461,7 +1512,11 @@ async function runUiCommand(opts = {}) {
|
|
|
1461
1512
|
const handleSignal = async (signal) => {
|
|
1462
1513
|
if (handlingSignal) return;
|
|
1463
1514
|
handlingSignal = true;
|
|
1464
|
-
const decision = await resolveUiShutdownDecision(
|
|
1515
|
+
const decision = await resolveUiShutdownDecision(
|
|
1516
|
+
signal,
|
|
1517
|
+
void 0,
|
|
1518
|
+
{ confirmOnSigint: runtime.confirmShutdownOnSigint }
|
|
1519
|
+
);
|
|
1465
1520
|
if (decision === "keep") {
|
|
1466
1521
|
handlingSignal = false;
|
|
1467
1522
|
return;
|
|
@@ -1474,21 +1529,28 @@ async function runUiCommand(opts = {}) {
|
|
|
1474
1529
|
process.on("SIGTERM", () => {
|
|
1475
1530
|
void handleSignal("SIGTERM");
|
|
1476
1531
|
});
|
|
1532
|
+
const modeLabel = runtime.mode === "server" ? " (server admin)" : "";
|
|
1477
1533
|
console.log(`
|
|
1478
|
-
${pc.bold("relay-ai UI")} ${pc.cyan(url)}
|
|
1534
|
+
${pc.bold("relay-ai UI")}${modeLabel} ${pc.cyan(url)}
|
|
1479
1535
|
${pc.dim("Press Ctrl+C to stop")}
|
|
1480
1536
|
`);
|
|
1537
|
+
if (runtime.mode === "server") {
|
|
1538
|
+
console.log(` ${pc.dim("Gateway API (when started from Server tab): http://127.0.0.1:17645")}
|
|
1539
|
+
`);
|
|
1540
|
+
}
|
|
1481
1541
|
if (traceLogPath) {
|
|
1482
1542
|
console.log(` ${pc.dim(`Trace log: ${traceLogPath}`)}
|
|
1483
1543
|
`);
|
|
1484
1544
|
trace?.(`ui server listening ${url}`);
|
|
1485
1545
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1546
|
+
if (runtime.openBrowser) {
|
|
1547
|
+
try {
|
|
1548
|
+
const { default: open } = await import("open");
|
|
1549
|
+
await open(url);
|
|
1550
|
+
trace?.(`browser open ${url}`);
|
|
1551
|
+
} catch {
|
|
1552
|
+
trace?.(`browser open failed ${url}`);
|
|
1553
|
+
}
|
|
1492
1554
|
}
|
|
1493
1555
|
await new Promise(() => {
|
|
1494
1556
|
});
|
|
@@ -1497,7 +1559,9 @@ async function runUiCommand(opts = {}) {
|
|
|
1497
1559
|
export {
|
|
1498
1560
|
formatUiServerLifecycleMessage,
|
|
1499
1561
|
isUiApiRoute,
|
|
1562
|
+
resolveUiMode,
|
|
1563
|
+
resolveUiRuntimeConfig,
|
|
1500
1564
|
resolveUiShutdownDecision,
|
|
1501
1565
|
runUiCommand
|
|
1502
1566
|
};
|
|
1503
|
-
//# sourceMappingURL=ui-command-
|
|
1567
|
+
//# sourceMappingURL=ui-command-DJZIIIWC.js.map
|