mirror of
https://github.com/workos/skills.git
synced 2026-09-14 20:07:08 +08:00
180 lines
5.5 KiB
TypeScript
180 lines
5.5 KiB
TypeScript
import { join } from 'node:path';
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { refineSkill, rateLimitDelay } from './lib/refiner.ts';
|
|
import type { GeneratedSkill } from './lib/types.ts';
|
|
|
|
/**
|
|
* Standalone batch refiner. Reads existing SKILL.md scaffolds from disk,
|
|
* refines them via Anthropic API, and writes back. Supports parallel execution
|
|
* via --concurrency flag.
|
|
*
|
|
* Usage:
|
|
* npx tsx scripts/refine-batch.ts # all generated skills, concurrency=5
|
|
* npx tsx scripts/refine-batch.ts --concurrency=8 # all generated, 8 at a time
|
|
* npx tsx scripts/refine-batch.ts workos-sso workos-mfa # specific skills only
|
|
* npx tsx scripts/refine-batch.ts --model=claude-opus-4-6 # use opus
|
|
*/
|
|
|
|
const SKIP = new Set(['workos-integrations']);
|
|
|
|
const GENERATED_SKILLS = [
|
|
'workos',
|
|
'workos-email',
|
|
'workos-widgets',
|
|
'workos-vault',
|
|
'workos-sso',
|
|
'workos-rbac',
|
|
'workos-migrate-supabase-auth',
|
|
'workos-migrate-stytch',
|
|
'workos-migrate-the-standalone-sso-api',
|
|
'workos-migrate-other-services',
|
|
'workos-migrate-firebase',
|
|
'workos-migrate-descope',
|
|
'workos-migrate-clerk',
|
|
'workos-migrate-better-auth',
|
|
'workos-migrate-aws-cognito',
|
|
'workos-migrate-auth0',
|
|
'workos-mfa',
|
|
'workos-events',
|
|
'workos-directory-sync',
|
|
'workos-custom-domains',
|
|
'workos-audit-logs',
|
|
'workos-admin-portal',
|
|
'workos-api-admin-portal',
|
|
'workos-api-audit-logs',
|
|
'workos-api-authkit',
|
|
'workos-api-directory-sync',
|
|
'workos-api-events',
|
|
'workos-api-organization',
|
|
'workos-api-roles',
|
|
'workos-api-sso',
|
|
'workos-api-vault',
|
|
'workos-api-widgets',
|
|
];
|
|
|
|
function parseArgs() {
|
|
const args = process.argv.slice(2);
|
|
const concurrencyArg = args.find((a) => a.startsWith('--concurrency='));
|
|
const modelArg = args.find((a) => a.startsWith('--model='));
|
|
const skillNames = args.filter((a) => !a.startsWith('--'));
|
|
|
|
return {
|
|
concurrency: concurrencyArg ? parseInt(concurrencyArg.split('=')[1]) : 5,
|
|
model: modelArg?.split('=')[1] ?? undefined,
|
|
skills: skillNames.length > 0 ? skillNames : GENERATED_SKILLS,
|
|
};
|
|
}
|
|
|
|
async function readSkillFromDisk(name: string): Promise<GeneratedSkill> {
|
|
const path = `plugins/workos/skills/${name}/SKILL.md`;
|
|
const fullPath = join(process.cwd(), path);
|
|
const content = await readFile(fullPath, 'utf8');
|
|
return {
|
|
name,
|
|
path,
|
|
content,
|
|
sizeBytes: Buffer.byteLength(content, 'utf8'),
|
|
generated: true,
|
|
};
|
|
}
|
|
|
|
async function writeSkillToDisk(skill: GeneratedSkill): Promise<void> {
|
|
const fullPath = join(process.cwd(), skill.path);
|
|
await writeFile(fullPath, skill.content);
|
|
}
|
|
|
|
/** Process a batch of skills concurrently */
|
|
async function processBatch(
|
|
skills: GeneratedSkill[],
|
|
options: { apiKey: string; model?: string; goldStandard: string },
|
|
): Promise<{ success: number; failed: number }> {
|
|
let success = 0;
|
|
let failed = 0;
|
|
|
|
const promises = skills.map(async (skill) => {
|
|
try {
|
|
const refined = await refineSkill(skill, options);
|
|
await writeSkillToDisk(refined);
|
|
const delta = (((refined.sizeBytes - skill.sizeBytes) / skill.sizeBytes) * 100).toFixed(0);
|
|
console.log(
|
|
` ✓ ${skill.name.padEnd(40)} ${(skill.sizeBytes / 1024).toFixed(1)}KB → ${(refined.sizeBytes / 1024).toFixed(1)}KB (${delta}%)`,
|
|
);
|
|
success++;
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.error(` ✗ ${skill.name.padEnd(40)} FAILED: ${msg}`);
|
|
failed++;
|
|
}
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
return { success, failed };
|
|
}
|
|
|
|
async function main() {
|
|
const flags = parseArgs();
|
|
|
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
console.error('Error: ANTHROPIC_API_KEY env var required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const goldStandardPath = join(process.cwd(), 'plugins/workos/skills/workos-authkit-nextjs/SKILL.md');
|
|
const goldStandard = await readFile(goldStandardPath, 'utf8');
|
|
|
|
const skillNames = flags.skills.filter((s) => !SKIP.has(s));
|
|
console.log(`Refining ${skillNames.length} skills with concurrency=${flags.concurrency}`);
|
|
if (flags.model) console.log(` Model: ${flags.model}`);
|
|
console.log(` Gold standard: workos-authkit-nextjs (${(goldStandard.length / 1024).toFixed(1)}KB)\n`);
|
|
|
|
// Read all scaffolds from disk
|
|
const skills: GeneratedSkill[] = [];
|
|
for (const name of skillNames) {
|
|
try {
|
|
skills.push(await readSkillFromDisk(name));
|
|
} catch {
|
|
console.error(` ⚠ Skipping ${name}: SKILL.md not found on disk`);
|
|
}
|
|
}
|
|
|
|
// Split into batches and process
|
|
const options = {
|
|
apiKey: process.env.ANTHROPIC_API_KEY!,
|
|
model: flags.model,
|
|
goldStandard,
|
|
};
|
|
|
|
let totalSuccess = 0;
|
|
let totalFailed = 0;
|
|
const batches: GeneratedSkill[][] = [];
|
|
|
|
for (let i = 0; i < skills.length; i += flags.concurrency) {
|
|
batches.push(skills.slice(i, i + flags.concurrency));
|
|
}
|
|
|
|
for (let b = 0; b < batches.length; b++) {
|
|
console.log(`--- Batch ${b + 1}/${batches.length} (${batches[b].length} skills) ---`);
|
|
const { success, failed } = await processBatch(batches[b], options, b + 1, batches.length);
|
|
totalSuccess += success;
|
|
totalFailed += failed;
|
|
|
|
// Brief pause between batches to be nice to rate limits
|
|
if (b < batches.length - 1) {
|
|
await rateLimitDelay();
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`\n✓ Refinement complete. ${totalSuccess} succeeded, ${totalFailed} failed out of ${skills.length} total.`,
|
|
);
|
|
|
|
if (totalFailed > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Fatal:', err.message);
|
|
process.exit(1);
|
|
});
|