@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.
@@ -5,6 +5,7 @@ export interface CreateDiagnosticBundleOptions {
5
5
  logFile?: string;
6
6
  configFile?: string;
7
7
  snapshotFile?: string;
8
+ sessionsFile?: string;
8
9
  doctor?: {
9
10
  text: string;
10
11
  json: unknown;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.62",
3
+ "version": "0.2.63",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 () => {
@@ -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);