#!/usr/bin/env node import { i as __toESM, n as __exportAll } from "./_chunks/rolldown-runtime.mjs"; import { l as pD, u as require_picocolors } from "./_chunks/libs/@clack/core.mjs"; 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"; import "./_chunks/libs/@kwsites/file-exists.mjs"; import "./_chunks/libs/@kwsites/promise-deferred.mjs"; import { t as esm_default } from "./_chunks/libs/simple-git.mjs"; import { t as xdgConfig } from "./_chunks/libs/xdg-basedir.mjs"; import { t as require_dist } from "./_chunks/libs/@vercel/detect-agent.mjs"; import { execSync, spawnSync } from "child_process"; import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs"; import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path"; import { homedir, platform, tmpdir } from "os"; import { fileURLToPath } from "url"; import { stripVTControlCharacters } from "node:util"; import * as readline from "readline"; import { Writable } from "stream"; import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises"; import { parse } from "yaml"; import { createHash } from "node:crypto"; import { gunzipSync, inflateRawSync } from "node:zlib"; import { createHash as createHash$1 } from "crypto"; var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1); function getOwnerRepo(parsed) { if (parsed.type === "local") return null; const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/); if (sshMatch) { let path = sshMatch[1]; path = path.replace(/\.git$/, ""); if (path.includes("/")) return path; return null; } if (!parsed.url.startsWith("http://") && !parsed.url.startsWith("https://")) return null; try { let path = new URL(parsed.url).pathname.slice(1); path = path.replace(/\.git$/, ""); if (path.includes("/")) return path; } catch {} return null; } function parseOwnerRepo(ownerRepo) { const match = ownerRepo.match(/^([^/]+)\/([^/]+)$/); if (match) return { owner: match[1], repo: match[2] }; return null; } async function isRepoPrivate(owner, repo) { try { const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`); if (!res.ok) return null; return (await res.json()).private === true; } catch { return null; } } function sanitizeSubpath(subpath) { const segments = subpath.replace(/\\/g, "/").split("/"); for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`); return subpath; } function isLocalPath(input) { return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input); } const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" }; function decodeFragmentValue(value) { try { return decodeURIComponent(value); } catch { return value; } } function looksLikeGitSource(input) { if (input.startsWith("github:") || input.startsWith("gitlab:") || input.startsWith("git@")) return true; if (input.startsWith("http://") || input.startsWith("https://")) try { const parsed = new URL(input); const pathname = parsed.pathname; if (parsed.hostname === "github.com") return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname); if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname); } catch {} if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true; return !input.includes(":") && !input.startsWith(".") && !input.startsWith("/") && /^([^/]+)\/([^/]+)(?:\/(.+)|@(.+))?$/.test(input); } function parseFragmentRef(input) { const hashIndex = input.indexOf("#"); if (hashIndex < 0) return { inputWithoutFragment: input }; const inputWithoutFragment = input.slice(0, hashIndex); const fragment = input.slice(hashIndex + 1); if (!fragment || !looksLikeGitSource(inputWithoutFragment)) return { inputWithoutFragment: input }; const atIndex = fragment.indexOf("@"); if (atIndex === -1) return { inputWithoutFragment, ref: decodeFragmentValue(fragment) }; const ref = fragment.slice(0, atIndex); const skillFilter = fragment.slice(atIndex + 1); return { inputWithoutFragment, ref: ref ? decodeFragmentValue(ref) : void 0, skillFilter: skillFilter ? decodeFragmentValue(skillFilter) : void 0 }; } function appendFragmentRef(input, ref, skillFilter) { if (!ref) return input; return `${input}#${ref}${skillFilter ? `@${skillFilter}` : ""}`; } function parseSource(input) { if (isLocalPath(input)) { const resolvedPath = resolve(input); return { type: "local", url: resolvedPath, localPath: resolvedPath }; } const { inputWithoutFragment, ref: fragmentRef, skillFilter: fragmentSkillFilter } = parseFragmentRef(input); input = inputWithoutFragment; const alias = SOURCE_ALIASES[input]; if (alias) input = alias; const githubPrefixMatch = input.match(/^github:(.+)$/); if (githubPrefixMatch) return parseSource(appendFragmentRef(githubPrefixMatch[1], fragmentRef, fragmentSkillFilter)); const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/); if (gitlabPrefixMatch) return parseSource(appendFragmentRef(`https://gitlab.com/${gitlabPrefixMatch[1]}`, fragmentRef, fragmentSkillFilter)); const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/); if (githubTreeWithPathMatch) { const [, owner, repo, ref, subpath] = githubTreeWithPathMatch; return { type: "github", url: `https://github.com/${owner}/${repo}.git`, ref: ref || fragmentRef, subpath: subpath ? sanitizeSubpath(subpath) : subpath }; } const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/); if (githubTreeMatch) { const [, owner, repo, ref] = githubTreeMatch; return { type: "github", url: `https://github.com/${owner}/${repo}.git`, ref: ref || fragmentRef }; } const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/); if (githubRepoMatch) { const [, owner, repo] = githubRepoMatch; return { type: "github", url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`, ...fragmentRef ? { ref: fragmentRef } : {} }; } const gitlabTreeWithPathMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)\/(.+)/); if (gitlabTreeWithPathMatch) { const [, protocol, hostname, repoPath, ref, subpath] = gitlabTreeWithPathMatch; if (hostname !== "github.com" && repoPath) return { type: "gitlab", url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`, ref: ref || fragmentRef, subpath: subpath ? sanitizeSubpath(subpath) : subpath }; } const gitlabTreeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)$/); if (gitlabTreeMatch) { const [, protocol, hostname, repoPath, ref] = gitlabTreeMatch; if (hostname !== "github.com" && repoPath) return { type: "gitlab", url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`, ref: ref || fragmentRef }; } const gitlabRepoMatch = input.match(/gitlab\.com\/(.+?)(?:\.git)?\/?$/); if (gitlabRepoMatch) { const repoPath = gitlabRepoMatch[1]; if (repoPath.includes("/")) return { type: "gitlab", url: `https://gitlab.com/${repoPath}.git`, ...fragmentRef ? { ref: fragmentRef } : {} }; } const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/); if (atSkillMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) { const [, owner, repo, skillFilter] = atSkillMatch; return { type: "github", url: `https://github.com/${owner}/${repo}.git`, ...fragmentRef ? { ref: fragmentRef } : {}, skillFilter: fragmentSkillFilter || skillFilter }; } const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+?))?\/?$/); if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) { const [, owner, repo, subpath] = shorthandMatch; return { type: "github", url: `https://github.com/${owner}/${repo}.git`, ...fragmentRef ? { ref: fragmentRef } : {}, subpath: subpath ? sanitizeSubpath(subpath) : subpath, ...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {} }; } if (isWellKnownUrl(input)) return { type: "well-known", url: input }; return { type: "git", url: input, ...fragmentRef ? { ref: fragmentRef } : {} }; } function isWellKnownUrl(input) { if (!input.startsWith("http://") && !input.startsWith("https://")) return false; try { const parsed = new URL(input); if ([ "github.com", "gitlab.com", "raw.githubusercontent.com" ].includes(parsed.hostname)) return false; if (input.endsWith(".git")) return false; return true; } catch { return false; } } const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g; const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g; const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g; const SIMPLE_ESC_RE = /\x1b[\x20-\x7e]/g; const C1_RE = /[\x80-\x9f]/g; const CONTROL_RE = /[\x00-\x06\x07\x08\x0b\x0c\x0d-\x1a\x1c-\x1f\x7f]/g; function stripTerminalEscapes(str) { return str.replace(OSC_RE, "").replace(DCS_PM_APC_RE, "").replace(CSI_RE, "").replace(SIMPLE_ESC_RE, "").replace(C1_RE, "").replace(CONTROL_RE, ""); } function sanitizeMetadata(str) { return stripTerminalEscapes(str).replace(/[\r\n]+/g, " ").trim(); } const silentOutput = new Writable({ write(_chunk, _encoding, callback) { callback(); } }); const S_STEP_ACTIVE = import_picocolors.default.green("◆"); const S_STEP_CANCEL = import_picocolors.default.red("■"); const S_STEP_SUBMIT = import_picocolors.default.green("◇"); const S_RADIO_ACTIVE = import_picocolors.default.green("●"); const S_RADIO_INACTIVE = import_picocolors.default.dim("○"); import_picocolors.default.green("✓"); const S_BULLET = import_picocolors.default.green("•"); const S_BAR = import_picocolors.default.dim("│"); const S_BAR_H = import_picocolors.default.dim("─"); const cancelSymbol = Symbol("cancel"); function approxStringWidth(plain) { let width = 0; for (const ch of plain) { const code = ch.codePointAt(0); if (code === 0) continue; width += code >= 4352 && code <= 4447 || code >= 8986 && code <= 8987 || code >= 9001 && code <= 9002 || code >= 9193 && code <= 9196 || code === 9200 || code === 9203 || code >= 9725 && code <= 9726 || code >= 9748 && code <= 9749 || code >= 9800 && code <= 9811 || code >= 9855 && code <= 9855 || code >= 9875 && code <= 9875 || code >= 9889 && code <= 9889 || code >= 9898 && code <= 9899 || code >= 9917 && code <= 9918 || code >= 9924 && code <= 9925 || code >= 9934 && code <= 9934 || code >= 9940 && code <= 9940 || code >= 9962 && code <= 9962 || code >= 9970 && code <= 9971 || code >= 9973 && code <= 9973 || code >= 9978 && code <= 9978 || code >= 9981 && code <= 9981 || code >= 9989 && code <= 9989 || code >= 9994 && code <= 9995 || code >= 10024 && code <= 10024 || code >= 10060 && code <= 10060 || code >= 10062 && code <= 10062 || code >= 10067 && code <= 10069 || code >= 10071 && code <= 10071 || code >= 10133 && code <= 10135 || code >= 10160 && code <= 10160 || code >= 10175 && code <= 10175 || code >= 11035 && code <= 11036 || code >= 11088 && code <= 11088 || code >= 11093 && code <= 11093 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 43360 && code <= 43388 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 126976 && code <= 129535 ? 2 : 1; } return width; } function visualRowsForLine(line, columns) { const plain = stripVTControlCharacters(line); const cols = Math.max(1, columns); const w = approxStringWidth(plain); return Math.max(1, Math.ceil(w / cols)); } function countVisualRowsForLines(lines, columns) { const cols = columns !== void 0 && columns > 0 ? columns : process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80; return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0); } async function searchMultiselect(options) { const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options; return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: silentOutput, terminal: false }); if (process.stdin.isTTY) process.stdin.setRawMode(true); readline.emitKeypressEvents(process.stdin, rl); let query = ""; let cursor = 0; const selected = new Set(initialSelected); let lastRenderHeight = 0; const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : []; const filter = (item, q) => { if (!q) return true; const lowerQ = q.toLowerCase(); return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ); }; const getFiltered = () => { return items.filter((item) => filter(item, query)); }; const clearRender = () => { if (lastRenderHeight > 0) { process.stdout.write(`\x1b[${lastRenderHeight}A`); for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B"); process.stdout.write(`\x1b[${lastRenderHeight}A`); } }; const render = (state = "active") => { clearRender(); const lines = []; const filtered = getFiltered(); const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT; lines.push(`${icon} ${import_picocolors.default.bold(message)}`); if (state === "active") { if (lockedSection && lockedSection.items.length > 0) { lines.push(`${S_BAR}`); const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`; lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`); for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`); lines.push(`${S_BAR}`); lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`); } const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`; lines.push(searchLine); lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`); lines.push(`${S_BAR}`); const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible)); const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible); const visibleItems = filtered.slice(visibleStart, visibleEnd); if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`); else { for (let i = 0; i < visibleItems.length; i++) { const item = visibleItems[i]; const actualIndex = visibleStart + i; const isSelected = selected.has(item.value); const isCursor = actualIndex === cursor; const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE; const label = isCursor ? import_picocolors.default.underline(item.label) : item.label; const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : ""; const prefix = isCursor ? import_picocolors.default.cyan("❯") : " "; lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`); } const hiddenBefore = visibleStart; const hiddenAfter = filtered.length - visibleEnd; if (hiddenBefore > 0 || hiddenAfter > 0) { const parts = []; if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`); if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`); lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`); } } lines.push(`${S_BAR}`); const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)]; if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`); else { const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`; lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`); } lines.push(`${import_picocolors.default.dim("└")}`); } else if (state === "submit") { const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)]; lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`); } else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`); process.stdout.write(lines.join("\n") + "\n"); lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns); }; const cleanup = () => { process.stdin.removeListener("keypress", keypressHandler); if (process.stdin.isTTY) process.stdin.setRawMode(false); rl.close(); }; const submit = () => { if (required && selected.size === 0 && lockedValues.length === 0) return; render("submit"); cleanup(); resolve([...lockedValues, ...Array.from(selected)]); }; const cancel = () => { render("cancel"); cleanup(); resolve(cancelSymbol); }; const keypressHandler = (_str, key) => { if (!key) return; const filtered = getFiltered(); if (key.name === "return") { submit(); return; } if (key.name === "escape" || key.ctrl && key.name === "c") { cancel(); return; } if (key.name === "up") { cursor = Math.max(0, cursor - 1); render(); return; } if (key.name === "down") { cursor = Math.min(filtered.length - 1, cursor + 1); render(); return; } if (key.name === "space") { const item = filtered[cursor]; if (item) if (selected.has(item.value)) selected.delete(item.value); else selected.add(item.value); render(); return; } if (key.name === "backspace") { query = query.slice(0, -1); cursor = 0; render(); return; } if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { query += key.sequence; cursor = 0; render(); return; } }; process.stdin.on("keypress", keypressHandler); render(); }); } const DEFAULT_CLONE_TIMEOUT_MS = 3e5; const CLONE_TIMEOUT_MS = (() => { const raw = process.env.SKILLS_CLONE_TIMEOUT_MS; if (!raw) return DEFAULT_CLONE_TIMEOUT_MS; const parsed = Number.parseInt(raw, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS; })(); var GitCloneError = class extends Error { url; isTimeout; isAuthError; constructor(message, url, isTimeout = false, isAuthError = false) { super(message); this.name = "GitCloneError"; this.url = url; this.isTimeout = isTimeout; this.isAuthError = isAuthError; } }; async function cloneRepo(url, ref) { const tempDir = await mkdtemp(join(tmpdir(), "skills-")); const git = esm_default({ timeout: { block: CLONE_TIMEOUT_MS }, env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_LFS_SKIP_SMUDGE: "1" }, config: [ "filter.lfs.required=false", "filter.lfs.smudge=", "filter.lfs.clean=", "filter.lfs.process=" ] }); const cloneOptions = ref ? [ "--depth", "1", "--branch", ref ] : ["--depth", "1"]; try { await git.clone(url, tempDir, cloneOptions); return tempDir; } catch (error) { await rm(tempDir, { recursive: true, force: true }).catch(() => {}); const errorMessage = error instanceof Error ? error.message : String(error); const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out"); const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found"); if (isTimeout) throw new GitCloneError(`Clone timed out after ${Math.round(CLONE_TIMEOUT_MS / 1e3)}s. Common causes:\n - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n - Slow network: retry, or clone manually and pass the local path to 'skills add'\n - Private repo without credentials: ensure auth is configured\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false); 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); throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false); } } async function cleanupTempDir(dir) { const normalizedDir = normalize(resolve(dir)); const normalizedTmpDir = normalize(resolve(tmpdir())); if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory"); await rm(dir, { recursive: true, force: true }); } function parseFrontmatter(raw) { const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!match) return { data: {}, content: raw }; return { data: parse(match[1]) ?? {}, content: match[2] ?? "" }; } function isContainedIn(targetPath, basePath) { const normalizedBase = normalize(resolve(basePath)); const normalizedTarget = normalize(resolve(targetPath)); return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase; } function isValidRelativePath(path) { return path.startsWith("./"); } async function getPluginSkillPaths(basePath) { const searchDirs = []; const addPluginSkillPaths = (pluginBase, skills) => { if (!isContainedIn(pluginBase, basePath)) return; if (skills && skills.length > 0) for (const skillPath of skills) { if (!isValidRelativePath(skillPath)) continue; const skillDir = dirname(join(pluginBase, skillPath)); if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir); } searchDirs.push(join(pluginBase, "skills")); }; try { const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8"); const manifest = JSON.parse(content); const pluginRoot = manifest.metadata?.pluginRoot; if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) { if (typeof plugin.source !== "string" && plugin.source !== void 0) continue; if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue; addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills); } } catch {} try { const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8"); addPluginSkillPaths(basePath, JSON.parse(content).skills); } catch {} return searchDirs; } async function getPluginGroupings(basePath) { const groupings = /* @__PURE__ */ new Map(); try { const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8"); const manifest = JSON.parse(content); const pluginRoot = manifest.metadata?.pluginRoot; if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) { if (!plugin.name) continue; if (typeof plugin.source !== "string" && plugin.source !== void 0) continue; if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue; const pluginBase = join(basePath, pluginRoot ?? "", plugin.source ?? ""); if (!isContainedIn(pluginBase, basePath)) continue; if (plugin.skills && plugin.skills.length > 0) for (const skillPath of plugin.skills) { if (!isValidRelativePath(skillPath)) continue; const skillDir = join(pluginBase, skillPath); if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), plugin.name); } } } catch {} try { const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8"); const manifest = JSON.parse(content); if (manifest.name && manifest.skills && manifest.skills.length > 0) for (const skillPath of manifest.skills) { if (!isValidRelativePath(skillPath)) continue; const skillDir = join(basePath, skillPath); if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), manifest.name); } } catch {} return groupings; } const SKIP_DIRS = [ "node_modules", ".git", "dist", "build", "__pycache__" ]; function shouldInstallInternalSkills() { const envValue = process.env.INSTALL_INTERNAL_SKILLS; return envValue === "1" || envValue === "true"; } async function hasSkillMd(dir) { try { return (await stat(join(dir, "SKILL.md"))).isFile(); } catch { return false; } } async function parseSkillMd(skillMdPath, options) { try { const content = await readFile(skillMdPath, "utf-8"); const { data } = parseFrontmatter(content); if (!data.name || !data.description) return null; if (typeof data.name !== "string" || typeof data.description !== "string") return null; if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null; return { name: sanitizeMetadata(data.name), description: sanitizeMetadata(data.description), path: dirname(skillMdPath), rawContent: content, metadata: data.metadata }; } catch { return null; } } async function findSkillDirs(dir, depth = 0, maxDepth = 5) { if (depth > maxDepth) return []; try { const [hasSkill, entries] = await Promise.all([hasSkillMd(dir), readdir(dir, { withFileTypes: true }).catch(() => [])]); const currentDir = hasSkill ? [dir] : []; 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))); return [...currentDir, ...subDirResults.flat()]; } catch { return []; } } function isSubpathSafe(basePath, subpath) { const normalizedBase = normalize(resolve(basePath)); const normalizedTarget = normalize(resolve(join(basePath, subpath))); return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase; } async function discoverSkills(basePath, subpath, options) { const skills = []; const seenNames = /* @__PURE__ */ new Set(); 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.`); const searchPath = subpath ? join(basePath, subpath) : basePath; const pluginGroupings = await getPluginGroupings(searchPath); const enhanceSkill = (skill) => { const resolvedPath = resolve(skill.path); if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath); return skill; }; if (await hasSkillMd(searchPath)) { let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options); if (skill) { skill = enhanceSkill(skill); skills.push(skill); seenNames.add(skill.name); if (!options?.fullDepth) return skills; } } const prioritySearchDirs = [ searchPath, join(searchPath, "skills"), join(searchPath, "skills/.curated"), join(searchPath, "skills/.experimental"), join(searchPath, "skills/.system"), join(searchPath, ".agents/skills"), join(searchPath, ".claude/skills"), join(searchPath, ".cline/skills"), join(searchPath, ".codebuddy/skills"), join(searchPath, ".codex/skills"), join(searchPath, ".commandcode/skills"), join(searchPath, ".continue/skills"), join(searchPath, ".github/skills"), join(searchPath, ".goose/skills"), join(searchPath, ".iflow/skills"), join(searchPath, ".junie/skills"), join(searchPath, ".kilocode/skills"), join(searchPath, ".kiro/skills"), join(searchPath, ".mux/skills"), join(searchPath, ".neovate/skills"), join(searchPath, ".opencode/skills"), join(searchPath, ".openhands/skills"), join(searchPath, ".pi/skills"), join(searchPath, ".qoder/skills"), join(searchPath, ".roo/skills"), join(searchPath, ".trae/skills"), join(searchPath, ".windsurf/skills"), join(searchPath, ".zencoder/skills") ]; prioritySearchDirs.push(...await getPluginSkillPaths(searchPath)); for (const dir of prioritySearchDirs) try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) if (entry.isDirectory()) { const skillDir = join(dir, entry.name); if (await hasSkillMd(skillDir)) { let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options); if (skill && !seenNames.has(skill.name)) { skill = enhanceSkill(skill); skills.push(skill); seenNames.add(skill.name); } } } } catch {} if (skills.length === 0 || options?.fullDepth) { const allSkillDirs = await findSkillDirs(searchPath); for (const skillDir of allSkillDirs) { let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options); if (skill && !seenNames.has(skill.name)) { skill = enhanceSkill(skill); skills.push(skill); seenNames.add(skill.name); } } } return skills; } function getSkillDisplayName(skill) { return skill.name || basename(skill.path); } function filterSkills(skills, inputNames) { const normalizedInputs = inputNames.map((n) => n.toLowerCase()); return skills.filter((skill) => { const name = skill.name.toLowerCase(); const displayName = getSkillDisplayName(skill).toLowerCase(); return normalizedInputs.some((input) => input === name || input === displayName); }); } const home = homedir(); const configHome = xdgConfig ?? join(home, ".config"); const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex"); const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"); const vibeHome = process.env.VIBE_HOME?.trim() || join(home, ".vibe"); function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) { if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills"); if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills"); if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills"); return join(homeDir, ".openclaw/skills"); } const agents = { "aider-desk": { name: "aider-desk", displayName: "AiderDesk", skillsDir: ".aider-desk/skills", globalSkillsDir: join(home, ".aider-desk/skills"), detectInstalled: async () => { return existsSync(join(home, ".aider-desk")); } }, amp: { name: "amp", displayName: "Amp", skillsDir: ".agents/skills", globalSkillsDir: join(configHome, "agents/skills"), detectInstalled: async () => { return existsSync(join(configHome, "amp")); } }, antigravity: { name: "antigravity", displayName: "Antigravity", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".gemini/antigravity/skills"), detectInstalled: async () => { return existsSync(join(home, ".gemini/antigravity")); } }, augment: { name: "augment", displayName: "Augment", skillsDir: ".augment/skills", globalSkillsDir: join(home, ".augment/skills"), detectInstalled: async () => { return existsSync(join(home, ".augment")); } }, bob: { name: "bob", displayName: "IBM Bob", skillsDir: ".bob/skills", globalSkillsDir: join(home, ".bob/skills"), detectInstalled: async () => { return existsSync(join(home, ".bob")); } }, "claude-code": { name: "claude-code", displayName: "Claude Code", skillsDir: ".claude/skills", globalSkillsDir: join(claudeHome, "skills"), detectInstalled: async () => { return existsSync(claudeHome); } }, openclaw: { name: "openclaw", displayName: "OpenClaw", skillsDir: "skills", globalSkillsDir: getOpenClawGlobalSkillsDir(), detectInstalled: async () => { return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot")); } }, cline: { name: "cline", displayName: "Cline", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".agents", "skills"), detectInstalled: async () => { return existsSync(join(home, ".cline")); } }, "codearts-agent": { name: "codearts-agent", displayName: "CodeArts Agent", skillsDir: ".codeartsdoer/skills", globalSkillsDir: join(home, ".codeartsdoer/skills"), detectInstalled: async () => { return existsSync(join(home, ".codeartsdoer")); } }, codebuddy: { name: "codebuddy", displayName: "CodeBuddy", skillsDir: ".codebuddy/skills", globalSkillsDir: join(home, ".codebuddy/skills"), detectInstalled: async () => { return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy")); } }, codemaker: { name: "codemaker", displayName: "Codemaker", skillsDir: ".codemaker/skills", globalSkillsDir: join(home, ".codemaker/skills"), detectInstalled: async () => { return existsSync(join(home, ".codemaker")); } }, codestudio: { name: "codestudio", displayName: "Code Studio", skillsDir: ".codestudio/skills", globalSkillsDir: join(home, ".codestudio/skills"), detectInstalled: async () => { return existsSync(join(home, ".codestudio")); } }, codex: { name: "codex", displayName: "Codex", skillsDir: ".agents/skills", globalSkillsDir: join(codexHome, "skills"), detectInstalled: async () => { return existsSync(codexHome) || existsSync("/etc/codex"); } }, "command-code": { name: "command-code", displayName: "Command Code", skillsDir: ".commandcode/skills", globalSkillsDir: join(home, ".commandcode/skills"), detectInstalled: async () => { return existsSync(join(home, ".commandcode")); } }, continue: { name: "continue", displayName: "Continue", skillsDir: ".continue/skills", globalSkillsDir: join(home, ".continue/skills"), detectInstalled: async () => { return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue")); } }, cortex: { name: "cortex", displayName: "Cortex Code", skillsDir: ".cortex/skills", globalSkillsDir: join(home, ".snowflake/cortex/skills"), detectInstalled: async () => { return existsSync(join(home, ".snowflake/cortex")); } }, crush: { name: "crush", displayName: "Crush", skillsDir: ".crush/skills", globalSkillsDir: join(home, ".config/crush/skills"), detectInstalled: async () => { return existsSync(join(home, ".config/crush")); } }, cursor: { name: "cursor", displayName: "Cursor", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".cursor/skills"), detectInstalled: async () => { return existsSync(join(home, ".cursor")); } }, deepagents: { name: "deepagents", displayName: "Deep Agents", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".deepagents/agent/skills"), detectInstalled: async () => { return existsSync(join(home, ".deepagents")); } }, devin: { name: "devin", displayName: "Devin for Terminal", skillsDir: ".devin/skills", globalSkillsDir: join(configHome, "devin/skills"), detectInstalled: async () => { return existsSync(join(configHome, "devin")); } }, dexto: { name: "dexto", displayName: "Dexto", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".agents/skills"), detectInstalled: async () => { return existsSync(join(home, ".dexto")); } }, droid: { name: "droid", displayName: "Droid", skillsDir: ".factory/skills", globalSkillsDir: join(home, ".factory/skills"), detectInstalled: async () => { return existsSync(join(home, ".factory")); } }, firebender: { name: "firebender", displayName: "Firebender", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".firebender/skills"), detectInstalled: async () => { return existsSync(join(home, ".firebender")); } }, forgecode: { name: "forgecode", displayName: "ForgeCode", skillsDir: ".forge/skills", globalSkillsDir: join(home, ".forge/skills"), detectInstalled: async () => { return existsSync(join(home, ".forge")); } }, "gemini-cli": { name: "gemini-cli", displayName: "Gemini CLI", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".gemini/skills"), detectInstalled: async () => { return existsSync(join(home, ".gemini")); } }, "github-copilot": { name: "github-copilot", displayName: "GitHub Copilot", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".copilot/skills"), detectInstalled: async () => { return existsSync(join(home, ".copilot")); } }, goose: { name: "goose", displayName: "Goose", skillsDir: ".goose/skills", globalSkillsDir: join(configHome, "goose/skills"), detectInstalled: async () => { return existsSync(join(configHome, "goose")); } }, "hermes-agent": { name: "hermes-agent", displayName: "Hermes Agent", skillsDir: ".hermes/skills", globalSkillsDir: join(home, ".hermes/skills"), detectInstalled: async () => { return existsSync(join(home, ".hermes")); } }, junie: { name: "junie", displayName: "Junie", skillsDir: ".junie/skills", globalSkillsDir: join(home, ".junie/skills"), detectInstalled: async () => { return existsSync(join(home, ".junie")); } }, "iflow-cli": { name: "iflow-cli", displayName: "iFlow CLI", skillsDir: ".iflow/skills", globalSkillsDir: join(home, ".iflow/skills"), detectInstalled: async () => { return existsSync(join(home, ".iflow")); } }, kilo: { name: "kilo", displayName: "Kilo Code", skillsDir: ".kilocode/skills", globalSkillsDir: join(home, ".kilocode/skills"), detectInstalled: async () => { return existsSync(join(home, ".kilocode")); } }, "kimi-cli": { name: "kimi-cli", displayName: "Kimi Code CLI", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".config/agents/skills"), detectInstalled: async () => { return existsSync(join(home, ".kimi")); } }, "kiro-cli": { name: "kiro-cli", displayName: "Kiro CLI", skillsDir: ".kiro/skills", globalSkillsDir: join(home, ".kiro/skills"), detectInstalled: async () => { return existsSync(join(home, ".kiro")); } }, kode: { name: "kode", displayName: "Kode", skillsDir: ".kode/skills", globalSkillsDir: join(home, ".kode/skills"), detectInstalled: async () => { return existsSync(join(home, ".kode")); } }, mcpjam: { name: "mcpjam", displayName: "MCPJam", skillsDir: ".mcpjam/skills", globalSkillsDir: join(home, ".mcpjam/skills"), detectInstalled: async () => { return existsSync(join(home, ".mcpjam")); } }, "mistral-vibe": { name: "mistral-vibe", displayName: "Mistral Vibe", skillsDir: ".vibe/skills", globalSkillsDir: join(vibeHome, "skills"), detectInstalled: async () => { return existsSync(vibeHome); } }, mux: { name: "mux", displayName: "Mux", skillsDir: ".mux/skills", globalSkillsDir: join(home, ".mux/skills"), detectInstalled: async () => { return existsSync(join(home, ".mux")); } }, opencode: { name: "opencode", displayName: "OpenCode", skillsDir: ".agents/skills", globalSkillsDir: join(configHome, "opencode/skills"), detectInstalled: async () => { return existsSync(join(configHome, "opencode")); } }, openhands: { name: "openhands", displayName: "OpenHands", skillsDir: ".openhands/skills", globalSkillsDir: join(home, ".openhands/skills"), detectInstalled: async () => { return existsSync(join(home, ".openhands")); } }, pi: { name: "pi", displayName: "Pi", skillsDir: ".pi/skills", globalSkillsDir: join(home, ".pi/agent/skills"), detectInstalled: async () => { return existsSync(join(home, ".pi/agent")); } }, qoder: { name: "qoder", displayName: "Qoder", skillsDir: ".qoder/skills", globalSkillsDir: join(home, ".qoder/skills"), detectInstalled: async () => { return existsSync(join(home, ".qoder")); } }, "qwen-code": { name: "qwen-code", displayName: "Qwen Code", skillsDir: ".qwen/skills", globalSkillsDir: join(home, ".qwen/skills"), detectInstalled: async () => { return existsSync(join(home, ".qwen")); } }, replit: { name: "replit", displayName: "Replit", skillsDir: ".agents/skills", globalSkillsDir: join(configHome, "agents/skills"), showInUniversalList: false, detectInstalled: async () => { return existsSync(join(process.cwd(), ".replit")); } }, rovodev: { name: "rovodev", displayName: "Rovo Dev", skillsDir: ".rovodev/skills", globalSkillsDir: join(home, ".rovodev/skills"), detectInstalled: async () => { return existsSync(join(home, ".rovodev")); } }, roo: { name: "roo", displayName: "Roo Code", skillsDir: ".roo/skills", globalSkillsDir: join(home, ".roo/skills"), detectInstalled: async () => { return existsSync(join(home, ".roo")); } }, "tabnine-cli": { name: "tabnine-cli", displayName: "Tabnine CLI", skillsDir: ".tabnine/agent/skills", globalSkillsDir: join(home, ".tabnine/agent/skills"), detectInstalled: async () => { return existsSync(join(home, ".tabnine")); } }, trae: { name: "trae", displayName: "Trae", skillsDir: ".trae/skills", globalSkillsDir: join(home, ".trae/skills"), detectInstalled: async () => { return existsSync(join(home, ".trae")); } }, "trae-cn": { name: "trae-cn", displayName: "Trae CN", skillsDir: ".trae/skills", globalSkillsDir: join(home, ".trae-cn/skills"), detectInstalled: async () => { return existsSync(join(home, ".trae-cn")); } }, warp: { name: "warp", displayName: "Warp", skillsDir: ".agents/skills", globalSkillsDir: join(home, ".agents/skills"), detectInstalled: async () => { return existsSync(join(home, ".warp")); } }, windsurf: { name: "windsurf", displayName: "Windsurf", skillsDir: ".windsurf/skills", globalSkillsDir: join(home, ".codeium/windsurf/skills"), detectInstalled: async () => { return existsSync(join(home, ".codeium/windsurf")); } }, zencoder: { name: "zencoder", displayName: "Zencoder", skillsDir: ".zencoder/skills", globalSkillsDir: join(home, ".zencoder/skills"), detectInstalled: async () => { return existsSync(join(home, ".zencoder")); } }, neovate: { name: "neovate", displayName: "Neovate", skillsDir: ".neovate/skills", globalSkillsDir: join(home, ".neovate/skills"), detectInstalled: async () => { return existsSync(join(home, ".neovate")); } }, pochi: { name: "pochi", displayName: "Pochi", skillsDir: ".pochi/skills", globalSkillsDir: join(home, ".pochi/skills"), detectInstalled: async () => { return existsSync(join(home, ".pochi")); } }, adal: { name: "adal", displayName: "AdaL", skillsDir: ".adal/skills", globalSkillsDir: join(home, ".adal/skills"), detectInstalled: async () => { return existsSync(join(home, ".adal")); } }, universal: { name: "universal", displayName: "Universal", skillsDir: ".agents/skills", globalSkillsDir: join(configHome, "agents/skills"), showInUniversalList: false, detectInstalled: async () => false } }; async function detectInstalledAgents() { return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({ type, installed: await config.detectInstalled() })))).filter((r) => r.installed).map((r) => r.type); } function getUniversalAgents() { return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type); } function getNonUniversalAgents() { return Object.entries(agents).filter(([_, config]) => config.skillsDir !== ".agents/skills").map(([type]) => type); } function isUniversalAgent(type) { return agents[type].skillsDir === ".agents/skills"; } const AGENTS_DIR$2 = ".agents"; const SKILLS_SUBDIR = "skills"; function sanitizeName(name) { return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill"; } function isPathSafe(basePath, targetPath) { const normalizedBase = normalize(resolve(basePath)); const normalizedTarget = normalize(resolve(targetPath)); return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase; } async function isDirEntryOrSymlinkToDir(entry, entryPath) { if (entry.isDirectory()) return true; if (!entry.isSymbolicLink()) return false; try { return (await stat(entryPath)).isDirectory(); } catch { return false; } } function getCanonicalSkillsDir(global, cwd) { return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$2, SKILLS_SUBDIR); } function getAgentBaseDir(agentType, global, cwd) { if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd); const agent = agents[agentType]; const baseDir = global ? homedir() : cwd || process.cwd(); if (global) { if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir); return agent.globalSkillsDir; } return join(baseDir, agent.skillsDir); } function resolveSymlinkTarget(linkPath, linkTarget) { return resolve(dirname(linkPath), linkTarget); } async function cleanAndCreateDirectory(path) { try { await rm(path, { recursive: true, force: true }); } catch {} await mkdir(path, { recursive: true }); } async function resolveParentSymlinks(path) { const resolved = resolve(path); const dir = dirname(resolved); const base = basename(resolved); try { return join(await realpath(dir), base); } catch { return resolved; } } async function createSymlink(target, linkPath) { try { const resolvedTarget = resolve(target); const resolvedLinkPath = resolve(linkPath); const [realTarget, realLinkPath] = await Promise.all([realpath(resolvedTarget).catch(() => resolvedTarget), realpath(resolvedLinkPath).catch(() => resolvedLinkPath)]); if (realTarget === realLinkPath) return true; if (await resolveParentSymlinks(target) === await resolveParentSymlinks(linkPath)) return true; try { if ((await lstat(linkPath)).isSymbolicLink()) { if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true; await rm(linkPath); } else await rm(linkPath, { recursive: true }); } catch (err) { if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try { await rm(linkPath, { force: true }); } catch {} } const linkDir = dirname(linkPath); await mkdir(linkDir, { recursive: true }); await symlink(relative(await resolveParentSymlinks(linkDir), target), linkPath, platform() === "win32" ? "junction" : void 0); return true; } catch { return false; } } async function installSkillForAgent(skill, agentType, options = {}) { const agent = agents[agentType]; const isGlobal = options.global ?? false; const cwd = options.cwd || process.cwd(); if (isGlobal && agent.globalSkillsDir === void 0) return { success: false, path: "", mode: options.mode ?? "symlink", error: `${agent.displayName} does not support global skill installation` }; const skillName = sanitizeName(skill.name || basename(skill.path)); const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd); const canonicalDir = join(canonicalBase, skillName); const agentBase = getAgentBaseDir(agentType, isGlobal, cwd); const agentDir = join(agentBase, skillName); const installMode = options.mode ?? "symlink"; if (!isPathSafe(canonicalBase, canonicalDir)) return { success: false, path: agentDir, mode: installMode, error: "Invalid skill name: potential path traversal detected" }; if (!isPathSafe(agentBase, agentDir)) return { success: false, path: agentDir, mode: installMode, error: "Invalid skill name: potential path traversal detected" }; try { if (installMode === "copy") { await cleanAndCreateDirectory(agentDir); await copyDirectory(skill.path, agentDir); return { success: true, path: agentDir, mode: "copy" }; } await cleanAndCreateDirectory(canonicalDir); await copyDirectory(skill.path, canonicalDir); if (isGlobal && isUniversalAgent(agentType)) return { success: true, path: canonicalDir, canonicalPath: canonicalDir, mode: "symlink" }; if (!isGlobal && !isUniversalAgent(agentType)) { if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0]))) return { success: true, path: canonicalDir, canonicalPath: canonicalDir, mode: "symlink", skipped: true }; } if (!await createSymlink(canonicalDir, agentDir)) { await cleanAndCreateDirectory(agentDir); await copyDirectory(skill.path, agentDir); return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink", symlinkFailed: true }; } return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink" }; } catch (error) { return { success: false, path: agentDir, mode: installMode, error: error instanceof Error ? error.message : "Unknown error" }; } } const EXCLUDE_FILES = new Set(["metadata.json"]); const EXCLUDE_DIRS = new Set([ ".git", "__pycache__", "__pypackages__" ]); const isExcluded = (name, isDirectory = false) => { if (EXCLUDE_FILES.has(name)) return true; if (isDirectory && EXCLUDE_DIRS.has(name)) return true; return false; }; async function copyDirectory(src, dest) { await mkdir(dest, { recursive: true }); const entries = await readdir(src, { withFileTypes: true }); await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); if (entry.isDirectory()) await copyDirectory(srcPath, destPath); else try { await cp(srcPath, destPath, { dereference: true, recursive: true }); } catch (err) { if (err instanceof Error && "code" in err && err.code === "ENOENT" && entry.isSymbolicLink()) console.warn(`Skipping broken symlink: ${srcPath}`); else throw err; } })); } async function isSkillInstalled(skillName, agentType, options = {}) { const agent = agents[agentType]; const sanitized = sanitizeName(skillName); if (options.global && agent.globalSkillsDir === void 0) return false; const targetBase = options.global ? agent.globalSkillsDir : join(options.cwd || process.cwd(), agent.skillsDir); const skillDir = join(targetBase, sanitized); if (!isPathSafe(targetBase, skillDir)) return false; try { await access(skillDir); return true; } catch { return false; } } function getInstallPath(skillName, agentType, options = {}) { agents[agentType]; options.cwd || process.cwd(); const sanitized = sanitizeName(skillName); const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd); const installPath = join(targetBase, sanitized); if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected"); return installPath; } function getCanonicalPath(skillName, options = {}) { const sanitized = sanitizeName(skillName); const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd); const canonicalPath = join(canonicalBase, sanitized); if (!isPathSafe(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected"); return canonicalPath; } async function installWellKnownSkillForAgent(skill, agentType, options = {}) { const agent = agents[agentType]; const isGlobal = options.global ?? false; const cwd = options.cwd || process.cwd(); const installMode = options.mode ?? "symlink"; if (isGlobal && agent.globalSkillsDir === void 0) return { success: false, path: "", mode: installMode, error: `${agent.displayName} does not support global skill installation` }; const skillName = sanitizeName(skill.installName); const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd); const canonicalDir = join(canonicalBase, skillName); const agentBase = getAgentBaseDir(agentType, isGlobal, cwd); const agentDir = join(agentBase, skillName); if (!isPathSafe(canonicalBase, canonicalDir)) return { success: false, path: agentDir, mode: installMode, error: "Invalid skill name: potential path traversal detected" }; if (!isPathSafe(agentBase, agentDir)) return { success: false, path: agentDir, mode: installMode, error: "Invalid skill name: potential path traversal detected" }; async function writeSkillFiles(targetDir) { for (const [filePath, content] of skill.files) { const fullPath = join(targetDir, filePath); if (!isPathSafe(targetDir, fullPath)) continue; const parentDir = dirname(fullPath); if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true }); await writeFile(fullPath, content); } } try { if (installMode === "copy") { await cleanAndCreateDirectory(agentDir); await writeSkillFiles(agentDir); return { success: true, path: agentDir, mode: "copy" }; } await cleanAndCreateDirectory(canonicalDir); await writeSkillFiles(canonicalDir); if (isGlobal && isUniversalAgent(agentType)) return { success: true, path: canonicalDir, canonicalPath: canonicalDir, mode: "symlink" }; if (!await createSymlink(canonicalDir, agentDir)) { await cleanAndCreateDirectory(agentDir); await writeSkillFiles(agentDir); return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink", symlinkFailed: true }; } return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink" }; } catch (error) { return { success: false, path: agentDir, mode: installMode, error: error instanceof Error ? error.message : "Unknown error" }; } } async function installBlobSkillForAgent(skill, agentType, options = {}) { const agent = agents[agentType]; const isGlobal = options.global ?? false; const cwd = options.cwd || process.cwd(); const installMode = options.mode ?? "symlink"; if (isGlobal && agent.globalSkillsDir === void 0) return { success: false, path: "", mode: installMode, error: `${agent.displayName} does not support global skill installation` }; const skillName = sanitizeName(skill.installName); const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd); const canonicalDir = join(canonicalBase, skillName); const agentBase = getAgentBaseDir(agentType, isGlobal, cwd); const agentDir = join(agentBase, skillName); if (!isPathSafe(canonicalBase, canonicalDir)) return { success: false, path: agentDir, mode: installMode, error: "Invalid skill name: potential path traversal detected" }; if (!isPathSafe(agentBase, agentDir)) return { success: false, path: agentDir, mode: installMode, error: "Invalid skill name: potential path traversal detected" }; async function writeSkillFiles(targetDir) { for (const file of skill.files) { const fullPath = join(targetDir, file.path); if (!isPathSafe(targetDir, fullPath)) continue; const parentDir = dirname(fullPath); if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true }); await writeFile(fullPath, file.contents, "utf-8"); } } try { if (installMode === "copy") { await cleanAndCreateDirectory(agentDir); await writeSkillFiles(agentDir); return { success: true, path: agentDir, mode: "copy" }; } await cleanAndCreateDirectory(canonicalDir); await writeSkillFiles(canonicalDir); if (isGlobal && isUniversalAgent(agentType)) return { success: true, path: canonicalDir, canonicalPath: canonicalDir, mode: "symlink" }; if (!isGlobal && !isUniversalAgent(agentType)) { if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0]))) return { success: true, path: canonicalDir, canonicalPath: canonicalDir, mode: "symlink", skipped: true }; } if (!await createSymlink(canonicalDir, agentDir)) { await cleanAndCreateDirectory(agentDir); await writeSkillFiles(agentDir); return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink", symlinkFailed: true }; } return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink" }; } catch (error) { return { success: false, path: agentDir, mode: installMode, error: error instanceof Error ? error.message : "Unknown error" }; } } async function listInstalledSkills(options = {}) { const cwd = options.cwd || process.cwd(); const skillsMap = /* @__PURE__ */ new Map(); const scopes = []; const detectedAgents = await detectInstalledAgents(); const agentFilter = options.agentFilter; const agentsToCheck = agentFilter ? detectedAgents.filter((a) => agentFilter.includes(a)) : detectedAgents; const scopeTypes = []; if (options.global === void 0) scopeTypes.push({ global: false }, { global: true }); else scopeTypes.push({ global: options.global }); for (const { global: isGlobal } of scopeTypes) { scopes.push({ global: isGlobal, path: getCanonicalSkillsDir(isGlobal, cwd) }); for (const agentType of agentsToCheck) { const agent = agents[agentType]; if (isGlobal && agent.globalSkillsDir === void 0) continue; const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir); if (!scopes.some((s) => s.path === agentDir && s.global === isGlobal)) scopes.push({ global: isGlobal, path: agentDir, agentType }); } const allAgentTypes = Object.keys(agents); for (const agentType of allAgentTypes) { if (agentsToCheck.includes(agentType)) continue; const agent = agents[agentType]; if (isGlobal && agent.globalSkillsDir === void 0) continue; const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir); if (scopes.some((s) => s.path === agentDir && s.global === isGlobal)) continue; if (existsSync(agentDir)) scopes.push({ global: isGlobal, path: agentDir, agentType }); } } for (const scope of scopes) try { const entries = await readdir(scope.path, { withFileTypes: true }); for (const entry of entries) { const skillDir = join(scope.path, entry.name); if (!await isDirEntryOrSymlinkToDir(entry, skillDir)) continue; const skillMdPath = join(skillDir, "SKILL.md"); try { await stat(skillMdPath); } catch { continue; } const skill = await parseSkillMd(skillMdPath); if (!skill) continue; const scopeKey = scope.global ? "global" : "project"; const skillKey = `${scopeKey}:${skill.name}`; if (scope.agentType) { if (skillsMap.has(skillKey)) { const existing = skillsMap.get(skillKey); if (!existing.agents.includes(scope.agentType)) existing.agents.push(scope.agentType); } else skillsMap.set(skillKey, { name: skill.name, description: skill.description, path: skillDir, canonicalPath: skillDir, scope: scopeKey, agents: [scope.agentType] }); continue; } const sanitizedSkillName = sanitizeName(skill.name); const installedAgents = []; for (const agentType of agentsToCheck) { const agent = agents[agentType]; if (scope.global && agent.globalSkillsDir === void 0) continue; const agentBase = scope.global ? agent.globalSkillsDir : join(cwd, agent.skillsDir); let found = false; const possibleNames = Array.from(new Set([ entry.name, sanitizedSkillName, skill.name.toLowerCase().replace(/\s+/g, "-").replace(/[\/\\:\0]/g, "") ])); for (const possibleName of possibleNames) { const agentSkillDir = join(agentBase, possibleName); if (!isPathSafe(agentBase, agentSkillDir)) continue; try { await access(agentSkillDir); found = true; break; } catch {} } if (!found) try { const agentEntries = await readdir(agentBase, { withFileTypes: true }); for (const agentEntry of agentEntries) { const candidateDir = join(agentBase, agentEntry.name); if (!await isDirEntryOrSymlinkToDir(agentEntry, candidateDir)) continue; if (!isPathSafe(agentBase, candidateDir)) continue; try { const candidateSkillMd = join(candidateDir, "SKILL.md"); await stat(candidateSkillMd); const candidateSkill = await parseSkillMd(candidateSkillMd); if (candidateSkill && candidateSkill.name === skill.name) { found = true; break; } } catch {} } } catch {} if (found) installedAgents.push(agentType); } if (skillsMap.has(skillKey)) { const existing = skillsMap.get(skillKey); for (const agent of installedAgents) if (!existing.agents.includes(agent)) existing.agents.push(agent); } else skillsMap.set(skillKey, { name: skill.name, description: skill.description, path: skillDir, canonicalPath: skillDir, scope: scopeKey, agents: installedAgents }); } } catch {} return Array.from(skillsMap.values()); } const TELEMETRY_URL = "https://add-skill.vercel.sh/t"; const AUDIT_URL = "https://add-skill.vercel.sh/audit"; let cliVersion = null; let detectedAgentName = null; function setDetectedAgent(agentName) { detectedAgentName = agentName; } function isCI() { 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); } function isEnabled() { return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK; } function setVersion(version) { cliVersion = version; } async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) { if (skillSlugs.length === 0) return null; try { const params = new URLSearchParams({ source, skills: skillSlugs.join(",") }); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal }); clearTimeout(timeout); if (!response.ok) return null; return await response.json(); } catch { return null; } } const pendingTelemetry = []; function track(data) { if (!isEnabled()) return; try { const params = new URLSearchParams(); if (cliVersion) params.set("v", cliVersion); if (isCI()) params.set("ci", "1"); if (detectedAgentName) params.set("agent", detectedAgentName); for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value)); const p = fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {}).then(() => {}); pendingTelemetry.push(p); } catch {} } async function flushTelemetry(timeoutMs = 5e3) { if (pendingTelemetry.length === 0) return; const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs)); await Promise.race([Promise.all(pendingTelemetry), timeout]); } var import_dist = require_dist(); let cachedResult = null; const agentNameToType = { cursor: "cursor", "cursor-cli": "cursor", claude: "claude-code", cowork: "claude-code", devin: "universal", replit: "replit", gemini: "gemini-cli", codex: "codex", antigravity: "antigravity", "augment-cli": "augment", opencode: "opencode", "github-copilot": "github-copilot" }; async function detectAgent() { if (cachedResult) return cachedResult; cachedResult = await (0, import_dist.determineAgent)(); if (cachedResult.isAgent) setDetectedAgent(cachedResult.agent.name); return cachedResult; } async function isRunningInAgent() { return (await detectAgent()).isAgent; } function getAgentType(agentName) { return agentNameToType[agentName] ?? null; } var ProviderRegistryImpl = class { providers = []; register(provider) { if (this.providers.some((p) => p.id === provider.id)) throw new Error(`Provider with id "${provider.id}" already registered`); this.providers.push(provider); } findProvider(url) { for (const provider of this.providers) if (provider.match(url).matches) return provider; return null; } getProviders() { return [...this.providers]; } }; new ProviderRegistryImpl(); const DISCOVERY_SCHEMA_V2 = "https://schemas.agentskills.io/discovery/0.2.0/schema.json"; const MAX_ARCHIVE_UNPACKED_BYTES = 50 * 1024 * 1024; const MAX_ARCHIVE_FILES = 1e3; var WellKnownProvider = class { id = "well-known"; displayName = "Well-Known Skills"; WELL_KNOWN_PATHS = [".well-known/agent-skills", ".well-known/skills"]; INDEX_FILE = "index.json"; match(url) { if (!url.startsWith("http://") && !url.startsWith("https://")) return { matches: false }; try { const parsed = new URL(url); if ([ "github.com", "gitlab.com", "huggingface.co" ].includes(parsed.hostname)) return { matches: false }; return { matches: true, sourceIdentifier: `wellknown/${parsed.hostname}` }; } catch { return { matches: false }; } } async fetchIndex(baseUrl) { return (await this.fetchIndexCandidates(baseUrl))[0] ?? null; } async fetchIndexCandidates(baseUrl) { try { const parsed = new URL(baseUrl); const basePath = parsed.pathname.replace(/\/$/, ""); const urlsToTry = []; for (const wellKnownPath of this.WELL_KNOWN_PATHS) { urlsToTry.push({ indexUrl: `${parsed.protocol}//${parsed.host}${basePath}/${wellKnownPath}/${this.INDEX_FILE}`, baseUrl: `${parsed.protocol}//${parsed.host}${basePath}`, wellKnownPath }); if (basePath && basePath !== "") urlsToTry.push({ indexUrl: `${parsed.protocol}//${parsed.host}/${wellKnownPath}/${this.INDEX_FILE}`, baseUrl: `${parsed.protocol}//${parsed.host}`, wellKnownPath }); } const candidates = []; for (const { indexUrl, baseUrl: resolvedBase, wellKnownPath } of urlsToTry) try { const response = await fetch(indexUrl); if (!response.ok) continue; const rawIndex = await response.json(); const normalized = this.normalizeIndex(rawIndex, indexUrl, wellKnownPath); if (!normalized) continue; candidates.push({ index: normalized.index, entries: normalized.entries, resolvedBaseUrl: resolvedBase, resolvedWellKnownPath: wellKnownPath, indexUrl }); } catch { continue; } return candidates; } catch { return []; } } normalizeIndex(rawIndex, indexUrl, resolvedWellKnownPath) { if (!rawIndex || typeof rawIndex !== "object") return null; const record = rawIndex; if (!Array.isArray(record.skills)) return null; const schema = record.$schema; if (schema === DISCOVERY_SCHEMA_V2) { const entries = []; const v2Entries = []; for (const entry of record.skills) { if (!this.isValidSkillEntryV2(entry)) continue; const artifactUrl = new URL(entry.url, indexUrl).toString(); entries.push({ version: "0.2.0", name: entry.name, description: entry.description, type: entry.type, artifactUrl, digest: entry.digest, indexEntry: entry }); v2Entries.push(entry); } if (entries.length === 0) return null; return { index: { $schema: DISCOVERY_SCHEMA_V2, skills: v2Entries }, entries }; } if (schema !== void 0) return null; const v1Entries = []; const entries = []; for (const entry of record.skills) { if (!this.isValidSkillEntryV1(entry)) return null; v1Entries.push(entry); entries.push({ version: "0.1.0", name: entry.name, description: entry.description, files: entry.files, baseUrl: this.getLegacySkillBaseUrl(indexUrl, resolvedWellKnownPath), wellKnownPath: resolvedWellKnownPath, indexEntry: entry }); } return { index: { skills: v1Entries }, entries }; } getLegacySkillBaseUrl(indexUrl, wellKnownPath) { const parsed = new URL(indexUrl); const marker = `/${wellKnownPath}/${this.INDEX_FILE}`; return `${parsed.protocol}//${parsed.host}${parsed.pathname.slice(0, -marker.length)}`; } isValidSkillName(name) { if (typeof name !== "string") return false; if (name.length < 1 || name.length > 64) return false; if (!/^[a-z0-9-]+$/.test(name)) return false; if (name.startsWith("-") || name.endsWith("-")) return false; if (name.includes("--")) return false; return true; } isSafeLegacyFilePath(filePath) { if (typeof filePath !== "string" || filePath.length === 0) return false; if (filePath.startsWith("/") || filePath.startsWith("\\") || filePath.includes("..")) return false; if (filePath.includes("\0")) return false; return true; } isValidSkillEntryV1(entry) { if (!entry || typeof entry !== "object") return false; const e = entry; if (!this.isValidSkillName(e.name)) return false; if (typeof e.description !== "string" || !e.description) return false; if (!Array.isArray(e.files) || e.files.length === 0) return false; for (const file of e.files) if (!this.isSafeLegacyFilePath(file)) return false; return e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md"); } isValidSkillEntryV2(entry) { if (!entry || typeof entry !== "object") return false; const e = entry; if (!this.isValidSkillName(e.name)) return false; if (typeof e.description !== "string" || !e.description || e.description.length > 1024) return false; if (e.type !== "skill-md" && e.type !== "archive") return false; if (typeof e.url !== "string" || !e.url) return false; if (typeof e.digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(e.digest)) return false; try { new URL(e.url, "https://example.com/.well-known/agent-skills/index.json"); } catch { return false; } return true; } async fetchSkill(url) { try { const parsed = new URL(url); const candidates = await this.fetchIndexCandidates(url); for (const result of candidates) { const { entries } = result; let skillName = null; const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/); if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1]; else if (entries.length === 1) skillName = entries[0].name; if (!skillName) continue; const skillEntry = entries.find((s) => s.name === skillName); if (!skillEntry) continue; const skill = await this.fetchSkillByEntry(skillEntry); if (skill) return skill; } return null; } catch { return null; } } async fetchSkillByEntry(baseUrlOrEntry, legacyEntry, legacyWellKnownPath) { if (typeof baseUrlOrEntry === "string") { if (!legacyEntry) return null; return this.fetchLegacySkillByEntry({ version: "0.1.0", name: legacyEntry.name, description: legacyEntry.description, files: legacyEntry.files, baseUrl: baseUrlOrEntry, wellKnownPath: legacyWellKnownPath ?? this.WELL_KNOWN_PATHS[0], indexEntry: legacyEntry }); } if (baseUrlOrEntry.version === "0.1.0") return this.fetchLegacySkillByEntry(baseUrlOrEntry); return this.fetchArtifactSkillByEntry(baseUrlOrEntry); } async fetchLegacySkillByEntry(entry) { try { const skillBaseUrl = `${entry.baseUrl.replace(/\/$/, "")}/${entry.wellKnownPath}/${entry.name}`; const skillMdUrl = `${skillBaseUrl}/SKILL.md`; const response = await fetch(skillMdUrl); if (!response.ok) return null; const content = await response.text(); const { data } = parseFrontmatter(content); if (typeof data.name !== "string" || typeof data.description !== "string") return null; const files = /* @__PURE__ */ new Map(); files.set("SKILL.md", content); const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => { try { const fileUrl = `${skillBaseUrl}/${filePath}`; const fileResponse = await fetch(fileUrl); if (fileResponse.ok) { const fileContent = await fileResponse.arrayBuffer(); return { path: filePath, content: new Uint8Array(fileContent) }; } } catch {} return null; }); const fileResults = await Promise.all(filePromises); for (const result of fileResults) if (result) files.set(result.path, result.content); return this.createSkill({ name: data.name, description: data.description, content, installName: entry.name, sourceUrl: skillMdUrl, metadata: data.metadata, files, indexEntry: entry.indexEntry }); } catch { return null; } } async fetchArtifactSkillByEntry(entry) { try { const response = await fetch(entry.artifactUrl); if (!response.ok) return null; const contentType = response.headers.get("content-type") ?? ""; const bytes = new Uint8Array(await response.arrayBuffer()); if (this.computeDigest(bytes) !== entry.digest) return null; if (entry.type === "skill-md") { const content = new TextDecoder().decode(bytes); const { data } = parseFrontmatter(content); if (typeof data.name !== "string" || typeof data.description !== "string") return null; const files = /* @__PURE__ */ new Map(); files.set("SKILL.md", content); return this.createSkill({ name: data.name, description: data.description, content, installName: entry.name, sourceUrl: entry.artifactUrl, metadata: data.metadata, files, indexEntry: entry.indexEntry }); } const files = this.extractArchive(bytes, entry.artifactUrl, contentType); const skillMdBytes = files.get("SKILL.md"); if (!skillMdBytes) return null; const content = typeof skillMdBytes === "string" ? skillMdBytes : new TextDecoder().decode(skillMdBytes); files.set("SKILL.md", content); const { data } = parseFrontmatter(content); if (typeof data.name !== "string" || typeof data.description !== "string") return null; return this.createSkill({ name: data.name, description: data.description, content, installName: entry.name, sourceUrl: entry.artifactUrl, metadata: data.metadata, files, indexEntry: entry.indexEntry }); } catch { return null; } } createSkill(input) { return { name: sanitizeMetadata(input.name), description: sanitizeMetadata(input.description), content: input.content, installName: input.installName, sourceUrl: input.sourceUrl, metadata: input.metadata && typeof input.metadata === "object" ? input.metadata : void 0, files: input.files, indexEntry: input.indexEntry }; } async fetchAllSkills(url) { try { const candidates = await this.fetchIndexCandidates(url); for (const result of candidates) { const skillPromises = result.entries.map((entry) => this.fetchSkillByEntry(entry)); const skills = (await Promise.all(skillPromises)).filter((s) => s !== null); if (skills.length > 0) return skills; } return []; } catch { return []; } } computeDigest(bytes) { return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; } extractArchive(bytes, artifactUrl, contentType) { if (this.isZipArchive(bytes, artifactUrl, contentType)) return this.extractZip(bytes); if (this.isTarGzArchive(bytes, artifactUrl, contentType)) return this.extractTarGz(bytes); throw new Error("Unsupported archive format"); } isZipArchive(bytes, artifactUrl, contentType) { return contentType.includes("application/zip") || artifactUrl.toLowerCase().endsWith(".zip") || bytes[0] === 80 && bytes[1] === 75; } isTarGzArchive(bytes, artifactUrl, contentType) { const lower = artifactUrl.toLowerCase(); return contentType.includes("application/gzip") || contentType.includes("application/x-gzip") || lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || bytes[0] === 31 && bytes[1] === 139; } normalizeArchivePath(rawPath) { if (!rawPath || rawPath.includes("\0")) return null; if (rawPath.startsWith("/") || rawPath.startsWith("\\")) return null; if (/^[A-Za-z]:/.test(rawPath)) return null; if (rawPath.includes("\\")) return null; const parts = rawPath.split("/").filter(Boolean); if (parts.length === 0) return null; if (parts.some((part) => part === "." || part === "..")) return null; return parts.join("/"); } addArchiveFile(files, path, content, runningTotal) { const normalizedPath = this.normalizeArchivePath(path); if (!normalizedPath) throw new Error(`Unsafe archive path: ${path}`); runningTotal.bytes += content.byteLength; if (runningTotal.bytes > MAX_ARCHIVE_UNPACKED_BYTES) throw new Error("Archive exceeds maximum unpacked size"); if (files.size >= MAX_ARCHIVE_FILES) throw new Error("Archive contains too many files"); files.set(normalizedPath, content); } extractTarGz(bytes) { const tar = gunzipSync(Buffer.from(bytes)); const files = /* @__PURE__ */ new Map(); const runningTotal = { bytes: 0 }; let offset = 0; while (offset + 512 <= tar.length) { const header = tar.subarray(offset, offset + 512); if (header.every((byte) => byte === 0)) break; const name = this.readTarString(header, 0, 100); const sizeText = this.readTarString(header, 124, 12).trim(); const typeFlag = header[156]; const prefix = this.readTarString(header, 345, 155); const path = prefix ? `${prefix}/${name}` : name; const size = Number.parseInt(sizeText || "0", 8); if (!Number.isFinite(size) || size < 0) throw new Error("Invalid tar entry size"); offset += 512; if (typeFlag === 50 || typeFlag === 49) throw new Error("Archive links are not supported"); if (typeFlag === 0 || typeFlag === 48) { const content = tar.subarray(offset, offset + size); this.addArchiveFile(files, path, new Uint8Array(content), runningTotal); } offset += Math.ceil(size / 512) * 512; } if (!files.has("SKILL.md")) throw new Error("Archive missing root SKILL.md"); return files; } readTarString(buffer, offset, length) { const slice = buffer.subarray(offset, offset + length); const nul = slice.indexOf(0); return new TextDecoder().decode(nul >= 0 ? slice.subarray(0, nul) : slice); } extractZip(bytes) { const buffer = Buffer.from(bytes); const eocdOffset = this.findZipEndOfCentralDirectory(buffer); if (eocdOffset < 0) throw new Error("Invalid zip archive"); const totalEntries = buffer.readUInt16LE(eocdOffset + 10); const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16); const files = /* @__PURE__ */ new Map(); const runningTotal = { bytes: 0 }; let offset = centralDirectoryOffset; for (let i = 0; i < totalEntries; i++) { if (buffer.readUInt32LE(offset) !== 33639248) throw new Error("Invalid zip directory"); const flags = buffer.readUInt16LE(offset + 8); const method = buffer.readUInt16LE(offset + 10); const compressedSize = buffer.readUInt32LE(offset + 20); const uncompressedSize = buffer.readUInt32LE(offset + 24); const fileNameLength = buffer.readUInt16LE(offset + 28); const extraLength = buffer.readUInt16LE(offset + 30); const commentLength = buffer.readUInt16LE(offset + 32); const externalAttributes = buffer.readUInt32LE(offset + 38); const localHeaderOffset = buffer.readUInt32LE(offset + 42); const nameStart = offset + 46; const rawName = buffer.subarray(nameStart, nameStart + fileNameLength); const fileName = new TextDecoder(flags & 2048 ? "utf-8" : void 0).decode(rawName); offset = nameStart + fileNameLength + extraLength + commentLength; if (fileName.endsWith("/")) continue; if (flags & 1) throw new Error("Encrypted zip entries are not supported"); const fileType = externalAttributes >>> 16 & 61440; if (fileType === 40960 || fileType === 4096) throw new Error("Archive links are not supported"); if (buffer.readUInt32LE(localHeaderOffset) !== 67324752) throw new Error("Invalid zip local header"); const localFileNameLength = buffer.readUInt16LE(localHeaderOffset + 26); const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28); const dataStart = localHeaderOffset + 30 + localFileNameLength + localExtraLength; const compressed = buffer.subarray(dataStart, dataStart + compressedSize); let content; if (method === 0) content = compressed; else if (method === 8) content = inflateRawSync(compressed); else throw new Error(`Unsupported zip compression method: ${method}`); if (content.byteLength !== uncompressedSize) throw new Error("Zip entry size mismatch"); this.addArchiveFile(files, fileName, new Uint8Array(content), runningTotal); } if (!files.has("SKILL.md")) throw new Error("Archive missing root SKILL.md"); return files; } findZipEndOfCentralDirectory(buffer) { const minOffset = Math.max(0, buffer.length - 65535 - 22); for (let offset = buffer.length - 22; offset >= minOffset; offset--) if (buffer.readUInt32LE(offset) === 101010256) return offset; return -1; } toRawUrl(url) { try { const parsed = new URL(url); if (url.toLowerCase().endsWith("/skill.md")) return url; const primaryPath = this.WELL_KNOWN_PATHS[0]; const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/); if (pathMatch && pathMatch[1]) { const basePath = parsed.pathname.replace(/\/.well-known\/(?:agent-skills|skills)\/.*$/, ""); return `${parsed.protocol}//${parsed.host}${basePath}/${primaryPath}/${pathMatch[1]}/SKILL.md`; } const basePath = parsed.pathname.replace(/\/$/, ""); return `${parsed.protocol}//${parsed.host}${basePath}/${primaryPath}/${this.INDEX_FILE}`; } catch { return url; } } getSourceIdentifier(url) { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return "unknown"; } } async hasSkillsIndex(url) { return await this.fetchIndex(url) !== null; } }; const wellKnownProvider = new WellKnownProvider(); const AGENTS_DIR$1 = ".agents"; const LOCK_FILE$1 = ".skill-lock.json"; const CURRENT_VERSION$1 = 3; function getSkillLockPath$1() { const xdgStateHome = process.env.XDG_STATE_HOME; if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE$1); return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1); } async function readSkillLock$1() { const lockPath = getSkillLockPath$1(); try { const content = await readFile(lockPath, "utf-8"); const parsed = JSON.parse(content); if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile(); if (parsed.version < CURRENT_VERSION$1) return createEmptyLockFile(); return parsed; } catch (error) { return createEmptyLockFile(); } } async function writeSkillLock(lock) { const lockPath = getSkillLockPath$1(); await mkdir(dirname(lockPath), { recursive: true }); await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8"); } let _ghWarningShown = false; function getGitHubToken() { if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN; if (process.env.GH_TOKEN) return process.env.GH_TOKEN; if (!_ghWarningShown) { process.stderr.write("warn: GitHub API rate limit reached; reading a token via `gh auth token`.\n Set GITHUB_TOKEN in your environment to skip this fallback.\n"); _ghWarningShown = true; } try { const token = execSync("gh auth token", { encoding: "utf-8", stdio: [ "pipe", "pipe", "pipe" ] }).trim(); if (token) return token; } catch {} return null; } async function fetchSkillFolderHash(ownerRepo, skillPath, getToken, ref) { const { fetchRepoTree, getSkillFolderHashFromTree } = await Promise.resolve().then(() => blob_exports); const tree = await fetchRepoTree(ownerRepo, ref, getToken ?? void 0); if (!tree) return null; return getSkillFolderHashFromTree(tree, skillPath); } async function addSkillToLock(skillName, entry) { const lock = await readSkillLock$1(); const now = (/* @__PURE__ */ new Date()).toISOString(); const existingEntry = lock.skills[skillName]; lock.skills[skillName] = { ...entry, installedAt: existingEntry?.installedAt ?? now, updatedAt: now }; await writeSkillLock(lock); } async function removeSkillFromLock(skillName) { const lock = await readSkillLock$1(); if (!(skillName in lock.skills)) return false; delete lock.skills[skillName]; await writeSkillLock(lock); return true; } async function getSkillFromLock(skillName) { return (await readSkillLock$1()).skills[skillName] ?? null; } async function getAllLockedSkills() { return (await readSkillLock$1()).skills; } function createEmptyLockFile() { return { version: CURRENT_VERSION$1, skills: {}, dismissed: {} }; } async function isPromptDismissed(promptKey) { return (await readSkillLock$1()).dismissed?.[promptKey] === true; } async function dismissPrompt(promptKey) { const lock = await readSkillLock$1(); if (!lock.dismissed) lock.dismissed = {}; lock.dismissed[promptKey] = true; await writeSkillLock(lock); } async function getLastSelectedAgents() { return (await readSkillLock$1()).lastSelectedAgents; } async function saveSelectedAgents(agents) { const lock = await readSkillLock$1(); lock.lastSelectedAgents = agents; await writeSkillLock(lock); } const LOCAL_LOCK_FILE = "skills-lock.json"; const CURRENT_VERSION = 1; function getLocalLockPath(cwd) { return join(cwd || process.cwd(), LOCAL_LOCK_FILE); } async function readLocalLock(cwd) { const lockPath = getLocalLockPath(cwd); try { const content = await readFile(lockPath, "utf-8"); const parsed = JSON.parse(content); if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock(); if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock(); return parsed; } catch { return createEmptyLocalLock(); } } async function writeLocalLock(lock, cwd) { const lockPath = getLocalLockPath(cwd); const sortedSkills = {}; for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key]; const sorted = { version: lock.version, skills: sortedSkills }; await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8"); } async function computeSkillFolderHash(skillDir) { const files = []; await collectFiles(skillDir, skillDir, files); files.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); const hash = createHash$1("sha256"); for (const file of files) { hash.update(file.relativePath); hash.update(file.content); } return hash.digest("hex"); } async function collectFiles(baseDir, currentDir, results) { const entries = await readdir(currentDir, { withFileTypes: true }); await Promise.all(entries.map(async (entry) => { const fullPath = join(currentDir, entry.name); if (entry.isDirectory()) { if (entry.name === ".git" || entry.name === "node_modules") return; await collectFiles(baseDir, fullPath, results); } else if (entry.isFile()) { const content = await readFile(fullPath); const relativePath = relative(baseDir, fullPath).split("\\").join("/"); results.push({ relativePath, content }); } })); } async function addSkillToLocalLock(skillName, entry, cwd) { const lock = await readLocalLock(cwd); lock.skills[skillName] = entry; await writeLocalLock(lock, cwd); } function createEmptyLocalLock() { return { version: CURRENT_VERSION, skills: {} }; } var blob_exports = /* @__PURE__ */ __exportAll({ fetchRepoTree: () => fetchRepoTree, findSkillMdPaths: () => findSkillMdPaths, getSkillFolderHashFromTree: () => getSkillFolderHashFromTree, toSkillSlug: () => toSkillSlug, tryBlobInstall: () => tryBlobInstall }); const DOWNLOAD_BASE_URL = process.env.SKILLS_DOWNLOAD_URL || "https://skills.sh"; const FETCH_TIMEOUT = 1e4; function toSkillSlug(name) { return name.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, ""); } let _rateLimitedThisSession = false; async function fetchTreeBranch(ownerRepo, branch, token) { try { const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${encodeURIComponent(branch)}?recursive=1`; const headers = { Accept: "application/vnd.github.v3+json", "User-Agent": "skills-cli" }; if (token) headers["Authorization"] = `Bearer ${token}`; const response = await fetch(url, { headers, signal: AbortSignal.timeout(FETCH_TIMEOUT) }); if (response.ok) { const data = await response.json(); return { tree: { sha: data.sha, branch, tree: data.tree }, rateLimited: false }; } return { tree: null, rateLimited: response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0" }; } catch { return { tree: null, rateLimited: false }; } } async function fetchRepoTree(ownerRepo, ref, getToken) { const branches = ref ? [ref] : [ "HEAD", "main", "master" ]; if (_rateLimitedThisSession && getToken) { const token = getToken(); if (!token) return null; for (const branch of branches) { const result = await fetchTreeBranch(ownerRepo, branch, token); if (result.tree) return result.tree; } return null; } let rateLimited = false; for (const branch of branches) { const result = await fetchTreeBranch(ownerRepo, branch, null); if (result.tree) return result.tree; if (result.rateLimited) { rateLimited = true; break; } } if (!rateLimited || !getToken) return null; _rateLimitedThisSession = true; const token = getToken(); if (!token) return null; for (const branch of branches) { const result = await fetchTreeBranch(ownerRepo, branch, token); if (result.tree) return result.tree; } return null; } function getSkillFolderHashFromTree(tree, skillPath) { let folderPath = skillPath.replace(/\\/g, "/"); if (folderPath.toLowerCase().endsWith("/skill.md")) folderPath = folderPath.slice(0, -9); else if (folderPath.toLowerCase().endsWith("skill.md")) folderPath = folderPath.slice(0, -8); if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1); if (!folderPath) return tree.sha; return tree.tree.find((e) => e.type === "tree" && e.path === folderPath)?.sha ?? null; } const PRIORITY_PREFIXES = [ "", "skills/", "skills/.curated/", "skills/.experimental/", "skills/.system/", ".agents/skills/", ".claude/skills/", ".cline/skills/", ".codebuddy/skills/", ".codex/skills/", ".commandcode/skills/", ".continue/skills/", ".github/skills/", ".goose/skills/", ".iflow/skills/", ".junie/skills/", ".kilocode/skills/", ".kiro/skills/", ".mux/skills/", ".neovate/skills/", ".opencode/skills/", ".openhands/skills/", ".pi/skills/", ".qoder/skills/", ".roo/skills/", ".trae/skills/", ".windsurf/skills/", ".zencoder/skills/" ]; function findSkillMdPaths(tree, subpath) { const allSkillMds = tree.tree.filter((e) => e.type === "blob" && e.path.toLowerCase().endsWith("skill.md")).map((e) => e.path); const prefix = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : ""; const filtered = prefix ? allSkillMds.filter((p) => p.startsWith(prefix) || p === prefix + "SKILL.md") : allSkillMds; if (filtered.length === 0) return []; const priorityResults = []; const seen = /* @__PURE__ */ new Set(); for (const priorityPrefix of PRIORITY_PREFIXES) { const fullPrefix = prefix + priorityPrefix; for (const skillMd of filtered) { if (!skillMd.startsWith(fullPrefix)) continue; const rest = skillMd.slice(fullPrefix.length); if (rest.toLowerCase() === "skill.md") { if (!seen.has(skillMd)) { priorityResults.push(skillMd); seen.add(skillMd); } continue; } const parts = rest.split("/"); if (parts.length === 2 && parts[1].toLowerCase() === "skill.md") { if (!seen.has(skillMd)) { priorityResults.push(skillMd); seen.add(skillMd); } } } } if (priorityResults.length > 0) return priorityResults; return filtered.filter((p) => { return p.split("/").length <= 6; }); } async function fetchSkillMdContent(ownerRepo, branch, skillMdPath) { try { const url = `https://raw.githubusercontent.com/${ownerRepo}/${branch}/${skillMdPath}`; const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) }); if (!response.ok) return null; return await response.text(); } catch { return null; } } async function fetchSkillDownload(source, slug) { try { const [owner, repo] = source.split("/"); const url = `${DOWNLOAD_BASE_URL}/api/download/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(slug)}`; const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) }); if (!response.ok) return null; return await response.json(); } catch { return null; } } async function tryBlobInstall(ownerRepo, options = {}) { const tree = await fetchRepoTree(ownerRepo, options.ref, options.getToken); if (!tree) return null; let skillMdPaths = findSkillMdPaths(tree, options.subpath); if (skillMdPaths.length === 0) return null; if (options.skillFilter) { const filterSlug = toSkillSlug(options.skillFilter); const filtered = skillMdPaths.filter((p) => { const parts = p.split("/"); if (parts.length < 2) return false; const folderName = parts[parts.length - 2]; return toSkillSlug(folderName) === filterSlug; }); if (filtered.length > 0) skillMdPaths = filtered; } const mdFetches = await Promise.all(skillMdPaths.map(async (mdPath) => { return { mdPath, content: await fetchSkillMdContent(ownerRepo, tree.branch, mdPath) }; })); const parsedSkills = []; for (const { mdPath, content } of mdFetches) { if (!content) continue; const { data } = parseFrontmatter(content); if (!data.name || !data.description) continue; if (typeof data.name !== "string" || typeof data.description !== "string") continue; if (data.metadata?.internal === true && !options.includeInternal) continue; const safeName = sanitizeMetadata(data.name); const safeDescription = sanitizeMetadata(data.description); parsedSkills.push({ mdPath, name: safeName, description: safeDescription, content, slug: toSkillSlug(safeName), metadata: data.metadata }); } if (parsedSkills.length === 0) return null; let filteredSkills = parsedSkills; if (options.skillFilter) { const filterSlug = toSkillSlug(options.skillFilter); const nameFiltered = parsedSkills.filter((s) => s.slug === filterSlug); if (nameFiltered.length > 0) filteredSkills = nameFiltered; if (filteredSkills.length === 0) return null; } const source = ownerRepo.toLowerCase(); const downloads = await Promise.all(filteredSkills.map(async (skill) => { return { skill, download: await fetchSkillDownload(source, skill.slug) }; })); if (!downloads.every((d) => d.download !== null)) return null; return { skills: downloads.map(({ skill, download }) => { const mdPathLower = skill.mdPath.toLowerCase(); mdPathLower.endsWith("/skill.md") ? skill.mdPath.slice(0, -9) : mdPathLower === "skill.md" || skill.mdPath.slice(0, -9); return { name: skill.name, description: skill.description, path: "", rawContent: skill.content, metadata: skill.metadata, files: download.files, snapshotHash: download.hash, repoPath: skill.mdPath }; }), tree }; } var version$1 = "1.5.7"; const isCancelled$1 = (value) => typeof value === "symbol"; async function isSourcePrivate(source) { const ownerRepo = parseOwnerRepo(source); if (!ownerRepo) return false; return isRepoPrivate(ownerRepo.owner, ownerRepo.repo); } function initTelemetry(version) { setVersion(version); } function riskLabel(risk) { switch (risk) { case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk")); case "high": return import_picocolors.default.red("High Risk"); case "medium": return import_picocolors.default.yellow("Med Risk"); case "low": return import_picocolors.default.green("Low Risk"); case "safe": return import_picocolors.default.green("Safe"); default: return import_picocolors.default.dim("--"); } } function socketLabel(audit) { if (!audit) return import_picocolors.default.dim("--"); const count = audit.alerts ?? 0; return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts"); } function padEnd(str, width) { const visible = stripTerminalEscapes(str); const pad = Math.max(0, width - visible.length); return str + " ".repeat(pad); } function buildSecurityLines(auditData, skills, source) { if (!auditData) return []; if (!skills.some((s) => { const data = auditData[s.slug]; return data && Object.keys(data).length > 0; })) return []; const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36); const lines = []; const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Gen"), 18) + padEnd(import_picocolors.default.dim("Socket"), 18) + import_picocolors.default.dim("Snyk"); lines.push(header); for (const skill of skills) { const data = auditData[skill.slug]; const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName; const ath = data?.ath ? riskLabel(data.ath.risk) : import_picocolors.default.dim("--"); const socket = data?.socket ? socketLabel(data.socket) : import_picocolors.default.dim("--"); const snyk = data?.snyk ? riskLabel(data.snyk.risk) : import_picocolors.default.dim("--"); lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(ath, 18) + padEnd(socket, 18) + snyk); } lines.push(""); lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`); return lines; } function shortenPath$2(fullPath, cwd) { const home = homedir(); if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length); if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length); return fullPath; } function formatList$1(items, maxShow = 5) { if (items.length <= maxShow) return items.join(", "); const shown = items.slice(0, maxShow); const remaining = items.length - maxShow; return `${shown.join(", ")} +${remaining} more`; } function splitAgentsByType(agentTypes) { const universal = []; const symlinked = []; for (const a of agentTypes) if (isUniversalAgent(a)) universal.push(agents[a].displayName); else symlinked.push(agents[a].displayName); return { universal, symlinked }; } function buildAgentSummaryLines(targetAgents, installMode) { const lines = []; const { universal, symlinked } = splitAgentsByType(targetAgents); if (installMode === "symlink") { if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`); if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`); } else { const allNames = targetAgents.map((a) => agents[a].displayName); lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`); } return lines; } function ensureUniversalAgents(targetAgents) { const universalAgents = getUniversalAgents(); const result = [...targetAgents]; for (const ua of universalAgents) if (!result.includes(ua)) result.push(ua); return result; } function buildResultLines(results, targetAgents) { const lines = []; const { universal, symlinked: symlinkAgents } = splitAgentsByType(targetAgents); const successfulSymlinks = results.filter((r) => !r.symlinkFailed && !r.skipped && !universal.includes(r.agent)).map((r) => r.agent); const failedSymlinks = results.filter((r) => r.symlinkFailed && !r.skipped).map((r) => r.agent); if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`); if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList$1(successfulSymlinks)}`); if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList$1(failedSymlinks)}`); return lines; } function multiselect(opts) { return fe({ ...opts, options: opts.options, message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}` }); } async function promptForAgents(message, choices) { let lastSelected; try { lastSelected = await getLastSelectedAgents(); } catch {} const validAgents = choices.map((c) => c.value); const defaultValues = [ "claude-code", "opencode", "codex" ].filter((a) => validAgents.includes(a)); let initialValues = []; if (lastSelected && lastSelected.length > 0) initialValues = lastSelected.filter((a) => validAgents.includes(a)); if (initialValues.length === 0) initialValues = defaultValues; const selected = await searchMultiselect({ message, items: choices, initialSelected: initialValues, required: true }); if (!isCancelled$1(selected)) try { await saveSelectedAgents(selected); } catch {} return selected; } async function selectAgentsInteractive(options) { const supportsGlobalFilter = (a) => !options.global || agents[a].globalSkillsDir; const universalAgents = getUniversalAgents().filter(supportsGlobalFilter); const otherAgents = getNonUniversalAgents().filter(supportsGlobalFilter); const universalSection = { title: "Universal (.agents/skills)", items: universalAgents.map((a) => ({ value: a, label: agents[a].displayName })) }; const otherChoices = otherAgents.map((a) => ({ value: a, label: agents[a].displayName, hint: options.global ? agents[a].globalSkillsDir : agents[a].skillsDir })); let lastSelected; try { lastSelected = await getLastSelectedAgents(); } catch {} const selected = await searchMultiselect({ message: "Which agents do you want to install to?", items: otherChoices, initialSelected: lastSelected ? lastSelected.filter((a) => otherAgents.includes(a) && !universalAgents.includes(a)) : [], lockedSection: universalSection }); if (!isCancelled$1(selected)) try { await saveSelectedAgents(selected); } catch {} return selected; } setVersion(version$1); async function handleWellKnownSkills(source, url, options, spinner) { spinner.start("Discovering skills from well-known endpoint..."); const skills = await wellKnownProvider.fetchAllSkills(url); if (skills.length === 0) { spinner.stop(import_picocolors.default.red("No skills found")); Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file.")); process.exit(1); } spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`); for (const skill of skills) { M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`); M.message(import_picocolors.default.dim(skill.description)); if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`)); } if (options.list) { console.log(); M.step(import_picocolors.default.bold("Available Skills")); for (const skill of skills) { M.message(` ${import_picocolors.default.cyan(skill.installName)}`); M.message(` ${import_picocolors.default.dim(skill.description)}`); if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`); } console.log(); Se("Run without --list to install"); process.exit(0); } let selectedSkills; if (options.skill?.includes("*")) { selectedSkills = skills; M.info(`Installing all ${skills.length} skills`); } else if (options.skill && options.skill.length > 0) { selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase())); if (selectedSkills.length === 0) { M.error(`No matching skills found for: ${options.skill.join(", ")}`); M.info("Available skills:"); for (const s of skills) M.message(` - ${s.installName}`); process.exit(1); } } else if (skills.length === 1) { selectedSkills = skills; const firstSkill = skills[0]; M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`); } else if (options.yes) { selectedSkills = skills; M.info(`Installing all ${skills.length} skills`); } else { const selected = await multiselect({ message: "Select skills to install", options: skills.map((s) => ({ value: s, label: s.installName, hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description })), required: true }); if (pD(selected)) { xe("Installation cancelled"); process.exit(0); } selectedSkills = selected; } let targetAgents; const validAgents = Object.keys(agents); if (options.agent?.includes("*")) { targetAgents = validAgents; M.info(`Installing to all ${targetAgents.length} agents`); } else if (options.agent && options.agent.length > 0) { const invalidAgents = options.agent.filter((a) => !validAgents.includes(a)); if (invalidAgents.length > 0) { M.error(`Invalid agents: ${invalidAgents.join(", ")}`); M.info(`Valid agents: ${validAgents.join(", ")}`); process.exit(1); } targetAgents = options.agent; } else { spinner.start("Loading agents..."); const installedAgents = await detectInstalledAgents(); const totalAgents = Object.keys(agents).length; spinner.stop(`${totalAgents} agents`); if (installedAgents.length === 0) if (options.yes) { targetAgents = validAgents; M.info("Installing to all agents"); } else { M.info("Select agents to install skills to"); const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({ value: key, label: config.displayName }))); if (pD(selected)) { xe("Installation cancelled"); process.exit(0); } targetAgents = selected; } else if (installedAgents.length === 1 || options.yes) { targetAgents = ensureUniversalAgents(installedAgents); if (installedAgents.length === 1) { const firstAgent = installedAgents[0]; M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`); } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`); } else { const selected = await selectAgentsInteractive({ global: options.global }); if (pD(selected)) { xe("Installation cancelled"); process.exit(0); } targetAgents = selected; } } let installGlobally = options.global ?? false; const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0); if (options.global === void 0 && !options.yes && supportsGlobal) { const scope = await ve({ message: "Installation scope", options: [{ value: false, label: "Project", hint: "Install in current directory (committed with your project)" }, { value: true, label: "Global", hint: "Install in home directory (available across all projects)" }] }); if (pD(scope)) { xe("Installation cancelled"); process.exit(0); } installGlobally = scope; } let installMode = options.copy ? "copy" : "symlink"; const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir)); if (!options.copy && !options.yes && uniqueDirs.size > 1) { const modeChoice = await ve({ message: "Installation method", options: [{ value: "symlink", label: "Symlink (Recommended)", hint: "Single source of truth, easy updates" }, { value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }] }); if (pD(modeChoice)) { xe("Installation cancelled"); process.exit(0); } installMode = modeChoice; } else if (uniqueDirs.size <= 1) installMode = "copy"; const cwd = process.cwd(); const summaryLines = []; targetAgents.map((a) => agents[a].displayName); const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({ skillName: skill.installName, agent, installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally }) })))); const overwriteStatus = /* @__PURE__ */ new Map(); for (const { skillName, agent, installed } of overwriteChecks) { if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map()); overwriteStatus.get(skillName).set(agent, installed); } for (const skill of selectedSkills) { if (summaryLines.length > 0) summaryLines.push(""); const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd); summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`); summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode)); if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`); const skillOverwrites = overwriteStatus.get(skill.installName); const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName); if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`); } console.log(); Me(summaryLines.join("\n"), "Installation Summary"); if (!options.yes) { const confirmed = await ye({ message: "Proceed with installation?" }); if (pD(confirmed) || !confirmed) { xe("Installation cancelled"); process.exit(0); } } const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url); const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null); spinner.start("Installing skills..."); const results = []; for (const skill of selectedSkills) for (const agent of targetAgents) { const result = await installWellKnownSkillForAgent(skill, agent, { global: installGlobally, mode: installMode }); results.push({ skill: skill.installName, agent: agents[agent].displayName, ...result }); } spinner.stop("Installation complete"); console.log(); const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); const skillFiles = {}; for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl; if (await wellKnownPrivacyPromise !== true) track({ event: "install", source: sourceIdentifier, skills: selectedSkills.map((s) => s.installName).join(","), agents: targetAgents.join(","), ...installGlobally && { global: "1" }, skillFiles: JSON.stringify(skillFiles), sourceType: "well-known" }); if (successful.length > 0 && installGlobally) { const successfulSkillNames = new Set(successful.map((r) => r.skill)); for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try { await addSkillToLock(skill.installName, { source: sourceIdentifier, sourceType: "well-known", sourceUrl: skill.sourceUrl, skillFolderHash: "" }); } catch {} } if (successful.length > 0 && !installGlobally) { const successfulSkillNames = new Set(successful.map((r) => r.skill)); for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try { const matchingResult = successful.find((r) => r.skill === skill.installName); const installDir = matchingResult?.canonicalPath || matchingResult?.path; if (installDir) { const computedHash = await computeSkillFolderHash(installDir); await addSkillToLocalLock(skill.installName, { source: sourceIdentifier, sourceType: "well-known", computedHash }, cwd); } } catch {} } if (successful.length > 0) { const bySkill = /* @__PURE__ */ new Map(); for (const r of successful) { const skillResults = bySkill.get(r.skill) || []; skillResults.push(r); bySkill.set(r.skill, skillResults); } const skillCount = bySkill.size; const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed); const copiedAgents = symlinkFailures.map((r) => r.agent); const resultLines = []; for (const [skillName, skillResults] of bySkill) { const firstResult = skillResults[0]; if (firstResult.mode === "copy") { resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`); for (const r of skillResults) { const shortPath = shortenPath$2(r.path, cwd); resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`); } } else { if (firstResult.canonicalPath) { const shortPath = shortenPath$2(firstResult.canonicalPath, cwd); resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`); } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`); resultLines.push(...buildResultLines(skillResults, targetAgents)); } } const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`); Me(resultLines.join("\n"), title); if (symlinkFailures.length > 0) { M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`)); M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support.")); } } if (failed.length > 0) { console.log(); M.error(import_picocolors.default.red(`Failed to install ${failed.length}`)); for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`); } console.log(); Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions.")); await promptForFindSkills(options, targetAgents); } async function runAdd(args, options = {}) { const source = args[0]; let installTipShown = false; const showInstallTip = () => { if (installTipShown) return; M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts.")); installTipShown = true; }; if (!source) { console.log(); console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source")); console.log(); console.log(import_picocolors.default.dim(" Usage:")); console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("")} ${import_picocolors.default.dim("[options]")}`); console.log(); console.log(import_picocolors.default.dim(" Example:")); console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`); console.log(); process.exit(1); } if (options.all) { options.skill = ["*"]; options.agent = ["*"]; options.yes = true; } const agentResult = await detectAgent(); if (agentResult.isAgent) { options.yes = true; if (!options.agent || options.agent.length === 0) { const mappedAgent = getAgentType(agentResult.agent.name); if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]); } } console.log(); if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills "))); if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively"); else if (!process.stdin.isTTY) showInstallTip(); let tempDir = null; try { const spinner = Y(); spinner.start("Parsing source..."); const parsed = parseSource(source); 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)}` : ""}`); const ownerRepoRaw = getOwnerRepo(parsed); const repoPrivacyPromise = (() => { if (!ownerRepoRaw) return Promise.resolve(null); const ownerRepo = parseOwnerRepo(ownerRepoRaw); if (!ownerRepo) return Promise.resolve(null); return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null); })(); if (ownerRepoRaw?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) { console.log(); M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions."))); M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality.")); M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious.")); console.log(); M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx skills add ${source} --dangerously-accept-openclaw-risks`)}\n`); Se(import_picocolors.default.red("Installation blocked")); process.exit(1); } if (parsed.type === "well-known") { await handleWellKnownSkills(source, parsed.url, options, spinner); return; } if (parsed.skillFilter) { options.skill = options.skill || []; if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter); } const includeInternal = !!(options.skill && options.skill.length > 0); let skills; let blobResult = null; if (parsed.type === "local") { spinner.start("Validating local path..."); if (!existsSync(parsed.localPath)) { spinner.stop(import_picocolors.default.red("Path not found")); Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`)); process.exit(1); } spinner.stop("Local path validated"); spinner.start("Discovering skills..."); skills = await discoverSkills(parsed.localPath, parsed.subpath, { includeInternal, fullDepth: options.fullDepth }); } else if (parsed.type === "github" && !options.fullDepth) { const BLOB_ALLOWED_OWNERS = [ "vercel", "vercel-labs", "heygen-com" ]; const ownerRepo = getOwnerRepo(parsed); const owner = ownerRepo?.split("/")[0]?.toLowerCase(); if (ownerRepo && owner && BLOB_ALLOWED_OWNERS.includes(owner)) { spinner.start("Fetching skills..."); blobResult = await tryBlobInstall(ownerRepo, { subpath: parsed.subpath, skillFilter: parsed.skillFilter, ref: parsed.ref, getToken: getGitHubToken, includeInternal }); if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone...")); } if (blobResult) { skills = blobResult.skills; spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`); } else { spinner.start("Cloning repository..."); tempDir = await cloneRepo(parsed.url, parsed.ref); spinner.stop("Repository cloned"); spinner.start("Discovering skills..."); skills = await discoverSkills(tempDir, parsed.subpath, { includeInternal, fullDepth: options.fullDepth }); } } else { spinner.start("Cloning repository..."); tempDir = await cloneRepo(parsed.url, parsed.ref); spinner.stop("Repository cloned"); spinner.start("Discovering skills..."); skills = await discoverSkills(tempDir, parsed.subpath, { includeInternal, fullDepth: options.fullDepth }); } if (skills.length === 0) { spinner.stop(import_picocolors.default.red("No skills found")); Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description.")); await cleanup(tempDir); process.exit(1); } if (!blobResult) spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`); if (options.list) { console.log(); M.step(import_picocolors.default.bold("Available Skills")); const groupedSkills = {}; const ungroupedSkills = []; for (const skill of skills) if (skill.pluginName) { const group = skill.pluginName; if (!groupedSkills[group]) groupedSkills[group] = []; groupedSkills[group].push(skill); } else ungroupedSkills.push(skill); const sortedGroups = Object.keys(groupedSkills).sort(); for (const group of sortedGroups) { const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); console.log(import_picocolors.default.bold(title)); for (const skill of groupedSkills[group]) { M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`); M.message(` ${import_picocolors.default.dim(skill.description)}`); } console.log(); } if (ungroupedSkills.length > 0) { if (sortedGroups.length > 0) console.log(import_picocolors.default.bold("General")); for (const skill of ungroupedSkills) { M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`); M.message(` ${import_picocolors.default.dim(skill.description)}`); } } console.log(); Se("Use --skill to install specific skills"); await cleanup(tempDir); process.exit(0); } let selectedSkills; if (options.skill?.includes("*")) { selectedSkills = skills; M.info(`Installing all ${skills.length} skills`); } else if (options.skill && options.skill.length > 0) { selectedSkills = filterSkills(skills, options.skill); if (selectedSkills.length === 0) { M.error(`No matching skills found for: ${options.skill.join(", ")}`); M.info("Available skills:"); for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`); await cleanup(tempDir); process.exit(1); } M.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`); } else if (skills.length === 1) { selectedSkills = skills; const firstSkill = skills[0]; M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(firstSkill))}`); M.message(import_picocolors.default.dim(firstSkill.description)); } else if (options.yes) { selectedSkills = skills; M.info(`Installing all ${skills.length} skills`); } else { const sortedSkills = [...skills].sort((a, b) => { if (a.pluginName && !b.pluginName) return -1; if (!a.pluginName && b.pluginName) return 1; if (a.pluginName && b.pluginName && a.pluginName !== b.pluginName) return a.pluginName.localeCompare(b.pluginName); return getSkillDisplayName(a).localeCompare(getSkillDisplayName(b)); }); const hasGroups = sortedSkills.some((s) => s.pluginName); let selected; if (hasGroups) { const kebabToTitle = (s) => s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); const grouped = {}; for (const s of sortedSkills) { const groupName = s.pluginName ? kebabToTitle(s.pluginName) : "Other"; if (!grouped[groupName]) grouped[groupName] = []; grouped[groupName].push({ value: s, label: getSkillDisplayName(s), hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description }); } selected = await be({ message: `Select skills to install ${import_picocolors.default.dim("(space to toggle)")}`, options: grouped, required: true }); } else selected = await multiselect({ message: "Select skills to install", options: sortedSkills.map((s) => ({ value: s, label: getSkillDisplayName(s), hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description })), required: true }); if (pD(selected)) { xe("Installation cancelled"); await cleanup(tempDir); process.exit(0); } selectedSkills = selected; } const ownerRepoForAudit = getOwnerRepo(parsed); const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => getSkillDisplayName(s))) : Promise.resolve(null); let targetAgents; const validAgents = Object.keys(agents); if (options.agent?.includes("*")) { targetAgents = validAgents; M.info(`Installing to all ${targetAgents.length} agents`); } else if (options.agent && options.agent.length > 0) { const invalidAgents = options.agent.filter((a) => !validAgents.includes(a)); if (invalidAgents.length > 0) { M.error(`Invalid agents: ${invalidAgents.join(", ")}`); M.info(`Valid agents: ${validAgents.join(", ")}`); await cleanup(tempDir); process.exit(1); } targetAgents = options.agent; } else { spinner.start("Loading agents..."); const installedAgents = await detectInstalledAgents(); const totalAgents = Object.keys(agents).length; spinner.stop(`${totalAgents} agents`); if (installedAgents.length === 0) if (options.yes) { targetAgents = validAgents; M.info("Installing to all agents"); } else { M.info("Select agents to install skills to"); const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({ value: key, label: config.displayName }))); if (pD(selected)) { xe("Installation cancelled"); await cleanup(tempDir); process.exit(0); } targetAgents = selected; } else if (installedAgents.length === 1 || options.yes) { targetAgents = ensureUniversalAgents(installedAgents); if (installedAgents.length === 1) { const firstAgent = installedAgents[0]; M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`); } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`); } else { const selected = await selectAgentsInteractive({ global: options.global }); if (pD(selected)) { xe("Installation cancelled"); await cleanup(tempDir); process.exit(0); } targetAgents = selected; } } let installGlobally = options.global ?? false; const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0); if (options.global === void 0 && !options.yes && supportsGlobal) { const scope = await ve({ message: "Installation scope", options: [{ value: false, label: "Project", hint: "Install in current directory (committed with your project)" }, { value: true, label: "Global", hint: "Install in home directory (available across all projects)" }] }); if (pD(scope)) { xe("Installation cancelled"); await cleanup(tempDir); process.exit(0); } installGlobally = scope; } let installMode = options.copy ? "copy" : "symlink"; const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir)); if (!options.copy && !options.yes && uniqueDirs.size > 1) { const modeChoice = await ve({ message: "Installation method", options: [{ value: "symlink", label: "Symlink (Recommended)", hint: "Single source of truth, easy updates" }, { value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }] }); if (pD(modeChoice)) { xe("Installation cancelled"); await cleanup(tempDir); process.exit(0); } installMode = modeChoice; } else if (uniqueDirs.size <= 1) installMode = "copy"; const cwd = process.cwd(); const summaryLines = []; targetAgents.map((a) => agents[a].displayName); const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({ skillName: skill.name, agent, installed: await isSkillInstalled(skill.name, agent, { global: installGlobally }) })))); const overwriteStatus = /* @__PURE__ */ new Map(); for (const { skillName, agent, installed } of overwriteChecks) { if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map()); overwriteStatus.get(skillName).set(agent, installed); } const groupedSummary = {}; const ungroupedSummary = []; for (const skill of selectedSkills) if (skill.pluginName) { const group = skill.pluginName; if (!groupedSummary[group]) groupedSummary[group] = []; groupedSummary[group].push(skill); } else ungroupedSummary.push(skill); const printSkillSummary = (skills) => { for (const skill of skills) { if (summaryLines.length > 0) summaryLines.push(""); const shortCanonical = shortenPath$2(getCanonicalPath(skill.name, { global: installGlobally }), cwd); summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`); summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode)); const skillOverwrites = overwriteStatus.get(skill.name); const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName); if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`); } }; const sortedGroups = Object.keys(groupedSummary).sort(); for (const group of sortedGroups) { const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); summaryLines.push(""); summaryLines.push(import_picocolors.default.bold(title)); printSkillSummary(groupedSummary[group]); } if (ungroupedSummary.length > 0) { if (sortedGroups.length > 0) { summaryLines.push(""); summaryLines.push(import_picocolors.default.bold("General")); } printSkillSummary(ungroupedSummary); } console.log(); Me(summaryLines.join("\n"), "Installation Summary"); try { const auditData = await auditPromise; if (auditData && ownerRepoForAudit) { const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({ slug: getSkillDisplayName(s), displayName: getSkillDisplayName(s) })), ownerRepoForAudit); if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments"); } } catch {} if (!options.yes) { const confirmed = await ye({ message: "Proceed with installation?" }); if (pD(confirmed) || !confirmed) { xe("Installation cancelled"); await cleanup(tempDir); process.exit(0); } } spinner.start("Installing skills..."); const results = []; for (const skill of selectedSkills) for (const agent of targetAgents) { let result; if (blobResult && "files" in skill) { const blobSkill = skill; result = await installBlobSkillForAgent({ installName: blobSkill.name, files: blobSkill.files }, agent, { global: installGlobally, mode: installMode }); } else result = await installSkillForAgent(skill, agent, { global: installGlobally, mode: installMode }); results.push({ skill: getSkillDisplayName(skill), agent: agents[agent].displayName, pluginName: skill.pluginName, ...result }); } spinner.stop("Installation complete"); console.log(); const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); const skillFiles = {}; for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath; else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md"; else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md"; else continue; const normalizedSource = getOwnerRepo(parsed); const lockSource = parsed.url.startsWith("git@") ? parsed.url : normalizedSource; if (normalizedSource) if (parseOwnerRepo(normalizedSource)) { if (await repoPrivacyPromise === false) track({ event: "install", source: normalizedSource, skills: selectedSkills.map((s) => s.name).join(","), agents: targetAgents.join(","), ...installGlobally && { global: "1" }, skillFiles: JSON.stringify(skillFiles) }); } else track({ event: "install", source: normalizedSource, skills: selectedSkills.map((s) => s.name).join(","), agents: targetAgents.join(","), ...installGlobally && { global: "1" }, skillFiles: JSON.stringify(skillFiles) }); if (successful.length > 0 && installGlobally && normalizedSource) { const successfulSkillNames = new Set(successful.map((r) => r.skill)); let cachedTree; if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken); for (const skill of selectedSkills) { const skillDisplayName = getSkillDisplayName(skill); if (successfulSkillNames.has(skillDisplayName)) try { let skillFolderHash = ""; const skillPathValue = skillFiles[skill.name]; if (blobResult && skillPathValue) { const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue); if (hash) skillFolderHash = hash; } else if (parsed.type === "github" && skillPathValue && cachedTree) { const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue); if (hash) skillFolderHash = hash; } else if (skillPathValue && tempDir) { const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue))); if (hash) skillFolderHash = hash; } await addSkillToLock(skill.name, { source: lockSource || normalizedSource, sourceType: parsed.type, sourceUrl: parsed.url, ref: parsed.ref, skillPath: skillPathValue, skillFolderHash, pluginName: skill.pluginName }); } catch {} } } if (successful.length > 0 && !installGlobally) { const successfulSkillNames = new Set(successful.map((r) => r.skill)); for (const skill of selectedSkills) { const skillDisplayName = getSkillDisplayName(skill); if (successfulSkillNames.has(skillDisplayName)) try { const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path); const skillPathValue = skillFiles[skill.name]; await addSkillToLocalLock(skill.name, { source: lockSource || parsed.url, ref: parsed.ref, sourceType: parsed.type, ...skillPathValue && { skillPath: skillPathValue }, computedHash }, cwd); } catch {} } } if (successful.length > 0) { const bySkill = /* @__PURE__ */ new Map(); const groupedResults = {}; const ungroupedResults = []; for (const r of successful) { const skillResults = bySkill.get(r.skill) || []; skillResults.push(r); bySkill.set(r.skill, skillResults); if (skillResults.length === 1) if (r.pluginName) { const group = r.pluginName; if (!groupedResults[group]) groupedResults[group] = []; groupedResults[group].push(r); } else ungroupedResults.push(r); } const skillCount = bySkill.size; const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed); const copiedAgents = symlinkFailures.map((r) => r.agent); const resultLines = []; const printSkillResults = (entries) => { for (const entry of entries) { const skillResults = bySkill.get(entry.skill) || []; const firstResult = skillResults[0]; if (firstResult.mode === "copy") { resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill} ${import_picocolors.default.dim("(copied)")}`); for (const r of skillResults) { const shortPath = shortenPath$2(r.path, cwd); resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`); } } else { if (firstResult.canonicalPath) { const shortPath = shortenPath$2(firstResult.canonicalPath, cwd); resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`); } else resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill}`); resultLines.push(...buildResultLines(skillResults, targetAgents)); } } }; const sortedResultGroups = Object.keys(groupedResults).sort(); for (const group of sortedResultGroups) { const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); resultLines.push(""); resultLines.push(import_picocolors.default.bold(title)); printSkillResults(groupedResults[group]); } if (ungroupedResults.length > 0) { if (sortedResultGroups.length > 0) { resultLines.push(""); resultLines.push(import_picocolors.default.bold("General")); } printSkillResults(ungroupedResults); } const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`); Me(resultLines.join("\n"), title); if (symlinkFailures.length > 0) { M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`)); M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support.")); } } if (failed.length > 0) { console.log(); M.error(import_picocolors.default.red(`Failed to install ${failed.length}`)); for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`); } console.log(); Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions.")); await promptForFindSkills(options, targetAgents); } catch (error) { if (error instanceof GitCloneError) { M.error(import_picocolors.default.red("Failed to clone repository")); for (const line of error.message.split("\n")) M.message(import_picocolors.default.dim(line)); } else M.error(error instanceof Error ? error.message : "Unknown error occurred"); showInstallTip(); Se(import_picocolors.default.red("Installation failed")); process.exit(1); } finally { await cleanup(tempDir); } } async function cleanup(tempDir) { if (tempDir) try { await cleanupTempDir(tempDir); } catch {} } async function promptForFindSkills(options, targetAgents) { if (!process.stdin.isTTY) return; if (options?.yes) return; try { if (await isPromptDismissed("findSkillsPrompt")) return; if (await isSkillInstalled("find-skills", "claude-code", { global: true })) { await dismissPrompt("findSkillsPrompt"); return; } console.log(); M.message(import_picocolors.default.dim("One-time prompt - you won't be asked again if you dismiss.")); const install = await ye({ message: `Install the ${import_picocolors.default.cyan("find-skills")} skill? It helps your agent discover and suggest skills.` }); if (pD(install)) { await dismissPrompt("findSkillsPrompt"); return; } if (install) { await dismissPrompt("findSkillsPrompt"); const findSkillsAgents = targetAgents?.filter((a) => a !== "replit"); if (!findSkillsAgents || findSkillsAgents.length === 0) return; console.log(); M.step("Installing find-skills skill..."); try { await runAdd(["vercel-labs/skills"], { skill: ["find-skills"], global: true, yes: true, agent: findSkillsAgents }); } catch { M.warn("Failed to install find-skills. You can try again with:"); M.message(import_picocolors.default.dim(" npx skills add vercel-labs/skills@find-skills -g -y --all")); } } else { await dismissPrompt("findSkillsPrompt"); M.message(import_picocolors.default.dim("You can install it later with: npx skills add vercel-labs/skills@find-skills")); } } catch {} } function parseAddOptions(args) { const options = {}; const source = []; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "-g" || arg === "--global") options.global = true; else if (arg === "-y" || arg === "--yes") options.yes = true; else if (arg === "-l" || arg === "--list") options.list = true; else if (arg === "--all") options.all = true; else if (arg === "-a" || arg === "--agent") { options.agent = options.agent || []; i++; let nextArg = args[i]; while (i < args.length && nextArg && !nextArg.startsWith("-")) { options.agent.push(nextArg); i++; nextArg = args[i]; } i--; } else if (arg === "-s" || arg === "--skill") { options.skill = options.skill || []; i++; let nextArg = args[i]; while (i < args.length && nextArg && !nextArg.startsWith("-")) { options.skill.push(nextArg); i++; nextArg = args[i]; } i--; } else if (arg === "--full-depth") options.fullDepth = true; else if (arg === "--copy") options.copy = true; else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true; else if (arg && !arg.startsWith("-")) source.push(arg); } return { source, options }; } const RESET$2 = "\x1B[0m"; const BOLD$2 = "\x1B[1m"; const DIM$2 = "\x1B[38;5;102m"; const TEXT$1 = "\x1B[38;5;145m"; const CYAN$1 = "\x1B[36m"; const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh"; function formatInstalls(count) { if (!count || count <= 0) return ""; if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`; if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`; return `${count} install${count === 1 ? "" : "s"}`; } async function searchSkillsAPI(query) { try { const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`; const res = await fetch(url); if (!res.ok) return []; return (await res.json()).skills.map((skill) => ({ name: sanitizeMetadata(skill.name), slug: sanitizeMetadata(skill.id), source: sanitizeMetadata(skill.source || ""), installs: skill.installs })).sort((a, b) => (b.installs || 0) - (a.installs || 0)); } catch { return []; } } const HIDE_CURSOR = "\x1B[?25l"; const SHOW_CURSOR = "\x1B[?25h"; const CLEAR_DOWN = "\x1B[J"; const MOVE_UP = (n) => `\x1b[${n}A`; const MOVE_TO_COL = (n) => `\x1b[${n}G`; async function runSearchPrompt(initialQuery = "") { let results = []; let selectedIndex = 0; let query = initialQuery; let loading = false; let debounceTimer = null; let lastRenderedLines = 0; if (process.stdin.isTTY) process.stdin.setRawMode(true); readline.emitKeypressEvents(process.stdin); process.stdin.resume(); process.stdout.write(HIDE_CURSOR); function render() { if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1)); process.stdout.write(CLEAR_DOWN); const lines = []; const cursor = `${BOLD$2}_${RESET$2}`; lines.push(`${TEXT$1}Search skills:${RESET$2} ${query}${cursor}`); lines.push(""); if (!query || query.length < 2) lines.push(`${DIM$2}Start typing to search (min 2 chars)${RESET$2}`); else if (results.length === 0 && loading) lines.push(`${DIM$2}Searching...${RESET$2}`); else if (results.length === 0) lines.push(`${DIM$2}No skills found${RESET$2}`); else { const visible = results.slice(0, 8); for (let i = 0; i < visible.length; i++) { const skill = visible[i]; const isSelected = i === selectedIndex; const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " "; const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`; const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : ""; const installs = formatInstalls(skill.installs); const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""; const loadingIndicator = loading && i === 0 ? ` ${DIM$2}...${RESET$2}` : ""; lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`); } } lines.push(""); lines.push(`${DIM$2}up/down navigate | enter select | esc cancel${RESET$2}`); for (const line of lines) process.stdout.write(line + "\n"); lastRenderedLines = lines.length; } function triggerSearch(q) { if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null; } loading = false; if (!q || q.length < 2) { results = []; selectedIndex = 0; render(); return; } loading = true; render(); const debounceMs = Math.max(150, 350 - q.length * 50); debounceTimer = setTimeout(async () => { try { results = await searchSkillsAPI(q); selectedIndex = 0; } catch { results = []; } finally { loading = false; debounceTimer = null; render(); } }, debounceMs); } if (initialQuery) triggerSearch(initialQuery); render(); return new Promise((resolve) => { function cleanup() { process.stdin.removeListener("keypress", handleKeypress); if (process.stdin.isTTY) process.stdin.setRawMode(false); process.stdout.write(SHOW_CURSOR); process.stdin.pause(); } function handleKeypress(_ch, key) { if (!key) return; if (key.name === "escape" || key.ctrl && key.name === "c") { cleanup(); resolve(null); return; } if (key.name === "return") { cleanup(); resolve(results[selectedIndex] || null); return; } if (key.name === "up") { selectedIndex = Math.max(0, selectedIndex - 1); render(); return; } if (key.name === "down") { selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1); render(); return; } if (key.name === "backspace") { if (query.length > 0) { query = query.slice(0, -1); triggerSearch(query); } return; } if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { const char = key.sequence; if (char >= " " && char <= "~") { query += char; triggerSearch(query); } } } process.stdin.on("keypress", handleKeypress); }); } function getOwnerRepoFromString(pkg) { const atIndex = pkg.lastIndexOf("@"); const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/); if (match) return { owner: match[1], repo: match[2] }; return null; } async function isRepoPublic(owner, repo) { return await isRepoPrivate(owner, repo) === false; } async function runFind(args) { const query = args.join(" "); const isNonInteractive = !process.stdin.isTTY; const agentTip = `${DIM$2}Tip: if running in a coding agent, follow these steps:${RESET$2} ${DIM$2} 1) npx skills find [query]${RESET$2} ${DIM$2} 2) npx skills add ${RESET$2}`; if (query) { const results = await searchSkillsAPI(query); track({ event: "find", query, resultCount: String(results.length) }); if (results.length === 0) { console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`); return; } console.log(`${DIM$2}Install with${RESET$2} npx skills add `); console.log(); for (const skill of results.slice(0, 6)) { const pkg = skill.source || skill.slug; const installs = formatInstalls(skill.installs); console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`); console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`); console.log(); } return; } if (isNonInteractive || await isRunningInAgent()) { console.log(agentTip); console.log(); console.log(`${DIM$2}Usage: npx skills find ${RESET$2}`); return; } const selected = await runSearchPrompt(); track({ event: "find", query: "", resultCount: selected ? "1" : "0", interactive: "1" }); if (!selected) { console.log(`${DIM$2}Search cancelled${RESET$2}`); console.log(); return; } const pkg = selected.source || selected.slug; const skillName = selected.name; console.log(); console.log(`${TEXT$1}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`); console.log(); const { source, options } = parseAddOptions([ pkg, "--skill", skillName ]); await runAdd(source, options); console.log(); const info = getOwnerRepoFromString(pkg); if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$2}View the skill at${RESET$2} ${TEXT$1}https://skills.sh/${selected.slug}${RESET$2}`); else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$1}https://skills.sh${RESET$2}`); console.log(); } const isCancelled = (value) => typeof value === "symbol"; function shortenPath$1(fullPath, cwd) { const home = homedir(); if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length); if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length); return fullPath; } async function discoverNodeModuleSkills(cwd) { const nodeModulesDir = join(cwd, "node_modules"); const skills = []; let topNames; try { topNames = await readdir(nodeModulesDir); } catch { return skills; } const processPackageDir = async (pkgDir, packageName) => { const rootSkill = await parseSkillMd(join(pkgDir, "SKILL.md")); if (rootSkill) { skills.push({ ...rootSkill, packageName }); return; } const searchDirs = [ pkgDir, join(pkgDir, "skills"), join(pkgDir, ".agents", "skills") ]; for (const searchDir of searchDirs) try { const entries = await readdir(searchDir); for (const name of entries) { const skillDir = join(searchDir, name); try { if (!(await stat(skillDir)).isDirectory()) continue; } catch { continue; } const skill = await parseSkillMd(join(skillDir, "SKILL.md")); if (skill) skills.push({ ...skill, packageName }); } } catch {} }; await Promise.all(topNames.map(async (name) => { if (name.startsWith(".")) return; const fullPath = join(nodeModulesDir, name); try { if (!(await stat(fullPath)).isDirectory()) return; } catch { return; } if (name.startsWith("@")) try { const scopeNames = await readdir(fullPath); await Promise.all(scopeNames.map(async (scopedName) => { const scopedPath = join(fullPath, scopedName); try { if (!(await stat(scopedPath)).isDirectory()) return; } catch { return; } await processPackageDir(scopedPath, `${name}/${scopedName}`); })); } catch {} else await processPackageDir(fullPath, name); })); return skills; } async function runSync(args, options = {}) { const cwd = process.cwd(); const agentResult = await detectAgent(); if (agentResult.isAgent) { options.yes = true; if (!options.agent || options.agent.length === 0) { const mappedAgent = getAgentType(agentResult.agent.name); if (mappedAgent) { const agentList = [mappedAgent]; for (const ua of getUniversalAgents()) if (!agentList.includes(ua)) agentList.push(ua); options.agent = agentList; } } } console.log(); if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills experimental_sync "))); if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively"); const spinner = Y(); spinner.start("Scanning node_modules for skills..."); const discoveredSkills = await discoverNodeModuleSkills(cwd); if (discoveredSkills.length === 0) { spinner.stop(import_picocolors.default.yellow("No skills found")); Se(import_picocolors.default.dim("No SKILL.md files found in node_modules.")); return; } spinner.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`); for (const skill of discoveredSkills) { M.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`); if (skill.description) M.message(import_picocolors.default.dim(` ${skill.description}`)); } const localLock = await readLocalLock(cwd); const toInstall = []; const upToDate = []; if (options.force) { toInstall.push(...discoveredSkills); M.info(import_picocolors.default.dim("Force mode: reinstalling all skills")); } else { for (const skill of discoveredSkills) { const existingEntry = localLock.skills[skill.name]; if (existingEntry) { if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) { upToDate.push(skill.name); continue; } } toInstall.push(skill); } if (upToDate.length > 0) M.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`)); if (toInstall.length === 0) { console.log(); Se(import_picocolors.default.green("All skills are up to date.")); return; } } M.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`); let targetAgents; const validAgents = Object.keys(agents); const universalAgents = getUniversalAgents(); if (options.agent?.includes("*")) { targetAgents = validAgents; M.info(`Installing to all ${targetAgents.length} agents`); } else if (options.agent && options.agent.length > 0) { const invalidAgents = options.agent.filter((a) => !validAgents.includes(a)); if (invalidAgents.length > 0) { M.error(`Invalid agents: ${invalidAgents.join(", ")}`); M.info(`Valid agents: ${validAgents.join(", ")}`); process.exit(1); } targetAgents = options.agent; } else { spinner.start("Loading agents..."); const installedAgents = await detectInstalledAgents(); const totalAgents = Object.keys(agents).length; spinner.stop(`${totalAgents} agents`); if (installedAgents.length === 0) if (options.yes) { targetAgents = universalAgents; M.info("Installing to universal agents"); } else { const selected = await searchMultiselect({ message: "Which agents do you want to install to?", items: getNonUniversalAgents().map((a) => ({ value: a, label: agents[a].displayName, hint: agents[a].skillsDir })), initialSelected: [], lockedSection: { title: "Universal (.agents/skills)", items: universalAgents.map((a) => ({ value: a, label: agents[a].displayName })) } }); if (isCancelled(selected)) { xe("Sync cancelled"); process.exit(0); } targetAgents = selected; } else if (installedAgents.length === 1 || options.yes) { targetAgents = [...installedAgents]; for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua); } else { const selected = await searchMultiselect({ message: "Which agents do you want to install to?", items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({ value: a, label: agents[a].displayName, hint: agents[a].skillsDir })), initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)), lockedSection: { title: "Universal (.agents/skills)", items: universalAgents.map((a) => ({ value: a, label: agents[a].displayName })) } }); if (isCancelled(selected)) { xe("Sync cancelled"); process.exit(0); } targetAgents = selected; } } const summaryLines = []; for (const skill of toInstall) { const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd); summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`); summaryLines.push(` ${import_picocolors.default.dim(shortCanonical)}`); } console.log(); Me(summaryLines.join("\n"), "Sync Summary"); if (!options.yes) { const confirmed = await ye({ message: "Proceed with sync?" }); if (pD(confirmed) || !confirmed) { xe("Sync cancelled"); process.exit(0); } } spinner.start("Syncing skills..."); const results = []; for (const skill of toInstall) for (const agent of targetAgents) { const result = await installSkillForAgent(skill, agent, { global: false, cwd, mode: "symlink" }); results.push({ skill: skill.name, packageName: skill.packageName, agent: agents[agent].displayName, success: result.success, path: result.path, canonicalPath: result.canonicalPath, error: result.error }); } spinner.stop("Sync complete"); const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); const successfulSkillNames = new Set(successful.map((r) => r.skill)); for (const skill of toInstall) if (successfulSkillNames.has(skill.name)) try { const computedHash = await computeSkillFolderHash(skill.path); await addSkillToLocalLock(skill.name, { source: skill.packageName, sourceType: "node_modules", computedHash }, cwd); } catch {} console.log(); if (successful.length > 0) { const bySkill = /* @__PURE__ */ new Map(); for (const r of successful) { const skillResults = bySkill.get(r.skill) || []; skillResults.push(r); bySkill.set(r.skill, skillResults); } const resultLines = []; for (const [skillName, skillResults] of bySkill) { const firstResult = skillResults[0]; const pkg = toInstall.find((s) => s.name === skillName)?.packageName; if (firstResult.canonicalPath) { const shortPath = shortenPath$1(firstResult.canonicalPath, cwd); resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`); resultLines.push(` ${import_picocolors.default.dim(shortPath)}`); } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`); } const skillCount = bySkill.size; const title = import_picocolors.default.green(`Synced ${skillCount} skill${skillCount !== 1 ? "s" : ""}`); Me(resultLines.join("\n"), title); } if (failed.length > 0) { console.log(); M.error(import_picocolors.default.red(`Failed to install ${failed.length}`)); for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`); } track({ event: "experimental_sync", skillCount: String(toInstall.length), successCount: String(successfulSkillNames.size), agents: targetAgents.join(",") }); console.log(); Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions.")); } function parseSyncOptions(args) { const options = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "-y" || arg === "--yes") options.yes = true; else if (arg === "-f" || arg === "--force") options.force = true; else if (arg === "-a" || arg === "--agent") { options.agent = options.agent || []; i++; let nextArg = args[i]; while (i < args.length && nextArg && !nextArg.startsWith("-")) { options.agent.push(nextArg); i++; nextArg = args[i]; } i--; } } return { options }; } async function runInstallFromLock(args) { const lock = await readLocalLock(process.cwd()); const skillEntries = Object.entries(lock.skills); if (skillEntries.length === 0) { M.warn("No project skills found in skills-lock.json"); M.info(`Add project-level skills with ${import_picocolors.default.cyan("npx skills add ")} (without ${import_picocolors.default.cyan("-g")})`); return; } const universalAgentNames = getUniversalAgents(); const nodeModuleSkills = []; const bySource = /* @__PURE__ */ new Map(); for (const [skillName, entry] of skillEntries) { if (entry.sourceType === "node_modules") { nodeModuleSkills.push(skillName); continue; } const installSource = entry.ref ? `${entry.source}#${entry.ref}` : entry.source; const existing = bySource.get(installSource); if (existing) existing.skills.push(skillName); else bySource.set(installSource, { sourceType: entry.sourceType, skills: [skillName] }); } const remoteCount = skillEntries.length - nodeModuleSkills.length; 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/")}`); for (const [source, { skills }] of bySource) try { await runAdd([source], { skill: skills, agent: universalAgentNames, yes: true }); } catch (error) { M.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${error instanceof Error ? error.message : "Unknown error"}`); } if (nodeModuleSkills.length > 0) { M.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`); try { const { options: syncOptions } = parseSyncOptions(args); await runSync(args, { ...syncOptions, yes: true, agent: universalAgentNames }); } catch (error) { M.error(`Failed to sync node_modules skills: ${error instanceof Error ? error.message : "Unknown error"}`); } } } const RESET$1 = "\x1B[0m"; const BOLD$1 = "\x1B[1m"; const DIM$1 = "\x1B[38;5;102m"; const CYAN = "\x1B[36m"; const YELLOW = "\x1B[33m"; function shortenPath(fullPath, cwd) { const home = homedir(); if (fullPath.startsWith(home)) return fullPath.replace(home, "~"); if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length); return fullPath; } function formatList(items, maxShow = 5) { if (items.length <= maxShow) return items.join(", "); const shown = items.slice(0, maxShow); const remaining = items.length - maxShow; return `${shown.join(", ")} +${remaining} more`; } function parseListOptions(args) { const options = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "-g" || arg === "--global") options.global = true; else if (arg === "--json") options.json = true; else if (arg === "-a" || arg === "--agent") { options.agent = options.agent || []; while (i + 1 < args.length && !args[i + 1].startsWith("-")) options.agent.push(args[++i]); } } return options; } async function runList(args) { const options = parseListOptions(args); const scope = options.global === true ? true : false; let agentFilter; if (options.agent && options.agent.length > 0) { const validAgents = Object.keys(agents); const invalidAgents = options.agent.filter((a) => !validAgents.includes(a)); if (invalidAgents.length > 0) { console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$1}`); console.log(`${DIM$1}Valid agents: ${validAgents.join(", ")}${RESET$1}`); process.exit(1); } agentFilter = options.agent; } const installedSkills = await listInstalledSkills({ global: scope, agentFilter }); if (options.json) { const jsonOutput = installedSkills.map((skill) => ({ name: skill.name, path: skill.canonicalPath, scope: skill.scope, agents: skill.agents.map((a) => agents[a].displayName) })); console.log(JSON.stringify(jsonOutput, null, 2)); return; } const lockedSkills = await getAllLockedSkills(); const cwd = process.cwd(); const scopeLabel = scope ? "Global" : "Project"; if (installedSkills.length === 0) { if (options.json) { console.log("[]"); return; } console.log(`${DIM$1}No ${scopeLabel.toLowerCase()} skills found.${RESET$1}`); if (scope) console.log(`${DIM$1}Try listing project skills without -g${RESET$1}`); else console.log(`${DIM$1}Try listing global skills with -g${RESET$1}`); return; } function printSkill(skill, indent = false) { const prefix = indent ? " " : ""; const shortPath = shortenPath(skill.canonicalPath, cwd); const agentNames = skill.agents.map((a) => agents[a].displayName); const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$1}`; console.log(`${prefix}${CYAN}${sanitizeMetadata(skill.name)}${RESET$1} ${DIM$1}${shortPath}${RESET$1}`); console.log(`${prefix} ${DIM$1}Agents:${RESET$1} ${agentInfo}`); } console.log(`${BOLD$1}${scopeLabel} Skills${RESET$1}`); console.log(); const groupedSkills = {}; const ungroupedSkills = []; for (const skill of installedSkills) { const lockEntry = lockedSkills[skill.name]; if (lockEntry?.pluginName) { const group = lockEntry.pluginName; if (!groupedSkills[group]) groupedSkills[group] = []; groupedSkills[group].push(skill); } else ungroupedSkills.push(skill); } if (Object.keys(groupedSkills).length > 0) { const sortedGroups = Object.keys(groupedSkills).sort(); for (const group of sortedGroups) { const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); console.log(`${BOLD$1}${title}${RESET$1}`); const skills = groupedSkills[group]; if (skills) for (const skill of skills) printSkill(skill, true); console.log(); } if (ungroupedSkills.length > 0) { console.log(`${BOLD$1}General${RESET$1}`); for (const skill of ungroupedSkills) printSkill(skill, true); console.log(); } } else { for (const skill of installedSkills) printSkill(skill); console.log(); } } async function removeCommand(skillNames, options) { const agentResult = await detectAgent(); if (agentResult.isAgent) { options.yes = true; M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — removing non-interactively"); } const isGlobal = options.global ?? false; const cwd = process.cwd(); const spinner = Y(); spinner.start("Scanning for installed skills..."); const skillNamesSet = /* @__PURE__ */ new Set(); const scanDir = async (dir) => { try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) if (entry.isDirectory()) skillNamesSet.add(entry.name); } catch (err) { if (err instanceof Error && err.code !== "ENOENT") M.warn(`Could not scan directory ${dir}: ${err.message}`); } }; if (isGlobal) { await scanDir(getCanonicalSkillsDir(true, cwd)); for (const agent of Object.values(agents)) if (agent.globalSkillsDir !== void 0) await scanDir(agent.globalSkillsDir); } else { await scanDir(getCanonicalSkillsDir(false, cwd)); for (const agent of Object.values(agents)) await scanDir(join(cwd, agent.skillsDir)); } const installedSkills = Array.from(skillNamesSet).sort(); spinner.stop(`Found ${installedSkills.length} unique installed skill(s)`); if (installedSkills.length === 0) { Se(import_picocolors.default.yellow("No skills found to remove.")); return; } if (options.agent && options.agent.length > 0) { const validAgents = Object.keys(agents); const invalidAgents = options.agent.filter((a) => !validAgents.includes(a)); if (invalidAgents.length > 0) { M.error(`Invalid agents: ${invalidAgents.join(", ")}`); M.info(`Valid agents: ${validAgents.join(", ")}`); process.exit(1); } } let selectedSkills = []; if (options.all) selectedSkills = installedSkills; else if (skillNames.length > 0) { selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase())); if (selectedSkills.length === 0) { M.error(`No matching skills found for: ${skillNames.join(", ")}`); return; } } else { const choices = installedSkills.map((s) => ({ value: s, label: s })); const selected = await fe({ message: `Select skills to remove ${import_picocolors.default.dim("(space to toggle)")}`, options: choices, required: true }); if (pD(selected)) { xe("Removal cancelled"); process.exit(0); } selectedSkills = selected; } let targetAgents; if (options.agent && options.agent.length > 0) targetAgents = options.agent; else { targetAgents = Object.keys(agents); spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`); } if (!options.yes) { console.log(); M.info("Skills to remove:"); for (const skill of selectedSkills) M.message(` ${import_picocolors.default.red("•")} ${skill}`); console.log(); const confirmed = await ye({ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?` }); if (pD(confirmed) || !confirmed) { xe("Removal cancelled"); process.exit(0); } } spinner.start("Removing skills..."); const results = []; for (const skillName of selectedSkills) try { const canonicalPath = getCanonicalPath(skillName, { global: isGlobal, cwd }); for (const agentKey of targetAgents) { const agent = agents[agentKey]; const skillPath = getInstallPath(skillName, agentKey, { global: isGlobal, cwd }); const pathsToCleanup = new Set([skillPath]); const sanitizedName = sanitizeName(skillName); if (isGlobal && agent.globalSkillsDir) pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName)); else pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName)); for (const pathToCleanup of pathsToCleanup) { if (pathToCleanup === canonicalPath) continue; try { if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, { recursive: true, force: true }); } catch (err) { M.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`); } } } const remainingAgents = (await detectInstalledAgents()).filter((a) => !targetAgents.includes(a)); let isStillUsed = false; for (const agentKey of remainingAgents) if (await lstat(getInstallPath(skillName, agentKey, { global: isGlobal, cwd })).catch(() => null)) { isStillUsed = true; break; } if (!isStillUsed) await rm(canonicalPath, { recursive: true, force: true }); const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null; const effectiveSource = lockEntry?.source || "local"; const effectiveSourceType = lockEntry?.sourceType || "local"; if (isGlobal) await removeSkillFromLock(skillName); results.push({ skill: skillName, success: true, source: effectiveSource, sourceType: effectiveSourceType }); } catch (err) { results.push({ skill: skillName, success: false, error: err instanceof Error ? err.message : String(err) }); } spinner.stop("Removal process complete"); const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); if (successful.length > 0) { const bySource = /* @__PURE__ */ new Map(); for (const r of successful) { const source = r.source || "local"; const existing = bySource.get(source) || { skills: [] }; existing.skills.push(r.skill); existing.sourceType = r.sourceType; bySource.set(source, existing); } for (const [source, data] of bySource) track({ event: "remove", source, skills: data.skills.join(","), agents: targetAgents.join(","), ...isGlobal && { global: "1" }, sourceType: data.sourceType }); } if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} skill(s)`)); if (failed.length > 0) { M.error(import_picocolors.default.red(`Failed to remove ${failed.length} skill(s)`)); for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill}: ${r.error}`); } console.log(); Se(import_picocolors.default.green("Done!")); } function parseRemoveOptions(args) { const options = {}; const skills = []; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "-g" || arg === "--global") options.global = true; else if (arg === "-y" || arg === "--yes") options.yes = true; else if (arg === "--all") options.all = true; else if (arg === "-a" || arg === "--agent") { options.agent = options.agent || []; i++; let nextArg = args[i]; while (i < args.length && nextArg && !nextArg.startsWith("-")) { options.agent.push(nextArg); i++; nextArg = args[i]; } i--; } else if (arg && !arg.startsWith("-")) skills.push(arg); } return { skills, options }; } function formatSourceInput(sourceUrl, ref) { if (!ref) return sourceUrl; return `${sourceUrl}#${ref}`; } function deriveSkillFolder(skillPath) { let folder = skillPath; if (folder.endsWith("/SKILL.md")) folder = folder.slice(0, -9); else if (folder.endsWith("SKILL.md")) folder = folder.slice(0, -8); if (folder.endsWith("/")) folder = folder.slice(0, -1); return folder; } function appendFolderAndRef(source, skillPath, ref) { const folder = deriveSkillFolder(skillPath); const withFolder = folder ? `${source}/${folder}` : source; return ref ? `${withFolder}#${ref}` : withFolder; } function buildUpdateInstallSource(entry) { if (!entry.skillPath) return formatSourceInput(entry.sourceUrl, entry.ref); return appendFolderAndRef(entry.source, entry.skillPath, entry.ref); } function buildLocalUpdateSource(entry) { if (!entry.skillPath) return formatSourceInput(entry.source, entry.ref); return appendFolderAndRef(entry.source, entry.skillPath, entry.ref); } const __dirname = dirname(fileURLToPath(import.meta.url)); function getVersion() { try { const pkgPath = join(__dirname, "..", "package.json"); return JSON.parse(readFileSync(pkgPath, "utf-8")).version; } catch { return "0.0.0"; } } const VERSION = getVersion(); initTelemetry(VERSION); const RESET = "\x1B[0m"; const BOLD = "\x1B[1m"; const DIM = "\x1B[38;5;102m"; const TEXT = "\x1B[38;5;145m"; const LOGO_LINES = [ "███████╗██╗ ██╗██╗██╗ ██╗ ███████╗", "██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝", "███████╗█████╔╝ ██║██║ ██║ ███████╗", "╚════██║██╔═██╗ ██║██║ ██║ ╚════██║", "███████║██║ ██╗██║███████╗███████╗███████║", "╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝" ]; const GRAYS = [ "\x1B[38;5;250m", "\x1B[38;5;248m", "\x1B[38;5;245m", "\x1B[38;5;243m", "\x1B[38;5;240m", "\x1B[38;5;238m" ]; function showLogo() { console.log(); LOGO_LINES.forEach((line, i) => { console.log(`${GRAYS[i]}${line}${RESET}`); }); } function showBanner() { showLogo(); console.log(); console.log(`${DIM}The open agent skills ecosystem${RESET}`); console.log(); console.log(` ${DIM}$${RESET} ${TEXT}npx skills add ${DIM}${RESET} ${DIM}Add a new skill${RESET}`); console.log(` ${DIM}$${RESET} ${TEXT}npx skills remove${RESET} ${DIM}Remove installed skills${RESET}`); console.log(` ${DIM}$${RESET} ${TEXT}npx skills list${RESET} ${DIM}List installed skills${RESET}`); console.log(` ${DIM}$${RESET} ${TEXT}npx skills find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`); console.log(); console.log(` ${DIM}$${RESET} ${TEXT}npx skills update${RESET} ${DIM}Update installed skills${RESET}`); console.log(); console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`); console.log(` ${DIM}$${RESET} ${TEXT}npx skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`); console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`); console.log(); console.log(`${DIM}try:${RESET} npx skills add vercel-labs/agent-skills`); console.log(); console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`); console.log(); } function showHelp() { console.log(` ${BOLD}Usage:${RESET} skills [options] ${BOLD}Manage Skills:${RESET} add Add a skill package (alias: a) e.g. vercel-labs/agent-skills https://github.com/vercel-labs/agent-skills remove [skills] Remove installed skills list, ls List installed skills find [query] Search for skills interactively ${BOLD}Updates:${RESET} update [skills...] Update skills to latest versions (alias: upgrade) ${BOLD}Update Options:${RESET} -g, --global Update global skills only -p, --project Update project skills only -y, --yes Skip scope prompt (auto-detect: project if in a project, else global) ${BOLD}Project:${RESET} experimental_install Restore skills from skills-lock.json init [name] Initialize a skill (creates /SKILL.md or ./SKILL.md) experimental_sync Sync skills from node_modules into agent directories ${BOLD}Add Options:${RESET} -g, --global Install skill globally (user-level) instead of project-level -a, --agent Specify agents to install to (use '*' for all agents) -s, --skill Specify skill names to install (use '*' for all skills) -l, --list List available skills in the repository without installing -y, --yes Skip confirmation prompts --copy Copy files instead of symlinking to agent directories --all Shorthand for --skill '*' --agent '*' -y --full-depth Search all subdirectories even when a root SKILL.md exists ${BOLD}Remove Options:${RESET} -g, --global Remove from global scope -a, --agent Remove from specific agents (use '*' for all agents) -s, --skill Specify skills to remove (use '*' for all skills) -y, --yes Skip confirmation prompts --all Shorthand for --skill '*' --agent '*' -y ${BOLD}Experimental Sync Options:${RESET} -a, --agent Specify agents to install to (use '*' for all agents) -y, --yes Skip confirmation prompts ${BOLD}List Options:${RESET} -g, --global List global skills (default: project) -a, --agent Filter by specific agents --json Output as JSON (machine-readable, no ANSI codes) ${BOLD}Options:${RESET} --help, -h Show this help message --version, -v Show version number ${BOLD}Examples:${RESET} ${DIM}$${RESET} skills add vercel-labs/agent-skills ${DIM}$${RESET} skills add vercel-labs/agent-skills -g ${DIM}$${RESET} skills add vercel-labs/agent-skills --agent claude-code cursor ${DIM}$${RESET} skills add vercel-labs/agent-skills --skill pr-review commit ${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET} ${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET} ${DIM}$${RESET} skills rm --global frontend-design ${DIM}$${RESET} skills list ${DIM}# list project skills${RESET} ${DIM}$${RESET} skills ls -g ${DIM}# list global skills${RESET} ${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET} ${DIM}$${RESET} skills ls --json ${DIM}# JSON output${RESET} ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET} ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET} ${DIM}$${RESET} skills update ${DIM}$${RESET} skills update my-skill ${DIM}# update a single skill${RESET} ${DIM}$${RESET} skills update -g ${DIM}# update global skills only${RESET} ${DIM}$${RESET} skills experimental_install ${DIM}# restore from skills-lock.json${RESET} ${DIM}$${RESET} skills init my-skill ${DIM}$${RESET} skills experimental_sync ${DIM}# sync from node_modules${RESET} ${DIM}$${RESET} skills experimental_sync -y ${DIM}# sync without prompts${RESET} Discover more skills at ${TEXT}https://skills.sh/${RESET} `); } function showRemoveHelp() { console.log(` ${BOLD}Usage:${RESET} skills remove [skills...] [options] ${BOLD}Description:${RESET} Remove installed skills from agents. If no skill names are provided, an interactive selection menu will be shown. ${BOLD}Arguments:${RESET} skills Optional skill names to remove (space-separated) ${BOLD}Options:${RESET} -g, --global Remove from global scope (~/) instead of project scope -a, --agent Remove from specific agents (use '*' for all agents) -s, --skill Specify skills to remove (use '*' for all skills) -y, --yes Skip confirmation prompts --all Shorthand for --skill '*' --agent '*' -y ${BOLD}Examples:${RESET} ${DIM}$${RESET} skills remove ${DIM}# interactive selection${RESET} ${DIM}$${RESET} skills remove my-skill ${DIM}# remove specific skill${RESET} ${DIM}$${RESET} skills remove skill1 skill2 -y ${DIM}# remove multiple skills${RESET} ${DIM}$${RESET} skills remove --global my-skill ${DIM}# remove from global scope${RESET} ${DIM}$${RESET} skills rm --agent claude-code my-skill ${DIM}# remove from specific agent${RESET} ${DIM}$${RESET} skills remove --all ${DIM}# remove all skills${RESET} ${DIM}$${RESET} skills remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET} Discover more skills at ${TEXT}https://skills.sh/${RESET} `); } function runInit(args) { const cwd = process.cwd(); const skillName = args[0] || basename(cwd); const hasName = args[0] !== void 0; const skillDir = hasName ? join(cwd, skillName) : cwd; const skillFile = join(skillDir, "SKILL.md"); const displayPath = hasName ? `${skillName}/SKILL.md` : "SKILL.md"; if (existsSync(skillFile)) { console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`); return; } if (hasName) mkdirSync(skillDir, { recursive: true }); writeFileSync(skillFile, `--- name: ${skillName} description: A brief description of what this skill does --- # ${skillName} Instructions for the agent to follow when this skill is activated. ## When to use Describe when this skill should be used. ## Instructions 1. First step 2. Second step 3. Additional steps as needed `); console.log(`${TEXT}Initialized skill: ${DIM}${skillName}${RESET}`); console.log(); console.log(`${DIM}Created:${RESET}`); console.log(` ${displayPath}`); console.log(); console.log(`${DIM}Next steps:${RESET}`); console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`); console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`); console.log(); console.log(`${DIM}Publishing:${RESET}`); console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx skills add /${RESET}`); console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx skills add https://example.com/${displayPath}${RESET}`); console.log(); console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`); console.log(); } const AGENTS_DIR = ".agents"; const LOCK_FILE = ".skill-lock.json"; const CURRENT_LOCK_VERSION = 3; function getSkillLockPath() { const xdgStateHome = process.env.XDG_STATE_HOME; if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE); return join(homedir(), AGENTS_DIR, LOCK_FILE); } function readSkillLock() { const lockPath = getSkillLockPath(); try { const content = readFileSync(lockPath, "utf-8"); const parsed = JSON.parse(content); if (typeof parsed.version !== "number" || !parsed.skills) return { version: CURRENT_LOCK_VERSION, skills: {} }; if (parsed.version < CURRENT_LOCK_VERSION) return { version: CURRENT_LOCK_VERSION, skills: {} }; return parsed; } catch { return { version: CURRENT_LOCK_VERSION, skills: {} }; } } function parseUpdateOptions(args) { const options = {}; const positional = []; for (const arg of args) if (arg === "-g" || arg === "--global") options.global = true; else if (arg === "-p" || arg === "--project") options.project = true; else if (arg === "-y" || arg === "--yes") options.yes = true; else if (!arg.startsWith("-")) positional.push(arg); if (positional.length > 0) options.skills = positional; return options; } function hasProjectSkills(cwd) { const dir = cwd || process.cwd(); if (existsSync(join(dir, "skills-lock.json"))) return true; const skillsDir = join(dir, ".agents", "skills"); try { const entries = readdirSync(skillsDir, { withFileTypes: true }); for (const entry of entries) if (entry.isDirectory()) { if (existsSync(join(skillsDir, entry.name, "SKILL.md"))) return true; } } catch {} return false; } async function resolveUpdateScope(options) { if (options.skills && options.skills.length > 0) { if (options.global) return "global"; if (options.project) return "project"; return "both"; } if (options.global && options.project) return "both"; if (options.global) return "global"; if (options.project) return "project"; if (options.yes || !process.stdin.isTTY) return hasProjectSkills() ? "project" : "global"; const scope = await ve({ message: "Update scope", options: [ { value: "project", label: "Project", hint: "Update skills in current directory" }, { value: "global", label: "Global", hint: "Update skills in home directory" }, { value: "both", label: "Both", hint: "Update all skills" } ] }); if (pD(scope)) { xe("Cancelled"); process.exit(0); } return scope; } function matchesSkillFilter(name, filter) { if (!filter || filter.length === 0) return true; const lower = name.toLowerCase(); return filter.some((f) => f.toLowerCase() === lower); } function getSkipReason(entry) { if (entry.sourceType === "local") return "Local path"; if (entry.sourceType === "git") return "Git URL"; if (entry.sourceType === "well-known") return "Well-known skill"; if (!entry.skillFolderHash) return "Private or deleted repo"; if (!entry.skillPath) return "No skill path recorded"; return "No version tracking"; } function getInstallSource(skill) { let url = skill.sourceUrl; if (skill.sourceType === "well-known") { const idx = url.indexOf("/.well-known/"); if (idx !== -1) url = url.slice(0, idx); } return formatSourceInput(url, skill.ref); } function printSkippedSkills(skipped) { if (skipped.length === 0) return; console.log(); console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`); const grouped = /* @__PURE__ */ new Map(); for (const skill of skipped) { const source = getInstallSource(skill); const existing = grouped.get(source) || []; existing.push(skill); grouped.set(source, existing); } for (const [source, skills] of grouped) { if (skills.length === 1) { const skill = skills[0]; console.log(` ${TEXT}•${RESET} ${sanitizeMetadata(skill.name)} ${DIM}(${skill.reason})${RESET}`); } else { const reason = skills[0].reason; const names = skills.map((s) => sanitizeMetadata(s.name)).join(", "); console.log(` ${TEXT}•${RESET} ${names} ${DIM}(${reason})${RESET}`); } console.log(` ${DIM}To update: ${TEXT}npx skills add ${source} -g -y${RESET}`); } } async function getProjectSkillsForUpdate(skillFilter) { const localLock = await readLocalLock(); const skills = []; for (const [name, entry] of Object.entries(localLock.skills)) { if (!matchesSkillFilter(name, skillFilter)) continue; if (entry.sourceType === "node_modules" || entry.sourceType === "local") continue; skills.push({ name, source: entry.source, entry }); } return skills; } async function updateGlobalSkills(skillFilter) { const lock = readSkillLock(); const skillNames = Object.keys(lock.skills); let successCount = 0; let failCount = 0; if (skillNames.length === 0) { if (!skillFilter) { console.log(`${DIM}No global skills tracked in lock file.${RESET}`); console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add -g${RESET}`); } return { successCount, failCount, checkedCount: 0 }; } const updates = []; const skipped = []; const checkable = []; for (const skillName of skillNames) { if (!matchesSkillFilter(skillName, skillFilter)) continue; const entry = lock.skills[skillName]; if (!entry) continue; if (!entry.skillFolderHash || !entry.skillPath) { skipped.push({ name: skillName, reason: getSkipReason(entry), sourceUrl: entry.sourceUrl, sourceType: entry.sourceType, ref: entry.ref }); continue; } checkable.push({ name: skillName, entry }); } for (let i = 0; i < checkable.length; i++) { const { name: skillName, entry } = checkable[i]; process.stdout.write(`\r${DIM}Checking global skill ${i + 1}/${checkable.length}: ${sanitizeMetadata(skillName)}${RESET}\x1b[K`); try { const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, getGitHubToken, entry.ref); if (latestHash && latestHash !== entry.skillFolderHash) updates.push({ name: skillName, source: entry.source, entry }); } catch {} } if (checkable.length > 0) process.stdout.write("\r\x1B[K"); const checkedCount = checkable.length + skipped.length; if (checkable.length === 0 && skipped.length === 0) { if (!skillFilter) console.log(`${DIM}No global skills to check.${RESET}`); return { successCount, failCount, checkedCount: 0 }; } if (checkable.length === 0 && skipped.length > 0) { printSkippedSkills(skipped); return { successCount, failCount, checkedCount }; } if (updates.length === 0) { console.log(`${TEXT}✓ All global skills are up to date${RESET}`); return { successCount, failCount, checkedCount }; } console.log(`${TEXT}Found ${updates.length} global update(s)${RESET}`); console.log(); for (const update of updates) { const safeName = sanitizeMetadata(update.name); console.log(`${TEXT}Updating ${safeName}...${RESET}`); const installUrl = buildUpdateInstallSource(update.entry); const cliEntry = join(__dirname, "..", "bin", "cli.mjs"); if (!existsSync(cliEntry)) { failCount++; console.log(` ${DIM}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET}`); continue; } if (spawnSync(process.execPath, [ cliEntry, "add", installUrl, "-g", "-y" ], { stdio: [ "inherit", "pipe", "pipe" ], encoding: "utf-8", shell: process.platform === "win32" }).status === 0) { successCount++; console.log(` ${TEXT}✓${RESET} Updated ${safeName}`); } else { failCount++; console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`); } } printSkippedSkills(skipped); return { successCount, failCount, checkedCount }; } async function updateProjectSkills(skillFilter) { const projectSkills = await getProjectSkillsForUpdate(skillFilter); let successCount = 0; let failCount = 0; if (projectSkills.length === 0) { if (!skillFilter) { console.log(`${DIM}No project skills to update.${RESET}`); console.log(`${DIM}Install project skills with${RESET} ${TEXT}npx skills add ${RESET}`); } return { successCount, failCount, foundCount: 0 }; } const updatable = projectSkills.filter((s) => s.entry.skillPath); const legacy = projectSkills.filter((s) => !s.entry.skillPath); if (updatable.length === 0) { console.log(`${DIM}No project skills can be updated in place.${RESET}`); printLegacyProjectSkills(legacy); return { successCount, failCount, foundCount: projectSkills.length }; } const cwd = process.cwd(); const targetAgentNames = []; let hasUniversal = false; for (const [type, config] of Object.entries(agents)) if (isUniversalAgent(type)) { if (!hasUniversal && existsSync(join(cwd, ".agents"))) hasUniversal = true; } else { const agentRoot = config.skillsDir.split("/")[0]; if (existsSync(join(cwd, agentRoot))) targetAgentNames.push(config.displayName); } const targetParts = []; if (hasUniversal) targetParts.push("Universal"); targetParts.push(...targetAgentNames); if (targetParts.length > 0) console.log(`${TEXT}Updating for: ${targetParts.join(", ")}${RESET}`); console.log(`${TEXT}Refreshing ${updatable.length} skill(s)...${RESET}`); console.log(); for (const skill of updatable) { const safeName = sanitizeMetadata(skill.name); console.log(`${TEXT}Updating ${safeName}...${RESET}`); const installUrl = buildLocalUpdateSource(skill.entry); const cliEntry = join(__dirname, "..", "bin", "cli.mjs"); if (!existsSync(cliEntry)) { failCount++; console.log(` ${DIM}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET}`); continue; } if (spawnSync(process.execPath, [ cliEntry, "add", installUrl, "--skill", skill.name, "-y" ], { stdio: [ "inherit", "pipe", "pipe" ], encoding: "utf-8", shell: process.platform === "win32" }).status === 0) { successCount++; console.log(` ${TEXT}✓${RESET} Updated ${safeName}`); } else { failCount++; console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`); } } printLegacyProjectSkills(legacy); return { successCount, failCount, foundCount: projectSkills.length }; } function printLegacyProjectSkills(legacy) { if (legacy.length === 0) return; console.log(); console.log(`${DIM}${legacy.length} project skill(s) cannot be updated automatically (installed before skillPath tracking):${RESET}`); for (const skill of legacy) { const reinstall = formatSourceInput(skill.entry.source, skill.entry.ref); console.log(` ${TEXT}•${RESET} ${sanitizeMetadata(skill.name)}`); console.log(` ${DIM}To refresh: ${TEXT}npx skills add ${reinstall} -y${RESET}`); } } async function runUpdate(args = []) { const options = parseUpdateOptions(args); const scope = await resolveUpdateScope(options); if (options.skills) console.log(`${TEXT}Updating ${options.skills.join(", ")}...${RESET}`); else console.log(`${TEXT}Checking for skill updates...${RESET}`); console.log(); let totalSuccess = 0; let totalFail = 0; let totalFound = 0; if (scope === "global" || scope === "both") { if (scope === "both" && !options.skills) console.log(`${BOLD}Global Skills${RESET}`); const { successCount, failCount, checkedCount } = await updateGlobalSkills(options.skills); totalSuccess += successCount; totalFail += failCount; totalFound += checkedCount; if (scope === "both" && !options.skills) console.log(); } if (scope === "project" || scope === "both") { if (scope === "both" && !options.skills) console.log(`${BOLD}Project Skills${RESET}`); const { successCount, failCount, foundCount } = await updateProjectSkills(options.skills); totalSuccess += successCount; totalFail += failCount; totalFound += foundCount; } if (options.skills && totalFound === 0) console.log(`${DIM}No installed skills found matching: ${options.skills.join(", ")}${RESET}`); console.log(); if (totalSuccess > 0) console.log(`${TEXT}✓ Updated ${totalSuccess} skill(s)${RESET}`); if (totalFail > 0) console.log(`${DIM}Failed to update ${totalFail} skill(s)${RESET}`); if (totalSuccess === 0 && totalFail === 0) {} track({ event: "update", scope, skillCount: String(totalSuccess + totalFail), successCount: String(totalSuccess), failCount: String(totalFail) }); console.log(); } async function main() { const args = process.argv.slice(2); const inAgent = await isRunningInAgent(); if (args.length === 0) { if (!inAgent) showBanner(); return; } const command = args[0]; const restArgs = args.slice(1); switch (command) { case "find": case "search": case "f": case "s": if (!inAgent) showLogo(); console.log(); await runFind(restArgs); break; case "init": if (!inAgent) showLogo(); console.log(); runInit(restArgs); break; case "experimental_install": if (!inAgent) showLogo(); await runInstallFromLock(restArgs); break; case "i": case "install": case "a": case "add": { if (!inAgent) showLogo(); const { source: addSource, options: addOpts } = parseAddOptions(restArgs); await runAdd(addSource, addOpts); break; } case "remove": case "rm": case "r": if (restArgs.includes("--help") || restArgs.includes("-h")) { showRemoveHelp(); break; } const { skills, options: removeOptions } = parseRemoveOptions(restArgs); await removeCommand(skills, removeOptions); break; case "experimental_sync": { if (!inAgent) showLogo(); const { options: syncOptions } = parseSyncOptions(restArgs); await runSync(restArgs, syncOptions); break; } case "list": case "ls": await runList(restArgs); break; case "check": case "update": case "upgrade": await runUpdate(restArgs); break; case "--help": case "-h": showHelp(); break; case "--version": case "-v": console.log(VERSION); break; default: console.log(`Unknown command: ${command}`); console.log(`Run ${BOLD}skills --help${RESET} for usage.`); } } main().finally(() => flushTelemetry().then(() => process.exit(0))); export {};