skills 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +377 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { spawn } from "child_process";
|
|
5
|
-
import {
|
|
4
|
+
import { spawn, spawnSync } from "child_process";
|
|
5
|
+
import {
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
statSync
|
|
12
|
+
} from "fs";
|
|
6
13
|
import { basename, join } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { createHash } from "crypto";
|
|
7
16
|
var RESET = "\x1B[0m";
|
|
8
17
|
var BOLD = "\x1B[1m";
|
|
9
18
|
var DIM = "\x1B[38;5;102m";
|
|
@@ -53,14 +62,22 @@ ${BOLD}Commands:${RESET}
|
|
|
53
62
|
add <package> Add a skill package
|
|
54
63
|
e.g. vercel-labs/agent-skills
|
|
55
64
|
https://github.com/vercel-labs/agent-skills
|
|
65
|
+
check Check for available skill updates
|
|
66
|
+
update Update all skills to latest versions
|
|
67
|
+
generate-lock Generate lock file from installed skills
|
|
56
68
|
|
|
57
69
|
${BOLD}Options:${RESET}
|
|
58
70
|
--help, -h Show this help message
|
|
59
71
|
--version, -v Show version number
|
|
72
|
+
--refresh, -r Force refresh from source (check)
|
|
73
|
+
--dry-run Preview changes without writing (generate-lock)
|
|
60
74
|
|
|
61
75
|
${BOLD}Examples:${RESET}
|
|
62
76
|
${DIM}$${RESET} skills init my-skill
|
|
63
77
|
${DIM}$${RESET} skills add vercel-labs/agent-skills
|
|
78
|
+
${DIM}$${RESET} skills check
|
|
79
|
+
${DIM}$${RESET} skills update
|
|
80
|
+
${DIM}$${RESET} skills generate-lock --dry-run
|
|
64
81
|
|
|
65
82
|
Discover more skills at ${TEXT}https://skills.sh/${RESET}
|
|
66
83
|
`);
|
|
@@ -72,7 +89,10 @@ function getOwnerRepo(pkg) {
|
|
|
72
89
|
}
|
|
73
90
|
const githubMatch = pkg.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
74
91
|
if (githubMatch && githubMatch[1] && githubMatch[2]) {
|
|
75
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
owner: githubMatch[1],
|
|
94
|
+
repo: githubMatch[2].replace(/\.git$/, "")
|
|
95
|
+
};
|
|
76
96
|
}
|
|
77
97
|
return null;
|
|
78
98
|
}
|
|
@@ -160,6 +180,343 @@ Describe when this skill should be used.
|
|
|
160
180
|
console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
|
|
161
181
|
console.log();
|
|
162
182
|
}
|
|
183
|
+
var AGENTS_DIR = ".agents";
|
|
184
|
+
var SKILLS_SUBDIR = "skills";
|
|
185
|
+
var LOCK_FILE = ".skill-lock.json";
|
|
186
|
+
var SEARCH_API_URL = "https://skills.sh/api/skills/search";
|
|
187
|
+
var CHECK_UPDATES_API_URL = "https://add-skill.vercel.sh/check-updates";
|
|
188
|
+
var CURRENT_LOCK_VERSION = 2;
|
|
189
|
+
function getSkillLockPath() {
|
|
190
|
+
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
191
|
+
}
|
|
192
|
+
function readSkillLock() {
|
|
193
|
+
const lockPath = getSkillLockPath();
|
|
194
|
+
try {
|
|
195
|
+
const content = readFileSync(lockPath, "utf-8");
|
|
196
|
+
const parsed = JSON.parse(content);
|
|
197
|
+
if (typeof parsed.version !== "number" || !parsed.skills) {
|
|
198
|
+
return { version: CURRENT_LOCK_VERSION, skills: {} };
|
|
199
|
+
}
|
|
200
|
+
if (parsed.version < CURRENT_LOCK_VERSION) {
|
|
201
|
+
return { version: CURRENT_LOCK_VERSION, skills: {} };
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
} catch {
|
|
205
|
+
return { version: CURRENT_LOCK_VERSION, skills: {} };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function writeSkillLock(lock) {
|
|
209
|
+
const lockPath = getSkillLockPath();
|
|
210
|
+
const dir = join(homedir(), AGENTS_DIR);
|
|
211
|
+
if (!existsSync(dir)) {
|
|
212
|
+
mkdirSync(dir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
215
|
+
}
|
|
216
|
+
function getInstalledSkillNames() {
|
|
217
|
+
const skillsDir = join(homedir(), AGENTS_DIR, SKILLS_SUBDIR);
|
|
218
|
+
const skillNames = [];
|
|
219
|
+
try {
|
|
220
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
223
|
+
const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
|
|
224
|
+
try {
|
|
225
|
+
const stat = statSync(skillMdPath);
|
|
226
|
+
if (stat.isFile()) {
|
|
227
|
+
skillNames.push(entry.name);
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
try {
|
|
231
|
+
const contents = readdirSync(join(skillsDir, entry.name));
|
|
232
|
+
if (contents.length > 0) {
|
|
233
|
+
skillNames.push(entry.name);
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
return skillNames;
|
|
241
|
+
}
|
|
242
|
+
async function fuzzyMatchSkills(skillNames, apiUrl = SEARCH_API_URL) {
|
|
243
|
+
if (skillNames.length === 0)
|
|
244
|
+
return {};
|
|
245
|
+
const response = await fetch(apiUrl, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: { "Content-Type": "application/json" },
|
|
248
|
+
body: JSON.stringify({ skills: skillNames })
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
252
|
+
}
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
return data.matches;
|
|
255
|
+
}
|
|
256
|
+
function inferSourceType(source) {
|
|
257
|
+
if (source.startsWith("mintlify/"))
|
|
258
|
+
return "mintlify";
|
|
259
|
+
if (source.startsWith("huggingface/"))
|
|
260
|
+
return "huggingface";
|
|
261
|
+
return "github";
|
|
262
|
+
}
|
|
263
|
+
function buildSourceUrl(source, sourceType) {
|
|
264
|
+
switch (sourceType) {
|
|
265
|
+
case "github":
|
|
266
|
+
return `https://github.com/${source}.git`;
|
|
267
|
+
case "mintlify":
|
|
268
|
+
return source;
|
|
269
|
+
case "huggingface":
|
|
270
|
+
const parts = source.replace("huggingface/", "").split("/");
|
|
271
|
+
return `https://huggingface.co/spaces/${parts.join("/")}`;
|
|
272
|
+
default:
|
|
273
|
+
return source;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function runGenerateLock(args) {
|
|
277
|
+
const dryRun = args.includes("--dry-run");
|
|
278
|
+
const apiUrlIdx = args.indexOf("--api-url");
|
|
279
|
+
const apiUrl = apiUrlIdx !== -1 && args[apiUrlIdx + 1] ? args[apiUrlIdx + 1] : SEARCH_API_URL;
|
|
280
|
+
console.log(`${TEXT}Scanning for installed skills...${RESET}`);
|
|
281
|
+
const installedSkills = getInstalledSkillNames();
|
|
282
|
+
if (installedSkills.length === 0) {
|
|
283
|
+
console.log(`${DIM}No installed skills found.${RESET}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
console.log(`${DIM}Found ${installedSkills.length} installed skill(s)${RESET}`);
|
|
287
|
+
console.log();
|
|
288
|
+
const existingLock = readSkillLock();
|
|
289
|
+
const existingCount = Object.keys(existingLock.skills).length;
|
|
290
|
+
const skillsToMatch = installedSkills.filter((skill) => !(skill in existingLock.skills));
|
|
291
|
+
if (skillsToMatch.length === 0) {
|
|
292
|
+
console.log(`${TEXT}All skills already in lock file.${RESET}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
console.log(`${TEXT}Matching ${skillsToMatch.length} skill(s) against database...${RESET}`);
|
|
296
|
+
console.log();
|
|
297
|
+
const matches = await fuzzyMatchSkills(skillsToMatch, apiUrl);
|
|
298
|
+
const now = new Date().toISOString();
|
|
299
|
+
const updatedLock = { ...existingLock };
|
|
300
|
+
let matchedCount = 0;
|
|
301
|
+
let skippedCount = 0;
|
|
302
|
+
const EXACT_MATCH_THRESHOLD = 1000;
|
|
303
|
+
for (const skillName of skillsToMatch) {
|
|
304
|
+
const match = matches[skillName];
|
|
305
|
+
if (match && match.score >= EXACT_MATCH_THRESHOLD) {
|
|
306
|
+
matchedCount++;
|
|
307
|
+
const sourceType = inferSourceType(match.source);
|
|
308
|
+
const sourceUrl = match.sourceUrl || buildSourceUrl(match.source, sourceType);
|
|
309
|
+
console.log(`${TEXT}✓${RESET} ${skillName}`);
|
|
310
|
+
console.log(` ${DIM}source: ${match.source}${RESET}`);
|
|
311
|
+
updatedLock.skills[skillName] = {
|
|
312
|
+
source: match.source,
|
|
313
|
+
sourceType,
|
|
314
|
+
sourceUrl,
|
|
315
|
+
contentHash: "",
|
|
316
|
+
installedAt: now,
|
|
317
|
+
updatedAt: now
|
|
318
|
+
};
|
|
319
|
+
} else {
|
|
320
|
+
skippedCount++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(`${TEXT}Matched:${RESET} ${matchedCount}`);
|
|
325
|
+
console.log(`${DIM}Skipped: ${skippedCount} (no exact match)${RESET}`);
|
|
326
|
+
console.log();
|
|
327
|
+
if (matchedCount === 0) {
|
|
328
|
+
console.log(`${DIM}No new skills to add to lock file.${RESET}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (dryRun) {
|
|
332
|
+
console.log(`${DIM}Dry run - no changes written${RESET}`);
|
|
333
|
+
console.log();
|
|
334
|
+
console.log(JSON.stringify(updatedLock, null, 2));
|
|
335
|
+
} else {
|
|
336
|
+
writeSkillLock(updatedLock);
|
|
337
|
+
console.log(`${TEXT}Lock file updated:${RESET} ${DIM}~/.agents/.skill-lock.json${RESET}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function computeContentHash(content) {
|
|
341
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
342
|
+
}
|
|
343
|
+
function getSkillContentPath(skillName) {
|
|
344
|
+
return join(homedir(), AGENTS_DIR, SKILLS_SUBDIR, skillName, "SKILL.md");
|
|
345
|
+
}
|
|
346
|
+
function readSkillContent(skillName) {
|
|
347
|
+
const skillPath = getSkillContentPath(skillName);
|
|
348
|
+
try {
|
|
349
|
+
return readFileSync(skillPath, "utf-8");
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function runCheck(args = []) {
|
|
355
|
+
const forceRefresh = args.includes("--refresh") || args.includes("-r");
|
|
356
|
+
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
357
|
+
console.log();
|
|
358
|
+
const lock = readSkillLock();
|
|
359
|
+
const skillNames = Object.keys(lock.skills);
|
|
360
|
+
if (skillNames.length === 0) {
|
|
361
|
+
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
362
|
+
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const checkRequest = {
|
|
366
|
+
skills: []
|
|
367
|
+
};
|
|
368
|
+
if (forceRefresh) {
|
|
369
|
+
checkRequest.forceRefresh = true;
|
|
370
|
+
}
|
|
371
|
+
for (const skillName of skillNames) {
|
|
372
|
+
const entry = lock.skills[skillName];
|
|
373
|
+
if (!entry)
|
|
374
|
+
continue;
|
|
375
|
+
let contentHash = entry.contentHash;
|
|
376
|
+
if (!contentHash) {
|
|
377
|
+
const content = readSkillContent(skillName);
|
|
378
|
+
if (content) {
|
|
379
|
+
contentHash = computeContentHash(content);
|
|
380
|
+
} else {
|
|
381
|
+
console.log(`${DIM}Skipping ${skillName}: cannot read SKILL.md${RESET}`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
checkRequest.skills.push({
|
|
386
|
+
name: skillName,
|
|
387
|
+
source: entry.source,
|
|
388
|
+
path: entry.skillPath,
|
|
389
|
+
contentHash
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (checkRequest.skills.length === 0) {
|
|
393
|
+
console.log(`${DIM}No skills to check.${RESET}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
console.log(`${DIM}Checking ${checkRequest.skills.length} skill(s) for updates...${RESET}`);
|
|
397
|
+
try {
|
|
398
|
+
const response = await fetch(CHECK_UPDATES_API_URL, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: { "Content-Type": "application/json" },
|
|
401
|
+
body: JSON.stringify(checkRequest)
|
|
402
|
+
});
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
405
|
+
}
|
|
406
|
+
const data = await response.json();
|
|
407
|
+
console.log();
|
|
408
|
+
if (data.updates.length === 0) {
|
|
409
|
+
console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
410
|
+
} else {
|
|
411
|
+
console.log(`${TEXT}${data.updates.length} update(s) available:${RESET}`);
|
|
412
|
+
console.log();
|
|
413
|
+
for (const update of data.updates) {
|
|
414
|
+
console.log(` ${TEXT}↑${RESET} ${update.name}`);
|
|
415
|
+
console.log(` ${DIM}source: ${update.source}${RESET}`);
|
|
416
|
+
}
|
|
417
|
+
console.log();
|
|
418
|
+
console.log(`${DIM}Run${RESET} ${TEXT}npx skills update${RESET} ${DIM}to update all skills${RESET}`);
|
|
419
|
+
}
|
|
420
|
+
if (data.errors && data.errors.length > 0) {
|
|
421
|
+
console.log();
|
|
422
|
+
console.log(`${DIM}Could not check ${data.errors.length} skill(s):${RESET}`);
|
|
423
|
+
for (const err of data.errors) {
|
|
424
|
+
console.log(` ${DIM}${err.name}: ${err.error}${RESET}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.log(`${TEXT}Error checking for updates:${RESET} ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
console.log();
|
|
432
|
+
}
|
|
433
|
+
async function runUpdate() {
|
|
434
|
+
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
435
|
+
console.log();
|
|
436
|
+
const lock = readSkillLock();
|
|
437
|
+
const skillNames = Object.keys(lock.skills);
|
|
438
|
+
if (skillNames.length === 0) {
|
|
439
|
+
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
440
|
+
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const checkRequest = {
|
|
444
|
+
skills: []
|
|
445
|
+
};
|
|
446
|
+
for (const skillName of skillNames) {
|
|
447
|
+
const entry = lock.skills[skillName];
|
|
448
|
+
if (!entry)
|
|
449
|
+
continue;
|
|
450
|
+
let contentHash = entry.contentHash;
|
|
451
|
+
if (!contentHash) {
|
|
452
|
+
const content = readSkillContent(skillName);
|
|
453
|
+
if (content) {
|
|
454
|
+
contentHash = computeContentHash(content);
|
|
455
|
+
} else {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
checkRequest.skills.push({
|
|
460
|
+
name: skillName,
|
|
461
|
+
source: entry.source,
|
|
462
|
+
path: entry.skillPath,
|
|
463
|
+
contentHash
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (checkRequest.skills.length === 0) {
|
|
467
|
+
console.log(`${DIM}No skills to check.${RESET}`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
let updates = [];
|
|
471
|
+
try {
|
|
472
|
+
const response = await fetch(CHECK_UPDATES_API_URL, {
|
|
473
|
+
method: "POST",
|
|
474
|
+
headers: { "Content-Type": "application/json" },
|
|
475
|
+
body: JSON.stringify(checkRequest)
|
|
476
|
+
});
|
|
477
|
+
if (!response.ok) {
|
|
478
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
479
|
+
}
|
|
480
|
+
const data = await response.json();
|
|
481
|
+
updates = data.updates;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.log(`${TEXT}Error checking for updates:${RESET} ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
if (updates.length === 0) {
|
|
487
|
+
console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
488
|
+
console.log();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
|
|
492
|
+
console.log();
|
|
493
|
+
let successCount = 0;
|
|
494
|
+
let failCount = 0;
|
|
495
|
+
for (const update of updates) {
|
|
496
|
+
const entry = lock.skills[update.name];
|
|
497
|
+
if (!entry)
|
|
498
|
+
continue;
|
|
499
|
+
console.log(`${TEXT}Updating ${update.name}...${RESET}`);
|
|
500
|
+
const result = spawnSync("npx", ["-y", "add-skill", entry.sourceUrl, "--skill", update.name, "-g", "-y"], {
|
|
501
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
502
|
+
});
|
|
503
|
+
if (result.status === 0) {
|
|
504
|
+
successCount++;
|
|
505
|
+
console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
|
|
506
|
+
} else {
|
|
507
|
+
failCount++;
|
|
508
|
+
console.log(` ${DIM}✗ Failed to update ${update.name}${RESET}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
console.log();
|
|
512
|
+
if (successCount > 0) {
|
|
513
|
+
console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
|
|
514
|
+
}
|
|
515
|
+
if (failCount > 0) {
|
|
516
|
+
console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
|
|
517
|
+
}
|
|
518
|
+
console.log();
|
|
519
|
+
}
|
|
163
520
|
function main() {
|
|
164
521
|
const args = process.argv.slice(2);
|
|
165
522
|
if (args.length === 0) {
|
|
@@ -182,6 +539,23 @@ function main() {
|
|
|
182
539
|
console.log();
|
|
183
540
|
runAddSkill(restArgs);
|
|
184
541
|
break;
|
|
542
|
+
case "check":
|
|
543
|
+
showLogo();
|
|
544
|
+
console.log();
|
|
545
|
+
runCheck(restArgs);
|
|
546
|
+
break;
|
|
547
|
+
case "update":
|
|
548
|
+
case "upgrade":
|
|
549
|
+
showLogo();
|
|
550
|
+
console.log();
|
|
551
|
+
runUpdate();
|
|
552
|
+
break;
|
|
553
|
+
case "generate-lock":
|
|
554
|
+
case "gen-lock":
|
|
555
|
+
showLogo();
|
|
556
|
+
console.log();
|
|
557
|
+
runGenerateLock(restArgs);
|
|
558
|
+
break;
|
|
185
559
|
case "--help":
|
|
186
560
|
case "-h":
|
|
187
561
|
showHelp();
|