Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src-api/src/app/api/skills.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
67 changes: 67 additions & 0 deletions src-api/src/shared/skills/config.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src-api/src/shared/skills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export {
type SkillMetadata,
type SkillsConfig,
} from './loader';

export { registerFilesystemSkills } from "./register";
62 changes: 62 additions & 0 deletions src-api/src/shared/skills/register.ts
Original file line number Diff line number Diff line change
@@ -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<SkillContentBlock[]> => {
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<number> {
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;
}