@botcord/daemon 0.2.5 → 0.2.8

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.
Files changed (88) hide show
  1. package/dist/agent-discovery.d.ts +4 -0
  2. package/dist/agent-discovery.js +8 -0
  3. package/dist/agent-workspace.d.ts +62 -0
  4. package/dist/agent-workspace.js +140 -8
  5. package/dist/config.d.ts +64 -1
  6. package/dist/config.js +73 -1
  7. package/dist/daemon-config-map.d.ts +27 -9
  8. package/dist/daemon-config-map.js +105 -8
  9. package/dist/daemon.d.ts +2 -0
  10. package/dist/daemon.js +76 -6
  11. package/dist/doctor.d.ts +27 -1
  12. package/dist/doctor.js +22 -1
  13. package/dist/gateway/cli-resolver.d.ts +34 -0
  14. package/dist/gateway/cli-resolver.js +74 -0
  15. package/dist/gateway/dispatcher.d.ts +31 -1
  16. package/dist/gateway/dispatcher.js +337 -29
  17. package/dist/gateway/gateway.d.ts +29 -1
  18. package/dist/gateway/gateway.js +10 -0
  19. package/dist/gateway/index.d.ts +2 -0
  20. package/dist/gateway/index.js +2 -0
  21. package/dist/gateway/policy-resolver.d.ts +57 -0
  22. package/dist/gateway/policy-resolver.js +123 -0
  23. package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
  24. package/dist/gateway/runtimes/acp-stream.js +394 -0
  25. package/dist/gateway/runtimes/codex.js +7 -0
  26. package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
  27. package/dist/gateway/runtimes/hermes-agent.js +180 -0
  28. package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
  29. package/dist/gateway/runtimes/ndjson-stream.js +16 -3
  30. package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
  31. package/dist/gateway/runtimes/openclaw-acp.js +500 -0
  32. package/dist/gateway/runtimes/registry.d.ts +4 -0
  33. package/dist/gateway/runtimes/registry.js +22 -0
  34. package/dist/gateway/transcript-paths.d.ts +30 -0
  35. package/dist/gateway/transcript-paths.js +114 -0
  36. package/dist/gateway/transcript.d.ts +123 -0
  37. package/dist/gateway/transcript.js +147 -0
  38. package/dist/gateway/types.d.ts +31 -0
  39. package/dist/index.js +309 -27
  40. package/dist/mention-scan.d.ts +22 -0
  41. package/dist/mention-scan.js +35 -0
  42. package/dist/openclaw-discovery.d.ts +28 -0
  43. package/dist/openclaw-discovery.js +228 -0
  44. package/dist/provision.d.ts +113 -1
  45. package/dist/provision.js +564 -12
  46. package/dist/system-context.d.ts +5 -4
  47. package/dist/system-context.js +35 -5
  48. package/dist/url-utils.d.ts +9 -0
  49. package/dist/url-utils.js +18 -0
  50. package/package.json +3 -2
  51. package/src/__tests__/agent-workspace.test.ts +93 -0
  52. package/src/__tests__/daemon-config-map.test.ts +79 -0
  53. package/src/__tests__/openclaw-acp.test.ts +234 -0
  54. package/src/__tests__/openclaw-discovery.test.ts +150 -0
  55. package/src/__tests__/policy-resolver.test.ts +124 -0
  56. package/src/__tests__/policy-updated-handler.test.ts +144 -0
  57. package/src/__tests__/provision.test.ts +265 -0
  58. package/src/__tests__/system-context.test.ts +52 -0
  59. package/src/__tests__/url-utils.test.ts +37 -0
  60. package/src/agent-discovery.ts +8 -0
  61. package/src/agent-workspace.ts +173 -7
  62. package/src/config.ts +168 -4
  63. package/src/daemon-config-map.ts +154 -9
  64. package/src/daemon.ts +96 -6
  65. package/src/doctor.ts +49 -2
  66. package/src/gateway/__tests__/dispatcher.test.ts +65 -0
  67. package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
  68. package/src/gateway/__tests__/transcript.test.ts +496 -0
  69. package/src/gateway/cli-resolver.ts +92 -0
  70. package/src/gateway/dispatcher.ts +394 -26
  71. package/src/gateway/gateway.ts +46 -0
  72. package/src/gateway/index.ts +25 -0
  73. package/src/gateway/policy-resolver.ts +171 -0
  74. package/src/gateway/runtimes/acp-stream.ts +535 -0
  75. package/src/gateway/runtimes/codex.ts +7 -0
  76. package/src/gateway/runtimes/hermes-agent.ts +206 -0
  77. package/src/gateway/runtimes/ndjson-stream.ts +16 -3
  78. package/src/gateway/runtimes/openclaw-acp.ts +606 -0
  79. package/src/gateway/runtimes/registry.ts +24 -0
  80. package/src/gateway/transcript-paths.ts +145 -0
  81. package/src/gateway/transcript.ts +300 -0
  82. package/src/gateway/types.ts +32 -0
  83. package/src/index.ts +321 -30
  84. package/src/mention-scan.ts +38 -0
  85. package/src/openclaw-discovery.ts +262 -0
  86. package/src/provision.ts +682 -14
  87. package/src/system-context.ts +41 -9
  88. package/src/url-utils.ts +17 -0
package/dist/index.js CHANGED
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
3
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync, rmSync } from "node:fs";
4
4
  import { homedir, hostname } from "node:os";
5
5
  import path from "node:path";
6
- import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, PID_PATH, SNAPSHOT_PATH, CONFIG_FILE_PATH, } from "./config.js";
6
+ import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, PID_PATH, SNAPSHOT_PATH, CONFIG_FILE_PATH, CONFIG_MISSING, } from "./config.js";
7
7
  import { resolveBootAgents } from "./agent-discovery.js";
8
+ import { defaultTranscriptRoot, resolveTranscriptEnabled, transcriptAgentRoot, transcriptFilePath, } from "./gateway/index.js";
8
9
  import { startDaemon } from "./daemon.js";
9
10
  import { log, LOG_FILE_PATH } from "./log.js";
10
11
  import { detectRuntimes, getAdapterModule, listAdapterIds } from "./adapters/runtimes.js";
11
12
  import { pollDeviceToken, requestDeviceCode, } from "@botcord/protocol-core";
12
13
  import { AUTH_EXPIRED_FLAG_PATH, clearAuthExpiredFlag, loadUserAuth, saveUserAuth, userAuthFromTokenResponse, } from "./user-auth.js";
13
14
  import { renderStatus } from "./status-render.js";
15
+ import { appendNextParam } from "./url-utils.js";
14
16
  import { channelsFromDaemonConfig, defaultHttpFetcher, renderDoctor, runDoctor, } from "./doctor.js";
15
17
  import { clearWorkingMemory, readWorkingMemory, resolveMemoryDir, updateWorkingMemory, DEFAULT_SECTION, } from "./working-memory.js";
18
+ import { discoverLocalOpenclawGateways, mergeOpenclawGateways, openclawDiscoveryConfigEnabled, } from "./openclaw-discovery.js";
16
19
  const ADAPTER_LIST = listAdapterIds().join("|");
17
20
  const DEFAULT_HUB = "https://api.botcord.chat";
18
21
  /**
@@ -29,12 +32,8 @@ const HELP = `botcord-daemon — BotCord local daemon
29
32
  Usage: botcord-daemon <command> [options]
30
33
 
31
34
  Commands:
32
- init [--agent <ag_xxx> ...] [--cwd <path>]
33
- Create ~/.botcord/daemon/config.json.
34
- Without --agent, the daemon discovers
35
- identities from ~/.botcord/credentials
36
- at startup (repeat --agent to pin).
37
35
  start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
36
+ [--agent <ag_xxx> ...] [--cwd <path>]
38
37
  Start the daemon in the foreground by
39
38
  default. Pass --background (alias -d)
40
39
  to detach and return to the shell.
@@ -48,9 +47,24 @@ Commands:
48
47
  (defaults to hostname). Non-TTY
49
48
  environments must mount a pre-existing
50
49
  user-auth.json (plan §6.4).
50
+ On first run, auto-creates
51
+ ~/.botcord/daemon/config.json with a
52
+ default route (claude-code, $HOME) and
53
+ credential auto-discovery. Pass
54
+ --agent/--cwd to seed the file
55
+ (ignored once config exists).
51
56
  stop Stop the running daemon (SIGTERM)
52
57
  status Print daemon status (pid, agent)
53
58
  logs [-f] Print log tail (use -f to follow)
59
+ transcript enable|disable|status Toggle persistent transcript logging
60
+ transcript list --agent <ag_xxx> List rooms with transcripts for an agent
61
+ transcript tail --agent <ag_xxx> --room <rm_xxx> [--topic <tp>] [-n 50] [-f]
62
+ Tail recent transcript records (NDJSON)
63
+ transcript dump --agent <ag_xxx> --room <rm_xxx> [--topic <tp>]
64
+ Print full transcript file to stdout
65
+ transcript prune --agent <ag_xxx> [--older-than 30d] [--all]
66
+ Remove rotated transcript files (or all
67
+ for the agent with --all --yes)
54
68
  route add [match flags] --adapter <${ADAPTER_LIST}> --cwd <path>
55
69
  match flags (first match wins; at least one conversation/sender selector required):
56
70
  --conversation-id <rm_xxx> (alias: --room <rm_xxx>)
@@ -154,19 +168,32 @@ function pidAlive(pid) {
154
168
  return false;
155
169
  }
156
170
  }
157
- async function cmdInit(args) {
158
- // `--agent` is optional as of P1: when omitted, the daemon discovers
159
- // agent identities from `~/.botcord/credentials/*.json` at startup.
160
- // Every repeated `--agent ag_xxx` still pins an explicit id (the
161
- // canonical `agents: [...]` config shape).
162
- const agents = args.lists.agent ?? [];
163
- const cwd = typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
164
- log.info("cmd init", { agents, cwd });
165
- const cfg = initDefaultConfig(agents, cwd);
166
- saveConfig(cfg);
167
- console.log(`wrote ${CONFIG_FILE_PATH}`);
168
- if (agents.length === 0) {
169
- console.log("no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials at start");
171
+ /**
172
+ * Load the daemon config, auto-creating `~/.botcord/daemon/config.json`
173
+ * with sensible defaults on first run. `--agent` (repeated) pins explicit
174
+ * agent ids; `--cwd` overrides the defaultRoute working directory. Both
175
+ * are seed-only — they are ignored once a config already exists, since
176
+ * `route` and direct edits to `config.json` are the canonical way to
177
+ * change a configured daemon.
178
+ */
179
+ function loadOrInitConfig(args) {
180
+ try {
181
+ return loadConfig();
182
+ }
183
+ catch (err) {
184
+ const missing = err instanceof Error && err.code === CONFIG_MISSING;
185
+ if (!missing)
186
+ throw err;
187
+ const agents = args.lists.agent ?? [];
188
+ const cwd = typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
189
+ const cfg = initDefaultConfig(agents, cwd);
190
+ saveConfig(cfg);
191
+ log.info("auto-initialized daemon config", { agents, cwd, path: CONFIG_FILE_PATH });
192
+ console.log(`wrote default config to ${CONFIG_FILE_PATH}`);
193
+ if (agents.length === 0) {
194
+ console.log("no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials");
195
+ }
196
+ return cfg;
170
197
  }
171
198
  }
172
199
  /**
@@ -200,10 +227,15 @@ async function runDeviceCodeFlow(opts) {
200
227
  label: opts.label ?? null,
201
228
  });
202
229
  const dc = await requestDeviceCode(opts.hubUrl, opts.label ? { label: opts.label } : undefined);
203
- const display = dc.verificationUriComplete ?? dc.verificationUri;
230
+ const base = dc.verificationUriComplete ?? dc.verificationUri;
231
+ const display = appendNextParam(base, "/settings/daemons");
204
232
  console.log("");
205
- console.log(`Visit ${display}`);
206
- console.log(`Code: ${dc.userCode}`);
233
+ console.log("Open this URL in a browser where you're signed in to BotCord");
234
+ console.log("(typically your laptop, NOT this machine):");
235
+ console.log("");
236
+ console.log(` ${display}`);
237
+ console.log("");
238
+ console.log(`Or enter this code at ${dc.verificationUri}: ${dc.userCode}`);
207
239
  console.log("Waiting for authorization (Ctrl-C to abort)...");
208
240
  const expiresAt = Date.now() + dc.expiresIn * 1000;
209
241
  let intervalSec = dc.interval;
@@ -289,7 +321,29 @@ async function ensureUserAuthForStart(args) {
289
321
  return runDeviceCodeFlow({ hubUrl, label });
290
322
  }
291
323
  async function cmdStart(args) {
292
- const cfg = loadConfig();
324
+ let cfg = loadOrInitConfig(args);
325
+ if (openclawDiscoveryConfigEnabled(cfg)) {
326
+ try {
327
+ const found = await discoverLocalOpenclawGateways({
328
+ searchPaths: cfg.openclawDiscovery?.searchPaths,
329
+ defaultPorts: cfg.openclawDiscovery?.defaultPorts,
330
+ timeoutMs: 500,
331
+ });
332
+ const merged = mergeOpenclawGateways(cfg, found);
333
+ if (merged.changed) {
334
+ cfg = merged.cfg;
335
+ saveConfig(cfg);
336
+ log.info("openclaw discovery: gateways merged", {
337
+ added: merged.added.map((g) => ({ name: g.name, url: g.url })),
338
+ });
339
+ }
340
+ }
341
+ catch (err) {
342
+ log.warn("openclaw discovery failed; continuing", {
343
+ error: err instanceof Error ? err.message : String(err),
344
+ });
345
+ }
346
+ }
293
347
  // Foreground is now the default. --background (alias -d) detaches.
294
348
  // --foreground is still accepted (no-op) for backwards compatibility and
295
349
  // is also what the detached child re-execs itself with.
@@ -492,6 +546,222 @@ async function cmdLogs(args) {
492
546
  const lines = data.split("\n");
493
547
  console.log(lines.slice(-100).join("\n"));
494
548
  }
549
+ // ---------------------------------------------------------------------------
550
+ // transcript subcommands (design §5)
551
+ // ---------------------------------------------------------------------------
552
+ function transcriptStringFlag(args, name) {
553
+ const v = args.flags[name];
554
+ return typeof v === "string" && v.length > 0 ? v : null;
555
+ }
556
+ function parseDurationToMs(s) {
557
+ const m = /^(\d+)\s*([smhd])?$/.exec(s.trim());
558
+ if (!m)
559
+ return null;
560
+ const n = Number(m[1]);
561
+ const unit = m[2] ?? "d";
562
+ const mult = {
563
+ s: 1000,
564
+ m: 60_000,
565
+ h: 3_600_000,
566
+ d: 86_400_000,
567
+ };
568
+ return n * mult[unit];
569
+ }
570
+ async function cmdTranscript(args) {
571
+ switch (args.sub) {
572
+ case "enable":
573
+ return cmdTranscriptToggle(true);
574
+ case "disable":
575
+ return cmdTranscriptToggle(false);
576
+ case "status":
577
+ return cmdTranscriptStatus();
578
+ case "list":
579
+ return cmdTranscriptList(args);
580
+ case "tail":
581
+ return cmdTranscriptTail(args);
582
+ case "dump":
583
+ return cmdTranscriptDump(args);
584
+ case "prune":
585
+ return cmdTranscriptPrune(args);
586
+ default:
587
+ console.error("usage: botcord-daemon transcript <enable|disable|status|list|tail|dump|prune>");
588
+ process.exit(1);
589
+ }
590
+ }
591
+ function cmdTranscriptToggle(enable) {
592
+ let cfg;
593
+ try {
594
+ cfg = loadConfig();
595
+ }
596
+ catch (err) {
597
+ const e = err;
598
+ if (e.code === CONFIG_MISSING) {
599
+ console.error(`daemon config not found — run \`botcord-daemon start\` once to initialize, then retry`);
600
+ process.exit(1);
601
+ }
602
+ throw err;
603
+ }
604
+ cfg.transcript = { ...(cfg.transcript ?? {}), enabled: enable };
605
+ saveConfig(cfg);
606
+ console.log(`transcript persistence ${enable ? "enabled" : "disabled"} (next daemon start)`);
607
+ }
608
+ function cmdTranscriptStatus() {
609
+ let cfg = null;
610
+ try {
611
+ cfg = loadConfig();
612
+ }
613
+ catch (err) {
614
+ const e = err;
615
+ if (e.code !== CONFIG_MISSING)
616
+ throw err;
617
+ }
618
+ const configEnabled = cfg?.transcript?.enabled === true;
619
+ const env = process.env.BOTCORD_TRANSCRIPT;
620
+ const effective = resolveTranscriptEnabled(env, configEnabled);
621
+ let source;
622
+ if (env === "1" || env === "0")
623
+ source = `env BOTCORD_TRANSCRIPT=${env}`;
624
+ else if (configEnabled)
625
+ source = "config (transcript.enabled=true)";
626
+ else
627
+ source = "default-off";
628
+ console.log(`enabled: ${effective}`);
629
+ console.log(`source: ${source}`);
630
+ console.log(`root: ${defaultTranscriptRoot()}`);
631
+ }
632
+ function cmdTranscriptList(args) {
633
+ const agent = transcriptStringFlag(args, "agent");
634
+ if (!agent) {
635
+ console.error("transcript list requires --agent <ag_xxx>");
636
+ process.exit(1);
637
+ }
638
+ const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
639
+ if (!existsSync(root)) {
640
+ return; // no rooms → empty output
641
+ }
642
+ for (const entry of readdirSync(root)) {
643
+ const dir = path.join(root, entry);
644
+ let st;
645
+ try {
646
+ st = statSync(dir);
647
+ }
648
+ catch {
649
+ continue;
650
+ }
651
+ if (!st.isDirectory())
652
+ continue;
653
+ console.log(entry);
654
+ }
655
+ }
656
+ function cmdTranscriptTail(args) {
657
+ const agent = transcriptStringFlag(args, "agent");
658
+ const room = transcriptStringFlag(args, "room");
659
+ if (!agent || !room) {
660
+ console.error("transcript tail requires --agent <ag_xxx> --room <rm_xxx>");
661
+ process.exit(1);
662
+ }
663
+ const topic = transcriptStringFlag(args, "topic");
664
+ const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
665
+ if (!existsSync(file)) {
666
+ console.error(`no transcript at ${file}`);
667
+ process.exit(1);
668
+ }
669
+ const follow = args.flags.f === true || args.flags.follow === true;
670
+ const nFlag = transcriptStringFlag(args, "n");
671
+ const n = nFlag && /^\d+$/.test(nFlag) ? Number(nFlag) : 50;
672
+ if (follow) {
673
+ const child = spawn("tail", ["-n", String(n), "-f", file], { stdio: "inherit" });
674
+ process.on("SIGINT", () => child.kill("SIGINT"));
675
+ return new Promise((resolve) => {
676
+ child.on("close", () => resolve());
677
+ });
678
+ }
679
+ const data = readFileSync(file, "utf8");
680
+ const lines = data.split("\n").filter((l) => l.length > 0);
681
+ console.log(lines.slice(-n).join("\n"));
682
+ }
683
+ function cmdTranscriptDump(args) {
684
+ const agent = transcriptStringFlag(args, "agent");
685
+ const room = transcriptStringFlag(args, "room");
686
+ if (!agent || !room) {
687
+ console.error("transcript dump requires --agent <ag_xxx> --room <rm_xxx>");
688
+ process.exit(1);
689
+ }
690
+ const topic = transcriptStringFlag(args, "topic");
691
+ const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
692
+ if (!existsSync(file)) {
693
+ console.error(`no transcript at ${file}`);
694
+ process.exit(1);
695
+ }
696
+ process.stdout.write(readFileSync(file, "utf8"));
697
+ }
698
+ function cmdTranscriptPrune(args) {
699
+ const agent = transcriptStringFlag(args, "agent");
700
+ if (!agent) {
701
+ console.error("transcript prune requires --agent <ag_xxx>");
702
+ process.exit(1);
703
+ }
704
+ const all = args.flags.all === true;
705
+ const olderThanFlag = transcriptStringFlag(args, "older-than");
706
+ const yes = args.flags.yes === true;
707
+ const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
708
+ if (!existsSync(root))
709
+ return;
710
+ if (all) {
711
+ if (!yes) {
712
+ console.error(`transcript prune --all will delete every transcript under ${root}; rerun with --yes to confirm`);
713
+ process.exit(1);
714
+ }
715
+ rmSync(root, { recursive: true, force: true });
716
+ console.log(`removed ${root}`);
717
+ return;
718
+ }
719
+ // Default and --older-than: prune rotated files only (the "{topic}.STAMP.jsonl" form).
720
+ // Active files (`{topic}.jsonl` / `_default.jsonl`) are never touched.
721
+ const cutoffMs = olderThanFlag ? parseDurationToMs(olderThanFlag) : null;
722
+ if (olderThanFlag && cutoffMs === null) {
723
+ console.error(`transcript prune --older-than: invalid duration "${olderThanFlag}" (use 30d / 12h / 30m / 60s)`);
724
+ process.exit(1);
725
+ }
726
+ const cutoff = cutoffMs !== null ? Date.now() - cutoffMs : null;
727
+ let removed = 0;
728
+ for (const roomEntry of readdirSync(root)) {
729
+ const dir = path.join(root, roomEntry);
730
+ let st;
731
+ try {
732
+ st = statSync(dir);
733
+ }
734
+ catch {
735
+ continue;
736
+ }
737
+ if (!st.isDirectory())
738
+ continue;
739
+ for (const f of readdirSync(dir)) {
740
+ // rotated files: <topic>.<YYYYMMDD-HHMMSS>.jsonl — must contain a stamp segment
741
+ if (!/^.+\.\d{8}-\d{6}\.jsonl$/.test(f))
742
+ continue;
743
+ const full = path.join(dir, f);
744
+ if (cutoff !== null) {
745
+ try {
746
+ const fst = statSync(full);
747
+ if (fst.mtimeMs >= cutoff)
748
+ continue;
749
+ }
750
+ catch {
751
+ continue;
752
+ }
753
+ }
754
+ try {
755
+ unlinkSync(full);
756
+ removed += 1;
757
+ }
758
+ catch (err) {
759
+ console.error(`failed to remove ${full}: ${err instanceof Error ? err.message : String(err)}`);
760
+ }
761
+ }
762
+ }
763
+ console.log(`removed ${removed} rotated transcript file(s)`);
764
+ }
495
765
  function formatRouteMatch(m) {
496
766
  const parts = [];
497
767
  if (m.channel)
@@ -793,13 +1063,25 @@ async function cmdDoctor(args) {
793
1063
  // Doctor should not hard-fail when no config exists yet; channel probes
794
1064
  // simply produce an empty list in that case.
795
1065
  let channels = [];
1066
+ let cfgForEndpoints = null;
796
1067
  try {
797
1068
  const cfg = loadConfig();
1069
+ cfgForEndpoints = cfg;
798
1070
  channels = channelsFromDaemonConfig(cfg);
799
1071
  }
800
1072
  catch {
801
1073
  channels = [];
802
1074
  }
1075
+ if (cfgForEndpoints?.openclawGateways && cfgForEndpoints.openclawGateways.length > 0) {
1076
+ const { collectRuntimeSnapshotAsync } = await import("./provision.js");
1077
+ const snap = await collectRuntimeSnapshotAsync({ cfg: cfgForEndpoints });
1078
+ const byId = new Map(snap.runtimes.map((r) => [r.id, r]));
1079
+ for (const e of entries) {
1080
+ const r = byId.get(e.id);
1081
+ if (r?.endpoints)
1082
+ e.endpoints = r.endpoints;
1083
+ }
1084
+ }
803
1085
  const credentialsPath = (accountId) => path.join(homedir(), ".botcord", "credentials", `${accountId}.json`);
804
1086
  const input = await runDoctor(entries, channels, {
805
1087
  credentialsPath,
@@ -821,9 +1103,6 @@ async function main() {
821
1103
  }
822
1104
  try {
823
1105
  switch (args.cmd) {
824
- case "init":
825
- await cmdInit(args);
826
- break;
827
1106
  case "start":
828
1107
  await cmdStart(args);
829
1108
  break;
@@ -836,6 +1115,9 @@ async function main() {
836
1115
  case "logs":
837
1116
  await cmdLogs(args);
838
1117
  break;
1118
+ case "transcript":
1119
+ await cmdTranscript(args);
1120
+ break;
839
1121
  case "route":
840
1122
  await cmdRoute(args);
841
1123
  break;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Mention text-fallback (design §4.2). The Hub's `messages.mentioned` flag is
3
+ * sender-supplied and therefore not trustworthy on its own; we OR it with a
4
+ * local scan for `@<display_name>` or `@<agent_id>` so an agent that the
5
+ * sender forgot (or refused) to mark mentioned still wakes when addressed.
6
+ *
7
+ * Kept tiny and synchronous — runs on every inbound message. Both inputs are
8
+ * normalized to lowercase to keep the match case-insensitive.
9
+ */
10
+ export interface MentionTargets {
11
+ /** Daemon-known agent id (e.g. `ag_xxx`). Always included when present. */
12
+ agentId?: string;
13
+ /** Display name from the agent's credentials. */
14
+ displayName?: string;
15
+ }
16
+ /**
17
+ * Return `true` when `text` contains an `@`-prefixed mention of `agentId`
18
+ * or `displayName`. Matches a literal `@` followed by the target — both the
19
+ * `@` and the target are required because plain occurrences of the
20
+ * displayName in conversation should NOT count as a mention.
21
+ */
22
+ export declare function scanMention(text: string | undefined, targets: MentionTargets): boolean;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Mention text-fallback (design §4.2). The Hub's `messages.mentioned` flag is
3
+ * sender-supplied and therefore not trustworthy on its own; we OR it with a
4
+ * local scan for `@<display_name>` or `@<agent_id>` so an agent that the
5
+ * sender forgot (or refused) to mark mentioned still wakes when addressed.
6
+ *
7
+ * Kept tiny and synchronous — runs on every inbound message. Both inputs are
8
+ * normalized to lowercase to keep the match case-insensitive.
9
+ */
10
+ /**
11
+ * Return `true` when `text` contains an `@`-prefixed mention of `agentId`
12
+ * or `displayName`. Matches a literal `@` followed by the target — both the
13
+ * `@` and the target are required because plain occurrences of the
14
+ * displayName in conversation should NOT count as a mention.
15
+ */
16
+ export function scanMention(text, targets) {
17
+ if (!text)
18
+ return false;
19
+ const lower = text.toLowerCase();
20
+ const candidates = [];
21
+ if (targets.agentId)
22
+ candidates.push(targets.agentId.toLowerCase());
23
+ if (targets.displayName) {
24
+ const trimmed = targets.displayName.trim();
25
+ if (trimmed)
26
+ candidates.push(trimmed.toLowerCase());
27
+ }
28
+ for (const c of candidates) {
29
+ if (!c)
30
+ continue;
31
+ if (lower.includes("@" + c))
32
+ return true;
33
+ }
34
+ return false;
35
+ }
@@ -0,0 +1,28 @@
1
+ import type { DaemonConfig, OpenclawGatewayProfile } from "./config.js";
2
+ import { type WsEndpointProbeFn } from "./provision.js";
3
+ export type DiscoveredOpenclawGatewaySource = "config-file" | "env" | "default-port";
4
+ export interface DiscoveredOpenclawGateway {
5
+ name: string;
6
+ url: string;
7
+ token?: string;
8
+ tokenFile?: string;
9
+ source: DiscoveredOpenclawGatewaySource;
10
+ }
11
+ export interface OpenclawGatewayDiscoveryOptions {
12
+ searchPaths?: string[];
13
+ defaultPorts?: number[];
14
+ probe?: WsEndpointProbeFn;
15
+ timeoutMs?: number;
16
+ env?: NodeJS.ProcessEnv;
17
+ }
18
+ export interface MergeOpenclawGatewayResult {
19
+ cfg: DaemonConfig;
20
+ changed: boolean;
21
+ added: OpenclawGatewayProfile[];
22
+ }
23
+ export declare function discoverLocalOpenclawGateways(opts?: OpenclawGatewayDiscoveryOptions): Promise<DiscoveredOpenclawGateway[]>;
24
+ export declare function mergeOpenclawGateways(cfg: DaemonConfig, found: DiscoveredOpenclawGateway[]): MergeOpenclawGatewayResult;
25
+ export declare function defaultOpenclawDiscoverySearchPaths(): string[];
26
+ export declare function defaultOpenclawDiscoveryPorts(): number[];
27
+ export declare function openclawDiscoveryConfigEnabled(cfg: DaemonConfig): boolean;
28
+ export declare function openclawAutoProvisionEnabled(cfg: DaemonConfig): boolean;