@biggora/claude-plugins 1.3.0 → 1.3.1

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/README.md CHANGED
@@ -226,6 +226,7 @@ Skills are simpler than plugins — just a SKILL.md file with optional supportin
226
226
  ```
227
227
  my-skill/
228
228
  SKILL.md # Required: skill instructions with YAML frontmatter
229
+ commands/ # Slash commands (optional, copied to ~/.claude/commands/ on install)
229
230
  references/ # Reference docs (optional)
230
231
  scripts/ # Helper scripts (optional)
231
232
  ```
@@ -262,8 +263,9 @@ The plugin registry is a GitHub-hosted JSON file at [biggora/claude-plugins-regi
262
263
 
263
264
  - **Plugins** are installed via `git clone` to `~/.claude/plugins/<name>`
264
265
  - **Skills** are installed via `git clone` + copy to `~/.claude/skills/<name>`
266
+ - **Slash commands** bundled with skills are automatically copied to `~/.claude/commands/`
265
267
  - Updates use `git pull --ff-only` to safely fast-forward
266
- - Claude Code automatically discovers plugins and skills in `~/.claude/`
268
+ - Claude Code automatically discovers plugins in `~/.claude/plugins/`, skills in `~/.claude/skills/`, and commands in `~/.claude/commands/`
267
269
  - Restart Claude Code after installing or removing plugins/skills
268
270
 
269
271
  ## Requirements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biggora/claude-plugins",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "CLI marketplace for discovering, installing, and managing Claude Code plugins",
5
5
  "keywords": [
6
6
  "claude",
package/specs/coding.md CHANGED
@@ -28,3 +28,8 @@ create skill eslint-expert src/skills/eslint-expert. The skill should include kn
28
28
  create slash command typescript-fix for the TypeScript expert (typescript-expert) skill. with some content like this:
29
29
  "You are a TypeScript expert with deep knowledge of complex typing patterns, modern JavaScript (ES6+), ESLint configuration, and enterprise-grade development practices. You specialize in building reliable, maintainable, and performant TypeScript solutions for complex business problems, with a solid understanding of JavaScript fundamentals and code quality tools. Read package.json to identify the command to run the linter. Use the `typescript-expert` skill to run `npm run typecheck`, `pnpm run typecheck`, or `tsc --noEmit` in the project directory and resolve TypeScript errors."
30
30
 
31
+ the slash command is copied to the same skill subfolder ~/.claude/skills/typescript-expert/commands/typescript-fix.md, but must be copied to ~/.claude/commands/typescript-fix.md
32
+
33
+
34
+ try this command npx skills add ./ --skill typescript-expert and check C:\Users\biggora\.claude\commands. Looks
35
+ like it does not work
@@ -1,11 +1,11 @@
1
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';
2
+ import { existsSync, readFileSync, readdirSync, cpSync, rmSync, mkdirSync, writeFileSync, statSync, copyFileSync } from 'node:fs';
3
+ import { join, basename, resolve } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
- import { getSkillsDir, getPluginsDir } from '../../config.js';
5
+ import { getSkillsDir, getCommandsDir } from '../../config.js';
6
6
  import { fetchRegistry, findPlugin } from '../../registry.js';
7
7
  import { log, spinner } from '../../utils.js';
8
- import { hasPluginComponents, detectComponents, findInstalledSkill } from './resolve.js';
8
+ import { detectComponents, findInstalledSkill } from './resolve.js';
9
9
 
10
10
  export function parseFrontmatter(content) {
11
11
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -49,6 +49,15 @@ function isUrl(str) {
49
49
  return str.startsWith('http://') || str.startsWith('https://') || str.startsWith('git@');
50
50
  }
51
51
 
52
+ function isLocalPath(str) {
53
+ if (str.startsWith('./') || str.startsWith('../') || str.startsWith('/')) return true;
54
+ // Windows absolute paths like C:\ or E:\
55
+ if (/^[a-zA-Z]:[/\\]/.test(str)) return true;
56
+ // Explicit . for current directory
57
+ if (str === '.') return true;
58
+ return false;
59
+ }
60
+
52
61
  function makeTempDir() {
53
62
  const dir = join(tmpdir(), `claude-skill-${Date.now()}`);
54
63
  mkdirSync(dir, { recursive: true });
@@ -57,9 +66,19 @@ function makeTempDir() {
57
66
 
58
67
  export async function add(source, options = {}) {
59
68
  let repoUrl = source;
69
+ let sourceDir = null;
60
70
 
61
- // If not a URL, look up in registry
62
- if (!isUrl(source)) {
71
+ if (isLocalPath(source)) {
72
+ // Local directory — use directly, no git clone
73
+ sourceDir = resolve(source);
74
+ if (!existsSync(sourceDir)) {
75
+ log.error(`Local path "${sourceDir}" does not exist`);
76
+ process.exit(1);
77
+ }
78
+ repoUrl = sourceDir;
79
+ log.info(`Using local path: ${sourceDir}`);
80
+ } else if (!isUrl(source)) {
81
+ // Not a URL and not a local path — look up in registry
63
82
  const registry = await fetchRegistry();
64
83
  const entry = findPlugin(registry, source);
65
84
  if (!entry) {
@@ -71,29 +90,36 @@ export async function add(source, options = {}) {
71
90
  log.info(`Resolved "${source}" to ${repoUrl}`);
72
91
  }
73
92
 
74
- const tmpDir = makeTempDir();
75
- const spin = spinner(`Cloning ${repoUrl}...`);
76
- spin.start();
77
-
78
- try {
79
- const gitUrl = repoUrl.endsWith('.git') ? repoUrl : `${repoUrl}.git`;
80
- execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpDir], {
81
- stdio: 'pipe',
82
- });
83
- spin.succeed('Repository cloned');
84
- } catch (err) {
85
- spin.fail('Failed to clone repository');
86
- log.error(err.message);
87
- rmSync(tmpDir, { recursive: true, force: true });
88
- process.exit(1);
93
+ let tmpDir = null;
94
+
95
+ // Clone if remote URL, otherwise use local path directly
96
+ if (!sourceDir) {
97
+ tmpDir = makeTempDir();
98
+ const spin = spinner(`Cloning ${repoUrl}...`);
99
+ spin.start();
100
+
101
+ try {
102
+ const gitUrl = repoUrl.endsWith('.git') ? repoUrl : `${repoUrl}.git`;
103
+ execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpDir], {
104
+ stdio: 'pipe',
105
+ });
106
+ spin.succeed('Repository cloned');
107
+ } catch (err) {
108
+ spin.fail('Failed to clone repository');
109
+ log.error(err.message);
110
+ rmSync(tmpDir, { recursive: true, force: true });
111
+ process.exit(1);
112
+ }
89
113
  }
90
114
 
115
+ const searchRoot = sourceDir || tmpDir;
116
+
91
117
  // Find all skill directories
92
- const skillDirs = findSkillDirs(tmpDir);
118
+ const skillDirs = findSkillDirs(searchRoot);
93
119
 
94
120
  if (!skillDirs.length) {
95
121
  log.error('No SKILL.md found in repository');
96
- rmSync(tmpDir, { recursive: true, force: true });
122
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
97
123
  process.exit(1);
98
124
  }
99
125
 
@@ -118,22 +144,22 @@ export async function add(source, options = {}) {
118
144
  const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
119
145
  log.dim(` - ${fm.name || basename(d)}`);
120
146
  }
121
- rmSync(tmpDir, { recursive: true, force: true });
147
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
122
148
  process.exit(1);
123
149
  }
124
150
  } else if (skillDirs.length === 1) {
125
151
  targetDir = skillDirs[0];
126
152
  } else {
127
153
  // Check if root has SKILL.md
128
- if (existsSync(join(tmpDir, 'SKILL.md'))) {
129
- targetDir = tmpDir;
154
+ if (existsSync(join(searchRoot, 'SKILL.md'))) {
155
+ targetDir = searchRoot;
130
156
  } else {
131
157
  log.warn('Multiple skills found. Use --skill <name> to select one:');
132
158
  for (const d of skillDirs) {
133
159
  const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
134
160
  log.dim(` - ${fm.name || basename(d)}`);
135
161
  }
136
- rmSync(tmpDir, { recursive: true, force: true });
162
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
137
163
  process.exit(1);
138
164
  }
139
165
  }
@@ -141,32 +167,30 @@ export async function add(source, options = {}) {
141
167
  // Parse skill metadata
142
168
  const skillMd = readFileSync(join(targetDir, 'SKILL.md'), 'utf-8');
143
169
  const frontmatter = parseFrontmatter(skillMd);
144
- const skillName = frontmatter.name || options.skill || basename(targetDir === tmpDir ? repoUrl.replace(/\.git$/, '').split('/').pop() : targetDir);
170
+ const skillName = frontmatter.name || options.skill || basename(targetDir === searchRoot ? repoUrl.replace(/\.git$/, '').split('/').pop() : targetDir);
145
171
 
146
- // Detect plugin components (commands, hooks, agents) alongside SKILL.md
172
+ // Detect components alongside SKILL.md
147
173
  const components = detectComponents(targetDir);
148
- const installAsPlugin = hasPluginComponents(targetDir);
149
174
 
150
- const destDir = installAsPlugin ? getPluginsDir() : getSkillsDir();
151
- const dest = join(destDir, skillName);
175
+ const skillsDir = getSkillsDir();
176
+ const dest = join(skillsDir, skillName);
152
177
 
153
178
  // Check both locations for existing installation
154
179
  const existing = findInstalledSkill(skillName);
155
180
  if (existing) {
156
181
  log.warn(`Skill "${skillName}" is already installed at ${existing.dir}`);
157
182
  log.dim(`Run "claude-plugins skills update ${skillName}" to update it`);
158
- rmSync(tmpDir, { recursive: true, force: true });
183
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
159
184
  return;
160
185
  }
161
186
 
162
187
  if (existsSync(dest)) {
163
188
  log.warn(`"${skillName}" already exists at ${dest}`);
164
- rmSync(tmpDir, { recursive: true, force: true });
189
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
165
190
  return;
166
191
  }
167
192
 
168
- const label = installAsPlugin ? 'skill + components' : 'skill';
169
- const spin2 = spinner(`Installing ${label} "${skillName}"...`);
193
+ const spin2 = spinner(`Installing skill "${skillName}"...`);
170
194
  spin2.start();
171
195
 
172
196
  try {
@@ -178,24 +202,8 @@ export async function add(source, options = {}) {
178
202
  rmSync(gitInDest, { recursive: true, force: true });
179
203
  }
180
204
 
181
- // Generate .claude-plugin/plugin.json if installing as plugin and none exists
182
- if (installAsPlugin && !existsSync(join(dest, '.claude-plugin', 'plugin.json'))) {
183
- const pluginDir = join(dest, '.claude-plugin');
184
- mkdirSync(pluginDir, { recursive: true });
185
-
186
- const pluginJson = {
187
- name: skillName,
188
- version: '1.0.0',
189
- description: frontmatter.description || `Skill: ${skillName}`,
190
- commands: listCommandFiles(dest),
191
- _generatedBy: 'claude-plugins skills add',
192
- };
193
-
194
- writeFileSync(
195
- join(pluginDir, 'plugin.json'),
196
- JSON.stringify(pluginJson, null, 2)
197
- );
198
- }
205
+ // Copy slash commands to ~/.claude/commands/ so Claude Code discovers them
206
+ const installedCommands = installCommands(dest, skillName);
199
207
 
200
208
  // Write origin metadata
201
209
  writeFileSync(
@@ -204,8 +212,8 @@ export async function add(source, options = {}) {
204
212
  {
205
213
  repository: repoUrl,
206
214
  skill: options.skill || null,
207
- installedAs: installAsPlugin ? 'plugin-skill' : 'skill',
208
215
  components,
216
+ installedCommands,
209
217
  installedAt: new Date().toISOString(),
210
218
  },
211
219
  null,
@@ -219,10 +227,13 @@ export async function add(source, options = {}) {
219
227
  log.dim(` ${frontmatter.description}`);
220
228
  }
221
229
 
222
- if (installAsPlugin) {
223
- const extras = components.filter((c) => c !== 'skill');
230
+ if (installedCommands.length) {
231
+ log.info(`Commands: ${installedCommands.map((c) => `/${c}`).join(', ')}`);
232
+ log.dim(` Copied to ${getCommandsDir()}`);
233
+ }
234
+
235
+ if (components.length > 1) {
224
236
  log.info(`Detected: ${components.join(', ')}`);
225
- log.dim(`Installed as plugin so Claude Code discovers ${extras.join(', ')}`);
226
237
  }
227
238
 
228
239
  log.dim('\nRestart Claude Code to load the skill.');
@@ -230,21 +241,38 @@ export async function add(source, options = {}) {
230
241
  spin2.fail(`Failed to install skill "${skillName}"`);
231
242
  log.error(err.message);
232
243
  } finally {
233
- rmSync(tmpDir, { recursive: true, force: true });
244
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
234
245
  }
235
246
  }
236
247
 
237
248
  /**
238
- * List command file names (without extension) from a commands/ directory.
249
+ * Copy command .md files from skill's commands/ dir to ~/.claude/commands/.
250
+ * Returns array of installed command names (without extension).
239
251
  */
240
- function listCommandFiles(dir) {
241
- const cmdsDir = join(dir, 'commands');
252
+ function installCommands(skillDir, skillName) {
253
+ const cmdsDir = join(skillDir, 'commands');
242
254
  if (!existsSync(cmdsDir)) return [];
255
+
256
+ const commandsDir = getCommandsDir();
257
+ const installed = [];
258
+
243
259
  try {
244
- return readdirSync(cmdsDir)
245
- .filter((f) => f.endsWith('.md'))
246
- .map((f) => `/${f.replace(/\.md$/, '')}`);
260
+ const files = readdirSync(cmdsDir).filter((f) => f.endsWith('.md'));
261
+ for (const file of files) {
262
+ const src = join(cmdsDir, file);
263
+ const destFile = join(commandsDir, file);
264
+
265
+ if (existsSync(destFile)) {
266
+ log.warn(`Command "${file}" already exists in ${commandsDir}, skipping`);
267
+ continue;
268
+ }
269
+
270
+ copyFileSync(src, destFile);
271
+ installed.push(file.replace(/\.md$/, ''));
272
+ }
247
273
  } catch {
248
- return [];
274
+ // ignore errors reading commands dir
249
275
  }
276
+
277
+ return installed;
250
278
  }
@@ -13,11 +13,13 @@ export async function list() {
13
13
 
14
14
  console.log(chalk.bold(`\n ${skills.length} skill${skills.length === 1 ? '' : 's'} installed\n`));
15
15
 
16
- const rows = skills.map(({ name, meta, location }) => {
17
- const type = location === 'plugins' ? chalk.cyan('plugin') : chalk.dim('skill');
18
- return [name, truncate(meta.description, 45), type, truncate(meta.repository, 35)];
16
+ const rows = skills.map(({ name, meta }) => {
17
+ const cmds = meta.commands.length
18
+ ? meta.commands.map((c) => `/${c}`).join(', ')
19
+ : '';
20
+ return [name, truncate(meta.description, 45), cmds, truncate(meta.repository, 35)];
19
21
  });
20
22
 
21
- formatTable(rows, ['Name', 'Description', 'Type', 'Repository']);
23
+ formatTable(rows, ['Name', 'Description', 'Commands', 'Repository']);
22
24
  console.log();
23
25
  }
@@ -1,6 +1,8 @@
1
- import { rmSync } from 'node:fs';
1
+ import { existsSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getCommandsDir } from '../../config.js';
2
4
  import { log, spinner } from '../../utils.js';
3
- import { findInstalledSkill } from './resolve.js';
5
+ import { findInstalledSkill, readOrigin } from './resolve.js';
4
6
 
5
7
  export async function remove(name) {
6
8
  const found = findInstalledSkill(name);
@@ -15,8 +17,25 @@ export async function remove(name) {
15
17
  spin.start();
16
18
 
17
19
  try {
20
+ // Clean up commands that were copied to ~/.claude/commands/
21
+ const origin = readOrigin(found.dir);
22
+ if (origin && origin.installedCommands?.length) {
23
+ const commandsDir = getCommandsDir();
24
+ for (const cmd of origin.installedCommands) {
25
+ const cmdFile = join(commandsDir, `${cmd}.md`);
26
+ if (existsSync(cmdFile)) {
27
+ rmSync(cmdFile);
28
+ }
29
+ }
30
+ }
31
+
18
32
  rmSync(found.dir, { recursive: true, force: true });
19
- spin.succeed(`Removed skill "${name}" from ${found.location}`);
33
+ spin.succeed(`Removed skill "${name}"`);
34
+
35
+ if (origin?.installedCommands?.length) {
36
+ log.dim(` Also removed commands: ${origin.installedCommands.map((c) => `/${c}`).join(', ')}`);
37
+ }
38
+
20
39
  log.dim('Restart Claude Code to apply changes.');
21
40
  } catch (err) {
22
41
  spin.fail(`Failed to remove skill "${name}"`);
@@ -1,41 +1,28 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { getSkillsDir, getPluginsDir } from '../../config.js';
3
+ import { getSkillsDir } from '../../config.js';
4
4
  import { parseFrontmatter } from './add.js';
5
5
 
6
6
  /**
7
- * Plugin component directories that Claude Code auto-discovers.
8
- * When a skill repo contains any of these, it must be installed
9
- * as a plugin so Claude Code can find them.
7
+ * Component directories that can accompany a SKILL.md.
10
8
  */
11
- export const PLUGIN_COMPONENT_DIRS = ['commands', 'hooks', 'agents'];
9
+ export const COMPONENT_DIRS = ['commands', 'hooks', 'agents'];
12
10
 
13
11
  /**
14
- * Check if a directory contains plugin components (commands, hooks, agents)
15
- * beyond just SKILL.md + references.
16
- */
17
- export function hasPluginComponents(dir) {
18
- return PLUGIN_COMPONENT_DIRS.some((name) => {
19
- const p = join(dir, name);
20
- return existsSync(p);
21
- });
22
- }
23
-
24
- /**
25
- * Detect which plugin components exist in a directory.
12
+ * Detect which components exist in a directory.
26
13
  * Returns an array of found component names.
27
14
  */
28
15
  export function detectComponents(dir) {
29
16
  const found = [];
30
17
  if (existsSync(join(dir, 'SKILL.md'))) found.push('skill');
31
- for (const name of PLUGIN_COMPONENT_DIRS) {
18
+ for (const name of COMPONENT_DIRS) {
32
19
  if (existsSync(join(dir, name))) found.push(name);
33
20
  }
34
21
  return found;
35
22
  }
36
23
 
37
24
  /**
38
- * Read .origin.json from a skill/plugin directory.
25
+ * Read .origin.json from a skill directory.
39
26
  */
40
27
  export function readOrigin(dir) {
41
28
  const originPath = join(dir, '.origin.json');
@@ -52,7 +39,7 @@ export function readOrigin(dir) {
52
39
  */
53
40
  export function readSkillMeta(dir, dirName) {
54
41
  const skillMdPath = join(dir, 'SKILL.md');
55
- const meta = { name: dirName, description: '', repository: '-' };
42
+ const meta = { name: dirName, description: '', repository: '-', commands: [] };
56
43
 
57
44
  if (existsSync(skillMdPath)) {
58
45
  try {
@@ -67,53 +54,36 @@ export function readSkillMeta(dir, dirName) {
67
54
  const origin = readOrigin(dir);
68
55
  if (origin) {
69
56
  meta.repository = origin.repository || '-';
70
- meta.installedAs = origin.installedAs || 'skill';
71
57
  meta.skill = origin.skill || null;
58
+ meta.commands = origin.installedCommands || [];
72
59
  }
73
60
 
74
61
  return meta;
75
62
  }
76
63
 
77
64
  /**
78
- * Find a skill by name across both ~/.claude/skills/ and ~/.claude/plugins/.
65
+ * Find a skill by name in ~/.claude/skills/.
79
66
  * Returns { dir, location } or null.
80
67
  */
81
68
  export function findInstalledSkill(name) {
82
69
  const skillsDir = getSkillsDir();
83
- const pluginsDir = getPluginsDir();
84
-
85
- // Check ~/.claude/skills/ first
86
70
  const skillPath = join(skillsDir, name);
71
+
87
72
  if (existsSync(skillPath) && existsSync(join(skillPath, 'SKILL.md'))) {
88
73
  return { dir: skillPath, location: 'skills' };
89
74
  }
90
75
 
91
- // Check ~/.claude/plugins/ for skills installed as plugins
92
- const pluginPath = join(pluginsDir, name);
93
- if (existsSync(pluginPath)) {
94
- const origin = readOrigin(pluginPath);
95
- if (origin && origin.installedAs === 'plugin-skill') {
96
- return { dir: pluginPath, location: 'plugins' };
97
- }
98
- // Also check if it has a SKILL.md (might be installed via skills add)
99
- if (existsSync(join(pluginPath, 'SKILL.md'))) {
100
- return { dir: pluginPath, location: 'plugins' };
101
- }
102
- }
103
-
104
76
  return null;
105
77
  }
106
78
 
107
79
  /**
108
- * List all installed skills from both directories.
109
- * Returns array of { name, dir, location, meta }.
80
+ * List all installed skills from ~/.claude/skills/.
81
+ * Returns array of { name, dir, meta }.
110
82
  */
111
83
  export function listAllSkills() {
112
84
  const skillsDir = getSkillsDir();
113
- const pluginsDir = getPluginsDir();
114
85
  const results = [];
115
86
 
116
- // Skills from ~/.claude/skills/
117
87
  try {
118
88
  const entries = readdirSync(skillsDir, { withFileTypes: true });
119
89
  for (const entry of entries) {
@@ -123,7 +93,6 @@ export function listAllSkills() {
123
93
  results.push({
124
94
  name: entry.name,
125
95
  dir,
126
- location: 'skills',
127
96
  meta: readSkillMeta(dir, entry.name),
128
97
  });
129
98
  }
@@ -131,25 +100,5 @@ export function listAllSkills() {
131
100
  // directory might not exist yet
132
101
  }
133
102
 
134
- // Skills from ~/.claude/plugins/ (installed as plugin-skill)
135
- try {
136
- const entries = readdirSync(pluginsDir, { withFileTypes: true });
137
- for (const entry of entries) {
138
- if (!entry.isDirectory()) continue;
139
- const dir = join(pluginsDir, entry.name);
140
- const origin = readOrigin(dir);
141
- if (origin && origin.installedAs === 'plugin-skill') {
142
- results.push({
143
- name: entry.name,
144
- dir,
145
- location: 'plugins',
146
- meta: readSkillMeta(dir, entry.name),
147
- });
148
- }
149
- }
150
- } catch {
151
- // directory might not exist yet
152
- }
153
-
154
103
  return results;
155
104
  }
package/src/config.js CHANGED
@@ -7,6 +7,7 @@ const isWindows = process.platform === 'win32';
7
7
 
8
8
  export const PLUGINS_DIR = join(home, '.claude', 'plugins');
9
9
  export const SKILLS_DIR = join(home, '.claude', 'skills');
10
+ export const COMMANDS_DIR = join(home, '.claude', 'commands');
10
11
  export const CACHE_DIR = join(home, '.claude', '.cache', 'claude-plugins');
11
12
  export const CACHE_TTL = 1000 * 60 * 15; // 15 minutes
12
13
  export const REGISTRY_URL =
@@ -27,6 +28,10 @@ export function getSkillsDir() {
27
28
  return ensureDir(SKILLS_DIR);
28
29
  }
29
30
 
31
+ export function getCommandsDir() {
32
+ return ensureDir(COMMANDS_DIR);
33
+ }
34
+
30
35
  export function getCacheDir() {
31
36
  return ensureDir(CACHE_DIR);
32
37
  }