skills 1.5.23 → 1.5.24

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 +145 -47
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -685,6 +685,25 @@ const CLONE_TIMEOUT_MS = (() => {
685
685
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
686
686
  })();
687
687
  const execFileAsync = promisify(execFile);
688
+ function isCommitSha(ref) {
689
+ return /^[0-9a-f]{40}$/i.test(ref);
690
+ }
691
+ function isMissingRefError(message) {
692
+ return /Remote branch .* not found in upstream origin/i.test(message) || /couldn't find remote ref/i.test(message) || /upload-pack: not our ref/i.test(message);
693
+ }
694
+ async function cloneAtSha(url, sha, tempDir, extraEnv) {
695
+ const git = createGitClient(extraEnv);
696
+ await git.cwd(tempDir);
697
+ await git.init();
698
+ await git.addRemote("origin", url);
699
+ await git.fetch([
700
+ "--depth",
701
+ "1",
702
+ "origin",
703
+ sha
704
+ ]);
705
+ await git.checkout("FETCH_HEAD");
706
+ }
688
707
  var GitCloneError = class extends Error {
689
708
  url;
690
709
  isTimeout;
@@ -742,7 +761,7 @@ function isGitHubSsoAuthError(message) {
742
761
  function isAuthFailure(message) {
743
762
  return message.includes("Authentication failed") || message.includes("could not read Username") || message.includes("Permission denied") || message.includes("Repository not found") || message.includes("requested URL returned error: 403") || isGitHubSsoAuthError(message);
744
763
  }
745
- function createGitClient(sshCommand) {
764
+ function createGitClient(extraEnv) {
746
765
  const git = esm_default({
747
766
  timeout: { block: CLONE_TIMEOUT_MS },
748
767
  config: [
@@ -778,7 +797,7 @@ function createGitClient(sshCommand) {
778
797
  GIT_TERMINAL_PROMPT: "0",
779
798
  GIT_ALLOW_PROTOCOL: ALLOWED_GIT_PROTOCOLS,
780
799
  GIT_LFS_SKIP_SMUDGE: "1",
781
- ...sshCommand ? { GIT_SSH_COMMAND: sshCommand } : {}
800
+ ...extraEnv
782
801
  });
783
802
  return git;
784
803
  }
@@ -846,12 +865,18 @@ async function cloneRepo(url, ref) {
846
865
  "--branch",
847
866
  ref
848
867
  ] : ["--depth", "1"];
868
+ const refCanBeSha = !!ref && isCommitSha(ref);
849
869
  const repo = parseGitHubRepoUrl(url);
850
870
  try {
851
871
  await createGitClient().clone(url, tempDir, cloneOptions);
852
872
  return tempDir;
853
873
  } catch (error) {
854
874
  const errorMessage = error instanceof Error ? error.message : String(error);
875
+ if (refCanBeSha && isMissingRefError(errorMessage)) try {
876
+ await resetTempDir(tempDir);
877
+ await cloneAtSha(url, ref, tempDir);
878
+ return tempDir;
879
+ } catch {}
855
880
  const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
856
881
  const isAuthError = isAuthFailure(errorMessage);
857
882
  if (isTimeout) {
@@ -868,7 +893,16 @@ async function cloneRepo(url, ref) {
868
893
  } catch {}
869
894
  try {
870
895
  await resetTempDir(tempDir);
871
- await createGitClient(process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes").clone(repo.sshUrl, tempDir, cloneOptions);
896
+ const sshEnv = { GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes" };
897
+ try {
898
+ await createGitClient(sshEnv).clone(repo.sshUrl, tempDir, cloneOptions);
899
+ } catch (sshError) {
900
+ const sshMessage = sshError instanceof Error ? sshError.message : String(sshError);
901
+ if (refCanBeSha && isMissingRefError(sshMessage)) {
902
+ await resetTempDir(tempDir);
903
+ await cloneAtSha(repo.sshUrl, ref, tempDir, sshEnv);
904
+ } else throw sshError;
905
+ }
872
906
  return tempDir;
873
907
  } catch {}
874
908
  }
@@ -1127,7 +1161,7 @@ const AGENT_PROJECT_SKILL_DIRS = [
1127
1161
  ".zcode/skills",
1128
1162
  ".zencoder/skills"
1129
1163
  ];
1130
- function normalizeSkillName(name) {
1164
+ function normalizeSkillName$1(name) {
1131
1165
  return name.toLowerCase().replace(/[\s_]+/g, "-");
1132
1166
  }
1133
1167
  function normalizeRelativePath(path) {
@@ -1207,7 +1241,7 @@ async function discoverSkills(basePath, subpath, options) {
1207
1241
  const seenNames = /* @__PURE__ */ new Set();
1208
1242
  const parsedSkillPaths = /* @__PURE__ */ new Set();
1209
1243
  const localLock = await readLocalLock(basePath);
1210
- const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
1244
+ const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName$1));
1211
1245
  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.`);
1212
1246
  const searchPath = subpath ? join(basePath, subpath) : basePath;
1213
1247
  const pluginGroupings = await getPluginGroupings(searchPath);
@@ -1220,8 +1254,8 @@ async function discoverSkills(basePath, subpath, options) {
1220
1254
  if (lockedSkillNames.size === 0) return false;
1221
1255
  const relativeDir = normalizeRelativePath(relative(basePath, skill.path));
1222
1256
  if (!AGENT_PROJECT_SKILL_DIRS.some((dir) => relativeDir === dir || relativeDir.startsWith(`${dir}/`))) return false;
1223
- const skillName = normalizeSkillName(skill.name);
1224
- const directoryName = normalizeSkillName(basename(skill.path));
1257
+ const skillName = normalizeSkillName$1(skill.name);
1258
+ const directoryName = normalizeSkillName$1(basename(skill.path));
1225
1259
  return lockedSkillNames.has(skillName) || lockedSkillNames.has(directoryName);
1226
1260
  };
1227
1261
  const parseSkillAt = async (skillDir) => {
@@ -1254,7 +1288,7 @@ async function discoverSkills(basePath, subpath, options) {
1254
1288
  const tryAddSkillAt = async (skillDir) => {
1255
1289
  if (!await hasSkillMd(skillDir)) return false;
1256
1290
  let skill = await parseSkillAt(skillDir);
1257
- if (!skill || seenNames.has(skill.name)) return true;
1291
+ if (!skill || !options?.includeDuplicateNames && seenNames.has(skill.name)) return true;
1258
1292
  if (isInstalledProjectSkill(skill)) return true;
1259
1293
  skill = enhanceSkill(skill);
1260
1294
  skills.push(skill);
@@ -1277,7 +1311,7 @@ async function discoverSkills(basePath, subpath, options) {
1277
1311
  const allSkillDirs = await findSkillDirs(searchPath);
1278
1312
  for (const skillDir of allSkillDirs) {
1279
1313
  let skill = await parseSkillAt(skillDir);
1280
- if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
1314
+ if (skill && (options?.includeDuplicateNames || !seenNames.has(skill.name)) && !isInstalledProjectSkill(skill)) {
1281
1315
  skill = enhanceSkill(skill);
1282
1316
  skills.push(skill);
1283
1317
  seenNames.add(skill.name);
@@ -4017,7 +4051,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
4017
4051
  tree
4018
4052
  };
4019
4053
  }
4020
- var version$1 = "1.5.23";
4054
+ var version$1 = "1.5.24";
4021
4055
  const isCancelled$1 = (value) => typeof value === "symbol";
4022
4056
  const EVE_AGENT_LABEL = "eve agent";
4023
4057
  async function isSourcePrivate(source) {
@@ -6366,6 +6400,46 @@ function parseRemoveOptions(args) {
6366
6400
  options
6367
6401
  };
6368
6402
  }
6403
+ function normalizeSkillName(name) {
6404
+ return name.toLowerCase().replace(/[\s_]+/g, "-");
6405
+ }
6406
+ function normalizeSkillPath(path) {
6407
+ return path.replace(/\\/g, "/").replace(/\/+/g, "/");
6408
+ }
6409
+ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
6410
+ const discoveredPaths = new Set(discovered.map((skill) => normalizeSkillPath(skill.skillPath)));
6411
+ const pathsByName = /* @__PURE__ */ new Map();
6412
+ for (const skill of discovered) {
6413
+ const key = normalizeSkillName(skill.name);
6414
+ const paths = pathsByName.get(key) ?? /* @__PURE__ */ new Set();
6415
+ paths.add(normalizeSkillPath(skill.skillPath));
6416
+ pathsByName.set(key, paths);
6417
+ }
6418
+ const deletedSkills = [];
6419
+ const ambiguousSkills = [];
6420
+ const resolvedPaths = /* @__PURE__ */ new Map();
6421
+ for (const name of lockedSkillNames) {
6422
+ const lockedPath = lockSkills[name]?.skillPath;
6423
+ if (!lockedPath) continue;
6424
+ const normalizedLockedPath = normalizeSkillPath(lockedPath);
6425
+ const candidates = [...pathsByName.get(normalizeSkillName(name)) ?? []];
6426
+ if (candidates.length > 1) {
6427
+ ambiguousSkills.push(name);
6428
+ continue;
6429
+ }
6430
+ if (discoveredPaths.has(normalizedLockedPath)) {
6431
+ resolvedPaths.set(name, normalizedLockedPath);
6432
+ continue;
6433
+ }
6434
+ if (candidates.length === 1) resolvedPaths.set(name, candidates[0]);
6435
+ else deletedSkills.push(name);
6436
+ }
6437
+ return {
6438
+ deletedSkills,
6439
+ ambiguousSkills,
6440
+ resolvedPaths
6441
+ };
6442
+ }
6369
6443
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
6370
6444
  const RESET$1 = "\x1B[0m";
6371
6445
  const BOLD$1 = "\x1B[1m";
@@ -6512,14 +6586,15 @@ async function promptDeletions(source, deletedSkills, isGlobal, options) {
6512
6586
  });
6513
6587
  }
6514
6588
  }
6515
- async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discoveredPaths) {
6516
- const deletedSkills = allLockedForSource.filter((name) => {
6517
- const entry = lockSkills[name];
6518
- if (!entry?.skillPath) return false;
6519
- return !discoveredPaths.includes(entry.skillPath);
6520
- });
6521
- await promptDeletions(source, deletedSkills, isGlobal, options);
6522
- return deletedSkills;
6589
+ async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered) {
6590
+ const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered);
6591
+ if (resolution.ambiguousSkills.length > 0) {
6592
+ console.log();
6593
+ console.log(`${DIM$1}Warning:${RESET$1} Multiple current paths match these skills from ${DIM$1}${source}${RESET$1}; skipping them rather than deleting or migrating the wrong skill:`);
6594
+ for (const name of resolution.ambiguousSkills) console.log(` ${DIM$1}•${RESET$1} ${sanitizeMetadata(name)}`);
6595
+ }
6596
+ await promptDeletions(source, resolution.deletedSkills, isGlobal, options);
6597
+ return resolution;
6523
6598
  }
6524
6599
  async function checkWellKnownForUpdates(baseUrl, items) {
6525
6600
  let indexResult;
@@ -6703,36 +6778,42 @@ async function updateGlobalSkills(options = {}) {
6703
6778
  const tree = await fetchRepoTree(source, firstEntry.ref, getGitHubToken);
6704
6779
  if (tree) {
6705
6780
  const discoveredPaths = tree.tree.filter((entry) => entry.type === "blob").map((entry) => entry.path);
6706
- const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredPaths);
6707
- const deletedSkillSet = new Set(deletedSkills);
6708
- for (const { name: skillName, entry } of itemsForSource) {
6709
- if (deletedSkillSet.has(skillName)) continue;
6710
- const latestHash = getSkillFolderHashFromTree(tree, entry.skillPath);
6711
- if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
6712
- name: skillName,
6713
- source,
6714
- entry
6715
- });
6781
+ if (!Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name).some((name) => lock.skills[name]?.skillPath && !discoveredPaths.includes(lock.skills[name].skillPath))) {
6782
+ for (const { name: skillName, entry } of itemsForSource) {
6783
+ const latestHash = getSkillFolderHashFromTree(tree, entry.skillPath);
6784
+ if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
6785
+ name: skillName,
6786
+ source,
6787
+ entry
6788
+ });
6789
+ }
6790
+ continue;
6716
6791
  }
6717
- continue;
6718
- }
6719
- console.log(` ${DIM$1}GitHub API unavailable; checking via Git clone${RESET$1}`);
6792
+ console.log(` ${DIM$1}Skill paths changed; resolving via Git clone${RESET$1}`);
6793
+ } else console.log(` ${DIM$1}GitHub API unavailable; checking via Git clone${RESET$1}`);
6720
6794
  }
6721
6795
  tempDir = await cloneRepo(sourceUrl, firstEntry.ref);
6722
- const discoveredPaths = (await discoverSkills(tempDir, void 0, { fullDepth: true })).map((skill) => {
6723
- return join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/");
6724
- });
6725
- const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredPaths);
6726
- const deletedSkillSet = new Set(deletedSkills);
6796
+ const discoveredLocations = (await discoverSkills(tempDir, void 0, {
6797
+ fullDepth: true,
6798
+ includeDuplicateNames: true
6799
+ })).map((skill) => ({
6800
+ name: skill.name,
6801
+ skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
6802
+ }));
6803
+ const resolution = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredLocations);
6804
+ const deletedSkillSet = new Set(resolution.deletedSkills);
6727
6805
  for (const { name: skillName, entry } of itemsForSource) {
6728
6806
  if (deletedSkillSet.has(skillName)) continue;
6729
- const skillPath = entry.skillPath;
6730
- if (!discoveredPaths.includes(skillPath)) continue;
6807
+ const skillPath = resolution.resolvedPaths.get(skillName);
6808
+ if (!skillPath) continue;
6731
6809
  const latestHash = isGitHubSource && /^[0-9a-f]{40}$/i.test(entry.skillFolderHash) ? await getGitTreeHash(tempDir, skillPath) : await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
6732
- if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
6810
+ if (skillPath !== entry.skillPath || latestHash && latestHash !== entry.skillFolderHash) updates.push({
6733
6811
  name: skillName,
6734
6812
  source,
6735
- entry
6813
+ entry: {
6814
+ ...entry,
6815
+ skillPath
6816
+ }
6736
6817
  });
6737
6818
  }
6738
6819
  } catch (error) {
@@ -6910,6 +6991,7 @@ async function updateProjectSkills(options = {}) {
6910
6991
  const allLockedForSource = Object.entries(localLock.skills).filter(([_, entry]) => (entry.sourceUrl || entry.source) === source && entry.ref === ref).map(([name, _]) => name);
6911
6992
  let tempDir = null;
6912
6993
  let deletedSkills = [];
6994
+ let resolvedPaths = null;
6913
6995
  if (cloneSource === null) {
6914
6996
  failCount += skillsForSource.length;
6915
6997
  console.log(`${DIM$1}✗ Cannot update ${source}: skills-lock.json is missing sourceUrl for this generic Git source${RESET$1}`);
@@ -6917,27 +6999,43 @@ async function updateProjectSkills(options = {}) {
6917
6999
  }
6918
7000
  try {
6919
7001
  tempDir = await cloneRepo(cloneSource, ref);
6920
- const discoveredPaths = (await discoverSkills(tempDir, void 0, { fullDepth: true })).map((s) => {
6921
- return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
6922
- });
6923
- deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
7002
+ const discoveredLocations = (await discoverSkills(tempDir, void 0, {
7003
+ fullDepth: true,
7004
+ includeDuplicateNames: true
7005
+ })).map((skill) => ({
7006
+ name: skill.name,
7007
+ skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7008
+ }));
7009
+ const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations);
7010
+ deletedSkills = resolution.deletedSkills;
7011
+ resolvedPaths = resolution.resolvedPaths;
6924
7012
  } catch (error) {
6925
7013
  console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}${RESET$1}`);
6926
7014
  } finally {
6927
7015
  if (tempDir) await cleanupTempDir(tempDir);
6928
7016
  }
7017
+ if (resolvedPaths === null) {
7018
+ failCount += skillsForSource.length;
7019
+ continue;
7020
+ }
6929
7021
  const remainingSkills = skillsForSource.filter((s) => !deletedSkills.includes(s.name));
6930
7022
  for (const skill of remainingSkills) {
6931
7023
  const safeName = sanitizeMetadata(skill.name);
7024
+ const resolvedPath = resolvedPaths?.get(skill.name);
7025
+ if (resolvedPaths && !resolvedPath) continue;
7026
+ const entry = resolvedPath ? {
7027
+ ...skill.entry,
7028
+ skillPath: resolvedPath
7029
+ } : skill.entry;
6932
7030
  console.log(`${TEXT$1}Updating ${safeName}…${RESET$1}`);
6933
- const installUrl = buildLocalUpdateSource(skill.entry);
7031
+ const installUrl = buildLocalUpdateSource(entry);
6934
7032
  if (!installUrl) {
6935
7033
  failCount++;
6936
7034
  console.log(` ${DIM$1}✗ Cannot update ${safeName}: skills-lock.json is missing sourceUrl for this generic Git source${RESET$1}`);
6937
7035
  continue;
6938
7036
  }
6939
7037
  const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
6940
- const fullDepthArgs = shouldUseFullDepthForUpdate(skill.entry) ? ["--full-depth"] : [];
7038
+ const fullDepthArgs = shouldUseFullDepthForUpdate(entry) ? ["--full-depth"] : [];
6941
7039
  if (spawnSync(process.execPath, [
6942
7040
  cliEntry,
6943
7041
  "add",
@@ -6954,7 +7052,7 @@ async function updateProjectSkills(options = {}) {
6954
7052
  "pipe"
6955
7053
  ],
6956
7054
  encoding: "utf-8",
6957
- env: getUpdateChildEnv(skill.entry.sourceType),
7055
+ env: getUpdateChildEnv(entry.sourceType),
6958
7056
  shell: false
6959
7057
  }).status === 0) {
6960
7058
  successCount++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.5.23",
3
+ "version": "1.5.24",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {