nextclaw 0.4.16 → 0.5.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +246 -244
  2. package/package.json +4 -4
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";
@@ -663,6 +627,7 @@ function buildClawHubArgs(slug, options) {
663
627
  // src/cli/runtime.ts
664
628
  var LOGO = "\u{1F916}";
665
629
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
630
+ var FORCED_PUBLIC_UI_HOST = "0.0.0.0";
666
631
  function isIndexSegment(raw) {
667
632
  return /^[0-9]+$/.test(raw);
668
633
  }
@@ -986,12 +951,75 @@ var ConfigReloader = class {
986
951
  };
987
952
  var CliRuntime = class {
988
953
  logo;
954
+ restartCoordinator;
955
+ serviceRestartTask = null;
989
956
  constructor(options = {}) {
990
957
  this.logo = options.logo ?? LOGO;
958
+ this.restartCoordinator = new RestartCoordinator({
959
+ readServiceState,
960
+ isProcessRunning,
961
+ currentPid: () => process.pid,
962
+ restartBackgroundService: async (reason) => this.restartBackgroundService(reason),
963
+ scheduleProcessExit: (delayMs, reason) => this.scheduleProcessExit(delayMs, reason)
964
+ });
991
965
  }
992
966
  get version() {
993
967
  return getPackageVersion();
994
968
  }
969
+ scheduleProcessExit(delayMs, reason) {
970
+ console.warn(`Gateway restart requested (${reason}).`);
971
+ setTimeout(() => {
972
+ process.exit(0);
973
+ }, delayMs);
974
+ }
975
+ async restartBackgroundService(reason) {
976
+ if (this.serviceRestartTask) {
977
+ return this.serviceRestartTask;
978
+ }
979
+ this.serviceRestartTask = (async () => {
980
+ const state = readServiceState();
981
+ if (!state || !isProcessRunning(state.pid) || state.pid === process.pid) {
982
+ return false;
983
+ }
984
+ const uiHost = FORCED_PUBLIC_UI_HOST;
985
+ const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
986
+ console.log(`Applying changes (${reason}): restarting ${APP_NAME} background service...`);
987
+ await this.stopService();
988
+ await this.startService({
989
+ uiOverrides: {
990
+ enabled: true,
991
+ host: uiHost,
992
+ port: uiPort
993
+ },
994
+ open: false
995
+ });
996
+ return true;
997
+ })();
998
+ try {
999
+ return await this.serviceRestartTask;
1000
+ } finally {
1001
+ this.serviceRestartTask = null;
1002
+ }
1003
+ }
1004
+ async requestRestart(params) {
1005
+ const result = await this.restartCoordinator.requestRestart({
1006
+ reason: params.reason,
1007
+ strategy: params.strategy,
1008
+ delayMs: params.delayMs,
1009
+ manualMessage: params.manualMessage
1010
+ });
1011
+ if (result.status === "manual-required" || result.status === "restart-in-progress") {
1012
+ console.log(result.message);
1013
+ return;
1014
+ }
1015
+ if (result.status === "service-restarted") {
1016
+ if (!params.silentOnServiceRestart) {
1017
+ console.log(result.message);
1018
+ }
1019
+ return;
1020
+ }
1021
+ console.warn(result.message);
1022
+ }
995
1023
  async onboard() {
996
1024
  console.warn(`Warning: ${APP_NAME} onboard is deprecated. Use "${APP_NAME} init" instead.`);
997
1025
  await this.init({ source: "onboard" });
@@ -1036,87 +1064,43 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1036
1064
  }
1037
1065
  }
1038
1066
  async gateway(opts) {
1039
- const uiOverrides = {};
1067
+ const uiOverrides = {
1068
+ host: FORCED_PUBLIC_UI_HOST
1069
+ };
1040
1070
  if (opts.ui) {
1041
1071
  uiOverrides.enabled = true;
1042
1072
  }
1043
- if (opts.uiHost) {
1044
- uiOverrides.host = String(opts.uiHost);
1045
- }
1046
1073
  if (opts.uiPort) {
1047
1074
  uiOverrides.port = Number(opts.uiPort);
1048
1075
  }
1049
1076
  if (opts.uiOpen) {
1050
1077
  uiOverrides.open = true;
1051
1078
  }
1052
- if (opts.public) {
1053
- uiOverrides.enabled = true;
1054
- if (!opts.uiHost) {
1055
- uiOverrides.host = "0.0.0.0";
1056
- }
1057
- }
1058
1079
  await this.startGateway({ uiOverrides });
1059
1080
  }
1060
1081
  async ui(opts) {
1061
1082
  const uiOverrides = {
1062
1083
  enabled: true,
1084
+ host: FORCED_PUBLIC_UI_HOST,
1063
1085
  open: Boolean(opts.open)
1064
1086
  };
1065
- if (opts.host) {
1066
- uiOverrides.host = String(opts.host);
1067
- }
1068
1087
  if (opts.port) {
1069
1088
  uiOverrides.port = Number(opts.port);
1070
1089
  }
1071
- if (opts.public && !opts.host) {
1072
- uiOverrides.host = "0.0.0.0";
1073
- }
1074
1090
  await this.startGateway({ uiOverrides, allowMissingProvider: true });
1075
1091
  }
1076
1092
  async start(opts) {
1077
1093
  await this.init({ source: "start", auto: true });
1078
1094
  const uiOverrides = {
1079
1095
  enabled: true,
1096
+ host: FORCED_PUBLIC_UI_HOST,
1080
1097
  open: false
1081
1098
  };
1082
- if (opts.uiHost) {
1083
- uiOverrides.host = String(opts.uiHost);
1084
- }
1085
1099
  if (opts.uiPort) {
1086
1100
  uiOverrides.port = Number(opts.uiPort);
1087
1101
  }
1088
- if (opts.public && !opts.uiHost) {
1089
- uiOverrides.host = "0.0.0.0";
1090
- }
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
1102
  await this.startService({
1117
1103
  uiOverrides,
1118
- frontend: Boolean(opts.frontend),
1119
- frontendPort: Number(opts.frontendPort),
1120
1104
  open: Boolean(opts.open)
1121
1105
  });
1122
1106
  }
@@ -1136,40 +1120,14 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1136
1120
  async serve(opts) {
1137
1121
  const uiOverrides = {
1138
1122
  enabled: true,
1123
+ host: FORCED_PUBLIC_UI_HOST,
1139
1124
  open: false
1140
1125
  };
1141
- if (opts.uiHost) {
1142
- uiOverrides.host = String(opts.uiHost);
1143
- }
1144
1126
  if (opts.uiPort) {
1145
1127
  uiOverrides.port = Number(opts.uiPort);
1146
1128
  }
1147
- if (opts.public && !opts.uiHost) {
1148
- uiOverrides.host = "0.0.0.0";
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
1129
  await this.runForeground({
1170
1130
  uiOverrides,
1171
- frontend: shouldStartFrontend,
1172
- frontendPort,
1173
1131
  open: Boolean(opts.open)
1174
1132
  });
1175
1133
  }
@@ -1189,6 +1147,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1189
1147
  bus,
1190
1148
  providerManager,
1191
1149
  workspace,
1150
+ model: config2.agents.defaults.model,
1151
+ maxIterations: config2.agents.defaults.maxToolIterations,
1152
+ maxTokens: config2.agents.defaults.maxTokens,
1153
+ temperature: config2.agents.defaults.temperature,
1192
1154
  braveApiKey: config2.tools.web.search.apiKey || void 0,
1193
1155
  execConfig: config2.tools.exec,
1194
1156
  restrictToWorkspace: config2.tools.restrictToWorkspace,
@@ -1428,7 +1390,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1428
1390
  }
1429
1391
  console.log(JSON.stringify(result.value ?? null, null, 2));
1430
1392
  }
1431
- configSet(pathExpr, value, opts = {}) {
1393
+ async configSet(pathExpr, value, opts = {}) {
1432
1394
  let parsedPath;
1433
1395
  try {
1434
1396
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -1445,18 +1407,24 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1445
1407
  process.exit(1);
1446
1408
  return;
1447
1409
  }
1448
- const config2 = loadConfig();
1410
+ const prevConfig = loadConfig();
1411
+ const nextConfig = structuredClone(prevConfig);
1449
1412
  try {
1450
- setAtConfigPath(config2, parsedPath, parsedValue);
1413
+ setAtConfigPath(nextConfig, parsedPath, parsedValue);
1451
1414
  } catch (error) {
1452
1415
  console.error(String(error));
1453
1416
  process.exit(1);
1454
1417
  return;
1455
1418
  }
1456
- saveConfig(config2);
1457
- console.log(`Updated ${pathExpr}. Restart the gateway to apply.`);
1419
+ saveConfig(nextConfig);
1420
+ await this.requestRestartForConfigDiff({
1421
+ prevConfig,
1422
+ nextConfig,
1423
+ reason: `config.set ${pathExpr}`,
1424
+ manualMessage: `Updated ${pathExpr}. Restart the gateway to apply.`
1425
+ });
1458
1426
  }
1459
- configUnset(pathExpr) {
1427
+ async configUnset(pathExpr) {
1460
1428
  let parsedPath;
1461
1429
  try {
1462
1430
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -1465,27 +1433,53 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1465
1433
  process.exit(1);
1466
1434
  return;
1467
1435
  }
1468
- const config2 = loadConfig();
1469
- const removed = unsetAtConfigPath(config2, parsedPath);
1436
+ const prevConfig = loadConfig();
1437
+ const nextConfig = structuredClone(prevConfig);
1438
+ const removed = unsetAtConfigPath(nextConfig, parsedPath);
1470
1439
  if (!removed) {
1471
1440
  console.error(`Config path not found: ${pathExpr}`);
1472
1441
  process.exit(1);
1473
1442
  return;
1474
1443
  }
1475
- saveConfig(config2);
1476
- console.log(`Removed ${pathExpr}. Restart the gateway to apply.`);
1444
+ saveConfig(nextConfig);
1445
+ await this.requestRestartForConfigDiff({
1446
+ prevConfig,
1447
+ nextConfig,
1448
+ reason: `config.unset ${pathExpr}`,
1449
+ manualMessage: `Removed ${pathExpr}. Restart the gateway to apply.`
1450
+ });
1477
1451
  }
1478
- pluginsEnable(id) {
1452
+ async requestRestartForConfigDiff(params) {
1453
+ const changedPaths = diffConfigPaths(params.prevConfig, params.nextConfig);
1454
+ if (!changedPaths.length) {
1455
+ return;
1456
+ }
1457
+ const plan = buildReloadPlan(changedPaths);
1458
+ if (plan.restartRequired.length === 0) {
1459
+ return;
1460
+ }
1461
+ await this.requestRestart({
1462
+ reason: `${params.reason} (${plan.restartRequired.join(", ")})`,
1463
+ manualMessage: params.manualMessage
1464
+ });
1465
+ }
1466
+ async pluginsEnable(id) {
1479
1467
  const config2 = loadConfig();
1480
1468
  const next = enablePluginInConfig(config2, id);
1481
1469
  saveConfig(next);
1482
- console.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
1470
+ await this.requestRestart({
1471
+ reason: `plugin enabled: ${id}`,
1472
+ manualMessage: `Enabled plugin "${id}". Restart the gateway to apply.`
1473
+ });
1483
1474
  }
1484
- pluginsDisable(id) {
1475
+ async pluginsDisable(id) {
1485
1476
  const config2 = loadConfig();
1486
1477
  const next = disablePluginInConfig(config2, id);
1487
1478
  saveConfig(next);
1488
- console.log(`Disabled plugin "${id}". Restart the gateway to apply.`);
1479
+ await this.requestRestart({
1480
+ reason: `plugin disabled: ${id}`,
1481
+ manualMessage: `Disabled plugin "${id}". Restart the gateway to apply.`
1482
+ });
1489
1483
  }
1490
1484
  async pluginsUninstall(id, opts = {}) {
1491
1485
  const config2 = loadConfig();
@@ -1582,7 +1576,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1582
1576
  removed.push("directory");
1583
1577
  }
1584
1578
  console.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`);
1585
- console.log("Restart the gateway to apply changes.");
1579
+ await this.requestRestart({
1580
+ reason: `plugin uninstalled: ${pluginId}`,
1581
+ manualMessage: "Restart the gateway to apply changes."
1582
+ });
1586
1583
  }
1587
1584
  async pluginsInstall(pathOrSpec, opts = {}) {
1588
1585
  const fileSpec = this.resolveFileNpmSpecToLocalPath(pathOrSpec);
@@ -1611,7 +1608,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1611
1608
  });
1612
1609
  saveConfig(next3);
1613
1610
  console.log(`Linked plugin path: ${resolved}`);
1614
- console.log("Restart the gateway to load plugins.");
1611
+ await this.requestRestart({
1612
+ reason: `plugin linked: ${probe.pluginId}`,
1613
+ manualMessage: "Restart the gateway to load plugins."
1614
+ });
1615
1615
  return;
1616
1616
  }
1617
1617
  const result2 = await installPluginFromPath({
@@ -1635,7 +1635,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1635
1635
  });
1636
1636
  saveConfig(next2);
1637
1637
  console.log(`Installed plugin: ${result2.pluginId}`);
1638
- console.log("Restart the gateway to load plugins.");
1638
+ await this.requestRestart({
1639
+ reason: `plugin installed: ${result2.pluginId}`,
1640
+ manualMessage: "Restart the gateway to load plugins."
1641
+ });
1639
1642
  return;
1640
1643
  }
1641
1644
  if (opts.link) {
@@ -1667,7 +1670,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1667
1670
  });
1668
1671
  saveConfig(next);
1669
1672
  console.log(`Installed plugin: ${result.pluginId}`);
1670
- console.log("Restart the gateway to load plugins.");
1673
+ await this.requestRestart({
1674
+ reason: `plugin installed: ${result.pluginId}`,
1675
+ manualMessage: "Restart the gateway to load plugins."
1676
+ });
1671
1677
  }
1672
1678
  pluginsDoctor() {
1673
1679
  const config2 = loadConfig();
@@ -1757,7 +1763,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1757
1763
  console.error(`Bridge failed: ${result.status ?? 1}`);
1758
1764
  }
1759
1765
  }
1760
- channelsAdd(opts) {
1766
+ async channelsAdd(opts) {
1761
1767
  const channelId = opts.channel?.trim();
1762
1768
  if (!channelId) {
1763
1769
  console.error("--channel is required");
@@ -1808,7 +1814,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1808
1814
  next = enablePluginInConfig(next, binding.pluginId);
1809
1815
  saveConfig(next);
1810
1816
  console.log(`Configured channel "${binding.channelId}" via plugin "${binding.pluginId}".`);
1811
- console.log("Restart the gateway to apply changes.");
1817
+ await this.requestRestart({
1818
+ reason: `channel configured via plugin: ${binding.pluginId}`,
1819
+ manualMessage: "Restart the gateway to apply changes."
1820
+ });
1812
1821
  }
1813
1822
  toPluginConfigView(config2, bindings) {
1814
1823
  const view = JSON.parse(JSON.stringify(config2));
@@ -2032,7 +2041,11 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2032
2041
  loadConfig,
2033
2042
  getExtensionChannels: () => extensionRegistry.channels,
2034
2043
  onRestartRequired: (paths) => {
2035
- console.warn(`Config changes require restart: ${paths.join(", ")}`);
2044
+ void this.requestRestart({
2045
+ reason: `config reload requires restart: ${paths.join(", ")}`,
2046
+ manualMessage: `Config changes require restart: ${paths.join(", ")}`,
2047
+ strategy: "background-service-or-manual"
2048
+ });
2036
2049
  }
2037
2050
  });
2038
2051
  const gatewayController = new GatewayControllerImpl({
@@ -2040,7 +2053,16 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2040
2053
  cron: cron2,
2041
2054
  getConfigPath,
2042
2055
  saveConfig,
2043
- getPluginUiMetadata: () => pluginUiMetadata
2056
+ getPluginUiMetadata: () => pluginUiMetadata,
2057
+ requestRestart: async (options2) => {
2058
+ await this.requestRestart({
2059
+ reason: options2?.reason ?? "gateway tool restart",
2060
+ manualMessage: "Restart the gateway to apply changes.",
2061
+ strategy: "background-service-or-exit",
2062
+ delayMs: options2?.delayMs,
2063
+ silentOnServiceRestart: true
2064
+ });
2065
+ }
2044
2066
  });
2045
2067
  const agent = new AgentLoop({
2046
2068
  bus,
@@ -2048,6 +2070,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2048
2070
  workspace,
2049
2071
  model: config2.agents.defaults.model,
2050
2072
  maxIterations: config2.agents.defaults.maxToolIterations,
2073
+ maxTokens: config2.agents.defaults.maxTokens,
2074
+ temperature: config2.agents.defaults.temperature,
2051
2075
  braveApiKey: config2.tools.web.search.apiKey || void 0,
2052
2076
  execConfig: config2.tools.exec,
2053
2077
  cronService: cron2,
@@ -2178,7 +2202,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2178
2202
  }
2179
2203
  async printPublicUiUrls(host, port) {
2180
2204
  if (isLoopbackHost(host)) {
2181
- console.log('Public URL: disabled (UI host is loopback). Use "--public" or "--ui-host 0.0.0.0" to expose it.');
2205
+ console.log("Public URL: disabled (UI host is loopback). Current release expects public exposure; run nextclaw restart.");
2182
2206
  return;
2183
2207
  }
2184
2208
  const publicIp = await resolvePublicIp();
@@ -2213,34 +2237,14 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2213
2237
  async runForeground(options) {
2214
2238
  const config2 = loadConfig();
2215
2239
  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.");
2240
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2241
+ if (options.open) {
2242
+ openBrowser(uiUrl);
2238
2243
  }
2239
- const uiStaticDir = shouldStartFrontend && frontendDir ? null : staticDir;
2240
2244
  await this.startGateway({
2241
2245
  uiOverrides: options.uiOverrides,
2242
2246
  allowMissingProvider: true,
2243
- uiStaticDir
2247
+ uiStaticDir: resolveUiStaticDir()
2244
2248
  });
2245
2249
  }
2246
2250
  async startService(options) {
@@ -2274,7 +2278,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2274
2278
  console.log(
2275
2279
  `Note: requested UI bind (${uiConfig.host}:${uiConfig.port}) differs from running service (${parsedUi.host}:${parsedUi.port}).`
2276
2280
  );
2277
- console.log(`Run: ${APP_NAME} restart${uiConfig.host === "0.0.0.0" ? " --public" : ""}`);
2281
+ console.log(`Run: ${APP_NAME} restart`);
2278
2282
  }
2279
2283
  console.log(`Logs: ${existing.logPath}`);
2280
2284
  console.log(`Stop: ${APP_NAME} stop`);
@@ -2283,8 +2287,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2283
2287
  if (existing) {
2284
2288
  clearServiceState();
2285
2289
  }
2286
- if (!staticDir && !options.frontend) {
2287
- console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
2290
+ if (!staticDir) {
2291
+ console.log("Warning: UI frontend not found in package assets.");
2288
2292
  }
2289
2293
  const logPath = resolveServiceLogPath();
2290
2294
  const logDir = resolve4(logPath, "..");
@@ -2292,9 +2296,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2292
2296
  const logFd = openSync(logPath, "a");
2293
2297
  const serveArgs = buildServeArgs({
2294
2298
  uiHost: uiConfig.host,
2295
- uiPort: uiConfig.port,
2296
- frontend: options.frontend,
2297
- frontendPort: options.frontendPort
2299
+ uiPort: uiConfig.port
2298
2300
  });
2299
2301
  const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
2300
2302
  env: process.env,
@@ -2595,11 +2597,11 @@ var runtime = new CliRuntime({ logo: LOGO });
2595
2597
  program.name(APP_NAME2).description(`${LOGO} ${APP_NAME2} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
2596
2598
  program.command("onboard").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.onboard());
2597
2599
  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
- 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
- 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));
2600
+ 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-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
2601
+ program.command("ui").description(`Start the ${APP_NAME2} UI with gateway`).option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
2602
+ program.command("start").description(`Start the ${APP_NAME2} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
2603
+ program.command("restart").description(`Restart the ${APP_NAME2} background service`).option("--ui-port <port>", "UI port").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
2604
+ program.command("serve").description(`Run the ${APP_NAME2} gateway + UI in the foreground`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
2603
2605
  program.command("stop").description(`Stop the ${APP_NAME2} background service`).action(async () => runtime.stop());
2604
2606
  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
2607
  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.5.0",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -38,9 +38,9 @@
38
38
  "dependencies": {
39
39
  "chokidar": "^3.6.0",
40
40
  "commander": "^12.1.0",
41
- "@nextclaw/core": "^0.4.14",
42
- "@nextclaw/server": "^0.3.5",
43
- "@nextclaw/openclaw-compat": "^0.1.2"
41
+ "@nextclaw/core": "^0.5.0",
42
+ "@nextclaw/server": "^0.3.6",
43
+ "@nextclaw/openclaw-compat": "^0.1.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^20.17.6",