nextclaw 0.4.14 → 0.4.16

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 } from "net";
85
+ import { createServer, 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) {
@@ -92,6 +93,41 @@ function resolveUiApiBase(host, port) {
92
93
  const normalizedHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
93
94
  return `http://${normalizedHost}:${port}`;
94
95
  }
96
+ function isLoopbackHost(host) {
97
+ const normalized = host.trim().toLowerCase();
98
+ return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
99
+ }
100
+ var PUBLIC_IP_CHECK_URLS = ["https://api.ipify.org", "https://ifconfig.me/ip"];
101
+ async function fetchPublicIpFrom(url, timeoutMs) {
102
+ const controller = new AbortController();
103
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
104
+ try {
105
+ const response = await fetch(url, {
106
+ signal: controller.signal,
107
+ headers: {
108
+ Accept: "text/plain"
109
+ }
110
+ });
111
+ if (!response.ok) {
112
+ return null;
113
+ }
114
+ const text = (await response.text()).trim();
115
+ return isIP(text) ? text : null;
116
+ } catch {
117
+ return null;
118
+ } finally {
119
+ clearTimeout(timer);
120
+ }
121
+ }
122
+ async function resolvePublicIp(timeoutMs = 1500) {
123
+ for (const endpoint of PUBLIC_IP_CHECK_URLS) {
124
+ const candidate = await fetchPublicIpFrom(endpoint, timeoutMs);
125
+ if (candidate) {
126
+ return candidate;
127
+ }
128
+ }
129
+ return null;
130
+ }
95
131
  function isDevRuntime() {
96
132
  return import.meta.url.includes("/src/cli/") || process.env.NEXTCLAW_DEV === "1";
97
133
  }
@@ -812,6 +848,21 @@ function unsetAtConfigPath(root, pathSegments) {
812
848
  delete record[last];
813
849
  return true;
814
850
  }
851
+ var MissingProvider = class extends LLMProvider {
852
+ constructor(defaultModel) {
853
+ super(null, null);
854
+ this.defaultModel = defaultModel;
855
+ }
856
+ setDefaultModel(model) {
857
+ this.defaultModel = model;
858
+ }
859
+ async chat() {
860
+ throw new Error("No API key configured yet. Configure provider credentials in UI and retry.");
861
+ }
862
+ getDefaultModel() {
863
+ return this.defaultModel;
864
+ }
865
+ };
815
866
  var ConfigReloader = class {
816
867
  constructor(options) {
817
868
  this.options = options;
@@ -828,6 +879,9 @@ var ConfigReloader = class {
828
879
  getChannels() {
829
880
  return this.channels;
830
881
  }
882
+ setApplyAgentRuntimeConfig(callback) {
883
+ this.options.applyAgentRuntimeConfig = callback;
884
+ }
831
885
  async applyReloadPlan(nextConfig) {
832
886
  const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
833
887
  if (!changedPaths.length) {
@@ -837,9 +891,15 @@ var ConfigReloader = class {
837
891
  const plan = buildReloadPlan(changedPaths);
838
892
  if (plan.restartChannels) {
839
893
  await this.reloadChannels(nextConfig);
894
+ console.log("Config reload: channels restarted.");
840
895
  }
841
896
  if (plan.reloadProviders) {
842
897
  await this.reloadProvider(nextConfig);
898
+ console.log("Config reload: provider settings applied.");
899
+ }
900
+ if (plan.reloadAgent) {
901
+ this.options.applyAgentRuntimeConfig?.(nextConfig);
902
+ console.log("Config reload: agent defaults applied.");
843
903
  }
844
904
  if (plan.restartRequired.length > 0) {
845
905
  this.options.onRestartRequired(plan.restartRequired);
@@ -989,6 +1049,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
989
1049
  if (opts.uiOpen) {
990
1050
  uiOverrides.open = true;
991
1051
  }
1052
+ if (opts.public) {
1053
+ uiOverrides.enabled = true;
1054
+ if (!opts.uiHost) {
1055
+ uiOverrides.host = "0.0.0.0";
1056
+ }
1057
+ }
992
1058
  await this.startGateway({ uiOverrides });
993
1059
  }
994
1060
  async ui(opts) {
@@ -1002,6 +1068,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1002
1068
  if (opts.port) {
1003
1069
  uiOverrides.port = Number(opts.port);
1004
1070
  }
1071
+ if (opts.public && !opts.host) {
1072
+ uiOverrides.host = "0.0.0.0";
1073
+ }
1005
1074
  await this.startGateway({ uiOverrides, allowMissingProvider: true });
1006
1075
  }
1007
1076
  async start(opts) {
@@ -1016,6 +1085,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1016
1085
  if (opts.uiPort) {
1017
1086
  uiOverrides.port = Number(opts.uiPort);
1018
1087
  }
1088
+ if (opts.public && !opts.uiHost) {
1089
+ uiOverrides.host = "0.0.0.0";
1090
+ }
1019
1091
  const devMode = isDevRuntime();
1020
1092
  if (devMode) {
1021
1093
  const requestedUiPort = Number.isFinite(Number(opts.uiPort)) ? Number(opts.uiPort) : 18792;
@@ -1072,6 +1144,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1072
1144
  if (opts.uiPort) {
1073
1145
  uiOverrides.port = Number(opts.uiPort);
1074
1146
  }
1147
+ if (opts.public && !opts.uiHost) {
1148
+ uiOverrides.host = "0.0.0.0";
1149
+ }
1075
1150
  const devMode = isDevRuntime();
1076
1151
  if (devMode && uiOverrides.port === void 0) {
1077
1152
  uiOverrides.port = 18792;
@@ -1936,7 +2011,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1936
2011
  this.logPluginDiagnostics(pluginRegistry);
1937
2012
  const bus = new MessageBus();
1938
2013
  const provider = options.allowMissingProvider === true ? this.makeProvider(config2, { allowMissing: true }) : this.makeProvider(config2);
1939
- const providerManager = provider ? new ProviderManager(provider) : null;
2014
+ const providerManager = new ProviderManager(provider ?? this.makeMissingProvider(config2));
1940
2015
  const sessionManager = new SessionManager(workspace);
1941
2016
  const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
1942
2017
  const cron2 = new CronService(cronStorePath);
@@ -1944,11 +2019,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1944
2019
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
1945
2020
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
1946
2021
  if (!provider) {
1947
- this.startUiIfEnabled(uiConfig, uiStaticDir);
1948
- console.log("Warning: No API key configured. UI server only.");
1949
- await new Promise(() => {
1950
- });
1951
- return;
2022
+ console.warn("Warning: No API key configured. The gateway is running, but agent replies are disabled until provider config is set.");
1952
2023
  }
1953
2024
  const channels2 = new ChannelManager(config2, bus, sessionManager, extensionRegistry.channels);
1954
2025
  const reloader = new ConfigReloader({
@@ -1957,7 +2028,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1957
2028
  bus,
1958
2029
  sessionManager,
1959
2030
  providerManager,
1960
- makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
2031
+ makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }) ?? this.makeMissingProvider(nextConfig),
1961
2032
  loadConfig,
1962
2033
  getExtensionChannels: () => extensionRegistry.channels,
1963
2034
  onRestartRequired: (paths) => {
@@ -1973,7 +2044,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1973
2044
  });
1974
2045
  const agent = new AgentLoop({
1975
2046
  bus,
1976
- providerManager: providerManager ?? new ProviderManager(provider),
2047
+ providerManager,
1977
2048
  workspace,
1978
2049
  model: config2.agents.defaults.model,
1979
2050
  maxIterations: config2.agents.defaults.maxToolIterations,
@@ -1993,6 +2064,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1993
2064
  accountId
1994
2065
  })
1995
2066
  });
2067
+ reloader.setApplyAgentRuntimeConfig((nextConfig) => agent.applyRuntimeConfig(nextConfig));
1996
2068
  const pluginChannelBindings = getPluginChannelBindings(pluginRegistry);
1997
2069
  setPluginRuntimeBridge({
1998
2070
  loadConfig: () => this.toPluginConfigView(loadConfig(), pluginChannelBindings),
@@ -2104,6 +2176,20 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2104
2176
  setPluginRuntimeBridge(null);
2105
2177
  }
2106
2178
  }
2179
+ async printPublicUiUrls(host, port) {
2180
+ 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.');
2182
+ return;
2183
+ }
2184
+ const publicIp = await resolvePublicIp();
2185
+ if (!publicIp) {
2186
+ console.log("Public URL: UI is exposed, but automatic public IP detection failed.");
2187
+ return;
2188
+ }
2189
+ const publicBase = `http://${publicIp}:${port}`;
2190
+ console.log(`Public UI (if firewall/NAT allows): ${publicBase}`);
2191
+ console.log(`Public API (if firewall/NAT allows): ${publicBase}/api`);
2192
+ }
2107
2193
  startUiIfEnabled(uiConfig, uiStaticDir) {
2108
2194
  if (!uiConfig.enabled) {
2109
2195
  return;
@@ -2119,6 +2205,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2119
2205
  if (uiStaticDir) {
2120
2206
  console.log(`\u2713 UI frontend: ${uiUrl}`);
2121
2207
  }
2208
+ void this.printPublicUiUrls(uiServer.host, uiServer.port);
2122
2209
  if (uiConfig.open) {
2123
2210
  openBrowser(uiUrl);
2124
2211
  }
@@ -2167,6 +2254,28 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2167
2254
  console.log(`\u2713 ${APP_NAME} is already running (PID ${existing.pid})`);
2168
2255
  console.log(`UI: ${existing.uiUrl}`);
2169
2256
  console.log(`API: ${existing.apiUrl}`);
2257
+ const parsedUi = (() => {
2258
+ try {
2259
+ const parsed = new URL(existing.uiUrl);
2260
+ const port = Number(parsed.port || 80);
2261
+ return {
2262
+ host: existing.uiHost ?? parsed.hostname,
2263
+ port: Number.isFinite(port) ? port : existing.uiPort ?? 18791
2264
+ };
2265
+ } catch {
2266
+ return {
2267
+ host: existing.uiHost ?? "127.0.0.1",
2268
+ port: existing.uiPort ?? 18791
2269
+ };
2270
+ }
2271
+ })();
2272
+ await this.printPublicUiUrls(parsedUi.host, parsedUi.port);
2273
+ if (parsedUi.host !== uiConfig.host || parsedUi.port !== uiConfig.port) {
2274
+ console.log(
2275
+ `Note: requested UI bind (${uiConfig.host}:${uiConfig.port}) differs from running service (${parsedUi.host}:${parsedUi.port}).`
2276
+ );
2277
+ console.log(`Run: ${APP_NAME} restart${uiConfig.host === "0.0.0.0" ? " --public" : ""}`);
2278
+ }
2170
2279
  console.log(`Logs: ${existing.logPath}`);
2171
2280
  console.log(`Stop: ${APP_NAME} stop`);
2172
2281
  return;
@@ -2203,12 +2312,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2203
2312
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2204
2313
  uiUrl,
2205
2314
  apiUrl,
2315
+ uiHost: uiConfig.host,
2316
+ uiPort: uiConfig.port,
2206
2317
  logPath
2207
2318
  };
2208
2319
  writeServiceState(state);
2209
2320
  console.log(`\u2713 ${APP_NAME} started in background (PID ${state.pid})`);
2210
2321
  console.log(`UI: ${uiUrl}`);
2211
2322
  console.log(`API: ${apiUrl}`);
2323
+ await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
2212
2324
  console.log(`Logs: ${logPath}`);
2213
2325
  console.log(`Stop: ${APP_NAME} stop`);
2214
2326
  if (options.open) {
@@ -2258,6 +2370,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2258
2370
  const normalized = answer.trim().toLowerCase();
2259
2371
  return normalized === "y" || normalized === "yes";
2260
2372
  }
2373
+ makeMissingProvider(config2) {
2374
+ return new MissingProvider(config2.agents.defaults.model);
2375
+ }
2261
2376
  makeProvider(config2, options) {
2262
2377
  const provider = getProvider(config2);
2263
2378
  const model = config2.agents.defaults.model;
@@ -2480,11 +2595,11 @@ var runtime = new CliRuntime({ logo: LOGO });
2480
2595
  program.name(APP_NAME2).description(`${LOGO} ${APP_NAME2} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
2481
2596
  program.command("onboard").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.onboard());
2482
2597
  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) }));
2483
- 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).action(async (opts) => runtime.gateway(opts));
2484
- 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").action(async (opts) => runtime.ui(opts));
2485
- 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).action(async (opts) => runtime.start(opts));
2486
- 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).action(async (opts) => runtime.restart(opts));
2487
- 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).action(async (opts) => runtime.serve(opts));
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));
2488
2603
  program.command("stop").description(`Stop the ${APP_NAME2} background service`).action(async () => runtime.stop());
2489
2604
  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));
2490
2605
  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.14",
3
+ "version": "0.4.16",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "dependencies": {
39
39
  "chokidar": "^3.6.0",
40
40
  "commander": "^12.1.0",
41
- "@nextclaw/core": "^0.4.10",
41
+ "@nextclaw/core": "^0.4.14",
42
42
  "@nextclaw/server": "^0.3.5",
43
43
  "@nextclaw/openclaw-compat": "^0.1.2"
44
44
  },