ai-agent-skills 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.js +234 -13
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -24,7 +24,7 @@ const AGENT_PATHS = {
24
24
  copilot: path.join(process.cwd(), '.github', 'skills'),
25
25
  project: path.join(process.cwd(), '.skills'),
26
26
  goose: path.join(os.homedir(), '.config', 'goose', 'skills'),
27
- opencode: path.join(os.homedir(), '.opencode', 'skill'),
27
+ opencode: path.join(os.homedir(), '.config', 'opencode', 'skill'),
28
28
  codex: path.join(os.homedir(), '.codex', 'skills'),
29
29
  letta: path.join(os.homedir(), '.letta', 'skills'),
30
30
  };
@@ -70,6 +70,41 @@ function saveConfig(config) {
70
70
  }
71
71
  }
72
72
 
73
+ // ============ SKILL METADATA SUPPORT ============
74
+
75
+ const SKILL_META_FILE = '.skill-meta.json';
76
+
77
+ function writeSkillMeta(skillPath, meta) {
78
+ try {
79
+ const metaPath = path.join(skillPath, SKILL_META_FILE);
80
+ const now = new Date().toISOString();
81
+ const metadata = {
82
+ ...meta,
83
+ // Preserve original installedAt if it exists, otherwise set it
84
+ installedAt: meta.installedAt || now,
85
+ // Always update the updatedAt timestamp
86
+ updatedAt: now
87
+ };
88
+ fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
89
+ return true;
90
+ } catch (e) {
91
+ // Non-fatal - skill still works without metadata
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function readSkillMeta(skillPath) {
97
+ try {
98
+ const metaPath = path.join(skillPath, SKILL_META_FILE);
99
+ if (fs.existsSync(metaPath)) {
100
+ return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
101
+ }
102
+ } catch (e) {
103
+ // Ignore - treat as legacy skill
104
+ }
105
+ return null;
106
+ }
107
+
73
108
  // ============ SECURITY VALIDATION ============
74
109
 
75
110
  function validateSkillName(name) {
@@ -367,6 +402,12 @@ function installSkill(skillName, agent = 'claude', dryRun = false) {
367
402
 
368
403
  copyDir(sourcePath, destPath);
369
404
 
405
+ // Write metadata for update tracking
406
+ writeSkillMeta(destPath, {
407
+ source: 'registry',
408
+ name: skillName
409
+ });
410
+
370
411
  success(`\nInstalled: ${skillName}`);
371
412
  info(`Agent: ${agent}`);
372
413
  info(`Location: ${destPath}`);
@@ -474,32 +515,133 @@ function listInstalledSkills(agent = 'claude') {
474
515
  log(`${colors.dim}Uninstall: npx ai-agent-skills uninstall <name> --agent ${agent}${colors.reset}`);
475
516
  }
476
517
 
477
- function updateSkill(skillName, agent = 'claude', dryRun = false) {
518
+ // Update from bundled registry
519
+ function updateFromRegistry(skillName, agent, destPath, dryRun) {
520
+ const sourcePath = path.join(SKILLS_DIR, skillName);
521
+
522
+ if (!fs.existsSync(sourcePath)) {
523
+ error(`Skill "${skillName}" not found in repository.`);
524
+ return false;
525
+ }
526
+
527
+ if (dryRun) {
528
+ log(`\n${colors.bold}Dry Run${colors.reset} (no changes made)\n`);
529
+ info(`Would update: ${skillName} (from registry)`);
530
+ info(`Agent: ${agent}`);
531
+ info(`Path: ${destPath}`);
532
+ return true;
533
+ }
534
+
478
535
  try {
479
- validateSkillName(skillName);
536
+ fs.rmSync(destPath, { recursive: true });
537
+ copyDir(sourcePath, destPath);
538
+
539
+ // Write metadata
540
+ writeSkillMeta(destPath, {
541
+ source: 'registry',
542
+ name: skillName
543
+ });
544
+
545
+ success(`\nUpdated: ${skillName}`);
546
+ info(`Agent: ${agent}`);
547
+ info(`Location: ${destPath}`);
548
+ return true;
480
549
  } catch (e) {
481
- error(e.message);
550
+ error(`Failed to update skill: ${e.message}`);
482
551
  return false;
483
552
  }
553
+ }
484
554
 
485
- const sourcePath = path.join(SKILLS_DIR, skillName);
486
- const destDir = AGENT_PATHS[agent] || AGENT_PATHS.claude;
487
- const destPath = path.join(destDir, skillName);
555
+ // Update from GitHub repository
556
+ function updateFromGitHub(meta, skillName, agent, destPath, dryRun) {
557
+ const { execFileSync } = require('child_process');
558
+ const repo = meta.repo;
559
+
560
+ // Validate repo format
561
+ if (!repo || typeof repo !== 'string' || !repo.includes('/')) {
562
+ error(`Invalid repository in metadata: ${repo}`);
563
+ error(`Try reinstalling the skill from GitHub.`);
564
+ return false;
565
+ }
566
+
567
+ if (dryRun) {
568
+ log(`\n${colors.bold}Dry Run${colors.reset} (no changes made)\n`);
569
+ info(`Would update: ${skillName} (from github:${repo})`);
570
+ info(`Agent: ${agent}`);
571
+ info(`Path: ${destPath}`);
572
+ return true;
573
+ }
574
+
575
+ const tempDir = path.join(os.tmpdir(), `ai-skills-update-${Date.now()}`);
576
+
577
+ try {
578
+ info(`Updating ${skillName} from ${repo}...`);
579
+ const repoUrl = `https://github.com/${repo}.git`;
580
+ execFileSync('git', ['clone', '--depth', '1', repoUrl, tempDir], { stdio: 'pipe' });
581
+
582
+ // Determine source path in cloned repo
583
+ let sourcePath;
584
+ if (meta.isRootSkill) {
585
+ sourcePath = tempDir;
586
+ } else if (meta.skillPath) {
587
+ // Check if skills/ subdirectory exists
588
+ const skillsSubdir = path.join(tempDir, 'skills', meta.skillPath);
589
+ const directPath = path.join(tempDir, meta.skillPath);
590
+ sourcePath = fs.existsSync(skillsSubdir) ? skillsSubdir : directPath;
591
+ } else {
592
+ sourcePath = tempDir;
593
+ }
594
+
595
+ if (!fs.existsSync(sourcePath) || !fs.existsSync(path.join(sourcePath, 'SKILL.md'))) {
596
+ error(`Skill not found in repository ${repo}`);
597
+ fs.rmSync(tempDir, { recursive: true });
598
+ return false;
599
+ }
600
+
601
+ fs.rmSync(destPath, { recursive: true });
602
+ copyDir(sourcePath, destPath);
603
+
604
+ // Preserve metadata
605
+ writeSkillMeta(destPath, meta);
606
+
607
+ fs.rmSync(tempDir, { recursive: true });
608
+
609
+ success(`\nUpdated: ${skillName}`);
610
+ info(`Source: github:${repo}`);
611
+ info(`Agent: ${agent}`);
612
+ info(`Location: ${destPath}`);
613
+ return true;
614
+ } catch (e) {
615
+ error(`Failed to update from GitHub: ${e.message}`);
616
+ try { fs.rmSync(tempDir, { recursive: true }); } catch {}
617
+ return false;
618
+ }
619
+ }
620
+
621
+ // Update from local path
622
+ function updateFromLocalPath(meta, skillName, agent, destPath, dryRun) {
623
+ const sourcePath = meta.path;
624
+
625
+ if (!sourcePath || typeof sourcePath !== 'string') {
626
+ error(`Invalid path in metadata.`);
627
+ error(`Try reinstalling the skill from the local path.`);
628
+ return false;
629
+ }
488
630
 
489
631
  if (!fs.existsSync(sourcePath)) {
490
- error(`Skill "${skillName}" not found in repository.`);
632
+ error(`Source path no longer exists: ${sourcePath}`);
491
633
  return false;
492
634
  }
493
635
 
494
- if (!fs.existsSync(destPath)) {
495
- error(`Skill "${skillName}" is not installed for ${agent}.`);
496
- log(`\nUse 'install' to add it first.`);
636
+ // Verify it's still a valid skill directory
637
+ if (!fs.existsSync(path.join(sourcePath, 'SKILL.md'))) {
638
+ error(`Source is no longer a valid skill (missing SKILL.md): ${sourcePath}`);
497
639
  return false;
498
640
  }
499
641
 
500
642
  if (dryRun) {
501
643
  log(`\n${colors.bold}Dry Run${colors.reset} (no changes made)\n`);
502
- info(`Would update: ${skillName}`);
644
+ info(`Would update: ${skillName} (from local:${sourcePath})`);
503
645
  info(`Agent: ${agent}`);
504
646
  info(`Path: ${destPath}`);
505
647
  return true;
@@ -509,16 +651,57 @@ function updateSkill(skillName, agent = 'claude', dryRun = false) {
509
651
  fs.rmSync(destPath, { recursive: true });
510
652
  copyDir(sourcePath, destPath);
511
653
 
654
+ // Preserve metadata
655
+ writeSkillMeta(destPath, meta);
656
+
512
657
  success(`\nUpdated: ${skillName}`);
658
+ info(`Source: local:${sourcePath}`);
513
659
  info(`Agent: ${agent}`);
514
660
  info(`Location: ${destPath}`);
515
661
  return true;
516
662
  } catch (e) {
517
- error(`Failed to update skill: ${e.message}`);
663
+ error(`Failed to update from local path: ${e.message}`);
518
664
  return false;
519
665
  }
520
666
  }
521
667
 
668
+ function updateSkill(skillName, agent = 'claude', dryRun = false) {
669
+ try {
670
+ validateSkillName(skillName);
671
+ } catch (e) {
672
+ error(e.message);
673
+ return false;
674
+ }
675
+
676
+ const destDir = AGENT_PATHS[agent] || AGENT_PATHS.claude;
677
+ const destPath = path.join(destDir, skillName);
678
+
679
+ if (!fs.existsSync(destPath)) {
680
+ error(`Skill "${skillName}" is not installed for ${agent}.`);
681
+ log(`\nUse 'install' to add it first.`);
682
+ return false;
683
+ }
684
+
685
+ // Read metadata to determine source
686
+ const meta = readSkillMeta(destPath);
687
+
688
+ if (!meta) {
689
+ // Legacy skill without metadata - try registry
690
+ return updateFromRegistry(skillName, agent, destPath, dryRun);
691
+ }
692
+
693
+ // Route to correct update method based on source
694
+ switch (meta.source) {
695
+ case 'github':
696
+ return updateFromGitHub(meta, skillName, agent, destPath, dryRun);
697
+ case 'local':
698
+ return updateFromLocalPath(meta, skillName, agent, destPath, dryRun);
699
+ case 'registry':
700
+ default:
701
+ return updateFromRegistry(skillName, agent, destPath, dryRun);
702
+ }
703
+ }
704
+
522
705
  function updateAllSkills(agent = 'claude', dryRun = false) {
523
706
  const installed = getInstalledSkills(agent);
524
707
 
@@ -910,6 +1093,14 @@ async function installFromGitHub(source, agent = 'claude', dryRun = false) {
910
1093
  }
911
1094
 
912
1095
  copyDir(skillPath, destPath);
1096
+
1097
+ // Write metadata for update tracking
1098
+ writeSkillMeta(destPath, {
1099
+ source: 'github',
1100
+ repo: `${owner}/${repo}`,
1101
+ skillPath: skillName
1102
+ });
1103
+
913
1104
  success(`\nInstalled: ${skillName} from ${owner}/${repo}`);
914
1105
  info(`Location: ${destPath}`);
915
1106
  } else if (isRootSkill) {
@@ -933,6 +1124,14 @@ async function installFromGitHub(source, agent = 'claude', dryRun = false) {
933
1124
  }
934
1125
 
935
1126
  copyDir(tempDir, destPath);
1127
+
1128
+ // Write metadata for update tracking
1129
+ writeSkillMeta(destPath, {
1130
+ source: 'github',
1131
+ repo: `${owner}/${repo}`,
1132
+ isRootSkill: true
1133
+ });
1134
+
936
1135
  success(`\nInstalled: ${skillName} from ${owner}/${repo}`);
937
1136
  info(`Location: ${destPath}`);
938
1137
  } else {
@@ -952,6 +1151,14 @@ async function installFromGitHub(source, agent = 'claude', dryRun = false) {
952
1151
  }
953
1152
 
954
1153
  copyDir(skillPath, destPath);
1154
+
1155
+ // Write metadata for update tracking
1156
+ writeSkillMeta(destPath, {
1157
+ source: 'github',
1158
+ repo: `${owner}/${repo}`,
1159
+ skillPath: entry.name
1160
+ });
1161
+
955
1162
  log(` ${colors.green}✓${colors.reset} ${entry.name}`);
956
1163
  installed++;
957
1164
  }
@@ -1010,6 +1217,13 @@ function installFromLocalPath(source, agent = 'claude', dryRun = false) {
1010
1217
  }
1011
1218
 
1012
1219
  copyDir(sourcePath, destPath);
1220
+
1221
+ // Write metadata for update tracking
1222
+ writeSkillMeta(destPath, {
1223
+ source: 'local',
1224
+ path: sourcePath
1225
+ });
1226
+
1013
1227
  success(`\nInstalled: ${skillName} from local path`);
1014
1228
  info(`Location: ${destPath}`);
1015
1229
  } else {
@@ -1029,6 +1243,13 @@ function installFromLocalPath(source, agent = 'claude', dryRun = false) {
1029
1243
  }
1030
1244
 
1031
1245
  copyDir(skillPath, destPath);
1246
+
1247
+ // Write metadata for update tracking
1248
+ writeSkillMeta(destPath, {
1249
+ source: 'local',
1250
+ path: skillPath
1251
+ });
1252
+
1032
1253
  log(` ${colors.green}✓${colors.reset} ${entry.name}`);
1033
1254
  installed++;
1034
1255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agent-skills",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Install curated AI agent skills with one command. Works with Claude Code, Cursor, Amp, VS Code, and all Agent Skills compatible tools.",
5
5
  "main": "cli.js",
6
6
  "bin": {