@botcord/daemon 0.2.89 → 0.2.91
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/channels/botcord.js +35 -22
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +103 -9
- package/dist/runtime-models.js +46 -0
- package/dist/self-restart.d.ts +29 -0
- package/dist/self-restart.js +172 -0
- package/dist/skill-index.d.ts +14 -2
- package/dist/skill-index.js +109 -41
- 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__/provision.test.ts +23 -0
- package/src/__tests__/runtime-discovery.test.ts +6 -3
- package/src/__tests__/runtime-models.test.ts +53 -0
- package/src/__tests__/self-restart.test.ts +57 -0
- package/src/__tests__/skill-index.test.ts +130 -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__/botcord-channel.test.ts +38 -0
- package/src/gateway/channels/botcord.ts +41 -22
- package/src/provision.ts +111 -14
- package/src/runtime-models.ts +47 -0
- package/src/self-restart.ts +218 -0
- package/src/skill-index.ts +130 -53
- package/src/skill-installer.ts +472 -0
- package/src/system-context.ts +7 -1
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,76 @@ 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 agentGemini = [
|
|
22
|
+
{
|
|
23
|
+
dir: path.join(agentWorkspaceDir(agentId), ".gemini", "skills"),
|
|
24
|
+
source: "agent-gemini",
|
|
25
|
+
runtime: "gemini",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
dir: path.join(agentWorkspaceDir(agentId), ".agents", "skills"),
|
|
29
|
+
source: "agent-agents",
|
|
30
|
+
runtime: "gemini",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const agentHermes = hermesSkillRoot(agentId, opts.hermesProfile);
|
|
34
|
+
const dirs = [];
|
|
35
|
+
switch (runtimeFamily(opts.runtime)) {
|
|
36
|
+
case "codex":
|
|
37
|
+
dirs.push(agentCodex);
|
|
38
|
+
if (includeGlobal) {
|
|
39
|
+
dirs.push({
|
|
40
|
+
dir: path.join(homedir(), ".codex", "skills"),
|
|
41
|
+
source: "global-codex",
|
|
42
|
+
runtime: "codex",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "hermes":
|
|
47
|
+
dirs.push(agentHermes);
|
|
48
|
+
break;
|
|
49
|
+
case "gemini":
|
|
50
|
+
dirs.push(...agentGemini);
|
|
51
|
+
if (includeGlobal) {
|
|
52
|
+
dirs.push({
|
|
53
|
+
dir: path.join(homedir(), ".gemini", "skills"),
|
|
54
|
+
source: "global-gemini",
|
|
55
|
+
runtime: "gemini",
|
|
56
|
+
}, {
|
|
57
|
+
dir: path.join(homedir(), ".agents", "skills"),
|
|
58
|
+
source: "global-agents",
|
|
59
|
+
runtime: "gemini",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "claude":
|
|
64
|
+
dirs.push(agentClaude);
|
|
65
|
+
if (includeGlobal) {
|
|
66
|
+
dirs.push({
|
|
67
|
+
dir: path.join(homedir(), ".claude", "skills"),
|
|
68
|
+
source: "global-claude",
|
|
69
|
+
runtime: "claude-code",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "other":
|
|
74
|
+
break;
|
|
27
75
|
}
|
|
28
76
|
const envDirs = parseSkillDirsEnv(process.env.BOTCORD_SKILL_DIRS);
|
|
29
77
|
for (const dir of [...envDirs, ...(opts.extraDirs ?? [])]) {
|
|
30
|
-
dirs.push({
|
|
78
|
+
dirs.push({
|
|
79
|
+
dir,
|
|
80
|
+
source: "external",
|
|
81
|
+
...(opts.runtime ? { runtime: opts.runtime } : {}),
|
|
82
|
+
...(opts.hermesProfile ? { profile: opts.hermesProfile } : {}),
|
|
83
|
+
});
|
|
31
84
|
}
|
|
32
85
|
return dedupeDirs(expandSkillRoots(dirs));
|
|
33
86
|
}
|
|
@@ -65,6 +118,8 @@ export function scanSoftSkills(agentId, opts = {}) {
|
|
|
65
118
|
name: parsed.name,
|
|
66
119
|
path: skillMd,
|
|
67
120
|
source: root.source,
|
|
121
|
+
...(root.runtime ? { runtime: root.runtime } : {}),
|
|
122
|
+
...(root.profile ? { profile: root.profile } : {}),
|
|
68
123
|
description: parsed.description,
|
|
69
124
|
mtimeMs: st.mtimeMs,
|
|
70
125
|
};
|
|
@@ -79,9 +134,14 @@ export function scanSoftSkills(agentId, opts = {}) {
|
|
|
79
134
|
export function collectAgentSkillSnapshot(agentId, opts = {}) {
|
|
80
135
|
return {
|
|
81
136
|
agentId,
|
|
137
|
+
...(opts.runtime ? { runtime: opts.runtime } : {}),
|
|
82
138
|
skills: scanSoftSkills(agentId, opts).map((skill) => ({
|
|
83
139
|
name: skill.name,
|
|
84
|
-
source: skill.source
|
|
140
|
+
source: snapshotSource(skill.source),
|
|
141
|
+
sourceDetail: skill.source,
|
|
142
|
+
...(skill.runtime ? { runtime: skill.runtime } : {}),
|
|
143
|
+
path: skill.path,
|
|
144
|
+
...(skill.profile ? { profile: skill.profile } : {}),
|
|
85
145
|
...(skill.description ? { description: skill.description } : {}),
|
|
86
146
|
mtimeMs: skill.mtimeMs,
|
|
87
147
|
})),
|
|
@@ -157,7 +217,7 @@ function dedupeDirs(dirs) {
|
|
|
157
217
|
if (seen.has(resolved))
|
|
158
218
|
continue;
|
|
159
219
|
seen.add(resolved);
|
|
160
|
-
out.push({ dir: resolved
|
|
220
|
+
out.push({ ...entry, dir: resolved });
|
|
161
221
|
}
|
|
162
222
|
return out;
|
|
163
223
|
}
|
|
@@ -166,45 +226,53 @@ function expandSkillRoots(dirs) {
|
|
|
166
226
|
for (const entry of dirs) {
|
|
167
227
|
out.push(entry);
|
|
168
228
|
if (entry.source.includes("codex")) {
|
|
169
|
-
out.push({ dir: path.join(entry.dir, ".system")
|
|
229
|
+
out.push({ ...entry, dir: path.join(entry.dir, ".system") });
|
|
170
230
|
}
|
|
171
231
|
}
|
|
172
232
|
return out;
|
|
173
233
|
}
|
|
234
|
+
function hermesSkillRoot(agentId, profile) {
|
|
235
|
+
if (profile) {
|
|
236
|
+
try {
|
|
237
|
+
return {
|
|
238
|
+
dir: path.join(hermesProfileHomeDir(profile), "skills"),
|
|
239
|
+
source: "agent-hermes-profile",
|
|
240
|
+
runtime: "hermes-agent",
|
|
241
|
+
profile,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Corrupt legacy credentials should not make the whole skill snapshot fail.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
dir: path.join(agentHermesHomeDir(agentId), "skills"),
|
|
250
|
+
source: "agent-hermes",
|
|
251
|
+
runtime: "hermes-agent",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
174
254
|
function runtimeFamily(runtime) {
|
|
175
255
|
if (runtime === "codex")
|
|
176
256
|
return "codex";
|
|
257
|
+
if (runtime === "gemini")
|
|
258
|
+
return "gemini";
|
|
259
|
+
if (runtime === "hermes-agent")
|
|
260
|
+
return "hermes";
|
|
261
|
+
if (!runtime)
|
|
262
|
+
return "claude";
|
|
177
263
|
if (runtime === "claude-code")
|
|
178
264
|
return "claude";
|
|
179
265
|
return "other";
|
|
180
266
|
}
|
|
181
|
-
function priority(source,
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
case "global-claude":
|
|
191
|
-
return 3;
|
|
192
|
-
default:
|
|
193
|
-
return 4;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
switch (source) {
|
|
197
|
-
case "agent-claude":
|
|
198
|
-
return 0;
|
|
199
|
-
case "agent-codex":
|
|
200
|
-
return 1;
|
|
201
|
-
case "global-claude":
|
|
202
|
-
return 2;
|
|
203
|
-
case "global-codex":
|
|
204
|
-
return 3;
|
|
205
|
-
default:
|
|
206
|
-
return 4;
|
|
207
|
-
}
|
|
267
|
+
function priority(source, _runtime) {
|
|
268
|
+
if (source.startsWith("agent-"))
|
|
269
|
+
return 0;
|
|
270
|
+
if (source.startsWith("global-"))
|
|
271
|
+
return 1;
|
|
272
|
+
return 2;
|
|
273
|
+
}
|
|
274
|
+
function snapshotSource(source) {
|
|
275
|
+
return source.startsWith("agent-") ? "workspace" : "runtime-global";
|
|
208
276
|
}
|
|
209
277
|
function unquote(value) {
|
|
210
278
|
const trimmed = value.trim();
|
|
@@ -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
|
+
}
|
package/dist/system-context.d.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { GatewayInboundMessage } from "./gateway/index.js";
|
|
28
28
|
import type { ActivityTracker } from "./activity-tracker.js";
|
|
29
|
+
import type { SkillIndexOptions } from "./skill-index.js";
|
|
29
30
|
/**
|
|
30
31
|
* Async per-turn room-context builder (see `room-context.ts`). Returns the
|
|
31
32
|
* rendered `[BotCord Room Context]` block, or `null` when there is nothing
|
|
@@ -59,6 +60,11 @@ export interface SystemContextDeps {
|
|
|
59
60
|
* dirs each turn. Return null to suppress the block.
|
|
60
61
|
*/
|
|
61
62
|
skillIndexBuilder?: (message: GatewayInboundMessage) => string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Runtime/profile options for the default soft skill scanner. Kept lazy so
|
|
65
|
+
* hot-provisioned runtime changes are visible without rebuilding this closure.
|
|
66
|
+
*/
|
|
67
|
+
skillIndexOptions?: (message: GatewayInboundMessage) => SkillIndexOptions;
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Build a {@link SystemContextBuilder} for the gateway dispatcher.
|
package/dist/system-context.js
CHANGED
|
@@ -112,7 +112,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
112
112
|
try {
|
|
113
113
|
if (deps.skillIndexBuilder)
|
|
114
114
|
return deps.skillIndexBuilder(message);
|
|
115
|
-
return buildSoftSkillIndexPrompt(deps.agentId);
|
|
115
|
+
return buildSoftSkillIndexPrompt(deps.agentId, deps.skillIndexOptions?.(message) ?? {});
|
|
116
116
|
}
|
|
117
117
|
catch (err) {
|
|
118
118
|
log.warn("system-context: skill index build failed — skipping skill block", {
|