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