@botcord/daemon 0.2.61 → 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.
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createAcpTraceLogger } from "../../acp-logs.js";
2
3
  import { consoleLogger } from "../log.js";
3
4
  /**
4
5
  * Minimal bidirectional ACP (Agent Client Protocol) client used by runtime
@@ -22,22 +23,32 @@ const ASSISTANT_TEXT_CAP = 1 * 1024 * 1024;
22
23
  const KILL_GRACE_MS = 5_000;
23
24
  /** Deadline for the initial `initialize` handshake. */
24
25
  const INITIALIZE_TIMEOUT_MS = 30_000;
26
+ /** Short drain window for late `session/update` chunks after a prompt RPC error. */
27
+ const PROMPT_ERROR_DRAIN_MS = 750;
25
28
  /** ACP protocol version this client targets. */
26
29
  export const ACP_PROTOCOL_VERSION = 1;
30
+ function stringField(obj, key) {
31
+ if (!obj || typeof obj !== "object")
32
+ return undefined;
33
+ const value = obj[key];
34
+ return typeof value === "string" && value.length > 0 ? value : undefined;
35
+ }
27
36
  /** Minimal newline-JSON-RPC framing on top of a child process's stdio. */
28
37
  class AcpConnection {
29
38
  child;
30
39
  handlers;
31
40
  logId;
41
+ trace;
32
42
  nextId = 1;
33
43
  pending = new Map();
34
44
  stdoutBuf = "";
35
45
  closed = false;
36
46
  closeReason = null;
37
- constructor(child, handlers, logId) {
47
+ constructor(child, handlers, logId, trace = null) {
38
48
  this.child = child;
39
49
  this.handlers = handlers;
40
50
  this.logId = logId;
51
+ this.trace = trace;
41
52
  child.stdout.setEncoding("utf8");
42
53
  child.stdout.on("data", (chunk) => this.onStdout(chunk));
43
54
  child.stdout.on("end", () => this.fail(new Error("stdout closed")));
@@ -61,6 +72,7 @@ class AcpConnection {
61
72
  msg = JSON.parse(line);
62
73
  }
63
74
  catch {
75
+ this.trace?.write({ stream: "stdout_non_json", chunk: line });
64
76
  log.warn(`${this.logId} non-json acp line`, { line: line.slice(0, 200) });
65
77
  return;
66
78
  }
@@ -73,10 +85,25 @@ class AcpConnection {
73
85
  return;
74
86
  this.pending.delete(msg.id);
75
87
  if (msg.error) {
88
+ this.trace?.write({
89
+ stream: "rpc_in",
90
+ direction: "in",
91
+ id: msg.id,
92
+ status: "error",
93
+ code: typeof msg.error.code === "number" ? msg.error.code : undefined,
94
+ error: msg.error.message ?? "(no message)",
95
+ });
76
96
  const err = new Error(`acp error ${msg.error.code ?? "?"}: ${msg.error.message ?? "(no message)"}`);
77
97
  pending.reject(err);
78
98
  }
79
99
  else {
100
+ this.trace?.write({
101
+ stream: "rpc_in",
102
+ direction: "in",
103
+ id: msg.id,
104
+ status: "response",
105
+ result: msg.result ?? null,
106
+ });
80
107
  pending.resolve(msg.result ?? null);
81
108
  }
82
109
  return;
@@ -84,9 +111,24 @@ class AcpConnection {
84
111
  if (typeof msg.method === "string") {
85
112
  // Server→client request (has `id`) or notification (no `id`)
86
113
  if (msg.id !== undefined) {
114
+ this.trace?.write({
115
+ stream: "rpc_in",
116
+ direction: "in",
117
+ id: msg.id,
118
+ method: msg.method,
119
+ status: "request",
120
+ params: msg.params,
121
+ });
87
122
  void this.handleServerRequest(msg.id, msg.method, msg.params);
88
123
  }
89
124
  else {
125
+ this.trace?.write({
126
+ stream: "rpc_in",
127
+ direction: "in",
128
+ method: msg.method,
129
+ status: "notification",
130
+ params: msg.params,
131
+ });
90
132
  try {
91
133
  this.handlers.onNotification(msg.method, msg.params);
92
134
  }
@@ -114,6 +156,15 @@ class AcpConnection {
114
156
  const reply = error
115
157
  ? { jsonrpc: "2.0", id, error }
116
158
  : { jsonrpc: "2.0", id, result: result ?? null };
159
+ this.trace?.write({
160
+ stream: "rpc_out",
161
+ direction: "out",
162
+ id,
163
+ status: error ? "error" : "response",
164
+ code: error?.code,
165
+ error: error?.message,
166
+ result: error ? undefined : result ?? null,
167
+ });
117
168
  this.writeMessage(reply);
118
169
  }
119
170
  writeMessage(obj) {
@@ -136,10 +187,25 @@ class AcpConnection {
136
187
  resolve: (v) => resolve(v),
137
188
  reject,
138
189
  });
190
+ this.trace?.write({
191
+ stream: "rpc_out",
192
+ direction: "out",
193
+ id,
194
+ method,
195
+ status: "request",
196
+ params,
197
+ });
139
198
  this.writeMessage({ jsonrpc: "2.0", id, method, params });
140
199
  });
141
200
  }
142
201
  notify(method, params) {
202
+ this.trace?.write({
203
+ stream: "rpc_out",
204
+ direction: "out",
205
+ method,
206
+ status: "notification",
207
+ params,
208
+ });
143
209
  this.writeMessage({ jsonrpc: "2.0", method, params });
144
210
  }
145
211
  fail(err) {
@@ -205,6 +271,20 @@ export class AcpRuntimeAdapter {
205
271
  env: this.spawnEnv(opts),
206
272
  stdio: ["pipe", "pipe", "pipe"],
207
273
  });
274
+ const trace = createAcpTraceLogger({
275
+ runtime: this.id,
276
+ accountId: opts.accountId,
277
+ turnId: stringField(opts.context, "turnId"),
278
+ roomId: stringField(opts.context, "roomId"),
279
+ topicId: stringField(opts.context, "topicId") ?? null,
280
+ hermesProfile: opts.hermesProfile,
281
+ sessionId: opts.sessionId,
282
+ });
283
+ trace?.write({
284
+ stream: "child_start",
285
+ pid: child.pid,
286
+ params: { command: binary, args, cwd: opts.cwd },
287
+ });
208
288
  let killTimer = null;
209
289
  const onAbort = () => {
210
290
  if (child.killed)
@@ -235,6 +315,7 @@ export class AcpRuntimeAdapter {
235
315
  child.stderr.setEncoding("utf8");
236
316
  child.stderr.on("data", (chunk) => {
237
317
  stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_CAP);
318
+ trace?.write({ stream: "stderr", pid: child.pid, chunk });
238
319
  });
239
320
  const state = {
240
321
  finalText: "",
@@ -289,11 +370,22 @@ export class AcpRuntimeAdapter {
289
370
  const err = new Error(`unknown server request: ${method}`);
290
371
  throw err;
291
372
  },
292
- }, this.id);
373
+ }, this.id, trace);
293
374
  const childExit = new Promise((resolve) => {
294
- child.on("close", (code) => resolve(code ?? 0));
375
+ child.on("close", (code, signal) => {
376
+ trace?.write({ stream: "child_exit", pid: child.pid, code, signal });
377
+ resolve(code ?? 0);
378
+ });
379
+ child.on("error", (err) => {
380
+ trace?.write({
381
+ stream: "child_error",
382
+ pid: child.pid,
383
+ error: err instanceof Error ? err.message : String(err),
384
+ });
385
+ });
295
386
  });
296
387
  let newSessionId = opts.sessionId ?? "";
388
+ let promptStarted = false;
297
389
  try {
298
390
  // 1) initialize
299
391
  await this.withTimeout(conn.request("initialize", {
@@ -335,6 +427,7 @@ export class AcpRuntimeAdapter {
335
427
  }
336
428
  newSessionId = sessionId;
337
429
  // 3) session/prompt
430
+ promptStarted = true;
338
431
  const promptResult = (await conn.request("session/prompt", {
339
432
  sessionId,
340
433
  prompt: [{ type: "text", text: opts.text }],
@@ -373,6 +466,9 @@ export class AcpRuntimeAdapter {
373
466
  const tail = stderrTail.slice(-STDERR_ERROR_SNIPPET).trim();
374
467
  state.errorText =
375
468
  state.errorText ?? (tail ? `${baseMsg}; stderr: ${tail}` : baseMsg);
469
+ if (promptStarted && !opts.signal.aborted) {
470
+ await sleepUnlessAborted(PROMPT_ERROR_DRAIN_MS, opts.signal);
471
+ }
376
472
  try {
377
473
  child.stdin.end();
378
474
  }
@@ -417,3 +513,18 @@ export class AcpRuntimeAdapter {
417
513
  });
418
514
  }
419
515
  }
516
+ function sleepUnlessAborted(ms, signal) {
517
+ if (signal.aborted)
518
+ return Promise.resolve();
519
+ return new Promise((resolve) => {
520
+ const t = setTimeout(done, ms);
521
+ if (typeof t.unref === "function")
522
+ t.unref();
523
+ function done() {
524
+ signal.removeEventListener("abort", done);
525
+ clearTimeout(t);
526
+ resolve();
527
+ }
528
+ signal.addEventListener("abort", done, { once: true });
529
+ });
530
+ }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createAcpTraceLogger } from "../../acp-logs.js";
2
3
  import { readCommandVersion, resolveCommandOnPath, } from "./probe.js";
3
4
  import { consoleLogger } from "../log.js";
4
5
  const log = consoleLogger;
@@ -282,6 +283,12 @@ export class OpenclawAcpAdapter {
282
283
  }
283
284
  if (!finalText) {
284
285
  const stopReason = pickStopReason(promptResult);
286
+ if (!stopReason || stopReason === "end_turn") {
287
+ return {
288
+ text: "",
289
+ newSessionId: acpSessionId,
290
+ };
291
+ }
285
292
  const warningTail = handle.nonJsonStdoutTail.slice(-8).join("\n").trim();
286
293
  const detail = warningTail ? `; stdout: ${truncateDetail(warningTail, 1000)}` : "";
287
294
  const reason = stopReason ? `prompt stopped: ${stopReason}` : "empty assistant response";
@@ -356,10 +363,22 @@ export class OpenclawAcpAdapter {
356
363
  const args = ["acp", "--url", gateway.url];
357
364
  if (gateway.token)
358
365
  args.push("--token", gateway.token);
366
+ const accountId = key.split("::", 1)[0];
367
+ const trace = createAcpTraceLogger({
368
+ runtime: acpRuntimeLogName(gateway),
369
+ accountId,
370
+ gatewayName: gateway.name,
371
+ gatewayUrl: gateway.url,
372
+ });
359
373
  const child = this.spawnFn(command, args, {
360
374
  stdio: ["pipe", "pipe", "pipe"],
361
375
  env: { ...process.env },
362
376
  });
377
+ trace?.write({
378
+ stream: "child_start",
379
+ pid: child.pid,
380
+ params: { command, args, gateway: gateway.name },
381
+ });
363
382
  const handle = {
364
383
  child,
365
384
  pending: new Map(),
@@ -372,18 +391,26 @@ export class OpenclawAcpAdapter {
372
391
  closed: false,
373
392
  spawnedUrl: gateway.url,
374
393
  spawnedToken: gateway.token,
394
+ trace,
375
395
  };
376
396
  child.stdout.setEncoding("utf8");
377
397
  child.stdout.on("data", (chunk) => onStdoutChunk(handle, chunk));
378
398
  child.stderr.setEncoding("utf8");
379
399
  child.stderr.on("data", (chunk) => {
400
+ trace?.write({ stream: "stderr", pid: child.pid, chunk });
380
401
  log.debug("openclaw-acp.stderr", { key, chunk: chunk.slice(0, 500) });
381
402
  });
382
403
  child.on("exit", (code, signal) => {
404
+ trace?.write({ stream: "child_exit", pid: child.pid, code, signal });
383
405
  shutdownHandle(handle, `exit code=${code ?? "null"} signal=${signal ?? "null"}`);
384
406
  ACP_POOL.delete(key);
385
407
  });
386
408
  child.on("error", (err) => {
409
+ trace?.write({
410
+ stream: "child_error",
411
+ pid: child.pid,
412
+ error: err instanceof Error ? err.message : String(err),
413
+ });
387
414
  log.warn("openclaw-acp.child-error", {
388
415
  key,
389
416
  error: err instanceof Error ? err.message : String(err),
@@ -426,6 +453,18 @@ export class OpenclawAcpAdapter {
426
453
  // ---------------------------------------------------------------------------
427
454
  // JSON-RPC stdio plumbing
428
455
  // ---------------------------------------------------------------------------
456
+ function acpRuntimeLogName(gateway) {
457
+ if (gateway.name.toLowerCase().includes("qclaw"))
458
+ return "qclaw-acp";
459
+ try {
460
+ if (new URL(gateway.url).port === "28789")
461
+ return "qclaw-acp";
462
+ }
463
+ catch {
464
+ // Fall back to OpenClaw.
465
+ }
466
+ return "openclaw-acp";
467
+ }
429
468
  function onStdoutChunk(handle, chunk) {
430
469
  handle.buffer += chunk;
431
470
  let idx;
@@ -447,6 +486,7 @@ function onStdoutChunk(handle, chunk) {
447
486
  error: err instanceof Error ? err.message : String(err),
448
487
  line: line.slice(0, 200),
449
488
  });
489
+ handle.trace?.write({ stream: "stdout_non_json", chunk: line });
450
490
  continue;
451
491
  }
452
492
  routeMessage(handle, msg);
@@ -461,15 +501,37 @@ function routeMessage(handle, msg) {
461
501
  handle.pending.delete(id);
462
502
  if (msg.error) {
463
503
  const message = formatRpcError(msg.error);
504
+ handle.trace?.write({
505
+ stream: "rpc_in",
506
+ direction: "in",
507
+ id,
508
+ status: "error",
509
+ code: typeof msg.error.code === "number" ? msg.error.code : undefined,
510
+ error: message,
511
+ });
464
512
  pending.reject(new Error(message));
465
513
  }
466
514
  else {
515
+ handle.trace?.write({
516
+ stream: "rpc_in",
517
+ direction: "in",
518
+ id,
519
+ status: "response",
520
+ result: msg.result ?? null,
521
+ });
467
522
  pending.resolve(msg.result);
468
523
  }
469
524
  return;
470
525
  }
471
526
  // Notification.
472
527
  if (msg?.method && msg?.params) {
528
+ handle.trace?.write({
529
+ stream: "rpc_in",
530
+ direction: "in",
531
+ method: msg.method,
532
+ status: "notification",
533
+ params: msg.params,
534
+ });
473
535
  const sid = msg.params?.sessionId;
474
536
  if (typeof sid === "string") {
475
537
  const sub = handle.subscribers.get(sid);
@@ -492,6 +554,14 @@ function sendRequest(handle, method, params) {
492
554
  return new Promise((resolve, reject) => {
493
555
  const id = handle.nextId++;
494
556
  handle.pending.set(id, { resolve, reject, method });
557
+ handle.trace?.write({
558
+ stream: "rpc_out",
559
+ direction: "out",
560
+ id,
561
+ method,
562
+ status: "request",
563
+ params,
564
+ });
495
565
  const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
496
566
  try {
497
567
  handle.child.stdin.write(frame);
@@ -505,6 +575,13 @@ function sendRequest(handle, method, params) {
505
575
  function sendNotification(handle, method, params) {
506
576
  if (handle.closed)
507
577
  return;
578
+ handle.trace?.write({
579
+ stream: "rpc_out",
580
+ direction: "out",
581
+ method,
582
+ status: "notification",
583
+ params,
584
+ });
508
585
  const frame = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
509
586
  try {
510
587
  handle.child.stdin.write(frame);
package/dist/index.js CHANGED
@@ -250,6 +250,33 @@ function loadOrInitConfig(args) {
250
250
  return cfg;
251
251
  }
252
252
  }
253
+ async function refreshDiscoveredOpenclawGateways(cfg, source) {
254
+ if (!openclawDiscoveryConfigEnabled(cfg))
255
+ return cfg;
256
+ try {
257
+ const found = await discoverLocalOpenclawGateways({
258
+ searchPaths: cfg.openclawDiscovery?.searchPaths,
259
+ defaultPorts: cfg.openclawDiscovery?.defaultPorts,
260
+ timeoutMs: 500,
261
+ });
262
+ const merged = mergeOpenclawGateways(cfg, found);
263
+ if (!merged.changed)
264
+ return cfg;
265
+ saveConfig(merged.cfg);
266
+ log.info("openclaw discovery: gateways merged", {
267
+ source,
268
+ added: merged.added.map((g) => ({ name: g.name, url: g.url })),
269
+ });
270
+ return merged.cfg;
271
+ }
272
+ catch (err) {
273
+ log.warn("openclaw discovery failed; continuing", {
274
+ source,
275
+ error: err instanceof Error ? err.message : String(err),
276
+ });
277
+ return cfg;
278
+ }
279
+ }
253
280
  /**
254
281
  * Read the current user-auth record without throwing on parse / permission
255
282
  * errors — those are returned as `null` so the caller treats them like a
@@ -465,28 +492,7 @@ async function ensureUserAuthForStart(args) {
465
492
  }
466
493
  async function cmdStart(args) {
467
494
  let cfg = loadOrInitConfig(args);
468
- if (openclawDiscoveryConfigEnabled(cfg)) {
469
- try {
470
- const found = await discoverLocalOpenclawGateways({
471
- searchPaths: cfg.openclawDiscovery?.searchPaths,
472
- defaultPorts: cfg.openclawDiscovery?.defaultPorts,
473
- timeoutMs: 500,
474
- });
475
- const merged = mergeOpenclawGateways(cfg, found);
476
- if (merged.changed) {
477
- cfg = merged.cfg;
478
- saveConfig(cfg);
479
- log.info("openclaw discovery: gateways merged", {
480
- added: merged.added.map((g) => ({ name: g.name, url: g.url })),
481
- });
482
- }
483
- }
484
- catch (err) {
485
- log.warn("openclaw discovery failed; continuing", {
486
- error: err instanceof Error ? err.message : String(err),
487
- });
488
- }
489
- }
495
+ cfg = await refreshDiscoveredOpenclawGateways(cfg, "start");
490
496
  // Foreground is now the default. --background (alias -d) detaches.
491
497
  // --foreground is still accepted (no-op) for backwards compatibility and
492
498
  // is also what the detached child re-execs itself with.
@@ -1241,8 +1247,8 @@ async function cmdDoctor(args) {
1241
1247
  let cfgForEndpoints = null;
1242
1248
  try {
1243
1249
  const cfg = loadConfig();
1244
- cfgForEndpoints = cfg;
1245
- channels = channelsFromDaemonConfig(cfg);
1250
+ cfgForEndpoints = await refreshDiscoveredOpenclawGateways(cfg, "doctor");
1251
+ channels = channelsFromDaemonConfig(cfgForEndpoints);
1246
1252
  }
1247
1253
  catch {
1248
1254
  channels = [];
@@ -304,6 +304,7 @@ export function mergeOpenclawGateways(cfg, found) {
304
304
  }
305
305
  function discoverFromConfigDir(root) {
306
306
  const dir = expandHome(root);
307
+ const rootIsQclaw = path.basename(dir) === ".qclaw";
307
308
  let names;
308
309
  try {
309
310
  names = readdirSync(dir);
@@ -324,8 +325,9 @@ function discoverFromConfigDir(root) {
324
325
  const parsed = name.endsWith(".json") ? parseJsonConfig(raw) : parseTomlConfig(raw);
325
326
  if (!parsed?.url)
326
327
  continue;
328
+ const namePrefix = rootIsQclaw || name.toLowerCase() === "qclaw.json" ? "qclaw" : "openclaw";
327
329
  const item = {
328
- name: nameFromUrl(parsed.url),
330
+ name: nameFromUrl(parsed.url, namePrefix),
329
331
  url: parsed.url,
330
332
  source: "config-file",
331
333
  };
@@ -454,24 +456,30 @@ function dedupeDiscovered(items) {
454
456
  for (const item of items) {
455
457
  const key = normalizeUrlKey(item.url);
456
458
  const prev = byUrl.get(key);
457
- if (!prev || priority[item.source] > priority[prev.source] || hasMoreAuth(item, prev)) {
459
+ if (!prev ||
460
+ priority[item.source] > priority[prev.source] ||
461
+ hasMoreAuth(item, prev) ||
462
+ prefersQclawName(item, prev)) {
458
463
  byUrl.set(key, item);
459
464
  }
460
465
  }
461
466
  return [...byUrl.values()];
462
467
  }
468
+ function prefersQclawName(a, b) {
469
+ return a.name.startsWith("qclaw-") && !b.name.startsWith("qclaw-");
470
+ }
463
471
  function hasMoreAuth(a, b) {
464
472
  const score = (x) => (x.token ? 2 : x.tokenFile ? 1 : 0);
465
473
  return score(a) > score(b);
466
474
  }
467
- function nameFromUrl(raw) {
475
+ function nameFromUrl(raw, prefix = "openclaw") {
468
476
  try {
469
477
  const u = new URL(raw);
470
478
  const base = `${u.hostname}-${u.port || (u.protocol === "wss:" ? "443" : "80")}`;
471
- return `openclaw-${base.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
479
+ return `${prefix}-${base.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
472
480
  }
473
481
  catch {
474
- return "openclaw-local";
482
+ return `${prefix}-local`;
475
483
  }
476
484
  }
477
485
  function uniqueName(base, existing) {
package/dist/provision.js CHANGED
@@ -10,6 +10,7 @@ import path from "node:path";
10
10
  import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, writeCredentialsFile, } from "@botcord/protocol-core";
11
11
  import { loadConfig, resolveConfiguredAgentIds, saveConfig, } from "./config.js";
12
12
  import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes, prepareGatewayProfile, } from "./daemon-config-map.js";
13
+ import { discoverLocalOpenclawGateways, mergeOpenclawGateways, openclawDiscoveryConfigEnabled, } from "./openclaw-discovery.js";
13
14
  import { agentHomeDir, agentStateDir, agentWorkspaceDir, applyAgentIdentity, ensureAttachedHermesProfileSkills, ensureAgentWorkspace, } from "./agent-workspace.js";
14
15
  import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
15
16
  import { createGatewayControl } from "./gateway-control.js";
@@ -199,6 +200,7 @@ export function createProvisioner(opts) {
199
200
  let cfgForProbe;
200
201
  try {
201
202
  cfgForProbe = loadConfig();
203
+ cfgForProbe = await refreshDiscoveredOpenclawGateways(cfgForProbe);
202
204
  }
203
205
  catch {
204
206
  cfgForProbe = undefined;
@@ -284,6 +286,33 @@ export function createProvisioner(opts) {
284
286
  }
285
287
  };
286
288
  }
289
+ async function refreshDiscoveredOpenclawGateways(cfg) {
290
+ if (!openclawDiscoveryConfigEnabled(cfg))
291
+ return cfg;
292
+ try {
293
+ const found = await discoverLocalOpenclawGateways({
294
+ searchPaths: cfg.openclawDiscovery?.searchPaths,
295
+ defaultPorts: cfg.openclawDiscovery?.defaultPorts,
296
+ timeoutMs: 500,
297
+ });
298
+ const merged = mergeOpenclawGateways(cfg, found);
299
+ if (!merged.changed)
300
+ return cfg;
301
+ saveConfig(merged.cfg);
302
+ daemonLog.info("openclaw discovery: gateways merged", {
303
+ source: "list_runtimes",
304
+ added: merged.added.map((g) => ({ name: g.name, url: g.url })),
305
+ });
306
+ return merged.cfg;
307
+ }
308
+ catch (err) {
309
+ daemonLog.warn("openclaw discovery failed; continuing", {
310
+ source: "list_runtimes",
311
+ error: err instanceof Error ? err.message : String(err),
312
+ });
313
+ return cfg;
314
+ }
315
+ }
287
316
  async function handleWakeAgent(gateway, raw) {
288
317
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
289
318
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.61",
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": {
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ const originalHome = process.env.HOME;
8
+
9
+ afterEach(() => {
10
+ if (originalHome === undefined) delete process.env.HOME;
11
+ else process.env.HOME = originalHome;
12
+ delete process.env.BOTCORD_ACP_LOGS;
13
+ delete process.env.BOTCORD_ACP_TRACE;
14
+ vi.resetModules();
15
+ });
16
+
17
+ describe("ACP trace logs", () => {
18
+ it("writes redacted safe-mode jsonl and lists it for diagnostics", async () => {
19
+ const home = mkdtempSync(path.join(tmpdir(), "botcord-acp-log-home-"));
20
+ process.env.HOME = home;
21
+ vi.resetModules();
22
+ const { createAcpTraceLogger, listAcpTraceLogFiles } = await import("../acp-logs.js");
23
+
24
+ const logger = createAcpTraceLogger({
25
+ runtime: "openclaw-acp",
26
+ accountId: "ag_test",
27
+ roomId: "rm_test",
28
+ gatewayName: "qclaw-127-0-0-1-28789",
29
+ gatewayUrl: "ws://127.0.0.1:28789",
30
+ });
31
+ expect(logger).not.toBeNull();
32
+ logger!.write({
33
+ stream: "rpc_out",
34
+ direction: "out",
35
+ id: 1,
36
+ method: "session/prompt",
37
+ status: "request",
38
+ params: {
39
+ sessionId: "sess_1",
40
+ token: "secret-token",
41
+ prompt: [{ type: "text", text: "hello from a user prompt" }],
42
+ },
43
+ });
44
+
45
+ const raw = readFileSync(logger!.path, "utf8");
46
+ expect(raw).toContain('"method":"session/prompt"');
47
+ expect(raw).toContain('"preview":"[REDACTED]"');
48
+ expect(raw).toContain('"textBytes"');
49
+ expect(raw).toContain('"textPreview"');
50
+ expect(raw).not.toContain("secret-token");
51
+ const files = listAcpTraceLogFiles();
52
+ expect(files).toHaveLength(1);
53
+ expect(files[0].path).toBe(logger!.path);
54
+ });
55
+
56
+ it("bundles ACP and runtime logs in diagnostics", async () => {
57
+ const home = mkdtempSync(path.join(tmpdir(), "botcord-diag-acp-home-"));
58
+ process.env.HOME = home;
59
+ vi.resetModules();
60
+ const { createAcpTraceLogger } = await import("../acp-logs.js");
61
+ const { createDiagnosticBundle } = await import("../diagnostics.js");
62
+
63
+ const logger = createAcpTraceLogger({ runtime: "hermes-agent", accountId: "ag_hermes" });
64
+ logger!.write({ stream: "stderr", chunk: "hermes auth token=secret\n" });
65
+ const botcordDaemon = path.join(home, ".botcord", "daemon");
66
+ const qclawLogs = path.join(home, ".qclaw", "logs");
67
+ mkdirSync(botcordDaemon, { recursive: true });
68
+ writeFileSync(path.join(home, ".botcord", "daemon.log"), "daemon\n", { flag: "w" });
69
+ writeFileSync(path.join(home, ".botcord", "snapshot.json"), "{}\n", { flag: "w" });
70
+ writeFileSync(path.join(botcordDaemon, "config.json"), "{}\n", { flag: "w" });
71
+ mkdirSync(qclawLogs, { recursive: true });
72
+ writeFileSync(path.join(qclawLogs, "qclaw.log"), "qclaw token=secret\n");
73
+
74
+ const bundle = await createDiagnosticBundle({
75
+ diagnosticsDir: path.join(home, ".botcord", "diagnostics"),
76
+ doctor: { text: "doctor ok", json: { ok: true } },
77
+ });
78
+
79
+ expect(existsSync(bundle.path)).toBe(true);
80
+ const listing = execFileSync("unzip", ["-l", bundle.path], { encoding: "utf8" });
81
+ expect(listing).toContain("acp-logs/hermes-agent");
82
+ expect(listing).toContain("runtime-logs/qclaw/qclaw.log");
83
+ const acpLog = execFileSync("unzip", ["-p", bundle.path, "acp-logs/hermes-agent/ag_hermes.jsonl"], {
84
+ encoding: "utf8",
85
+ });
86
+ expect(acpLog).toContain("[REDACTED]");
87
+ });
88
+ });