All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { parse as parseYaml } from 'yaml';
|
|
import { RECOMMENDED_SKILLS } from '../constants.js';
|
|
|
|
export interface SkillEntry {
|
|
name: string;
|
|
description: string;
|
|
version?: string;
|
|
recommended: boolean;
|
|
source: 'canonical' | 'local';
|
|
}
|
|
|
|
export function loadSkillsCatalog(mosaicHome: string): SkillEntry[] {
|
|
const skills: SkillEntry[] = [];
|
|
|
|
// Load canonical skills
|
|
const canonicalDir = join(mosaicHome, 'skills');
|
|
if (existsSync(canonicalDir)) {
|
|
skills.push(...loadSkillsFromDir(canonicalDir, 'canonical'));
|
|
}
|
|
|
|
// Fallback to source repo
|
|
const sourceDir = join(mosaicHome, 'sources', 'agent-skills', 'skills');
|
|
if (skills.length === 0 && existsSync(sourceDir)) {
|
|
skills.push(...loadSkillsFromDir(sourceDir, 'canonical'));
|
|
}
|
|
|
|
// Load local skills
|
|
const localDir = join(mosaicHome, 'skills-local');
|
|
if (existsSync(localDir)) {
|
|
skills.push(...loadSkillsFromDir(localDir, 'local'));
|
|
}
|
|
|
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
function loadSkillsFromDir(dir: string, source: 'canonical' | 'local'): SkillEntry[] {
|
|
const entries: SkillEntry[] = [];
|
|
|
|
let dirEntries;
|
|
try {
|
|
dirEntries = readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
return entries;
|
|
}
|
|
|
|
for (const entry of dirEntries) {
|
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
|
|
const skillMdPath = join(dir, entry.name, 'SKILL.md');
|
|
if (!existsSync(skillMdPath)) continue;
|
|
|
|
try {
|
|
const content = readFileSync(skillMdPath, 'utf-8');
|
|
const frontmatter = parseFrontmatter(content);
|
|
|
|
entries.push({
|
|
name: (frontmatter['name'] as string | undefined) ?? entry.name,
|
|
description: (frontmatter['description'] as string | undefined) ?? '',
|
|
version: frontmatter['version'] as string | undefined,
|
|
recommended: RECOMMENDED_SKILLS.has(entry.name),
|
|
source,
|
|
});
|
|
} catch {
|
|
// Skip malformed skills
|
|
entries.push({
|
|
name: entry.name,
|
|
description: '',
|
|
recommended: RECOMMENDED_SKILLS.has(entry.name),
|
|
source,
|
|
});
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function parseFrontmatter(content: string): Record<string, unknown> {
|
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
if (!match?.[1]) return {};
|
|
|
|
try {
|
|
return (parseYaml(match[1]) as Record<string, unknown>) ?? {};
|
|
} catch {
|
|
// Fallback: simple key-value parsing
|
|
const result: Record<string, string> = {};
|
|
for (const line of match[1].split('\n')) {
|
|
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)/);
|
|
if (kv?.[1] !== undefined && kv[2] !== undefined) {
|
|
result[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|