@jacobbd/relay-ai 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,12 @@ 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,
28
+ getEnvServerPassword,
27
29
  getSavedServerPassword,
28
30
  getServerExposedProviders,
29
31
  getServerFavoritesOnly,
@@ -32,6 +34,7 @@ import {
32
34
  getServerMaskGatewayIds,
33
35
  getUiDebugLogPath,
34
36
  guiCallbackRedirectUri,
37
+ hostFromHeader,
35
38
  loadPreferences,
36
39
  loadRegistry,
37
40
  loadServerModels,
@@ -49,6 +52,8 @@ import {
49
52
  requestGithubDeviceCode,
50
53
  requestOpenAiDeviceCode,
51
54
  requestXaiDeviceCode,
55
+ resolveAdvertiseAddresses,
56
+ resolveAdvertiseGatewayPort,
52
57
  resolveProviderCredential,
53
58
  resolveServerUpstreamApiKey,
54
59
  saveNativeOAuthCredential,
@@ -67,14 +72,14 @@ import {
67
72
  supportsClaudeTransparentMode,
68
73
  validateCustomEndpointUrl,
69
74
  writeSecureLogLine
70
- } from "./chunk-44KQK6Y5.js";
75
+ } from "./chunk-P4S42QJK.js";
71
76
  import {
72
77
  __toCommonJS,
73
78
  init_provider_templates,
74
79
  listAddableTemplates,
75
80
  listVisibleOAuthTemplates,
76
81
  provider_templates_exports
77
- } from "./chunk-MVBA7ABV.js";
82
+ } from "./chunk-EJONCU3B.js";
78
83
 
79
84
  // src/ui-command.ts
80
85
  import { createServer } from "http";
@@ -381,25 +386,30 @@ function buildModelRows(models, gateway) {
381
386
  return rows.sort((a, b) => a.providerLabel.localeCompare(b.providerLabel) || a.name.localeCompare(b.name));
382
387
  }
383
388
  async function buildSavedConfig() {
389
+ const envPassword = getEnvServerPassword();
384
390
  return {
385
391
  favoritesOnly: getServerFavoritesOnly(),
386
392
  freeModelsOnly: getServerFreeModelsOnly(),
387
393
  exposedProviders: getServerExposedProviders(),
388
394
  maskGatewayIds: getServerMaskGatewayIds(),
389
395
  listenMode: getServerListenMode(),
390
- hasSavedPassword: await hasSavedPasswordCached()
396
+ hasSavedPassword: await hasSavedPasswordCached(),
397
+ hasEnvPassword: Boolean(envPassword),
398
+ ...envPassword ? { prefillPassword: envPassword } : {}
391
399
  };
392
400
  }
393
- async function getServerStatus() {
401
+ async function getServerStatus(opts) {
394
402
  const saved = await buildSavedConfig();
395
403
  if (!running) return { running: false, saved };
396
404
  const { handle, config, serverPassword, providerSummary, modelRows } = running;
405
+ const publicPort = resolveAdvertiseGatewayPort(handle.port);
406
+ const loopback = formatGatewayUrls("127.0.0.1", publicPort);
397
407
  const payload = {
398
408
  running: true,
399
409
  saved,
400
410
  listenMode: config.listenMode,
401
- anthropicUrl: `http://127.0.0.1:${handle.port}/anthropic`,
402
- openaiUrl: `http://127.0.0.1:${handle.port}/openai/v1`,
411
+ anthropicUrl: loopback.anthropicUrl,
412
+ openaiUrl: loopback.openaiUrl,
403
413
  exposedProviders: config.exposedProviders,
404
414
  favoritesOnly: config.favoritesOnly,
405
415
  freeModelsOnly: config.freeModelsOnly,
@@ -408,26 +418,27 @@ async function getServerStatus() {
408
418
  models: modelRows
409
419
  };
410
420
  if (config.listenMode === "network") {
411
- payload.networkUrls = getLocalIps().map(({ name, address }) => ({
412
- name,
413
- anthropicUrl: `http://${address}:${handle.port}/anthropic`,
414
- openaiUrl: `http://${address}:${handle.port}/openai/v1`
415
- }));
421
+ payload.networkUrls = resolveAdvertiseAddresses({ requestHost: opts?.requestHost }).map(
422
+ ({ name, address }) => {
423
+ const urls = formatGatewayUrls(address, publicPort);
424
+ return { name, anthropicUrl: urls.anthropicUrl, openaiUrl: urls.openaiUrl };
425
+ }
426
+ );
416
427
  payload.apiKey = serverPassword ?? void 0;
417
428
  } else {
418
429
  payload.apiKey = "any non-empty value";
419
430
  }
420
431
  return payload;
421
432
  }
422
- function startGatewayServer(req) {
433
+ function startGatewayServer(req, opts) {
423
434
  if (running) return Promise.resolve({ ok: false, error: "Server is already running. Stop it first." });
424
435
  if (startInFlight) return startInFlight;
425
- startInFlight = doStartGatewayServer(req).finally(() => {
436
+ startInFlight = doStartGatewayServer(req, opts).finally(() => {
426
437
  startInFlight = null;
427
438
  });
428
439
  return startInFlight;
429
440
  }
430
- async function doStartGatewayServer(req) {
441
+ async function doStartGatewayServer(req, opts) {
431
442
  if (req.listenMode !== "local" && req.listenMode !== "network") {
432
443
  return { ok: false, error: "Invalid listen mode." };
433
444
  }
@@ -438,16 +449,26 @@ async function doStartGatewayServer(req) {
438
449
  let serverPassword = null;
439
450
  if (req.listenMode === "network") {
440
451
  if (req.passwordMode === "saved") {
441
- const saved = await getSavedServerPassword();
442
- if (!saved) return { ok: false, error: "No saved password found \u2014 enter a new password." };
443
- serverPassword = saved;
452
+ const configured = await getSavedServerPassword() ?? getEnvServerPassword();
453
+ if (!configured) {
454
+ return {
455
+ ok: false,
456
+ error: "No configured password found \u2014 set RELAY_AI_SERVER_PASSWORD, or enter a new password."
457
+ };
458
+ }
459
+ serverPassword = configured;
444
460
  } else {
445
461
  const trimmed = (req.password ?? "").trim();
446
- if (!trimmed) return { ok: false, error: "A server password is required for network mode." };
447
- serverPassword = trimmed;
448
- if (req.savePassword) {
449
- await setSavedServerPassword(trimmed);
450
- hasSavedPasswordCache = { value: true, expiresAt: Date.now() + SAVED_PASSWORD_CACHE_TTL_MS };
462
+ if (!trimmed) {
463
+ const configured = getEnvServerPassword() ?? await getSavedServerPassword();
464
+ if (!configured) return { ok: false, error: "A server password is required for network mode." };
465
+ serverPassword = configured;
466
+ } else {
467
+ serverPassword = trimmed;
468
+ if (req.savePassword) {
469
+ await setSavedServerPassword(trimmed);
470
+ hasSavedPasswordCache = { value: true, expiresAt: Date.now() + SAVED_PASSWORD_CACHE_TTL_MS };
471
+ }
451
472
  }
452
473
  }
453
474
  }
@@ -515,7 +536,7 @@ async function doStartGatewayServer(req) {
515
536
  providerSummary: summarizeServerProviders(models),
516
537
  modelRows: buildModelRows(models, gateway)
517
538
  };
518
- return { ok: true, status: await getServerStatus() };
539
+ return { ok: true, status: await getServerStatus({ requestHost: opts?.requestHost }) };
519
540
  }
520
541
  async function stopGatewayServer() {
521
542
  if (running) {
@@ -605,16 +626,18 @@ function handleUiApiRequest(req, res, opts = {}) {
605
626
  handleOAuthStatus(req, res);
606
627
  } else if (url.startsWith("/oauth/callback") && req.method === "GET") {
607
628
  handleOAuthCallback(req, res);
608
- } else if (url === "/api/apps" && req.method === "GET") {
609
- handleGetApps(res);
610
- } else if (url === "/api/apps/path" && req.method === "POST") {
611
- handleSetAppPath(req, res);
612
- } else if (url === "/api/apps/launch" && req.method === "POST") {
613
- handleLaunchApp(req, res, opts);
614
- } else if (url === "/api/apps/browse-folder" && req.method === "POST") {
615
- handleBrowseFolder(res);
629
+ } else if (url.startsWith("/api/apps")) {
630
+ if (opts.uiMode === "server") {
631
+ sendJson(res, 403, { error: "App launch is unavailable in server admin UI mode." });
632
+ return;
633
+ }
634
+ if (url === "/api/apps" && req.method === "GET") handleGetApps(res);
635
+ else if (url === "/api/apps/path" && req.method === "POST") handleSetAppPath(req, res);
636
+ else if (url === "/api/apps/launch" && req.method === "POST") handleLaunchApp(req, res, opts);
637
+ else if (url === "/api/apps/browse-folder" && req.method === "POST") handleBrowseFolder(res);
638
+ else sendJson(res, 404, { error: "Not found" });
616
639
  } else if (url === "/api/server/status" && req.method === "GET") {
617
- handleGetServerStatus(res);
640
+ handleGetServerStatus(req, res);
618
641
  } else if (url === "/api/server/providers" && req.method === "GET") {
619
642
  handleGetServerProviders(res);
620
643
  } else if (url === "/api/server/start" && req.method === "POST") {
@@ -723,7 +746,7 @@ async function handlePostKeys(req, res) {
723
746
  if (saved) {
724
747
  sendJson(res, 200, { ok: true });
725
748
  } else {
726
- sendJson(res, 500, { error: "Keychain unavailable \u2014 key not saved" });
749
+ sendJson(res, 500, { error: "Credential store unavailable \u2014 key not saved" });
727
750
  }
728
751
  } catch (err) {
729
752
  sendJson(res, 400, { error: String(err) });
@@ -798,7 +821,7 @@ async function handleAddProvider(req, res) {
798
821
  sendJson(res, 400, { error: "templateId required" });
799
822
  return;
800
823
  }
801
- const { listSupportedTemplates } = await import("./provider-templates-6XYKAZB5.js");
824
+ const { listSupportedTemplates } = await import("./provider-templates-BPGB5V2L.js");
802
825
  const template = listSupportedTemplates().find((t) => t.id === templateId);
803
826
  if (!template) {
804
827
  sendJson(res, 404, { error: `Template '${templateId}' not found` });
@@ -1199,9 +1222,9 @@ async function handleSetAppPath(req, res) {
1199
1222
  sendJson(res, 500, { error: String(err) });
1200
1223
  }
1201
1224
  }
1202
- async function handleGetServerStatus(res) {
1225
+ async function handleGetServerStatus(req, res) {
1203
1226
  try {
1204
- sendJson(res, 200, await getServerStatus());
1227
+ sendJson(res, 200, await getServerStatus({ requestHost: hostFromHeader(req.headers.host) }));
1205
1228
  } catch (err) {
1206
1229
  sendJson(res, 500, { error: String(err) });
1207
1230
  }
@@ -1229,17 +1252,20 @@ async function handleStartServer(req, res, opts) {
1229
1252
  sendJson(res, 400, { error: 'listenMode must be "local" or "network"' });
1230
1253
  return;
1231
1254
  }
1255
+ const listenMode = opts.uiMode === "server" ? "network" : body.listenMode;
1232
1256
  const request = {
1233
1257
  favoritesOnly: body.favoritesOnly,
1234
1258
  freeModelsOnly: Boolean(body.freeModelsOnly),
1235
1259
  exposedProviders: Array.isArray(body.exposedProviders) ? body.exposedProviders : null,
1236
1260
  maskGatewayIds: body.maskGatewayIds,
1237
- listenMode: body.listenMode,
1261
+ listenMode,
1238
1262
  passwordMode: body.passwordMode === "saved" ? "saved" : "new",
1239
1263
  password: typeof body.password === "string" ? body.password : void 0,
1240
1264
  savePassword: Boolean(body.savePassword)
1241
1265
  };
1242
- const result = await startGatewayServer(request);
1266
+ const result = await startGatewayServer(request, {
1267
+ requestHost: hostFromHeader(req.headers.host)
1268
+ });
1243
1269
  if (result.ok) {
1244
1270
  notifyServerLifecycle(opts, {
1245
1271
  type: "started",
@@ -1337,6 +1363,7 @@ async function handleBrowseFolder(res) {
1337
1363
  var __dirname = dirname(fileURLToPath(import.meta.url));
1338
1364
  var PUBLIC_DIR = join2(__dirname, "ui", "public");
1339
1365
  var LOCK_FILE = join2(getAppHome(), "ui.lock");
1366
+ var DEFAULT_SERVER_UI_PORT = 8787;
1340
1367
  var MIME = {
1341
1368
  ".html": "text/html; charset=utf-8",
1342
1369
  ".js": "application/javascript; charset=utf-8",
@@ -1349,14 +1376,47 @@ function ext(path) {
1349
1376
  const i = path.lastIndexOf(".");
1350
1377
  return i >= 0 ? path.slice(i) : "";
1351
1378
  }
1352
- function buildStaticCache() {
1379
+ function resolveUiMode(opts = {}, env = process.env) {
1380
+ if (opts.serverMode) return "server";
1381
+ return env.RELAY_AI_UI_MODE === "server" ? "server" : "full";
1382
+ }
1383
+ function resolveServerUiPort(opts, env) {
1384
+ if (opts.port != null) return opts.port;
1385
+ const envPort = Number(env.RELAY_AI_UI_PORT);
1386
+ return Number.isFinite(envPort) && envPort > 0 ? envPort : DEFAULT_SERVER_UI_PORT;
1387
+ }
1388
+ function resolveUiRuntimeConfig(opts = {}, env = process.env) {
1389
+ const mode = resolveUiMode(opts, env);
1390
+ if (mode === "server") {
1391
+ return {
1392
+ mode,
1393
+ host: "0.0.0.0",
1394
+ port: resolveServerUiPort(opts, env),
1395
+ openBrowser: false,
1396
+ confirmShutdownOnSigint: false
1397
+ };
1398
+ }
1399
+ return {
1400
+ mode,
1401
+ host: "127.0.0.1",
1402
+ port: opts.port ?? 0,
1403
+ openBrowser: true,
1404
+ confirmShutdownOnSigint: true
1405
+ };
1406
+ }
1407
+ function buildStaticCache(mode) {
1353
1408
  const cache = /* @__PURE__ */ new Map();
1354
1409
  try {
1355
1410
  for (const name of readdirSync(PUBLIC_DIR)) {
1356
1411
  const mime = MIME[ext(name)];
1357
1412
  if (!mime) continue;
1358
1413
  const raw = readFileSync(join2(PUBLIC_DIR, name));
1359
- const content = name === "index.html" ? Buffer.from(raw.toString("utf8").replace("{{VERSION}}", VERSION)) : raw;
1414
+ let content = raw;
1415
+ if (name === "index.html") {
1416
+ content = Buffer.from(
1417
+ raw.toString("utf8").replaceAll("{{VERSION}}", VERSION).replaceAll("{{UI_MODE}}", mode)
1418
+ );
1419
+ }
1360
1420
  cache.set(`/${name}`, { content, mime });
1361
1421
  }
1362
1422
  } catch {
@@ -1392,13 +1452,16 @@ function formatUiServerLifecycleMessage(event) {
1392
1452
  async function resolveUiShutdownDecision(signal, promptClose = () => p.confirm({
1393
1453
  message: "Relay-AI UI is still running. Close it?",
1394
1454
  initialValue: true
1395
- })) {
1396
- if (signal !== "SIGINT") return "close";
1455
+ }), opts) {
1456
+ const confirmOnSigint = opts?.confirmOnSigint !== false;
1457
+ if (signal !== "SIGINT" || !confirmOnSigint) return "close";
1397
1458
  const shouldClose = await promptClose();
1398
1459
  if (p.isCancel(shouldClose)) return "close";
1399
1460
  return shouldClose ? "close" : "keep";
1400
1461
  }
1401
1462
  async function runUiCommand(opts = {}) {
1463
+ const runtime = resolveUiRuntimeConfig(opts);
1464
+ await ensureOpencodeCloudProviders();
1402
1465
  const existing = checkExistingServer();
1403
1466
  if (existing) {
1404
1467
  console.log(`
@@ -1409,10 +1472,10 @@ async function runUiCommand(opts = {}) {
1409
1472
  if (opts.trace) {
1410
1473
  process.env.RELAY_AI_TRACE = "1";
1411
1474
  }
1412
- const staticCache = buildStaticCache();
1475
+ const staticCache = buildStaticCache(runtime.mode);
1413
1476
  const traceLogPath = opts.trace ? getUiDebugLogPath() : void 0;
1414
1477
  const trace = traceLogPath ? makeTraceLogger(traceLogPath) : void 0;
1415
- trace?.("ui server starting");
1478
+ trace?.(`ui server starting mode=${runtime.mode}`);
1416
1479
  const server = createServer((req, res) => {
1417
1480
  const url2 = req.url ?? "/";
1418
1481
  res.setHeader("X-Content-Type-Options", "nosniff");
@@ -1420,6 +1483,7 @@ async function runUiCommand(opts = {}) {
1420
1483
  handleUiApiRequest(req, res, {
1421
1484
  trace: opts.trace,
1422
1485
  traceLogPath,
1486
+ uiMode: runtime.mode,
1423
1487
  onServerLifecycle: (event) => {
1424
1488
  console.log(`
1425
1489
  ${formatUiServerLifecycleMessage(event)}
@@ -1440,7 +1504,7 @@ async function runUiCommand(opts = {}) {
1440
1504
  res.end("Not found");
1441
1505
  });
1442
1506
  await new Promise((resolve, reject) => {
1443
- server.listen(0, "127.0.0.1", () => resolve());
1507
+ server.listen(runtime.port, runtime.host, () => resolve());
1444
1508
  server.once("error", reject);
1445
1509
  });
1446
1510
  const addr = server.address();
@@ -1449,9 +1513,10 @@ async function runUiCommand(opts = {}) {
1449
1513
  return 1;
1450
1514
  }
1451
1515
  const port = addr.port;
1452
- const url = `http://127.0.0.1:${port}`;
1516
+ const displayHost = runtime.host === "0.0.0.0" ? "127.0.0.1" : runtime.host;
1517
+ const url = `http://${displayHost}:${port}`;
1453
1518
  mkdirSync(getAppHome(), { recursive: true });
1454
- writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port }));
1519
+ writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port, mode: runtime.mode }));
1455
1520
  const cleanup = () => {
1456
1521
  removeLock();
1457
1522
  server.close();
@@ -1461,7 +1526,11 @@ async function runUiCommand(opts = {}) {
1461
1526
  const handleSignal = async (signal) => {
1462
1527
  if (handlingSignal) return;
1463
1528
  handlingSignal = true;
1464
- const decision = await resolveUiShutdownDecision(signal);
1529
+ const decision = await resolveUiShutdownDecision(
1530
+ signal,
1531
+ void 0,
1532
+ { confirmOnSigint: runtime.confirmShutdownOnSigint }
1533
+ );
1465
1534
  if (decision === "keep") {
1466
1535
  handlingSignal = false;
1467
1536
  return;
@@ -1474,21 +1543,28 @@ async function runUiCommand(opts = {}) {
1474
1543
  process.on("SIGTERM", () => {
1475
1544
  void handleSignal("SIGTERM");
1476
1545
  });
1546
+ const modeLabel = runtime.mode === "server" ? " (server admin)" : "";
1477
1547
  console.log(`
1478
- ${pc.bold("relay-ai UI")} ${pc.cyan(url)}
1548
+ ${pc.bold("relay-ai UI")}${modeLabel} ${pc.cyan(url)}
1479
1549
  ${pc.dim("Press Ctrl+C to stop")}
1480
1550
  `);
1551
+ if (runtime.mode === "server") {
1552
+ console.log(` ${pc.dim("Gateway API (when started from Server tab): http://127.0.0.1:17645")}
1553
+ `);
1554
+ }
1481
1555
  if (traceLogPath) {
1482
1556
  console.log(` ${pc.dim(`Trace log: ${traceLogPath}`)}
1483
1557
  `);
1484
1558
  trace?.(`ui server listening ${url}`);
1485
1559
  }
1486
- try {
1487
- const { default: open } = await import("open");
1488
- await open(url);
1489
- trace?.(`browser open ${url}`);
1490
- } catch {
1491
- trace?.(`browser open failed ${url}`);
1560
+ if (runtime.openBrowser) {
1561
+ try {
1562
+ const { default: open } = await import("open");
1563
+ await open(url);
1564
+ trace?.(`browser open ${url}`);
1565
+ } catch {
1566
+ trace?.(`browser open failed ${url}`);
1567
+ }
1492
1568
  }
1493
1569
  await new Promise(() => {
1494
1570
  });
@@ -1497,7 +1573,9 @@ async function runUiCommand(opts = {}) {
1497
1573
  export {
1498
1574
  formatUiServerLifecycleMessage,
1499
1575
  isUiApiRoute,
1576
+ resolveUiMode,
1577
+ resolveUiRuntimeConfig,
1500
1578
  resolveUiShutdownDecision,
1501
1579
  runUiCommand
1502
1580
  };
1503
- //# sourceMappingURL=ui-command-M7KMKIQC.js.map
1581
+ //# sourceMappingURL=ui-command-EQJIZRZZ.js.map