@voidagency/skills 1.0.0

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/cli.mjs ADDED
@@ -0,0 +1,4730 @@
1
+ #!/usr/bin/env node
2
+ import { r as __toESM } from "./_chunks/rolldown-runtime.mjs";
3
+ import { l as pD, u as require_picocolors } from "./_chunks/libs/@clack/core.mjs";
4
+ import { a as Y, c as ve, i as Se, l as xe, n as M, o as be, r as Me, s as fe, t as Ie, u as ye } from "./_chunks/libs/@clack/prompts.mjs";
5
+ import "./_chunks/libs/@kwsites/file-exists.mjs";
6
+ import "./_chunks/libs/@kwsites/promise-deferred.mjs";
7
+ import { t as esm_default } from "./_chunks/libs/simple-git.mjs";
8
+ import { t as require_gray_matter } from "./_chunks/libs/gray-matter.mjs";
9
+ import "./_chunks/libs/extend-shallow.mjs";
10
+ import "./_chunks/libs/esprima.mjs";
11
+ import { t as xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
12
+ import { execSync, spawnSync } from "child_process";
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
+ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
15
+ import { homedir, platform, tmpdir } from "os";
16
+ import { createHash } from "crypto";
17
+ import { fileURLToPath } from "url";
18
+ import * as readline from "readline";
19
+ import { Writable } from "stream";
20
+ import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
21
+ //#region src/source-parser.ts
22
+ var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
23
+ /**
24
+ * Extract owner/repo (or group/subgroup/repo for GitLab) from a parsed source
25
+ * for lockfile tracking and telemetry.
26
+ * Returns null for local paths or unparseable sources.
27
+ * Supports any Git host with an owner/repo URL structure, including GitLab subgroups.
28
+ */
29
+ function getOwnerRepo(parsed) {
30
+ if (parsed.type === "local") return null;
31
+ const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
32
+ if (sshMatch) {
33
+ let path = sshMatch[1];
34
+ path = path.replace(/\.git$/, "");
35
+ if (path.includes("/")) return path;
36
+ return null;
37
+ }
38
+ if (!parsed.url.startsWith("http://") && !parsed.url.startsWith("https://")) return null;
39
+ try {
40
+ let path = new URL(parsed.url).pathname.slice(1);
41
+ path = path.replace(/\.git$/, "");
42
+ if (path.includes("/")) return path;
43
+ } catch {}
44
+ return null;
45
+ }
46
+ /**
47
+ * Extract owner and repo from an owner/repo string.
48
+ * Returns null if the format is invalid.
49
+ */
50
+ function parseOwnerRepo(ownerRepo) {
51
+ const match = ownerRepo.match(/^([^/]+)\/([^/]+)$/);
52
+ if (match) return {
53
+ owner: match[1],
54
+ repo: match[2]
55
+ };
56
+ return null;
57
+ }
58
+ /**
59
+ * Check if a GitHub repository is private.
60
+ * Returns true if private, false if public, null if unable to determine.
61
+ * Only works for GitHub repositories (GitLab not supported).
62
+ */
63
+ async function isRepoPrivate(owner, repo) {
64
+ try {
65
+ const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
66
+ if (!res.ok) return null;
67
+ return (await res.json()).private === true;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * Sanitizes a subpath to prevent path traversal attacks.
74
+ * Rejects subpaths containing ".." segments that could escape the repository root.
75
+ * Returns the sanitized subpath, or throws if the subpath is unsafe.
76
+ */
77
+ function sanitizeSubpath(subpath) {
78
+ const segments = subpath.replace(/\\/g, "/").split("/");
79
+ for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
80
+ return subpath;
81
+ }
82
+ /**
83
+ * Check if a string represents a local file system path
84
+ */
85
+ function isLocalPath(input) {
86
+ return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
87
+ }
88
+ /**
89
+ * Parse a source string into a structured format
90
+ * Supports: local paths, GitHub URLs, GitLab URLs, GitHub shorthand, well-known URLs, and direct git URLs
91
+ */
92
+ const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
93
+ function parseSource(input) {
94
+ const alias = SOURCE_ALIASES[input];
95
+ if (alias) input = alias;
96
+ const githubPrefixMatch = input.match(/^github:(.+)$/);
97
+ if (githubPrefixMatch) return parseSource(githubPrefixMatch[1]);
98
+ const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
99
+ if (gitlabPrefixMatch) return parseSource(`https://gitlab.com/${gitlabPrefixMatch[1]}`);
100
+ const bitbucketPrefixMatch = input.match(/^bitbucket:(.+)$/);
101
+ if (bitbucketPrefixMatch) return parseSource(`https://bitbucket.org/${bitbucketPrefixMatch[1]}`);
102
+ const atIdx = input.lastIndexOf("@");
103
+ if (atIdx > 0) {
104
+ const pathPart = input.slice(0, atIdx).trim();
105
+ const skillPart = input.slice(atIdx + 1).trim();
106
+ if (skillPart && isLocalPath(pathPart)) return {
107
+ type: "local",
108
+ url: resolve(pathPart),
109
+ localPath: resolve(pathPart),
110
+ skillFilter: skillPart
111
+ };
112
+ }
113
+ if (isLocalPath(input)) {
114
+ const resolvedPath = resolve(input);
115
+ return {
116
+ type: "local",
117
+ url: resolvedPath,
118
+ localPath: resolvedPath
119
+ };
120
+ }
121
+ const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
122
+ if (githubTreeWithPathMatch) {
123
+ const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
124
+ return {
125
+ type: "github",
126
+ url: `https://github.com/${owner}/${repo}.git`,
127
+ ref,
128
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
129
+ };
130
+ }
131
+ const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
132
+ if (githubTreeMatch) {
133
+ const [, owner, repo, ref] = githubTreeMatch;
134
+ return {
135
+ type: "github",
136
+ url: `https://github.com/${owner}/${repo}.git`,
137
+ ref
138
+ };
139
+ }
140
+ const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
141
+ if (githubRepoMatch) {
142
+ const [, owner, repo] = githubRepoMatch;
143
+ return {
144
+ type: "github",
145
+ url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`
146
+ };
147
+ }
148
+ const gitlabTreeWithPathMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)\/(.+)/);
149
+ if (gitlabTreeWithPathMatch) {
150
+ const [, protocol, hostname, repoPath, ref, subpath] = gitlabTreeWithPathMatch;
151
+ if (hostname !== "github.com" && repoPath) return {
152
+ type: "gitlab",
153
+ url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
154
+ ref,
155
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
156
+ };
157
+ }
158
+ const gitlabTreeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)$/);
159
+ if (gitlabTreeMatch) {
160
+ const [, protocol, hostname, repoPath, ref] = gitlabTreeMatch;
161
+ if (hostname !== "github.com" && repoPath) return {
162
+ type: "gitlab",
163
+ url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
164
+ ref
165
+ };
166
+ }
167
+ const gitlabRepoMatch = input.match(/gitlab\.com\/(.+?)(?:\.git)?\/?$/);
168
+ if (gitlabRepoMatch) {
169
+ const repoPath = gitlabRepoMatch[1];
170
+ if (repoPath.includes("/")) return {
171
+ type: "gitlab",
172
+ url: `https://gitlab.com/${repoPath}.git`
173
+ };
174
+ }
175
+ const bitbucketSrcWithPathMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/src\/([^/]+)\/(.+)/);
176
+ if (bitbucketSrcWithPathMatch) {
177
+ const [, owner, repo, ref, subpath] = bitbucketSrcWithPathMatch;
178
+ return {
179
+ type: "bitbucket",
180
+ url: `https://bitbucket.org/${owner}/${repo}.git`,
181
+ ref,
182
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
183
+ };
184
+ }
185
+ const bitbucketSrcMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/src\/([^/]+)\/?$/);
186
+ if (bitbucketSrcMatch) {
187
+ const [, owner, repo, ref] = bitbucketSrcMatch;
188
+ return {
189
+ type: "bitbucket",
190
+ url: `https://bitbucket.org/${owner}/${repo}.git`,
191
+ ref
192
+ };
193
+ }
194
+ const bitbucketRepoMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)(?:\.git)?\/?$/);
195
+ if (bitbucketRepoMatch) {
196
+ const [, owner, repo] = bitbucketRepoMatch;
197
+ return {
198
+ type: "bitbucket",
199
+ url: `https://bitbucket.org/${owner}/${repo.replace(/\.git$/, "")}.git`
200
+ };
201
+ }
202
+ const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
203
+ if (atSkillMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
204
+ const [, owner, repo, skillFilter] = atSkillMatch;
205
+ return {
206
+ type: "github",
207
+ url: `https://github.com/${owner}/${repo}.git`,
208
+ skillFilter
209
+ };
210
+ }
211
+ const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
212
+ if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
213
+ const [, owner, repo, subpath] = shorthandMatch;
214
+ return {
215
+ type: "github",
216
+ url: `https://github.com/${owner}/${repo}.git`,
217
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
218
+ };
219
+ }
220
+ if (isWellKnownUrl(input)) return {
221
+ type: "well-known",
222
+ url: input
223
+ };
224
+ return {
225
+ type: "git",
226
+ url: input
227
+ };
228
+ }
229
+ /**
230
+ * Check if a URL could be a well-known skills endpoint.
231
+ * Must be HTTP(S) and not a known git host (GitHub, GitLab).
232
+ * Also excludes URLs that look like git repos (.git suffix).
233
+ */
234
+ function isWellKnownUrl(input) {
235
+ if (!input.startsWith("http://") && !input.startsWith("https://")) return false;
236
+ try {
237
+ const parsed = new URL(input);
238
+ if ([
239
+ "github.com",
240
+ "gitlab.com",
241
+ "bitbucket.org",
242
+ "raw.githubusercontent.com"
243
+ ].includes(parsed.hostname)) return false;
244
+ if (input.endsWith(".git")) return false;
245
+ return true;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ //#endregion
251
+ //#region src/prompts/search-multiselect.ts
252
+ const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
253
+ callback();
254
+ } });
255
+ const S_STEP_ACTIVE = import_picocolors.default.green("◆");
256
+ const S_STEP_CANCEL = import_picocolors.default.red("■");
257
+ const S_STEP_SUBMIT = import_picocolors.default.green("◇");
258
+ const S_RADIO_ACTIVE = import_picocolors.default.green("●");
259
+ const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
260
+ import_picocolors.default.green("✓");
261
+ const S_BULLET = import_picocolors.default.green("•");
262
+ const S_BAR = import_picocolors.default.dim("│");
263
+ const S_BAR_H = import_picocolors.default.dim("─");
264
+ const cancelSymbol = Symbol("cancel");
265
+ /**
266
+ * Interactive search multiselect prompt.
267
+ * Allows users to filter a long list by typing and select multiple items.
268
+ * Optionally supports a "locked" section that displays always-selected items.
269
+ */
270
+ async function searchMultiselect(options) {
271
+ const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options;
272
+ return new Promise((resolve) => {
273
+ const rl = readline.createInterface({
274
+ input: process.stdin,
275
+ output: silentOutput,
276
+ terminal: false
277
+ });
278
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
279
+ readline.emitKeypressEvents(process.stdin, rl);
280
+ let query = "";
281
+ let cursor = 0;
282
+ const selected = new Set(initialSelected);
283
+ let lastRenderHeight = 0;
284
+ const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
285
+ const filter = (item, q) => {
286
+ if (!q) return true;
287
+ const lowerQ = q.toLowerCase();
288
+ return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ);
289
+ };
290
+ const getFiltered = () => {
291
+ return items.filter((item) => filter(item, query));
292
+ };
293
+ const clearRender = () => {
294
+ if (lastRenderHeight > 0) {
295
+ process.stdout.write(`\x1b[${lastRenderHeight}A`);
296
+ for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
297
+ process.stdout.write(`\x1b[${lastRenderHeight}A`);
298
+ }
299
+ };
300
+ const render = (state = "active") => {
301
+ clearRender();
302
+ const lines = [];
303
+ const filtered = getFiltered();
304
+ const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
305
+ lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
306
+ if (state === "active") {
307
+ if (lockedSection && lockedSection.items.length > 0) {
308
+ lines.push(`${S_BAR}`);
309
+ const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
310
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
311
+ for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
312
+ lines.push(`${S_BAR}`);
313
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
314
+ }
315
+ const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
316
+ lines.push(searchLine);
317
+ lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
318
+ lines.push(`${S_BAR}`);
319
+ const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
320
+ const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
321
+ const visibleItems = filtered.slice(visibleStart, visibleEnd);
322
+ if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
323
+ else {
324
+ for (let i = 0; i < visibleItems.length; i++) {
325
+ const item = visibleItems[i];
326
+ const actualIndex = visibleStart + i;
327
+ const isSelected = selected.has(item.value);
328
+ const isCursor = actualIndex === cursor;
329
+ const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
330
+ const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
331
+ const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
332
+ const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
333
+ lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
334
+ }
335
+ const hiddenBefore = visibleStart;
336
+ const hiddenAfter = filtered.length - visibleEnd;
337
+ if (hiddenBefore > 0 || hiddenAfter > 0) {
338
+ const parts = [];
339
+ if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
340
+ if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
341
+ lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
342
+ }
343
+ }
344
+ lines.push(`${S_BAR}`);
345
+ const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
346
+ if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
347
+ else {
348
+ const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
349
+ lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
350
+ }
351
+ lines.push(`${import_picocolors.default.dim("└")}`);
352
+ } else if (state === "submit") {
353
+ const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
354
+ lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
355
+ } else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
356
+ process.stdout.write(lines.join("\n") + "\n");
357
+ lastRenderHeight = lines.length;
358
+ };
359
+ const cleanup = () => {
360
+ process.stdin.removeListener("keypress", keypressHandler);
361
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
362
+ rl.close();
363
+ };
364
+ const submit = () => {
365
+ if (required && selected.size === 0 && lockedValues.length === 0) return;
366
+ render("submit");
367
+ cleanup();
368
+ resolve([...lockedValues, ...Array.from(selected)]);
369
+ };
370
+ const cancel = () => {
371
+ render("cancel");
372
+ cleanup();
373
+ resolve(cancelSymbol);
374
+ };
375
+ const keypressHandler = (_str, key) => {
376
+ if (!key) return;
377
+ const filtered = getFiltered();
378
+ if (key.name === "return") {
379
+ submit();
380
+ return;
381
+ }
382
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
383
+ cancel();
384
+ return;
385
+ }
386
+ if (key.name === "up") {
387
+ cursor = Math.max(0, cursor - 1);
388
+ render();
389
+ return;
390
+ }
391
+ if (key.name === "down") {
392
+ cursor = Math.min(filtered.length - 1, cursor + 1);
393
+ render();
394
+ return;
395
+ }
396
+ if (key.name === "space") {
397
+ const item = filtered[cursor];
398
+ if (item) if (selected.has(item.value)) selected.delete(item.value);
399
+ else selected.add(item.value);
400
+ render();
401
+ return;
402
+ }
403
+ if (key.name === "backspace") {
404
+ query = query.slice(0, -1);
405
+ cursor = 0;
406
+ render();
407
+ return;
408
+ }
409
+ if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
410
+ query += key.sequence;
411
+ cursor = 0;
412
+ render();
413
+ return;
414
+ }
415
+ };
416
+ process.stdin.on("keypress", keypressHandler);
417
+ render();
418
+ });
419
+ }
420
+ //#endregion
421
+ //#region src/git.ts
422
+ const CLONE_TIMEOUT_MS = 6e4;
423
+ var GitCloneError = class extends Error {
424
+ url;
425
+ isTimeout;
426
+ isAuthError;
427
+ constructor(message, url, isTimeout = false, isAuthError = false) {
428
+ super(message);
429
+ this.name = "GitCloneError";
430
+ this.url = url;
431
+ this.isTimeout = isTimeout;
432
+ this.isAuthError = isAuthError;
433
+ }
434
+ };
435
+ async function cloneRepo(url, ref) {
436
+ const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
437
+ const git = esm_default({
438
+ timeout: { block: CLONE_TIMEOUT_MS },
439
+ env: {
440
+ ...process.env,
441
+ GIT_TERMINAL_PROMPT: "0"
442
+ }
443
+ });
444
+ const cloneOptions = ref ? [
445
+ "--depth",
446
+ "1",
447
+ "--branch",
448
+ ref
449
+ ] : ["--depth", "1"];
450
+ try {
451
+ await git.clone(url, tempDir, cloneOptions);
452
+ return tempDir;
453
+ } catch (error) {
454
+ await rm(tempDir, {
455
+ recursive: true,
456
+ force: true
457
+ }).catch(() => {});
458
+ const errorMessage = error instanceof Error ? error.message : String(error);
459
+ const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
460
+ const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
461
+ if (isTimeout) throw new GitCloneError("Clone timed out after 60s. This often happens with private repos that require authentication.\n Ensure you have access and your SSH keys or credentials are configured:\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)", url, true, false);
462
+ if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
463
+ throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
464
+ }
465
+ }
466
+ async function cleanupTempDir(dir) {
467
+ const normalizedDir = normalize(resolve(dir));
468
+ const normalizedTmpDir = normalize(resolve(tmpdir()));
469
+ if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
470
+ await rm(dir, {
471
+ recursive: true,
472
+ force: true
473
+ });
474
+ }
475
+ //#endregion
476
+ //#region src/plugin-manifest.ts
477
+ var import_gray_matter = /* @__PURE__ */ __toESM(require_gray_matter(), 1);
478
+ /**
479
+ * Check if a path is contained within a base directory.
480
+ * Prevents path traversal attacks via `..` segments or absolute paths.
481
+ */
482
+ function isContainedIn(targetPath, basePath) {
483
+ const normalizedBase = normalize(resolve(basePath));
484
+ const normalizedTarget = normalize(resolve(targetPath));
485
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
486
+ }
487
+ /**
488
+ * Validate that a relative path follows Claude Code conventions.
489
+ * Paths must start with './' per the plugin manifest spec.
490
+ */
491
+ function isValidRelativePath(path) {
492
+ return path.startsWith("./");
493
+ }
494
+ /**
495
+ * Extract skill search directories from plugin manifests.
496
+ * Handles both marketplace.json (multi-plugin) and plugin.json (single plugin).
497
+ * Only resolves local paths - remote sources are skipped.
498
+ *
499
+ * Returns directories that CONTAIN skills (to be searched for child SKILL.md files).
500
+ * For explicit skill paths in manifests, adds the parent directory so the
501
+ * existing discovery loop finds them.
502
+ */
503
+ async function getPluginSkillPaths(basePath) {
504
+ const searchDirs = [];
505
+ const addPluginSkillPaths = (pluginBase, skills) => {
506
+ if (!isContainedIn(pluginBase, basePath)) return;
507
+ if (skills && skills.length > 0) for (const skillPath of skills) {
508
+ if (!isValidRelativePath(skillPath)) continue;
509
+ const skillDir = dirname(join(pluginBase, skillPath));
510
+ if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
511
+ }
512
+ searchDirs.push(join(pluginBase, "skills"));
513
+ };
514
+ try {
515
+ const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
516
+ const manifest = JSON.parse(content);
517
+ const pluginRoot = manifest.metadata?.pluginRoot;
518
+ if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
519
+ if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
520
+ if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
521
+ addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
522
+ }
523
+ } catch {}
524
+ try {
525
+ const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
526
+ addPluginSkillPaths(basePath, JSON.parse(content).skills);
527
+ } catch {}
528
+ return searchDirs;
529
+ }
530
+ /**
531
+ * Get a map of skill directory paths to plugin names from plugin manifests.
532
+ * This allows grouping skills by their parent plugin.
533
+ *
534
+ * Returns Map<AbsolutePath, PluginName>
535
+ */
536
+ async function getPluginGroupings(basePath) {
537
+ const groupings = /* @__PURE__ */ new Map();
538
+ try {
539
+ const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
540
+ const manifest = JSON.parse(content);
541
+ const pluginRoot = manifest.metadata?.pluginRoot;
542
+ if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
543
+ if (!plugin.name) continue;
544
+ if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
545
+ if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
546
+ const pluginBase = join(basePath, pluginRoot ?? "", plugin.source ?? "");
547
+ if (!isContainedIn(pluginBase, basePath)) continue;
548
+ if (plugin.skills && plugin.skills.length > 0) for (const skillPath of plugin.skills) {
549
+ if (!isValidRelativePath(skillPath)) continue;
550
+ const skillDir = join(pluginBase, skillPath);
551
+ if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), plugin.name);
552
+ }
553
+ }
554
+ } catch {}
555
+ try {
556
+ const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
557
+ const manifest = JSON.parse(content);
558
+ if (manifest.name && manifest.skills && manifest.skills.length > 0) for (const skillPath of manifest.skills) {
559
+ if (!isValidRelativePath(skillPath)) continue;
560
+ const skillDir = join(basePath, skillPath);
561
+ if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), manifest.name);
562
+ }
563
+ } catch {}
564
+ return groupings;
565
+ }
566
+ //#endregion
567
+ //#region src/skills.ts
568
+ const SKIP_DIRS = [
569
+ "node_modules",
570
+ ".git",
571
+ "dist",
572
+ "build",
573
+ "__pycache__"
574
+ ];
575
+ /**
576
+ * Check if internal skills should be installed.
577
+ * Internal skills are hidden by default unless INSTALL_INTERNAL_SKILLS=1 is set.
578
+ */
579
+ function shouldInstallInternalSkills() {
580
+ const envValue = process.env.INSTALL_INTERNAL_SKILLS;
581
+ return envValue === "1" || envValue === "true";
582
+ }
583
+ async function hasSkillMd(dir) {
584
+ try {
585
+ return (await stat(join(dir, "SKILL.md"))).isFile();
586
+ } catch {
587
+ return false;
588
+ }
589
+ }
590
+ async function parseSkillMd(skillMdPath, options) {
591
+ try {
592
+ const content = await readFile(skillMdPath, "utf-8");
593
+ const { data } = (0, import_gray_matter.default)(content);
594
+ if (!data.name || !data.description) return null;
595
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
596
+ if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
597
+ return {
598
+ name: data.name,
599
+ description: data.description,
600
+ path: dirname(skillMdPath),
601
+ rawContent: content,
602
+ metadata: data.metadata
603
+ };
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
609
+ if (depth > maxDepth) return [];
610
+ try {
611
+ const [hasSkill, entries] = await Promise.all([hasSkillMd(dir), readdir(dir, { withFileTypes: true }).catch(() => [])]);
612
+ const currentDir = hasSkill ? [dir] : [];
613
+ const subDirResults = await Promise.all(entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.includes(entry.name)).map((entry) => findSkillDirs(join(dir, entry.name), depth + 1, maxDepth)));
614
+ return [...currentDir, ...subDirResults.flat()];
615
+ } catch {
616
+ return [];
617
+ }
618
+ }
619
+ /**
620
+ * Validates that a resolved subpath stays within the base directory.
621
+ * Prevents path traversal attacks where subpath contains ".." segments
622
+ * that would escape the cloned repository directory.
623
+ */
624
+ function isSubpathSafe(basePath, subpath) {
625
+ const normalizedBase = normalize(resolve(basePath));
626
+ const normalizedTarget = normalize(resolve(join(basePath, subpath)));
627
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
628
+ }
629
+ async function discoverSkills(basePath, subpath, options) {
630
+ const skills = [];
631
+ const seenNames = /* @__PURE__ */ new Set();
632
+ if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
633
+ const searchPath = subpath ? join(basePath, subpath) : basePath;
634
+ const pluginGroupings = await getPluginGroupings(searchPath);
635
+ const enhanceSkill = (skill) => {
636
+ const resolvedPath = resolve(skill.path);
637
+ if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath);
638
+ return skill;
639
+ };
640
+ if (await hasSkillMd(searchPath)) {
641
+ let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
642
+ if (skill) {
643
+ skill = enhanceSkill(skill);
644
+ skills.push(skill);
645
+ seenNames.add(skill.name);
646
+ if (!options?.fullDepth) return skills;
647
+ }
648
+ }
649
+ const prioritySearchDirs = [
650
+ searchPath,
651
+ join(searchPath, "skills"),
652
+ join(searchPath, "skills/.curated"),
653
+ join(searchPath, "skills/.experimental"),
654
+ join(searchPath, "skills/.system"),
655
+ join(searchPath, ".agent/skills"),
656
+ join(searchPath, ".agents/skills"),
657
+ join(searchPath, ".claude/skills"),
658
+ join(searchPath, ".cline/skills"),
659
+ join(searchPath, ".codebuddy/skills"),
660
+ join(searchPath, ".codex/skills"),
661
+ join(searchPath, ".commandcode/skills"),
662
+ join(searchPath, ".continue/skills"),
663
+ join(searchPath, ".github/skills"),
664
+ join(searchPath, ".goose/skills"),
665
+ join(searchPath, ".iflow/skills"),
666
+ join(searchPath, ".junie/skills"),
667
+ join(searchPath, ".kilocode/skills"),
668
+ join(searchPath, ".kiro/skills"),
669
+ join(searchPath, ".mux/skills"),
670
+ join(searchPath, ".neovate/skills"),
671
+ join(searchPath, ".opencode/skills"),
672
+ join(searchPath, ".openhands/skills"),
673
+ join(searchPath, ".pi/skills"),
674
+ join(searchPath, ".qoder/skills"),
675
+ join(searchPath, ".roo/skills"),
676
+ join(searchPath, ".trae/skills"),
677
+ join(searchPath, ".windsurf/skills"),
678
+ join(searchPath, ".zencoder/skills")
679
+ ];
680
+ prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
681
+ for (const dir of prioritySearchDirs) try {
682
+ const entries = await readdir(dir, { withFileTypes: true });
683
+ for (const entry of entries) if (entry.isDirectory()) {
684
+ const skillDir = join(dir, entry.name);
685
+ if (await hasSkillMd(skillDir)) {
686
+ let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
687
+ if (skill && !seenNames.has(skill.name)) {
688
+ skill = enhanceSkill(skill);
689
+ skills.push(skill);
690
+ seenNames.add(skill.name);
691
+ }
692
+ }
693
+ }
694
+ } catch {}
695
+ if (skills.length === 0 || options?.fullDepth) {
696
+ const allSkillDirs = await findSkillDirs(searchPath);
697
+ for (const skillDir of allSkillDirs) {
698
+ let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
699
+ if (skill && !seenNames.has(skill.name)) {
700
+ skill = enhanceSkill(skill);
701
+ skills.push(skill);
702
+ seenNames.add(skill.name);
703
+ }
704
+ }
705
+ }
706
+ return skills;
707
+ }
708
+ function getSkillDisplayName(skill) {
709
+ return skill.name || basename(skill.path);
710
+ }
711
+ /**
712
+ * Filter skills based on user input (case-insensitive direct matching).
713
+ * Multi-word skill names must be quoted on the command line.
714
+ */
715
+ function filterSkills(skills, inputNames) {
716
+ const normalizedInputs = inputNames.map((n) => n.toLowerCase());
717
+ return skills.filter((skill) => {
718
+ const name = skill.name.toLowerCase();
719
+ const displayName = getSkillDisplayName(skill).toLowerCase();
720
+ return normalizedInputs.some((input) => input === name || input === displayName);
721
+ });
722
+ }
723
+ //#endregion
724
+ //#region src/agents.ts
725
+ const home = homedir();
726
+ const configHome = xdgConfig ?? join(home, ".config");
727
+ const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
728
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
729
+ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
730
+ if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
731
+ if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
732
+ if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills");
733
+ return join(homeDir, ".openclaw/skills");
734
+ }
735
+ const agents = {
736
+ amp: {
737
+ name: "amp",
738
+ displayName: "Amp",
739
+ skillsDir: ".agents/skills",
740
+ globalSkillsDir: join(configHome, "agents/skills"),
741
+ detectInstalled: async () => {
742
+ return existsSync(join(configHome, "amp"));
743
+ }
744
+ },
745
+ antigravity: {
746
+ name: "antigravity",
747
+ displayName: "Antigravity",
748
+ skillsDir: ".agent/skills",
749
+ globalSkillsDir: join(home, ".gemini/antigravity/skills"),
750
+ detectInstalled: async () => {
751
+ return existsSync(join(home, ".gemini/antigravity"));
752
+ }
753
+ },
754
+ augment: {
755
+ name: "augment",
756
+ displayName: "Augment",
757
+ skillsDir: ".augment/skills",
758
+ globalSkillsDir: join(home, ".augment/skills"),
759
+ detectInstalled: async () => {
760
+ return existsSync(join(home, ".augment"));
761
+ }
762
+ },
763
+ "claude-code": {
764
+ name: "claude-code",
765
+ displayName: "Claude Code",
766
+ skillsDir: ".claude/skills",
767
+ globalSkillsDir: join(claudeHome, "skills"),
768
+ detectInstalled: async () => {
769
+ return existsSync(claudeHome);
770
+ }
771
+ },
772
+ openclaw: {
773
+ name: "openclaw",
774
+ displayName: "OpenClaw",
775
+ skillsDir: "skills",
776
+ globalSkillsDir: getOpenClawGlobalSkillsDir(),
777
+ detectInstalled: async () => {
778
+ return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"));
779
+ }
780
+ },
781
+ cline: {
782
+ name: "cline",
783
+ displayName: "Cline",
784
+ skillsDir: ".agents/skills",
785
+ globalSkillsDir: join(home, ".agents", "skills"),
786
+ detectInstalled: async () => {
787
+ return existsSync(join(home, ".cline"));
788
+ }
789
+ },
790
+ codebuddy: {
791
+ name: "codebuddy",
792
+ displayName: "CodeBuddy",
793
+ skillsDir: ".codebuddy/skills",
794
+ globalSkillsDir: join(home, ".codebuddy/skills"),
795
+ detectInstalled: async () => {
796
+ return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"));
797
+ }
798
+ },
799
+ codex: {
800
+ name: "codex",
801
+ displayName: "Codex",
802
+ skillsDir: ".agents/skills",
803
+ globalSkillsDir: join(codexHome, "skills"),
804
+ detectInstalled: async () => {
805
+ return existsSync(codexHome) || existsSync("/etc/codex");
806
+ }
807
+ },
808
+ "command-code": {
809
+ name: "command-code",
810
+ displayName: "Command Code",
811
+ skillsDir: ".commandcode/skills",
812
+ globalSkillsDir: join(home, ".commandcode/skills"),
813
+ detectInstalled: async () => {
814
+ return existsSync(join(home, ".commandcode"));
815
+ }
816
+ },
817
+ continue: {
818
+ name: "continue",
819
+ displayName: "Continue",
820
+ skillsDir: ".continue/skills",
821
+ globalSkillsDir: join(home, ".continue/skills"),
822
+ detectInstalled: async () => {
823
+ return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
824
+ }
825
+ },
826
+ cortex: {
827
+ name: "cortex",
828
+ displayName: "Cortex Code",
829
+ skillsDir: ".cortex/skills",
830
+ globalSkillsDir: join(home, ".snowflake/cortex/skills"),
831
+ detectInstalled: async () => {
832
+ return existsSync(join(home, ".snowflake/cortex"));
833
+ }
834
+ },
835
+ crush: {
836
+ name: "crush",
837
+ displayName: "Crush",
838
+ skillsDir: ".crush/skills",
839
+ globalSkillsDir: join(home, ".config/crush/skills"),
840
+ detectInstalled: async () => {
841
+ return existsSync(join(home, ".config/crush"));
842
+ }
843
+ },
844
+ cursor: {
845
+ name: "cursor",
846
+ displayName: "Cursor",
847
+ skillsDir: ".agents/skills",
848
+ globalSkillsDir: join(home, ".cursor/skills"),
849
+ detectInstalled: async () => {
850
+ return existsSync(join(home, ".cursor"));
851
+ }
852
+ },
853
+ droid: {
854
+ name: "droid",
855
+ displayName: "Droid",
856
+ skillsDir: ".factory/skills",
857
+ globalSkillsDir: join(home, ".factory/skills"),
858
+ detectInstalled: async () => {
859
+ return existsSync(join(home, ".factory"));
860
+ }
861
+ },
862
+ "gemini-cli": {
863
+ name: "gemini-cli",
864
+ displayName: "Gemini CLI",
865
+ skillsDir: ".agents/skills",
866
+ globalSkillsDir: join(home, ".gemini/skills"),
867
+ detectInstalled: async () => {
868
+ return existsSync(join(home, ".gemini"));
869
+ }
870
+ },
871
+ "github-copilot": {
872
+ name: "github-copilot",
873
+ displayName: "GitHub Copilot",
874
+ skillsDir: ".agents/skills",
875
+ globalSkillsDir: join(home, ".copilot/skills"),
876
+ detectInstalled: async () => {
877
+ return existsSync(join(home, ".copilot"));
878
+ }
879
+ },
880
+ goose: {
881
+ name: "goose",
882
+ displayName: "Goose",
883
+ skillsDir: ".goose/skills",
884
+ globalSkillsDir: join(configHome, "goose/skills"),
885
+ detectInstalled: async () => {
886
+ return existsSync(join(configHome, "goose"));
887
+ }
888
+ },
889
+ junie: {
890
+ name: "junie",
891
+ displayName: "Junie",
892
+ skillsDir: ".junie/skills",
893
+ globalSkillsDir: join(home, ".junie/skills"),
894
+ detectInstalled: async () => {
895
+ return existsSync(join(home, ".junie"));
896
+ }
897
+ },
898
+ "iflow-cli": {
899
+ name: "iflow-cli",
900
+ displayName: "iFlow CLI",
901
+ skillsDir: ".iflow/skills",
902
+ globalSkillsDir: join(home, ".iflow/skills"),
903
+ detectInstalled: async () => {
904
+ return existsSync(join(home, ".iflow"));
905
+ }
906
+ },
907
+ kilo: {
908
+ name: "kilo",
909
+ displayName: "Kilo Code",
910
+ skillsDir: ".kilocode/skills",
911
+ globalSkillsDir: join(home, ".kilocode/skills"),
912
+ detectInstalled: async () => {
913
+ return existsSync(join(home, ".kilocode"));
914
+ }
915
+ },
916
+ "kimi-cli": {
917
+ name: "kimi-cli",
918
+ displayName: "Kimi Code CLI",
919
+ skillsDir: ".agents/skills",
920
+ globalSkillsDir: join(home, ".config/agents/skills"),
921
+ detectInstalled: async () => {
922
+ return existsSync(join(home, ".kimi"));
923
+ }
924
+ },
925
+ "kiro-cli": {
926
+ name: "kiro-cli",
927
+ displayName: "Kiro CLI",
928
+ skillsDir: ".kiro/skills",
929
+ globalSkillsDir: join(home, ".kiro/skills"),
930
+ detectInstalled: async () => {
931
+ return existsSync(join(home, ".kiro"));
932
+ }
933
+ },
934
+ kode: {
935
+ name: "kode",
936
+ displayName: "Kode",
937
+ skillsDir: ".kode/skills",
938
+ globalSkillsDir: join(home, ".kode/skills"),
939
+ detectInstalled: async () => {
940
+ return existsSync(join(home, ".kode"));
941
+ }
942
+ },
943
+ mcpjam: {
944
+ name: "mcpjam",
945
+ displayName: "MCPJam",
946
+ skillsDir: ".mcpjam/skills",
947
+ globalSkillsDir: join(home, ".mcpjam/skills"),
948
+ detectInstalled: async () => {
949
+ return existsSync(join(home, ".mcpjam"));
950
+ }
951
+ },
952
+ "mistral-vibe": {
953
+ name: "mistral-vibe",
954
+ displayName: "Mistral Vibe",
955
+ skillsDir: ".vibe/skills",
956
+ globalSkillsDir: join(home, ".vibe/skills"),
957
+ detectInstalled: async () => {
958
+ return existsSync(join(home, ".vibe"));
959
+ }
960
+ },
961
+ mux: {
962
+ name: "mux",
963
+ displayName: "Mux",
964
+ skillsDir: ".mux/skills",
965
+ globalSkillsDir: join(home, ".mux/skills"),
966
+ detectInstalled: async () => {
967
+ return existsSync(join(home, ".mux"));
968
+ }
969
+ },
970
+ opencode: {
971
+ name: "opencode",
972
+ displayName: "OpenCode",
973
+ skillsDir: ".agents/skills",
974
+ globalSkillsDir: join(configHome, "opencode/skills"),
975
+ detectInstalled: async () => {
976
+ return existsSync(join(configHome, "opencode"));
977
+ }
978
+ },
979
+ openhands: {
980
+ name: "openhands",
981
+ displayName: "OpenHands",
982
+ skillsDir: ".openhands/skills",
983
+ globalSkillsDir: join(home, ".openhands/skills"),
984
+ detectInstalled: async () => {
985
+ return existsSync(join(home, ".openhands"));
986
+ }
987
+ },
988
+ pi: {
989
+ name: "pi",
990
+ displayName: "Pi",
991
+ skillsDir: ".pi/skills",
992
+ globalSkillsDir: join(home, ".pi/agent/skills"),
993
+ detectInstalled: async () => {
994
+ return existsSync(join(home, ".pi/agent"));
995
+ }
996
+ },
997
+ qoder: {
998
+ name: "qoder",
999
+ displayName: "Qoder",
1000
+ skillsDir: ".qoder/skills",
1001
+ globalSkillsDir: join(home, ".qoder/skills"),
1002
+ detectInstalled: async () => {
1003
+ return existsSync(join(home, ".qoder"));
1004
+ }
1005
+ },
1006
+ "qwen-code": {
1007
+ name: "qwen-code",
1008
+ displayName: "Qwen Code",
1009
+ skillsDir: ".qwen/skills",
1010
+ globalSkillsDir: join(home, ".qwen/skills"),
1011
+ detectInstalled: async () => {
1012
+ return existsSync(join(home, ".qwen"));
1013
+ }
1014
+ },
1015
+ replit: {
1016
+ name: "replit",
1017
+ displayName: "Replit",
1018
+ skillsDir: ".agents/skills",
1019
+ globalSkillsDir: join(configHome, "agents/skills"),
1020
+ showInUniversalList: false,
1021
+ detectInstalled: async () => {
1022
+ return existsSync(join(process.cwd(), ".replit"));
1023
+ }
1024
+ },
1025
+ roo: {
1026
+ name: "roo",
1027
+ displayName: "Roo Code",
1028
+ skillsDir: ".roo/skills",
1029
+ globalSkillsDir: join(home, ".roo/skills"),
1030
+ detectInstalled: async () => {
1031
+ return existsSync(join(home, ".roo"));
1032
+ }
1033
+ },
1034
+ trae: {
1035
+ name: "trae",
1036
+ displayName: "Trae",
1037
+ skillsDir: ".trae/skills",
1038
+ globalSkillsDir: join(home, ".trae/skills"),
1039
+ detectInstalled: async () => {
1040
+ return existsSync(join(home, ".trae"));
1041
+ }
1042
+ },
1043
+ "trae-cn": {
1044
+ name: "trae-cn",
1045
+ displayName: "Trae CN",
1046
+ skillsDir: ".trae/skills",
1047
+ globalSkillsDir: join(home, ".trae-cn/skills"),
1048
+ detectInstalled: async () => {
1049
+ return existsSync(join(home, ".trae-cn"));
1050
+ }
1051
+ },
1052
+ windsurf: {
1053
+ name: "windsurf",
1054
+ displayName: "Windsurf",
1055
+ skillsDir: ".windsurf/skills",
1056
+ globalSkillsDir: join(home, ".codeium/windsurf/skills"),
1057
+ detectInstalled: async () => {
1058
+ return existsSync(join(home, ".codeium/windsurf"));
1059
+ }
1060
+ },
1061
+ zencoder: {
1062
+ name: "zencoder",
1063
+ displayName: "Zencoder",
1064
+ skillsDir: ".zencoder/skills",
1065
+ globalSkillsDir: join(home, ".zencoder/skills"),
1066
+ detectInstalled: async () => {
1067
+ return existsSync(join(home, ".zencoder"));
1068
+ }
1069
+ },
1070
+ neovate: {
1071
+ name: "neovate",
1072
+ displayName: "Neovate",
1073
+ skillsDir: ".neovate/skills",
1074
+ globalSkillsDir: join(home, ".neovate/skills"),
1075
+ detectInstalled: async () => {
1076
+ return existsSync(join(home, ".neovate"));
1077
+ }
1078
+ },
1079
+ pochi: {
1080
+ name: "pochi",
1081
+ displayName: "Pochi",
1082
+ skillsDir: ".pochi/skills",
1083
+ globalSkillsDir: join(home, ".pochi/skills"),
1084
+ detectInstalled: async () => {
1085
+ return existsSync(join(home, ".pochi"));
1086
+ }
1087
+ },
1088
+ adal: {
1089
+ name: "adal",
1090
+ displayName: "AdaL",
1091
+ skillsDir: ".adal/skills",
1092
+ globalSkillsDir: join(home, ".adal/skills"),
1093
+ detectInstalled: async () => {
1094
+ return existsSync(join(home, ".adal"));
1095
+ }
1096
+ },
1097
+ universal: {
1098
+ name: "universal",
1099
+ displayName: "Universal",
1100
+ skillsDir: ".agents/skills",
1101
+ globalSkillsDir: join(configHome, "agents/skills"),
1102
+ showInUniversalList: false,
1103
+ detectInstalled: async () => false
1104
+ }
1105
+ };
1106
+ async function detectInstalledAgents() {
1107
+ return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
1108
+ type,
1109
+ installed: await config.detectInstalled()
1110
+ })))).filter((r) => r.installed).map((r) => r.type);
1111
+ }
1112
+ /**
1113
+ * Returns agents that use the universal .agents/skills directory.
1114
+ * These agents share a common skill location and don't need symlinks.
1115
+ * Agents with showInUniversalList: false are excluded.
1116
+ */
1117
+ function getUniversalAgents() {
1118
+ return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
1119
+ }
1120
+ /**
1121
+ * Returns agents that use agent-specific skill directories (not universal).
1122
+ * These agents need symlinks from the canonical .agents/skills location.
1123
+ */
1124
+ function getNonUniversalAgents() {
1125
+ return Object.entries(agents).filter(([_, config]) => config.skillsDir !== ".agents/skills").map(([type]) => type);
1126
+ }
1127
+ /**
1128
+ * Check if an agent uses the universal .agents/skills directory.
1129
+ */
1130
+ function isUniversalAgent(type) {
1131
+ return agents[type].skillsDir === ".agents/skills";
1132
+ }
1133
+ //#endregion
1134
+ //#region src/constants.ts
1135
+ const AGENTS_DIR$2 = ".agents";
1136
+ const SKILLS_SUBDIR = "skills";
1137
+ //#endregion
1138
+ //#region src/installer.ts
1139
+ /**
1140
+ * Sanitizes a filename/directory name to prevent path traversal attacks
1141
+ * and ensures it follows kebab-case convention
1142
+ * @param name - The name to sanitize
1143
+ * @returns Sanitized name safe for use in file paths
1144
+ */
1145
+ function sanitizeName(name) {
1146
+ return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
1147
+ }
1148
+ /**
1149
+ * Validates that a path is within an expected base directory
1150
+ * @param basePath - The expected base directory
1151
+ * @param targetPath - The path to validate
1152
+ * @returns true if targetPath is within basePath
1153
+ */
1154
+ function isPathSafe(basePath, targetPath) {
1155
+ const normalizedBase = normalize(resolve(basePath));
1156
+ const normalizedTarget = normalize(resolve(targetPath));
1157
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
1158
+ }
1159
+ function getCanonicalSkillsDir(global, cwd) {
1160
+ return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$2, SKILLS_SUBDIR);
1161
+ }
1162
+ /**
1163
+ * Gets the base directory for an agent's skills, respecting universal agents.
1164
+ * Universal agents always use the canonical directory, which prevents
1165
+ * redundant symlinks and double-listing of skills.
1166
+ */
1167
+ function getAgentBaseDir(agentType, global, cwd) {
1168
+ if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
1169
+ const agent = agents[agentType];
1170
+ const baseDir = global ? homedir() : cwd || process.cwd();
1171
+ if (global) {
1172
+ if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
1173
+ return agent.globalSkillsDir;
1174
+ }
1175
+ return join(baseDir, agent.skillsDir);
1176
+ }
1177
+ function resolveSymlinkTarget(linkPath, linkTarget) {
1178
+ return resolve(dirname(linkPath), linkTarget);
1179
+ }
1180
+ /**
1181
+ * Cleans and recreates a directory for skill installation.
1182
+ *
1183
+ * This ensures:
1184
+ * 1. Renamed/deleted files from previous installs are removed
1185
+ * 2. Symlinks (including self-referential ones causing ELOOP) are handled
1186
+ * when canonical and agent paths resolve to the same location
1187
+ */
1188
+ async function cleanAndCreateDirectory(path) {
1189
+ try {
1190
+ await rm(path, {
1191
+ recursive: true,
1192
+ force: true
1193
+ });
1194
+ } catch {}
1195
+ await mkdir(path, { recursive: true });
1196
+ }
1197
+ /**
1198
+ * Resolve a path's parent directory through symlinks, keeping the final component.
1199
+ * This handles the case where a parent directory (e.g., ~/.claude/skills) is a symlink
1200
+ * to another location (e.g., ~/.agents/skills). In that case, computing relative paths
1201
+ * from the symlink path produces broken symlinks.
1202
+ *
1203
+ * Returns the real path of the parent + the original basename.
1204
+ * If realpath fails (parent doesn't exist), returns the original resolved path.
1205
+ */
1206
+ async function resolveParentSymlinks(path) {
1207
+ const resolved = resolve(path);
1208
+ const dir = dirname(resolved);
1209
+ const base = basename(resolved);
1210
+ try {
1211
+ return join(await realpath(dir), base);
1212
+ } catch {
1213
+ return resolved;
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Creates a symlink, handling cross-platform differences
1218
+ * Returns true if symlink was created, false if fallback to copy is needed
1219
+ */
1220
+ async function createSymlink(target, linkPath) {
1221
+ try {
1222
+ const resolvedTarget = resolve(target);
1223
+ const resolvedLinkPath = resolve(linkPath);
1224
+ const [realTarget, realLinkPath] = await Promise.all([realpath(resolvedTarget).catch(() => resolvedTarget), realpath(resolvedLinkPath).catch(() => resolvedLinkPath)]);
1225
+ if (realTarget === realLinkPath) return true;
1226
+ if (await resolveParentSymlinks(target) === await resolveParentSymlinks(linkPath)) return true;
1227
+ try {
1228
+ if ((await lstat(linkPath)).isSymbolicLink()) {
1229
+ if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
1230
+ await rm(linkPath);
1231
+ } else await rm(linkPath, { recursive: true });
1232
+ } catch (err) {
1233
+ if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
1234
+ await rm(linkPath, { force: true });
1235
+ } catch {}
1236
+ }
1237
+ const linkDir = dirname(linkPath);
1238
+ await mkdir(linkDir, { recursive: true });
1239
+ await symlink(relative(await resolveParentSymlinks(linkDir), target), linkPath, platform() === "win32" ? "junction" : void 0);
1240
+ return true;
1241
+ } catch {
1242
+ return false;
1243
+ }
1244
+ }
1245
+ async function installSkillForAgent(skill, agentType, options = {}) {
1246
+ const agent = agents[agentType];
1247
+ const isGlobal = options.global ?? false;
1248
+ const cwd = options.cwd || process.cwd();
1249
+ if (isGlobal && agent.globalSkillsDir === void 0) return {
1250
+ success: false,
1251
+ path: "",
1252
+ mode: options.mode ?? "symlink",
1253
+ error: `${agent.displayName} does not support global skill installation`
1254
+ };
1255
+ const skillName = sanitizeName(skill.name || basename(skill.path));
1256
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
1257
+ const canonicalDir = join(canonicalBase, skillName);
1258
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
1259
+ const agentDir = join(agentBase, skillName);
1260
+ const installMode = options.mode ?? "symlink";
1261
+ if (!isPathSafe(canonicalBase, canonicalDir)) return {
1262
+ success: false,
1263
+ path: agentDir,
1264
+ mode: installMode,
1265
+ error: "Invalid skill name: potential path traversal detected"
1266
+ };
1267
+ if (!isPathSafe(agentBase, agentDir)) return {
1268
+ success: false,
1269
+ path: agentDir,
1270
+ mode: installMode,
1271
+ error: "Invalid skill name: potential path traversal detected"
1272
+ };
1273
+ try {
1274
+ if (installMode === "copy") {
1275
+ await cleanAndCreateDirectory(agentDir);
1276
+ await copyDirectory(skill.path, agentDir);
1277
+ return {
1278
+ success: true,
1279
+ path: agentDir,
1280
+ mode: "copy"
1281
+ };
1282
+ }
1283
+ await cleanAndCreateDirectory(canonicalDir);
1284
+ await copyDirectory(skill.path, canonicalDir);
1285
+ if (isGlobal && isUniversalAgent(agentType)) return {
1286
+ success: true,
1287
+ path: canonicalDir,
1288
+ canonicalPath: canonicalDir,
1289
+ mode: "symlink"
1290
+ };
1291
+ if (!await createSymlink(canonicalDir, agentDir)) {
1292
+ await cleanAndCreateDirectory(agentDir);
1293
+ await copyDirectory(skill.path, agentDir);
1294
+ return {
1295
+ success: true,
1296
+ path: agentDir,
1297
+ canonicalPath: canonicalDir,
1298
+ mode: "symlink",
1299
+ symlinkFailed: true
1300
+ };
1301
+ }
1302
+ return {
1303
+ success: true,
1304
+ path: agentDir,
1305
+ canonicalPath: canonicalDir,
1306
+ mode: "symlink"
1307
+ };
1308
+ } catch (error) {
1309
+ return {
1310
+ success: false,
1311
+ path: agentDir,
1312
+ mode: installMode,
1313
+ error: error instanceof Error ? error.message : "Unknown error"
1314
+ };
1315
+ }
1316
+ }
1317
+ const EXCLUDE_FILES = new Set(["metadata.json"]);
1318
+ const EXCLUDE_DIRS = new Set([".git"]);
1319
+ const isExcluded = (name, isDirectory = false) => {
1320
+ if (EXCLUDE_FILES.has(name)) return true;
1321
+ if (name.startsWith("_")) return true;
1322
+ if (isDirectory && EXCLUDE_DIRS.has(name)) return true;
1323
+ return false;
1324
+ };
1325
+ async function copyDirectory(src, dest) {
1326
+ await mkdir(dest, { recursive: true });
1327
+ const entries = await readdir(src, { withFileTypes: true });
1328
+ await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
1329
+ const srcPath = join(src, entry.name);
1330
+ const destPath = join(dest, entry.name);
1331
+ if (entry.isDirectory()) await copyDirectory(srcPath, destPath);
1332
+ else await cp(srcPath, destPath, {
1333
+ dereference: true,
1334
+ recursive: true
1335
+ });
1336
+ }));
1337
+ }
1338
+ async function isSkillInstalled(skillName, agentType, options = {}) {
1339
+ const agent = agents[agentType];
1340
+ const sanitized = sanitizeName(skillName);
1341
+ if (options.global && agent.globalSkillsDir === void 0) return false;
1342
+ const targetBase = options.global ? agent.globalSkillsDir : join(options.cwd || process.cwd(), agent.skillsDir);
1343
+ const skillDir = join(targetBase, sanitized);
1344
+ if (!isPathSafe(targetBase, skillDir)) return false;
1345
+ try {
1346
+ await access(skillDir);
1347
+ return true;
1348
+ } catch {
1349
+ return false;
1350
+ }
1351
+ }
1352
+ function getInstallPath(skillName, agentType, options = {}) {
1353
+ agents[agentType];
1354
+ options.cwd || process.cwd();
1355
+ const sanitized = sanitizeName(skillName);
1356
+ const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
1357
+ const installPath = join(targetBase, sanitized);
1358
+ if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
1359
+ return installPath;
1360
+ }
1361
+ /**
1362
+ * Gets the canonical .agents/skills/<skill> path
1363
+ */
1364
+ function getCanonicalPath(skillName, options = {}) {
1365
+ const sanitized = sanitizeName(skillName);
1366
+ const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
1367
+ const canonicalPath = join(canonicalBase, sanitized);
1368
+ if (!isPathSafe(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
1369
+ return canonicalPath;
1370
+ }
1371
+ /**
1372
+ * Install a well-known skill with multiple files.
1373
+ * The skill directory name is derived from the installName field.
1374
+ * All files from the skill's files map are written to the installation directory.
1375
+ * Supports symlink mode (writes to canonical location and symlinks to agent dirs)
1376
+ * or copy mode (writes directly to each agent dir).
1377
+ */
1378
+ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1379
+ const agent = agents[agentType];
1380
+ const isGlobal = options.global ?? false;
1381
+ const cwd = options.cwd || process.cwd();
1382
+ const installMode = options.mode ?? "symlink";
1383
+ if (isGlobal && agent.globalSkillsDir === void 0) return {
1384
+ success: false,
1385
+ path: "",
1386
+ mode: installMode,
1387
+ error: `${agent.displayName} does not support global skill installation`
1388
+ };
1389
+ const skillName = sanitizeName(skill.installName);
1390
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
1391
+ const canonicalDir = join(canonicalBase, skillName);
1392
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
1393
+ const agentDir = join(agentBase, skillName);
1394
+ if (!isPathSafe(canonicalBase, canonicalDir)) return {
1395
+ success: false,
1396
+ path: agentDir,
1397
+ mode: installMode,
1398
+ error: "Invalid skill name: potential path traversal detected"
1399
+ };
1400
+ if (!isPathSafe(agentBase, agentDir)) return {
1401
+ success: false,
1402
+ path: agentDir,
1403
+ mode: installMode,
1404
+ error: "Invalid skill name: potential path traversal detected"
1405
+ };
1406
+ /**
1407
+ * Write all skill files to a directory (assumes directory already exists)
1408
+ */
1409
+ async function writeSkillFiles(targetDir) {
1410
+ for (const [filePath, content] of skill.files) {
1411
+ const fullPath = join(targetDir, filePath);
1412
+ if (!isPathSafe(targetDir, fullPath)) continue;
1413
+ const parentDir = dirname(fullPath);
1414
+ if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
1415
+ await writeFile(fullPath, content, "utf-8");
1416
+ }
1417
+ }
1418
+ try {
1419
+ if (installMode === "copy") {
1420
+ await cleanAndCreateDirectory(agentDir);
1421
+ await writeSkillFiles(agentDir);
1422
+ return {
1423
+ success: true,
1424
+ path: agentDir,
1425
+ mode: "copy"
1426
+ };
1427
+ }
1428
+ await cleanAndCreateDirectory(canonicalDir);
1429
+ await writeSkillFiles(canonicalDir);
1430
+ if (isGlobal && isUniversalAgent(agentType)) return {
1431
+ success: true,
1432
+ path: canonicalDir,
1433
+ canonicalPath: canonicalDir,
1434
+ mode: "symlink"
1435
+ };
1436
+ if (!await createSymlink(canonicalDir, agentDir)) {
1437
+ await cleanAndCreateDirectory(agentDir);
1438
+ await writeSkillFiles(agentDir);
1439
+ return {
1440
+ success: true,
1441
+ path: agentDir,
1442
+ canonicalPath: canonicalDir,
1443
+ mode: "symlink",
1444
+ symlinkFailed: true
1445
+ };
1446
+ }
1447
+ return {
1448
+ success: true,
1449
+ path: agentDir,
1450
+ canonicalPath: canonicalDir,
1451
+ mode: "symlink"
1452
+ };
1453
+ } catch (error) {
1454
+ return {
1455
+ success: false,
1456
+ path: agentDir,
1457
+ mode: installMode,
1458
+ error: error instanceof Error ? error.message : "Unknown error"
1459
+ };
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Lists all installed skills from canonical locations
1464
+ * @param options - Options for listing skills
1465
+ * @returns Array of installed skills with metadata
1466
+ */
1467
+ async function listInstalledSkills(options = {}) {
1468
+ const cwd = options.cwd || process.cwd();
1469
+ const skillsMap = /* @__PURE__ */ new Map();
1470
+ const scopes = [];
1471
+ const detectedAgents = await detectInstalledAgents();
1472
+ const agentFilter = options.agentFilter;
1473
+ const agentsToCheck = agentFilter ? detectedAgents.filter((a) => agentFilter.includes(a)) : detectedAgents;
1474
+ const scopeTypes = [];
1475
+ if (options.global === void 0) scopeTypes.push({ global: false }, { global: true });
1476
+ else scopeTypes.push({ global: options.global });
1477
+ for (const { global: isGlobal } of scopeTypes) {
1478
+ scopes.push({
1479
+ global: isGlobal,
1480
+ path: getCanonicalSkillsDir(isGlobal, cwd)
1481
+ });
1482
+ for (const agentType of agentsToCheck) {
1483
+ const agent = agents[agentType];
1484
+ if (isGlobal && agent.globalSkillsDir === void 0) continue;
1485
+ const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1486
+ if (!scopes.some((s) => s.path === agentDir && s.global === isGlobal)) scopes.push({
1487
+ global: isGlobal,
1488
+ path: agentDir,
1489
+ agentType
1490
+ });
1491
+ }
1492
+ }
1493
+ for (const scope of scopes) try {
1494
+ const entries = await readdir(scope.path, { withFileTypes: true });
1495
+ for (const entry of entries) {
1496
+ if (!entry.isDirectory()) continue;
1497
+ const skillDir = join(scope.path, entry.name);
1498
+ const skillMdPath = join(skillDir, "SKILL.md");
1499
+ try {
1500
+ await stat(skillMdPath);
1501
+ } catch {
1502
+ continue;
1503
+ }
1504
+ const skill = await parseSkillMd(skillMdPath);
1505
+ if (!skill) continue;
1506
+ const scopeKey = scope.global ? "global" : "project";
1507
+ const skillKey = `${scopeKey}:${skill.name}`;
1508
+ if (scope.agentType) {
1509
+ if (skillsMap.has(skillKey)) {
1510
+ const existing = skillsMap.get(skillKey);
1511
+ if (!existing.agents.includes(scope.agentType)) existing.agents.push(scope.agentType);
1512
+ } else skillsMap.set(skillKey, {
1513
+ name: skill.name,
1514
+ description: skill.description,
1515
+ path: skillDir,
1516
+ canonicalPath: skillDir,
1517
+ scope: scopeKey,
1518
+ agents: [scope.agentType]
1519
+ });
1520
+ continue;
1521
+ }
1522
+ const sanitizedSkillName = sanitizeName(skill.name);
1523
+ const installedAgents = [];
1524
+ for (const agentType of agentsToCheck) {
1525
+ const agent = agents[agentType];
1526
+ if (scope.global && agent.globalSkillsDir === void 0) continue;
1527
+ const agentBase = scope.global ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1528
+ let found = false;
1529
+ const possibleNames = Array.from(new Set([
1530
+ entry.name,
1531
+ sanitizedSkillName,
1532
+ skill.name.toLowerCase().replace(/\s+/g, "-").replace(/[\/\\:\0]/g, "")
1533
+ ]));
1534
+ for (const possibleName of possibleNames) {
1535
+ const agentSkillDir = join(agentBase, possibleName);
1536
+ if (!isPathSafe(agentBase, agentSkillDir)) continue;
1537
+ try {
1538
+ await access(agentSkillDir);
1539
+ found = true;
1540
+ break;
1541
+ } catch {}
1542
+ }
1543
+ if (!found) try {
1544
+ const agentEntries = await readdir(agentBase, { withFileTypes: true });
1545
+ for (const agentEntry of agentEntries) {
1546
+ if (!agentEntry.isDirectory()) continue;
1547
+ const candidateDir = join(agentBase, agentEntry.name);
1548
+ if (!isPathSafe(agentBase, candidateDir)) continue;
1549
+ try {
1550
+ const candidateSkillMd = join(candidateDir, "SKILL.md");
1551
+ await stat(candidateSkillMd);
1552
+ const candidateSkill = await parseSkillMd(candidateSkillMd);
1553
+ if (candidateSkill && candidateSkill.name === skill.name) {
1554
+ found = true;
1555
+ break;
1556
+ }
1557
+ } catch {}
1558
+ }
1559
+ } catch {}
1560
+ if (found) installedAgents.push(agentType);
1561
+ }
1562
+ if (skillsMap.has(skillKey)) {
1563
+ const existing = skillsMap.get(skillKey);
1564
+ for (const agent of installedAgents) if (!existing.agents.includes(agent)) existing.agents.push(agent);
1565
+ } else skillsMap.set(skillKey, {
1566
+ name: skill.name,
1567
+ description: skill.description,
1568
+ path: skillDir,
1569
+ canonicalPath: skillDir,
1570
+ scope: scopeKey,
1571
+ agents: installedAgents
1572
+ });
1573
+ }
1574
+ } catch {}
1575
+ return Array.from(skillsMap.values());
1576
+ }
1577
+ //#endregion
1578
+ //#region src/telemetry.ts
1579
+ const TELEMETRY_URL = process.env.SKILLS_TELEMETRY_URL || "";
1580
+ const AUDIT_URL = process.env.SKILLS_AUDIT_URL || "";
1581
+ let cliVersion = null;
1582
+ function isCI() {
1583
+ return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
1584
+ }
1585
+ function isEnabled() {
1586
+ return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
1587
+ }
1588
+ function setVersion(version) {
1589
+ cliVersion = version;
1590
+ }
1591
+ /**
1592
+ * Fetch security audit results for skills from the audit API.
1593
+ * Returns null on any error or timeout — never blocks installation.
1594
+ */
1595
+ async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
1596
+ if (!AUDIT_URL || skillSlugs.length === 0) return null;
1597
+ try {
1598
+ const params = new URLSearchParams({
1599
+ source,
1600
+ skills: skillSlugs.join(",")
1601
+ });
1602
+ const controller = new AbortController();
1603
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1604
+ const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
1605
+ clearTimeout(timeout);
1606
+ if (!response.ok) return null;
1607
+ return await response.json();
1608
+ } catch {
1609
+ return null;
1610
+ }
1611
+ }
1612
+ function track(data) {
1613
+ if (!TELEMETRY_URL || !isEnabled()) return;
1614
+ try {
1615
+ const params = new URLSearchParams();
1616
+ if (cliVersion) params.set("v", cliVersion);
1617
+ if (isCI()) params.set("ci", "1");
1618
+ for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
1619
+ fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {});
1620
+ } catch {}
1621
+ }
1622
+ //#endregion
1623
+ //#region src/providers/registry.ts
1624
+ var ProviderRegistryImpl = class {
1625
+ providers = [];
1626
+ register(provider) {
1627
+ if (this.providers.some((p) => p.id === provider.id)) throw new Error(`Provider with id "${provider.id}" already registered`);
1628
+ this.providers.push(provider);
1629
+ }
1630
+ findProvider(url) {
1631
+ for (const provider of this.providers) if (provider.match(url).matches) return provider;
1632
+ return null;
1633
+ }
1634
+ getProviders() {
1635
+ return [...this.providers];
1636
+ }
1637
+ };
1638
+ new ProviderRegistryImpl();
1639
+ //#endregion
1640
+ //#region src/providers/wellknown.ts
1641
+ /**
1642
+ * Well-known skills provider using RFC 8615 well-known URIs.
1643
+ *
1644
+ * Organizations can publish skills at:
1645
+ * https://example.com/.well-known/skills/
1646
+ *
1647
+ * URL formats supported:
1648
+ * - https://example.com (discovers all skills from root)
1649
+ * - https://example.com/docs (discovers from /docs/.well-known/skills/)
1650
+ * - https://example.com/.well-known/skills (discovers all skills)
1651
+ * - https://example.com/.well-known/skills/skill-name (specific skill)
1652
+ *
1653
+ * The source identifier is "wellknown/{hostname}" or "wellknown/{hostname}/path".
1654
+ */
1655
+ var WellKnownProvider = class {
1656
+ id = "well-known";
1657
+ displayName = "Well-Known Skills";
1658
+ WELL_KNOWN_PATH = ".well-known/skills";
1659
+ INDEX_FILE = "index.json";
1660
+ /**
1661
+ * Check if a URL could be a well-known skills endpoint.
1662
+ * This is a fallback provider - it matches any HTTP(S) URL that is not
1663
+ * a recognized pattern (GitHub, GitLab, owner/repo shorthand, etc.)
1664
+ */
1665
+ match(url) {
1666
+ if (!url.startsWith("http://") && !url.startsWith("https://")) return { matches: false };
1667
+ try {
1668
+ const parsed = new URL(url);
1669
+ if ([
1670
+ "github.com",
1671
+ "gitlab.com",
1672
+ "huggingface.co"
1673
+ ].includes(parsed.hostname)) return { matches: false };
1674
+ return {
1675
+ matches: true,
1676
+ sourceIdentifier: `wellknown/${parsed.hostname}`
1677
+ };
1678
+ } catch {
1679
+ return { matches: false };
1680
+ }
1681
+ }
1682
+ /**
1683
+ * Fetch the skills index from a well-known endpoint.
1684
+ * Tries both the path-relative .well-known and the root .well-known.
1685
+ */
1686
+ async fetchIndex(baseUrl) {
1687
+ try {
1688
+ const parsed = new URL(baseUrl);
1689
+ const basePath = parsed.pathname.replace(/\/$/, "");
1690
+ const urlsToTry = [{
1691
+ indexUrl: `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
1692
+ baseUrl: `${parsed.protocol}//${parsed.host}${basePath}`
1693
+ }];
1694
+ if (basePath && basePath !== "") urlsToTry.push({
1695
+ indexUrl: `${parsed.protocol}//${parsed.host}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
1696
+ baseUrl: `${parsed.protocol}//${parsed.host}`
1697
+ });
1698
+ for (const { indexUrl, baseUrl: resolvedBase } of urlsToTry) try {
1699
+ const response = await fetch(indexUrl);
1700
+ if (!response.ok) continue;
1701
+ const index = await response.json();
1702
+ if (!index.skills || !Array.isArray(index.skills)) continue;
1703
+ let allValid = true;
1704
+ for (const entry of index.skills) if (!this.isValidSkillEntry(entry)) {
1705
+ allValid = false;
1706
+ break;
1707
+ }
1708
+ if (allValid) return {
1709
+ index,
1710
+ resolvedBaseUrl: resolvedBase
1711
+ };
1712
+ } catch {
1713
+ continue;
1714
+ }
1715
+ return null;
1716
+ } catch {
1717
+ return null;
1718
+ }
1719
+ }
1720
+ /**
1721
+ * Validate a skill entry from the index.
1722
+ */
1723
+ isValidSkillEntry(entry) {
1724
+ if (!entry || typeof entry !== "object") return false;
1725
+ const e = entry;
1726
+ if (typeof e.name !== "string" || !e.name) return false;
1727
+ if (typeof e.description !== "string" || !e.description) return false;
1728
+ if (!Array.isArray(e.files) || e.files.length === 0) return false;
1729
+ if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name) && e.name.length > 1) {
1730
+ if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) return false;
1731
+ }
1732
+ for (const file of e.files) {
1733
+ if (typeof file !== "string") return false;
1734
+ if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
1735
+ }
1736
+ if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
1737
+ return true;
1738
+ }
1739
+ /**
1740
+ * Fetch a single skill and all its files from a well-known endpoint.
1741
+ */
1742
+ async fetchSkill(url) {
1743
+ try {
1744
+ const parsed = new URL(url);
1745
+ const result = await this.fetchIndex(url);
1746
+ if (!result) return null;
1747
+ const { index, resolvedBaseUrl } = result;
1748
+ let skillName = null;
1749
+ const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
1750
+ if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
1751
+ else if (index.skills.length === 1) skillName = index.skills[0].name;
1752
+ if (!skillName) return null;
1753
+ const skillEntry = index.skills.find((s) => s.name === skillName);
1754
+ if (!skillEntry) return null;
1755
+ return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry);
1756
+ } catch {
1757
+ return null;
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Fetch a skill by its index entry.
1762
+ * @param baseUrl - The base URL (e.g., https://example.com or https://example.com/docs)
1763
+ * @param entry - The skill entry from index.json
1764
+ */
1765
+ async fetchSkillByEntry(baseUrl, entry) {
1766
+ try {
1767
+ const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${entry.name}`;
1768
+ const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
1769
+ const response = await fetch(skillMdUrl);
1770
+ if (!response.ok) return null;
1771
+ const content = await response.text();
1772
+ const { data } = (0, import_gray_matter.default)(content);
1773
+ if (!data.name || !data.description) return null;
1774
+ const files = /* @__PURE__ */ new Map();
1775
+ files.set("SKILL.md", content);
1776
+ const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
1777
+ try {
1778
+ const fileUrl = `${skillBaseUrl}/${filePath}`;
1779
+ const fileResponse = await fetch(fileUrl);
1780
+ if (fileResponse.ok) return {
1781
+ path: filePath,
1782
+ content: await fileResponse.text()
1783
+ };
1784
+ } catch {}
1785
+ return null;
1786
+ });
1787
+ const fileResults = await Promise.all(filePromises);
1788
+ for (const result of fileResults) if (result) files.set(result.path, result.content);
1789
+ return {
1790
+ name: data.name,
1791
+ description: data.description,
1792
+ content,
1793
+ installName: entry.name,
1794
+ sourceUrl: skillMdUrl,
1795
+ metadata: data.metadata,
1796
+ files,
1797
+ indexEntry: entry
1798
+ };
1799
+ } catch {
1800
+ return null;
1801
+ }
1802
+ }
1803
+ /**
1804
+ * Fetch all skills from a well-known endpoint.
1805
+ */
1806
+ async fetchAllSkills(url) {
1807
+ try {
1808
+ const result = await this.fetchIndex(url);
1809
+ if (!result) return [];
1810
+ const { index, resolvedBaseUrl } = result;
1811
+ const skillPromises = index.skills.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry));
1812
+ return (await Promise.all(skillPromises)).filter((s) => s !== null);
1813
+ } catch {
1814
+ return [];
1815
+ }
1816
+ }
1817
+ /**
1818
+ * Convert a user-facing URL to a skill URL.
1819
+ * For well-known, this extracts the base domain and constructs the proper path.
1820
+ */
1821
+ toRawUrl(url) {
1822
+ try {
1823
+ const parsed = new URL(url);
1824
+ if (url.toLowerCase().endsWith("/skill.md")) return url;
1825
+ const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
1826
+ if (pathMatch && pathMatch[1]) {
1827
+ const basePath = parsed.pathname.replace(/\/.well-known\/skills\/.*$/, "");
1828
+ return `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${pathMatch[1]}/SKILL.md`;
1829
+ }
1830
+ const basePath = parsed.pathname.replace(/\/$/, "");
1831
+ return `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`;
1832
+ } catch {
1833
+ return url;
1834
+ }
1835
+ }
1836
+ /**
1837
+ * Get the source identifier for telemetry/storage.
1838
+ * Returns the full hostname with www. stripped.
1839
+ * e.g., "https://mintlify.com/docs" → "mintlify.com"
1840
+ * "https://mppx-discovery-skills.vercel.app" → "mppx-discovery-skills.vercel.app"
1841
+ * "https://www.example.com" → "example.com"
1842
+ * "https://docs.lovable.dev" → "docs.lovable.dev"
1843
+ */
1844
+ getSourceIdentifier(url) {
1845
+ try {
1846
+ return new URL(url).hostname.replace(/^www\./, "");
1847
+ } catch {
1848
+ return "unknown";
1849
+ }
1850
+ }
1851
+ /**
1852
+ * Check if a URL has a well-known skills index.
1853
+ */
1854
+ async hasSkillsIndex(url) {
1855
+ return await this.fetchIndex(url) !== null;
1856
+ }
1857
+ };
1858
+ const wellKnownProvider = new WellKnownProvider();
1859
+ //#endregion
1860
+ //#region src/skill-lock.ts
1861
+ const AGENTS_DIR$1 = ".agents";
1862
+ const LOCK_FILE$1 = ".skill-lock.json";
1863
+ const CURRENT_VERSION$1 = 3;
1864
+ /**
1865
+ * Get the path to the global skill lock file.
1866
+ * Located at ~/.agents/.skill-lock.json
1867
+ */
1868
+ function getSkillLockPath$1() {
1869
+ return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1);
1870
+ }
1871
+ /**
1872
+ * Read the skill lock file.
1873
+ * Returns an empty lock file structure if the file doesn't exist.
1874
+ * Wipes the lock file if it's an old format (version < CURRENT_VERSION).
1875
+ */
1876
+ async function readSkillLock$1() {
1877
+ const lockPath = getSkillLockPath$1();
1878
+ try {
1879
+ const content = await readFile(lockPath, "utf-8");
1880
+ const parsed = JSON.parse(content);
1881
+ if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
1882
+ if (parsed.version < CURRENT_VERSION$1) return createEmptyLockFile();
1883
+ return parsed;
1884
+ } catch (error) {
1885
+ return createEmptyLockFile();
1886
+ }
1887
+ }
1888
+ /**
1889
+ * Write the skill lock file.
1890
+ * Creates the directory if it doesn't exist.
1891
+ */
1892
+ async function writeSkillLock(lock) {
1893
+ const lockPath = getSkillLockPath$1();
1894
+ await mkdir(dirname(lockPath), { recursive: true });
1895
+ await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
1896
+ }
1897
+ /**
1898
+ * Get GitHub token from user's environment.
1899
+ * Tries in order:
1900
+ * 1. GITHUB_TOKEN environment variable
1901
+ * 2. GH_TOKEN environment variable
1902
+ * 3. gh CLI auth token (if gh is installed)
1903
+ *
1904
+ * @returns The token string or null if not available
1905
+ */
1906
+ function getGitHubToken() {
1907
+ if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
1908
+ if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
1909
+ try {
1910
+ const token = execSync("gh auth token", {
1911
+ encoding: "utf-8",
1912
+ stdio: [
1913
+ "pipe",
1914
+ "pipe",
1915
+ "pipe"
1916
+ ]
1917
+ }).trim();
1918
+ if (token) return token;
1919
+ } catch {}
1920
+ return null;
1921
+ }
1922
+ /**
1923
+ * Fetch the tree SHA (folder hash) for a skill folder using GitHub's Trees API.
1924
+ * This makes ONE API call to get the entire repo tree, then extracts the SHA
1925
+ * for the specific skill folder.
1926
+ *
1927
+ * @param ownerRepo - GitHub owner/repo (e.g., "vercel-labs/agent-skills")
1928
+ * @param skillPath - Path to skill folder or SKILL.md (e.g., "skills/react-best-practices/SKILL.md")
1929
+ * @param token - Optional GitHub token for authenticated requests (higher rate limits)
1930
+ * @returns The tree SHA for the skill folder, or null if not found
1931
+ */
1932
+ async function fetchSkillFolderHash(ownerRepo, skillPath, token) {
1933
+ let folderPath = skillPath.replace(/\\/g, "/");
1934
+ if (folderPath.endsWith("/SKILL.md")) folderPath = folderPath.slice(0, -9);
1935
+ else if (folderPath.endsWith("SKILL.md")) folderPath = folderPath.slice(0, -8);
1936
+ if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1);
1937
+ for (const branch of ["main", "master"]) try {
1938
+ const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
1939
+ const headers = {
1940
+ Accept: "application/vnd.github.v3+json",
1941
+ "User-Agent": "skills-cli"
1942
+ };
1943
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1944
+ const response = await fetch(url, { headers });
1945
+ if (!response.ok) continue;
1946
+ const data = await response.json();
1947
+ if (!folderPath) return data.sha;
1948
+ const folderEntry = data.tree.find((entry) => entry.type === "tree" && entry.path === folderPath);
1949
+ if (folderEntry) return folderEntry.sha;
1950
+ } catch {
1951
+ continue;
1952
+ }
1953
+ return null;
1954
+ }
1955
+ /**
1956
+ * Add or update a skill entry in the lock file.
1957
+ */
1958
+ async function addSkillToLock(skillName, entry) {
1959
+ const lock = await readSkillLock$1();
1960
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1961
+ const existingEntry = lock.skills[skillName];
1962
+ lock.skills[skillName] = {
1963
+ ...entry,
1964
+ installedAt: existingEntry?.installedAt ?? now,
1965
+ updatedAt: now
1966
+ };
1967
+ await writeSkillLock(lock);
1968
+ }
1969
+ /**
1970
+ * Remove a skill from the lock file.
1971
+ */
1972
+ async function removeSkillFromLock(skillName) {
1973
+ const lock = await readSkillLock$1();
1974
+ if (!(skillName in lock.skills)) return false;
1975
+ delete lock.skills[skillName];
1976
+ await writeSkillLock(lock);
1977
+ return true;
1978
+ }
1979
+ /**
1980
+ * Get a skill entry from the lock file.
1981
+ */
1982
+ async function getSkillFromLock(skillName) {
1983
+ return (await readSkillLock$1()).skills[skillName] ?? null;
1984
+ }
1985
+ /**
1986
+ * Get all skills from the lock file.
1987
+ */
1988
+ async function getAllLockedSkills() {
1989
+ return (await readSkillLock$1()).skills;
1990
+ }
1991
+ /**
1992
+ * Create an empty lock file structure.
1993
+ */
1994
+ function createEmptyLockFile() {
1995
+ return {
1996
+ version: CURRENT_VERSION$1,
1997
+ skills: {},
1998
+ dismissed: {}
1999
+ };
2000
+ }
2001
+ /**
2002
+ * Check if a prompt has been dismissed.
2003
+ */
2004
+ async function isPromptDismissed(promptKey) {
2005
+ return (await readSkillLock$1()).dismissed?.[promptKey] === true;
2006
+ }
2007
+ /**
2008
+ * Mark a prompt as dismissed.
2009
+ */
2010
+ async function dismissPrompt(promptKey) {
2011
+ const lock = await readSkillLock$1();
2012
+ if (!lock.dismissed) lock.dismissed = {};
2013
+ lock.dismissed[promptKey] = true;
2014
+ await writeSkillLock(lock);
2015
+ }
2016
+ /**
2017
+ * Get the last selected agents.
2018
+ */
2019
+ async function getLastSelectedAgents() {
2020
+ return (await readSkillLock$1()).lastSelectedAgents;
2021
+ }
2022
+ /**
2023
+ * Save the selected agents to the lock file.
2024
+ */
2025
+ async function saveSelectedAgents(agents) {
2026
+ const lock = await readSkillLock$1();
2027
+ lock.lastSelectedAgents = agents;
2028
+ await writeSkillLock(lock);
2029
+ }
2030
+ //#endregion
2031
+ //#region src/local-lock.ts
2032
+ const LOCAL_LOCK_FILE = "skills-lock.json";
2033
+ const CURRENT_VERSION = 1;
2034
+ /**
2035
+ * Get the path to the local skill lock file for a project.
2036
+ */
2037
+ function getLocalLockPath(cwd) {
2038
+ return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
2039
+ }
2040
+ /**
2041
+ * Read the local skill lock file.
2042
+ * Returns an empty lock file structure if the file doesn't exist
2043
+ * or is corrupted (e.g., merge conflict markers).
2044
+ */
2045
+ async function readLocalLock(cwd) {
2046
+ const lockPath = getLocalLockPath(cwd);
2047
+ try {
2048
+ const content = await readFile(lockPath, "utf-8");
2049
+ const parsed = JSON.parse(content);
2050
+ if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
2051
+ if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
2052
+ return parsed;
2053
+ } catch {
2054
+ return createEmptyLocalLock();
2055
+ }
2056
+ }
2057
+ /**
2058
+ * Write the local skill lock file.
2059
+ * Skills are sorted alphabetically by name for deterministic output.
2060
+ */
2061
+ async function writeLocalLock(lock, cwd) {
2062
+ const lockPath = getLocalLockPath(cwd);
2063
+ const sortedSkills = {};
2064
+ for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
2065
+ const sorted = {
2066
+ version: lock.version,
2067
+ skills: sortedSkills
2068
+ };
2069
+ await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
2070
+ }
2071
+ /**
2072
+ * Compute a SHA-256 hash from all files in a skill directory.
2073
+ * Reads all files recursively, sorts them by relative path for determinism,
2074
+ * and produces a single hash from their concatenated contents.
2075
+ */
2076
+ async function computeSkillFolderHash(skillDir) {
2077
+ const files = [];
2078
+ await collectFiles(skillDir, skillDir, files);
2079
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
2080
+ const hash = createHash("sha256");
2081
+ for (const file of files) {
2082
+ hash.update(file.relativePath);
2083
+ hash.update(file.content);
2084
+ }
2085
+ return hash.digest("hex");
2086
+ }
2087
+ async function collectFiles(baseDir, currentDir, results) {
2088
+ const entries = await readdir(currentDir, { withFileTypes: true });
2089
+ await Promise.all(entries.map(async (entry) => {
2090
+ const fullPath = join(currentDir, entry.name);
2091
+ if (entry.isDirectory()) {
2092
+ if (entry.name === ".git" || entry.name === "node_modules") return;
2093
+ await collectFiles(baseDir, fullPath, results);
2094
+ } else if (entry.isFile()) {
2095
+ const content = await readFile(fullPath);
2096
+ const relativePath = relative(baseDir, fullPath).split("\\").join("/");
2097
+ results.push({
2098
+ relativePath,
2099
+ content
2100
+ });
2101
+ }
2102
+ }));
2103
+ }
2104
+ /**
2105
+ * Add or update a skill entry in the local lock file.
2106
+ */
2107
+ async function addSkillToLocalLock(skillName, entry, cwd) {
2108
+ const lock = await readLocalLock(cwd);
2109
+ lock.skills[skillName] = entry;
2110
+ await writeLocalLock(lock, cwd);
2111
+ }
2112
+ function createEmptyLocalLock() {
2113
+ return {
2114
+ version: CURRENT_VERSION,
2115
+ skills: {}
2116
+ };
2117
+ }
2118
+ //#endregion
2119
+ //#region package.json
2120
+ var version$1 = "1.0.0";
2121
+ //#endregion
2122
+ //#region src/add.ts
2123
+ const isCancelled$1 = (value) => typeof value === "symbol";
2124
+ /**
2125
+ * Check if a source identifier (owner/repo format) represents a private GitHub repo.
2126
+ * Returns true if private, false if public, null if unable to determine or not a GitHub repo.
2127
+ */
2128
+ async function isSourcePrivate(source) {
2129
+ const ownerRepo = parseOwnerRepo(source);
2130
+ if (!ownerRepo) return false;
2131
+ return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
2132
+ }
2133
+ function initTelemetry(version) {
2134
+ setVersion(version);
2135
+ }
2136
+ function riskLabel(risk) {
2137
+ switch (risk) {
2138
+ case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
2139
+ case "high": return import_picocolors.default.red("High Risk");
2140
+ case "medium": return import_picocolors.default.yellow("Med Risk");
2141
+ case "low": return import_picocolors.default.green("Low Risk");
2142
+ case "safe": return import_picocolors.default.green("Safe");
2143
+ default: return import_picocolors.default.dim("--");
2144
+ }
2145
+ }
2146
+ function socketLabel(audit) {
2147
+ if (!audit) return import_picocolors.default.dim("--");
2148
+ const count = audit.alerts ?? 0;
2149
+ return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
2150
+ }
2151
+ /** Pad a string to a given visible width (ignoring ANSI escape codes). */
2152
+ function padEnd(str, width) {
2153
+ const visible = str.replace(/\x1b\[[0-9;]*m/g, "");
2154
+ const pad = Math.max(0, width - visible.length);
2155
+ return str + " ".repeat(pad);
2156
+ }
2157
+ /**
2158
+ * Render a compact security table showing partner audit results.
2159
+ * Returns the lines to display, or empty array if no data.
2160
+ */
2161
+ function buildSecurityLines(auditData, skills, source) {
2162
+ if (!auditData) return [];
2163
+ if (!skills.some((s) => {
2164
+ const data = auditData[s.slug];
2165
+ return data && Object.keys(data).length > 0;
2166
+ })) return [];
2167
+ const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
2168
+ const lines = [];
2169
+ const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Gen"), 18) + padEnd(import_picocolors.default.dim("Socket"), 18) + import_picocolors.default.dim("Snyk");
2170
+ lines.push(header);
2171
+ for (const skill of skills) {
2172
+ const data = auditData[skill.slug];
2173
+ const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
2174
+ const ath = data?.ath ? riskLabel(data.ath.risk) : import_picocolors.default.dim("--");
2175
+ const socket = data?.socket ? socketLabel(data.socket) : import_picocolors.default.dim("--");
2176
+ const snyk = data?.snyk ? riskLabel(data.snyk.risk) : import_picocolors.default.dim("--");
2177
+ lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(ath, 18) + padEnd(socket, 18) + snyk);
2178
+ }
2179
+ lines.push("");
2180
+ lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`your skills directory${source}`)}`);
2181
+ return lines;
2182
+ }
2183
+ /**
2184
+ * Shortens a path for display: replaces homedir with ~ and cwd with .
2185
+ * Handles both Unix and Windows path separators.
2186
+ */
2187
+ function shortenPath$2(fullPath, cwd) {
2188
+ const home = homedir();
2189
+ if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
2190
+ if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
2191
+ return fullPath;
2192
+ }
2193
+ /**
2194
+ * Formats a list of items, truncating if too many
2195
+ */
2196
+ function formatList$1(items, maxShow = 5) {
2197
+ if (items.length <= maxShow) return items.join(", ");
2198
+ const shown = items.slice(0, maxShow);
2199
+ const remaining = items.length - maxShow;
2200
+ return `${shown.join(", ")} +${remaining} more`;
2201
+ }
2202
+ /**
2203
+ * Splits agents into universal and non-universal (symlinked) groups.
2204
+ * Returns display names for each group.
2205
+ */
2206
+ function splitAgentsByType(agentTypes) {
2207
+ const universal = [];
2208
+ const symlinked = [];
2209
+ for (const a of agentTypes) if (isUniversalAgent(a)) universal.push(agents[a].displayName);
2210
+ else symlinked.push(agents[a].displayName);
2211
+ return {
2212
+ universal,
2213
+ symlinked
2214
+ };
2215
+ }
2216
+ /**
2217
+ * Builds summary lines showing universal vs symlinked agents
2218
+ */
2219
+ function buildAgentSummaryLines(targetAgents, installMode) {
2220
+ const lines = [];
2221
+ const { universal, symlinked } = splitAgentsByType(targetAgents);
2222
+ if (installMode === "symlink") {
2223
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
2224
+ if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
2225
+ } else {
2226
+ const allNames = targetAgents.map((a) => agents[a].displayName);
2227
+ lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
2228
+ }
2229
+ return lines;
2230
+ }
2231
+ /**
2232
+ * Ensures universal agents are always included in the target agents list.
2233
+ * Used when -y flag is passed or when auto-selecting agents.
2234
+ */
2235
+ function ensureUniversalAgents(targetAgents) {
2236
+ const universalAgents = getUniversalAgents();
2237
+ const result = [...targetAgents];
2238
+ for (const ua of universalAgents) if (!result.includes(ua)) result.push(ua);
2239
+ return result;
2240
+ }
2241
+ /**
2242
+ * Builds result lines from installation results, splitting by universal vs symlinked
2243
+ */
2244
+ function buildResultLines(results, targetAgents) {
2245
+ const lines = [];
2246
+ const { universal, symlinked: symlinkAgents } = splitAgentsByType(targetAgents);
2247
+ const successfulSymlinks = results.filter((r) => !r.symlinkFailed && !universal.includes(r.agent)).map((r) => r.agent);
2248
+ const failedSymlinks = results.filter((r) => r.symlinkFailed).map((r) => r.agent);
2249
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
2250
+ if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList$1(successfulSymlinks)}`);
2251
+ if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList$1(failedSymlinks)}`);
2252
+ return lines;
2253
+ }
2254
+ /**
2255
+ * Wrapper around p.multiselect that adds a hint for keyboard usage.
2256
+ * Accepts options with required labels (matching our usage pattern).
2257
+ */
2258
+ function multiselect(opts) {
2259
+ return fe({
2260
+ ...opts,
2261
+ options: opts.options,
2262
+ message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}`
2263
+ });
2264
+ }
2265
+ /**
2266
+ * Prompts the user to select agents using interactive search.
2267
+ * Pre-selects the last used agents if available.
2268
+ * Saves the selection for future use.
2269
+ */
2270
+ async function promptForAgents(message, choices) {
2271
+ let lastSelected;
2272
+ try {
2273
+ lastSelected = await getLastSelectedAgents();
2274
+ } catch {}
2275
+ const validAgents = choices.map((c) => c.value);
2276
+ const defaultValues = [
2277
+ "claude-code",
2278
+ "opencode",
2279
+ "codex"
2280
+ ].filter((a) => validAgents.includes(a));
2281
+ let initialValues = [];
2282
+ if (lastSelected && lastSelected.length > 0) initialValues = lastSelected.filter((a) => validAgents.includes(a));
2283
+ if (initialValues.length === 0) initialValues = defaultValues;
2284
+ const selected = await searchMultiselect({
2285
+ message,
2286
+ items: choices,
2287
+ initialSelected: initialValues,
2288
+ required: true
2289
+ });
2290
+ if (!isCancelled$1(selected)) try {
2291
+ await saveSelectedAgents(selected);
2292
+ } catch {}
2293
+ return selected;
2294
+ }
2295
+ /**
2296
+ * Interactive agent selection using fuzzy search.
2297
+ * Shows universal agents as locked (always selected), and other agents as selectable.
2298
+ */
2299
+ async function selectAgentsInteractive(options) {
2300
+ const supportsGlobalFilter = (a) => !options.global || agents[a].globalSkillsDir;
2301
+ const universalAgents = getUniversalAgents().filter(supportsGlobalFilter);
2302
+ const otherAgents = getNonUniversalAgents().filter(supportsGlobalFilter);
2303
+ const universalSection = {
2304
+ title: "Universal (.agents/skills)",
2305
+ items: universalAgents.map((a) => ({
2306
+ value: a,
2307
+ label: agents[a].displayName
2308
+ }))
2309
+ };
2310
+ const otherChoices = otherAgents.map((a) => ({
2311
+ value: a,
2312
+ label: agents[a].displayName,
2313
+ hint: options.global ? agents[a].globalSkillsDir : agents[a].skillsDir
2314
+ }));
2315
+ let lastSelected;
2316
+ try {
2317
+ lastSelected = await getLastSelectedAgents();
2318
+ } catch {}
2319
+ const selected = await searchMultiselect({
2320
+ message: "Which agents do you want to install to?",
2321
+ items: otherChoices,
2322
+ initialSelected: lastSelected ? lastSelected.filter((a) => otherAgents.includes(a) && !universalAgents.includes(a)) : [],
2323
+ lockedSection: universalSection
2324
+ });
2325
+ if (!isCancelled$1(selected)) try {
2326
+ await saveSelectedAgents(selected);
2327
+ } catch {}
2328
+ return selected;
2329
+ }
2330
+ setVersion(version$1);
2331
+ /**
2332
+ * Handle skills from a well-known endpoint (RFC 8615).
2333
+ * Discovers skills from /.well-known/skills/index.json
2334
+ */
2335
+ async function handleWellKnownSkills(source, url, options, spinner) {
2336
+ spinner.start("Discovering skills from well-known endpoint...");
2337
+ const skills = await wellKnownProvider.fetchAllSkills(url);
2338
+ if (skills.length === 0) {
2339
+ spinner.stop(import_picocolors.default.red("No skills found"));
2340
+ Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/skills/index.json file."));
2341
+ process.exit(1);
2342
+ }
2343
+ spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
2344
+ for (const skill of skills) {
2345
+ M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
2346
+ M.message(import_picocolors.default.dim(skill.description));
2347
+ if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
2348
+ }
2349
+ if (options.list) {
2350
+ console.log();
2351
+ M.step(import_picocolors.default.bold("Available Skills"));
2352
+ for (const skill of skills) {
2353
+ M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
2354
+ M.message(` ${import_picocolors.default.dim(skill.description)}`);
2355
+ if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
2356
+ }
2357
+ console.log();
2358
+ Se("Run without --list to install");
2359
+ process.exit(0);
2360
+ }
2361
+ let selectedSkills;
2362
+ if (options.skill?.includes("*")) {
2363
+ selectedSkills = skills;
2364
+ M.info(`Installing all ${skills.length} skills`);
2365
+ } else if (options.skill && options.skill.length > 0) {
2366
+ selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
2367
+ if (selectedSkills.length === 0) {
2368
+ M.error(`No matching skills found for: ${options.skill.join(", ")}`);
2369
+ M.info("Available skills:");
2370
+ for (const s of skills) M.message(` - ${s.installName}`);
2371
+ process.exit(1);
2372
+ }
2373
+ } else if (skills.length === 1) {
2374
+ selectedSkills = skills;
2375
+ const firstSkill = skills[0];
2376
+ M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
2377
+ } else if (options.yes) {
2378
+ selectedSkills = skills;
2379
+ M.info(`Installing all ${skills.length} skills`);
2380
+ } else {
2381
+ const selected = await multiselect({
2382
+ message: "Select skills to install",
2383
+ options: skills.map((s) => ({
2384
+ value: s,
2385
+ label: s.installName,
2386
+ hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
2387
+ })),
2388
+ required: true
2389
+ });
2390
+ if (pD(selected)) {
2391
+ xe("Installation cancelled");
2392
+ process.exit(0);
2393
+ }
2394
+ selectedSkills = selected;
2395
+ }
2396
+ let targetAgents;
2397
+ const validAgents = Object.keys(agents);
2398
+ if (options.agent?.includes("*")) {
2399
+ targetAgents = validAgents;
2400
+ M.info(`Installing to all ${targetAgents.length} agents`);
2401
+ } else if (options.agent && options.agent.length > 0) {
2402
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
2403
+ if (invalidAgents.length > 0) {
2404
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
2405
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
2406
+ process.exit(1);
2407
+ }
2408
+ targetAgents = options.agent;
2409
+ } else {
2410
+ spinner.start("Loading agents...");
2411
+ const installedAgents = await detectInstalledAgents();
2412
+ const totalAgents = Object.keys(agents).length;
2413
+ spinner.stop(`${totalAgents} agents`);
2414
+ if (installedAgents.length === 0) if (options.yes) {
2415
+ targetAgents = validAgents;
2416
+ M.info("Installing to all agents");
2417
+ } else {
2418
+ M.info("Select agents to install skills to");
2419
+ const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
2420
+ value: key,
2421
+ label: config.displayName
2422
+ })));
2423
+ if (pD(selected)) {
2424
+ xe("Installation cancelled");
2425
+ process.exit(0);
2426
+ }
2427
+ targetAgents = selected;
2428
+ }
2429
+ else if (installedAgents.length === 1 || options.yes) {
2430
+ targetAgents = ensureUniversalAgents(installedAgents);
2431
+ if (installedAgents.length === 1) {
2432
+ const firstAgent = installedAgents[0];
2433
+ M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
2434
+ } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
2435
+ } else {
2436
+ const selected = await selectAgentsInteractive({ global: options.global });
2437
+ if (pD(selected)) {
2438
+ xe("Installation cancelled");
2439
+ process.exit(0);
2440
+ }
2441
+ targetAgents = selected;
2442
+ }
2443
+ }
2444
+ let installGlobally = options.global ?? false;
2445
+ const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
2446
+ if (options.global === void 0 && !options.yes && supportsGlobal) {
2447
+ const scope = await ve({
2448
+ message: "Installation scope",
2449
+ options: [{
2450
+ value: false,
2451
+ label: "Project",
2452
+ hint: "Install in current directory (committed with your project)"
2453
+ }, {
2454
+ value: true,
2455
+ label: "Global",
2456
+ hint: "Install in home directory (available across all projects)"
2457
+ }]
2458
+ });
2459
+ if (pD(scope)) {
2460
+ xe("Installation cancelled");
2461
+ process.exit(0);
2462
+ }
2463
+ installGlobally = scope;
2464
+ }
2465
+ let installMode = options.copy ? "copy" : "symlink";
2466
+ if (!options.copy && !options.yes) {
2467
+ const modeChoice = await ve({
2468
+ message: "Installation method",
2469
+ options: [{
2470
+ value: "symlink",
2471
+ label: "Symlink (Recommended)",
2472
+ hint: "Single source of truth, easy updates"
2473
+ }, {
2474
+ value: "copy",
2475
+ label: "Copy to all agents",
2476
+ hint: "Independent copies for each agent"
2477
+ }]
2478
+ });
2479
+ if (pD(modeChoice)) {
2480
+ xe("Installation cancelled");
2481
+ process.exit(0);
2482
+ }
2483
+ installMode = modeChoice;
2484
+ }
2485
+ const cwd = process.cwd();
2486
+ const summaryLines = [];
2487
+ targetAgents.map((a) => agents[a].displayName);
2488
+ const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
2489
+ skillName: skill.installName,
2490
+ agent,
2491
+ installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
2492
+ }))));
2493
+ const overwriteStatus = /* @__PURE__ */ new Map();
2494
+ for (const { skillName, agent, installed } of overwriteChecks) {
2495
+ if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
2496
+ overwriteStatus.get(skillName).set(agent, installed);
2497
+ }
2498
+ for (const skill of selectedSkills) {
2499
+ if (summaryLines.length > 0) summaryLines.push("");
2500
+ const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
2501
+ summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2502
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2503
+ if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
2504
+ const skillOverwrites = overwriteStatus.get(skill.installName);
2505
+ const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
2506
+ if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
2507
+ }
2508
+ console.log();
2509
+ Me(summaryLines.join("\n"), "Installation Summary");
2510
+ if (!options.yes) {
2511
+ const confirmed = await ye({ message: "Proceed with installation?" });
2512
+ if (pD(confirmed) || !confirmed) {
2513
+ xe("Installation cancelled");
2514
+ process.exit(0);
2515
+ }
2516
+ }
2517
+ spinner.start("Installing skills...");
2518
+ const results = [];
2519
+ for (const skill of selectedSkills) for (const agent of targetAgents) {
2520
+ const result = await installWellKnownSkillForAgent(skill, agent, {
2521
+ global: installGlobally,
2522
+ mode: installMode
2523
+ });
2524
+ results.push({
2525
+ skill: skill.installName,
2526
+ agent: agents[agent].displayName,
2527
+ ...result
2528
+ });
2529
+ }
2530
+ spinner.stop("Installation complete");
2531
+ console.log();
2532
+ const successful = results.filter((r) => r.success);
2533
+ const failed = results.filter((r) => !r.success);
2534
+ const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
2535
+ const skillFiles = {};
2536
+ for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
2537
+ if (await isSourcePrivate(sourceIdentifier) !== true) track({
2538
+ event: "install",
2539
+ source: sourceIdentifier,
2540
+ skills: selectedSkills.map((s) => s.installName).join(","),
2541
+ agents: targetAgents.join(","),
2542
+ ...installGlobally && { global: "1" },
2543
+ skillFiles: JSON.stringify(skillFiles),
2544
+ sourceType: "well-known"
2545
+ });
2546
+ if (successful.length > 0 && installGlobally) {
2547
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
2548
+ for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
2549
+ await addSkillToLock(skill.installName, {
2550
+ source: sourceIdentifier,
2551
+ sourceType: "well-known",
2552
+ sourceUrl: skill.sourceUrl,
2553
+ skillFolderHash: ""
2554
+ });
2555
+ } catch {}
2556
+ }
2557
+ if (successful.length > 0 && !installGlobally) {
2558
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
2559
+ for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
2560
+ const matchingResult = successful.find((r) => r.skill === skill.installName);
2561
+ const installDir = matchingResult?.canonicalPath || matchingResult?.path;
2562
+ if (installDir) {
2563
+ const computedHash = await computeSkillFolderHash(installDir);
2564
+ await addSkillToLocalLock(skill.installName, {
2565
+ source: sourceIdentifier,
2566
+ sourceType: "well-known",
2567
+ computedHash
2568
+ }, cwd);
2569
+ }
2570
+ } catch {}
2571
+ }
2572
+ if (successful.length > 0) {
2573
+ const bySkill = /* @__PURE__ */ new Map();
2574
+ for (const r of successful) {
2575
+ const skillResults = bySkill.get(r.skill) || [];
2576
+ skillResults.push(r);
2577
+ bySkill.set(r.skill, skillResults);
2578
+ }
2579
+ const skillCount = bySkill.size;
2580
+ const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
2581
+ const copiedAgents = symlinkFailures.map((r) => r.agent);
2582
+ const resultLines = [];
2583
+ for (const [skillName, skillResults] of bySkill) {
2584
+ const firstResult = skillResults[0];
2585
+ if (firstResult.mode === "copy") {
2586
+ resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
2587
+ for (const r of skillResults) {
2588
+ const shortPath = shortenPath$2(r.path, cwd);
2589
+ resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
2590
+ }
2591
+ } else {
2592
+ if (firstResult.canonicalPath) {
2593
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
2594
+ resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
2595
+ } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
2596
+ resultLines.push(...buildResultLines(skillResults, targetAgents));
2597
+ }
2598
+ }
2599
+ const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
2600
+ Me(resultLines.join("\n"), title);
2601
+ if (symlinkFailures.length > 0) {
2602
+ M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
2603
+ M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
2604
+ }
2605
+ }
2606
+ if (failed.length > 0) {
2607
+ console.log();
2608
+ M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
2609
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
2610
+ }
2611
+ console.log();
2612
+ Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
2613
+ await promptForFindSkills(options, targetAgents);
2614
+ }
2615
+ async function runAdd(args, options = {}) {
2616
+ const source = args[0];
2617
+ let installTipShown = false;
2618
+ const showInstallTip = () => {
2619
+ if (installTipShown) return;
2620
+ M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
2621
+ installTipShown = true;
2622
+ };
2623
+ if (!source) {
2624
+ console.log();
2625
+ console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
2626
+ console.log();
2627
+ console.log(import_picocolors.default.dim(" Usage:"));
2628
+ console.log(` ${import_picocolors.default.cyan("npx @voidagency/skills add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
2629
+ console.log();
2630
+ console.log(import_picocolors.default.dim(" Example:"));
2631
+ console.log(` ${import_picocolors.default.cyan("npx @voidagency/skills add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
2632
+ console.log();
2633
+ process.exit(1);
2634
+ }
2635
+ if (options.all) {
2636
+ options.skill = ["*"];
2637
+ options.agent = ["*"];
2638
+ options.yes = true;
2639
+ }
2640
+ console.log();
2641
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
2642
+ if (!process.stdin.isTTY) showInstallTip();
2643
+ let tempDir = null;
2644
+ try {
2645
+ const spinner = Y();
2646
+ spinner.start("Parsing source...");
2647
+ const parsed = parseSource(source);
2648
+ spinner.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
2649
+ if (parsed.type === "well-known") {
2650
+ await handleWellKnownSkills(source, parsed.url, options, spinner);
2651
+ return;
2652
+ }
2653
+ let skillsDir;
2654
+ if (parsed.type === "local") {
2655
+ spinner.start("Validating local path...");
2656
+ if (!existsSync(parsed.localPath)) {
2657
+ spinner.stop(import_picocolors.default.red("Path not found"));
2658
+ Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
2659
+ process.exit(1);
2660
+ }
2661
+ skillsDir = parsed.localPath;
2662
+ spinner.stop("Local path validated");
2663
+ } else {
2664
+ spinner.start("Cloning repository...");
2665
+ tempDir = await cloneRepo(parsed.url, parsed.ref);
2666
+ skillsDir = tempDir;
2667
+ spinner.stop("Repository cloned");
2668
+ }
2669
+ if (parsed.skillFilter) {
2670
+ options.skill = options.skill || [];
2671
+ if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
2672
+ }
2673
+ const includeInternal = !!(options.skill && options.skill.length > 0);
2674
+ spinner.start("Discovering skills...");
2675
+ const skills = await discoverSkills(skillsDir, parsed.subpath, {
2676
+ includeInternal,
2677
+ fullDepth: options.fullDepth
2678
+ });
2679
+ if (skills.length === 0) {
2680
+ spinner.stop(import_picocolors.default.red("No skills found"));
2681
+ Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
2682
+ await cleanup(tempDir);
2683
+ process.exit(1);
2684
+ }
2685
+ spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
2686
+ if (options.list) {
2687
+ console.log();
2688
+ M.step(import_picocolors.default.bold("Available Skills"));
2689
+ const groupedSkills = {};
2690
+ const ungroupedSkills = [];
2691
+ for (const skill of skills) if (skill.pluginName) {
2692
+ const group = skill.pluginName;
2693
+ if (!groupedSkills[group]) groupedSkills[group] = [];
2694
+ groupedSkills[group].push(skill);
2695
+ } else ungroupedSkills.push(skill);
2696
+ const sortedGroups = Object.keys(groupedSkills).sort();
2697
+ for (const group of sortedGroups) {
2698
+ const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2699
+ console.log(import_picocolors.default.bold(title));
2700
+ for (const skill of groupedSkills[group]) {
2701
+ M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
2702
+ M.message(` ${import_picocolors.default.dim(skill.description)}`);
2703
+ }
2704
+ console.log();
2705
+ }
2706
+ if (ungroupedSkills.length > 0) {
2707
+ if (sortedGroups.length > 0) console.log(import_picocolors.default.bold("General"));
2708
+ for (const skill of ungroupedSkills) {
2709
+ M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
2710
+ M.message(` ${import_picocolors.default.dim(skill.description)}`);
2711
+ }
2712
+ }
2713
+ console.log();
2714
+ Se("Use --skill <name> to install specific skills");
2715
+ await cleanup(tempDir);
2716
+ process.exit(0);
2717
+ }
2718
+ let selectedSkills;
2719
+ if (options.skill?.includes("*")) {
2720
+ selectedSkills = skills;
2721
+ M.info(`Installing all ${skills.length} skills`);
2722
+ } else if (options.skill && options.skill.length > 0) {
2723
+ selectedSkills = filterSkills(skills, options.skill);
2724
+ if (selectedSkills.length === 0) {
2725
+ M.error(`No matching skills found for: ${options.skill.join(", ")}`);
2726
+ M.info("Available skills:");
2727
+ for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`);
2728
+ await cleanup(tempDir);
2729
+ process.exit(1);
2730
+ }
2731
+ M.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
2732
+ } else if (skills.length === 1) {
2733
+ selectedSkills = skills;
2734
+ const firstSkill = skills[0];
2735
+ M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(firstSkill))}`);
2736
+ M.message(import_picocolors.default.dim(firstSkill.description));
2737
+ } else if (options.yes) {
2738
+ selectedSkills = skills;
2739
+ M.info(`Installing all ${skills.length} skills`);
2740
+ } else {
2741
+ const sortedSkills = [...skills].sort((a, b) => {
2742
+ if (a.pluginName && !b.pluginName) return -1;
2743
+ if (!a.pluginName && b.pluginName) return 1;
2744
+ if (a.pluginName && b.pluginName && a.pluginName !== b.pluginName) return a.pluginName.localeCompare(b.pluginName);
2745
+ return getSkillDisplayName(a).localeCompare(getSkillDisplayName(b));
2746
+ });
2747
+ const hasGroups = sortedSkills.some((s) => s.pluginName);
2748
+ let selected;
2749
+ if (hasGroups) {
2750
+ const kebabToTitle = (s) => s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2751
+ const grouped = {};
2752
+ for (const s of sortedSkills) {
2753
+ const groupName = s.pluginName ? kebabToTitle(s.pluginName) : "Other";
2754
+ if (!grouped[groupName]) grouped[groupName] = [];
2755
+ grouped[groupName].push({
2756
+ value: s,
2757
+ label: getSkillDisplayName(s),
2758
+ hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
2759
+ });
2760
+ }
2761
+ selected = await be({
2762
+ message: `Select skills to install ${import_picocolors.default.dim("(space to toggle)")}`,
2763
+ options: grouped,
2764
+ required: true
2765
+ });
2766
+ } else selected = await multiselect({
2767
+ message: "Select skills to install",
2768
+ options: sortedSkills.map((s) => ({
2769
+ value: s,
2770
+ label: getSkillDisplayName(s),
2771
+ hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
2772
+ })),
2773
+ required: true
2774
+ });
2775
+ if (pD(selected)) {
2776
+ xe("Installation cancelled");
2777
+ await cleanup(tempDir);
2778
+ process.exit(0);
2779
+ }
2780
+ selectedSkills = selected;
2781
+ }
2782
+ const ownerRepoForAudit = getOwnerRepo(parsed);
2783
+ const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => getSkillDisplayName(s))) : Promise.resolve(null);
2784
+ let targetAgents;
2785
+ const validAgents = Object.keys(agents);
2786
+ if (options.agent?.includes("*")) {
2787
+ targetAgents = validAgents;
2788
+ M.info(`Installing to all ${targetAgents.length} agents`);
2789
+ } else if (options.agent && options.agent.length > 0) {
2790
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
2791
+ if (invalidAgents.length > 0) {
2792
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
2793
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
2794
+ await cleanup(tempDir);
2795
+ process.exit(1);
2796
+ }
2797
+ targetAgents = options.agent;
2798
+ } else {
2799
+ spinner.start("Loading agents...");
2800
+ const installedAgents = await detectInstalledAgents();
2801
+ const totalAgents = Object.keys(agents).length;
2802
+ spinner.stop(`${totalAgents} agents`);
2803
+ if (installedAgents.length === 0) if (options.yes) {
2804
+ targetAgents = validAgents;
2805
+ M.info("Installing to all agents");
2806
+ } else {
2807
+ M.info("Select agents to install skills to");
2808
+ const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
2809
+ value: key,
2810
+ label: config.displayName
2811
+ })));
2812
+ if (pD(selected)) {
2813
+ xe("Installation cancelled");
2814
+ await cleanup(tempDir);
2815
+ process.exit(0);
2816
+ }
2817
+ targetAgents = selected;
2818
+ }
2819
+ else if (installedAgents.length === 1 || options.yes) {
2820
+ targetAgents = ensureUniversalAgents(installedAgents);
2821
+ if (installedAgents.length === 1) {
2822
+ const firstAgent = installedAgents[0];
2823
+ M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
2824
+ } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
2825
+ } else {
2826
+ const selected = await selectAgentsInteractive({ global: options.global });
2827
+ if (pD(selected)) {
2828
+ xe("Installation cancelled");
2829
+ await cleanup(tempDir);
2830
+ process.exit(0);
2831
+ }
2832
+ targetAgents = selected;
2833
+ }
2834
+ }
2835
+ let installGlobally = options.global ?? false;
2836
+ const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
2837
+ if (options.global === void 0 && !options.yes && supportsGlobal) {
2838
+ const scope = await ve({
2839
+ message: "Installation scope",
2840
+ options: [{
2841
+ value: false,
2842
+ label: "Project",
2843
+ hint: "Install in current directory (committed with your project)"
2844
+ }, {
2845
+ value: true,
2846
+ label: "Global",
2847
+ hint: "Install in home directory (available across all projects)"
2848
+ }]
2849
+ });
2850
+ if (pD(scope)) {
2851
+ xe("Installation cancelled");
2852
+ await cleanup(tempDir);
2853
+ process.exit(0);
2854
+ }
2855
+ installGlobally = scope;
2856
+ }
2857
+ let installMode = options.copy ? "copy" : "symlink";
2858
+ if (!options.copy && !options.yes) {
2859
+ const modeChoice = await ve({
2860
+ message: "Installation method",
2861
+ options: [{
2862
+ value: "symlink",
2863
+ label: "Symlink (Recommended)",
2864
+ hint: "Single source of truth, easy updates"
2865
+ }, {
2866
+ value: "copy",
2867
+ label: "Copy to all agents",
2868
+ hint: "Independent copies for each agent"
2869
+ }]
2870
+ });
2871
+ if (pD(modeChoice)) {
2872
+ xe("Installation cancelled");
2873
+ await cleanup(tempDir);
2874
+ process.exit(0);
2875
+ }
2876
+ installMode = modeChoice;
2877
+ }
2878
+ const cwd = process.cwd();
2879
+ const summaryLines = [];
2880
+ targetAgents.map((a) => agents[a].displayName);
2881
+ const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
2882
+ skillName: skill.name,
2883
+ agent,
2884
+ installed: await isSkillInstalled(skill.name, agent, { global: installGlobally })
2885
+ }))));
2886
+ const overwriteStatus = /* @__PURE__ */ new Map();
2887
+ for (const { skillName, agent, installed } of overwriteChecks) {
2888
+ if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
2889
+ overwriteStatus.get(skillName).set(agent, installed);
2890
+ }
2891
+ const groupedSummary = {};
2892
+ const ungroupedSummary = [];
2893
+ for (const skill of selectedSkills) if (skill.pluginName) {
2894
+ const group = skill.pluginName;
2895
+ if (!groupedSummary[group]) groupedSummary[group] = [];
2896
+ groupedSummary[group].push(skill);
2897
+ } else ungroupedSummary.push(skill);
2898
+ const printSkillSummary = (skills) => {
2899
+ for (const skill of skills) {
2900
+ if (summaryLines.length > 0) summaryLines.push("");
2901
+ const shortCanonical = shortenPath$2(getCanonicalPath(skill.name, { global: installGlobally }), cwd);
2902
+ summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2903
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2904
+ const skillOverwrites = overwriteStatus.get(skill.name);
2905
+ const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
2906
+ if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
2907
+ }
2908
+ };
2909
+ const sortedGroups = Object.keys(groupedSummary).sort();
2910
+ for (const group of sortedGroups) {
2911
+ const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2912
+ summaryLines.push("");
2913
+ summaryLines.push(import_picocolors.default.bold(title));
2914
+ printSkillSummary(groupedSummary[group]);
2915
+ }
2916
+ if (ungroupedSummary.length > 0) {
2917
+ if (sortedGroups.length > 0) {
2918
+ summaryLines.push("");
2919
+ summaryLines.push(import_picocolors.default.bold("General"));
2920
+ }
2921
+ printSkillSummary(ungroupedSummary);
2922
+ }
2923
+ console.log();
2924
+ Me(summaryLines.join("\n"), "Installation Summary");
2925
+ try {
2926
+ const auditData = await auditPromise;
2927
+ if (auditData && ownerRepoForAudit) {
2928
+ const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({
2929
+ slug: getSkillDisplayName(s),
2930
+ displayName: getSkillDisplayName(s)
2931
+ })), ownerRepoForAudit);
2932
+ if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
2933
+ }
2934
+ } catch {}
2935
+ if (!options.yes) {
2936
+ const confirmed = await ye({ message: "Proceed with installation?" });
2937
+ if (pD(confirmed) || !confirmed) {
2938
+ xe("Installation cancelled");
2939
+ await cleanup(tempDir);
2940
+ process.exit(0);
2941
+ }
2942
+ }
2943
+ spinner.start("Installing skills...");
2944
+ const results = [];
2945
+ for (const skill of selectedSkills) for (const agent of targetAgents) {
2946
+ const result = await installSkillForAgent(skill, agent, {
2947
+ global: installGlobally,
2948
+ mode: installMode
2949
+ });
2950
+ results.push({
2951
+ skill: getSkillDisplayName(skill),
2952
+ agent: agents[agent].displayName,
2953
+ pluginName: skill.pluginName,
2954
+ ...result
2955
+ });
2956
+ }
2957
+ spinner.stop("Installation complete");
2958
+ console.log();
2959
+ const successful = results.filter((r) => r.success);
2960
+ const failed = results.filter((r) => !r.success);
2961
+ const skillFiles = {};
2962
+ for (const skill of selectedSkills) {
2963
+ let relativePath;
2964
+ if (tempDir && skill.path === tempDir) relativePath = "SKILL.md";
2965
+ else if (tempDir && skill.path.startsWith(tempDir + sep)) relativePath = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
2966
+ else continue;
2967
+ skillFiles[skill.name] = relativePath;
2968
+ }
2969
+ const normalizedSource = getOwnerRepo(parsed);
2970
+ if (normalizedSource) {
2971
+ const ownerRepo = parseOwnerRepo(normalizedSource);
2972
+ if (ownerRepo) {
2973
+ if (await isRepoPrivate(ownerRepo.owner, ownerRepo.repo) === false) track({
2974
+ event: "install",
2975
+ source: normalizedSource,
2976
+ skills: selectedSkills.map((s) => s.name).join(","),
2977
+ agents: targetAgents.join(","),
2978
+ ...installGlobally && { global: "1" },
2979
+ skillFiles: JSON.stringify(skillFiles)
2980
+ });
2981
+ } else track({
2982
+ event: "install",
2983
+ source: normalizedSource,
2984
+ skills: selectedSkills.map((s) => s.name).join(","),
2985
+ agents: targetAgents.join(","),
2986
+ ...installGlobally && { global: "1" },
2987
+ skillFiles: JSON.stringify(skillFiles)
2988
+ });
2989
+ }
2990
+ if (successful.length > 0 && installGlobally && normalizedSource) {
2991
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
2992
+ for (const skill of selectedSkills) {
2993
+ const skillDisplayName = getSkillDisplayName(skill);
2994
+ if (successfulSkillNames.has(skillDisplayName)) try {
2995
+ let skillFolderHash = "";
2996
+ const skillPathValue = skillFiles[skill.name];
2997
+ if (parsed.type === "github" && skillPathValue) {
2998
+ const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken());
2999
+ if (hash) skillFolderHash = hash;
3000
+ }
3001
+ await addSkillToLock(skill.name, {
3002
+ source: normalizedSource,
3003
+ sourceType: parsed.type,
3004
+ sourceUrl: parsed.url,
3005
+ skillPath: skillPathValue,
3006
+ skillFolderHash,
3007
+ pluginName: skill.pluginName
3008
+ });
3009
+ } catch {}
3010
+ }
3011
+ }
3012
+ if (successful.length > 0 && !installGlobally) {
3013
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
3014
+ for (const skill of selectedSkills) {
3015
+ const skillDisplayName = getSkillDisplayName(skill);
3016
+ if (successfulSkillNames.has(skillDisplayName)) try {
3017
+ const computedHash = await computeSkillFolderHash(skill.path);
3018
+ await addSkillToLocalLock(skill.name, {
3019
+ source: normalizedSource || parsed.url,
3020
+ sourceType: parsed.type,
3021
+ computedHash
3022
+ }, cwd);
3023
+ } catch {}
3024
+ }
3025
+ }
3026
+ if (successful.length > 0) {
3027
+ const bySkill = /* @__PURE__ */ new Map();
3028
+ const groupedResults = {};
3029
+ const ungroupedResults = [];
3030
+ for (const r of successful) {
3031
+ const skillResults = bySkill.get(r.skill) || [];
3032
+ skillResults.push(r);
3033
+ bySkill.set(r.skill, skillResults);
3034
+ if (skillResults.length === 1) if (r.pluginName) {
3035
+ const group = r.pluginName;
3036
+ if (!groupedResults[group]) groupedResults[group] = [];
3037
+ groupedResults[group].push(r);
3038
+ } else ungroupedResults.push(r);
3039
+ }
3040
+ const skillCount = bySkill.size;
3041
+ const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
3042
+ const copiedAgents = symlinkFailures.map((r) => r.agent);
3043
+ const resultLines = [];
3044
+ const printSkillResults = (entries) => {
3045
+ for (const entry of entries) {
3046
+ const skillResults = bySkill.get(entry.skill) || [];
3047
+ const firstResult = skillResults[0];
3048
+ if (firstResult.mode === "copy") {
3049
+ resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill} ${import_picocolors.default.dim("(copied)")}`);
3050
+ for (const r of skillResults) {
3051
+ const shortPath = shortenPath$2(r.path, cwd);
3052
+ resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
3053
+ }
3054
+ } else {
3055
+ if (firstResult.canonicalPath) {
3056
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3057
+ resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3058
+ } else resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill}`);
3059
+ resultLines.push(...buildResultLines(skillResults, targetAgents));
3060
+ }
3061
+ }
3062
+ };
3063
+ const sortedResultGroups = Object.keys(groupedResults).sort();
3064
+ for (const group of sortedResultGroups) {
3065
+ const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3066
+ resultLines.push("");
3067
+ resultLines.push(import_picocolors.default.bold(title));
3068
+ printSkillResults(groupedResults[group]);
3069
+ }
3070
+ if (ungroupedResults.length > 0) {
3071
+ if (sortedResultGroups.length > 0) {
3072
+ resultLines.push("");
3073
+ resultLines.push(import_picocolors.default.bold("General"));
3074
+ }
3075
+ printSkillResults(ungroupedResults);
3076
+ }
3077
+ const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3078
+ Me(resultLines.join("\n"), title);
3079
+ if (symlinkFailures.length > 0) {
3080
+ M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
3081
+ M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
3082
+ }
3083
+ }
3084
+ if (failed.length > 0) {
3085
+ console.log();
3086
+ M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3087
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3088
+ }
3089
+ console.log();
3090
+ Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3091
+ await promptForFindSkills(options, targetAgents);
3092
+ } catch (error) {
3093
+ if (error instanceof GitCloneError) {
3094
+ M.error(import_picocolors.default.red("Failed to clone repository"));
3095
+ for (const line of error.message.split("\n")) M.message(import_picocolors.default.dim(line));
3096
+ } else M.error(error instanceof Error ? error.message : "Unknown error occurred");
3097
+ showInstallTip();
3098
+ Se(import_picocolors.default.red("Installation failed"));
3099
+ process.exit(1);
3100
+ } finally {
3101
+ await cleanup(tempDir);
3102
+ }
3103
+ }
3104
+ async function cleanup(tempDir) {
3105
+ if (tempDir) try {
3106
+ await cleanupTempDir(tempDir);
3107
+ } catch {}
3108
+ }
3109
+ /**
3110
+ * Prompt user to install the find-skills skill after their first installation.
3111
+ */
3112
+ async function promptForFindSkills(options, targetAgents) {
3113
+ if (!process.stdin.isTTY) return;
3114
+ if (options?.yes) return;
3115
+ try {
3116
+ if (await isPromptDismissed("findSkillsPrompt")) return;
3117
+ if (await isSkillInstalled("find-skills", "claude-code", { global: true })) {
3118
+ await dismissPrompt("findSkillsPrompt");
3119
+ return;
3120
+ }
3121
+ console.log();
3122
+ M.message(import_picocolors.default.dim("One-time prompt - you won't be asked again if you dismiss."));
3123
+ const install = await ye({ message: `Install the ${import_picocolors.default.cyan("find-skills")} skill? It helps your agent discover and suggest skills.` });
3124
+ if (pD(install)) {
3125
+ await dismissPrompt("findSkillsPrompt");
3126
+ return;
3127
+ }
3128
+ if (install) {
3129
+ await dismissPrompt("findSkillsPrompt");
3130
+ const findSkillsAgents = targetAgents?.filter((a) => a !== "replit");
3131
+ if (!findSkillsAgents || findSkillsAgents.length === 0) return;
3132
+ console.log();
3133
+ M.step("Installing find-skills skill...");
3134
+ try {
3135
+ await runAdd(["vercel-labs/skills"], {
3136
+ skill: ["find-skills"],
3137
+ global: true,
3138
+ yes: true,
3139
+ agent: findSkillsAgents
3140
+ });
3141
+ } catch {
3142
+ M.warn("Failed to install find-skills. You can try again with:");
3143
+ M.message(import_picocolors.default.dim(" npx @voidagency/skills add vercel-labs/skills@find-skills -g -y --all"));
3144
+ }
3145
+ } else {
3146
+ await dismissPrompt("findSkillsPrompt");
3147
+ M.message(import_picocolors.default.dim("You can install it later with: npx @voidagency/skills add vercel-labs/skills@find-skills"));
3148
+ }
3149
+ } catch {}
3150
+ }
3151
+ function parseAddOptions(args) {
3152
+ const options = {};
3153
+ const source = [];
3154
+ for (let i = 0; i < args.length; i++) {
3155
+ const arg = args[i];
3156
+ if (arg === "-g" || arg === "--global") options.global = true;
3157
+ else if (arg === "-y" || arg === "--yes") options.yes = true;
3158
+ else if (arg === "-l" || arg === "--list") options.list = true;
3159
+ else if (arg === "--all") options.all = true;
3160
+ else if (arg === "-a" || arg === "--agent") {
3161
+ options.agent = options.agent || [];
3162
+ i++;
3163
+ let nextArg = args[i];
3164
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
3165
+ options.agent.push(nextArg);
3166
+ i++;
3167
+ nextArg = args[i];
3168
+ }
3169
+ i--;
3170
+ } else if (arg === "-s" || arg === "--skill") {
3171
+ options.skill = options.skill || [];
3172
+ i++;
3173
+ let nextArg = args[i];
3174
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
3175
+ options.skill.push(nextArg);
3176
+ i++;
3177
+ nextArg = args[i];
3178
+ }
3179
+ i--;
3180
+ } else if (arg === "--full-depth") options.fullDepth = true;
3181
+ else if (arg === "--copy") options.copy = true;
3182
+ else if (arg && !arg.startsWith("-")) source.push(arg);
3183
+ }
3184
+ return {
3185
+ source,
3186
+ options
3187
+ };
3188
+ }
3189
+ //#endregion
3190
+ //#region src/agent-install.ts
3191
+ /**
3192
+ * Install an agent definition (AGENTS.md + manifest) and its dependent skills.
3193
+ * Agent repos should contain agent.json: { "skills": ["owner/repo", "owner/repo@skill", ...] }
3194
+ */
3195
+ const AGENT_DEFINITIONS_DIR = "agent-definitions";
3196
+ function getAgentDefinitionsBase() {
3197
+ return join(homedir(), AGENTS_DIR$2, AGENT_DEFINITIONS_DIR);
3198
+ }
3199
+ /**
3200
+ * Sanitize a source string into a safe directory name (e.g. "owner/repo" -> "owner-repo").
3201
+ */
3202
+ function slugFromSource(source) {
3203
+ return source.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "agent";
3204
+ }
3205
+ /**
3206
+ * Parse "source" or "source@agent-name" for add-agent.
3207
+ * Returns { source, agentName } where agentName is undefined when repo root is the agent.
3208
+ */
3209
+ function parseAgentSource(input) {
3210
+ const at = input.lastIndexOf("@");
3211
+ if (at < 0) return { source: input.trim() };
3212
+ const source = input.slice(0, at).trim();
3213
+ const agentName = input.slice(at + 1).trim();
3214
+ if (!agentName) return { source };
3215
+ return {
3216
+ source,
3217
+ agentName
3218
+ };
3219
+ }
3220
+ /**
3221
+ * Load agent.json from a directory. Looks for agent.json in the given dir.
3222
+ */
3223
+ async function loadAgentManifest(dir) {
3224
+ const path = join(dir, "agent.json");
3225
+ try {
3226
+ const raw = await readFile(path, "utf-8");
3227
+ const data = JSON.parse(raw);
3228
+ if (!data || typeof data !== "object") return null;
3229
+ return data;
3230
+ } catch {
3231
+ return null;
3232
+ }
3233
+ }
3234
+ /**
3235
+ * Install an agent definition from a source (GitHub/Bitbucket URL or owner/repo).
3236
+ * Optional agent name: use source@agent-name to install from agents/<agent-name>/ in the repo.
3237
+ * Clones the repo, reads agent.json (from root or agents/<agent-name>/), installs each listed skill, then copies AGENTS.md and agent.json to ~/.agents/agent-definitions/<slug>.
3238
+ */
3239
+ async function runAddAgent(agentSource, options = {}) {
3240
+ const { source, agentName } = parseAgentSource(agentSource);
3241
+ const parsed = parseSource(source);
3242
+ let repoDir;
3243
+ if (parsed.type === "local") {
3244
+ repoDir = resolve(process.cwd(), parsed.localPath);
3245
+ if (!existsSync(repoDir)) {
3246
+ M.error(`Local path does not exist: ${repoDir}`);
3247
+ process.exit(1);
3248
+ }
3249
+ }
3250
+ let tempDir = null;
3251
+ try {
3252
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" add-agent ")));
3253
+ const spinner = Y();
3254
+ if (parsed.type === "local") {
3255
+ spinner.start("Using local path...");
3256
+ spinner.stop("Using local path.");
3257
+ } else {
3258
+ spinner.start("Cloning agent repository...");
3259
+ tempDir = await cloneRepo(parsed.url, parsed.ref);
3260
+ spinner.stop("Repository cloned.");
3261
+ repoDir = tempDir;
3262
+ }
3263
+ const agentDirInRepo = agentName ? join(repoDir, "agents", agentName) : repoDir;
3264
+ const manifest = await loadAgentManifest(agentDirInRepo);
3265
+ if (!manifest || typeof manifest !== "object") {
3266
+ if (agentName) M.error(`No agent found at agents/${agentName}/. Expected agents/${agentName}/AGENTS.md and agents/${agentName}/agent.json.`);
3267
+ else M.error("No agent.json found in the repository.");
3268
+ process.exit(1);
3269
+ }
3270
+ const baseSlug = parsed.type === "local" ? slugFromSource(repoDir) : getOwnerRepo(parsed) || slugFromSource(source);
3271
+ const slug = agentName ? `${baseSlug}-${slugFromSource(agentName)}` : baseSlug;
3272
+ const skillsList = Array.isArray(manifest.skills) ? manifest.skills : [];
3273
+ if (skillsList.length > 0) M.info(`Installing ${skillsList.length} skill(s) from agent manifest...`);
3274
+ for (const skillSource of skillsList) {
3275
+ const trimmed = String(skillSource).trim();
3276
+ if (!trimmed) continue;
3277
+ spinner.start(`Installing skill: ${trimmed}`);
3278
+ try {
3279
+ await runAdd([trimmed], options);
3280
+ spinner.stop(`Installed: ${trimmed}`);
3281
+ } catch (err) {
3282
+ spinner.stop(`Failed: ${trimmed}`);
3283
+ M.warn(`Could not install skill ${trimmed}: ${err instanceof Error ? err.message : err}`);
3284
+ }
3285
+ }
3286
+ const destDir = join(getAgentDefinitionsBase(), slug);
3287
+ await mkdir(destDir, { recursive: true });
3288
+ const entries = await readdir(agentDirInRepo, { withFileTypes: true });
3289
+ for (const ent of entries) await cp(join(agentDirInRepo, ent.name), join(destDir, ent.name), { recursive: ent.isDirectory() });
3290
+ M.success(`Agent definition saved to ${destDir}`);
3291
+ } finally {
3292
+ if (tempDir) await cleanupTempDir(tempDir).catch(() => {});
3293
+ }
3294
+ }
3295
+ /**
3296
+ * Parse add-agent options from argv. Reuses add options (global, agent, yes).
3297
+ */
3298
+ function parseAddAgentOptions(args) {
3299
+ const options = {};
3300
+ let source = "";
3301
+ for (let i = 0; i < args.length; i++) {
3302
+ const arg = args[i];
3303
+ if (arg === "-g" || arg === "--global") options.global = true;
3304
+ else if (arg === "-y" || arg === "--yes") options.yes = true;
3305
+ else if (arg === "-a" || arg === "--agent") {
3306
+ options.agent = options.agent || [];
3307
+ i++;
3308
+ let nextArg = args[i];
3309
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
3310
+ options.agent.push(nextArg);
3311
+ i++;
3312
+ nextArg = args[i];
3313
+ }
3314
+ i--;
3315
+ } else if (arg && !arg.startsWith("-")) source = arg;
3316
+ }
3317
+ return {
3318
+ source,
3319
+ options
3320
+ };
3321
+ }
3322
+ //#endregion
3323
+ //#region src/find.ts
3324
+ const RESET$2 = "\x1B[0m";
3325
+ const BOLD$2 = "\x1B[1m";
3326
+ const DIM$2 = "\x1B[38;5;102m";
3327
+ const TEXT$1 = "\x1B[38;5;145m";
3328
+ const CYAN$1 = "\x1B[36m";
3329
+ const SEARCH_API_BASE = process.env.SKILLS_API_URL || "";
3330
+ function formatInstalls(count) {
3331
+ if (!count || count <= 0) return "";
3332
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
3333
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
3334
+ return `${count} install${count === 1 ? "" : "s"}`;
3335
+ }
3336
+ async function searchSkillsAPI(query) {
3337
+ if (!SEARCH_API_BASE) return [];
3338
+ try {
3339
+ const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`;
3340
+ const res = await fetch(url);
3341
+ if (!res.ok) return [];
3342
+ return (await res.json()).skills.map((skill) => ({
3343
+ name: skill.name,
3344
+ slug: skill.id,
3345
+ source: skill.source || "",
3346
+ installs: skill.installs
3347
+ }));
3348
+ } catch {
3349
+ return [];
3350
+ }
3351
+ }
3352
+ const HIDE_CURSOR = "\x1B[?25l";
3353
+ const SHOW_CURSOR = "\x1B[?25h";
3354
+ const CLEAR_DOWN = "\x1B[J";
3355
+ const MOVE_UP = (n) => `\x1b[${n}A`;
3356
+ const MOVE_TO_COL = (n) => `\x1b[${n}G`;
3357
+ async function runSearchPrompt(initialQuery = "") {
3358
+ let results = [];
3359
+ let selectedIndex = 0;
3360
+ let query = initialQuery;
3361
+ let loading = false;
3362
+ let debounceTimer = null;
3363
+ let lastRenderedLines = 0;
3364
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
3365
+ readline.emitKeypressEvents(process.stdin);
3366
+ process.stdin.resume();
3367
+ process.stdout.write(HIDE_CURSOR);
3368
+ function render() {
3369
+ if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
3370
+ process.stdout.write(CLEAR_DOWN);
3371
+ const lines = [];
3372
+ const cursor = `${BOLD$2}_${RESET$2}`;
3373
+ lines.push(`${TEXT$1}Search skills:${RESET$2} ${query}${cursor}`);
3374
+ lines.push("");
3375
+ if (!query || query.length < 2) lines.push(`${DIM$2}Start typing to search (min 2 chars)${RESET$2}`);
3376
+ else if (results.length === 0 && loading) lines.push(`${DIM$2}Searching...${RESET$2}`);
3377
+ else if (results.length === 0) lines.push(`${DIM$2}No skills found${RESET$2}`);
3378
+ else {
3379
+ const visible = results.slice(0, 8);
3380
+ for (let i = 0; i < visible.length; i++) {
3381
+ const skill = visible[i];
3382
+ const isSelected = i === selectedIndex;
3383
+ const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
3384
+ const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
3385
+ const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : "";
3386
+ const installs = formatInstalls(skill.installs);
3387
+ const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : "";
3388
+ const loadingIndicator = loading && i === 0 ? ` ${DIM$2}...${RESET$2}` : "";
3389
+ lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
3390
+ }
3391
+ }
3392
+ lines.push("");
3393
+ lines.push(`${DIM$2}up/down navigate | enter select | esc cancel${RESET$2}`);
3394
+ for (const line of lines) process.stdout.write(line + "\n");
3395
+ lastRenderedLines = lines.length;
3396
+ }
3397
+ function triggerSearch(q) {
3398
+ if (debounceTimer) {
3399
+ clearTimeout(debounceTimer);
3400
+ debounceTimer = null;
3401
+ }
3402
+ loading = false;
3403
+ if (!q || q.length < 2) {
3404
+ results = [];
3405
+ selectedIndex = 0;
3406
+ render();
3407
+ return;
3408
+ }
3409
+ loading = true;
3410
+ render();
3411
+ const debounceMs = Math.max(150, 350 - q.length * 50);
3412
+ debounceTimer = setTimeout(async () => {
3413
+ try {
3414
+ results = await searchSkillsAPI(q);
3415
+ selectedIndex = 0;
3416
+ } catch {
3417
+ results = [];
3418
+ } finally {
3419
+ loading = false;
3420
+ debounceTimer = null;
3421
+ render();
3422
+ }
3423
+ }, debounceMs);
3424
+ }
3425
+ if (initialQuery) triggerSearch(initialQuery);
3426
+ render();
3427
+ return new Promise((resolve) => {
3428
+ function cleanup() {
3429
+ process.stdin.removeListener("keypress", handleKeypress);
3430
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
3431
+ process.stdout.write(SHOW_CURSOR);
3432
+ process.stdin.pause();
3433
+ }
3434
+ function handleKeypress(_ch, key) {
3435
+ if (!key) return;
3436
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
3437
+ cleanup();
3438
+ resolve(null);
3439
+ return;
3440
+ }
3441
+ if (key.name === "return") {
3442
+ cleanup();
3443
+ resolve(results[selectedIndex] || null);
3444
+ return;
3445
+ }
3446
+ if (key.name === "up") {
3447
+ selectedIndex = Math.max(0, selectedIndex - 1);
3448
+ render();
3449
+ return;
3450
+ }
3451
+ if (key.name === "down") {
3452
+ selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1);
3453
+ render();
3454
+ return;
3455
+ }
3456
+ if (key.name === "backspace") {
3457
+ if (query.length > 0) {
3458
+ query = query.slice(0, -1);
3459
+ triggerSearch(query);
3460
+ }
3461
+ return;
3462
+ }
3463
+ if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
3464
+ const char = key.sequence;
3465
+ if (char >= " " && char <= "~") {
3466
+ query += char;
3467
+ triggerSearch(query);
3468
+ }
3469
+ }
3470
+ }
3471
+ process.stdin.on("keypress", handleKeypress);
3472
+ });
3473
+ }
3474
+ function getOwnerRepoFromString(pkg) {
3475
+ const atIndex = pkg.lastIndexOf("@");
3476
+ const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/);
3477
+ if (match) return {
3478
+ owner: match[1],
3479
+ repo: match[2]
3480
+ };
3481
+ return null;
3482
+ }
3483
+ async function isRepoPublic(owner, repo) {
3484
+ return await isRepoPrivate(owner, repo) === false;
3485
+ }
3486
+ async function runFind(args) {
3487
+ const query = args.join(" ");
3488
+ const isNonInteractive = !process.stdin.isTTY;
3489
+ const agentTip = `${DIM$2}Tip: if running in a coding agent, follow these steps:${RESET$2}
3490
+ ${DIM$2} 1) npx @voidagency/skills find [query]${RESET$2}
3491
+ ${DIM$2} 2) npx @voidagency/skills add <owner/repo@skill>${RESET$2}`;
3492
+ if (query) {
3493
+ const results = await searchSkillsAPI(query);
3494
+ track({
3495
+ event: "find",
3496
+ query,
3497
+ resultCount: String(results.length)
3498
+ });
3499
+ if (results.length === 0) {
3500
+ console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`);
3501
+ return;
3502
+ }
3503
+ console.log(`${DIM$2}Install with${RESET$2} npx @voidagency/skills add <owner/repo@skill>`);
3504
+ console.log();
3505
+ for (const skill of results.slice(0, 6)) {
3506
+ const pkg = skill.source || skill.slug;
3507
+ const installs = formatInstalls(skill.installs);
3508
+ console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
3509
+ console.log(`${DIM$2}└ your skills directory/${skill.slug}${RESET$2}`);
3510
+ console.log();
3511
+ }
3512
+ return;
3513
+ }
3514
+ if (isNonInteractive) {
3515
+ console.log(agentTip);
3516
+ console.log();
3517
+ }
3518
+ const selected = await runSearchPrompt();
3519
+ track({
3520
+ event: "find",
3521
+ query: "",
3522
+ resultCount: selected ? "1" : "0",
3523
+ interactive: "1"
3524
+ });
3525
+ if (!selected) {
3526
+ console.log(`${DIM$2}Search cancelled${RESET$2}`);
3527
+ console.log();
3528
+ return;
3529
+ }
3530
+ const pkg = selected.source || selected.slug;
3531
+ const skillName = selected.name;
3532
+ console.log();
3533
+ console.log(`${TEXT$1}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`);
3534
+ console.log();
3535
+ const { source, options } = parseAddOptions([
3536
+ pkg,
3537
+ "--skill",
3538
+ skillName
3539
+ ]);
3540
+ await runAdd(source, options);
3541
+ console.log();
3542
+ const info = getOwnerRepoFromString(pkg);
3543
+ if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$2}View the skill at${RESET$2} ${TEXT$1}your skills directory/${selected.slug}${RESET$2}`);
3544
+ else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$1}your skills directory${RESET$2}`);
3545
+ console.log();
3546
+ }
3547
+ //#endregion
3548
+ //#region src/sync.ts
3549
+ const isCancelled = (value) => typeof value === "symbol";
3550
+ /**
3551
+ * Shortens a path for display: replaces homedir with ~ and cwd with .
3552
+ */
3553
+ function shortenPath$1(fullPath, cwd) {
3554
+ const home = homedir();
3555
+ if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
3556
+ if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
3557
+ return fullPath;
3558
+ }
3559
+ /**
3560
+ * Crawl node_modules for SKILL.md files.
3561
+ * Searches both top-level packages and scoped packages (@org/pkg).
3562
+ * Returns discovered skills with their source package name.
3563
+ */
3564
+ async function discoverNodeModuleSkills(cwd) {
3565
+ const nodeModulesDir = join(cwd, "node_modules");
3566
+ const skills = [];
3567
+ let topNames;
3568
+ try {
3569
+ topNames = await readdir(nodeModulesDir);
3570
+ } catch {
3571
+ return skills;
3572
+ }
3573
+ const processPackageDir = async (pkgDir, packageName) => {
3574
+ const rootSkill = await parseSkillMd(join(pkgDir, "SKILL.md"));
3575
+ if (rootSkill) {
3576
+ skills.push({
3577
+ ...rootSkill,
3578
+ packageName
3579
+ });
3580
+ return;
3581
+ }
3582
+ const searchDirs = [
3583
+ pkgDir,
3584
+ join(pkgDir, "skills"),
3585
+ join(pkgDir, ".agents", "skills")
3586
+ ];
3587
+ for (const searchDir of searchDirs) try {
3588
+ const entries = await readdir(searchDir);
3589
+ for (const name of entries) {
3590
+ const skillDir = join(searchDir, name);
3591
+ try {
3592
+ if (!(await stat(skillDir)).isDirectory()) continue;
3593
+ } catch {
3594
+ continue;
3595
+ }
3596
+ const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
3597
+ if (skill) skills.push({
3598
+ ...skill,
3599
+ packageName
3600
+ });
3601
+ }
3602
+ } catch {}
3603
+ };
3604
+ await Promise.all(topNames.map(async (name) => {
3605
+ if (name.startsWith(".")) return;
3606
+ const fullPath = join(nodeModulesDir, name);
3607
+ try {
3608
+ if (!(await stat(fullPath)).isDirectory()) return;
3609
+ } catch {
3610
+ return;
3611
+ }
3612
+ if (name.startsWith("@")) try {
3613
+ const scopeNames = await readdir(fullPath);
3614
+ await Promise.all(scopeNames.map(async (scopedName) => {
3615
+ const scopedPath = join(fullPath, scopedName);
3616
+ try {
3617
+ if (!(await stat(scopedPath)).isDirectory()) return;
3618
+ } catch {
3619
+ return;
3620
+ }
3621
+ await processPackageDir(scopedPath, `${name}/${scopedName}`);
3622
+ }));
3623
+ } catch {}
3624
+ else await processPackageDir(fullPath, name);
3625
+ }));
3626
+ return skills;
3627
+ }
3628
+ async function runSync(args, options = {}) {
3629
+ const cwd = process.cwd();
3630
+ console.log();
3631
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills experimental_sync ")));
3632
+ const spinner = Y();
3633
+ spinner.start("Scanning node_modules for skills...");
3634
+ const discoveredSkills = await discoverNodeModuleSkills(cwd);
3635
+ if (discoveredSkills.length === 0) {
3636
+ spinner.stop(import_picocolors.default.yellow("No skills found"));
3637
+ Se(import_picocolors.default.dim("No SKILL.md files found in node_modules."));
3638
+ return;
3639
+ }
3640
+ spinner.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
3641
+ for (const skill of discoveredSkills) {
3642
+ M.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
3643
+ if (skill.description) M.message(import_picocolors.default.dim(` ${skill.description}`));
3644
+ }
3645
+ const localLock = await readLocalLock(cwd);
3646
+ const toInstall = [];
3647
+ const upToDate = [];
3648
+ if (options.force) {
3649
+ toInstall.push(...discoveredSkills);
3650
+ M.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
3651
+ } else {
3652
+ for (const skill of discoveredSkills) {
3653
+ const existingEntry = localLock.skills[skill.name];
3654
+ if (existingEntry) {
3655
+ if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
3656
+ upToDate.push(skill.name);
3657
+ continue;
3658
+ }
3659
+ }
3660
+ toInstall.push(skill);
3661
+ }
3662
+ if (upToDate.length > 0) M.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
3663
+ if (toInstall.length === 0) {
3664
+ console.log();
3665
+ Se(import_picocolors.default.green("All skills are up to date."));
3666
+ return;
3667
+ }
3668
+ }
3669
+ M.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
3670
+ let targetAgents;
3671
+ const validAgents = Object.keys(agents);
3672
+ const universalAgents = getUniversalAgents();
3673
+ if (options.agent?.includes("*")) {
3674
+ targetAgents = validAgents;
3675
+ M.info(`Installing to all ${targetAgents.length} agents`);
3676
+ } else if (options.agent && options.agent.length > 0) {
3677
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3678
+ if (invalidAgents.length > 0) {
3679
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3680
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
3681
+ process.exit(1);
3682
+ }
3683
+ targetAgents = options.agent;
3684
+ } else {
3685
+ spinner.start("Loading agents...");
3686
+ const installedAgents = await detectInstalledAgents();
3687
+ const totalAgents = Object.keys(agents).length;
3688
+ spinner.stop(`${totalAgents} agents`);
3689
+ if (installedAgents.length === 0) if (options.yes) {
3690
+ targetAgents = universalAgents;
3691
+ M.info("Installing to universal agents");
3692
+ } else {
3693
+ const selected = await searchMultiselect({
3694
+ message: "Which agents do you want to install to?",
3695
+ items: getNonUniversalAgents().map((a) => ({
3696
+ value: a,
3697
+ label: agents[a].displayName,
3698
+ hint: agents[a].skillsDir
3699
+ })),
3700
+ initialSelected: [],
3701
+ lockedSection: {
3702
+ title: "Universal (.agents/skills)",
3703
+ items: universalAgents.map((a) => ({
3704
+ value: a,
3705
+ label: agents[a].displayName
3706
+ }))
3707
+ }
3708
+ });
3709
+ if (isCancelled(selected)) {
3710
+ xe("Sync cancelled");
3711
+ process.exit(0);
3712
+ }
3713
+ targetAgents = selected;
3714
+ }
3715
+ else if (installedAgents.length === 1 || options.yes) {
3716
+ targetAgents = [...installedAgents];
3717
+ for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
3718
+ } else {
3719
+ const selected = await searchMultiselect({
3720
+ message: "Which agents do you want to install to?",
3721
+ items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
3722
+ value: a,
3723
+ label: agents[a].displayName,
3724
+ hint: agents[a].skillsDir
3725
+ })),
3726
+ initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
3727
+ lockedSection: {
3728
+ title: "Universal (.agents/skills)",
3729
+ items: universalAgents.map((a) => ({
3730
+ value: a,
3731
+ label: agents[a].displayName
3732
+ }))
3733
+ }
3734
+ });
3735
+ if (isCancelled(selected)) {
3736
+ xe("Sync cancelled");
3737
+ process.exit(0);
3738
+ }
3739
+ targetAgents = selected;
3740
+ }
3741
+ }
3742
+ const summaryLines = [];
3743
+ for (const skill of toInstall) {
3744
+ const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd);
3745
+ summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
3746
+ summaryLines.push(` ${import_picocolors.default.dim(shortCanonical)}`);
3747
+ }
3748
+ console.log();
3749
+ Me(summaryLines.join("\n"), "Sync Summary");
3750
+ if (!options.yes) {
3751
+ const confirmed = await ye({ message: "Proceed with sync?" });
3752
+ if (pD(confirmed) || !confirmed) {
3753
+ xe("Sync cancelled");
3754
+ process.exit(0);
3755
+ }
3756
+ }
3757
+ spinner.start("Syncing skills...");
3758
+ const results = [];
3759
+ for (const skill of toInstall) for (const agent of targetAgents) {
3760
+ const result = await installSkillForAgent(skill, agent, {
3761
+ global: false,
3762
+ cwd,
3763
+ mode: "symlink"
3764
+ });
3765
+ results.push({
3766
+ skill: skill.name,
3767
+ packageName: skill.packageName,
3768
+ agent: agents[agent].displayName,
3769
+ success: result.success,
3770
+ path: result.path,
3771
+ canonicalPath: result.canonicalPath,
3772
+ error: result.error
3773
+ });
3774
+ }
3775
+ spinner.stop("Sync complete");
3776
+ const successful = results.filter((r) => r.success);
3777
+ const failed = results.filter((r) => !r.success);
3778
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
3779
+ for (const skill of toInstall) if (successfulSkillNames.has(skill.name)) try {
3780
+ const computedHash = await computeSkillFolderHash(skill.path);
3781
+ await addSkillToLocalLock(skill.name, {
3782
+ source: skill.packageName,
3783
+ sourceType: "node_modules",
3784
+ computedHash
3785
+ }, cwd);
3786
+ } catch {}
3787
+ console.log();
3788
+ if (successful.length > 0) {
3789
+ const bySkill = /* @__PURE__ */ new Map();
3790
+ for (const r of successful) {
3791
+ const skillResults = bySkill.get(r.skill) || [];
3792
+ skillResults.push(r);
3793
+ bySkill.set(r.skill, skillResults);
3794
+ }
3795
+ const resultLines = [];
3796
+ for (const [skillName, skillResults] of bySkill) {
3797
+ const firstResult = skillResults[0];
3798
+ const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
3799
+ if (firstResult.canonicalPath) {
3800
+ const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
3801
+ resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
3802
+ resultLines.push(` ${import_picocolors.default.dim(shortPath)}`);
3803
+ } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
3804
+ }
3805
+ const skillCount = bySkill.size;
3806
+ const title = import_picocolors.default.green(`Synced ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3807
+ Me(resultLines.join("\n"), title);
3808
+ }
3809
+ if (failed.length > 0) {
3810
+ console.log();
3811
+ M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3812
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3813
+ }
3814
+ track({
3815
+ event: "experimental_sync",
3816
+ skillCount: String(toInstall.length),
3817
+ successCount: String(successfulSkillNames.size),
3818
+ agents: targetAgents.join(",")
3819
+ });
3820
+ console.log();
3821
+ Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3822
+ }
3823
+ function parseSyncOptions(args) {
3824
+ const options = {};
3825
+ for (let i = 0; i < args.length; i++) {
3826
+ const arg = args[i];
3827
+ if (arg === "-y" || arg === "--yes") options.yes = true;
3828
+ else if (arg === "-f" || arg === "--force") options.force = true;
3829
+ else if (arg === "-a" || arg === "--agent") {
3830
+ options.agent = options.agent || [];
3831
+ i++;
3832
+ let nextArg = args[i];
3833
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
3834
+ options.agent.push(nextArg);
3835
+ i++;
3836
+ nextArg = args[i];
3837
+ }
3838
+ i--;
3839
+ }
3840
+ }
3841
+ return { options };
3842
+ }
3843
+ //#endregion
3844
+ //#region src/install.ts
3845
+ /**
3846
+ * Install all skills from the local skills-lock.json.
3847
+ * Groups skills by source and calls `runAdd` for each group.
3848
+ *
3849
+ * Only installs to .agents/skills/ (universal agents) -- the canonical
3850
+ * project-level location. Does not install to agent-specific directories.
3851
+ *
3852
+ * node_modules skills are handled via experimental_sync.
3853
+ */
3854
+ async function runInstallFromLock(args) {
3855
+ const lock = await readLocalLock(process.cwd());
3856
+ const skillEntries = Object.entries(lock.skills);
3857
+ if (skillEntries.length === 0) {
3858
+ M.warn("No project skills found in skills-lock.json");
3859
+ M.info(`Add project-level skills with ${import_picocolors.default.cyan("npx @voidagency/skills add <package>")} (without ${import_picocolors.default.cyan("-g")})`);
3860
+ return;
3861
+ }
3862
+ const universalAgentNames = getUniversalAgents();
3863
+ const nodeModuleSkills = [];
3864
+ const bySource = /* @__PURE__ */ new Map();
3865
+ for (const [skillName, entry] of skillEntries) {
3866
+ if (entry.sourceType === "node_modules") {
3867
+ nodeModuleSkills.push(skillName);
3868
+ continue;
3869
+ }
3870
+ const existing = bySource.get(entry.source);
3871
+ if (existing) existing.skills.push(skillName);
3872
+ else bySource.set(entry.source, {
3873
+ sourceType: entry.sourceType,
3874
+ skills: [skillName]
3875
+ });
3876
+ }
3877
+ const remoteCount = skillEntries.length - nodeModuleSkills.length;
3878
+ if (remoteCount > 0) M.info(`Restoring ${import_picocolors.default.cyan(String(remoteCount))} skill${remoteCount !== 1 ? "s" : ""} from skills-lock.json into ${import_picocolors.default.dim(".agents/skills/")}`);
3879
+ for (const [source, { skills }] of bySource) try {
3880
+ await runAdd([source], {
3881
+ skill: skills,
3882
+ agent: universalAgentNames,
3883
+ yes: true
3884
+ });
3885
+ } catch (error) {
3886
+ M.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${error instanceof Error ? error.message : "Unknown error"}`);
3887
+ }
3888
+ if (nodeModuleSkills.length > 0) {
3889
+ M.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
3890
+ try {
3891
+ const { options: syncOptions } = parseSyncOptions(args);
3892
+ await runSync(args, {
3893
+ ...syncOptions,
3894
+ yes: true,
3895
+ agent: universalAgentNames
3896
+ });
3897
+ } catch (error) {
3898
+ M.error(`Failed to sync node_modules skills: ${error instanceof Error ? error.message : "Unknown error"}`);
3899
+ }
3900
+ }
3901
+ }
3902
+ //#endregion
3903
+ //#region src/list.ts
3904
+ const RESET$1 = "\x1B[0m";
3905
+ const BOLD$1 = "\x1B[1m";
3906
+ const DIM$1 = "\x1B[38;5;102m";
3907
+ const CYAN = "\x1B[36m";
3908
+ const YELLOW = "\x1B[33m";
3909
+ /**
3910
+ * Shortens a path for display: replaces homedir with ~ and cwd with .
3911
+ */
3912
+ function shortenPath(fullPath, cwd) {
3913
+ const home = homedir();
3914
+ if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
3915
+ if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
3916
+ return fullPath;
3917
+ }
3918
+ /**
3919
+ * Formats a list of items, truncating if too many
3920
+ */
3921
+ function formatList(items, maxShow = 5) {
3922
+ if (items.length <= maxShow) return items.join(", ");
3923
+ const shown = items.slice(0, maxShow);
3924
+ const remaining = items.length - maxShow;
3925
+ return `${shown.join(", ")} +${remaining} more`;
3926
+ }
3927
+ function parseListOptions(args) {
3928
+ const options = {};
3929
+ for (let i = 0; i < args.length; i++) {
3930
+ const arg = args[i];
3931
+ if (arg === "-g" || arg === "--global") options.global = true;
3932
+ else if (arg === "-a" || arg === "--agent") {
3933
+ options.agent = options.agent || [];
3934
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) options.agent.push(args[++i]);
3935
+ }
3936
+ }
3937
+ return options;
3938
+ }
3939
+ async function runList(args) {
3940
+ const options = parseListOptions(args);
3941
+ const scope = options.global === true ? true : false;
3942
+ let agentFilter;
3943
+ if (options.agent && options.agent.length > 0) {
3944
+ const validAgents = Object.keys(agents);
3945
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3946
+ if (invalidAgents.length > 0) {
3947
+ console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$1}`);
3948
+ console.log(`${DIM$1}Valid agents: ${validAgents.join(", ")}${RESET$1}`);
3949
+ process.exit(1);
3950
+ }
3951
+ agentFilter = options.agent;
3952
+ }
3953
+ const installedSkills = await listInstalledSkills({
3954
+ global: scope,
3955
+ agentFilter
3956
+ });
3957
+ const lockedSkills = await getAllLockedSkills();
3958
+ const cwd = process.cwd();
3959
+ const scopeLabel = scope ? "Global" : "Project";
3960
+ if (installedSkills.length === 0) {
3961
+ console.log(`${DIM$1}No ${scopeLabel.toLowerCase()} skills found.${RESET$1}`);
3962
+ if (scope) console.log(`${DIM$1}Try listing project skills without -g${RESET$1}`);
3963
+ else console.log(`${DIM$1}Try listing global skills with -g${RESET$1}`);
3964
+ return;
3965
+ }
3966
+ function printSkill(skill, indent = false) {
3967
+ const prefix = indent ? " " : "";
3968
+ const shortPath = shortenPath(skill.canonicalPath, cwd);
3969
+ const agentNames = skill.agents.map((a) => agents[a].displayName);
3970
+ const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$1}`;
3971
+ console.log(`${prefix}${CYAN}${skill.name}${RESET$1} ${DIM$1}${shortPath}${RESET$1}`);
3972
+ console.log(`${prefix} ${DIM$1}Agents:${RESET$1} ${agentInfo}`);
3973
+ }
3974
+ console.log(`${BOLD$1}${scopeLabel} Skills${RESET$1}`);
3975
+ console.log();
3976
+ const groupedSkills = {};
3977
+ const ungroupedSkills = [];
3978
+ for (const skill of installedSkills) {
3979
+ const lockEntry = lockedSkills[skill.name];
3980
+ if (lockEntry?.pluginName) {
3981
+ const group = lockEntry.pluginName;
3982
+ if (!groupedSkills[group]) groupedSkills[group] = [];
3983
+ groupedSkills[group].push(skill);
3984
+ } else ungroupedSkills.push(skill);
3985
+ }
3986
+ if (Object.keys(groupedSkills).length > 0) {
3987
+ const sortedGroups = Object.keys(groupedSkills).sort();
3988
+ for (const group of sortedGroups) {
3989
+ const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3990
+ console.log(`${BOLD$1}${title}${RESET$1}`);
3991
+ const skills = groupedSkills[group];
3992
+ if (skills) for (const skill of skills) printSkill(skill, true);
3993
+ console.log();
3994
+ }
3995
+ if (ungroupedSkills.length > 0) {
3996
+ console.log(`${BOLD$1}General${RESET$1}`);
3997
+ for (const skill of ungroupedSkills) printSkill(skill, true);
3998
+ console.log();
3999
+ }
4000
+ } else {
4001
+ for (const skill of installedSkills) printSkill(skill);
4002
+ console.log();
4003
+ }
4004
+ }
4005
+ //#endregion
4006
+ //#region src/remove.ts
4007
+ async function removeCommand(skillNames, options) {
4008
+ const isGlobal = options.global ?? false;
4009
+ const cwd = process.cwd();
4010
+ const spinner = Y();
4011
+ spinner.start("Scanning for installed skills...");
4012
+ const skillNamesSet = /* @__PURE__ */ new Set();
4013
+ const scanDir = async (dir) => {
4014
+ try {
4015
+ const entries = await readdir(dir, { withFileTypes: true });
4016
+ for (const entry of entries) if (entry.isDirectory()) skillNamesSet.add(entry.name);
4017
+ } catch (err) {
4018
+ if (err instanceof Error && err.code !== "ENOENT") M.warn(`Could not scan directory ${dir}: ${err.message}`);
4019
+ }
4020
+ };
4021
+ if (isGlobal) {
4022
+ await scanDir(getCanonicalSkillsDir(true, cwd));
4023
+ for (const agent of Object.values(agents)) if (agent.globalSkillsDir !== void 0) await scanDir(agent.globalSkillsDir);
4024
+ } else {
4025
+ await scanDir(getCanonicalSkillsDir(false, cwd));
4026
+ for (const agent of Object.values(agents)) await scanDir(join(cwd, agent.skillsDir));
4027
+ }
4028
+ const installedSkills = Array.from(skillNamesSet).sort();
4029
+ spinner.stop(`Found ${installedSkills.length} unique installed skill(s)`);
4030
+ if (installedSkills.length === 0) {
4031
+ Se(import_picocolors.default.yellow("No skills found to remove."));
4032
+ return;
4033
+ }
4034
+ if (options.agent && options.agent.length > 0) {
4035
+ const validAgents = Object.keys(agents);
4036
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
4037
+ if (invalidAgents.length > 0) {
4038
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
4039
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
4040
+ process.exit(1);
4041
+ }
4042
+ }
4043
+ let selectedSkills = [];
4044
+ if (options.all) selectedSkills = installedSkills;
4045
+ else if (skillNames.length > 0) {
4046
+ selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
4047
+ if (selectedSkills.length === 0) {
4048
+ M.error(`No matching skills found for: ${skillNames.join(", ")}`);
4049
+ return;
4050
+ }
4051
+ } else {
4052
+ const choices = installedSkills.map((s) => ({
4053
+ value: s,
4054
+ label: s
4055
+ }));
4056
+ const selected = await fe({
4057
+ message: `Select skills to remove ${import_picocolors.default.dim("(space to toggle)")}`,
4058
+ options: choices,
4059
+ required: true
4060
+ });
4061
+ if (pD(selected)) {
4062
+ xe("Removal cancelled");
4063
+ process.exit(0);
4064
+ }
4065
+ selectedSkills = selected;
4066
+ }
4067
+ let targetAgents;
4068
+ if (options.agent && options.agent.length > 0) targetAgents = options.agent;
4069
+ else {
4070
+ targetAgents = Object.keys(agents);
4071
+ spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
4072
+ }
4073
+ if (!options.yes) {
4074
+ console.log();
4075
+ M.info("Skills to remove:");
4076
+ for (const skill of selectedSkills) M.message(` ${import_picocolors.default.red("•")} ${skill}`);
4077
+ console.log();
4078
+ const confirmed = await ye({ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?` });
4079
+ if (pD(confirmed) || !confirmed) {
4080
+ xe("Removal cancelled");
4081
+ process.exit(0);
4082
+ }
4083
+ }
4084
+ spinner.start("Removing skills...");
4085
+ const results = [];
4086
+ for (const skillName of selectedSkills) try {
4087
+ const canonicalPath = getCanonicalPath(skillName, {
4088
+ global: isGlobal,
4089
+ cwd
4090
+ });
4091
+ for (const agentKey of targetAgents) {
4092
+ const agent = agents[agentKey];
4093
+ const skillPath = getInstallPath(skillName, agentKey, {
4094
+ global: isGlobal,
4095
+ cwd
4096
+ });
4097
+ const pathsToCleanup = new Set([skillPath]);
4098
+ const sanitizedName = sanitizeName(skillName);
4099
+ if (isGlobal && agent.globalSkillsDir) pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName));
4100
+ else pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName));
4101
+ for (const pathToCleanup of pathsToCleanup) {
4102
+ if (pathToCleanup === canonicalPath) continue;
4103
+ try {
4104
+ if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
4105
+ recursive: true,
4106
+ force: true
4107
+ });
4108
+ } catch (err) {
4109
+ M.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`);
4110
+ }
4111
+ }
4112
+ }
4113
+ const remainingAgents = (await detectInstalledAgents()).filter((a) => !targetAgents.includes(a));
4114
+ let isStillUsed = false;
4115
+ for (const agentKey of remainingAgents) if (await lstat(getInstallPath(skillName, agentKey, {
4116
+ global: isGlobal,
4117
+ cwd
4118
+ })).catch(() => null)) {
4119
+ isStillUsed = true;
4120
+ break;
4121
+ }
4122
+ if (!isStillUsed) await rm(canonicalPath, {
4123
+ recursive: true,
4124
+ force: true
4125
+ });
4126
+ const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
4127
+ const effectiveSource = lockEntry?.source || "local";
4128
+ const effectiveSourceType = lockEntry?.sourceType || "local";
4129
+ if (isGlobal) await removeSkillFromLock(skillName);
4130
+ results.push({
4131
+ skill: skillName,
4132
+ success: true,
4133
+ source: effectiveSource,
4134
+ sourceType: effectiveSourceType
4135
+ });
4136
+ } catch (err) {
4137
+ results.push({
4138
+ skill: skillName,
4139
+ success: false,
4140
+ error: err instanceof Error ? err.message : String(err)
4141
+ });
4142
+ }
4143
+ spinner.stop("Removal process complete");
4144
+ const successful = results.filter((r) => r.success);
4145
+ const failed = results.filter((r) => !r.success);
4146
+ if (successful.length > 0) {
4147
+ const bySource = /* @__PURE__ */ new Map();
4148
+ for (const r of successful) {
4149
+ const source = r.source || "local";
4150
+ const existing = bySource.get(source) || { skills: [] };
4151
+ existing.skills.push(r.skill);
4152
+ existing.sourceType = r.sourceType;
4153
+ bySource.set(source, existing);
4154
+ }
4155
+ for (const [source, data] of bySource) track({
4156
+ event: "remove",
4157
+ source,
4158
+ skills: data.skills.join(","),
4159
+ agents: targetAgents.join(","),
4160
+ ...isGlobal && { global: "1" },
4161
+ sourceType: data.sourceType
4162
+ });
4163
+ }
4164
+ if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} skill(s)`));
4165
+ if (failed.length > 0) {
4166
+ M.error(import_picocolors.default.red(`Failed to remove ${failed.length} skill(s)`));
4167
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill}: ${r.error}`);
4168
+ }
4169
+ console.log();
4170
+ Se(import_picocolors.default.green("Done!"));
4171
+ }
4172
+ /**
4173
+ * Parse command line options for the remove command.
4174
+ * Separates skill names from options flags.
4175
+ */
4176
+ function parseRemoveOptions(args) {
4177
+ const options = {};
4178
+ const skills = [];
4179
+ for (let i = 0; i < args.length; i++) {
4180
+ const arg = args[i];
4181
+ if (arg === "-g" || arg === "--global") options.global = true;
4182
+ else if (arg === "-y" || arg === "--yes") options.yes = true;
4183
+ else if (arg === "--all") options.all = true;
4184
+ else if (arg === "-a" || arg === "--agent") {
4185
+ options.agent = options.agent || [];
4186
+ i++;
4187
+ let nextArg = args[i];
4188
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
4189
+ options.agent.push(nextArg);
4190
+ i++;
4191
+ nextArg = args[i];
4192
+ }
4193
+ i--;
4194
+ } else if (arg && !arg.startsWith("-")) skills.push(arg);
4195
+ }
4196
+ return {
4197
+ skills,
4198
+ options
4199
+ };
4200
+ }
4201
+ //#endregion
4202
+ //#region src/cli.ts
4203
+ const __dirname = dirname(fileURLToPath(import.meta.url));
4204
+ function getVersion() {
4205
+ try {
4206
+ const pkgPath = join(__dirname, "..", "package.json");
4207
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
4208
+ } catch {
4209
+ return "0.0.0";
4210
+ }
4211
+ }
4212
+ const VERSION = getVersion();
4213
+ initTelemetry(VERSION);
4214
+ const RESET = "\x1B[0m";
4215
+ const BOLD = "\x1B[1m";
4216
+ const DIM = "\x1B[38;5;102m";
4217
+ const TEXT = "\x1B[38;5;145m";
4218
+ const LOGO_LINES = [
4219
+ "███████╗██╗ ██╗██╗██╗ ██╗ ███████╗",
4220
+ "██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝",
4221
+ "███████╗█████╔╝ ██║██║ ██║ ███████╗",
4222
+ "╚════██║██╔═██╗ ██║██║ ██║ ╚════██║",
4223
+ "███████║██║ ██╗██║███████╗███████╗███████║",
4224
+ "╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝"
4225
+ ];
4226
+ const GRAYS = [
4227
+ "\x1B[38;5;250m",
4228
+ "\x1B[38;5;248m",
4229
+ "\x1B[38;5;245m",
4230
+ "\x1B[38;5;243m",
4231
+ "\x1B[38;5;240m",
4232
+ "\x1B[38;5;238m"
4233
+ ];
4234
+ function showLogo() {
4235
+ console.log();
4236
+ LOGO_LINES.forEach((line, i) => {
4237
+ console.log(`${GRAYS[i]}${line}${RESET}`);
4238
+ });
4239
+ }
4240
+ function showBanner() {
4241
+ showLogo();
4242
+ console.log();
4243
+ console.log(`${DIM}The open agent skills ecosystem${RESET}`);
4244
+ console.log();
4245
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
4246
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills remove${RESET} ${DIM}Remove installed skills${RESET}`);
4247
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills list${RESET} ${DIM}List installed skills${RESET}`);
4248
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
4249
+ console.log();
4250
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills check${RESET} ${DIM}Check for updates${RESET}`);
4251
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills update${RESET} ${DIM}Update all skills${RESET}`);
4252
+ console.log();
4253
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
4254
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
4255
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
4256
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @voidagency/skills add-agent ${DIM}<source>${RESET} ${DIM}Install agent and its skills${RESET}`);
4257
+ console.log();
4258
+ console.log(`${DIM}try:${RESET} npx @voidagency/skills add vercel-labs/agent-skills`);
4259
+ console.log();
4260
+ console.log(`Discover more skills at ${TEXT}your skills directory${RESET}`);
4261
+ console.log();
4262
+ }
4263
+ function showHelp() {
4264
+ console.log(`
4265
+ ${BOLD}Usage:${RESET} skills <command> [options]
4266
+
4267
+ ${BOLD}Manage Skills:${RESET}
4268
+ add <package> Add a skill package (alias: a)
4269
+ e.g. vercel-labs/agent-skills
4270
+ https://github.com/vercel-labs/agent-skills
4271
+ add-agent <source> Install an agent definition and its skills. Use source@agent-name
4272
+ for multi-agent repos (agent in agents/<name>/).
4273
+ remove [skills] Remove installed skills
4274
+ list, ls List installed skills
4275
+ find [query] Search for skills interactively
4276
+
4277
+ ${BOLD}Updates:${RESET}
4278
+ check Check for available skill updates
4279
+ update Update all skills to latest versions
4280
+
4281
+ ${BOLD}Project:${RESET}
4282
+ experimental_install Restore skills from skills-lock.json
4283
+ init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
4284
+ experimental_sync Sync skills from node_modules into agent directories
4285
+
4286
+ ${BOLD}Add Options:${RESET}
4287
+ -g, --global Install skill globally (user-level) instead of project-level
4288
+ -a, --agent <agents> Specify agents to install to (use '*' for all agents)
4289
+ -s, --skill <skills> Specify skill names to install (use '*' for all skills)
4290
+ -l, --list List available skills in the repository without installing
4291
+ -y, --yes Skip confirmation prompts
4292
+ --copy Copy files instead of symlinking to agent directories
4293
+ --all Shorthand for --skill '*' --agent '*' -y
4294
+ --full-depth Search all subdirectories even when a root SKILL.md exists
4295
+
4296
+ ${BOLD}Remove Options:${RESET}
4297
+ -g, --global Remove from global scope
4298
+ -a, --agent <agents> Remove from specific agents (use '*' for all agents)
4299
+ -s, --skill <skills> Specify skills to remove (use '*' for all skills)
4300
+ -y, --yes Skip confirmation prompts
4301
+ --all Shorthand for --skill '*' --agent '*' -y
4302
+
4303
+ ${BOLD}Experimental Sync Options:${RESET}
4304
+ -a, --agent <agents> Specify agents to install to (use '*' for all agents)
4305
+ -y, --yes Skip confirmation prompts
4306
+
4307
+ ${BOLD}List Options:${RESET}
4308
+ -g, --global List global skills (default: project)
4309
+ -a, --agent <agents> Filter by specific agents
4310
+
4311
+ ${BOLD}Options:${RESET}
4312
+ --help, -h Show this help message
4313
+ --version, -v Show version number
4314
+
4315
+ ${BOLD}Examples:${RESET}
4316
+ ${DIM}$${RESET} skills add vercel-labs/agent-skills
4317
+ ${DIM}$${RESET} skills add vercel-labs/agent-skills -g
4318
+ ${DIM}$${RESET} skills add vercel-labs/agent-skills --agent claude-code cursor
4319
+ ${DIM}$${RESET} skills add vercel-labs/agent-skills --skill pr-review commit
4320
+ ${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET}
4321
+ ${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET}
4322
+ ${DIM}$${RESET} skills rm --global frontend-design
4323
+ ${DIM}$${RESET} skills list ${DIM}# list project skills${RESET}
4324
+ ${DIM}$${RESET} skills ls -g ${DIM}# list global skills${RESET}
4325
+ ${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET}
4326
+ ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET}
4327
+ ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET}
4328
+ ${DIM}$${RESET} skills check
4329
+ ${DIM}$${RESET} skills update
4330
+ ${DIM}$${RESET} skills experimental_install ${DIM}# restore from skills-lock.json${RESET}
4331
+ ${DIM}$${RESET} skills init my-skill
4332
+ ${DIM}$${RESET} skills experimental_sync ${DIM}# sync from node_modules${RESET}
4333
+ ${DIM}$${RESET} skills experimental_sync -y ${DIM}# sync without prompts${RESET}
4334
+
4335
+ Discover more skills at ${TEXT}your skills directory${RESET}
4336
+ `);
4337
+ }
4338
+ function showRemoveHelp() {
4339
+ console.log(`
4340
+ ${BOLD}Usage:${RESET} skills remove [skills...] [options]
4341
+
4342
+ ${BOLD}Description:${RESET}
4343
+ Remove installed skills from agents. If no skill names are provided,
4344
+ an interactive selection menu will be shown.
4345
+
4346
+ ${BOLD}Arguments:${RESET}
4347
+ skills Optional skill names to remove (space-separated)
4348
+
4349
+ ${BOLD}Options:${RESET}
4350
+ -g, --global Remove from global scope (~/) instead of project scope
4351
+ -a, --agent Remove from specific agents (use '*' for all agents)
4352
+ -s, --skill Specify skills to remove (use '*' for all skills)
4353
+ -y, --yes Skip confirmation prompts
4354
+ --all Shorthand for --skill '*' --agent '*' -y
4355
+
4356
+ ${BOLD}Examples:${RESET}
4357
+ ${DIM}$${RESET} skills remove ${DIM}# interactive selection${RESET}
4358
+ ${DIM}$${RESET} skills remove my-skill ${DIM}# remove specific skill${RESET}
4359
+ ${DIM}$${RESET} skills remove skill1 skill2 -y ${DIM}# remove multiple skills${RESET}
4360
+ ${DIM}$${RESET} skills remove --global my-skill ${DIM}# remove from global scope${RESET}
4361
+ ${DIM}$${RESET} skills rm --agent claude-code my-skill ${DIM}# remove from specific agent${RESET}
4362
+ ${DIM}$${RESET} skills remove --all ${DIM}# remove all skills${RESET}
4363
+ ${DIM}$${RESET} skills remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
4364
+
4365
+ Discover more skills at ${TEXT}your skills directory${RESET}
4366
+ `);
4367
+ }
4368
+ function runInit(args) {
4369
+ const cwd = process.cwd();
4370
+ const skillName = args[0] || basename(cwd);
4371
+ const hasName = args[0] !== void 0;
4372
+ const skillDir = hasName ? join(cwd, skillName) : cwd;
4373
+ const skillFile = join(skillDir, "SKILL.md");
4374
+ const displayPath = hasName ? `${skillName}/SKILL.md` : "SKILL.md";
4375
+ if (existsSync(skillFile)) {
4376
+ console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
4377
+ return;
4378
+ }
4379
+ if (hasName) mkdirSync(skillDir, { recursive: true });
4380
+ writeFileSync(skillFile, `---
4381
+ name: ${skillName}
4382
+ description: A brief description of what this skill does
4383
+ ---
4384
+
4385
+ # ${skillName}
4386
+
4387
+ Instructions for the agent to follow when this skill is activated.
4388
+
4389
+ ## When to use
4390
+
4391
+ Describe when this skill should be used.
4392
+
4393
+ ## Instructions
4394
+
4395
+ 1. First step
4396
+ 2. Second step
4397
+ 3. Additional steps as needed
4398
+ `);
4399
+ console.log(`${TEXT}Initialized skill: ${DIM}${skillName}${RESET}`);
4400
+ console.log();
4401
+ console.log(`${DIM}Created:${RESET}`);
4402
+ console.log(` ${displayPath}`);
4403
+ console.log();
4404
+ console.log(`${DIM}Next steps:${RESET}`);
4405
+ console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
4406
+ console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
4407
+ console.log();
4408
+ console.log(`${DIM}Publishing:${RESET}`);
4409
+ console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx @voidagency/skills add <owner>/<repo>${RESET}`);
4410
+ console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx @voidagency/skills add https://example.com/${displayPath}${RESET}`);
4411
+ console.log();
4412
+ console.log(`Browse existing skills for inspiration at ${TEXT}your skills directory${RESET}`);
4413
+ console.log();
4414
+ }
4415
+ const AGENTS_DIR = ".agents";
4416
+ const LOCK_FILE = ".skill-lock.json";
4417
+ const CURRENT_LOCK_VERSION = 3;
4418
+ function getSkillLockPath() {
4419
+ return join(homedir(), AGENTS_DIR, LOCK_FILE);
4420
+ }
4421
+ function readSkillLock() {
4422
+ const lockPath = getSkillLockPath();
4423
+ try {
4424
+ const content = readFileSync(lockPath, "utf-8");
4425
+ const parsed = JSON.parse(content);
4426
+ if (typeof parsed.version !== "number" || !parsed.skills) return {
4427
+ version: CURRENT_LOCK_VERSION,
4428
+ skills: {}
4429
+ };
4430
+ if (parsed.version < CURRENT_LOCK_VERSION) return {
4431
+ version: CURRENT_LOCK_VERSION,
4432
+ skills: {}
4433
+ };
4434
+ return parsed;
4435
+ } catch {
4436
+ return {
4437
+ version: CURRENT_LOCK_VERSION,
4438
+ skills: {}
4439
+ };
4440
+ }
4441
+ }
4442
+ /**
4443
+ * Determine why a skill cannot be checked for updates automatically.
4444
+ */
4445
+ function getSkipReason(entry) {
4446
+ if (entry.sourceType === "local") return "Local path";
4447
+ if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
4448
+ if (entry.sourceType === "bitbucket") return "Bitbucket (reinstall to update)";
4449
+ if (!entry.skillFolderHash) return "No version hash available";
4450
+ if (!entry.skillPath) return "No skill path recorded";
4451
+ return "No version tracking";
4452
+ }
4453
+ /**
4454
+ * Print a list of skills that cannot be checked automatically,
4455
+ * with the reason and a manual update command for each.
4456
+ */
4457
+ function printSkippedSkills(skipped) {
4458
+ if (skipped.length === 0) return;
4459
+ console.log();
4460
+ console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
4461
+ for (const skill of skipped) {
4462
+ console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
4463
+ console.log(` ${DIM}To update: ${TEXT}npx @voidagency/skills add ${skill.sourceUrl} -g -y${RESET}`);
4464
+ }
4465
+ }
4466
+ async function runCheck(args = []) {
4467
+ console.log(`${TEXT}Checking for skill updates...${RESET}`);
4468
+ console.log();
4469
+ const lock = readSkillLock();
4470
+ const skillNames = Object.keys(lock.skills);
4471
+ if (skillNames.length === 0) {
4472
+ console.log(`${DIM}No skills tracked in lock file.${RESET}`);
4473
+ console.log(`${DIM}Install skills with${RESET} ${TEXT}npx @voidagency/skills add <package>${RESET}`);
4474
+ return;
4475
+ }
4476
+ const token = getGitHubToken();
4477
+ const skillsBySource = /* @__PURE__ */ new Map();
4478
+ const skipped = [];
4479
+ for (const skillName of skillNames) {
4480
+ const entry = lock.skills[skillName];
4481
+ if (!entry) continue;
4482
+ if (!entry.skillFolderHash || !entry.skillPath) {
4483
+ skipped.push({
4484
+ name: skillName,
4485
+ reason: getSkipReason(entry),
4486
+ sourceUrl: entry.sourceUrl
4487
+ });
4488
+ continue;
4489
+ }
4490
+ const existing = skillsBySource.get(entry.source) || [];
4491
+ existing.push({
4492
+ name: skillName,
4493
+ entry
4494
+ });
4495
+ skillsBySource.set(entry.source, existing);
4496
+ }
4497
+ const totalSkills = skillNames.length - skipped.length;
4498
+ if (totalSkills === 0) {
4499
+ console.log(`${DIM}No GitHub skills to check.${RESET}`);
4500
+ printSkippedSkills(skipped);
4501
+ return;
4502
+ }
4503
+ console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
4504
+ const updates = [];
4505
+ const errors = [];
4506
+ for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
4507
+ const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
4508
+ if (!latestHash) {
4509
+ errors.push({
4510
+ name,
4511
+ source,
4512
+ error: "Could not fetch from GitHub"
4513
+ });
4514
+ continue;
4515
+ }
4516
+ if (latestHash !== entry.skillFolderHash) updates.push({
4517
+ name,
4518
+ source
4519
+ });
4520
+ } catch (err) {
4521
+ errors.push({
4522
+ name,
4523
+ source,
4524
+ error: err instanceof Error ? err.message : "Unknown error"
4525
+ });
4526
+ }
4527
+ console.log();
4528
+ if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
4529
+ else {
4530
+ console.log(`${TEXT}${updates.length} update(s) available:${RESET}`);
4531
+ console.log();
4532
+ for (const update of updates) {
4533
+ console.log(` ${TEXT}↑${RESET} ${update.name}`);
4534
+ console.log(` ${DIM}source: ${update.source}${RESET}`);
4535
+ }
4536
+ console.log();
4537
+ console.log(`${DIM}Run${RESET} ${TEXT}npx @voidagency/skills update${RESET} ${DIM}to update all skills${RESET}`);
4538
+ }
4539
+ if (errors.length > 0) {
4540
+ console.log();
4541
+ console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
4542
+ }
4543
+ printSkippedSkills(skipped);
4544
+ track({
4545
+ event: "check",
4546
+ skillCount: String(totalSkills),
4547
+ updatesAvailable: String(updates.length)
4548
+ });
4549
+ console.log();
4550
+ }
4551
+ async function runUpdate() {
4552
+ console.log(`${TEXT}Checking for skill updates...${RESET}`);
4553
+ console.log();
4554
+ const lock = readSkillLock();
4555
+ const skillNames = Object.keys(lock.skills);
4556
+ if (skillNames.length === 0) {
4557
+ console.log(`${DIM}No skills tracked in lock file.${RESET}`);
4558
+ console.log(`${DIM}Install skills with${RESET} ${TEXT}npx @voidagency/skills add <package>${RESET}`);
4559
+ return;
4560
+ }
4561
+ const token = getGitHubToken();
4562
+ const updates = [];
4563
+ const skipped = [];
4564
+ for (const skillName of skillNames) {
4565
+ const entry = lock.skills[skillName];
4566
+ if (!entry) continue;
4567
+ if (!entry.skillFolderHash || !entry.skillPath) {
4568
+ skipped.push({
4569
+ name: skillName,
4570
+ reason: getSkipReason(entry),
4571
+ sourceUrl: entry.sourceUrl
4572
+ });
4573
+ continue;
4574
+ }
4575
+ try {
4576
+ const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
4577
+ if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
4578
+ name: skillName,
4579
+ source: entry.source,
4580
+ entry
4581
+ });
4582
+ } catch {}
4583
+ }
4584
+ if (skillNames.length - skipped.length === 0) {
4585
+ console.log(`${DIM}No skills to check.${RESET}`);
4586
+ printSkippedSkills(skipped);
4587
+ return;
4588
+ }
4589
+ if (updates.length === 0) {
4590
+ console.log(`${TEXT}✓ All skills are up to date${RESET}`);
4591
+ console.log();
4592
+ return;
4593
+ }
4594
+ console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
4595
+ console.log();
4596
+ let successCount = 0;
4597
+ let failCount = 0;
4598
+ for (const update of updates) {
4599
+ console.log(`${TEXT}Updating ${update.name}...${RESET}`);
4600
+ let installUrl = update.entry.sourceUrl;
4601
+ if (update.entry.skillPath) {
4602
+ let skillFolder = update.entry.skillPath;
4603
+ if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
4604
+ else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
4605
+ if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
4606
+ installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
4607
+ installUrl = `${installUrl}/tree/main/${skillFolder}`;
4608
+ }
4609
+ if (spawnSync("npx", [
4610
+ "-y",
4611
+ "@voidagency/skills",
4612
+ "add",
4613
+ installUrl,
4614
+ "-g",
4615
+ "-y"
4616
+ ], {
4617
+ stdio: [
4618
+ "inherit",
4619
+ "pipe",
4620
+ "pipe"
4621
+ ],
4622
+ shell: process.platform === "win32"
4623
+ }).status === 0) {
4624
+ successCount++;
4625
+ console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
4626
+ } else {
4627
+ failCount++;
4628
+ console.log(` ${DIM}✗ Failed to update ${update.name}${RESET}`);
4629
+ }
4630
+ }
4631
+ console.log();
4632
+ if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
4633
+ if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
4634
+ track({
4635
+ event: "update",
4636
+ skillCount: String(updates.length),
4637
+ successCount: String(successCount),
4638
+ failCount: String(failCount)
4639
+ });
4640
+ console.log();
4641
+ }
4642
+ async function main() {
4643
+ const args = process.argv.slice(2);
4644
+ if (args.length === 0) {
4645
+ showBanner();
4646
+ return;
4647
+ }
4648
+ const command = args[0];
4649
+ const restArgs = args.slice(1);
4650
+ switch (command) {
4651
+ case "find":
4652
+ case "search":
4653
+ case "f":
4654
+ case "s":
4655
+ showLogo();
4656
+ console.log();
4657
+ await runFind(restArgs);
4658
+ break;
4659
+ case "init":
4660
+ showLogo();
4661
+ console.log();
4662
+ runInit(restArgs);
4663
+ break;
4664
+ case "experimental_install":
4665
+ showLogo();
4666
+ await runInstallFromLock(restArgs);
4667
+ break;
4668
+ case "add-agent": {
4669
+ showLogo();
4670
+ const { source: agentSource, options: agentOpts } = parseAddAgentOptions(restArgs);
4671
+ if (!agentSource) {
4672
+ console.log(`${BOLD}Usage:${RESET} npx @voidagency/skills add-agent <source> [options]\n`);
4673
+ console.log(`${DIM}Examples:${RESET} npx @voidagency/skills add-agent owner/repo -g -y\n npx @voidagency/skills add-agent bitbucket:org/skills@backend -a cursor -g -y\n`);
4674
+ process.exit(1);
4675
+ }
4676
+ await runAddAgent(agentSource, agentOpts);
4677
+ break;
4678
+ }
4679
+ case "i":
4680
+ case "install":
4681
+ case "a":
4682
+ case "add": {
4683
+ showLogo();
4684
+ const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
4685
+ await runAdd(addSource, addOpts);
4686
+ break;
4687
+ }
4688
+ case "remove":
4689
+ case "rm":
4690
+ case "r":
4691
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
4692
+ showRemoveHelp();
4693
+ break;
4694
+ }
4695
+ const { skills, options: removeOptions } = parseRemoveOptions(restArgs);
4696
+ await removeCommand(skills, removeOptions);
4697
+ break;
4698
+ case "experimental_sync": {
4699
+ showLogo();
4700
+ const { options: syncOptions } = parseSyncOptions(restArgs);
4701
+ await runSync(restArgs, syncOptions);
4702
+ break;
4703
+ }
4704
+ case "list":
4705
+ case "ls":
4706
+ await runList(restArgs);
4707
+ break;
4708
+ case "check":
4709
+ runCheck(restArgs);
4710
+ break;
4711
+ case "update":
4712
+ case "upgrade":
4713
+ runUpdate();
4714
+ break;
4715
+ case "--help":
4716
+ case "-h":
4717
+ showHelp();
4718
+ break;
4719
+ case "--version":
4720
+ case "-v":
4721
+ console.log(VERSION);
4722
+ break;
4723
+ default:
4724
+ console.log(`Unknown command: ${command}`);
4725
+ console.log(`Run ${BOLD}skills --help${RESET} for usage.`);
4726
+ }
4727
+ }
4728
+ main();
4729
+ //#endregion
4730
+ export {};