nextclaw 0.4.16 → 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.
Files changed (2) hide show
  1. package/dist/cli/index.js +203 -206
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -82,7 +82,7 @@ import {
82
82
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
83
83
  import { join, resolve } from "path";
84
84
  import { spawn } from "child_process";
85
- import { createServer, isIP } from "net";
85
+ import { isIP } from "net";
86
86
  import { fileURLToPath } from "url";
87
87
  import { getDataDir, getPackageVersion as getCorePackageVersion } from "@nextclaw/core";
88
88
  function resolveUiConfig(config2, overrides) {
@@ -128,48 +128,9 @@ async function resolvePublicIp(timeoutMs = 1500) {
128
128
  }
129
129
  return null;
130
130
  }
131
- function isDevRuntime() {
132
- return import.meta.url.includes("/src/cli/") || process.env.NEXTCLAW_DEV === "1";
133
- }
134
- function normalizeHostForPortCheck(host) {
135
- return host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
136
- }
137
- async function findAvailablePort(port, host, attempts = 20) {
138
- const basePort = Number.isFinite(port) ? port : 0;
139
- let candidate = basePort;
140
- for (let i = 0; i < attempts; i += 1) {
141
- const ok = await isPortAvailable(candidate, host);
142
- if (ok) {
143
- return candidate;
144
- }
145
- candidate += 1;
146
- }
147
- return basePort;
148
- }
149
- async function isPortAvailable(port, host) {
150
- const checkHost = normalizeHostForPortCheck(host);
151
- return await canBindPort(port, checkHost);
152
- }
153
- async function canBindPort(port, host) {
154
- return await new Promise((resolve5) => {
155
- const server = createServer();
156
- server.unref();
157
- server.once("error", () => resolve5(false));
158
- server.listen({ port, host }, () => {
159
- server.close(() => resolve5(true));
160
- });
161
- });
162
- }
163
131
  function buildServeArgs(options) {
164
132
  const cliPath = fileURLToPath(new URL("./index.js", import.meta.url));
165
- const args = [cliPath, "serve", "--ui-host", options.uiHost, "--ui-port", String(options.uiPort)];
166
- if (options.frontend) {
167
- args.push("--frontend");
168
- }
169
- if (Number.isFinite(options.frontendPort)) {
170
- args.push("--frontend-port", String(options.frontendPort));
171
- }
172
- return args;
133
+ return [cliPath, "serve", "--ui-host", options.uiHost, "--ui-port", String(options.uiPort)];
173
134
  }
174
135
  function readServiceState() {
175
136
  const path = resolveServiceStatePath();
@@ -298,64 +259,6 @@ function getPackageVersion() {
298
259
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
299
260
  return resolveVersionFromPackageTree(cliDir, "nextclaw") ?? resolveVersionFromPackageTree(cliDir) ?? getCorePackageVersion();
300
261
  }
301
- function startUiFrontend(options) {
302
- const uiDir = options.dir ?? resolveUiFrontendDir();
303
- if (!uiDir) {
304
- return null;
305
- }
306
- const runner = resolveUiFrontendRunner();
307
- if (!runner) {
308
- console.log("Warning: pnpm/npm not found. Skipping UI frontend.");
309
- return null;
310
- }
311
- const args = [...runner.args];
312
- if (options.port) {
313
- if (runner.useArgSeparator) {
314
- args.push("--");
315
- }
316
- args.push("--port", String(options.port));
317
- }
318
- const env = { ...process.env, VITE_API_BASE: options.apiBase };
319
- const child = spawn(runner.cmd, args, { cwd: uiDir, stdio: "inherit", env });
320
- child.on("exit", (code) => {
321
- if (code && code !== 0) {
322
- console.log(`UI frontend exited with code ${code}`);
323
- }
324
- });
325
- const url = `http://127.0.0.1:${options.port}`;
326
- console.log(`\u2713 UI frontend: ${url}`);
327
- return { url, dir: uiDir };
328
- }
329
- function resolveUiFrontendRunner() {
330
- if (which("pnpm")) {
331
- return { cmd: "pnpm", args: ["dev"], useArgSeparator: false };
332
- }
333
- if (which("npm")) {
334
- return { cmd: "npm", args: ["run", "dev"], useArgSeparator: true };
335
- }
336
- return null;
337
- }
338
- function resolveUiFrontendDir() {
339
- const candidates = [];
340
- const envDir = process.env.NEXTCLAW_UI_DIR;
341
- if (envDir) {
342
- candidates.push(envDir);
343
- }
344
- const cwd = process.cwd();
345
- candidates.push(join(cwd, "packages", "nextclaw-ui"));
346
- candidates.push(join(cwd, "nextclaw-ui"));
347
- const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
348
- const pkgRoot = resolve(cliDir, "..", "..");
349
- candidates.push(join(pkgRoot, "..", "nextclaw-ui"));
350
- candidates.push(join(pkgRoot, "..", "..", "packages", "nextclaw-ui"));
351
- candidates.push(join(pkgRoot, "..", "..", "nextclaw-ui"));
352
- for (const dir of candidates) {
353
- if (existsSync(join(dir, "package.json"))) {
354
- return dir;
355
- }
356
- }
357
- return null;
358
- }
359
262
  function printAgentResponse(response) {
360
263
  console.log("\n" + response + "\n");
361
264
  }
@@ -461,17 +364,21 @@ var mergeDeep = (base, patch) => {
461
364
  }
462
365
  return next;
463
366
  };
464
- var scheduleRestart = (delayMs, reason) => {
465
- const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
466
- console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
467
- setTimeout(() => {
468
- process.exit(0);
469
- }, delay);
470
- };
471
367
  var GatewayControllerImpl = class {
472
368
  constructor(deps) {
473
369
  this.deps = deps;
474
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
+ }
475
382
  status() {
476
383
  return {
477
384
  channels: this.deps.reloader.getChannels().enabledChannels,
@@ -483,7 +390,7 @@ var GatewayControllerImpl = class {
483
390
  return this.deps.reloader.reloadConfig(reason);
484
391
  }
485
392
  async restart(options) {
486
- scheduleRestart(options?.delayMs, options?.reason);
393
+ await this.requestRestart(options);
487
394
  return "Restart scheduled";
488
395
  }
489
396
  async getConfig() {
@@ -528,7 +435,7 @@ var GatewayControllerImpl = class {
528
435
  }
529
436
  this.deps.saveConfig(validated);
530
437
  const delayMs = params.restartDelayMs ?? 0;
531
- scheduleRestart(delayMs, "config.apply");
438
+ await this.requestRestart({ delayMs, reason: "config.apply" });
532
439
  return {
533
440
  ok: true,
534
441
  note: params.note ?? null,
@@ -564,7 +471,7 @@ var GatewayControllerImpl = class {
564
471
  }
565
472
  this.deps.saveConfig(validated);
566
473
  const delayMs = params.restartDelayMs ?? 0;
567
- scheduleRestart(delayMs, "config.patch");
474
+ await this.requestRestart({ delayMs, reason: "config.patch" });
568
475
  return {
569
476
  ok: true,
570
477
  note: params.note ?? null,
@@ -579,7 +486,7 @@ var GatewayControllerImpl = class {
579
486
  return { ok: false, error: result.error ?? "update failed", steps: result.steps };
580
487
  }
581
488
  const delayMs = params.restartDelayMs ?? 0;
582
- scheduleRestart(delayMs, "update.run");
489
+ await this.requestRestart({ delayMs, reason: "update.run" });
583
490
  return {
584
491
  ok: true,
585
492
  note: params.note ?? null,
@@ -590,6 +497,63 @@ var GatewayControllerImpl = class {
590
497
  }
591
498
  };
592
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
+
593
557
  // src/cli/skills/clawhub.ts
594
558
  import { spawnSync as spawnSync2 } from "child_process";
595
559
  import { existsSync as existsSync3 } from "fs";
@@ -986,12 +950,75 @@ var ConfigReloader = class {
986
950
  };
987
951
  var CliRuntime = class {
988
952
  logo;
953
+ restartCoordinator;
954
+ serviceRestartTask = null;
989
955
  constructor(options = {}) {
990
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
+ });
991
964
  }
992
965
  get version() {
993
966
  return getPackageVersion();
994
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
+ }
995
1022
  async onboard() {
996
1023
  console.warn(`Warning: ${APP_NAME} onboard is deprecated. Use "${APP_NAME} init" instead.`);
997
1024
  await this.init({ source: "onboard" });
@@ -1088,35 +1115,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1088
1115
  if (opts.public && !opts.uiHost) {
1089
1116
  uiOverrides.host = "0.0.0.0";
1090
1117
  }
1091
- const devMode = isDevRuntime();
1092
- if (devMode) {
1093
- const requestedUiPort = Number.isFinite(Number(opts.uiPort)) ? Number(opts.uiPort) : 18792;
1094
- const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : 5174;
1095
- const uiHost = uiOverrides.host ?? "127.0.0.1";
1096
- const devUiPort = await findAvailablePort(requestedUiPort, uiHost);
1097
- const shouldStartFrontend = opts.frontend === void 0 ? true : Boolean(opts.frontend);
1098
- const devFrontendPort = shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
1099
- uiOverrides.port = devUiPort;
1100
- if (requestedUiPort !== devUiPort) {
1101
- console.log(`Dev mode: UI port ${requestedUiPort} is in use, switched to ${devUiPort}.`);
1102
- }
1103
- if (shouldStartFrontend && requestedFrontendPort !== devFrontendPort) {
1104
- console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${devFrontendPort}.`);
1105
- }
1106
- console.log(`Dev mode: UI ${devUiPort}, Frontend ${devFrontendPort}`);
1107
- console.log("Dev mode runs in the foreground (Ctrl+C to stop).");
1108
- await this.runForeground({
1109
- uiOverrides,
1110
- frontend: shouldStartFrontend,
1111
- frontendPort: devFrontendPort,
1112
- open: Boolean(opts.open)
1113
- });
1114
- return;
1115
- }
1116
1118
  await this.startService({
1117
1119
  uiOverrides,
1118
- frontend: Boolean(opts.frontend),
1119
- frontendPort: Number(opts.frontendPort),
1120
1120
  open: Boolean(opts.open)
1121
1121
  });
1122
1122
  }
@@ -1147,29 +1147,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1147
1147
  if (opts.public && !opts.uiHost) {
1148
1148
  uiOverrides.host = "0.0.0.0";
1149
1149
  }
1150
- const devMode = isDevRuntime();
1151
- if (devMode && uiOverrides.port === void 0) {
1152
- uiOverrides.port = 18792;
1153
- }
1154
- const shouldStartFrontend = Boolean(opts.frontend);
1155
- const defaultFrontendPort = devMode ? 5174 : 5173;
1156
- const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : defaultFrontendPort;
1157
- if (devMode && uiOverrides.port !== void 0) {
1158
- const uiHost = uiOverrides.host ?? "127.0.0.1";
1159
- const uiPort = await findAvailablePort(uiOverrides.port, uiHost);
1160
- if (uiPort !== uiOverrides.port) {
1161
- console.log(`Dev mode: UI port ${uiOverrides.port} is in use, switched to ${uiPort}.`);
1162
- uiOverrides.port = uiPort;
1163
- }
1164
- }
1165
- const frontendPort = devMode && shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
1166
- if (devMode && shouldStartFrontend && frontendPort !== requestedFrontendPort) {
1167
- console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${frontendPort}.`);
1168
- }
1169
1150
  await this.runForeground({
1170
1151
  uiOverrides,
1171
- frontend: shouldStartFrontend,
1172
- frontendPort,
1173
1152
  open: Boolean(opts.open)
1174
1153
  });
1175
1154
  }
@@ -1428,7 +1407,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1428
1407
  }
1429
1408
  console.log(JSON.stringify(result.value ?? null, null, 2));
1430
1409
  }
1431
- configSet(pathExpr, value, opts = {}) {
1410
+ async configSet(pathExpr, value, opts = {}) {
1432
1411
  let parsedPath;
1433
1412
  try {
1434
1413
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -1454,9 +1433,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1454
1433
  return;
1455
1434
  }
1456
1435
  saveConfig(config2);
1457
- 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
+ });
1458
1440
  }
1459
- configUnset(pathExpr) {
1441
+ async configUnset(pathExpr) {
1460
1442
  let parsedPath;
1461
1443
  try {
1462
1444
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -1473,19 +1455,28 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1473
1455
  return;
1474
1456
  }
1475
1457
  saveConfig(config2);
1476
- 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
+ });
1477
1462
  }
1478
- pluginsEnable(id) {
1463
+ async pluginsEnable(id) {
1479
1464
  const config2 = loadConfig();
1480
1465
  const next = enablePluginInConfig(config2, id);
1481
1466
  saveConfig(next);
1482
- 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
+ });
1483
1471
  }
1484
- pluginsDisable(id) {
1472
+ async pluginsDisable(id) {
1485
1473
  const config2 = loadConfig();
1486
1474
  const next = disablePluginInConfig(config2, id);
1487
1475
  saveConfig(next);
1488
- 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
+ });
1489
1480
  }
1490
1481
  async pluginsUninstall(id, opts = {}) {
1491
1482
  const config2 = loadConfig();
@@ -1582,7 +1573,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1582
1573
  removed.push("directory");
1583
1574
  }
1584
1575
  console.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`);
1585
- 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
+ });
1586
1580
  }
1587
1581
  async pluginsInstall(pathOrSpec, opts = {}) {
1588
1582
  const fileSpec = this.resolveFileNpmSpecToLocalPath(pathOrSpec);
@@ -1611,7 +1605,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1611
1605
  });
1612
1606
  saveConfig(next3);
1613
1607
  console.log(`Linked plugin path: ${resolved}`);
1614
- 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
+ });
1615
1612
  return;
1616
1613
  }
1617
1614
  const result2 = await installPluginFromPath({
@@ -1635,7 +1632,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1635
1632
  });
1636
1633
  saveConfig(next2);
1637
1634
  console.log(`Installed plugin: ${result2.pluginId}`);
1638
- 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
+ });
1639
1639
  return;
1640
1640
  }
1641
1641
  if (opts.link) {
@@ -1667,7 +1667,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1667
1667
  });
1668
1668
  saveConfig(next);
1669
1669
  console.log(`Installed plugin: ${result.pluginId}`);
1670
- 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
+ });
1671
1674
  }
1672
1675
  pluginsDoctor() {
1673
1676
  const config2 = loadConfig();
@@ -1757,7 +1760,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1757
1760
  console.error(`Bridge failed: ${result.status ?? 1}`);
1758
1761
  }
1759
1762
  }
1760
- channelsAdd(opts) {
1763
+ async channelsAdd(opts) {
1761
1764
  const channelId = opts.channel?.trim();
1762
1765
  if (!channelId) {
1763
1766
  console.error("--channel is required");
@@ -1808,7 +1811,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1808
1811
  next = enablePluginInConfig(next, binding.pluginId);
1809
1812
  saveConfig(next);
1810
1813
  console.log(`Configured channel "${binding.channelId}" via plugin "${binding.pluginId}".`);
1811
- 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
+ });
1812
1818
  }
1813
1819
  toPluginConfigView(config2, bindings) {
1814
1820
  const view = JSON.parse(JSON.stringify(config2));
@@ -2032,7 +2038,11 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2032
2038
  loadConfig,
2033
2039
  getExtensionChannels: () => extensionRegistry.channels,
2034
2040
  onRestartRequired: (paths) => {
2035
- 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
+ });
2036
2046
  }
2037
2047
  });
2038
2048
  const gatewayController = new GatewayControllerImpl({
@@ -2040,7 +2050,16 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2040
2050
  cron: cron2,
2041
2051
  getConfigPath,
2042
2052
  saveConfig,
2043
- 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
+ }
2044
2063
  });
2045
2064
  const agent = new AgentLoop({
2046
2065
  bus,
@@ -2213,34 +2232,14 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2213
2232
  async runForeground(options) {
2214
2233
  const config2 = loadConfig();
2215
2234
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
2216
- const shouldStartFrontend = options.frontend;
2217
- const frontendPort = Number.isFinite(options.frontendPort) ? options.frontendPort : 5173;
2218
- const frontendDir = shouldStartFrontend ? resolveUiFrontendDir() : null;
2219
- const staticDir = resolveUiStaticDir();
2220
- let frontendUrl = null;
2221
- if (shouldStartFrontend && frontendDir) {
2222
- const frontend = startUiFrontend({
2223
- apiBase: resolveUiApiBase(uiConfig.host, uiConfig.port),
2224
- port: frontendPort,
2225
- dir: frontendDir
2226
- });
2227
- frontendUrl = frontend?.url ?? null;
2228
- } else if (shouldStartFrontend && !frontendDir) {
2229
- console.log("Warning: UI frontend not found. Start it separately.");
2230
- }
2231
- if (!frontendUrl && staticDir) {
2232
- frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2233
- }
2234
- if (options.open && frontendUrl) {
2235
- openBrowser(frontendUrl);
2236
- } else if (options.open && !frontendUrl) {
2237
- 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);
2238
2238
  }
2239
- const uiStaticDir = shouldStartFrontend && frontendDir ? null : staticDir;
2240
2239
  await this.startGateway({
2241
2240
  uiOverrides: options.uiOverrides,
2242
2241
  allowMissingProvider: true,
2243
- uiStaticDir
2242
+ uiStaticDir: resolveUiStaticDir()
2244
2243
  });
2245
2244
  }
2246
2245
  async startService(options) {
@@ -2283,8 +2282,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2283
2282
  if (existing) {
2284
2283
  clearServiceState();
2285
2284
  }
2286
- if (!staticDir && !options.frontend) {
2287
- 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.");
2288
2287
  }
2289
2288
  const logPath = resolveServiceLogPath();
2290
2289
  const logDir = resolve4(logPath, "..");
@@ -2292,9 +2291,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2292
2291
  const logFd = openSync(logPath, "a");
2293
2292
  const serveArgs = buildServeArgs({
2294
2293
  uiHost: uiConfig.host,
2295
- uiPort: uiConfig.port,
2296
- frontend: options.frontend,
2297
- frontendPort: options.frontendPort
2294
+ uiPort: uiConfig.port
2298
2295
  });
2299
2296
  const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
2300
2297
  env: process.env,
@@ -2597,9 +2594,9 @@ program.command("onboard").description(`Initialize ${APP_NAME2} configuration an
2597
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) }));
2598
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));
2599
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));
2600
- 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));
2601
- 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));
2602
- 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));
2603
2600
  program.command("stop").description(`Stop the ${APP_NAME2} background service`).action(async () => runtime.stop());
2604
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));
2605
2602
  program.command("update").description(`Update ${APP_NAME2}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",