@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.
@@ -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
- export declare function defaultSkillDirs(agentId: string, opts?: SkillIndexOptions): Array<{
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 {};
@@ -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 dirs = runtimeFamily(opts.runtime) === "codex"
19
- ? [agentCodex, agentClaude]
20
- : [agentClaude, agentCodex];
21
- if (includeGlobal) {
22
- const globalClaude = { dir: path.join(homedir(), ".claude", "skills"), source: "global-claude" };
23
- const globalCodex = { dir: path.join(homedir(), ".codex", "skills"), source: "global-codex" };
24
- dirs.push(...(runtimeFamily(opts.runtime) === "codex"
25
- ? [globalCodex, globalClaude]
26
- : [globalClaude, globalCodex]));
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({ dir, source: "external" });
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.startsWith("agent-") ? "workspace" : "runtime-global",
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, source: entry.source });
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"), source: entry.source });
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, runtime) {
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
- }
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
+ }
@@ -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.
@@ -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", {