@rbbtsn0w/adg 0.1.0-alpha.1

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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/bin/adg.ts +758 -0
  4. package/docs/agents-spec.md +132 -0
  5. package/docs/authoring.md +352 -0
  6. package/package.json +50 -0
  7. package/schemas/adg-plugin.schema.json +77 -0
  8. package/schemas/marketplace.schema.json +86 -0
  9. package/schemas/plugin-lock.schema.json +90 -0
  10. package/src/adapters/anthropic.ts +54 -0
  11. package/src/adapters/index.ts +24 -0
  12. package/src/adapters/openai.ts +37 -0
  13. package/src/adapters/reverse.ts +60 -0
  14. package/src/agents/claude.ts +124 -0
  15. package/src/agents/codex.ts +67 -0
  16. package/src/agents/index.ts +12 -0
  17. package/src/agents/registry.ts +30 -0
  18. package/src/agents/types.ts +47 -0
  19. package/src/commands/adapt.ts +36 -0
  20. package/src/commands/import.ts +69 -0
  21. package/src/commands/init.ts +146 -0
  22. package/src/commands/install.ts +411 -0
  23. package/src/commands/link.ts +61 -0
  24. package/src/commands/list.ts +28 -0
  25. package/src/commands/marketplace.ts +198 -0
  26. package/src/commands/migrate.ts +84 -0
  27. package/src/commands/multiselect-skills.ts +137 -0
  28. package/src/commands/remove.ts +136 -0
  29. package/src/commands/select-agents.ts +45 -0
  30. package/src/commands/select-components.ts +66 -0
  31. package/src/commands/select-plugins.ts +28 -0
  32. package/src/commands/select-scope.ts +21 -0
  33. package/src/commands/update.ts +85 -0
  34. package/src/commands/validate.ts +57 -0
  35. package/src/components.ts +90 -0
  36. package/src/deps.ts +64 -0
  37. package/src/fsutil.ts +38 -0
  38. package/src/hash.ts +61 -0
  39. package/src/lock.ts +57 -0
  40. package/src/manifest.ts +113 -0
  41. package/src/marketplace.ts +41 -0
  42. package/src/package.ts +74 -0
  43. package/src/paths.ts +129 -0
  44. package/src/semver.ts +67 -0
  45. package/src/skills.ts +88 -0
  46. package/src/sources.ts +159 -0
  47. package/src/types.ts +140 -0
  48. package/vendor/skills/LICENSE +29 -0
  49. package/vendor/skills/PROVENANCE.md +60 -0
  50. package/vendor/skills/ThirdPartyNoticeText.txt +117 -0
  51. package/vendor/skills/package.json +143 -0
  52. package/vendor/skills/src/add.ts +1999 -0
  53. package/vendor/skills/src/agents.ts +755 -0
  54. package/vendor/skills/src/blob.ts +567 -0
  55. package/vendor/skills/src/cli.ts +387 -0
  56. package/vendor/skills/src/constants.ts +3 -0
  57. package/vendor/skills/src/detect-agent.ts +62 -0
  58. package/vendor/skills/src/find.ts +357 -0
  59. package/vendor/skills/src/frontmatter.ts +16 -0
  60. package/vendor/skills/src/git-tree.ts +36 -0
  61. package/vendor/skills/src/git.ts +277 -0
  62. package/vendor/skills/src/install.ts +91 -0
  63. package/vendor/skills/src/installer.ts +1097 -0
  64. package/vendor/skills/src/list.ts +231 -0
  65. package/vendor/skills/src/local-lock.ts +182 -0
  66. package/vendor/skills/src/plugin-manifest.ts +183 -0
  67. package/vendor/skills/src/prompts/search-multiselect.ts +387 -0
  68. package/vendor/skills/src/providers/index.ts +14 -0
  69. package/vendor/skills/src/providers/registry.ts +51 -0
  70. package/vendor/skills/src/providers/types.ts +97 -0
  71. package/vendor/skills/src/providers/wellknown.ts +804 -0
  72. package/vendor/skills/src/remove.ts +323 -0
  73. package/vendor/skills/src/sanitize.ts +65 -0
  74. package/vendor/skills/src/self-cli.ts +20 -0
  75. package/vendor/skills/src/skill-lock.ts +329 -0
  76. package/vendor/skills/src/skills.ts +316 -0
  77. package/vendor/skills/src/source-parser.ts +438 -0
  78. package/vendor/skills/src/sync.ts +478 -0
  79. package/vendor/skills/src/telemetry.ts +186 -0
  80. package/vendor/skills/src/test-utils.ts +73 -0
  81. package/vendor/skills/src/types.ts +128 -0
  82. package/vendor/skills/src/update-source.ts +90 -0
  83. package/vendor/skills/src/update.ts +749 -0
  84. package/vendor/skills/src/use.ts +675 -0
@@ -0,0 +1,675 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { cp, mkdir, mkdtemp, readdir, readFile, realpath, stat, writeFile } from 'fs/promises';
4
+ import { dirname, join, normalize, relative, resolve, sep } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { agents } from './agents.ts';
7
+ import { tryBlobInstall, type BlobInstallResult, type BlobSkill } from './blob.ts';
8
+ import { cloneRepo, cleanupTempDir, GitCloneError } from './git.ts';
9
+ import { sanitizeName } from './installer.ts';
10
+ import { getGitHubToken } from './skill-lock.ts';
11
+ import { discoverSkills, filterSkills, getSkillDisplayName } from './skills.ts';
12
+ import { getOwnerRepo, parseSource } from './source-parser.ts';
13
+ import type { AgentType, Skill } from './types.ts';
14
+ import {
15
+ wellKnownProvider,
16
+ type WellKnownSkill,
17
+ type WellKnownFileContent,
18
+ } from './providers/wellknown.ts';
19
+
20
+ export interface UseOptions {
21
+ skill?: string;
22
+ agent?: string[];
23
+ fullDepth?: boolean;
24
+ dangerouslyAcceptOpenclawRisks?: boolean;
25
+ help?: boolean;
26
+ }
27
+
28
+ export interface ParseUseOptionsResult {
29
+ source: string[];
30
+ options: UseOptions;
31
+ errors: string[];
32
+ }
33
+
34
+ export type UseSkill =
35
+ | {
36
+ kind: 'blob';
37
+ name: string;
38
+ directoryName: string;
39
+ rawContent: string;
40
+ files: Array<{ path: string; contents: string }>;
41
+ }
42
+ | {
43
+ kind: 'disk';
44
+ name: string;
45
+ directoryName: string;
46
+ rawContent?: string;
47
+ path: string;
48
+ }
49
+ | {
50
+ kind: 'well-known';
51
+ name: string;
52
+ directoryName: string;
53
+ rawContent: string;
54
+ files: Map<string, WellKnownFileContent>;
55
+ };
56
+
57
+ export interface MaterializedUseSkill {
58
+ tempRoot: string;
59
+ skillDir: string;
60
+ skillMd: string;
61
+ hasSupportingFiles: boolean;
62
+ }
63
+
64
+ export interface AgentProcess {
65
+ on: (event: 'error' | 'close', listener: (...args: any[]) => void) => AgentProcess;
66
+ }
67
+
68
+ export type AgentSpawn = (
69
+ command: string,
70
+ args: string[],
71
+ options: { stdio: 'inherit' }
72
+ ) => AgentProcess;
73
+
74
+ interface UseAgentConfig {
75
+ command: string;
76
+ args: string[];
77
+ }
78
+
79
+ const BLOB_ALLOWED_OWNERS = ['vercel', 'vercel-labs', 'heygen-com'];
80
+ const EXCLUDE_FILES = new Set(['metadata.json']);
81
+ const EXCLUDE_DIRS = new Set(['.git', '__pycache__', '__pypackages__']);
82
+ const USE_AGENT_CONFIGS: Partial<Record<AgentType, UseAgentConfig>> = {
83
+ 'claude-code': { command: 'claude', args: [] },
84
+ codex: { command: 'codex', args: [] },
85
+ };
86
+ const SUPPORTED_USE_AGENTS = Object.keys(USE_AGENT_CONFIGS) as AgentType[];
87
+
88
+ export function parseUseOptions(args: string[]): ParseUseOptionsResult {
89
+ const source: string[] = [];
90
+ const options: UseOptions = {};
91
+ const errors: string[] = [];
92
+
93
+ for (let i = 0; i < args.length; i++) {
94
+ const arg = args[i];
95
+ if (!arg) continue;
96
+
97
+ if (arg === '--help' || arg === '-h') {
98
+ options.help = true;
99
+ } else if (arg === '--full-depth') {
100
+ options.fullDepth = true;
101
+ } else if (arg === '--dangerously-accept-openclaw-risks') {
102
+ options.dangerouslyAcceptOpenclawRisks = true;
103
+ } else if (arg === '--skill' || arg === '-s') {
104
+ const value = args[i + 1];
105
+ if (!value || value.startsWith('-')) {
106
+ errors.push(`${arg} requires a skill name`);
107
+ } else if (options.skill) {
108
+ errors.push('Only one --skill value can be provided');
109
+ i++;
110
+ } else {
111
+ options.skill = value;
112
+ i++;
113
+ }
114
+ } else if (arg === '--agent' || arg === '-a') {
115
+ // Take exactly one value (like --skill). Consuming greedily here would
116
+ // swallow the source in `skills use --agent claude-code <source>`. Repeated
117
+ // --agent flags collect into the array and are rejected as >1 downstream.
118
+ const value = args[i + 1];
119
+ if (!value || value.startsWith('-')) {
120
+ errors.push(`${arg} requires an agent name`);
121
+ } else {
122
+ options.agent = options.agent || [];
123
+ options.agent.push(value);
124
+ i++;
125
+ }
126
+ } else if (arg.startsWith('-')) {
127
+ errors.push(`Unknown option: ${arg}`);
128
+ } else {
129
+ source.push(arg);
130
+ }
131
+ }
132
+
133
+ errors.push(...validateUseAgentOption(options.agent));
134
+
135
+ return { source, options, errors };
136
+ }
137
+
138
+ export function buildUsePrompt(input: {
139
+ skillMd: string;
140
+ supportDir?: string;
141
+ hasSupportingFiles: boolean;
142
+ }): string {
143
+ const sections = [
144
+ "You are being given a Skill to execute for the user's next request.",
145
+ 'Use the following SKILL.md as your instructions:',
146
+ `<SKILL.md>\n${input.skillMd}\n</SKILL.md>`,
147
+ ];
148
+
149
+ if (input.hasSupportingFiles && input.supportDir) {
150
+ sections.push(
151
+ `Supporting files for this skill were downloaded to:\n${input.supportDir}\n\nWhen the SKILL.md references relative paths, read them from that directory.`
152
+ );
153
+ }
154
+
155
+ return sections.join('\n\n') + '\n';
156
+ }
157
+
158
+ export async function materializeUseSkill(skill: UseSkill): Promise<MaterializedUseSkill> {
159
+ const tempRoot = await mkdtemp(join(tmpdir(), 'skills-use-'));
160
+ const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
161
+
162
+ if (!isPathSafe(tempRoot, skillDir)) {
163
+ throw new Error('Invalid skill name: potential path traversal detected');
164
+ }
165
+
166
+ await mkdir(skillDir, { recursive: true });
167
+
168
+ if (skill.kind === 'blob') {
169
+ await writeSnapshotFiles(skillDir, skill.files);
170
+ } else if (skill.kind === 'well-known') {
171
+ await writeMapFiles(skillDir, skill.files);
172
+ } else {
173
+ await copySkillDirectory(skill.path, skillDir);
174
+ }
175
+
176
+ const skillMd = skill.rawContent ?? (await readFile(join(skillDir, 'SKILL.md'), 'utf-8'));
177
+ const hasSupportingFiles = await containsSupportingFiles(skillDir, skillDir);
178
+
179
+ return { tempRoot, skillDir, skillMd, hasSupportingFiles };
180
+ }
181
+
182
+ export async function runUse(
183
+ sourceArgs: string[],
184
+ options: UseOptions = {},
185
+ parseErrors: string[] = []
186
+ ): Promise<void> {
187
+ let cloneTempDir: string | null = null;
188
+
189
+ try {
190
+ if (options.help) {
191
+ console.log(getUseHelp());
192
+ return;
193
+ }
194
+
195
+ if (parseErrors.length > 0) {
196
+ fail(parseErrors.join('\n'));
197
+ }
198
+
199
+ if (sourceArgs.length === 0) {
200
+ fail(`Missing required argument: source\n\n${getUseHelp()}`);
201
+ }
202
+
203
+ if (sourceArgs.length > 1) {
204
+ fail(`Expected one source, received ${sourceArgs.length}: ${sourceArgs.join(', ')}`);
205
+ }
206
+
207
+ const useAgent = options.agent?.[0] as AgentType | undefined;
208
+ if (useAgent && !USE_AGENT_CONFIGS[useAgent]) {
209
+ fail(formatUnsupportedAgentError(useAgent));
210
+ }
211
+
212
+ const source = sourceArgs[0]!;
213
+ const parsed = parseSource(source);
214
+ const ownerRepoRaw = getOwnerRepo(parsed);
215
+ const sourceOwner = ownerRepoRaw?.split('/')[0]?.toLowerCase();
216
+
217
+ if (sourceOwner === 'openclaw' && !options.dangerouslyAcceptOpenclawRisks) {
218
+ fail(
219
+ [
220
+ 'OpenClaw skills are unverified community submissions.',
221
+ 'Skills run with full agent permissions and could be malicious.',
222
+ `If you understand the risks, re-run with: skills use ${source} --dangerously-accept-openclaw-risks`,
223
+ ].join('\n')
224
+ );
225
+ }
226
+
227
+ const selector = resolveSelector(parsed.skillFilter, options.skill);
228
+ const includeInternal = selector !== undefined;
229
+
230
+ let selectedSkill: UseSkill;
231
+
232
+ if (parsed.type === 'well-known') {
233
+ const skills = await wellKnownProvider.fetchAllSkills(parsed.url);
234
+ selectedSkill = selectWellKnownSkill(skills, selector, source);
235
+ } else {
236
+ let skills: Skill[];
237
+ let blobResult: BlobInstallResult | null = null;
238
+
239
+ if (parsed.type === 'local') {
240
+ if (!existsSync(parsed.localPath!)) {
241
+ fail(`Local path does not exist: ${parsed.localPath}`);
242
+ }
243
+ skills = await discoverSkills(parsed.localPath!, parsed.subpath, {
244
+ includeInternal,
245
+ fullDepth: options.fullDepth,
246
+ });
247
+ } else if (parsed.type === 'github' && !options.fullDepth) {
248
+ const ownerRepo = getOwnerRepo(parsed);
249
+ const owner = ownerRepo?.split('/')[0]?.toLowerCase();
250
+ if (ownerRepo && owner && BLOB_ALLOWED_OWNERS.includes(owner)) {
251
+ blobResult = await tryBlobInstall(ownerRepo, {
252
+ subpath: parsed.subpath,
253
+ skillFilter: selector,
254
+ ref: parsed.ref,
255
+ getToken: getGitHubToken,
256
+ includeInternal,
257
+ });
258
+ }
259
+
260
+ if (blobResult) {
261
+ skills = blobResult.skills;
262
+ } else {
263
+ cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
264
+ skills = await discoverSkills(cloneTempDir, parsed.subpath, {
265
+ includeInternal,
266
+ fullDepth: options.fullDepth,
267
+ });
268
+ }
269
+ } else {
270
+ cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
271
+ skills = await discoverSkills(cloneTempDir, parsed.subpath, {
272
+ includeInternal,
273
+ fullDepth: options.fullDepth,
274
+ });
275
+ }
276
+
277
+ const selected = selectSkill(skills, selector, source);
278
+ if (blobResult && isBlobSkill(selected)) {
279
+ selectedSkill = {
280
+ kind: 'blob',
281
+ name: selected.name,
282
+ directoryName: selected.name,
283
+ rawContent: selected.rawContent ?? getSkillMdFromSnapshot(selected.files),
284
+ files: selected.files,
285
+ };
286
+ } else {
287
+ selectedSkill = {
288
+ kind: 'disk',
289
+ name: selected.name,
290
+ directoryName: selected.name,
291
+ rawContent: selected.rawContent,
292
+ path: selected.path,
293
+ };
294
+ }
295
+ }
296
+
297
+ const materialized = await materializeUseSkill(selectedSkill);
298
+ await removeTempDir(cloneTempDir);
299
+ cloneTempDir = null;
300
+
301
+ const prompt = buildUsePrompt({
302
+ skillMd: materialized.skillMd,
303
+ supportDir: materialized.skillDir,
304
+ hasSupportingFiles: materialized.hasSupportingFiles,
305
+ });
306
+
307
+ if (useAgent) {
308
+ // The agent reads supportDir while it runs, so clean up only once it exits.
309
+ let exitCode: number;
310
+ try {
311
+ exitCode = await launchAgentInteractively(useAgent, prompt);
312
+ } finally {
313
+ await removeTempDir(materialized.tempRoot);
314
+ }
315
+ if (exitCode !== 0) {
316
+ process.exit(exitCode);
317
+ }
318
+ return;
319
+ }
320
+
321
+ // When piping the prompt out, a downstream agent reads supportDir *after*
322
+ // this process exits, so the temp dir must survive. Without supporting files
323
+ // nothing references it and it can be removed right away.
324
+ if (!materialized.hasSupportingFiles) {
325
+ await removeTempDir(materialized.tempRoot);
326
+ }
327
+
328
+ process.stdout.write(prompt);
329
+ } catch (error) {
330
+ await removeTempDir(cloneTempDir);
331
+ if (error instanceof GitCloneError) {
332
+ fail(error.message);
333
+ }
334
+ if (error instanceof UseCommandError) {
335
+ fail(error.message);
336
+ }
337
+ fail(error instanceof Error ? error.message : 'Unknown error');
338
+ }
339
+ }
340
+
341
+ export async function launchAgentInteractively(
342
+ agent: AgentType,
343
+ prompt: string,
344
+ spawnImpl: AgentSpawn = spawnAgent
345
+ ): Promise<number> {
346
+ const config = USE_AGENT_CONFIGS[agent];
347
+ if (!config) {
348
+ throw new UseCommandError(formatUnsupportedAgentError(agent));
349
+ }
350
+
351
+ return new Promise((resolve, reject) => {
352
+ const child = spawnImpl(config.command, [...config.args, prompt], {
353
+ stdio: 'inherit',
354
+ });
355
+ let settled = false;
356
+
357
+ child.on('error', (error: NodeJS.ErrnoException) => {
358
+ if (settled) return;
359
+ settled = true;
360
+ if (error.code === 'ENOENT') {
361
+ reject(
362
+ new UseCommandError(
363
+ `Could not launch ${agents[agent].displayName}: command not found: ${config.command}`
364
+ )
365
+ );
366
+ return;
367
+ }
368
+ reject(error);
369
+ });
370
+
371
+ child.on('close', (code: number | null) => {
372
+ if (settled) return;
373
+ settled = true;
374
+ resolve(code ?? 1);
375
+ });
376
+ });
377
+ }
378
+
379
+ function spawnAgent(command: string, args: string[]): AgentProcess {
380
+ return spawn(command, args, { stdio: 'inherit' }) as AgentProcess;
381
+ }
382
+
383
+ function getUseHelp(): string {
384
+ return `Usage: skills use <source>[@<skill>] [options]
385
+
386
+ Generate a prompt for using one skill without installing it.
387
+
388
+ Options:
389
+ -s, --skill <skill> Select the skill to use
390
+ -a, --agent <agent> Start one supported agent interactively (${SUPPORTED_USE_AGENTS.join(', ')})
391
+ --full-depth Search nested directories like skills add --full-depth
392
+ --dangerously-accept-openclaw-risks
393
+ Allow unverified OpenClaw community skills
394
+ -h, --help Show this help message
395
+
396
+ Examples:
397
+ skills use vercel-labs/agent-skills@web-design-guidelines | claude
398
+ skills use vercel-labs/agent-skills --skill web-design-guidelines --agent claude-code
399
+ skills use vercel-labs/agent-skills@web-design-guidelines --agent codex`;
400
+ }
401
+
402
+ function resolveSelector(sourceSelector?: string, optionSelector?: string): string | undefined {
403
+ if (sourceSelector && optionSelector) {
404
+ if (sourceSelector.toLowerCase() !== optionSelector.toLowerCase()) {
405
+ throw new UseCommandError(
406
+ `Conflicting skill selectors: source selects "${sourceSelector}" but --skill selects "${optionSelector}". Provide one selector.`
407
+ );
408
+ }
409
+ return optionSelector;
410
+ }
411
+
412
+ return optionSelector ?? sourceSelector;
413
+ }
414
+
415
+ function selectSkill(skills: Skill[], selector: string | undefined, source: string): Skill {
416
+ if (skills.length === 0) {
417
+ throw new UseCommandError(
418
+ 'No valid skills found. Skills require a SKILL.md with name and description.'
419
+ );
420
+ }
421
+
422
+ if (!selector) {
423
+ if (skills.length === 1) return skills[0]!;
424
+ throw new UseCommandError(formatMultipleSkillsError(source, skills.map(getSkillDisplayName)));
425
+ }
426
+
427
+ const selected = filterSkills(skills, [selector]);
428
+ if (selected.length === 0) {
429
+ throw new UseCommandError(formatNoMatchError(selector, skills.map(getSkillDisplayName)));
430
+ }
431
+ if (selected.length > 1) {
432
+ throw new UseCommandError(`Skill selector "${selector}" matched multiple skills.`);
433
+ }
434
+
435
+ return selected[0]!;
436
+ }
437
+
438
+ function selectWellKnownSkill(
439
+ skills: WellKnownSkill[],
440
+ selector: string | undefined,
441
+ source: string
442
+ ): UseSkill {
443
+ if (skills.length === 0) {
444
+ throw new UseCommandError(
445
+ 'No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file.'
446
+ );
447
+ }
448
+
449
+ let selected: WellKnownSkill[];
450
+ if (!selector) {
451
+ if (skills.length !== 1) {
452
+ throw new UseCommandError(
453
+ formatMultipleSkillsError(
454
+ source,
455
+ skills.map((s) => s.installName)
456
+ )
457
+ );
458
+ }
459
+ selected = skills;
460
+ } else {
461
+ selected = skills.filter(
462
+ (skill) =>
463
+ skill.installName.toLowerCase() === selector.toLowerCase() ||
464
+ skill.name.toLowerCase() === selector.toLowerCase()
465
+ );
466
+ if (selected.length === 0) {
467
+ throw new UseCommandError(
468
+ formatNoMatchError(
469
+ selector,
470
+ skills.map((s) => s.installName)
471
+ )
472
+ );
473
+ }
474
+ if (selected.length > 1) {
475
+ throw new UseCommandError(`Skill selector "${selector}" matched multiple skills.`);
476
+ }
477
+ }
478
+
479
+ const skill = selected[0]!;
480
+ return {
481
+ kind: 'well-known',
482
+ name: skill.name,
483
+ directoryName: skill.installName,
484
+ rawContent: skill.content,
485
+ files: skill.files,
486
+ };
487
+ }
488
+
489
+ function formatMultipleSkillsError(source: string, names: string[]): string {
490
+ return [
491
+ 'This source contains multiple skills. Specify exactly one skill:',
492
+ ...names.map((name) => ` - ${name}`),
493
+ '',
494
+ `Examples:\n skills use ${source}@${names[0] ?? '<skill>'}\n skills use ${source} --skill ${names[0] ?? '<skill>'}`,
495
+ ].join('\n');
496
+ }
497
+
498
+ function formatNoMatchError(selector: string, names: string[]): string {
499
+ return [
500
+ `No matching skill found for: ${selector}`,
501
+ 'Available skills:',
502
+ ...names.map((name) => ` - ${name}`),
503
+ ].join('\n');
504
+ }
505
+
506
+ function validateUseAgentOption(agentValues: string[] | undefined): string[] {
507
+ if (!agentValues || agentValues.length === 0) return [];
508
+
509
+ const errors: string[] = [];
510
+ // Validate against the agents `use` can actually launch, not the full agent
511
+ // registry, so the parse-time message matches runtime support.
512
+ const invalidAgents = agentValues.filter(
513
+ (agent) => agent !== '*' && !SUPPORTED_USE_AGENTS.includes(agent as AgentType)
514
+ );
515
+
516
+ if (agentValues.includes('*')) {
517
+ errors.push("skills use --agent does not support '*'; specify exactly one agent.");
518
+ }
519
+ if (agentValues.length > 1) {
520
+ errors.push('skills use --agent accepts exactly one agent.');
521
+ }
522
+ if (invalidAgents.length > 0) {
523
+ errors.push(
524
+ `Unsupported agents for skills use --agent: ${invalidAgents.join(', ')}\n` +
525
+ `Supported agents: ${SUPPORTED_USE_AGENTS.join(', ')}`
526
+ );
527
+ }
528
+
529
+ return errors;
530
+ }
531
+
532
+ function formatUnsupportedAgentError(agent: AgentType): string {
533
+ return [
534
+ `Running ${agents[agent].displayName} is not supported yet.`,
535
+ `Supported agents for skills use --agent: ${SUPPORTED_USE_AGENTS.join(', ')}`,
536
+ ].join('\n');
537
+ }
538
+
539
+ async function writeSnapshotFiles(
540
+ targetDir: string,
541
+ files: Array<{ path: string; contents: string }>
542
+ ): Promise<void> {
543
+ for (const file of files) {
544
+ await writeSafeFile(targetDir, file.path, file.contents);
545
+ }
546
+ }
547
+
548
+ async function writeMapFiles(
549
+ targetDir: string,
550
+ files: Map<string, WellKnownFileContent>
551
+ ): Promise<void> {
552
+ for (const [path, contents] of files) {
553
+ await writeSafeFile(targetDir, path, contents);
554
+ }
555
+ }
556
+
557
+ async function writeSafeFile(
558
+ targetDir: string,
559
+ filePath: string,
560
+ contents: WellKnownFileContent
561
+ ): Promise<void> {
562
+ const fullPath = join(targetDir, filePath);
563
+ if (!isPathSafe(targetDir, fullPath)) return;
564
+
565
+ await mkdir(dirname(fullPath), { recursive: true });
566
+ if (typeof contents === 'string') {
567
+ await writeFile(fullPath, contents, 'utf-8');
568
+ } else {
569
+ await writeFile(fullPath, contents);
570
+ }
571
+ }
572
+
573
+ async function copySkillDirectory(src: string, dest: string, sourceRoot?: string): Promise<void> {
574
+ // The realpath of the top-level skill source. Carried through recursion so
575
+ // every symlink can be checked against it: a symlink resolving outside this
576
+ // root is refused, otherwise a malicious skill could smuggle host files
577
+ // (e.g. ~/.ssh/id_rsa) into the materialized skill the agent is told to read.
578
+ const root = sourceRoot ?? (await realpath(src));
579
+
580
+ await mkdir(dest, { recursive: true });
581
+ const entries = await readdir(src, { withFileTypes: true });
582
+
583
+ await Promise.all(
584
+ entries
585
+ .filter((entry) => !isExcluded(entry.name, entry.isDirectory()))
586
+ .map(async (entry) => {
587
+ const srcPath = join(src, entry.name);
588
+ const destPath = join(dest, entry.name);
589
+ if (!isPathSafe(dest, destPath)) return;
590
+
591
+ if (entry.isSymbolicLink()) {
592
+ let realTarget: string;
593
+ try {
594
+ realTarget = await realpath(srcPath);
595
+ } catch (err) {
596
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
597
+ console.error(`Skipping broken symlink: ${srcPath}`);
598
+ return;
599
+ }
600
+ throw err;
601
+ }
602
+
603
+ if (!isPathSafe(root, realTarget)) {
604
+ console.error(`Skipping symlink that points outside the skill: ${srcPath}`);
605
+ return;
606
+ }
607
+
608
+ // Re-walk a symlinked directory so its own entries are checked too,
609
+ // instead of blindly dereferencing the whole subtree.
610
+ if ((await stat(realTarget)).isDirectory()) {
611
+ await copySkillDirectory(realTarget, destPath, root);
612
+ } else {
613
+ await cp(realTarget, destPath, { dereference: true });
614
+ }
615
+ return;
616
+ }
617
+
618
+ if (entry.isDirectory()) {
619
+ await copySkillDirectory(srcPath, destPath, root);
620
+ return;
621
+ }
622
+
623
+ await cp(srcPath, destPath, { dereference: true, recursive: true });
624
+ })
625
+ );
626
+ }
627
+
628
+ async function containsSupportingFiles(rootDir: string, currentDir: string): Promise<boolean> {
629
+ const entries = await readdir(currentDir, { withFileTypes: true });
630
+
631
+ for (const entry of entries) {
632
+ const entryPath = join(currentDir, entry.name);
633
+ const relPath = relative(rootDir, entryPath).split(sep).join('/');
634
+ if (entry.isDirectory()) {
635
+ if (await containsSupportingFiles(rootDir, entryPath)) return true;
636
+ } else if (relPath.toLowerCase() !== 'skill.md') {
637
+ return true;
638
+ }
639
+ }
640
+
641
+ return false;
642
+ }
643
+
644
+ function isBlobSkill(skill: Skill): skill is BlobSkill {
645
+ return Array.isArray((skill as BlobSkill).files);
646
+ }
647
+
648
+ function getSkillMdFromSnapshot(files: Array<{ path: string; contents: string }>): string {
649
+ const skillMd = files.find((file) => file.path.toLowerCase() === 'skill.md');
650
+ return skillMd?.contents ?? '';
651
+ }
652
+
653
+ function isExcluded(name: string, isDirectory: boolean): boolean {
654
+ return EXCLUDE_FILES.has(name) || (isDirectory && EXCLUDE_DIRS.has(name));
655
+ }
656
+
657
+ function isPathSafe(basePath: string, targetPath: string): boolean {
658
+ const normalizedBase = normalize(resolve(basePath));
659
+ const normalizedTarget = normalize(resolve(targetPath));
660
+
661
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
662
+ }
663
+
664
+ async function removeTempDir(tempDir: string | null): Promise<void> {
665
+ if (tempDir) {
666
+ await cleanupTempDir(tempDir).catch(() => {});
667
+ }
668
+ }
669
+
670
+ function fail(message: string): never {
671
+ console.error(message);
672
+ process.exit(1);
673
+ }
674
+
675
+ class UseCommandError extends Error {}