nextclaw 0.5.2 → 0.5.4

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
@@ -63,6 +63,7 @@ import {
63
63
  writeFileSync as writeFileSync2
64
64
  } from "fs";
65
65
  import { dirname, join as join3, resolve as resolve4 } from "path";
66
+ import { createServer as createNetServer } from "net";
66
67
  import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
67
68
  import { createInterface } from "readline";
68
69
  import { createRequire } from "module";
@@ -1925,26 +1926,321 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1925
1926
  const ok = await service.runJob(jobId, Boolean(opts.force));
1926
1927
  console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
1927
1928
  }
1928
- status() {
1929
+ async status(opts = {}) {
1930
+ const report = await this.collectRuntimeStatus({
1931
+ verbose: Boolean(opts.verbose),
1932
+ fix: Boolean(opts.fix)
1933
+ });
1934
+ if (opts.json) {
1935
+ console.log(JSON.stringify(report, null, 2));
1936
+ process.exitCode = report.exitCode;
1937
+ return;
1938
+ }
1939
+ console.log(`${this.logo} ${APP_NAME} Status`);
1940
+ console.log(`Level: ${report.level}`);
1941
+ console.log(`Generated: ${report.generatedAt}`);
1942
+ console.log("");
1943
+ const processLabel = report.process.running ? `running (PID ${report.process.pid})` : report.process.staleState ? "stale-state" : "stopped";
1944
+ console.log(`Process: ${processLabel}`);
1945
+ console.log(`State file: ${report.serviceStatePath} ${report.serviceStateExists ? "\u2713" : "\u2717"}`);
1946
+ if (report.process.startedAt) {
1947
+ console.log(`Started: ${report.process.startedAt}`);
1948
+ }
1949
+ console.log(`Managed health: ${report.health.managed.state} (${report.health.managed.detail})`);
1950
+ if (!report.process.running) {
1951
+ console.log(`Configured health: ${report.health.configured.state} (${report.health.configured.detail})`);
1952
+ }
1953
+ console.log(`UI: ${report.endpoints.uiUrl ?? report.endpoints.configuredUiUrl}`);
1954
+ console.log(`API: ${report.endpoints.apiUrl ?? report.endpoints.configuredApiUrl}`);
1955
+ console.log(`Config: ${report.configPath} ${report.configExists ? "\u2713" : "\u2717"}`);
1956
+ console.log(`Workspace: ${report.workspacePath} ${report.workspaceExists ? "\u2713" : "\u2717"}`);
1957
+ console.log(`Model: ${report.model}`);
1958
+ for (const provider of report.providers) {
1959
+ console.log(`${provider.name}: ${provider.configured ? "\u2713" : "not set"}${provider.detail ? ` (${provider.detail})` : ""}`);
1960
+ }
1961
+ if (report.fixActions.length > 0) {
1962
+ console.log("");
1963
+ console.log("Fix actions:");
1964
+ for (const action of report.fixActions) {
1965
+ console.log(`- ${action}`);
1966
+ }
1967
+ }
1968
+ if (report.issues.length > 0) {
1969
+ console.log("");
1970
+ console.log("Issues:");
1971
+ for (const issue of report.issues) {
1972
+ console.log(`- ${issue}`);
1973
+ }
1974
+ }
1975
+ if (report.recommendations.length > 0) {
1976
+ console.log("");
1977
+ console.log("Recommendations:");
1978
+ for (const recommendation of report.recommendations) {
1979
+ console.log(`- ${recommendation}`);
1980
+ }
1981
+ }
1982
+ if (opts.verbose && report.logTail.length > 0) {
1983
+ console.log("");
1984
+ console.log("Recent logs:");
1985
+ for (const line of report.logTail) {
1986
+ console.log(line);
1987
+ }
1988
+ }
1989
+ process.exitCode = report.exitCode;
1990
+ }
1991
+ async doctor(opts = {}) {
1992
+ const report = await this.collectRuntimeStatus({
1993
+ verbose: Boolean(opts.verbose),
1994
+ fix: Boolean(opts.fix)
1995
+ });
1996
+ const checkPort = await this.checkPortAvailability({
1997
+ host: report.process.running ? report.endpoints.uiUrl ? new URL(report.endpoints.uiUrl).hostname : "127.0.0.1" : "127.0.0.1",
1998
+ port: (() => {
1999
+ try {
2000
+ const base = report.process.running && report.endpoints.uiUrl ? report.endpoints.uiUrl : report.endpoints.configuredUiUrl;
2001
+ return Number(new URL(base).port || 80);
2002
+ } catch {
2003
+ return 18791;
2004
+ }
2005
+ })()
2006
+ });
2007
+ const providerConfigured = report.providers.some((provider) => provider.configured);
2008
+ const checks = [
2009
+ {
2010
+ name: "config-file",
2011
+ status: report.configExists ? "pass" : "fail",
2012
+ detail: report.configPath
2013
+ },
2014
+ {
2015
+ name: "workspace-dir",
2016
+ status: report.workspaceExists ? "pass" : "warn",
2017
+ detail: report.workspacePath
2018
+ },
2019
+ {
2020
+ name: "service-state",
2021
+ status: report.process.staleState ? "fail" : report.process.running ? "pass" : "warn",
2022
+ detail: report.process.running ? `PID ${report.process.pid}` : report.process.staleState ? "state exists but process is not running" : "service not running"
2023
+ },
2024
+ {
2025
+ name: "service-health",
2026
+ status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : report.health.configured.state === "ok" ? "warn" : "warn",
2027
+ detail: report.process.running ? `${report.health.managed.state}: ${report.health.managed.detail}` : `${report.health.configured.state}: ${report.health.configured.detail}`
2028
+ },
2029
+ {
2030
+ name: "ui-port-availability",
2031
+ status: report.process.running ? "pass" : checkPort.available ? "pass" : "fail",
2032
+ detail: report.process.running ? "managed by running service" : checkPort.available ? "available" : checkPort.detail
2033
+ },
2034
+ {
2035
+ name: "provider-config",
2036
+ status: providerConfigured ? "pass" : "warn",
2037
+ detail: providerConfigured ? "at least one provider configured" : "no provider api key configured"
2038
+ }
2039
+ ];
2040
+ const failed = checks.filter((check) => check.status === "fail");
2041
+ const warned = checks.filter((check) => check.status === "warn");
2042
+ const exitCode = failed.length > 0 ? 1 : warned.length > 0 ? 1 : 0;
2043
+ if (opts.json) {
2044
+ console.log(
2045
+ JSON.stringify(
2046
+ {
2047
+ generatedAt: report.generatedAt,
2048
+ checks,
2049
+ status: report,
2050
+ exitCode
2051
+ },
2052
+ null,
2053
+ 2
2054
+ )
2055
+ );
2056
+ process.exitCode = exitCode;
2057
+ return;
2058
+ }
2059
+ console.log(`${this.logo} ${APP_NAME} Doctor`);
2060
+ console.log(`Generated: ${report.generatedAt}`);
2061
+ console.log("");
2062
+ for (const check of checks) {
2063
+ const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "!" : "\u2717";
2064
+ console.log(`${icon} ${check.name}: ${check.detail}`);
2065
+ }
2066
+ if (report.recommendations.length > 0) {
2067
+ console.log("");
2068
+ console.log("Recommendations:");
2069
+ for (const recommendation of report.recommendations) {
2070
+ console.log(`- ${recommendation}`);
2071
+ }
2072
+ }
2073
+ if (opts.verbose && report.logTail.length > 0) {
2074
+ console.log("");
2075
+ console.log("Recent logs:");
2076
+ for (const line of report.logTail) {
2077
+ console.log(line);
2078
+ }
2079
+ }
2080
+ process.exitCode = exitCode;
2081
+ }
2082
+ async collectRuntimeStatus(params) {
1929
2083
  const configPath = getConfigPath();
1930
2084
  const config2 = loadConfig();
1931
- const workspace = getWorkspacePath(config2.agents.defaults.workspace);
1932
- console.log(`${this.logo} ${APP_NAME} Status
1933
- `);
1934
- console.log(`Config: ${configPath} ${existsSync4(configPath) ? "\u2713" : "\u2717"}`);
1935
- console.log(`Workspace: ${workspace} ${existsSync4(workspace) ? "\u2713" : "\u2717"}`);
1936
- console.log(`Model: ${config2.agents.defaults.model}`);
1937
- for (const spec of PROVIDERS) {
2085
+ const workspacePath = getWorkspacePath(config2.agents.defaults.workspace);
2086
+ const serviceStatePath = resolve4(getDataDir2(), "run", "service.json");
2087
+ const fixActions = [];
2088
+ let serviceState = readServiceState();
2089
+ if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
2090
+ clearServiceState();
2091
+ fixActions.push("Cleared stale service state file.");
2092
+ serviceState = readServiceState();
2093
+ }
2094
+ const managedByState = Boolean(serviceState);
2095
+ const running = Boolean(serviceState && isProcessRunning(serviceState.pid));
2096
+ const staleState = Boolean(serviceState && !running);
2097
+ const configuredUi = resolveUiConfig(config2, { enabled: true, host: config2.ui.host, port: config2.ui.port });
2098
+ const configuredUiUrl = resolveUiApiBase(configuredUi.host, configuredUi.port);
2099
+ const configuredApiUrl = `${configuredUiUrl}/api`;
2100
+ const managedUiUrl = serviceState?.uiUrl ?? null;
2101
+ const managedApiUrl = serviceState?.apiUrl ?? null;
2102
+ const managedHealth = running && managedApiUrl ? await this.probeApiHealth(`${managedApiUrl}/health`) : { state: "unreachable", detail: "service not running" };
2103
+ const configuredHealth = await this.probeApiHealth(`${configuredApiUrl}/health`, 900);
2104
+ const orphanSuspected = !running && configuredHealth.state === "ok";
2105
+ const providers = PROVIDERS.map((spec) => {
1938
2106
  const provider = config2.providers[spec.name];
1939
2107
  if (!provider) {
1940
- continue;
2108
+ return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
1941
2109
  }
1942
2110
  if (spec.isLocal) {
1943
- console.log(`${spec.displayName ?? spec.name}: ${provider.apiBase ? `\u2713 ${provider.apiBase}` : "not set"}`);
1944
- } else {
1945
- console.log(`${spec.displayName ?? spec.name}: ${provider.apiKey ? "\u2713" : "not set"}`);
2111
+ return {
2112
+ name: spec.displayName ?? spec.name,
2113
+ configured: Boolean(provider.apiBase),
2114
+ detail: provider.apiBase ? provider.apiBase : "apiBase not set"
2115
+ };
1946
2116
  }
2117
+ return {
2118
+ name: spec.displayName ?? spec.name,
2119
+ configured: Boolean(provider.apiKey),
2120
+ detail: provider.apiKey ? "apiKey set" : "apiKey not set"
2121
+ };
2122
+ });
2123
+ const issues = [];
2124
+ const recommendations = [];
2125
+ if (!existsSync4(configPath)) {
2126
+ issues.push("Config file is missing.");
2127
+ recommendations.push(`Run ${APP_NAME} init to create config files.`);
1947
2128
  }
2129
+ if (!existsSync4(workspacePath)) {
2130
+ issues.push("Workspace directory does not exist.");
2131
+ recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
2132
+ }
2133
+ if (staleState) {
2134
+ issues.push("Service state is stale (state exists but process is not running).");
2135
+ recommendations.push(`Run ${APP_NAME} status --fix to clean stale state.`);
2136
+ }
2137
+ if (running && managedHealth.state !== "ok") {
2138
+ issues.push(`Managed service health check failed: ${managedHealth.detail}`);
2139
+ recommendations.push(`Check logs at ${serviceState?.logPath ?? resolveServiceLogPath()}.`);
2140
+ }
2141
+ if (!running) {
2142
+ recommendations.push(`Run ${APP_NAME} start to launch the service.`);
2143
+ }
2144
+ if (orphanSuspected) {
2145
+ issues.push("A service appears healthy on configured API endpoint, but state is missing/stale.");
2146
+ recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
2147
+ }
2148
+ if (!providers.some((provider) => provider.configured)) {
2149
+ recommendations.push("Configure at least one provider API key in UI or config before expecting agent replies.");
2150
+ }
2151
+ const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveServiceLogPath(), 25) : [];
2152
+ const level = running ? managedHealth.state === "ok" ? issues.length > 0 ? "degraded" : "healthy" : "degraded" : "stopped";
2153
+ const exitCode = level === "healthy" ? 0 : level === "degraded" ? 1 : 2;
2154
+ return {
2155
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2156
+ configPath,
2157
+ configExists: existsSync4(configPath),
2158
+ workspacePath,
2159
+ workspaceExists: existsSync4(workspacePath),
2160
+ model: config2.agents.defaults.model,
2161
+ providers,
2162
+ serviceStatePath,
2163
+ serviceStateExists: existsSync4(serviceStatePath),
2164
+ fixActions,
2165
+ process: {
2166
+ managedByState,
2167
+ pid: serviceState?.pid ?? null,
2168
+ running,
2169
+ staleState,
2170
+ orphanSuspected,
2171
+ startedAt: serviceState?.startedAt ?? null
2172
+ },
2173
+ endpoints: {
2174
+ uiUrl: managedUiUrl,
2175
+ apiUrl: managedApiUrl,
2176
+ configuredUiUrl,
2177
+ configuredApiUrl
2178
+ },
2179
+ health: {
2180
+ managed: managedHealth,
2181
+ configured: configuredHealth
2182
+ },
2183
+ issues,
2184
+ recommendations,
2185
+ logTail,
2186
+ level,
2187
+ exitCode
2188
+ };
2189
+ }
2190
+ async probeApiHealth(url, timeoutMs = 1500) {
2191
+ const controller = new AbortController();
2192
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2193
+ try {
2194
+ const response = await fetch(url, {
2195
+ method: "GET",
2196
+ signal: controller.signal
2197
+ });
2198
+ if (!response.ok) {
2199
+ return { state: "invalid-response", detail: `HTTP ${response.status}` };
2200
+ }
2201
+ const payload = await response.json();
2202
+ if (payload?.ok === true && payload?.data?.status === "ok") {
2203
+ return { state: "ok", detail: "health endpoint returned ok", payload };
2204
+ }
2205
+ return { state: "invalid-response", detail: "unexpected health payload", payload };
2206
+ } catch (error) {
2207
+ return { state: "unreachable", detail: String(error) };
2208
+ } finally {
2209
+ clearTimeout(timer);
2210
+ }
2211
+ }
2212
+ readLogTail(path, maxLines = 25) {
2213
+ if (!existsSync4(path)) {
2214
+ return [];
2215
+ }
2216
+ try {
2217
+ const lines = readFileSync3(path, "utf-8").split(/\r?\n/).filter(Boolean);
2218
+ if (lines.length <= maxLines) {
2219
+ return lines;
2220
+ }
2221
+ return lines.slice(lines.length - maxLines);
2222
+ } catch {
2223
+ return [];
2224
+ }
2225
+ }
2226
+ async checkPortAvailability(params) {
2227
+ return await new Promise((resolve5) => {
2228
+ const server = createNetServer();
2229
+ server.once("error", (error) => {
2230
+ resolve5({
2231
+ available: false,
2232
+ detail: `bind failed on ${params.host}:${params.port} (${String(error)})`
2233
+ });
2234
+ });
2235
+ server.listen(params.port, params.host, () => {
2236
+ server.close(() => {
2237
+ resolve5({
2238
+ available: true,
2239
+ detail: `bind ok on ${params.host}:${params.port}`
2240
+ });
2241
+ });
2242
+ });
2243
+ });
1948
2244
  }
1949
2245
  loadPluginRegistry(config2, workspaceDir) {
1950
2246
  return loadOpenClawPlugins({
@@ -2490,6 +2786,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2490
2786
  { source: "USER.md", target: "USER.md" },
2491
2787
  { source: "IDENTITY.md", target: "IDENTITY.md" },
2492
2788
  { source: "TOOLS.md", target: "TOOLS.md" },
2789
+ { source: "USAGE.md", target: "USAGE.md" },
2493
2790
  { source: "BOOT.md", target: "BOOT.md" },
2494
2791
  { source: "BOOTSTRAP.md", target: "BOOTSTRAP.md" },
2495
2792
  { source: "HEARTBEAT.md", target: "HEARTBEAT.md" },
@@ -2534,10 +2831,6 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2534
2831
  return 0;
2535
2832
  }
2536
2833
  const force = Boolean(options.force);
2537
- const existing = readdirSync(targetDir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith("."));
2538
- if (!force && existing.length > 0) {
2539
- return 0;
2540
- }
2541
2834
  let seeded = 0;
2542
2835
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2543
2836
  if (!entry.isDirectory()) {
@@ -2685,5 +2978,6 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
2685
2978
  cron.command("remove <jobId>").action((jobId) => runtime.cronRemove(jobId));
2686
2979
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => runtime.cronEnable(jobId, opts));
2687
2980
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
2688
- program.command("status").description(`Show ${APP_NAME2} status`).action(() => runtime.status());
2981
+ program.command("status").description(`Show ${APP_NAME2} status`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.status(opts));
2982
+ program.command("doctor").description(`Run ${APP_NAME2} diagnostics`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.doctor(opts));
2689
2983
  program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
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.5.1",
41
+ "@nextclaw/core": "^0.5.2",
42
42
  "@nextclaw/server": "^0.3.7",
43
43
  "@nextclaw/openclaw-compat": "^0.1.3"
44
44
  },
@@ -57,7 +57,7 @@
57
57
  "scripts": {
58
58
  "dev": "tsx watch --tsconfig tsconfig.json src/cli/index.ts",
59
59
  "dev:build": "tsx src/cli/index.ts",
60
- "build": "tsup src/index.ts src/cli/index.ts --format esm --dts --out-dir dist && node scripts/copy-ui-dist.mjs",
60
+ "build": "node scripts/sync-usage-template.mjs && tsup src/index.ts src/cli/index.ts --format esm --dts --out-dir dist && node scripts/copy-ui-dist.mjs",
61
61
  "start": "node dist/cli.js",
62
62
  "lint": "eslint .",
63
63
  "tsc": "tsc -p tsconfig.json",
@@ -14,6 +14,7 @@ Before doing anything else:
14
14
  2. Read `USER.md` (who you are helping)
15
15
  3. Read `memory/YYYY-MM-DD.md` for today and yesterday
16
16
  4. If in the main session with your human, also read `MEMORY.md`
17
+ 5. For NextClaw self-management tasks (service/plugins/channels/config/cron), read `USAGE.md` first
17
18
 
18
19
  ## Memory
19
20
 
@@ -0,0 +1,616 @@
1
+ <!-- Generated by packages/nextclaw/scripts/sync-usage-template.mjs -->
2
+ <!-- Do not edit this file directly; edit docs/USAGE.md instead. -->
3
+ # NextClaw User Guide
4
+
5
+ This guide covers installation, configuration, channels, tools, automation, and troubleshooting for NextClaw.
6
+
7
+ ---
8
+
9
+ ## AI Self-Management Contract
10
+
11
+ When NextClaw AI needs to operate the product itself (status/doctor/plugins/channels/config/cron), follow these rules:
12
+
13
+ 1. **Read this guide first** (`USAGE.md`) before executing management commands.
14
+ 2. **Prefer machine-readable output** (`--json`) whenever available.
15
+ 3. **Close the loop after changes** with `nextclaw status --json` (and `nextclaw doctor --json` when needed).
16
+ 4. **Be explicit about restart semantics** (hot-apply, auto-restart, or manual restart required).
17
+ 5. **Never invent commands**; use documented commands or `nextclaw --help` / `nextclaw <subcommand> --help`.
18
+
19
+ ---
20
+
21
+ ## Table of contents
22
+
23
+ - [AI Self-Management Contract](#ai-self-management-contract)
24
+ - [Quick Start](#quick-start)
25
+ - [Configuration](#configuration)
26
+ - [Workspace](#workspace)
27
+ - [Commands](#commands)
28
+ - [Plugins (OpenClaw compatibility)](#plugins-openclaw-compatibility)
29
+ - [Channels](#channels)
30
+ - [Tools](#tools)
31
+ - [Cron & Heartbeat](#cron--heartbeat)
32
+ - [Troubleshooting](#troubleshooting)
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ 1. Install:
39
+
40
+ ```bash
41
+ npm i -g nextclaw
42
+ ```
43
+
44
+ 2. Start the service (gateway + config UI in the background):
45
+
46
+ ```bash
47
+ nextclaw start
48
+ ```
49
+
50
+ 3. Open **http://127.0.0.1:18791** in your browser. Set a provider (e.g. OpenRouter) and model in the UI.
51
+
52
+ 4. Optionally run `nextclaw init` to create a workspace with agent templates, or chat from the CLI:
53
+
54
+ ```bash
55
+ nextclaw agent -m "Hello!"
56
+ ```
57
+
58
+ 5. Stop the service when done:
59
+
60
+ ```bash
61
+ nextclaw stop
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Configuration
67
+
68
+ - **Config file:** `~/.nextclaw/config.json`
69
+ - **Data directory:** Override with `NEXTCLAW_HOME=/path/to/dir` (config path becomes `$NEXTCLAW_HOME/config.json`).
70
+
71
+ ### Minimal config
72
+
73
+ ```json
74
+ {
75
+ "providers": {
76
+ "openrouter": { "apiKey": "sk-or-v1-xxx" }
77
+ },
78
+ "agents": {
79
+ "defaults": { "model": "minimax/MiniMax-M2.5" }
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Provider examples
85
+
86
+ **OpenRouter (recommended)**
87
+
88
+ ```json
89
+ {
90
+ "providers": { "openrouter": { "apiKey": "sk-or-v1-xxx" } },
91
+ "agents": { "defaults": { "model": "minimax/MiniMax-M2.5" } }
92
+ }
93
+ ```
94
+
95
+ **MiniMax (Mainland China)**
96
+
97
+ ```json
98
+ {
99
+ "providers": {
100
+ "minimax": {
101
+ "apiKey": "sk-api-xxx",
102
+ "apiBase": "https://api.minimaxi.com/v1"
103
+ }
104
+ },
105
+ "agents": { "defaults": { "model": "minimax/MiniMax-M2.5" } }
106
+ }
107
+ ```
108
+
109
+ **Local vLLM (or any OpenAI-compatible server)**
110
+
111
+ ```json
112
+ {
113
+ "providers": {
114
+ "vllm": {
115
+ "apiKey": "dummy",
116
+ "apiBase": "http://localhost:8000/v1"
117
+ }
118
+ },
119
+ "agents": { "defaults": { "model": "meta-llama/Llama-3.1-8B-Instruct" } }
120
+ }
121
+ ```
122
+
123
+ Supported providers include OpenRouter, OpenAI, Anthropic, MiniMax, Moonshot, Gemini, DeepSeek, DashScope, Zhipu, Groq, vLLM, and AiHubMix. You can configure them in the UI or by editing `config.json`.
124
+
125
+ ### Runtime config apply behavior (no restart)
126
+
127
+ When the gateway is already running, config changes from the UI or `nextclaw config set` are hot-applied for these paths:
128
+
129
+ - `providers.*`
130
+ - `channels.*`
131
+ - `agents.defaults.model`
132
+ - `agents.defaults.maxToolIterations`
133
+ - `agents.defaults.maxTokens`
134
+ - `agents.defaults.temperature`
135
+ - `agents.context.*`
136
+ - `tools.*`
137
+
138
+ Restart is still required for:
139
+
140
+ - `plugins.*`
141
+ - UI bind port (`--port` / `--ui-port`)
142
+
143
+ To confirm hot reload succeeded, check gateway console logs or `${NEXTCLAW_HOME:-~/.nextclaw}/logs/service.log` for messages like `Config reload: ... applied.`
144
+
145
+ ---
146
+
147
+ ## Workspace
148
+
149
+ - **Default path:** `~/.nextclaw/workspace`
150
+ - Override in config:
151
+
152
+ ```json
153
+ {
154
+ "agents": { "defaults": { "workspace": "~/my-nextclaw" } }
155
+ }
156
+ ```
157
+
158
+ Initialize the workspace (creates template files if missing):
159
+
160
+ ```bash
161
+ nextclaw init
162
+ ```
163
+
164
+ Use `nextclaw init --force` to overwrite existing template files.
165
+
166
+ Created under the workspace:
167
+
168
+ | File / folder | Purpose |
169
+ |-----------------|----------------------------------|
170
+ | `AGENTS.md` | System instructions for the agent |
171
+ | `SOUL.md` | Personality and values |
172
+ | `USER.md` | User profile hints |
173
+ | `IDENTITY.md` | Identity context |
174
+ | `TOOLS.md` | Tool usage guidelines |
175
+ | `USAGE.md` | CLI operation guide for users and AI |
176
+ | `BOOT.md` / `BOOTSTRAP.md` | Boot context |
177
+ | `HEARTBEAT.md` | Tasks checked periodically |
178
+ | `memory/MEMORY.md` | Long-term notes |
179
+ | `skills/` | Custom skills |
180
+
181
+ **Heartbeat:** When the gateway is running, `HEARTBEAT.md` in the workspace is checked every 30 minutes. If it contains actionable tasks, the agent will process them.
182
+
183
+ ---
184
+
185
+ ## Commands
186
+
187
+ | Command | Description |
188
+ |---------|-------------|
189
+ | `nextclaw start` | Start gateway + UI in the background |
190
+ | `nextclaw restart` | Restart the background service with optional start flags |
191
+ | `nextclaw stop` | Stop the background service |
192
+ | `nextclaw ui` | Start UI and gateway in the foreground |
193
+ | `nextclaw gateway` | Start gateway only (for channels) |
194
+ | `nextclaw serve` | Run gateway + UI in the foreground (no background) |
195
+ | `nextclaw agent -m "message"` | Send a one-off message to the agent |
196
+ | `nextclaw agent` | Interactive chat in the terminal |
197
+ | `nextclaw status` | Show runtime process/health/config status (`--json`, `--verbose`, `--fix`) |
198
+ | `nextclaw init` | Initialize workspace and template files |
199
+ | `nextclaw init --force` | Re-run init and overwrite templates |
200
+ | `nextclaw update` | Self-update the CLI |
201
+ | `nextclaw channels status` | Show enabled channels and status |
202
+ | `nextclaw doctor` | Run runtime diagnostics (`--json`, `--verbose`, `--fix`) |
203
+ | `nextclaw channels login` | Open QR login for supported channels |
204
+ | `nextclaw channels add --channel <id> [--code/--token/...]` | Run plugin channel setup (OpenClaw-compatible) and write config |
205
+ | `nextclaw cron list` | List scheduled jobs |
206
+ | `nextclaw cron add ...` | Add a cron job (see [Cron](#cron--heartbeat)) |
207
+ | `nextclaw cron remove <jobId>` | Remove a job |
208
+ | `nextclaw cron enable <jobId>` | Enable a job (use `--disable` to disable) |
209
+ | `nextclaw cron run <jobId>` | Run a job once (optionally with `--force` if disabled) |
210
+ | `nextclaw skills install <slug>` | Install a skill from ClawHub |
211
+ | `nextclaw clawhub install <slug>` | Same as `skills install` |
212
+ | `nextclaw plugins list` | List discovered OpenClaw-compatible plugins |
213
+ | `nextclaw plugins info <id>` | Show details of a plugin |
214
+ | `nextclaw config get <path>` | Get config value by path (use `--json` for structured output) |
215
+ | `nextclaw config set <path> <value>` | Set config value by path (use `--json` to parse value as JSON) |
216
+ | `nextclaw config unset <path>` | Remove config value by path |
217
+ | `nextclaw plugins install <path-or-spec>` | Install from local path, archive, or npm package |
218
+ | `nextclaw plugins enable <id>` | Enable a plugin in config |
219
+ | `nextclaw plugins disable <id>` | Disable a plugin in config |
220
+ | `nextclaw plugins uninstall <id>` | Remove plugin config/install record (supports `--dry-run`, `--force`, `--keep-files`) |
221
+ | `nextclaw plugins doctor` | Diagnose plugin load conflicts/errors |
222
+
223
+ Gateway options (when running `nextclaw gateway` or `nextclaw start`):
224
+
225
+ - `--ui` — enable the UI server with the gateway
226
+ - `--ui-port <port>` — UI port (default 18791 for start)
227
+ - `--ui-open` — open the browser when the UI starts
228
+
229
+ If service is already running, new UI port flags do not hot-apply; use `nextclaw restart ...` to apply them.
230
+
231
+ Status/diagnostics tips:
232
+
233
+ - `nextclaw status` shows runtime truth (process + health + config summary).
234
+ - `nextclaw status --json` outputs machine-readable status and sets exit code (`0` healthy, `1` degraded, `2` stopped).
235
+ - `nextclaw status --fix` safely clears stale service state if PID is dead.
236
+ - `nextclaw doctor` runs additional checks (state coherence, health, port availability, provider readiness).
237
+
238
+ ---
239
+
240
+ ## Plugins (OpenClaw compatibility)
241
+
242
+ NextClaw supports OpenClaw-compatible plugins while keeping compatibility logic isolated in `@nextclaw/openclaw-compat`.
243
+ For architecture boundaries and capability matrix, see [OpenClaw plugin compatibility guide](./openclaw-plugin-compat.md).
244
+
245
+ Typical flow:
246
+
247
+ ```bash
248
+ # 1) Inspect discovered plugins
249
+ nextclaw plugins list
250
+
251
+ # 2) Install (path/archive/npm)
252
+ nextclaw plugins install ./my-plugin
253
+ nextclaw plugins install ./my-plugin.tgz
254
+ nextclaw plugins install @scope/openclaw-plugin
255
+
256
+ # 3) Inspect and toggle
257
+ nextclaw plugins info my-plugin
258
+ nextclaw config get plugins.entries.my-plugin.config --json
259
+ nextclaw plugins disable my-plugin
260
+ nextclaw plugins enable my-plugin
261
+
262
+ # 4) Uninstall
263
+ nextclaw plugins uninstall my-plugin --dry-run
264
+ nextclaw plugins uninstall my-plugin --force
265
+ ```
266
+
267
+ Notes:
268
+
269
+ - Plugin config is merged under `plugins.entries.<id>.config`.
270
+ - `plugins uninstall --keep-config` is accepted as a backward-compatible alias of `--keep-files`.
271
+ - If a plugin tool/channel/provider conflicts with a built-in capability, NextClaw rejects the conflicting registration and reports diagnostics.
272
+
273
+ ---
274
+
275
+ ## Self-update
276
+
277
+ Use the built-in updater:
278
+
279
+ ```bash
280
+ nextclaw update
281
+ ```
282
+
283
+ Behavior:
284
+
285
+ - If `NEXTCLAW_UPDATE_COMMAND` is set, the CLI executes it (useful for custom update flows).
286
+ - Otherwise it falls back to `npm i -g nextclaw`.
287
+ - If the background service is running, restart it after the update to apply changes.
288
+
289
+ If the gateway is running, you can also ask the agent to update; the agent will call the gateway update tool only when you explicitly request it, and a restart will be scheduled afterward.
290
+
291
+ ---
292
+
293
+ ## Channels
294
+
295
+ All message channels use a common **allowFrom** rule:
296
+
297
+ - **Empty `allowFrom`** (`[]`): allow all senders.
298
+ - **Non-empty `allowFrom`**: only messages from the listed user IDs are accepted.
299
+
300
+ Configure channels in the UI at http://127.0.0.1:18791 or in `~/.nextclaw/config.json` under `channels`.
301
+
302
+ ### Discord
303
+
304
+ 1. Create a bot in the [Discord Developer Portal](https://discord.com/developers/applications) and get the bot token.
305
+ 2. Enable **MESSAGE CONTENT INTENT** for the bot.
306
+ 3. Invite the bot to your server with permissions to read and send messages.
307
+
308
+ ```json
309
+ {
310
+ "channels": {
311
+ "discord": {
312
+ "enabled": true,
313
+ "token": "YOUR_BOT_TOKEN",
314
+ "allowFrom": []
315
+ }
316
+ }
317
+ }
318
+ ```
319
+
320
+ ### Telegram
321
+
322
+ 1. Create a bot via [@BotFather](https://t.me/BotFather) and get the token.
323
+ 2. Get your user ID (e.g. from [@userinfobot](https://t.me/userinfobot)).
324
+ 3. Add your user ID to `allowFrom` to restrict who can use the bot.
325
+
326
+ ```json
327
+ {
328
+ "channels": {
329
+ "telegram": {
330
+ "enabled": true,
331
+ "token": "YOUR_BOT_TOKEN",
332
+ "allowFrom": ["YOUR_USER_ID"]
333
+ }
334
+ }
335
+ }
336
+ ```
337
+
338
+ Optional: set `"proxy": "http://localhost:7890"` (or your proxy URL) for network access.
339
+
340
+ ### Slack
341
+
342
+ Socket mode is the typical setup. You need a **Bot Token** and an **App-Level Token** (with `connections:write`).
343
+
344
+ ```json
345
+ {
346
+ "channels": {
347
+ "slack": {
348
+ "enabled": true,
349
+ "mode": "socket",
350
+ "botToken": "xoxb-...",
351
+ "appToken": "xapp-...",
352
+ "dm": { "enabled": true, "allowFrom": [] }
353
+ }
354
+ }
355
+ }
356
+ ```
357
+
358
+ - `dm.enabled`: allow DMs to the bot.
359
+ - `dm.allowFrom`: restrict DMs to these user IDs; empty means allow all.
360
+
361
+ ### Feishu (Lark)
362
+
363
+ Create an app in the [Feishu open platform](https://open.feishu.com/), obtain App ID, App Secret, and (if using encryption) Encrypt Key and Verification Token.
364
+
365
+ ```json
366
+ {
367
+ "channels": {
368
+ "feishu": {
369
+ "enabled": true,
370
+ "appId": "YOUR_APP_ID",
371
+ "appSecret": "YOUR_APP_SECRET",
372
+ "encryptKey": "",
373
+ "verificationToken": "",
374
+ "allowFrom": []
375
+ }
376
+ }
377
+ }
378
+ ```
379
+
380
+ ### DingTalk
381
+
382
+ Create an app in the [DingTalk open platform](https://open.dingtalk.com/) and get Client ID and Client Secret.
383
+
384
+ ```json
385
+ {
386
+ "channels": {
387
+ "dingtalk": {
388
+ "enabled": true,
389
+ "clientId": "YOUR_CLIENT_ID",
390
+ "clientSecret": "YOUR_CLIENT_SECRET",
391
+ "allowFrom": []
392
+ }
393
+ }
394
+ }
395
+ ```
396
+
397
+ ### WhatsApp
398
+
399
+ WhatsApp typically requires a bridge (e.g. a companion service). Configure the bridge URL and optional allowlist:
400
+
401
+ ```json
402
+ {
403
+ "channels": {
404
+ "whatsapp": {
405
+ "enabled": true,
406
+ "bridgeUrl": "ws://localhost:3001",
407
+ "allowFrom": []
408
+ }
409
+ }
410
+ }
411
+ ```
412
+
413
+ Use `nextclaw channels login` when the bridge supports QR-based linking.
414
+
415
+ ### Email
416
+
417
+ Configure IMAP (inbox) and SMTP (sending). The agent can read and reply to emails.
418
+
419
+ ```json
420
+ {
421
+ "channels": {
422
+ "email": {
423
+ "enabled": true,
424
+ "consentGranted": true,
425
+ "imapHost": "imap.example.com",
426
+ "imapPort": 993,
427
+ "imapUsername": "you@example.com",
428
+ "imapPassword": "YOUR_PASSWORD",
429
+ "imapMailbox": "INBOX",
430
+ "imapUseSsl": true,
431
+ "smtpHost": "smtp.example.com",
432
+ "smtpPort": 587,
433
+ "smtpUsername": "you@example.com",
434
+ "smtpPassword": "YOUR_PASSWORD",
435
+ "smtpUseTls": true,
436
+ "fromAddress": "you@example.com",
437
+ "autoReplyEnabled": true,
438
+ "pollIntervalSeconds": 30,
439
+ "allowFrom": []
440
+ }
441
+ }
442
+ }
443
+ ```
444
+
445
+ Set `consentGranted` to `true` after you understand that the agent will read and send mail. Use `allowFrom` to restrict to certain sender addresses if desired.
446
+
447
+ ### QQ
448
+
449
+ Use the QQ open platform app credentials.
450
+
451
+ ```json
452
+ {
453
+ "channels": {
454
+ "qq": {
455
+ "enabled": true,
456
+ "appId": "YOUR_APP_ID",
457
+ "secret": "YOUR_SECRET",
458
+ "markdownSupport": false,
459
+ "allowFrom": []
460
+ }
461
+ }
462
+ }
463
+ ```
464
+
465
+ ### Mochat
466
+
467
+ Mochat uses a claw token and optional socket URL. Configure base URL, socket, and (optionally) sessions/panels and group rules.
468
+
469
+ ```json
470
+ {
471
+ "channels": {
472
+ "mochat": {
473
+ "enabled": true,
474
+ "baseUrl": "https://mochat.io",
475
+ "socketUrl": "",
476
+ "clawToken": "YOUR_CLAW_TOKEN",
477
+ "agentUserId": "",
478
+ "sessions": [],
479
+ "panels": [],
480
+ "allowFrom": []
481
+ }
482
+ }
483
+ }
484
+ ```
485
+
486
+ After changing channel config, NextClaw hot-reloads channel runtime automatically when the gateway is running.
487
+
488
+ ---
489
+
490
+ ## Tools
491
+
492
+ ### Web search (Brave)
493
+
494
+ Add a Brave Search API key to enable web search for the agent:
495
+
496
+ ```json
497
+ {
498
+ "tools": {
499
+ "web": {
500
+ "search": { "apiKey": "YOUR_BRAVE_KEY", "maxResults": 5 }
501
+ }
502
+ }
503
+ }
504
+ ```
505
+
506
+ ### Command execution (exec)
507
+
508
+ Allow the agent to run shell commands:
509
+
510
+ ```json
511
+ {
512
+ "tools": {
513
+ "exec": { "timeout": 60 }
514
+ },
515
+ "restrictToWorkspace": false
516
+ }
517
+ ```
518
+
519
+ - `timeout`: max seconds per command.
520
+ - `restrictToWorkspace`: if `true`, commands are restricted to the agent workspace directory; if `false`, the agent can run commands in other paths (use with care).
521
+
522
+ ---
523
+
524
+ ## Cron & Heartbeat
525
+
526
+ ### Cron
527
+
528
+ Schedule one-off or recurring tasks. The agent receives the message at the scheduled time.
529
+
530
+ List jobs:
531
+
532
+ ```bash
533
+ nextclaw cron list
534
+ ```
535
+
536
+ Add a one-time job (run at a specific time, ISO format):
537
+
538
+ ```bash
539
+ nextclaw cron add -n "reminder" -m "Stand up and stretch" --at "2026-02-15T09:00:00"
540
+ ```
541
+
542
+ Add a recurring job (cron expression):
543
+
544
+ ```bash
545
+ nextclaw cron add -n "daily-summary" -m "Summarize yesterday" -c "0 9 * * *"
546
+ ```
547
+
548
+ Add a job that runs every N seconds:
549
+
550
+ ```bash
551
+ nextclaw cron add -n "ping" -m "Ping" -e 3600
552
+ ```
553
+
554
+ Optional: deliver the agent’s reply to a channel:
555
+
556
+ ```bash
557
+ nextclaw cron add -n "daily" -m "Daily briefing" -c "0 9 * * *" --deliver --to <recipient> --channel <channel>
558
+ ```
559
+
560
+ Remove, enable, or disable a job:
561
+
562
+ ```bash
563
+ nextclaw cron remove <jobId>
564
+ nextclaw cron enable <jobId>
565
+ nextclaw cron enable <jobId> --disable
566
+ ```
567
+
568
+ Run a job once (e.g. for testing):
569
+
570
+ ```bash
571
+ nextclaw cron run <jobId>
572
+ ```
573
+
574
+ ### Heartbeat
575
+
576
+ When the gateway is running, it checks the workspace file `HEARTBEAT.md` periodically (e.g. every 30 minutes). If the file contains actionable tasks, the agent processes them. Edit `HEARTBEAT.md` in your workspace to add or change tasks.
577
+
578
+ ---
579
+
580
+ ## UI (optional)
581
+
582
+ You can tune the UI server in config:
583
+
584
+ ```json
585
+ {
586
+ "ui": {
587
+ "enabled": true,
588
+ "host": "0.0.0.0",
589
+ "port": 18791,
590
+ "open": false
591
+ }
592
+ }
593
+ ```
594
+
595
+ - `enabled`: whether the UI server is started with the gateway (e.g. when using `nextclaw start`).
596
+ - `host` / `port`: bind address and port; `ui.host` is read-only in practice (CLI start paths always enforce `0.0.0.0`).
597
+ - `open`: open the default browser when the UI starts.
598
+
599
+ Default URL when using `nextclaw start`: **http://127.0.0.1:18791**.
600
+
601
+ NextClaw binds UI to `0.0.0.0` by default and attempts to detect/print a public IP-based URL at startup.
602
+
603
+ ---
604
+
605
+ ## Troubleshooting
606
+
607
+ | Issue | What to check |
608
+ |-------|----------------|
609
+ | **401 / invalid API key** | Verify the provider `apiKey` and `apiBase` in config or UI. Ensure no extra spaces or wrong key. |
610
+ | **Unknown model** | Confirm the model ID is supported by your provider (e.g. OpenRouter model list). |
611
+ | **No replies on a channel** | Ensure the channel is `enabled`, `allowFrom` includes your user ID if set, and the gateway is running (`nextclaw start` or `nextclaw gateway`). Run `nextclaw channels status` to see channel status. |
612
+ | **Port already in use** | Change `ui.port` in config or use `--ui-port` when starting. Default UI port is 18791, gateway 18790. |
613
+ | **Config not loading** | Ensure `NEXTCLAW_HOME` (if set) points to the directory that contains `config.json`. Run `nextclaw status` to see which config file is used. |
614
+ | **Agent not responding in CLI** | Run `nextclaw init` if you have not yet; ensure a provider and model are set and the provider key is valid. |
615
+
616
+ ---