From ee7916c732ae1418ec05c228037577eb787a1f26 Mon Sep 17 00:00:00 2001 From: buuzzy Date: Fri, 17 Apr 2026 09:23:07 +0800 Subject: [PATCH] feat: per-skill enable/disable configuration Users can toggle individual skills on/off via API without deleting files. Config persisted to ~/.workany/skills-config.json. - shared/skills/config.ts: persistent enable/disable state - shared/skills/register.ts: SDK registration respecting disabled list - app/api/skills.ts: GET/POST /skills/config endpoints --- src-api/src/app/api/skills.ts | 45 ++++++++++++++++++ src-api/src/shared/skills/config.ts | 67 +++++++++++++++++++++++++++ src-api/src/shared/skills/index.ts | 2 + src-api/src/shared/skills/register.ts | 62 +++++++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 src-api/src/app/api/skills.ts create mode 100644 src-api/src/shared/skills/config.ts create mode 100644 src-api/src/shared/skills/register.ts diff --git a/src-api/src/app/api/skills.ts b/src-api/src/app/api/skills.ts new file mode 100644 index 0000000..9c5a670 --- /dev/null +++ b/src-api/src/app/api/skills.ts @@ -0,0 +1,45 @@ +/** + * Skills Configuration API Routes + * + * Manages per-skill enable/disable state. + */ + +import { Hono } from 'hono'; +import { + getDisabledSkills, + setSkillEnabled, + saveSkillsConfig, +} from '@/shared/skills/config'; + +export const skillsRoutes = new Hono(); + +/** + * GET /skills/config — list disabled skills + */ +skillsRoutes.get('/config', (c) => { + return c.json({ disabledSkills: getDisabledSkills() }); +}); + +/** + * POST /skills/config — bulk update disabled skills list + */ +skillsRoutes.post('/config', async (c) => { + const body = await c.req.json<{ disabledSkills: string[] }>(); + if (!Array.isArray(body.disabledSkills)) { + return c.json({ error: 'disabledSkills must be an array' }, 400); + } + saveSkillsConfig({ disabledSkills: body.disabledSkills }); + return c.json({ ok: true, disabledSkills: body.disabledSkills }); +}); + +/** + * POST /skills/toggle — toggle a single skill + */ +skillsRoutes.post('/toggle', async (c) => { + const body = await c.req.json<{ name: string; enabled: boolean }>(); + if (!body.name) { + return c.json({ error: 'name is required' }, 400); + } + setSkillEnabled(body.name, body.enabled); + return c.json({ ok: true, name: body.name, enabled: body.enabled }); +}); diff --git a/src-api/src/shared/skills/config.ts b/src-api/src/shared/skills/config.ts new file mode 100644 index 0000000..64a98a4 --- /dev/null +++ b/src-api/src/shared/skills/config.ts @@ -0,0 +1,67 @@ +/** + * Per-skill enable/disable configuration. + * + * Stores a list of disabled skill names in ~/.workany/skills-config.json. + * All skills are enabled by default; only explicitly disabled ones are listed. + */ + +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { getAppDir } from '@/config/constants'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface SkillsConfigFile { + disabledSkills: string[]; +} + +// --------------------------------------------------------------------------- +// Persistence +// --------------------------------------------------------------------------- + +function configPath(): string { + return join(getAppDir(), 'skills-config.json'); +} + +export function loadSkillsConfig(): SkillsConfigFile { + const p = configPath(); + if (!existsSync(p)) return { disabledSkills: [] }; + try { + const data = JSON.parse(readFileSync(p, 'utf-8')) as SkillsConfigFile; + return { disabledSkills: Array.isArray(data.disabledSkills) ? data.disabledSkills : [] }; + } catch { + return { disabledSkills: [] }; + } +} + +export function saveSkillsConfig(config: SkillsConfigFile): void { + const dir = getAppDir(); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(configPath(), JSON.stringify(config, null, 2), 'utf-8'); +} + +// --------------------------------------------------------------------------- +// Convenience helpers +// --------------------------------------------------------------------------- + +export function isSkillDisabled(skillName: string): boolean { + const config = loadSkillsConfig(); + return config.disabledSkills.includes(skillName); +} + +export function setSkillEnabled(skillName: string, enabled: boolean): void { + const config = loadSkillsConfig(); + const set = new Set(config.disabledSkills); + if (enabled) { + set.delete(skillName); + } else { + set.add(skillName); + } + saveSkillsConfig({ disabledSkills: [...set] }); +} + +export function getDisabledSkills(): string[] { + return loadSkillsConfig().disabledSkills; +} diff --git a/src-api/src/shared/skills/index.ts b/src-api/src/shared/skills/index.ts index 1610e8d..bd2b1af 100644 --- a/src-api/src/shared/skills/index.ts +++ b/src-api/src/shared/skills/index.ts @@ -16,3 +16,5 @@ export { type SkillMetadata, type SkillsConfig, } from './loader'; + +export { registerFilesystemSkills } from "./register"; diff --git a/src-api/src/shared/skills/register.ts b/src-api/src/shared/skills/register.ts new file mode 100644 index 0000000..a8fc51b --- /dev/null +++ b/src-api/src/shared/skills/register.ts @@ -0,0 +1,62 @@ +/** + * Filesystem Skills → SDK Registry Bridge + * + * Loads SKILL.md-based skills from ~/.workany/skills/ and registers them + * with @codeany/open-agent-sdk's skill registry so the Agent's Skill + * tool can discover and invoke them at runtime. + */ + +import { registerSkill } from '@codeany/open-agent-sdk'; +import type { SkillContentBlock } from '@codeany/open-agent-sdk'; + +import { loadAllSkills } from './loader'; +import type { LoadedSkill } from './loader'; +import { getDisabledSkills } from './config'; + +function buildGetPrompt(skill: LoadedSkill) { + return async (args: string): Promise => { + const contextNote = args + ? `\n\n## User Arguments\n${args}` + : ''; + + return [{ type: 'text', text: skill.content + contextNote }]; + }; +} + +/** + * Load all filesystem skills and register enabled ones with the SDK. + * Skills listed in ~/.workany/skills-config.json as disabled are skipped. + * Safe to call multiple times; duplicate names are silently skipped by the registry. + */ +export async function registerFilesystemSkills(): Promise { + const skills = await loadAllSkills(); + const disabled = new Set(getDisabledSkills()); + + let registered = 0; + for (const skill of skills) { + if (disabled.has(skill.name)) { + console.log(`[Skills] Skipped disabled skill: ${skill.name}`); + continue; + } + + registerSkill({ + name: skill.name, + description: skill.metadata.description || skill.name, + whenToUse: skill.metadata.description, + argumentHint: skill.metadata.argumentHint, + userInvocable: true, + allowedTools: ['Bash', 'Read', 'Write', 'Grep', 'Glob', 'WebFetch', 'WebSearch'], + getPrompt: buildGetPrompt(skill), + }); + + console.log(`[Skills] Registered SDK skill: ${skill.name}`); + registered++; + } + + if (disabled.size > 0) { + console.log(`[Skills] ${disabled.size} skill(s) disabled, ${registered} registered`); + } else { + console.log(`[Skills] Total registered: ${registered} filesystem skill(s)`); + } + return registered; +}