@biggora/claude-plugins 1.0.0 → 1.1.0

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 (80) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/CLAUDE.md +55 -0
  3. package/LICENSE +1 -1
  4. package/README.md +208 -39
  5. package/bin/cli.js +39 -0
  6. package/package.json +30 -17
  7. package/registry/registry.json +166 -1
  8. package/registry/schema.json +10 -0
  9. package/src/commands/skills/add.js +194 -0
  10. package/src/commands/skills/list.js +52 -0
  11. package/src/commands/skills/remove.js +27 -0
  12. package/src/commands/skills/update.js +74 -0
  13. package/src/config.js +5 -0
  14. package/src/skills/codex-cli/SKILL.md +265 -0
  15. package/src/skills/commafeed-api/SKILL.md +1012 -0
  16. package/src/skills/gemini-cli/SKILL.md +379 -0
  17. package/src/skills/gemini-cli/references/commands.md +145 -0
  18. package/src/skills/gemini-cli/references/configuration.md +182 -0
  19. package/src/skills/gemini-cli/references/headless-and-scripting.md +181 -0
  20. package/src/skills/gemini-cli/references/mcp-and-extensions.md +254 -0
  21. package/src/skills/n8n-api/SKILL.md +623 -0
  22. package/src/skills/notebook-lm/SKILL.md +217 -0
  23. package/src/skills/notebook-lm/references/artifact-options.md +168 -0
  24. package/src/skills/notebook-lm/references/auth.md +58 -0
  25. package/src/skills/notebook-lm/references/workflows.md +144 -0
  26. package/src/skills/screen-recording/SKILL.md +309 -0
  27. package/src/skills/screen-recording/references/approach1-programmatic.md +311 -0
  28. package/src/skills/screen-recording/references/approach2-xvfb.md +232 -0
  29. package/src/skills/screen-recording/references/design-patterns.md +168 -0
  30. package/src/skills/test-mobile-app/SKILL.md +212 -0
  31. package/src/skills/test-mobile-app/references/report-template.md +95 -0
  32. package/src/skills/test-mobile-app/references/setup-appium.md +154 -0
  33. package/src/skills/test-mobile-app/scripts/analyze_apk.py +164 -0
  34. package/src/skills/test-mobile-app/scripts/check_environment.py +116 -0
  35. package/src/skills/test-mobile-app/scripts/generate_report.py +250 -0
  36. package/src/skills/test-mobile-app/scripts/run_tests.py +326 -0
  37. package/src/skills/test-web-ui/SKILL.md +232 -0
  38. package/src/skills/test-web-ui/references/test_case_schema.md +102 -0
  39. package/src/skills/test-web-ui/scripts/discover.py +176 -0
  40. package/src/skills/test-web-ui/scripts/generate_report.py +237 -0
  41. package/src/skills/test-web-ui/scripts/run_tests.py +296 -0
  42. package/src/skills/text-to-speech/SKILL.md +236 -0
  43. package/src/skills/text-to-speech/references/espeak-cli.md +277 -0
  44. package/src/skills/text-to-speech/references/kokoro-onnx.md +124 -0
  45. package/src/skills/text-to-speech/references/online-engines.md +128 -0
  46. package/src/skills/text-to-speech/references/pyttsx3-espeak.md +143 -0
  47. package/src/skills/tm-search/SKILL.md +240 -0
  48. package/src/skills/tm-search/references/field-guide.md +79 -0
  49. package/src/skills/tm-search/references/scraping-fallback.md +140 -0
  50. package/src/skills/tm-search/scripts/tm_search.py +375 -0
  51. package/src/skills/wp-rest-api/SKILL.md +114 -0
  52. package/src/skills/wp-rest-api/references/authentication.md +18 -0
  53. package/src/skills/wp-rest-api/references/custom-content-types.md +20 -0
  54. package/src/skills/wp-rest-api/references/discovery-and-params.md +20 -0
  55. package/src/skills/wp-rest-api/references/responses-and-fields.md +30 -0
  56. package/src/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
  57. package/src/skills/wp-rest-api/references/schema.md +22 -0
  58. package/src/skills/youtube-search/SKILL.md +412 -0
  59. package/src/skills/youtube-search/references/parsing-examples.md +159 -0
  60. package/src/skills/youtube-search/references/youtube-api-quota.md +85 -0
  61. package/src/skills/youtube-thumbnail/SKILL.md +1060 -0
  62. package/tests/commands/info.test.js +49 -0
  63. package/tests/commands/install.test.js +36 -0
  64. package/tests/commands/list.test.js +66 -0
  65. package/tests/commands/publish.test.js +182 -0
  66. package/tests/commands/search.test.js +45 -0
  67. package/tests/commands/uninstall.test.js +29 -0
  68. package/tests/commands/update.test.js +59 -0
  69. package/tests/functional/skills-lifecycle.test.js +293 -0
  70. package/tests/helpers/fixtures.js +63 -0
  71. package/tests/integration/cli.test.js +83 -0
  72. package/tests/skills/add.test.js +138 -0
  73. package/tests/skills/list.test.js +63 -0
  74. package/tests/skills/remove.test.js +38 -0
  75. package/tests/skills/update.test.js +60 -0
  76. package/tests/unit/config.test.js +31 -0
  77. package/tests/unit/registry.test.js +79 -0
  78. package/tests/unit/utils.test.js +150 -0
  79. package/tests/validation/registry-schema.test.js +112 -0
  80. package/tests/validation/skills-validation.test.js +96 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./schema.json",
3
3
  "version": "1.0.0",
4
- "updated": "2026-02-08",
4
+ "updated": "2026-03-06",
5
5
  "plugins": [
6
6
  {
7
7
  "name": "code-optimizer",
@@ -44,6 +44,171 @@
44
44
  "license": "MIT",
45
45
  "commands": ["/fix-typescript-eslint"],
46
46
  "category": "code-quality"
47
+ },
48
+ {
49
+ "name": "screen-recording",
50
+ "version": "1.0.0",
51
+ "description": "Autonomous video creation skill — product demos, presentation videos, UI walkthroughs, and narrated screencasts via Pillow, MoviePy, and pyttsx3",
52
+ "author": {
53
+ "name": "biggora",
54
+ "url": "https://github.com/biggora"
55
+ },
56
+ "repository": "https://github.com/biggora/screen-recording",
57
+ "keywords": ["video", "screencast", "demo", "recording", "narration", "moviepy"],
58
+ "license": "MIT",
59
+ "commands": [],
60
+ "category": "other",
61
+ "type": "skill"
62
+ },
63
+ {
64
+ "name": "text-to-speech",
65
+ "version": "1.0.0",
66
+ "description": "Converts text to speech audio files autonomously using pyttsx3, espeak-ng, Kokoro ONNX, or cloud TTS engines with multilingual support",
67
+ "author": {
68
+ "name": "biggora",
69
+ "url": "https://github.com/biggora"
70
+ },
71
+ "repository": "https://github.com/biggora/text-to-speech",
72
+ "keywords": ["tts", "audio", "narration", "speech", "espeak", "voiceover"],
73
+ "license": "MIT",
74
+ "commands": [],
75
+ "category": "other",
76
+ "type": "skill"
77
+ },
78
+ {
79
+ "name": "youtube-thumbnail",
80
+ "version": "1.0.0",
81
+ "description": "Generates professional YouTube thumbnails in 11 strategic styles with auto-detection of AI image backends and Pillow compositing",
82
+ "author": {
83
+ "name": "biggora",
84
+ "url": "https://github.com/biggora"
85
+ },
86
+ "repository": "https://github.com/biggora/youtube-thumbnail",
87
+ "keywords": ["youtube", "thumbnail", "image", "design", "pillow", "ai-image"],
88
+ "license": "MIT",
89
+ "commands": [],
90
+ "category": "other",
91
+ "type": "skill"
92
+ },
93
+ {
94
+ "name": "youtube-search",
95
+ "version": "1.0.0",
96
+ "description": "Search YouTube for videos, channels, playlists and extract metadata, transcripts, and analytics via multiple API methods",
97
+ "author": {
98
+ "name": "biggora",
99
+ "url": "https://github.com/biggora"
100
+ },
101
+ "repository": "https://github.com/biggora/youtube-search",
102
+ "keywords": ["youtube", "search", "video", "metadata", "transcript", "analytics"],
103
+ "license": "MIT",
104
+ "commands": [],
105
+ "category": "workflow",
106
+ "type": "skill"
107
+ },
108
+ {
109
+ "name": "wp-rest-api",
110
+ "version": "1.0.0",
111
+ "description": "Build, extend, and debug WordPress REST API endpoints — routes, controllers, schema validation, authentication, and custom fields",
112
+ "author": {
113
+ "name": "biggora",
114
+ "url": "https://github.com/biggora"
115
+ },
116
+ "repository": "https://github.com/biggora/wp-rest-api",
117
+ "keywords": ["wordpress", "rest-api", "php", "endpoints", "wp-cli", "custom-post-types"],
118
+ "license": "MIT",
119
+ "commands": [],
120
+ "category": "workflow",
121
+ "type": "skill"
122
+ },
123
+ {
124
+ "name": "notebook-lm",
125
+ "version": "1.0.0",
126
+ "description": "Automate Google NotebookLM — create notebooks, add sources, chat, generate audio/video overviews, quizzes, reports, and mind maps",
127
+ "author": {
128
+ "name": "biggora",
129
+ "url": "https://github.com/biggora"
130
+ },
131
+ "repository": "https://github.com/biggora/notebook-lm",
132
+ "keywords": ["notebooklm", "google", "research", "podcast", "audio", "automation"],
133
+ "license": "MIT",
134
+ "commands": [],
135
+ "category": "workflow",
136
+ "type": "skill"
137
+ },
138
+ {
139
+ "name": "codex-cli",
140
+ "version": "1.0.0",
141
+ "description": "Install, configure, and automate tasks using OpenAI Codex CLI — approval modes, sandbox policies, MCP servers, and CI integration",
142
+ "author": {
143
+ "name": "biggora",
144
+ "url": "https://github.com/biggora"
145
+ },
146
+ "repository": "https://github.com/biggora/codex-cli",
147
+ "keywords": ["openai", "codex", "cli", "automation", "ci-cd", "agent"],
148
+ "license": "MIT",
149
+ "commands": [],
150
+ "category": "devops",
151
+ "type": "skill"
152
+ },
153
+ {
154
+ "name": "gemini-cli",
155
+ "version": "1.0.0",
156
+ "description": "Install, configure, and script with Gemini CLI — headless prompts, MCP servers, custom slash commands, extensions, and CI automation",
157
+ "author": {
158
+ "name": "biggora",
159
+ "url": "https://github.com/biggora"
160
+ },
161
+ "repository": "https://github.com/biggora/gemini-cli",
162
+ "keywords": ["gemini", "google", "cli", "automation", "mcp", "scripting"],
163
+ "license": "MIT",
164
+ "commands": [],
165
+ "category": "devops",
166
+ "type": "skill"
167
+ },
168
+ {
169
+ "name": "tm-search",
170
+ "version": "1.0.0",
171
+ "description": "Search, validate, and check availability of US trademarks via USPTO APIs — keyword search, batch validation, and status lookup",
172
+ "author": {
173
+ "name": "biggora",
174
+ "url": "https://github.com/biggora"
175
+ },
176
+ "repository": "https://github.com/biggora/tm-search",
177
+ "keywords": ["trademark", "uspto", "brand", "search", "validation", "legal"],
178
+ "license": "MIT",
179
+ "commands": [],
180
+ "category": "workflow",
181
+ "type": "skill"
182
+ },
183
+ {
184
+ "name": "test-web-ui",
185
+ "version": "1.0.0",
186
+ "description": "Automated web QA — discovers site structure, generates use cases and test plans, runs Playwright tests, and produces HTML reports",
187
+ "author": {
188
+ "name": "biggora",
189
+ "url": "https://github.com/biggora"
190
+ },
191
+ "repository": "https://github.com/biggora/test-web-ui",
192
+ "keywords": ["qa", "playwright", "web-testing", "automation", "test-report", "e2e"],
193
+ "license": "MIT",
194
+ "commands": [],
195
+ "category": "testing",
196
+ "type": "skill"
197
+ },
198
+ {
199
+ "name": "test-mobile-app",
200
+ "version": "1.0.0",
201
+ "description": "Automated mobile app testing — analyzes APK/source, generates use cases, runs Appium tests on emulator, and produces QA reports",
202
+ "author": {
203
+ "name": "biggora",
204
+ "url": "https://github.com/biggora"
205
+ },
206
+ "repository": "https://github.com/biggora/test-mobile-app",
207
+ "keywords": ["mobile", "appium", "android", "qa", "testing", "automation"],
208
+ "license": "MIT",
209
+ "commands": [],
210
+ "category": "testing",
211
+ "type": "skill"
47
212
  }
48
213
  ]
49
214
  }
@@ -41,6 +41,16 @@
41
41
  "category": {
42
42
  "type": "string",
43
43
  "enum": ["code-quality", "workflow", "testing", "documentation", "security", "devops", "other"]
44
+ },
45
+ "type": {
46
+ "type": "string",
47
+ "enum": ["plugin", "skill"],
48
+ "default": "plugin",
49
+ "description": "Whether this entry is a full plugin or a standalone skill"
50
+ },
51
+ "skillsDir": {
52
+ "type": "string",
53
+ "description": "Subdirectory path for multi-skill repos"
44
54
  }
45
55
  }
46
56
  }
@@ -0,0 +1,194 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, readdirSync, cpSync, rmSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
3
+ import { join, basename } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { getSkillsDir } from '../../config.js';
6
+ import { fetchRegistry, findPlugin } from '../../registry.js';
7
+ import { log, spinner } from '../../utils.js';
8
+
9
+ export function parseFrontmatter(content) {
10
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
11
+ if (!match) return {};
12
+ const meta = {};
13
+ for (const line of match[1].split(/\r?\n/)) {
14
+ const idx = line.indexOf(':');
15
+ if (idx > 0) {
16
+ const key = line.slice(0, idx).trim();
17
+ const val = line.slice(idx + 1).trim();
18
+ meta[key] = val;
19
+ }
20
+ }
21
+ return meta;
22
+ }
23
+
24
+ export function findSkillDirs(dir, depth = 0, maxDepth = 3) {
25
+ const results = [];
26
+ if (depth > maxDepth) return results;
27
+
28
+ if (existsSync(join(dir, 'SKILL.md'))) {
29
+ results.push(dir);
30
+ }
31
+
32
+ let entries;
33
+ try {
34
+ entries = readdirSync(dir, { withFileTypes: true });
35
+ } catch {
36
+ return results;
37
+ }
38
+
39
+ for (const entry of entries) {
40
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
41
+ results.push(...findSkillDirs(join(dir, entry.name), depth + 1, maxDepth));
42
+ }
43
+
44
+ return results;
45
+ }
46
+
47
+ function isUrl(str) {
48
+ return str.startsWith('http://') || str.startsWith('https://') || str.startsWith('git@');
49
+ }
50
+
51
+ function makeTempDir() {
52
+ const dir = join(tmpdir(), `claude-skill-${Date.now()}`);
53
+ mkdirSync(dir, { recursive: true });
54
+ return dir;
55
+ }
56
+
57
+ export async function add(source, options = {}) {
58
+ let repoUrl = source;
59
+
60
+ // If not a URL, look up in registry
61
+ if (!isUrl(source)) {
62
+ const registry = await fetchRegistry();
63
+ const entry = findPlugin(registry, source);
64
+ if (!entry) {
65
+ log.error(`Skill "${source}" not found in registry`);
66
+ log.dim('Run "claude-plugins search <query>" to find plugins and skills');
67
+ process.exit(1);
68
+ }
69
+ repoUrl = entry.repository;
70
+ log.info(`Resolved "${source}" to ${repoUrl}`);
71
+ }
72
+
73
+ const tmpDir = makeTempDir();
74
+ const spin = spinner(`Cloning ${repoUrl}...`);
75
+ spin.start();
76
+
77
+ try {
78
+ const gitUrl = repoUrl.endsWith('.git') ? repoUrl : `${repoUrl}.git`;
79
+ execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpDir], {
80
+ stdio: 'pipe',
81
+ });
82
+ spin.succeed('Repository cloned');
83
+ } catch (err) {
84
+ spin.fail('Failed to clone repository');
85
+ log.error(err.message);
86
+ rmSync(tmpDir, { recursive: true, force: true });
87
+ process.exit(1);
88
+ }
89
+
90
+ // Find all skill directories
91
+ const skillDirs = findSkillDirs(tmpDir);
92
+
93
+ if (!skillDirs.length) {
94
+ log.error('No SKILL.md found in repository');
95
+ rmSync(tmpDir, { recursive: true, force: true });
96
+ process.exit(1);
97
+ }
98
+
99
+ let targetDir;
100
+
101
+ if (options.skill) {
102
+ // Find the skill matching --skill name
103
+ targetDir = skillDirs.find((d) => {
104
+ const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
105
+ return (fm.name || basename(d)).toLowerCase() === options.skill.toLowerCase();
106
+ });
107
+
108
+ if (!targetDir) {
109
+ // Also try matching by directory name
110
+ targetDir = skillDirs.find((d) => basename(d).toLowerCase() === options.skill.toLowerCase());
111
+ }
112
+
113
+ if (!targetDir) {
114
+ log.error(`Skill "${options.skill}" not found in repository`);
115
+ log.info('Available skills:');
116
+ for (const d of skillDirs) {
117
+ const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
118
+ log.dim(` - ${fm.name || basename(d)}`);
119
+ }
120
+ rmSync(tmpDir, { recursive: true, force: true });
121
+ process.exit(1);
122
+ }
123
+ } else if (skillDirs.length === 1) {
124
+ targetDir = skillDirs[0];
125
+ } else {
126
+ // Check if root has SKILL.md
127
+ if (existsSync(join(tmpDir, 'SKILL.md'))) {
128
+ targetDir = tmpDir;
129
+ } else {
130
+ log.warn('Multiple skills found. Use --skill <name> to select one:');
131
+ for (const d of skillDirs) {
132
+ const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
133
+ log.dim(` - ${fm.name || basename(d)}`);
134
+ }
135
+ rmSync(tmpDir, { recursive: true, force: true });
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ // Parse skill metadata
141
+ const skillMd = readFileSync(join(targetDir, 'SKILL.md'), 'utf-8');
142
+ const frontmatter = parseFrontmatter(skillMd);
143
+ const skillName = frontmatter.name || options.skill || basename(targetDir === tmpDir ? repoUrl.replace(/\.git$/, '').split('/').pop() : targetDir);
144
+
145
+ const skillsDir = getSkillsDir();
146
+ const dest = join(skillsDir, skillName);
147
+
148
+ if (existsSync(dest)) {
149
+ log.warn(`Skill "${skillName}" is already installed at ${dest}`);
150
+ log.dim(`Run "claude-plugins skills update ${skillName}" to update it`);
151
+ rmSync(tmpDir, { recursive: true, force: true });
152
+ return;
153
+ }
154
+
155
+ // Copy skill files
156
+ const spin2 = spinner(`Installing skill "${skillName}"...`);
157
+ spin2.start();
158
+
159
+ try {
160
+ cpSync(targetDir, dest, { recursive: true });
161
+
162
+ // Remove .git directory if copied from root
163
+ const gitInDest = join(dest, '.git');
164
+ if (existsSync(gitInDest)) {
165
+ rmSync(gitInDest, { recursive: true, force: true });
166
+ }
167
+
168
+ // Write origin metadata
169
+ writeFileSync(
170
+ join(dest, '.origin.json'),
171
+ JSON.stringify(
172
+ {
173
+ repository: repoUrl,
174
+ skill: options.skill || null,
175
+ installedAt: new Date().toISOString(),
176
+ },
177
+ null,
178
+ 2
179
+ )
180
+ );
181
+
182
+ spin2.succeed(`Installed skill "${skillName}"`);
183
+ log.dim(` ${dest}`);
184
+ if (frontmatter.description) {
185
+ log.dim(` ${frontmatter.description}`);
186
+ }
187
+ log.dim('\nRestart Claude Code to load the skill.');
188
+ } catch (err) {
189
+ spin2.fail(`Failed to install skill "${skillName}"`);
190
+ log.error(err.message);
191
+ } finally {
192
+ rmSync(tmpDir, { recursive: true, force: true });
193
+ }
194
+ }
@@ -0,0 +1,52 @@
1
+ import { readdirSync, existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { getSkillsDir } from '../../config.js';
5
+ import { log, formatTable, truncate } from '../../utils.js';
6
+ import { parseFrontmatter } from './add.js';
7
+
8
+ export async function list() {
9
+ const skillsDir = getSkillsDir();
10
+ const entries = readdirSync(skillsDir, { withFileTypes: true }).filter(
11
+ (e) => e.isDirectory()
12
+ );
13
+
14
+ if (!entries.length) {
15
+ log.info('No skills installed');
16
+ log.dim('Run "claude-plugins skills add <source>" to install a skill');
17
+ return;
18
+ }
19
+
20
+ console.log(chalk.bold(`\n ${entries.length} skill${entries.length === 1 ? '' : 's'} installed\n`));
21
+
22
+ const rows = entries.map((entry) => {
23
+ const dir = join(skillsDir, entry.name);
24
+ const skillMdPath = join(dir, 'SKILL.md');
25
+ const originPath = join(dir, '.origin.json');
26
+ let description = '';
27
+ let repo = '-';
28
+
29
+ if (existsSync(skillMdPath)) {
30
+ try {
31
+ const fm = parseFrontmatter(readFileSync(skillMdPath, 'utf-8'));
32
+ description = fm.description || '';
33
+ } catch {
34
+ // ignore
35
+ }
36
+ }
37
+
38
+ if (existsSync(originPath)) {
39
+ try {
40
+ const origin = JSON.parse(readFileSync(originPath, 'utf-8'));
41
+ repo = origin.repository || '-';
42
+ } catch {
43
+ // ignore
44
+ }
45
+ }
46
+
47
+ return [entry.name, truncate(description, 50), truncate(repo, 40)];
48
+ });
49
+
50
+ formatTable(rows, ['Name', 'Description', 'Repository']);
51
+ console.log();
52
+ }
@@ -0,0 +1,27 @@
1
+ import { existsSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getSkillsDir } from '../../config.js';
4
+ import { log, spinner } from '../../utils.js';
5
+
6
+ export async function remove(name) {
7
+ const dest = join(getSkillsDir(), name);
8
+
9
+ if (!existsSync(dest)) {
10
+ log.error(`Skill "${name}" is not installed`);
11
+ log.dim('Run "claude-plugins skills list" to see installed skills');
12
+ process.exit(1);
13
+ }
14
+
15
+ const spin = spinner(`Removing skill "${name}"...`);
16
+ spin.start();
17
+
18
+ try {
19
+ rmSync(dest, { recursive: true, force: true });
20
+ spin.succeed(`Removed skill "${name}"`);
21
+ log.dim('Restart Claude Code to apply changes.');
22
+ } catch (err) {
23
+ spin.fail(`Failed to remove skill "${name}"`);
24
+ log.error(err.message);
25
+ process.exit(1);
26
+ }
27
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getSkillsDir } from '../../config.js';
4
+ import { log, spinner } from '../../utils.js';
5
+ import { add } from './add.js';
6
+ import { remove } from './remove.js';
7
+
8
+ function readOrigin(skillDir) {
9
+ const originPath = join(skillDir, '.origin.json');
10
+ if (!existsSync(originPath)) return null;
11
+ try {
12
+ return JSON.parse(readFileSync(originPath, 'utf-8'));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ async function updateOne(name) {
19
+ const skillsDir = getSkillsDir();
20
+ const dest = join(skillsDir, name);
21
+
22
+ if (!existsSync(dest)) {
23
+ log.error(`Skill "${name}" is not installed`);
24
+ return false;
25
+ }
26
+
27
+ const origin = readOrigin(dest);
28
+ if (!origin || !origin.repository) {
29
+ log.warn(`Skill "${name}" has no origin metadata, skipping`);
30
+ return false;
31
+ }
32
+
33
+ const spin = spinner(`Updating skill "${name}"...`);
34
+ spin.start();
35
+ spin.stop();
36
+
37
+ try {
38
+ // Remove and re-add
39
+ await remove(name);
40
+ await add(origin.repository, { skill: origin.skill || undefined });
41
+ return true;
42
+ } catch (err) {
43
+ log.error(`Failed to update skill "${name}": ${err.message}`);
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export async function update(name) {
49
+ if (name) {
50
+ await updateOne(name);
51
+ return;
52
+ }
53
+
54
+ const skillsDir = getSkillsDir();
55
+ const entries = readdirSync(skillsDir, { withFileTypes: true }).filter(
56
+ (e) => e.isDirectory()
57
+ );
58
+
59
+ if (!entries.length) {
60
+ log.info('No skills installed');
61
+ return;
62
+ }
63
+
64
+ log.info(`Updating ${entries.length} skill${entries.length === 1 ? '' : 's'}...\n`);
65
+
66
+ let updated = 0;
67
+ for (const entry of entries) {
68
+ if (await updateOne(entry.name)) updated++;
69
+ }
70
+
71
+ console.log();
72
+ log.success(`${updated}/${entries.length} skills updated`);
73
+ log.dim('Restart Claude Code to apply changes.');
74
+ }
package/src/config.js CHANGED
@@ -6,6 +6,7 @@ const home = homedir();
6
6
  const isWindows = process.platform === 'win32';
7
7
 
8
8
  export const PLUGINS_DIR = join(home, '.claude', 'plugins');
9
+ export const SKILLS_DIR = join(home, '.claude', 'skills');
9
10
  export const CACHE_DIR = join(home, '.claude', '.cache', 'claude-plugins');
10
11
  export const CACHE_TTL = 1000 * 60 * 15; // 15 minutes
11
12
  export const REGISTRY_URL =
@@ -22,6 +23,10 @@ export function getPluginsDir() {
22
23
  return ensureDir(PLUGINS_DIR);
23
24
  }
24
25
 
26
+ export function getSkillsDir() {
27
+ return ensureDir(SKILLS_DIR);
28
+ }
29
+
25
30
  export function getCacheDir() {
26
31
  return ensureDir(CACHE_DIR);
27
32
  }