openclaw-tools 0.2.1
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/LICENSE +21 -0
- package/README.md +239 -0
- package/README.md.bak +247 -0
- package/README.openclaw-cli.md +239 -0
- package/README.openclaw-daemon.md +239 -0
- package/README.openclaw-gateway.md +239 -0
- package/README.openclaw-health.md +239 -0
- package/README.openclaw-helper.md +239 -0
- package/README.openclaw-install.md +239 -0
- package/README.openclaw-manage.md +239 -0
- package/README.openclaw-monitor.md +239 -0
- package/README.openclaw-run.md +239 -0
- package/README.openclaw-service.md +239 -0
- package/README.openclaw-setup.md +239 -0
- package/README.openclaw-start.md +239 -0
- package/README.openclaw-tools.md +239 -0
- package/README.openclaw-utils.md +239 -0
- package/README.openclaw-watch.md +239 -0
- package/README.zh-CN.md +213 -0
- package/dist/chunk-FUWKRMIF.js +470 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +567 -0
- package/dist/server-PP5Y3LAF.js +7 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BINARY_NAME,
|
|
4
|
+
DISPLAY_NAME,
|
|
5
|
+
DOCTOR_LOG_DIR,
|
|
6
|
+
PID_FILE,
|
|
7
|
+
__require,
|
|
8
|
+
addRestartRecord,
|
|
9
|
+
checkHealth,
|
|
10
|
+
detectOpenClaw,
|
|
11
|
+
ensureDoctorHome,
|
|
12
|
+
getRestartCommand,
|
|
13
|
+
getStartCommand,
|
|
14
|
+
getStopCommand,
|
|
15
|
+
initLogger,
|
|
16
|
+
loadConfig,
|
|
17
|
+
log,
|
|
18
|
+
runOpenClawCmd,
|
|
19
|
+
startDashboard
|
|
20
|
+
} from "./chunk-FUWKRMIF.js";
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
import { createRequire } from "module";
|
|
24
|
+
import { spawnSync } from "child_process";
|
|
25
|
+
import { Command } from "commander";
|
|
26
|
+
|
|
27
|
+
// src/commands/watch.ts
|
|
28
|
+
import { spawn } from "child_process";
|
|
29
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync, openSync } from "fs";
|
|
30
|
+
import chalk from "chalk";
|
|
31
|
+
|
|
32
|
+
// src/core/process-manager.ts
|
|
33
|
+
import { exec } from "child_process";
|
|
34
|
+
import { promisify } from "util";
|
|
35
|
+
var execAsync = promisify(exec);
|
|
36
|
+
async function runShell(command) {
|
|
37
|
+
try {
|
|
38
|
+
const { stdout } = await execAsync(command, { timeout: 12e4 });
|
|
39
|
+
return { success: true, output: stdout.trim() };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
42
|
+
return { success: false, error };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function restartGateway(info) {
|
|
46
|
+
const cmd = getRestartCommand(info);
|
|
47
|
+
log("warn", `Restarting gateway: ${cmd}`);
|
|
48
|
+
const result = await runShell(cmd);
|
|
49
|
+
if (result.success) {
|
|
50
|
+
log("success", "Gateway restarted");
|
|
51
|
+
} else {
|
|
52
|
+
log("error", `Gateway restart failed: ${result.error}`);
|
|
53
|
+
}
|
|
54
|
+
addRestartRecord({
|
|
55
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56
|
+
reason: "health check failed",
|
|
57
|
+
success: result.success
|
|
58
|
+
});
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
async function startGateway(info) {
|
|
62
|
+
const cmd = getStartCommand(info);
|
|
63
|
+
log("info", `Starting gateway: ${cmd}`);
|
|
64
|
+
const result = await runShell(cmd);
|
|
65
|
+
if (result.success) log("success", "Gateway started");
|
|
66
|
+
else log("error", `Gateway start failed: ${result.error}`);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
async function stopGateway(info) {
|
|
70
|
+
const cmd = getStopCommand(info);
|
|
71
|
+
log("info", `Stopping gateway: ${cmd}`);
|
|
72
|
+
const result = await runShell(cmd);
|
|
73
|
+
if (result.success) log("success", "Gateway stopped");
|
|
74
|
+
else log("error", `Gateway stop failed: ${result.error}`);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
var RestartThrottle = class {
|
|
78
|
+
constructor(maxPerHour) {
|
|
79
|
+
this.maxPerHour = maxPerHour;
|
|
80
|
+
}
|
|
81
|
+
timestamps = [];
|
|
82
|
+
canRestart() {
|
|
83
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
84
|
+
this.timestamps = this.timestamps.filter((t) => t > oneHourAgo);
|
|
85
|
+
return this.timestamps.length < this.maxPerHour;
|
|
86
|
+
}
|
|
87
|
+
record() {
|
|
88
|
+
this.timestamps.push(Date.now());
|
|
89
|
+
}
|
|
90
|
+
recentCount() {
|
|
91
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
92
|
+
return this.timestamps.filter((t) => t > oneHourAgo).length;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/commands/watch.ts
|
|
97
|
+
import { join } from "path";
|
|
98
|
+
async function watchDaemon(options) {
|
|
99
|
+
if (options.daemon) {
|
|
100
|
+
return daemonize(options);
|
|
101
|
+
}
|
|
102
|
+
const config = loadConfig(options.config);
|
|
103
|
+
initLogger();
|
|
104
|
+
ensureDoctorHome();
|
|
105
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
106
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
107
|
+
log("info", "OpenClaw Doctor started (foreground)");
|
|
108
|
+
log("info", `Gateway port: ${info.gatewayPort}`);
|
|
109
|
+
log("info", `Channels: ${info.channels.join(", ") || "none detected"}`);
|
|
110
|
+
log("info", `Check interval: ${config.checkInterval}s`);
|
|
111
|
+
log("info", `PID: ${process.pid}`);
|
|
112
|
+
if (options.dashboard) {
|
|
113
|
+
const { startDashboard: startDashboard2 } = await import("./server-PP5Y3LAF.js");
|
|
114
|
+
startDashboard2({ config: options.config });
|
|
115
|
+
}
|
|
116
|
+
const throttle = new RestartThrottle(config.maxRestartsPerHour);
|
|
117
|
+
let consecutiveFailures = 0;
|
|
118
|
+
let isRestarting = false;
|
|
119
|
+
async function tick() {
|
|
120
|
+
if (isRestarting) return;
|
|
121
|
+
const result = await checkHealth(info);
|
|
122
|
+
if (result.healthy) {
|
|
123
|
+
consecutiveFailures = 0;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
consecutiveFailures++;
|
|
127
|
+
log(
|
|
128
|
+
"warn",
|
|
129
|
+
`Consecutive failures: ${consecutiveFailures}/${config.failThreshold}`
|
|
130
|
+
);
|
|
131
|
+
if (consecutiveFailures >= config.failThreshold) {
|
|
132
|
+
if (!throttle.canRestart()) {
|
|
133
|
+
log(
|
|
134
|
+
"error",
|
|
135
|
+
`Restart throttled: ${throttle.recentCount()} restarts in the last hour (max ${config.maxRestartsPerHour})`
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
isRestarting = true;
|
|
140
|
+
consecutiveFailures = 0;
|
|
141
|
+
throttle.record();
|
|
142
|
+
await restartGateway(info);
|
|
143
|
+
log("info", "Waiting 30s for gateway to start...");
|
|
144
|
+
await new Promise((r) => setTimeout(r, 6e4));
|
|
145
|
+
isRestarting = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
await tick();
|
|
149
|
+
setInterval(tick, config.checkInterval * 1e3);
|
|
150
|
+
const cleanup = () => {
|
|
151
|
+
log("info", "OpenClaw Doctor stopped");
|
|
152
|
+
try {
|
|
153
|
+
unlinkSync(PID_FILE);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
process.exit(0);
|
|
157
|
+
};
|
|
158
|
+
process.on("SIGINT", cleanup);
|
|
159
|
+
process.on("SIGTERM", cleanup);
|
|
160
|
+
}
|
|
161
|
+
function daemonize(options) {
|
|
162
|
+
ensureDoctorHome();
|
|
163
|
+
const existingPid = readDaemonPid();
|
|
164
|
+
if (existingPid && isProcessAlive(existingPid)) {
|
|
165
|
+
console.log(chalk.yellow(`Doctor is already running (PID ${existingPid})`));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const execArgv = process.execArgv.filter(
|
|
169
|
+
(a) => !a.includes("--eval")
|
|
170
|
+
);
|
|
171
|
+
const scriptArgs = process.argv.slice(1).filter(
|
|
172
|
+
(a) => a !== "-d" && a !== "--daemon"
|
|
173
|
+
);
|
|
174
|
+
const fullArgs = [...execArgv, ...scriptArgs];
|
|
175
|
+
const outLog = join(DOCTOR_LOG_DIR, "daemon.out.log");
|
|
176
|
+
const errLog = join(DOCTOR_LOG_DIR, "daemon.err.log");
|
|
177
|
+
const out = openSync(outLog, "a");
|
|
178
|
+
const err = openSync(errLog, "a");
|
|
179
|
+
const child = spawn(process.execPath, fullArgs, {
|
|
180
|
+
detached: true,
|
|
181
|
+
stdio: ["ignore", out, err],
|
|
182
|
+
env: { ...process.env, OPENCLAW_DOCTOR_DAEMON: "1" }
|
|
183
|
+
});
|
|
184
|
+
const pid = child.pid;
|
|
185
|
+
writeFileSync(PID_FILE, String(pid));
|
|
186
|
+
child.unref();
|
|
187
|
+
console.log(chalk.green(`Doctor started in background (PID ${pid})`));
|
|
188
|
+
console.log(chalk.gray(` Logs: ${outLog}`));
|
|
189
|
+
console.log(chalk.gray(` Stop: openclaw-doctor stop`));
|
|
190
|
+
}
|
|
191
|
+
async function stopDaemon(options) {
|
|
192
|
+
const pid = readDaemonPid();
|
|
193
|
+
if (!pid) {
|
|
194
|
+
console.log(chalk.yellow("Doctor is not running (no PID file)"));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!isProcessAlive(pid)) {
|
|
198
|
+
console.log(chalk.yellow(`Doctor is not running (PID ${pid} is dead, cleaning up)`));
|
|
199
|
+
try {
|
|
200
|
+
unlinkSync(PID_FILE);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
process.kill(pid, "SIGTERM");
|
|
207
|
+
console.log(chalk.green(`Doctor stopped (PID ${pid})`));
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.log(chalk.red(`Failed to stop Doctor (PID ${pid}): ${err}`));
|
|
210
|
+
}
|
|
211
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
212
|
+
try {
|
|
213
|
+
unlinkSync(PID_FILE);
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function readDaemonPid() {
|
|
218
|
+
if (!existsSync(PID_FILE)) return null;
|
|
219
|
+
const raw = readFileSync(PID_FILE, "utf-8").trim();
|
|
220
|
+
const pid = parseInt(raw, 10);
|
|
221
|
+
return isNaN(pid) ? null : pid;
|
|
222
|
+
}
|
|
223
|
+
function isProcessAlive(pid) {
|
|
224
|
+
try {
|
|
225
|
+
process.kill(pid, 0);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/commands/status.ts
|
|
233
|
+
import chalk2 from "chalk";
|
|
234
|
+
async function showStatus(options) {
|
|
235
|
+
const config = loadConfig(options.config);
|
|
236
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
237
|
+
const result = await checkHealth(info);
|
|
238
|
+
if (options.json) {
|
|
239
|
+
console.log(
|
|
240
|
+
JSON.stringify(
|
|
241
|
+
{
|
|
242
|
+
gateway: {
|
|
243
|
+
healthy: result.healthy,
|
|
244
|
+
reachable: result.gateway,
|
|
245
|
+
port: info.gatewayPort,
|
|
246
|
+
durationMs: result.durationMs
|
|
247
|
+
},
|
|
248
|
+
channels: result.channels,
|
|
249
|
+
agents: info.agents,
|
|
250
|
+
openclaw: {
|
|
251
|
+
version: info.version,
|
|
252
|
+
profile: info.profile,
|
|
253
|
+
configPath: info.configPath
|
|
254
|
+
},
|
|
255
|
+
doctor: {
|
|
256
|
+
checkInterval: config.checkInterval,
|
|
257
|
+
failThreshold: config.failThreshold
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
null,
|
|
261
|
+
2
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
process.exit(result.healthy ? 0 : 1);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
console.log(chalk2.bold("\n OpenClaw Doctor\n"));
|
|
268
|
+
if (result.healthy) {
|
|
269
|
+
console.log(
|
|
270
|
+
chalk2.green.bold(` Gateway: HEALTHY`) + chalk2.gray(` (port ${info.gatewayPort}, ${result.durationMs}ms)`)
|
|
271
|
+
);
|
|
272
|
+
} else if (result.gateway) {
|
|
273
|
+
console.log(chalk2.yellow.bold(` Gateway: DEGRADED`) + chalk2.gray(` (responded but ok=false)`));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(chalk2.red.bold(` Gateway: UNREACHABLE`));
|
|
276
|
+
if (result.error) console.log(chalk2.red(` ${result.error}`));
|
|
277
|
+
}
|
|
278
|
+
if (result.channels && result.channels.length > 0) {
|
|
279
|
+
console.log();
|
|
280
|
+
for (const ch of result.channels ?? []) {
|
|
281
|
+
const icon = ch.ok ? chalk2.green("ok") : chalk2.red("fail");
|
|
282
|
+
console.log(` ${chalk2.gray("Channel")} ${ch.name}: ${icon}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (info.agents.length > 0) {
|
|
286
|
+
console.log();
|
|
287
|
+
const agentList = info.agents.map((a) => a.isDefault ? `${a.name} (default)` : a.name).join(", ");
|
|
288
|
+
console.log(chalk2.gray(` Agents: ${agentList}`));
|
|
289
|
+
}
|
|
290
|
+
console.log();
|
|
291
|
+
console.log(chalk2.gray(` OpenClaw ${info.version ?? "unknown"}`));
|
|
292
|
+
console.log(chalk2.gray(` Config: ${info.configPath}`));
|
|
293
|
+
console.log();
|
|
294
|
+
process.exit(result.healthy ? 0 : 1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/commands/doctor.ts
|
|
298
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
299
|
+
import chalk3 from "chalk";
|
|
300
|
+
function findConfigIssues(configPath) {
|
|
301
|
+
if (!existsSync2(configPath)) return [];
|
|
302
|
+
let raw;
|
|
303
|
+
try {
|
|
304
|
+
raw = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
305
|
+
} catch {
|
|
306
|
+
return [{ path: "root", message: "Config file is not valid JSON", fix: () => {
|
|
307
|
+
} }];
|
|
308
|
+
}
|
|
309
|
+
const issues = [];
|
|
310
|
+
if (Array.isArray(raw.agents?.list)) {
|
|
311
|
+
for (let i = 0; i < raw.agents.list.length; i++) {
|
|
312
|
+
const agent = raw.agents.list[i];
|
|
313
|
+
if (Array.isArray(agent.workspace)) {
|
|
314
|
+
const idx = i;
|
|
315
|
+
issues.push({
|
|
316
|
+
path: `agents.list.${i}.workspace`,
|
|
317
|
+
message: `Invalid input: expected string, received array`,
|
|
318
|
+
fix: () => {
|
|
319
|
+
raw.agents.list[idx].workspace = raw.agents.list[idx].workspace[0];
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (issues.length > 0) {
|
|
326
|
+
const originalFixes = issues.map((issue) => issue.fix);
|
|
327
|
+
for (let i = 0; i < issues.length; i++) {
|
|
328
|
+
const origFix = originalFixes[i];
|
|
329
|
+
issues[i].fix = () => {
|
|
330
|
+
origFix();
|
|
331
|
+
writeFileSync2(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return issues;
|
|
336
|
+
}
|
|
337
|
+
async function runDoctor(options) {
|
|
338
|
+
const config = loadConfig(options.config);
|
|
339
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
340
|
+
console.log(chalk3.bold("\n OpenClaw Doctor \u2014 Full Diagnostics\n"));
|
|
341
|
+
console.log(chalk3.gray(" [0/4] Config validation"));
|
|
342
|
+
const issues = findConfigIssues(info.configPath);
|
|
343
|
+
if (issues.length === 0) {
|
|
344
|
+
console.log(chalk3.green(" Config: valid"));
|
|
345
|
+
} else {
|
|
346
|
+
for (const issue of issues) {
|
|
347
|
+
console.log(chalk3.red(` ${issue.path}: ${issue.message}`));
|
|
348
|
+
}
|
|
349
|
+
if (options.fix) {
|
|
350
|
+
for (const issue of issues) {
|
|
351
|
+
issue.fix();
|
|
352
|
+
console.log(chalk3.green(` Fixed: ${issue.path}`));
|
|
353
|
+
}
|
|
354
|
+
console.log(chalk3.green(` Config saved: ${info.configPath}`));
|
|
355
|
+
} else {
|
|
356
|
+
console.log(chalk3.yellow(" Run with --fix to auto-repair"));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
console.log(chalk3.gray("\n [1/4] OpenClaw binary"));
|
|
360
|
+
if (info.cliBinPath) {
|
|
361
|
+
console.log(chalk3.green(` Found: ${info.cliBinPath}`));
|
|
362
|
+
console.log(chalk3.gray(` Node: ${info.nodePath}`));
|
|
363
|
+
if (info.version) console.log(chalk3.gray(` Version: ${info.version}`));
|
|
364
|
+
} else {
|
|
365
|
+
console.log(chalk3.red(" Not found \u2014 openclaw CLI is not installed or not in PATH"));
|
|
366
|
+
}
|
|
367
|
+
console.log(chalk3.gray("\n [2/4] Gateway health"));
|
|
368
|
+
const result = await checkHealth(info);
|
|
369
|
+
if (result.healthy) {
|
|
370
|
+
console.log(chalk3.green(` Gateway: healthy (port ${info.gatewayPort}, ${result.durationMs}ms)`));
|
|
371
|
+
} else if (result.gateway) {
|
|
372
|
+
console.log(chalk3.yellow(` Gateway: responded but degraded`));
|
|
373
|
+
} else {
|
|
374
|
+
console.log(chalk3.red(` Gateway: unreachable`));
|
|
375
|
+
if (result.error) console.log(chalk3.red(` ${result.error}`));
|
|
376
|
+
}
|
|
377
|
+
console.log(chalk3.gray("\n [3/4] Channels"));
|
|
378
|
+
if (result.channels.length > 0) {
|
|
379
|
+
for (const ch of result.channels) {
|
|
380
|
+
const status = ch.ok ? chalk3.green("ok") : chalk3.red("fail");
|
|
381
|
+
console.log(` ${ch.name}: ${status}`);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
console.log(chalk3.yellow(" No channel data available"));
|
|
385
|
+
}
|
|
386
|
+
console.log(chalk3.gray("\n [4/4] OpenClaw built-in doctor"));
|
|
387
|
+
const doctorOutput = await runOpenClawCmd(info, "doctor");
|
|
388
|
+
if (doctorOutput) {
|
|
389
|
+
const lines = doctorOutput.split("\n");
|
|
390
|
+
const startIdx = lines.findIndex((l) => l.includes("OpenClaw doctor") || l.includes("Gateway service"));
|
|
391
|
+
const relevant = startIdx >= 0 ? lines.slice(startIdx) : lines;
|
|
392
|
+
for (const line of relevant) {
|
|
393
|
+
console.log(` ${line}`);
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
console.log(chalk3.yellow(" Could not run openclaw doctor"));
|
|
397
|
+
}
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/commands/logs.ts
|
|
402
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
403
|
+
import chalk4 from "chalk";
|
|
404
|
+
function showLogs(options) {
|
|
405
|
+
const config = loadConfig(options.config);
|
|
406
|
+
const maxLines = parseInt(options.lines ?? "50", 10);
|
|
407
|
+
if (options.doctor) {
|
|
408
|
+
showDoctorLogs(maxLines);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
412
|
+
const logFile = options.error ? `${info.logDir}/gateway.err.log` : `${info.logDir}/gateway.log`;
|
|
413
|
+
if (!existsSync3(logFile)) {
|
|
414
|
+
console.log(chalk4.yellow(`Log file not found: ${logFile}`));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
console.log(chalk4.blue.bold(`
|
|
418
|
+
${logFile}
|
|
419
|
+
`));
|
|
420
|
+
const content = readFileSync3(logFile, "utf-8");
|
|
421
|
+
const lines = content.trim().split("\n");
|
|
422
|
+
const tail = lines.slice(-maxLines);
|
|
423
|
+
for (const line of tail) {
|
|
424
|
+
if (line.includes("[error]") || line.includes("[ERROR]")) {
|
|
425
|
+
console.log(chalk4.red(line));
|
|
426
|
+
} else if (line.includes("[warn]") || line.includes("[WARN]")) {
|
|
427
|
+
console.log(chalk4.yellow(line));
|
|
428
|
+
} else {
|
|
429
|
+
console.log(chalk4.gray(line));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
console.log();
|
|
433
|
+
}
|
|
434
|
+
function showDoctorLogs(maxLines) {
|
|
435
|
+
const { readdirSync } = __require("fs");
|
|
436
|
+
const { join: join2 } = __require("path");
|
|
437
|
+
if (!existsSync3(DOCTOR_LOG_DIR)) {
|
|
438
|
+
console.log(chalk4.yellow("No doctor logs found."));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const files = readdirSync(DOCTOR_LOG_DIR).filter((f) => f.endsWith(".log")).sort().reverse();
|
|
442
|
+
if (files.length === 0) {
|
|
443
|
+
console.log(chalk4.yellow("No doctor log files found."));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const latest = files[0];
|
|
447
|
+
console.log(chalk4.blue.bold(`
|
|
448
|
+
${join2(DOCTOR_LOG_DIR, latest)}
|
|
449
|
+
`));
|
|
450
|
+
const content = readFileSync3(join2(DOCTOR_LOG_DIR, latest), "utf-8");
|
|
451
|
+
const lines = content.trim().split("\n");
|
|
452
|
+
const tail = lines.slice(-maxLines);
|
|
453
|
+
for (const line of tail) {
|
|
454
|
+
if (line.includes("[ERROR]")) {
|
|
455
|
+
console.log(chalk4.red(line));
|
|
456
|
+
} else if (line.includes("[WARN]")) {
|
|
457
|
+
console.log(chalk4.yellow(line));
|
|
458
|
+
} else if (line.includes("[SUCCESS]")) {
|
|
459
|
+
console.log(chalk4.green(line));
|
|
460
|
+
} else {
|
|
461
|
+
console.log(chalk4.gray(line));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
console.log();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/commands/gateway.ts
|
|
468
|
+
import chalk5 from "chalk";
|
|
469
|
+
async function gatewayStart(options) {
|
|
470
|
+
const config = loadConfig(options.config);
|
|
471
|
+
initLogger();
|
|
472
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
473
|
+
const result = await startGateway(info);
|
|
474
|
+
if (result.success) {
|
|
475
|
+
console.log(chalk5.green("Gateway started"));
|
|
476
|
+
} else {
|
|
477
|
+
console.log(chalk5.red(`Failed to start gateway: ${result.error}`));
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function gatewayStop(options) {
|
|
482
|
+
const config = loadConfig(options.config);
|
|
483
|
+
initLogger();
|
|
484
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
485
|
+
const result = await stopGateway(info);
|
|
486
|
+
if (result.success) {
|
|
487
|
+
console.log(chalk5.green("Gateway stopped"));
|
|
488
|
+
} else {
|
|
489
|
+
console.log(chalk5.red(`Failed to stop gateway: ${result.error}`));
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function gatewayRestart(options) {
|
|
494
|
+
const config = loadConfig(options.config);
|
|
495
|
+
initLogger();
|
|
496
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
497
|
+
const result = await restartGateway(info);
|
|
498
|
+
if (result.success) {
|
|
499
|
+
console.log(chalk5.green("Gateway restarted"));
|
|
500
|
+
} else {
|
|
501
|
+
console.log(chalk5.red(`Failed to restart gateway: ${result.error}`));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/index.ts
|
|
507
|
+
var require2 = createRequire(import.meta.url);
|
|
508
|
+
var { version } = require2("../package.json");
|
|
509
|
+
var program = new Command();
|
|
510
|
+
program.name(BINARY_NAME).description(`${DISPLAY_NAME} \u2014 health monitor and management for OpenClaw services`).version(version);
|
|
511
|
+
var addGlobalOpts = (cmd) => cmd.option("-c, --config <path>", "Path to config file").option("--profile <name>", "OpenClaw profile (default, dev, ...)", "default");
|
|
512
|
+
addGlobalOpts(
|
|
513
|
+
program.command("watch").description("Start health monitoring daemon").option("-d, --daemon", "Run in background").option("--dashboard", "Also start web dashboard")
|
|
514
|
+
).action(watchDaemon);
|
|
515
|
+
addGlobalOpts(
|
|
516
|
+
program.command("unwatch").description("Stop monitoring daemon")
|
|
517
|
+
).action(stopDaemon);
|
|
518
|
+
addGlobalOpts(
|
|
519
|
+
program.command("status").description("Show gateway and channel health").option("--json", "Machine-readable JSON output")
|
|
520
|
+
).action(showStatus);
|
|
521
|
+
addGlobalOpts(
|
|
522
|
+
program.command("doctor").description("Run full diagnostics (our checks + openclaw doctor)").option("--fix", "Auto-fix common config issues")
|
|
523
|
+
).action(runDoctor);
|
|
524
|
+
addGlobalOpts(
|
|
525
|
+
program.command("monitor").description("Start local monitoring web UI (http://localhost:9090)")
|
|
526
|
+
).action(startDashboard);
|
|
527
|
+
var gw = program.command("gateway").description("Manage the OpenClaw gateway service");
|
|
528
|
+
addGlobalOpts(gw.command("start").description("Start the gateway")).action(gatewayStart);
|
|
529
|
+
addGlobalOpts(gw.command("stop").description("Stop the gateway")).action(gatewayStop);
|
|
530
|
+
addGlobalOpts(gw.command("restart").description("Restart the gateway")).action(gatewayRestart);
|
|
531
|
+
addGlobalOpts(
|
|
532
|
+
program.command("logs").description("View logs (proxies to openclaw logs; use --doctor for our own logs)").option("-n, --lines <count>", "Number of lines to show", "50").option("--error", "Show gateway error logs").option("--doctor", "Show our own event logs").option("--tail", "Follow logs in real time").allowUnknownOption()
|
|
533
|
+
).action((options, cmd) => {
|
|
534
|
+
if (options.doctor) {
|
|
535
|
+
showLogs({ ...options, doctor: true });
|
|
536
|
+
} else {
|
|
537
|
+
proxyToOpenclaw(cmd.args.length ? cmd.args : [], ["logs", ...process.argv.slice(3)]);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
program.command("*", { hidden: true, isDefault: false }).allowUnknownOption().action(() => {
|
|
541
|
+
});
|
|
542
|
+
program.addHelpText("after", `
|
|
543
|
+
Proxy: any unrecognized command is forwarded to the openclaw CLI.
|
|
544
|
+
e.g. "${BINARY_NAME} channels list" \u2192 "openclaw channels list"
|
|
545
|
+
`);
|
|
546
|
+
var knownCommands = program.commands.map((c) => c.name());
|
|
547
|
+
var rawArgs = process.argv.slice(2);
|
|
548
|
+
var firstArg = rawArgs[0];
|
|
549
|
+
if (firstArg && !firstArg.startsWith("-") && !knownCommands.includes(firstArg)) {
|
|
550
|
+
proxyToOpenclaw(rawArgs);
|
|
551
|
+
} else {
|
|
552
|
+
program.parse();
|
|
553
|
+
}
|
|
554
|
+
function proxyToOpenclaw(args, override) {
|
|
555
|
+
const info = detectOpenClaw("default");
|
|
556
|
+
const bin = info.cliBinPath;
|
|
557
|
+
if (!bin) {
|
|
558
|
+
console.error("openclaw CLI not found. Please install openclaw first.");
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
const passArgs = override ?? args;
|
|
562
|
+
const result = spawnSync(info.nodePath, [bin, ...passArgs], {
|
|
563
|
+
stdio: "inherit",
|
|
564
|
+
env: process.env
|
|
565
|
+
});
|
|
566
|
+
process.exit(result.status ?? 0);
|
|
567
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-tools",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Health check and auto-restart daemon for local OpenClaw services",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-tools": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"lint": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"health-check",
|
|
18
|
+
"daemon",
|
|
19
|
+
"doctor",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Sobranier/openclaw-doctor.git"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"README.zh-CN.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.6.2",
|
|
38
|
+
"commander": "^14.0.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.3.3",
|
|
42
|
+
"tsup": "^8.5.1",
|
|
43
|
+
"tsx": "^4.21.0",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"author": "pdd <ywq1991@gmail.com>",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/Sobranier/openclaw-doctor/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/Sobranier/openclaw-doctor#readme"
|
|
51
|
+
}
|