coding-agent, mom: add skills API export and mom skills auto-discovery

coding-agent:
- Export loadSkillsFromDir, formatSkillsForPrompt, and related types
- Refactor skills.ts to expose public API

mom:
- Add skills auto-discovery from workspace/skills and channel/skills
- Fix skill loading to use host paths (not Docker container paths)
- Update README and system prompt with SKILL.md format docs
This commit is contained in:
Mario Zechner 2025-12-13 00:56:10 +01:00
parent 439f55b0eb
commit e707ac4cd0
8 changed files with 175 additions and 65 deletions

View file

@ -8,14 +8,12 @@ export interface SkillFrontmatter {
description: string;
}
export type SkillSource = "user" | "project" | "claude-user" | "claude-project" | "codex-user";
export interface Skill {
name: string;
description: string;
filePath: string;
baseDir: string;
source: SkillSource;
source: string;
}
type SkillFormat = "recursive" | "claude";
@ -60,9 +58,27 @@ function parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; bod
return { frontmatter, body };
}
function loadSkillsFromDir(
export interface LoadSkillsFromDirOptions {
/** Directory to scan for skills */
dir: string;
/** Source identifier for these skills */
source: string;
/** Use colon-separated path names (e.g., db:migrate) instead of simple directory name */
useColonPath?: boolean;
}
/**
* Load skills from a directory recursively.
* Skills are directories containing a SKILL.md file with frontmatter including a description.
*/
export function loadSkillsFromDir(options: LoadSkillsFromDirOptions, subdir: string = ""): Skill[] {
const { dir, source, useColonPath = false } = options;
return loadSkillsFromDirInternal(dir, source, "recursive", useColonPath, subdir);
}
function loadSkillsFromDirInternal(
dir: string,
source: SkillSource,
source: string,
format: SkillFormat,
useColonPath: boolean = false,
subdir: string = "",
@ -91,7 +107,7 @@ function loadSkillsFromDir(
// Recursive format: scan directories, look for SKILL.md files
if (entry.isDirectory()) {
const newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
skills.push(...loadSkillsFromDir(fullPath, source, format, useColonPath, newSubdir));
skills.push(...loadSkillsFromDirInternal(fullPath, source, format, useColonPath, newSubdir));
} else if (entry.isFile() && entry.name === "SKILL.md") {
try {
const rawContent = readFileSync(fullPath, "utf-8");
@ -153,34 +169,60 @@ function loadSkillsFromDir(
return skills;
}
/**
* Format skills for inclusion in a system prompt.
*/
export function formatSkillsForPrompt(skills: Skill[]): string {
if (skills.length === 0) {
return "";
}
const lines = [
"\n\n<available_skills>",
"The following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"Skills may contain {baseDir} placeholders - replace them with the skill's base directory path.\n",
];
for (const skill of skills) {
lines.push(`- ${skill.name}: ${skill.description}`);
lines.push(` File: ${skill.filePath}`);
lines.push(` Base directory: ${skill.baseDir}`);
}
lines.push("</available_skills>");
return lines.join("\n");
}
export function loadSkills(): Skill[] {
const skillMap = new Map<string, Skill>();
// Codex: recursive, simple directory name
const codexUserDir = join(homedir(), ".codex", "skills");
for (const skill of loadSkillsFromDir(codexUserDir, "codex-user", "recursive", false)) {
for (const skill of loadSkillsFromDirInternal(codexUserDir, "codex-user", "recursive", false)) {
skillMap.set(skill.name, skill);
}
// Claude: single level only
const claudeUserDir = join(homedir(), ".claude", "skills");
for (const skill of loadSkillsFromDir(claudeUserDir, "claude-user", "claude", false)) {
for (const skill of loadSkillsFromDirInternal(claudeUserDir, "claude-user", "claude", false)) {
skillMap.set(skill.name, skill);
}
const claudeProjectDir = resolve(process.cwd(), ".claude", "skills");
for (const skill of loadSkillsFromDir(claudeProjectDir, "claude-project", "claude", false)) {
for (const skill of loadSkillsFromDirInternal(claudeProjectDir, "claude-project", "claude", false)) {
skillMap.set(skill.name, skill);
}
// Pi: recursive, colon-separated path names
const globalSkillsDir = join(homedir(), CONFIG_DIR_NAME, "agent", "skills");
for (const skill of loadSkillsFromDir(globalSkillsDir, "user", "recursive", true)) {
for (const skill of loadSkillsFromDirInternal(globalSkillsDir, "user", "recursive", true)) {
skillMap.set(skill.name, skill);
}
const projectSkillsDir = resolve(process.cwd(), CONFIG_DIR_NAME, "skills");
for (const skill of loadSkillsFromDir(projectSkillsDir, "project", "recursive", true)) {
for (const skill of loadSkillsFromDirInternal(projectSkillsDir, "project", "recursive", true)) {
skillMap.set(skill.name, skill);
}

View file

@ -6,7 +6,7 @@ import chalk from "chalk";
import { existsSync, readFileSync } from "fs";
import { join, resolve } from "path";
import { getAgentDir, getDocsPath, getReadmePath } from "../config.js";
import { loadSkills, type Skill } from "./skills.js";
import { formatSkillsForPrompt, loadSkills } from "./skills.js";
import type { ToolName } from "./tools/index.js";
/** Tool descriptions for system prompt */
@ -102,29 +102,6 @@ export function loadProjectContextFiles(): Array<{ path: string; content: string
return contextFiles;
}
function buildSkillsSection(skills: Skill[]): string {
if (skills.length === 0) {
return "";
}
const lines = [
"\n\n<available_skills>",
"The following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"Skills may contain {baseDir} placeholders - replace them with the skill's base directory path.\n",
];
for (const skill of skills) {
lines.push(`- ${skill.name}: ${skill.description}`);
lines.push(` File: ${skill.filePath}`);
lines.push(` Base directory: ${skill.baseDir}`);
}
lines.push("</available_skills>");
return lines.join("\n");
}
export interface BuildSystemPromptOptions {
customPrompt?: string;
selectedTools?: ToolName[];
@ -173,7 +150,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const customPromptHasRead = !selectedTools || selectedTools.includes("read");
if (skillsEnabled && customPromptHasRead) {
const skills = loadSkills();
prompt += buildSkillsSection(skills);
prompt += formatSkillsForPrompt(skills);
}
// Add date/time and working directory last
@ -279,7 +256,7 @@ Documentation:
// Append skills section (only if read tool is available)
if (skillsEnabled && hasRead) {
const skills = loadSkills();
prompt += buildSkillsSection(skills);
prompt += formatSkillsForPrompt(skills);
}
// Add date/time and working directory last