mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-31 06:03:29 +08:00
* feat: add agent description compression with lazy loading (#491) Agent descriptions consume ~26k tokens (121KB across 27 agents). This adds a compression library with three modes: - catalog: metadata only (~2-3k tokens) for agent selection - summary: metadata + first paragraph (~4-5k tokens) for routing - full: no compression, for when agent is invoked Includes lazy-load function to fetch full agent body on demand. 21 tests covering parsing, compression, filtering, and real agents dir. * fix: update JSDoc to include all stats fields in buildAgentCatalog Add compressedBytes and mode to the documented return type, matching the actual implementation.
245 lines
6.0 KiB
JavaScript
245 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Parse YAML frontmatter from a markdown string.
|
|
* Returns { frontmatter: {}, body: string }.
|
|
*/
|
|
function parseFrontmatter(content) {
|
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
|
|
if (!match) {
|
|
return { frontmatter: {}, body: content };
|
|
}
|
|
|
|
const frontmatter = {};
|
|
for (const line of match[1].split('\n')) {
|
|
const colonIdx = line.indexOf(':');
|
|
if (colonIdx === -1) continue;
|
|
|
|
const key = line.slice(0, colonIdx).trim();
|
|
let value = line.slice(colonIdx + 1).trim();
|
|
|
|
// Handle JSON arrays (e.g. tools: ["Read", "Grep"])
|
|
if (value.startsWith('[') && value.endsWith(']')) {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch {
|
|
// keep as string
|
|
}
|
|
}
|
|
|
|
// Strip surrounding quotes
|
|
if (typeof value === 'string' && value.startsWith('"') && value.endsWith('"')) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
|
|
frontmatter[key] = value;
|
|
}
|
|
|
|
return { frontmatter, body: match[2] || '' };
|
|
}
|
|
|
|
/**
|
|
* Extract the first meaningful paragraph from agent body as a summary.
|
|
* Skips headings, list items, code blocks, and table rows.
|
|
*/
|
|
function extractSummary(body, maxSentences = 1) {
|
|
const lines = body.split('\n');
|
|
const paragraphs = [];
|
|
let current = [];
|
|
let inCodeBlock = false;
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
|
|
// Track fenced code blocks
|
|
if (trimmed.startsWith('```')) {
|
|
inCodeBlock = !inCodeBlock;
|
|
continue;
|
|
}
|
|
if (inCodeBlock) continue;
|
|
|
|
if (trimmed === '') {
|
|
if (current.length > 0) {
|
|
paragraphs.push(current.join(' '));
|
|
current = [];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Skip headings, list items (bold, plain, asterisk), numbered lists, table rows
|
|
if (
|
|
trimmed.startsWith('#') ||
|
|
trimmed.startsWith('- ') ||
|
|
trimmed.startsWith('* ') ||
|
|
/^\d+\.\s/.test(trimmed) ||
|
|
trimmed.startsWith('|')
|
|
) {
|
|
if (current.length > 0) {
|
|
paragraphs.push(current.join(' '));
|
|
current = [];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
current.push(trimmed);
|
|
}
|
|
if (current.length > 0) {
|
|
paragraphs.push(current.join(' '));
|
|
}
|
|
|
|
const firstParagraph = paragraphs.find(p => p.length > 0);
|
|
if (!firstParagraph) return '';
|
|
|
|
const sentences = firstParagraph.match(/[^.!?]+[.!?]+/g) || [firstParagraph];
|
|
return sentences.slice(0, maxSentences).map(s => s.trim()).join(' ').trim();
|
|
}
|
|
|
|
/**
|
|
* Load and parse a single agent file.
|
|
*/
|
|
function loadAgent(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
const fileName = path.basename(filePath, '.md');
|
|
|
|
return {
|
|
fileName,
|
|
name: frontmatter.name || fileName,
|
|
description: frontmatter.description || '',
|
|
tools: Array.isArray(frontmatter.tools) ? frontmatter.tools : [],
|
|
model: frontmatter.model || 'sonnet',
|
|
body,
|
|
byteSize: Buffer.byteLength(content, 'utf8'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load all agents from a directory.
|
|
*/
|
|
function loadAgents(agentsDir) {
|
|
if (!fs.existsSync(agentsDir)) return [];
|
|
|
|
return fs.readdirSync(agentsDir)
|
|
.filter(f => f.endsWith('.md'))
|
|
.sort()
|
|
.map(f => loadAgent(path.join(agentsDir, f)));
|
|
}
|
|
|
|
/**
|
|
* Compress an agent to catalog entry (metadata only).
|
|
*/
|
|
function compressToCatalog(agent) {
|
|
return {
|
|
name: agent.name,
|
|
description: agent.description,
|
|
tools: agent.tools,
|
|
model: agent.model,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compress an agent to summary entry (metadata + first paragraph).
|
|
*/
|
|
function compressToSummary(agent) {
|
|
return {
|
|
...compressToCatalog(agent),
|
|
summary: extractSummary(agent.body),
|
|
};
|
|
}
|
|
|
|
const allowedModes = ['catalog', 'summary', 'full'];
|
|
|
|
/**
|
|
* Build a compressed catalog from a directory of agents.
|
|
*
|
|
* Modes:
|
|
* - 'catalog': name, description, tools, model only (~2-3k tokens for 27 agents)
|
|
* - 'summary': catalog + first paragraph summary (~4-5k tokens)
|
|
* - 'full': no compression, full body included
|
|
*
|
|
* Returns { agents: [], stats: { totalAgents, originalBytes, compressedBytes, compressedTokenEstimate, mode } }
|
|
*/
|
|
function buildAgentCatalog(agentsDir, options = {}) {
|
|
const mode = options.mode || 'catalog';
|
|
|
|
if (!allowedModes.includes(mode)) {
|
|
throw new Error(`Invalid mode "${mode}". Allowed modes: ${allowedModes.join(', ')}`);
|
|
}
|
|
|
|
const filter = options.filter || null;
|
|
|
|
let agents = loadAgents(agentsDir);
|
|
|
|
if (typeof filter === 'function') {
|
|
agents = agents.filter(filter);
|
|
}
|
|
|
|
const originalBytes = agents.reduce((sum, a) => sum + a.byteSize, 0);
|
|
|
|
let compressed;
|
|
if (mode === 'catalog') {
|
|
compressed = agents.map(compressToCatalog);
|
|
} else if (mode === 'summary') {
|
|
compressed = agents.map(compressToSummary);
|
|
} else {
|
|
compressed = agents.map(a => ({
|
|
name: a.name,
|
|
description: a.description,
|
|
tools: a.tools,
|
|
model: a.model,
|
|
body: a.body,
|
|
}));
|
|
}
|
|
|
|
const compressedJson = JSON.stringify(compressed);
|
|
// Rough token estimate: ~4 chars per token for English text
|
|
const compressedTokenEstimate = Math.ceil(compressedJson.length / 4);
|
|
|
|
return {
|
|
agents: compressed,
|
|
stats: {
|
|
totalAgents: agents.length,
|
|
originalBytes,
|
|
compressedBytes: Buffer.byteLength(compressedJson, 'utf8'),
|
|
compressedTokenEstimate,
|
|
mode,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Lazy-load a single agent's full content by name.
|
|
* Returns null if not found.
|
|
*/
|
|
function lazyLoadAgent(agentsDir, agentName) {
|
|
// Validate agentName: only allow alphanumeric, hyphen, underscore
|
|
if (!/^[\w-]+$/.test(agentName)) {
|
|
return null;
|
|
}
|
|
|
|
const filePath = path.resolve(agentsDir, `${agentName}.md`);
|
|
|
|
// Verify the resolved path is still within agentsDir
|
|
const resolvedAgentsDir = path.resolve(agentsDir);
|
|
if (!filePath.startsWith(resolvedAgentsDir + path.sep)) {
|
|
return null;
|
|
}
|
|
|
|
if (!fs.existsSync(filePath)) return null;
|
|
return loadAgent(filePath);
|
|
}
|
|
|
|
module.exports = {
|
|
buildAgentCatalog,
|
|
compressToCatalog,
|
|
compressToSummary,
|
|
extractSummary,
|
|
lazyLoadAgent,
|
|
loadAgent,
|
|
loadAgents,
|
|
parseFrontmatter,
|
|
};
|