@botcord/daemon 0.2.89 → 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.
- package/dist/cloud-daemon.js +18 -4
- package/dist/daemon.d.ts +2 -3
- package/dist/daemon.js +21 -6
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +83 -7
- package/dist/runtime-models.js +46 -0
- package/dist/skill-index.d.ts +14 -2
- package/dist/skill-index.js +81 -34
- package/dist/skill-installer.d.ts +61 -0
- package/dist/skill-installer.js +340 -0
- package/dist/system-context.d.ts +6 -0
- package/dist/system-context.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/runtime-discovery.test.ts +6 -3
- package/src/__tests__/runtime-models.test.ts +53 -0
- package/src/__tests__/skill-index.test.ts +89 -13
- package/src/__tests__/skill-installer.test.ts +224 -0
- package/src/cloud-daemon.ts +17 -4
- package/src/daemon.ts +23 -8
- package/src/provision.ts +90 -12
- package/src/runtime-models.ts +47 -0
- package/src/skill-index.ts +103 -47
- package/src/skill-installer.ts +472 -0
- package/src/system-context.ts +7 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
agentCodexHomeDir,
|
|
15
|
+
agentWorkspaceDir,
|
|
16
|
+
} from "../agent-workspace.js";
|
|
17
|
+
import {
|
|
18
|
+
buildVercelSkillsArgs,
|
|
19
|
+
installAgentSkillManifest,
|
|
20
|
+
installBotLearnArchiveManifest,
|
|
21
|
+
installVercelSkillsForAgent,
|
|
22
|
+
} from "../skill-installer.js";
|
|
23
|
+
|
|
24
|
+
let tmpHome = "";
|
|
25
|
+
let prevHome: string | undefined;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmpHome = mkdtempSync(path.join(tmpdir(), "skill-installer-"));
|
|
29
|
+
prevHome = process.env.HOME;
|
|
30
|
+
process.env.HOME = tmpHome;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
35
|
+
else process.env.HOME = prevHome;
|
|
36
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("skill installer", () => {
|
|
40
|
+
it("installs a manifest into the loaded agent runtime path and returns a refreshed snapshot", () => {
|
|
41
|
+
const result = installAgentSkillManifest(
|
|
42
|
+
"ag_manifest",
|
|
43
|
+
{
|
|
44
|
+
name: "review-helper",
|
|
45
|
+
description: "Review pull requests",
|
|
46
|
+
files: [{ path: "references/checklist.md", content: "Check tests\n" }],
|
|
47
|
+
},
|
|
48
|
+
{ runtime: "codex" },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const skillDir = path.join(agentCodexHomeDir("ag_manifest"), "skills", "review-helper");
|
|
52
|
+
expect(readFileSync(path.join(skillDir, "SKILL.md"), "utf8")).toContain("name: review-helper");
|
|
53
|
+
expect(readFileSync(path.join(skillDir, "references", "checklist.md"), "utf8")).toBe("Check tests\n");
|
|
54
|
+
expect(result.installed).toEqual([
|
|
55
|
+
{
|
|
56
|
+
name: "review-helper",
|
|
57
|
+
targets: ["codex"],
|
|
58
|
+
paths: [skillDir],
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
expect(result.snapshot.skills.map((s) => s.name)).toContain("review-helper");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("installs BotLearn archive-style manifests containing multiple skills", () => {
|
|
65
|
+
const result = installBotLearnArchiveManifest(
|
|
66
|
+
"ag_archive",
|
|
67
|
+
{
|
|
68
|
+
targetRuntimes: ["claude-code"],
|
|
69
|
+
skills: [
|
|
70
|
+
{
|
|
71
|
+
id: "planner",
|
|
72
|
+
description: "Plan work",
|
|
73
|
+
skillMd: "---\nname: planner\ndescription: Plan work\n---\n\n# Planner\n",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "tester",
|
|
77
|
+
markdown: "---\nname: tester\n---\n\n# Tester\n",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{ runtime: "claude-code" },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const skillsRoot = path.join(agentWorkspaceDir("ag_archive"), ".claude", "skills");
|
|
85
|
+
expect(existsSync(path.join(skillsRoot, "planner", "SKILL.md"))).toBe(true);
|
|
86
|
+
expect(existsSync(path.join(skillsRoot, "tester", "SKILL.md"))).toBe(true);
|
|
87
|
+
expect(result.installed.map((skill) => skill.name)).toEqual(["planner", "tester"]);
|
|
88
|
+
expect(result.snapshot.skills.map((skill) => skill.name).sort()).toEqual(["planner", "tester"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects unsafe names and file paths before writing", () => {
|
|
92
|
+
expect(() => installAgentSkillManifest("ag_unsafe_name", {
|
|
93
|
+
name: "../bad",
|
|
94
|
+
})).toThrow(/unsafe skill name/);
|
|
95
|
+
|
|
96
|
+
expect(() => installAgentSkillManifest("ag_unsafe_file", {
|
|
97
|
+
name: "safe",
|
|
98
|
+
files: [{ path: "../escape.txt", content: "no" }],
|
|
99
|
+
})).toThrow(/unsafe skill file path/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not leave a partial skill dir when source file validation fails", () => {
|
|
103
|
+
const sourceRoot = mkdtempSync(path.join(tmpdir(), "skill-source-"));
|
|
104
|
+
try {
|
|
105
|
+
expect(() => installAgentSkillManifest("ag_missing_source", {
|
|
106
|
+
name: "missing-source",
|
|
107
|
+
files: [{ path: "references/missing.md", sourcePath: "missing.md" }],
|
|
108
|
+
}, {
|
|
109
|
+
runtime: "codex",
|
|
110
|
+
sourceRoot,
|
|
111
|
+
})).toThrow();
|
|
112
|
+
|
|
113
|
+
const skillDir = path.join(agentCodexHomeDir("ag_missing_source"), "skills", "missing-source");
|
|
114
|
+
expect(existsSync(skillDir)).toBe(false);
|
|
115
|
+
} finally {
|
|
116
|
+
rmSync(sourceRoot, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not leave a partial skill dir when an inline file is oversized", () => {
|
|
121
|
+
const oversized = "x".repeat((256 * 1024) + 1);
|
|
122
|
+
|
|
123
|
+
expect(() => installAgentSkillManifest("ag_oversized", {
|
|
124
|
+
name: "oversized",
|
|
125
|
+
files: [{ path: "references/large.md", content: oversized }],
|
|
126
|
+
}, {
|
|
127
|
+
runtime: "codex",
|
|
128
|
+
})).toThrow(/skill file too large/);
|
|
129
|
+
|
|
130
|
+
const skillDir = path.join(agentCodexHomeDir("ag_oversized"), "skills", "oversized");
|
|
131
|
+
expect(existsSync(skillDir)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("builds a non-interactive vercel-labs/skills command for injected execution", () => {
|
|
135
|
+
expect(buildVercelSkillsArgs("https://github.com/vercel-labs/skills", ["frontend-design"], ["codex"]))
|
|
136
|
+
.toEqual([
|
|
137
|
+
"--yes",
|
|
138
|
+
"skills",
|
|
139
|
+
"add",
|
|
140
|
+
"https://github.com/vercel-labs/skills",
|
|
141
|
+
"--global",
|
|
142
|
+
"--copy",
|
|
143
|
+
"--yes",
|
|
144
|
+
"--skill",
|
|
145
|
+
"frontend-design",
|
|
146
|
+
"--agent",
|
|
147
|
+
"codex",
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("imports skills produced by an injected vercel-labs/skills executor without network", async () => {
|
|
152
|
+
const executor = vi.fn(async (_command, _args, options) => {
|
|
153
|
+
const home = options.env?.HOME as string;
|
|
154
|
+
const namespaced = path.join(home, ".codex", "skills", "vercel-labs", "find-skills");
|
|
155
|
+
mkdirSync(namespaced, { recursive: true });
|
|
156
|
+
writeFileSync(
|
|
157
|
+
path.join(namespaced, "SKILL.md"),
|
|
158
|
+
"---\nname: find-skills\ndescription: Find skills\n---\n\n# Find Skills\n",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = await installVercelSkillsForAgent({
|
|
163
|
+
agentId: "ag_vercel",
|
|
164
|
+
packageSpec: "https://github.com/vercel-labs/skills",
|
|
165
|
+
skills: ["find-skills"],
|
|
166
|
+
runtime: "codex",
|
|
167
|
+
executor,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const installed = path.join(agentCodexHomeDir("ag_vercel"), "skills", "find-skills", "SKILL.md");
|
|
171
|
+
expect(existsSync(installed)).toBe(true);
|
|
172
|
+
expect(result.installed).toEqual([
|
|
173
|
+
{
|
|
174
|
+
name: "find-skills",
|
|
175
|
+
targets: ["codex"],
|
|
176
|
+
paths: [path.dirname(installed)],
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
expect(result.snapshot.skills.map((skill) => skill.name)).toContain("find-skills");
|
|
180
|
+
expect(executor).toHaveBeenCalledWith(
|
|
181
|
+
"npx",
|
|
182
|
+
expect.arrayContaining(["skills", "add", "https://github.com/vercel-labs/skills"]),
|
|
183
|
+
expect.objectContaining({ cwd: agentWorkspaceDir("ag_vercel") }),
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("rejects untrusted vercel package specs", async () => {
|
|
188
|
+
await expect(installVercelSkillsForAgent({
|
|
189
|
+
agentId: "ag_untrusted_vercel",
|
|
190
|
+
packageSpec: "attacker/skills",
|
|
191
|
+
runtime: "codex",
|
|
192
|
+
executor: vi.fn(),
|
|
193
|
+
})).rejects.toThrow(/unsupported vercel skills packageSpec/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("rejects symlinked files from vercel skill import without installing a skill dir", async () => {
|
|
197
|
+
const outside = mkdtempSync(path.join(tmpdir(), "skill-outside-"));
|
|
198
|
+
try {
|
|
199
|
+
writeFileSync(path.join(outside, "secret.txt"), "do not copy");
|
|
200
|
+
const executor = vi.fn(async (_command, _args, options) => {
|
|
201
|
+
const home = options.env?.HOME as string;
|
|
202
|
+
const skillDir = path.join(home, ".codex", "skills", "vercel-labs", "linked-skill");
|
|
203
|
+
mkdirSync(skillDir, { recursive: true });
|
|
204
|
+
writeFileSync(
|
|
205
|
+
path.join(skillDir, "SKILL.md"),
|
|
206
|
+
"---\nname: linked-skill\n---\n\n# Linked Skill\n",
|
|
207
|
+
);
|
|
208
|
+
symlinkSync(path.join(outside, "secret.txt"), path.join(skillDir, "secret.txt"));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await expect(installVercelSkillsForAgent({
|
|
212
|
+
agentId: "ag_vercel_symlink",
|
|
213
|
+
packageSpec: "https://github.com/vercel-labs/skills",
|
|
214
|
+
runtime: "codex",
|
|
215
|
+
executor,
|
|
216
|
+
})).rejects.toThrow(/rejects symlink/);
|
|
217
|
+
|
|
218
|
+
const skillDir = path.join(agentCodexHomeDir("ag_vercel_symlink"), "skills", "linked-skill");
|
|
219
|
+
expect(existsSync(skillDir)).toBe(false);
|
|
220
|
+
} finally {
|
|
221
|
+
rmSync(outside, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
package/src/cloud-daemon.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { CloudAuthManager, asUserAuthManager } from "./cloud-auth.js";
|
|
|
42
42
|
import type { CloudModeConfig } from "./cloud-mode.js";
|
|
43
43
|
import { buildCloudRunSettleHook } from "./cloud-settle.js";
|
|
44
44
|
import type { InstalledAgentInfo, OnAgentInstalledHook } from "./provision.js";
|
|
45
|
+
import type { SkillIndexOptions } from "./skill-index.js";
|
|
45
46
|
|
|
46
47
|
// Cloud daemons follow the same cadence as local — keeps dashboard
|
|
47
48
|
// "runtimes last detected" behavior identical across both kinds.
|
|
@@ -125,6 +126,15 @@ export async function startCloudDaemon(
|
|
|
125
126
|
const credentialPathByAgentId = new Map<string, string>();
|
|
126
127
|
const hubUrlByAgentId = new Map<string, string>();
|
|
127
128
|
const displayNameByAgent = new Map<string, string>();
|
|
129
|
+
const runtimeByAgentId = new Map<string, string>();
|
|
130
|
+
const hermesProfileByAgentId = new Map<string, string>();
|
|
131
|
+
const skillIndexOptionsForAgent = (agentId: string): SkillIndexOptions => {
|
|
132
|
+
const hermesProfile = hermesProfileByAgentId.get(agentId);
|
|
133
|
+
return {
|
|
134
|
+
runtime: runtimeByAgentId.get(agentId) ?? opts.config.defaultRoute.adapter,
|
|
135
|
+
...(hermesProfile ? { hermesProfile } : {}),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
128
138
|
// Seed each per-agent hub URL with the cloud-mode value so that even
|
|
129
139
|
// before the first credential file is written the room-context fetcher
|
|
130
140
|
// has somewhere sensible to point.
|
|
@@ -235,15 +245,15 @@ export async function startCloudDaemon(
|
|
|
235
245
|
};
|
|
236
246
|
|
|
237
247
|
const installedAgentIds = new Set<string>();
|
|
238
|
-
const runtimeByAgentId = new Map<string, string>();
|
|
239
248
|
let controlChannel: ControlChannel | null = null;
|
|
240
249
|
const pushInstalledAgentSkillSnapshot = (agentId: string, reason: string): void => {
|
|
241
250
|
if (!controlChannel) return;
|
|
242
|
-
const
|
|
243
|
-
const pushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
251
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
252
|
+
const pushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
244
253
|
logger.info("cloud control-channel: agent_skill_snapshot pushed", {
|
|
245
254
|
agentId,
|
|
246
|
-
runtime,
|
|
255
|
+
runtime: skillIndexOptions.runtime,
|
|
256
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
247
257
|
reason,
|
|
248
258
|
ok: pushed,
|
|
249
259
|
});
|
|
@@ -252,6 +262,8 @@ export async function startCloudDaemon(
|
|
|
252
262
|
const onAgentInstalled: OnAgentInstalledHook = (info: InstalledAgentInfo) => {
|
|
253
263
|
installedAgentIds.add(info.agentId);
|
|
254
264
|
if (info.runtime) runtimeByAgentId.set(info.agentId, info.runtime);
|
|
265
|
+
if (info.hermesProfile) hermesProfileByAgentId.set(info.agentId, info.hermesProfile);
|
|
266
|
+
else if (info.runtime) hermesProfileByAgentId.delete(info.agentId);
|
|
255
267
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
256
268
|
if (info.hubUrl) hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
257
269
|
if (info.displayName) displayNameByAgent.set(info.agentId, info.displayName);
|
|
@@ -262,6 +274,7 @@ export async function startCloudDaemon(
|
|
|
262
274
|
agentId: info.agentId,
|
|
263
275
|
activityTracker,
|
|
264
276
|
roomContextBuilder,
|
|
277
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
265
278
|
// Cloud daemons run isolated — no loop-risk guard wired in PR1;
|
|
266
279
|
// the runtime adapter's wall-time budget enforces the equivalent.
|
|
267
280
|
loopRiskBuilder: () => null,
|
package/src/daemon.ts
CHANGED
|
@@ -52,7 +52,7 @@ import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-res
|
|
|
52
52
|
import { scanMention } from "./mention-scan.js";
|
|
53
53
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
54
54
|
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
55
|
-
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
55
|
+
import { collectAgentSkillSnapshot, type SkillIndexOptions } from "./skill-index.js";
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
@@ -249,7 +249,7 @@ export function pushRuntimeSnapshot(
|
|
|
249
249
|
export function pushAgentSkillSnapshot(
|
|
250
250
|
sink: RuntimeSnapshotSink,
|
|
251
251
|
agentId: string,
|
|
252
|
-
opts:
|
|
252
|
+
opts: SkillIndexOptions = {},
|
|
253
253
|
): boolean {
|
|
254
254
|
const snap = collectAgentSkillSnapshot(agentId, opts);
|
|
255
255
|
const ok = sink.send({
|
|
@@ -354,6 +354,13 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
354
354
|
);
|
|
355
355
|
|
|
356
356
|
const gwConfig = toGatewayConfig(opts.config, { agentIds, agentRuntimes });
|
|
357
|
+
const skillIndexOptionsForAgent = (agentId: string): SkillIndexOptions => {
|
|
358
|
+
const meta = agentRuntimes[agentId];
|
|
359
|
+
return {
|
|
360
|
+
runtime: meta?.runtime ?? opts.config.defaultRoute.adapter,
|
|
361
|
+
...(meta?.hermesProfile ? { hermesProfile: meta.hermesProfile } : {}),
|
|
362
|
+
};
|
|
363
|
+
};
|
|
357
364
|
|
|
358
365
|
// Per-agent hub URL — read from each credential file at boot. Used to
|
|
359
366
|
// populate `BOTCORD_HUB` for runtime CLI subprocesses so the bundled
|
|
@@ -420,6 +427,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
420
427
|
activityTracker,
|
|
421
428
|
roomContextBuilder,
|
|
422
429
|
loopRiskBuilder,
|
|
430
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(aid),
|
|
423
431
|
}),
|
|
424
432
|
);
|
|
425
433
|
}
|
|
@@ -530,11 +538,16 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
530
538
|
// next room-context fetch re-loads the BotCordClient against the new
|
|
531
539
|
// credential file.
|
|
532
540
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
533
|
-
if (info.runtime) {
|
|
534
|
-
|
|
541
|
+
if (info.runtime || info.hermesProfile) {
|
|
542
|
+
const next = {
|
|
535
543
|
...(agentRuntimes[info.agentId] ?? {}),
|
|
536
|
-
runtime: info.runtime,
|
|
544
|
+
...(info.runtime ? { runtime: info.runtime } : {}),
|
|
545
|
+
...(info.hermesProfile ? { hermesProfile: info.hermesProfile } : {}),
|
|
537
546
|
};
|
|
547
|
+
if (info.runtime && !info.hermesProfile) {
|
|
548
|
+
delete next.hermesProfile;
|
|
549
|
+
}
|
|
550
|
+
agentRuntimes[info.agentId] = next;
|
|
538
551
|
}
|
|
539
552
|
if (info.hubUrl) hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
540
553
|
if (info.displayName) displayNameByAgent.set(info.agentId, info.displayName);
|
|
@@ -546,6 +559,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
546
559
|
activityTracker,
|
|
547
560
|
roomContextBuilder,
|
|
548
561
|
loopRiskBuilder,
|
|
562
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
549
563
|
}),
|
|
550
564
|
);
|
|
551
565
|
}
|
|
@@ -677,11 +691,12 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
677
691
|
ok: pushed,
|
|
678
692
|
});
|
|
679
693
|
for (const agentId of agentIds) {
|
|
680
|
-
const
|
|
681
|
-
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
694
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
695
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
682
696
|
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
683
697
|
agentId,
|
|
684
|
-
runtime,
|
|
698
|
+
runtime: skillIndexOptions.runtime,
|
|
699
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
685
700
|
ok: skillsPushed,
|
|
686
701
|
});
|
|
687
702
|
}
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
520
|
-
const result = collectAgentSkillSnapshot(params.agentId,
|
|
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", {
|
package/src/runtime-models.ts
CHANGED
|
@@ -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
|
}
|