/** * Narrowing the discovered skill list to what the agent can actually invoke. * * OpenChamber scans every root it knows about. OpenCode loads a narrower set, * governed by environment flags the browser cannot read — the server reports * them alongside the scan. * * OpenCode's own skill-list endpoint cannot serve as the authority here: * measured against 1.18.14, it returns only global and builtin skills and omits * the project `.agents`/`.claude` skills the agent demonstrably has. Mirroring * its discovery rules is the only way to match what the agent sees. * * The rules, from `opencode/src/skill/index.ts`: * * - `OPENCODE_DISABLE_EXTERNAL_SKILLS` drops `.claude` and `.agents` entirely; * - `OPENCODE_DISABLE_CLAUDE_CODE` (broad) or `..._CLAUDE_CODE_SKILLS` * (specific) drops `.claude` only — `.agents` is always scanned; * - names are deduplicated, last scan winning, and `.agents` is scanned after * `.claude`, so `.agents` wins a collision. This matters here because * `.claude/skills` entries are commonly symlinks back into `.agents/skills`. */ type ExternalSkillFlags = { /** `.claude` roots are not loaded. */ claudeDisabled: boolean; /** Neither `.claude` nor `.agents` roots are loaded. */ allDisabled: boolean; }; type SkillLike = { name: string; path: string }; const CLAUDE_ROOT = /(^|\/)\.claude\//; const AGENTS_ROOT = /(^|\/)\.agents\//; type SkillRoot = 'claude' | 'agents' | 'opencode'; export const resolveSkillRoot = (skillPath: string): SkillRoot => { if (CLAUDE_ROOT.test(skillPath)) return 'claude'; if (AGENTS_ROOT.test(skillPath)) return 'agents'; return 'opencode'; }; export const filterSkillsByRuntimeFlags = ( skills: readonly T[], flags: ExternalSkillFlags | null | undefined, ): T[] => { // No flags reported means an older server or a failed read. Filtering on a // guess would hide skills that do work, so the list passes through. if (!flags) return [...skills]; const allowed = skills.filter((skill) => { const root = resolveSkillRoot(skill.path); if (root === 'opencode') return true; if (flags.allDisabled) return false; if (root === 'claude') return !flags.claudeDisabled; return true; }); // Deduplicate by name, preferring `.agents` — the same order OpenCode's // last-write-wins scan produces. const byName = new Map(); for (const skill of allowed) { const existing = byName.get(skill.name); if (!existing) { byName.set(skill.name, skill); continue; } if (resolveSkillRoot(existing.path) === 'claude' && resolveSkillRoot(skill.path) === 'agents') { byName.set(skill.name, skill); } } return [...byName.values()]; };