Browse Source

Remove custom skills (#86)

* Remove custom skills

* Cleanups
Alvin 2 months ago
parent
commit
a95b94d0d0

+ 31 - 38
src/agents/index.ts

@@ -2,11 +2,14 @@ import type { AgentConfig as SDKAgentConfig } from '@opencode-ai/sdk';
 import {
   type AgentOverrideConfig,
   DEFAULT_MODELS,
+  getAgentOverride,
   loadAgentPrompt,
   type PluginConfig,
   SUBAGENT_NAMES,
 } from '../config';
-import { getAgentMcpList } from '../tools/skill/builtin';
+import { getAgentMcpList } from '../config/agent-mcps';
+import { getSkillPermissionsForAgent } from '../cli/skills';
+
 import { createDesignerAgent } from './designer';
 import { createExplorerAgent } from './explorer';
 import { createFixerAgent } from './fixer';
@@ -22,30 +25,6 @@ type AgentFactory = (
   customAppendPrompt?: string,
 ) => AgentDefinition;
 
-// Backward Compatibility
-
-/** Map old agent names to new names for backward compatibility */
-const AGENT_ALIASES: Record<string, string> = {
-  explore: 'explorer',
-  'frontend-ui-ux-engineer': 'designer',
-};
-
-/**
- * Get agent override config by name, supporting backward-compatible aliases.
- * Checks both the current name and any legacy alias names.
- */
-function getOverride(
-  overrides: Record<string, AgentOverrideConfig>,
-  name: string,
-): AgentOverrideConfig | undefined {
-  return (
-    overrides[name] ??
-    overrides[
-      Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === name) ?? ''
-    ]
-  );
-}
-
 // Agent Configuration Helpers
 
 /**
@@ -63,16 +42,32 @@ function applyOverrides(
 
 /**
  * Apply default permissions to an agent.
- * Currently sets 'question' permission to 'allow' for all agents.
+ * Sets 'question' permission to 'allow' and includes skill permission presets.
+ * If configuredSkills is provided, it honors that list instead of defaults.
  */
-function applyDefaultPermissions(agent: AgentDefinition): void {
+function applyDefaultPermissions(
+  agent: AgentDefinition,
+  configuredSkills?: string[],
+): void {
   const existing = (agent.config.permission ?? {}) as Record<
     string,
-    'ask' | 'allow' | 'deny'
+    'ask' | 'allow' | 'deny' | Record<string, 'ask' | 'allow' | 'deny'>
   >;
+
+  // Get skill-specific permissions for this agent
+  const skillPermissions = getSkillPermissionsForAgent(
+    agent.name,
+    configuredSkills,
+  );
+
   agent.config.permission = {
     ...existing,
     question: 'allow',
+    // Apply skill permissions as nested object under 'skill' key
+    skill: {
+      ...(typeof existing.skill === 'object' ? existing.skill : {}),
+      ...skillPermissions,
+    },
   } as SDKAgentConfig['permission'];
 }
 
@@ -104,15 +99,12 @@ const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
  * @returns Array of agent definitions (orchestrator first, then subagents)
  */
 export function createAgents(config?: PluginConfig): AgentDefinition[] {
-  const agentOverrides = config?.agents ?? {};
-
   // TEMP: If fixer has no config, inherit from librarian's model to avoid breaking
   // existing users who don't have fixer in their config yet
   const getModelForAgent = (name: SubagentName): string => {
-    if (name === 'fixer' && !getOverride(agentOverrides, 'fixer')?.model) {
+    if (name === 'fixer' && !getAgentOverride(config, 'fixer')?.model) {
       return (
-        getOverride(agentOverrides, 'librarian')?.model ??
-        DEFAULT_MODELS.librarian
+        getAgentOverride(config, 'librarian')?.model ?? DEFAULT_MODELS.librarian
       );
     }
     return DEFAULT_MODELS[name];
@@ -130,18 +122,19 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
     );
   });
 
-  // 2. Apply overrides to each agent
+  // 2. Apply overrides and default permissions to each agent
   const allSubAgents = protoSubAgents.map((agent) => {
-    const override = getOverride(agentOverrides, agent.name);
+    const override = getAgentOverride(config, agent.name);
     if (override) {
       applyOverrides(agent, override);
     }
+    applyDefaultPermissions(agent, override?.skills);
     return agent;
   });
 
   // 3. Create Orchestrator (with its own overrides and custom prompts)
   const orchestratorModel =
-    getOverride(agentOverrides, 'orchestrator')?.model ??
+    getAgentOverride(config, 'orchestrator')?.model ??
     DEFAULT_MODELS.orchestrator;
   const orchestratorPrompts = loadAgentPrompt('orchestrator');
   const orchestrator = createOrchestratorAgent(
@@ -149,8 +142,8 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
     orchestratorPrompts.prompt,
     orchestratorPrompts.appendPrompt,
   );
-  applyDefaultPermissions(orchestrator);
-  const oOverride = getOverride(agentOverrides, 'orchestrator');
+  const oOverride = getAgentOverride(config, 'orchestrator');
+  applyDefaultPermissions(orchestrator, oOverride?.skills);
   if (oOverride) {
     applyOverrides(orchestrator, oOverride);
   }

+ 33 - 0
src/cli/install.ts

@@ -9,6 +9,7 @@ import {
   isOpenCodeInstalled,
   writeLiteConfig,
 } from './config-manager';
+import { RECOMMENDED_SKILLS, installSkill } from './skills';
 import type {
   BooleanArg,
   ConfigMergeResult,
@@ -155,6 +156,7 @@ function argsToConfig(args: InstallArgs): InstallConfig {
     hasOpenAI: args.openai === 'yes',
     hasOpencodeZen: true, // Always enabled - free models available to all users
     hasTmux: args.tmux === 'yes',
+    installSkills: args.skills === 'yes',
   };
 }
 
@@ -213,11 +215,25 @@ async function runInteractiveMode(
     //   console.log()
     // }
 
+    // Skills prompt
+    console.log(`${BOLD}Recommended Skills:${RESET}`);
+    for (const skill of RECOMMENDED_SKILLS) {
+      console.log(`  ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
+    }
+    console.log();
+    const skills = await askYesNo(
+      rl,
+      'Install recommended skills?',
+      'yes',
+    );
+    console.log();
+
     return {
       hasAntigravity: antigravity === 'yes',
       hasOpenAI: openai === 'yes',
       hasOpencodeZen: true,
       hasTmux: false,
+      installSkills: skills === 'yes',
     };
   } finally {
     rl.close();
@@ -233,6 +249,7 @@ async function runInstall(config: InstallConfig): Promise<number> {
   // Calculate total steps dynamically
   let totalSteps = 4; // Base: check opencode, add plugin, disable default agents, write lite config
   if (config.hasAntigravity) totalSteps += 1; // provider config only (no auth plugin needed)
+  if (config.installSkills) totalSteps += 1; // skills installation
 
   let step = 1;
 
@@ -259,6 +276,22 @@ async function runInstall(config: InstallConfig): Promise<number> {
   const liteResult = writeLiteConfig(config);
   if (!handleStepResult(liteResult, 'Config written')) return 1;
 
+  // Install skills if requested
+  if (config.installSkills) {
+    printStep(step++, totalSteps, 'Installing recommended skills...');
+    let skillsInstalled = 0;
+    for (const skill of RECOMMENDED_SKILLS) {
+      printInfo(`Installing ${skill.name}...`);
+      if (installSkill(skill)) {
+        printSuccess(`Installed: ${skill.name}`);
+        skillsInstalled++;
+      } else {
+        printWarning(`Failed to install: ${skill.name}`);
+      }
+    }
+    printSuccess(`${skillsInstalled}/${RECOMMENDED_SKILLS.length} skills installed`);
+  }
+
   // Summary
   console.log();
   console.log(formatConfigSummary(config));

+ 18 - 2
src/cli/providers.test.ts

@@ -10,6 +10,7 @@ describe('providers', () => {
       hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: false,
     });
 
     expect(config.preset).toBe('cliproxy');
@@ -32,6 +33,7 @@ describe('providers', () => {
       hasOpenAI: true,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: false,
     });
 
     expect(config.preset).toBe('cliproxy');
@@ -54,6 +56,7 @@ describe('providers', () => {
       hasOpenAI: true,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: false,
     });
 
     expect(config.preset).toBe('openai');
@@ -74,6 +77,7 @@ describe('providers', () => {
       hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: false,
     });
 
     expect(config.preset).toBe('zen-free');
@@ -92,6 +96,7 @@ describe('providers', () => {
       hasOpenAI: false,
       hasOpencodeZen: true,
       hasTmux: false,
+      installSkills: false,
     });
 
     expect(config.preset).toBe('zen-free');
@@ -109,6 +114,7 @@ describe('providers', () => {
       hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: true,
+      installSkills: false,
     });
 
     expect(config.tmux).toBeDefined();
@@ -121,19 +127,28 @@ describe('providers', () => {
       hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: true,
     });
 
     const agents = (config.presets as any).cliproxy;
-    expect(agents.orchestrator.skills).toContain('*');
-    expect(agents.fixer.skills).toBeDefined();
+    // Orchestrator should always have '*'
+    expect(agents.orchestrator.skills).toEqual(['*']);
+
+    // Designer should have 'agent-browser'
+    expect(agents.designer.skills).toContain('agent-browser');
+
+    // Fixer should have no skills by default (empty recommended list)
+    expect(agents.fixer.skills).toEqual([]);
   });
 
+
   test('generateLiteConfig includes mcps field', () => {
     const config = generateLiteConfig({
       hasAntigravity: true,
       hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: false,
     });
 
     const agents = (config.presets as any).cliproxy;
@@ -149,6 +164,7 @@ describe('providers', () => {
       hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: false,
+      installSkills: false,
     });
 
     const agents = (config.presets as any)['zen-free'];

+ 56 - 136
src/cli/providers.ts

@@ -1,9 +1,8 @@
-import {
-  DEFAULT_AGENT_MCPS,
-  DEFAULT_AGENT_SKILLS,
-} from '../tools/skill/builtin';
+import { DEFAULT_AGENT_MCPS } from '../config/agent-mcps';
+import { RECOMMENDED_SKILLS } from './skills';
 import type { InstallConfig } from './types';
 
+
 /**
  * Provider configurations for Cliproxy (Antigravity via cliproxy)
  */
@@ -81,140 +80,61 @@ export function generateLiteConfig(
     presets: {},
   };
 
-  // Only generate preset based on user selection
-  if (installConfig.hasAntigravity && installConfig.hasOpenAI) {
-    // Mixed preset: cliproxy base with OpenAI oracle
-    (config.presets as Record<string, unknown>).cliproxy = {
-      orchestrator: {
-        model: 'cliproxy/gemini-claude-opus-4-5-thinking',
-        skills: ['*'],
-        mcps: ['websearch'],
-      },
-      oracle: {
-        model: 'openai/gpt-5.2-codex',
-        variant: 'high',
-        skills: [],
-        mcps: [],
-      },
-      librarian: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'low',
-        skills: [],
-        mcps: ['websearch', 'context7', 'grep_app'],
-      },
-      explorer: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'low',
-        skills: [],
-        mcps: [],
-      },
-      designer: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'medium',
-        skills: ['playwright'],
-        mcps: [],
-      },
-      fixer: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'low',
-        skills: [],
-        mcps: [],
-      },
-    };
-    config.preset = 'cliproxy';
-  } else if (installConfig.hasAntigravity) {
-    // Cliproxy only
-    (config.presets as Record<string, unknown>).cliproxy = {
-      orchestrator: {
-        model: 'cliproxy/gemini-claude-opus-4-5-thinking',
-        skills: ['*'],
-        mcps: ['websearch'],
-      },
-      oracle: {
-        model: 'cliproxy/gemini-3-pro-preview',
-        variant: 'high',
-        skills: [],
-        mcps: [],
-      },
-      librarian: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'low',
-        skills: [],
-        mcps: ['websearch', 'context7', 'grep_app'],
-      },
-      explorer: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'low',
-        skills: [],
-        mcps: [],
-      },
-      designer: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'medium',
-        skills: ['playwright'],
-        mcps: [],
-      },
-      fixer: {
-        model: 'cliproxy/gemini-3-flash-preview',
-        variant: 'low',
-        skills: [],
-        mcps: [],
-      },
+  // Determine active preset name
+  let activePreset: 'cliproxy' | 'openai' | 'zen-free' = 'zen-free';
+  if (installConfig.hasAntigravity) activePreset = 'cliproxy';
+  else if (installConfig.hasOpenAI) activePreset = 'openai';
+
+  config.preset = activePreset;
+
+  const createAgentConfig = (agentName: string, modelInfo: { model: string; variant?: string }) => {
+    const isOrchestrator = agentName === 'orchestrator';
+
+    // Skills: orchestrator gets "*", others get recommended skills for their role
+    const skills = isOrchestrator
+      ? ['*']
+      : RECOMMENDED_SKILLS.filter(
+        (s) =>
+          s.allowedAgents.includes('*') ||
+          s.allowedAgents.includes(agentName),
+      ).map((s) => s.skillName);
+
+    // Special case for designer and agent-browser skill
+    if (agentName === 'designer' && !skills.includes('agent-browser')) {
+      skills.push('agent-browser');
+    }
+
+    return {
+      model: modelInfo.model,
+      variant: modelInfo.variant,
+      skills,
+      mcps: DEFAULT_AGENT_MCPS[agentName as keyof typeof DEFAULT_AGENT_MCPS] ?? [],
     };
-    config.preset = 'cliproxy';
-  } else if (installConfig.hasOpenAI) {
-    // OpenAI only
-    const createAgents = (
-      models: Record<string, { model: string; variant?: string }>,
-    ): Record<
-      string,
-      { model: string; variant?: string; skills: string[]; mcps: string[] }
-    > =>
-      Object.fromEntries(
-        Object.entries(models).map(([k, v]) => [
-          k,
-          {
-            model: v.model,
-            variant: v.variant,
-            skills:
-              DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ??
-              [],
-            mcps:
-              DEFAULT_AGENT_MCPS[k as keyof typeof DEFAULT_AGENT_MCPS] ?? [],
-          },
-        ]),
-      );
-    (config.presets as Record<string, unknown>).openai = createAgents(
-      MODEL_MAPPINGS.openai,
-    );
-    config.preset = 'openai';
-  } else {
-    // Zen free only
-    const createAgents = (
-      models: Record<string, { model: string; variant?: string }>,
-    ): Record<
-      string,
-      { model: string; variant?: string; skills: string[]; mcps: string[] }
-    > =>
-      Object.fromEntries(
-        Object.entries(models).map(([k, v]) => [
-          k,
-          {
-            model: v.model,
-            variant: v.variant,
-            skills:
-              DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ??
-              [],
-            mcps:
-              DEFAULT_AGENT_MCPS[k as keyof typeof DEFAULT_AGENT_MCPS] ?? [],
-          },
-        ]),
-      );
-    (config.presets as Record<string, unknown>)['zen-free'] = createAgents(
-      MODEL_MAPPINGS['zen-free'],
+  };
+
+  const buildPreset = (mappingName: keyof typeof MODEL_MAPPINGS) => {
+    const mapping = MODEL_MAPPINGS[mappingName];
+    return Object.fromEntries(
+      Object.entries(mapping).map(([agentName, modelInfo]) => {
+        let activeModelInfo = { ...modelInfo };
+
+        // Hybrid case: Antigravity + OpenAI (use OpenAI for Oracle)
+        if (
+          activePreset === 'cliproxy' &&
+          installConfig.hasOpenAI &&
+          agentName === 'oracle'
+        ) {
+          activeModelInfo = { ...MODEL_MAPPINGS.openai.oracle };
+        }
+
+        return [agentName, createAgentConfig(agentName, activeModelInfo)];
+      }),
     );
-    config.preset = 'zen-free';
-  }
+  };
+
+  (config.presets as Record<string, unknown>)[activePreset] = buildPreset(
+    activePreset === 'cliproxy' ? 'antigravity' : activePreset,
+  );
 
   if (installConfig.hasTmux) {
     config.tmux = {

+ 42 - 0
src/cli/skills.test.ts

@@ -0,0 +1,42 @@
+import { describe, expect, it } from 'bun:test';
+import { getSkillPermissionsForAgent } from './skills';
+
+describe('skills permissions', () => {
+    it('should allow all skills for orchestrator by default', () => {
+        const permissions = getSkillPermissionsForAgent('orchestrator');
+        expect(permissions['*']).toBe('allow');
+    });
+
+    it('should deny all skills for other agents by default', () => {
+        const permissions = getSkillPermissionsForAgent('designer');
+        expect(permissions['*']).toBe('deny');
+    });
+
+    it('should allow recommended skills for specific agents', () => {
+        // Designer should have agent-browser allowed
+        const designerPerms = getSkillPermissionsForAgent('designer');
+        expect(designerPerms['agent-browser']).toBe('allow');
+
+        // Developer (orchestrator) should have simplify allowed (and everything else via *)
+        const orchPerms = getSkillPermissionsForAgent('orchestrator');
+        expect(orchPerms.simplify).toBe('allow');
+    });
+
+    it('should honor explicit skill list overrides', () => {
+        // Override with empty list
+        const emptyPerms = getSkillPermissionsForAgent('orchestrator', []);
+        expect(emptyPerms['*']).toBe('deny');
+        expect(Object.keys(emptyPerms).length).toBe(1);
+
+        // Override with specific list
+        const specificPerms = getSkillPermissionsForAgent('designer', ['my-skill', '!bad-skill']);
+        expect(specificPerms['*']).toBe('deny');
+        expect(specificPerms['my-skill']).toBe('allow');
+        expect(specificPerms['bad-skill']).toBe('deny');
+    });
+
+    it('should honor wildcard in explicit list', () => {
+        const wildcardPerms = getSkillPermissionsForAgent('designer', ['*']);
+        expect(wildcardPerms['*']).toBe('allow');
+    });
+});

+ 133 - 0
src/cli/skills.ts

@@ -0,0 +1,133 @@
+import { spawnSync } from 'node:child_process';
+
+/**
+ * A recommended skill to install via `npx skills add`.
+ */
+export interface RecommendedSkill {
+    /** Human-readable name for prompts */
+    name: string;
+    /** GitHub repo URL for `npx skills add` */
+    repo: string;
+    /** Skill name within the repo (--skill flag) */
+    skillName: string;
+    /** List of agents that should auto-allow this skill */
+    allowedAgents: string[];
+    /** Description shown to user during install */
+    description: string;
+    /** Optional commands to run after the skill is added */
+    postInstallCommands?: string[];
+}
+
+/**
+ * List of recommended skills.
+ * Add new skills here to include them in the installation flow.
+ */
+export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
+    {
+        name: 'simplify',
+        repo: 'https://github.com/brianlovin/claude-config',
+        skillName: 'simplify',
+        allowedAgents: ['orchestrator'],
+        description: 'YAGNI code simplification expert',
+    },
+    {
+        name: 'agent-browser',
+        repo: 'https://github.com/vercel-labs/agent-browser',
+        skillName: 'agent-browser',
+        allowedAgents: ['designer'],
+        description: 'High-performance browser automation',
+        postInstallCommands: [
+            'npm install -g agent-browser',
+            'agent-browser install',
+        ],
+    },
+];
+
+/**
+ * Install a skill using `npx skills add`.
+ * @param skill - The skill to install
+ * @returns True if installation succeeded, false otherwise
+ */
+export function installSkill(skill: RecommendedSkill): boolean {
+    const args = [
+        'skills',
+        'add',
+        skill.repo,
+        '--skill',
+        skill.skillName,
+        '-a',
+        'opencode',
+        '-y',
+        '--global',
+    ];
+
+    try {
+        const result = spawnSync('npx', args, { stdio: 'inherit' });
+        if (result.status !== 0) {
+            return false;
+        }
+
+        // Run post-install commands if any
+        if (skill.postInstallCommands && skill.postInstallCommands.length > 0) {
+            console.log(`Running post-install commands for ${skill.name}...`);
+            for (const cmd of skill.postInstallCommands) {
+                console.log(`> ${cmd}`);
+                const [command, ...cmdArgs] = cmd.split(' ');
+                const cmdResult = spawnSync(command, cmdArgs, { stdio: 'inherit' });
+                if (cmdResult.status !== 0) {
+                    console.warn(`Post-install command failed: ${cmd}`);
+                }
+            }
+        }
+
+        return true;
+    } catch (error) {
+        console.error(`Failed to install skill: ${skill.name}`, error);
+        return false;
+    }
+}
+
+/**
+ * Get permission presets for a specific agent based on recommended skills.
+ * @param agentName - The name of the agent
+ * @param skillList - Optional explicit list of skills to allow (overrides recommendations)
+ * @returns Permission rules for the skill permission type
+ */
+export function getSkillPermissionsForAgent(
+    agentName: string,
+    skillList?: string[],
+): Record<string, 'allow' | 'ask' | 'deny'> {
+    // Orchestrator gets all skills by default, others are restricted
+    const permissions: Record<string, 'allow' | 'ask' | 'deny'> = {
+        '*': agentName === 'orchestrator' ? 'allow' : 'deny',
+    };
+
+    // If the user provided an explicit skill list (even empty), honor it
+    if (skillList) {
+        permissions['*'] = 'deny';
+        for (const name of skillList) {
+            if (name === '*') {
+                permissions['*'] = 'allow';
+            } else if (name.startsWith('!')) {
+                permissions[name.slice(1)] = 'deny';
+            } else {
+                permissions[name] = 'allow';
+            }
+        }
+        return permissions;
+    }
+
+    // Otherwise, use recommended defaults
+    for (const skill of RECOMMENDED_SKILLS) {
+        const isAllowed =
+            skill.allowedAgents.includes('*') ||
+            skill.allowedAgents.includes(agentName);
+        if (isAllowed) {
+            permissions[skill.skillName] = 'allow';
+        }
+    }
+
+    return permissions;
+}
+
+

+ 3 - 0
src/cli/types.ts

@@ -5,6 +5,7 @@ export interface InstallArgs {
   antigravity?: BooleanArg;
   openai?: BooleanArg;
   tmux?: BooleanArg;
+  skills?: BooleanArg;
 }
 
 export interface OpenCodeConfig {
@@ -19,8 +20,10 @@ export interface InstallConfig {
   hasOpenAI: boolean;
   hasOpencodeZen: boolean;
   hasTmux: boolean;
+  installSkills: boolean;
 }
 
+
 export interface ConfigMergeResult {
   success: boolean;
   configPath: string;

+ 64 - 0
src/config/agent-mcps.ts

@@ -0,0 +1,64 @@
+import {
+    type AgentName,
+    getAgentOverride,
+    McpNameSchema,
+    type PluginConfig,
+} from '.';
+
+/** Default MCPs per agent - "*" means all MCPs, "!item" excludes specific MCPs */
+
+export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
+    orchestrator: ['websearch'],
+    designer: [],
+    oracle: [],
+    librarian: ['websearch', 'context7', 'grep_app'],
+    explorer: [],
+    fixer: [],
+};
+
+/**
+ * Parse a list with wildcard and exclusion syntax.
+ */
+export function parseList(items: string[], allAvailable: string[]): string[] {
+    if (!items || items.length === 0) {
+        return [];
+    }
+
+    const allow = items.filter((i) => !i.startsWith('!'));
+    const deny = items.filter((i) => i.startsWith('!')).map((i) => i.slice(1));
+
+    if (deny.includes('*')) {
+        return [];
+    }
+
+    if (allow.includes('*')) {
+        return allAvailable.filter((item) => !deny.includes(item));
+    }
+
+    return allow.filter((item) => !deny.includes(item));
+}
+
+/**
+ * Get available MCP names from schema and config.
+ */
+export function getAvailableMcpNames(config?: PluginConfig): string[] {
+    const builtinMcps = McpNameSchema.options;
+    const disabled = new Set(config?.disabled_mcps ?? []);
+    return builtinMcps.filter((name) => !disabled.has(name));
+}
+
+/**
+ * Get the MCP list for an agent (from config or defaults).
+ */
+export function getAgentMcpList(
+    agentName: string,
+    config?: PluginConfig,
+): string[] {
+    const agentConfig = getAgentOverride(config, agentName);
+    if (agentConfig?.mcps !== undefined) {
+        return agentConfig.mcps;
+    }
+
+    const defaultMcps = DEFAULT_AGENT_MCPS[agentName as AgentName];
+    return defaultMcps ?? [];
+}

+ 5 - 0
src/config/constants.ts

@@ -1,4 +1,9 @@
 // Agent names
+export const AGENT_ALIASES: Record<string, string> = {
+  explore: 'explorer',
+  'frontend-ui-ux-engineer': 'designer',
+};
+
 export const SUBAGENT_NAMES = [
   'explorer',
   'librarian',

+ 1 - 0
src/config/index.ts

@@ -1,3 +1,4 @@
 export * from './constants';
 export { loadAgentPrompt, loadPluginConfig } from './loader';
 export * from './schema';
+export { getAgentOverride } from './utils';

+ 23 - 0
src/config/utils.ts

@@ -0,0 +1,23 @@
+import { AGENT_ALIASES } from './constants';
+import type { AgentOverrideConfig, PluginConfig } from './schema';
+
+/**
+ * Get agent override config by name, supporting backward-compatible aliases.
+ * Checks both the current name and any legacy alias names.
+ * 
+ * @param config - The plugin configuration
+ * @param name - The current agent name
+ * @returns The agent-specific override configuration if found
+ */
+export function getAgentOverride(
+    config: PluginConfig | undefined,
+    name: string,
+): AgentOverrideConfig | undefined {
+    const overrides = config?.agents ?? {};
+    return (
+        overrides[name] ??
+        overrides[
+        Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === name) ?? ''
+        ]
+    );
+}

+ 1 - 6
src/index.ts

@@ -2,6 +2,7 @@ import type { Plugin } from '@opencode-ai/plugin';
 import { getAgentConfigs } from './agents';
 import { BackgroundTaskManager, TmuxSessionManager } from './background';
 import { loadPluginConfig, type TmuxConfig } from './config';
+import { parseList } from './config/agent-mcps';
 import {
   createAutoUpdateCheckerHook,
   createPhaseReminderHook,
@@ -13,15 +14,12 @@ import {
   ast_grep_replace,
   ast_grep_search,
   createBackgroundTools,
-  createSkillTools,
   grep,
   lsp_diagnostics,
   lsp_find_references,
   lsp_goto_definition,
   lsp_rename,
-  SkillMcpManager,
 } from './tools';
-import { parseList } from './tools/skill/builtin';
 import { startTmuxCheck } from './utils';
 import { log } from './utils/logger';
 
@@ -55,8 +53,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     config,
   );
   const mcps = createBuiltinMcps(config.disabled_mcps);
-  const skillMcpManager = SkillMcpManager.getInstance();
-  const skillTools = createSkillTools(skillMcpManager, config);
 
   // Initialize TmuxSessionManager to handle OpenCode's built-in Task tool sessions
   const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
@@ -88,7 +84,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ast_grep_search,
       ast_grep_replace,
       antigravity_quota,
-      ...skillTools,
     },
 
     mcp: mcps,

+ 0 - 3
src/tools/index.ts

@@ -14,6 +14,3 @@ export {
 
 // Antigravity quota tool
 export { antigravity_quota } from './quota';
-
-// Skill tools
-export { createSkillTools, SkillMcpManager } from './skill';

+ 0 - 642
src/tools/skill/builtin.test.ts

@@ -1,642 +0,0 @@
-import { describe, expect, test } from 'bun:test';
-import type { PluginConfig } from '../../config/schema';
-import {
-  canAgentUseMcp,
-  canAgentUseSkill,
-  DEFAULT_AGENT_MCPS,
-  DEFAULT_AGENT_SKILLS,
-  getAgentMcpList,
-  getBuiltinSkills,
-  getSkillByName,
-  getSkillsForAgent,
-  parseList,
-} from './builtin';
-
-describe('getBuiltinSkills', () => {
-  test('returns all builtin skills', () => {
-    const skills = getBuiltinSkills();
-    expect(skills.length).toBeGreaterThan(0);
-
-    const names = skills.map((s) => s.name);
-    expect(names).toContain('simplify');
-    expect(names).toContain('playwright');
-  });
-});
-
-describe('getSkillByName', () => {
-  test('returns skill by exact name', () => {
-    const skill = getSkillByName('simplify');
-    expect(skill).toBeDefined();
-    expect(skill?.name).toBe('simplify');
-  });
-
-  test('returns undefined for unknown skill', () => {
-    const skill = getSkillByName('nonexistent-skill');
-    expect(skill).toBeUndefined();
-  });
-
-  test('returns playwright skill with mcpConfig', () => {
-    const skill = getSkillByName('playwright');
-    expect(skill).toBeDefined();
-    expect(skill?.mcpConfig).toBeDefined();
-    expect(skill?.mcpConfig?.playwright).toBeDefined();
-  });
-});
-
-describe('DEFAULT_AGENT_SKILLS', () => {
-  test('orchestrator has wildcard access', () => {
-    expect(DEFAULT_AGENT_SKILLS.orchestrator).toContain('*');
-  });
-
-  test('designer has playwright skill', () => {
-    expect(DEFAULT_AGENT_SKILLS.designer).toContain('playwright');
-  });
-
-  test('oracle has no skills by default', () => {
-    expect(DEFAULT_AGENT_SKILLS.oracle).toEqual([]);
-  });
-
-  test('librarian has no skills by default', () => {
-    expect(DEFAULT_AGENT_SKILLS.librarian).toEqual([]);
-  });
-
-  test('explorer has no skills by default', () => {
-    expect(DEFAULT_AGENT_SKILLS.explorer).toEqual([]);
-  });
-
-  test('fixer has no skills by default', () => {
-    expect(DEFAULT_AGENT_SKILLS.fixer).toEqual([]);
-  });
-});
-
-describe('getSkillsForAgent', () => {
-  test('returns all skills for orchestrator (wildcard)', () => {
-    const skills = getSkillsForAgent('orchestrator');
-    const allSkills = getBuiltinSkills();
-    expect(skills.length).toBe(allSkills.length);
-  });
-
-  test('returns playwright for designer', () => {
-    const skills = getSkillsForAgent('designer');
-    const names = skills.map((s) => s.name);
-    expect(names).toContain('playwright');
-  });
-
-  test('returns empty for oracle', () => {
-    const skills = getSkillsForAgent('oracle');
-    expect(skills).toEqual([]);
-  });
-
-  test('respects config override for agent skills', () => {
-    const config: PluginConfig = {
-      agents: {
-        oracle: { skills: ['simplify'] },
-      },
-    };
-    const skills = getSkillsForAgent('oracle', config);
-    expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('simplify');
-  });
-
-  test('config wildcard overrides default', () => {
-    const config: PluginConfig = {
-      agents: {
-        explorer: { skills: ['*'] },
-      },
-    };
-    const skills = getSkillsForAgent('explorer', config);
-    const allSkills = getBuiltinSkills();
-    expect(skills.length).toBe(allSkills.length);
-  });
-
-  test('config empty array removes default skills', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { skills: [] },
-      },
-    };
-    const skills = getSkillsForAgent('designer', config);
-    expect(skills).toEqual([]);
-  });
-
-  test("backward compat: 'explore' alias config applies to explorer", () => {
-    const config: PluginConfig = {
-      agents: {
-        explore: { skills: ['playwright'] },
-      },
-    };
-    const skills = getSkillsForAgent('explorer', config);
-    expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('playwright');
-  });
-
-  test("backward compat: 'frontend-ui-ux-engineer' alias applies to designer", () => {
-    const config: PluginConfig = {
-      agents: {
-        'frontend-ui-ux-engineer': { skills: ['simplify'] },
-      },
-    };
-    const skills = getSkillsForAgent('designer', config);
-    expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('simplify');
-  });
-
-  test('returns empty for unknown agent without config', () => {
-    const skills = getSkillsForAgent('unknown-agent');
-    expect(skills).toEqual([]);
-  });
-});
-
-describe('canAgentUseSkill', () => {
-  test('orchestrator can use any skill (wildcard)', () => {
-    expect(canAgentUseSkill('orchestrator', 'simplify')).toBe(true);
-    expect(canAgentUseSkill('orchestrator', 'playwright')).toBe(true);
-    // Note: parseList doesn't filter non-existent items when using explicit allowlist
-    // but canAgentUseSkill checks against actual skill names
-  });
-
-  test('designer can use playwright', () => {
-    expect(canAgentUseSkill('designer', 'playwright')).toBe(true);
-  });
-
-  test('designer cannot use simplify by default', () => {
-    expect(canAgentUseSkill('designer', 'simplify')).toBe(false);
-  });
-
-  test('oracle cannot use any skill by default', () => {
-    expect(canAgentUseSkill('oracle', 'simplify')).toBe(false);
-    expect(canAgentUseSkill('oracle', 'playwright')).toBe(false);
-  });
-
-  test('respects config override', () => {
-    const config: PluginConfig = {
-      agents: {
-        oracle: { skills: ['simplify'] },
-      },
-    };
-    expect(canAgentUseSkill('oracle', 'simplify', config)).toBe(true);
-    expect(canAgentUseSkill('oracle', 'playwright', config)).toBe(false);
-  });
-
-  test('config wildcard grants all permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        librarian: { skills: ['*'] },
-      },
-    };
-    expect(canAgentUseSkill('librarian', 'simplify', config)).toBe(true);
-    expect(canAgentUseSkill('librarian', 'playwright', config)).toBe(true);
-    // Note: parseList expands wildcard to all available skills
-  });
-
-  test('config empty array denies all', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { skills: [] },
-      },
-    };
-    expect(canAgentUseSkill('designer', 'playwright', config)).toBe(false);
-  });
-
-  test('backward compat: alias config affects agent permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        explore: { skills: ['playwright'] },
-      },
-    };
-    expect(canAgentUseSkill('explorer', 'playwright', config)).toBe(true);
-    expect(canAgentUseSkill('explorer', 'simplify', config)).toBe(false);
-  });
-
-  test('unknown agent returns false without config', () => {
-    expect(canAgentUseSkill('unknown-agent', 'playwright')).toBe(false);
-  });
-});
-
-describe('parseList', () => {
-  test('returns empty array for empty input', () => {
-    expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
-  });
-
-  test('returns empty array for undefined input', () => {
-    expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
-  });
-
-  test('returns explicit items when no wildcard', () => {
-    expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
-  });
-
-  test('expands wildcard to all available items', () => {
-    expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
-  });
-
-  test('excludes items with ! prefix', () => {
-    expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
-  });
-
-  test('excludes multiple items with ! prefix', () => {
-    expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
-      'a',
-      'd',
-    ]);
-  });
-
-  test('deny wins in case of conflict', () => {
-    expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
-  });
-
-  test('!* denies all items', () => {
-    expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
-  });
-
-  test('!* overrides wildcard', () => {
-    expect(parseList(['*', '!*'], ['a', 'b', 'c'])).toEqual([]);
-  });
-
-  test('handles mixed allow and deny without wildcard', () => {
-    expect(parseList(['a', 'b', '!b'], ['a', 'b', 'c'])).toEqual(['a']);
-  });
-
-  test('excludes non-existent items gracefully', () => {
-    expect(parseList(['*', '!nonexistent'], ['a', 'b'])).toEqual(['a', 'b']);
-  });
-
-  test('returns explicit allowlist minus denials', () => {
-    expect(parseList(['a', 'd'], ['a', 'b', 'c'])).toEqual(['a', 'd']);
-  });
-});
-
-describe('DEFAULT_AGENT_MCPS', () => {
-  test('orchestrator has websearch MCP', () => {
-    expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
-  });
-
-  test('designer has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
-  });
-
-  test('oracle has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
-  });
-
-  test('librarian has websearch, context7, and grep_app MCPs', () => {
-    expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
-    expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
-    expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
-  });
-
-  test('explorer has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
-  });
-
-  test('fixer has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
-  });
-});
-
-describe('getAgentMcpList', () => {
-  test('returns default MCPs for orchestrator', () => {
-    const mcps = getAgentMcpList('orchestrator');
-    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
-  });
-
-  test('returns default MCPs for librarian', () => {
-    const mcps = getAgentMcpList('librarian');
-    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
-  });
-
-  test('returns empty for designer', () => {
-    const mcps = getAgentMcpList('designer');
-    expect(mcps).toEqual([]);
-  });
-
-  test('respects config override for agent MCPs', () => {
-    const config: PluginConfig = {
-      agents: {
-        oracle: { mcps: ['websearch'] },
-      },
-    };
-    const mcps = getAgentMcpList('oracle', config);
-    expect(mcps).toEqual(['websearch']);
-  });
-
-  test('config wildcard overrides default', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { mcps: ['*'] },
-      },
-    };
-    const mcps = getAgentMcpList('designer', config);
-    expect(mcps).toEqual(['*']);
-  });
-
-  test('config empty array removes default MCPs', () => {
-    const config: PluginConfig = {
-      agents: {
-        librarian: { mcps: [] },
-      },
-    };
-    const mcps = getAgentMcpList('librarian', config);
-    expect(mcps).toEqual([]);
-  });
-
-  test('backward compat: alias config applies to agent', () => {
-    const config: PluginConfig = {
-      agents: {
-        explore: { mcps: ['websearch'] },
-      },
-    };
-    const mcps = getAgentMcpList('explorer', config);
-    expect(mcps).toEqual(['websearch']);
-  });
-
-  test('returns empty for unknown agent without config', () => {
-    const mcps = getAgentMcpList('unknown-agent');
-    expect(mcps).toEqual([]);
-  });
-});
-
-describe('canAgentUseMcp', () => {
-  test('orchestrator can use websearch by default', () => {
-    expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
-  });
-
-  test('librarian can use websearch, context7, and grep_app by default', () => {
-    expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
-    expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
-    expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
-  });
-
-  test('designer cannot use any MCP by default', () => {
-    expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
-    expect(canAgentUseMcp('designer', 'context7')).toBe(false);
-  });
-
-  test('respects config override', () => {
-    const config: PluginConfig = {
-      agents: {
-        oracle: { mcps: ['websearch'] },
-      },
-    };
-    expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
-    expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
-  });
-
-  test('config wildcard grants all MCP permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { mcps: ['*'] },
-      },
-    };
-    expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
-  });
-
-  test('config wildcard grants skill MCP permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { mcps: ['*'] },
-      },
-    };
-    expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
-  });
-
-  test('config empty array denies all MCPs', () => {
-    const config: PluginConfig = {
-      agents: {
-        librarian: { mcps: [] },
-      },
-    };
-    expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
-  });
-
-  test('respects exclusion syntax', () => {
-    const config: PluginConfig = {
-      agents: {
-        orchestrator: { mcps: ['*', '!websearch'] },
-      },
-    };
-    // canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
-    // which is ['websearch'], so excluding websearch leaves empty
-    expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
-  });
-
-  test('backward compat: alias config affects agent permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        explore: { mcps: ['websearch'] },
-      },
-    };
-    expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
-    expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
-  });
-
-  test('unknown agent returns false without config', () => {
-    expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
-  });
-});
-
-describe('parseList', () => {
-  test('returns empty array for empty input', () => {
-    expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
-  });
-
-  test('returns empty array for undefined input', () => {
-    expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
-  });
-
-  test('returns explicit items when no wildcard', () => {
-    expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
-  });
-
-  test('expands wildcard to all available items', () => {
-    expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
-  });
-
-  test('excludes items with ! prefix', () => {
-    expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
-  });
-
-  test('excludes multiple items with ! prefix', () => {
-    expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
-      'a',
-      'd',
-    ]);
-  });
-
-  test('deny wins in case of conflict', () => {
-    expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
-  });
-
-  test('!* denies all items', () => {
-    expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
-  });
-});
-
-describe('DEFAULT_AGENT_MCPS', () => {
-  test('orchestrator has websearch MCP', () => {
-    expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
-  });
-
-  test('designer has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
-  });
-
-  test('oracle has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
-  });
-
-  test('librarian has websearch, context7, and grep_app MCPs', () => {
-    expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
-    expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
-    expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
-  });
-
-  test('explorer has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
-  });
-
-  test('fixer has no MCPs by default', () => {
-    expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
-  });
-});
-
-describe('getAgentMcpList', () => {
-  test('returns default MCPs for orchestrator', () => {
-    const mcps = getAgentMcpList('orchestrator');
-    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
-  });
-
-  test('returns default MCPs for librarian', () => {
-    const mcps = getAgentMcpList('librarian');
-    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
-  });
-
-  test('returns empty for designer', () => {
-    const mcps = getAgentMcpList('designer');
-    expect(mcps).toEqual([]);
-  });
-
-  test('respects config override for agent MCPs', () => {
-    const config: PluginConfig = {
-      agents: {
-        oracle: { mcps: ['websearch'] },
-      },
-    };
-    const mcps = getAgentMcpList('oracle', config);
-    expect(mcps).toEqual(['websearch']);
-  });
-
-  test('config wildcard overrides default', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { mcps: ['*'] },
-      },
-    };
-    const mcps = getAgentMcpList('designer', config);
-    expect(mcps).toEqual(['*']);
-  });
-
-  test('config empty array removes default MCPs', () => {
-    const config: PluginConfig = {
-      agents: {
-        librarian: { mcps: [] },
-      },
-    };
-    const mcps = getAgentMcpList('librarian', config);
-    expect(mcps).toEqual([]);
-  });
-
-  test('backward compat: alias config applies to agent', () => {
-    const config: PluginConfig = {
-      agents: {
-        explore: { mcps: ['websearch'] },
-      },
-    };
-    const mcps = getAgentMcpList('explorer', config);
-    expect(mcps).toEqual(['websearch']);
-  });
-
-  test('returns empty for unknown agent without config', () => {
-    const mcps = getAgentMcpList('unknown-agent');
-    expect(mcps).toEqual([]);
-  });
-});
-
-describe('canAgentUseMcp', () => {
-  test('orchestrator can use websearch by default', () => {
-    expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
-  });
-
-  test('librarian can use websearch, context7, and grep_app by default', () => {
-    expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
-    expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
-    expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
-  });
-
-  test('designer cannot use any MCP by default', () => {
-    expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
-    expect(canAgentUseMcp('designer', 'context7')).toBe(false);
-  });
-
-  test('respects config override', () => {
-    const config: PluginConfig = {
-      agents: {
-        oracle: { mcps: ['websearch'] },
-      },
-    };
-    expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
-    expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
-  });
-
-  test('config wildcard grants all MCP permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { mcps: ['*'] },
-      },
-    };
-    expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
-  });
-
-  test('config wildcard grants skill MCP permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        designer: { mcps: ['*'] },
-      },
-    };
-    expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
-  });
-
-  test('config empty array denies all MCPs', () => {
-    const config: PluginConfig = {
-      agents: {
-        librarian: { mcps: [] },
-      },
-    };
-    expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
-  });
-
-  test('respects exclusion syntax', () => {
-    const config: PluginConfig = {
-      agents: {
-        orchestrator: { mcps: ['*', '!websearch'] },
-      },
-    };
-    // canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
-    // which is ['websearch'], so excluding websearch leaves empty
-    expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
-  });
-
-  test('backward compat: alias config affects agent permissions', () => {
-    const config: PluginConfig = {
-      agents: {
-        explore: { mcps: ['websearch'] },
-      },
-    };
-    expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
-    expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
-  });
-
-  test('unknown agent returns false without config', () => {
-    expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
-  });
-});

+ 0 - 324
src/tools/skill/builtin.ts

@@ -1,324 +0,0 @@
-import type { AgentName, PluginConfig } from '../../config/schema';
-import { McpNameSchema } from '../../config/schema';
-import type { SkillDefinition } from './types';
-
-/** Map old agent names to new names for backward compatibility */
-const AGENT_ALIASES: Record<string, string> = {
-  explore: 'explorer',
-  'frontend-ui-ux-engineer': 'designer',
-};
-
-/** Default skills per agent - "*" means all skills, "!item" excludes specific skills */
-export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
-  orchestrator: ['*'],
-  designer: ['playwright'],
-  oracle: [],
-  librarian: [],
-  explorer: [],
-  fixer: [],
-};
-
-/** Default MCPs per agent - "*" means all MCPs, "!item" excludes specific MCPs */
-export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
-  orchestrator: ['websearch'],
-  designer: [],
-  oracle: [],
-  librarian: ['websearch', 'context7', 'grep_app'],
-  explorer: [],
-  fixer: [],
-};
-
-/**
- * Parse a list with wildcard and exclusion syntax.
- * Supports:
- * - "*" to expand to all available items
- * - "!item" to exclude specific items
- * - Conflicts: deny wins (principle of least privilege)
- *
- * @param items - The list to parse (may contain "*" and "!item")
- * @param allAvailable - All available items to expand "*" against
- * @returns The resolved list of allowed items
- *
- * @example
- * parseList(["*", "!playwright"], ["playwright", "yagni"]) // ["yagni"]
- * parseList(["a", "c"], ["a", "b", "c"]) // ["a", "c"]
- * parseList(["!*"], ["a", "b"]) // []
- */
-export function parseList(items: string[], allAvailable: string[]): string[] {
-  if (!items || items.length === 0) {
-    return [];
-  }
-
-  const allow = items.filter((i) => !i.startsWith('!'));
-  const deny = items.filter((i) => i.startsWith('!')).map((i) => i.slice(1));
-
-  // Handle "!*" - deny all
-  if (deny.includes('*')) {
-    return [];
-  }
-
-  // If "*" is in allow, expand to all available minus denials
-  if (allow.includes('*')) {
-    return allAvailable.filter((item) => !deny.includes(item));
-  }
-
-  // Otherwise, return explicit allowlist minus denials
-  // Deny wins in case of conflict
-  return allow.filter((item) => !deny.includes(item));
-}
-
-const YAGNI_TEMPLATE = `# Simplify Skill
-
-You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
-
-When reviewing code, you will:
-
-1. **Analyze Every Line**: Question the necessity of each line of code. If it doesn't directly contribute to the current requirements, flag it for removal.
-
-2. **Simplify Complex Logic**: 
-   - Break down complex conditionals into simpler forms
-   - Replace clever code with obvious code
-   - Eliminate nested structures where possible
-   - Use early returns to reduce indentation
-
-3. **Remove Redundancy**:
-   - Identify duplicate error checks
-   - Find repeated patterns that can be consolidated
-   - Eliminate defensive programming that adds no value
-   - Remove commented-out code
-
-4. **Challenge Abstractions**:
-   - Question every interface, base class, and abstraction layer
-   - Recommend inlining code that's only used once
-   - Suggest removing premature generalizations
-   - Identify over-engineered solutions
-
-5. **Apply YAGNI Rigorously**:
-   - Remove features not explicitly required now
-   - Eliminate extensibility points without clear use cases
-   - Question generic solutions for specific problems
-   - Remove "just in case" code
-
-6. **Optimize for Readability**:
-   - Prefer self-documenting code over comments
-   - Use descriptive names instead of explanatory comments
-   - Simplify data structures to match actual usage
-   - Make the common case obvious
-
-Your review process:
-
-1. First, identify the core purpose of the code
-2. List everything that doesn't directly serve that purpose
-3. For each complex section, propose a simpler alternative
-4. Create a prioritized list of simplification opportunities
-5. Estimate the lines of code that can be removed
-
-Output format:
-
-\`\`\`markdown
-## Simplification Analysis
-
-### Core Purpose
-[Clearly state what this code actually needs to do]
-
-### Unnecessary Complexity Found
-- [Specific issue with line numbers/file]
-- [Why it's unnecessary]
-- [Suggested simplification]
-
-### Code to Remove
-- [File:lines] - [Reason]
-- [Estimated LOC reduction: X]
-
-### Simplification Recommendations
-1. [Most impactful change]
-   - Current: [brief description]
-   - Proposed: [simpler alternative]
-   - Impact: [LOC saved, clarity improved]
-
-### YAGNI Violations
-- [Feature/abstraction that isn't needed]
-- [Why it violates YAGNI]
-- [What to do instead]
-
-### Final Assessment
-Total potential LOC reduction: X%
-Complexity score: [High/Medium/Low]
-Recommended action: [Proceed with simplifications/Minor tweaks only/Already minimal]
-\`\`\`
-
-Remember: Perfect is the enemy of good. The simplest code that works is often the best code. Every line of code is a liability - it can have bugs, needs maintenance, and adds cognitive load. Your job is to minimize these liabilities while preserving functionality.`;
-
-const PLAYWRIGHT_TEMPLATE = `# Playwright Browser Automation Skill
-
-This skill provides browser automation capabilities via the Playwright MCP server.
-
-**Capabilities**:
-- Navigate to web pages
-- Click elements and interact with UI
-- Fill forms and submit data
-- Take screenshots
-- Extract content from pages
-- Verify visual state
-- Run automated tests
-
-**Common Use Cases**:
-- Verify frontend changes visually
-- Test responsive design across viewports
-- Capture screenshots for documentation
-- Scrape web content
-- Automate browser-based workflows
-
-**Process**:
-1. Load the skill to access MCP tools
-2. Use playwright MCP tools for browser automation
-3. Screenshots are saved to a session subdirectory (check tool output for exact path)
-4. Report results with screenshot paths when relevant
-
-**Example Workflow** (Designer agent):
-1. Make UI changes to component
-2. Use playwright to open page
-3. Take screenshot of before/after
-4. Verify responsive behavior
-5. Return results with visual proof`;
-
-const yagniEnforcementSkill: SkillDefinition = {
-  name: 'simplify',
-  description:
-    'Code complexity analysis and YAGNI enforcement. Use after major refactors or before finalizing PRs to simplify code.',
-  template: YAGNI_TEMPLATE,
-};
-
-const playwrightSkill: SkillDefinition = {
-  name: 'playwright',
-  description:
-    'MUST USE for any browser-related tasks. Browser automation via Playwright MCP - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.',
-  template: PLAYWRIGHT_TEMPLATE,
-  mcpConfig: {
-    playwright: {
-      command: 'npx',
-      args: ['@playwright/mcp@latest'],
-    },
-  },
-};
-
-const builtinSkillsMap = new Map<string, SkillDefinition>([
-  [yagniEnforcementSkill.name, yagniEnforcementSkill],
-  [playwrightSkill.name, playwrightSkill],
-]);
-
-export function getBuiltinSkills(): SkillDefinition[] {
-  return Array.from(builtinSkillsMap.values());
-}
-
-export function getSkillByName(name: string): SkillDefinition | undefined {
-  return builtinSkillsMap.get(name);
-}
-
-export function getAvailableMcpNames(config?: PluginConfig): string[] {
-  const builtinMcps = McpNameSchema.options;
-  const skillMcps = getBuiltinSkills().flatMap((skill) =>
-    Object.keys(skill.mcpConfig ?? {}),
-  );
-  const disabled = new Set(config?.disabled_mcps ?? []);
-  const allMcps = Array.from(new Set([...builtinMcps, ...skillMcps]));
-  return allMcps.filter((name) => !disabled.has(name));
-}
-
-/**
- * Get skills available for a specific agent
- * @param agentName - The name of the agent
- * @param config - Optional plugin config with agent overrides
- */
-export function getSkillsForAgent(
-  agentName: string,
-  config?: PluginConfig,
-): SkillDefinition[] {
-  const allSkills = getBuiltinSkills();
-  const allSkillNames = allSkills.map((s) => s.name);
-  const agentSkills = parseList(
-    getAgentSkillList(agentName, config),
-    allSkillNames,
-  );
-
-  return allSkills.filter((skill) => agentSkills.includes(skill.name));
-}
-
-/**
- * Check if an agent can use a specific skill
- */
-export function canAgentUseSkill(
-  agentName: string,
-  skillName: string,
-  config?: PluginConfig,
-): boolean {
-  const allSkills = getBuiltinSkills();
-  const allSkillNames = allSkills.map((s) => s.name);
-  const agentSkills = parseList(
-    getAgentSkillList(agentName, config),
-    allSkillNames,
-  );
-
-  return agentSkills.includes(skillName);
-}
-
-/**
- * Check if an agent can use a specific MCP
- */
-export function canAgentUseMcp(
-  agentName: string,
-  mcpName: string,
-  config?: PluginConfig,
-): boolean {
-  const agentMcps = parseList(
-    getAgentMcpList(agentName, config),
-    getAvailableMcpNames(config),
-  );
-
-  return agentMcps.includes(mcpName);
-}
-
-/**
- * Get the skill list for an agent (from config or defaults)
- * Supports backward compatibility with old agent names via AGENT_ALIASES
- */
-function getAgentSkillList(agentName: string, config?: PluginConfig): string[] {
-  // Check if config has override for this agent (new name first, then alias)
-  const agentConfig =
-    config?.agents?.[agentName] ??
-    config?.agents?.[
-      Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === agentName) ??
-        ''
-    ];
-  if (agentConfig?.skills !== undefined) {
-    return agentConfig.skills;
-  }
-
-  // Fall back to defaults
-  const defaultSkills = DEFAULT_AGENT_SKILLS[agentName as AgentName];
-  return defaultSkills ?? [];
-}
-
-/**
- * Get the MCP list for an agent (from config or defaults)
- * Supports backward compatibility with old agent names via AGENT_ALIASES
- */
-export function getAgentMcpList(
-  agentName: string,
-  config?: PluginConfig,
-): string[] {
-  // Check if config has override for this agent (new name first, then alias)
-  const agentConfig =
-    config?.agents?.[agentName] ??
-    config?.agents?.[
-      Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === agentName) ??
-        ''
-    ];
-  if (agentConfig?.mcps !== undefined) {
-    return agentConfig.mcps;
-  }
-
-  // Fall back to defaults
-  const defaultMcps = DEFAULT_AGENT_MCPS[agentName as AgentName];
-  return defaultMcps ?? [];
-}

+ 0 - 6
src/tools/skill/constants.ts

@@ -1,6 +0,0 @@
-export const SKILL_TOOL_DESCRIPTION = `Loads a skill and returns its instructions and available MCP tools. Use this to activate specialized capabilities like Playwright browser automation.`;
-
-export const SKILL_LIST_TOOL_DESCRIPTION =
-  'Lists skills available to the current agent.';
-
-export const SKILL_MCP_TOOL_DESCRIPTION = `Invokes a tool exposed by a skill's MCP server. Use after loading a skill to perform actions like browser automation.`;

+ 0 - 8
src/tools/skill/index.ts

@@ -1,8 +0,0 @@
-export { SkillMcpManager } from './mcp-manager';
-export { createSkillTools } from './tools';
-export type {
-  McpServerConfig,
-  SkillArgs,
-  SkillDefinition,
-  SkillMcpArgs,
-} from './types';

+ 0 - 15
src/tools/skill/mcp-manager.test.ts

@@ -1,15 +0,0 @@
-import { describe, expect, test } from 'bun:test';
-import { SkillMcpManager } from './mcp-manager';
-
-describe('SkillMcpManager', () => {
-  test('returns singleton instance', () => {
-    const instance1 = SkillMcpManager.getInstance();
-    const instance2 = SkillMcpManager.getInstance();
-
-    expect(instance1).toBe(instance2);
-    expect(instance1).toBeDefined();
-  });
-});
-
-// Note: Connection and tool-calling tests require actual MCP servers
-// and are better suited for integration tests, not unit tests.

+ 0 - 307
src/tools/skill/mcp-manager.ts

@@ -1,307 +0,0 @@
-import { Client } from '@modelcontextprotocol/sdk/client/index.js';
-import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
-import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
-import type {
-  Prompt,
-  Resource,
-  Tool,
-} from '@modelcontextprotocol/sdk/types.js';
-import type {
-  ConnectionType,
-  ManagedClient,
-  ManagedHttpClient,
-  ManagedStdioClient,
-  McpServerConfig,
-  SkillMcpClientInfo,
-} from './types';
-
-function getConnectionType(config: McpServerConfig): ConnectionType {
-  return 'url' in config ? 'http' : 'stdio';
-}
-
-export class SkillMcpManager {
-  private static instance: SkillMcpManager;
-  private clients: Map<string, ManagedClient> = new Map();
-  private pendingConnections: Map<string, Promise<Client>> = new Map();
-  private cleanupInterval: ReturnType<typeof setInterval> | null = null;
-  private readonly IDLE_TIMEOUT = 5 * 60 * 1000;
-
-  private constructor() {
-    this.startCleanupTimer();
-    this.registerProcessCleanup();
-  }
-
-  static getInstance(): SkillMcpManager {
-    if (!SkillMcpManager.instance) {
-      SkillMcpManager.instance = new SkillMcpManager();
-    }
-    return SkillMcpManager.instance;
-  }
-
-  private registerProcessCleanup(): void {
-    const cleanup = () => {
-      for (const [, managed] of this.clients) {
-        try {
-          managed.client.close();
-        } catch {}
-        try {
-          managed.transport.close();
-        } catch {}
-      }
-      this.clients.clear();
-      if (this.cleanupInterval) {
-        clearInterval(this.cleanupInterval);
-        this.cleanupInterval = null;
-      }
-    };
-
-    process.on('exit', cleanup);
-    process.on('SIGINT', () => {
-      cleanup();
-      process.exit(0);
-    });
-    process.on('SIGTERM', () => {
-      cleanup();
-      process.exit(0);
-    });
-  }
-
-  private getClientKey(info: SkillMcpClientInfo): string {
-    return `${info.sessionId}:${info.skillName}:${info.serverName}`;
-  }
-
-  private async createClient(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Client> {
-    const connectionType = getConnectionType(config);
-
-    if (connectionType === 'http') {
-      return this.createHttpClient(info, config);
-    }
-
-    return this.createStdioClient(info, config);
-  }
-
-  private async createHttpClient(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Client> {
-    if (!('url' in config)) {
-      throw new Error(
-        `MCP server "${info.serverName}" missing url for HTTP connection.`,
-      );
-    }
-
-    const url = new URL(config.url);
-    const requestInit: RequestInit = {};
-    if (config.headers && Object.keys(config.headers).length > 0) {
-      requestInit.headers = config.headers;
-    }
-
-    const transport = new StreamableHTTPClientTransport(url, {
-      requestInit:
-        Object.keys(requestInit).length > 0 ? requestInit : undefined,
-    });
-
-    const client = new Client(
-      {
-        name: `skill-mcp-${info.skillName}-${info.serverName}`,
-        version: '1.0.0',
-      },
-      { capabilities: {} },
-    );
-
-    try {
-      await client.connect(transport);
-    } catch (error) {
-      try {
-        await transport.close();
-      } catch {
-        // ignore transport close errors
-      }
-      const errorMessage =
-        error instanceof Error ? error.message : String(error);
-      throw new Error(
-        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`,
-      );
-    }
-
-    const managedClient: ManagedHttpClient = {
-      client,
-      transport,
-      skillName: info.skillName,
-      lastUsedAt: Date.now(),
-      connectionType: 'http',
-    };
-
-    this.clients.set(this.getClientKey(info), managedClient);
-    this.startCleanupTimer();
-    return client;
-  }
-
-  private async createStdioClient(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Client> {
-    if (!('command' in config)) {
-      throw new Error(
-        `MCP server "${info.serverName}" missing command for stdio connection.`,
-      );
-    }
-
-    const transport = new StdioClientTransport({
-      command: config.command,
-      args: config.args || [],
-      env: config.env,
-      stderr: 'ignore',
-    });
-
-    const client = new Client(
-      {
-        name: `skill-mcp-${info.skillName}-${info.serverName}`,
-        version: '1.0.0',
-      },
-      { capabilities: {} },
-    );
-
-    try {
-      await client.connect(transport);
-    } catch (error) {
-      try {
-        await transport.close();
-      } catch {
-        // ignore transport close errors
-      }
-      const errorMessage =
-        error instanceof Error ? error.message : String(error);
-      throw new Error(
-        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`,
-      );
-    }
-
-    const managedClient: ManagedStdioClient = {
-      client,
-      transport,
-      skillName: info.skillName,
-      lastUsedAt: Date.now(),
-      connectionType: 'stdio',
-    };
-
-    this.clients.set(this.getClientKey(info), managedClient);
-    this.startCleanupTimer();
-    return client;
-  }
-
-  private async getOrCreateClient(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Client> {
-    const key = this.getClientKey(info);
-    const existing = this.clients.get(key);
-    if (existing) {
-      existing.lastUsedAt = Date.now();
-      return existing.client;
-    }
-
-    const pending = this.pendingConnections.get(key);
-    if (pending) {
-      return pending;
-    }
-
-    const connectionPromise = this.createClient(info, config);
-    this.pendingConnections.set(key, connectionPromise);
-
-    try {
-      return await connectionPromise;
-    } finally {
-      this.pendingConnections.delete(key);
-    }
-  }
-
-  async listTools(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Tool[]> {
-    const client = await this.getOrCreateClient(info, config);
-    const result = await client.listTools();
-    return result.tools;
-  }
-
-  async listResources(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Resource[]> {
-    const client = await this.getOrCreateClient(info, config);
-    const result = await client.listResources();
-    return result.resources;
-  }
-
-  async listPrompts(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-  ): Promise<Prompt[]> {
-    const client = await this.getOrCreateClient(info, config);
-    const result = await client.listPrompts();
-    return result.prompts;
-  }
-
-  async callTool(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-    name: string,
-    args: Record<string, unknown>,
-  ): Promise<unknown> {
-    const client = await this.getOrCreateClient(info, config);
-    const result = await client.callTool({ name, arguments: args });
-    return result.content;
-  }
-
-  async readResource(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-    uri: string,
-  ): Promise<unknown> {
-    const client = await this.getOrCreateClient(info, config);
-    const result = await client.readResource({ uri });
-    return result.contents;
-  }
-
-  async getPrompt(
-    info: SkillMcpClientInfo,
-    config: McpServerConfig,
-    name: string,
-    args: Record<string, string>,
-  ): Promise<unknown> {
-    const client = await this.getOrCreateClient(info, config);
-    const result = await client.getPrompt({ name, arguments: args });
-    return result.messages;
-  }
-
-  private startCleanupTimer(): void {
-    if (this.cleanupInterval) return;
-    this.cleanupInterval = setInterval(() => {
-      this.cleanupIdleClients();
-    }, 60_000);
-    this.cleanupInterval.unref();
-  }
-
-  private async cleanupIdleClients(): Promise<void> {
-    const now = Date.now();
-    for (const [key, managed] of this.clients) {
-      if (now - managed.lastUsedAt > this.IDLE_TIMEOUT) {
-        this.clients.delete(key);
-        try {
-          await managed.client.close();
-        } catch {
-          // ignore close errors
-        }
-        try {
-          await managed.transport.close();
-        } catch {
-          // ignore transport close errors
-        }
-      }
-    }
-  }
-}

+ 0 - 277
src/tools/skill/tools.ts

@@ -1,277 +0,0 @@
-import type {
-  Prompt,
-  Resource,
-  Tool,
-} from '@modelcontextprotocol/sdk/types.js';
-import { type ToolDefinition, tool } from '@opencode-ai/plugin';
-import type { PluginConfig } from '../../config/schema';
-import {
-  canAgentUseMcp,
-  canAgentUseSkill,
-  getBuiltinSkills,
-  getSkillByName,
-  getSkillsForAgent,
-} from './builtin';
-import {
-  SKILL_LIST_TOOL_DESCRIPTION,
-  SKILL_MCP_TOOL_DESCRIPTION,
-  SKILL_TOOL_DESCRIPTION,
-} from './constants';
-import type { SkillMcpManager } from './mcp-manager';
-import type { SkillArgs, SkillDefinition, SkillMcpArgs } from './types';
-
-type ToolContext = {
-  sessionID: string;
-  messageID: string;
-  agent: string;
-  abort: AbortSignal;
-};
-
-function formatSkillsList(skills: SkillDefinition[]): string {
-  if (skills.length === 0) return 'No skills available for this agent.';
-
-  return skills
-    .map((skill) => `- ${skill.name}: ${skill.description}`)
-    .join('\n');
-}
-
-async function formatMcpCapabilities(
-  skill: SkillDefinition,
-  manager: SkillMcpManager,
-  sessionId: string,
-  agentName: string,
-  pluginConfig?: PluginConfig,
-): Promise<string | null> {
-  if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) {
-    return null;
-  }
-
-  const sections: string[] = ['', '## Available MCP Servers', ''];
-
-  for (const [serverName, config] of Object.entries(skill.mcpConfig)) {
-    // Check if this agent can use this MCP
-    if (!canAgentUseMcp(agentName, serverName, pluginConfig)) {
-      continue; // Skip this MCP - agent doesn't have permission
-    }
-
-    const info = {
-      serverName,
-      skillName: skill.name,
-      sessionId,
-    };
-
-    sections.push(`### ${serverName}`);
-    sections.push('');
-
-    try {
-      const [tools, resources, prompts] = await Promise.all([
-        manager.listTools(info, config).catch(() => []),
-        manager.listResources(info, config).catch(() => []),
-        manager.listPrompts(info, config).catch(() => []),
-      ]);
-
-      if (tools.length > 0) {
-        sections.push('**Tools:**');
-        sections.push('');
-        for (const t of tools as Tool[]) {
-          sections.push(`#### \`${t.name}\``);
-          if (t.description) {
-            sections.push(t.description);
-          }
-          sections.push('');
-          sections.push('**inputSchema:**');
-          sections.push('```json');
-          sections.push(JSON.stringify(t.inputSchema, null, 2));
-          sections.push('```');
-          sections.push('');
-        }
-      }
-
-      if (resources.length > 0) {
-        sections.push(
-          `**Resources**: ${(resources as Resource[])
-            .map((r) => r.uri)
-            .join(', ')}`,
-        );
-      }
-
-      if (prompts.length > 0) {
-        sections.push(
-          `**Prompts**: ${(prompts as Prompt[]).map((p) => p.name).join(', ')}`,
-        );
-      }
-
-      if (
-        tools.length === 0 &&
-        resources.length === 0 &&
-        prompts.length === 0
-      ) {
-        sections.push('*No capabilities discovered*');
-      }
-    } catch (error) {
-      const errorMessage =
-        error instanceof Error ? error.message : String(error);
-      sections.push(`*Failed to connect: ${errorMessage.split('\n')[0]}*`);
-    }
-
-    sections.push('');
-    sections.push(
-      `Use \`omos_skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`,
-    );
-    sections.push('');
-  }
-
-  return sections.join('\n');
-}
-
-export function createSkillTools(
-  manager: SkillMcpManager,
-  pluginConfig?: PluginConfig,
-): {
-  omos_skill: ToolDefinition;
-  omos_skill_list: ToolDefinition;
-  omos_skill_mcp: ToolDefinition;
-} {
-  const allSkills = getBuiltinSkills();
-  const description = SKILL_TOOL_DESCRIPTION;
-
-  const skill: ToolDefinition = tool({
-    description,
-    args: {
-      name: tool.schema
-        .string()
-        .describe('The skill identifier from available_skills'),
-    },
-    async execute(args: SkillArgs, toolContext) {
-      const tctx = toolContext as ToolContext | undefined;
-      const sessionId = tctx?.sessionID ? String(tctx.sessionID) : 'unknown';
-      const agentName = tctx?.agent ?? 'orchestrator';
-
-      const skillDefinition = getSkillByName(args.name);
-      if (!skillDefinition) {
-        const available = allSkills.map((s) => s.name).join(', ');
-        throw new Error(
-          `Skill "${args.name}" not found. Available skills: ${available || 'none'}`,
-        );
-      }
-
-      // Check if this agent can use this skill
-      if (!canAgentUseSkill(agentName, args.name, pluginConfig)) {
-        const allowedSkills = getSkillsForAgent(agentName, pluginConfig);
-        const allowedNames = allowedSkills.map((s) => s.name).join(', ');
-        throw new Error(
-          `Agent "${agentName}" cannot use skill "${args.name}". ` +
-            `Available skills for this agent: ${allowedNames || 'none'}`,
-        );
-      }
-
-      const output = [
-        `## Skill: ${skillDefinition.name}`,
-        '',
-        skillDefinition.template.trim(),
-      ];
-
-      if (skillDefinition.mcpConfig) {
-        const mcpInfo = await formatMcpCapabilities(
-          skillDefinition,
-          manager,
-          sessionId,
-          agentName,
-          pluginConfig,
-        );
-        if (mcpInfo) {
-          output.push(mcpInfo);
-        }
-      }
-
-      return output.join('\n');
-    },
-  });
-
-  const skill_list: ToolDefinition = tool({
-    description: SKILL_LIST_TOOL_DESCRIPTION,
-    args: {},
-    async execute(_, toolContext) {
-      const tctx = toolContext as ToolContext | undefined;
-      const agentName = tctx?.agent ?? 'orchestrator';
-      const skills = getSkillsForAgent(agentName, pluginConfig);
-      return formatSkillsList(skills);
-    },
-  });
-
-  const skill_mcp: ToolDefinition = tool({
-    description: SKILL_MCP_TOOL_DESCRIPTION,
-    args: {
-      skillName: tool.schema
-        .string()
-        .describe('Skill name that provides the MCP'),
-      mcpName: tool.schema.string().describe('MCP server name'),
-      toolName: tool.schema.string().describe('Tool name to invoke'),
-      toolArgs: tool.schema
-        .record(tool.schema.string(), tool.schema.any())
-        .optional(),
-    },
-    async execute(args: SkillMcpArgs, toolContext) {
-      const tctx = toolContext as ToolContext | undefined;
-      const sessionId = tctx?.sessionID ? String(tctx.sessionID) : 'unknown';
-      const agentName = tctx?.agent ?? 'orchestrator';
-
-      const skillDefinition = getSkillByName(args.skillName);
-      if (!skillDefinition) {
-        const available = allSkills.map((s) => s.name).join(', ');
-        throw new Error(
-          `Skill "${args.skillName}" not found. Available skills: ${available || 'none'}`,
-        );
-      }
-
-      // Check if this agent can use this skill
-      if (!canAgentUseSkill(agentName, args.skillName, pluginConfig)) {
-        throw new Error(
-          `Agent "${agentName}" cannot use skill "${args.skillName}".`,
-        );
-      }
-
-      // Check if this agent can use this MCP
-      if (!canAgentUseMcp(agentName, args.mcpName, pluginConfig)) {
-        throw new Error(
-          `Agent "${agentName}" cannot use MCP "${args.mcpName}".`,
-        );
-      }
-
-      if (
-        !skillDefinition.mcpConfig ||
-        !skillDefinition.mcpConfig[args.mcpName]
-      ) {
-        throw new Error(
-          `Skill "${args.skillName}" has no MCP named "${args.mcpName}".`,
-        );
-      }
-
-      const config = skillDefinition.mcpConfig[args.mcpName];
-      const info = {
-        serverName: args.mcpName,
-        skillName: skillDefinition.name,
-        sessionId,
-      };
-
-      const result = await manager.callTool(
-        info,
-        config,
-        args.toolName,
-        args.toolArgs || {},
-      );
-
-      if (typeof result === 'string') {
-        return result;
-      }
-
-      return JSON.stringify(result);
-    },
-  });
-
-  return {
-    omos_skill: skill,
-    omos_skill_list: skill_list,
-    omos_skill_mcp: skill_mcp,
-  };
-}

+ 0 - 104
src/tools/skill/types.ts

@@ -1,104 +0,0 @@
-import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
-import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
-import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
-
-/**
- * Stdio MCP server configuration (local process)
- */
-export interface StdioMcpServer {
-  type?: 'stdio';
-  command: string;
-  args?: string[];
-  env?: Record<string, string>;
-}
-
-/**
- * HTTP MCP server configuration (remote server)
- */
-export interface HttpMcpServer {
-  type: 'http' | 'sse';
-  url: string;
-  headers?: Record<string, string>;
-}
-
-/**
- * MCP server configuration - either stdio or http
- */
-export type McpServerConfig = StdioMcpServer | HttpMcpServer;
-
-/**
- * Skill MCP configuration - map of server names to their configs
- */
-export type SkillMcpConfig = Record<string, McpServerConfig>;
-
-/**
- * Skill definition
- */
-export interface SkillDefinition {
-  name: string;
-  description: string;
-  template: string;
-  mcpConfig?: SkillMcpConfig;
-}
-
-/**
- * Info for identifying a managed MCP client
- */
-export interface SkillMcpClientInfo {
-  serverName: string;
-  skillName: string;
-  sessionId: string;
-}
-
-/**
- * Connection type for managed clients
- */
-export type ConnectionType = 'stdio' | 'http';
-
-/**
- * Base interface for managed MCP clients
- */
-interface ManagedClientBase {
-  client: Client;
-  skillName: string;
-  lastUsedAt: number;
-  connectionType: ConnectionType;
-}
-
-/**
- * Managed stdio client
- */
-export interface ManagedStdioClient extends ManagedClientBase {
-  connectionType: 'stdio';
-  transport: StdioClientTransport;
-}
-
-/**
- * Managed HTTP client
- */
-export interface ManagedHttpClient extends ManagedClientBase {
-  connectionType: 'http';
-  transport: StreamableHTTPClientTransport;
-}
-
-/**
- * Managed client - either stdio or http
- */
-export type ManagedClient = ManagedStdioClient | ManagedHttpClient;
-
-/**
- * Args for the skill tool
- */
-export interface SkillArgs {
-  name: string;
-}
-
-/**
- * Args for the skill_mcp tool
- */
-export interface SkillMcpArgs {
-  skillName: string;
-  mcpName: string;
-  toolName: string;
-  toolArgs?: Record<string, unknown>;
-}