skills 1.4.3 → 1.4.5

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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +127 -31
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The CLI for the open agent skills ecosystem.
4
4
 
5
5
  <!-- agent-list:start -->
6
- Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [37 more](#available-agents).
6
+ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [38 more](#available-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
9
  ## Install a Skill
@@ -214,7 +214,7 @@ Skills can be installed to any of these agents:
214
214
  | Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
215
215
  | Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
216
216
  | OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
217
- | Cline | `cline` | `.agents/skills/` | `~/.agents/skills/` |
217
+ | Cline, Warp | `cline`, `warp` | `.agents/skills/` | `~/.agents/skills/` |
218
218
  | CodeBuddy | `codebuddy` | `.codebuddy/skills/` | `~/.codebuddy/skills/` |
219
219
  | Codex | `codex` | `.agents/skills/` | `~/.codex/skills/` |
220
220
  | Command Code | `command-code` | `.commandcode/skills/` | `~/.commandcode/skills/` |
package/dist/cli.mjs CHANGED
@@ -21,6 +21,13 @@ import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpat
21
21
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
22
22
  function getOwnerRepo(parsed) {
23
23
  if (parsed.type === "local") return null;
24
+ const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
25
+ if (sshMatch) {
26
+ let path = sshMatch[1];
27
+ path = path.replace(/\.git$/, "");
28
+ if (path.includes("/")) return path;
29
+ return null;
30
+ }
24
31
  if (!parsed.url.startsWith("http://") && !parsed.url.startsWith("https://")) return null;
25
32
  try {
26
33
  let path = new URL(parsed.url).pathname.slice(1);
@@ -46,6 +53,11 @@ async function isRepoPrivate(owner, repo) {
46
53
  return null;
47
54
  }
48
55
  }
56
+ function sanitizeSubpath(subpath) {
57
+ const segments = subpath.replace(/\\/g, "/").split("/");
58
+ for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
59
+ return subpath;
60
+ }
49
61
  function isLocalPath(input) {
50
62
  return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
51
63
  }
@@ -53,6 +65,10 @@ const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills
53
65
  function parseSource(input) {
54
66
  const alias = SOURCE_ALIASES[input];
55
67
  if (alias) input = alias;
68
+ const githubPrefixMatch = input.match(/^github:(.+)$/);
69
+ if (githubPrefixMatch) return parseSource(githubPrefixMatch[1]);
70
+ const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
71
+ if (gitlabPrefixMatch) return parseSource(`https://gitlab.com/${gitlabPrefixMatch[1]}`);
56
72
  if (isLocalPath(input)) {
57
73
  const resolvedPath = resolve(input);
58
74
  return {
@@ -68,7 +84,7 @@ function parseSource(input) {
68
84
  type: "github",
69
85
  url: `https://github.com/${owner}/${repo}.git`,
70
86
  ref,
71
- subpath
87
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
72
88
  };
73
89
  }
74
90
  const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
@@ -95,7 +111,7 @@ function parseSource(input) {
95
111
  type: "gitlab",
96
112
  url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
97
113
  ref,
98
- subpath
114
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
99
115
  };
100
116
  }
101
117
  const gitlabTreeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)$/);
@@ -130,7 +146,7 @@ function parseSource(input) {
130
146
  return {
131
147
  type: "github",
132
148
  url: `https://github.com/${owner}/${repo}.git`,
133
- subpath
149
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath
134
150
  };
135
151
  }
136
152
  if (isWellKnownUrl(input)) return {
@@ -486,9 +502,15 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
486
502
  return [];
487
503
  }
488
504
  }
505
+ function isSubpathSafe(basePath, subpath) {
506
+ const normalizedBase = normalize(resolve(basePath));
507
+ const normalizedTarget = normalize(resolve(join(basePath, subpath)));
508
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
509
+ }
489
510
  async function discoverSkills(basePath, subpath, options) {
490
511
  const skills = [];
491
512
  const seenNames = /* @__PURE__ */ new Set();
513
+ if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
492
514
  const searchPath = subpath ? join(basePath, subpath) : basePath;
493
515
  const pluginGroupings = await getPluginGroupings(searchPath);
494
516
  const enhanceSkill = (skill) => {
@@ -902,6 +924,15 @@ const agents = {
902
924
  return existsSync(join(home, ".trae-cn"));
903
925
  }
904
926
  },
927
+ warp: {
928
+ name: "warp",
929
+ displayName: "Warp",
930
+ skillsDir: ".agents/skills",
931
+ globalSkillsDir: join(home, ".agents/skills"),
932
+ detectInstalled: async () => {
933
+ return existsSync(join(home, ".warp"));
934
+ }
935
+ },
905
936
  windsurf: {
906
937
  name: "windsurf",
907
938
  displayName: "Windsurf",
@@ -1114,10 +1145,14 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1114
1145
  }
1115
1146
  }
1116
1147
  const EXCLUDE_FILES = new Set(["metadata.json"]);
1117
- const EXCLUDE_DIRS = new Set([".git"]);
1148
+ const EXCLUDE_DIRS = new Set([
1149
+ ".git",
1150
+ "__pycache__",
1151
+ "__pypackages__"
1152
+ ]);
1118
1153
  const isExcluded = (name, isDirectory = false) => {
1119
1154
  if (EXCLUDE_FILES.has(name)) return true;
1120
- if (name.startsWith("_")) return true;
1155
+ if (name.startsWith(".")) return true;
1121
1156
  if (isDirectory && EXCLUDE_DIRS.has(name)) return true;
1122
1157
  return false;
1123
1158
  };
@@ -1128,10 +1163,15 @@ async function copyDirectory(src, dest) {
1128
1163
  const srcPath = join(src, entry.name);
1129
1164
  const destPath = join(dest, entry.name);
1130
1165
  if (entry.isDirectory()) await copyDirectory(srcPath, destPath);
1131
- else await cp(srcPath, destPath, {
1132
- dereference: true,
1133
- recursive: true
1134
- });
1166
+ else try {
1167
+ await cp(srcPath, destPath, {
1168
+ dereference: true,
1169
+ recursive: true
1170
+ });
1171
+ } catch (err) {
1172
+ if (err instanceof Error && "code" in err && err.code === "ENOENT" && entry.isSymbolicLink()) console.warn(`Skipping broken symlink: ${srcPath}`);
1173
+ else throw err;
1174
+ }
1135
1175
  }));
1136
1176
  }
1137
1177
  async function isSkillInstalled(skillName, agentType, options = {}) {
@@ -1579,6 +1619,8 @@ const AGENTS_DIR$1 = ".agents";
1579
1619
  const LOCK_FILE$1 = ".skill-lock.json";
1580
1620
  const CURRENT_VERSION$1 = 3;
1581
1621
  function getSkillLockPath$1() {
1622
+ const xdgStateHome = process.env.XDG_STATE_HOME;
1623
+ if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE$1);
1582
1624
  return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1);
1583
1625
  }
1584
1626
  async function readSkillLock$1() {
@@ -1751,7 +1793,7 @@ function createEmptyLocalLock() {
1751
1793
  skills: {}
1752
1794
  };
1753
1795
  }
1754
- var version$1 = "1.4.3";
1796
+ var version$1 = "1.4.5";
1755
1797
  const isCancelled$1 = (value) => typeof value === "symbol";
1756
1798
  async function isSourcePrivate(source) {
1757
1799
  const ownerRepo = parseOwnerRepo(source);
@@ -2048,7 +2090,8 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2048
2090
  installGlobally = scope;
2049
2091
  }
2050
2092
  let installMode = options.copy ? "copy" : "symlink";
2051
- if (!options.copy && !options.yes) {
2093
+ const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
2094
+ if (!options.copy && !options.yes && uniqueDirs.size > 1) {
2052
2095
  const modeChoice = await ve({
2053
2096
  message: "Installation method",
2054
2097
  options: [{
@@ -2066,7 +2109,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2066
2109
  process.exit(0);
2067
2110
  }
2068
2111
  installMode = modeChoice;
2069
- }
2112
+ } else if (uniqueDirs.size <= 1) installMode = "copy";
2070
2113
  const cwd = process.cwd();
2071
2114
  const summaryLines = [];
2072
2115
  targetAgents.map((a) => agents[a].displayName);
@@ -2440,7 +2483,8 @@ async function runAdd(args, options = {}) {
2440
2483
  installGlobally = scope;
2441
2484
  }
2442
2485
  let installMode = options.copy ? "copy" : "symlink";
2443
- if (!options.copy && !options.yes) {
2486
+ const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
2487
+ if (!options.copy && !options.yes && uniqueDirs.size > 1) {
2444
2488
  const modeChoice = await ve({
2445
2489
  message: "Installation method",
2446
2490
  options: [{
@@ -2459,7 +2503,7 @@ async function runAdd(args, options = {}) {
2459
2503
  process.exit(0);
2460
2504
  }
2461
2505
  installMode = modeChoice;
2462
- }
2506
+ } else if (uniqueDirs.size <= 1) installMode = "copy";
2463
2507
  const cwd = process.cwd();
2464
2508
  const summaryLines = [];
2465
2509
  targetAgents.map((a) => agents[a].displayName);
@@ -2552,6 +2596,7 @@ async function runAdd(args, options = {}) {
2552
2596
  skillFiles[skill.name] = relativePath;
2553
2597
  }
2554
2598
  const normalizedSource = getOwnerRepo(parsed);
2599
+ const lockSource = parsed.url.startsWith("git@") ? parsed.url : normalizedSource;
2555
2600
  if (normalizedSource) {
2556
2601
  const ownerRepo = parseOwnerRepo(normalizedSource);
2557
2602
  if (ownerRepo) {
@@ -2580,11 +2625,11 @@ async function runAdd(args, options = {}) {
2580
2625
  let skillFolderHash = "";
2581
2626
  const skillPathValue = skillFiles[skill.name];
2582
2627
  if (parsed.type === "github" && skillPathValue) {
2583
- const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue);
2628
+ const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken());
2584
2629
  if (hash) skillFolderHash = hash;
2585
2630
  }
2586
2631
  await addSkillToLock(skill.name, {
2587
- source: normalizedSource,
2632
+ source: lockSource || normalizedSource,
2588
2633
  sourceType: parsed.type,
2589
2634
  sourceUrl: parsed.url,
2590
2635
  skillPath: skillPathValue,
@@ -2601,7 +2646,7 @@ async function runAdd(args, options = {}) {
2601
2646
  if (successfulSkillNames.has(skillDisplayName)) try {
2602
2647
  const computedHash = await computeSkillFolderHash(skill.path);
2603
2648
  await addSkillToLocalLock(skill.name, {
2604
- source: normalizedSource || parsed.url,
2649
+ source: lockSource || parsed.url,
2605
2650
  sourceType: parsed.type,
2606
2651
  computedHash
2607
2652
  }, cwd);
@@ -2790,7 +2835,7 @@ async function searchSkillsAPI(query) {
2790
2835
  slug: skill.id,
2791
2836
  source: skill.source || "",
2792
2837
  installs: skill.installs
2793
- }));
2838
+ })).sort((a, b) => (b.installs || 0) - (a.installs || 0));
2794
2839
  } catch {
2795
2840
  return [];
2796
2841
  }
@@ -3346,6 +3391,7 @@ function parseListOptions(args) {
3346
3391
  for (let i = 0; i < args.length; i++) {
3347
3392
  const arg = args[i];
3348
3393
  if (arg === "-g" || arg === "--global") options.global = true;
3394
+ else if (arg === "--json") options.json = true;
3349
3395
  else if (arg === "-a" || arg === "--agent") {
3350
3396
  options.agent = options.agent || [];
3351
3397
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) options.agent.push(args[++i]);
@@ -3371,10 +3417,24 @@ async function runList(args) {
3371
3417
  global: scope,
3372
3418
  agentFilter
3373
3419
  });
3420
+ if (options.json) {
3421
+ const jsonOutput = installedSkills.map((skill) => ({
3422
+ name: skill.name,
3423
+ path: skill.canonicalPath,
3424
+ scope: skill.scope,
3425
+ agents: skill.agents.map((a) => agents[a].displayName)
3426
+ }));
3427
+ console.log(JSON.stringify(jsonOutput, null, 2));
3428
+ return;
3429
+ }
3374
3430
  const lockedSkills = await getAllLockedSkills();
3375
3431
  const cwd = process.cwd();
3376
3432
  const scopeLabel = scope ? "Global" : "Project";
3377
3433
  if (installedSkills.length === 0) {
3434
+ if (options.json) {
3435
+ console.log("[]");
3436
+ return;
3437
+ }
3378
3438
  console.log(`${DIM$1}No ${scopeLabel.toLowerCase()} skills found.${RESET$1}`);
3379
3439
  if (scope) console.log(`${DIM$1}Try listing project skills without -g${RESET$1}`);
3380
3440
  else console.log(`${DIM$1}Try listing global skills with -g${RESET$1}`);
@@ -3713,6 +3773,7 @@ ${BOLD}Experimental Sync Options:${RESET}
3713
3773
  ${BOLD}List Options:${RESET}
3714
3774
  -g, --global List global skills (default: project)
3715
3775
  -a, --agent <agents> Filter by specific agents
3776
+ --json Output as JSON (machine-readable, no ANSI codes)
3716
3777
 
3717
3778
  ${BOLD}Options:${RESET}
3718
3779
  --help, -h Show this help message
@@ -3729,6 +3790,7 @@ ${BOLD}Examples:${RESET}
3729
3790
  ${DIM}$${RESET} skills list ${DIM}# list project skills${RESET}
3730
3791
  ${DIM}$${RESET} skills ls -g ${DIM}# list global skills${RESET}
3731
3792
  ${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET}
3793
+ ${DIM}$${RESET} skills ls --json ${DIM}# JSON output${RESET}
3732
3794
  ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET}
3733
3795
  ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET}
3734
3796
  ${DIM}$${RESET} skills check
@@ -3822,6 +3884,8 @@ const AGENTS_DIR = ".agents";
3822
3884
  const LOCK_FILE = ".skill-lock.json";
3823
3885
  const CURRENT_LOCK_VERSION = 3;
3824
3886
  function getSkillLockPath() {
3887
+ const xdgStateHome = process.env.XDG_STATE_HOME;
3888
+ if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
3825
3889
  return join(homedir(), AGENTS_DIR, LOCK_FILE);
3826
3890
  }
3827
3891
  function readSkillLock() {
@@ -3845,6 +3909,22 @@ function readSkillLock() {
3845
3909
  };
3846
3910
  }
3847
3911
  }
3912
+ function getSkipReason(entry) {
3913
+ if (entry.sourceType === "local") return "Local path";
3914
+ if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
3915
+ if (!entry.skillFolderHash) return "No version hash available";
3916
+ if (!entry.skillPath) return "No skill path recorded";
3917
+ return "No version tracking";
3918
+ }
3919
+ function printSkippedSkills(skipped) {
3920
+ if (skipped.length === 0) return;
3921
+ console.log();
3922
+ console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
3923
+ for (const skill of skipped) {
3924
+ console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
3925
+ console.log(` ${DIM}To update: ${TEXT}npx skills add ${skill.sourceUrl} -g -y${RESET}`);
3926
+ }
3927
+ }
3848
3928
  async function runCheck(args = []) {
3849
3929
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
3850
3930
  console.log();
@@ -3857,12 +3937,16 @@ async function runCheck(args = []) {
3857
3937
  }
3858
3938
  const token = getGitHubToken();
3859
3939
  const skillsBySource = /* @__PURE__ */ new Map();
3860
- let skippedCount = 0;
3940
+ const skipped = [];
3861
3941
  for (const skillName of skillNames) {
3862
3942
  const entry = lock.skills[skillName];
3863
3943
  if (!entry) continue;
3864
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) {
3865
- skippedCount++;
3944
+ if (!entry.skillFolderHash || !entry.skillPath) {
3945
+ skipped.push({
3946
+ name: skillName,
3947
+ reason: getSkipReason(entry),
3948
+ sourceUrl: entry.sourceUrl
3949
+ });
3866
3950
  continue;
3867
3951
  }
3868
3952
  const existing = skillsBySource.get(entry.source) || [];
@@ -3872,9 +3956,10 @@ async function runCheck(args = []) {
3872
3956
  });
3873
3957
  skillsBySource.set(entry.source, existing);
3874
3958
  }
3875
- const totalSkills = skillNames.length - skippedCount;
3959
+ const totalSkills = skillNames.length - skipped.length;
3876
3960
  if (totalSkills === 0) {
3877
3961
  console.log(`${DIM}No GitHub skills to check.${RESET}`);
3962
+ printSkippedSkills(skipped);
3878
3963
  return;
3879
3964
  }
3880
3965
  console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
@@ -3917,6 +4002,7 @@ async function runCheck(args = []) {
3917
4002
  console.log();
3918
4003
  console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
3919
4004
  }
4005
+ printSkippedSkills(skipped);
3920
4006
  track({
3921
4007
  event: "check",
3922
4008
  skillCount: String(totalSkills),
@@ -3936,12 +4022,18 @@ async function runUpdate() {
3936
4022
  }
3937
4023
  const token = getGitHubToken();
3938
4024
  const updates = [];
3939
- let checkedCount = 0;
4025
+ const skipped = [];
3940
4026
  for (const skillName of skillNames) {
3941
4027
  const entry = lock.skills[skillName];
3942
4028
  if (!entry) continue;
3943
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) continue;
3944
- checkedCount++;
4029
+ if (!entry.skillFolderHash || !entry.skillPath) {
4030
+ skipped.push({
4031
+ name: skillName,
4032
+ reason: getSkipReason(entry),
4033
+ sourceUrl: entry.sourceUrl
4034
+ });
4035
+ continue;
4036
+ }
3945
4037
  try {
3946
4038
  const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
3947
4039
  if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
@@ -3951,8 +4043,9 @@ async function runUpdate() {
3951
4043
  });
3952
4044
  } catch {}
3953
4045
  }
3954
- if (checkedCount === 0) {
4046
+ if (skillNames.length - skipped.length === 0) {
3955
4047
  console.log(`${DIM}No skills to check.${RESET}`);
4048
+ printSkippedSkills(skipped);
3956
4049
  return;
3957
4050
  }
3958
4051
  if (updates.length === 0) {
@@ -3982,11 +4075,14 @@ async function runUpdate() {
3982
4075
  installUrl,
3983
4076
  "-g",
3984
4077
  "-y"
3985
- ], { stdio: [
3986
- "inherit",
3987
- "pipe",
3988
- "pipe"
3989
- ] }).status === 0) {
4078
+ ], {
4079
+ stdio: [
4080
+ "inherit",
4081
+ "pipe",
4082
+ "pipe"
4083
+ ],
4084
+ shell: process.platform === "win32"
4085
+ }).status === 0) {
3990
4086
  successCount++;
3991
4087
  console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
3992
4088
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,6 +71,7 @@
71
71
  "roo",
72
72
  "trae",
73
73
  "trae-cn",
74
+ "warp",
74
75
  "windsurf",
75
76
  "zencoder",
76
77
  "neovate",