@botcord/daemon 0.2.27 → 0.2.28

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.
@@ -28,6 +28,8 @@ export interface DiscoveredAgentCredential {
28
28
  openclawGateway?: string;
29
29
  /** OpenClaw agent profile override from credentials. */
30
30
  openclawAgent?: string;
31
+ /** Hermes profile name from credentials (only meaningful for hermes-agent). */
32
+ hermesProfile?: string;
31
33
  /** Key id from the credentials file — surfaced so boot-time workspace
32
34
  * seeding (see daemon-agent-workspace-plan.md §9) can render identity.md
33
35
  * without re-reading the file. */
@@ -102,6 +102,8 @@ export function discoverAgentCredentials(opts = {}) {
102
102
  entry.openclawGateway = creds.openclawGateway;
103
103
  if (creds.openclawAgent)
104
104
  entry.openclawAgent = creds.openclawAgent;
105
+ if (creds.hermesProfile)
106
+ entry.hermesProfile = creds.hermesProfile;
105
107
  if (creds.keyId)
106
108
  entry.keyId = creds.keyId;
107
109
  if (creds.savedAt)
@@ -48,7 +48,9 @@ export declare function ensureAgentCodexHome(agentId: string): string;
48
48
  * loader (which only sees this isolated HERMES_HOME, not `~/.hermes`)
49
49
  * can discover them.
50
50
  */
51
- export declare function ensureAgentHermesWorkspace(agentId: string): {
51
+ export declare function ensureAgentHermesWorkspace(agentId: string, opts?: {
52
+ attached?: boolean;
53
+ }): {
52
54
  hermesHome: string;
53
55
  hermesWorkspace: string;
54
56
  };
@@ -327,11 +327,19 @@ export function ensureAgentCodexHome(agentId) {
327
327
  * loader (which only sees this isolated HERMES_HOME, not `~/.hermes`)
328
328
  * can discover them.
329
329
  */
330
- export function ensureAgentHermesWorkspace(agentId) {
330
+ export function ensureAgentHermesWorkspace(agentId, opts = {}) {
331
331
  const hermesHome = agentHermesHomeDir(agentId);
332
332
  const hermesWorkspace = agentHermesWorkspaceDir(agentId);
333
- mkdirTolerant(hermesHome);
334
333
  mkdirTolerant(hermesWorkspace);
334
+ // Attach mode: HERMES_HOME points at the user's `~/.hermes/profiles/<n>/`
335
+ // so we MUST NOT touch the per-agent isolated home. The cwd
336
+ // (`hermesWorkspace`) is still ours and `prepareTurn` writes AGENTS.md
337
+ // there — that's the only thing the daemon is allowed to author when
338
+ // attached to a user-owned profile.
339
+ if (opts.attached) {
340
+ return { hermesHome, hermesWorkspace };
341
+ }
342
+ mkdirTolerant(hermesHome);
335
343
  writeIfMissing(path.join(hermesHome, ".env"), "# hermes-agent environment overrides for this BotCord agent.\n" +
336
344
  "# Add e.g. HERMES_INFERENCE_PROVIDER=openrouter, OPENROUTER_API_KEY=...\n");
337
345
  seedHermesConfig(hermesHome);
@@ -8,6 +8,8 @@ export interface AgentRuntimeMeta {
8
8
  openclawGateway?: string;
9
9
  /** Optional override of the OpenClaw agent profile within the gateway. */
10
10
  openclawAgent?: string;
11
+ /** Hermes profile name to attach to (`runtime === "hermes-agent"` only). */
12
+ hermesProfile?: string;
11
13
  }
12
14
  /** Profile + tokenFile-resolved bearer token. Exported so other module-boundary
13
15
  * paths (runtime probing, post-provision hot-add) reuse the same resolver
@@ -245,6 +245,9 @@ export function buildManagedRoutes(agentIds, agentRuntimes, defaultRoute, opencl
245
245
  }
246
246
  route.gateway = resolved;
247
247
  }
248
+ if (runtime === "hermes-agent" && meta.hermesProfile) {
249
+ route.hermesProfile = meta.hermesProfile;
250
+ }
248
251
  out.set(agentId, route);
249
252
  }
250
253
  return out;
package/dist/daemon.d.ts CHANGED
@@ -110,6 +110,7 @@ export interface BootBackfillResult {
110
110
  cwd?: string;
111
111
  openclawGateway?: string;
112
112
  openclawAgent?: string;
113
+ hermesProfile?: string;
113
114
  }>;
114
115
  }
115
116
  /**
package/dist/daemon.js CHANGED
@@ -425,12 +425,13 @@ export function backfillBootAgents(agents, opts) {
425
425
  for (const a of agents) {
426
426
  if (a.credentialsFile)
427
427
  credentialPathByAgentId.set(a.agentId, a.credentialsFile);
428
- if (a.runtime || a.cwd || a.openclawGateway || a.openclawAgent) {
428
+ if (a.runtime || a.cwd || a.openclawGateway || a.openclawAgent || a.hermesProfile) {
429
429
  agentRuntimes[a.agentId] = {
430
430
  ...(a.runtime ? { runtime: a.runtime } : {}),
431
431
  ...(a.cwd ? { cwd: a.cwd } : {}),
432
432
  ...(a.openclawGateway ? { openclawGateway: a.openclawGateway } : {}),
433
433
  ...(a.openclawAgent ? { openclawAgent: a.openclawAgent } : {}),
434
+ ...(a.hermesProfile ? { hermesProfile: a.hermesProfile } : {}),
434
435
  };
435
436
  }
436
437
  // Seed files are written only when missing (see `ensureAgentWorkspace`),
@@ -837,6 +837,7 @@ export class Dispatcher {
837
837
  onBlock,
838
838
  onStatus,
839
839
  gateway: route.gateway,
840
+ ...(route.hermesProfile ? { hermesProfile: route.hermesProfile } : {}),
840
841
  });
841
842
  }
842
843
  catch (err) {
@@ -9,6 +9,41 @@ import type { RuntimeProbeResult, RuntimeRunOptions } from "../types.js";
9
9
  export declare function resolveHermesAcpCommand(deps?: ProbeDeps): string | null;
10
10
  /** Probe whether `hermes-acp` is installed and report its version. */
11
11
  export declare function probeHermesAgent(deps?: ProbeDeps): RuntimeProbeResult;
12
+ /**
13
+ * Discovered hermes profile entry (daemon-side shape; wire shape lives in
14
+ * protocol-core's `HermesProfileProbe`). Occupancy is filled in later by
15
+ * `provision.ts` from local credentials, not here.
16
+ */
17
+ export interface HermesProfileInfo {
18
+ name: string;
19
+ home: string;
20
+ isDefault?: boolean;
21
+ isActive?: boolean;
22
+ modelName?: string;
23
+ sessionsCount?: number;
24
+ hasSoul?: boolean;
25
+ }
26
+ /**
27
+ * Resolve the hermes root (`~/.hermes`) — this is the location of the
28
+ * synthetic `default` profile per upstream's "default profile = HERMES_HOME
29
+ * itself" convention (`hermes_cli/profiles.py:8`).
30
+ */
31
+ export declare function hermesRootDir(): string;
32
+ export declare function isValidHermesProfileName(name: string): boolean;
33
+ /**
34
+ * Resolve a hermes profile's HERMES_HOME directory. `default` maps to
35
+ * `~/.hermes`; all other names map to `~/.hermes/profiles/<name>`. Mirrors
36
+ * `hermes_cli/profiles.py:get_profile_dir`.
37
+ */
38
+ export declare function hermesProfileHomeDir(name: string): string;
39
+ /**
40
+ * Enumerate available hermes profiles on this device. Pure local filesystem
41
+ * scan — does not invoke any hermes binary. Returns the synthetic `default`
42
+ * entry first when `~/.hermes` exists (which it should, given that the probe
43
+ * already located `hermes-acp`); each `~/.hermes/profiles/<name>/` directory
44
+ * follows.
45
+ */
46
+ export declare function listHermesProfiles(): HermesProfileInfo[];
12
47
  /**
13
48
  * Hermes Agent adapter. Drives `hermes-acp` (the ACP stdio adapter shipped
14
49
  * with `pip install "hermes-agent[acp]"`).
@@ -1,4 +1,5 @@
1
- import { mkdirSync, renameSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
  import { agentHermesHomeDir, agentHermesWorkspaceDir, ensureAgentHermesWorkspace, } from "../../agent-workspace.js";
4
5
  import { buildCliEnv } from "../cli-resolver.js";
@@ -43,6 +44,122 @@ export function probeHermesAgent(deps = {}) {
43
44
  version: readCommandVersion(command, [], deps) ?? undefined,
44
45
  };
45
46
  }
47
+ /**
48
+ * Resolve the hermes root (`~/.hermes`) — this is the location of the
49
+ * synthetic `default` profile per upstream's "default profile = HERMES_HOME
50
+ * itself" convention (`hermes_cli/profiles.py:8`).
51
+ */
52
+ export function hermesRootDir() {
53
+ return path.join(homedir(), ".hermes");
54
+ }
55
+ /** Profile-name shape mirrors `hermes_cli/profiles.py:_PROFILE_ID_RE`. */
56
+ const HERMES_PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
57
+ export function isValidHermesProfileName(name) {
58
+ return name === "default" || HERMES_PROFILE_NAME_RE.test(name);
59
+ }
60
+ /**
61
+ * Resolve a hermes profile's HERMES_HOME directory. `default` maps to
62
+ * `~/.hermes`; all other names map to `~/.hermes/profiles/<name>`. Mirrors
63
+ * `hermes_cli/profiles.py:get_profile_dir`.
64
+ */
65
+ export function hermesProfileHomeDir(name) {
66
+ if (!isValidHermesProfileName(name)) {
67
+ throw new Error(`Invalid hermes profile name: ${name}`);
68
+ }
69
+ if (name === "default")
70
+ return hermesRootDir();
71
+ return path.join(hermesRootDir(), "profiles", name);
72
+ }
73
+ function readActiveProfileName() {
74
+ try {
75
+ const raw = readFileSync(path.join(hermesRootDir(), "active_profile"), "utf8").trim();
76
+ return raw || "default";
77
+ }
78
+ catch {
79
+ return "default";
80
+ }
81
+ }
82
+ function readProfileModelName(profileHome) {
83
+ try {
84
+ const raw = readFileSync(path.join(profileHome, "config.yaml"), "utf8");
85
+ // Cheap surface-level YAML peek — config.yaml's first block is
86
+ // `model:\n default: <name>`. Avoid pulling in a YAML dependency for
87
+ // a single optional field.
88
+ const match = raw.match(/^model:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+default:\s*([^\n#]+)/m);
89
+ if (!match)
90
+ return undefined;
91
+ return match[1].trim().replace(/^['"]|['"]$/g, "") || undefined;
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
97
+ function countSessions(profileHome) {
98
+ try {
99
+ const dir = path.join(profileHome, "sessions");
100
+ if (!existsSync(dir))
101
+ return 0;
102
+ return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).length;
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ function hasSoul(profileHome) {
109
+ return existsSync(path.join(profileHome, "SOUL.md"));
110
+ }
111
+ /**
112
+ * Enumerate available hermes profiles on this device. Pure local filesystem
113
+ * scan — does not invoke any hermes binary. Returns the synthetic `default`
114
+ * entry first when `~/.hermes` exists (which it should, given that the probe
115
+ * already located `hermes-acp`); each `~/.hermes/profiles/<name>/` directory
116
+ * follows.
117
+ */
118
+ export function listHermesProfiles() {
119
+ const out = [];
120
+ const root = hermesRootDir();
121
+ const active = readActiveProfileName();
122
+ if (existsSync(root)) {
123
+ out.push({
124
+ name: "default",
125
+ home: root,
126
+ isDefault: true,
127
+ isActive: active === "default",
128
+ modelName: readProfileModelName(root),
129
+ sessionsCount: countSessions(root),
130
+ hasSoul: hasSoul(root),
131
+ });
132
+ }
133
+ const profilesDir = path.join(root, "profiles");
134
+ let entries = [];
135
+ try {
136
+ entries = readdirSync(profilesDir);
137
+ }
138
+ catch {
139
+ return out;
140
+ }
141
+ for (const name of entries) {
142
+ if (!HERMES_PROFILE_NAME_RE.test(name))
143
+ continue;
144
+ const home = path.join(profilesDir, name);
145
+ try {
146
+ if (!statSync(home).isDirectory())
147
+ continue;
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ out.push({
153
+ name,
154
+ home,
155
+ isActive: active === name,
156
+ modelName: readProfileModelName(home),
157
+ sessionsCount: countSessions(home),
158
+ hasSoul: hasSoul(home),
159
+ });
160
+ }
161
+ return out;
162
+ }
46
163
  /**
47
164
  * Hermes Agent adapter. Drives `hermes-acp` (the ACP stdio adapter shipped
48
165
  * with `pip install "hermes-agent[acp]"`).
@@ -115,7 +232,15 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
115
232
  // Route dangerous tool calls through ACP request_permission.
116
233
  HERMES_INTERACTIVE: "1",
117
234
  };
118
- if (opts.accountId) {
235
+ // Attach mode: BotCord agent shares a hermes profile (state.db /
236
+ // sessions / skills / .env) with the user's command-line `hermes`. In
237
+ // this mode we DO NOT seed a private home — the profile is wholly owned
238
+ // by the user, and AGENTS.md is written under the per-agent
239
+ // hermes-workspace cwd (NOT into the profile root) by `prepareTurn`.
240
+ if (opts.hermesProfile) {
241
+ env.HERMES_HOME = hermesProfileHomeDir(opts.hermesProfile);
242
+ }
243
+ else if (opts.accountId) {
119
244
  env.HERMES_HOME = agentHermesHomeDir(opts.accountId);
120
245
  }
121
246
  return env;
@@ -134,7 +259,9 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
134
259
  prepareTurn(opts) {
135
260
  if (!opts.accountId)
136
261
  return;
137
- const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId);
262
+ const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId, {
263
+ attached: !!opts.hermesProfile,
264
+ });
138
265
  const target = path.join(hermesWorkspace, "AGENTS.md");
139
266
  const tmp = path.join(hermesWorkspace, `.AGENTS.md.${process.pid}.tmp`);
140
267
  mkdirSync(hermesWorkspace, { recursive: true, mode: 0o700 });
@@ -12,9 +12,11 @@ interface SpawnDeps {
12
12
  *
13
13
  * Spawns `openclaw acp --url <gateway> [--token <token>]` per
14
14
  * `(accountId, gatewayName)` pair and reuses the process across turns. The
15
- * child speaks JSON-RPC over stdio; we send `initialize` once, then
16
- * `newSession` (with `_meta.sessionKey`) when the daemon has no persisted
17
- * runtime session id, and `prompt` for each turn. Streaming `session/update`
15
+ * child speaks JSON-RPC over stdio; we send `initialize` once, then derive a
16
+ * stable OpenClaw `sessionKey` for the BotCord conversation. The persisted
17
+ * `runtimeSessionId` is only an ACP transport handle cached from a previous
18
+ * turn, so every resume first goes through `session/load` with
19
+ * `_meta.sessionKey` before `prompt`. Streaming `session/update`
18
20
  * notifications are relayed to `onBlock`.
19
21
  *
20
22
  * Process-pool lifetime + abort/cancel semantics live at module scope; see
@@ -29,6 +31,7 @@ export declare class OpenclawAcpAdapter implements RuntimeAdapter {
29
31
  private acquireHandle;
30
32
  private spawnAcpProcess;
31
33
  private newSession;
34
+ private loadSession;
32
35
  private prompt;
33
36
  }
34
37
  /**
@@ -75,9 +75,11 @@ export function probeOpenclaw(deps = {}) {
75
75
  *
76
76
  * Spawns `openclaw acp --url <gateway> [--token <token>]` per
77
77
  * `(accountId, gatewayName)` pair and reuses the process across turns. The
78
- * child speaks JSON-RPC over stdio; we send `initialize` once, then
79
- * `newSession` (with `_meta.sessionKey`) when the daemon has no persisted
80
- * runtime session id, and `prompt` for each turn. Streaming `session/update`
78
+ * child speaks JSON-RPC over stdio; we send `initialize` once, then derive a
79
+ * stable OpenClaw `sessionKey` for the BotCord conversation. The persisted
80
+ * `runtimeSessionId` is only an ACP transport handle cached from a previous
81
+ * turn, so every resume first goes through `session/load` with
82
+ * `_meta.sessionKey` before `prompt`. Streaming `session/update`
81
83
  * notifications are relayed to `onBlock`.
82
84
  *
83
85
  * Process-pool lifetime + abort/cancel semantics live at module scope; see
@@ -117,6 +119,8 @@ export class OpenclawAcpAdapter {
117
119
  handle.inFlight += 1;
118
120
  if (handle.idleTimer)
119
121
  clearTimeout(handle.idleTimer);
122
+ // ACP session ids are process-local transport handles. They are useful as
123
+ // a cache, but the stable conversation identity is `sessionKey`.
120
124
  let acpSessionId = opts.sessionId ?? "";
121
125
  let seq = 0;
122
126
  let assistantText = "";
@@ -155,8 +159,28 @@ export class OpenclawAcpAdapter {
155
159
  };
156
160
  let abortListener;
157
161
  try {
158
- // Ensure we have an ACP session id. When the dispatcher doesn't carry
159
- // one, ask the child to create or rebind one for our sessionKey.
162
+ // Ensure we have a live ACP transport session. If the dispatcher passes a
163
+ // cached session id, ask OpenClaw to load/rebind it with the stable
164
+ // sessionKey. If that handle is gone, discard it and create a fresh one.
165
+ if (acpSessionId) {
166
+ try {
167
+ acpSessionId = await this.loadSession(handle, {
168
+ sessionId: acpSessionId,
169
+ cwd: opts.cwd,
170
+ sessionKey,
171
+ });
172
+ }
173
+ catch (err) {
174
+ if (!isSessionNotFoundError(err))
175
+ throw err;
176
+ log.warn("openclaw-acp.session-load-not-found", {
177
+ accountId: opts.accountId,
178
+ oldSessionId: acpSessionId,
179
+ sessionKey,
180
+ });
181
+ acpSessionId = "";
182
+ }
183
+ }
160
184
  if (!acpSessionId) {
161
185
  try {
162
186
  acpSessionId = await this.newSession(handle, {
@@ -185,11 +209,16 @@ export class OpenclawAcpAdapter {
185
209
  });
186
210
  }
187
211
  catch (err) {
188
- const msg = err.message ?? "prompt failed";
189
212
  // If the child says the session is gone (process restart, GC),
190
213
  // recreate it so the next turn doesn't hard-fail.
191
- if (/session not found|unknown session/i.test(msg)) {
214
+ if (isSessionNotFoundError(err)) {
192
215
  try {
216
+ const oldSessionId = acpSessionId;
217
+ log.warn("openclaw-acp.prompt-session-not-found-retry", {
218
+ accountId: opts.accountId,
219
+ oldSessionId,
220
+ sessionKey,
221
+ });
193
222
  const fresh = await this.newSession(handle, {
194
223
  cwd: opts.cwd,
195
224
  sessionKey,
@@ -197,6 +226,12 @@ export class OpenclawAcpAdapter {
197
226
  handle.subscribers.delete(acpSessionId);
198
227
  acpSessionId = fresh;
199
228
  handle.subscribers.set(acpSessionId, onNotification);
229
+ log.info("openclaw-acp.session-recreated", {
230
+ accountId: opts.accountId,
231
+ oldSessionId,
232
+ newSessionId: acpSessionId,
233
+ sessionKey,
234
+ });
200
235
  promptResult = await this.prompt(handle, {
201
236
  sessionId: acpSessionId,
202
237
  text: opts.text,
@@ -223,7 +258,8 @@ export class OpenclawAcpAdapter {
223
258
  };
224
259
  }
225
260
  catch (err) {
226
- return failResult(acpSessionId, `openclaw-acp: ${err.message}`);
261
+ const message = err instanceof Error ? err.message : String(err);
262
+ return failResult(isSessionNotFoundError(err) ? "" : acpSessionId, `openclaw-acp: ${message}`);
227
263
  }
228
264
  finally {
229
265
  if (abortListener && opts.signal) {
@@ -332,6 +368,18 @@ export class OpenclawAcpAdapter {
332
368
  }
333
369
  return result.sessionId;
334
370
  }
371
+ async loadSession(handle, args) {
372
+ const result = (await sendRequest(handle, "session/load", {
373
+ sessionId: args.sessionId,
374
+ cwd: args.cwd,
375
+ mcpServers: [],
376
+ _meta: { sessionKey: args.sessionKey },
377
+ }));
378
+ if (result?.sessionId && typeof result.sessionId === "string") {
379
+ return result.sessionId;
380
+ }
381
+ return args.sessionId;
382
+ }
335
383
  async prompt(handle, args) {
336
384
  return sendRequest(handle, "session/prompt", {
337
385
  sessionId: args.sessionId,
@@ -372,7 +420,7 @@ function routeMessage(handle, msg) {
372
420
  return;
373
421
  handle.pending.delete(id);
374
422
  if (msg.error) {
375
- const message = typeof msg.error?.message === "string" ? msg.error.message : "rpc error";
423
+ const message = formatRpcError(msg.error);
376
424
  pending.reject(new Error(message));
377
425
  }
378
426
  else {
@@ -435,6 +483,24 @@ function failResult(sessionId, error) {
435
483
  error,
436
484
  };
437
485
  }
486
+ function formatRpcError(error) {
487
+ if (!error || typeof error !== "object")
488
+ return "rpc error";
489
+ const e = error;
490
+ const message = typeof e.message === "string" ? e.message : "rpc error";
491
+ const data = e.data;
492
+ if (data && typeof data === "object") {
493
+ const details = data.details;
494
+ if (typeof details === "string" && details.length > 0) {
495
+ return `${message}: ${details}`;
496
+ }
497
+ }
498
+ return message;
499
+ }
500
+ function isSessionNotFoundError(err) {
501
+ const msg = err instanceof Error ? err.message : String(err);
502
+ return /session(?:\s+[\w-]+)?\s+not\s+found|unknown\s+session/i.test(msg);
503
+ }
438
504
  function classifyAcpUpdate(note) {
439
505
  const update = note.params?.update;
440
506
  const kind = update?.sessionUpdate;
@@ -36,6 +36,14 @@ export interface GatewayRoute {
36
36
  trustLevel?: TrustLevel;
37
37
  /** Required when `runtime === "openclaw-acp"`. Resolved at config-load time. */
38
38
  gateway?: ResolvedOpenclawGateway;
39
+ /**
40
+ * Hermes profile name to attach to. Set when `runtime === "hermes-agent"`
41
+ * and the agent is bound to a specific `~/.hermes/profiles/<name>/`. The
42
+ * dispatcher forwards this to the adapter as
43
+ * {@link RuntimeRunOptions.hermesProfile}, which is what the adapter uses
44
+ * to switch `HERMES_HOME` at spawn time.
45
+ */
46
+ hermesProfile?: string;
39
47
  }
40
48
  /**
41
49
  * Per-channel configuration entry. Channel-specific extras (e.g. BotCord
@@ -306,6 +314,15 @@ export interface RuntimeRunOptions {
306
314
  * lifting service URLs out of `extraArgs` into typed first-class fields.
307
315
  */
308
316
  gateway?: ResolvedOpenclawGateway;
317
+ /**
318
+ * Hermes profile to attach to. Only meaningful when `runtime ===
319
+ * "hermes-agent"`. When set, the adapter switches
320
+ * `HERMES_HOME=~/.hermes/profiles/<name>/` (or `~/.hermes` for `default`)
321
+ * so the BotCord agent shares state.db / sessions / skills with the
322
+ * user's command-line `hermes`. Mirrors how `gateway` is lifted out of
323
+ * `extraArgs` for the openclaw-acp runtime.
324
+ */
325
+ hermesProfile?: string;
309
326
  }
310
327
  /** Result returned by a runtime adapter after a turn completes. */
311
328
  export interface RuntimeRunResult {
@@ -1,6 +1,6 @@
1
1
  import type { DaemonConfig, OpenclawGatewayProfile } from "./config.js";
2
2
  import { type WsEndpointProbeFn } from "./provision.js";
3
- export type DiscoveredOpenclawGatewaySource = "config-file" | "env" | "default-port";
3
+ export type DiscoveredOpenclawGatewaySource = "config-file" | "env" | "systemd-unit" | "default-port";
4
4
  export interface DiscoveredOpenclawGateway {
5
5
  name: string;
6
6
  url: string;
@@ -14,6 +14,7 @@ export interface OpenclawGatewayDiscoveryOptions {
14
14
  probe?: WsEndpointProbeFn;
15
15
  timeoutMs?: number;
16
16
  env?: NodeJS.ProcessEnv;
17
+ systemdUnitPaths?: string[];
17
18
  }
18
19
  export interface MergeOpenclawGatewayResult {
19
20
  cfg: DaemonConfig;
@@ -25,5 +26,6 @@ export declare function mergeOpenclawGateways(cfg: DaemonConfig, found: Discover
25
26
  export declare function defaultOpenclawDiscoverySearchPaths(): string[];
26
27
  export declare function defaultOpenclawDiscoveryPorts(): number[];
27
28
  export declare function defaultOpenclawDiscoveryTokenFilePaths(): string[];
29
+ export declare function defaultOpenclawDiscoverySystemdUnitPaths(): string[];
28
30
  export declare function openclawDiscoveryConfigEnabled(cfg: DaemonConfig): boolean;
29
31
  export declare function openclawAutoProvisionEnabled(cfg: DaemonConfig): boolean;