@rbbtsn0w/adg 0.1.0-alpha.1 → 0.1.0-beta.2

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 (110) hide show
  1. package/dist/bin/adg.js +703 -0
  2. package/dist/src/adapters/anthropic.js +54 -0
  3. package/dist/src/adapters/index.js +10 -0
  4. package/dist/src/adapters/openai.js +30 -0
  5. package/dist/src/adapters/reverse.js +53 -0
  6. package/dist/src/agents/claude.js +118 -0
  7. package/dist/src/agents/codex.js +61 -0
  8. package/{src/agents/index.ts → dist/src/agents/index.js} +6 -8
  9. package/dist/src/agents/registry.js +24 -0
  10. package/dist/src/agents/types.js +1 -0
  11. package/dist/src/commands/adapt.js +26 -0
  12. package/dist/src/commands/import.js +51 -0
  13. package/dist/src/commands/init.js +104 -0
  14. package/dist/src/commands/install.js +257 -0
  15. package/dist/src/commands/link.js +34 -0
  16. package/dist/src/commands/list.js +19 -0
  17. package/dist/src/commands/marketplace.js +124 -0
  18. package/dist/src/commands/migrate.js +60 -0
  19. package/dist/src/commands/multiselect-skills.js +103 -0
  20. package/dist/src/commands/remove.js +102 -0
  21. package/dist/src/commands/select-agents.js +40 -0
  22. package/dist/src/commands/select-components.js +61 -0
  23. package/dist/src/commands/select-plugins.js +25 -0
  24. package/dist/src/commands/select-scope.js +20 -0
  25. package/dist/src/commands/update.js +50 -0
  26. package/dist/src/commands/validate.js +50 -0
  27. package/dist/src/components.js +90 -0
  28. package/dist/src/deps.js +46 -0
  29. package/dist/src/fsutil.js +32 -0
  30. package/dist/src/hash.js +51 -0
  31. package/dist/src/lock.js +51 -0
  32. package/dist/src/manifest.js +110 -0
  33. package/dist/src/marketplace.js +39 -0
  34. package/{src/package.ts → dist/src/package.js} +37 -42
  35. package/{src/paths.ts → dist/src/paths.js} +54 -60
  36. package/dist/src/semver.js +55 -0
  37. package/dist/src/skills.js +79 -0
  38. package/dist/src/sources.js +122 -0
  39. package/dist/src/types.js +19 -0
  40. package/dist/vendor/skills/package.json +143 -0
  41. package/dist/vendor/skills/src/add.js +1663 -0
  42. package/dist/vendor/skills/src/agents.js +729 -0
  43. package/dist/vendor/skills/src/blob.js +436 -0
  44. package/dist/vendor/skills/src/cli.js +340 -0
  45. package/dist/vendor/skills/src/constants.js +3 -0
  46. package/dist/vendor/skills/src/detect-agent.js +56 -0
  47. package/dist/vendor/skills/src/find.js +294 -0
  48. package/dist/vendor/skills/src/frontmatter.js +13 -0
  49. package/dist/vendor/skills/src/git-tree.js +32 -0
  50. package/dist/vendor/skills/src/git.js +235 -0
  51. package/dist/vendor/skills/src/install.js +75 -0
  52. package/dist/vendor/skills/src/installer.js +924 -0
  53. package/dist/vendor/skills/src/list.js +201 -0
  54. package/dist/vendor/skills/src/local-lock.js +109 -0
  55. package/dist/vendor/skills/src/plugin-manifest.js +152 -0
  56. package/dist/vendor/skills/src/prompts/search-multiselect.js +312 -0
  57. package/dist/vendor/skills/src/providers/index.js +4 -0
  58. package/dist/vendor/skills/src/providers/registry.js +42 -0
  59. package/dist/vendor/skills/src/providers/types.js +1 -0
  60. package/dist/vendor/skills/src/providers/wellknown.js +625 -0
  61. package/dist/vendor/skills/src/remove.js +263 -0
  62. package/dist/vendor/skills/src/sanitize.js +57 -0
  63. package/dist/vendor/skills/src/self-cli.js +15 -0
  64. package/dist/vendor/skills/src/skill-lock.js +237 -0
  65. package/dist/vendor/skills/src/skills.js +264 -0
  66. package/dist/vendor/skills/src/source-parser.js +367 -0
  67. package/dist/vendor/skills/src/sync.js +404 -0
  68. package/dist/vendor/skills/src/telemetry.js +101 -0
  69. package/dist/vendor/skills/src/test-utils.js +59 -0
  70. package/dist/vendor/skills/src/types.js +1 -0
  71. package/dist/vendor/skills/src/update-source.js +76 -0
  72. package/dist/vendor/skills/src/update.js +590 -0
  73. package/dist/vendor/skills/src/use.js +505 -0
  74. package/package.json +15 -7
  75. package/bin/adg.ts +0 -758
  76. package/src/adapters/anthropic.ts +0 -54
  77. package/src/adapters/index.ts +0 -24
  78. package/src/adapters/openai.ts +0 -37
  79. package/src/adapters/reverse.ts +0 -60
  80. package/src/agents/claude.ts +0 -124
  81. package/src/agents/codex.ts +0 -67
  82. package/src/agents/registry.ts +0 -30
  83. package/src/agents/types.ts +0 -47
  84. package/src/commands/adapt.ts +0 -36
  85. package/src/commands/import.ts +0 -69
  86. package/src/commands/init.ts +0 -146
  87. package/src/commands/install.ts +0 -411
  88. package/src/commands/link.ts +0 -61
  89. package/src/commands/list.ts +0 -28
  90. package/src/commands/marketplace.ts +0 -198
  91. package/src/commands/migrate.ts +0 -84
  92. package/src/commands/multiselect-skills.ts +0 -137
  93. package/src/commands/remove.ts +0 -136
  94. package/src/commands/select-agents.ts +0 -45
  95. package/src/commands/select-components.ts +0 -66
  96. package/src/commands/select-plugins.ts +0 -28
  97. package/src/commands/select-scope.ts +0 -21
  98. package/src/commands/update.ts +0 -85
  99. package/src/commands/validate.ts +0 -57
  100. package/src/components.ts +0 -90
  101. package/src/deps.ts +0 -64
  102. package/src/fsutil.ts +0 -38
  103. package/src/hash.ts +0 -61
  104. package/src/lock.ts +0 -57
  105. package/src/manifest.ts +0 -113
  106. package/src/marketplace.ts +0 -41
  107. package/src/semver.ts +0 -67
  108. package/src/skills.ts +0 -88
  109. package/src/sources.ts +0 -159
  110. package/src/types.ts +0 -140
@@ -0,0 +1,924 @@
1
+ import { mkdir, cp, access, readdir, symlink, lstat, rm, readlink, writeFile, stat, realpath, } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join, basename, normalize, resolve, sep, relative, dirname } from 'path';
4
+ import { homedir, platform } from 'os';
5
+ import { agents, detectInstalledAgents, isUniversalAgent } from "./agents.js";
6
+ import { AGENTS_DIR, SKILLS_SUBDIR } from "./constants.js";
7
+ import { parseSkillMd } from "./skills.js";
8
+ /**
9
+ * Sanitizes a filename/directory name to prevent path traversal attacks
10
+ * and ensures it follows kebab-case convention
11
+ * @param name - The name to sanitize
12
+ * @returns Sanitized name safe for use in file paths
13
+ */
14
+ export function sanitizeName(name) {
15
+ const sanitized = name
16
+ .toLowerCase()
17
+ // Replace any sequence of characters that are NOT lowercase letters (a-z),
18
+ // digits (0-9), dots (.), or underscores (_) with a single hyphen.
19
+ // This converts spaces, special chars, and path traversal attempts (../) into hyphens.
20
+ .replace(/[^a-z0-9._]+/g, '-')
21
+ // Remove leading/trailing dots and hyphens to prevent hidden files (.) and
22
+ // ensure clean directory names. The pattern matches:
23
+ // - ^[.\-]+ : one or more dots or hyphens at the start
24
+ // - [.\-]+$ : one or more dots or hyphens at the end
25
+ .replace(/^[.\-]+|[.\-]+$/g, '');
26
+ // Limit to 255 chars (common filesystem limit), fallback to 'unnamed-skill' if empty
27
+ return sanitized.substring(0, 255) || 'unnamed-skill';
28
+ }
29
+ /**
30
+ * Validates that a path is within an expected base directory
31
+ * @param basePath - The expected base directory
32
+ * @param targetPath - The path to validate
33
+ * @returns true if targetPath is within basePath
34
+ */
35
+ function isPathSafe(basePath, targetPath) {
36
+ const normalizedBase = normalize(resolve(basePath));
37
+ const normalizedTarget = normalize(resolve(targetPath));
38
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
39
+ }
40
+ // Dirent.isDirectory() is false for symlinks; follow and verify the target is a directory.
41
+ async function isDirEntryOrSymlinkToDir(entry, entryPath) {
42
+ if (entry.isDirectory())
43
+ return true;
44
+ if (!entry.isSymbolicLink())
45
+ return false;
46
+ try {
47
+ return (await stat(entryPath)).isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ export function getCanonicalSkillsDir(global, cwd) {
54
+ const baseDir = global ? homedir() : cwd || process.cwd();
55
+ return join(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
56
+ }
57
+ /**
58
+ * Gets the base directory for an agent's skills, respecting universal agents.
59
+ * Universal agents always use the canonical directory, which prevents
60
+ * redundant symlinks and double-listing of skills.
61
+ */
62
+ export function getAgentBaseDir(agentType, global, cwd) {
63
+ if (isUniversalAgent(agentType)) {
64
+ return getCanonicalSkillsDir(global, cwd);
65
+ }
66
+ const agent = agents[agentType];
67
+ const baseDir = global ? homedir() : cwd || process.cwd();
68
+ if (global) {
69
+ if (agent.globalSkillsDir === undefined) {
70
+ // This should be caught by callers checking support
71
+ return join(baseDir, agent.skillsDir);
72
+ }
73
+ return agent.globalSkillsDir;
74
+ }
75
+ return join(baseDir, agent.skillsDir);
76
+ }
77
+ function resolveSymlinkTarget(linkPath, linkTarget) {
78
+ return resolve(dirname(linkPath), linkTarget);
79
+ }
80
+ /**
81
+ * Cleans and recreates a directory for skill installation.
82
+ *
83
+ * This ensures:
84
+ * 1. Renamed/deleted files from previous installs are removed
85
+ * 2. Symlinks (including self-referential ones causing ELOOP) are handled
86
+ * when canonical and agent paths resolve to the same location
87
+ */
88
+ async function cleanAndCreateDirectory(path) {
89
+ try {
90
+ await rm(path, { recursive: true, force: true });
91
+ }
92
+ catch {
93
+ // Ignore cleanup errors - mkdir will fail if there's a real problem
94
+ }
95
+ await mkdir(path, { recursive: true });
96
+ }
97
+ /**
98
+ * Resolve a path's parent directory through symlinks, keeping the final component.
99
+ * This handles the case where a parent directory (e.g., ~/.claude/skills) is a symlink
100
+ * to another location (e.g., ~/.agents/skills). In that case, computing relative paths
101
+ * from the symlink path produces broken symlinks.
102
+ *
103
+ * Returns the real path of the parent + the original basename.
104
+ * If realpath fails (parent doesn't exist), returns the original resolved path.
105
+ */
106
+ async function resolveParentSymlinks(path) {
107
+ const resolved = resolve(path);
108
+ const dir = dirname(resolved);
109
+ const base = basename(resolved);
110
+ try {
111
+ const realDir = await realpath(dir);
112
+ return join(realDir, base);
113
+ }
114
+ catch {
115
+ return resolved;
116
+ }
117
+ }
118
+ /**
119
+ * Creates a symlink, handling cross-platform differences
120
+ * Returns true if symlink was created, false if fallback to copy is needed
121
+ */
122
+ async function createSymlink(target, linkPath) {
123
+ try {
124
+ const resolvedTarget = resolve(target);
125
+ const resolvedLinkPath = resolve(linkPath);
126
+ // Use realpath to handle cases where parent directories are symlinked.
127
+ // This prevents deleting the canonical directory if the agent directory
128
+ // is a symlink to the canonical location.
129
+ const [realTarget, realLinkPath] = await Promise.all([
130
+ realpath(resolvedTarget).catch(() => resolvedTarget),
131
+ realpath(resolvedLinkPath).catch(() => resolvedLinkPath),
132
+ ]);
133
+ if (realTarget === realLinkPath) {
134
+ return true;
135
+ }
136
+ // Also check with symlinks resolved in parent directories.
137
+ // This handles cases where e.g. ~/.claude/skills is a symlink to ~/.agents/skills,
138
+ // so ~/.claude/skills/<skill> and ~/.agents/skills/<skill> are physically the same.
139
+ const realTargetWithParents = await resolveParentSymlinks(target);
140
+ const realLinkPathWithParents = await resolveParentSymlinks(linkPath);
141
+ if (realTargetWithParents === realLinkPathWithParents) {
142
+ return true;
143
+ }
144
+ try {
145
+ const stats = await lstat(linkPath);
146
+ if (stats.isSymbolicLink()) {
147
+ const existingTarget = await readlink(linkPath);
148
+ if (resolveSymlinkTarget(linkPath, existingTarget) === resolvedTarget) {
149
+ return true;
150
+ }
151
+ await rm(linkPath);
152
+ }
153
+ else {
154
+ await rm(linkPath, { recursive: true });
155
+ }
156
+ }
157
+ catch (err) {
158
+ // ELOOP = circular symlink, ENOENT = doesn't exist
159
+ // For ELOOP, try to remove the broken symlink
160
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'ELOOP') {
161
+ try {
162
+ await rm(linkPath, { force: true });
163
+ }
164
+ catch {
165
+ // If we can't remove it, symlink creation will fail and trigger copy fallback
166
+ }
167
+ }
168
+ // For ENOENT or other errors, continue to symlink creation
169
+ }
170
+ const linkDir = dirname(linkPath);
171
+ await mkdir(linkDir, { recursive: true });
172
+ // Use the real (symlink-resolved) parent directory for computing the relative path.
173
+ // This ensures the symlink target is correct even when the link's parent dir is a symlink.
174
+ const realLinkDir = await resolveParentSymlinks(linkDir);
175
+ const relativePath = relative(realLinkDir, target);
176
+ const symlinkType = platform() === 'win32' ? 'junction' : undefined;
177
+ await symlink(relativePath, linkPath, symlinkType);
178
+ return true;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ export async function installSkillForAgent(skill, agentType, options = {}) {
185
+ const agent = agents[agentType];
186
+ const isGlobal = options.global ?? false;
187
+ const cwd = options.cwd || process.cwd();
188
+ // Check if agent supports global installation
189
+ if (isGlobal && agent.globalSkillsDir === undefined) {
190
+ return {
191
+ success: false,
192
+ path: '',
193
+ mode: options.mode ?? 'symlink',
194
+ error: `${agent.displayName} does not support global skill installation`,
195
+ };
196
+ }
197
+ // Sanitize skill name to prevent directory traversal
198
+ const rawSkillName = skill.name || basename(skill.path);
199
+ const skillName = sanitizeName(rawSkillName);
200
+ // Canonical location: .agents/skills/<skill-name>
201
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
202
+ const canonicalDir = join(canonicalBase, skillName);
203
+ // Agent-specific location (for symlink)
204
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
205
+ const agentDir = join(agentBase, skillName);
206
+ const installMode = options.mode ?? 'symlink';
207
+ // Validate paths
208
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
209
+ return {
210
+ success: false,
211
+ path: agentDir,
212
+ mode: installMode,
213
+ error: 'Invalid skill name: potential path traversal detected',
214
+ };
215
+ }
216
+ if (!isPathSafe(agentBase, agentDir)) {
217
+ return {
218
+ success: false,
219
+ path: agentDir,
220
+ mode: installMode,
221
+ error: 'Invalid skill name: potential path traversal detected',
222
+ };
223
+ }
224
+ try {
225
+ // For copy mode, skip canonical directory and copy directly to agent location
226
+ if (installMode === 'copy') {
227
+ await cleanAndCreateDirectory(agentDir);
228
+ await copyDirectory(skill.path, agentDir);
229
+ return {
230
+ success: true,
231
+ path: agentDir,
232
+ mode: 'copy',
233
+ };
234
+ }
235
+ // Symlink mode: copy to canonical location and symlink to agent location
236
+ await cleanAndCreateDirectory(canonicalDir);
237
+ await copyDirectory(skill.path, canonicalDir);
238
+ // For universal agents with global install, the skill is already in the canonical
239
+ // ~/.agents/skills directory. Skip creating a symlink to the agent-specific global dir
240
+ // (e.g. ~/.copilot/skills) to avoid duplicates.
241
+ if (isGlobal && isUniversalAgent(agentType)) {
242
+ return {
243
+ success: true,
244
+ path: canonicalDir,
245
+ canonicalPath: canonicalDir,
246
+ mode: 'symlink',
247
+ };
248
+ }
249
+ // For project-level installs, skip creating symlinks for non-universal agents
250
+ // whose config directory doesn't already exist in the project. This prevents
251
+ // creating directories like .windsurf/, .kiro/, etc. when those agents aren't
252
+ // actually used in this project. The skill is already available in .agents/skills/.
253
+ if (!isGlobal && !isUniversalAgent(agentType)) {
254
+ const agentRootDir = join(cwd, agents[agentType].skillsDir.split('/')[0]);
255
+ if (!existsSync(agentRootDir)) {
256
+ return {
257
+ success: true,
258
+ path: canonicalDir,
259
+ canonicalPath: canonicalDir,
260
+ mode: 'symlink',
261
+ skipped: true,
262
+ };
263
+ }
264
+ }
265
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
266
+ if (!symlinkCreated) {
267
+ // Symlink failed, fall back to copy
268
+ await cleanAndCreateDirectory(agentDir);
269
+ await copyDirectory(skill.path, agentDir);
270
+ return {
271
+ success: true,
272
+ path: agentDir,
273
+ canonicalPath: canonicalDir,
274
+ mode: 'symlink',
275
+ symlinkFailed: true,
276
+ };
277
+ }
278
+ return {
279
+ success: true,
280
+ path: agentDir,
281
+ canonicalPath: canonicalDir,
282
+ mode: 'symlink',
283
+ };
284
+ }
285
+ catch (error) {
286
+ return {
287
+ success: false,
288
+ path: agentDir,
289
+ mode: installMode,
290
+ error: error instanceof Error ? error.message : 'Unknown error',
291
+ };
292
+ }
293
+ }
294
+ const EXCLUDE_FILES = new Set(['metadata.json']);
295
+ const EXCLUDE_DIRS = new Set(['.git', '__pycache__', '__pypackages__']);
296
+ const isExcluded = (name, isDirectory = false) => {
297
+ if (EXCLUDE_FILES.has(name))
298
+ return true;
299
+ if (isDirectory && EXCLUDE_DIRS.has(name))
300
+ return true;
301
+ return false;
302
+ };
303
+ async function copyDirectory(src, dest) {
304
+ await mkdir(dest, { recursive: true });
305
+ const entries = await readdir(src, { withFileTypes: true });
306
+ // Copy files and directories in parallel
307
+ await Promise.all(entries
308
+ .filter((entry) => !isExcluded(entry.name, entry.isDirectory()))
309
+ .map(async (entry) => {
310
+ const srcPath = join(src, entry.name);
311
+ const destPath = join(dest, entry.name);
312
+ if (entry.isDirectory()) {
313
+ await copyDirectory(srcPath, destPath);
314
+ }
315
+ else {
316
+ try {
317
+ await cp(srcPath, destPath, {
318
+ // If the file is a symlink to elsewhere in a remote skill, it may not
319
+ // resolve correctly once it has been copied to the local location.
320
+ // `dereference: true` tells Node to copy the file instead of copying
321
+ // the symlink. `recursive: true` handles symlinks pointing to directories.
322
+ dereference: true,
323
+ recursive: true,
324
+ });
325
+ }
326
+ catch (err) {
327
+ // Skip broken symlinks (e.g., pointing to absolute paths on another machine)
328
+ // instead of aborting the entire install.
329
+ if (err instanceof Error &&
330
+ 'code' in err &&
331
+ err.code === 'ENOENT' &&
332
+ entry.isSymbolicLink()) {
333
+ console.warn(`Skipping broken symlink: ${srcPath}`);
334
+ }
335
+ else {
336
+ throw err;
337
+ }
338
+ }
339
+ }
340
+ }));
341
+ }
342
+ export async function isSkillInstalled(skillName, agentType, options = {}) {
343
+ const agent = agents[agentType];
344
+ const sanitized = sanitizeName(skillName);
345
+ // Agent doesn't support global installation
346
+ if (options.global && agent.globalSkillsDir === undefined) {
347
+ return false;
348
+ }
349
+ const targetBase = options.global
350
+ ? agent.globalSkillsDir
351
+ : join(options.cwd || process.cwd(), agent.skillsDir);
352
+ const skillDir = join(targetBase, sanitized);
353
+ if (!isPathSafe(targetBase, skillDir)) {
354
+ return false;
355
+ }
356
+ try {
357
+ await access(skillDir);
358
+ return true;
359
+ }
360
+ catch {
361
+ return false;
362
+ }
363
+ }
364
+ export function getInstallPath(skillName, agentType, options = {}) {
365
+ const agent = agents[agentType];
366
+ const cwd = options.cwd || process.cwd();
367
+ const sanitized = sanitizeName(skillName);
368
+ const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
369
+ const installPath = join(targetBase, sanitized);
370
+ if (!isPathSafe(targetBase, installPath)) {
371
+ throw new Error('Invalid skill name: potential path traversal detected');
372
+ }
373
+ return installPath;
374
+ }
375
+ /**
376
+ * Gets the canonical .agents/skills/<skill> path
377
+ */
378
+ export function getCanonicalPath(skillName, options = {}) {
379
+ const sanitized = sanitizeName(skillName);
380
+ const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
381
+ const canonicalPath = join(canonicalBase, sanitized);
382
+ if (!isPathSafe(canonicalBase, canonicalPath)) {
383
+ throw new Error('Invalid skill name: potential path traversal detected');
384
+ }
385
+ return canonicalPath;
386
+ }
387
+ /**
388
+ * Install a remote skill from any host provider.
389
+ * The skill directory name is derived from the installName field.
390
+ * Supports symlink mode (writes to canonical location and symlinks to agent dirs)
391
+ * or copy mode (writes directly to each agent dir).
392
+ */
393
+ export async function installRemoteSkillForAgent(skill, agentType, options = {}) {
394
+ const agent = agents[agentType];
395
+ const isGlobal = options.global ?? false;
396
+ const cwd = options.cwd || process.cwd();
397
+ const installMode = options.mode ?? 'symlink';
398
+ // Check if agent supports global installation
399
+ if (isGlobal && agent.globalSkillsDir === undefined) {
400
+ return {
401
+ success: false,
402
+ path: '',
403
+ mode: installMode,
404
+ error: `${agent.displayName} does not support global skill installation`,
405
+ };
406
+ }
407
+ // Use installName as the skill directory name
408
+ const skillName = sanitizeName(skill.installName);
409
+ // Canonical location: .agents/skills/<skill-name>
410
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
411
+ const canonicalDir = join(canonicalBase, skillName);
412
+ // Agent-specific location (for symlink)
413
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
414
+ const agentDir = join(agentBase, skillName);
415
+ // Validate paths
416
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
417
+ return {
418
+ success: false,
419
+ path: agentDir,
420
+ mode: installMode,
421
+ error: 'Invalid skill name: potential path traversal detected',
422
+ };
423
+ }
424
+ if (!isPathSafe(agentBase, agentDir)) {
425
+ return {
426
+ success: false,
427
+ path: agentDir,
428
+ mode: installMode,
429
+ error: 'Invalid skill name: potential path traversal detected',
430
+ };
431
+ }
432
+ try {
433
+ // For copy mode, write directly to agent location
434
+ if (installMode === 'copy') {
435
+ await cleanAndCreateDirectory(agentDir);
436
+ const skillMdPath = join(agentDir, 'SKILL.md');
437
+ await writeFile(skillMdPath, skill.content, 'utf-8');
438
+ return {
439
+ success: true,
440
+ path: agentDir,
441
+ mode: 'copy',
442
+ };
443
+ }
444
+ // Symlink mode: write to canonical location and symlink to agent location
445
+ await cleanAndCreateDirectory(canonicalDir);
446
+ const skillMdPath = join(canonicalDir, 'SKILL.md');
447
+ await writeFile(skillMdPath, skill.content, 'utf-8');
448
+ // For universal agents with global install, skip creating agent-specific symlink
449
+ if (isGlobal && isUniversalAgent(agentType)) {
450
+ return {
451
+ success: true,
452
+ path: canonicalDir,
453
+ canonicalPath: canonicalDir,
454
+ mode: 'symlink',
455
+ };
456
+ }
457
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
458
+ if (!symlinkCreated) {
459
+ // Symlink failed, fall back to copy
460
+ await cleanAndCreateDirectory(agentDir);
461
+ const agentSkillMdPath = join(agentDir, 'SKILL.md');
462
+ await writeFile(agentSkillMdPath, skill.content, 'utf-8');
463
+ return {
464
+ success: true,
465
+ path: agentDir,
466
+ canonicalPath: canonicalDir,
467
+ mode: 'symlink',
468
+ symlinkFailed: true,
469
+ };
470
+ }
471
+ return {
472
+ success: true,
473
+ path: agentDir,
474
+ canonicalPath: canonicalDir,
475
+ mode: 'symlink',
476
+ };
477
+ }
478
+ catch (error) {
479
+ return {
480
+ success: false,
481
+ path: agentDir,
482
+ mode: installMode,
483
+ error: error instanceof Error ? error.message : 'Unknown error',
484
+ };
485
+ }
486
+ }
487
+ /**
488
+ * Install a well-known skill with multiple files.
489
+ * The skill directory name is derived from the installName field.
490
+ * All files from the skill's files map are written to the installation directory.
491
+ * Supports symlink mode (writes to canonical location and symlinks to agent dirs)
492
+ * or copy mode (writes directly to each agent dir).
493
+ */
494
+ export async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
495
+ const agent = agents[agentType];
496
+ const isGlobal = options.global ?? false;
497
+ const cwd = options.cwd || process.cwd();
498
+ const installMode = options.mode ?? 'symlink';
499
+ // Check if agent supports global installation
500
+ if (isGlobal && agent.globalSkillsDir === undefined) {
501
+ return {
502
+ success: false,
503
+ path: '',
504
+ mode: installMode,
505
+ error: `${agent.displayName} does not support global skill installation`,
506
+ };
507
+ }
508
+ // Use installName as the skill directory name
509
+ const skillName = sanitizeName(skill.installName);
510
+ // Canonical location: .agents/skills/<skill-name>
511
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
512
+ const canonicalDir = join(canonicalBase, skillName);
513
+ // Agent-specific location (for symlink)
514
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
515
+ const agentDir = join(agentBase, skillName);
516
+ // Validate paths
517
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
518
+ return {
519
+ success: false,
520
+ path: agentDir,
521
+ mode: installMode,
522
+ error: 'Invalid skill name: potential path traversal detected',
523
+ };
524
+ }
525
+ if (!isPathSafe(agentBase, agentDir)) {
526
+ return {
527
+ success: false,
528
+ path: agentDir,
529
+ mode: installMode,
530
+ error: 'Invalid skill name: potential path traversal detected',
531
+ };
532
+ }
533
+ /**
534
+ * Write all skill files to a directory (assumes directory already exists)
535
+ */
536
+ async function writeSkillFiles(targetDir) {
537
+ for (const [filePath, content] of skill.files) {
538
+ // Validate file path doesn't escape the target directory
539
+ const fullPath = join(targetDir, filePath);
540
+ if (!isPathSafe(targetDir, fullPath)) {
541
+ continue; // Skip files that would escape the directory
542
+ }
543
+ // Create parent directories if needed
544
+ const parentDir = dirname(fullPath);
545
+ if (parentDir !== targetDir) {
546
+ await mkdir(parentDir, { recursive: true });
547
+ }
548
+ await writeFile(fullPath, content);
549
+ }
550
+ }
551
+ try {
552
+ // For copy mode, write directly to agent location
553
+ if (installMode === 'copy') {
554
+ await cleanAndCreateDirectory(agentDir);
555
+ await writeSkillFiles(agentDir);
556
+ return {
557
+ success: true,
558
+ path: agentDir,
559
+ mode: 'copy',
560
+ };
561
+ }
562
+ // Symlink mode: write to canonical location and symlink to agent location
563
+ await cleanAndCreateDirectory(canonicalDir);
564
+ await writeSkillFiles(canonicalDir);
565
+ // For universal agents with global install, skip creating agent-specific symlink
566
+ if (isGlobal && isUniversalAgent(agentType)) {
567
+ return {
568
+ success: true,
569
+ path: canonicalDir,
570
+ canonicalPath: canonicalDir,
571
+ mode: 'symlink',
572
+ };
573
+ }
574
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
575
+ if (!symlinkCreated) {
576
+ // Symlink failed, fall back to copy
577
+ await cleanAndCreateDirectory(agentDir);
578
+ await writeSkillFiles(agentDir);
579
+ return {
580
+ success: true,
581
+ path: agentDir,
582
+ canonicalPath: canonicalDir,
583
+ mode: 'symlink',
584
+ symlinkFailed: true,
585
+ };
586
+ }
587
+ return {
588
+ success: true,
589
+ path: agentDir,
590
+ canonicalPath: canonicalDir,
591
+ mode: 'symlink',
592
+ };
593
+ }
594
+ catch (error) {
595
+ return {
596
+ success: false,
597
+ path: agentDir,
598
+ mode: installMode,
599
+ error: error instanceof Error ? error.message : 'Unknown error',
600
+ };
601
+ }
602
+ }
603
+ /**
604
+ * Install a blob-downloaded skill (fetched from skills.sh download API).
605
+ * Similar to installWellKnownSkillForAgent but takes the snapshot file format
606
+ * (array of { path, contents }) instead of a Map.
607
+ */
608
+ export async function installBlobSkillForAgent(skill, agentType, options = {}) {
609
+ const agent = agents[agentType];
610
+ const isGlobal = options.global ?? false;
611
+ const cwd = options.cwd || process.cwd();
612
+ const installMode = options.mode ?? 'symlink';
613
+ if (isGlobal && agent.globalSkillsDir === undefined) {
614
+ return {
615
+ success: false,
616
+ path: '',
617
+ mode: installMode,
618
+ error: `${agent.displayName} does not support global skill installation`,
619
+ };
620
+ }
621
+ const skillName = sanitizeName(skill.installName);
622
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
623
+ const canonicalDir = join(canonicalBase, skillName);
624
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
625
+ const agentDir = join(agentBase, skillName);
626
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
627
+ return {
628
+ success: false,
629
+ path: agentDir,
630
+ mode: installMode,
631
+ error: 'Invalid skill name: potential path traversal detected',
632
+ };
633
+ }
634
+ if (!isPathSafe(agentBase, agentDir)) {
635
+ return {
636
+ success: false,
637
+ path: agentDir,
638
+ mode: installMode,
639
+ error: 'Invalid skill name: potential path traversal detected',
640
+ };
641
+ }
642
+ async function writeSkillFiles(targetDir) {
643
+ for (const file of skill.files) {
644
+ const fullPath = join(targetDir, file.path);
645
+ if (!isPathSafe(targetDir, fullPath))
646
+ continue;
647
+ const parentDir = dirname(fullPath);
648
+ if (parentDir !== targetDir) {
649
+ await mkdir(parentDir, { recursive: true });
650
+ }
651
+ await writeFile(fullPath, file.contents, 'utf-8');
652
+ }
653
+ }
654
+ try {
655
+ if (installMode === 'copy') {
656
+ await cleanAndCreateDirectory(agentDir);
657
+ await writeSkillFiles(agentDir);
658
+ return { success: true, path: agentDir, mode: 'copy' };
659
+ }
660
+ // Symlink mode
661
+ await cleanAndCreateDirectory(canonicalDir);
662
+ await writeSkillFiles(canonicalDir);
663
+ if (isGlobal && isUniversalAgent(agentType)) {
664
+ return {
665
+ success: true,
666
+ path: canonicalDir,
667
+ canonicalPath: canonicalDir,
668
+ mode: 'symlink',
669
+ };
670
+ }
671
+ // For project-level installs, skip creating symlinks for non-universal agents
672
+ // whose config directory doesn't already exist in the project.
673
+ if (!isGlobal && !isUniversalAgent(agentType)) {
674
+ const agentRootDir = join(cwd, agents[agentType].skillsDir.split('/')[0]);
675
+ if (!existsSync(agentRootDir)) {
676
+ return {
677
+ success: true,
678
+ path: canonicalDir,
679
+ canonicalPath: canonicalDir,
680
+ mode: 'symlink',
681
+ skipped: true,
682
+ };
683
+ }
684
+ }
685
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
686
+ if (!symlinkCreated) {
687
+ await cleanAndCreateDirectory(agentDir);
688
+ await writeSkillFiles(agentDir);
689
+ return {
690
+ success: true,
691
+ path: agentDir,
692
+ canonicalPath: canonicalDir,
693
+ mode: 'symlink',
694
+ symlinkFailed: true,
695
+ };
696
+ }
697
+ return {
698
+ success: true,
699
+ path: agentDir,
700
+ canonicalPath: canonicalDir,
701
+ mode: 'symlink',
702
+ };
703
+ }
704
+ catch (error) {
705
+ return {
706
+ success: false,
707
+ path: agentDir,
708
+ mode: installMode,
709
+ error: error instanceof Error ? error.message : 'Unknown error',
710
+ };
711
+ }
712
+ }
713
+ /**
714
+ * Lists all installed skills from canonical locations
715
+ * @param options - Options for listing skills
716
+ * @returns Array of installed skills with metadata
717
+ */
718
+ export async function listInstalledSkills(options = {}) {
719
+ const cwd = options.cwd || process.cwd();
720
+ // Use a Map to deduplicate skills by scope:name
721
+ const skillsMap = new Map();
722
+ const scopes = [];
723
+ // Detect which agents are actually installed
724
+ const detectedAgents = await detectInstalledAgents();
725
+ const agentFilter = options.agentFilter;
726
+ const agentsToCheck = agentFilter
727
+ ? detectedAgents.filter((a) => agentFilter.includes(a))
728
+ : detectedAgents;
729
+ // Determine which scopes to scan
730
+ const scopeTypes = [];
731
+ if (options.global === undefined) {
732
+ scopeTypes.push({ global: false }, { global: true });
733
+ }
734
+ else {
735
+ scopeTypes.push({ global: options.global });
736
+ }
737
+ // Build list of directories to scan: canonical + each installed agent's directory
738
+ //
739
+ // Scanning workflow:
740
+ //
741
+ // detectInstalledAgents()
742
+ // │
743
+ // ▼
744
+ // for each scope (project / global)
745
+ // │
746
+ // ├──▶ scan canonical dir ──▶ .agents/skills, ~/.agents/skills
747
+ // │
748
+ // ├──▶ scan each installed agent's dir ──▶ .cursor/skills, .claude/skills, ...
749
+ // │
750
+ // ▼
751
+ // deduplicate by skill name
752
+ //
753
+ // Trade-off: More readdir() calls, but most non-existent dirs fail fast.
754
+ // Skills in agent-specific dirs skip the expensive "check all agents" loop.
755
+ //
756
+ for (const { global: isGlobal } of scopeTypes) {
757
+ // Add canonical directory
758
+ scopes.push({ global: isGlobal, path: getCanonicalSkillsDir(isGlobal, cwd) });
759
+ // Add each installed agent's skills directory
760
+ for (const agentType of agentsToCheck) {
761
+ const agent = agents[agentType];
762
+ if (isGlobal && agent.globalSkillsDir === undefined) {
763
+ continue;
764
+ }
765
+ const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
766
+ // Avoid duplicate paths
767
+ if (!scopes.some((s) => s.path === agentDir && s.global === isGlobal)) {
768
+ scopes.push({ global: isGlobal, path: agentDir, agentType });
769
+ }
770
+ }
771
+ // Also scan skill directories for agents NOT in agentsToCheck, in case
772
+ // skills were installed with `--agent <name>` but the agent is no longer
773
+ // detected (e.g. ~/.openclaw was removed). Only add dirs that actually
774
+ // exist on disk to avoid unnecessary readdir errors.
775
+ const allAgentTypes = Object.keys(agents);
776
+ for (const agentType of allAgentTypes) {
777
+ if (agentsToCheck.includes(agentType))
778
+ continue;
779
+ const agent = agents[agentType];
780
+ if (isGlobal && agent.globalSkillsDir === undefined)
781
+ continue;
782
+ const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
783
+ if (scopes.some((s) => s.path === agentDir && s.global === isGlobal))
784
+ continue;
785
+ if (existsSync(agentDir)) {
786
+ scopes.push({ global: isGlobal, path: agentDir, agentType });
787
+ }
788
+ }
789
+ }
790
+ for (const scope of scopes) {
791
+ try {
792
+ const entries = await readdir(scope.path, { withFileTypes: true });
793
+ for (const entry of entries) {
794
+ const skillDir = join(scope.path, entry.name);
795
+ if (!(await isDirEntryOrSymlinkToDir(entry, skillDir)))
796
+ continue;
797
+ const skillMdPath = join(skillDir, 'SKILL.md');
798
+ // Check if SKILL.md exists
799
+ try {
800
+ await stat(skillMdPath);
801
+ }
802
+ catch {
803
+ // SKILL.md doesn't exist, skip this directory
804
+ continue;
805
+ }
806
+ // Parse the skill
807
+ const skill = await parseSkillMd(skillMdPath);
808
+ if (!skill) {
809
+ continue;
810
+ }
811
+ const scopeKey = scope.global ? 'global' : 'project';
812
+ const skillKey = `${scopeKey}:${skill.name}`;
813
+ // If scanning an agent-specific directory, attribute directly to that agent
814
+ if (scope.agentType) {
815
+ if (skillsMap.has(skillKey)) {
816
+ const existing = skillsMap.get(skillKey);
817
+ if (!existing.agents.includes(scope.agentType)) {
818
+ existing.agents.push(scope.agentType);
819
+ }
820
+ }
821
+ else {
822
+ skillsMap.set(skillKey, {
823
+ name: skill.name,
824
+ description: skill.description,
825
+ path: skillDir,
826
+ canonicalPath: skillDir,
827
+ scope: scopeKey,
828
+ agents: [scope.agentType],
829
+ });
830
+ }
831
+ continue;
832
+ }
833
+ // For canonical directory, check which agents have this skill
834
+ const sanitizedSkillName = sanitizeName(skill.name);
835
+ const installedAgents = [];
836
+ for (const agentType of agentsToCheck) {
837
+ const agent = agents[agentType];
838
+ if (scope.global && agent.globalSkillsDir === undefined) {
839
+ continue;
840
+ }
841
+ const agentBase = getAgentBaseDir(agentType, scope.global, cwd);
842
+ let found = false;
843
+ // Try exact directory name matches
844
+ const possibleNames = Array.from(new Set([
845
+ entry.name,
846
+ sanitizedSkillName,
847
+ skill.name
848
+ .toLowerCase()
849
+ .replace(/\s+/g, '-')
850
+ .replace(/[\/\\:\0]/g, ''),
851
+ ]));
852
+ for (const possibleName of possibleNames) {
853
+ const agentSkillDir = join(agentBase, possibleName);
854
+ if (!isPathSafe(agentBase, agentSkillDir))
855
+ continue;
856
+ try {
857
+ await access(agentSkillDir);
858
+ found = true;
859
+ break;
860
+ }
861
+ catch {
862
+ // Try next name
863
+ }
864
+ }
865
+ // Fallback: scan all directories and check SKILL.md files
866
+ // Handles cases where directory names don't match (e.g., "git-review" vs "Git Review Before Commit")
867
+ if (!found) {
868
+ try {
869
+ const agentEntries = await readdir(agentBase, { withFileTypes: true });
870
+ for (const agentEntry of agentEntries) {
871
+ const candidateDir = join(agentBase, agentEntry.name);
872
+ if (!(await isDirEntryOrSymlinkToDir(agentEntry, candidateDir)))
873
+ continue;
874
+ if (!isPathSafe(agentBase, candidateDir))
875
+ continue;
876
+ try {
877
+ const candidateSkillMd = join(candidateDir, 'SKILL.md');
878
+ await stat(candidateSkillMd);
879
+ const candidateSkill = await parseSkillMd(candidateSkillMd);
880
+ if (candidateSkill && candidateSkill.name === skill.name) {
881
+ found = true;
882
+ break;
883
+ }
884
+ }
885
+ catch {
886
+ // Not a valid skill directory
887
+ }
888
+ }
889
+ }
890
+ catch {
891
+ // Agent base directory doesn't exist
892
+ }
893
+ }
894
+ if (found) {
895
+ installedAgents.push(agentType);
896
+ }
897
+ }
898
+ if (skillsMap.has(skillKey)) {
899
+ // Merge agents
900
+ const existing = skillsMap.get(skillKey);
901
+ for (const agent of installedAgents) {
902
+ if (!existing.agents.includes(agent)) {
903
+ existing.agents.push(agent);
904
+ }
905
+ }
906
+ }
907
+ else {
908
+ skillsMap.set(skillKey, {
909
+ name: skill.name,
910
+ description: skill.description,
911
+ path: skillDir,
912
+ canonicalPath: skillDir,
913
+ scope: scopeKey,
914
+ agents: installedAgents,
915
+ });
916
+ }
917
+ }
918
+ }
919
+ catch {
920
+ // Directory doesn't exist, skip
921
+ }
922
+ }
923
+ return Array.from(skillsMap.values());
924
+ }