- agent/library.yaml: catalog of all 48 skills (37 operational + 11 features), 4 agents with skill assignments, 2 prompt refs - src/skill-library.ts: loadLibrary, searchSkills, getSkillContent, getAgentSkills, validateLibrary, reloadLibrary - scripts/skill-list.ts: grouped table output with color, optional search query - scripts/skill-add.ts: add skill from local/codeberg/github/raw source - scripts/skill-sync.ts: refresh all remote-sourced skills in cache - scripts/validate-library.ts: validate all local: sources exist on disk - .agent/identities/: COORDINATOR, SYSADMIN, DB_ADMIN, GIT_ADMIN stubs - .agent/context/FREEBSD.md: FreeBSD gotchas context for agents Typecheck passes. `just skill-list` and `just skill-search` ready to wire up in the justfile (Phase 4). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: pass | Tests: pass — Tests 942 passed (942)
165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
/**
|
|
* scripts/skill-add.ts — Add a skill to the library catalog.
|
|
*
|
|
* Usage:
|
|
* just skill-add local:.agent/skills/my-skill/SKILL.md
|
|
* just skill-add codeberg:Clawdie/Skills/some-skill/SKILL.md
|
|
* just skill-add github:disler/the-library/some-skill/SKILL.md
|
|
* just skill-add raw:https://example.com/SKILL.md
|
|
*
|
|
* Fetches the SKILL.md, extracts id/description/tags from frontmatter,
|
|
* and appends an entry to agent/library.yaml.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import https from 'https';
|
|
|
|
import { parse, stringify } from 'yaml';
|
|
|
|
import { PROJECT_ROOT } from '../src/config.js';
|
|
import { reloadLibrary } from '../src/skill-library.js';
|
|
|
|
const LIBRARY_PATH = path.join(PROJECT_ROOT, 'agent', 'library.yaml');
|
|
const CACHE_DIR = path.join(PROJECT_ROOT, '.agent', 'library-cache');
|
|
|
|
const source = process.argv[2];
|
|
|
|
if (!source) {
|
|
console.error('Usage: just skill-add <source>');
|
|
console.error(' source: local:<path>, codeberg:<owner>/<repo>/<path>, github:<owner>/<repo>/<path>, raw:<url>');
|
|
process.exit(1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resolve source to a URL or local path
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function sourceToUrl(src: string): string | null {
|
|
if (src.startsWith('local:')) return null;
|
|
if (src.startsWith('raw:')) return src.slice('raw:'.length);
|
|
if (src.startsWith('codeberg:')) {
|
|
const rest = src.slice('codeberg:'.length);
|
|
const [owner, repo, ...fileParts] = rest.split('/');
|
|
return `https://codeberg.org/${owner}/${repo}/raw/branch/main/${fileParts.join('/')}`;
|
|
}
|
|
if (src.startsWith('github:')) {
|
|
const rest = src.slice('github:'.length);
|
|
const [owner, repo, ...fileParts] = rest.split('/');
|
|
return `https://raw.githubusercontent.com/${owner}/${repo}/main/${fileParts.join('/')}`;
|
|
}
|
|
throw new Error(`Unknown source prefix: ${src}`);
|
|
}
|
|
|
|
function fetchUrl(url: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
https.get(url, (res) => {
|
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
fetchUrl(res.headers.location!).then(resolve).catch(reject);
|
|
return;
|
|
}
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
return;
|
|
}
|
|
const chunks: Buffer[] = [];
|
|
res.on('data', (c: Buffer) => chunks.push(c));
|
|
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
res.on('error', reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Parse SKILL.md frontmatter
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface SkillFrontmatter {
|
|
name?: string;
|
|
description?: string;
|
|
tags?: string[];
|
|
}
|
|
|
|
function parseFrontmatter(content: string): SkillFrontmatter {
|
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
if (!match) return {};
|
|
try {
|
|
return parse(match[1]) as SkillFrontmatter;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function main(): Promise<void> {
|
|
let content: string;
|
|
|
|
const url = sourceToUrl(source);
|
|
if (url) {
|
|
console.log(`Fetching: ${url}`);
|
|
content = await fetchUrl(url);
|
|
|
|
// Cache it
|
|
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
const safe = source.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
fs.writeFileSync(path.join(CACHE_DIR, safe), content, 'utf-8');
|
|
console.log('Cached.');
|
|
} else {
|
|
// local:
|
|
const localPath = path.join(PROJECT_ROOT, source.slice('local:'.length));
|
|
const skillMd = fs.statSync(localPath).isDirectory()
|
|
? path.join(localPath, 'SKILL.md')
|
|
: localPath;
|
|
if (!fs.existsSync(skillMd)) {
|
|
console.error(`Not found: ${skillMd}`);
|
|
process.exit(1);
|
|
}
|
|
content = fs.readFileSync(skillMd, 'utf-8');
|
|
}
|
|
|
|
const fm = parseFrontmatter(content);
|
|
|
|
// Derive id from source path
|
|
const parts = source.replace(/^(local:|codeberg:|github:|raw:)/, '').split('/');
|
|
const fileName = parts[parts.length - 1].replace(/\.md$/, '');
|
|
const dirName = parts[parts.length - 2] ?? fileName;
|
|
const id = dirName === 'SKILL' ? fileName : dirName;
|
|
|
|
const description = fm.description ?? '(no description)';
|
|
const tags = fm.tags ?? [];
|
|
|
|
// Load existing library and append
|
|
if (!fs.existsSync(LIBRARY_PATH)) {
|
|
console.error(`Library not found: ${LIBRARY_PATH}`);
|
|
process.exit(1);
|
|
}
|
|
const lib = parse(fs.readFileSync(LIBRARY_PATH, 'utf-8')) as { skills?: unknown[] };
|
|
lib.skills = lib.skills ?? [];
|
|
|
|
// Check for duplicate
|
|
const existing = (lib.skills as Array<{ id: string }>).find((s) => s.id === id);
|
|
if (existing) {
|
|
console.log(`Skill "${id}" already in library. Update library.yaml manually if needed.`);
|
|
process.exit(0);
|
|
}
|
|
|
|
(lib.skills as unknown[]).push({ id, source, description, tags });
|
|
|
|
fs.writeFileSync(LIBRARY_PATH, stringify(lib), 'utf-8');
|
|
reloadLibrary();
|
|
|
|
console.log(`Added to library:`);
|
|
console.log(` id: ${id}`);
|
|
console.log(` source: ${source}`);
|
|
console.log(` description: ${description}`);
|
|
console.log(` tags: ${tags.join(', ') || '(none)'}`);
|
|
console.log('\nEdit agent/library.yaml to adjust tags or description if needed.');
|
|
}
|
|
|
|
main().catch((err: Error) => {
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
});
|