@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.
- package/dist/agent-discovery.d.ts +4 -0
- package/dist/agent-discovery.js +8 -0
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -8
- package/dist/config.d.ts +49 -1
- package/dist/config.js +57 -1
- package/dist/daemon-config-map.d.ts +27 -9
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +52 -5
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.js +22 -1
- package/dist/gateway/cli-resolver.d.ts +34 -0
- package/dist/gateway/cli-resolver.js +74 -0
- package/dist/gateway/dispatcher.d.ts +31 -1
- package/dist/gateway/dispatcher.js +337 -29
- package/dist/gateway/gateway.d.ts +29 -1
- package/dist/gateway/gateway.js +10 -0
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +2 -0
- package/dist/gateway/policy-resolver.d.ts +57 -0
- package/dist/gateway/policy-resolver.js +123 -0
- package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
- package/dist/gateway/runtimes/acp-stream.js +394 -0
- package/dist/gateway/runtimes/codex.js +7 -0
- package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
- package/dist/gateway/runtimes/hermes-agent.js +180 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
- package/dist/gateway/runtimes/ndjson-stream.js +16 -3
- package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
- package/dist/gateway/runtimes/openclaw-acp.js +500 -0
- package/dist/gateway/runtimes/registry.d.ts +4 -0
- package/dist/gateway/runtimes/registry.js +22 -0
- package/dist/gateway/transcript-paths.d.ts +30 -0
- package/dist/gateway/transcript-paths.js +114 -0
- package/dist/gateway/transcript.d.ts +123 -0
- package/dist/gateway/transcript.js +147 -0
- package/dist/gateway/types.d.ts +31 -0
- package/dist/index.js +286 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/provision.d.ts +72 -1
- package/dist/provision.js +370 -7
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/package.json +2 -1
- package/src/__tests__/agent-workspace.test.ts +93 -0
- package/src/__tests__/daemon-config-map.test.ts +79 -0
- package/src/__tests__/openclaw-acp.test.ts +234 -0
- package/src/__tests__/policy-resolver.test.ts +124 -0
- package/src/__tests__/policy-updated-handler.test.ts +144 -0
- package/src/__tests__/provision.test.ts +160 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +8 -0
- package/src/agent-workspace.ts +173 -7
- package/src/config.ts +132 -4
- package/src/daemon-config-map.ts +154 -9
- package/src/daemon.ts +66 -5
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +65 -0
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
- package/src/gateway/__tests__/transcript.test.ts +496 -0
- package/src/gateway/cli-resolver.ts +92 -0
- package/src/gateway/dispatcher.ts +394 -26
- package/src/gateway/gateway.ts +46 -0
- package/src/gateway/index.ts +25 -0
- package/src/gateway/policy-resolver.ts +171 -0
- package/src/gateway/runtimes/acp-stream.ts +535 -0
- package/src/gateway/runtimes/codex.ts +7 -0
- package/src/gateway/runtimes/hermes-agent.ts +206 -0
- package/src/gateway/runtimes/ndjson-stream.ts +16 -3
- package/src/gateway/runtimes/openclaw-acp.ts +606 -0
- package/src/gateway/runtimes/registry.ts +24 -0
- package/src/gateway/transcript-paths.ts +145 -0
- package/src/gateway/transcript.ts +300 -0
- package/src/gateway/types.ts +32 -0
- package/src/index.ts +295 -30
- package/src/mention-scan.ts +38 -0
- package/src/provision.ts +438 -9
- package/src/system-context.ts +41 -9
- 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. */
|
package/dist/agent-discovery.js
CHANGED
|
@@ -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;
|
package/dist/agent-workspace.js
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
76
|
-
response, but nothing loads it automatically; keep it
|
|
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
|
|
@@ -74,6 +101,26 @@ export interface DaemonConfig {
|
|
|
74
101
|
routes: RouteRule[];
|
|
75
102
|
/** If true, stream blocks (only meaningful for rm_oc_* rooms). */
|
|
76
103
|
streamBlocks: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Persistent transcript-logging settings (design §3 / §6). Defaults to
|
|
106
|
+
* disabled — see `BOTCORD_TRANSCRIPT` for env-driven temporary overrides.
|
|
107
|
+
*/
|
|
108
|
+
transcript?: TranscriptConfig;
|
|
109
|
+
/**
|
|
110
|
+
* Optional registry of OpenClaw gateway endpoints. Routes / managed routes
|
|
111
|
+
* with `adapter === "openclaw-acp"` reference these by `name`. Resolution
|
|
112
|
+
* to {@link ResolvedOpenclawGateway} happens eagerly in `toGatewayConfig`
|
|
113
|
+
* so the dispatcher never re-queries this list.
|
|
114
|
+
*/
|
|
115
|
+
openclawGateways?: OpenclawGatewayProfile[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Persistent transcript settings (design §6). Default-off — `botcord-daemon
|
|
119
|
+
* transcript enable` flips `enabled` and `transcript disable` flips it back.
|
|
120
|
+
* The env var `BOTCORD_TRANSCRIPT` can override at boot.
|
|
121
|
+
*/
|
|
122
|
+
export interface TranscriptConfig {
|
|
123
|
+
enabled?: boolean;
|
|
77
124
|
}
|
|
78
125
|
/**
|
|
79
126
|
* Return the explicit agent-id list written to disk, or `null` when the
|
|
@@ -97,6 +144,7 @@ export declare function resolveConfiguredAgentIds(cfg: DaemonConfig): string[] |
|
|
|
97
144
|
* before discovery). Throws when neither `agents` nor `agentId` is set.
|
|
98
145
|
*/
|
|
99
146
|
export declare function resolveAgentIds(cfg: DaemonConfig): string[];
|
|
147
|
+
export declare const CONFIG_MISSING = "CONFIG_MISSING";
|
|
100
148
|
export declare function loadConfig(): DaemonConfig;
|
|
101
149
|
export declare function saveConfig(cfg: DaemonConfig): void;
|
|
102
150
|
/**
|
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
|
-
|
|
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)
|
|
@@ -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>;
|