skills 1.4.6 → 1.4.7

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 +5 -1
  2. package/dist/cli.mjs +116 -37
  3. package/package.json +3 -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 [39 more](#available-agents).
6
+ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [41 more](#available-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
9
  ## Install a Skill
@@ -212,6 +212,7 @@ Skills can be installed to any of these agents:
212
212
  | Amp, Kimi Code CLI, Replit, Universal | `amp`, `kimi-cli`, `replit`, `universal` | `.agents/skills/` | `~/.config/agents/skills/` |
213
213
  | Antigravity | `antigravity` | `.agents/skills/` | `~/.gemini/antigravity/skills/` |
214
214
  | Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
215
+ | IBM Bob | `bob` | `.bob/skills/` | `~/.bob/skills/` |
215
216
  | Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
216
217
  | OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
217
218
  | Cline, Warp | `cline`, `warp` | `.agents/skills/` | `~/.agents/skills/` |
@@ -224,6 +225,7 @@ Skills can be installed to any of these agents:
224
225
  | Cursor | `cursor` | `.agents/skills/` | `~/.cursor/skills/` |
225
226
  | Deep Agents | `deepagents` | `.agents/skills/` | `~/.deepagents/agent/skills/` |
226
227
  | Droid | `droid` | `.factory/skills/` | `~/.factory/skills/` |
228
+ | Firebender | `firebender` | `.agents/skills/` | `~/.firebender/skills/` |
227
229
  | Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
228
230
  | GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
229
231
  | Goose | `goose` | `.goose/skills/` | `~/.config/goose/skills/` |
@@ -319,6 +321,7 @@ The CLI searches for skills in these locations within a repository:
319
321
  - `skills/.system/`
320
322
  - `.agents/skills/`
321
323
  - `.augment/skills/`
324
+ - `.bob/skills/`
322
325
  - `.claude/skills/`
323
326
  - `./skills/`
324
327
  - `.codebuddy/skills/`
@@ -433,6 +436,7 @@ Telemetry is automatically disabled in CI environments.
433
436
  - [Command Code Skills Documentation](https://commandcode.ai/docs/skills)
434
437
  - [Crush Skills Documentation](https://github.com/charmbracelet/crush?tab=readme-ov-file#agent-skills)
435
438
  - [Cursor Skills Documentation](https://cursor.com/docs/context/skills)
439
+ - [Firebender Skills Documentation](https://docs.firebender.com/multi-agent/skills)
436
440
  - [Gemini CLI Skills Documentation](https://geminicli.com/docs/cli/skills/)
437
441
  - [GitHub Copilot Agent Skills](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
438
442
  - [iFlow CLI Skills Documentation](https://platform.iflow.cn/en/cli/examples/skill)
package/dist/cli.mjs CHANGED
@@ -62,13 +62,48 @@ function isLocalPath(input) {
62
62
  return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
63
63
  }
64
64
  const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
65
+ function decodeFragmentValue(value) {
66
+ try {
67
+ return decodeURIComponent(value);
68
+ } catch {
69
+ return value;
70
+ }
71
+ }
72
+ function looksLikeGitSource(input) {
73
+ if (input.startsWith("github:") || input.startsWith("gitlab:") || input.startsWith("git@")) return true;
74
+ if (input.startsWith("http://") || input.startsWith("https://")) try {
75
+ const parsed = new URL(input);
76
+ const pathname = parsed.pathname;
77
+ if (parsed.hostname === "github.com") return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
78
+ if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
79
+ } catch {}
80
+ if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true;
81
+ return !input.includes(":") && !input.startsWith(".") && !input.startsWith("/") && /^([^/]+)\/([^/]+)(?:\/(.+)|@(.+))?$/.test(input);
82
+ }
83
+ function parseFragmentRef(input) {
84
+ const hashIndex = input.indexOf("#");
85
+ if (hashIndex < 0) return { inputWithoutFragment: input };
86
+ const inputWithoutFragment = input.slice(0, hashIndex);
87
+ const fragment = input.slice(hashIndex + 1);
88
+ if (!fragment || !looksLikeGitSource(inputWithoutFragment)) return { inputWithoutFragment: input };
89
+ const atIndex = fragment.indexOf("@");
90
+ if (atIndex === -1) return {
91
+ inputWithoutFragment,
92
+ ref: decodeFragmentValue(fragment)
93
+ };
94
+ const ref = fragment.slice(0, atIndex);
95
+ const skillFilter = fragment.slice(atIndex + 1);
96
+ return {
97
+ inputWithoutFragment,
98
+ ref: ref ? decodeFragmentValue(ref) : void 0,
99
+ skillFilter: skillFilter ? decodeFragmentValue(skillFilter) : void 0
100
+ };
101
+ }
102
+ function appendFragmentRef(input, ref, skillFilter) {
103
+ if (!ref) return input;
104
+ return `${input}#${ref}${skillFilter ? `@${skillFilter}` : ""}`;
105
+ }
65
106
  function parseSource(input) {
66
- const alias = SOURCE_ALIASES[input];
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]}`);
72
107
  if (isLocalPath(input)) {
73
108
  const resolvedPath = resolve(input);
74
109
  return {
@@ -77,13 +112,21 @@ function parseSource(input) {
77
112
  localPath: resolvedPath
78
113
  };
79
114
  }
115
+ const { inputWithoutFragment, ref: fragmentRef, skillFilter: fragmentSkillFilter } = parseFragmentRef(input);
116
+ input = inputWithoutFragment;
117
+ const alias = SOURCE_ALIASES[input];
118
+ if (alias) input = alias;
119
+ const githubPrefixMatch = input.match(/^github:(.+)$/);
120
+ if (githubPrefixMatch) return parseSource(appendFragmentRef(githubPrefixMatch[1], fragmentRef, fragmentSkillFilter));
121
+ const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
122
+ if (gitlabPrefixMatch) return parseSource(appendFragmentRef(`https://gitlab.com/${gitlabPrefixMatch[1]}`, fragmentRef, fragmentSkillFilter));
80
123
  const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
81
124
  if (githubTreeWithPathMatch) {
82
125
  const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
83
126
  return {
84
127
  type: "github",
85
128
  url: `https://github.com/${owner}/${repo}.git`,
86
- ref,
129
+ ref: ref || fragmentRef,
87
130
  subpath: subpath ? sanitizeSubpath(subpath) : subpath
88
131
  };
89
132
  }
@@ -93,7 +136,7 @@ function parseSource(input) {
93
136
  return {
94
137
  type: "github",
95
138
  url: `https://github.com/${owner}/${repo}.git`,
96
- ref
139
+ ref: ref || fragmentRef
97
140
  };
98
141
  }
99
142
  const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
@@ -101,7 +144,8 @@ function parseSource(input) {
101
144
  const [, owner, repo] = githubRepoMatch;
102
145
  return {
103
146
  type: "github",
104
- url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`
147
+ url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`,
148
+ ...fragmentRef ? { ref: fragmentRef } : {}
105
149
  };
106
150
  }
107
151
  const gitlabTreeWithPathMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)\/(.+)/);
@@ -110,7 +154,7 @@ function parseSource(input) {
110
154
  if (hostname !== "github.com" && repoPath) return {
111
155
  type: "gitlab",
112
156
  url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
113
- ref,
157
+ ref: ref || fragmentRef,
114
158
  subpath: subpath ? sanitizeSubpath(subpath) : subpath
115
159
  };
116
160
  }
@@ -120,7 +164,7 @@ function parseSource(input) {
120
164
  if (hostname !== "github.com" && repoPath) return {
121
165
  type: "gitlab",
122
166
  url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
123
- ref
167
+ ref: ref || fragmentRef
124
168
  };
125
169
  }
126
170
  const gitlabRepoMatch = input.match(/gitlab\.com\/(.+?)(?:\.git)?\/?$/);
@@ -128,7 +172,8 @@ function parseSource(input) {
128
172
  const repoPath = gitlabRepoMatch[1];
129
173
  if (repoPath.includes("/")) return {
130
174
  type: "gitlab",
131
- url: `https://gitlab.com/${repoPath}.git`
175
+ url: `https://gitlab.com/${repoPath}.git`,
176
+ ...fragmentRef ? { ref: fragmentRef } : {}
132
177
  };
133
178
  }
134
179
  const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
@@ -137,16 +182,19 @@ function parseSource(input) {
137
182
  return {
138
183
  type: "github",
139
184
  url: `https://github.com/${owner}/${repo}.git`,
140
- skillFilter
185
+ ...fragmentRef ? { ref: fragmentRef } : {},
186
+ skillFilter: fragmentSkillFilter || skillFilter
141
187
  };
142
188
  }
143
- const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
189
+ const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+?))?\/?$/);
144
190
  if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
145
191
  const [, owner, repo, subpath] = shorthandMatch;
146
192
  return {
147
193
  type: "github",
148
194
  url: `https://github.com/${owner}/${repo}.git`,
149
- subpath: subpath ? sanitizeSubpath(subpath) : subpath
195
+ ...fragmentRef ? { ref: fragmentRef } : {},
196
+ subpath: subpath ? sanitizeSubpath(subpath) : subpath,
197
+ ...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
150
198
  };
151
199
  }
152
200
  if (isWellKnownUrl(input)) return {
@@ -155,7 +203,8 @@ function parseSource(input) {
155
203
  };
156
204
  return {
157
205
  type: "git",
158
- url: input
206
+ url: input,
207
+ ...fragmentRef ? { ref: fragmentRef } : {}
159
208
  };
160
209
  }
161
210
  function isWellKnownUrl(input) {
@@ -634,6 +683,15 @@ const agents = {
634
683
  return existsSync(join(home, ".augment"));
635
684
  }
636
685
  },
686
+ bob: {
687
+ name: "bob",
688
+ displayName: "IBM Bob",
689
+ skillsDir: ".bob/skills",
690
+ globalSkillsDir: join(home, ".bob/skills"),
691
+ detectInstalled: async () => {
692
+ return existsSync(join(home, ".bob"));
693
+ }
694
+ },
637
695
  "claude-code": {
638
696
  name: "claude-code",
639
697
  displayName: "Claude Code",
@@ -742,6 +800,15 @@ const agents = {
742
800
  return existsSync(join(home, ".factory"));
743
801
  }
744
802
  },
803
+ firebender: {
804
+ name: "firebender",
805
+ displayName: "Firebender",
806
+ skillsDir: ".agents/skills",
807
+ globalSkillsDir: join(home, ".firebender/skills"),
808
+ detectInstalled: async () => {
809
+ return existsSync(join(home, ".firebender"));
810
+ }
811
+ },
745
812
  "gemini-cli": {
746
813
  name: "gemini-cli",
747
814
  displayName: "Gemini CLI",
@@ -1685,13 +1752,14 @@ function getGitHubToken() {
1685
1752
  } catch {}
1686
1753
  return null;
1687
1754
  }
1688
- async function fetchSkillFolderHash(ownerRepo, skillPath, token) {
1755
+ async function fetchSkillFolderHash(ownerRepo, skillPath, token, ref) {
1689
1756
  let folderPath = skillPath.replace(/\\/g, "/");
1690
1757
  if (folderPath.endsWith("/SKILL.md")) folderPath = folderPath.slice(0, -9);
1691
1758
  else if (folderPath.endsWith("SKILL.md")) folderPath = folderPath.slice(0, -8);
1692
1759
  if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1);
1693
- for (const branch of ["main", "master"]) try {
1694
- const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
1760
+ const branches = ref ? [ref] : ["main", "master"];
1761
+ for (const branch of branches) try {
1762
+ const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
1695
1763
  const headers = {
1696
1764
  Accept: "application/vnd.github.v3+json",
1697
1765
  "User-Agent": "skills-cli"
@@ -1822,7 +1890,7 @@ function createEmptyLocalLock() {
1822
1890
  skills: {}
1823
1891
  };
1824
1892
  }
1825
- var version$1 = "1.4.6";
1893
+ var version$1 = "1.4.7";
1826
1894
  const isCancelled$1 = (value) => typeof value === "symbol";
1827
1895
  async function isSourcePrivate(source) {
1828
1896
  const ownerRepo = parseOwnerRepo(source);
@@ -2654,13 +2722,14 @@ async function runAdd(args, options = {}) {
2654
2722
  let skillFolderHash = "";
2655
2723
  const skillPathValue = skillFiles[skill.name];
2656
2724
  if (parsed.type === "github" && skillPathValue) {
2657
- const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken());
2725
+ const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken(), parsed.ref);
2658
2726
  if (hash) skillFolderHash = hash;
2659
2727
  }
2660
2728
  await addSkillToLock(skill.name, {
2661
2729
  source: lockSource || normalizedSource,
2662
2730
  sourceType: parsed.type,
2663
2731
  sourceUrl: parsed.url,
2732
+ ref: parsed.ref,
2664
2733
  skillPath: skillPathValue,
2665
2734
  skillFolderHash,
2666
2735
  pluginName: skill.pluginName
@@ -2676,6 +2745,7 @@ async function runAdd(args, options = {}) {
2676
2745
  const computedHash = await computeSkillFolderHash(skill.path);
2677
2746
  await addSkillToLocalLock(skill.name, {
2678
2747
  source: lockSource || parsed.url,
2748
+ ref: parsed.ref,
2679
2749
  sourceType: parsed.type,
2680
2750
  computedHash
2681
2751
  }, cwd);
@@ -3366,9 +3436,10 @@ async function runInstallFromLock(args) {
3366
3436
  nodeModuleSkills.push(skillName);
3367
3437
  continue;
3368
3438
  }
3369
- const existing = bySource.get(entry.source);
3439
+ const installSource = entry.ref ? `${entry.source}#${entry.ref}` : entry.source;
3440
+ const existing = bySource.get(installSource);
3370
3441
  if (existing) existing.skills.push(skillName);
3371
- else bySource.set(entry.source, {
3442
+ else bySource.set(installSource, {
3372
3443
  sourceType: entry.sourceType,
3373
3444
  skills: [skillName]
3374
3445
  });
@@ -3698,6 +3769,20 @@ function parseRemoveOptions(args) {
3698
3769
  options
3699
3770
  };
3700
3771
  }
3772
+ function formatSourceInput(sourceUrl, ref) {
3773
+ if (!ref) return sourceUrl;
3774
+ return `${sourceUrl}#${ref}`;
3775
+ }
3776
+ function buildUpdateInstallSource(entry) {
3777
+ if (!entry.skillPath) return formatSourceInput(entry.sourceUrl, entry.ref);
3778
+ let skillFolder = entry.skillPath;
3779
+ if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
3780
+ else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
3781
+ if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
3782
+ let installSource = skillFolder ? `${entry.source}/${skillFolder}` : entry.source;
3783
+ if (entry.ref) installSource = `${installSource}#${entry.ref}`;
3784
+ return installSource;
3785
+ }
3701
3786
  const __dirname = dirname(fileURLToPath(import.meta.url));
3702
3787
  function getVersion() {
3703
3788
  try {
@@ -3951,7 +4036,7 @@ function printSkippedSkills(skipped) {
3951
4036
  console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
3952
4037
  for (const skill of skipped) {
3953
4038
  console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
3954
- console.log(` ${DIM}To update: ${TEXT}npx skills add ${skill.sourceUrl} -g -y${RESET}`);
4039
+ console.log(` ${DIM}To update: ${TEXT}npx skills add ${formatSourceInput(skill.sourceUrl, skill.ref)} -g -y${RESET}`);
3955
4040
  }
3956
4041
  }
3957
4042
  async function runCheck(args = []) {
@@ -3974,7 +4059,8 @@ async function runCheck(args = []) {
3974
4059
  skipped.push({
3975
4060
  name: skillName,
3976
4061
  reason: getSkipReason(entry),
3977
- sourceUrl: entry.sourceUrl
4062
+ sourceUrl: entry.sourceUrl,
4063
+ ref: entry.ref
3978
4064
  });
3979
4065
  continue;
3980
4066
  }
@@ -3995,7 +4081,7 @@ async function runCheck(args = []) {
3995
4081
  const updates = [];
3996
4082
  const errors = [];
3997
4083
  for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
3998
- const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
4084
+ const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token, entry.ref);
3999
4085
  if (!latestHash) {
4000
4086
  errors.push({
4001
4087
  name,
@@ -4064,12 +4150,13 @@ async function runUpdate() {
4064
4150
  skipped.push({
4065
4151
  name: skillName,
4066
4152
  reason: getSkipReason(entry),
4067
- sourceUrl: entry.sourceUrl
4153
+ sourceUrl: entry.sourceUrl,
4154
+ ref: entry.ref
4068
4155
  });
4069
4156
  continue;
4070
4157
  }
4071
4158
  try {
4072
- const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
4159
+ const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token, entry.ref);
4073
4160
  if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
4074
4161
  name: skillName,
4075
4162
  source: entry.source,
@@ -4093,15 +4180,7 @@ async function runUpdate() {
4093
4180
  let failCount = 0;
4094
4181
  for (const update of updates) {
4095
4182
  console.log(`${TEXT}Updating ${update.name}...${RESET}`);
4096
- let installUrl = update.entry.sourceUrl;
4097
- if (update.entry.skillPath) {
4098
- let skillFolder = update.entry.skillPath;
4099
- if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
4100
- else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
4101
- if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
4102
- installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
4103
- installUrl = `${installUrl}/tree/main/${skillFolder}`;
4104
- }
4183
+ const installUrl = buildUpdateInstallSource(update.entry);
4105
4184
  const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
4106
4185
  if (!existsSync(cliEntry)) {
4107
4186
  failCount++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,6 +39,7 @@
39
39
  "amp",
40
40
  "antigravity",
41
41
  "augment",
42
+ "bob",
42
43
  "claude-code",
43
44
  "openclaw",
44
45
  "cline",
@@ -51,6 +52,7 @@
51
52
  "cursor",
52
53
  "deepagents",
53
54
  "droid",
55
+ "firebender",
54
56
  "gemini-cli",
55
57
  "github-copilot",
56
58
  "goose",