nextclaw 0.5.1 → 0.5.3

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 +322 -14
  2. package/package.json +1 -1
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({
@@ -2360,7 +2656,18 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2360
2656
  }
2361
2657
  try {
2362
2658
  const response = await fetch(params.healthUrl, { method: "GET" });
2363
- if (response.ok) {
2659
+ if (!response.ok) {
2660
+ await new Promise((resolve5) => setTimeout(resolve5, 200));
2661
+ continue;
2662
+ }
2663
+ const payload = await response.json();
2664
+ const healthy = payload?.ok === true && payload?.data?.status === "ok";
2665
+ if (!healthy) {
2666
+ await new Promise((resolve5) => setTimeout(resolve5, 200));
2667
+ continue;
2668
+ }
2669
+ await new Promise((resolve5) => setTimeout(resolve5, 300));
2670
+ if (isProcessRunning(params.pid)) {
2364
2671
  return true;
2365
2672
  }
2366
2673
  } catch {
@@ -2674,5 +2981,6 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
2674
2981
  cron.command("remove <jobId>").action((jobId) => runtime.cronRemove(jobId));
2675
2982
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => runtime.cronEnable(jobId, opts));
2676
2983
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
2677
- program.command("status").description(`Show ${APP_NAME2} status`).action(() => runtime.status());
2984
+ 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));
2985
+ 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));
2678
2986
  program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",