feat: add skills system for extensible capability packages

Implement a three-tier skill system (bundled/managed/workspace) that
extends Flynn's abilities via SKILL.md instructions injected into the
system prompt.

- SkillManifest/Skill types with requirements gating (OS, binaries, env)
- Loader: discovers skills from directories, validates manifests,
  checks system requirements, infers manifest from SKILL.md if missing
- SkillRegistry: holds skills, generates system prompt additions,
  supports override by name (workspace > managed > bundled)
- SkillInstaller: copies/removes skills in managed directory with
  upgrade support
- Config: add skills.workspace_dir, managed_dir, bundled_dir options
- Daemon: loads all skills at startup, injects available skill
  instructions into the system prompt
- Tests: 45 new tests (loader 22, registry 11, installer 12)
This commit is contained in:
William Valentin
2026-02-05 20:20:03 -08:00
parent cd839c7f0c
commit 7c41ffad71
10 changed files with 1221 additions and 2 deletions
+31 -2
View File
@@ -9,6 +9,7 @@ import { GatewayServer } from '../gateway/index.js';
import { ChannelRegistry, TelegramAdapter, WebChatAdapter } from '../channels/index.js';
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
import { McpManager } from '../mcp/index.js';
import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js';
import { resolve } from 'path';
import { homedir } from 'os';
import { mkdirSync, readFileSync, existsSync } from 'fs';
@@ -25,6 +26,8 @@ export interface DaemonContext {
gateway: GatewayServer;
channelRegistry: ChannelRegistry;
mcpManager: McpManager;
skillRegistry: SkillRegistry;
skillInstaller: SkillInstaller;
}
function loadSystemPrompt(): string {
@@ -199,11 +202,35 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
console.log('MCP servers stopped');
});
// Initialize skills system
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
const skillRegistry = new SkillRegistry();
const skillInstaller = new SkillInstaller(config.skills.managed_dir ?? defaultManagedDir);
const skills = loadAllSkills({
bundledDir: config.skills.bundled_dir,
managedDir: config.skills.managed_dir ?? defaultManagedDir,
workspaceDir: config.skills.workspace_dir,
});
for (const skill of skills) {
skillRegistry.register(skill);
}
if (skills.length > 0) {
const available = skillRegistry.listAvailable().length;
console.log(`Loaded ${skills.length} skill(s) (${available} available)`);
}
// Initialize model router
const modelRouter = createModelRouter(config);
// Load system prompt once for reuse
const systemPrompt = loadSystemPrompt();
// Load system prompt and append skill instructions
let systemPrompt = loadSystemPrompt();
const skillAdditions = skillRegistry.getSystemPromptAdditions();
if (skillAdditions) {
systemPrompt = `${systemPrompt}\n\n# Available Skills\n\n${skillAdditions}`;
}
// Initialize gateway WebSocket server
const gateway = new GatewayServer({
@@ -296,6 +323,8 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
gateway,
channelRegistry,
mcpManager,
skillRegistry,
skillInstaller,
};
}