@botcord/daemon 0.2.62 → 0.2.63
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/diagnostics.d.ts +1 -0
- package/dist/diagnostics.js +106 -1
- package/package.json +1 -1
- package/src/__tests__/diagnostics.test.ts +23 -0
- package/src/diagnostics.ts +106 -0
package/dist/diagnostics.d.ts
CHANGED
package/dist/diagnostics.js
CHANGED
|
@@ -2,9 +2,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir, hostname, platform, release, arch } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { deflateRawSync } from "node:zlib";
|
|
6
8
|
import { AUTH_EXPIRED_FLAG_PATH, USER_AUTH_PATH, } from "./user-auth.js";
|
|
7
|
-
import { CONFIG_FILE_PATH, PID_PATH, SNAPSHOT_PATH, loadConfig, saveConfig, } from "./config.js";
|
|
9
|
+
import { CONFIG_FILE_PATH, PID_PATH, SESSIONS_PATH, SNAPSHOT_PATH, loadConfig, saveConfig, } from "./config.js";
|
|
8
10
|
import { listDaemonLogFiles, LOG_FILE_PATH } from "./log.js";
|
|
9
11
|
import { listAcpTraceLogFiles, listRuntimeLogFiles } from "./acp-logs.js";
|
|
10
12
|
import { channelsFromDaemonConfig, defaultHttpFetcher, renderDoctor, runDoctor, } from "./doctor.js";
|
|
@@ -14,6 +16,24 @@ import { discoverLocalOpenclawGateways, mergeOpenclawGateways, openclawDiscovery
|
|
|
14
16
|
const DIAGNOSTICS_DIR = path.join(homedir(), ".botcord", "diagnostics");
|
|
15
17
|
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
16
18
|
const DEFAULT_ROTATED_LOGS_IN_BUNDLE = 5;
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
21
|
+
const ENV_ALLOWLIST = new Set([
|
|
22
|
+
"NODE_ENV",
|
|
23
|
+
"PATH",
|
|
24
|
+
"BOTCORD_HUB",
|
|
25
|
+
"BOTCORD_DAEMON_HOME",
|
|
26
|
+
"BOTCORD_DAEMON_CONFIG",
|
|
27
|
+
"BOTCORD_DAEMON_LOG",
|
|
28
|
+
"BOTCORD_DAEMON_SNAPSHOT_INTERVAL_MS",
|
|
29
|
+
"BOTCORD_HERMES_AGENT_BIN",
|
|
30
|
+
"BOTCORD_CLAUDE_CODE_BIN",
|
|
31
|
+
"BOTCORD_CODEX_BIN",
|
|
32
|
+
"BOTCORD_GEMINI_BIN",
|
|
33
|
+
"BOTCORD_DEEPSEEK_TUI_BIN",
|
|
34
|
+
"BOTCORD_KIMI_CLI_BIN",
|
|
35
|
+
"OPENCLAW_ACP_URL",
|
|
36
|
+
]);
|
|
17
37
|
const SECRET_PATTERNS = [
|
|
18
38
|
[/(Authorization:\s*Bearer\s+)[^\s"']+/gi, "$1[REDACTED]"],
|
|
19
39
|
[/("?(?:accessToken|access_token|refreshToken|refresh_token|token|privateKey|private_key|secret)"?\s*:\s*")[^"]+(")/gi, "$1[REDACTED]$2"],
|
|
@@ -38,6 +58,82 @@ function safeReadText(file) {
|
|
|
38
58
|
return `read failed: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
39
59
|
}
|
|
40
60
|
}
|
|
61
|
+
function readJsonFile(file) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
64
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
65
|
+
? parsed
|
|
66
|
+
: null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function findDaemonPackageJson(startFile) {
|
|
73
|
+
let dir = path.dirname(startFile);
|
|
74
|
+
for (let i = 0; i < 6; i += 1) {
|
|
75
|
+
const candidate = path.join(dir, "package.json");
|
|
76
|
+
const parsed = readJsonFile(candidate);
|
|
77
|
+
if (parsed?.name === "@botcord/daemon")
|
|
78
|
+
return parsed;
|
|
79
|
+
const next = path.dirname(dir);
|
|
80
|
+
if (next === dir)
|
|
81
|
+
break;
|
|
82
|
+
dir = next;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function readInstalledPackageVersion(packageJsonSpecifier) {
|
|
87
|
+
try {
|
|
88
|
+
const pkgPath = require.resolve(packageJsonSpecifier);
|
|
89
|
+
const parsed = readJsonFile(pkgPath);
|
|
90
|
+
return typeof parsed?.version === "string" ? parsed.version : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function daemonRuntimeSummary() {
|
|
97
|
+
const pkg = findDaemonPackageJson(MODULE_PATH);
|
|
98
|
+
const version = typeof pkg?.version === "string" ? pkg.version : null;
|
|
99
|
+
const startedAtMs = Date.now() - Math.round(process.uptime() * 1000);
|
|
100
|
+
return {
|
|
101
|
+
packageName: typeof pkg?.name === "string" ? pkg.name : "@botcord/daemon",
|
|
102
|
+
version,
|
|
103
|
+
modulePath: MODULE_PATH,
|
|
104
|
+
entrypoint: process.argv[1] ?? null,
|
|
105
|
+
execPath: process.execPath,
|
|
106
|
+
argv: process.argv.map((arg) => redact(arg)),
|
|
107
|
+
execArgv: process.execArgv.map((arg) => redact(arg)),
|
|
108
|
+
cwd: process.cwd(),
|
|
109
|
+
pid: process.pid,
|
|
110
|
+
ppid: process.ppid,
|
|
111
|
+
uptimeSec: Math.round(process.uptime()),
|
|
112
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
113
|
+
versions: {
|
|
114
|
+
node: process.version,
|
|
115
|
+
v8: process.versions.v8,
|
|
116
|
+
uv: process.versions.uv,
|
|
117
|
+
openssl: process.versions.openssl,
|
|
118
|
+
},
|
|
119
|
+
packages: {
|
|
120
|
+
"@botcord/daemon": version,
|
|
121
|
+
"@botcord/cli": readInstalledPackageVersion("@botcord/cli/package.json"),
|
|
122
|
+
"@botcord/protocol-core": readInstalledPackageVersion("@botcord/protocol-core/package.json"),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function safeEnvironmentSummary() {
|
|
127
|
+
const out = {};
|
|
128
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
129
|
+
if (!value)
|
|
130
|
+
continue;
|
|
131
|
+
if (!ENV_ALLOWLIST.has(key) && !key.startsWith("BOTCORD_DAEMON_"))
|
|
132
|
+
continue;
|
|
133
|
+
out[key] = redact(value);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
41
137
|
function readUserAuthSummary() {
|
|
42
138
|
const raw = safeReadText(USER_AUTH_PATH);
|
|
43
139
|
if (!raw)
|
|
@@ -260,6 +356,7 @@ export async function createDiagnosticBundle(opts = {}) {
|
|
|
260
356
|
const logFile = opts.logFile ?? LOG_FILE_PATH;
|
|
261
357
|
const configFile = opts.configFile ?? CONFIG_FILE_PATH;
|
|
262
358
|
const snapshotFile = opts.snapshotFile ?? SNAPSHOT_PATH;
|
|
359
|
+
const sessionsFile = opts.sessionsFile ?? SESSIONS_PATH;
|
|
263
360
|
const includeAllLogs = opts.includeAllLogs === true;
|
|
264
361
|
const logs = bundledLogs(logFile, includeAllLogs);
|
|
265
362
|
const acpLogs = listAcpTraceLogFiles(includeAllLogs);
|
|
@@ -273,9 +370,12 @@ export async function createDiagnosticBundle(opts = {}) {
|
|
|
273
370
|
release: release(),
|
|
274
371
|
arch: arch(),
|
|
275
372
|
node: process.version,
|
|
373
|
+
daemon: daemonRuntimeSummary(),
|
|
374
|
+
environment: safeEnvironmentSummary(),
|
|
276
375
|
pidPath: PID_PATH,
|
|
277
376
|
pid: process.pid,
|
|
278
377
|
configPath: configFile,
|
|
378
|
+
sessionsPath: sessionsFile,
|
|
279
379
|
snapshotPath: snapshotFile,
|
|
280
380
|
logPath: logFile,
|
|
281
381
|
logsBundled: logs.map((entry) => ({
|
|
@@ -343,6 +443,11 @@ export async function createDiagnosticBundle(opts = {}) {
|
|
|
343
443
|
name: "snapshot.json",
|
|
344
444
|
data: snapshot ?? `no snapshot file at ${snapshotFile}\n`,
|
|
345
445
|
});
|
|
446
|
+
const sessions = safeReadText(sessionsFile);
|
|
447
|
+
entries.push({
|
|
448
|
+
name: "sessions.json.redacted",
|
|
449
|
+
data: sessions ?? `no sessions file at ${sessionsFile}\n`,
|
|
450
|
+
});
|
|
346
451
|
const zip = createZip(entries);
|
|
347
452
|
const out = path.join(diagnosticsDir, filename);
|
|
348
453
|
writeFileSync(out, zip, { mode: 0o600 });
|
package/package.json
CHANGED
|
@@ -11,16 +11,22 @@ describe("diagnostics bundle", () => {
|
|
|
11
11
|
const logFile = path.join(tmp, "daemon.log");
|
|
12
12
|
const configFile = path.join(tmp, "config.json");
|
|
13
13
|
const snapshotFile = path.join(tmp, "snapshot.json");
|
|
14
|
+
const sessionsFile = path.join(tmp, "sessions.json");
|
|
14
15
|
const diagnosticsDir = path.join(tmp, "diagnostics");
|
|
15
16
|
writeFileSync(logFile, 'Authorization: Bearer secret-token\n{"refreshToken":"drt_secret"}\n');
|
|
16
17
|
writeFileSync(configFile, '{"token":"agent-secret","ok":true}\n');
|
|
17
18
|
writeFileSync(snapshotFile, '{"version":1}\n');
|
|
19
|
+
writeFileSync(
|
|
20
|
+
sessionsFile,
|
|
21
|
+
'{"version":1,"entries":{"k":{"runtimeSessionId":"sess_1","token":"session-secret"}}}\n',
|
|
22
|
+
);
|
|
18
23
|
|
|
19
24
|
const bundle = await createDiagnosticBundle({
|
|
20
25
|
diagnosticsDir,
|
|
21
26
|
logFile,
|
|
22
27
|
configFile,
|
|
23
28
|
snapshotFile,
|
|
29
|
+
sessionsFile,
|
|
24
30
|
doctor: { text: "doctor ok", json: { ok: true } },
|
|
25
31
|
});
|
|
26
32
|
expect(bundle.filename).toMatch(/^botcord-daemon-diagnostics-.*\.zip$/);
|
|
@@ -42,12 +48,29 @@ describe("diagnostics bundle", () => {
|
|
|
42
48
|
expect(listing).toContain("doctor.json");
|
|
43
49
|
expect(listing).toContain("status.json");
|
|
44
50
|
expect(listing).toContain("config.json.redacted");
|
|
51
|
+
expect(listing).toContain("sessions.json.redacted");
|
|
45
52
|
|
|
46
53
|
const log = execFileSync("unzip", ["-p", bundle.path, "daemon.log"], {
|
|
47
54
|
encoding: "utf8",
|
|
48
55
|
});
|
|
49
56
|
expect(log).toContain("Authorization: Bearer [REDACTED]");
|
|
50
57
|
expect(log).toContain('"refreshToken":"[REDACTED]"');
|
|
58
|
+
|
|
59
|
+
const status = JSON.parse(execFileSync("unzip", ["-p", bundle.path, "status.json"], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
}));
|
|
62
|
+
expect(status.daemon.packageName).toBe("@botcord/daemon");
|
|
63
|
+
expect(status.daemon.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
64
|
+
expect(status.daemon.entrypoint).toBeTruthy();
|
|
65
|
+
expect(status.daemon.packages["@botcord/daemon"]).toBe(status.daemon.version);
|
|
66
|
+
expect(status.environment.PATH).toBeTruthy();
|
|
67
|
+
expect(status.sessionsPath).toBe(sessionsFile);
|
|
68
|
+
|
|
69
|
+
const sessions = execFileSync("unzip", ["-p", bundle.path, "sessions.json.redacted"], {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
});
|
|
72
|
+
expect(sessions).toContain('"runtimeSessionId":"sess_1"');
|
|
73
|
+
expect(sessions).toContain('"token":"[REDACTED]"');
|
|
51
74
|
}, 20_000);
|
|
52
75
|
|
|
53
76
|
it("bundles active log plus latest 5 rotated logs by default, or all with includeAllLogs", async () => {
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir, hostname, platform, release, arch } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { deflateRawSync } from "node:zlib";
|
|
6
8
|
import {
|
|
7
9
|
AUTH_EXPIRED_FLAG_PATH,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
import {
|
|
13
15
|
CONFIG_FILE_PATH,
|
|
14
16
|
PID_PATH,
|
|
17
|
+
SESSIONS_PATH,
|
|
15
18
|
SNAPSHOT_PATH,
|
|
16
19
|
loadConfig,
|
|
17
20
|
saveConfig,
|
|
@@ -38,12 +41,31 @@ import {
|
|
|
38
41
|
const DIAGNOSTICS_DIR = path.join(homedir(), ".botcord", "diagnostics");
|
|
39
42
|
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
40
43
|
const DEFAULT_ROTATED_LOGS_IN_BUNDLE = 5;
|
|
44
|
+
const require = createRequire(import.meta.url);
|
|
45
|
+
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
46
|
+
const ENV_ALLOWLIST = new Set([
|
|
47
|
+
"NODE_ENV",
|
|
48
|
+
"PATH",
|
|
49
|
+
"BOTCORD_HUB",
|
|
50
|
+
"BOTCORD_DAEMON_HOME",
|
|
51
|
+
"BOTCORD_DAEMON_CONFIG",
|
|
52
|
+
"BOTCORD_DAEMON_LOG",
|
|
53
|
+
"BOTCORD_DAEMON_SNAPSHOT_INTERVAL_MS",
|
|
54
|
+
"BOTCORD_HERMES_AGENT_BIN",
|
|
55
|
+
"BOTCORD_CLAUDE_CODE_BIN",
|
|
56
|
+
"BOTCORD_CODEX_BIN",
|
|
57
|
+
"BOTCORD_GEMINI_BIN",
|
|
58
|
+
"BOTCORD_DEEPSEEK_TUI_BIN",
|
|
59
|
+
"BOTCORD_KIMI_CLI_BIN",
|
|
60
|
+
"OPENCLAW_ACP_URL",
|
|
61
|
+
]);
|
|
41
62
|
|
|
42
63
|
export interface CreateDiagnosticBundleOptions {
|
|
43
64
|
diagnosticsDir?: string;
|
|
44
65
|
logFile?: string;
|
|
45
66
|
configFile?: string;
|
|
46
67
|
snapshotFile?: string;
|
|
68
|
+
sessionsFile?: string;
|
|
47
69
|
doctor?: { text: string; json: unknown };
|
|
48
70
|
includeAllLogs?: boolean;
|
|
49
71
|
}
|
|
@@ -89,6 +111,81 @@ function safeReadText(file: string): string | null {
|
|
|
89
111
|
}
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
function readJsonFile(file: string): Record<string, unknown> | null {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
|
|
117
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
118
|
+
? parsed as Record<string, unknown>
|
|
119
|
+
: null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findDaemonPackageJson(startFile: string): Record<string, unknown> | null {
|
|
126
|
+
let dir = path.dirname(startFile);
|
|
127
|
+
for (let i = 0; i < 6; i += 1) {
|
|
128
|
+
const candidate = path.join(dir, "package.json");
|
|
129
|
+
const parsed = readJsonFile(candidate);
|
|
130
|
+
if (parsed?.name === "@botcord/daemon") return parsed;
|
|
131
|
+
const next = path.dirname(dir);
|
|
132
|
+
if (next === dir) break;
|
|
133
|
+
dir = next;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readInstalledPackageVersion(packageJsonSpecifier: string): string | null {
|
|
139
|
+
try {
|
|
140
|
+
const pkgPath = require.resolve(packageJsonSpecifier);
|
|
141
|
+
const parsed = readJsonFile(pkgPath);
|
|
142
|
+
return typeof parsed?.version === "string" ? parsed.version : null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function daemonRuntimeSummary(): Record<string, unknown> {
|
|
149
|
+
const pkg = findDaemonPackageJson(MODULE_PATH);
|
|
150
|
+
const version = typeof pkg?.version === "string" ? pkg.version : null;
|
|
151
|
+
const startedAtMs = Date.now() - Math.round(process.uptime() * 1000);
|
|
152
|
+
return {
|
|
153
|
+
packageName: typeof pkg?.name === "string" ? pkg.name : "@botcord/daemon",
|
|
154
|
+
version,
|
|
155
|
+
modulePath: MODULE_PATH,
|
|
156
|
+
entrypoint: process.argv[1] ?? null,
|
|
157
|
+
execPath: process.execPath,
|
|
158
|
+
argv: process.argv.map((arg) => redact(arg)),
|
|
159
|
+
execArgv: process.execArgv.map((arg) => redact(arg)),
|
|
160
|
+
cwd: process.cwd(),
|
|
161
|
+
pid: process.pid,
|
|
162
|
+
ppid: process.ppid,
|
|
163
|
+
uptimeSec: Math.round(process.uptime()),
|
|
164
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
165
|
+
versions: {
|
|
166
|
+
node: process.version,
|
|
167
|
+
v8: process.versions.v8,
|
|
168
|
+
uv: process.versions.uv,
|
|
169
|
+
openssl: process.versions.openssl,
|
|
170
|
+
},
|
|
171
|
+
packages: {
|
|
172
|
+
"@botcord/daemon": version,
|
|
173
|
+
"@botcord/cli": readInstalledPackageVersion("@botcord/cli/package.json"),
|
|
174
|
+
"@botcord/protocol-core": readInstalledPackageVersion("@botcord/protocol-core/package.json"),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safeEnvironmentSummary(): Record<string, string> {
|
|
180
|
+
const out: Record<string, string> = {};
|
|
181
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
182
|
+
if (!value) continue;
|
|
183
|
+
if (!ENV_ALLOWLIST.has(key) && !key.startsWith("BOTCORD_DAEMON_")) continue;
|
|
184
|
+
out[key] = redact(value);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
92
189
|
function readUserAuthSummary(): Record<string, unknown> | null {
|
|
93
190
|
const raw = safeReadText(USER_AUTH_PATH);
|
|
94
191
|
if (!raw) return null;
|
|
@@ -329,6 +426,7 @@ export async function createDiagnosticBundle(
|
|
|
329
426
|
const logFile = opts.logFile ?? LOG_FILE_PATH;
|
|
330
427
|
const configFile = opts.configFile ?? CONFIG_FILE_PATH;
|
|
331
428
|
const snapshotFile = opts.snapshotFile ?? SNAPSHOT_PATH;
|
|
429
|
+
const sessionsFile = opts.sessionsFile ?? SESSIONS_PATH;
|
|
332
430
|
const includeAllLogs = opts.includeAllLogs === true;
|
|
333
431
|
const logs = bundledLogs(logFile, includeAllLogs);
|
|
334
432
|
const acpLogs = listAcpTraceLogFiles(includeAllLogs);
|
|
@@ -343,9 +441,12 @@ export async function createDiagnosticBundle(
|
|
|
343
441
|
release: release(),
|
|
344
442
|
arch: arch(),
|
|
345
443
|
node: process.version,
|
|
444
|
+
daemon: daemonRuntimeSummary(),
|
|
445
|
+
environment: safeEnvironmentSummary(),
|
|
346
446
|
pidPath: PID_PATH,
|
|
347
447
|
pid: process.pid,
|
|
348
448
|
configPath: configFile,
|
|
449
|
+
sessionsPath: sessionsFile,
|
|
349
450
|
snapshotPath: snapshotFile,
|
|
350
451
|
logPath: logFile,
|
|
351
452
|
logsBundled: logs.map((entry) => ({
|
|
@@ -413,6 +514,11 @@ export async function createDiagnosticBundle(
|
|
|
413
514
|
name: "snapshot.json",
|
|
414
515
|
data: snapshot ?? `no snapshot file at ${snapshotFile}\n`,
|
|
415
516
|
});
|
|
517
|
+
const sessions = safeReadText(sessionsFile);
|
|
518
|
+
entries.push({
|
|
519
|
+
name: "sessions.json.redacted",
|
|
520
|
+
data: sessions ?? `no sessions file at ${sessionsFile}\n`,
|
|
521
|
+
});
|
|
416
522
|
|
|
417
523
|
const zip = createZip(entries);
|
|
418
524
|
const out = path.join(diagnosticsDir, filename);
|