/** * v2.5.11: discover Claude Code's real, enabled commands + plugin skills from * disk so the coder slash menu shows them (claude is PTY — no ACP discovery). * * Scope (v1): user-global only — `~/.claude/commands/*.md` plus the enabled * plugins listed in `~/.claude/settings.json:enabledPlugins` (user-scope install * paths from `~/.claude/plugins/.../installed_plugins.json`). Project-local * plugins and `/.claude/commands` are deferred. Names are bare. */ import { readFileSync, readdirSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import type { AgentCommand } from './provider-types.js'; /** Minimal frontmatter reader — single-line `key: value` between `---` fences. */ function frontmatterField(content: string, field: string): string | undefined { const block = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!block?.[1]) return undefined; const m = block[1].match(new RegExp(`^${field}:\\s*(.+)$`, 'm')); return m?.[1]?.trim().replace(/^["']|["']$/g, '') || undefined; } function readCommandDir(dir: string): AgentCommand[] { if (!existsSync(dir)) return []; let files: string[]; try { files = readdirSync(dir); } catch { return []; } const out: AgentCommand[] = []; for (const f of files) { if (!f.endsWith('.md')) continue; let description: string | undefined; try { description = frontmatterField(readFileSync(join(dir, f), 'utf8'), 'description'); } catch { /* unreadable — still list the command by name */ } out.push({ name: f.slice(0, -3), kind: 'command', ...(description ? { description } : {}) }); } return out; } function readSkillDir(dir: string): AgentCommand[] { if (!existsSync(dir)) return []; let entries: string[]; try { entries = readdirSync(dir); } catch { return []; } const out: AgentCommand[] = []; for (const sub of entries) { const skillMd = join(dir, sub, 'SKILL.md'); if (!existsSync(skillMd)) continue; let content: string; try { content = readFileSync(skillMd, 'utf8'); } catch { continue; } out.push({ name: frontmatterField(content, 'name') ?? sub, kind: 'skill', ...(() => { const d = frontmatterField(content, 'description'); return d ? { description: d } : {}; })(), }); } return out; } export function discoverClaudeCommands(): AgentCommand[] { const root = join(homedir(), '.claude'); const out: AgentCommand[] = []; // User custom commands. out.push(...readCommandDir(join(root, 'commands'))); // Enabled plugins (user-scope installs). try { const settings = JSON.parse(readFileSync(join(root, 'settings.json'), 'utf8')) as { enabledPlugins?: Record; }; const installed = JSON.parse( readFileSync(join(root, 'plugins', 'installed_plugins.json'), 'utf8'), ) as { plugins?: Record> }; const enabled = settings.enabledPlugins ?? {}; const plugins = installed.plugins ?? {}; for (const [key, on] of Object.entries(enabled)) { if (!on) continue; const installs = plugins[key] ?? []; const installPath = (installs.find((i) => i.scope === 'user') ?? installs[0])?.installPath; if (!installPath || !existsSync(installPath)) continue; out.push(...readSkillDir(join(installPath, 'skills'))); out.push(...readCommandDir(join(installPath, 'commands'))); } } catch { /* missing/unreadable plugin config → user commands only */ } // Dedupe by name (first wins). const seen = new Set(); return out.filter((c) => (seen.has(c.name) ? false : (seen.add(c.name), true))); }