clawdie-ai/scripts/skill-sync.ts
Clawdie AI 2983111a5d feat(phase2): add skill library catalog and tooling
- 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)
2026-04-13 23:06:50 +00:00

121 lines
3.4 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* scripts/skill-sync.ts — Refresh all remote-sourced skills in the cache.
*
* Usage:
* just skill-sync
*
* Iterates all non-local: sources in library.yaml, fetches the latest
* content, and updates .agent/library-cache/.
*/
import fs from 'fs';
import path from 'path';
import https from 'https';
import { parse } from 'yaml';
import { PROJECT_ROOT } from '../src/config.js';
const LIBRARY_PATH = path.join(PROJECT_ROOT, 'agent', 'library.yaml');
const CACHE_DIR = path.join(PROJECT_ROOT, '.agent', 'library-cache');
interface Entry {
id: string;
source: string;
}
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('/')}`;
}
return null;
}
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);
});
}
async function main(): Promise<void> {
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?: Entry[];
features?: Entry[];
agents?: Entry[];
prompts?: Entry[];
};
const allEntries: Entry[] = [
...(lib.skills ?? []),
...(lib.features ?? []),
...(lib.agents ?? []),
...(lib.prompts ?? []),
];
const remote = allEntries.filter((e) => !e.source.startsWith('local:'));
if (remote.length === 0) {
console.log('No remote sources in library — nothing to sync.');
return;
}
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
let updated = 0;
let failed = 0;
for (const entry of remote) {
const url = sourceToUrl(entry.source);
if (!url) {
console.warn(` [skip] ${entry.id}: unknown source prefix`);
continue;
}
try {
process.stdout.write(` Fetching ${entry.id}... `);
const content = await fetchUrl(url);
const safe = entry.source.replace(/[^a-zA-Z0-9._-]/g, '_');
fs.writeFileSync(path.join(CACHE_DIR, safe), content, 'utf-8');
console.log('ok');
updated++;
} catch (err) {
console.log(`FAILED (${(err as Error).message})`);
failed++;
}
}
console.log(`\nSync complete: ${updated} updated, ${failed} failed.`);
if (failed > 0) process.exit(1);
}
main().catch((err: Error) => {
console.error(err.message);
process.exit(1);
});