openclaw-service 0.4.0 → 0.6.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/dist/index.js +1752 -147
- package/dist/index.pkg.cjs +7385 -0
- package/package.json +6 -7
- package/README.aiclaw.md +0 -25
- package/README.aliclaw.md +0 -25
- package/README.alpha-claw.md +0 -25
- package/README.alphaclaw.md +0 -25
- package/README.amazonclaw.md +0 -25
- package/README.amzclaw.md +0 -25
- package/README.anthropicclaw.md +0 -25
- package/README.appleclaw.md +0 -25
- package/README.autoopenclaw.md +0 -25
- package/README.awsclaw.md +0 -25
- package/README.bdclaw.md +0 -25
- package/README.blclaw.md +0 -25
- package/README.bytclaw.md +0 -25
- package/README.claw-open.md +0 -25
- package/README.clawjs.md +0 -25
- package/README.coclaw.md +0 -25
- package/README.copaw.md +0 -25
- package/README.ddclaw.md +0 -25
- package/README.duclaw.md +0 -25
- package/README.dyclaw.md +0 -25
- package/README.easyclaw.md +0 -25
- package/README.fastclaw.md +0 -25
- package/README.fbclaw.md +0 -25
- package/README.googleclaw.md +0 -25
- package/README.hello-claw.md +0 -25
- package/README.hwclaw.md +0 -25
- package/README.jdclaw.md +0 -25
- package/README.kimiclaw.md +0 -25
- package/README.ksclaw.md +0 -25
- package/README.maxclaw.md +0 -25
- package/README.md.bak +0 -75
- package/README.megaclaw.md +0 -25
- package/README.metaclaw.md +0 -25
- package/README.miclaw.md +0 -25
- package/README.msclaw.md +0 -25
- package/README.mtclaw.md +0 -25
- package/README.nflxclaw.md +0 -25
- package/README.nvdaclaw.md +0 -25
- package/README.open-claw.md +0 -25
- package/README.openaiclaw.md +0 -25
- package/README.openclaw-cli.md +0 -25
- package/README.openclaw-daemon.md +0 -25
- package/README.openclaw-gateway.md +0 -25
- package/README.openclaw-health.md +0 -25
- package/README.openclaw-helper.md +0 -25
- package/README.openclaw-install.md +0 -25
- package/README.openclaw-manage.md +0 -25
- package/README.openclaw-monitor.md +0 -25
- package/README.openclaw-run.md +0 -25
- package/README.openclaw-service.md +0 -25
- package/README.openclaw-setup.md +0 -25
- package/README.openclaw-start.md +0 -25
- package/README.openclaw-tools.md +0 -25
- package/README.openclaw-upgrade.md +0 -25
- package/README.openclaw-utils.md +0 -25
- package/README.openclaw-watch.md +0 -25
- package/README.pddclaw.md +0 -25
- package/README.qclaw-cli.md +0 -25
- package/README.qclaw.md +0 -25
- package/README.smartclaw.md +0 -25
- package/README.ttclaw.md +0 -25
- package/README.txclaw.md +0 -25
- package/README.uberclaw.md +0 -25
- package/README.volclaw.md +0 -25
- package/README.wxclaw.md +0 -25
- package/README.xclaw.md +0 -25
- package/README.zhclaw.md +0 -25
- package/dist/chunk-UXL57WT4.js +0 -1149
- package/dist/server-GZ3MD6AH.js +0 -7
package/dist/index.js
CHANGED
|
@@ -1,48 +1,1379 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/brand.ts
|
|
19
|
+
import { basename, join } from "path";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
function toDisplayName(bin) {
|
|
22
|
+
if (bin === "openclaw-cli") return "OpenClaw CLI";
|
|
23
|
+
if (bin === "openclaw-doctor") return "OpenClaw Doctor";
|
|
24
|
+
return bin.split("-").map((s) => s ? s[0].toUpperCase() + s.slice(1) : s).join(" ");
|
|
25
|
+
}
|
|
26
|
+
var detectedBin, BINARY_NAME, APP_HOME, DISPLAY_NAME;
|
|
27
|
+
var init_brand = __esm({
|
|
28
|
+
"src/brand.ts"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
detectedBin = basename(process.argv[1] ?? "openclaw-cli").replace(/\.[cm]?js$/, "");
|
|
31
|
+
BINARY_NAME = detectedBin || "openclaw-cli";
|
|
32
|
+
APP_HOME = join(homedir(), ".openclaw-doctor");
|
|
33
|
+
DISPLAY_NAME = toDisplayName(BINARY_NAME);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// src/config.ts
|
|
38
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
39
|
+
import { join as join2, resolve } from "path";
|
|
40
|
+
function ensureDoctorHome() {
|
|
41
|
+
if (!existsSync(DOCTOR_HOME)) {
|
|
42
|
+
mkdirSync(DOCTOR_HOME, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
if (!existsSync(DOCTOR_LOG_DIR)) {
|
|
45
|
+
mkdirSync(DOCTOR_LOG_DIR, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveConfigPath(configPath) {
|
|
49
|
+
if (configPath) return configPath;
|
|
50
|
+
if (existsSync(LOCAL_CONFIG)) return LOCAL_CONFIG;
|
|
51
|
+
return CONFIG_PATH;
|
|
52
|
+
}
|
|
53
|
+
function loadConfig(configPath) {
|
|
54
|
+
const file = resolveConfigPath(configPath);
|
|
55
|
+
if (existsSync(file)) {
|
|
56
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
57
|
+
return {
|
|
58
|
+
...defaults,
|
|
59
|
+
...raw,
|
|
60
|
+
notify: {
|
|
61
|
+
webhook: { ...defaults.notify.webhook, ...raw.notify?.webhook ?? {} },
|
|
62
|
+
system: { ...defaults.notify.system, ...raw.notify?.system ?? {} }
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
ensureDoctorHome();
|
|
67
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(defaults, null, 2) + "\n");
|
|
68
|
+
return { ...defaults };
|
|
69
|
+
}
|
|
70
|
+
var DOCTOR_HOME, CONFIG_PATH, DOCTOR_LOG_DIR, PID_FILE, STOP_FLAG_FILE, defaults, LOCAL_CONFIG;
|
|
71
|
+
var init_config = __esm({
|
|
72
|
+
"src/config.ts"() {
|
|
73
|
+
"use strict";
|
|
74
|
+
init_brand();
|
|
75
|
+
DOCTOR_HOME = APP_HOME;
|
|
76
|
+
CONFIG_PATH = join2(APP_HOME, "config.json");
|
|
77
|
+
DOCTOR_LOG_DIR = join2(APP_HOME, "logs");
|
|
78
|
+
PID_FILE = join2(APP_HOME, "daemon.pid");
|
|
79
|
+
STOP_FLAG_FILE = join2(APP_HOME, "gateway.stopped");
|
|
80
|
+
defaults = {
|
|
81
|
+
checkInterval: 30,
|
|
82
|
+
failThreshold: 5,
|
|
83
|
+
dashboardPort: 9090,
|
|
84
|
+
maxRestartsPerHour: 5,
|
|
85
|
+
openclawProfile: "default",
|
|
86
|
+
notify: {
|
|
87
|
+
webhook: {
|
|
88
|
+
enabled: false,
|
|
89
|
+
url: "",
|
|
90
|
+
bodyTemplate: '{"msgtype":"text","text":{"content":"{{message}}"}}'
|
|
91
|
+
},
|
|
92
|
+
system: {
|
|
93
|
+
enabled: true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
LOCAL_CONFIG = resolve(process.cwd(), "doctor.config.json");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// src/core/logger.ts
|
|
102
|
+
import { appendFileSync } from "fs";
|
|
103
|
+
import { join as join3 } from "path";
|
|
104
|
+
import chalk from "chalk";
|
|
105
|
+
function initLogger(dir) {
|
|
106
|
+
logDir = dir ?? DOCTOR_LOG_DIR;
|
|
107
|
+
ensureDoctorHome();
|
|
108
|
+
}
|
|
109
|
+
function getLogFile() {
|
|
110
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
111
|
+
return join3(logDir, `${date}.log`);
|
|
112
|
+
}
|
|
113
|
+
function log(level, message) {
|
|
114
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
115
|
+
const line = `[${time}] [${level.toUpperCase()}] ${message}`;
|
|
116
|
+
const colorFn = level === "error" ? chalk.red : level === "warn" ? chalk.yellow : level === "success" ? chalk.green : chalk.blue;
|
|
117
|
+
console.log(colorFn(line));
|
|
118
|
+
try {
|
|
119
|
+
appendFileSync(getLogFile(), line + "\n");
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function addCheckRecord(record) {
|
|
124
|
+
checkHistory.push(record);
|
|
125
|
+
if (checkHistory.length > MAX_HISTORY) checkHistory.shift();
|
|
126
|
+
}
|
|
127
|
+
function addRestartRecord(record) {
|
|
128
|
+
restartHistory.push(record);
|
|
129
|
+
if (restartHistory.length > MAX_HISTORY) restartHistory.shift();
|
|
130
|
+
}
|
|
131
|
+
function getCheckHistory() {
|
|
132
|
+
return [...checkHistory];
|
|
133
|
+
}
|
|
134
|
+
function getRestartHistory() {
|
|
135
|
+
return [...restartHistory];
|
|
136
|
+
}
|
|
137
|
+
var logDir, checkHistory, restartHistory, MAX_HISTORY;
|
|
138
|
+
var init_logger = __esm({
|
|
139
|
+
"src/core/logger.ts"() {
|
|
140
|
+
"use strict";
|
|
141
|
+
init_config();
|
|
142
|
+
logDir = DOCTOR_LOG_DIR;
|
|
143
|
+
checkHistory = [];
|
|
144
|
+
restartHistory = [];
|
|
145
|
+
MAX_HISTORY = 100;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// src/core/openclaw.ts
|
|
150
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync } from "fs";
|
|
151
|
+
import { join as join4 } from "path";
|
|
152
|
+
import { homedir as homedir2 } from "os";
|
|
153
|
+
import { execSync } from "child_process";
|
|
154
|
+
import { exec } from "child_process";
|
|
155
|
+
import { promisify } from "util";
|
|
156
|
+
import * as http from "http";
|
|
157
|
+
function getOpenClawHome(profile) {
|
|
158
|
+
if (profile === "dev") return join4(homedir2(), ".openclaw-dev");
|
|
159
|
+
if (profile !== "default") return join4(homedir2(), `.openclaw-${profile}`);
|
|
160
|
+
return join4(homedir2(), ".openclaw");
|
|
161
|
+
}
|
|
162
|
+
function findOpenClawBin() {
|
|
163
|
+
const plistDir = join4(homedir2(), "Library", "LaunchAgents");
|
|
164
|
+
if (existsSync3(plistDir)) {
|
|
165
|
+
const plists = readdirSync(plistDir).filter(
|
|
166
|
+
(f) => f.includes("openclaw") && f.endsWith(".plist")
|
|
167
|
+
);
|
|
168
|
+
for (const plist of plists) {
|
|
169
|
+
const content = readFileSync2(join4(plistDir, plist), "utf-8");
|
|
170
|
+
const nodeMatch = content.match(
|
|
171
|
+
/<string>(\/[^<]*\/bin\/node)<\/string>/
|
|
172
|
+
);
|
|
173
|
+
const cliMatch = content.match(
|
|
174
|
+
/<string>(\/[^<]*openclaw[^<]*\.(?:js|mjs))<\/string>/
|
|
175
|
+
);
|
|
176
|
+
if (nodeMatch && cliMatch) {
|
|
177
|
+
return { nodePath: nodeMatch[1], cliBinPath: cliMatch[1] };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const bin = execSync("which openclaw", { encoding: "utf-8" }).trim();
|
|
183
|
+
if (bin) return { nodePath: process.execPath, cliBinPath: bin };
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function findLaunchdLabel() {
|
|
189
|
+
const plistDir = join4(homedir2(), "Library", "LaunchAgents");
|
|
190
|
+
if (!existsSync3(plistDir)) return "ai.openclaw.gateway";
|
|
191
|
+
const plists = readdirSync(plistDir).filter(
|
|
192
|
+
(f) => f.includes("openclaw") && f.endsWith(".plist")
|
|
193
|
+
);
|
|
194
|
+
if (plists.length > 0) {
|
|
195
|
+
return plists[0].replace(".plist", "");
|
|
196
|
+
}
|
|
197
|
+
return "ai.openclaw.gateway";
|
|
198
|
+
}
|
|
199
|
+
function detectOpenClaw(profile = "default") {
|
|
200
|
+
const home = getOpenClawHome(profile);
|
|
201
|
+
const configPath = join4(home, "openclaw.json");
|
|
202
|
+
const logDir2 = join4(home, "logs");
|
|
203
|
+
const defaults2 = {
|
|
204
|
+
configPath,
|
|
205
|
+
gatewayPort: 18789,
|
|
206
|
+
gatewayToken: "",
|
|
207
|
+
launchdLabel: findLaunchdLabel(),
|
|
208
|
+
nodePath: process.execPath,
|
|
209
|
+
cliBinPath: "",
|
|
210
|
+
logDir: logDir2,
|
|
211
|
+
profile,
|
|
212
|
+
channels: [],
|
|
213
|
+
agents: [],
|
|
214
|
+
version: null
|
|
215
|
+
};
|
|
216
|
+
const binInfo = findOpenClawBin();
|
|
217
|
+
if (binInfo) {
|
|
218
|
+
defaults2.nodePath = binInfo.nodePath;
|
|
219
|
+
defaults2.cliBinPath = binInfo.cliBinPath;
|
|
220
|
+
}
|
|
221
|
+
if (!existsSync3(configPath)) {
|
|
222
|
+
return defaults2;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const raw = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
226
|
+
defaults2.gatewayPort = raw.gateway?.port ?? defaults2.gatewayPort;
|
|
227
|
+
defaults2.gatewayToken = raw.gateway?.auth?.token ?? "";
|
|
228
|
+
defaults2.version = raw.meta?.lastTouchedVersion ?? null;
|
|
229
|
+
if (raw.channels) {
|
|
230
|
+
defaults2.channels = Object.entries(raw.channels).filter(([, v]) => v.enabled !== false).map(([k]) => k);
|
|
231
|
+
}
|
|
232
|
+
if (raw.agents?.list) {
|
|
233
|
+
defaults2.agents = raw.agents.list.map(
|
|
234
|
+
(a) => ({
|
|
235
|
+
id: a.id,
|
|
236
|
+
name: a.name ?? a.id,
|
|
237
|
+
isDefault: a.default ?? false,
|
|
238
|
+
model: typeof a.model === "string" ? a.model : a.model?.primary ?? raw.agents?.defaults?.model?.primary ?? void 0,
|
|
239
|
+
workspace: a.workspace
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
return defaults2;
|
|
246
|
+
}
|
|
247
|
+
async function runOpenClawCmd(info, args) {
|
|
248
|
+
if (!info.cliBinPath) return null;
|
|
249
|
+
try {
|
|
250
|
+
const { stdout } = await execAsync(`"${info.nodePath}" "${info.cliBinPath}" ${args}`, {
|
|
251
|
+
timeout: 3e4,
|
|
252
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" }
|
|
253
|
+
});
|
|
254
|
+
return stdout.trim();
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function getGatewayHealthHttp(info) {
|
|
260
|
+
return new Promise((resolve3) => {
|
|
261
|
+
const url = `http://127.0.0.1:${info.gatewayPort}/health`;
|
|
262
|
+
const req = http.get(url, { timeout: 5e3 }, (res) => {
|
|
263
|
+
let data = "";
|
|
264
|
+
res.on("data", (chunk) => {
|
|
265
|
+
data += chunk;
|
|
266
|
+
});
|
|
267
|
+
res.on("end", () => {
|
|
268
|
+
try {
|
|
269
|
+
resolve3(JSON.parse(data));
|
|
270
|
+
} catch {
|
|
271
|
+
resolve3(null);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
req.on("error", () => resolve3(null));
|
|
276
|
+
req.on("timeout", () => {
|
|
277
|
+
req.destroy();
|
|
278
|
+
resolve3(null);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async function getGatewayHealth(info) {
|
|
283
|
+
const raw = await runOpenClawCmd(info, "health --json");
|
|
284
|
+
if (!raw) return null;
|
|
285
|
+
try {
|
|
286
|
+
const jsonStart = raw.indexOf("{");
|
|
287
|
+
if (jsonStart === -1) return null;
|
|
288
|
+
return JSON.parse(raw.slice(jsonStart));
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function getRestartCommand(info) {
|
|
294
|
+
const uid = process.getuid?.() ?? 501;
|
|
295
|
+
return `launchctl kickstart -k gui/${uid}/${info.launchdLabel}`;
|
|
296
|
+
}
|
|
297
|
+
function getStopCommand(info) {
|
|
298
|
+
const uid = process.getuid?.() ?? 501;
|
|
299
|
+
return `launchctl bootout gui/${uid}/${info.launchdLabel} 2>/dev/null || launchctl kill SIGTERM gui/${uid}/${info.launchdLabel} 2>/dev/null || true`;
|
|
300
|
+
}
|
|
301
|
+
function getStartCommand(info) {
|
|
302
|
+
const uid = process.getuid?.() ?? 501;
|
|
303
|
+
const plistDir = `${process.env.HOME}/Library/LaunchAgents`;
|
|
304
|
+
return `(launchctl bootstrap gui/${uid} ${plistDir}/${info.launchdLabel}.plist 2>/dev/null || true) && launchctl kickstart gui/${uid}/${info.launchdLabel}`;
|
|
305
|
+
}
|
|
306
|
+
var execAsync;
|
|
307
|
+
var init_openclaw = __esm({
|
|
308
|
+
"src/core/openclaw.ts"() {
|
|
309
|
+
"use strict";
|
|
310
|
+
execAsync = promisify(exec);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// src/core/health-checker.ts
|
|
315
|
+
async function checkHealth(info) {
|
|
316
|
+
const start = Date.now();
|
|
317
|
+
let health = await getGatewayHealthHttp(info);
|
|
318
|
+
if (!health) {
|
|
319
|
+
log("warn", "HTTP probe failed, falling back to CLI health check");
|
|
320
|
+
health = await getGatewayHealth(info);
|
|
321
|
+
}
|
|
322
|
+
const durationMs = Date.now() - start;
|
|
323
|
+
if (!health) {
|
|
324
|
+
const error = "Gateway unreachable (openclaw health failed)";
|
|
325
|
+
addCheckRecord({
|
|
326
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
327
|
+
healthy: false,
|
|
328
|
+
error,
|
|
329
|
+
responseTime: durationMs
|
|
330
|
+
});
|
|
331
|
+
log("error", `Health check failed: ${error} (${durationMs}ms)`);
|
|
332
|
+
return { healthy: false, gateway: false, channels: [], agentRuntimes: [], durationMs, error };
|
|
333
|
+
}
|
|
334
|
+
const channels = health.channels ? Object.entries(health.channels).map(([name, ch]) => ({
|
|
335
|
+
name,
|
|
336
|
+
ok: ch.probe?.ok ?? false
|
|
337
|
+
})) : [];
|
|
338
|
+
const healthy = health.ok;
|
|
339
|
+
const agentRuntimes = health.agents ?? [];
|
|
340
|
+
addCheckRecord({
|
|
341
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
342
|
+
healthy,
|
|
343
|
+
responseTime: durationMs
|
|
344
|
+
});
|
|
345
|
+
if (healthy) {
|
|
346
|
+
log("success", `Health OK \u2014 gateway up, ${channels.length} channels (${durationMs}ms)`);
|
|
347
|
+
} else {
|
|
348
|
+
log("warn", `Health degraded \u2014 gateway responded but ok=false (${durationMs}ms)`);
|
|
349
|
+
}
|
|
350
|
+
return { healthy, gateway: true, channels, agentRuntimes, durationMs, raw: health };
|
|
351
|
+
}
|
|
352
|
+
var init_health_checker = __esm({
|
|
353
|
+
"src/core/health-checker.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
init_logger();
|
|
356
|
+
init_openclaw();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// src/core/process-manager.ts
|
|
361
|
+
import { exec as exec2 } from "child_process";
|
|
362
|
+
import { promisify as promisify2 } from "util";
|
|
363
|
+
async function runShell(command) {
|
|
364
|
+
try {
|
|
365
|
+
const { stdout } = await execAsync2(command, { timeout: 12e4 });
|
|
366
|
+
return { success: true, output: stdout.trim() };
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
369
|
+
return { success: false, error };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function restartGateway(info) {
|
|
373
|
+
const cmd = getRestartCommand(info);
|
|
374
|
+
log("warn", `Restarting gateway: ${cmd}`);
|
|
375
|
+
const result = await runShell(cmd);
|
|
376
|
+
if (result.success) {
|
|
377
|
+
log("success", "Gateway restarted");
|
|
378
|
+
} else {
|
|
379
|
+
log("error", `Gateway restart failed: ${result.error}`);
|
|
380
|
+
}
|
|
381
|
+
addRestartRecord({
|
|
382
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
383
|
+
reason: "health check failed",
|
|
384
|
+
success: result.success
|
|
385
|
+
});
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
async function startGateway(info) {
|
|
389
|
+
const cmd = getStartCommand(info);
|
|
390
|
+
log("info", `Starting gateway: ${cmd}`);
|
|
391
|
+
const result = await runShell(cmd);
|
|
392
|
+
if (result.success) log("success", "Gateway started");
|
|
393
|
+
else log("error", `Gateway start failed: ${result.error}`);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
async function stopGateway(info) {
|
|
397
|
+
const cmd = getStopCommand(info);
|
|
398
|
+
log("info", `Stopping gateway: ${cmd}`);
|
|
399
|
+
const result = await runShell(cmd);
|
|
400
|
+
if (result.success) log("success", "Gateway stopped");
|
|
401
|
+
else log("error", `Gateway stop failed: ${result.error}`);
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
var execAsync2, RestartThrottle;
|
|
405
|
+
var init_process_manager = __esm({
|
|
406
|
+
"src/core/process-manager.ts"() {
|
|
407
|
+
"use strict";
|
|
408
|
+
init_logger();
|
|
409
|
+
init_openclaw();
|
|
410
|
+
execAsync2 = promisify2(exec2);
|
|
411
|
+
RestartThrottle = class {
|
|
412
|
+
constructor(maxPerHour) {
|
|
413
|
+
this.maxPerHour = maxPerHour;
|
|
414
|
+
}
|
|
415
|
+
timestamps = [];
|
|
416
|
+
canRestart() {
|
|
417
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
418
|
+
this.timestamps = this.timestamps.filter((t) => t > oneHourAgo);
|
|
419
|
+
return this.timestamps.length < this.maxPerHour;
|
|
420
|
+
}
|
|
421
|
+
record() {
|
|
422
|
+
this.timestamps.push(Date.now());
|
|
423
|
+
}
|
|
424
|
+
recentCount() {
|
|
425
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
426
|
+
return this.timestamps.filter((t) => t > oneHourAgo).length;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// src/core/workspace-scanner.ts
|
|
433
|
+
import { existsSync as existsSync5, statSync, readdirSync as readdirSync2 } from "fs";
|
|
434
|
+
import { join as join6 } from "path";
|
|
435
|
+
import { homedir as homedir3 } from "os";
|
|
436
|
+
function expandHome(p) {
|
|
437
|
+
return p.startsWith("~/") ? join6(homedir3(), p.slice(2)) : p;
|
|
438
|
+
}
|
|
439
|
+
function dirSizeKB(dir, depth = 0) {
|
|
440
|
+
if (depth > 4 || !existsSync5(dir)) return 0;
|
|
441
|
+
let total = 0;
|
|
442
|
+
try {
|
|
443
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
444
|
+
if (["node_modules", ".git", ".DS_Store"].includes(entry.name)) continue;
|
|
445
|
+
const full = join6(dir, entry.name);
|
|
446
|
+
if (entry.isDirectory()) {
|
|
447
|
+
total += dirSizeKB(full, depth + 1);
|
|
448
|
+
} else {
|
|
449
|
+
try {
|
|
450
|
+
total += statSync(full).size;
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
return Math.round(total / 1024);
|
|
458
|
+
}
|
|
459
|
+
function scanWorkspaces(info) {
|
|
460
|
+
const results = [];
|
|
461
|
+
for (const agent of info.agents) {
|
|
462
|
+
const workspaceRaw = agent.workspace;
|
|
463
|
+
if (!workspaceRaw) continue;
|
|
464
|
+
const workspacePath = expandHome(workspaceRaw);
|
|
465
|
+
let memoryFileSizeKB = 0;
|
|
466
|
+
try {
|
|
467
|
+
const memPath = join6(workspacePath, "MEMORY.md");
|
|
468
|
+
if (existsSync5(memPath)) {
|
|
469
|
+
memoryFileSizeKB = Math.round(statSync(memPath).size / 1024);
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
let totalWorkspaceSizeKB = 0;
|
|
474
|
+
try {
|
|
475
|
+
totalWorkspaceSizeKB = dirSizeKB(workspacePath);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
let sessionCount = 0;
|
|
479
|
+
try {
|
|
480
|
+
const sessDir = join6(homedir3(), ".openclaw", "agents", agent.id, "sessions");
|
|
481
|
+
if (existsSync5(sessDir)) {
|
|
482
|
+
sessionCount = readdirSync2(sessDir).filter((f) => f.endsWith(".jsonl") || f.endsWith(".json")).length;
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
results.push({
|
|
487
|
+
agentId: agent.id,
|
|
488
|
+
agentName: agent.name,
|
|
489
|
+
workspacePath,
|
|
490
|
+
memoryFileSizeKB,
|
|
491
|
+
memoryWarning: memoryFileSizeKB > 50,
|
|
492
|
+
totalWorkspaceSizeKB,
|
|
493
|
+
sessionCount,
|
|
494
|
+
model: agent.model
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return results;
|
|
498
|
+
}
|
|
499
|
+
var init_workspace_scanner = __esm({
|
|
500
|
+
"src/core/workspace-scanner.ts"() {
|
|
501
|
+
"use strict";
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// src/core/cost-scanner.ts
|
|
506
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
507
|
+
import { join as join7 } from "path";
|
|
508
|
+
import { homedir as homedir4 } from "os";
|
|
509
|
+
function parseSessionCosts(filePath, sinceMs) {
|
|
510
|
+
let cost = 0;
|
|
511
|
+
let tokens = 0;
|
|
512
|
+
try {
|
|
513
|
+
const lines = readFileSync4(filePath, "utf-8").split("\n").filter(Boolean);
|
|
514
|
+
for (const line of lines) {
|
|
515
|
+
try {
|
|
516
|
+
const msg = JSON.parse(line);
|
|
517
|
+
if (msg.type !== "message" || !msg.message?.usage?.cost) continue;
|
|
518
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).getTime() : msg.message?.timestamp ?? 0;
|
|
519
|
+
if (ts < sinceMs) continue;
|
|
520
|
+
cost += msg.message.usage.cost.total ?? 0;
|
|
521
|
+
tokens += msg.message.usage.totalTokens ?? 0;
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
return { cost, tokens };
|
|
528
|
+
}
|
|
529
|
+
function scanCosts(agents) {
|
|
530
|
+
const now = Date.now();
|
|
531
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
532
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
533
|
+
const weekStart = now - 7 * 24 * 3600 * 1e3;
|
|
534
|
+
const result = [];
|
|
535
|
+
for (const agent of agents) {
|
|
536
|
+
const sessDir = join7(homedir4(), ".openclaw", "agents", agent.id, "sessions");
|
|
537
|
+
if (!existsSync6(sessDir)) continue;
|
|
538
|
+
let todayCost = 0, weekCost = 0, totalTokens = 0, sessionCount = 0;
|
|
539
|
+
const files = readdirSync3(sessDir).filter((f) => f.endsWith(".jsonl"));
|
|
540
|
+
sessionCount = files.length;
|
|
541
|
+
for (const file of files) {
|
|
542
|
+
const fpath = join7(sessDir, file);
|
|
543
|
+
try {
|
|
544
|
+
const mtime = statSync2(fpath).mtimeMs;
|
|
545
|
+
if (mtime < weekStart) continue;
|
|
546
|
+
} catch {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const week = parseSessionCosts(fpath, weekStart);
|
|
550
|
+
weekCost += week.cost;
|
|
551
|
+
totalTokens += week.tokens;
|
|
552
|
+
const today = parseSessionCosts(fpath, todayStart.getTime());
|
|
553
|
+
todayCost += today.cost;
|
|
554
|
+
}
|
|
555
|
+
result.push({ agentId: agent.id, agentName: agent.name, todayCost, weekCost, totalTokens, sessionCount });
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
agents: result,
|
|
559
|
+
todayTotal: result.reduce((s, a) => s + a.todayCost, 0),
|
|
560
|
+
weekTotal: result.reduce((s, a) => s + a.weekCost, 0),
|
|
561
|
+
currency: "USD"
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
var init_cost_scanner = __esm({
|
|
565
|
+
"src/core/cost-scanner.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// src/dashboard/server.ts
|
|
571
|
+
var server_exports = {};
|
|
572
|
+
__export(server_exports, {
|
|
573
|
+
startDashboard: () => startDashboard
|
|
574
|
+
});
|
|
575
|
+
import { createServer } from "http";
|
|
576
|
+
import { readFileSync as readFileSync5, existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
|
|
577
|
+
import { join as join8 } from "path";
|
|
578
|
+
import chalk2 from "chalk";
|
|
579
|
+
import { hostname as osHostname } from "os";
|
|
580
|
+
function readDoctorLogs(maxLines = 50) {
|
|
581
|
+
if (!existsSync7(DOCTOR_LOG_DIR)) return [];
|
|
582
|
+
const files = readdirSync4(DOCTOR_LOG_DIR).filter((f) => f.endsWith(".log")).sort().reverse();
|
|
583
|
+
if (files.length === 0) return [];
|
|
584
|
+
const content = readFileSync5(join8(DOCTOR_LOG_DIR, files[0]), "utf-8");
|
|
585
|
+
const lines = content.trim().split("\n");
|
|
586
|
+
return lines.slice(-maxLines);
|
|
587
|
+
}
|
|
588
|
+
function renderShell() {
|
|
589
|
+
return `<!DOCTYPE html>
|
|
590
|
+
<html lang="en">
|
|
591
|
+
<head>
|
|
592
|
+
<meta charset="utf-8">
|
|
593
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
594
|
+
<title>OpenClaw Doctor</title>
|
|
595
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
596
|
+
<style>
|
|
597
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
598
|
+
body { font-family:system-ui,-apple-system,sans-serif; background:#050810; color:#f0f4ff; min-height:100vh; }
|
|
599
|
+
|
|
600
|
+
/* Navbar */
|
|
601
|
+
.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; }
|
|
602
|
+
.nav-left { display:flex; align-items:center; gap:0.5rem; font-weight:700; font-size:1rem; white-space:nowrap; }
|
|
603
|
+
.nav-left .ver { font-weight:400; color:#6b7fa3; font-size:0.8rem; }
|
|
604
|
+
.nav-center { display:flex; align-items:center; gap:0.5rem; }
|
|
605
|
+
.status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
|
|
606
|
+
.status-label { font-weight:600; font-size:0.9rem; }
|
|
607
|
+
.nav-right { color:#6b7fa3; font-size:0.75rem; white-space:nowrap; }
|
|
608
|
+
|
|
609
|
+
/* Tabs */
|
|
610
|
+
.tabs { display:flex; border-bottom:1px solid #1a2744; background:#0d1424; overflow-x:auto; }
|
|
611
|
+
.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; }
|
|
612
|
+
.tab:hover { color:#f0f4ff; }
|
|
613
|
+
.tab.active { color:#f0f4ff; border-bottom-color:#007AFF; }
|
|
614
|
+
|
|
615
|
+
/* Content */
|
|
616
|
+
.content { padding:1.5rem; max-width:1200px; margin:0 auto; }
|
|
617
|
+
|
|
618
|
+
/* Cards */
|
|
619
|
+
.card { background:#0d1424; border:1px solid #1a2744; border-radius:12px; padding:1.25rem; margin-bottom:1rem; }
|
|
620
|
+
.card-title { color:#6b7fa3; font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.5rem; }
|
|
621
|
+
|
|
622
|
+
.big-status { font-size:2rem; font-weight:700; }
|
|
623
|
+
.meta-row { color:#6b7fa3; font-size:0.8rem; margin-top:0.25rem; }
|
|
624
|
+
|
|
625
|
+
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
|
|
626
|
+
@media (max-width:768px) { .grid2 { grid-template-columns:1fr; } }
|
|
627
|
+
|
|
628
|
+
/* Tables */
|
|
629
|
+
table { width:100%; border-collapse:collapse; }
|
|
630
|
+
th, td { text-align:left; padding:0.4rem 0.75rem; border-bottom:1px solid #1a2744; font-size:0.8rem; }
|
|
631
|
+
th { color:#6b7fa3; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.05em; }
|
|
632
|
+
|
|
633
|
+
/* Tags */
|
|
634
|
+
.tag { display:inline-block; padding:0.15rem 0.5rem; border-radius:9999px; font-size:0.7rem; font-weight:600; }
|
|
635
|
+
.tag-ok { background:rgba(0,166,126,0.15); color:#00A67E; }
|
|
636
|
+
.tag-fail { background:#ef444422; color:#ef4444; }
|
|
637
|
+
.tag-default { background:rgba(0,122,255,0.15); color:#007AFF; font-size:0.65rem; margin-left:0.35rem; }
|
|
638
|
+
|
|
639
|
+
/* Buttons */
|
|
640
|
+
.btn { padding:0.5rem 1rem; border:none; border-radius:0.375rem; cursor:pointer; font-size:0.8rem; font-weight:600; transition:opacity 0.15s; }
|
|
641
|
+
.btn:hover { opacity:0.85; }
|
|
642
|
+
.btn:disabled { opacity:0.5; cursor:not-allowed; }
|
|
643
|
+
.btn-blue { background:#007AFF; color:#fff; }
|
|
644
|
+
.btn-amber { background:#f59e0b; color:#fff; }
|
|
645
|
+
.btn-group { display:flex; gap:0.5rem; flex-wrap:wrap; }
|
|
646
|
+
|
|
647
|
+
/* Result box */
|
|
648
|
+
.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; }
|
|
649
|
+
|
|
650
|
+
/* Logs */
|
|
651
|
+
.log-line { font-family:ui-monospace,monospace; font-size:0.75rem; padding:0.2rem 0; line-height:1.4; word-break:break-all; }
|
|
652
|
+
.log-info { color:#6b7fa3; }
|
|
653
|
+
.log-warn { color:#eab308; }
|
|
654
|
+
.log-error { color:#ef4444; }
|
|
655
|
+
.log-success { color:#00A67E; }
|
|
656
|
+
|
|
657
|
+
/* Config */
|
|
658
|
+
.cfg-row { display:flex; justify-content:space-between; padding:0.5rem 0; border-bottom:1px solid #1a2744; font-size:0.85rem; }
|
|
659
|
+
.cfg-key { color:#6b7fa3; }
|
|
660
|
+
.cfg-val { color:#f0f4ff; font-family:ui-monospace,monospace; }
|
|
661
|
+
|
|
662
|
+
/* Loading */
|
|
663
|
+
.loading { color:#6b7fa3; text-align:center; padding:3rem 0; }
|
|
664
|
+
</style>
|
|
665
|
+
</head>
|
|
666
|
+
<body x-data="dashboard()" x-init="init()">
|
|
667
|
+
|
|
668
|
+
<!-- Navbar -->
|
|
669
|
+
<div class="navbar">
|
|
670
|
+
<div class="nav-left">
|
|
671
|
+
<span>🦞 OpenClaw Doctor</span>
|
|
672
|
+
<span class="ver">v${pkgVersion}</span>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="nav-center">
|
|
675
|
+
<span class="status-dot" :style="'background:' + statusColor"></span>
|
|
676
|
+
<span class="status-label" :style="'color:' + statusColor" x-text="statusText"></span>
|
|
677
|
+
</div>
|
|
678
|
+
<div class="nav-actions" style="display:flex;gap:0.5rem;align-items:center;">
|
|
679
|
+
<button class="btn" style="background:#00A67E;color:#fff;padding:0.35rem 0.75rem;font-size:0.75rem;" :disabled="actionLoading" @click="doStart()">\u25B6 Start</button>
|
|
680
|
+
<button class="btn btn-blue" style="padding:0.35rem 0.75rem;font-size:0.75rem;" :disabled="actionLoading" @click="doRestart()">\u21BA Restart</button>
|
|
681
|
+
<button class="btn" style="background:#ef4444;color:#fff;padding:0.35rem 0.75rem;font-size:0.75rem;" :disabled="actionLoading" @click="doStop()">\u25A0 Stop</button>
|
|
682
|
+
</div>
|
|
683
|
+
<div class="nav-right" x-text="lastCheck ? 'Updated ' + lastCheck : 'Loading...'"></div>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<!-- Tabs -->
|
|
687
|
+
<div class="tabs">
|
|
688
|
+
<template x-for="t in ['Overview','Cost','Restarts','Logs','Config']">
|
|
689
|
+
<div class="tab" :class="{ active: tab === t }" @click="tab = t" x-text="t"></div>
|
|
690
|
+
</template>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<!-- Content -->
|
|
694
|
+
<div class="content">
|
|
695
|
+
|
|
696
|
+
<!-- Loading state -->
|
|
697
|
+
<template x-if="!loaded">
|
|
698
|
+
<div class="loading">Loading...</div>
|
|
699
|
+
</template>
|
|
700
|
+
|
|
701
|
+
<!-- Overview Tab -->
|
|
702
|
+
<template x-if="loaded && tab === 'Overview'">
|
|
703
|
+
<div>
|
|
704
|
+
<div class="card">
|
|
705
|
+
<div class="big-status" :style="'color:' + statusColor" x-text="statusText"></div>
|
|
706
|
+
<div class="meta-row">
|
|
707
|
+
Gateway :<span x-text="data.info?.gatewayPort ?? '?'"></span>
|
|
708
|
+
| Latency: <span x-text="(data.durationMs ?? '-') + 'ms'"></span>
|
|
709
|
+
| Profile: <span x-text="data.info?.profile ?? '?'"></span>
|
|
710
|
+
| OpenClaw <span x-text="data.info?.version ?? '?'"></span>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<div class="grid2">
|
|
715
|
+
<!-- Channels -->
|
|
716
|
+
<div class="card">
|
|
717
|
+
<div class="card-title">Channels</div>
|
|
718
|
+
<template x-if="data.channels && data.channels.length > 0">
|
|
719
|
+
<table>
|
|
720
|
+
<thead><tr><th>Name</th><th>Status</th></tr></thead>
|
|
721
|
+
<tbody>
|
|
722
|
+
<template x-for="c in data.channels" :key="c.name">
|
|
723
|
+
<tr>
|
|
724
|
+
<td x-text="c.name"></td>
|
|
725
|
+
<td><span class="tag" :class="c.ok ? 'tag-ok' : 'tag-fail'" x-text="c.ok ? 'OK' : 'FAIL'"></span></td>
|
|
726
|
+
</tr>
|
|
727
|
+
</template>
|
|
728
|
+
</tbody>
|
|
729
|
+
</table>
|
|
730
|
+
</template>
|
|
731
|
+
<template x-if="!data.channels || data.channels.length === 0">
|
|
732
|
+
<div style="color:#6b7fa3;font-size:0.8rem;">No channels</div>
|
|
733
|
+
</template>
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<!-- Agents -->
|
|
737
|
+
<div class="card">
|
|
738
|
+
<div class="card-title">Agents</div>
|
|
739
|
+
<template x-if="data.agents && data.agents.length > 0">
|
|
740
|
+
<table>
|
|
741
|
+
<thead><tr><th>Name</th><th>Model</th><th>Sessions</th><th>Last Active</th><th></th></tr></thead>
|
|
742
|
+
<tbody>
|
|
743
|
+
<template x-for="a in data.agents" :key="a.id">
|
|
744
|
+
<tr x-data="{ rt() { return (data.agentRuntimes||[]).find(r=>r.agentId===a.id) } }">
|
|
745
|
+
<td x-text="a.name"></td>
|
|
746
|
+
<td style="color:#6b7fa3;font-size:0.78rem;" x-text="a.model ? a.model.replace('anthropic/','').replace('openai/','') : '\u2014'"></td>
|
|
747
|
+
<td style="color:#6b7fa3;font-size:0.78rem;" x-text="rt()?.sessions?.count ?? '\u2014'"></td>
|
|
748
|
+
<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>
|
|
749
|
+
<td><template x-if="a.isDefault"><span class="tag tag-default">default</span></template></td>
|
|
750
|
+
</tr>
|
|
751
|
+
</template>
|
|
752
|
+
</tbody>
|
|
753
|
+
</table>
|
|
754
|
+
</template>
|
|
755
|
+
<template x-if="!data.agents || data.agents.length === 0">
|
|
756
|
+
<div style="color:#6b7fa3;font-size:0.8rem;">No agents</div>
|
|
757
|
+
</template>
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
|
|
761
|
+
<!-- Recent checks -->
|
|
762
|
+
<div class="card">
|
|
763
|
+
<div class="card-title">Recent Health Checks</div>
|
|
764
|
+
<template x-if="data.checks && data.checks.length > 0">
|
|
765
|
+
<div style="overflow-x:auto;">
|
|
766
|
+
<table>
|
|
767
|
+
<thead><tr><th>Time</th><th>Status</th><th>Latency</th><th>Error</th></tr></thead>
|
|
768
|
+
<tbody>
|
|
769
|
+
<template x-for="c in data.checks.slice().reverse().slice(0, 10)" :key="c.timestamp">
|
|
770
|
+
<tr>
|
|
771
|
+
<td x-text="fmtTime(c.timestamp)"></td>
|
|
772
|
+
<td><span class="tag" :class="c.healthy ? 'tag-ok' : 'tag-fail'" x-text="c.healthy ? 'OK' : 'FAIL'"></span></td>
|
|
773
|
+
<td x-text="(c.responseTime ?? '-') + 'ms'"></td>
|
|
774
|
+
<td style="color:#ef4444;" x-text="c.error ?? ''"></td>
|
|
775
|
+
</tr>
|
|
776
|
+
</template>
|
|
777
|
+
</tbody>
|
|
778
|
+
</table>
|
|
779
|
+
</div>
|
|
780
|
+
</template>
|
|
781
|
+
<template x-if="!data.checks || data.checks.length === 0">
|
|
782
|
+
<div style="color:#6b7fa3;font-size:0.8rem;">No checks yet</div>
|
|
783
|
+
</template>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
</template>
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
<!-- Workspace Health -->
|
|
790
|
+
<div class="card">
|
|
791
|
+
<div class="card-title">Workspace Health</div>
|
|
792
|
+
<template x-if="data.workspaces && data.workspaces.length > 0">
|
|
793
|
+
<table>
|
|
794
|
+
<thead><tr><th>Agent</th><th>MEMORY.md</th><th>Sessions</th><th>Workspace Size</th><th></th></tr></thead>
|
|
795
|
+
<tbody>
|
|
796
|
+
<template x-for="w in data.workspaces" :key="w.agentId">
|
|
797
|
+
<tr>
|
|
798
|
+
<td x-text="w.agentName"></td>
|
|
799
|
+
<td x-text="w.memoryFileSizeKB + ' KB'"></td>
|
|
800
|
+
<td x-text="w.sessionCount"></td>
|
|
801
|
+
<td x-text="w.totalWorkspaceSizeKB + ' KB'"></td>
|
|
802
|
+
<td>
|
|
803
|
+
<template x-if="w.memoryWarning">
|
|
804
|
+
<span class="tag" style="background:rgba(245,158,11,0.15);color:#f59e0b;">\u26A0 Large</span>
|
|
805
|
+
</template>
|
|
806
|
+
</td>
|
|
807
|
+
</tr>
|
|
808
|
+
</template>
|
|
809
|
+
</tbody>
|
|
810
|
+
</table>
|
|
811
|
+
</template>
|
|
812
|
+
<template x-if="!data.workspaces || data.workspaces.length === 0">
|
|
813
|
+
<div style="color:#6b7fa3;font-size:0.8rem;">No workspace data</div>
|
|
814
|
+
</template>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
<!-- Restarts Tab -->
|
|
818
|
+
<template x-if="loaded && tab === 'Restarts'">
|
|
819
|
+
<div>
|
|
820
|
+
<div class="card">
|
|
821
|
+
<div class="card-title">Actions</div>
|
|
822
|
+
<div class="btn-group">
|
|
823
|
+
<button class="btn btn-blue" :disabled="actionLoading" @click="doRestart()">Restart Gateway</button>
|
|
824
|
+
<button class="btn btn-amber" :disabled="actionLoading" @click="doDoctor()">Run Doctor Fix</button>
|
|
825
|
+
</div>
|
|
826
|
+
<template x-if="actionResult">
|
|
827
|
+
<div class="result-box" x-text="actionResult"></div>
|
|
828
|
+
</template>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<div class="card">
|
|
832
|
+
<div class="card-title">Restart History</div>
|
|
833
|
+
<template x-if="data.restarts && data.restarts.length > 0">
|
|
834
|
+
<div style="overflow-x:auto;">
|
|
835
|
+
<table>
|
|
836
|
+
<thead><tr><th>Time</th><th>Reason</th><th>Result</th></tr></thead>
|
|
837
|
+
<tbody>
|
|
838
|
+
<template x-for="r in data.restarts.slice().reverse()" :key="r.timestamp">
|
|
839
|
+
<tr>
|
|
840
|
+
<td x-text="fmtTime(r.timestamp)"></td>
|
|
841
|
+
<td x-text="r.reason"></td>
|
|
842
|
+
<td><span class="tag" :class="r.success ? 'tag-ok' : 'tag-fail'" x-text="r.success ? 'OK' : 'FAIL'"></span></td>
|
|
843
|
+
</tr>
|
|
844
|
+
</template>
|
|
845
|
+
</tbody>
|
|
846
|
+
</table>
|
|
847
|
+
</div>
|
|
848
|
+
</template>
|
|
849
|
+
<template x-if="!data.restarts || data.restarts.length === 0">
|
|
850
|
+
<div style="color:#6b7fa3;font-size:0.8rem;">No restarts</div>
|
|
851
|
+
</template>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</template>
|
|
855
|
+
|
|
856
|
+
<!-- Logs Tab -->
|
|
857
|
+
<template x-if="loaded && tab === 'Logs'">
|
|
858
|
+
<div>
|
|
859
|
+
<div class="card">
|
|
860
|
+
<div class="card-title">Doctor Logs (latest 50)</div>
|
|
861
|
+
<template x-if="logs.length > 0">
|
|
862
|
+
<div style="max-height:600px;overflow-y:auto;">
|
|
863
|
+
<template x-for="(line, i) in logs" :key="i">
|
|
864
|
+
<div class="log-line" :class="logClass(line)" x-text="line"></div>
|
|
865
|
+
</template>
|
|
866
|
+
</div>
|
|
867
|
+
</template>
|
|
868
|
+
<template x-if="logs.length === 0">
|
|
869
|
+
<div style="color:#6b7fa3;font-size:0.8rem;">No logs yet</div>
|
|
870
|
+
</template>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
</template>
|
|
874
|
+
|
|
875
|
+
<!-- Cost Tab -->
|
|
876
|
+
<template x-if="loaded && tab === 'Cost'">
|
|
877
|
+
<div>
|
|
878
|
+
<template x-if="costData">
|
|
879
|
+
<div>
|
|
880
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
|
|
881
|
+
<div class="card" style="text-align:center;">
|
|
882
|
+
<div class="card-title">Today</div>
|
|
883
|
+
<div style="font-size:2rem;font-weight:700;color:#f0f4ff;" x-text="'$' + (costData.todayTotal||0).toFixed(4)"></div>
|
|
884
|
+
</div>
|
|
885
|
+
<div class="card" style="text-align:center;">
|
|
886
|
+
<div class="card-title">This Week</div>
|
|
887
|
+
<div style="font-size:2rem;font-weight:700;color:#f0f4ff;" x-text="'$' + (costData.weekTotal||0).toFixed(4)"></div>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
<div class="card">
|
|
891
|
+
<div class="card-title">By Agent</div>
|
|
892
|
+
<table>
|
|
893
|
+
<thead><tr><th>Agent</th><th>Today</th><th>This Week</th><th>Sessions</th></tr></thead>
|
|
894
|
+
<tbody>
|
|
895
|
+
<template x-for="a in costData.agents" :key="a.agentId">
|
|
896
|
+
<tr>
|
|
897
|
+
<td x-text="a.agentName"></td>
|
|
898
|
+
<td x-text="'$' + (a.todayCost||0).toFixed(4)"></td>
|
|
899
|
+
<td x-text="'$' + (a.weekCost||0).toFixed(4)"></td>
|
|
900
|
+
<td style="color:#6b7fa3;" x-text="a.sessionCount"></td>
|
|
901
|
+
</tr>
|
|
902
|
+
</template>
|
|
903
|
+
</tbody>
|
|
904
|
+
</table>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
</template>
|
|
908
|
+
<template x-if="!costData">
|
|
909
|
+
<div class="loading">Loading cost data...</div>
|
|
910
|
+
</template>
|
|
911
|
+
</div>
|
|
912
|
+
</template>
|
|
913
|
+
|
|
914
|
+
<!-- Config Tab -->
|
|
915
|
+
<template x-if="loaded && tab === 'Config'">
|
|
916
|
+
<div>
|
|
917
|
+
<div class="card">
|
|
918
|
+
<div class="card-title">Current Configuration</div>
|
|
919
|
+
<template x-if="data.config">
|
|
920
|
+
<div>
|
|
921
|
+
<div class="cfg-row"><span class="cfg-key">checkInterval</span><span class="cfg-val" x-text="data.config.checkInterval + 's'"></span></div>
|
|
922
|
+
<div class="cfg-row"><span class="cfg-key">failThreshold</span><span class="cfg-val" x-text="data.config.failThreshold"></span></div>
|
|
923
|
+
<div class="cfg-row"><span class="cfg-key">dashboardPort</span><span class="cfg-val" x-text="data.config.dashboardPort"></span></div>
|
|
924
|
+
<div class="cfg-row"><span class="cfg-key">maxRestartsPerHour</span><span class="cfg-val" x-text="data.config.maxRestartsPerHour"></span></div>
|
|
925
|
+
<div class="cfg-row"><span class="cfg-key">openclawProfile</span><span class="cfg-val" x-text="data.config.openclawProfile"></span></div>
|
|
926
|
+
<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>
|
|
927
|
+
<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>
|
|
928
|
+
</div>
|
|
929
|
+
</template>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</template>
|
|
933
|
+
|
|
934
|
+
</div>
|
|
935
|
+
|
|
936
|
+
<script>
|
|
937
|
+
function dashboard() {
|
|
938
|
+
return {
|
|
939
|
+
tab: 'Overview',
|
|
940
|
+
costData: null,
|
|
941
|
+
loaded: false,
|
|
942
|
+
data: {},
|
|
943
|
+
logs: [],
|
|
944
|
+
lastCheck: '',
|
|
945
|
+
actionLoading: false,
|
|
946
|
+
actionResult: '',
|
|
947
|
+
|
|
948
|
+
get statusText() {
|
|
949
|
+
if (!this.data || !this.loaded) return 'LOADING';
|
|
950
|
+
if (this.data.healthy) return 'HEALTHY';
|
|
951
|
+
if (this.data.gateway) return 'DEGRADED';
|
|
952
|
+
return 'UNREACHABLE';
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
get statusColor() {
|
|
956
|
+
if (!this.data || !this.loaded) return '#64748b';
|
|
957
|
+
if (this.data.healthy) return '#00A67E';
|
|
958
|
+
if (this.data.gateway) return '#f59e0b';
|
|
959
|
+
return '#ef4444';
|
|
960
|
+
},
|
|
961
|
+
|
|
962
|
+
async init() {
|
|
963
|
+
await this.refresh();
|
|
964
|
+
await this.refreshLogs();
|
|
965
|
+
setInterval(() => this.refresh(), 10000);
|
|
966
|
+
setInterval(() => this.refreshLogs(), 10000);
|
|
967
|
+
},
|
|
968
|
+
|
|
969
|
+
async refresh() {
|
|
970
|
+
try {
|
|
971
|
+
const res = await fetch('/api/status');
|
|
972
|
+
this.data = await res.json();
|
|
973
|
+
this.lastCheck = new Date().toLocaleTimeString();
|
|
974
|
+
this.loaded = true;
|
|
975
|
+
} catch (e) {
|
|
976
|
+
console.error('Failed to fetch status', e);
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
|
|
980
|
+
async refreshCost() {
|
|
981
|
+
try {
|
|
982
|
+
const r = await fetch('/api/cost');
|
|
983
|
+
this.costData = await r.json();
|
|
984
|
+
} catch {}
|
|
985
|
+
},
|
|
986
|
+
async refreshLogs() {
|
|
987
|
+
try {
|
|
988
|
+
const res = await fetch('/api/logs');
|
|
989
|
+
const d = await res.json();
|
|
990
|
+
this.logs = d.lines ?? [];
|
|
991
|
+
} catch (e) {
|
|
992
|
+
console.error('Failed to fetch logs', e);
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
|
|
996
|
+
async doRestart() {
|
|
997
|
+
this.actionLoading = true;
|
|
998
|
+
this.actionResult = '';
|
|
999
|
+
try {
|
|
1000
|
+
const res = await fetch('/api/restart', { method: 'POST' });
|
|
1001
|
+
const d = await res.json();
|
|
1002
|
+
this.actionResult = d.success ? 'Gateway restarted successfully.' : ('Restart failed: ' + (d.error ?? 'unknown'));
|
|
1003
|
+
await this.refresh();
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
this.actionResult = 'Request failed: ' + e.message;
|
|
1006
|
+
}
|
|
1007
|
+
this.actionLoading = false;
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
async doStart() {
|
|
1011
|
+
this.actionLoading = true;
|
|
1012
|
+
this.actionResult = '';
|
|
1013
|
+
try {
|
|
1014
|
+
const res = await fetch('/api/gateway/start', { method: 'POST' });
|
|
1015
|
+
const d = await res.json();
|
|
1016
|
+
this.actionResult = d.success ? 'Gateway started.' : ('Start failed: ' + (d.message ?? 'unknown'));
|
|
1017
|
+
await this.refresh();
|
|
1018
|
+
} catch (e) {
|
|
1019
|
+
this.actionResult = 'Request failed: ' + e.message;
|
|
1020
|
+
}
|
|
1021
|
+
this.actionLoading = false;
|
|
1022
|
+
},
|
|
1023
|
+
|
|
1024
|
+
async doStop() {
|
|
1025
|
+
this.actionLoading = true;
|
|
1026
|
+
this.actionResult = '';
|
|
1027
|
+
try {
|
|
1028
|
+
const res = await fetch('/api/gateway/stop', { method: 'POST' });
|
|
1029
|
+
const d = await res.json();
|
|
1030
|
+
this.actionResult = d.success ? 'Gateway stopped.' : ('Stop failed: ' + (d.message ?? 'unknown'));
|
|
1031
|
+
await this.refresh();
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
this.actionResult = 'Request failed: ' + e.message;
|
|
1034
|
+
}
|
|
1035
|
+
this.actionLoading = false;
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
async doDoctor() {
|
|
1039
|
+
this.actionLoading = true;
|
|
1040
|
+
this.actionResult = '';
|
|
1041
|
+
try {
|
|
1042
|
+
const res = await fetch('/api/doctor', { method: 'POST' });
|
|
1043
|
+
const d = await res.json();
|
|
1044
|
+
this.actionResult = d.output ?? 'No output';
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
this.actionResult = 'Request failed: ' + e.message;
|
|
1047
|
+
}
|
|
1048
|
+
this.actionLoading = false;
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
fmtTime(iso) {
|
|
1052
|
+
if (!iso) return '';
|
|
1053
|
+
try { return new Date(iso).toLocaleString(); } catch { return iso; }
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
logClass(line) {
|
|
1057
|
+
if (line.includes('[ERROR]')) return 'log-error';
|
|
1058
|
+
if (line.includes('[WARN]')) return 'log-warn';
|
|
1059
|
+
if (line.includes('[SUCCESS]')) return 'log-success';
|
|
1060
|
+
return 'log-info';
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
</script>
|
|
1065
|
+
</body>
|
|
1066
|
+
</html>`;
|
|
1067
|
+
}
|
|
1068
|
+
async function handleApiStatus(info, configPath, res) {
|
|
1069
|
+
try {
|
|
1070
|
+
const live = await checkHealth(info);
|
|
1071
|
+
const config = loadConfig(configPath);
|
|
1072
|
+
const workspaces = scanWorkspaces(info);
|
|
1073
|
+
const agentRuntimes = live.agentRuntimes ?? [];
|
|
1074
|
+
const payload = {
|
|
1075
|
+
healthy: live.healthy,
|
|
1076
|
+
gateway: live.gateway,
|
|
1077
|
+
channels: live.channels,
|
|
1078
|
+
agents: info.agents,
|
|
1079
|
+
agentRuntimes,
|
|
1080
|
+
durationMs: live.durationMs,
|
|
1081
|
+
checks: getCheckHistory(),
|
|
1082
|
+
restarts: getRestartHistory(),
|
|
1083
|
+
config,
|
|
1084
|
+
workspaces,
|
|
1085
|
+
info: {
|
|
1086
|
+
version: info.version,
|
|
1087
|
+
gatewayPort: info.gatewayPort,
|
|
1088
|
+
profile: info.profile
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1092
|
+
res.end(JSON.stringify(payload));
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1095
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function handleApiRestart(info, res) {
|
|
1099
|
+
try {
|
|
1100
|
+
const result = await restartGateway(info);
|
|
1101
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1102
|
+
res.end(JSON.stringify({ success: result.success, message: result.output ?? result.error ?? "" }));
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1105
|
+
res.end(JSON.stringify({ success: false, message: String(err) }));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function handleApiGatewayStart(info, res) {
|
|
1109
|
+
try {
|
|
1110
|
+
const result = await startGateway(info);
|
|
1111
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1112
|
+
res.end(JSON.stringify({ success: result.success, message: result.output ?? result.error ?? "" }));
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1115
|
+
res.end(JSON.stringify({ success: false, message: String(err) }));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async function handleApiGatewayStop(info, res) {
|
|
1119
|
+
try {
|
|
1120
|
+
const result = await stopGateway(info);
|
|
1121
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1122
|
+
res.end(JSON.stringify({ success: result.success, message: result.output ?? result.error ?? "" }));
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1125
|
+
res.end(JSON.stringify({ success: false, message: String(err) }));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async function handleApiDoctor(info, res) {
|
|
1129
|
+
try {
|
|
1130
|
+
const output = await runOpenClawCmd(info, "doctor --non-interactive");
|
|
1131
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1132
|
+
res.end(JSON.stringify({ output: output ?? "No output" }));
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1135
|
+
res.end(JSON.stringify({ output: "Error: " + String(err) }));
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async function handleApiCost(info, res) {
|
|
1139
|
+
try {
|
|
1140
|
+
const costs = scanCosts(info.agents);
|
|
1141
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1142
|
+
res.end(JSON.stringify(costs));
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1145
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function handleApiLogs(res) {
|
|
1149
|
+
const lines = readDoctorLogs(50);
|
|
1150
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1151
|
+
res.end(JSON.stringify({ lines }));
|
|
1152
|
+
}
|
|
1153
|
+
function startDashboard(options) {
|
|
1154
|
+
const config = loadConfig(options.config);
|
|
1155
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
1156
|
+
const port = config.dashboardPort;
|
|
1157
|
+
const shell = renderShell();
|
|
1158
|
+
const server = createServer(async (req, res) => {
|
|
1159
|
+
const url = req.url ?? "/";
|
|
1160
|
+
const method = req.method ?? "GET";
|
|
1161
|
+
if (method === "GET" && url === "/") {
|
|
1162
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1163
|
+
res.end(shell);
|
|
1164
|
+
} else if (method === "GET" && url === "/api/status") {
|
|
1165
|
+
await handleApiStatus(info, options.config, res);
|
|
1166
|
+
} else if (method === "GET" && url === "/api/cost") {
|
|
1167
|
+
await handleApiCost(info, res);
|
|
1168
|
+
} else if (method === "GET" && url === "/api/logs") {
|
|
1169
|
+
handleApiLogs(res);
|
|
1170
|
+
} else if (method === "POST" && url === "/api/restart") {
|
|
1171
|
+
await handleApiRestart(info, res);
|
|
1172
|
+
} else if (method === "POST" && url === "/api/gateway/start") {
|
|
1173
|
+
await handleApiGatewayStart(info, res);
|
|
1174
|
+
} else if (method === "POST" && url === "/api/gateway/stop") {
|
|
1175
|
+
await handleApiGatewayStop(info, res);
|
|
1176
|
+
} else if (method === "POST" && url === "/api/doctor") {
|
|
1177
|
+
await handleApiDoctor(info, res);
|
|
1178
|
+
} else {
|
|
1179
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1180
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
server.listen(port, () => {
|
|
1184
|
+
log("info", `Dashboard running at http://localhost:${port}`);
|
|
1185
|
+
console.log(chalk2.green.bold(`
|
|
1186
|
+
Dashboard: http://localhost:${port}
|
|
1187
|
+
`));
|
|
1188
|
+
});
|
|
1189
|
+
const REMOTE_CONFIG_PATH = join8(DOCTOR_HOME, "remote.json");
|
|
1190
|
+
const REMOTE_API_URL2 = "https://api.openclaw-cli.app";
|
|
1191
|
+
let gatewayStartTime = null;
|
|
1192
|
+
const proxyFetch2 = async (url, opts) => {
|
|
1193
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.ALL_PROXY || process.env.all_proxy;
|
|
1194
|
+
if (proxyUrl) {
|
|
1195
|
+
try {
|
|
1196
|
+
const { ProxyAgent, fetch: uf } = await import("undici");
|
|
1197
|
+
return uf(url, { ...opts, dispatcher: new ProxyAgent(proxyUrl) });
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return fetch(url, opts);
|
|
1202
|
+
};
|
|
1203
|
+
const HEARTBEAT_INTERVAL_MS = 10 * 60 * 1e3;
|
|
1204
|
+
const POLL_INTERVAL_MS = 3e4;
|
|
1205
|
+
let lastReportedHealthy = null;
|
|
1206
|
+
let lastReportedChannels = "";
|
|
1207
|
+
let lastReportedAt = 0;
|
|
1208
|
+
const doReport = async (reason) => {
|
|
1209
|
+
try {
|
|
1210
|
+
if (!existsSync7(REMOTE_CONFIG_PATH)) return;
|
|
1211
|
+
const cfg = JSON.parse(readFileSync5(REMOTE_CONFIG_PATH, "utf-8"));
|
|
1212
|
+
if (!cfg.enabled || !cfg.machineToken) return;
|
|
1213
|
+
const status = await checkHealth(info);
|
|
1214
|
+
if (status.healthy) {
|
|
1215
|
+
if (!gatewayStartTime) gatewayStartTime = Date.now();
|
|
1216
|
+
} else {
|
|
1217
|
+
gatewayStartTime = null;
|
|
1218
|
+
}
|
|
1219
|
+
const uptimeSeconds = gatewayStartTime ? Math.floor((Date.now() - gatewayStartTime) / 1e3) : 0;
|
|
1220
|
+
const body = {
|
|
1221
|
+
machineId: cfg.machineId,
|
|
1222
|
+
hostname: osHostname(),
|
|
1223
|
+
os: process.platform + "/" + process.arch,
|
|
1224
|
+
version: pkgVersion ?? "unknown",
|
|
1225
|
+
gateway: {
|
|
1226
|
+
healthy: status.healthy,
|
|
1227
|
+
port: info.gatewayPort,
|
|
1228
|
+
durationMs: status.durationMs ?? 0,
|
|
1229
|
+
label: info.launchdLabel ?? "",
|
|
1230
|
+
uptimeSeconds
|
|
1231
|
+
},
|
|
1232
|
+
agents: info.agents.map((a) => ({
|
|
1233
|
+
id: a.id,
|
|
1234
|
+
name: a.name,
|
|
1235
|
+
isDefault: a.isDefault
|
|
1236
|
+
})),
|
|
1237
|
+
channels: status.channels ?? [],
|
|
1238
|
+
ts: Date.now()
|
|
1239
|
+
};
|
|
1240
|
+
const controller = new AbortController();
|
|
1241
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1242
|
+
await proxyFetch2(cfg.reportUrl, {
|
|
1243
|
+
method: "POST",
|
|
1244
|
+
headers: {
|
|
1245
|
+
"Content-Type": "application/json",
|
|
1246
|
+
Authorization: "Bearer " + cfg.machineToken
|
|
1247
|
+
},
|
|
1248
|
+
body: JSON.stringify(body),
|
|
1249
|
+
signal: controller.signal
|
|
1250
|
+
});
|
|
1251
|
+
clearTimeout(timer);
|
|
1252
|
+
lastReportedHealthy = status.healthy;
|
|
1253
|
+
lastReportedChannels = JSON.stringify(status.channels ?? []);
|
|
1254
|
+
lastReportedAt = Date.now();
|
|
1255
|
+
log("info", `[remote-report] sent (reason=${reason})`);
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
setInterval(async () => {
|
|
1260
|
+
try {
|
|
1261
|
+
if (!existsSync7(REMOTE_CONFIG_PATH)) return;
|
|
1262
|
+
const cfg = JSON.parse(readFileSync5(REMOTE_CONFIG_PATH, "utf-8"));
|
|
1263
|
+
if (!cfg.enabled || !cfg.machineToken) return;
|
|
1264
|
+
const status = await checkHealth(info);
|
|
1265
|
+
const channelsKey = JSON.stringify(status.channels ?? []);
|
|
1266
|
+
const now = Date.now();
|
|
1267
|
+
const healthChanged = lastReportedHealthy !== status.healthy;
|
|
1268
|
+
const channelsChanged = lastReportedChannels !== channelsKey;
|
|
1269
|
+
const heartbeatDue = now - lastReportedAt >= HEARTBEAT_INTERVAL_MS;
|
|
1270
|
+
if (healthChanged || channelsChanged || heartbeatDue) {
|
|
1271
|
+
const reason = healthChanged ? "health-change" : channelsChanged ? "channels-change" : "heartbeat";
|
|
1272
|
+
await doReport(reason);
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
}, POLL_INTERVAL_MS);
|
|
1277
|
+
const executedCommands = /* @__PURE__ */ new Set();
|
|
1278
|
+
setInterval(async () => {
|
|
1279
|
+
try {
|
|
1280
|
+
if (!existsSync7(REMOTE_CONFIG_PATH)) return;
|
|
1281
|
+
const cfg = JSON.parse(readFileSync5(REMOTE_CONFIG_PATH, "utf-8"));
|
|
1282
|
+
if (!cfg.enabled || !cfg.machineToken) return;
|
|
1283
|
+
const controller = new AbortController();
|
|
1284
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
1285
|
+
const res = await proxyFetch2(REMOTE_API_URL2 + "/v1/control/pending", {
|
|
1286
|
+
headers: { Authorization: "Bearer " + cfg.machineToken },
|
|
1287
|
+
signal: controller.signal
|
|
1288
|
+
});
|
|
1289
|
+
clearTimeout(timer);
|
|
1290
|
+
if (!res.ok) return;
|
|
1291
|
+
const data = await res.json();
|
|
1292
|
+
const cmd = data.command;
|
|
1293
|
+
if (!cmd || executedCommands.has(cmd.commandId)) return;
|
|
1294
|
+
executedCommands.add(cmd.commandId);
|
|
1295
|
+
log("info", `[remote-control] action=${cmd.action} commandId=${cmd.commandId}`);
|
|
1296
|
+
let ok = true;
|
|
1297
|
+
let error;
|
|
1298
|
+
try {
|
|
1299
|
+
if (cmd.action === "start") {
|
|
1300
|
+
await startGateway(info);
|
|
1301
|
+
gatewayStartTime = Date.now();
|
|
1302
|
+
} else if (cmd.action === "stop") {
|
|
1303
|
+
await stopGateway(info);
|
|
1304
|
+
gatewayStartTime = null;
|
|
1305
|
+
} else if (cmd.action === "restart") {
|
|
1306
|
+
await restartGateway(info);
|
|
1307
|
+
gatewayStartTime = Date.now();
|
|
1308
|
+
}
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
ok = false;
|
|
1311
|
+
error = String(err);
|
|
1312
|
+
log("warn", `[remote-control] failed: ${error}`);
|
|
1313
|
+
}
|
|
1314
|
+
const ackCtrl = new AbortController();
|
|
1315
|
+
const ackTimer = setTimeout(() => ackCtrl.abort(), 3e3);
|
|
1316
|
+
await proxyFetch2(REMOTE_API_URL2 + "/v1/control/ack", {
|
|
1317
|
+
method: "POST",
|
|
1318
|
+
headers: {
|
|
1319
|
+
"Content-Type": "application/json",
|
|
1320
|
+
Authorization: "Bearer " + cfg.machineToken
|
|
1321
|
+
},
|
|
1322
|
+
body: JSON.stringify({ commandId: cmd.commandId, ok, error }),
|
|
1323
|
+
signal: ackCtrl.signal
|
|
1324
|
+
}).catch(() => {
|
|
1325
|
+
});
|
|
1326
|
+
clearTimeout(ackTimer);
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
}, 15e3);
|
|
1330
|
+
}
|
|
1331
|
+
var _PKG_VER, pkgVersion;
|
|
1332
|
+
var init_server = __esm({
|
|
1333
|
+
"src/dashboard/server.ts"() {
|
|
1334
|
+
"use strict";
|
|
1335
|
+
init_config();
|
|
1336
|
+
init_openclaw();
|
|
1337
|
+
init_health_checker();
|
|
1338
|
+
init_logger();
|
|
1339
|
+
init_process_manager();
|
|
1340
|
+
init_workspace_scanner();
|
|
1341
|
+
init_cost_scanner();
|
|
1342
|
+
_PKG_VER = true ? "0.6.1" : "0.2.1";
|
|
1343
|
+
pkgVersion = _PKG_VER;
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
22
1346
|
|
|
23
1347
|
// src/index.ts
|
|
1348
|
+
init_brand();
|
|
24
1349
|
import { spawnSync } from "child_process";
|
|
25
1350
|
import { Command } from "commander";
|
|
26
1351
|
|
|
27
1352
|
// src/commands/watch.ts
|
|
1353
|
+
init_config();
|
|
1354
|
+
init_logger();
|
|
1355
|
+
init_health_checker();
|
|
1356
|
+
init_process_manager();
|
|
1357
|
+
init_openclaw();
|
|
28
1358
|
import { spawn } from "child_process";
|
|
29
|
-
import { writeFileSync as
|
|
30
|
-
import
|
|
31
|
-
import { join as
|
|
1359
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync, openSync } from "fs";
|
|
1360
|
+
import chalk3 from "chalk";
|
|
1361
|
+
import { join as join9 } from "path";
|
|
32
1362
|
|
|
33
1363
|
// src/telemetry.ts
|
|
1364
|
+
init_brand();
|
|
34
1365
|
import { createHash, randomUUID } from "crypto";
|
|
35
|
-
import { execSync } from "child_process";
|
|
36
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
37
|
-
import { join } from "path";
|
|
1366
|
+
import { execSync as execSync2 } from "child_process";
|
|
1367
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
|
|
1368
|
+
import { join as join5 } from "path";
|
|
38
1369
|
var MEASUREMENT_ID = "G-B46J8RT804";
|
|
39
1370
|
var API_SECRET = "qkqms1nURj2S02Q3WqO7GQ";
|
|
40
1371
|
var ENDPOINT = `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
|
|
41
|
-
var TELEMETRY_FILE =
|
|
1372
|
+
var TELEMETRY_FILE = join5(APP_HOME, "telemetry.json");
|
|
42
1373
|
function loadState() {
|
|
43
|
-
if (
|
|
1374
|
+
if (existsSync4(TELEMETRY_FILE)) {
|
|
44
1375
|
try {
|
|
45
|
-
return JSON.parse(
|
|
1376
|
+
return JSON.parse(readFileSync3(TELEMETRY_FILE, "utf-8"));
|
|
46
1377
|
} catch {
|
|
47
1378
|
}
|
|
48
1379
|
}
|
|
@@ -56,7 +1387,7 @@ function loadState() {
|
|
|
56
1387
|
}
|
|
57
1388
|
function saveState(state) {
|
|
58
1389
|
try {
|
|
59
|
-
|
|
1390
|
+
writeFileSync2(TELEMETRY_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
60
1391
|
} catch {
|
|
61
1392
|
}
|
|
62
1393
|
}
|
|
@@ -65,7 +1396,7 @@ function sha256(input) {
|
|
|
65
1396
|
}
|
|
66
1397
|
function tryExec(cmd) {
|
|
67
1398
|
try {
|
|
68
|
-
return
|
|
1399
|
+
return execSync2(cmd, { stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).toString().trim();
|
|
69
1400
|
} catch {
|
|
70
1401
|
return null;
|
|
71
1402
|
}
|
|
@@ -165,7 +1496,7 @@ async function watchDaemon(options) {
|
|
|
165
1496
|
initLogger();
|
|
166
1497
|
ensureDoctorHome();
|
|
167
1498
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
168
|
-
|
|
1499
|
+
writeFileSync3(PID_FILE, String(process.pid));
|
|
169
1500
|
trackCommand("watch start", true).catch(() => {
|
|
170
1501
|
});
|
|
171
1502
|
log("info", "OpenClaw Doctor started (foreground)");
|
|
@@ -174,7 +1505,7 @@ async function watchDaemon(options) {
|
|
|
174
1505
|
log("info", `Check interval: ${config.checkInterval}s`);
|
|
175
1506
|
log("info", `PID: ${process.pid}`);
|
|
176
1507
|
if (options.dashboard) {
|
|
177
|
-
const { startDashboard: startDashboard2 } = await
|
|
1508
|
+
const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
178
1509
|
startDashboard2({ config: options.config });
|
|
179
1510
|
}
|
|
180
1511
|
const throttle = new RestartThrottle(config.maxRestartsPerHour);
|
|
@@ -182,6 +1513,10 @@ async function watchDaemon(options) {
|
|
|
182
1513
|
let isRestarting = false;
|
|
183
1514
|
async function tick() {
|
|
184
1515
|
if (isRestarting) return;
|
|
1516
|
+
if (existsSync8(STOP_FLAG_FILE)) {
|
|
1517
|
+
log("info", "Gateway is manually stopped \u2014 skipping auto-restart");
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
185
1520
|
const result = await checkHealth(info);
|
|
186
1521
|
if (result.healthy) {
|
|
187
1522
|
consecutiveFailures = 0;
|
|
@@ -204,7 +1539,7 @@ async function watchDaemon(options) {
|
|
|
204
1539
|
consecutiveFailures = 0;
|
|
205
1540
|
throttle.record();
|
|
206
1541
|
await restartGateway(info);
|
|
207
|
-
log("info", "Waiting
|
|
1542
|
+
log("info", "Waiting 60s for gateway to start...");
|
|
208
1543
|
await new Promise((r) => setTimeout(r, 6e4));
|
|
209
1544
|
isRestarting = false;
|
|
210
1545
|
}
|
|
@@ -226,7 +1561,7 @@ function daemonize(options) {
|
|
|
226
1561
|
ensureDoctorHome();
|
|
227
1562
|
const existingPid = readDaemonPid();
|
|
228
1563
|
if (existingPid && isProcessAlive(existingPid)) {
|
|
229
|
-
console.log(
|
|
1564
|
+
console.log(chalk3.yellow(`Doctor is already running (PID ${existingPid})`));
|
|
230
1565
|
return;
|
|
231
1566
|
}
|
|
232
1567
|
const execArgv = process.execArgv.filter(
|
|
@@ -236,8 +1571,8 @@ function daemonize(options) {
|
|
|
236
1571
|
(a) => a !== "-d" && a !== "--daemon"
|
|
237
1572
|
);
|
|
238
1573
|
const fullArgs = [...execArgv, ...scriptArgs];
|
|
239
|
-
const outLog =
|
|
240
|
-
const errLog =
|
|
1574
|
+
const outLog = join9(DOCTOR_LOG_DIR, "daemon.out.log");
|
|
1575
|
+
const errLog = join9(DOCTOR_LOG_DIR, "daemon.err.log");
|
|
241
1576
|
const out = openSync(outLog, "a");
|
|
242
1577
|
const err = openSync(errLog, "a");
|
|
243
1578
|
const child = spawn(process.execPath, fullArgs, {
|
|
@@ -246,20 +1581,20 @@ function daemonize(options) {
|
|
|
246
1581
|
env: { ...process.env, OPENCLAW_DOCTOR_DAEMON: "1" }
|
|
247
1582
|
});
|
|
248
1583
|
const pid = child.pid;
|
|
249
|
-
|
|
1584
|
+
writeFileSync3(PID_FILE, String(pid));
|
|
250
1585
|
child.unref();
|
|
251
|
-
console.log(
|
|
252
|
-
console.log(
|
|
253
|
-
console.log(
|
|
1586
|
+
console.log(chalk3.green(`Doctor started in background (PID ${pid})`));
|
|
1587
|
+
console.log(chalk3.gray(` Logs: ${outLog}`));
|
|
1588
|
+
console.log(chalk3.gray(` Stop: openclaw-doctor stop`));
|
|
254
1589
|
}
|
|
255
1590
|
async function stopDaemon(options) {
|
|
256
1591
|
const pid = readDaemonPid();
|
|
257
1592
|
if (!pid) {
|
|
258
|
-
console.log(
|
|
1593
|
+
console.log(chalk3.yellow("Doctor is not running (no PID file)"));
|
|
259
1594
|
return;
|
|
260
1595
|
}
|
|
261
1596
|
if (!isProcessAlive(pid)) {
|
|
262
|
-
console.log(
|
|
1597
|
+
console.log(chalk3.yellow(`Doctor is not running (PID ${pid} is dead, cleaning up)`));
|
|
263
1598
|
try {
|
|
264
1599
|
unlinkSync(PID_FILE);
|
|
265
1600
|
} catch {
|
|
@@ -268,11 +1603,11 @@ async function stopDaemon(options) {
|
|
|
268
1603
|
}
|
|
269
1604
|
try {
|
|
270
1605
|
process.kill(pid, "SIGTERM");
|
|
271
|
-
console.log(
|
|
1606
|
+
console.log(chalk3.green(`Doctor stopped (PID ${pid})`));
|
|
272
1607
|
trackCommand("watch stop", true).catch(() => {
|
|
273
1608
|
});
|
|
274
1609
|
} catch (err) {
|
|
275
|
-
console.log(
|
|
1610
|
+
console.log(chalk3.red(`Failed to stop Doctor (PID ${pid}): ${err}`));
|
|
276
1611
|
trackCommand("watch stop", false).catch(() => {
|
|
277
1612
|
});
|
|
278
1613
|
}
|
|
@@ -283,8 +1618,8 @@ async function stopDaemon(options) {
|
|
|
283
1618
|
}
|
|
284
1619
|
}
|
|
285
1620
|
function readDaemonPid() {
|
|
286
|
-
if (!
|
|
287
|
-
const raw =
|
|
1621
|
+
if (!existsSync8(PID_FILE)) return null;
|
|
1622
|
+
const raw = readFileSync6(PID_FILE, "utf-8").trim();
|
|
288
1623
|
const pid = parseInt(raw, 10);
|
|
289
1624
|
return isNaN(pid) ? null : pid;
|
|
290
1625
|
}
|
|
@@ -298,7 +1633,10 @@ function isProcessAlive(pid) {
|
|
|
298
1633
|
}
|
|
299
1634
|
|
|
300
1635
|
// src/commands/status.ts
|
|
301
|
-
|
|
1636
|
+
init_config();
|
|
1637
|
+
init_openclaw();
|
|
1638
|
+
init_health_checker();
|
|
1639
|
+
import chalk4 from "chalk";
|
|
302
1640
|
async function showStatus(options) {
|
|
303
1641
|
const config = loadConfig(options.config);
|
|
304
1642
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
@@ -332,32 +1670,32 @@ async function showStatus(options) {
|
|
|
332
1670
|
process.exit(result.healthy ? 0 : 1);
|
|
333
1671
|
return;
|
|
334
1672
|
}
|
|
335
|
-
console.log(
|
|
1673
|
+
console.log(chalk4.bold("\n OpenClaw Doctor\n"));
|
|
336
1674
|
if (result.healthy) {
|
|
337
1675
|
console.log(
|
|
338
|
-
|
|
1676
|
+
chalk4.green.bold(` Gateway: HEALTHY`) + chalk4.gray(` (port ${info.gatewayPort}, ${result.durationMs}ms)`)
|
|
339
1677
|
);
|
|
340
1678
|
} else if (result.gateway) {
|
|
341
|
-
console.log(
|
|
1679
|
+
console.log(chalk4.yellow.bold(` Gateway: DEGRADED`) + chalk4.gray(` (responded but ok=false)`));
|
|
342
1680
|
} else {
|
|
343
|
-
console.log(
|
|
344
|
-
if (result.error) console.log(
|
|
1681
|
+
console.log(chalk4.red.bold(` Gateway: UNREACHABLE`));
|
|
1682
|
+
if (result.error) console.log(chalk4.red(` ${result.error}`));
|
|
345
1683
|
}
|
|
346
1684
|
if (result.channels && result.channels.length > 0) {
|
|
347
1685
|
console.log();
|
|
348
1686
|
for (const ch of result.channels ?? []) {
|
|
349
|
-
const icon = ch.ok ?
|
|
350
|
-
console.log(` ${
|
|
1687
|
+
const icon = ch.ok ? chalk4.green("ok") : chalk4.red("fail");
|
|
1688
|
+
console.log(` ${chalk4.gray("Channel")} ${ch.name}: ${icon}`);
|
|
351
1689
|
}
|
|
352
1690
|
}
|
|
353
1691
|
if (info.agents.length > 0) {
|
|
354
1692
|
console.log();
|
|
355
1693
|
const agentList = info.agents.map((a) => a.isDefault ? `${a.name} (default)` : a.name).join(", ");
|
|
356
|
-
console.log(
|
|
1694
|
+
console.log(chalk4.gray(` Agents: ${agentList}`));
|
|
357
1695
|
}
|
|
358
1696
|
console.log();
|
|
359
|
-
console.log(
|
|
360
|
-
console.log(
|
|
1697
|
+
console.log(chalk4.gray(` OpenClaw ${info.version ?? "unknown"}`));
|
|
1698
|
+
console.log(chalk4.gray(` Config: ${info.configPath}`));
|
|
361
1699
|
console.log();
|
|
362
1700
|
trackCommand("status", true).catch(() => {
|
|
363
1701
|
});
|
|
@@ -365,13 +1703,16 @@ async function showStatus(options) {
|
|
|
365
1703
|
}
|
|
366
1704
|
|
|
367
1705
|
// src/commands/doctor.ts
|
|
368
|
-
|
|
369
|
-
|
|
1706
|
+
init_config();
|
|
1707
|
+
init_openclaw();
|
|
1708
|
+
init_health_checker();
|
|
1709
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync9 } from "fs";
|
|
1710
|
+
import chalk5 from "chalk";
|
|
370
1711
|
function findConfigIssues(configPath) {
|
|
371
|
-
if (!
|
|
1712
|
+
if (!existsSync9(configPath)) return [];
|
|
372
1713
|
let raw;
|
|
373
1714
|
try {
|
|
374
|
-
raw = JSON.parse(
|
|
1715
|
+
raw = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
375
1716
|
} catch {
|
|
376
1717
|
return [{ path: "root", message: "Config file is not valid JSON", fix: () => {
|
|
377
1718
|
} }];
|
|
@@ -398,7 +1739,7 @@ function findConfigIssues(configPath) {
|
|
|
398
1739
|
const origFix = originalFixes[i];
|
|
399
1740
|
issues[i].fix = () => {
|
|
400
1741
|
origFix();
|
|
401
|
-
|
|
1742
|
+
writeFileSync4(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
402
1743
|
};
|
|
403
1744
|
}
|
|
404
1745
|
}
|
|
@@ -407,53 +1748,53 @@ function findConfigIssues(configPath) {
|
|
|
407
1748
|
async function runDoctor(options) {
|
|
408
1749
|
const config = loadConfig(options.config);
|
|
409
1750
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
410
|
-
console.log(
|
|
411
|
-
console.log(
|
|
1751
|
+
console.log(chalk5.bold("\n OpenClaw Doctor \u2014 Full Diagnostics\n"));
|
|
1752
|
+
console.log(chalk5.gray(" [0/4] Config validation"));
|
|
412
1753
|
const issues = findConfigIssues(info.configPath);
|
|
413
1754
|
if (issues.length === 0) {
|
|
414
|
-
console.log(
|
|
1755
|
+
console.log(chalk5.green(" Config: valid"));
|
|
415
1756
|
} else {
|
|
416
1757
|
for (const issue of issues) {
|
|
417
|
-
console.log(
|
|
1758
|
+
console.log(chalk5.red(` ${issue.path}: ${issue.message}`));
|
|
418
1759
|
}
|
|
419
1760
|
if (options.fix) {
|
|
420
1761
|
for (const issue of issues) {
|
|
421
1762
|
issue.fix();
|
|
422
|
-
console.log(
|
|
1763
|
+
console.log(chalk5.green(` Fixed: ${issue.path}`));
|
|
423
1764
|
}
|
|
424
|
-
console.log(
|
|
1765
|
+
console.log(chalk5.green(` Config saved: ${info.configPath}`));
|
|
425
1766
|
} else {
|
|
426
|
-
console.log(
|
|
1767
|
+
console.log(chalk5.yellow(" Run with --fix to auto-repair"));
|
|
427
1768
|
}
|
|
428
1769
|
}
|
|
429
|
-
console.log(
|
|
1770
|
+
console.log(chalk5.gray("\n [1/4] OpenClaw binary"));
|
|
430
1771
|
if (info.cliBinPath) {
|
|
431
|
-
console.log(
|
|
432
|
-
console.log(
|
|
433
|
-
if (info.version) console.log(
|
|
1772
|
+
console.log(chalk5.green(` Found: ${info.cliBinPath}`));
|
|
1773
|
+
console.log(chalk5.gray(` Node: ${info.nodePath}`));
|
|
1774
|
+
if (info.version) console.log(chalk5.gray(` Version: ${info.version}`));
|
|
434
1775
|
} else {
|
|
435
|
-
console.log(
|
|
1776
|
+
console.log(chalk5.red(" Not found \u2014 openclaw CLI is not installed or not in PATH"));
|
|
436
1777
|
}
|
|
437
|
-
console.log(
|
|
1778
|
+
console.log(chalk5.gray("\n [2/4] Gateway health"));
|
|
438
1779
|
const result = await checkHealth(info);
|
|
439
1780
|
if (result.healthy) {
|
|
440
|
-
console.log(
|
|
1781
|
+
console.log(chalk5.green(` Gateway: healthy (port ${info.gatewayPort}, ${result.durationMs}ms)`));
|
|
441
1782
|
} else if (result.gateway) {
|
|
442
|
-
console.log(
|
|
1783
|
+
console.log(chalk5.yellow(` Gateway: responded but degraded`));
|
|
443
1784
|
} else {
|
|
444
|
-
console.log(
|
|
445
|
-
if (result.error) console.log(
|
|
1785
|
+
console.log(chalk5.red(` Gateway: unreachable`));
|
|
1786
|
+
if (result.error) console.log(chalk5.red(` ${result.error}`));
|
|
446
1787
|
}
|
|
447
|
-
console.log(
|
|
1788
|
+
console.log(chalk5.gray("\n [3/4] Channels"));
|
|
448
1789
|
if (result.channels.length > 0) {
|
|
449
1790
|
for (const ch of result.channels) {
|
|
450
|
-
const status = ch.ok ?
|
|
1791
|
+
const status = ch.ok ? chalk5.green("ok") : chalk5.red("fail");
|
|
451
1792
|
console.log(` ${ch.name}: ${status}`);
|
|
452
1793
|
}
|
|
453
1794
|
} else {
|
|
454
|
-
console.log(
|
|
1795
|
+
console.log(chalk5.yellow(" No channel data available"));
|
|
455
1796
|
}
|
|
456
|
-
console.log(
|
|
1797
|
+
console.log(chalk5.gray("\n [4/4] OpenClaw built-in doctor"));
|
|
457
1798
|
const doctorOutput = await runOpenClawCmd(info, "doctor");
|
|
458
1799
|
if (doctorOutput) {
|
|
459
1800
|
const lines = doctorOutput.split("\n");
|
|
@@ -463,24 +1804,24 @@ async function runDoctor(options) {
|
|
|
463
1804
|
console.log(` ${line}`);
|
|
464
1805
|
}
|
|
465
1806
|
} else {
|
|
466
|
-
console.log(
|
|
1807
|
+
console.log(chalk5.yellow(" Could not run openclaw doctor"));
|
|
467
1808
|
}
|
|
468
1809
|
if (options.fix) {
|
|
469
|
-
console.log(
|
|
1810
|
+
console.log(chalk5.gray("\n [5/5] Auto-repair"));
|
|
470
1811
|
if (!result.healthy) {
|
|
471
|
-
console.log(
|
|
1812
|
+
console.log(chalk5.yellow(" Gateway unhealthy \u2014 running openclaw doctor --repair --non-interactive"));
|
|
472
1813
|
const repairOutput = await runOpenClawCmd(info, "doctor --repair --non-interactive");
|
|
473
1814
|
if (repairOutput) {
|
|
474
1815
|
const lines = repairOutput.split("\n");
|
|
475
1816
|
for (const line of lines.slice(0, 30)) {
|
|
476
1817
|
if (line.trim()) console.log(` ${line}`);
|
|
477
1818
|
}
|
|
478
|
-
console.log(
|
|
1819
|
+
console.log(chalk5.green(" Repair completed"));
|
|
479
1820
|
} else {
|
|
480
|
-
console.log(
|
|
1821
|
+
console.log(chalk5.yellow(" Could not run repair (openclaw CLI unavailable)"));
|
|
481
1822
|
}
|
|
482
1823
|
} else {
|
|
483
|
-
console.log(
|
|
1824
|
+
console.log(chalk5.green(" Gateway healthy \u2014 no repair needed"));
|
|
484
1825
|
}
|
|
485
1826
|
}
|
|
486
1827
|
trackCommand("doctor", true).catch(() => {
|
|
@@ -489,8 +1830,11 @@ async function runDoctor(options) {
|
|
|
489
1830
|
}
|
|
490
1831
|
|
|
491
1832
|
// src/commands/logs.ts
|
|
492
|
-
|
|
493
|
-
|
|
1833
|
+
init_config();
|
|
1834
|
+
init_openclaw();
|
|
1835
|
+
init_config();
|
|
1836
|
+
import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
|
|
1837
|
+
import chalk6 from "chalk";
|
|
494
1838
|
function showLogs(options) {
|
|
495
1839
|
const config = loadConfig(options.config);
|
|
496
1840
|
const maxLines = parseInt(options.lines ?? "50", 10);
|
|
@@ -500,74 +1844,85 @@ function showLogs(options) {
|
|
|
500
1844
|
}
|
|
501
1845
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
502
1846
|
const logFile = options.error ? `${info.logDir}/gateway.err.log` : `${info.logDir}/gateway.log`;
|
|
503
|
-
if (!
|
|
504
|
-
console.log(
|
|
1847
|
+
if (!existsSync10(logFile)) {
|
|
1848
|
+
console.log(chalk6.yellow(`Log file not found: ${logFile}`));
|
|
505
1849
|
return;
|
|
506
1850
|
}
|
|
507
|
-
console.log(
|
|
1851
|
+
console.log(chalk6.blue.bold(`
|
|
508
1852
|
${logFile}
|
|
509
1853
|
`));
|
|
510
|
-
const content =
|
|
1854
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
511
1855
|
const lines = content.trim().split("\n");
|
|
512
1856
|
const tail = lines.slice(-maxLines);
|
|
513
1857
|
for (const line of tail) {
|
|
514
1858
|
if (line.includes("[error]") || line.includes("[ERROR]")) {
|
|
515
|
-
console.log(
|
|
1859
|
+
console.log(chalk6.red(line));
|
|
516
1860
|
} else if (line.includes("[warn]") || line.includes("[WARN]")) {
|
|
517
|
-
console.log(
|
|
1861
|
+
console.log(chalk6.yellow(line));
|
|
518
1862
|
} else {
|
|
519
|
-
console.log(
|
|
1863
|
+
console.log(chalk6.gray(line));
|
|
520
1864
|
}
|
|
521
1865
|
}
|
|
522
1866
|
console.log();
|
|
523
1867
|
}
|
|
524
1868
|
function showDoctorLogs(maxLines) {
|
|
525
|
-
const { readdirSync } = __require("fs");
|
|
526
|
-
const { join:
|
|
527
|
-
if (!
|
|
528
|
-
console.log(
|
|
1869
|
+
const { readdirSync: readdirSync5 } = __require("fs");
|
|
1870
|
+
const { join: join12 } = __require("path");
|
|
1871
|
+
if (!existsSync10(DOCTOR_LOG_DIR)) {
|
|
1872
|
+
console.log(chalk6.yellow("No doctor logs found."));
|
|
529
1873
|
return;
|
|
530
1874
|
}
|
|
531
|
-
const files =
|
|
1875
|
+
const files = readdirSync5(DOCTOR_LOG_DIR).filter((f) => f.endsWith(".log")).sort().reverse();
|
|
532
1876
|
if (files.length === 0) {
|
|
533
|
-
console.log(
|
|
1877
|
+
console.log(chalk6.yellow("No doctor log files found."));
|
|
534
1878
|
return;
|
|
535
1879
|
}
|
|
536
1880
|
const latest = files[0];
|
|
537
|
-
console.log(
|
|
538
|
-
${
|
|
1881
|
+
console.log(chalk6.blue.bold(`
|
|
1882
|
+
${join12(DOCTOR_LOG_DIR, latest)}
|
|
539
1883
|
`));
|
|
540
|
-
const content =
|
|
1884
|
+
const content = readFileSync8(join12(DOCTOR_LOG_DIR, latest), "utf-8");
|
|
541
1885
|
const lines = content.trim().split("\n");
|
|
542
1886
|
const tail = lines.slice(-maxLines);
|
|
543
1887
|
for (const line of tail) {
|
|
544
1888
|
if (line.includes("[ERROR]")) {
|
|
545
|
-
console.log(
|
|
1889
|
+
console.log(chalk6.red(line));
|
|
546
1890
|
} else if (line.includes("[WARN]")) {
|
|
547
|
-
console.log(
|
|
1891
|
+
console.log(chalk6.yellow(line));
|
|
548
1892
|
} else if (line.includes("[SUCCESS]")) {
|
|
549
|
-
console.log(
|
|
1893
|
+
console.log(chalk6.green(line));
|
|
550
1894
|
} else {
|
|
551
|
-
console.log(
|
|
1895
|
+
console.log(chalk6.gray(line));
|
|
552
1896
|
}
|
|
553
1897
|
}
|
|
554
1898
|
console.log();
|
|
555
1899
|
}
|
|
556
1900
|
|
|
557
1901
|
// src/commands/gateway.ts
|
|
558
|
-
|
|
559
|
-
|
|
1902
|
+
init_config();
|
|
1903
|
+
init_config();
|
|
1904
|
+
init_openclaw();
|
|
1905
|
+
init_process_manager();
|
|
1906
|
+
init_logger();
|
|
1907
|
+
import chalk7 from "chalk";
|
|
1908
|
+
import { writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
1909
|
+
var _VER = true ? "0.6.1" : void 0;
|
|
560
1910
|
async function gatewayStart(options) {
|
|
561
1911
|
const config = loadConfig(options.config);
|
|
562
1912
|
initLogger();
|
|
1913
|
+
ensureDoctorHome();
|
|
563
1914
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
1915
|
+
try {
|
|
1916
|
+
unlinkSync2(STOP_FLAG_FILE);
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
564
1919
|
const result = await startGateway(info);
|
|
565
1920
|
if (result.success) {
|
|
566
|
-
console.log(
|
|
1921
|
+
console.log(chalk7.green("Gateway started (auto-restart resumed)"));
|
|
567
1922
|
trackCommand("gateway start", true, _VER).catch(() => {
|
|
568
1923
|
});
|
|
569
1924
|
} else {
|
|
570
|
-
console.log(
|
|
1925
|
+
console.log(chalk7.red(`Failed to start gateway: ${result.error}`));
|
|
571
1926
|
trackCommand("gateway start", false, _VER).catch(() => {
|
|
572
1927
|
});
|
|
573
1928
|
process.exit(1);
|
|
@@ -576,14 +1931,17 @@ async function gatewayStart(options) {
|
|
|
576
1931
|
async function gatewayStop(options) {
|
|
577
1932
|
const config = loadConfig(options.config);
|
|
578
1933
|
initLogger();
|
|
1934
|
+
ensureDoctorHome();
|
|
579
1935
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
580
1936
|
const result = await stopGateway(info);
|
|
581
1937
|
if (result.success) {
|
|
582
|
-
|
|
1938
|
+
writeFileSync5(STOP_FLAG_FILE, (/* @__PURE__ */ new Date()).toISOString());
|
|
1939
|
+
console.log(chalk7.green("Gateway stopped (auto-restart paused)"));
|
|
1940
|
+
console.log(chalk7.gray(" Run `gateway start` to resume."));
|
|
583
1941
|
trackCommand("gateway stop", true, _VER).catch(() => {
|
|
584
1942
|
});
|
|
585
1943
|
} else {
|
|
586
|
-
console.log(
|
|
1944
|
+
console.log(chalk7.red(`Failed to stop gateway: ${result.error}`));
|
|
587
1945
|
trackCommand("gateway stop", false, _VER).catch(() => {
|
|
588
1946
|
});
|
|
589
1947
|
process.exit(1);
|
|
@@ -592,14 +1950,19 @@ async function gatewayStop(options) {
|
|
|
592
1950
|
async function gatewayRestart(options) {
|
|
593
1951
|
const config = loadConfig(options.config);
|
|
594
1952
|
initLogger();
|
|
1953
|
+
ensureDoctorHome();
|
|
595
1954
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
1955
|
+
try {
|
|
1956
|
+
unlinkSync2(STOP_FLAG_FILE);
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
596
1959
|
const result = await restartGateway(info);
|
|
597
1960
|
if (result.success) {
|
|
598
|
-
console.log(
|
|
1961
|
+
console.log(chalk7.green("Gateway restarted (auto-restart resumed)"));
|
|
599
1962
|
trackCommand("gateway restart", true, _VER).catch(() => {
|
|
600
1963
|
});
|
|
601
1964
|
} else {
|
|
602
|
-
console.log(
|
|
1965
|
+
console.log(chalk7.red(`Failed to restart gateway: ${result.error}`));
|
|
603
1966
|
trackCommand("gateway restart", false, _VER).catch(() => {
|
|
604
1967
|
});
|
|
605
1968
|
process.exit(1);
|
|
@@ -607,42 +1970,44 @@ async function gatewayRestart(options) {
|
|
|
607
1970
|
}
|
|
608
1971
|
|
|
609
1972
|
// src/commands/memory.ts
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
import
|
|
613
|
-
import {
|
|
614
|
-
|
|
615
|
-
|
|
1973
|
+
init_config();
|
|
1974
|
+
init_openclaw();
|
|
1975
|
+
import chalk8 from "chalk";
|
|
1976
|
+
import { existsSync as existsSync12, statSync as statSync3 } from "fs";
|
|
1977
|
+
import { join as join10 } from "path";
|
|
1978
|
+
import { homedir as homedir5 } from "os";
|
|
1979
|
+
function expandHome2(p) {
|
|
1980
|
+
return p.startsWith("~/") ? join10(homedir5(), p.slice(2)) : p;
|
|
616
1981
|
}
|
|
617
1982
|
async function memoryStatus(options) {
|
|
618
1983
|
const config = loadConfig(options.config);
|
|
619
1984
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
620
|
-
console.log(
|
|
1985
|
+
console.log(chalk8.bold("\n Memory Status\n"));
|
|
621
1986
|
for (const agent of info.agents) {
|
|
622
1987
|
const ws = agent.workspace;
|
|
623
1988
|
if (!ws) continue;
|
|
624
|
-
const wsPath =
|
|
625
|
-
const memPath =
|
|
626
|
-
const exists =
|
|
627
|
-
const sizeKB = exists ? Math.round(
|
|
1989
|
+
const wsPath = expandHome2(ws);
|
|
1990
|
+
const memPath = join10(wsPath, "MEMORY.md");
|
|
1991
|
+
const exists = existsSync12(memPath);
|
|
1992
|
+
const sizeKB = exists ? Math.round(statSync3(memPath).size / 1024) : 0;
|
|
628
1993
|
const warn = sizeKB > 50;
|
|
629
|
-
const indicator = warn ?
|
|
630
|
-
const sizeStr = warn ?
|
|
631
|
-
console.log(` ${indicator} ${agent.name.padEnd(16)} MEMORY.md: ${sizeStr}${warn ?
|
|
1994
|
+
const indicator = warn ? chalk8.yellow("\u26A0") : chalk8.green("\u2713");
|
|
1995
|
+
const sizeStr = warn ? chalk8.yellow(`${sizeKB}KB`) : chalk8.gray(`${sizeKB}KB`);
|
|
1996
|
+
console.log(` ${indicator} ${agent.name.padEnd(16)} MEMORY.md: ${sizeStr}${warn ? chalk8.yellow(" \u2014 exceeds 50KB, may waste tokens") : ""}`);
|
|
632
1997
|
}
|
|
633
1998
|
console.log();
|
|
634
1999
|
}
|
|
635
2000
|
async function memorySearch(query, options) {
|
|
636
2001
|
const config = loadConfig(options.config);
|
|
637
2002
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
638
|
-
console.log(
|
|
2003
|
+
console.log(chalk8.bold(`
|
|
639
2004
|
Searching memory: "${query}"
|
|
640
2005
|
`));
|
|
641
2006
|
const output = await runOpenClawCmd(info, `memory search "${query}"`);
|
|
642
2007
|
if (output) {
|
|
643
2008
|
console.log(output);
|
|
644
2009
|
} else {
|
|
645
|
-
console.log(
|
|
2010
|
+
console.log(chalk8.yellow(" No results or openclaw memory search unavailable"));
|
|
646
2011
|
}
|
|
647
2012
|
console.log();
|
|
648
2013
|
}
|
|
@@ -650,41 +2015,276 @@ async function memoryCompact(options) {
|
|
|
650
2015
|
const config = loadConfig(options.config);
|
|
651
2016
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
652
2017
|
const flag = options.dryRun ? "--dry-run" : "";
|
|
653
|
-
console.log(
|
|
2018
|
+
console.log(chalk8.bold(`
|
|
654
2019
|
Memory Compact${options.dryRun ? " (dry run)" : ""}
|
|
655
2020
|
`));
|
|
656
2021
|
const output = await runOpenClawCmd(info, `memory compact ${flag}`);
|
|
657
2022
|
if (output) {
|
|
658
2023
|
console.log(output);
|
|
659
2024
|
} else {
|
|
660
|
-
console.log(
|
|
2025
|
+
console.log(chalk8.yellow(" openclaw memory compact not available"));
|
|
661
2026
|
}
|
|
662
2027
|
console.log();
|
|
663
2028
|
}
|
|
664
2029
|
|
|
2030
|
+
// src/index.ts
|
|
2031
|
+
init_server();
|
|
2032
|
+
init_openclaw();
|
|
2033
|
+
|
|
665
2034
|
// src/commands/telemetry.ts
|
|
666
|
-
import
|
|
2035
|
+
import chalk9 from "chalk";
|
|
667
2036
|
function telemetryOn() {
|
|
668
2037
|
setOptOut(false);
|
|
669
|
-
console.log(
|
|
2038
|
+
console.log(chalk9.green("\u2713 Telemetry enabled. Thanks for helping improve OpenClaw!"));
|
|
670
2039
|
}
|
|
671
2040
|
function telemetryOff() {
|
|
672
2041
|
setOptOut(true);
|
|
673
|
-
console.log(
|
|
2042
|
+
console.log(chalk9.yellow("\u2713 Telemetry disabled. Set OPENCLAW_NO_TELEMETRY=1 to suppress permanently."));
|
|
674
2043
|
}
|
|
675
2044
|
function telemetryStatus() {
|
|
676
2045
|
const { optOut, clientId } = getTelemetryStatus();
|
|
677
|
-
const status = optOut ?
|
|
2046
|
+
const status = optOut ? chalk9.red("disabled") : chalk9.green("enabled");
|
|
678
2047
|
console.log(`Telemetry: ${status}`);
|
|
679
|
-
console.log(`Client ID: ${
|
|
2048
|
+
console.log(`Client ID: ${chalk9.dim(clientId)}`);
|
|
2049
|
+
console.log();
|
|
2050
|
+
console.log(chalk9.dim("Toggle: openclaw telemetry on/off"));
|
|
2051
|
+
console.log(chalk9.dim("Env: OPENCLAW_NO_TELEMETRY=1"));
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/commands/remote.ts
|
|
2055
|
+
init_config();
|
|
2056
|
+
import chalk10 from "chalk";
|
|
2057
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync13, mkdirSync as mkdirSync3 } from "fs";
|
|
2058
|
+
import { join as join11 } from "path";
|
|
2059
|
+
import { randomUUID as randomUUID2, randomBytes, createHash as createHash2 } from "crypto";
|
|
2060
|
+
import { createServer as createServer2 } from "http";
|
|
2061
|
+
import { exec as exec3 } from "child_process";
|
|
2062
|
+
import { hostname } from "os";
|
|
2063
|
+
async function proxyFetch(url, init) {
|
|
2064
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.ALL_PROXY || process.env.all_proxy;
|
|
2065
|
+
if (proxyUrl) {
|
|
2066
|
+
try {
|
|
2067
|
+
const { ProxyAgent, fetch: undiciFetch } = await import("undici");
|
|
2068
|
+
const dispatcher = new ProxyAgent(proxyUrl);
|
|
2069
|
+
return undiciFetch(url, { ...init, dispatcher });
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return fetch(url, init);
|
|
2074
|
+
}
|
|
2075
|
+
var REMOTE_API_URL = "https://api.openclaw-cli.app";
|
|
2076
|
+
var REMOTE_CONFIG_FILE = join11(DOCTOR_HOME, "remote.json");
|
|
2077
|
+
function loadRemoteConfig() {
|
|
2078
|
+
try {
|
|
2079
|
+
if (existsSync13(REMOTE_CONFIG_FILE)) {
|
|
2080
|
+
return JSON.parse(readFileSync10(REMOTE_CONFIG_FILE, "utf-8"));
|
|
2081
|
+
}
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
return {
|
|
2085
|
+
enabled: false,
|
|
2086
|
+
machineId: randomUUID2(),
|
|
2087
|
+
machineToken: "",
|
|
2088
|
+
reportUrl: REMOTE_API_URL + "/v1/report",
|
|
2089
|
+
lastReport: null
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
function saveRemoteConfig(config) {
|
|
2093
|
+
const dir = join11(REMOTE_CONFIG_FILE, "..");
|
|
2094
|
+
if (!existsSync13(dir)) mkdirSync3(dir, { recursive: true });
|
|
2095
|
+
writeFileSync6(REMOTE_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
2096
|
+
}
|
|
2097
|
+
var OAUTH_CLIENT_ID = Buffer.from(
|
|
2098
|
+
"MjM5NDk1OTI0Nzk4LTJtZWFhaTllcjZybTR1bnN0bW4zZmRldHRqZHM2bGJjLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t",
|
|
2099
|
+
"base64"
|
|
2100
|
+
).toString();
|
|
2101
|
+
var _OCS = Buffer.from("R09DU1BYLUNaUU9jN1RKYnp3dk1TNWRxMU91N0IwcFBRU1U=", "base64").toString();
|
|
2102
|
+
var OAUTH_REDIRECT_URI = "http://localhost:9876/callback";
|
|
2103
|
+
var OAUTH_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
2104
|
+
var OAUTH_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
2105
|
+
var OAUTH_SCOPE = "openid email profile";
|
|
2106
|
+
var OAUTH_TIMEOUT_MS = 6e4;
|
|
2107
|
+
function generateCodeVerifier() {
|
|
2108
|
+
return randomBytes(32).toString("base64url");
|
|
2109
|
+
}
|
|
2110
|
+
function generateCodeChallenge(verifier) {
|
|
2111
|
+
return createHash2("sha256").update(verifier).digest("base64url");
|
|
2112
|
+
}
|
|
2113
|
+
function openBrowser(url) {
|
|
2114
|
+
const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2115
|
+
exec3(cmd);
|
|
2116
|
+
}
|
|
2117
|
+
function waitForOAuthCallback(codeVerifier) {
|
|
2118
|
+
return new Promise((resolve3, reject) => {
|
|
2119
|
+
const server = createServer2(async (req, res) => {
|
|
2120
|
+
if (!req.url?.startsWith("/callback")) {
|
|
2121
|
+
res.writeHead(404);
|
|
2122
|
+
res.end("Not found");
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
const url = new URL(req.url, "http://localhost:9876");
|
|
2126
|
+
const code = url.searchParams.get("code");
|
|
2127
|
+
const error = url.searchParams.get("error");
|
|
2128
|
+
if (error || !code) {
|
|
2129
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2130
|
+
res.end(
|
|
2131
|
+
`<html><body style='text-align:center;padding:40px;font-family:system-ui'><h2 style='color:#e53e3e'>Login failed</h2><p>${error || "No authorization code received."}</p></body></html>`
|
|
2132
|
+
);
|
|
2133
|
+
clearTimeout(timeout);
|
|
2134
|
+
server.close();
|
|
2135
|
+
reject(new Error(error || "No authorization code received"));
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
try {
|
|
2139
|
+
const tokenRes = await proxyFetch(OAUTH_TOKEN_ENDPOINT, {
|
|
2140
|
+
method: "POST",
|
|
2141
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2142
|
+
body: new URLSearchParams({
|
|
2143
|
+
code,
|
|
2144
|
+
client_id: OAUTH_CLIENT_ID,
|
|
2145
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
2146
|
+
grant_type: "authorization_code",
|
|
2147
|
+
code_verifier: codeVerifier,
|
|
2148
|
+
client_secret: _OCS
|
|
2149
|
+
}).toString()
|
|
2150
|
+
});
|
|
2151
|
+
if (!tokenRes.ok) {
|
|
2152
|
+
const errText = await tokenRes.text();
|
|
2153
|
+
throw new Error(`Token exchange failed: ${tokenRes.status} ${errText}`);
|
|
2154
|
+
}
|
|
2155
|
+
const tokenData = await tokenRes.json();
|
|
2156
|
+
const payload = JSON.parse(
|
|
2157
|
+
Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString()
|
|
2158
|
+
);
|
|
2159
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2160
|
+
res.end(
|
|
2161
|
+
"<html><head><meta charset='utf-8'></head><body style='text-align:center;padding:40px;font-family:system-ui;background:#0d0f14;color:#e2e8f0'><h2 style='color:#38a169'>✓ Logged in! This tab will close in <span id='c'>5</span>s...</h2><script>var n=5;var t=setInterval(function(){n--;document.getElementById('c').textContent=n;if(n<=0){clearInterval(t);window.close();}},1000);</script></body></html>"
|
|
2162
|
+
);
|
|
2163
|
+
clearTimeout(timeout);
|
|
2164
|
+
server.close();
|
|
2165
|
+
resolve3({ idToken: tokenData.id_token, email: payload.email });
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2168
|
+
res.end(
|
|
2169
|
+
`<html><body style='text-align:center;padding:40px;font-family:system-ui'><h2 style='color:#e53e3e'>Login failed</h2><p>${String(err)}</p></body></html>`
|
|
2170
|
+
);
|
|
2171
|
+
clearTimeout(timeout);
|
|
2172
|
+
server.close();
|
|
2173
|
+
reject(err);
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
const timeout = setTimeout(() => {
|
|
2177
|
+
server.close();
|
|
2178
|
+
reject(new Error("Login timed out \u2014 no callback received within 60 seconds"));
|
|
2179
|
+
}, OAUTH_TIMEOUT_MS);
|
|
2180
|
+
server.listen(9876, "127.0.0.1");
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
async function remoteLogin(_options) {
|
|
2184
|
+
const config = loadRemoteConfig();
|
|
2185
|
+
console.log(chalk10.cyan.bold("\n Remote Monitoring Login\n"));
|
|
2186
|
+
console.log(chalk10.gray(" Opening browser for Google sign-in...\n"));
|
|
2187
|
+
const codeVerifier = generateCodeVerifier();
|
|
2188
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
2189
|
+
const authUrl = OAUTH_AUTH_ENDPOINT + "?" + new URLSearchParams({
|
|
2190
|
+
client_id: OAUTH_CLIENT_ID,
|
|
2191
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
2192
|
+
response_type: "code",
|
|
2193
|
+
scope: OAUTH_SCOPE,
|
|
2194
|
+
code_challenge: codeChallenge,
|
|
2195
|
+
code_challenge_method: "S256",
|
|
2196
|
+
access_type: "offline"
|
|
2197
|
+
}).toString();
|
|
2198
|
+
openBrowser(authUrl);
|
|
2199
|
+
let idToken;
|
|
2200
|
+
let email;
|
|
2201
|
+
try {
|
|
2202
|
+
const result = await waitForOAuthCallback(codeVerifier);
|
|
2203
|
+
idToken = result.idToken;
|
|
2204
|
+
email = result.email;
|
|
2205
|
+
} catch (err) {
|
|
2206
|
+
console.log(chalk10.red(` ${String(err)}`));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
console.log(chalk10.gray(" Registering this machine..."));
|
|
2210
|
+
try {
|
|
2211
|
+
const host = hostname();
|
|
2212
|
+
const os = process.platform + "/" + process.arch;
|
|
2213
|
+
const controller = new AbortController();
|
|
2214
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
2215
|
+
const res = await proxyFetch(REMOTE_API_URL + "/v1/machines/register", {
|
|
2216
|
+
method: "POST",
|
|
2217
|
+
headers: {
|
|
2218
|
+
"Content-Type": "application/json",
|
|
2219
|
+
Authorization: "Bearer " + idToken
|
|
2220
|
+
},
|
|
2221
|
+
body: JSON.stringify({ hostname: host, os, version: "unknown" }),
|
|
2222
|
+
signal: controller.signal
|
|
2223
|
+
});
|
|
2224
|
+
clearTimeout(timer);
|
|
2225
|
+
if (!res.ok) {
|
|
2226
|
+
const err = await res.text();
|
|
2227
|
+
console.log(chalk10.red(` Registration failed: ${res.status} ${err}`));
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
const data = await res.json();
|
|
2231
|
+
config.machineId = data.machineId;
|
|
2232
|
+
config.machineToken = data.machineToken;
|
|
2233
|
+
config.enabled = true;
|
|
2234
|
+
config.reportUrl = REMOTE_API_URL + "/v1/report";
|
|
2235
|
+
saveRemoteConfig(config);
|
|
2236
|
+
console.log(chalk10.green.bold(`
|
|
2237
|
+
Logged in as ${email}`));
|
|
2238
|
+
console.log(chalk10.gray(` Machine registered: ${host}`));
|
|
2239
|
+
console.log(
|
|
2240
|
+
chalk10.gray(" Remote monitoring ready. Run: openclaw-cli remote enable\n")
|
|
2241
|
+
);
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
console.log(chalk10.red(` Error: ${err}`));
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
async function remoteEnable(_options) {
|
|
2247
|
+
const config = loadRemoteConfig();
|
|
2248
|
+
if (!config.machineToken) {
|
|
2249
|
+
console.log(
|
|
2250
|
+
chalk10.red(" \u2717 Not logged in. Run: openclaw-cli remote login")
|
|
2251
|
+
);
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
config.enabled = true;
|
|
2255
|
+
saveRemoteConfig(config);
|
|
2256
|
+
console.log(chalk10.green(" Remote reporting enabled."));
|
|
2257
|
+
}
|
|
2258
|
+
async function remoteDisable(_options) {
|
|
2259
|
+
const config = loadRemoteConfig();
|
|
2260
|
+
config.enabled = false;
|
|
2261
|
+
saveRemoteConfig(config);
|
|
2262
|
+
console.log(chalk10.yellow(" Remote reporting disabled."));
|
|
2263
|
+
}
|
|
2264
|
+
async function remoteStatus(_options) {
|
|
2265
|
+
const config = loadRemoteConfig();
|
|
2266
|
+
console.log(chalk10.cyan.bold("\n Remote Monitoring Status\n"));
|
|
2267
|
+
console.log(
|
|
2268
|
+
` Enabled: ${config.enabled ? chalk10.green("yes") : chalk10.gray("no")}`
|
|
2269
|
+
);
|
|
2270
|
+
console.log(
|
|
2271
|
+
` Machine ID: ${chalk10.white(config.machineId || "(none)")}`
|
|
2272
|
+
);
|
|
2273
|
+
console.log(
|
|
2274
|
+
` Token: ${config.machineToken ? chalk10.green("configured") : chalk10.red("not set")}`
|
|
2275
|
+
);
|
|
2276
|
+
console.log(
|
|
2277
|
+
` Report URL: ${chalk10.gray(config.reportUrl)}`
|
|
2278
|
+
);
|
|
2279
|
+
console.log(
|
|
2280
|
+
` Last Report: ${config.lastReport ? chalk10.gray(config.lastReport) : chalk10.gray("never")}`
|
|
2281
|
+
);
|
|
680
2282
|
console.log();
|
|
681
|
-
console.log(chalk7.dim("Toggle: openclaw telemetry on/off"));
|
|
682
|
-
console.log(chalk7.dim("Env: OPENCLAW_NO_TELEMETRY=1"));
|
|
683
2283
|
}
|
|
684
2284
|
|
|
685
2285
|
// src/index.ts
|
|
686
|
-
var
|
|
687
|
-
var version =
|
|
2286
|
+
var _PKG_VER2 = true ? "0.6.1" : "0.2.1";
|
|
2287
|
+
var version = _PKG_VER2;
|
|
688
2288
|
printFirstRunNotice();
|
|
689
2289
|
var program = new Command();
|
|
690
2290
|
program.name(BINARY_NAME).description(`${DISPLAY_NAME} \u2014 health monitor and management for OpenClaw services`).version(version);
|
|
@@ -720,6 +2320,11 @@ addGlobalOpts(
|
|
|
720
2320
|
addGlobalOpts(
|
|
721
2321
|
mem.command("compact").description("Compact agent memory (proxies to openclaw memory compact)").option("--dry-run", "Preview without applying")
|
|
722
2322
|
).action(memoryCompact);
|
|
2323
|
+
var remote = program.command("remote").description("Remote monitoring \u2014 report gateway status to openclaw-cli.app");
|
|
2324
|
+
addGlobalOpts(remote.command("login").description("Authenticate and register this machine")).action(remoteLogin);
|
|
2325
|
+
addGlobalOpts(remote.command("enable").description("Enable remote reporting")).action(remoteEnable);
|
|
2326
|
+
addGlobalOpts(remote.command("disable").description("Disable remote reporting")).action(remoteDisable);
|
|
2327
|
+
addGlobalOpts(remote.command("status").description("Show remote monitoring config")).action(remoteStatus);
|
|
723
2328
|
addGlobalOpts(
|
|
724
2329
|
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()
|
|
725
2330
|
).action((options, cmd) => {
|