nextclaw 0.4.15 → 0.4.17

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/dist/cli/index.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  MessageBus,
22
22
  AgentLoop,
23
23
  LiteLLMProvider,
24
+ LLMProvider,
24
25
  ProviderManager,
25
26
  ChannelManager,
26
27
  SessionManager,
@@ -81,7 +82,7 @@ import {
81
82
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
82
83
  import { join, resolve } from "path";
83
84
  import { spawn } from "child_process";
84
- import { createServer, isIP } from "net";
85
+ import { isIP } from "net";
85
86
  import { fileURLToPath } from "url";
86
87
  import { getDataDir, getPackageVersion as getCorePackageVersion } from "@nextclaw/core";
87
88
  function resolveUiConfig(config2, overrides) {
@@ -127,48 +128,9 @@ async function resolvePublicIp(timeoutMs = 1500) {
127
128
  }
128
129
  return null;
129
130
  }
130
- function isDevRuntime() {
131
- return import.meta.url.includes("/src/cli/") || process.env.NEXTCLAW_DEV === "1";
132
- }
133
- function normalizeHostForPortCheck(host) {
134
- return host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
135
- }
136
- async function findAvailablePort(port, host, attempts = 20) {
137
- const basePort = Number.isFinite(port) ? port : 0;
138
- let candidate = basePort;
139
- for (let i = 0; i < attempts; i += 1) {
140
- const ok = await isPortAvailable(candidate, host);
141
- if (ok) {
142
- return candidate;
143
- }
144
- candidate += 1;
145
- }
146
- return basePort;
147
- }
148
- async function isPortAvailable(port, host) {
149
- const checkHost = normalizeHostForPortCheck(host);
150
- return await canBindPort(port, checkHost);
151
- }
152
- async function canBindPort(port, host) {
153
- return await new Promise((resolve5) => {
154
- const server = createServer();
155
- server.unref();
156
- server.once("error", () => resolve5(false));
157
- server.listen({ port, host }, () => {
158
- server.close(() => resolve5(true));
159
- });
160
- });
161
- }
162
131
  function buildServeArgs(options) {
163
132
  const cliPath = fileURLToPath(new URL("./index.js", import.meta.url));
164
- const args = [cliPath, "serve", "--ui-host", options.uiHost, "--ui-port", String(options.uiPort)];
165
- if (options.frontend) {
166
- args.push("--frontend");
167
- }
168
- if (Number.isFinite(options.frontendPort)) {
169
- args.push("--frontend-port", String(options.frontendPort));
170
- }
171
- return args;
133
+ return [cliPath, "serve", "--ui-host", options.uiHost, "--ui-port", String(options.uiPort)];
172
134
  }
173
135
  function readServiceState() {
174
136
  const path = resolveServiceStatePath();
@@ -297,64 +259,6 @@ function getPackageVersion() {
297
259
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
298
260
  return resolveVersionFromPackageTree(cliDir, "nextclaw") ?? resolveVersionFromPackageTree(cliDir) ?? getCorePackageVersion();
299
261
  }
300
- function startUiFrontend(options) {
301
- const uiDir = options.dir ?? resolveUiFrontendDir();
302
- if (!uiDir) {
303
- return null;
304
- }
305
- const runner = resolveUiFrontendRunner();
306
- if (!runner) {
307
- console.log("Warning: pnpm/npm not found. Skipping UI frontend.");
308
- return null;
309
- }
310
- const args = [...runner.args];
311
- if (options.port) {
312
- if (runner.useArgSeparator) {
313
- args.push("--");
314
- }
315
- args.push("--port", String(options.port));
316
- }
317
- const env = { ...process.env, VITE_API_BASE: options.apiBase };
318
- const child = spawn(runner.cmd, args, { cwd: uiDir, stdio: "inherit", env });
319
- child.on("exit", (code) => {
320
- if (code && code !== 0) {
321
- console.log(`UI frontend exited with code ${code}`);
322
- }
323
- });
324
- const url = `http://127.0.0.1:${options.port}`;
325
- console.log(`\u2713 UI frontend: ${url}`);
326
- return { url, dir: uiDir };
327
- }
328
- function resolveUiFrontendRunner() {
329
- if (which("pnpm")) {
330
- return { cmd: "pnpm", args: ["dev"], useArgSeparator: false };
331
- }
332
- if (which("npm")) {
333
- return { cmd: "npm", args: ["run", "dev"], useArgSeparator: true };
334
- }
335
- return null;
336
- }
337
- function resolveUiFrontendDir() {
338
- const candidates = [];
339
- const envDir = process.env.NEXTCLAW_UI_DIR;
340
- if (envDir) {
341
- candidates.push(envDir);
342
- }
343
- const cwd = process.cwd();
344
- candidates.push(join(cwd, "packages", "nextclaw-ui"));
345
- candidates.push(join(cwd, "nextclaw-ui"));
346
- const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
347
- const pkgRoot = resolve(cliDir, "..", "..");
348
- candidates.push(join(pkgRoot, "..", "nextclaw-ui"));
349
- candidates.push(join(pkgRoot, "..", "..", "packages", "nextclaw-ui"));
350
- candidates.push(join(pkgRoot, "..", "..", "nextclaw-ui"));
351
- for (const dir of candidates) {
352
- if (existsSync(join(dir, "package.json"))) {
353
- return dir;
354
- }
355
- }
356
- return null;
357
- }
358
262
  function printAgentResponse(response) {
359
263
  console.log("\n" + response + "\n");
360
264
  }
@@ -460,17 +364,21 @@ var mergeDeep = (base, patch) => {
460
364
  }
461
365
  return next;
462
366
  };
463
- var scheduleRestart = (delayMs, reason) => {
464
- const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
465
- console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
466
- setTimeout(() => {
467
- process.exit(0);
468
- }, delay);
469
- };
470
367
  var GatewayControllerImpl = class {
471
368
  constructor(deps) {
472
369
  this.deps = deps;
473
370
  }
371
+ async requestRestart(options) {
372
+ if (this.deps.requestRestart) {
373
+ await this.deps.requestRestart(options);
374
+ return;
375
+ }
376
+ const delay = typeof options?.delayMs === "number" && Number.isFinite(options.delayMs) ? Math.max(0, options.delayMs) : 100;
377
+ console.log(`Gateway restart requested via tool${options?.reason ? ` (${options.reason})` : ""}.`);
378
+ setTimeout(() => {
379
+ process.exit(0);
380
+ }, delay);
381
+ }
474
382
  status() {
475
383
  return {
476
384
  channels: this.deps.reloader.getChannels().enabledChannels,
@@ -482,7 +390,7 @@ var GatewayControllerImpl = class {
482
390
  return this.deps.reloader.reloadConfig(reason);
483
391
  }
484
392
  async restart(options) {
485
- scheduleRestart(options?.delayMs, options?.reason);
393
+ await this.requestRestart(options);
486
394
  return "Restart scheduled";
487
395
  }
488
396
  async getConfig() {
@@ -527,7 +435,7 @@ var GatewayControllerImpl = class {
527
435
  }
528
436
  this.deps.saveConfig(validated);
529
437
  const delayMs = params.restartDelayMs ?? 0;
530
- scheduleRestart(delayMs, "config.apply");
438
+ await this.requestRestart({ delayMs, reason: "config.apply" });
531
439
  return {
532
440
  ok: true,
533
441
  note: params.note ?? null,
@@ -563,7 +471,7 @@ var GatewayControllerImpl = class {
563
471
  }
564
472
  this.deps.saveConfig(validated);
565
473
  const delayMs = params.restartDelayMs ?? 0;
566
- scheduleRestart(delayMs, "config.patch");
474
+ await this.requestRestart({ delayMs, reason: "config.patch" });
567
475
  return {
568
476
  ok: true,
569
477
  note: params.note ?? null,
@@ -578,7 +486,7 @@ var GatewayControllerImpl = class {
578
486
  return { ok: false, error: result.error ?? "update failed", steps: result.steps };
579
487
  }
580
488
  const delayMs = params.restartDelayMs ?? 0;
581
- scheduleRestart(delayMs, "update.run");
489
+ await this.requestRestart({ delayMs, reason: "update.run" });
582
490
  return {
583
491
  ok: true,
584
492
  note: params.note ?? null,
@@ -589,6 +497,63 @@ var GatewayControllerImpl = class {
589
497
  }
590
498
  };
591
499
 
500
+ // src/cli/restart-coordinator.ts
501
+ var RestartCoordinator = class {
502
+ constructor(deps) {
503
+ this.deps = deps;
504
+ }
505
+ restartingService = false;
506
+ exitScheduled = false;
507
+ async requestRestart(request) {
508
+ const reason = request.reason.trim() || "config changed";
509
+ const strategy = request.strategy ?? "background-service-or-manual";
510
+ if (strategy !== "exit-process") {
511
+ const state = this.deps.readServiceState();
512
+ const serviceRunning = Boolean(state && this.deps.isProcessRunning(state.pid));
513
+ const managedByCurrentProcess = Boolean(state && state.pid === this.deps.currentPid());
514
+ if (serviceRunning && !managedByCurrentProcess) {
515
+ if (this.restartingService) {
516
+ return {
517
+ status: "restart-in-progress",
518
+ message: "Restart already in progress; skipping duplicate request."
519
+ };
520
+ }
521
+ this.restartingService = true;
522
+ try {
523
+ const restarted = await this.deps.restartBackgroundService(reason);
524
+ if (restarted) {
525
+ return {
526
+ status: "service-restarted",
527
+ message: `Restarted background service to apply changes (${reason}).`
528
+ };
529
+ }
530
+ } finally {
531
+ this.restartingService = false;
532
+ }
533
+ }
534
+ }
535
+ if (strategy === "background-service-or-exit" || strategy === "exit-process") {
536
+ if (this.exitScheduled) {
537
+ return {
538
+ status: "exit-scheduled",
539
+ message: "Restart already scheduled; skipping duplicate request."
540
+ };
541
+ }
542
+ const delay = typeof request.delayMs === "number" && Number.isFinite(request.delayMs) ? Math.max(0, Math.floor(request.delayMs)) : 100;
543
+ this.exitScheduled = true;
544
+ this.deps.scheduleProcessExit(delay, reason);
545
+ return {
546
+ status: "exit-scheduled",
547
+ message: `Restart scheduled (${reason}).`
548
+ };
549
+ }
550
+ return {
551
+ status: "manual-required",
552
+ message: request.manualMessage ?? "Restart the gateway to apply changes."
553
+ };
554
+ }
555
+ };
556
+
592
557
  // src/cli/skills/clawhub.ts
593
558
  import { spawnSync as spawnSync2 } from "child_process";
594
559
  import { existsSync as existsSync3 } from "fs";
@@ -847,6 +812,21 @@ function unsetAtConfigPath(root, pathSegments) {
847
812
  delete record[last];
848
813
  return true;
849
814
  }
815
+ var MissingProvider = class extends LLMProvider {
816
+ constructor(defaultModel) {
817
+ super(null, null);
818
+ this.defaultModel = defaultModel;
819
+ }
820
+ setDefaultModel(model) {
821
+ this.defaultModel = model;
822
+ }
823
+ async chat() {
824
+ throw new Error("No API key configured yet. Configure provider credentials in UI and retry.");
825
+ }
826
+ getDefaultModel() {
827
+ return this.defaultModel;
828
+ }
829
+ };
850
830
  var ConfigReloader = class {
851
831
  constructor(options) {
852
832
  this.options = options;
@@ -863,6 +843,9 @@ var ConfigReloader = class {
863
843
  getChannels() {
864
844
  return this.channels;
865
845
  }
846
+ setApplyAgentRuntimeConfig(callback) {
847
+ this.options.applyAgentRuntimeConfig = callback;
848
+ }
866
849
  async applyReloadPlan(nextConfig) {
867
850
  const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
868
851
  if (!changedPaths.length) {
@@ -872,9 +855,15 @@ var ConfigReloader = class {
872
855
  const plan = buildReloadPlan(changedPaths);
873
856
  if (plan.restartChannels) {
874
857
  await this.reloadChannels(nextConfig);
858
+ console.log("Config reload: channels restarted.");
875
859
  }
876
860
  if (plan.reloadProviders) {
877
861
  await this.reloadProvider(nextConfig);
862
+ console.log("Config reload: provider settings applied.");
863
+ }
864
+ if (plan.reloadAgent) {
865
+ this.options.applyAgentRuntimeConfig?.(nextConfig);
866
+ console.log("Config reload: agent defaults applied.");
878
867
  }
879
868
  if (plan.restartRequired.length > 0) {
880
869
  this.options.onRestartRequired(plan.restartRequired);
@@ -961,12 +950,75 @@ var ConfigReloader = class {
961
950
  };
962
951
  var CliRuntime = class {
963
952
  logo;
953
+ restartCoordinator;
954
+ serviceRestartTask = null;
964
955
  constructor(options = {}) {
965
956
  this.logo = options.logo ?? LOGO;
957
+ this.restartCoordinator = new RestartCoordinator({
958
+ readServiceState,
959
+ isProcessRunning,
960
+ currentPid: () => process.pid,
961
+ restartBackgroundService: async (reason) => this.restartBackgroundService(reason),
962
+ scheduleProcessExit: (delayMs, reason) => this.scheduleProcessExit(delayMs, reason)
963
+ });
966
964
  }
967
965
  get version() {
968
966
  return getPackageVersion();
969
967
  }
968
+ scheduleProcessExit(delayMs, reason) {
969
+ console.warn(`Gateway restart requested (${reason}).`);
970
+ setTimeout(() => {
971
+ process.exit(0);
972
+ }, delayMs);
973
+ }
974
+ async restartBackgroundService(reason) {
975
+ if (this.serviceRestartTask) {
976
+ return this.serviceRestartTask;
977
+ }
978
+ this.serviceRestartTask = (async () => {
979
+ const state = readServiceState();
980
+ if (!state || !isProcessRunning(state.pid) || state.pid === process.pid) {
981
+ return false;
982
+ }
983
+ const uiHost = state.uiHost ?? "127.0.0.1";
984
+ const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
985
+ console.log(`Applying changes (${reason}): restarting ${APP_NAME} background service...`);
986
+ await this.stopService();
987
+ await this.startService({
988
+ uiOverrides: {
989
+ enabled: true,
990
+ host: uiHost,
991
+ port: uiPort
992
+ },
993
+ open: false
994
+ });
995
+ return true;
996
+ })();
997
+ try {
998
+ return await this.serviceRestartTask;
999
+ } finally {
1000
+ this.serviceRestartTask = null;
1001
+ }
1002
+ }
1003
+ async requestRestart(params) {
1004
+ const result = await this.restartCoordinator.requestRestart({
1005
+ reason: params.reason,
1006
+ strategy: params.strategy,
1007
+ delayMs: params.delayMs,
1008
+ manualMessage: params.manualMessage
1009
+ });
1010
+ if (result.status === "manual-required" || result.status === "restart-in-progress") {
1011
+ console.log(result.message);
1012
+ return;
1013
+ }
1014
+ if (result.status === "service-restarted") {
1015
+ if (!params.silentOnServiceRestart) {
1016
+ console.log(result.message);
1017
+ }
1018
+ return;
1019
+ }
1020
+ console.warn(result.message);
1021
+ }
970
1022
  async onboard() {
971
1023
  console.warn(`Warning: ${APP_NAME} onboard is deprecated. Use "${APP_NAME} init" instead.`);
972
1024
  await this.init({ source: "onboard" });
@@ -1063,35 +1115,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1063
1115
  if (opts.public && !opts.uiHost) {
1064
1116
  uiOverrides.host = "0.0.0.0";
1065
1117
  }
1066
- const devMode = isDevRuntime();
1067
- if (devMode) {
1068
- const requestedUiPort = Number.isFinite(Number(opts.uiPort)) ? Number(opts.uiPort) : 18792;
1069
- const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : 5174;
1070
- const uiHost = uiOverrides.host ?? "127.0.0.1";
1071
- const devUiPort = await findAvailablePort(requestedUiPort, uiHost);
1072
- const shouldStartFrontend = opts.frontend === void 0 ? true : Boolean(opts.frontend);
1073
- const devFrontendPort = shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
1074
- uiOverrides.port = devUiPort;
1075
- if (requestedUiPort !== devUiPort) {
1076
- console.log(`Dev mode: UI port ${requestedUiPort} is in use, switched to ${devUiPort}.`);
1077
- }
1078
- if (shouldStartFrontend && requestedFrontendPort !== devFrontendPort) {
1079
- console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${devFrontendPort}.`);
1080
- }
1081
- console.log(`Dev mode: UI ${devUiPort}, Frontend ${devFrontendPort}`);
1082
- console.log("Dev mode runs in the foreground (Ctrl+C to stop).");
1083
- await this.runForeground({
1084
- uiOverrides,
1085
- frontend: shouldStartFrontend,
1086
- frontendPort: devFrontendPort,
1087
- open: Boolean(opts.open)
1088
- });
1089
- return;
1090
- }
1091
1118
  await this.startService({
1092
1119
  uiOverrides,
1093
- frontend: Boolean(opts.frontend),
1094
- frontendPort: Number(opts.frontendPort),
1095
1120
  open: Boolean(opts.open)
1096
1121
  });
1097
1122
  }
@@ -1122,29 +1147,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1122
1147
  if (opts.public && !opts.uiHost) {
1123
1148
  uiOverrides.host = "0.0.0.0";
1124
1149
  }
1125
- const devMode = isDevRuntime();
1126
- if (devMode && uiOverrides.port === void 0) {
1127
- uiOverrides.port = 18792;
1128
- }
1129
- const shouldStartFrontend = Boolean(opts.frontend);
1130
- const defaultFrontendPort = devMode ? 5174 : 5173;
1131
- const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : defaultFrontendPort;
1132
- if (devMode && uiOverrides.port !== void 0) {
1133
- const uiHost = uiOverrides.host ?? "127.0.0.1";
1134
- const uiPort = await findAvailablePort(uiOverrides.port, uiHost);
1135
- if (uiPort !== uiOverrides.port) {
1136
- console.log(`Dev mode: UI port ${uiOverrides.port} is in use, switched to ${uiPort}.`);
1137
- uiOverrides.port = uiPort;
1138
- }
1139
- }
1140
- const frontendPort = devMode && shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
1141
- if (devMode && shouldStartFrontend && frontendPort !== requestedFrontendPort) {
1142
- console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${frontendPort}.`);
1143
- }
1144
1150
  await this.runForeground({
1145
1151
  uiOverrides,
1146
- frontend: shouldStartFrontend,
1147
- frontendPort,
1148
1152
  open: Boolean(opts.open)
1149
1153
  });
1150
1154
  }
@@ -1403,7 +1407,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1403
1407
  }
1404
1408
  console.log(JSON.stringify(result.value ?? null, null, 2));
1405
1409
  }
1406
- configSet(pathExpr, value, opts = {}) {
1410
+ async configSet(pathExpr, value, opts = {}) {
1407
1411
  let parsedPath;
1408
1412
  try {
1409
1413
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -1429,9 +1433,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1429
1433
  return;
1430
1434
  }
1431
1435
  saveConfig(config2);
1432
- console.log(`Updated ${pathExpr}. Restart the gateway to apply.`);
1436
+ await this.requestRestart({
1437
+ reason: `config.set ${pathExpr}`,
1438
+ manualMessage: `Updated ${pathExpr}. Restart the gateway to apply.`
1439
+ });
1433
1440
  }
1434
- configUnset(pathExpr) {
1441
+ async configUnset(pathExpr) {
1435
1442
  let parsedPath;
1436
1443
  try {
1437
1444
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -1448,19 +1455,28 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1448
1455
  return;
1449
1456
  }
1450
1457
  saveConfig(config2);
1451
- console.log(`Removed ${pathExpr}. Restart the gateway to apply.`);
1458
+ await this.requestRestart({
1459
+ reason: `config.unset ${pathExpr}`,
1460
+ manualMessage: `Removed ${pathExpr}. Restart the gateway to apply.`
1461
+ });
1452
1462
  }
1453
- pluginsEnable(id) {
1463
+ async pluginsEnable(id) {
1454
1464
  const config2 = loadConfig();
1455
1465
  const next = enablePluginInConfig(config2, id);
1456
1466
  saveConfig(next);
1457
- console.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
1467
+ await this.requestRestart({
1468
+ reason: `plugin enabled: ${id}`,
1469
+ manualMessage: `Enabled plugin "${id}". Restart the gateway to apply.`
1470
+ });
1458
1471
  }
1459
- pluginsDisable(id) {
1472
+ async pluginsDisable(id) {
1460
1473
  const config2 = loadConfig();
1461
1474
  const next = disablePluginInConfig(config2, id);
1462
1475
  saveConfig(next);
1463
- console.log(`Disabled plugin "${id}". Restart the gateway to apply.`);
1476
+ await this.requestRestart({
1477
+ reason: `plugin disabled: ${id}`,
1478
+ manualMessage: `Disabled plugin "${id}". Restart the gateway to apply.`
1479
+ });
1464
1480
  }
1465
1481
  async pluginsUninstall(id, opts = {}) {
1466
1482
  const config2 = loadConfig();
@@ -1557,7 +1573,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1557
1573
  removed.push("directory");
1558
1574
  }
1559
1575
  console.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`);
1560
- console.log("Restart the gateway to apply changes.");
1576
+ await this.requestRestart({
1577
+ reason: `plugin uninstalled: ${pluginId}`,
1578
+ manualMessage: "Restart the gateway to apply changes."
1579
+ });
1561
1580
  }
1562
1581
  async pluginsInstall(pathOrSpec, opts = {}) {
1563
1582
  const fileSpec = this.resolveFileNpmSpecToLocalPath(pathOrSpec);
@@ -1586,7 +1605,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1586
1605
  });
1587
1606
  saveConfig(next3);
1588
1607
  console.log(`Linked plugin path: ${resolved}`);
1589
- console.log("Restart the gateway to load plugins.");
1608
+ await this.requestRestart({
1609
+ reason: `plugin linked: ${probe.pluginId}`,
1610
+ manualMessage: "Restart the gateway to load plugins."
1611
+ });
1590
1612
  return;
1591
1613
  }
1592
1614
  const result2 = await installPluginFromPath({
@@ -1610,7 +1632,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1610
1632
  });
1611
1633
  saveConfig(next2);
1612
1634
  console.log(`Installed plugin: ${result2.pluginId}`);
1613
- console.log("Restart the gateway to load plugins.");
1635
+ await this.requestRestart({
1636
+ reason: `plugin installed: ${result2.pluginId}`,
1637
+ manualMessage: "Restart the gateway to load plugins."
1638
+ });
1614
1639
  return;
1615
1640
  }
1616
1641
  if (opts.link) {
@@ -1642,7 +1667,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1642
1667
  });
1643
1668
  saveConfig(next);
1644
1669
  console.log(`Installed plugin: ${result.pluginId}`);
1645
- console.log("Restart the gateway to load plugins.");
1670
+ await this.requestRestart({
1671
+ reason: `plugin installed: ${result.pluginId}`,
1672
+ manualMessage: "Restart the gateway to load plugins."
1673
+ });
1646
1674
  }
1647
1675
  pluginsDoctor() {
1648
1676
  const config2 = loadConfig();
@@ -1732,7 +1760,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1732
1760
  console.error(`Bridge failed: ${result.status ?? 1}`);
1733
1761
  }
1734
1762
  }
1735
- channelsAdd(opts) {
1763
+ async channelsAdd(opts) {
1736
1764
  const channelId = opts.channel?.trim();
1737
1765
  if (!channelId) {
1738
1766
  console.error("--channel is required");
@@ -1783,7 +1811,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1783
1811
  next = enablePluginInConfig(next, binding.pluginId);
1784
1812
  saveConfig(next);
1785
1813
  console.log(`Configured channel "${binding.channelId}" via plugin "${binding.pluginId}".`);
1786
- console.log("Restart the gateway to apply changes.");
1814
+ await this.requestRestart({
1815
+ reason: `channel configured via plugin: ${binding.pluginId}`,
1816
+ manualMessage: "Restart the gateway to apply changes."
1817
+ });
1787
1818
  }
1788
1819
  toPluginConfigView(config2, bindings) {
1789
1820
  const view = JSON.parse(JSON.stringify(config2));
@@ -1986,7 +2017,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1986
2017
  this.logPluginDiagnostics(pluginRegistry);
1987
2018
  const bus = new MessageBus();
1988
2019
  const provider = options.allowMissingProvider === true ? this.makeProvider(config2, { allowMissing: true }) : this.makeProvider(config2);
1989
- const providerManager = provider ? new ProviderManager(provider) : null;
2020
+ const providerManager = new ProviderManager(provider ?? this.makeMissingProvider(config2));
1990
2021
  const sessionManager = new SessionManager(workspace);
1991
2022
  const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
1992
2023
  const cron2 = new CronService(cronStorePath);
@@ -1994,11 +2025,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1994
2025
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
1995
2026
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
1996
2027
  if (!provider) {
1997
- this.startUiIfEnabled(uiConfig, uiStaticDir);
1998
- console.log("Warning: No API key configured. UI server only.");
1999
- await new Promise(() => {
2000
- });
2001
- return;
2028
+ console.warn("Warning: No API key configured. The gateway is running, but agent replies are disabled until provider config is set.");
2002
2029
  }
2003
2030
  const channels2 = new ChannelManager(config2, bus, sessionManager, extensionRegistry.channels);
2004
2031
  const reloader = new ConfigReloader({
@@ -2007,11 +2034,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2007
2034
  bus,
2008
2035
  sessionManager,
2009
2036
  providerManager,
2010
- makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
2037
+ makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }) ?? this.makeMissingProvider(nextConfig),
2011
2038
  loadConfig,
2012
2039
  getExtensionChannels: () => extensionRegistry.channels,
2013
2040
  onRestartRequired: (paths) => {
2014
- console.warn(`Config changes require restart: ${paths.join(", ")}`);
2041
+ void this.requestRestart({
2042
+ reason: `config reload requires restart: ${paths.join(", ")}`,
2043
+ manualMessage: `Config changes require restart: ${paths.join(", ")}`,
2044
+ strategy: "background-service-or-manual"
2045
+ });
2015
2046
  }
2016
2047
  });
2017
2048
  const gatewayController = new GatewayControllerImpl({
@@ -2019,11 +2050,20 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2019
2050
  cron: cron2,
2020
2051
  getConfigPath,
2021
2052
  saveConfig,
2022
- getPluginUiMetadata: () => pluginUiMetadata
2053
+ getPluginUiMetadata: () => pluginUiMetadata,
2054
+ requestRestart: async (options2) => {
2055
+ await this.requestRestart({
2056
+ reason: options2?.reason ?? "gateway tool restart",
2057
+ manualMessage: "Restart the gateway to apply changes.",
2058
+ strategy: "background-service-or-exit",
2059
+ delayMs: options2?.delayMs,
2060
+ silentOnServiceRestart: true
2061
+ });
2062
+ }
2023
2063
  });
2024
2064
  const agent = new AgentLoop({
2025
2065
  bus,
2026
- providerManager: providerManager ?? new ProviderManager(provider),
2066
+ providerManager,
2027
2067
  workspace,
2028
2068
  model: config2.agents.defaults.model,
2029
2069
  maxIterations: config2.agents.defaults.maxToolIterations,
@@ -2043,6 +2083,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2043
2083
  accountId
2044
2084
  })
2045
2085
  });
2086
+ reloader.setApplyAgentRuntimeConfig((nextConfig) => agent.applyRuntimeConfig(nextConfig));
2046
2087
  const pluginChannelBindings = getPluginChannelBindings(pluginRegistry);
2047
2088
  setPluginRuntimeBridge({
2048
2089
  loadConfig: () => this.toPluginConfigView(loadConfig(), pluginChannelBindings),
@@ -2191,34 +2232,14 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2191
2232
  async runForeground(options) {
2192
2233
  const config2 = loadConfig();
2193
2234
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
2194
- const shouldStartFrontend = options.frontend;
2195
- const frontendPort = Number.isFinite(options.frontendPort) ? options.frontendPort : 5173;
2196
- const frontendDir = shouldStartFrontend ? resolveUiFrontendDir() : null;
2197
- const staticDir = resolveUiStaticDir();
2198
- let frontendUrl = null;
2199
- if (shouldStartFrontend && frontendDir) {
2200
- const frontend = startUiFrontend({
2201
- apiBase: resolveUiApiBase(uiConfig.host, uiConfig.port),
2202
- port: frontendPort,
2203
- dir: frontendDir
2204
- });
2205
- frontendUrl = frontend?.url ?? null;
2206
- } else if (shouldStartFrontend && !frontendDir) {
2207
- console.log("Warning: UI frontend not found. Start it separately.");
2208
- }
2209
- if (!frontendUrl && staticDir) {
2210
- frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2211
- }
2212
- if (options.open && frontendUrl) {
2213
- openBrowser(frontendUrl);
2214
- } else if (options.open && !frontendUrl) {
2215
- console.log("Warning: UI frontend not started. Browser not opened.");
2235
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2236
+ if (options.open) {
2237
+ openBrowser(uiUrl);
2216
2238
  }
2217
- const uiStaticDir = shouldStartFrontend && frontendDir ? null : staticDir;
2218
2239
  await this.startGateway({
2219
2240
  uiOverrides: options.uiOverrides,
2220
2241
  allowMissingProvider: true,
2221
- uiStaticDir
2242
+ uiStaticDir: resolveUiStaticDir()
2222
2243
  });
2223
2244
  }
2224
2245
  async startService(options) {
@@ -2232,6 +2253,28 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2232
2253
  console.log(`\u2713 ${APP_NAME} is already running (PID ${existing.pid})`);
2233
2254
  console.log(`UI: ${existing.uiUrl}`);
2234
2255
  console.log(`API: ${existing.apiUrl}`);
2256
+ const parsedUi = (() => {
2257
+ try {
2258
+ const parsed = new URL(existing.uiUrl);
2259
+ const port = Number(parsed.port || 80);
2260
+ return {
2261
+ host: existing.uiHost ?? parsed.hostname,
2262
+ port: Number.isFinite(port) ? port : existing.uiPort ?? 18791
2263
+ };
2264
+ } catch {
2265
+ return {
2266
+ host: existing.uiHost ?? "127.0.0.1",
2267
+ port: existing.uiPort ?? 18791
2268
+ };
2269
+ }
2270
+ })();
2271
+ await this.printPublicUiUrls(parsedUi.host, parsedUi.port);
2272
+ if (parsedUi.host !== uiConfig.host || parsedUi.port !== uiConfig.port) {
2273
+ console.log(
2274
+ `Note: requested UI bind (${uiConfig.host}:${uiConfig.port}) differs from running service (${parsedUi.host}:${parsedUi.port}).`
2275
+ );
2276
+ console.log(`Run: ${APP_NAME} restart${uiConfig.host === "0.0.0.0" ? " --public" : ""}`);
2277
+ }
2235
2278
  console.log(`Logs: ${existing.logPath}`);
2236
2279
  console.log(`Stop: ${APP_NAME} stop`);
2237
2280
  return;
@@ -2239,8 +2282,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2239
2282
  if (existing) {
2240
2283
  clearServiceState();
2241
2284
  }
2242
- if (!staticDir && !options.frontend) {
2243
- console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
2285
+ if (!staticDir) {
2286
+ console.log("Warning: UI frontend not found in package assets.");
2244
2287
  }
2245
2288
  const logPath = resolveServiceLogPath();
2246
2289
  const logDir = resolve4(logPath, "..");
@@ -2248,9 +2291,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2248
2291
  const logFd = openSync(logPath, "a");
2249
2292
  const serveArgs = buildServeArgs({
2250
2293
  uiHost: uiConfig.host,
2251
- uiPort: uiConfig.port,
2252
- frontend: options.frontend,
2253
- frontendPort: options.frontendPort
2294
+ uiPort: uiConfig.port
2254
2295
  });
2255
2296
  const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
2256
2297
  env: process.env,
@@ -2268,6 +2309,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2268
2309
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2269
2310
  uiUrl,
2270
2311
  apiUrl,
2312
+ uiHost: uiConfig.host,
2313
+ uiPort: uiConfig.port,
2271
2314
  logPath
2272
2315
  };
2273
2316
  writeServiceState(state);
@@ -2324,6 +2367,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2324
2367
  const normalized = answer.trim().toLowerCase();
2325
2368
  return normalized === "y" || normalized === "yes";
2326
2369
  }
2370
+ makeMissingProvider(config2) {
2371
+ return new MissingProvider(config2.agents.defaults.model);
2372
+ }
2327
2373
  makeProvider(config2, options) {
2328
2374
  const provider = getProvider(config2);
2329
2375
  const model = config2.agents.defaults.model;
@@ -2548,9 +2594,9 @@ program.command("onboard").description(`Initialize ${APP_NAME2} configuration an
2548
2594
  program.command("init").description(`Initialize ${APP_NAME2} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
2549
2595
  program.command("gateway").description(`Start the ${APP_NAME2} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.gateway(opts));
2550
2596
  program.command("ui").description(`Start the ${APP_NAME2} UI with gateway`).option("--host <host>", "UI host").option("--port <port>", "UI port").option("--no-open", "Disable opening browser").option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.ui(opts));
2551
- program.command("start").description(`Start the ${APP_NAME2} gateway + UI in the background`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.start(opts));
2552
- program.command("restart").description(`Restart the ${APP_NAME2} background service`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after restart", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.restart(opts));
2553
- program.command("serve").description(`Run the ${APP_NAME2} gateway + UI in the foreground`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.serve(opts));
2597
+ program.command("start").description(`Start the ${APP_NAME2} gateway + UI in the background`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.start(opts));
2598
+ program.command("restart").description(`Restart the ${APP_NAME2} background service`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--open", "Open browser after restart", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.restart(opts));
2599
+ program.command("serve").description(`Run the ${APP_NAME2} gateway + UI in the foreground`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).option("--public", "Expose UI on 0.0.0.0 and print public URL", false).action(async (opts) => runtime.serve(opts));
2554
2600
  program.command("stop").description(`Stop the ${APP_NAME2} background service`).action(async () => runtime.stop());
2555
2601
  program.command("agent").description("Interact with the agent directly").option("-m, --message <message>", "Message to send to the agent").option("-s, --session <session>", "Session ID", "cli:default").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => runtime.agent(opts));
2556
2602
  program.command("update").description(`Update ${APP_NAME2}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));