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.
- package/dist/cli/index.js +322 -14
- 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
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
-
|
|
2108
|
+
return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
|
|
1941
2109
|
}
|
|
1942
2110
|
if (spec.isLocal) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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);
|