volclaw 0.3.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.
Files changed (80) hide show
  1. package/CONTRIBUTING.md +105 -0
  2. package/LICENSE +21 -0
  3. package/README.aiclaw.md +22 -0
  4. package/README.aliclaw.md +22 -0
  5. package/README.autoopenclaw.md +22 -0
  6. package/README.claw-open.md +22 -0
  7. package/README.clawjs.md +22 -0
  8. package/README.fastclaw.md +22 -0
  9. package/README.md +22 -0
  10. package/README.md.bak +242 -0
  11. package/README.megaclaw.md +22 -0
  12. package/README.open-claw.md +22 -0
  13. package/README.openclaw-cli.md +239 -0
  14. package/README.openclaw-daemon.md +239 -0
  15. package/README.openclaw-gateway.md +239 -0
  16. package/README.openclaw-health.md +239 -0
  17. package/README.openclaw-helper.md +239 -0
  18. package/README.openclaw-install.md +239 -0
  19. package/README.openclaw-manage.md +239 -0
  20. package/README.openclaw-monitor.md +239 -0
  21. package/README.openclaw-run.md +239 -0
  22. package/README.openclaw-service.md +239 -0
  23. package/README.openclaw-setup.md +239 -0
  24. package/README.openclaw-start.md +239 -0
  25. package/README.openclaw-tools.md +239 -0
  26. package/README.openclaw-upgrade.md +13 -0
  27. package/README.openclaw-utils.md +239 -0
  28. package/README.openclaw-watch.md +239 -0
  29. package/README.qclaw-cli.md +22 -0
  30. package/README.qclaw.md +22 -0
  31. package/README.smartclaw.md +22 -0
  32. package/README.volclaw.md +22 -0
  33. package/README.zh-CN.md +213 -0
  34. package/app-dist/main.js +300 -0
  35. package/app-dist/package.json +3 -0
  36. package/app-dist/server-process.js +95 -0
  37. package/assets/demo.gif +0 -0
  38. package/assets/welcome.png +0 -0
  39. package/demo.tape +28 -0
  40. package/dist/chunk-LIZ6XXW3.js +1149 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +582 -0
  43. package/dist/server-ZYSNFLSO.js +7 -0
  44. package/homebrew/README.md +37 -0
  45. package/homebrew/openclaw-cli.rb +22 -0
  46. package/homebrew/openclaw-doctor.rb +26 -0
  47. package/package.aiclaw.json +25 -0
  48. package/package.aliclaw.json +25 -0
  49. package/package.autoopenclaw.json +25 -0
  50. package/package.claw-open.json +25 -0
  51. package/package.clawjs.json +25 -0
  52. package/package.fastclaw.json +25 -0
  53. package/package.json +25 -0
  54. package/package.json.bak +51 -0
  55. package/package.megaclaw.json +25 -0
  56. package/package.open-claw.json +25 -0
  57. package/package.openclaw-cli.json +51 -0
  58. package/package.openclaw-daemon.json +51 -0
  59. package/package.openclaw-gateway.json +51 -0
  60. package/package.openclaw-health.json +51 -0
  61. package/package.openclaw-helper.json +51 -0
  62. package/package.openclaw-install.json +51 -0
  63. package/package.openclaw-manage.json +51 -0
  64. package/package.openclaw-monitor.json +51 -0
  65. package/package.openclaw-run.json +51 -0
  66. package/package.openclaw-service.json +51 -0
  67. package/package.openclaw-setup.json +51 -0
  68. package/package.openclaw-start.json +51 -0
  69. package/package.openclaw-tools.json +51 -0
  70. package/package.openclaw-upgrade.json +50 -0
  71. package/package.openclaw-utils.json +51 -0
  72. package/package.openclaw-watch.json +51 -0
  73. package/package.qclaw-cli.json +25 -0
  74. package/package.qclaw.json +25 -0
  75. package/package.smartclaw.json +25 -0
  76. package/package.volclaw.json +25 -0
  77. package/scripts/post-app-compile.cjs +3 -0
  78. package/scripts/post-app-compile.js +7 -0
  79. package/scripts/publish.sh +120 -0
  80. package/scripts/publish.sh.bak +63 -0
@@ -0,0 +1,1149 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/dashboard/server.ts
10
+ import { createServer } from "http";
11
+ import { readFileSync as readFileSync4, existsSync as existsSync6, readdirSync as readdirSync4 } from "fs";
12
+ import { join as join7 } from "path";
13
+ import chalk2 from "chalk";
14
+
15
+ // src/config.ts
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
17
+ import { join as join2, resolve } from "path";
18
+
19
+ // src/brand.ts
20
+ import { basename } from "path";
21
+ import { homedir } from "os";
22
+ import { join } from "path";
23
+ var bin = basename(process.argv[1] ?? "openclaw-doctor").replace(/\.[cm]?js$/, "");
24
+ var KNOWN_BINS = [
25
+ "openclaw-cli",
26
+ "openclaw-manage",
27
+ "openclaw-doctor",
28
+ "openclaw-service",
29
+ "openclaw-daemon",
30
+ "openclaw-monitor",
31
+ "openclaw-helper",
32
+ "openclaw-tools",
33
+ "openclaw-utils",
34
+ "openclaw-gateway",
35
+ "openclaw-setup",
36
+ "openclaw-install",
37
+ "openclaw-run",
38
+ "openclaw-start",
39
+ "openclaw-watch",
40
+ "openclaw-health",
41
+ // new aliases
42
+ "qclaw",
43
+ "qclaw-cli",
44
+ "autoopenclaw",
45
+ "claw-open",
46
+ "open-claw",
47
+ "clawjs",
48
+ "aliclaw",
49
+ "fastclaw",
50
+ "smartclaw",
51
+ "aiclaw",
52
+ "megaclaw",
53
+ "volclaw"
54
+ ];
55
+ var BINARY_NAME = KNOWN_BINS.includes(bin) ? bin : "openclaw-cli";
56
+ var APP_HOME = join(homedir(), ".openclaw-doctor");
57
+ var DISPLAY_NAMES = {
58
+ "openclaw-cli": "OpenClaw CLI",
59
+ "openclaw-manage": "OpenClaw Manage",
60
+ "openclaw-doctor": "OpenClaw Doctor",
61
+ "openclaw-service": "OpenClaw Service",
62
+ "openclaw-daemon": "OpenClaw Daemon",
63
+ "openclaw-monitor": "OpenClaw Monitor",
64
+ "openclaw-helper": "OpenClaw Helper",
65
+ "openclaw-tools": "OpenClaw Tools",
66
+ "openclaw-utils": "OpenClaw Utils",
67
+ "openclaw-gateway": "OpenClaw Gateway",
68
+ "openclaw-setup": "OpenClaw Setup",
69
+ "openclaw-install": "OpenClaw Install",
70
+ "openclaw-run": "OpenClaw Run",
71
+ "openclaw-start": "OpenClaw Start",
72
+ "openclaw-watch": "OpenClaw Watch",
73
+ "openclaw-health": "OpenClaw Health",
74
+ // new aliases
75
+ "qclaw": "QClaw",
76
+ "qclaw-cli": "QClaw CLI",
77
+ "autoopenclaw": "AutoOpenClaw",
78
+ "claw-open": "Claw Open",
79
+ "open-claw": "Open Claw",
80
+ "clawjs": "ClawJS",
81
+ "aliclaw": "AliClaw",
82
+ "fastclaw": "FastClaw",
83
+ "smartclaw": "SmartClaw",
84
+ "aiclaw": "AIClaw",
85
+ "megaclaw": "MegaClaw",
86
+ "volclaw": "VolClaw"
87
+ };
88
+ var DISPLAY_NAME = DISPLAY_NAMES[BINARY_NAME] ?? "OpenClaw Doctor";
89
+
90
+ // src/config.ts
91
+ var DOCTOR_HOME = APP_HOME;
92
+ var CONFIG_PATH = join2(APP_HOME, "config.json");
93
+ var DOCTOR_LOG_DIR = join2(APP_HOME, "logs");
94
+ var PID_FILE = join2(APP_HOME, "daemon.pid");
95
+ var defaults = {
96
+ checkInterval: 30,
97
+ failThreshold: 5,
98
+ dashboardPort: 9090,
99
+ maxRestartsPerHour: 5,
100
+ openclawProfile: "default",
101
+ notify: {
102
+ webhook: {
103
+ enabled: false,
104
+ url: "",
105
+ bodyTemplate: '{"msgtype":"text","text":{"content":"{{message}}"}}'
106
+ },
107
+ system: {
108
+ enabled: true
109
+ }
110
+ }
111
+ };
112
+ function ensureDoctorHome() {
113
+ if (!existsSync(DOCTOR_HOME)) {
114
+ mkdirSync(DOCTOR_HOME, { recursive: true });
115
+ }
116
+ if (!existsSync(DOCTOR_LOG_DIR)) {
117
+ mkdirSync(DOCTOR_LOG_DIR, { recursive: true });
118
+ }
119
+ }
120
+ var LOCAL_CONFIG = resolve(process.cwd(), "doctor.config.json");
121
+ function resolveConfigPath(configPath) {
122
+ if (configPath) return configPath;
123
+ if (existsSync(LOCAL_CONFIG)) return LOCAL_CONFIG;
124
+ return CONFIG_PATH;
125
+ }
126
+ function loadConfig(configPath) {
127
+ const file = resolveConfigPath(configPath);
128
+ if (existsSync(file)) {
129
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
130
+ return {
131
+ ...defaults,
132
+ ...raw,
133
+ notify: {
134
+ webhook: { ...defaults.notify.webhook, ...raw.notify?.webhook ?? {} },
135
+ system: { ...defaults.notify.system, ...raw.notify?.system ?? {} }
136
+ }
137
+ };
138
+ }
139
+ ensureDoctorHome();
140
+ writeFileSync(CONFIG_PATH, JSON.stringify(defaults, null, 2) + "\n");
141
+ return { ...defaults };
142
+ }
143
+
144
+ // src/core/openclaw.ts
145
+ import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync } from "fs";
146
+ import { join as join3 } from "path";
147
+ import { homedir as homedir2 } from "os";
148
+ import { execSync } from "child_process";
149
+ import { exec } from "child_process";
150
+ import { promisify } from "util";
151
+ import * as http from "http";
152
+ var execAsync = promisify(exec);
153
+ function getOpenClawHome(profile) {
154
+ if (profile === "dev") return join3(homedir2(), ".openclaw-dev");
155
+ if (profile !== "default") return join3(homedir2(), `.openclaw-${profile}`);
156
+ return join3(homedir2(), ".openclaw");
157
+ }
158
+ function findOpenClawBin() {
159
+ const plistDir = join3(homedir2(), "Library", "LaunchAgents");
160
+ if (existsSync2(plistDir)) {
161
+ const plists = readdirSync(plistDir).filter(
162
+ (f) => f.includes("openclaw") && f.endsWith(".plist")
163
+ );
164
+ for (const plist of plists) {
165
+ const content = readFileSync2(join3(plistDir, plist), "utf-8");
166
+ const nodeMatch = content.match(
167
+ /<string>(\/[^<]*\/bin\/node)<\/string>/
168
+ );
169
+ const cliMatch = content.match(
170
+ /<string>(\/[^<]*openclaw[^<]*\.(?:js|mjs))<\/string>/
171
+ );
172
+ if (nodeMatch && cliMatch) {
173
+ return { nodePath: nodeMatch[1], cliBinPath: cliMatch[1] };
174
+ }
175
+ }
176
+ }
177
+ try {
178
+ const bin2 = execSync("which openclaw", { encoding: "utf-8" }).trim();
179
+ if (bin2) return { nodePath: process.execPath, cliBinPath: bin2 };
180
+ } catch {
181
+ }
182
+ return null;
183
+ }
184
+ function findLaunchdLabel() {
185
+ const plistDir = join3(homedir2(), "Library", "LaunchAgents");
186
+ if (!existsSync2(plistDir)) return "ai.openclaw.gateway";
187
+ const plists = readdirSync(plistDir).filter(
188
+ (f) => f.includes("openclaw") && f.endsWith(".plist")
189
+ );
190
+ if (plists.length > 0) {
191
+ return plists[0].replace(".plist", "");
192
+ }
193
+ return "ai.openclaw.gateway";
194
+ }
195
+ function detectOpenClaw(profile = "default") {
196
+ const home = getOpenClawHome(profile);
197
+ const configPath = join3(home, "openclaw.json");
198
+ const logDir2 = join3(home, "logs");
199
+ const defaults2 = {
200
+ configPath,
201
+ gatewayPort: 18789,
202
+ gatewayToken: "",
203
+ launchdLabel: findLaunchdLabel(),
204
+ nodePath: process.execPath,
205
+ cliBinPath: "",
206
+ logDir: logDir2,
207
+ profile,
208
+ channels: [],
209
+ agents: [],
210
+ version: null
211
+ };
212
+ const binInfo = findOpenClawBin();
213
+ if (binInfo) {
214
+ defaults2.nodePath = binInfo.nodePath;
215
+ defaults2.cliBinPath = binInfo.cliBinPath;
216
+ }
217
+ if (!existsSync2(configPath)) {
218
+ return defaults2;
219
+ }
220
+ try {
221
+ const raw = JSON.parse(readFileSync2(configPath, "utf-8"));
222
+ defaults2.gatewayPort = raw.gateway?.port ?? defaults2.gatewayPort;
223
+ defaults2.gatewayToken = raw.gateway?.auth?.token ?? "";
224
+ defaults2.version = raw.meta?.lastTouchedVersion ?? null;
225
+ if (raw.channels) {
226
+ defaults2.channels = Object.entries(raw.channels).filter(([, v]) => v.enabled !== false).map(([k]) => k);
227
+ }
228
+ if (raw.agents?.list) {
229
+ defaults2.agents = raw.agents.list.map(
230
+ (a) => ({
231
+ id: a.id,
232
+ name: a.name ?? a.id,
233
+ isDefault: a.default ?? false,
234
+ model: typeof a.model === "string" ? a.model : a.model?.primary ?? raw.agents?.defaults?.model?.primary ?? void 0,
235
+ workspace: a.workspace
236
+ })
237
+ );
238
+ }
239
+ } catch {
240
+ }
241
+ return defaults2;
242
+ }
243
+ async function runOpenClawCmd(info, args) {
244
+ if (!info.cliBinPath) return null;
245
+ try {
246
+ const { stdout } = await execAsync(`"${info.nodePath}" "${info.cliBinPath}" ${args}`, {
247
+ timeout: 3e4,
248
+ env: { ...process.env, NODE_NO_WARNINGS: "1" }
249
+ });
250
+ return stdout.trim();
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+ async function getGatewayHealthHttp(info) {
256
+ return new Promise((resolve3) => {
257
+ const url = `http://127.0.0.1:${info.gatewayPort}/health`;
258
+ const req = http.get(url, { timeout: 5e3 }, (res) => {
259
+ let data = "";
260
+ res.on("data", (chunk) => {
261
+ data += chunk;
262
+ });
263
+ res.on("end", () => {
264
+ try {
265
+ resolve3(JSON.parse(data));
266
+ } catch {
267
+ resolve3(null);
268
+ }
269
+ });
270
+ });
271
+ req.on("error", () => resolve3(null));
272
+ req.on("timeout", () => {
273
+ req.destroy();
274
+ resolve3(null);
275
+ });
276
+ });
277
+ }
278
+ async function getGatewayHealth(info) {
279
+ const raw = await runOpenClawCmd(info, "health --json");
280
+ if (!raw) return null;
281
+ try {
282
+ const jsonStart = raw.indexOf("{");
283
+ if (jsonStart === -1) return null;
284
+ return JSON.parse(raw.slice(jsonStart));
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+ function getRestartCommand(info) {
290
+ const uid = process.getuid?.() ?? 501;
291
+ return `launchctl kickstart -k gui/${uid}/${info.launchdLabel}`;
292
+ }
293
+ function getStopCommand(info) {
294
+ const uid = process.getuid?.() ?? 501;
295
+ return `launchctl kill SIGTERM gui/${uid}/${info.launchdLabel}`;
296
+ }
297
+ function getStartCommand(info) {
298
+ const uid = process.getuid?.() ?? 501;
299
+ return `launchctl kickstart gui/${uid}/${info.launchdLabel}`;
300
+ }
301
+
302
+ // src/core/logger.ts
303
+ import { appendFileSync } from "fs";
304
+ import { join as join4 } from "path";
305
+ import chalk from "chalk";
306
+ var logDir = DOCTOR_LOG_DIR;
307
+ function initLogger(dir) {
308
+ logDir = dir ?? DOCTOR_LOG_DIR;
309
+ ensureDoctorHome();
310
+ }
311
+ function getLogFile() {
312
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
313
+ return join4(logDir, `${date}.log`);
314
+ }
315
+ function log(level, message) {
316
+ const time = (/* @__PURE__ */ new Date()).toISOString();
317
+ const line = `[${time}] [${level.toUpperCase()}] ${message}`;
318
+ const colorFn = level === "error" ? chalk.red : level === "warn" ? chalk.yellow : level === "success" ? chalk.green : chalk.blue;
319
+ console.log(colorFn(line));
320
+ try {
321
+ appendFileSync(getLogFile(), line + "\n");
322
+ } catch {
323
+ }
324
+ }
325
+ var checkHistory = [];
326
+ var restartHistory = [];
327
+ var MAX_HISTORY = 100;
328
+ function addCheckRecord(record) {
329
+ checkHistory.push(record);
330
+ if (checkHistory.length > MAX_HISTORY) checkHistory.shift();
331
+ }
332
+ function addRestartRecord(record) {
333
+ restartHistory.push(record);
334
+ if (restartHistory.length > MAX_HISTORY) restartHistory.shift();
335
+ }
336
+ function getCheckHistory() {
337
+ return [...checkHistory];
338
+ }
339
+ function getRestartHistory() {
340
+ return [...restartHistory];
341
+ }
342
+
343
+ // src/core/health-checker.ts
344
+ async function checkHealth(info) {
345
+ const start = Date.now();
346
+ let health = await getGatewayHealthHttp(info);
347
+ if (!health) {
348
+ log("warn", "HTTP probe failed, falling back to CLI health check");
349
+ health = await getGatewayHealth(info);
350
+ }
351
+ const durationMs = Date.now() - start;
352
+ if (!health) {
353
+ const error = "Gateway unreachable (openclaw health failed)";
354
+ addCheckRecord({
355
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
356
+ healthy: false,
357
+ error,
358
+ responseTime: durationMs
359
+ });
360
+ log("error", `Health check failed: ${error} (${durationMs}ms)`);
361
+ return { healthy: false, gateway: false, channels: [], agentRuntimes: [], durationMs, error };
362
+ }
363
+ const channels = health.channels ? Object.entries(health.channels).map(([name, ch]) => ({
364
+ name,
365
+ ok: ch.probe?.ok ?? false
366
+ })) : [];
367
+ const healthy = health.ok;
368
+ const agentRuntimes = health.agents ?? [];
369
+ addCheckRecord({
370
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
371
+ healthy,
372
+ responseTime: durationMs
373
+ });
374
+ if (healthy) {
375
+ log("success", `Health OK \u2014 gateway up, ${channels.length} channels (${durationMs}ms)`);
376
+ } else {
377
+ log("warn", `Health degraded \u2014 gateway responded but ok=false (${durationMs}ms)`);
378
+ }
379
+ return { healthy, gateway: true, channels, agentRuntimes, durationMs, raw: health };
380
+ }
381
+
382
+ // src/core/process-manager.ts
383
+ import { exec as exec2 } from "child_process";
384
+ import { promisify as promisify2 } from "util";
385
+ var execAsync2 = promisify2(exec2);
386
+ async function runShell(command) {
387
+ try {
388
+ const { stdout } = await execAsync2(command, { timeout: 12e4 });
389
+ return { success: true, output: stdout.trim() };
390
+ } catch (err) {
391
+ const error = err instanceof Error ? err.message : String(err);
392
+ return { success: false, error };
393
+ }
394
+ }
395
+ async function restartGateway(info) {
396
+ const cmd = getRestartCommand(info);
397
+ log("warn", `Restarting gateway: ${cmd}`);
398
+ const result = await runShell(cmd);
399
+ if (result.success) {
400
+ log("success", "Gateway restarted");
401
+ } else {
402
+ log("error", `Gateway restart failed: ${result.error}`);
403
+ }
404
+ addRestartRecord({
405
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
406
+ reason: "health check failed",
407
+ success: result.success
408
+ });
409
+ return result;
410
+ }
411
+ async function startGateway(info) {
412
+ const cmd = getStartCommand(info);
413
+ log("info", `Starting gateway: ${cmd}`);
414
+ const result = await runShell(cmd);
415
+ if (result.success) log("success", "Gateway started");
416
+ else log("error", `Gateway start failed: ${result.error}`);
417
+ return result;
418
+ }
419
+ async function stopGateway(info) {
420
+ const cmd = getStopCommand(info);
421
+ log("info", `Stopping gateway: ${cmd}`);
422
+ const result = await runShell(cmd);
423
+ if (result.success) log("success", "Gateway stopped");
424
+ else log("error", `Gateway stop failed: ${result.error}`);
425
+ return result;
426
+ }
427
+ var RestartThrottle = class {
428
+ constructor(maxPerHour) {
429
+ this.maxPerHour = maxPerHour;
430
+ }
431
+ timestamps = [];
432
+ canRestart() {
433
+ const oneHourAgo = Date.now() - 36e5;
434
+ this.timestamps = this.timestamps.filter((t) => t > oneHourAgo);
435
+ return this.timestamps.length < this.maxPerHour;
436
+ }
437
+ record() {
438
+ this.timestamps.push(Date.now());
439
+ }
440
+ recentCount() {
441
+ const oneHourAgo = Date.now() - 36e5;
442
+ return this.timestamps.filter((t) => t > oneHourAgo).length;
443
+ }
444
+ };
445
+
446
+ // src/core/workspace-scanner.ts
447
+ import { existsSync as existsSync4, statSync, readdirSync as readdirSync2 } from "fs";
448
+ import { join as join5 } from "path";
449
+ import { homedir as homedir3 } from "os";
450
+ function expandHome(p) {
451
+ return p.startsWith("~/") ? join5(homedir3(), p.slice(2)) : p;
452
+ }
453
+ function dirSizeKB(dir, depth = 0) {
454
+ if (depth > 4 || !existsSync4(dir)) return 0;
455
+ let total = 0;
456
+ try {
457
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
458
+ if (["node_modules", ".git", ".DS_Store"].includes(entry.name)) continue;
459
+ const full = join5(dir, entry.name);
460
+ if (entry.isDirectory()) {
461
+ total += dirSizeKB(full, depth + 1);
462
+ } else {
463
+ try {
464
+ total += statSync(full).size;
465
+ } catch {
466
+ }
467
+ }
468
+ }
469
+ } catch {
470
+ }
471
+ return Math.round(total / 1024);
472
+ }
473
+ function scanWorkspaces(info) {
474
+ const results = [];
475
+ for (const agent of info.agents) {
476
+ const workspaceRaw = agent.workspace;
477
+ if (!workspaceRaw) continue;
478
+ const workspacePath = expandHome(workspaceRaw);
479
+ let memoryFileSizeKB = 0;
480
+ try {
481
+ const memPath = join5(workspacePath, "MEMORY.md");
482
+ if (existsSync4(memPath)) {
483
+ memoryFileSizeKB = Math.round(statSync(memPath).size / 1024);
484
+ }
485
+ } catch {
486
+ }
487
+ let totalWorkspaceSizeKB = 0;
488
+ try {
489
+ totalWorkspaceSizeKB = dirSizeKB(workspacePath);
490
+ } catch {
491
+ }
492
+ let sessionCount = 0;
493
+ try {
494
+ const sessDir = join5(homedir3(), ".openclaw", "agents", agent.id, "sessions");
495
+ if (existsSync4(sessDir)) {
496
+ sessionCount = readdirSync2(sessDir).filter((f) => f.endsWith(".jsonl") || f.endsWith(".json")).length;
497
+ }
498
+ } catch {
499
+ }
500
+ results.push({
501
+ agentId: agent.id,
502
+ agentName: agent.name,
503
+ workspacePath,
504
+ memoryFileSizeKB,
505
+ memoryWarning: memoryFileSizeKB > 50,
506
+ totalWorkspaceSizeKB,
507
+ sessionCount,
508
+ model: agent.model
509
+ });
510
+ }
511
+ return results;
512
+ }
513
+
514
+ // src/core/cost-scanner.ts
515
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
516
+ import { join as join6 } from "path";
517
+ import { homedir as homedir4 } from "os";
518
+ function parseSessionCosts(filePath, sinceMs) {
519
+ let cost = 0;
520
+ let tokens = 0;
521
+ try {
522
+ const lines = readFileSync3(filePath, "utf-8").split("\n").filter(Boolean);
523
+ for (const line of lines) {
524
+ try {
525
+ const msg = JSON.parse(line);
526
+ if (msg.type !== "message" || !msg.message?.usage?.cost) continue;
527
+ const ts = msg.timestamp ? new Date(msg.timestamp).getTime() : msg.message?.timestamp ?? 0;
528
+ if (ts < sinceMs) continue;
529
+ cost += msg.message.usage.cost.total ?? 0;
530
+ tokens += msg.message.usage.totalTokens ?? 0;
531
+ } catch {
532
+ }
533
+ }
534
+ } catch {
535
+ }
536
+ return { cost, tokens };
537
+ }
538
+ function scanCosts(agents) {
539
+ const now = Date.now();
540
+ const todayStart = /* @__PURE__ */ new Date();
541
+ todayStart.setHours(0, 0, 0, 0);
542
+ const weekStart = now - 7 * 24 * 3600 * 1e3;
543
+ const result = [];
544
+ for (const agent of agents) {
545
+ const sessDir = join6(homedir4(), ".openclaw", "agents", agent.id, "sessions");
546
+ if (!existsSync5(sessDir)) continue;
547
+ let todayCost = 0, weekCost = 0, totalTokens = 0, sessionCount = 0;
548
+ const files = readdirSync3(sessDir).filter((f) => f.endsWith(".jsonl"));
549
+ sessionCount = files.length;
550
+ for (const file of files) {
551
+ const fpath = join6(sessDir, file);
552
+ try {
553
+ const mtime = statSync2(fpath).mtimeMs;
554
+ if (mtime < weekStart) continue;
555
+ } catch {
556
+ continue;
557
+ }
558
+ const week = parseSessionCosts(fpath, weekStart);
559
+ weekCost += week.cost;
560
+ totalTokens += week.tokens;
561
+ const today = parseSessionCosts(fpath, todayStart.getTime());
562
+ todayCost += today.cost;
563
+ }
564
+ result.push({ agentId: agent.id, agentName: agent.name, todayCost, weekCost, totalTokens, sessionCount });
565
+ }
566
+ return {
567
+ agents: result,
568
+ todayTotal: result.reduce((s, a) => s + a.todayCost, 0),
569
+ weekTotal: result.reduce((s, a) => s + a.weekCost, 0),
570
+ currency: "USD"
571
+ };
572
+ }
573
+
574
+ // src/dashboard/server.ts
575
+ var _PKG_VER = true ? "0.3.1" : "0.2.1";
576
+ var pkgVersion = _PKG_VER;
577
+ function readDoctorLogs(maxLines = 50) {
578
+ if (!existsSync6(DOCTOR_LOG_DIR)) return [];
579
+ const files = readdirSync4(DOCTOR_LOG_DIR).filter((f) => f.endsWith(".log")).sort().reverse();
580
+ if (files.length === 0) return [];
581
+ const content = readFileSync4(join7(DOCTOR_LOG_DIR, files[0]), "utf-8");
582
+ const lines = content.trim().split("\n");
583
+ return lines.slice(-maxLines);
584
+ }
585
+ function renderShell() {
586
+ return `<!DOCTYPE html>
587
+ <html lang="en">
588
+ <head>
589
+ <meta charset="utf-8">
590
+ <meta name="viewport" content="width=device-width, initial-scale=1">
591
+ <title>OpenClaw Doctor</title>
592
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
593
+ <style>
594
+ * { margin:0; padding:0; box-sizing:border-box; }
595
+ body { font-family:system-ui,-apple-system,sans-serif; background:#050810; color:#f0f4ff; min-height:100vh; }
596
+
597
+ /* Navbar */
598
+ .navbar { display:flex; align-items:center; justify-content:space-between; padding:0.75rem 1.5rem; background:#0d1424; border-bottom:1px solid #1a2744; flex-wrap:wrap; gap:0.5rem; }
599
+ .nav-left { display:flex; align-items:center; gap:0.5rem; font-weight:700; font-size:1rem; white-space:nowrap; }
600
+ .nav-left .ver { font-weight:400; color:#6b7fa3; font-size:0.8rem; }
601
+ .nav-center { display:flex; align-items:center; gap:0.5rem; }
602
+ .status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
603
+ .status-label { font-weight:600; font-size:0.9rem; }
604
+ .nav-right { color:#6b7fa3; font-size:0.75rem; white-space:nowrap; }
605
+
606
+ /* Tabs */
607
+ .tabs { display:flex; border-bottom:1px solid #1a2744; background:#0d1424; overflow-x:auto; }
608
+ .tab { padding:0.65rem 1.25rem; cursor:pointer; color:#6b7fa3; font-size:0.85rem; border-bottom:2px solid transparent; white-space:nowrap; transition:color 0.15s; }
609
+ .tab:hover { color:#f0f4ff; }
610
+ .tab.active { color:#f0f4ff; border-bottom-color:#007AFF; }
611
+
612
+ /* Content */
613
+ .content { padding:1.5rem; max-width:1200px; margin:0 auto; }
614
+
615
+ /* Cards */
616
+ .card { background:#0d1424; border:1px solid #1a2744; border-radius:12px; padding:1.25rem; margin-bottom:1rem; }
617
+ .card-title { color:#6b7fa3; font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.5rem; }
618
+
619
+ .big-status { font-size:2rem; font-weight:700; }
620
+ .meta-row { color:#6b7fa3; font-size:0.8rem; margin-top:0.25rem; }
621
+
622
+ .grid2 { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
623
+ @media (max-width:768px) { .grid2 { grid-template-columns:1fr; } }
624
+
625
+ /* Tables */
626
+ table { width:100%; border-collapse:collapse; }
627
+ th, td { text-align:left; padding:0.4rem 0.75rem; border-bottom:1px solid #1a2744; font-size:0.8rem; }
628
+ th { color:#6b7fa3; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.05em; }
629
+
630
+ /* Tags */
631
+ .tag { display:inline-block; padding:0.15rem 0.5rem; border-radius:9999px; font-size:0.7rem; font-weight:600; }
632
+ .tag-ok { background:rgba(0,166,126,0.15); color:#00A67E; }
633
+ .tag-fail { background:#ef444422; color:#ef4444; }
634
+ .tag-default { background:rgba(0,122,255,0.15); color:#007AFF; font-size:0.65rem; margin-left:0.35rem; }
635
+
636
+ /* Buttons */
637
+ .btn { padding:0.5rem 1rem; border:none; border-radius:0.375rem; cursor:pointer; font-size:0.8rem; font-weight:600; transition:opacity 0.15s; }
638
+ .btn:hover { opacity:0.85; }
639
+ .btn:disabled { opacity:0.5; cursor:not-allowed; }
640
+ .btn-blue { background:#007AFF; color:#fff; }
641
+ .btn-amber { background:#f59e0b; color:#fff; }
642
+ .btn-group { display:flex; gap:0.5rem; flex-wrap:wrap; }
643
+
644
+ /* Result box */
645
+ .result-box { margin-top:0.75rem; padding:0.75rem; background:#030609; border-radius:8px; font-size:0.75rem; font-family:ui-monospace,monospace; white-space:pre-wrap; word-break:break-all; max-height:200px; overflow-y:auto; }
646
+
647
+ /* Logs */
648
+ .log-line { font-family:ui-monospace,monospace; font-size:0.75rem; padding:0.2rem 0; line-height:1.4; word-break:break-all; }
649
+ .log-info { color:#6b7fa3; }
650
+ .log-warn { color:#eab308; }
651
+ .log-error { color:#ef4444; }
652
+ .log-success { color:#00A67E; }
653
+
654
+ /* Config */
655
+ .cfg-row { display:flex; justify-content:space-between; padding:0.5rem 0; border-bottom:1px solid #1a2744; font-size:0.85rem; }
656
+ .cfg-key { color:#6b7fa3; }
657
+ .cfg-val { color:#f0f4ff; font-family:ui-monospace,monospace; }
658
+
659
+ /* Loading */
660
+ .loading { color:#6b7fa3; text-align:center; padding:3rem 0; }
661
+ </style>
662
+ </head>
663
+ <body x-data="dashboard()" x-init="init()">
664
+
665
+ <!-- Navbar -->
666
+ <div class="navbar">
667
+ <div class="nav-left">
668
+ <span>&#129438; OpenClaw Doctor</span>
669
+ <span class="ver">v${pkgVersion}</span>
670
+ </div>
671
+ <div class="nav-center">
672
+ <span class="status-dot" :style="'background:' + statusColor"></span>
673
+ <span class="status-label" :style="'color:' + statusColor" x-text="statusText"></span>
674
+ </div>
675
+ <div class="nav-right" x-text="lastCheck ? 'Updated ' + lastCheck : 'Loading...'"></div>
676
+ </div>
677
+
678
+ <!-- Tabs -->
679
+ <div class="tabs">
680
+ <template x-for="t in ['Overview','Cost','Restarts','Logs','Config']">
681
+ <div class="tab" :class="{ active: tab === t }" @click="tab = t" x-text="t"></div>
682
+ </template>
683
+ </div>
684
+
685
+ <!-- Content -->
686
+ <div class="content">
687
+
688
+ <!-- Loading state -->
689
+ <template x-if="!loaded">
690
+ <div class="loading">Loading...</div>
691
+ </template>
692
+
693
+ <!-- Overview Tab -->
694
+ <template x-if="loaded && tab === 'Overview'">
695
+ <div>
696
+ <div class="card">
697
+ <div class="big-status" :style="'color:' + statusColor" x-text="statusText"></div>
698
+ <div class="meta-row">
699
+ Gateway :<span x-text="data.info?.gatewayPort ?? '?'"></span>
700
+ &nbsp;|&nbsp; Latency: <span x-text="(data.durationMs ?? '-') + 'ms'"></span>
701
+ &nbsp;|&nbsp; Profile: <span x-text="data.info?.profile ?? '?'"></span>
702
+ &nbsp;|&nbsp; OpenClaw <span x-text="data.info?.version ?? '?'"></span>
703
+ </div>
704
+ </div>
705
+
706
+ <div class="grid2">
707
+ <!-- Channels -->
708
+ <div class="card">
709
+ <div class="card-title">Channels</div>
710
+ <template x-if="data.channels && data.channels.length > 0">
711
+ <table>
712
+ <thead><tr><th>Name</th><th>Status</th></tr></thead>
713
+ <tbody>
714
+ <template x-for="c in data.channels" :key="c.name">
715
+ <tr>
716
+ <td x-text="c.name"></td>
717
+ <td><span class="tag" :class="c.ok ? 'tag-ok' : 'tag-fail'" x-text="c.ok ? 'OK' : 'FAIL'"></span></td>
718
+ </tr>
719
+ </template>
720
+ </tbody>
721
+ </table>
722
+ </template>
723
+ <template x-if="!data.channels || data.channels.length === 0">
724
+ <div style="color:#6b7fa3;font-size:0.8rem;">No channels</div>
725
+ </template>
726
+ </div>
727
+
728
+ <!-- Agents -->
729
+ <div class="card">
730
+ <div class="card-title">Agents</div>
731
+ <template x-if="data.agents && data.agents.length > 0">
732
+ <table>
733
+ <thead><tr><th>Name</th><th>Model</th><th>Sessions</th><th>Last Active</th><th></th></tr></thead>
734
+ <tbody>
735
+ <template x-for="a in data.agents" :key="a.id">
736
+ <tr x-data="{ rt() { return (data.agentRuntimes||[]).find(r=>r.agentId===a.id) } }">
737
+ <td x-text="a.name"></td>
738
+ <td style="color:#6b7fa3;font-size:0.78rem;" x-text="a.model ? a.model.replace('anthropic/','').replace('openai/','') : '\u2014'"></td>
739
+ <td style="color:#6b7fa3;font-size:0.78rem;" x-text="rt()?.sessions?.count ?? '\u2014'"></td>
740
+ <td style="font-size:0.78rem;" x-text="rt()?.sessions?.recent?.[0]?.age != null ? (rt().sessions.recent[0].age < 60000 ? 'just now' : rt().sessions.recent[0].age < 3600000 ? Math.floor(rt().sessions.recent[0].age/60000)+'m ago' : Math.floor(rt().sessions.recent[0].age/3600000)+'h ago') : '\u2014'"></td>
741
+ <td><template x-if="a.isDefault"><span class="tag tag-default">default</span></template></td>
742
+ </tr>
743
+ </template>
744
+ </tbody>
745
+ </table>
746
+ </template>
747
+ <template x-if="!data.agents || data.agents.length === 0">
748
+ <div style="color:#6b7fa3;font-size:0.8rem;">No agents</div>
749
+ </template>
750
+ </div>
751
+ </div>
752
+
753
+ <!-- Recent checks -->
754
+ <div class="card">
755
+ <div class="card-title">Recent Health Checks</div>
756
+ <template x-if="data.checks && data.checks.length > 0">
757
+ <div style="overflow-x:auto;">
758
+ <table>
759
+ <thead><tr><th>Time</th><th>Status</th><th>Latency</th><th>Error</th></tr></thead>
760
+ <tbody>
761
+ <template x-for="c in data.checks.slice().reverse().slice(0, 10)" :key="c.timestamp">
762
+ <tr>
763
+ <td x-text="fmtTime(c.timestamp)"></td>
764
+ <td><span class="tag" :class="c.healthy ? 'tag-ok' : 'tag-fail'" x-text="c.healthy ? 'OK' : 'FAIL'"></span></td>
765
+ <td x-text="(c.responseTime ?? '-') + 'ms'"></td>
766
+ <td style="color:#ef4444;" x-text="c.error ?? ''"></td>
767
+ </tr>
768
+ </template>
769
+ </tbody>
770
+ </table>
771
+ </div>
772
+ </template>
773
+ <template x-if="!data.checks || data.checks.length === 0">
774
+ <div style="color:#6b7fa3;font-size:0.8rem;">No checks yet</div>
775
+ </template>
776
+ </div>
777
+ </div>
778
+ </template>
779
+
780
+
781
+ <!-- Workspace Health -->
782
+ <div class="card">
783
+ <div class="card-title">Workspace Health</div>
784
+ <template x-if="data.workspaces && data.workspaces.length > 0">
785
+ <table>
786
+ <thead><tr><th>Agent</th><th>MEMORY.md</th><th>Sessions</th><th>Workspace Size</th><th></th></tr></thead>
787
+ <tbody>
788
+ <template x-for="w in data.workspaces" :key="w.agentId">
789
+ <tr>
790
+ <td x-text="w.agentName"></td>
791
+ <td x-text="w.memoryFileSizeKB + ' KB'"></td>
792
+ <td x-text="w.sessionCount"></td>
793
+ <td x-text="w.totalWorkspaceSizeKB + ' KB'"></td>
794
+ <td>
795
+ <template x-if="w.memoryWarning">
796
+ <span class="tag" style="background:rgba(245,158,11,0.15);color:#f59e0b;">\u26A0 Large</span>
797
+ </template>
798
+ </td>
799
+ </tr>
800
+ </template>
801
+ </tbody>
802
+ </table>
803
+ </template>
804
+ <template x-if="!data.workspaces || data.workspaces.length === 0">
805
+ <div style="color:#6b7fa3;font-size:0.8rem;">No workspace data</div>
806
+ </template>
807
+ </div>
808
+
809
+ <!-- Restarts Tab -->
810
+ <template x-if="loaded && tab === 'Restarts'">
811
+ <div>
812
+ <div class="card">
813
+ <div class="card-title">Actions</div>
814
+ <div class="btn-group">
815
+ <button class="btn btn-blue" :disabled="actionLoading" @click="doRestart()">Restart Gateway</button>
816
+ <button class="btn btn-amber" :disabled="actionLoading" @click="doDoctor()">Run Doctor Fix</button>
817
+ </div>
818
+ <template x-if="actionResult">
819
+ <div class="result-box" x-text="actionResult"></div>
820
+ </template>
821
+ </div>
822
+
823
+ <div class="card">
824
+ <div class="card-title">Restart History</div>
825
+ <template x-if="data.restarts && data.restarts.length > 0">
826
+ <div style="overflow-x:auto;">
827
+ <table>
828
+ <thead><tr><th>Time</th><th>Reason</th><th>Result</th></tr></thead>
829
+ <tbody>
830
+ <template x-for="r in data.restarts.slice().reverse()" :key="r.timestamp">
831
+ <tr>
832
+ <td x-text="fmtTime(r.timestamp)"></td>
833
+ <td x-text="r.reason"></td>
834
+ <td><span class="tag" :class="r.success ? 'tag-ok' : 'tag-fail'" x-text="r.success ? 'OK' : 'FAIL'"></span></td>
835
+ </tr>
836
+ </template>
837
+ </tbody>
838
+ </table>
839
+ </div>
840
+ </template>
841
+ <template x-if="!data.restarts || data.restarts.length === 0">
842
+ <div style="color:#6b7fa3;font-size:0.8rem;">No restarts</div>
843
+ </template>
844
+ </div>
845
+ </div>
846
+ </template>
847
+
848
+ <!-- Logs Tab -->
849
+ <template x-if="loaded && tab === 'Logs'">
850
+ <div>
851
+ <div class="card">
852
+ <div class="card-title">Doctor Logs (latest 50)</div>
853
+ <template x-if="logs.length > 0">
854
+ <div style="max-height:600px;overflow-y:auto;">
855
+ <template x-for="(line, i) in logs" :key="i">
856
+ <div class="log-line" :class="logClass(line)" x-text="line"></div>
857
+ </template>
858
+ </div>
859
+ </template>
860
+ <template x-if="logs.length === 0">
861
+ <div style="color:#6b7fa3;font-size:0.8rem;">No logs yet</div>
862
+ </template>
863
+ </div>
864
+ </div>
865
+ </template>
866
+
867
+ <!-- Cost Tab -->
868
+ <template x-if="loaded && tab === 'Cost'">
869
+ <div>
870
+ <template x-if="costData">
871
+ <div>
872
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
873
+ <div class="card" style="text-align:center;">
874
+ <div class="card-title">Today</div>
875
+ <div style="font-size:2rem;font-weight:700;color:#f0f4ff;" x-text="'$' + (costData.todayTotal||0).toFixed(4)"></div>
876
+ </div>
877
+ <div class="card" style="text-align:center;">
878
+ <div class="card-title">This Week</div>
879
+ <div style="font-size:2rem;font-weight:700;color:#f0f4ff;" x-text="'$' + (costData.weekTotal||0).toFixed(4)"></div>
880
+ </div>
881
+ </div>
882
+ <div class="card">
883
+ <div class="card-title">By Agent</div>
884
+ <table>
885
+ <thead><tr><th>Agent</th><th>Today</th><th>This Week</th><th>Sessions</th></tr></thead>
886
+ <tbody>
887
+ <template x-for="a in costData.agents" :key="a.agentId">
888
+ <tr>
889
+ <td x-text="a.agentName"></td>
890
+ <td x-text="'$' + (a.todayCost||0).toFixed(4)"></td>
891
+ <td x-text="'$' + (a.weekCost||0).toFixed(4)"></td>
892
+ <td style="color:#6b7fa3;" x-text="a.sessionCount"></td>
893
+ </tr>
894
+ </template>
895
+ </tbody>
896
+ </table>
897
+ </div>
898
+ </div>
899
+ </template>
900
+ <template x-if="!costData">
901
+ <div class="loading">Loading cost data...</div>
902
+ </template>
903
+ </div>
904
+ </template>
905
+
906
+ <!-- Config Tab -->
907
+ <template x-if="loaded && tab === 'Config'">
908
+ <div>
909
+ <div class="card">
910
+ <div class="card-title">Current Configuration</div>
911
+ <template x-if="data.config">
912
+ <div>
913
+ <div class="cfg-row"><span class="cfg-key">checkInterval</span><span class="cfg-val" x-text="data.config.checkInterval + 's'"></span></div>
914
+ <div class="cfg-row"><span class="cfg-key">failThreshold</span><span class="cfg-val" x-text="data.config.failThreshold"></span></div>
915
+ <div class="cfg-row"><span class="cfg-key">dashboardPort</span><span class="cfg-val" x-text="data.config.dashboardPort"></span></div>
916
+ <div class="cfg-row"><span class="cfg-key">maxRestartsPerHour</span><span class="cfg-val" x-text="data.config.maxRestartsPerHour"></span></div>
917
+ <div class="cfg-row"><span class="cfg-key">openclawProfile</span><span class="cfg-val" x-text="data.config.openclawProfile"></span></div>
918
+ <div class="cfg-row"><span class="cfg-key">notify.webhook.enabled</span><span class="cfg-val" x-text="data.config.notify?.webhook?.enabled ?? false"></span></div>
919
+ <div class="cfg-row"><span class="cfg-key">notify.system.enabled</span><span class="cfg-val" x-text="data.config.notify?.system?.enabled ?? false"></span></div>
920
+ </div>
921
+ </template>
922
+ </div>
923
+ </div>
924
+ </template>
925
+
926
+ </div>
927
+
928
+ <script>
929
+ function dashboard() {
930
+ return {
931
+ tab: 'Overview',
932
+ costData: null,
933
+ loaded: false,
934
+ data: {},
935
+ logs: [],
936
+ lastCheck: '',
937
+ actionLoading: false,
938
+ actionResult: '',
939
+
940
+ get statusText() {
941
+ if (!this.data || !this.loaded) return 'LOADING';
942
+ if (this.data.healthy) return 'HEALTHY';
943
+ if (this.data.gateway) return 'DEGRADED';
944
+ return 'UNREACHABLE';
945
+ },
946
+
947
+ get statusColor() {
948
+ if (!this.data || !this.loaded) return '#64748b';
949
+ if (this.data.healthy) return '#00A67E';
950
+ if (this.data.gateway) return '#f59e0b';
951
+ return '#ef4444';
952
+ },
953
+
954
+ async init() {
955
+ await this.refresh();
956
+ await this.refreshLogs();
957
+ setInterval(() => this.refresh(), 10000);
958
+ setInterval(() => this.refreshLogs(), 10000);
959
+ },
960
+
961
+ async refresh() {
962
+ try {
963
+ const res = await fetch('/api/status');
964
+ this.data = await res.json();
965
+ this.lastCheck = new Date().toLocaleTimeString();
966
+ this.loaded = true;
967
+ } catch (e) {
968
+ console.error('Failed to fetch status', e);
969
+ }
970
+ },
971
+
972
+ async refreshCost() {
973
+ try {
974
+ const r = await fetch('/api/cost');
975
+ this.costData = await r.json();
976
+ } catch {}
977
+ },
978
+ async refreshLogs() {
979
+ try {
980
+ const res = await fetch('/api/logs');
981
+ const d = await res.json();
982
+ this.logs = d.lines ?? [];
983
+ } catch (e) {
984
+ console.error('Failed to fetch logs', e);
985
+ }
986
+ },
987
+
988
+ async doRestart() {
989
+ this.actionLoading = true;
990
+ this.actionResult = '';
991
+ try {
992
+ const res = await fetch('/api/restart', { method: 'POST' });
993
+ const d = await res.json();
994
+ this.actionResult = d.success ? 'Gateway restarted successfully.' : ('Restart failed: ' + (d.error ?? 'unknown'));
995
+ await this.refresh();
996
+ } catch (e) {
997
+ this.actionResult = 'Request failed: ' + e.message;
998
+ }
999
+ this.actionLoading = false;
1000
+ },
1001
+
1002
+ async doDoctor() {
1003
+ this.actionLoading = true;
1004
+ this.actionResult = '';
1005
+ try {
1006
+ const res = await fetch('/api/doctor', { method: 'POST' });
1007
+ const d = await res.json();
1008
+ this.actionResult = d.output ?? 'No output';
1009
+ } catch (e) {
1010
+ this.actionResult = 'Request failed: ' + e.message;
1011
+ }
1012
+ this.actionLoading = false;
1013
+ },
1014
+
1015
+ fmtTime(iso) {
1016
+ if (!iso) return '';
1017
+ try { return new Date(iso).toLocaleString(); } catch { return iso; }
1018
+ },
1019
+
1020
+ logClass(line) {
1021
+ if (line.includes('[ERROR]')) return 'log-error';
1022
+ if (line.includes('[WARN]')) return 'log-warn';
1023
+ if (line.includes('[SUCCESS]')) return 'log-success';
1024
+ return 'log-info';
1025
+ }
1026
+ };
1027
+ }
1028
+ </script>
1029
+ </body>
1030
+ </html>`;
1031
+ }
1032
+ async function handleApiStatus(info, configPath, res) {
1033
+ try {
1034
+ const live = await checkHealth(info);
1035
+ const config = loadConfig(configPath);
1036
+ const workspaces = scanWorkspaces(info);
1037
+ const agentRuntimes = live.agentRuntimes ?? [];
1038
+ const payload = {
1039
+ healthy: live.healthy,
1040
+ gateway: live.gateway,
1041
+ channels: live.channels,
1042
+ agents: info.agents,
1043
+ agentRuntimes,
1044
+ durationMs: live.durationMs,
1045
+ checks: getCheckHistory(),
1046
+ restarts: getRestartHistory(),
1047
+ config,
1048
+ workspaces,
1049
+ info: {
1050
+ version: info.version,
1051
+ gatewayPort: info.gatewayPort,
1052
+ profile: info.profile
1053
+ }
1054
+ };
1055
+ res.writeHead(200, { "Content-Type": "application/json" });
1056
+ res.end(JSON.stringify(payload));
1057
+ } catch (err) {
1058
+ res.writeHead(500, { "Content-Type": "application/json" });
1059
+ res.end(JSON.stringify({ error: String(err) }));
1060
+ }
1061
+ }
1062
+ async function handleApiRestart(info, res) {
1063
+ try {
1064
+ const result = await restartGateway(info);
1065
+ res.writeHead(200, { "Content-Type": "application/json" });
1066
+ res.end(JSON.stringify({ success: result.success, message: result.output ?? result.error ?? "" }));
1067
+ } catch (err) {
1068
+ res.writeHead(500, { "Content-Type": "application/json" });
1069
+ res.end(JSON.stringify({ success: false, message: String(err) }));
1070
+ }
1071
+ }
1072
+ async function handleApiDoctor(info, res) {
1073
+ try {
1074
+ const output = await runOpenClawCmd(info, "doctor --non-interactive");
1075
+ res.writeHead(200, { "Content-Type": "application/json" });
1076
+ res.end(JSON.stringify({ output: output ?? "No output" }));
1077
+ } catch (err) {
1078
+ res.writeHead(500, { "Content-Type": "application/json" });
1079
+ res.end(JSON.stringify({ output: "Error: " + String(err) }));
1080
+ }
1081
+ }
1082
+ async function handleApiCost(info, res) {
1083
+ try {
1084
+ const costs = scanCosts(info.agents);
1085
+ res.writeHead(200, { "Content-Type": "application/json" });
1086
+ res.end(JSON.stringify(costs));
1087
+ } catch (err) {
1088
+ res.writeHead(500, { "Content-Type": "application/json" });
1089
+ res.end(JSON.stringify({ error: String(err) }));
1090
+ }
1091
+ }
1092
+ function handleApiLogs(res) {
1093
+ const lines = readDoctorLogs(50);
1094
+ res.writeHead(200, { "Content-Type": "application/json" });
1095
+ res.end(JSON.stringify({ lines }));
1096
+ }
1097
+ function startDashboard(options) {
1098
+ const config = loadConfig(options.config);
1099
+ const info = detectOpenClaw(options.profile ?? config.openclawProfile);
1100
+ const port = config.dashboardPort;
1101
+ const shell = renderShell();
1102
+ const server = createServer(async (req, res) => {
1103
+ const url = req.url ?? "/";
1104
+ const method = req.method ?? "GET";
1105
+ if (method === "GET" && url === "/") {
1106
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1107
+ res.end(shell);
1108
+ } else if (method === "GET" && url === "/api/status") {
1109
+ await handleApiStatus(info, options.config, res);
1110
+ } else if (method === "GET" && url === "/api/cost") {
1111
+ await handleApiCost(info, res);
1112
+ } else if (method === "GET" && url === "/api/logs") {
1113
+ handleApiLogs(res);
1114
+ } else if (method === "POST" && url === "/api/restart") {
1115
+ await handleApiRestart(info, res);
1116
+ } else if (method === "POST" && url === "/api/doctor") {
1117
+ await handleApiDoctor(info, res);
1118
+ } else {
1119
+ res.writeHead(404, { "Content-Type": "application/json" });
1120
+ res.end(JSON.stringify({ error: "Not found" }));
1121
+ }
1122
+ });
1123
+ server.listen(port, () => {
1124
+ log("info", `Dashboard running at http://localhost:${port}`);
1125
+ console.log(chalk2.green.bold(`
1126
+ Dashboard: http://localhost:${port}
1127
+ `));
1128
+ });
1129
+ }
1130
+
1131
+ export {
1132
+ __require,
1133
+ BINARY_NAME,
1134
+ DISPLAY_NAME,
1135
+ DOCTOR_LOG_DIR,
1136
+ PID_FILE,
1137
+ ensureDoctorHome,
1138
+ loadConfig,
1139
+ initLogger,
1140
+ log,
1141
+ detectOpenClaw,
1142
+ runOpenClawCmd,
1143
+ checkHealth,
1144
+ restartGateway,
1145
+ startGateway,
1146
+ stopGateway,
1147
+ RestartThrottle,
1148
+ startDashboard
1149
+ };