skills 1.4.3 → 1.4.4

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/dist/cli.mjs +72 -18
  2. package/package.json +1 -1
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) => {
@@ -1751,7 +1773,7 @@ function createEmptyLocalLock() {
1751
1773
  skills: {}
1752
1774
  };
1753
1775
  }
1754
- var version$1 = "1.4.3";
1776
+ var version$1 = "1.4.4";
1755
1777
  const isCancelled$1 = (value) => typeof value === "symbol";
1756
1778
  async function isSourcePrivate(source) {
1757
1779
  const ownerRepo = parseOwnerRepo(source);
@@ -2580,7 +2602,7 @@ async function runAdd(args, options = {}) {
2580
2602
  let skillFolderHash = "";
2581
2603
  const skillPathValue = skillFiles[skill.name];
2582
2604
  if (parsed.type === "github" && skillPathValue) {
2583
- const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue);
2605
+ const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken());
2584
2606
  if (hash) skillFolderHash = hash;
2585
2607
  }
2586
2608
  await addSkillToLock(skill.name, {
@@ -3845,6 +3867,22 @@ function readSkillLock() {
3845
3867
  };
3846
3868
  }
3847
3869
  }
3870
+ function getSkipReason(entry) {
3871
+ if (entry.sourceType === "local") return "Local path";
3872
+ if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
3873
+ if (!entry.skillFolderHash) return "No version hash available";
3874
+ if (!entry.skillPath) return "No skill path recorded";
3875
+ return "No version tracking";
3876
+ }
3877
+ function printSkippedSkills(skipped) {
3878
+ if (skipped.length === 0) return;
3879
+ console.log();
3880
+ console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
3881
+ for (const skill of skipped) {
3882
+ console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
3883
+ console.log(` ${DIM}To update: ${TEXT}npx skills add ${skill.sourceUrl} -g -y${RESET}`);
3884
+ }
3885
+ }
3848
3886
  async function runCheck(args = []) {
3849
3887
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
3850
3888
  console.log();
@@ -3857,12 +3895,16 @@ async function runCheck(args = []) {
3857
3895
  }
3858
3896
  const token = getGitHubToken();
3859
3897
  const skillsBySource = /* @__PURE__ */ new Map();
3860
- let skippedCount = 0;
3898
+ const skipped = [];
3861
3899
  for (const skillName of skillNames) {
3862
3900
  const entry = lock.skills[skillName];
3863
3901
  if (!entry) continue;
3864
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) {
3865
- skippedCount++;
3902
+ if (!entry.skillFolderHash || !entry.skillPath) {
3903
+ skipped.push({
3904
+ name: skillName,
3905
+ reason: getSkipReason(entry),
3906
+ sourceUrl: entry.sourceUrl
3907
+ });
3866
3908
  continue;
3867
3909
  }
3868
3910
  const existing = skillsBySource.get(entry.source) || [];
@@ -3872,9 +3914,10 @@ async function runCheck(args = []) {
3872
3914
  });
3873
3915
  skillsBySource.set(entry.source, existing);
3874
3916
  }
3875
- const totalSkills = skillNames.length - skippedCount;
3917
+ const totalSkills = skillNames.length - skipped.length;
3876
3918
  if (totalSkills === 0) {
3877
3919
  console.log(`${DIM}No GitHub skills to check.${RESET}`);
3920
+ printSkippedSkills(skipped);
3878
3921
  return;
3879
3922
  }
3880
3923
  console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
@@ -3917,6 +3960,7 @@ async function runCheck(args = []) {
3917
3960
  console.log();
3918
3961
  console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
3919
3962
  }
3963
+ printSkippedSkills(skipped);
3920
3964
  track({
3921
3965
  event: "check",
3922
3966
  skillCount: String(totalSkills),
@@ -3936,12 +3980,18 @@ async function runUpdate() {
3936
3980
  }
3937
3981
  const token = getGitHubToken();
3938
3982
  const updates = [];
3939
- let checkedCount = 0;
3983
+ const skipped = [];
3940
3984
  for (const skillName of skillNames) {
3941
3985
  const entry = lock.skills[skillName];
3942
3986
  if (!entry) continue;
3943
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) continue;
3944
- checkedCount++;
3987
+ if (!entry.skillFolderHash || !entry.skillPath) {
3988
+ skipped.push({
3989
+ name: skillName,
3990
+ reason: getSkipReason(entry),
3991
+ sourceUrl: entry.sourceUrl
3992
+ });
3993
+ continue;
3994
+ }
3945
3995
  try {
3946
3996
  const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
3947
3997
  if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
@@ -3951,8 +4001,9 @@ async function runUpdate() {
3951
4001
  });
3952
4002
  } catch {}
3953
4003
  }
3954
- if (checkedCount === 0) {
4004
+ if (skillNames.length - skipped.length === 0) {
3955
4005
  console.log(`${DIM}No skills to check.${RESET}`);
4006
+ printSkippedSkills(skipped);
3956
4007
  return;
3957
4008
  }
3958
4009
  if (updates.length === 0) {
@@ -3982,11 +4033,14 @@ async function runUpdate() {
3982
4033
  installUrl,
3983
4034
  "-g",
3984
4035
  "-y"
3985
- ], { stdio: [
3986
- "inherit",
3987
- "pipe",
3988
- "pipe"
3989
- ] }).status === 0) {
4036
+ ], {
4037
+ stdio: [
4038
+ "inherit",
4039
+ "pipe",
4040
+ "pipe"
4041
+ ],
4042
+ shell: process.platform === "win32"
4043
+ }).status === 0) {
3990
4044
  successCount++;
3991
4045
  console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
3992
4046
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {