skill-master 0.1.6 → 0.1.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/installer.ts
4
- import { join as join6 } from "path";
4
+ import { join as join7, resolve as resolve3, relative } from "path";
5
5
  import { existsSync as existsSync6 } from "fs";
6
6
 
7
7
  // src/core/git-source.ts
@@ -145,6 +145,9 @@ function step(num, total, msg) {
145
145
  function blank() {
146
146
  console.log();
147
147
  }
148
+ function section(title) {
149
+ console.log(chalk.bold(title));
150
+ }
148
151
  function kv(key, value) {
149
152
  console.log(` ${chalk.gray(key + ":")} ${value}`);
150
153
  }
@@ -278,9 +281,123 @@ async function cloneRepo(url, branch) {
278
281
 
279
282
  // src/core/skill-parser.ts
280
283
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
281
- import { readdir } from "fs/promises";
282
- import { join as join2, basename } from "path";
284
+ import { readdir as readdir2 } from "fs/promises";
285
+ import { join as join3, basename, resolve as resolve2 } from "path";
283
286
  import { existsSync as existsSync3 } from "fs";
287
+
288
+ // src/core/plugin-manifest.ts
289
+ import { readFile as readFile2, readdir } from "fs/promises";
290
+ import { join as join2, dirname as dirname2, resolve, normalize, sep } from "path";
291
+ function isContainedIn(targetPath, basePath) {
292
+ const normalizedBase = normalize(resolve(basePath));
293
+ const normalizedTarget = normalize(resolve(targetPath));
294
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
295
+ }
296
+ function isValidRelativePath(path) {
297
+ return path.startsWith("./");
298
+ }
299
+ async function getPluginSkillPaths(basePath) {
300
+ const searchDirs = [];
301
+ const addPluginSkillPaths = (pluginBase, skills) => {
302
+ if (!isContainedIn(pluginBase, basePath)) return;
303
+ if (skills && skills.length > 0) {
304
+ for (const skillPath of skills) {
305
+ if (!isValidRelativePath(skillPath)) continue;
306
+ const skillDir = dirname2(join2(pluginBase, skillPath));
307
+ if (isContainedIn(skillDir, basePath)) {
308
+ searchDirs.push(skillDir);
309
+ }
310
+ }
311
+ }
312
+ searchDirs.push(join2(pluginBase, "skills"));
313
+ };
314
+ try {
315
+ const content = await readFile2(join2(basePath, ".claude-plugin/marketplace.json"), "utf-8");
316
+ const manifest = JSON.parse(content);
317
+ const pluginRoot = manifest.metadata?.pluginRoot;
318
+ const validPluginRoot = pluginRoot === void 0 || isValidRelativePath(pluginRoot);
319
+ if (validPluginRoot) {
320
+ for (const plugin of manifest.plugins ?? []) {
321
+ if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
322
+ if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
323
+ const pluginBase = join2(basePath, pluginRoot ?? "", plugin.source ?? "");
324
+ addPluginSkillPaths(pluginBase, plugin.skills);
325
+ }
326
+ }
327
+ } catch {
328
+ }
329
+ try {
330
+ const content = await readFile2(join2(basePath, ".claude-plugin/plugin.json"), "utf-8");
331
+ const manifest = JSON.parse(content);
332
+ addPluginSkillPaths(basePath, manifest.skills);
333
+ } catch {
334
+ }
335
+ return searchDirs;
336
+ }
337
+ async function scanSkillsDir(skillsDir, pluginName, basePath, groupings) {
338
+ try {
339
+ const entries = await readdir(skillsDir, { withFileTypes: true });
340
+ for (const entry of entries) {
341
+ if (!entry.isDirectory()) continue;
342
+ const skillPath = join2(skillsDir, entry.name);
343
+ if (isContainedIn(skillPath, basePath)) {
344
+ groupings.set(resolve(skillPath), pluginName);
345
+ }
346
+ }
347
+ } catch {
348
+ }
349
+ }
350
+ async function getPluginGroupings(basePath) {
351
+ const groupings = /* @__PURE__ */ new Map();
352
+ try {
353
+ const content = await readFile2(join2(basePath, ".claude-plugin/marketplace.json"), "utf-8");
354
+ const manifest = JSON.parse(content);
355
+ const pluginRoot = manifest.metadata?.pluginRoot;
356
+ const validPluginRoot = pluginRoot === void 0 || isValidRelativePath(pluginRoot);
357
+ if (validPluginRoot) {
358
+ for (const plugin of manifest.plugins ?? []) {
359
+ if (!plugin.name) continue;
360
+ if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
361
+ if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
362
+ const pluginBase = join2(basePath, pluginRoot ?? "", plugin.source ?? "");
363
+ if (!isContainedIn(pluginBase, basePath)) continue;
364
+ if (plugin.skills && plugin.skills.length > 0) {
365
+ for (const skillPath of plugin.skills) {
366
+ if (!isValidRelativePath(skillPath)) continue;
367
+ const skillDir = join2(pluginBase, skillPath);
368
+ if (isContainedIn(skillDir, basePath)) {
369
+ groupings.set(resolve(skillDir), plugin.name);
370
+ }
371
+ }
372
+ } else {
373
+ await scanSkillsDir(join2(pluginBase, "skills"), plugin.name, basePath, groupings);
374
+ }
375
+ }
376
+ }
377
+ } catch {
378
+ }
379
+ try {
380
+ const content = await readFile2(join2(basePath, ".claude-plugin/plugin.json"), "utf-8");
381
+ const manifest = JSON.parse(content);
382
+ if (manifest.name) {
383
+ if (manifest.skills && manifest.skills.length > 0) {
384
+ for (const skillPath of manifest.skills) {
385
+ if (!isValidRelativePath(skillPath)) continue;
386
+ const skillDir = join2(basePath, skillPath);
387
+ if (isContainedIn(skillDir, basePath)) {
388
+ groupings.set(resolve(skillDir), manifest.name);
389
+ }
390
+ }
391
+ } else {
392
+ await scanSkillsDir(join2(basePath, "skills"), manifest.name, basePath, groupings);
393
+ }
394
+ }
395
+ } catch {
396
+ }
397
+ return groupings;
398
+ }
399
+
400
+ // src/core/skill-parser.ts
284
401
  var TOOL_CAPABILITY_MAP = {
285
402
  "Bash": "shell",
286
403
  "Read": "read_file",
@@ -341,62 +458,108 @@ async function findSkillDirectory(dir) {
341
458
  }
342
459
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]);
343
460
  async function findAllSkillDirectories(dir, fullDepth = false) {
461
+ const results = await findAllSkillDirectoriesWithPlugins(dir, fullDepth);
462
+ return results.map((r) => r.path);
463
+ }
464
+ async function findAllSkillDirectoriesWithPlugins(dir, fullDepth = false) {
465
+ const pluginGroupings = await getPluginGroupings(dir);
466
+ const enhance = (skillPath) => {
467
+ const resolvedPath = resolve2(skillPath);
468
+ const pluginName = pluginGroupings.get(resolvedPath);
469
+ return { path: skillPath, pluginName };
470
+ };
344
471
  if (fullDepth) {
345
472
  const results2 = /* @__PURE__ */ new Set();
346
473
  await walkForSkills(dir, 0, 5, results2);
347
- return [...results2];
474
+ return [...results2].map(enhance);
348
475
  }
349
476
  const results = [];
350
- if (existsSync3(join2(dir, "SKILL.md"))) {
351
- results.push(dir);
477
+ if (existsSync3(join3(dir, "SKILL.md"))) {
478
+ results.push(enhance(dir));
352
479
  return results;
353
480
  }
354
- const skillsRoot = join2(dir, ".claude", "skills");
355
- if (existsSync3(skillsRoot)) {
481
+ const seenPaths = /* @__PURE__ */ new Set();
482
+ const pluginSkillPaths = await getPluginSkillPaths(dir);
483
+ const priorityDirs = [
484
+ ...pluginSkillPaths,
485
+ // Plugin manifest paths first
486
+ join3(dir, "skills"),
487
+ join3(dir, "skills", ".curated"),
488
+ join3(dir, "skills", ".experimental"),
489
+ join3(dir, "skills", ".system"),
490
+ join3(dir, ".agent", "skills"),
491
+ join3(dir, ".agents", "skills"),
492
+ join3(dir, ".claude", "skills"),
493
+ join3(dir, ".cline", "skills"),
494
+ join3(dir, ".codex", "skills"),
495
+ join3(dir, ".github", "skills"),
496
+ join3(dir, ".kiro", "skills"),
497
+ join3(dir, ".opencode", "skills"),
498
+ join3(dir, ".roo", "skills"),
499
+ join3(dir, ".windsurf", "skills")
500
+ ];
501
+ for (const searchDir of priorityDirs) {
356
502
  try {
357
- const entries = await readdir(skillsRoot, { withFileTypes: true });
503
+ const entries = await readdir2(searchDir, { withFileTypes: true });
358
504
  for (const entry of entries) {
359
505
  if (entry.isDirectory()) {
360
- const skillMdPath = join2(skillsRoot, entry.name, "SKILL.md");
361
- if (existsSync3(skillMdPath)) {
362
- results.push(join2(skillsRoot, entry.name));
506
+ const skillDir = join3(searchDir, entry.name);
507
+ if (existsSync3(join3(skillDir, "SKILL.md")) && !seenPaths.has(skillDir)) {
508
+ results.push(enhance(skillDir));
509
+ seenPaths.add(skillDir);
363
510
  }
364
511
  }
365
512
  }
366
513
  } catch {
367
514
  }
368
515
  }
369
- try {
370
- const topEntries = await readdir(dir, { withFileTypes: true });
371
- for (const entry of topEntries) {
372
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
373
- const skillMdPath = join2(dir, entry.name, "SKILL.md");
374
- if (existsSync3(skillMdPath)) {
375
- results.push(join2(dir, entry.name));
516
+ if (results.length === 0) {
517
+ try {
518
+ const topEntries = await readdir2(dir, { withFileTypes: true });
519
+ for (const entry of topEntries) {
520
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !SKIP_DIRS.has(entry.name)) {
521
+ const subDir = join3(dir, entry.name);
522
+ if (existsSync3(join3(subDir, "SKILL.md")) && !seenPaths.has(subDir)) {
523
+ results.push(enhance(subDir));
524
+ seenPaths.add(subDir);
525
+ }
526
+ try {
527
+ const subEntries = await readdir2(subDir, { withFileTypes: true });
528
+ for (const sub of subEntries) {
529
+ if (sub.isDirectory() && !sub.name.startsWith(".") && !SKIP_DIRS.has(sub.name)) {
530
+ const nested = join3(subDir, sub.name);
531
+ if (existsSync3(join3(nested, "SKILL.md")) && !seenPaths.has(nested)) {
532
+ results.push(enhance(nested));
533
+ seenPaths.add(nested);
534
+ }
535
+ }
536
+ }
537
+ } catch {
538
+ }
376
539
  }
377
540
  }
541
+ } catch {
378
542
  }
379
- } catch {
380
543
  }
381
544
  return results;
382
545
  }
383
546
  async function walkForSkills(dir, depth, maxDepth, results) {
384
547
  if (depth > maxDepth) return;
385
- if (existsSync3(join2(dir, "SKILL.md"))) {
548
+ if (existsSync3(join3(dir, "SKILL.md"))) {
386
549
  results.add(dir);
387
550
  }
388
551
  try {
389
- const entries = await readdir(dir, { withFileTypes: true });
552
+ const entries = await readdir2(dir, { withFileTypes: true });
390
553
  for (const entry of entries) {
391
554
  if (entry.isDirectory() && !entry.name.startsWith(".") && !SKIP_DIRS.has(entry.name)) {
392
- await walkForSkills(join2(dir, entry.name), depth + 1, maxDepth, results);
555
+ await walkForSkills(join3(dir, entry.name), depth + 1, maxDepth, results);
393
556
  }
394
557
  }
395
558
  } catch {
396
559
  }
397
560
  }
398
561
  async function readSkillMd(dir) {
399
- const content = await readTextSafe(join2(dir, "SKILL.md"));
562
+ const content = await readTextSafe(join3(dir, "SKILL.md"));
400
563
  if (!content) return null;
401
564
  return parseSkillMd(content, basename(dir));
402
565
  }
@@ -413,315 +576,364 @@ function extractEnvKeys(envExampleContent) {
413
576
  }
414
577
  return keys;
415
578
  }
579
+ async function discoverNodeModulesSkills(cwd) {
580
+ const nmDir = join3(cwd, "node_modules");
581
+ if (!existsSync3(nmDir)) return [];
582
+ const results = [];
583
+ try {
584
+ const entries = await readdir2(nmDir, { withFileTypes: true });
585
+ for (const entry of entries) {
586
+ if (!entry.isDirectory()) continue;
587
+ if (entry.name.startsWith("@")) {
588
+ try {
589
+ const scopeEntries = await readdir2(join3(nmDir, entry.name), { withFileTypes: true });
590
+ for (const scopeEntry of scopeEntries) {
591
+ if (scopeEntry.isDirectory()) {
592
+ const pkgDir = join3(nmDir, entry.name, scopeEntry.name);
593
+ if (existsSync3(join3(pkgDir, "SKILL.md"))) {
594
+ results.push(pkgDir);
595
+ }
596
+ }
597
+ }
598
+ } catch {
599
+ }
600
+ } else if (!entry.name.startsWith(".")) {
601
+ const pkgDir = join3(nmDir, entry.name);
602
+ if (existsSync3(join3(pkgDir, "SKILL.md"))) {
603
+ results.push(pkgDir);
604
+ }
605
+ }
606
+ }
607
+ } catch {
608
+ }
609
+ return results;
610
+ }
416
611
 
417
612
  // src/core/env-manager.ts
418
- import { join as join5 } from "path";
613
+ import { join as join6 } from "path";
419
614
 
420
615
  // src/utils/paths.ts
421
616
  import { homedir as homedir2 } from "os";
422
- import { join as join4 } from "path";
617
+ import { join as join5 } from "path";
423
618
 
424
619
  // src/platform/agents.ts
425
620
  import { existsSync as existsSync4 } from "fs";
426
- import { join as join3 } from "path";
621
+ import { join as join4 } from "path";
427
622
  import { homedir } from "os";
428
623
  var home = homedir();
429
- var configHome = process.env.XDG_CONFIG_HOME?.trim() || join3(home, ".config");
430
- var codexHome = process.env.CODEX_HOME?.trim() || join3(home, ".codex");
431
- var claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join3(home, ".claude");
624
+ var configHome = process.env.XDG_CONFIG_HOME?.trim() || join4(home, ".config");
625
+ var codexHome = process.env.CODEX_HOME?.trim() || join4(home, ".codex");
626
+ var claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join4(home, ".claude");
432
627
  var AGENTS = {
433
628
  amp: {
434
629
  name: "amp",
435
630
  displayName: "Amp",
436
631
  skillsDir: ".agents/skills",
437
- globalSkillsDir: join3(configHome, "agents/skills")
632
+ globalSkillsDir: join4(configHome, "agents/skills")
438
633
  },
439
634
  antigravity: {
440
635
  name: "antigravity",
441
636
  displayName: "Antigravity",
442
637
  skillsDir: ".agent/skills",
443
- globalSkillsDir: join3(home, ".gemini/antigravity/skills"),
638
+ globalSkillsDir: join4(home, ".gemini/antigravity/skills"),
444
639
  detectMarker: ".agent"
445
640
  },
446
641
  augment: {
447
642
  name: "augment",
448
643
  displayName: "Augment",
449
644
  skillsDir: ".augment/skills",
450
- globalSkillsDir: join3(home, ".augment/skills"),
645
+ globalSkillsDir: join4(home, ".augment/skills"),
451
646
  detectMarker: ".augment"
452
647
  },
453
648
  "claude-code": {
454
649
  name: "claude-code",
455
650
  displayName: "Claude Code",
456
651
  skillsDir: ".claude/skills",
457
- globalSkillsDir: join3(claudeHome, "skills"),
652
+ globalSkillsDir: join4(claudeHome, "skills"),
458
653
  detectMarker: ".claude"
459
654
  },
460
655
  openclaw: {
461
656
  name: "openclaw",
462
657
  displayName: "OpenClaw",
463
658
  skillsDir: "skills",
464
- globalSkillsDir: join3(home, ".openclaw/skills")
659
+ globalSkillsDir: join4(home, ".openclaw/skills")
465
660
  },
466
661
  cline: {
467
662
  name: "cline",
468
663
  displayName: "Cline",
469
- skillsDir: ".cline/skills",
470
- globalSkillsDir: join3(home, ".cline/skills"),
664
+ skillsDir: ".agents/skills",
665
+ globalSkillsDir: join4(home, ".agents", "skills"),
471
666
  detectMarker: ".cline"
472
667
  },
473
668
  codebuddy: {
474
669
  name: "codebuddy",
475
670
  displayName: "CodeBuddy",
476
671
  skillsDir: ".codebuddy/skills",
477
- globalSkillsDir: join3(home, ".codebuddy/skills"),
672
+ globalSkillsDir: join4(home, ".codebuddy/skills"),
478
673
  detectMarker: ".codebuddy"
479
674
  },
480
675
  codex: {
481
676
  name: "codex",
482
677
  displayName: "Codex",
483
678
  skillsDir: ".agents/skills",
484
- globalSkillsDir: join3(codexHome, "skills")
679
+ globalSkillsDir: join4(codexHome, "skills")
485
680
  },
486
681
  "command-code": {
487
682
  name: "command-code",
488
683
  displayName: "Command Code",
489
684
  skillsDir: ".commandcode/skills",
490
- globalSkillsDir: join3(home, ".commandcode/skills"),
685
+ globalSkillsDir: join4(home, ".commandcode/skills"),
491
686
  detectMarker: ".commandcode"
492
687
  },
688
+ cortex: {
689
+ name: "cortex",
690
+ displayName: "Cortex",
691
+ skillsDir: ".cortex/skills",
692
+ globalSkillsDir: join4(home, ".snowflake/cortex/skills")
693
+ },
493
694
  continue: {
494
695
  name: "continue",
495
696
  displayName: "Continue",
496
697
  skillsDir: ".continue/skills",
497
- globalSkillsDir: join3(home, ".continue/skills"),
698
+ globalSkillsDir: join4(home, ".continue/skills"),
498
699
  detectMarker: ".continue"
499
700
  },
500
701
  crush: {
501
702
  name: "crush",
502
703
  displayName: "Crush",
503
704
  skillsDir: ".crush/skills",
504
- globalSkillsDir: join3(home, ".config/crush/skills")
705
+ globalSkillsDir: join4(home, ".config/crush/skills")
505
706
  },
506
707
  cursor: {
507
708
  name: "cursor",
508
709
  displayName: "Cursor",
509
- skillsDir: ".cursor/skills",
510
- globalSkillsDir: join3(home, ".cursor/skills"),
710
+ skillsDir: ".agents/skills",
711
+ globalSkillsDir: join4(home, ".cursor/skills"),
511
712
  detectMarker: ".cursor"
512
713
  },
513
714
  droid: {
514
715
  name: "droid",
515
716
  displayName: "Droid",
516
717
  skillsDir: ".factory/skills",
517
- globalSkillsDir: join3(home, ".factory/skills"),
718
+ globalSkillsDir: join4(home, ".factory/skills"),
518
719
  detectMarker: ".factory"
519
720
  },
520
721
  "gemini-cli": {
521
722
  name: "gemini-cli",
522
723
  displayName: "Gemini CLI",
523
724
  skillsDir: ".agents/skills",
524
- globalSkillsDir: join3(home, ".gemini/skills")
725
+ globalSkillsDir: join4(home, ".gemini/skills")
525
726
  },
526
727
  "github-copilot": {
527
728
  name: "github-copilot",
528
729
  displayName: "GitHub Copilot",
529
730
  skillsDir: ".agents/skills",
530
- globalSkillsDir: join3(home, ".copilot/skills")
731
+ globalSkillsDir: join4(home, ".copilot/skills")
531
732
  },
532
733
  goose: {
533
734
  name: "goose",
534
735
  displayName: "Goose",
535
736
  skillsDir: ".goose/skills",
536
- globalSkillsDir: join3(configHome, "goose/skills"),
737
+ globalSkillsDir: join4(configHome, "goose/skills"),
537
738
  detectMarker: ".goose"
538
739
  },
539
740
  junie: {
540
741
  name: "junie",
541
742
  displayName: "Junie",
542
743
  skillsDir: ".junie/skills",
543
- globalSkillsDir: join3(home, ".junie/skills"),
744
+ globalSkillsDir: join4(home, ".junie/skills"),
544
745
  detectMarker: ".junie"
545
746
  },
546
747
  "iflow-cli": {
547
748
  name: "iflow-cli",
548
749
  displayName: "iFlow CLI",
549
750
  skillsDir: ".iflow/skills",
550
- globalSkillsDir: join3(home, ".iflow/skills"),
751
+ globalSkillsDir: join4(home, ".iflow/skills"),
551
752
  detectMarker: ".iflow"
552
753
  },
553
754
  kilo: {
554
755
  name: "kilo",
555
756
  displayName: "Kilo Code",
556
757
  skillsDir: ".kilocode/skills",
557
- globalSkillsDir: join3(home, ".kilocode/skills"),
758
+ globalSkillsDir: join4(home, ".kilocode/skills"),
558
759
  detectMarker: ".kilocode"
559
760
  },
560
761
  "kimi-cli": {
561
762
  name: "kimi-cli",
562
763
  displayName: "Kimi Code CLI",
563
764
  skillsDir: ".agents/skills",
564
- globalSkillsDir: join3(home, ".config/agents/skills")
765
+ globalSkillsDir: join4(home, ".config/agents/skills")
565
766
  },
566
767
  "kiro-cli": {
567
768
  name: "kiro-cli",
568
769
  displayName: "Kiro CLI",
569
770
  skillsDir: ".kiro/skills",
570
- globalSkillsDir: join3(home, ".kiro/skills"),
771
+ globalSkillsDir: join4(home, ".kiro/skills"),
571
772
  detectMarker: ".kiro"
572
773
  },
573
774
  kode: {
574
775
  name: "kode",
575
776
  displayName: "Kode",
576
777
  skillsDir: ".kode/skills",
577
- globalSkillsDir: join3(home, ".kode/skills"),
778
+ globalSkillsDir: join4(home, ".kode/skills"),
578
779
  detectMarker: ".kode"
579
780
  },
580
781
  mcpjam: {
581
782
  name: "mcpjam",
582
783
  displayName: "MCPJam",
583
784
  skillsDir: ".mcpjam/skills",
584
- globalSkillsDir: join3(home, ".mcpjam/skills"),
785
+ globalSkillsDir: join4(home, ".mcpjam/skills"),
585
786
  detectMarker: ".mcpjam"
586
787
  },
587
788
  "mistral-vibe": {
588
789
  name: "mistral-vibe",
589
790
  displayName: "Mistral Vibe",
590
791
  skillsDir: ".vibe/skills",
591
- globalSkillsDir: join3(home, ".vibe/skills"),
792
+ globalSkillsDir: join4(home, ".vibe/skills"),
592
793
  detectMarker: ".vibe"
593
794
  },
594
795
  mux: {
595
796
  name: "mux",
596
797
  displayName: "Mux",
597
798
  skillsDir: ".mux/skills",
598
- globalSkillsDir: join3(home, ".mux/skills"),
799
+ globalSkillsDir: join4(home, ".mux/skills"),
599
800
  detectMarker: ".mux"
600
801
  },
601
802
  opencode: {
602
803
  name: "opencode",
603
804
  displayName: "OpenCode",
604
- skillsDir: ".opencode/skills",
605
- globalSkillsDir: join3(configHome, "opencode/skills")
805
+ skillsDir: ".agents/skills",
806
+ globalSkillsDir: join4(configHome, "opencode/skills")
606
807
  },
607
808
  openhands: {
608
809
  name: "openhands",
609
810
  displayName: "OpenHands",
610
811
  skillsDir: ".openhands/skills",
611
- globalSkillsDir: join3(home, ".openhands/skills"),
812
+ globalSkillsDir: join4(home, ".openhands/skills"),
612
813
  detectMarker: ".openhands"
613
814
  },
614
815
  pi: {
615
816
  name: "pi",
616
817
  displayName: "Pi",
617
818
  skillsDir: ".pi/skills",
618
- globalSkillsDir: join3(home, ".pi/agent/skills"),
819
+ globalSkillsDir: join4(home, ".pi/agent/skills"),
619
820
  detectMarker: ".pi"
620
821
  },
621
822
  qoder: {
622
823
  name: "qoder",
623
824
  displayName: "Qoder",
624
825
  skillsDir: ".qoder/skills",
625
- globalSkillsDir: join3(home, ".qoder/skills"),
826
+ globalSkillsDir: join4(home, ".qoder/skills"),
626
827
  detectMarker: ".qoder"
627
828
  },
628
829
  "qwen-code": {
629
830
  name: "qwen-code",
630
831
  displayName: "Qwen Code",
631
832
  skillsDir: ".qwen/skills",
632
- globalSkillsDir: join3(home, ".qwen/skills"),
833
+ globalSkillsDir: join4(home, ".qwen/skills"),
633
834
  detectMarker: ".qwen"
634
835
  },
635
836
  replit: {
636
837
  name: "replit",
637
838
  displayName: "Replit",
638
839
  skillsDir: ".agents/skills",
639
- globalSkillsDir: join3(configHome, "agents/skills")
840
+ globalSkillsDir: join4(configHome, "agents/skills"),
841
+ showInUniversalList: false
640
842
  },
641
843
  roo: {
642
844
  name: "roo",
643
845
  displayName: "Roo Code",
644
846
  skillsDir: ".roo/skills",
645
- globalSkillsDir: join3(home, ".roo/skills"),
847
+ globalSkillsDir: join4(home, ".roo/skills"),
646
848
  detectMarker: ".roo"
647
849
  },
648
850
  trae: {
649
851
  name: "trae",
650
852
  displayName: "Trae",
651
853
  skillsDir: ".trae/skills",
652
- globalSkillsDir: join3(home, ".trae/skills"),
854
+ globalSkillsDir: join4(home, ".trae/skills"),
653
855
  detectMarker: ".trae"
654
856
  },
655
857
  "trae-cn": {
656
858
  name: "trae-cn",
657
859
  displayName: "Trae CN",
658
860
  skillsDir: ".trae/skills",
659
- globalSkillsDir: join3(home, ".trae-cn/skills")
861
+ globalSkillsDir: join4(home, ".trae-cn/skills")
660
862
  },
661
863
  windsurf: {
662
864
  name: "windsurf",
663
865
  displayName: "Windsurf",
664
866
  skillsDir: ".windsurf/skills",
665
- globalSkillsDir: join3(home, ".codeium/windsurf/skills"),
867
+ globalSkillsDir: join4(home, ".codeium/windsurf/skills"),
666
868
  detectMarker: ".windsurf"
667
869
  },
668
870
  zencoder: {
669
871
  name: "zencoder",
670
872
  displayName: "Zencoder",
671
873
  skillsDir: ".zencoder/skills",
672
- globalSkillsDir: join3(home, ".zencoder/skills"),
874
+ globalSkillsDir: join4(home, ".zencoder/skills"),
673
875
  detectMarker: ".zencoder"
674
876
  },
675
877
  neovate: {
676
878
  name: "neovate",
677
879
  displayName: "Neovate",
678
880
  skillsDir: ".neovate/skills",
679
- globalSkillsDir: join3(home, ".neovate/skills"),
881
+ globalSkillsDir: join4(home, ".neovate/skills"),
680
882
  detectMarker: ".neovate"
681
883
  },
682
884
  pochi: {
683
885
  name: "pochi",
684
886
  displayName: "Pochi",
685
887
  skillsDir: ".pochi/skills",
686
- globalSkillsDir: join3(home, ".pochi/skills"),
888
+ globalSkillsDir: join4(home, ".pochi/skills"),
687
889
  detectMarker: ".pochi"
688
890
  },
689
891
  adal: {
690
892
  name: "adal",
691
893
  displayName: "AdaL",
692
894
  skillsDir: ".adal/skills",
693
- globalSkillsDir: join3(home, ".adal/skills"),
895
+ globalSkillsDir: join4(home, ".adal/skills"),
694
896
  detectMarker: ".adal"
897
+ },
898
+ universal: {
899
+ name: "universal",
900
+ displayName: "Universal",
901
+ skillsDir: ".agents/skills",
902
+ globalSkillsDir: join4(configHome, "agents/skills"),
903
+ showInUniversalList: false
695
904
  }
696
905
  };
697
906
  function detectPlatform(cwd) {
698
907
  for (const [key, config] of Object.entries(AGENTS)) {
699
- if (config.detectMarker && existsSync4(join3(cwd, config.detectMarker))) {
908
+ if (config.detectMarker && existsSync4(join4(cwd, config.detectMarker))) {
700
909
  return key;
701
910
  }
702
911
  }
703
- if (existsSync4(join3(configHome, "opencode"))) {
912
+ if (existsSync4(join4(configHome, "opencode"))) {
704
913
  return "opencode";
705
914
  }
706
915
  return "claude-code";
707
916
  }
708
917
  function getAgentSkillPath(cwd, agent, name) {
709
- return join3(cwd, AGENTS[agent].skillsDir, name);
918
+ return join4(cwd, AGENTS[agent].skillsDir, name);
710
919
  }
711
920
  function getAgentGlobalSkillPath(agent, name) {
712
- return join3(AGENTS[agent].globalSkillsDir, name);
921
+ return join4(AGENTS[agent].globalSkillsDir, name);
922
+ }
923
+ function isUniversalAgent(type) {
924
+ return AGENTS[type].skillsDir === ".agents/skills";
713
925
  }
714
926
 
715
927
  // src/utils/paths.ts
716
- var AGENTS_HOME = join4(homedir2(), ".agents");
717
- var CONFIG_DIR = join4(AGENTS_HOME, "config");
718
- var SKILLS_DIR = join4(AGENTS_HOME, "skills");
719
- var REGISTRY_PATH = join4(AGENTS_HOME, "registry.json");
928
+ var AGENTS_HOME = join5(homedir2(), ".agents");
929
+ var CONFIG_DIR = join5(AGENTS_HOME, "config");
930
+ var SKILLS_DIR = join5(AGENTS_HOME, "skills");
931
+ var REGISTRY_PATH = join5(AGENTS_HOME, "registry.json");
720
932
  function getSkillCanonicalPath(name) {
721
- return join4(SKILLS_DIR, name);
933
+ return join5(SKILLS_DIR, name);
722
934
  }
723
935
  function getSkillConfigPath(name) {
724
- return join4(CONFIG_DIR, name);
936
+ return join5(CONFIG_DIR, name);
725
937
  }
726
938
 
727
939
  // src/core/env-manager.ts
@@ -746,9 +958,9 @@ function serializeEnv(data) {
746
958
  }
747
959
  async function backupEnv(skillName, agentSkillDir) {
748
960
  const locations = [
749
- join5(getSkillConfigPath(skillName), ".env"),
750
- ...agentSkillDir ? [join5(agentSkillDir, ".env")] : [],
751
- join5(getSkillCanonicalPath(skillName), ".env")
961
+ join6(getSkillConfigPath(skillName), ".env"),
962
+ ...agentSkillDir ? [join6(agentSkillDir, ".env")] : [],
963
+ join6(getSkillCanonicalPath(skillName), ".env")
752
964
  ];
753
965
  for (const loc of locations) {
754
966
  const content = await readTextSafe(loc);
@@ -763,16 +975,16 @@ async function backupEnv(skillName, agentSkillDir) {
763
975
  return null;
764
976
  }
765
977
  async function restoreEnv(skillName, envData, skillDir) {
766
- const exampleContent = await readTextSafe(join5(skillDir, ".env.example"));
978
+ const exampleContent = await readTextSafe(join6(skillDir, ".env.example"));
767
979
  let finalContent;
768
980
  if (exampleContent) {
769
981
  finalContent = mergeEnv(envData, exampleContent);
770
982
  } else {
771
983
  finalContent = serializeEnv(envData);
772
984
  }
773
- const configEnvPath = join5(getSkillConfigPath(skillName), ".env");
985
+ const configEnvPath = join6(getSkillConfigPath(skillName), ".env");
774
986
  await writeText(configEnvPath, finalContent);
775
- const skillEnvPath = join5(skillDir, ".env");
987
+ const skillEnvPath = join6(skillDir, ".env");
776
988
  await writeText(skillEnvPath, finalContent);
777
989
  debug(`Restored .env to ${configEnvPath} and ${skillEnvPath}`);
778
990
  }
@@ -796,7 +1008,7 @@ function mergeEnv(existing, exampleContent) {
796
1008
  }
797
1009
  async function getEnvStatus(skillName, requiredKeys) {
798
1010
  if (requiredKeys.length === 0) return "configured";
799
- const configEnvPath = join5(getSkillConfigPath(skillName), ".env");
1011
+ const configEnvPath = join6(getSkillConfigPath(skillName), ".env");
800
1012
  const content = await readTextSafe(configEnvPath);
801
1013
  if (!content) return "missing";
802
1014
  const data = parseEnvFile(content);
@@ -808,18 +1020,18 @@ async function getEnvStatus(skillName, requiredKeys) {
808
1020
  return "missing";
809
1021
  }
810
1022
  async function setEnvValue(skillName, key, value, skillDir) {
811
- const configEnvPath = join5(getSkillConfigPath(skillName), ".env");
1023
+ const configEnvPath = join6(getSkillConfigPath(skillName), ".env");
812
1024
  const content = await readTextSafe(configEnvPath);
813
1025
  const data = content ? parseEnvFile(content) : {};
814
1026
  data[key] = value;
815
1027
  const newContent = serializeEnv(data);
816
1028
  await writeText(configEnvPath, newContent);
817
1029
  if (skillDir) {
818
- await writeText(join5(skillDir, ".env"), newContent);
1030
+ await writeText(join6(skillDir, ".env"), newContent);
819
1031
  }
820
1032
  }
821
1033
  function getEnvEditPath(skillName) {
822
- return join5(getSkillConfigPath(skillName), ".env");
1034
+ return join6(getSkillConfigPath(skillName), ".env");
823
1035
  }
824
1036
 
825
1037
  // src/core/registry.ts
@@ -924,12 +1136,22 @@ async function getRegistryEntry(skillName) {
924
1136
 
925
1137
  // src/core/installer.ts
926
1138
  var TOTAL_STEPS = 9;
1139
+ function sanitizeName(name) {
1140
+ let sanitized = name.replace(/\.\./g, "").replace(/[^a-zA-Z0-9_.-]/g, "");
1141
+ sanitized = sanitized.replace(/^\.+/, "");
1142
+ return sanitized;
1143
+ }
1144
+ function isPathSafe(targetPath, baseDir) {
1145
+ const rel = relative(resolve3(baseDir), resolve3(targetPath));
1146
+ if (!rel || rel === ".") return false;
1147
+ return !rel.startsWith("..") && !resolve3(rel).includes("..");
1148
+ }
927
1149
  async function installSkill(options) {
928
1150
  const { source, cwd, copy = false, force = false, global: isGlobal = false } = options;
929
1151
  step(1, TOTAL_STEPS, "Fetching skill source...");
930
1152
  let sourceDir;
931
1153
  if (source.type === "git") {
932
- sourceDir = await cloneRepo(source.url, source.branch);
1154
+ sourceDir = source.localPath ?? await cloneRepo(source.url, source.branch);
933
1155
  } else if (source.type === "local") {
934
1156
  sourceDir = source.path;
935
1157
  if (!existsSync6(sourceDir)) {
@@ -948,7 +1170,10 @@ async function installSkill(options) {
948
1170
  if (!parsed) {
949
1171
  throw new SkillParseError("Failed to read SKILL.md");
950
1172
  }
951
- const skillName = parsed.frontmatter.name;
1173
+ const skillName = sanitizeName(parsed.frontmatter.name);
1174
+ if (!skillName) {
1175
+ throw new SkillParseError("Skill name is empty after sanitization");
1176
+ }
952
1177
  info(`Found skill: ${skillName}${parsed.frontmatter.version ? ` v${parsed.frontmatter.version}` : ""}`);
953
1178
  step(4, TOTAL_STEPS, "Detecting agent platform...");
954
1179
  const agent = options.agent ?? detectPlatform(cwd);
@@ -963,6 +1188,9 @@ async function installSkill(options) {
963
1188
  }
964
1189
  step(6, TOTAL_STEPS, "Installing to canonical path...");
965
1190
  const canonicalPath = getSkillCanonicalPath(skillName);
1191
+ if (!isPathSafe(canonicalPath, SKILLS_DIR)) {
1192
+ throw new SkillParseError(`Unsafe canonical path: ${canonicalPath}`);
1193
+ }
966
1194
  if (existsSync6(canonicalPath) && !force) {
967
1195
  info("Replacing existing installation");
968
1196
  }
@@ -974,18 +1202,25 @@ async function installSkill(options) {
974
1202
  await restoreEnv(skillName, envBackup, canonicalPath);
975
1203
  success(".env restored successfully");
976
1204
  } else {
977
- const examplePath = join6(canonicalPath, ".env.example");
1205
+ const examplePath = join7(canonicalPath, ".env.example");
978
1206
  if (existsSync6(examplePath)) {
979
1207
  warn("Found .env.example \u2014 run `skill-master env edit " + skillName + "` to configure");
980
1208
  }
981
1209
  }
982
1210
  step(8, TOTAL_STEPS, `Linking to ${agent} skills directory...`);
983
1211
  const agentPath = isGlobal ? getAgentGlobalSkillPath(agent, skillName) : getAgentSkillPath(cwd, agent, skillName);
984
- const linkType = await symlinkOrCopy(canonicalPath, agentPath, copy);
985
- success(`${linkType === "symlink" ? "Symlinked" : "Copied"} to ${agentPath}`);
1212
+ let installMode;
1213
+ if (isGlobal && isUniversalAgent(agent) && canonicalPath === agentPath) {
1214
+ installMode = "copy";
1215
+ success(`Canonical path is agent path (universal agent): ${agentPath}`);
1216
+ } else {
1217
+ const linkType = await symlinkOrCopy(canonicalPath, agentPath, copy);
1218
+ installMode = linkType;
1219
+ success(`${linkType === "symlink" ? "Symlinked" : "Copied"} to ${agentPath}`);
1220
+ }
986
1221
  step(9, TOTAL_STEPS, "Updating registry...");
987
1222
  const capabilities = parsed.frontmatter.capabilities ?? inferCapabilities(parsed.frontmatter["allowed-tools"] ?? []);
988
- const envExampleContent = await readTextSafe(join6(canonicalPath, ".env.example"));
1223
+ const envExampleContent = await readTextSafe(join7(canonicalPath, ".env.example"));
989
1224
  const envKeys = envExampleContent ? extractEnvKeys(envExampleContent) : [];
990
1225
  const now = (/* @__PURE__ */ new Date()).toISOString();
991
1226
  const agentInstall = {
@@ -1006,11 +1241,81 @@ async function installSkill(options) {
1006
1241
  await updateRegistry(skillName, entry);
1007
1242
  blank();
1008
1243
  success(`Skill "${skillName}" installed successfully!`);
1244
+ return {
1245
+ skillName,
1246
+ version: parsed.frontmatter.version,
1247
+ canonicalPath,
1248
+ agentPath,
1249
+ installMode
1250
+ };
1251
+ }
1252
+
1253
+ // src/core/local-lock.ts
1254
+ import { join as join8 } from "path";
1255
+ import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
1256
+ import { createHash } from "crypto";
1257
+ var LOCK_FILENAME = "skills-lock.json";
1258
+ var HASH_EXCLUDES = /* @__PURE__ */ new Set([".env", ".git", "node_modules"]);
1259
+ function getLocalLockPath(cwd) {
1260
+ return join8(cwd, LOCK_FILENAME);
1261
+ }
1262
+ function createEmptyLock() {
1263
+ return { version: 1, skills: {} };
1264
+ }
1265
+ async function readLocalLock(cwd) {
1266
+ const lockPath = getLocalLockPath(cwd);
1267
+ const data = await readJsonSafe(lockPath);
1268
+ if (!data || typeof data !== "object" || data.version !== 1 || data.skills === null || Array.isArray(data.skills) || typeof data.skills !== "object") {
1269
+ return createEmptyLock();
1270
+ }
1271
+ return data;
1272
+ }
1273
+ async function writeLocalLock(lock, cwd) {
1274
+ const lockPath = getLocalLockPath(cwd);
1275
+ const sorted = {};
1276
+ for (const key of Object.keys(lock.skills).sort()) {
1277
+ sorted[key] = lock.skills[key];
1278
+ }
1279
+ await atomicWriteJson(lockPath, { version: lock.version, skills: sorted });
1280
+ }
1281
+ async function computeSkillFolderHash(dirPath) {
1282
+ const hash = createHash("sha256");
1283
+ await hashDir(dirPath, "", hash);
1284
+ return hash.digest("hex");
1285
+ }
1286
+ async function hashDir(basePath, relativePath, hash) {
1287
+ const fullPath = relativePath ? join8(basePath, relativePath) : basePath;
1288
+ const entries = await readdir3(fullPath, { withFileTypes: true });
1289
+ const sorted = entries.sort((a, b) => a.name.localeCompare(b.name));
1290
+ for (const entry of sorted) {
1291
+ if (HASH_EXCLUDES.has(entry.name)) continue;
1292
+ const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1293
+ if (entry.isDirectory()) {
1294
+ await hashDir(basePath, entryRelative, hash);
1295
+ } else if (entry.isFile()) {
1296
+ hash.update(entryRelative);
1297
+ hash.update("\0");
1298
+ const content = await readFile3(join8(basePath, entryRelative));
1299
+ hash.update(content);
1300
+ hash.update("\0");
1301
+ }
1302
+ }
1303
+ }
1304
+ async function addSkillToLocalLock(skillName, entry, cwd) {
1305
+ const lock = await readLocalLock(cwd);
1306
+ lock.skills[skillName] = entry;
1307
+ await writeLocalLock(lock, cwd);
1308
+ }
1309
+ async function removeSkillFromLocalLock(skillName, cwd) {
1310
+ const lock = await readLocalLock(cwd);
1311
+ if (!(skillName in lock.skills)) return;
1312
+ delete lock.skills[skillName];
1313
+ await writeLocalLock(lock, cwd);
1009
1314
  }
1010
1315
 
1011
1316
  // src/commands/add.ts
1012
1317
  import { existsSync as existsSync7 } from "fs";
1013
- import { basename as basename2, join as join7 } from "path";
1318
+ import { basename as basename2, join as join9, relative as relative2 } from "path";
1014
1319
  function parseAddFlags(args) {
1015
1320
  const flags = {
1016
1321
  global: false,
@@ -1154,7 +1459,7 @@ async function add(args) {
1154
1459
  step(1, 9, "Fetching skill source...");
1155
1460
  sourceDir = await cloneRepo(parsed.url, parsed.ref);
1156
1461
  if (parsed.subpath) {
1157
- const sub = join7(sourceDir, parsed.subpath);
1462
+ const sub = join9(sourceDir, parsed.subpath);
1158
1463
  if (existsSync7(sub)) {
1159
1464
  sourceDir = sub;
1160
1465
  }
@@ -1165,14 +1470,14 @@ async function add(args) {
1165
1470
  throw new SkillNotFoundError(sourceDir);
1166
1471
  }
1167
1472
  }
1168
- const allSkillDirs = await findAllSkillDirectories(sourceDir, flags.fullDepth);
1473
+ const allSkillDirs = await findAllSkillDirectoriesWithPlugins(sourceDir, flags.fullDepth);
1169
1474
  if (allSkillDirs.length === 0) {
1170
1475
  throw new SkillNotFoundError(`No SKILL.md found in ${sourceDir}`);
1171
1476
  }
1172
1477
  if (flags.list) {
1173
1478
  blank();
1174
1479
  tableHeader("Skill", "Version", "Description");
1175
- for (const dir of allSkillDirs) {
1480
+ for (const { path: dir } of allSkillDirs) {
1176
1481
  const sk = await readSkillMd(dir);
1177
1482
  if (sk) {
1178
1483
  tableRow(
@@ -1189,18 +1494,18 @@ async function add(args) {
1189
1494
  if (flags.skill.length > 0 && !flags.skill.includes("*")) {
1190
1495
  const requested = new Set(flags.skill.map((s) => s.toLowerCase()));
1191
1496
  const filtered = [];
1192
- for (const dir of allSkillDirs) {
1193
- const sk = await readSkillMd(dir);
1497
+ for (const item of allSkillDirs) {
1498
+ const sk = await readSkillMd(item.path);
1194
1499
  if (!sk) continue;
1195
1500
  const name = sk.frontmatter.name.toLowerCase();
1196
- const dirName = basename2(dir).toLowerCase();
1501
+ const dirName = basename2(item.path).toLowerCase();
1197
1502
  if (requested.has(name) || requested.has(dirName)) {
1198
- filtered.push(dir);
1503
+ filtered.push(item);
1199
1504
  }
1200
1505
  }
1201
1506
  if (filtered.length === 0) {
1202
1507
  const available = [];
1203
- for (const dir of allSkillDirs) {
1508
+ for (const { path: dir } of allSkillDirs) {
1204
1509
  const sk = await readSkillMd(dir);
1205
1510
  if (sk) available.push(sk.frontmatter.name);
1206
1511
  }
@@ -1214,10 +1519,11 @@ async function add(args) {
1214
1519
  }
1215
1520
  const agents = flags.agent.length > 0 ? flags.agent : [void 0];
1216
1521
  try {
1217
- for (const dir of targetDirs) {
1522
+ for (const { path: dir, pluginName } of targetDirs) {
1218
1523
  for (const agent of agents) {
1219
- await installSkill({
1220
- source: { type: "local", path: dir },
1524
+ const installSource = parsed.type === "git" ? { type: "git", url: parsed.url, branch: parsed.ref, localPath: dir } : { type: "local", path: dir };
1525
+ const result = await installSkill({
1526
+ source: installSource,
1221
1527
  agent,
1222
1528
  cwd,
1223
1529
  global: flags.global,
@@ -1225,6 +1531,16 @@ async function add(args) {
1225
1531
  force: flags.force,
1226
1532
  yes: flags.yes
1227
1533
  });
1534
+ if (!flags.global) {
1535
+ const skillDir = relative2(sourceDir, dir);
1536
+ await addSkillToLocalLock(result.skillName, {
1537
+ source,
1538
+ sourceType: parsed.type === "git" ? "github" : "local",
1539
+ computedHash: await computeSkillFolderHash(result.canonicalPath),
1540
+ ...skillDir && skillDir !== "." ? { skillDir } : {},
1541
+ ...pluginName ? { pluginName } : {}
1542
+ }, cwd);
1543
+ }
1228
1544
  }
1229
1545
  }
1230
1546
  } catch (err) {
@@ -1413,6 +1729,7 @@ async function remove(args) {
1413
1729
  await removePath(getSkillConfigPath(skillName));
1414
1730
  success("Purged config directory");
1415
1731
  }
1732
+ await removeSkillFromLocalLock(skillName, process.cwd());
1416
1733
  }
1417
1734
  } else {
1418
1735
  for (const agentRecord of entry.agents) {
@@ -1426,6 +1743,7 @@ async function remove(args) {
1426
1743
  success("Purged config directory");
1427
1744
  }
1428
1745
  await removeFromRegistry(skillName);
1746
+ await removeSkillFromLocalLock(skillName, process.cwd());
1429
1747
  }
1430
1748
  success(`Skill "${skillName}" removed successfully!`);
1431
1749
  }
@@ -1547,6 +1865,9 @@ function parseListFlags(args) {
1547
1865
  }
1548
1866
  return flags;
1549
1867
  }
1868
+ function toTitleCase(str) {
1869
+ return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1870
+ }
1550
1871
  async function list(args = []) {
1551
1872
  const flags = parseListFlags(args);
1552
1873
  const skills = await listRegistry();
@@ -1560,14 +1881,55 @@ async function list(args = []) {
1560
1881
  info("No skills installed");
1561
1882
  return;
1562
1883
  }
1563
- blank();
1564
- tableHeader("Skill", "Version", "Platform(s)", "Installed");
1884
+ const cwd = process.cwd();
1885
+ const localLock = await readLocalLock(cwd);
1886
+ const groupedSkills = {};
1887
+ const ungroupedSkills = [];
1565
1888
  for (const [name, entry] of entries) {
1566
- const date = new Date(entry.installed_at).toLocaleDateString();
1567
- const platforms = entry.agents.map((a) => a.agent).join(", ");
1568
- tableRow(name, entry.version ?? "-", platforms, date);
1889
+ const lockEntry = localLock.skills[name];
1890
+ if (lockEntry?.pluginName) {
1891
+ const group = lockEntry.pluginName;
1892
+ if (!groupedSkills[group]) {
1893
+ groupedSkills[group] = [];
1894
+ }
1895
+ groupedSkills[group].push([name, entry]);
1896
+ } else {
1897
+ ungroupedSkills.push([name, entry]);
1898
+ }
1569
1899
  }
1900
+ const hasGroups = Object.keys(groupedSkills).length > 0;
1570
1901
  blank();
1902
+ if (hasGroups) {
1903
+ const sortedGroups = Object.keys(groupedSkills).sort();
1904
+ for (const group of sortedGroups) {
1905
+ section(toTitleCase(group));
1906
+ tableHeader("Skill", "Version", "Platform(s)", "Installed");
1907
+ for (const [name, entry] of groupedSkills[group]) {
1908
+ const date = new Date(entry.installed_at).toLocaleDateString();
1909
+ const platforms = entry.agents.map((a) => a.agent).join(", ");
1910
+ tableRow(name, entry.version ?? "-", platforms, date);
1911
+ }
1912
+ blank();
1913
+ }
1914
+ if (ungroupedSkills.length > 0) {
1915
+ section("General");
1916
+ tableHeader("Skill", "Version", "Platform(s)", "Installed");
1917
+ for (const [name, entry] of ungroupedSkills) {
1918
+ const date = new Date(entry.installed_at).toLocaleDateString();
1919
+ const platforms = entry.agents.map((a) => a.agent).join(", ");
1920
+ tableRow(name, entry.version ?? "-", platforms, date);
1921
+ }
1922
+ blank();
1923
+ }
1924
+ } else {
1925
+ tableHeader("Skill", "Version", "Platform(s)", "Installed");
1926
+ for (const [name, entry] of entries) {
1927
+ const date = new Date(entry.installed_at).toLocaleDateString();
1928
+ const platforms = entry.agents.map((a) => a.agent).join(", ");
1929
+ tableRow(name, entry.version ?? "-", platforms, date);
1930
+ }
1931
+ blank();
1932
+ }
1571
1933
  }
1572
1934
 
1573
1935
  // src/commands/info.ts
@@ -1703,13 +2065,14 @@ async function find(args) {
1703
2065
  process.exit(1);
1704
2066
  }
1705
2067
  const data = await response.json();
1706
- if (!Array.isArray(data) || data.length === 0) {
2068
+ const skills = data.skills ?? [];
2069
+ if (skills.length === 0) {
1707
2070
  info("No skills found matching your query.");
1708
2071
  return;
1709
2072
  }
1710
2073
  blank();
1711
2074
  tableHeader("Name", "Source", "Installs");
1712
- for (const item of data) {
2075
+ for (const item of skills) {
1713
2076
  tableRow(
1714
2077
  item.name ?? "\u2014",
1715
2078
  item.source ?? "\u2014",
@@ -1729,7 +2092,7 @@ async function find(args) {
1729
2092
 
1730
2093
  // src/commands/init.ts
1731
2094
  import { existsSync as existsSync9 } from "fs";
1732
- import { join as join8, basename as basename3 } from "path";
2095
+ import { join as join10, basename as basename3 } from "path";
1733
2096
  var SKILL_MD_TEMPLATE = `---
1734
2097
  name: {{NAME}}
1735
2098
  version: 0.1.0
@@ -1755,13 +2118,13 @@ async function init(args) {
1755
2118
  let targetDir;
1756
2119
  let skillName;
1757
2120
  if (nameArg) {
1758
- targetDir = join8(cwd, nameArg);
2121
+ targetDir = join10(cwd, nameArg);
1759
2122
  skillName = nameArg;
1760
2123
  } else {
1761
2124
  targetDir = cwd;
1762
2125
  skillName = basename3(cwd);
1763
2126
  }
1764
- const skillMdPath = join8(targetDir, "SKILL.md");
2127
+ const skillMdPath = join10(targetDir, "SKILL.md");
1765
2128
  if (existsSync9(skillMdPath)) {
1766
2129
  error(`SKILL.md already exists at ${skillMdPath}`);
1767
2130
  process.exit(1);
@@ -1834,6 +2197,301 @@ async function getRemoteHead(source) {
1834
2197
  }
1835
2198
  }
1836
2199
 
2200
+ // src/commands/sync.ts
2201
+ import { relative as relative3 } from "path";
2202
+ function parseSyncFlags(args) {
2203
+ const flags = {
2204
+ agent: [],
2205
+ yes: false,
2206
+ force: false,
2207
+ help: false
2208
+ };
2209
+ let i = 0;
2210
+ while (i < args.length) {
2211
+ const arg = args[i];
2212
+ switch (arg) {
2213
+ case "-h":
2214
+ case "--help":
2215
+ flags.help = true;
2216
+ i++;
2217
+ break;
2218
+ case "-a":
2219
+ case "--agent":
2220
+ i++;
2221
+ while (i < args.length && !args[i].startsWith("-")) {
2222
+ flags.agent.push(args[i]);
2223
+ i++;
2224
+ }
2225
+ break;
2226
+ case "-y":
2227
+ case "--yes":
2228
+ flags.yes = true;
2229
+ i++;
2230
+ break;
2231
+ case "-f":
2232
+ case "--force":
2233
+ flags.force = true;
2234
+ i++;
2235
+ break;
2236
+ default:
2237
+ if (arg.startsWith("-")) {
2238
+ throw new Error(`Unknown option: ${arg}`);
2239
+ }
2240
+ i++;
2241
+ break;
2242
+ }
2243
+ }
2244
+ return { flags };
2245
+ }
2246
+ function printSyncHelp() {
2247
+ console.log("Usage: skill-master sync [options]");
2248
+ console.log("");
2249
+ console.log("Discover and sync skills from node_modules.");
2250
+ console.log("");
2251
+ console.log("Options:");
2252
+ console.log(" -h, --help Show this help message");
2253
+ console.log(" -a, --agent <agents> Target agents (space-separated)");
2254
+ console.log(" -y, --yes Skip confirmations");
2255
+ console.log(" -f, --force Force reinstall even if unchanged");
2256
+ }
2257
+ async function sync(args) {
2258
+ const { flags } = parseSyncFlags(args);
2259
+ if (flags.help) {
2260
+ printSyncHelp();
2261
+ process.exit(0);
2262
+ }
2263
+ const cwd = process.cwd();
2264
+ info("Scanning node_modules for skills...");
2265
+ const skillDirs = await discoverNodeModulesSkills(cwd);
2266
+ if (skillDirs.length === 0) {
2267
+ info("No skills found in node_modules.");
2268
+ return;
2269
+ }
2270
+ const lock = await readLocalLock(cwd);
2271
+ const skills = [];
2272
+ for (const dir of skillDirs) {
2273
+ const parsed = await readSkillMd(dir);
2274
+ if (!parsed) continue;
2275
+ const name = sanitizeName(parsed.frontmatter.name);
2276
+ const currentHash = await computeSkillFolderHash(dir);
2277
+ const lockEntry = lock.skills[name];
2278
+ let status;
2279
+ if (!lockEntry) {
2280
+ status = "new";
2281
+ } else if (lockEntry.computedHash !== currentHash || flags.force) {
2282
+ status = "updated";
2283
+ } else {
2284
+ status = "unchanged";
2285
+ }
2286
+ skills.push({ dir, name, version: parsed.frontmatter.version, status });
2287
+ }
2288
+ const actionable = skills.filter((s) => s.status !== "unchanged");
2289
+ if (actionable.length === 0) {
2290
+ success("All node_modules skills are up to date.");
2291
+ return;
2292
+ }
2293
+ blank();
2294
+ tableHeader("Skill", "Version", "Status");
2295
+ for (const s of skills) {
2296
+ tableRow(s.name, s.version ?? "-", s.status);
2297
+ }
2298
+ blank();
2299
+ info(`${actionable.length} skill(s) to install/update.`);
2300
+ if (!flags.yes) {
2301
+ const readline = await import("readline");
2302
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2303
+ const answer = await new Promise((resolve4) => {
2304
+ rl.question("Proceed? [y/N] ", resolve4);
2305
+ });
2306
+ rl.close();
2307
+ if (answer.toLowerCase() !== "y") {
2308
+ info("Aborted.");
2309
+ return;
2310
+ }
2311
+ }
2312
+ const agents = flags.agent.length > 0 ? flags.agent : [void 0];
2313
+ for (const s of actionable) {
2314
+ for (const agent of agents) {
2315
+ try {
2316
+ const result = await installSkill({
2317
+ source: { type: "local", path: s.dir },
2318
+ agent,
2319
+ cwd,
2320
+ global: false,
2321
+ copy: false,
2322
+ force: flags.force,
2323
+ yes: true
2324
+ });
2325
+ await addSkillToLocalLock(result.skillName, {
2326
+ source: relative3(cwd, s.dir),
2327
+ sourceType: "node_modules",
2328
+ computedHash: await computeSkillFolderHash(result.canonicalPath)
2329
+ }, cwd);
2330
+ } catch (err) {
2331
+ error(`Failed to install ${s.name}: ${err.message}`);
2332
+ }
2333
+ }
2334
+ }
2335
+ blank();
2336
+ success(`Synced ${actionable.length} skill(s) from node_modules.`);
2337
+ }
2338
+
2339
+ // src/commands/restore.ts
2340
+ import { existsSync as existsSync10 } from "fs";
2341
+ function parseRestoreFlags(args) {
2342
+ const flags = { help: false };
2343
+ for (const arg of args) {
2344
+ if (arg === "-h" || arg === "--help") {
2345
+ flags.help = true;
2346
+ }
2347
+ }
2348
+ return { flags };
2349
+ }
2350
+ function printRestoreHelp() {
2351
+ console.log("Usage: skill-master restore [options]");
2352
+ console.log("");
2353
+ console.log("Restore skills from skills-lock.json.");
2354
+ console.log("");
2355
+ console.log("Options:");
2356
+ console.log(" -h, --help Show this help message");
2357
+ }
2358
+ async function restore(args) {
2359
+ const { flags } = parseRestoreFlags(args);
2360
+ if (flags.help) {
2361
+ printRestoreHelp();
2362
+ process.exit(0);
2363
+ }
2364
+ const cwd = process.cwd();
2365
+ const lock = await readLocalLock(cwd);
2366
+ const entries = Object.entries(lock.skills);
2367
+ if (entries.length === 0) {
2368
+ info("No skills found in skills-lock.json. Nothing to restore.");
2369
+ return;
2370
+ }
2371
+ info(`Restoring ${entries.length} skill(s) from skills-lock.json...`);
2372
+ blank();
2373
+ const github = [];
2374
+ const nodeModules = [];
2375
+ const local = [];
2376
+ for (const [name, entry] of entries) {
2377
+ switch (entry.sourceType) {
2378
+ case "github":
2379
+ github.push([name, entry]);
2380
+ break;
2381
+ case "node_modules":
2382
+ nodeModules.push([name, entry]);
2383
+ break;
2384
+ case "local":
2385
+ local.push([name, entry]);
2386
+ break;
2387
+ }
2388
+ }
2389
+ let installed = 0;
2390
+ let failed = 0;
2391
+ if (nodeModules.length > 0) {
2392
+ info(`Syncing ${nodeModules.length} node_modules skill(s)...`);
2393
+ const nmSkillDirs = await discoverNodeModulesSkills(cwd);
2394
+ const nmMap = /* @__PURE__ */ new Map();
2395
+ for (const dir of nmSkillDirs) {
2396
+ const parsed = await readSkillMd(dir);
2397
+ if (parsed) nmMap.set(sanitizeName(parsed.frontmatter.name), dir);
2398
+ }
2399
+ for (const [name] of nodeModules) {
2400
+ const dir = nmMap.get(name);
2401
+ if (!dir) {
2402
+ warn(`Skill "${name}" not found in node_modules \u2014 run npm install first`);
2403
+ failed++;
2404
+ continue;
2405
+ }
2406
+ try {
2407
+ const result = await installSkill({
2408
+ source: { type: "local", path: dir },
2409
+ cwd,
2410
+ global: false,
2411
+ yes: true
2412
+ });
2413
+ await addSkillToLocalLock(result.skillName, {
2414
+ source: dir,
2415
+ sourceType: "node_modules",
2416
+ computedHash: await computeSkillFolderHash(result.canonicalPath)
2417
+ }, cwd);
2418
+ installed++;
2419
+ } catch (err) {
2420
+ error(`Failed to restore "${name}": ${err.message}`);
2421
+ failed++;
2422
+ }
2423
+ }
2424
+ }
2425
+ for (const [name, entry] of github) {
2426
+ try {
2427
+ const parsed = parseSource(entry.source);
2428
+ if (parsed.type !== "git" || !parsed.url) {
2429
+ warn(`Invalid source for "${name}": ${entry.source}`);
2430
+ failed++;
2431
+ continue;
2432
+ }
2433
+ const sourceDir = await cloneRepo(parsed.url, parsed.ref);
2434
+ let skillPath = sourceDir;
2435
+ if (parsed.subpath) {
2436
+ skillPath = `${skillPath}/${parsed.subpath}`;
2437
+ }
2438
+ if (entry.skillDir) {
2439
+ skillPath = `${skillPath}/${entry.skillDir}`;
2440
+ }
2441
+ const result = await installSkill({
2442
+ source: { type: "local", path: skillPath },
2443
+ cwd,
2444
+ global: false,
2445
+ yes: true
2446
+ });
2447
+ await addSkillToLocalLock(result.skillName, {
2448
+ source: entry.source,
2449
+ sourceType: "github",
2450
+ computedHash: await computeSkillFolderHash(result.canonicalPath),
2451
+ ...entry.skillDir ? { skillDir: entry.skillDir } : {},
2452
+ ...entry.pluginName ? { pluginName: entry.pluginName } : {}
2453
+ }, cwd);
2454
+ installed++;
2455
+ } catch (err) {
2456
+ error(`Failed to restore "${name}": ${err.message}`);
2457
+ failed++;
2458
+ }
2459
+ }
2460
+ for (const [name, entry] of local) {
2461
+ if (!existsSync10(entry.source)) {
2462
+ warn(`Local source not found for "${name}": ${entry.source}`);
2463
+ failed++;
2464
+ continue;
2465
+ }
2466
+ try {
2467
+ const localPath = entry.skillDir ? `${entry.source}/${entry.skillDir}` : entry.source;
2468
+ const result = await installSkill({
2469
+ source: { type: "local", path: localPath },
2470
+ cwd,
2471
+ global: false,
2472
+ yes: true
2473
+ });
2474
+ await addSkillToLocalLock(result.skillName, {
2475
+ source: entry.source,
2476
+ sourceType: "local",
2477
+ computedHash: await computeSkillFolderHash(result.canonicalPath),
2478
+ ...entry.skillDir ? { skillDir: entry.skillDir } : {},
2479
+ ...entry.pluginName ? { pluginName: entry.pluginName } : {}
2480
+ }, cwd);
2481
+ installed++;
2482
+ } catch (err) {
2483
+ error(`Failed to restore "${name}": ${err.message}`);
2484
+ failed++;
2485
+ }
2486
+ }
2487
+ blank();
2488
+ if (failed > 0) {
2489
+ warn(`Restored ${installed} skill(s), ${failed} failed.`);
2490
+ } else {
2491
+ success(`Restored ${installed} skill(s) successfully.`);
2492
+ }
2493
+ }
2494
+
1837
2495
  // src/cli.ts
1838
2496
  var VERSION = "0.1.0";
1839
2497
  var HELP = `
@@ -1845,6 +2503,8 @@ Usage:
1845
2503
  skill-master list [options] List installed skills (alias: ls)
1846
2504
  skill-master find [query] Search for skills (aliases: search, f, s)
1847
2505
  skill-master update [skill] Update skills (alias: upgrade)
2506
+ skill-master sync [options] Sync skills from node_modules
2507
+ skill-master restore Restore skills from skills-lock.json (alias: install-lock)
1848
2508
  skill-master init [name] Create a new skill template
1849
2509
  skill-master check Check for skill updates
1850
2510
  skill-master env <list|set|edit> Manage environment variables
@@ -1862,12 +2522,19 @@ Add Options:
1862
2522
  --copy Copy instead of symlink
1863
2523
  --force Force reinstall
1864
2524
 
2525
+ Sync Options:
2526
+ -a, --agent <agents> Target agents (space-separated)
2527
+ -y, --yes Skip confirmations
2528
+ -f, --force Force reinstall even if unchanged
2529
+
1865
2530
  Examples:
1866
2531
  skill-master add owner/repo
1867
2532
  skill-master add https://github.com/user/skill -a claude-code cursor -y
1868
2533
  skill-master add ./local-skill --agent=cursor --copy
1869
2534
  skill-master remove my-skill --purge
1870
2535
  skill-master find "code review"
2536
+ skill-master sync -y
2537
+ skill-master restore
1871
2538
  skill-master init my-new-skill
1872
2539
  skill-master check
1873
2540
  `;
@@ -1923,6 +2590,15 @@ async function main() {
1923
2590
  case "check":
1924
2591
  await check(commandArgs);
1925
2592
  break;
2593
+ // sync — discover and install skills from node_modules
2594
+ case "sync":
2595
+ await sync(commandArgs);
2596
+ break;
2597
+ // restore with alias: install-lock
2598
+ case "restore":
2599
+ case "install-lock":
2600
+ await restore(commandArgs);
2601
+ break;
1926
2602
  // env, info, doctor — skill-master extensions
1927
2603
  case "env":
1928
2604
  await env(commandArgs);