@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
@@ -24,6 +24,10 @@ export interface DiscoveredAgentCredential {
24
24
  runtime?: string;
25
25
  /** Working directory cached alongside `runtime`. */
26
26
  cwd?: string;
27
+ /** OpenClaw gateway profile name from credentials (only meaningful for openclaw-acp). */
28
+ openclawGateway?: string;
29
+ /** OpenClaw agent profile override from credentials. */
30
+ openclawAgent?: string;
27
31
  /** Key id from the credentials file — surfaced so boot-time workspace
28
32
  * seeding (see daemon-agent-workspace-plan.md §9) can render identity.md
29
33
  * without re-reading the file. */
@@ -94,6 +94,10 @@ export function discoverAgentCredentials(opts = {}) {
94
94
  entry.runtime = creds.runtime;
95
95
  if (creds.cwd)
96
96
  entry.cwd = creds.cwd;
97
+ if (creds.openclawGateway)
98
+ entry.openclawGateway = creds.openclawGateway;
99
+ if (creds.openclawAgent)
100
+ entry.openclawAgent = creds.openclawAgent;
97
101
  if (creds.keyId)
98
102
  entry.keyId = creds.keyId;
99
103
  if (creds.savedAt)
@@ -153,6 +157,10 @@ export function resolveBootAgents(cfg, opts = {}) {
153
157
  entry.runtime = creds.runtime;
154
158
  if (creds.cwd)
155
159
  entry.cwd = creds.cwd;
160
+ if (creds.openclawGateway)
161
+ entry.openclawGateway = creds.openclawGateway;
162
+ if (creds.openclawAgent)
163
+ entry.openclawAgent = creds.openclawAgent;
156
164
  if (creds.keyId)
157
165
  entry.keyId = creds.keyId;
158
166
  if (creds.savedAt)
@@ -8,6 +8,20 @@ export declare function agentStateDir(agentId: string): string;
8
8
  * here — neither touching `~/.codex/` nor the agent's `workspace/` cwd.
9
9
  */
10
10
  export declare function agentCodexHomeDir(agentId: string): string;
11
+ /**
12
+ * Per-agent HERMES_HOME. Carries the hermes-acp `.env`, `state.db`, and
13
+ * `skills/` so each daemon-managed agent has an isolated hermes config
14
+ * tree and never reads/writes the user's `~/.hermes`.
15
+ */
16
+ export declare function agentHermesHomeDir(agentId: string): string;
17
+ /**
18
+ * Per-agent runtime cwd for hermes-acp. Distinct from `workspace/` so the
19
+ * adapter can rewrite `AGENTS.md` here every turn (carrying the dynamic
20
+ * systemContext) without clobbering the user/agent-editable workspace
21
+ * `AGENTS.md`. hermes discovers `AGENTS.md` from cwd upward, so the file
22
+ * must live alongside the spawn cwd.
23
+ */
24
+ export declare function agentHermesWorkspaceDir(agentId: string): string;
11
25
  export interface WorkspaceSeed {
12
26
  displayName?: string;
13
27
  bio?: string;
@@ -22,6 +36,16 @@ export interface WorkspaceSeed {
22
36
  * — the codex adapter writes it fresh per turn from `systemContext`.
23
37
  */
24
38
  export declare function ensureAgentCodexHome(agentId: string): string;
39
+ /**
40
+ * Idempotently create the per-agent HERMES_HOME and HERMES workspace
41
+ * directories. Writes a stub `.env` inside HERMES_HOME so hermes-acp's
42
+ * `_load_env` does not log "No .env found" on every spawn; users can edit
43
+ * this file to add API keys / model overrides.
44
+ */
45
+ export declare function ensureAgentHermesWorkspace(agentId: string): {
46
+ hermesHome: string;
47
+ hermesWorkspace: string;
48
+ };
25
49
  /**
26
50
  * Idempotently create the agent's home / workspace / state directories and
27
51
  * seed the workspace Markdown files. Existing files are never overwritten —
@@ -29,3 +53,41 @@ export declare function ensureAgentCodexHome(agentId: string): string;
29
53
  * State files are not touched here; working-memory.ts owns `state/`.
30
54
  */
31
55
  export declare function ensureAgentWorkspace(agentId: string, seed: WorkspaceSeed): void;
56
+ /** Patch fields accepted by {@link applyAgentIdentity}. `bio = null` clears it. */
57
+ export interface AgentIdentityPatch {
58
+ displayName?: string;
59
+ bio?: string | null;
60
+ }
61
+ /**
62
+ * Result of applying an identity patch. `changed` is true only when the
63
+ * file was rewritten on disk; `skipped` reports why (no-op vs. unable).
64
+ */
65
+ export interface AgentIdentityApplyResult {
66
+ changed: boolean;
67
+ skipped?: "missing-file" | "no-change" | "unparseable";
68
+ }
69
+ /**
70
+ * Surgically rewrite the `Display name` and `Bio` fields inside an existing
71
+ * `identity.md`, preserving anything the user has authored elsewhere
72
+ * (Role / Boundaries / arbitrary new sections). No-op when the file is
73
+ * missing — provisioning will create it with the correct values, and
74
+ * subsequent hello snapshots simply reapply the dashboard truth.
75
+ *
76
+ * The identity.md template carries `Role` / `Boundaries` headings after
77
+ * `## Bio`; we anchor the Bio rewrite on "next `##`" so user-added
78
+ * paragraphs inside Bio are replaced wholesale (the dashboard is the
79
+ * source of truth) without disturbing siblings.
80
+ */
81
+ export declare function applyAgentIdentity(agentId: string, patch: AgentIdentityPatch): AgentIdentityApplyResult;
82
+ /**
83
+ * Read the agent's `identity.md` verbatim, if it exists. Returns the raw
84
+ * contents (including the leading `# Identity` heading) so callers can
85
+ * splice it into the system context. Returns `null` when the workspace
86
+ * has not been provisioned yet, the file is empty, or the read fails.
87
+ *
88
+ * Each call hits disk — same contract as `readWorkingMemory`, so a
89
+ * dashboard-driven edit (`applyAgentIdentity` from a control frame, or
90
+ * a hello-snapshot reapply, or the agent's own self-edit) is visible
91
+ * on the very next turn without restarting the gateway.
92
+ */
93
+ export declare function readIdentity(agentId: string): string | null;
@@ -7,8 +7,16 @@
7
7
  * codex-home/ — per-agent CODEX_HOME used by the codex adapter so codex
8
8
  * reads a daemon-written AGENTS.md (systemContext carrier)
9
9
  * and stores its sessions/ without touching ~/.codex.
10
+ * hermes-home/ — per-agent HERMES_HOME used by the hermes-acp
11
+ * adapter (carries .env, state.db, skills/) so
12
+ * hermes-acp's per-user state stays isolated.
13
+ * hermes-workspace/ — per-agent runtime cwd for hermes-acp; the adapter
14
+ * writes systemContext into AGENTS.md here every turn.
15
+ * Kept separate from `workspace/` so daemon-written
16
+ * systemContext does not clobber the user/agent-
17
+ * editable workspace AGENTS.md.
10
18
  */
11
- import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
19
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
12
20
  import { homedir } from "node:os";
13
21
  import path from "node:path";
14
22
  // Accepted agent id pattern. Enforced at every path-builder entry so a
@@ -42,6 +50,24 @@ export function agentStateDir(agentId) {
42
50
  export function agentCodexHomeDir(agentId) {
43
51
  return path.join(agentHomeDir(agentId), "codex-home");
44
52
  }
53
+ /**
54
+ * Per-agent HERMES_HOME. Carries the hermes-acp `.env`, `state.db`, and
55
+ * `skills/` so each daemon-managed agent has an isolated hermes config
56
+ * tree and never reads/writes the user's `~/.hermes`.
57
+ */
58
+ export function agentHermesHomeDir(agentId) {
59
+ return path.join(agentHomeDir(agentId), "hermes-home");
60
+ }
61
+ /**
62
+ * Per-agent runtime cwd for hermes-acp. Distinct from `workspace/` so the
63
+ * adapter can rewrite `AGENTS.md` here every turn (carrying the dynamic
64
+ * systemContext) without clobbering the user/agent-editable workspace
65
+ * `AGENTS.md`. hermes discovers `AGENTS.md` from cwd upward, so the file
66
+ * must live alongside the spawn cwd.
67
+ */
68
+ export function agentHermesWorkspaceDir(agentId) {
69
+ return path.join(agentHomeDir(agentId), "hermes-workspace");
70
+ }
45
71
  const AGENTS_MD = `# Agent Workspace
46
72
 
47
73
  This directory is your persistent workspace. You run with \`cwd\` set here.
@@ -62,19 +88,23 @@ This directory is your persistent workspace. You run with \`cwd\` set here.
62
88
 
63
89
  ## How to use this
64
90
 
65
- You are **instructed** to skim \`identity.md\`, \`memory.md\`, \`task.md\` before each
66
- response and to write back what changed after meaningful turns. Nothing in the
67
- runtime enforces this the daemon does not auto-load these files into your
68
- context. Treat AGENTS.md as a convention, not a mechanism.
91
+ - \`identity.md\` is **auto-loaded** by the daemon and injected into every turn's
92
+ system context as the \`[BotCord Identity]\` block. Edits to this file (yours,
93
+ the dashboard's via \`applyAgentIdentity\`, or a hello-snapshot reapply) take
94
+ effect on the next turn no restart needed.
95
+ - \`memory.md\` and \`task.md\` are **convention, not mechanism**. The daemon does
96
+ not auto-load them; you are instructed to skim them before responding and to
97
+ write back what changed after meaningful turns. Keep them tight enough to be
98
+ worth re-reading.
69
99
  `;
70
100
  const MEMORY_MD = `# Memory
71
101
 
72
102
  <!--
73
103
  Long-lived facts about the user, past decisions, and preferences that should
74
104
  survive across conversations. Organize by topic. Keep entries short. Prune
75
- regularly — AGENTS.md instructs the runtime to consult this file before each
76
- response, but nothing loads it automatically; keep it short enough to be
77
- worth re-reading.
105
+ regularly — AGENTS.md instructs you to consult this file before each
106
+ response, but nothing loads it automatically (unlike identity.md); keep it
107
+ short enough to be worth re-reading.
78
108
  -->
79
109
  `;
80
110
  const TASK_MD = `# Current Task
@@ -190,6 +220,21 @@ export function ensureAgentCodexHome(agentId) {
190
220
  linkCodexAuth(dir);
191
221
  return dir;
192
222
  }
223
+ /**
224
+ * Idempotently create the per-agent HERMES_HOME and HERMES workspace
225
+ * directories. Writes a stub `.env` inside HERMES_HOME so hermes-acp's
226
+ * `_load_env` does not log "No .env found" on every spawn; users can edit
227
+ * this file to add API keys / model overrides.
228
+ */
229
+ export function ensureAgentHermesWorkspace(agentId) {
230
+ const hermesHome = agentHermesHomeDir(agentId);
231
+ const hermesWorkspace = agentHermesWorkspaceDir(agentId);
232
+ mkdirTolerant(hermesHome);
233
+ mkdirTolerant(hermesWorkspace);
234
+ writeIfMissing(path.join(hermesHome, ".env"), "# hermes-agent environment overrides for this BotCord agent.\n" +
235
+ "# Add e.g. HERMES_INFERENCE_PROVIDER=openrouter, OPENROUTER_API_KEY=...\n");
236
+ return { hermesHome, hermesWorkspace };
237
+ }
193
238
  /**
194
239
  * Idempotently create the agent's home / workspace / state directories and
195
240
  * seed the workspace Markdown files. Existing files are never overwritten —
@@ -208,6 +253,7 @@ export function ensureAgentWorkspace(agentId, seed) {
208
253
  mkdirTolerant(notes);
209
254
  mkdirTolerant(state);
210
255
  ensureAgentCodexHome(agentId);
256
+ ensureAgentHermesWorkspace(agentId);
211
257
  const agentsMdPath = path.join(workspace, "AGENTS.md");
212
258
  const claudeMdPath = path.join(workspace, "CLAUDE.md");
213
259
  writeIfMissing(agentsMdPath, AGENTS_MD);
@@ -217,3 +263,89 @@ export function ensureAgentWorkspace(agentId, seed) {
217
263
  writeIfMissing(path.join(workspace, "task.md"), TASK_MD);
218
264
  writeIfMissing(path.join(notes, ".gitkeep"), "");
219
265
  }
266
+ const DISPLAY_NAME_LINE = /^- \*\*Display name\*\*: .*$/m;
267
+ // Match the Bio section's body. Anchor on the next `##` heading when one
268
+ // exists, otherwise consume to end-of-file — keeps the rewrite working when
269
+ // the user has stripped Role/Boundaries sections.
270
+ const BIO_SECTION = /(## Bio\n\n)([\s\S]*?)(\n+##\s|$)/;
271
+ /**
272
+ * Surgically rewrite the `Display name` and `Bio` fields inside an existing
273
+ * `identity.md`, preserving anything the user has authored elsewhere
274
+ * (Role / Boundaries / arbitrary new sections). No-op when the file is
275
+ * missing — provisioning will create it with the correct values, and
276
+ * subsequent hello snapshots simply reapply the dashboard truth.
277
+ *
278
+ * The identity.md template carries `Role` / `Boundaries` headings after
279
+ * `## Bio`; we anchor the Bio rewrite on "next `##`" so user-added
280
+ * paragraphs inside Bio are replaced wholesale (the dashboard is the
281
+ * source of truth) without disturbing siblings.
282
+ */
283
+ export function applyAgentIdentity(agentId, patch) {
284
+ assertSafeAgentId(agentId);
285
+ const file = path.join(agentWorkspaceDir(agentId), "identity.md");
286
+ if (!existsSync(file)) {
287
+ return { changed: false, skipped: "missing-file" };
288
+ }
289
+ let text;
290
+ try {
291
+ text = readFileSync(file, "utf8");
292
+ }
293
+ catch {
294
+ return { changed: false, skipped: "missing-file" };
295
+ }
296
+ const original = text;
297
+ let touched = false;
298
+ if (typeof patch.displayName === "string") {
299
+ const value = patch.displayName.length > 0 ? patch.displayName : FIELD_PLACEHOLDER;
300
+ if (DISPLAY_NAME_LINE.test(text)) {
301
+ // Use a function replacer so `$1`, `$&` etc. inside the value are
302
+ // treated literally rather than as backreferences.
303
+ text = text.replace(DISPLAY_NAME_LINE, () => `- **Display name**: ${value}`);
304
+ touched = true;
305
+ }
306
+ else {
307
+ // Heavily-edited file without the canonical metadata block — bail
308
+ // out rather than guess where to splice.
309
+ return { changed: false, skipped: "unparseable" };
310
+ }
311
+ }
312
+ if (patch.bio !== undefined) {
313
+ const bioText = patch.bio !== null && patch.bio.trim().length > 0
314
+ ? patch.bio.trim()
315
+ : BIO_PLACEHOLDER;
316
+ if (BIO_SECTION.test(text)) {
317
+ text = text.replace(BIO_SECTION, (_match, head, _body, tail) => `${head}${bioText}${tail}`);
318
+ touched = true;
319
+ }
320
+ else {
321
+ return { changed: false, skipped: "unparseable" };
322
+ }
323
+ }
324
+ if (!touched || text === original) {
325
+ return { changed: false, skipped: "no-change" };
326
+ }
327
+ writeFileSync(file, text, { mode: 0o600 });
328
+ return { changed: true };
329
+ }
330
+ /**
331
+ * Read the agent's `identity.md` verbatim, if it exists. Returns the raw
332
+ * contents (including the leading `# Identity` heading) so callers can
333
+ * splice it into the system context. Returns `null` when the workspace
334
+ * has not been provisioned yet, the file is empty, or the read fails.
335
+ *
336
+ * Each call hits disk — same contract as `readWorkingMemory`, so a
337
+ * dashboard-driven edit (`applyAgentIdentity` from a control frame, or
338
+ * a hello-snapshot reapply, or the agent's own self-edit) is visible
339
+ * on the very next turn without restarting the gateway.
340
+ */
341
+ export function readIdentity(agentId) {
342
+ assertSafeAgentId(agentId);
343
+ const file = path.join(agentWorkspaceDir(agentId), "identity.md");
344
+ try {
345
+ const raw = readFileSync(file, "utf8");
346
+ return raw.trim().length > 0 ? raw : null;
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ }
package/dist/config.d.ts CHANGED
@@ -5,7 +5,23 @@ export declare const SNAPSHOT_PATH: string;
5
5
  * Adapter ids. Built-in adapters are enumerated for editor hints; any string
6
6
  * accepted by the registry is valid at runtime.
7
7
  */
8
- export type AdapterName = "claude-code" | "codex" | "gemini" | (string & {});
8
+ export type AdapterName = "claude-code" | "codex" | "gemini" | "openclaw-acp" | (string & {});
9
+ /**
10
+ * One OpenClaw gateway profile. Referenced by `RouteRule.gateway` and
11
+ * `DaemonRouteDefault.gateway` (and `StoredBotCordCredentials.openclawGateway`)
12
+ * via `name`. `tokenFile` is `~`-expanded and read at `toGatewayConfig` time;
13
+ * read failures do not block boot — the gateway becomes unusable but other
14
+ * gateways still work.
15
+ */
16
+ export interface OpenclawGatewayProfile {
17
+ name: string;
18
+ url: string;
19
+ /** Bearer token; mutually-exclusive priority is `token > tokenFile`. */
20
+ token?: string;
21
+ tokenFile?: string;
22
+ /** Default OpenClaw agent profile name when a route does not pin one. */
23
+ defaultAgent?: string;
24
+ }
9
25
  /**
10
26
  * Predicates selecting messages for a route. `roomId` / `roomPrefix` are
11
27
  * legacy aliases retained for backward compatibility with pre-P1 daemon
@@ -31,11 +47,22 @@ export interface RouteRule {
31
47
  cwd: string;
32
48
  /** Extra CLI flags appended to the adapter invocation. */
33
49
  extraArgs?: string[];
50
+ /**
51
+ * Required when `adapter === "openclaw-acp"`: name of an entry in
52
+ * `DaemonConfig.openclawGateways[]`.
53
+ */
54
+ gateway?: string;
55
+ /** Overrides `OpenclawGatewayProfile.defaultAgent` when set. */
56
+ openclawAgent?: string;
34
57
  }
35
58
  export interface DaemonRouteDefault {
36
59
  adapter: AdapterName;
37
60
  cwd: string;
38
61
  extraArgs?: string[];
62
+ /** Same semantics as `RouteRule.gateway`. */
63
+ gateway?: string;
64
+ /** Same semantics as `RouteRule.openclawAgent`. */
65
+ openclawAgent?: string;
39
66
  }
40
67
  /**
41
68
  * Daemon-layer hook controlling credential auto-discovery at boot. Kept
@@ -47,6 +74,16 @@ export interface AgentDiscoveryConfig {
47
74
  enabled?: boolean;
48
75
  credentialsDir?: string;
49
76
  }
77
+ export interface OpenclawDiscoveryConfig {
78
+ /** Defaults to true. */
79
+ enabled?: boolean;
80
+ /** Overrides the local config-file search roots. */
81
+ searchPaths?: string[];
82
+ /** Overrides the local loopback ports to probe. */
83
+ defaultPorts?: number[];
84
+ /** Defaults to true. When false, discovery only persists gateways. */
85
+ autoProvision?: boolean;
86
+ }
50
87
  export interface DaemonConfig {
51
88
  /**
52
89
  * @deprecated Kept for backward compatibility with pre-multi-agent configs.
@@ -74,6 +111,31 @@ export interface DaemonConfig {
74
111
  routes: RouteRule[];
75
112
  /** If true, stream blocks (only meaningful for rm_oc_* rooms). */
76
113
  streamBlocks: boolean;
114
+ /**
115
+ * Persistent transcript-logging settings (design §3 / §6). Defaults to
116
+ * disabled — see `BOTCORD_TRANSCRIPT` for env-driven temporary overrides.
117
+ */
118
+ transcript?: TranscriptConfig;
119
+ /**
120
+ * Optional registry of OpenClaw gateway endpoints. Routes / managed routes
121
+ * with `adapter === "openclaw-acp"` reference these by `name`. Resolution
122
+ * to {@link ResolvedOpenclawGateway} happens eagerly in `toGatewayConfig`
123
+ * so the dispatcher never re-queries this list.
124
+ */
125
+ openclawGateways?: OpenclawGatewayProfile[];
126
+ /**
127
+ * Daemon-side local OpenClaw discovery. Omitted means enabled with default
128
+ * search paths/ports and automatic adoption of discovered agents.
129
+ */
130
+ openclawDiscovery?: OpenclawDiscoveryConfig;
131
+ }
132
+ /**
133
+ * Persistent transcript settings (design §6). Default-off — `botcord-daemon
134
+ * transcript enable` flips `enabled` and `transcript disable` flips it back.
135
+ * The env var `BOTCORD_TRANSCRIPT` can override at boot.
136
+ */
137
+ export interface TranscriptConfig {
138
+ enabled?: boolean;
77
139
  }
78
140
  /**
79
141
  * Return the explicit agent-id list written to disk, or `null` when the
@@ -97,6 +159,7 @@ export declare function resolveConfiguredAgentIds(cfg: DaemonConfig): string[] |
97
159
  * before discovery). Throws when neither `agents` nor `agentId` is set.
98
160
  */
99
161
  export declare function resolveAgentIds(cfg: DaemonConfig): string[];
162
+ export declare const CONFIG_MISSING = "CONFIG_MISSING";
100
163
  export declare function loadConfig(): DaemonConfig;
101
164
  export declare function saveConfig(cfg: DaemonConfig): void;
102
165
  /**
package/dist/config.js CHANGED
@@ -70,9 +70,12 @@ function ensureDir() {
70
70
  // best-effort
71
71
  }
72
72
  }
73
+ export const CONFIG_MISSING = "CONFIG_MISSING";
73
74
  export function loadConfig() {
74
75
  if (!existsSync(CONFIG_PATH)) {
75
- throw new Error(`daemon config not found at ${CONFIG_PATH}. Run \`botcord-daemon init\` first.`);
76
+ const err = new Error(`daemon config not found at ${CONFIG_PATH}`);
77
+ err.code = CONFIG_MISSING;
78
+ throw err;
76
79
  }
77
80
  const raw = readFileSync(CONFIG_PATH, "utf8");
78
81
  const parsed = JSON.parse(raw);
@@ -96,6 +99,40 @@ export function loadConfig() {
96
99
  throw new Error(`daemon config missing defaultRoute.adapter/cwd (${CONFIG_PATH})`);
97
100
  }
98
101
  validateAdapter(parsed.defaultRoute.adapter, "defaultRoute.adapter");
102
+ const gatewaysRaw = parsed.openclawGateways;
103
+ const gatewayNames = new Set();
104
+ if (gatewaysRaw !== undefined) {
105
+ if (!Array.isArray(gatewaysRaw)) {
106
+ throw new Error(`daemon config "openclawGateways" must be an array (${CONFIG_PATH})`);
107
+ }
108
+ for (const [i, g] of gatewaysRaw.entries()) {
109
+ if (!g || typeof g !== "object") {
110
+ throw new Error(`daemon config openclawGateways[${i}] is not an object (${CONFIG_PATH})`);
111
+ }
112
+ const gg = g;
113
+ if (typeof gg.name !== "string" || gg.name.length === 0) {
114
+ throw new Error(`daemon config openclawGateways[${i}].name must be a non-empty string (${CONFIG_PATH})`);
115
+ }
116
+ if (typeof gg.url !== "string" || gg.url.length === 0) {
117
+ throw new Error(`daemon config openclawGateways[${i}].url must be a non-empty string (${CONFIG_PATH})`);
118
+ }
119
+ if (gatewayNames.has(gg.name)) {
120
+ throw new Error(`daemon config openclawGateways[${i}].name "${gg.name}" duplicated (${CONFIG_PATH})`);
121
+ }
122
+ gatewayNames.add(gg.name);
123
+ }
124
+ }
125
+ const validateGatewayRef = (adapter, gateway, where) => {
126
+ if (adapter === "openclaw-acp") {
127
+ if (typeof gateway !== "string" || gateway.length === 0) {
128
+ throw new Error(`daemon config ${where} adapter "openclaw-acp" requires a "gateway" name (${CONFIG_PATH})`);
129
+ }
130
+ if (!gatewayNames.has(gateway)) {
131
+ throw new Error(`daemon config ${where}.gateway "${gateway}" not in openclawGateways (${CONFIG_PATH})`);
132
+ }
133
+ }
134
+ };
135
+ validateGatewayRef(parsed.defaultRoute.adapter, parsed.defaultRoute.gateway, "defaultRoute");
99
136
  const routesRaw = parsed.routes ?? [];
100
137
  if (!Array.isArray(routesRaw)) {
101
138
  throw new Error(`daemon config "routes" must be an array (${CONFIG_PATH})`);
@@ -108,6 +145,7 @@ export function loadConfig() {
108
145
  throw new Error(`daemon config routes[${i}] missing string adapter/cwd (${CONFIG_PATH})`);
109
146
  }
110
147
  validateAdapter(r.adapter, `routes[${i}].adapter`);
148
+ validateGatewayRef(r.adapter, r.gateway, `routes[${i}]`);
111
149
  }
112
150
  // Preserve the on-disk shape as-is so `config` prints what the user wrote.
113
151
  // Resolution of agents vs agentId happens at the consumption boundary
@@ -117,6 +155,24 @@ export function loadConfig() {
117
155
  routes: routesRaw,
118
156
  streamBlocks: parsed.streamBlocks ?? true,
119
157
  };
158
+ if (parsed.transcript && typeof parsed.transcript === "object") {
159
+ const t = {};
160
+ if (typeof parsed.transcript.enabled === "boolean")
161
+ t.enabled = parsed.transcript.enabled;
162
+ out.transcript = t;
163
+ }
164
+ if (gatewaysRaw && Array.isArray(gatewaysRaw)) {
165
+ out.openclawGateways = gatewaysRaw.map((g) => {
166
+ const copy = { name: g.name, url: g.url };
167
+ if (typeof g.token === "string")
168
+ copy.token = g.token;
169
+ if (typeof g.tokenFile === "string")
170
+ copy.tokenFile = g.tokenFile;
171
+ if (typeof g.defaultAgent === "string")
172
+ copy.defaultAgent = g.defaultAgent;
173
+ return copy;
174
+ });
175
+ }
120
176
  if (hasAgents)
121
177
  out.agents = parsed.agents.slice();
122
178
  if (hasLegacy)
@@ -130,6 +186,22 @@ export function loadConfig() {
130
186
  }
131
187
  out.agentDiscovery = copy;
132
188
  }
189
+ const openclawDiscovery = parsed.openclawDiscovery;
190
+ if (openclawDiscovery && typeof openclawDiscovery === "object") {
191
+ const copy = {};
192
+ if (typeof openclawDiscovery.enabled === "boolean")
193
+ copy.enabled = openclawDiscovery.enabled;
194
+ if (Array.isArray(openclawDiscovery.searchPaths)) {
195
+ copy.searchPaths = openclawDiscovery.searchPaths.filter((p) => typeof p === "string" && p.length > 0);
196
+ }
197
+ if (Array.isArray(openclawDiscovery.defaultPorts)) {
198
+ copy.defaultPorts = openclawDiscovery.defaultPorts.filter((p) => Number.isInteger(p) && p > 0 && p < 65536);
199
+ }
200
+ if (typeof openclawDiscovery.autoProvision === "boolean") {
201
+ copy.autoProvision = openclawDiscovery.autoProvision;
202
+ }
203
+ out.openclawDiscovery = copy;
204
+ }
133
205
  return out;
134
206
  }
135
207
  function validateAdapter(id, field) {
@@ -1,5 +1,29 @@
1
1
  import type { GatewayConfig, GatewayRoute } from "./gateway/index.js";
2
- import type { DaemonConfig } from "./config.js";
2
+ import type { DaemonConfig, OpenclawGatewayProfile } from "./config.js";
3
+ /** Per-agent metadata cached from credentials, used by `buildManagedRoutes`. */
4
+ export interface AgentRuntimeMeta {
5
+ runtime?: string;
6
+ cwd?: string;
7
+ /** OpenClaw gateway profile name to lookup in the registry. */
8
+ openclawGateway?: string;
9
+ /** Optional override of the OpenClaw agent profile within the gateway. */
10
+ openclawAgent?: string;
11
+ }
12
+ /** Profile + tokenFile-resolved bearer token. Exported so other module-boundary
13
+ * paths (runtime probing, post-provision hot-add) reuse the same resolver
14
+ * instead of duplicating tokenFile semantics. */
15
+ export interface PreparedGatewayProfile extends OpenclawGatewayProfile {
16
+ /** Token actually usable at dispatch time; empty when load failed. */
17
+ resolvedToken?: string;
18
+ /** Reason `resolvedToken` is empty, for logs. */
19
+ tokenError?: string;
20
+ }
21
+ /** Resolve one profile's token (inline > tokenFile). Failures are swallowed
22
+ * into `tokenError`; `resolvedToken` is left undefined. Logs at warn for ops
23
+ * visibility. */
24
+ export declare function prepareGatewayProfile(p: OpenclawGatewayProfile): PreparedGatewayProfile;
25
+ /** Build a name → prepared-profile map for a config's gateway registry. */
26
+ export declare function prepareGatewayProfiles(profiles: OpenclawGatewayProfile[] | undefined): Map<string, PreparedGatewayProfile>;
3
27
  /** Options accepted by {@link toGatewayConfig}. */
4
28
  export interface ToGatewayConfigOptions {
5
29
  /**
@@ -14,10 +38,7 @@ export interface ToGatewayConfigOptions {
14
38
  * turns to its runtime. Explicit `cfg.routes` entries still win because
15
39
  * synthesized routes are appended after them.
16
40
  */
17
- agentRuntimes?: Record<string, {
18
- runtime?: string;
19
- cwd?: string;
20
- }>;
41
+ agentRuntimes?: Record<string, AgentRuntimeMeta>;
21
42
  }
22
43
  /**
23
44
  * Historical channel id used when the daemon bound a single agent. Kept as a
@@ -54,7 +75,4 @@ export declare function toGatewayConfig(cfg: DaemonConfig, opts?: ToGatewayConfi
54
75
  * Exported so `reload_config` and `provisionAgent` hot-add can share the
55
76
  * same synthesis logic (plan §10.5).
56
77
  */
57
- export declare function buildManagedRoutes(agentIds: string[], agentRuntimes: Record<string, {
58
- runtime?: string;
59
- cwd?: string;
60
- }>, defaultRoute: GatewayRoute): Map<string, GatewayRoute>;
78
+ export declare function buildManagedRoutes(agentIds: string[], agentRuntimes: Record<string, AgentRuntimeMeta>, defaultRoute: GatewayRoute, openclawProfiles?: Map<string, PreparedGatewayProfile>): Map<string, GatewayRoute>;