openclaw-service 0.5.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 +1737 -151
- package/dist/index.pkg.cjs +7385 -0
- package/package.json +1 -1
- 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-XSRH4RHR.js +0 -1152
- package/dist/server-3JMOADVR.js +0 -7
package/dist/index.js
CHANGED
|
@@ -1,49 +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
|
-
|
|
22
|
-
|
|
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
|
+
});
|
|
23
1346
|
|
|
24
1347
|
// src/index.ts
|
|
1348
|
+
init_brand();
|
|
25
1349
|
import { spawnSync } from "child_process";
|
|
26
1350
|
import { Command } from "commander";
|
|
27
1351
|
|
|
28
1352
|
// src/commands/watch.ts
|
|
1353
|
+
init_config();
|
|
1354
|
+
init_logger();
|
|
1355
|
+
init_health_checker();
|
|
1356
|
+
init_process_manager();
|
|
1357
|
+
init_openclaw();
|
|
29
1358
|
import { spawn } from "child_process";
|
|
30
|
-
import { writeFileSync as
|
|
31
|
-
import
|
|
32
|
-
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";
|
|
33
1362
|
|
|
34
1363
|
// src/telemetry.ts
|
|
1364
|
+
init_brand();
|
|
35
1365
|
import { createHash, randomUUID } from "crypto";
|
|
36
|
-
import { execSync } from "child_process";
|
|
37
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
38
|
-
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";
|
|
39
1369
|
var MEASUREMENT_ID = "G-B46J8RT804";
|
|
40
1370
|
var API_SECRET = "qkqms1nURj2S02Q3WqO7GQ";
|
|
41
1371
|
var ENDPOINT = `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
|
|
42
|
-
var TELEMETRY_FILE =
|
|
1372
|
+
var TELEMETRY_FILE = join5(APP_HOME, "telemetry.json");
|
|
43
1373
|
function loadState() {
|
|
44
|
-
if (
|
|
1374
|
+
if (existsSync4(TELEMETRY_FILE)) {
|
|
45
1375
|
try {
|
|
46
|
-
return JSON.parse(
|
|
1376
|
+
return JSON.parse(readFileSync3(TELEMETRY_FILE, "utf-8"));
|
|
47
1377
|
} catch {
|
|
48
1378
|
}
|
|
49
1379
|
}
|
|
@@ -57,7 +1387,7 @@ function loadState() {
|
|
|
57
1387
|
}
|
|
58
1388
|
function saveState(state) {
|
|
59
1389
|
try {
|
|
60
|
-
|
|
1390
|
+
writeFileSync2(TELEMETRY_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
61
1391
|
} catch {
|
|
62
1392
|
}
|
|
63
1393
|
}
|
|
@@ -66,7 +1396,7 @@ function sha256(input) {
|
|
|
66
1396
|
}
|
|
67
1397
|
function tryExec(cmd) {
|
|
68
1398
|
try {
|
|
69
|
-
return
|
|
1399
|
+
return execSync2(cmd, { stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).toString().trim();
|
|
70
1400
|
} catch {
|
|
71
1401
|
return null;
|
|
72
1402
|
}
|
|
@@ -166,7 +1496,7 @@ async function watchDaemon(options) {
|
|
|
166
1496
|
initLogger();
|
|
167
1497
|
ensureDoctorHome();
|
|
168
1498
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
169
|
-
|
|
1499
|
+
writeFileSync3(PID_FILE, String(process.pid));
|
|
170
1500
|
trackCommand("watch start", true).catch(() => {
|
|
171
1501
|
});
|
|
172
1502
|
log("info", "OpenClaw Doctor started (foreground)");
|
|
@@ -175,7 +1505,7 @@ async function watchDaemon(options) {
|
|
|
175
1505
|
log("info", `Check interval: ${config.checkInterval}s`);
|
|
176
1506
|
log("info", `PID: ${process.pid}`);
|
|
177
1507
|
if (options.dashboard) {
|
|
178
|
-
const { startDashboard: startDashboard2 } = await
|
|
1508
|
+
const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
179
1509
|
startDashboard2({ config: options.config });
|
|
180
1510
|
}
|
|
181
1511
|
const throttle = new RestartThrottle(config.maxRestartsPerHour);
|
|
@@ -183,7 +1513,7 @@ async function watchDaemon(options) {
|
|
|
183
1513
|
let isRestarting = false;
|
|
184
1514
|
async function tick() {
|
|
185
1515
|
if (isRestarting) return;
|
|
186
|
-
if (
|
|
1516
|
+
if (existsSync8(STOP_FLAG_FILE)) {
|
|
187
1517
|
log("info", "Gateway is manually stopped \u2014 skipping auto-restart");
|
|
188
1518
|
return;
|
|
189
1519
|
}
|
|
@@ -231,7 +1561,7 @@ function daemonize(options) {
|
|
|
231
1561
|
ensureDoctorHome();
|
|
232
1562
|
const existingPid = readDaemonPid();
|
|
233
1563
|
if (existingPid && isProcessAlive(existingPid)) {
|
|
234
|
-
console.log(
|
|
1564
|
+
console.log(chalk3.yellow(`Doctor is already running (PID ${existingPid})`));
|
|
235
1565
|
return;
|
|
236
1566
|
}
|
|
237
1567
|
const execArgv = process.execArgv.filter(
|
|
@@ -241,8 +1571,8 @@ function daemonize(options) {
|
|
|
241
1571
|
(a) => a !== "-d" && a !== "--daemon"
|
|
242
1572
|
);
|
|
243
1573
|
const fullArgs = [...execArgv, ...scriptArgs];
|
|
244
|
-
const outLog =
|
|
245
|
-
const errLog =
|
|
1574
|
+
const outLog = join9(DOCTOR_LOG_DIR, "daemon.out.log");
|
|
1575
|
+
const errLog = join9(DOCTOR_LOG_DIR, "daemon.err.log");
|
|
246
1576
|
const out = openSync(outLog, "a");
|
|
247
1577
|
const err = openSync(errLog, "a");
|
|
248
1578
|
const child = spawn(process.execPath, fullArgs, {
|
|
@@ -251,20 +1581,20 @@ function daemonize(options) {
|
|
|
251
1581
|
env: { ...process.env, OPENCLAW_DOCTOR_DAEMON: "1" }
|
|
252
1582
|
});
|
|
253
1583
|
const pid = child.pid;
|
|
254
|
-
|
|
1584
|
+
writeFileSync3(PID_FILE, String(pid));
|
|
255
1585
|
child.unref();
|
|
256
|
-
console.log(
|
|
257
|
-
console.log(
|
|
258
|
-
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`));
|
|
259
1589
|
}
|
|
260
1590
|
async function stopDaemon(options) {
|
|
261
1591
|
const pid = readDaemonPid();
|
|
262
1592
|
if (!pid) {
|
|
263
|
-
console.log(
|
|
1593
|
+
console.log(chalk3.yellow("Doctor is not running (no PID file)"));
|
|
264
1594
|
return;
|
|
265
1595
|
}
|
|
266
1596
|
if (!isProcessAlive(pid)) {
|
|
267
|
-
console.log(
|
|
1597
|
+
console.log(chalk3.yellow(`Doctor is not running (PID ${pid} is dead, cleaning up)`));
|
|
268
1598
|
try {
|
|
269
1599
|
unlinkSync(PID_FILE);
|
|
270
1600
|
} catch {
|
|
@@ -273,11 +1603,11 @@ async function stopDaemon(options) {
|
|
|
273
1603
|
}
|
|
274
1604
|
try {
|
|
275
1605
|
process.kill(pid, "SIGTERM");
|
|
276
|
-
console.log(
|
|
1606
|
+
console.log(chalk3.green(`Doctor stopped (PID ${pid})`));
|
|
277
1607
|
trackCommand("watch stop", true).catch(() => {
|
|
278
1608
|
});
|
|
279
1609
|
} catch (err) {
|
|
280
|
-
console.log(
|
|
1610
|
+
console.log(chalk3.red(`Failed to stop Doctor (PID ${pid}): ${err}`));
|
|
281
1611
|
trackCommand("watch stop", false).catch(() => {
|
|
282
1612
|
});
|
|
283
1613
|
}
|
|
@@ -288,8 +1618,8 @@ async function stopDaemon(options) {
|
|
|
288
1618
|
}
|
|
289
1619
|
}
|
|
290
1620
|
function readDaemonPid() {
|
|
291
|
-
if (!
|
|
292
|
-
const raw =
|
|
1621
|
+
if (!existsSync8(PID_FILE)) return null;
|
|
1622
|
+
const raw = readFileSync6(PID_FILE, "utf-8").trim();
|
|
293
1623
|
const pid = parseInt(raw, 10);
|
|
294
1624
|
return isNaN(pid) ? null : pid;
|
|
295
1625
|
}
|
|
@@ -303,7 +1633,10 @@ function isProcessAlive(pid) {
|
|
|
303
1633
|
}
|
|
304
1634
|
|
|
305
1635
|
// src/commands/status.ts
|
|
306
|
-
|
|
1636
|
+
init_config();
|
|
1637
|
+
init_openclaw();
|
|
1638
|
+
init_health_checker();
|
|
1639
|
+
import chalk4 from "chalk";
|
|
307
1640
|
async function showStatus(options) {
|
|
308
1641
|
const config = loadConfig(options.config);
|
|
309
1642
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
@@ -337,32 +1670,32 @@ async function showStatus(options) {
|
|
|
337
1670
|
process.exit(result.healthy ? 0 : 1);
|
|
338
1671
|
return;
|
|
339
1672
|
}
|
|
340
|
-
console.log(
|
|
1673
|
+
console.log(chalk4.bold("\n OpenClaw Doctor\n"));
|
|
341
1674
|
if (result.healthy) {
|
|
342
1675
|
console.log(
|
|
343
|
-
|
|
1676
|
+
chalk4.green.bold(` Gateway: HEALTHY`) + chalk4.gray(` (port ${info.gatewayPort}, ${result.durationMs}ms)`)
|
|
344
1677
|
);
|
|
345
1678
|
} else if (result.gateway) {
|
|
346
|
-
console.log(
|
|
1679
|
+
console.log(chalk4.yellow.bold(` Gateway: DEGRADED`) + chalk4.gray(` (responded but ok=false)`));
|
|
347
1680
|
} else {
|
|
348
|
-
console.log(
|
|
349
|
-
if (result.error) console.log(
|
|
1681
|
+
console.log(chalk4.red.bold(` Gateway: UNREACHABLE`));
|
|
1682
|
+
if (result.error) console.log(chalk4.red(` ${result.error}`));
|
|
350
1683
|
}
|
|
351
1684
|
if (result.channels && result.channels.length > 0) {
|
|
352
1685
|
console.log();
|
|
353
1686
|
for (const ch of result.channels ?? []) {
|
|
354
|
-
const icon = ch.ok ?
|
|
355
|
-
console.log(` ${
|
|
1687
|
+
const icon = ch.ok ? chalk4.green("ok") : chalk4.red("fail");
|
|
1688
|
+
console.log(` ${chalk4.gray("Channel")} ${ch.name}: ${icon}`);
|
|
356
1689
|
}
|
|
357
1690
|
}
|
|
358
1691
|
if (info.agents.length > 0) {
|
|
359
1692
|
console.log();
|
|
360
1693
|
const agentList = info.agents.map((a) => a.isDefault ? `${a.name} (default)` : a.name).join(", ");
|
|
361
|
-
console.log(
|
|
1694
|
+
console.log(chalk4.gray(` Agents: ${agentList}`));
|
|
362
1695
|
}
|
|
363
1696
|
console.log();
|
|
364
|
-
console.log(
|
|
365
|
-
console.log(
|
|
1697
|
+
console.log(chalk4.gray(` OpenClaw ${info.version ?? "unknown"}`));
|
|
1698
|
+
console.log(chalk4.gray(` Config: ${info.configPath}`));
|
|
366
1699
|
console.log();
|
|
367
1700
|
trackCommand("status", true).catch(() => {
|
|
368
1701
|
});
|
|
@@ -370,13 +1703,16 @@ async function showStatus(options) {
|
|
|
370
1703
|
}
|
|
371
1704
|
|
|
372
1705
|
// src/commands/doctor.ts
|
|
373
|
-
|
|
374
|
-
|
|
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";
|
|
375
1711
|
function findConfigIssues(configPath) {
|
|
376
|
-
if (!
|
|
1712
|
+
if (!existsSync9(configPath)) return [];
|
|
377
1713
|
let raw;
|
|
378
1714
|
try {
|
|
379
|
-
raw = JSON.parse(
|
|
1715
|
+
raw = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
380
1716
|
} catch {
|
|
381
1717
|
return [{ path: "root", message: "Config file is not valid JSON", fix: () => {
|
|
382
1718
|
} }];
|
|
@@ -403,7 +1739,7 @@ function findConfigIssues(configPath) {
|
|
|
403
1739
|
const origFix = originalFixes[i];
|
|
404
1740
|
issues[i].fix = () => {
|
|
405
1741
|
origFix();
|
|
406
|
-
|
|
1742
|
+
writeFileSync4(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
407
1743
|
};
|
|
408
1744
|
}
|
|
409
1745
|
}
|
|
@@ -412,53 +1748,53 @@ function findConfigIssues(configPath) {
|
|
|
412
1748
|
async function runDoctor(options) {
|
|
413
1749
|
const config = loadConfig(options.config);
|
|
414
1750
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
415
|
-
console.log(
|
|
416
|
-
console.log(
|
|
1751
|
+
console.log(chalk5.bold("\n OpenClaw Doctor \u2014 Full Diagnostics\n"));
|
|
1752
|
+
console.log(chalk5.gray(" [0/4] Config validation"));
|
|
417
1753
|
const issues = findConfigIssues(info.configPath);
|
|
418
1754
|
if (issues.length === 0) {
|
|
419
|
-
console.log(
|
|
1755
|
+
console.log(chalk5.green(" Config: valid"));
|
|
420
1756
|
} else {
|
|
421
1757
|
for (const issue of issues) {
|
|
422
|
-
console.log(
|
|
1758
|
+
console.log(chalk5.red(` ${issue.path}: ${issue.message}`));
|
|
423
1759
|
}
|
|
424
1760
|
if (options.fix) {
|
|
425
1761
|
for (const issue of issues) {
|
|
426
1762
|
issue.fix();
|
|
427
|
-
console.log(
|
|
1763
|
+
console.log(chalk5.green(` Fixed: ${issue.path}`));
|
|
428
1764
|
}
|
|
429
|
-
console.log(
|
|
1765
|
+
console.log(chalk5.green(` Config saved: ${info.configPath}`));
|
|
430
1766
|
} else {
|
|
431
|
-
console.log(
|
|
1767
|
+
console.log(chalk5.yellow(" Run with --fix to auto-repair"));
|
|
432
1768
|
}
|
|
433
1769
|
}
|
|
434
|
-
console.log(
|
|
1770
|
+
console.log(chalk5.gray("\n [1/4] OpenClaw binary"));
|
|
435
1771
|
if (info.cliBinPath) {
|
|
436
|
-
console.log(
|
|
437
|
-
console.log(
|
|
438
|
-
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}`));
|
|
439
1775
|
} else {
|
|
440
|
-
console.log(
|
|
1776
|
+
console.log(chalk5.red(" Not found \u2014 openclaw CLI is not installed or not in PATH"));
|
|
441
1777
|
}
|
|
442
|
-
console.log(
|
|
1778
|
+
console.log(chalk5.gray("\n [2/4] Gateway health"));
|
|
443
1779
|
const result = await checkHealth(info);
|
|
444
1780
|
if (result.healthy) {
|
|
445
|
-
console.log(
|
|
1781
|
+
console.log(chalk5.green(` Gateway: healthy (port ${info.gatewayPort}, ${result.durationMs}ms)`));
|
|
446
1782
|
} else if (result.gateway) {
|
|
447
|
-
console.log(
|
|
1783
|
+
console.log(chalk5.yellow(` Gateway: responded but degraded`));
|
|
448
1784
|
} else {
|
|
449
|
-
console.log(
|
|
450
|
-
if (result.error) console.log(
|
|
1785
|
+
console.log(chalk5.red(` Gateway: unreachable`));
|
|
1786
|
+
if (result.error) console.log(chalk5.red(` ${result.error}`));
|
|
451
1787
|
}
|
|
452
|
-
console.log(
|
|
1788
|
+
console.log(chalk5.gray("\n [3/4] Channels"));
|
|
453
1789
|
if (result.channels.length > 0) {
|
|
454
1790
|
for (const ch of result.channels) {
|
|
455
|
-
const status = ch.ok ?
|
|
1791
|
+
const status = ch.ok ? chalk5.green("ok") : chalk5.red("fail");
|
|
456
1792
|
console.log(` ${ch.name}: ${status}`);
|
|
457
1793
|
}
|
|
458
1794
|
} else {
|
|
459
|
-
console.log(
|
|
1795
|
+
console.log(chalk5.yellow(" No channel data available"));
|
|
460
1796
|
}
|
|
461
|
-
console.log(
|
|
1797
|
+
console.log(chalk5.gray("\n [4/4] OpenClaw built-in doctor"));
|
|
462
1798
|
const doctorOutput = await runOpenClawCmd(info, "doctor");
|
|
463
1799
|
if (doctorOutput) {
|
|
464
1800
|
const lines = doctorOutput.split("\n");
|
|
@@ -468,24 +1804,24 @@ async function runDoctor(options) {
|
|
|
468
1804
|
console.log(` ${line}`);
|
|
469
1805
|
}
|
|
470
1806
|
} else {
|
|
471
|
-
console.log(
|
|
1807
|
+
console.log(chalk5.yellow(" Could not run openclaw doctor"));
|
|
472
1808
|
}
|
|
473
1809
|
if (options.fix) {
|
|
474
|
-
console.log(
|
|
1810
|
+
console.log(chalk5.gray("\n [5/5] Auto-repair"));
|
|
475
1811
|
if (!result.healthy) {
|
|
476
|
-
console.log(
|
|
1812
|
+
console.log(chalk5.yellow(" Gateway unhealthy \u2014 running openclaw doctor --repair --non-interactive"));
|
|
477
1813
|
const repairOutput = await runOpenClawCmd(info, "doctor --repair --non-interactive");
|
|
478
1814
|
if (repairOutput) {
|
|
479
1815
|
const lines = repairOutput.split("\n");
|
|
480
1816
|
for (const line of lines.slice(0, 30)) {
|
|
481
1817
|
if (line.trim()) console.log(` ${line}`);
|
|
482
1818
|
}
|
|
483
|
-
console.log(
|
|
1819
|
+
console.log(chalk5.green(" Repair completed"));
|
|
484
1820
|
} else {
|
|
485
|
-
console.log(
|
|
1821
|
+
console.log(chalk5.yellow(" Could not run repair (openclaw CLI unavailable)"));
|
|
486
1822
|
}
|
|
487
1823
|
} else {
|
|
488
|
-
console.log(
|
|
1824
|
+
console.log(chalk5.green(" Gateway healthy \u2014 no repair needed"));
|
|
489
1825
|
}
|
|
490
1826
|
}
|
|
491
1827
|
trackCommand("doctor", true).catch(() => {
|
|
@@ -494,8 +1830,11 @@ async function runDoctor(options) {
|
|
|
494
1830
|
}
|
|
495
1831
|
|
|
496
1832
|
// src/commands/logs.ts
|
|
497
|
-
|
|
498
|
-
|
|
1833
|
+
init_config();
|
|
1834
|
+
init_openclaw();
|
|
1835
|
+
init_config();
|
|
1836
|
+
import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
|
|
1837
|
+
import chalk6 from "chalk";
|
|
499
1838
|
function showLogs(options) {
|
|
500
1839
|
const config = loadConfig(options.config);
|
|
501
1840
|
const maxLines = parseInt(options.lines ?? "50", 10);
|
|
@@ -505,64 +1844,69 @@ function showLogs(options) {
|
|
|
505
1844
|
}
|
|
506
1845
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
507
1846
|
const logFile = options.error ? `${info.logDir}/gateway.err.log` : `${info.logDir}/gateway.log`;
|
|
508
|
-
if (!
|
|
509
|
-
console.log(
|
|
1847
|
+
if (!existsSync10(logFile)) {
|
|
1848
|
+
console.log(chalk6.yellow(`Log file not found: ${logFile}`));
|
|
510
1849
|
return;
|
|
511
1850
|
}
|
|
512
|
-
console.log(
|
|
1851
|
+
console.log(chalk6.blue.bold(`
|
|
513
1852
|
${logFile}
|
|
514
1853
|
`));
|
|
515
|
-
const content =
|
|
1854
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
516
1855
|
const lines = content.trim().split("\n");
|
|
517
1856
|
const tail = lines.slice(-maxLines);
|
|
518
1857
|
for (const line of tail) {
|
|
519
1858
|
if (line.includes("[error]") || line.includes("[ERROR]")) {
|
|
520
|
-
console.log(
|
|
1859
|
+
console.log(chalk6.red(line));
|
|
521
1860
|
} else if (line.includes("[warn]") || line.includes("[WARN]")) {
|
|
522
|
-
console.log(
|
|
1861
|
+
console.log(chalk6.yellow(line));
|
|
523
1862
|
} else {
|
|
524
|
-
console.log(
|
|
1863
|
+
console.log(chalk6.gray(line));
|
|
525
1864
|
}
|
|
526
1865
|
}
|
|
527
1866
|
console.log();
|
|
528
1867
|
}
|
|
529
1868
|
function showDoctorLogs(maxLines) {
|
|
530
|
-
const { readdirSync } = __require("fs");
|
|
531
|
-
const { join:
|
|
532
|
-
if (!
|
|
533
|
-
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."));
|
|
534
1873
|
return;
|
|
535
1874
|
}
|
|
536
|
-
const files =
|
|
1875
|
+
const files = readdirSync5(DOCTOR_LOG_DIR).filter((f) => f.endsWith(".log")).sort().reverse();
|
|
537
1876
|
if (files.length === 0) {
|
|
538
|
-
console.log(
|
|
1877
|
+
console.log(chalk6.yellow("No doctor log files found."));
|
|
539
1878
|
return;
|
|
540
1879
|
}
|
|
541
1880
|
const latest = files[0];
|
|
542
|
-
console.log(
|
|
543
|
-
${
|
|
1881
|
+
console.log(chalk6.blue.bold(`
|
|
1882
|
+
${join12(DOCTOR_LOG_DIR, latest)}
|
|
544
1883
|
`));
|
|
545
|
-
const content =
|
|
1884
|
+
const content = readFileSync8(join12(DOCTOR_LOG_DIR, latest), "utf-8");
|
|
546
1885
|
const lines = content.trim().split("\n");
|
|
547
1886
|
const tail = lines.slice(-maxLines);
|
|
548
1887
|
for (const line of tail) {
|
|
549
1888
|
if (line.includes("[ERROR]")) {
|
|
550
|
-
console.log(
|
|
1889
|
+
console.log(chalk6.red(line));
|
|
551
1890
|
} else if (line.includes("[WARN]")) {
|
|
552
|
-
console.log(
|
|
1891
|
+
console.log(chalk6.yellow(line));
|
|
553
1892
|
} else if (line.includes("[SUCCESS]")) {
|
|
554
|
-
console.log(
|
|
1893
|
+
console.log(chalk6.green(line));
|
|
555
1894
|
} else {
|
|
556
|
-
console.log(
|
|
1895
|
+
console.log(chalk6.gray(line));
|
|
557
1896
|
}
|
|
558
1897
|
}
|
|
559
1898
|
console.log();
|
|
560
1899
|
}
|
|
561
1900
|
|
|
562
1901
|
// src/commands/gateway.ts
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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;
|
|
566
1910
|
async function gatewayStart(options) {
|
|
567
1911
|
const config = loadConfig(options.config);
|
|
568
1912
|
initLogger();
|
|
@@ -574,11 +1918,11 @@ async function gatewayStart(options) {
|
|
|
574
1918
|
}
|
|
575
1919
|
const result = await startGateway(info);
|
|
576
1920
|
if (result.success) {
|
|
577
|
-
console.log(
|
|
1921
|
+
console.log(chalk7.green("Gateway started (auto-restart resumed)"));
|
|
578
1922
|
trackCommand("gateway start", true, _VER).catch(() => {
|
|
579
1923
|
});
|
|
580
1924
|
} else {
|
|
581
|
-
console.log(
|
|
1925
|
+
console.log(chalk7.red(`Failed to start gateway: ${result.error}`));
|
|
582
1926
|
trackCommand("gateway start", false, _VER).catch(() => {
|
|
583
1927
|
});
|
|
584
1928
|
process.exit(1);
|
|
@@ -591,13 +1935,13 @@ async function gatewayStop(options) {
|
|
|
591
1935
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
592
1936
|
const result = await stopGateway(info);
|
|
593
1937
|
if (result.success) {
|
|
594
|
-
|
|
595
|
-
console.log(
|
|
596
|
-
console.log(
|
|
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."));
|
|
597
1941
|
trackCommand("gateway stop", true, _VER).catch(() => {
|
|
598
1942
|
});
|
|
599
1943
|
} else {
|
|
600
|
-
console.log(
|
|
1944
|
+
console.log(chalk7.red(`Failed to stop gateway: ${result.error}`));
|
|
601
1945
|
trackCommand("gateway stop", false, _VER).catch(() => {
|
|
602
1946
|
});
|
|
603
1947
|
process.exit(1);
|
|
@@ -614,11 +1958,11 @@ async function gatewayRestart(options) {
|
|
|
614
1958
|
}
|
|
615
1959
|
const result = await restartGateway(info);
|
|
616
1960
|
if (result.success) {
|
|
617
|
-
console.log(
|
|
1961
|
+
console.log(chalk7.green("Gateway restarted (auto-restart resumed)"));
|
|
618
1962
|
trackCommand("gateway restart", true, _VER).catch(() => {
|
|
619
1963
|
});
|
|
620
1964
|
} else {
|
|
621
|
-
console.log(
|
|
1965
|
+
console.log(chalk7.red(`Failed to restart gateway: ${result.error}`));
|
|
622
1966
|
trackCommand("gateway restart", false, _VER).catch(() => {
|
|
623
1967
|
});
|
|
624
1968
|
process.exit(1);
|
|
@@ -626,42 +1970,44 @@ async function gatewayRestart(options) {
|
|
|
626
1970
|
}
|
|
627
1971
|
|
|
628
1972
|
// src/commands/memory.ts
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
import
|
|
632
|
-
import {
|
|
633
|
-
|
|
634
|
-
|
|
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;
|
|
635
1981
|
}
|
|
636
1982
|
async function memoryStatus(options) {
|
|
637
1983
|
const config = loadConfig(options.config);
|
|
638
1984
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
639
|
-
console.log(
|
|
1985
|
+
console.log(chalk8.bold("\n Memory Status\n"));
|
|
640
1986
|
for (const agent of info.agents) {
|
|
641
1987
|
const ws = agent.workspace;
|
|
642
1988
|
if (!ws) continue;
|
|
643
|
-
const wsPath =
|
|
644
|
-
const memPath =
|
|
645
|
-
const exists =
|
|
646
|
-
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;
|
|
647
1993
|
const warn = sizeKB > 50;
|
|
648
|
-
const indicator = warn ?
|
|
649
|
-
const sizeStr = warn ?
|
|
650
|
-
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") : ""}`);
|
|
651
1997
|
}
|
|
652
1998
|
console.log();
|
|
653
1999
|
}
|
|
654
2000
|
async function memorySearch(query, options) {
|
|
655
2001
|
const config = loadConfig(options.config);
|
|
656
2002
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
657
|
-
console.log(
|
|
2003
|
+
console.log(chalk8.bold(`
|
|
658
2004
|
Searching memory: "${query}"
|
|
659
2005
|
`));
|
|
660
2006
|
const output = await runOpenClawCmd(info, `memory search "${query}"`);
|
|
661
2007
|
if (output) {
|
|
662
2008
|
console.log(output);
|
|
663
2009
|
} else {
|
|
664
|
-
console.log(
|
|
2010
|
+
console.log(chalk8.yellow(" No results or openclaw memory search unavailable"));
|
|
665
2011
|
}
|
|
666
2012
|
console.log();
|
|
667
2013
|
}
|
|
@@ -669,41 +2015,276 @@ async function memoryCompact(options) {
|
|
|
669
2015
|
const config = loadConfig(options.config);
|
|
670
2016
|
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
671
2017
|
const flag = options.dryRun ? "--dry-run" : "";
|
|
672
|
-
console.log(
|
|
2018
|
+
console.log(chalk8.bold(`
|
|
673
2019
|
Memory Compact${options.dryRun ? " (dry run)" : ""}
|
|
674
2020
|
`));
|
|
675
2021
|
const output = await runOpenClawCmd(info, `memory compact ${flag}`);
|
|
676
2022
|
if (output) {
|
|
677
2023
|
console.log(output);
|
|
678
2024
|
} else {
|
|
679
|
-
console.log(
|
|
2025
|
+
console.log(chalk8.yellow(" openclaw memory compact not available"));
|
|
680
2026
|
}
|
|
681
2027
|
console.log();
|
|
682
2028
|
}
|
|
683
2029
|
|
|
2030
|
+
// src/index.ts
|
|
2031
|
+
init_server();
|
|
2032
|
+
init_openclaw();
|
|
2033
|
+
|
|
684
2034
|
// src/commands/telemetry.ts
|
|
685
|
-
import
|
|
2035
|
+
import chalk9 from "chalk";
|
|
686
2036
|
function telemetryOn() {
|
|
687
2037
|
setOptOut(false);
|
|
688
|
-
console.log(
|
|
2038
|
+
console.log(chalk9.green("\u2713 Telemetry enabled. Thanks for helping improve OpenClaw!"));
|
|
689
2039
|
}
|
|
690
2040
|
function telemetryOff() {
|
|
691
2041
|
setOptOut(true);
|
|
692
|
-
console.log(
|
|
2042
|
+
console.log(chalk9.yellow("\u2713 Telemetry disabled. Set OPENCLAW_NO_TELEMETRY=1 to suppress permanently."));
|
|
693
2043
|
}
|
|
694
2044
|
function telemetryStatus() {
|
|
695
2045
|
const { optOut, clientId } = getTelemetryStatus();
|
|
696
|
-
const status = optOut ?
|
|
2046
|
+
const status = optOut ? chalk9.red("disabled") : chalk9.green("enabled");
|
|
697
2047
|
console.log(`Telemetry: ${status}`);
|
|
698
|
-
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
|
+
);
|
|
699
2282
|
console.log();
|
|
700
|
-
console.log(chalk7.dim("Toggle: openclaw telemetry on/off"));
|
|
701
|
-
console.log(chalk7.dim("Env: OPENCLAW_NO_TELEMETRY=1"));
|
|
702
2283
|
}
|
|
703
2284
|
|
|
704
2285
|
// src/index.ts
|
|
705
|
-
var
|
|
706
|
-
var version =
|
|
2286
|
+
var _PKG_VER2 = true ? "0.6.1" : "0.2.1";
|
|
2287
|
+
var version = _PKG_VER2;
|
|
707
2288
|
printFirstRunNotice();
|
|
708
2289
|
var program = new Command();
|
|
709
2290
|
program.name(BINARY_NAME).description(`${DISPLAY_NAME} \u2014 health monitor and management for OpenClaw services`).version(version);
|
|
@@ -739,6 +2320,11 @@ addGlobalOpts(
|
|
|
739
2320
|
addGlobalOpts(
|
|
740
2321
|
mem.command("compact").description("Compact agent memory (proxies to openclaw memory compact)").option("--dry-run", "Preview without applying")
|
|
741
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);
|
|
742
2328
|
addGlobalOpts(
|
|
743
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()
|
|
744
2330
|
).action((options, cmd) => {
|