@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.
- package/dist/cloud-daemon.js +18 -4
- package/dist/daemon.d.ts +2 -3
- package/dist/daemon.js +21 -6
- package/dist/gateway/runtimes/gemini.d.ts +57 -5
- package/dist/gateway/runtimes/gemini.js +266 -5
- package/dist/gateway/runtimes/registry.d.ts +1 -1
- package/dist/gateway/runtimes/registry.js +3 -2
- 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/gateway/__tests__/gemini-adapter.test.ts +357 -0
- package/src/gateway/runtimes/gemini.ts +301 -7
- package/src/gateway/runtimes/registry.ts +4 -2
- 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
package/dist/runtime-models.js
CHANGED
|
@@ -54,6 +54,36 @@ const KIMI_FALLBACK_MODELS = [
|
|
|
54
54
|
{ id: "kimi-k2-5-preview", displayName: "kimi-k2-5-preview", provider: "kimi", source: "builtin" },
|
|
55
55
|
{ id: "kimi-k2-0711", displayName: "kimi-k2-0711", provider: "kimi", source: "builtin" },
|
|
56
56
|
];
|
|
57
|
+
// Gemini CLI has no `gemini models list` command — the model set is
|
|
58
|
+
// hard-coded inside the bundle's `VALID_GEMINI_MODELS` (see `@google/gemini-cli`
|
|
59
|
+
// bundle `chunk-BE42OOYM.js:275832`). We mirror the documented user-facing
|
|
60
|
+
// names here as a static catalog; the actual availability depends on the
|
|
61
|
+
// user's auth tier (gcloud ADC / Vertex / GEMINI_API_KEY) and project quota.
|
|
62
|
+
// The `auto` alias is the default — it lets gemini pick the best model.
|
|
63
|
+
const GEMINI_FALLBACK_MODELS = [
|
|
64
|
+
{ id: "auto", displayName: "Auto (let Gemini pick)", provider: "google", source: "builtin", isDefault: true },
|
|
65
|
+
{ id: "pro", displayName: "Pro alias", provider: "google", source: "builtin" },
|
|
66
|
+
{ id: "flash", displayName: "Flash alias", provider: "google", source: "builtin" },
|
|
67
|
+
{ id: "flash-lite", displayName: "Flash Lite alias", provider: "google", source: "builtin" },
|
|
68
|
+
{ id: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", provider: "google", source: "builtin" },
|
|
69
|
+
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", provider: "google", source: "builtin" },
|
|
70
|
+
{ id: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", provider: "google", source: "builtin" },
|
|
71
|
+
{ id: "gemini-3-pro-preview", displayName: "Gemini 3 Pro (preview)", provider: "google", source: "builtin" },
|
|
72
|
+
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (preview)", provider: "google", source: "builtin" },
|
|
73
|
+
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (preview)", provider: "google", source: "builtin" },
|
|
74
|
+
{
|
|
75
|
+
id: "gemini-3.1-pro-preview-customtools",
|
|
76
|
+
displayName: "Gemini 3.1 Pro Custom Tools (preview)",
|
|
77
|
+
provider: "google",
|
|
78
|
+
source: "builtin",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "gemini-3.1-flash-lite-preview",
|
|
82
|
+
displayName: "Gemini 3.1 Flash Lite (preview)",
|
|
83
|
+
provider: "google",
|
|
84
|
+
source: "builtin",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
57
87
|
const backgroundRefreshes = new Set();
|
|
58
88
|
export function discoverRuntimeModelCatalog(entry) {
|
|
59
89
|
if (!entry.result.available)
|
|
@@ -126,6 +156,22 @@ function runtimeCatalogStrategy(entry) {
|
|
|
126
156
|
],
|
|
127
157
|
}),
|
|
128
158
|
};
|
|
159
|
+
case "gemini":
|
|
160
|
+
// Gemini CLI exposes no runtime discovery command and its thinking
|
|
161
|
+
// budget / level (see bundle `ThinkingLevel`, `thinkingBudget`) is
|
|
162
|
+
// configured per-installation in `~/.gemini/settings.json`, not via
|
|
163
|
+
// any CLI flag — so we ship a static model catalog with no parameter
|
|
164
|
+
// controls. settings.json + BOTCORD_GEMINI_BIN feed the cache key so
|
|
165
|
+
// user-side reconfig (e.g. switching auth type) busts the cache.
|
|
166
|
+
return {
|
|
167
|
+
id: entry.id,
|
|
168
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
169
|
+
settings: fileStatKey(path.join(homedir(), ".gemini", "settings.json")),
|
|
170
|
+
env: pickEnv(["BOTCORD_GEMINI_BIN", "GEMINI_CLI_HOME"]),
|
|
171
|
+
}),
|
|
172
|
+
discoverFresh: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
|
|
173
|
+
fallback: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
|
|
174
|
+
};
|
|
129
175
|
default:
|
|
130
176
|
return null;
|
|
131
177
|
}
|
package/dist/skill-index.d.ts
CHANGED
|
@@ -2,29 +2,41 @@ export interface SoftSkillEntry {
|
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
4
|
source: string;
|
|
5
|
+
runtime?: string;
|
|
6
|
+
profile?: string;
|
|
5
7
|
description?: string;
|
|
6
8
|
mtimeMs: number;
|
|
7
9
|
}
|
|
8
10
|
export interface AgentSkillSnapshotEntry {
|
|
9
11
|
name: string;
|
|
10
12
|
source: string;
|
|
13
|
+
sourceDetail?: string;
|
|
14
|
+
runtime?: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
profile?: string;
|
|
11
17
|
description?: string;
|
|
12
18
|
mtimeMs: number;
|
|
13
19
|
}
|
|
14
20
|
export interface AgentSkillSnapshot {
|
|
15
21
|
agentId: string;
|
|
22
|
+
runtime?: string;
|
|
16
23
|
skills: AgentSkillSnapshotEntry[];
|
|
17
24
|
probedAt: number;
|
|
18
25
|
}
|
|
19
26
|
export interface SkillIndexOptions {
|
|
20
27
|
extraDirs?: string[];
|
|
28
|
+
hermesProfile?: string;
|
|
21
29
|
includeGlobal?: boolean;
|
|
22
30
|
runtime?: string;
|
|
23
31
|
}
|
|
24
|
-
|
|
32
|
+
interface SkillRoot {
|
|
25
33
|
dir: string;
|
|
26
34
|
source: string;
|
|
27
|
-
|
|
35
|
+
runtime?: string;
|
|
36
|
+
profile?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function defaultSkillDirs(agentId: string, opts?: SkillIndexOptions): SkillRoot[];
|
|
28
39
|
export declare function scanSoftSkills(agentId: string, opts?: SkillIndexOptions): SoftSkillEntry[];
|
|
29
40
|
export declare function collectAgentSkillSnapshot(agentId: string, opts?: SkillIndexOptions): AgentSkillSnapshot;
|
|
30
41
|
export declare function buildSoftSkillIndexPrompt(agentId: string, opts?: SkillIndexOptions): string | null;
|
|
42
|
+
export {};
|
package/dist/skill-index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync, } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { agentCodexHomeDir, agentWorkspaceDir, } from "./agent-workspace.js";
|
|
4
|
+
import { agentCodexHomeDir, agentHermesHomeDir, agentWorkspaceDir, } from "./agent-workspace.js";
|
|
5
|
+
import { hermesProfileHomeDir } from "./gateway/runtimes/hermes-agent.js";
|
|
5
6
|
const MAX_SKILLS = 24;
|
|
6
7
|
const MAX_DESCRIPTION_CHARS = 260;
|
|
7
8
|
const MAX_SKILL_MD_READ_CHARS = 8192;
|
|
@@ -10,24 +11,50 @@ export function defaultSkillDirs(agentId, opts = {}) {
|
|
|
10
11
|
const agentClaude = {
|
|
11
12
|
dir: path.join(agentWorkspaceDir(agentId), ".claude", "skills"),
|
|
12
13
|
source: "agent-claude",
|
|
14
|
+
runtime: "claude-code",
|
|
13
15
|
};
|
|
14
16
|
const agentCodex = {
|
|
15
17
|
dir: path.join(agentCodexHomeDir(agentId), "skills"),
|
|
16
18
|
source: "agent-codex",
|
|
19
|
+
runtime: "codex",
|
|
17
20
|
};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const agentHermes = hermesSkillRoot(agentId, opts.hermesProfile);
|
|
22
|
+
const dirs = [];
|
|
23
|
+
switch (runtimeFamily(opts.runtime)) {
|
|
24
|
+
case "codex":
|
|
25
|
+
dirs.push(agentCodex);
|
|
26
|
+
if (includeGlobal) {
|
|
27
|
+
dirs.push({
|
|
28
|
+
dir: path.join(homedir(), ".codex", "skills"),
|
|
29
|
+
source: "global-codex",
|
|
30
|
+
runtime: "codex",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case "hermes":
|
|
35
|
+
dirs.push(agentHermes);
|
|
36
|
+
break;
|
|
37
|
+
case "claude":
|
|
38
|
+
dirs.push(agentClaude);
|
|
39
|
+
if (includeGlobal) {
|
|
40
|
+
dirs.push({
|
|
41
|
+
dir: path.join(homedir(), ".claude", "skills"),
|
|
42
|
+
source: "global-claude",
|
|
43
|
+
runtime: "claude-code",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case "other":
|
|
48
|
+
break;
|
|
27
49
|
}
|
|
28
50
|
const envDirs = parseSkillDirsEnv(process.env.BOTCORD_SKILL_DIRS);
|
|
29
51
|
for (const dir of [...envDirs, ...(opts.extraDirs ?? [])]) {
|
|
30
|
-
dirs.push({
|
|
52
|
+
dirs.push({
|
|
53
|
+
dir,
|
|
54
|
+
source: "external",
|
|
55
|
+
...(opts.runtime ? { runtime: opts.runtime } : {}),
|
|
56
|
+
...(opts.hermesProfile ? { profile: opts.hermesProfile } : {}),
|
|
57
|
+
});
|
|
31
58
|
}
|
|
32
59
|
return dedupeDirs(expandSkillRoots(dirs));
|
|
33
60
|
}
|
|
@@ -65,6 +92,8 @@ export function scanSoftSkills(agentId, opts = {}) {
|
|
|
65
92
|
name: parsed.name,
|
|
66
93
|
path: skillMd,
|
|
67
94
|
source: root.source,
|
|
95
|
+
...(root.runtime ? { runtime: root.runtime } : {}),
|
|
96
|
+
...(root.profile ? { profile: root.profile } : {}),
|
|
68
97
|
description: parsed.description,
|
|
69
98
|
mtimeMs: st.mtimeMs,
|
|
70
99
|
};
|
|
@@ -79,9 +108,14 @@ export function scanSoftSkills(agentId, opts = {}) {
|
|
|
79
108
|
export function collectAgentSkillSnapshot(agentId, opts = {}) {
|
|
80
109
|
return {
|
|
81
110
|
agentId,
|
|
111
|
+
...(opts.runtime ? { runtime: opts.runtime } : {}),
|
|
82
112
|
skills: scanSoftSkills(agentId, opts).map((skill) => ({
|
|
83
113
|
name: skill.name,
|
|
84
|
-
source: skill.source
|
|
114
|
+
source: snapshotSource(skill.source),
|
|
115
|
+
sourceDetail: skill.source,
|
|
116
|
+
...(skill.runtime ? { runtime: skill.runtime } : {}),
|
|
117
|
+
path: skill.path,
|
|
118
|
+
...(skill.profile ? { profile: skill.profile } : {}),
|
|
85
119
|
...(skill.description ? { description: skill.description } : {}),
|
|
86
120
|
mtimeMs: skill.mtimeMs,
|
|
87
121
|
})),
|
|
@@ -157,7 +191,7 @@ function dedupeDirs(dirs) {
|
|
|
157
191
|
if (seen.has(resolved))
|
|
158
192
|
continue;
|
|
159
193
|
seen.add(resolved);
|
|
160
|
-
out.push({ dir: resolved
|
|
194
|
+
out.push({ ...entry, dir: resolved });
|
|
161
195
|
}
|
|
162
196
|
return out;
|
|
163
197
|
}
|
|
@@ -166,46 +200,59 @@ function expandSkillRoots(dirs) {
|
|
|
166
200
|
for (const entry of dirs) {
|
|
167
201
|
out.push(entry);
|
|
168
202
|
if (entry.source.includes("codex")) {
|
|
169
|
-
out.push({ dir: path.join(entry.dir, ".system")
|
|
203
|
+
out.push({ ...entry, dir: path.join(entry.dir, ".system") });
|
|
170
204
|
}
|
|
171
205
|
}
|
|
172
206
|
return out;
|
|
173
207
|
}
|
|
208
|
+
function hermesSkillRoot(agentId, profile) {
|
|
209
|
+
if (profile) {
|
|
210
|
+
try {
|
|
211
|
+
return {
|
|
212
|
+
dir: path.join(hermesProfileHomeDir(profile), "skills"),
|
|
213
|
+
source: "agent-hermes-profile",
|
|
214
|
+
runtime: "hermes-agent",
|
|
215
|
+
profile,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Corrupt legacy credentials should not make the whole skill snapshot fail.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
dir: path.join(agentHermesHomeDir(agentId), "skills"),
|
|
224
|
+
source: "agent-hermes",
|
|
225
|
+
runtime: "hermes-agent",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
174
228
|
function runtimeFamily(runtime) {
|
|
175
229
|
if (runtime === "codex")
|
|
176
230
|
return "codex";
|
|
231
|
+
if (runtime === "hermes-agent")
|
|
232
|
+
return "hermes";
|
|
233
|
+
if (!runtime)
|
|
234
|
+
return "claude";
|
|
177
235
|
if (runtime === "claude-code")
|
|
178
236
|
return "claude";
|
|
179
237
|
return "other";
|
|
180
238
|
}
|
|
181
|
-
function priority(source,
|
|
182
|
-
if (runtimeFamily(runtime) === "codex") {
|
|
183
|
-
switch (source) {
|
|
184
|
-
case "agent-codex":
|
|
185
|
-
return 0;
|
|
186
|
-
case "global-codex":
|
|
187
|
-
return 1;
|
|
188
|
-
case "agent-claude":
|
|
189
|
-
return 2;
|
|
190
|
-
case "global-claude":
|
|
191
|
-
return 3;
|
|
192
|
-
default:
|
|
193
|
-
return 4;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
239
|
+
function priority(source, _runtime) {
|
|
196
240
|
switch (source) {
|
|
197
241
|
case "agent-claude":
|
|
198
|
-
return 0;
|
|
199
242
|
case "agent-codex":
|
|
200
|
-
|
|
243
|
+
case "agent-hermes":
|
|
244
|
+
case "agent-hermes-profile":
|
|
245
|
+
return 0;
|
|
201
246
|
case "global-claude":
|
|
202
|
-
return 2;
|
|
203
247
|
case "global-codex":
|
|
204
|
-
return
|
|
248
|
+
return 1;
|
|
205
249
|
default:
|
|
206
|
-
return
|
|
250
|
+
return 2;
|
|
207
251
|
}
|
|
208
252
|
}
|
|
253
|
+
function snapshotSource(source) {
|
|
254
|
+
return source.startsWith("agent-") ? "workspace" : "runtime-global";
|
|
255
|
+
}
|
|
209
256
|
function unquote(value) {
|
|
210
257
|
const trimmed = value.trim();
|
|
211
258
|
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ExecFileOptions } from "node:child_process";
|
|
2
|
+
import { type AgentSkillSnapshot } from "./skill-index.js";
|
|
3
|
+
export type SkillInstallTarget = "claude-code" | "codex";
|
|
4
|
+
export interface SkillFileManifest {
|
|
5
|
+
path: string;
|
|
6
|
+
content?: string;
|
|
7
|
+
sourcePath?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SkillManifestInput {
|
|
10
|
+
name?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
skillMd?: string;
|
|
14
|
+
markdown?: string;
|
|
15
|
+
files?: SkillFileManifest[];
|
|
16
|
+
targetRuntimes?: SkillInstallTarget[];
|
|
17
|
+
}
|
|
18
|
+
export interface SkillArchiveManifestInput {
|
|
19
|
+
name?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
skillMd?: string;
|
|
23
|
+
markdown?: string;
|
|
24
|
+
files?: SkillFileManifest[];
|
|
25
|
+
skills?: SkillManifestInput[];
|
|
26
|
+
targetRuntimes?: SkillInstallTarget[];
|
|
27
|
+
}
|
|
28
|
+
export interface NormalizedSkillManifest {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
skillMd: string;
|
|
32
|
+
files: SkillFileManifest[];
|
|
33
|
+
targetRuntimes?: SkillInstallTarget[];
|
|
34
|
+
}
|
|
35
|
+
export interface InstalledSkillRecord {
|
|
36
|
+
name: string;
|
|
37
|
+
targets: SkillInstallTarget[];
|
|
38
|
+
paths: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface AgentSkillInstallResult {
|
|
41
|
+
agentId: string;
|
|
42
|
+
installed: InstalledSkillRecord[];
|
|
43
|
+
snapshot: AgentSkillSnapshot;
|
|
44
|
+
}
|
|
45
|
+
export interface InstallAgentSkillManifestOptions {
|
|
46
|
+
runtime?: string;
|
|
47
|
+
sourceRoot?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface VercelSkillsInstallOptions {
|
|
50
|
+
agentId: string;
|
|
51
|
+
packageSpec: string;
|
|
52
|
+
skills?: string[];
|
|
53
|
+
runtime?: string;
|
|
54
|
+
executor?: VercelSkillsExecutor;
|
|
55
|
+
}
|
|
56
|
+
export type VercelSkillsExecutor = (command: string, args: string[], options: ExecFileOptions) => Promise<void>;
|
|
57
|
+
export declare function normalizeSkillManifest(input: SkillManifestInput): NormalizedSkillManifest;
|
|
58
|
+
export declare function installAgentSkillManifest(agentId: string, manifest: SkillManifestInput, opts?: InstallAgentSkillManifestOptions): AgentSkillInstallResult;
|
|
59
|
+
export declare function installBotLearnArchiveManifest(agentId: string, archive: SkillArchiveManifestInput, opts?: InstallAgentSkillManifestOptions): AgentSkillInstallResult;
|
|
60
|
+
export declare function installVercelSkillsForAgent(opts: VercelSkillsInstallOptions): Promise<AgentSkillInstallResult>;
|
|
61
|
+
export declare function buildVercelSkillsArgs(packageSpec: string, skills: string[] | undefined, targets: SkillInstallTarget[]): string[];
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { agentCodexHomeDir, agentWorkspaceDir, } from "./agent-workspace.js";
|
|
7
|
+
import { collectAgentSkillSnapshot, } from "./skill-index.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
10
|
+
const MAX_INLINE_FILE_BYTES = 256 * 1024;
|
|
11
|
+
const TRUSTED_VERCEL_PACKAGE_SPECS = new Set([
|
|
12
|
+
"https://github.com/vercel-labs/skills",
|
|
13
|
+
"github:vercel-labs/skills",
|
|
14
|
+
"vercel-labs/skills",
|
|
15
|
+
]);
|
|
16
|
+
export function normalizeSkillManifest(input) {
|
|
17
|
+
const rawName = input.name ?? input.id;
|
|
18
|
+
if (!rawName)
|
|
19
|
+
throw new Error("skill manifest requires name or id");
|
|
20
|
+
const name = assertSafeSkillName(rawName);
|
|
21
|
+
const description = sanitizeInline(input.description ?? "");
|
|
22
|
+
const skillMd = input.skillMd ?? input.markdown ?? renderSkillMarkdown(name, description);
|
|
23
|
+
if (!skillMd.trim())
|
|
24
|
+
throw new Error(`skill ${name} has empty SKILL.md content`);
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
...(description ? { description } : {}),
|
|
28
|
+
skillMd,
|
|
29
|
+
files: input.files ?? [],
|
|
30
|
+
...(input.targetRuntimes ? { targetRuntimes: normalizeTargets(input.targetRuntimes) } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function installAgentSkillManifest(agentId, manifest, opts = {}) {
|
|
34
|
+
const normalized = normalizeSkillManifest(manifest);
|
|
35
|
+
const installed = [
|
|
36
|
+
installNormalizedSkill(agentId, normalized, opts),
|
|
37
|
+
];
|
|
38
|
+
return {
|
|
39
|
+
agentId,
|
|
40
|
+
installed,
|
|
41
|
+
snapshot: collectAgentSkillSnapshot(agentId, { runtime: opts.runtime }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function installBotLearnArchiveManifest(agentId, archive, opts = {}) {
|
|
45
|
+
const skills = archive.skills && archive.skills.length > 0
|
|
46
|
+
? archive.skills
|
|
47
|
+
: [archive];
|
|
48
|
+
const installed = skills.map((skill) => installNormalizedSkill(agentId, normalizeSkillManifest({
|
|
49
|
+
...skill,
|
|
50
|
+
targetRuntimes: skill.targetRuntimes ?? archive.targetRuntimes,
|
|
51
|
+
}), opts));
|
|
52
|
+
return {
|
|
53
|
+
agentId,
|
|
54
|
+
installed,
|
|
55
|
+
snapshot: collectAgentSkillSnapshot(agentId, { runtime: opts.runtime }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function installVercelSkillsForAgent(opts) {
|
|
59
|
+
const packageSpec = normalizeTrustedVercelPackageSpec(opts.packageSpec);
|
|
60
|
+
const workspace = agentWorkspaceDir(opts.agentId);
|
|
61
|
+
const tempHome = mkdtempSync(path.join(tmpdir(), "botcord-skills-"));
|
|
62
|
+
const targets = targetsForRuntime(opts.runtime);
|
|
63
|
+
const executor = opts.executor ?? defaultVercelSkillsExecutor;
|
|
64
|
+
const args = buildVercelSkillsArgs(packageSpec, opts.skills, targets);
|
|
65
|
+
try {
|
|
66
|
+
await executor("npx", args, {
|
|
67
|
+
cwd: workspace,
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
HOME: tempHome,
|
|
71
|
+
USERPROFILE: tempHome,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const installed = importVercelInstalledSkills(opts.agentId, tempHome, targets);
|
|
75
|
+
return {
|
|
76
|
+
agentId: opts.agentId,
|
|
77
|
+
installed,
|
|
78
|
+
snapshot: collectAgentSkillSnapshot(opts.agentId, { runtime: opts.runtime }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
rmSync(tempHome, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function buildVercelSkillsArgs(packageSpec, skills, targets) {
|
|
86
|
+
const normalizedPackageSpec = normalizeTrustedVercelPackageSpec(packageSpec);
|
|
87
|
+
const args = ["--yes", "skills", "add", normalizedPackageSpec, "--global", "--copy", "--yes"];
|
|
88
|
+
for (const skill of skills ?? []) {
|
|
89
|
+
if (!skill.trim())
|
|
90
|
+
continue;
|
|
91
|
+
args.push("--skill", skill);
|
|
92
|
+
}
|
|
93
|
+
for (const target of targets) {
|
|
94
|
+
args.push("--agent", target === "codex" ? "codex" : "claude-code");
|
|
95
|
+
}
|
|
96
|
+
return args;
|
|
97
|
+
}
|
|
98
|
+
function installNormalizedSkill(agentId, manifest, opts) {
|
|
99
|
+
const targets = manifest.targetRuntimes ?? targetsForRuntime(opts.runtime);
|
|
100
|
+
validateSkillFiles(manifest.files, opts.sourceRoot);
|
|
101
|
+
const paths = [];
|
|
102
|
+
for (const target of targets) {
|
|
103
|
+
const skillDir = path.join(skillRootForTarget(agentId, target), manifest.name);
|
|
104
|
+
writeSkillDir(skillDir, manifest, opts.sourceRoot);
|
|
105
|
+
paths.push(skillDir);
|
|
106
|
+
}
|
|
107
|
+
return { name: manifest.name, targets, paths };
|
|
108
|
+
}
|
|
109
|
+
function writeSkillDir(skillDir, manifest, sourceRoot) {
|
|
110
|
+
const parent = path.dirname(skillDir);
|
|
111
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
112
|
+
const tempDir = mkdtempSync(path.join(parent, `.${path.basename(skillDir)}-tmp-`));
|
|
113
|
+
try {
|
|
114
|
+
writeFileSync(path.join(tempDir, "SKILL.md"), manifest.skillMd, { mode: 0o600 });
|
|
115
|
+
for (const file of manifest.files) {
|
|
116
|
+
const relativePath = assertSafeRelativePath(file.path);
|
|
117
|
+
const dest = path.join(tempDir, relativePath);
|
|
118
|
+
mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
119
|
+
if (file.content !== undefined) {
|
|
120
|
+
writeFileSync(dest, file.content, { mode: 0o600 });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
copySafeSourcePath(sourceRoot, file.sourcePath, dest);
|
|
124
|
+
}
|
|
125
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
126
|
+
renameSync(tempDir, skillDir);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function importVercelInstalledSkills(agentId, tempHome, targets) {
|
|
134
|
+
const byName = new Map();
|
|
135
|
+
for (const target of targets) {
|
|
136
|
+
const sourceRoot = target === "codex"
|
|
137
|
+
? path.join(tempHome, ".codex", "skills")
|
|
138
|
+
: path.join(tempHome, ".claude", "skills");
|
|
139
|
+
if (!existsSync(sourceRoot))
|
|
140
|
+
continue;
|
|
141
|
+
for (const sourceSkillDir of findSkillDirs(sourceRoot)) {
|
|
142
|
+
const name = assertSafeSkillName(readSkillName(sourceSkillDir));
|
|
143
|
+
const dest = path.join(skillRootForTarget(agentId, target), name);
|
|
144
|
+
copySafeSkillDir(sourceSkillDir, dest);
|
|
145
|
+
const existing = byName.get(name);
|
|
146
|
+
if (existing) {
|
|
147
|
+
existing.targets.push(target);
|
|
148
|
+
existing.paths.push(dest);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
byName.set(name, { name, targets: [target], paths: [dest] });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
156
|
+
}
|
|
157
|
+
function findSkillDirs(root) {
|
|
158
|
+
const out = [];
|
|
159
|
+
const visit = (dir, depth) => {
|
|
160
|
+
if (depth > 3)
|
|
161
|
+
return;
|
|
162
|
+
const skillMd = path.join(dir, "SKILL.md");
|
|
163
|
+
if (existsSync(skillMd) && lstatSync(skillMd).isFile()) {
|
|
164
|
+
out.push(dir);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let children;
|
|
168
|
+
try {
|
|
169
|
+
children = readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const child of children) {
|
|
175
|
+
const childPath = path.join(dir, child);
|
|
176
|
+
try {
|
|
177
|
+
const childStat = lstatSync(childPath);
|
|
178
|
+
if (childStat.isSymbolicLink())
|
|
179
|
+
continue;
|
|
180
|
+
if (childStat.isDirectory())
|
|
181
|
+
visit(childPath, depth + 1);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* skip unreadable entries */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
visit(root, 0);
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function readSkillName(skillDir) {
|
|
192
|
+
const fallback = path.basename(skillDir);
|
|
193
|
+
let raw = "";
|
|
194
|
+
try {
|
|
195
|
+
raw = readFileSync(path.join(skillDir, "SKILL.md"), "utf8").slice(0, 8192);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return fallback;
|
|
199
|
+
}
|
|
200
|
+
const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
201
|
+
const name = fm?.[1]?.match(/^name:\s*(.+?)\s*$/m)?.[1];
|
|
202
|
+
return name ? unquote(name).trim() : fallback;
|
|
203
|
+
}
|
|
204
|
+
function targetsForRuntime(runtime) {
|
|
205
|
+
if (runtime === "codex")
|
|
206
|
+
return ["codex"];
|
|
207
|
+
if (runtime === "claude-code")
|
|
208
|
+
return ["claude-code"];
|
|
209
|
+
return ["claude-code", "codex"];
|
|
210
|
+
}
|
|
211
|
+
function normalizeTargets(targets) {
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const target of targets) {
|
|
214
|
+
if (target !== "claude-code" && target !== "codex") {
|
|
215
|
+
throw new Error(`unsupported skill target: ${String(target)}`);
|
|
216
|
+
}
|
|
217
|
+
if (!out.includes(target))
|
|
218
|
+
out.push(target);
|
|
219
|
+
}
|
|
220
|
+
if (out.length === 0)
|
|
221
|
+
throw new Error("at least one target runtime is required");
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function normalizeTrustedVercelPackageSpec(packageSpec) {
|
|
225
|
+
const cleaned = packageSpec.trim();
|
|
226
|
+
if (!cleaned)
|
|
227
|
+
throw new Error("packageSpec is required");
|
|
228
|
+
if (!TRUSTED_VERCEL_PACKAGE_SPECS.has(cleaned)) {
|
|
229
|
+
throw new Error(`unsupported vercel skills packageSpec: ${cleaned}`);
|
|
230
|
+
}
|
|
231
|
+
return cleaned;
|
|
232
|
+
}
|
|
233
|
+
function validateSkillFiles(files, sourceRoot) {
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
assertSafeRelativePath(file.path);
|
|
236
|
+
if (file.content !== undefined) {
|
|
237
|
+
if (Buffer.byteLength(file.content, "utf8") > MAX_INLINE_FILE_BYTES) {
|
|
238
|
+
throw new Error(`skill file too large: ${file.path}`);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!sourceRoot || !file.sourcePath) {
|
|
243
|
+
throw new Error(`skill file ${file.path} requires content or sourcePath with sourceRoot`);
|
|
244
|
+
}
|
|
245
|
+
resolveSafeSourcePath(sourceRoot, file.sourcePath);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function resolveSafeSourcePath(sourceRoot, relativeSourcePath) {
|
|
249
|
+
const safeRelativePath = assertSafeRelativePath(relativeSourcePath);
|
|
250
|
+
const rootReal = realpathSync(sourceRoot);
|
|
251
|
+
const sourcePath = path.resolve(sourceRoot, safeRelativePath);
|
|
252
|
+
if (lstatSync(sourcePath).isSymbolicLink()) {
|
|
253
|
+
throw new Error(`unsafe source path symlink: ${relativeSourcePath}`);
|
|
254
|
+
}
|
|
255
|
+
const sourceReal = realpathSync(sourcePath);
|
|
256
|
+
if (sourceReal !== rootReal && !sourceReal.startsWith(`${rootReal}${path.sep}`)) {
|
|
257
|
+
throw new Error(`unsafe source path: ${relativeSourcePath}`);
|
|
258
|
+
}
|
|
259
|
+
return sourceReal;
|
|
260
|
+
}
|
|
261
|
+
function copySafeSourcePath(sourceRoot, relativeSourcePath, dest) {
|
|
262
|
+
const source = resolveSafeSourcePath(sourceRoot, relativeSourcePath);
|
|
263
|
+
copyNoSymlinks(source, dest);
|
|
264
|
+
}
|
|
265
|
+
function copySafeSkillDir(sourceSkillDir, dest) {
|
|
266
|
+
const parent = path.dirname(dest);
|
|
267
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
268
|
+
const tempDir = mkdtempSync(path.join(parent, `.${path.basename(dest)}-tmp-`));
|
|
269
|
+
try {
|
|
270
|
+
copyNoSymlinks(sourceSkillDir, tempDir);
|
|
271
|
+
rmSync(dest, { recursive: true, force: true });
|
|
272
|
+
renameSync(tempDir, dest);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function copyNoSymlinks(source, dest) {
|
|
280
|
+
const sourceStat = lstatSync(source);
|
|
281
|
+
if (sourceStat.isSymbolicLink()) {
|
|
282
|
+
throw new Error(`skill import rejects symlink: ${source}`);
|
|
283
|
+
}
|
|
284
|
+
if (sourceStat.isDirectory()) {
|
|
285
|
+
mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
286
|
+
for (const child of readdirSync(source)) {
|
|
287
|
+
copyNoSymlinks(path.join(source, child), path.join(dest, child));
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!sourceStat.isFile()) {
|
|
292
|
+
throw new Error(`skill import supports only files and directories: ${source}`);
|
|
293
|
+
}
|
|
294
|
+
cpSync(source, dest, { force: true, dereference: false });
|
|
295
|
+
}
|
|
296
|
+
function skillRootForTarget(agentId, target) {
|
|
297
|
+
return target === "codex"
|
|
298
|
+
? path.join(agentCodexHomeDir(agentId), "skills")
|
|
299
|
+
: path.join(agentWorkspaceDir(agentId), ".claude", "skills");
|
|
300
|
+
}
|
|
301
|
+
function assertSafeSkillName(value) {
|
|
302
|
+
const name = value.trim();
|
|
303
|
+
if (!SAFE_SKILL_NAME.test(name) || name === "." || name === "..") {
|
|
304
|
+
throw new Error(`unsafe skill name: ${JSON.stringify(value)}`);
|
|
305
|
+
}
|
|
306
|
+
return name;
|
|
307
|
+
}
|
|
308
|
+
function assertSafeRelativePath(value) {
|
|
309
|
+
const normalized = path.normalize(value);
|
|
310
|
+
if (!value ||
|
|
311
|
+
path.isAbsolute(value) ||
|
|
312
|
+
normalized === "." ||
|
|
313
|
+
normalized.startsWith("..") ||
|
|
314
|
+
normalized.split(path.sep).includes("..")) {
|
|
315
|
+
throw new Error(`unsafe skill file path: ${JSON.stringify(value)}`);
|
|
316
|
+
}
|
|
317
|
+
return normalized;
|
|
318
|
+
}
|
|
319
|
+
function renderSkillMarkdown(name, description) {
|
|
320
|
+
const desc = description ? `description: "${description.replace(/"/g, '\\"')}"\n` : "";
|
|
321
|
+
return `---\nname: ${name}\n${desc}---\n\n# ${name}\n`;
|
|
322
|
+
}
|
|
323
|
+
function sanitizeInline(value) {
|
|
324
|
+
return value.replace(/\s+/g, " ").trim();
|
|
325
|
+
}
|
|
326
|
+
function unquote(value) {
|
|
327
|
+
const trimmed = value.trim();
|
|
328
|
+
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
329
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
330
|
+
return trimmed.slice(1, -1);
|
|
331
|
+
}
|
|
332
|
+
return trimmed;
|
|
333
|
+
}
|
|
334
|
+
async function defaultVercelSkillsExecutor(command, args, options) {
|
|
335
|
+
await execFileAsync(command, args, {
|
|
336
|
+
...options,
|
|
337
|
+
encoding: "utf8",
|
|
338
|
+
maxBuffer: 1024 * 1024,
|
|
339
|
+
});
|
|
340
|
+
}
|