@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/src/index.ts CHANGED
@@ -1,6 +1,6 @@
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
6
  import {
@@ -11,11 +11,18 @@ import {
11
11
  PID_PATH,
12
12
  SNAPSHOT_PATH,
13
13
  CONFIG_FILE_PATH,
14
+ CONFIG_MISSING,
14
15
  type DaemonConfig,
15
16
  type RouteRule,
16
17
  type RouteRuleMatch,
17
18
  } from "./config.js";
18
19
  import { resolveBootAgents } from "./agent-discovery.js";
20
+ import {
21
+ defaultTranscriptRoot,
22
+ resolveTranscriptEnabled,
23
+ transcriptAgentRoot,
24
+ transcriptFilePath,
25
+ } from "./gateway/index.js";
19
26
  import { startDaemon } from "./daemon.js";
20
27
  import { log, LOG_FILE_PATH } from "./log.js";
21
28
  import { detectRuntimes, getAdapterModule, listAdapterIds } from "./adapters/runtimes.js";
@@ -34,6 +41,7 @@ import {
34
41
  type UserAuthRecord,
35
42
  } from "./user-auth.js";
36
43
  import { renderStatus, type StatusRenderInput } from "./status-render.js";
44
+ import { appendNextParam } from "./url-utils.js";
37
45
  import {
38
46
  channelsFromDaemonConfig,
39
47
  defaultHttpFetcher,
@@ -49,6 +57,11 @@ import {
49
57
  updateWorkingMemory,
50
58
  DEFAULT_SECTION,
51
59
  } from "./working-memory.js";
60
+ import {
61
+ discoverLocalOpenclawGateways,
62
+ mergeOpenclawGateways,
63
+ openclawDiscoveryConfigEnabled,
64
+ } from "./openclaw-discovery.js";
52
65
 
53
66
  const ADAPTER_LIST = listAdapterIds().join("|");
54
67
 
@@ -69,12 +82,8 @@ const HELP = `botcord-daemon — BotCord local daemon
69
82
  Usage: botcord-daemon <command> [options]
70
83
 
71
84
  Commands:
72
- init [--agent <ag_xxx> ...] [--cwd <path>]
73
- Create ~/.botcord/daemon/config.json.
74
- Without --agent, the daemon discovers
75
- identities from ~/.botcord/credentials
76
- at startup (repeat --agent to pin).
77
85
  start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
86
+ [--agent <ag_xxx> ...] [--cwd <path>]
78
87
  Start the daemon in the foreground by
79
88
  default. Pass --background (alias -d)
80
89
  to detach and return to the shell.
@@ -88,9 +97,24 @@ Commands:
88
97
  (defaults to hostname). Non-TTY
89
98
  environments must mount a pre-existing
90
99
  user-auth.json (plan §6.4).
100
+ On first run, auto-creates
101
+ ~/.botcord/daemon/config.json with a
102
+ default route (claude-code, $HOME) and
103
+ credential auto-discovery. Pass
104
+ --agent/--cwd to seed the file
105
+ (ignored once config exists).
91
106
  stop Stop the running daemon (SIGTERM)
92
107
  status Print daemon status (pid, agent)
93
108
  logs [-f] Print log tail (use -f to follow)
109
+ transcript enable|disable|status Toggle persistent transcript logging
110
+ transcript list --agent <ag_xxx> List rooms with transcripts for an agent
111
+ transcript tail --agent <ag_xxx> --room <rm_xxx> [--topic <tp>] [-n 50] [-f]
112
+ Tail recent transcript records (NDJSON)
113
+ transcript dump --agent <ag_xxx> --room <rm_xxx> [--topic <tp>]
114
+ Print full transcript file to stdout
115
+ transcript prune --agent <ag_xxx> [--older-than 30d] [--all]
116
+ Remove rotated transcript files (or all
117
+ for the agent with --all --yes)
94
118
  route add [match flags] --adapter <${ADAPTER_LIST}> --cwd <path>
95
119
  match flags (first match wins; at least one conversation/sender selector required):
96
120
  --conversation-id <rm_xxx> (alias: --room <rm_xxx>)
@@ -203,22 +227,33 @@ function pidAlive(pid: number): boolean {
203
227
  }
204
228
  }
205
229
 
206
- async function cmdInit(args: ParsedArgs): Promise<void> {
207
- // `--agent` is optional as of P1: when omitted, the daemon discovers
208
- // agent identities from `~/.botcord/credentials/*.json` at startup.
209
- // Every repeated `--agent ag_xxx` still pins an explicit id (the
210
- // canonical `agents: [...]` config shape).
211
- const agents = args.lists.agent ?? [];
212
- const cwd =
213
- typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
214
- log.info("cmd init", { agents, cwd });
215
- const cfg = initDefaultConfig(agents, cwd);
216
- saveConfig(cfg);
217
- console.log(`wrote ${CONFIG_FILE_PATH}`);
218
- if (agents.length === 0) {
219
- console.log(
220
- "no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials at start",
221
- );
230
+ /**
231
+ * Load the daemon config, auto-creating `~/.botcord/daemon/config.json`
232
+ * with sensible defaults on first run. `--agent` (repeated) pins explicit
233
+ * agent ids; `--cwd` overrides the defaultRoute working directory. Both
234
+ * are seed-only — they are ignored once a config already exists, since
235
+ * `route` and direct edits to `config.json` are the canonical way to
236
+ * change a configured daemon.
237
+ */
238
+ function loadOrInitConfig(args: ParsedArgs): DaemonConfig {
239
+ try {
240
+ return loadConfig();
241
+ } catch (err) {
242
+ const missing = err instanceof Error && (err as { code?: string }).code === CONFIG_MISSING;
243
+ if (!missing) throw err;
244
+ const agents = args.lists.agent ?? [];
245
+ const cwd =
246
+ typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
247
+ const cfg = initDefaultConfig(agents, cwd);
248
+ saveConfig(cfg);
249
+ log.info("auto-initialized daemon config", { agents, cwd, path: CONFIG_FILE_PATH });
250
+ console.log(`wrote default config to ${CONFIG_FILE_PATH}`);
251
+ if (agents.length === 0) {
252
+ console.log(
253
+ "no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials",
254
+ );
255
+ }
256
+ return cfg;
222
257
  }
223
258
  }
224
259
 
@@ -260,10 +295,15 @@ async function runDeviceCodeFlow(opts: {
260
295
  opts.hubUrl,
261
296
  opts.label ? { label: opts.label } : undefined,
262
297
  );
263
- const display = dc.verificationUriComplete ?? dc.verificationUri;
298
+ const base = dc.verificationUriComplete ?? dc.verificationUri;
299
+ const display = appendNextParam(base, "/settings/daemons");
264
300
  console.log("");
265
- console.log(`Visit ${display}`);
266
- console.log(`Code: ${dc.userCode}`);
301
+ console.log("Open this URL in a browser where you're signed in to BotCord");
302
+ console.log("(typically your laptop, NOT this machine):");
303
+ console.log("");
304
+ console.log(` ${display}`);
305
+ console.log("");
306
+ console.log(`Or enter this code at ${dc.verificationUri}: ${dc.userCode}`);
267
307
  console.log("Waiting for authorization (Ctrl-C to abort)...");
268
308
 
269
309
  const expiresAt = Date.now() + dc.expiresIn * 1000;
@@ -367,7 +407,28 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
367
407
  }
368
408
 
369
409
  async function cmdStart(args: ParsedArgs): Promise<void> {
370
- const cfg = loadConfig();
410
+ let cfg = loadOrInitConfig(args);
411
+ if (openclawDiscoveryConfigEnabled(cfg)) {
412
+ try {
413
+ const found = await discoverLocalOpenclawGateways({
414
+ searchPaths: cfg.openclawDiscovery?.searchPaths,
415
+ defaultPorts: cfg.openclawDiscovery?.defaultPorts,
416
+ timeoutMs: 500,
417
+ });
418
+ const merged = mergeOpenclawGateways(cfg, found);
419
+ if (merged.changed) {
420
+ cfg = merged.cfg;
421
+ saveConfig(cfg);
422
+ log.info("openclaw discovery: gateways merged", {
423
+ added: merged.added.map((g) => ({ name: g.name, url: g.url })),
424
+ });
425
+ }
426
+ } catch (err) {
427
+ log.warn("openclaw discovery failed; continuing", {
428
+ error: err instanceof Error ? err.message : String(err),
429
+ });
430
+ }
431
+ }
371
432
  // Foreground is now the default. --background (alias -d) detaches.
372
433
  // --foreground is still accepted (no-op) for backwards compatibility and
373
434
  // is also what the detached child re-execs itself with.
@@ -583,6 +644,225 @@ async function cmdLogs(args: ParsedArgs): Promise<void> {
583
644
  console.log(lines.slice(-100).join("\n"));
584
645
  }
585
646
 
647
+ // ---------------------------------------------------------------------------
648
+ // transcript subcommands (design §5)
649
+ // ---------------------------------------------------------------------------
650
+
651
+ function transcriptStringFlag(args: ParsedArgs, name: string): string | null {
652
+ const v = args.flags[name];
653
+ return typeof v === "string" && v.length > 0 ? v : null;
654
+ }
655
+
656
+ function parseDurationToMs(s: string): number | null {
657
+ const m = /^(\d+)\s*([smhd])?$/.exec(s.trim());
658
+ if (!m) return null;
659
+ const n = Number(m[1]);
660
+ const unit = m[2] ?? "d";
661
+ const mult: Record<string, number> = {
662
+ s: 1000,
663
+ m: 60_000,
664
+ h: 3_600_000,
665
+ d: 86_400_000,
666
+ };
667
+ return n * mult[unit];
668
+ }
669
+
670
+ async function cmdTranscript(args: ParsedArgs): Promise<void> {
671
+ switch (args.sub) {
672
+ case "enable":
673
+ return cmdTranscriptToggle(true);
674
+ case "disable":
675
+ return cmdTranscriptToggle(false);
676
+ case "status":
677
+ return cmdTranscriptStatus();
678
+ case "list":
679
+ return cmdTranscriptList(args);
680
+ case "tail":
681
+ return cmdTranscriptTail(args);
682
+ case "dump":
683
+ return cmdTranscriptDump(args);
684
+ case "prune":
685
+ return cmdTranscriptPrune(args);
686
+ default:
687
+ console.error("usage: botcord-daemon transcript <enable|disable|status|list|tail|dump|prune>");
688
+ process.exit(1);
689
+ }
690
+ }
691
+
692
+ function cmdTranscriptToggle(enable: boolean): void {
693
+ let cfg: DaemonConfig;
694
+ try {
695
+ cfg = loadConfig();
696
+ } catch (err) {
697
+ const e = err as Error & { code?: string };
698
+ if (e.code === CONFIG_MISSING) {
699
+ console.error(
700
+ `daemon config not found — run \`botcord-daemon start\` once to initialize, then retry`,
701
+ );
702
+ process.exit(1);
703
+ }
704
+ throw err;
705
+ }
706
+ cfg.transcript = { ...(cfg.transcript ?? {}), enabled: enable };
707
+ saveConfig(cfg);
708
+ console.log(
709
+ `transcript persistence ${enable ? "enabled" : "disabled"} (next daemon start)`,
710
+ );
711
+ }
712
+
713
+ function cmdTranscriptStatus(): void {
714
+ let cfg: DaemonConfig | null = null;
715
+ try {
716
+ cfg = loadConfig();
717
+ } catch (err) {
718
+ const e = err as Error & { code?: string };
719
+ if (e.code !== CONFIG_MISSING) throw err;
720
+ }
721
+ const configEnabled = cfg?.transcript?.enabled === true;
722
+ const env = process.env.BOTCORD_TRANSCRIPT;
723
+ const effective = resolveTranscriptEnabled(env, configEnabled);
724
+ let source: string;
725
+ if (env === "1" || env === "0") source = `env BOTCORD_TRANSCRIPT=${env}`;
726
+ else if (configEnabled) source = "config (transcript.enabled=true)";
727
+ else source = "default-off";
728
+ console.log(`enabled: ${effective}`);
729
+ console.log(`source: ${source}`);
730
+ console.log(`root: ${defaultTranscriptRoot()}`);
731
+ }
732
+
733
+ function cmdTranscriptList(args: ParsedArgs): void {
734
+ const agent = transcriptStringFlag(args, "agent");
735
+ if (!agent) {
736
+ console.error("transcript list requires --agent <ag_xxx>");
737
+ process.exit(1);
738
+ }
739
+ const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
740
+ if (!existsSync(root)) {
741
+ return; // no rooms → empty output
742
+ }
743
+ for (const entry of readdirSync(root)) {
744
+ const dir = path.join(root, entry);
745
+ let st;
746
+ try {
747
+ st = statSync(dir);
748
+ } catch {
749
+ continue;
750
+ }
751
+ if (!st.isDirectory()) continue;
752
+ console.log(entry);
753
+ }
754
+ }
755
+
756
+ function cmdTranscriptTail(args: ParsedArgs): Promise<void> | void {
757
+ const agent = transcriptStringFlag(args, "agent");
758
+ const room = transcriptStringFlag(args, "room");
759
+ if (!agent || !room) {
760
+ console.error("transcript tail requires --agent <ag_xxx> --room <rm_xxx>");
761
+ process.exit(1);
762
+ }
763
+ const topic = transcriptStringFlag(args, "topic");
764
+ const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
765
+ if (!existsSync(file)) {
766
+ console.error(`no transcript at ${file}`);
767
+ process.exit(1);
768
+ }
769
+ const follow = args.flags.f === true || args.flags.follow === true;
770
+ const nFlag = transcriptStringFlag(args, "n");
771
+ const n = nFlag && /^\d+$/.test(nFlag) ? Number(nFlag) : 50;
772
+ if (follow) {
773
+ const child = spawn("tail", ["-n", String(n), "-f", file], { stdio: "inherit" });
774
+ process.on("SIGINT", () => child.kill("SIGINT"));
775
+ return new Promise<void>((resolve) => {
776
+ child.on("close", () => resolve());
777
+ });
778
+ }
779
+ const data = readFileSync(file, "utf8");
780
+ const lines = data.split("\n").filter((l) => l.length > 0);
781
+ console.log(lines.slice(-n).join("\n"));
782
+ }
783
+
784
+ function cmdTranscriptDump(args: ParsedArgs): void {
785
+ const agent = transcriptStringFlag(args, "agent");
786
+ const room = transcriptStringFlag(args, "room");
787
+ if (!agent || !room) {
788
+ console.error("transcript dump requires --agent <ag_xxx> --room <rm_xxx>");
789
+ process.exit(1);
790
+ }
791
+ const topic = transcriptStringFlag(args, "topic");
792
+ const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
793
+ if (!existsSync(file)) {
794
+ console.error(`no transcript at ${file}`);
795
+ process.exit(1);
796
+ }
797
+ process.stdout.write(readFileSync(file, "utf8"));
798
+ }
799
+
800
+ function cmdTranscriptPrune(args: ParsedArgs): void {
801
+ const agent = transcriptStringFlag(args, "agent");
802
+ if (!agent) {
803
+ console.error("transcript prune requires --agent <ag_xxx>");
804
+ process.exit(1);
805
+ }
806
+ const all = args.flags.all === true;
807
+ const olderThanFlag = transcriptStringFlag(args, "older-than");
808
+ const yes = args.flags.yes === true;
809
+ const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
810
+ if (!existsSync(root)) return;
811
+
812
+ if (all) {
813
+ if (!yes) {
814
+ console.error(
815
+ `transcript prune --all will delete every transcript under ${root}; rerun with --yes to confirm`,
816
+ );
817
+ process.exit(1);
818
+ }
819
+ rmSync(root, { recursive: true, force: true });
820
+ console.log(`removed ${root}`);
821
+ return;
822
+ }
823
+
824
+ // Default and --older-than: prune rotated files only (the "{topic}.STAMP.jsonl" form).
825
+ // Active files (`{topic}.jsonl` / `_default.jsonl`) are never touched.
826
+ const cutoffMs = olderThanFlag ? parseDurationToMs(olderThanFlag) : null;
827
+ if (olderThanFlag && cutoffMs === null) {
828
+ console.error(`transcript prune --older-than: invalid duration "${olderThanFlag}" (use 30d / 12h / 30m / 60s)`);
829
+ process.exit(1);
830
+ }
831
+ const cutoff = cutoffMs !== null ? Date.now() - cutoffMs : null;
832
+
833
+ let removed = 0;
834
+ for (const roomEntry of readdirSync(root)) {
835
+ const dir = path.join(root, roomEntry);
836
+ let st;
837
+ try {
838
+ st = statSync(dir);
839
+ } catch {
840
+ continue;
841
+ }
842
+ if (!st.isDirectory()) continue;
843
+ for (const f of readdirSync(dir)) {
844
+ // rotated files: <topic>.<YYYYMMDD-HHMMSS>.jsonl — must contain a stamp segment
845
+ if (!/^.+\.\d{8}-\d{6}\.jsonl$/.test(f)) continue;
846
+ const full = path.join(dir, f);
847
+ if (cutoff !== null) {
848
+ try {
849
+ const fst = statSync(full);
850
+ if (fst.mtimeMs >= cutoff) continue;
851
+ } catch {
852
+ continue;
853
+ }
854
+ }
855
+ try {
856
+ unlinkSync(full);
857
+ removed += 1;
858
+ } catch (err) {
859
+ console.error(`failed to remove ${full}: ${err instanceof Error ? err.message : String(err)}`);
860
+ }
861
+ }
862
+ }
863
+ console.log(`removed ${removed} rotated transcript file(s)`);
864
+ }
865
+
586
866
  function formatRouteMatch(m: RouteRuleMatch): string {
587
867
  const parts: string[] = [];
588
868
  if (m.channel) parts.push(`channel=${m.channel}`);
@@ -893,16 +1173,27 @@ const fsFileReader: DoctorFileReader = {
893
1173
  };
894
1174
 
895
1175
  async function cmdDoctor(args: ParsedArgs): Promise<void> {
896
- const entries = detectRuntimes();
1176
+ const entries: import("./doctor.js").DoctorRuntimeEntry[] = detectRuntimes();
897
1177
  // Doctor should not hard-fail when no config exists yet; channel probes
898
1178
  // simply produce an empty list in that case.
899
1179
  let channels: ReturnType<typeof channelsFromDaemonConfig> = [];
1180
+ let cfgForEndpoints: import("./config.js").DaemonConfig | null = null;
900
1181
  try {
901
1182
  const cfg = loadConfig();
1183
+ cfgForEndpoints = cfg;
902
1184
  channels = channelsFromDaemonConfig(cfg);
903
1185
  } catch {
904
1186
  channels = [];
905
1187
  }
1188
+ if (cfgForEndpoints?.openclawGateways && cfgForEndpoints.openclawGateways.length > 0) {
1189
+ const { collectRuntimeSnapshotAsync } = await import("./provision.js");
1190
+ const snap = await collectRuntimeSnapshotAsync({ cfg: cfgForEndpoints });
1191
+ const byId = new Map(snap.runtimes.map((r) => [r.id, r]));
1192
+ for (const e of entries) {
1193
+ const r = byId.get(e.id);
1194
+ if (r?.endpoints) e.endpoints = r.endpoints;
1195
+ }
1196
+ }
906
1197
 
907
1198
  const credentialsPath = (accountId: string) =>
908
1199
  path.join(homedir(), ".botcord", "credentials", `${accountId}.json`);
@@ -929,9 +1220,6 @@ async function main(): Promise<void> {
929
1220
  }
930
1221
  try {
931
1222
  switch (args.cmd) {
932
- case "init":
933
- await cmdInit(args);
934
- break;
935
1223
  case "start":
936
1224
  await cmdStart(args);
937
1225
  break;
@@ -944,6 +1232,9 @@ async function main(): Promise<void> {
944
1232
  case "logs":
945
1233
  await cmdLogs(args);
946
1234
  break;
1235
+ case "transcript":
1236
+ await cmdTranscript(args);
1237
+ break;
947
1238
  case "route":
948
1239
  await cmdRoute(args);
949
1240
  break;
@@ -0,0 +1,38 @@
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
+ export interface MentionTargets {
12
+ /** Daemon-known agent id (e.g. `ag_xxx`). Always included when present. */
13
+ agentId?: string;
14
+ /** Display name from the agent's credentials. */
15
+ displayName?: string;
16
+ }
17
+
18
+ /**
19
+ * Return `true` when `text` contains an `@`-prefixed mention of `agentId`
20
+ * or `displayName`. Matches a literal `@` followed by the target — both the
21
+ * `@` and the target are required because plain occurrences of the
22
+ * displayName in conversation should NOT count as a mention.
23
+ */
24
+ export function scanMention(text: string | undefined, targets: MentionTargets): boolean {
25
+ if (!text) return false;
26
+ const lower = text.toLowerCase();
27
+ const candidates: string[] = [];
28
+ if (targets.agentId) candidates.push(targets.agentId.toLowerCase());
29
+ if (targets.displayName) {
30
+ const trimmed = targets.displayName.trim();
31
+ if (trimmed) candidates.push(trimmed.toLowerCase());
32
+ }
33
+ for (const c of candidates) {
34
+ if (!c) continue;
35
+ if (lower.includes("@" + c)) return true;
36
+ }
37
+ return false;
38
+ }