@botcord/daemon 0.2.5 → 0.2.6

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 (84) 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 +49 -1
  6. package/dist/config.js +57 -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 +52 -5
  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 +286 -27
  40. package/dist/mention-scan.d.ts +22 -0
  41. package/dist/mention-scan.js +35 -0
  42. package/dist/provision.d.ts +72 -1
  43. package/dist/provision.js +370 -7
  44. package/dist/system-context.d.ts +5 -4
  45. package/dist/system-context.js +35 -5
  46. package/dist/url-utils.d.ts +9 -0
  47. package/dist/url-utils.js +18 -0
  48. package/package.json +2 -1
  49. package/src/__tests__/agent-workspace.test.ts +93 -0
  50. package/src/__tests__/daemon-config-map.test.ts +79 -0
  51. package/src/__tests__/openclaw-acp.test.ts +234 -0
  52. package/src/__tests__/policy-resolver.test.ts +124 -0
  53. package/src/__tests__/policy-updated-handler.test.ts +144 -0
  54. package/src/__tests__/provision.test.ts +160 -0
  55. package/src/__tests__/system-context.test.ts +52 -0
  56. package/src/__tests__/url-utils.test.ts +37 -0
  57. package/src/agent-discovery.ts +8 -0
  58. package/src/agent-workspace.ts +173 -7
  59. package/src/config.ts +132 -4
  60. package/src/daemon-config-map.ts +154 -9
  61. package/src/daemon.ts +66 -5
  62. package/src/doctor.ts +49 -2
  63. package/src/gateway/__tests__/dispatcher.test.ts +65 -0
  64. package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
  65. package/src/gateway/__tests__/transcript.test.ts +496 -0
  66. package/src/gateway/cli-resolver.ts +92 -0
  67. package/src/gateway/dispatcher.ts +394 -26
  68. package/src/gateway/gateway.ts +46 -0
  69. package/src/gateway/index.ts +25 -0
  70. package/src/gateway/policy-resolver.ts +171 -0
  71. package/src/gateway/runtimes/acp-stream.ts +535 -0
  72. package/src/gateway/runtimes/codex.ts +7 -0
  73. package/src/gateway/runtimes/hermes-agent.ts +206 -0
  74. package/src/gateway/runtimes/ndjson-stream.ts +16 -3
  75. package/src/gateway/runtimes/openclaw-acp.ts +606 -0
  76. package/src/gateway/runtimes/registry.ts +24 -0
  77. package/src/gateway/transcript-paths.ts +145 -0
  78. package/src/gateway/transcript.ts +300 -0
  79. package/src/gateway/types.ts +32 -0
  80. package/src/index.ts +295 -30
  81. package/src/mention-scan.ts +38 -0
  82. package/src/provision.ts +438 -9
  83. package/src/system-context.ts +41 -9
  84. package/src/url-utils.ts +17 -0
package/dist/index.js CHANGED
@@ -1,16 +1,18 @@
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";
16
18
  const ADAPTER_LIST = listAdapterIds().join("|");
@@ -29,12 +31,8 @@ const HELP = `botcord-daemon — BotCord local daemon
29
31
  Usage: botcord-daemon <command> [options]
30
32
 
31
33
  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
34
  start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
35
+ [--agent <ag_xxx> ...] [--cwd <path>]
38
36
  Start the daemon in the foreground by
39
37
  default. Pass --background (alias -d)
40
38
  to detach and return to the shell.
@@ -48,9 +46,24 @@ Commands:
48
46
  (defaults to hostname). Non-TTY
49
47
  environments must mount a pre-existing
50
48
  user-auth.json (plan §6.4).
49
+ On first run, auto-creates
50
+ ~/.botcord/daemon/config.json with a
51
+ default route (claude-code, $HOME) and
52
+ credential auto-discovery. Pass
53
+ --agent/--cwd to seed the file
54
+ (ignored once config exists).
51
55
  stop Stop the running daemon (SIGTERM)
52
56
  status Print daemon status (pid, agent)
53
57
  logs [-f] Print log tail (use -f to follow)
58
+ transcript enable|disable|status Toggle persistent transcript logging
59
+ transcript list --agent <ag_xxx> List rooms with transcripts for an agent
60
+ transcript tail --agent <ag_xxx> --room <rm_xxx> [--topic <tp>] [-n 50] [-f]
61
+ Tail recent transcript records (NDJSON)
62
+ transcript dump --agent <ag_xxx> --room <rm_xxx> [--topic <tp>]
63
+ Print full transcript file to stdout
64
+ transcript prune --agent <ag_xxx> [--older-than 30d] [--all]
65
+ Remove rotated transcript files (or all
66
+ for the agent with --all --yes)
54
67
  route add [match flags] --adapter <${ADAPTER_LIST}> --cwd <path>
55
68
  match flags (first match wins; at least one conversation/sender selector required):
56
69
  --conversation-id <rm_xxx> (alias: --room <rm_xxx>)
@@ -154,19 +167,32 @@ function pidAlive(pid) {
154
167
  return false;
155
168
  }
156
169
  }
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");
170
+ /**
171
+ * Load the daemon config, auto-creating `~/.botcord/daemon/config.json`
172
+ * with sensible defaults on first run. `--agent` (repeated) pins explicit
173
+ * agent ids; `--cwd` overrides the defaultRoute working directory. Both
174
+ * are seed-only — they are ignored once a config already exists, since
175
+ * `route` and direct edits to `config.json` are the canonical way to
176
+ * change a configured daemon.
177
+ */
178
+ function loadOrInitConfig(args) {
179
+ try {
180
+ return loadConfig();
181
+ }
182
+ catch (err) {
183
+ const missing = err instanceof Error && err.code === CONFIG_MISSING;
184
+ if (!missing)
185
+ throw err;
186
+ const agents = args.lists.agent ?? [];
187
+ const cwd = typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
188
+ const cfg = initDefaultConfig(agents, cwd);
189
+ saveConfig(cfg);
190
+ log.info("auto-initialized daemon config", { agents, cwd, path: CONFIG_FILE_PATH });
191
+ console.log(`wrote default config to ${CONFIG_FILE_PATH}`);
192
+ if (agents.length === 0) {
193
+ console.log("no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials");
194
+ }
195
+ return cfg;
170
196
  }
171
197
  }
172
198
  /**
@@ -200,10 +226,15 @@ async function runDeviceCodeFlow(opts) {
200
226
  label: opts.label ?? null,
201
227
  });
202
228
  const dc = await requestDeviceCode(opts.hubUrl, opts.label ? { label: opts.label } : undefined);
203
- const display = dc.verificationUriComplete ?? dc.verificationUri;
229
+ const base = dc.verificationUriComplete ?? dc.verificationUri;
230
+ const display = appendNextParam(base, "/settings/daemons");
231
+ console.log("");
232
+ console.log("Open this URL in a browser where you're signed in to BotCord");
233
+ console.log("(typically your laptop, NOT this machine):");
234
+ console.log("");
235
+ console.log(` ${display}`);
204
236
  console.log("");
205
- console.log(`Visit ${display}`);
206
- console.log(`Code: ${dc.userCode}`);
237
+ console.log(`Or enter this code at ${dc.verificationUri}: ${dc.userCode}`);
207
238
  console.log("Waiting for authorization (Ctrl-C to abort)...");
208
239
  const expiresAt = Date.now() + dc.expiresIn * 1000;
209
240
  let intervalSec = dc.interval;
@@ -289,7 +320,7 @@ async function ensureUserAuthForStart(args) {
289
320
  return runDeviceCodeFlow({ hubUrl, label });
290
321
  }
291
322
  async function cmdStart(args) {
292
- const cfg = loadConfig();
323
+ const cfg = loadOrInitConfig(args);
293
324
  // Foreground is now the default. --background (alias -d) detaches.
294
325
  // --foreground is still accepted (no-op) for backwards compatibility and
295
326
  // is also what the detached child re-execs itself with.
@@ -492,6 +523,222 @@ async function cmdLogs(args) {
492
523
  const lines = data.split("\n");
493
524
  console.log(lines.slice(-100).join("\n"));
494
525
  }
526
+ // ---------------------------------------------------------------------------
527
+ // transcript subcommands (design §5)
528
+ // ---------------------------------------------------------------------------
529
+ function transcriptStringFlag(args, name) {
530
+ const v = args.flags[name];
531
+ return typeof v === "string" && v.length > 0 ? v : null;
532
+ }
533
+ function parseDurationToMs(s) {
534
+ const m = /^(\d+)\s*([smhd])?$/.exec(s.trim());
535
+ if (!m)
536
+ return null;
537
+ const n = Number(m[1]);
538
+ const unit = m[2] ?? "d";
539
+ const mult = {
540
+ s: 1000,
541
+ m: 60_000,
542
+ h: 3_600_000,
543
+ d: 86_400_000,
544
+ };
545
+ return n * mult[unit];
546
+ }
547
+ async function cmdTranscript(args) {
548
+ switch (args.sub) {
549
+ case "enable":
550
+ return cmdTranscriptToggle(true);
551
+ case "disable":
552
+ return cmdTranscriptToggle(false);
553
+ case "status":
554
+ return cmdTranscriptStatus();
555
+ case "list":
556
+ return cmdTranscriptList(args);
557
+ case "tail":
558
+ return cmdTranscriptTail(args);
559
+ case "dump":
560
+ return cmdTranscriptDump(args);
561
+ case "prune":
562
+ return cmdTranscriptPrune(args);
563
+ default:
564
+ console.error("usage: botcord-daemon transcript <enable|disable|status|list|tail|dump|prune>");
565
+ process.exit(1);
566
+ }
567
+ }
568
+ function cmdTranscriptToggle(enable) {
569
+ let cfg;
570
+ try {
571
+ cfg = loadConfig();
572
+ }
573
+ catch (err) {
574
+ const e = err;
575
+ if (e.code === CONFIG_MISSING) {
576
+ console.error(`daemon config not found — run \`botcord-daemon start\` once to initialize, then retry`);
577
+ process.exit(1);
578
+ }
579
+ throw err;
580
+ }
581
+ cfg.transcript = { ...(cfg.transcript ?? {}), enabled: enable };
582
+ saveConfig(cfg);
583
+ console.log(`transcript persistence ${enable ? "enabled" : "disabled"} (next daemon start)`);
584
+ }
585
+ function cmdTranscriptStatus() {
586
+ let cfg = null;
587
+ try {
588
+ cfg = loadConfig();
589
+ }
590
+ catch (err) {
591
+ const e = err;
592
+ if (e.code !== CONFIG_MISSING)
593
+ throw err;
594
+ }
595
+ const configEnabled = cfg?.transcript?.enabled === true;
596
+ const env = process.env.BOTCORD_TRANSCRIPT;
597
+ const effective = resolveTranscriptEnabled(env, configEnabled);
598
+ let source;
599
+ if (env === "1" || env === "0")
600
+ source = `env BOTCORD_TRANSCRIPT=${env}`;
601
+ else if (configEnabled)
602
+ source = "config (transcript.enabled=true)";
603
+ else
604
+ source = "default-off";
605
+ console.log(`enabled: ${effective}`);
606
+ console.log(`source: ${source}`);
607
+ console.log(`root: ${defaultTranscriptRoot()}`);
608
+ }
609
+ function cmdTranscriptList(args) {
610
+ const agent = transcriptStringFlag(args, "agent");
611
+ if (!agent) {
612
+ console.error("transcript list requires --agent <ag_xxx>");
613
+ process.exit(1);
614
+ }
615
+ const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
616
+ if (!existsSync(root)) {
617
+ return; // no rooms → empty output
618
+ }
619
+ for (const entry of readdirSync(root)) {
620
+ const dir = path.join(root, entry);
621
+ let st;
622
+ try {
623
+ st = statSync(dir);
624
+ }
625
+ catch {
626
+ continue;
627
+ }
628
+ if (!st.isDirectory())
629
+ continue;
630
+ console.log(entry);
631
+ }
632
+ }
633
+ function cmdTranscriptTail(args) {
634
+ const agent = transcriptStringFlag(args, "agent");
635
+ const room = transcriptStringFlag(args, "room");
636
+ if (!agent || !room) {
637
+ console.error("transcript tail requires --agent <ag_xxx> --room <rm_xxx>");
638
+ process.exit(1);
639
+ }
640
+ const topic = transcriptStringFlag(args, "topic");
641
+ const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
642
+ if (!existsSync(file)) {
643
+ console.error(`no transcript at ${file}`);
644
+ process.exit(1);
645
+ }
646
+ const follow = args.flags.f === true || args.flags.follow === true;
647
+ const nFlag = transcriptStringFlag(args, "n");
648
+ const n = nFlag && /^\d+$/.test(nFlag) ? Number(nFlag) : 50;
649
+ if (follow) {
650
+ const child = spawn("tail", ["-n", String(n), "-f", file], { stdio: "inherit" });
651
+ process.on("SIGINT", () => child.kill("SIGINT"));
652
+ return new Promise((resolve) => {
653
+ child.on("close", () => resolve());
654
+ });
655
+ }
656
+ const data = readFileSync(file, "utf8");
657
+ const lines = data.split("\n").filter((l) => l.length > 0);
658
+ console.log(lines.slice(-n).join("\n"));
659
+ }
660
+ function cmdTranscriptDump(args) {
661
+ const agent = transcriptStringFlag(args, "agent");
662
+ const room = transcriptStringFlag(args, "room");
663
+ if (!agent || !room) {
664
+ console.error("transcript dump requires --agent <ag_xxx> --room <rm_xxx>");
665
+ process.exit(1);
666
+ }
667
+ const topic = transcriptStringFlag(args, "topic");
668
+ const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
669
+ if (!existsSync(file)) {
670
+ console.error(`no transcript at ${file}`);
671
+ process.exit(1);
672
+ }
673
+ process.stdout.write(readFileSync(file, "utf8"));
674
+ }
675
+ function cmdTranscriptPrune(args) {
676
+ const agent = transcriptStringFlag(args, "agent");
677
+ if (!agent) {
678
+ console.error("transcript prune requires --agent <ag_xxx>");
679
+ process.exit(1);
680
+ }
681
+ const all = args.flags.all === true;
682
+ const olderThanFlag = transcriptStringFlag(args, "older-than");
683
+ const yes = args.flags.yes === true;
684
+ const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
685
+ if (!existsSync(root))
686
+ return;
687
+ if (all) {
688
+ if (!yes) {
689
+ console.error(`transcript prune --all will delete every transcript under ${root}; rerun with --yes to confirm`);
690
+ process.exit(1);
691
+ }
692
+ rmSync(root, { recursive: true, force: true });
693
+ console.log(`removed ${root}`);
694
+ return;
695
+ }
696
+ // Default and --older-than: prune rotated files only (the "{topic}.STAMP.jsonl" form).
697
+ // Active files (`{topic}.jsonl` / `_default.jsonl`) are never touched.
698
+ const cutoffMs = olderThanFlag ? parseDurationToMs(olderThanFlag) : null;
699
+ if (olderThanFlag && cutoffMs === null) {
700
+ console.error(`transcript prune --older-than: invalid duration "${olderThanFlag}" (use 30d / 12h / 30m / 60s)`);
701
+ process.exit(1);
702
+ }
703
+ const cutoff = cutoffMs !== null ? Date.now() - cutoffMs : null;
704
+ let removed = 0;
705
+ for (const roomEntry of readdirSync(root)) {
706
+ const dir = path.join(root, roomEntry);
707
+ let st;
708
+ try {
709
+ st = statSync(dir);
710
+ }
711
+ catch {
712
+ continue;
713
+ }
714
+ if (!st.isDirectory())
715
+ continue;
716
+ for (const f of readdirSync(dir)) {
717
+ // rotated files: <topic>.<YYYYMMDD-HHMMSS>.jsonl — must contain a stamp segment
718
+ if (!/^.+\.\d{8}-\d{6}\.jsonl$/.test(f))
719
+ continue;
720
+ const full = path.join(dir, f);
721
+ if (cutoff !== null) {
722
+ try {
723
+ const fst = statSync(full);
724
+ if (fst.mtimeMs >= cutoff)
725
+ continue;
726
+ }
727
+ catch {
728
+ continue;
729
+ }
730
+ }
731
+ try {
732
+ unlinkSync(full);
733
+ removed += 1;
734
+ }
735
+ catch (err) {
736
+ console.error(`failed to remove ${full}: ${err instanceof Error ? err.message : String(err)}`);
737
+ }
738
+ }
739
+ }
740
+ console.log(`removed ${removed} rotated transcript file(s)`);
741
+ }
495
742
  function formatRouteMatch(m) {
496
743
  const parts = [];
497
744
  if (m.channel)
@@ -793,13 +1040,25 @@ async function cmdDoctor(args) {
793
1040
  // Doctor should not hard-fail when no config exists yet; channel probes
794
1041
  // simply produce an empty list in that case.
795
1042
  let channels = [];
1043
+ let cfgForEndpoints = null;
796
1044
  try {
797
1045
  const cfg = loadConfig();
1046
+ cfgForEndpoints = cfg;
798
1047
  channels = channelsFromDaemonConfig(cfg);
799
1048
  }
800
1049
  catch {
801
1050
  channels = [];
802
1051
  }
1052
+ if (cfgForEndpoints?.openclawGateways && cfgForEndpoints.openclawGateways.length > 0) {
1053
+ const { collectRuntimeSnapshotAsync } = await import("./provision.js");
1054
+ const snap = await collectRuntimeSnapshotAsync({ cfg: cfgForEndpoints });
1055
+ const byId = new Map(snap.runtimes.map((r) => [r.id, r]));
1056
+ for (const e of entries) {
1057
+ const r = byId.get(e.id);
1058
+ if (r?.endpoints)
1059
+ e.endpoints = r.endpoints;
1060
+ }
1061
+ }
803
1062
  const credentialsPath = (accountId) => path.join(homedir(), ".botcord", "credentials", `${accountId}.json`);
804
1063
  const input = await runDoctor(entries, channels, {
805
1064
  credentialsPath,
@@ -821,9 +1080,6 @@ async function main() {
821
1080
  }
822
1081
  try {
823
1082
  switch (args.cmd) {
824
- case "init":
825
- await cmdInit(args);
826
- break;
827
1083
  case "start":
828
1084
  await cmdStart(args);
829
1085
  break;
@@ -836,6 +1092,9 @@ async function main() {
836
1092
  case "logs":
837
1093
  await cmdLogs(args);
838
1094
  break;
1095
+ case "transcript":
1096
+ await cmdTranscript(args);
1097
+ break;
839
1098
  case "route":
840
1099
  await cmdRoute(args);
841
1100
  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
+ }
@@ -1,5 +1,6 @@
1
- import { BotCordClient, type ControlAck, type ControlFrame, type ListRuntimesResult } from "@botcord/protocol-core";
1
+ import { BotCordClient, type AgentIdentitySnapshot, type ControlAck, type ControlFrame, type ListRuntimesResult } from "@botcord/protocol-core";
2
2
  import type { Gateway } from "./gateway/index.js";
3
+ import type { PolicyResolverLike } from "./gateway/policy-resolver.js";
3
4
  import { type DaemonConfig } from "./config.js";
4
5
  /** Options accepted by {@link createProvisioner}. */
5
6
  export interface ProvisionerOptions {
@@ -10,6 +11,14 @@ export interface ProvisionerOptions {
10
11
  * run without a real Hub.
11
12
  */
12
13
  register?: typeof BotCordClient.register;
14
+ /**
15
+ * Optional policy-resolver handle (PR3). When present, the
16
+ * `policy_updated` control frame routes through it: cache is invalidated
17
+ * for the (agent, room?) pair, and any embedded `policy` payload is
18
+ * applied directly so the next inbound sees the fresh policy without an
19
+ * extra round-trip.
20
+ */
21
+ policyResolver?: PolicyResolverLike;
13
22
  }
14
23
  /** The value a frame handler returns (minus the `id` which the channel fills in). */
15
24
  type AckBody = Omit<ControlAck, "id">;
@@ -36,6 +45,68 @@ export declare function removeAgentFromConfig(cfg: DaemonConfig, agentId: string
36
45
  * gateway already isolates from throwing) and reading the wall clock.
37
46
  */
38
47
  export declare function collectRuntimeSnapshot(): ListRuntimesResult;
48
+ /** Maximum number of `endpoints[]` entries persisted per runtime (RFC §3.8.2). */
49
+ export declare const RUNTIME_ENDPOINTS_CAP = 32;
50
+ /** Injection seam for L2 + L3 endpoint probes — kept testable + side-effect-free. */
51
+ export type WsEndpointProbeFn = (args: {
52
+ url: string;
53
+ token?: string;
54
+ timeoutMs: number;
55
+ }) => Promise<{
56
+ ok: boolean;
57
+ version?: string;
58
+ /**
59
+ * L3 — populated when `agents.list` succeeds. `id` is the stable key
60
+ * consumed by route lookups / `openclawAgent`; `name` is display-only.
61
+ */
62
+ agents?: Array<{
63
+ id: string;
64
+ name?: string;
65
+ workspace?: string;
66
+ model?: {
67
+ name?: string;
68
+ provider?: string;
69
+ };
70
+ }>;
71
+ error?: string;
72
+ }>;
73
+ /**
74
+ * Async variant that includes L2 (gateway reachability) and L3 (agent listing)
75
+ * probes for runtimes that talk to external services. Used by the production
76
+ * `list_runtimes` and first-connect snapshot paths.
77
+ *
78
+ * `cfg` is optional so existing callers without a loaded config (e.g. tests)
79
+ * can keep using the sync `collectRuntimeSnapshot()` — when absent, the result
80
+ * is identical to that function.
81
+ */
82
+ export declare function collectRuntimeSnapshotAsync(opts?: {
83
+ cfg?: {
84
+ openclawGateways?: Array<{
85
+ name: string;
86
+ url: string;
87
+ token?: string;
88
+ tokenFile?: string;
89
+ }>;
90
+ };
91
+ wsProbe?: WsEndpointProbeFn;
92
+ timeoutMs?: number;
93
+ }): Promise<ListRuntimesResult>;
94
+ interface HelloIdentityResult {
95
+ updated: number;
96
+ skipped: number;
97
+ }
98
+ /**
99
+ * Reconcile every agent identity carried by the `hello.agents` snapshot
100
+ * against the on-disk `identity.md`. Best-effort: a malformed entry or a
101
+ * file-system error for one agent never aborts the rest.
102
+ *
103
+ * Identity-snapshot semantics intentionally only touch the metadata
104
+ * line + Bio body — Role/Boundaries paragraphs the user authored locally
105
+ * are preserved (see `applyAgentIdentity`). Missing identity.md files
106
+ * (agent provisioned on a different daemon, or workspace cleared) are
107
+ * silently skipped.
108
+ */
109
+ export declare function applyHelloIdentitySnapshot(snapshot: AgentIdentitySnapshot[] | undefined): HelloIdentityResult;
39
110
  interface ReloadResult {
40
111
  reloaded: true;
41
112
  added: string[];