#!/usr/bin/env node /** * Token-footprint report for the plugin's always-loaded context surface: * the YAML frontmatter of every skills//SKILL.md and agents/.md * (what Claude Code reads for skill/agent dispatch on every session). * * Token counts are ESTIMATES: chars / 4, the common English-text heuristic. * No tokenizer dependency by design — the budget enforces an order of * magnitude, not an exact count. * * Usage: * node scripts/token-report.cjs # report; exit 0 (budgets warn only) * node scripts/token-report.cjs --check # exit 1 if any budget exceeded * node scripts/token-report.cjs [--check] [repo-root] * * Exit codes: 0 ok · 1 budget breach (--check) or nothing found · 2 bad usage. */ "use strict"; const fs = require("fs"); const path = require("path"); const args = process.argv.slice(2); const CHECK = args.includes("--check"); const unknown = args.find((a) => a.startsWith("-") && a !== "--check"); if (unknown) { console.error(`Unknown flag "${unknown}". Usage: token-report.cjs [--check] [repo-root]`); process.exit(2); } const ROOT = path.resolve( args.find((a) => a !== "--check") || path.join(__dirname, "..") ); // Budgets (estimated tokens). Chosen 2026-07-12 with ~40% headroom over the // then-largest items (largest agent ~240, largest skill ~167, total ~3.6k). // Raising a budget is a deliberate act — do it in a PR, not by accident. const BUDGET = { skill: 250, // per SKILL.md frontmatter agent: 350, // per agents/*.md frontmatter total: 5000, // whole always-loaded surface }; const estTokens = (s) => Math.round(s.length / 4); function frontmatter(text) { const lines = text.replace(/\r\n/g, "\n").split("\n"); if (lines[0].trim() !== "---") return null; const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---"); if (end === -1) return null; return lines.slice(1, end).join("\n"); } // Read a file's frontmatter; returns { tokens, broken }. A listed-but- // unreadable entry (dangling symlink, directory named *.md) is reported as // broken rather than crashing the run. function measure(fullPath) { let text; try { text = fs.readFileSync(fullPath, "utf8"); } catch { return { tokens: 0, broken: true }; } const fm = frontmatter(text); return { tokens: fm === null ? 0 : estTokens(fm), broken: fm === null }; } // True when a path exists as a directory entry at all — including a dangling // symlink, which fs.existsSync() (target-following) would miss. function entryExists(p) { try { fs.lstatSync(p); return true; } catch { return false; } } function collect() { const rows = []; const skillsDir = path.join(ROOT, "skills"); const agentsDir = path.join(ROOT, "agents"); if (fs.existsSync(skillsDir)) { for (const d of fs.readdirSync(skillsDir).sort()) { const f = path.join(skillsDir, d, "SKILL.md"); if (!entryExists(f)) continue; // absent SKILL.md is validate-plugin's territory rows.push({ kind: "skill", name: d, file: path.relative(ROOT, f), ...measure(f), }); } } if (fs.existsSync(agentsDir)) { for (const f of fs.readdirSync(agentsDir).sort()) { if (!f.endsWith(".md")) continue; const full = path.join(agentsDir, f); rows.push({ kind: "agent", name: f.replace(/\.md$/, ""), file: path.relative(ROOT, full), ...measure(full), }); } } return rows; } function main() { const rows = collect(); if (rows.length === 0) { console.error("FAIL — no skills or agents found. Wrong repo root?"); process.exit(1); } const violations = []; for (const r of rows) { if (r.broken) violations.push(`${r.file}: unreadable file or missing/unterminated frontmatter`); else if (r.tokens > BUDGET[r.kind]) { violations.push( `${r.file}: ~${r.tokens} tokens exceeds ${r.kind} budget ${BUDGET[r.kind]}` ); } } const total = rows.reduce((s, r) => s + r.tokens, 0); if (total > BUDGET.total) { violations.push(`total: ~${total} tokens exceeds total budget ${BUDGET.total}`); } rows.sort((a, b) => b.tokens - a.tokens); const width = Math.max(...rows.map((r) => r.name.length)); console.log(`Token-footprint report (estimate = chars/4) — ${ROOT}\n`); console.log(` ${"name".padEnd(width)} kind ~tokens budget`); for (const r of rows) { const over = r.broken || r.tokens > BUDGET[r.kind] ? " ← OVER" : ""; console.log( ` ${r.name.padEnd(width)} ${r.kind.padEnd(5)} ${String(r.tokens).padStart(7)} ${BUDGET[r.kind]}${over}` ); } console.log( `\n TOTAL ~${total} tokens (budget ${BUDGET.total}) across ${rows.length} items` ); if (violations.length > 0) { console.log(`\n${CHECK ? "FAIL" : "WARN"} — ${violations.length} over budget:`); for (const v of violations) console.log(` ✗ ${v}`); if (CHECK) process.exit(1); return; } console.log("\nOK — all items within budget."); } main();