add-skill 1.0.4 → 1.0.6

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/index.js CHANGED
@@ -290,6 +290,25 @@ var version = "1.0.0";
290
290
  program.name("add-skill").description("Install skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)").version(version).argument("<source>", "Git repo URL, GitHub shorthand (owner/repo), or direct path to skill").option("-g, --global", "Install skill globally (user-level) instead of project-level").option("-a, --agent <agents...>", "Specify agents to install to (opencode, claude-code, codex, cursor)").option("-s, --skill <skills...>", "Specify skill names to install (skip selection prompt)").option("-l, --list", "List available skills in the repository without installing").option("-y, --yes", "Skip confirmation prompts").action(async (source, options) => {
291
291
  await main(source, options);
292
292
  });
293
+ program.configureOutput({
294
+ outputError: (str, write) => {
295
+ if (str.includes("missing required argument 'source'")) {
296
+ console.log();
297
+ console.log(chalk.red("Error: Missing skill source"));
298
+ console.log();
299
+ console.log("Usage:");
300
+ console.log(` ${chalk.cyan("npx add-skill <source>")} ${chalk.dim("[options]")}`);
301
+ console.log();
302
+ console.log("Example:");
303
+ console.log(` ${chalk.dim("$")} npx add-skill vercel-labs/agent-skills`);
304
+ console.log();
305
+ console.log(`Run ${chalk.cyan("npx add-skill --help")} for more options.`);
306
+ console.log();
307
+ } else {
308
+ write(str);
309
+ }
310
+ }
311
+ });
293
312
  program.parse();
294
313
  async function main(source, options) {
295
314
  console.log();
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "add-skill",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "add-skill": "./dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
9
13
  "scripts": {
10
14
  "build": "tsup src/index.ts --format esm --dts --clean",
11
15
  "dev": "tsx src/index.ts",
@@ -20,6 +24,14 @@
20
24
  "cursor",
21
25
  "ai-agents"
22
26
  ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/vercel-labs/add-skill.git"
30
+ },
31
+ "homepage": "https://github.com/vercel-labs/add-skill#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/vercel-labs/add-skill/issues"
34
+ },
23
35
  "author": "",
24
36
  "license": "MIT",
25
37
  "dependencies": {
package/bun.lock DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "workspaces": {
4
- "": {
5
- "name": "add-skill",
6
- "devDependencies": {
7
- "@types/bun": "latest",
8
- },
9
- "peerDependencies": {
10
- "typescript": "^5",
11
- },
12
- },
13
- },
14
- "packages": {
15
- "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
16
-
17
- "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
18
-
19
- "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
20
-
21
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
22
-
23
- "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
24
- }
25
- }
package/src/agents.ts DELETED
@@ -1,61 +0,0 @@
1
- import { homedir } from 'os';
2
- import { join } from 'path';
3
- import { existsSync } from 'fs';
4
- import type { AgentConfig, AgentType } from './types.js';
5
-
6
- const home = homedir();
7
-
8
- export const agents: Record<AgentType, AgentConfig> = {
9
- opencode: {
10
- name: 'opencode',
11
- displayName: 'OpenCode',
12
- skillsDir: '.opencode/skill',
13
- globalSkillsDir: join(home, '.config/opencode/skill'),
14
- detectInstalled: async () => {
15
- return existsSync(join(home, '.config/opencode')) || existsSync(join(home, '.claude/skills'));
16
- },
17
- },
18
- 'claude-code': {
19
- name: 'claude-code',
20
- displayName: 'Claude Code',
21
- skillsDir: '.claude/skills',
22
- globalSkillsDir: join(home, '.claude/skills'),
23
- detectInstalled: async () => {
24
- return existsSync(join(home, '.claude'));
25
- },
26
- },
27
- codex: {
28
- name: 'codex',
29
- displayName: 'Codex',
30
- skillsDir: '.codex/skills',
31
- globalSkillsDir: join(home, '.codex/skills'),
32
- detectInstalled: async () => {
33
- return existsSync(join(home, '.codex'));
34
- },
35
- },
36
- cursor: {
37
- name: 'cursor',
38
- displayName: 'Cursor',
39
- skillsDir: '.cursor/skills',
40
- globalSkillsDir: join(home, '.cursor/skills'),
41
- detectInstalled: async () => {
42
- return existsSync(join(home, '.cursor'));
43
- },
44
- },
45
- };
46
-
47
- export async function detectInstalledAgents(): Promise<AgentType[]> {
48
- const installed: AgentType[] = [];
49
-
50
- for (const [type, config] of Object.entries(agents)) {
51
- if (await config.detectInstalled()) {
52
- installed.push(type as AgentType);
53
- }
54
- }
55
-
56
- return installed;
57
- }
58
-
59
- export function getAgentConfig(type: AgentType): AgentConfig {
60
- return agents[type];
61
- }
package/src/git.ts DELETED
@@ -1,83 +0,0 @@
1
- import simpleGit from 'simple-git';
2
- import { join } from 'path';
3
- import { mkdtemp, rm } from 'fs/promises';
4
- import { tmpdir } from 'os';
5
- import type { ParsedSource } from './types.js';
6
-
7
- export function parseSource(input: string): ParsedSource {
8
- // GitHub URL with path: https://github.com/owner/repo/tree/branch/path/to/skill
9
- const githubTreeMatch = input.match(
10
- /github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/
11
- );
12
- if (githubTreeMatch) {
13
- const [, owner, repo, , subpath] = githubTreeMatch;
14
- return {
15
- type: 'github',
16
- url: `https://github.com/${owner}/${repo}.git`,
17
- subpath,
18
- };
19
- }
20
-
21
- // GitHub URL: https://github.com/owner/repo
22
- const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
23
- if (githubRepoMatch) {
24
- const [, owner, repo] = githubRepoMatch;
25
- const cleanRepo = repo!.replace(/\.git$/, '');
26
- return {
27
- type: 'github',
28
- url: `https://github.com/${owner}/${cleanRepo}.git`,
29
- };
30
- }
31
-
32
- // GitLab URL with path: https://gitlab.com/owner/repo/-/tree/branch/path
33
- const gitlabTreeMatch = input.match(
34
- /gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/
35
- );
36
- if (gitlabTreeMatch) {
37
- const [, owner, repo, , subpath] = gitlabTreeMatch;
38
- return {
39
- type: 'gitlab',
40
- url: `https://gitlab.com/${owner}/${repo}.git`,
41
- subpath,
42
- };
43
- }
44
-
45
- // GitLab URL: https://gitlab.com/owner/repo
46
- const gitlabRepoMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
47
- if (gitlabRepoMatch) {
48
- const [, owner, repo] = gitlabRepoMatch;
49
- const cleanRepo = repo!.replace(/\.git$/, '');
50
- return {
51
- type: 'gitlab',
52
- url: `https://gitlab.com/${owner}/${cleanRepo}.git`,
53
- };
54
- }
55
-
56
- // GitHub shorthand: owner/repo or owner/repo/path/to/skill
57
- const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
58
- if (shorthandMatch && !input.includes(':')) {
59
- const [, owner, repo, subpath] = shorthandMatch;
60
- return {
61
- type: 'github',
62
- url: `https://github.com/${owner}/${repo}.git`,
63
- subpath,
64
- };
65
- }
66
-
67
- // Fallback: treat as direct git URL
68
- return {
69
- type: 'git',
70
- url: input,
71
- };
72
- }
73
-
74
- export async function cloneRepo(url: string): Promise<string> {
75
- const tempDir = await mkdtemp(join(tmpdir(), 'add-skill-'));
76
- const git = simpleGit();
77
- await git.clone(url, tempDir, ['--depth', '1']);
78
- return tempDir;
79
- }
80
-
81
- export async function cleanupTempDir(dir: string): Promise<void> {
82
- await rm(dir, { recursive: true, force: true });
83
- }
package/src/index.ts DELETED
@@ -1,309 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { program } from 'commander';
4
- import * as p from '@clack/prompts';
5
- import chalk from 'chalk';
6
- import { parseSource, cloneRepo, cleanupTempDir } from './git.js';
7
- import { discoverSkills, getSkillDisplayName } from './skills.js';
8
- import { installSkillForAgent, isSkillInstalled, getInstallPath } from './installer.js';
9
- import { detectInstalledAgents, agents } from './agents.js';
10
- import type { Skill, AgentType } from './types.js';
11
-
12
- const version = '1.0.0';
13
-
14
- interface Options {
15
- global?: boolean;
16
- agent?: string[];
17
- yes?: boolean;
18
- skill?: string[];
19
- list?: boolean;
20
- }
21
-
22
- program
23
- .name('add-skill')
24
- .description('Install skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)')
25
- .version(version)
26
- .argument('<source>', 'Git repo URL, GitHub shorthand (owner/repo), or direct path to skill')
27
- .option('-g, --global', 'Install skill globally (user-level) instead of project-level')
28
- .option('-a, --agent <agents...>', 'Specify agents to install to (opencode, claude-code, codex, cursor)')
29
- .option('-s, --skill <skills...>', 'Specify skill names to install (skip selection prompt)')
30
- .option('-l, --list', 'List available skills in the repository without installing')
31
- .option('-y, --yes', 'Skip confirmation prompts')
32
- .action(async (source: string, options: Options) => {
33
- await main(source, options);
34
- });
35
-
36
- program.parse();
37
-
38
- async function main(source: string, options: Options) {
39
- console.log();
40
- p.intro(chalk.bgCyan.black(' add-skill '));
41
-
42
- let tempDir: string | null = null;
43
-
44
- try {
45
- const spinner = p.spinner();
46
-
47
- spinner.start('Parsing source...');
48
- const parsed = parseSource(source);
49
- spinner.stop(`Source: ${chalk.cyan(parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ''}`);
50
-
51
- spinner.start('Cloning repository...');
52
- tempDir = await cloneRepo(parsed.url);
53
- spinner.stop('Repository cloned');
54
-
55
- spinner.start('Discovering skills...');
56
- const skills = await discoverSkills(tempDir, parsed.subpath);
57
-
58
- if (skills.length === 0) {
59
- spinner.stop(chalk.red('No skills found'));
60
- p.outro(chalk.red('No valid skills found. Skills require a SKILL.md with name and description.'));
61
- await cleanup(tempDir);
62
- process.exit(1);
63
- }
64
-
65
- spinner.stop(`Found ${chalk.green(skills.length)} skill${skills.length > 1 ? 's' : ''}`);
66
-
67
- if (options.list) {
68
- console.log();
69
- p.log.step(chalk.bold('Available Skills'));
70
- for (const skill of skills) {
71
- p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
72
- p.log.message(` ${chalk.dim(skill.description)}`);
73
- }
74
- console.log();
75
- p.outro('Use --skill <name> to install specific skills');
76
- await cleanup(tempDir);
77
- process.exit(0);
78
- }
79
-
80
- let selectedSkills: Skill[];
81
-
82
- if (options.skill && options.skill.length > 0) {
83
- selectedSkills = skills.filter(s =>
84
- options.skill!.some(name =>
85
- s.name.toLowerCase() === name.toLowerCase() ||
86
- getSkillDisplayName(s).toLowerCase() === name.toLowerCase()
87
- )
88
- );
89
-
90
- if (selectedSkills.length === 0) {
91
- p.log.error(`No matching skills found for: ${options.skill.join(', ')}`);
92
- p.log.info('Available skills:');
93
- for (const s of skills) {
94
- p.log.message(` - ${getSkillDisplayName(s)}`);
95
- }
96
- await cleanup(tempDir);
97
- process.exit(1);
98
- }
99
-
100
- p.log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? 's' : ''}: ${selectedSkills.map(s => chalk.cyan(getSkillDisplayName(s))).join(', ')}`);
101
- } else if (skills.length === 1) {
102
- selectedSkills = skills;
103
- const firstSkill = skills[0]!;
104
- p.log.info(`Skill: ${chalk.cyan(getSkillDisplayName(firstSkill))}`);
105
- p.log.message(chalk.dim(firstSkill.description));
106
- } else if (options.yes) {
107
- selectedSkills = skills;
108
- p.log.info(`Installing all ${skills.length} skills`);
109
- } else {
110
- const skillChoices = skills.map(s => ({
111
- value: s,
112
- label: getSkillDisplayName(s),
113
- hint: s.description.length > 60 ? s.description.slice(0, 57) + '...' : s.description,
114
- }));
115
-
116
- const selected = await p.multiselect({
117
- message: 'Select skills to install',
118
- options: skillChoices,
119
- required: true,
120
- });
121
-
122
- if (p.isCancel(selected)) {
123
- p.cancel('Installation cancelled');
124
- await cleanup(tempDir);
125
- process.exit(0);
126
- }
127
-
128
- selectedSkills = selected as Skill[];
129
- }
130
-
131
- let targetAgents: AgentType[];
132
-
133
- if (options.agent && options.agent.length > 0) {
134
- const validAgents = ['opencode', 'claude-code', 'codex', 'cursor'];
135
- const invalidAgents = options.agent.filter(a => !validAgents.includes(a));
136
-
137
- if (invalidAgents.length > 0) {
138
- p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
139
- p.log.info(`Valid agents: ${validAgents.join(', ')}`);
140
- await cleanup(tempDir);
141
- process.exit(1);
142
- }
143
-
144
- targetAgents = options.agent as AgentType[];
145
- } else {
146
- spinner.start('Detecting installed agents...');
147
- const installedAgents = await detectInstalledAgents();
148
- spinner.stop(`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? 's' : ''}`);
149
-
150
- if (installedAgents.length === 0) {
151
- if (options.yes) {
152
- targetAgents = ['opencode', 'claude-code', 'codex', 'cursor'];
153
- p.log.info('Installing to all agents (none detected)');
154
- } else {
155
- p.log.warn('No coding agents detected. You can still install skills.');
156
-
157
- const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
158
- value: key as AgentType,
159
- label: config.displayName,
160
- }));
161
-
162
- const selected = await p.multiselect({
163
- message: 'Select agents to install skills to',
164
- options: allAgentChoices,
165
- required: true,
166
- });
167
-
168
- if (p.isCancel(selected)) {
169
- p.cancel('Installation cancelled');
170
- await cleanup(tempDir);
171
- process.exit(0);
172
- }
173
-
174
- targetAgents = selected as AgentType[];
175
- }
176
- } else if (installedAgents.length === 1 || options.yes) {
177
- targetAgents = installedAgents;
178
- if (installedAgents.length === 1) {
179
- const firstAgent = installedAgents[0]!;
180
- p.log.info(`Installing to: ${chalk.cyan(agents[firstAgent].displayName)}`);
181
- } else {
182
- p.log.info(`Installing to: ${installedAgents.map(a => chalk.cyan(agents[a].displayName)).join(', ')}`);
183
- }
184
- } else {
185
- const agentChoices = installedAgents.map(a => ({
186
- value: a,
187
- label: agents[a].displayName,
188
- hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`,
189
- }));
190
-
191
- const selected = await p.multiselect({
192
- message: 'Select agents to install skills to',
193
- options: agentChoices,
194
- required: true,
195
- initialValues: installedAgents,
196
- });
197
-
198
- if (p.isCancel(selected)) {
199
- p.cancel('Installation cancelled');
200
- await cleanup(tempDir);
201
- process.exit(0);
202
- }
203
-
204
- targetAgents = selected as AgentType[];
205
- }
206
- }
207
-
208
- let installGlobally = options.global ?? false;
209
-
210
- if (options.global === undefined && !options.yes) {
211
- const scope = await p.select({
212
- message: 'Installation scope',
213
- options: [
214
- { value: false, label: 'Project', hint: 'Install in current directory (committed with your project)' },
215
- { value: true, label: 'Global', hint: 'Install in home directory (available across all projects)' },
216
- ],
217
- });
218
-
219
- if (p.isCancel(scope)) {
220
- p.cancel('Installation cancelled');
221
- await cleanup(tempDir);
222
- process.exit(0);
223
- }
224
-
225
- installGlobally = scope as boolean;
226
- }
227
-
228
- console.log();
229
- p.log.step(chalk.bold('Installation Summary'));
230
-
231
- for (const skill of selectedSkills) {
232
- p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
233
- for (const agent of targetAgents) {
234
- const path = getInstallPath(skill.name, agent, { global: installGlobally });
235
- const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
236
- const status = installed ? chalk.yellow(' (will overwrite)') : '';
237
- p.log.message(` ${chalk.dim('→')} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`);
238
- }
239
- }
240
- console.log();
241
-
242
- if (!options.yes) {
243
- const confirmed = await p.confirm({ message: 'Proceed with installation?' });
244
-
245
- if (p.isCancel(confirmed) || !confirmed) {
246
- p.cancel('Installation cancelled');
247
- await cleanup(tempDir);
248
- process.exit(0);
249
- }
250
- }
251
-
252
- spinner.start('Installing skills...');
253
-
254
- const results: { skill: string; agent: string; success: boolean; path: string; error?: string }[] = [];
255
-
256
- for (const skill of selectedSkills) {
257
- for (const agent of targetAgents) {
258
- const result = await installSkillForAgent(skill, agent, { global: installGlobally });
259
- results.push({
260
- skill: getSkillDisplayName(skill),
261
- agent: agents[agent].displayName,
262
- ...result,
263
- });
264
- }
265
- }
266
-
267
- spinner.stop('Installation complete');
268
-
269
- console.log();
270
- const successful = results.filter(r => r.success);
271
- const failed = results.filter(r => !r.success);
272
-
273
- if (successful.length > 0) {
274
- p.log.success(chalk.green(`Successfully installed ${successful.length} skill${successful.length !== 1 ? 's' : ''}`));
275
- for (const r of successful) {
276
- p.log.message(` ${chalk.green('✓')} ${r.skill} → ${r.agent}`);
277
- p.log.message(` ${chalk.dim(r.path)}`);
278
- }
279
- }
280
-
281
- if (failed.length > 0) {
282
- console.log();
283
- p.log.error(chalk.red(`Failed to install ${failed.length} skill${failed.length !== 1 ? 's' : ''}`));
284
- for (const r of failed) {
285
- p.log.message(` ${chalk.red('✗')} ${r.skill} → ${r.agent}`);
286
- p.log.message(` ${chalk.dim(r.error)}`);
287
- }
288
- }
289
-
290
- console.log();
291
- p.outro(chalk.green('Done!'));
292
- } catch (error) {
293
- p.log.error(error instanceof Error ? error.message : 'Unknown error occurred');
294
- p.outro(chalk.red('Installation failed'));
295
- process.exit(1);
296
- } finally {
297
- await cleanup(tempDir);
298
- }
299
- }
300
-
301
- async function cleanup(tempDir: string | null) {
302
- if (tempDir) {
303
- try {
304
- await cleanupTempDir(tempDir);
305
- } catch {
306
- // Ignore cleanup errors
307
- }
308
- }
309
- }
package/src/installer.ts DELETED
@@ -1,90 +0,0 @@
1
- import { mkdir, cp, access, readdir } from 'fs/promises';
2
- import { join, basename } from 'path';
3
- import type { Skill, AgentType } from './types.js';
4
- import { agents } from './agents.js';
5
-
6
- interface InstallResult {
7
- success: boolean;
8
- path: string;
9
- error?: string;
10
- }
11
-
12
- export async function installSkillForAgent(
13
- skill: Skill,
14
- agentType: AgentType,
15
- options: { global?: boolean; cwd?: string } = {}
16
- ): Promise<InstallResult> {
17
- const agent = agents[agentType];
18
- const skillName = skill.name || basename(skill.path);
19
-
20
- const targetBase = options.global
21
- ? agent.globalSkillsDir
22
- : join(options.cwd || process.cwd(), agent.skillsDir);
23
-
24
- const targetDir = join(targetBase, skillName);
25
-
26
- try {
27
- await mkdir(targetDir, { recursive: true });
28
- await copyDirectory(skill.path, targetDir);
29
-
30
- return { success: true, path: targetDir };
31
- } catch (error) {
32
- return {
33
- success: false,
34
- path: targetDir,
35
- error: error instanceof Error ? error.message : 'Unknown error',
36
- };
37
- }
38
- }
39
-
40
- async function copyDirectory(src: string, dest: string): Promise<void> {
41
- await mkdir(dest, { recursive: true });
42
-
43
- const entries = await readdir(src, { withFileTypes: true });
44
-
45
- for (const entry of entries) {
46
- const srcPath = join(src, entry.name);
47
- const destPath = join(dest, entry.name);
48
-
49
- if (entry.isDirectory()) {
50
- await copyDirectory(srcPath, destPath);
51
- } else {
52
- await cp(srcPath, destPath);
53
- }
54
- }
55
- }
56
-
57
- export async function isSkillInstalled(
58
- skillName: string,
59
- agentType: AgentType,
60
- options: { global?: boolean; cwd?: string } = {}
61
- ): Promise<boolean> {
62
- const agent = agents[agentType];
63
-
64
- const targetBase = options.global
65
- ? agent.globalSkillsDir
66
- : join(options.cwd || process.cwd(), agent.skillsDir);
67
-
68
- const skillDir = join(targetBase, skillName);
69
-
70
- try {
71
- await access(skillDir);
72
- return true;
73
- } catch {
74
- return false;
75
- }
76
- }
77
-
78
- export function getInstallPath(
79
- skillName: string,
80
- agentType: AgentType,
81
- options: { global?: boolean; cwd?: string } = {}
82
- ): string {
83
- const agent = agents[agentType];
84
-
85
- const targetBase = options.global
86
- ? agent.globalSkillsDir
87
- : join(options.cwd || process.cwd(), agent.skillsDir);
88
-
89
- return join(targetBase, skillName);
90
- }
package/src/skills.ts DELETED
@@ -1,129 +0,0 @@
1
- import { readdir, readFile, stat } from 'fs/promises';
2
- import { join, basename, dirname } from 'path';
3
- import matter from 'gray-matter';
4
- import type { Skill } from './types.js';
5
-
6
- const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__'];
7
-
8
- async function hasSkillMd(dir: string): Promise<boolean> {
9
- try {
10
- const skillPath = join(dir, 'SKILL.md');
11
- const stats = await stat(skillPath);
12
- return stats.isFile();
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- async function parseSkillMd(skillMdPath: string): Promise<Skill | null> {
19
- try {
20
- const content = await readFile(skillMdPath, 'utf-8');
21
- const { data } = matter(content);
22
-
23
- if (!data.name || !data.description) {
24
- return null;
25
- }
26
-
27
- return {
28
- name: data.name,
29
- description: data.description,
30
- path: dirname(skillMdPath),
31
- metadata: data.metadata,
32
- };
33
- } catch {
34
- return null;
35
- }
36
- }
37
-
38
- async function findSkillDirs(dir: string, depth = 0, maxDepth = 5): Promise<string[]> {
39
- const skillDirs: string[] = [];
40
-
41
- if (depth > maxDepth) return skillDirs;
42
-
43
- try {
44
- if (await hasSkillMd(dir)) {
45
- skillDirs.push(dir);
46
- }
47
-
48
- const entries = await readdir(dir, { withFileTypes: true });
49
-
50
- for (const entry of entries) {
51
- if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
52
- const subDirs = await findSkillDirs(join(dir, entry.name), depth + 1, maxDepth);
53
- skillDirs.push(...subDirs);
54
- }
55
- }
56
- } catch {
57
- // Ignore errors
58
- }
59
-
60
- return skillDirs;
61
- }
62
-
63
- export async function discoverSkills(basePath: string, subpath?: string): Promise<Skill[]> {
64
- const skills: Skill[] = [];
65
- const seenNames = new Set<string>();
66
- const searchPath = subpath ? join(basePath, subpath) : basePath;
67
-
68
- // If pointing directly at a skill, return just that
69
- if (await hasSkillMd(searchPath)) {
70
- const skill = await parseSkillMd(join(searchPath, 'SKILL.md'));
71
- if (skill) {
72
- skills.push(skill);
73
- return skills;
74
- }
75
- }
76
-
77
- // Search common skill locations first
78
- const prioritySearchDirs = [
79
- searchPath,
80
- join(searchPath, 'skills'),
81
- join(searchPath, 'skills/.curated'),
82
- join(searchPath, 'skills/.experimental'),
83
- join(searchPath, 'skills/.system'),
84
- join(searchPath, '.codex/skills'),
85
- join(searchPath, '.claude/skills'),
86
- join(searchPath, '.opencode/skill'),
87
- join(searchPath, '.cursor/skills'),
88
- ];
89
-
90
- for (const dir of prioritySearchDirs) {
91
- try {
92
- const entries = await readdir(dir, { withFileTypes: true });
93
-
94
- for (const entry of entries) {
95
- if (entry.isDirectory()) {
96
- const skillDir = join(dir, entry.name);
97
- if (await hasSkillMd(skillDir)) {
98
- const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
99
- if (skill && !seenNames.has(skill.name)) {
100
- skills.push(skill);
101
- seenNames.add(skill.name);
102
- }
103
- }
104
- }
105
- }
106
- } catch {
107
- // Directory doesn't exist
108
- }
109
- }
110
-
111
- // Fall back to recursive search if nothing found
112
- if (skills.length === 0) {
113
- const allSkillDirs = await findSkillDirs(searchPath);
114
-
115
- for (const skillDir of allSkillDirs) {
116
- const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
117
- if (skill && !seenNames.has(skill.name)) {
118
- skills.push(skill);
119
- seenNames.add(skill.name);
120
- }
121
- }
122
- }
123
-
124
- return skills;
125
- }
126
-
127
- export function getSkillDisplayName(skill: Skill): string {
128
- return skill.name || basename(skill.path);
129
- }
package/src/types.ts DELETED
@@ -1,22 +0,0 @@
1
- export type AgentType = 'opencode' | 'claude-code' | 'codex' | 'cursor';
2
-
3
- export interface Skill {
4
- name: string;
5
- description: string;
6
- path: string;
7
- metadata?: Record<string, string>;
8
- }
9
-
10
- export interface AgentConfig {
11
- name: string;
12
- displayName: string;
13
- skillsDir: string;
14
- globalSkillsDir: string;
15
- detectInstalled: () => Promise<boolean>;
16
- }
17
-
18
- export interface ParsedSource {
19
- type: 'github' | 'gitlab' | 'git';
20
- url: string;
21
- subpath?: string;
22
- }
package/tsconfig.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "lib": ["ESNext"],
4
- "target": "ESNext",
5
- "module": "ESNext",
6
- "moduleDetection": "force",
7
- "allowJs": true,
8
- "moduleResolution": "bundler",
9
- "allowImportingTsExtensions": true,
10
- "noEmit": true,
11
- "strict": true,
12
- "skipLibCheck": true,
13
- "noFallthroughCasesInSwitch": true,
14
- "noUncheckedIndexedAccess": true,
15
- "noUnusedLocals": false,
16
- "noUnusedParameters": false,
17
- "noPropertyAccessFromIndexSignature": false,
18
- "esModuleInterop": true
19
- },
20
- "include": ["src/**/*"],
21
- "exclude": ["node_modules", "dist"]
22
- }