@botcord/daemon 0.2.88 → 0.2.90

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.
@@ -91,14 +91,16 @@ export const hermesAgentModule: RuntimeModule = {
91
91
  'Install: pip install "hermes-agent[acp]" (or set BOTCORD_HERMES_AGENT_BIN to the absolute path of hermes-acp)',
92
92
  };
93
93
 
94
- /** Built-in runtime module entry for Gemini (probe-only stub). */
94
+ /** Built-in runtime module entry for Gemini CLI. */
95
95
  export const geminiModule: RuntimeModule = {
96
96
  id: "gemini",
97
97
  displayName: "Gemini CLI",
98
98
  binary: "gemini",
99
+ envVar: "BOTCORD_GEMINI_BIN",
99
100
  probe: () => probeGemini(),
100
101
  create: () => new GeminiAdapter(),
101
- supportsRun: false,
102
+ installHint:
103
+ "Install with `npm install -g @google/gemini-cli` (or `brew install gemini-cli`) and run `gemini` once to complete authentication. Override the binary with BOTCORD_GEMINI_BIN.",
102
104
  };
103
105
 
104
106
  /** Built-in runtime module entry for OpenClaw (ACP). */
package/src/provision.ts CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  type StoredBotCordCredentials,
29
29
  type UpdateAgentParams,
30
30
  type GatewayInboundFrame,
31
+ type InstallAgentSkillParams,
32
+ type ListAgentSkillsParams,
31
33
  } from "@botcord/protocol-core";
32
34
  import type { Gateway } from "./gateway/index.js";
33
35
  import type { GatewayInboundMessage } from "./gateway/index.js";
@@ -75,7 +77,12 @@ import { log as daemonLog } from "./log.js";
75
77
  import { discoverAgentCredentials } from "./agent-discovery.js";
76
78
  import { resolveMemoryDir } from "./working-memory.js";
77
79
  import { discoverRuntimeModelCatalog } from "./runtime-models.js";
78
- import { collectAgentSkillSnapshot } from "./skill-index.js";
80
+ import { collectAgentSkillSnapshot, type SkillIndexOptions } from "./skill-index.js";
81
+ import {
82
+ installAgentSkillManifest,
83
+ installBotLearnArchiveManifest,
84
+ installVercelSkillsForAgent,
85
+ } from "./skill-installer.js";
79
86
  import {
80
87
  buildRuntimeSelectionExtraArgs,
81
88
  mergeRuntimeExtraArgs,
@@ -85,14 +92,21 @@ import {
85
92
  type CloudGatewayTypingEmitter,
86
93
  } from "./cloud-gateway-runtime.js";
87
94
 
88
- interface ListAgentSkillsParams {
89
- agentId: string;
90
- }
91
-
92
- function runtimeForLoadedAgent(gateway: Gateway, agentId: string): string | undefined {
93
- return gateway.listManagedRoutes()
94
- .find((route) => route.match?.accountId === agentId)
95
- ?.runtime;
95
+ function skillIndexOptionsForLoadedAgent(gateway: Gateway, agentId: string): SkillIndexOptions {
96
+ const route = gateway.listManagedRoutes()
97
+ .find((entry) => entry.match?.accountId === agentId);
98
+ let credentials: StoredBotCordCredentials | null = null;
99
+ try {
100
+ credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
101
+ } catch {
102
+ credentials = null;
103
+ }
104
+ const runtime = route?.runtime ?? credentials?.runtime;
105
+ const hermesProfile = route?.hermesProfile ?? credentials?.hermesProfile;
106
+ return {
107
+ ...(runtime ? { runtime } : {}),
108
+ ...(hermesProfile ? { hermesProfile } : {}),
109
+ };
96
110
  }
97
111
 
98
112
  /**
@@ -108,6 +122,7 @@ export interface InstalledAgentInfo {
108
122
  hubUrl: string;
109
123
  displayName?: string;
110
124
  runtime?: string;
125
+ hermesProfile?: string;
111
126
  }
112
127
 
113
128
  /**
@@ -516,16 +531,77 @@ export function createProvisioner(opts: ProvisionerOptions): (
516
531
  },
517
532
  };
518
533
  }
519
- const runtime = runtimeForLoadedAgent(gateway, params.agentId);
520
- const result = collectAgentSkillSnapshot(params.agentId, { runtime });
534
+ const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
535
+ const result = collectAgentSkillSnapshot(params.agentId, skillIndexOptions);
521
536
  daemonLog.debug("list_agent_skills", {
522
537
  agentId: params.agentId,
523
- runtime,
538
+ runtime: skillIndexOptions.runtime,
539
+ hermesProfile: skillIndexOptions.hermesProfile ?? null,
524
540
  count: result.skills.length,
525
541
  });
526
542
  return { ok: true, result };
527
543
  }
528
544
 
545
+ case CONTROL_FRAME_TYPES.INSTALL_AGENT_SKILL: {
546
+ const params = (frame.params ?? {}) as unknown as InstallAgentSkillParams;
547
+ if (!params.agentId) {
548
+ return {
549
+ ok: false,
550
+ error: { code: "bad_params", message: "install_agent_skill requires params.agentId" },
551
+ };
552
+ }
553
+ const channels = gateway.snapshot().channels;
554
+ if (!channels[params.agentId]) {
555
+ return {
556
+ ok: false,
557
+ error: {
558
+ code: "agent_not_loaded",
559
+ message: `agent ${params.agentId} is not loaded in daemon gateway`,
560
+ },
561
+ };
562
+ }
563
+ const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
564
+ const runtime = skillIndexOptions.runtime;
565
+ const modes = [params.manifest, params.archiveManifest, params.vercel].filter(Boolean).length;
566
+ if (modes !== 1) {
567
+ return {
568
+ ok: false,
569
+ error: {
570
+ code: "bad_params",
571
+ message: "install_agent_skill requires exactly one of manifest, archiveManifest, or vercel",
572
+ },
573
+ };
574
+ }
575
+ let result;
576
+ try {
577
+ result = params.vercel
578
+ ? await installVercelSkillsForAgent({
579
+ agentId: params.agentId,
580
+ packageSpec: params.vercel.packageSpec,
581
+ skills: params.vercel.skills,
582
+ runtime,
583
+ })
584
+ : params.archiveManifest
585
+ ? installBotLearnArchiveManifest(params.agentId, params.archiveManifest, { runtime })
586
+ : installAgentSkillManifest(params.agentId, params.manifest!, { runtime });
587
+ } catch (err) {
588
+ return {
589
+ ok: false,
590
+ error: {
591
+ code: "skill_install_failed",
592
+ message: err instanceof Error ? err.message : String(err),
593
+ },
594
+ };
595
+ }
596
+ daemonLog.debug("install_agent_skill", {
597
+ agentId: params.agentId,
598
+ runtime,
599
+ installed: result.installed.map((s) => s.name),
600
+ snapshotCount: result.snapshot.skills.length,
601
+ });
602
+ return { ok: true, result };
603
+ }
604
+
529
605
  case "wake_agent": {
530
606
  return handleWakeAgent(gateway, frame.params);
531
607
  }
@@ -1118,6 +1194,7 @@ async function installLocalAgent(
1118
1194
  hubUrl: credentials.hubUrl,
1119
1195
  ...(credentials.displayName ? { displayName: credentials.displayName } : {}),
1120
1196
  ...(credentials.runtime ? { runtime: credentials.runtime } : {}),
1197
+ ...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
1121
1198
  });
1122
1199
  } catch (err) {
1123
1200
  // Hook misbehavior must not fail the install — the agent is already
@@ -1212,6 +1289,7 @@ async function installExistingOpenclawBinding(
1212
1289
  hubUrl: credentials.hubUrl,
1213
1290
  ...(credentials.displayName ? { displayName: credentials.displayName } : {}),
1214
1291
  ...(credentials.runtime ? { runtime: credentials.runtime } : {}),
1292
+ ...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
1215
1293
  });
1216
1294
  } catch (err) {
1217
1295
  daemonLog.error("provision.onAgentInstalled threw — caches may be stale", {
@@ -68,6 +68,37 @@ const KIMI_FALLBACK_MODELS: RuntimeModelProbe[] = [
68
68
  { id: "kimi-k2-0711", displayName: "kimi-k2-0711", provider: "kimi", source: "builtin" },
69
69
  ];
70
70
 
71
+ // Gemini CLI has no `gemini models list` command — the model set is
72
+ // hard-coded inside the bundle's `VALID_GEMINI_MODELS` (see `@google/gemini-cli`
73
+ // bundle `chunk-BE42OOYM.js:275832`). We mirror the documented user-facing
74
+ // names here as a static catalog; the actual availability depends on the
75
+ // user's auth tier (gcloud ADC / Vertex / GEMINI_API_KEY) and project quota.
76
+ // The `auto` alias is the default — it lets gemini pick the best model.
77
+ const GEMINI_FALLBACK_MODELS: RuntimeModelProbe[] = [
78
+ { id: "auto", displayName: "Auto (let Gemini pick)", provider: "google", source: "builtin", isDefault: true },
79
+ { id: "pro", displayName: "Pro alias", provider: "google", source: "builtin" },
80
+ { id: "flash", displayName: "Flash alias", provider: "google", source: "builtin" },
81
+ { id: "flash-lite", displayName: "Flash Lite alias", provider: "google", source: "builtin" },
82
+ { id: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", provider: "google", source: "builtin" },
83
+ { id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", provider: "google", source: "builtin" },
84
+ { id: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", provider: "google", source: "builtin" },
85
+ { id: "gemini-3-pro-preview", displayName: "Gemini 3 Pro (preview)", provider: "google", source: "builtin" },
86
+ { id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (preview)", provider: "google", source: "builtin" },
87
+ { id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (preview)", provider: "google", source: "builtin" },
88
+ {
89
+ id: "gemini-3.1-pro-preview-customtools",
90
+ displayName: "Gemini 3.1 Pro Custom Tools (preview)",
91
+ provider: "google",
92
+ source: "builtin",
93
+ },
94
+ {
95
+ id: "gemini-3.1-flash-lite-preview",
96
+ displayName: "Gemini 3.1 Flash Lite (preview)",
97
+ provider: "google",
98
+ source: "builtin",
99
+ },
100
+ ];
101
+
71
102
  export interface RuntimeModelDiscovery {
72
103
  models?: RuntimeModelProbe[];
73
104
  parameters?: RuntimeParameterProbe[];
@@ -160,6 +191,22 @@ function runtimeCatalogStrategy(entry: RuntimeProbeEntry): RuntimeCatalogStrateg
160
191
  ],
161
192
  }),
162
193
  };
194
+ case "gemini":
195
+ // Gemini CLI exposes no runtime discovery command and its thinking
196
+ // budget / level (see bundle `ThinkingLevel`, `thinkingBudget`) is
197
+ // configured per-installation in `~/.gemini/settings.json`, not via
198
+ // any CLI flag — so we ship a static model catalog with no parameter
199
+ // controls. settings.json + BOTCORD_GEMINI_BIN feed the cache key so
200
+ // user-side reconfig (e.g. switching auth type) busts the cache.
201
+ return {
202
+ id: entry.id,
203
+ contextKey: runtimeCatalogContextKey(entry, {
204
+ settings: fileStatKey(path.join(homedir(), ".gemini", "settings.json")),
205
+ env: pickEnv(["BOTCORD_GEMINI_BIN", "GEMINI_CLI_HOME"]),
206
+ }),
207
+ discoverFresh: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
208
+ fallback: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
209
+ };
163
210
  default:
164
211
  return null;
165
212
  }
@@ -8,8 +8,10 @@ import { homedir } from "node:os";
8
8
  import path from "node:path";
9
9
  import {
10
10
  agentCodexHomeDir,
11
+ agentHermesHomeDir,
11
12
  agentWorkspaceDir,
12
13
  } from "./agent-workspace.js";
14
+ import { hermesProfileHomeDir } from "./gateway/runtimes/hermes-agent.js";
13
15
 
14
16
  const MAX_SKILLS = 24;
15
17
  const MAX_DESCRIPTION_CHARS = 260;
@@ -19,6 +21,8 @@ export interface SoftSkillEntry {
19
21
  name: string;
20
22
  path: string;
21
23
  source: string;
24
+ runtime?: string;
25
+ profile?: string;
22
26
  description?: string;
23
27
  mtimeMs: number;
24
28
  }
@@ -26,53 +30,89 @@ export interface SoftSkillEntry {
26
30
  export interface AgentSkillSnapshotEntry {
27
31
  name: string;
28
32
  source: string;
33
+ sourceDetail?: string;
34
+ runtime?: string;
35
+ path?: string;
36
+ profile?: string;
29
37
  description?: string;
30
38
  mtimeMs: number;
31
39
  }
32
40
 
33
41
  export interface AgentSkillSnapshot {
34
42
  agentId: string;
43
+ runtime?: string;
35
44
  skills: AgentSkillSnapshotEntry[];
36
45
  probedAt: number;
37
46
  }
38
47
 
39
48
  export interface SkillIndexOptions {
40
49
  extraDirs?: string[];
50
+ hermesProfile?: string;
41
51
  includeGlobal?: boolean;
42
52
  runtime?: string;
43
53
  }
44
54
 
55
+ interface SkillRoot {
56
+ dir: string;
57
+ source: string;
58
+ runtime?: string;
59
+ profile?: string;
60
+ }
61
+
45
62
  export function defaultSkillDirs(
46
63
  agentId: string,
47
64
  opts: SkillIndexOptions = {},
48
- ): Array<{ dir: string; source: string }> {
65
+ ): SkillRoot[] {
49
66
  const includeGlobal = opts.includeGlobal !== false;
50
67
  const agentClaude = {
51
68
  dir: path.join(agentWorkspaceDir(agentId), ".claude", "skills"),
52
69
  source: "agent-claude",
70
+ runtime: "claude-code",
53
71
  };
54
72
  const agentCodex = {
55
73
  dir: path.join(agentCodexHomeDir(agentId), "skills"),
56
74
  source: "agent-codex",
75
+ runtime: "codex",
57
76
  };
58
- const dirs: Array<{ dir: string; source: string }> =
59
- runtimeFamily(opts.runtime) === "codex"
60
- ? [agentCodex, agentClaude]
61
- : [agentClaude, agentCodex];
62
-
63
- if (includeGlobal) {
64
- const globalClaude = { dir: path.join(homedir(), ".claude", "skills"), source: "global-claude" };
65
- const globalCodex = { dir: path.join(homedir(), ".codex", "skills"), source: "global-codex" };
66
- dirs.push(
67
- ...(runtimeFamily(opts.runtime) === "codex"
68
- ? [globalCodex, globalClaude]
69
- : [globalClaude, globalCodex]),
70
- );
77
+ const agentHermes = hermesSkillRoot(agentId, opts.hermesProfile);
78
+
79
+ const dirs: SkillRoot[] = [];
80
+ switch (runtimeFamily(opts.runtime)) {
81
+ case "codex":
82
+ dirs.push(agentCodex);
83
+ if (includeGlobal) {
84
+ dirs.push({
85
+ dir: path.join(homedir(), ".codex", "skills"),
86
+ source: "global-codex",
87
+ runtime: "codex",
88
+ });
89
+ }
90
+ break;
91
+ case "hermes":
92
+ dirs.push(agentHermes);
93
+ break;
94
+ case "claude":
95
+ dirs.push(agentClaude);
96
+ if (includeGlobal) {
97
+ dirs.push({
98
+ dir: path.join(homedir(), ".claude", "skills"),
99
+ source: "global-claude",
100
+ runtime: "claude-code",
101
+ });
102
+ }
103
+ break;
104
+ case "other":
105
+ break;
71
106
  }
72
107
 
73
108
  const envDirs = parseSkillDirsEnv(process.env.BOTCORD_SKILL_DIRS);
74
109
  for (const dir of [...envDirs, ...(opts.extraDirs ?? [])]) {
75
- dirs.push({ dir, source: "external" });
110
+ dirs.push({
111
+ dir,
112
+ source: "external",
113
+ ...(opts.runtime ? { runtime: opts.runtime } : {}),
114
+ ...(opts.hermesProfile ? { profile: opts.hermesProfile } : {}),
115
+ });
76
116
  }
77
117
 
78
118
  return dedupeDirs(expandSkillRoots(dirs));
@@ -114,6 +154,8 @@ export function scanSoftSkills(
114
154
  name: parsed.name,
115
155
  path: skillMd,
116
156
  source: root.source,
157
+ ...(root.runtime ? { runtime: root.runtime } : {}),
158
+ ...(root.profile ? { profile: root.profile } : {}),
117
159
  description: parsed.description,
118
160
  mtimeMs: st.mtimeMs,
119
161
  };
@@ -133,9 +175,14 @@ export function collectAgentSkillSnapshot(
133
175
  ): AgentSkillSnapshot {
134
176
  return {
135
177
  agentId,
178
+ ...(opts.runtime ? { runtime: opts.runtime } : {}),
136
179
  skills: scanSoftSkills(agentId, opts).map((skill) => ({
137
180
  name: skill.name,
138
- source: skill.source.startsWith("agent-") ? "workspace" : "runtime-global",
181
+ source: snapshotSource(skill.source),
182
+ sourceDetail: skill.source,
183
+ ...(skill.runtime ? { runtime: skill.runtime } : {}),
184
+ path: skill.path,
185
+ ...(skill.profile ? { profile: skill.profile } : {}),
139
186
  ...(skill.description ? { description: skill.description } : {}),
140
187
  mtimeMs: skill.mtimeMs,
141
188
  })),
@@ -217,68 +264,77 @@ function parseSkillDirsEnv(value: string | undefined): string[] {
217
264
  }
218
265
 
219
266
  function dedupeDirs(
220
- dirs: Array<{ dir: string; source: string }>,
221
- ): Array<{ dir: string; source: string }> {
267
+ dirs: SkillRoot[],
268
+ ): SkillRoot[] {
222
269
  const seen = new Set<string>();
223
- const out: Array<{ dir: string; source: string }> = [];
270
+ const out: SkillRoot[] = [];
224
271
  for (const entry of dirs) {
225
272
  const resolved = path.resolve(entry.dir);
226
273
  if (seen.has(resolved)) continue;
227
274
  seen.add(resolved);
228
- out.push({ dir: resolved, source: entry.source });
275
+ out.push({ ...entry, dir: resolved });
229
276
  }
230
277
  return out;
231
278
  }
232
279
 
233
- function expandSkillRoots(
234
- dirs: Array<{ dir: string; source: string }>,
235
- ): Array<{ dir: string; source: string }> {
236
- const out: Array<{ dir: string; source: string }> = [];
280
+ function expandSkillRoots(dirs: SkillRoot[]): SkillRoot[] {
281
+ const out: SkillRoot[] = [];
237
282
  for (const entry of dirs) {
238
283
  out.push(entry);
239
284
  if (entry.source.includes("codex")) {
240
- out.push({ dir: path.join(entry.dir, ".system"), source: entry.source });
285
+ out.push({ ...entry, dir: path.join(entry.dir, ".system") });
241
286
  }
242
287
  }
243
288
  return out;
244
289
  }
245
290
 
246
- function runtimeFamily(runtime: string | undefined): "codex" | "claude" | "other" {
291
+ function hermesSkillRoot(agentId: string, profile: string | undefined): SkillRoot {
292
+ if (profile) {
293
+ try {
294
+ return {
295
+ dir: path.join(hermesProfileHomeDir(profile), "skills"),
296
+ source: "agent-hermes-profile",
297
+ runtime: "hermes-agent",
298
+ profile,
299
+ };
300
+ } catch {
301
+ // Corrupt legacy credentials should not make the whole skill snapshot fail.
302
+ }
303
+ }
304
+ return {
305
+ dir: path.join(agentHermesHomeDir(agentId), "skills"),
306
+ source: "agent-hermes",
307
+ runtime: "hermes-agent",
308
+ };
309
+ }
310
+
311
+ function runtimeFamily(runtime: string | undefined): "codex" | "claude" | "hermes" | "other" {
247
312
  if (runtime === "codex") return "codex";
313
+ if (runtime === "hermes-agent") return "hermes";
314
+ if (!runtime) return "claude";
248
315
  if (runtime === "claude-code") return "claude";
249
316
  return "other";
250
317
  }
251
318
 
252
- function priority(source: string, runtime: string | undefined): number {
253
- if (runtimeFamily(runtime) === "codex") {
254
- switch (source) {
255
- case "agent-codex":
256
- return 0;
257
- case "global-codex":
258
- return 1;
259
- case "agent-claude":
260
- return 2;
261
- case "global-claude":
262
- return 3;
263
- default:
264
- return 4;
265
- }
266
- }
267
-
319
+ function priority(source: string, _runtime: string | undefined): number {
268
320
  switch (source) {
269
321
  case "agent-claude":
270
- return 0;
271
322
  case "agent-codex":
272
- return 1;
323
+ case "agent-hermes":
324
+ case "agent-hermes-profile":
325
+ return 0;
273
326
  case "global-claude":
274
- return 2;
275
327
  case "global-codex":
276
- return 3;
328
+ return 1;
277
329
  default:
278
- return 4;
330
+ return 2;
279
331
  }
280
332
  }
281
333
 
334
+ function snapshotSource(source: string): "workspace" | "runtime-global" {
335
+ return source.startsWith("agent-") ? "workspace" : "runtime-global";
336
+ }
337
+
282
338
  function unquote(value: string): string {
283
339
  const trimmed = value.trim();
284
340
  if (