Skip to main content
Glama
generate-readme.mjs9.58 kB
#!/usr/bin/env node import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { execSync } from 'node:child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); function titleCase(s) { if (!s) return s; return s.charAt(0).toUpperCase() + s.slice(1); } function buildProject() { try { execSync('npm run build', { stdio: 'inherit' }); } catch (e) { console.error('Build failed. Cannot generate README.'); process.exit(1); } } function parseCategoriesFromRegister() { const srcPath = resolve(process.cwd(), 'src/tools/browser/register.ts'); const content = readFileSync(srcPath, 'utf8'); const lines = content.split(/\r?\n/); const map = new Map(); // className -> Category for (const line of lines) { // Support multiple named imports per line: import { A, B } from './subdir/file.js' const m = line.match(/import\s*\{\s*([A-Za-z0-9_,\s]+)\s*\}\s*from\s*'\.\/(.+?)\/.+?';/); if (m) { const classNames = m[1] .split(',') .map(s => s.trim()) .filter(Boolean); const subdir = m[2]; const category = titleCase(subdir); for (const className of classNames) { map.set(className, category); } } } return map; } async function mapToolNameToCategory(classCategoryMap) { const distRegisterPath = resolve(process.cwd(), 'dist/tools/browser/register.js'); const moduleUrl = pathToFileURL(distRegisterPath).href; const mod = await import(moduleUrl); const classes = mod.BROWSER_TOOL_CLASSES || []; const nameToCategory = new Map(); // toolName -> Category for (const cls of classes) { const category = classCategoryMap.get(cls.name); try { const meta = cls.getMetadata?.() || {}; if (meta && meta.name && category) { nameToCategory.set(meta.name, category); } } catch {} } return nameToCategory; } function getToolsMetadata() { const indexPath = resolve(process.cwd(), 'dist/index.js'); const cmd = `node ${indexPath} --print-tools-json`; const json = execSync(cmd, { encoding: 'utf8' }); const metas = JSON.parse(json); // Shape to match existing downstream code expecting { cls, meta } return metas.map((meta) => ({ cls: { name: meta.name }, meta })); } function formatParams(inputSchema) { if (!inputSchema || !inputSchema.properties) return []; const req = new Set(Array.isArray(inputSchema.required) ? inputSchema.required : []); const props = inputSchema.properties; const keys = Object.keys(props); return keys.map(k => { const p = props[k] || {}; const type = p.type || 'any'; const desc = p.description || ''; const required = req.has(k) ? 'required' : 'optional'; return `- ${k} (${type}, ${required}): ${desc}`; }); } function formatExamples(examples) { if (!examples || examples.length === 0) return ''; const body = examples.map(ex => `- ${ex}`).join('\n'); return `- Examples:\n${body}`; } function formatOutputs(outputs) { if (!outputs) return ''; const list = Array.isArray(outputs) ? outputs : [outputs]; const lines = []; lines.push('- Output Format:'); let inSub = false; for (const raw of list) { const s = String(raw); const isBullet = /^\s*-\s+/.test(s); if (isBullet) { const text = s.replace(/^\s*-\s+/, ''); lines.push(`${inSub ? ' -' : ' -'} ${text}`); } else { lines.push(` - ${s}`); inSub = /:\s*$/.test(s); } if (!isBullet && !/:\s*$/.test(s)) { inSub = false; } } return lines.join('\n'); } function formatExampleOutputs(exampleOutputs) { if (!exampleOutputs || exampleOutputs.length === 0) return ''; const parts = []; for (const ex of exampleOutputs) { parts.push(`- Example Output (${ex.call}):`); parts.push('```'); parts.push(ex.output); parts.push('```'); } return parts.join('\n'); } function generateCoreToolsSection(grouped) { const lines = []; lines.push(''); for (const [category, tools] of grouped) { lines.push(`### ${category}`); lines.push(''); for (const t of tools) { lines.push(`#### \`${t.meta.name}\``); if (t.meta.description) { lines.push(`${t.meta.description}`); } const params = formatParams(t.meta.inputSchema); if (params.length) { lines.push(''); lines.push('- Parameters:'); lines.push(params.map(p => ` ${p}`).join('\n')); } const outputs = formatOutputs(t.meta.outputs); if (outputs) { lines.push(''); lines.push(outputs); } const examples = formatExamples(t.meta.examples); if (examples) { lines.push(''); lines.push(examples); } const exampleOutputs = formatExampleOutputs(t.meta.exampleOutputs); if (exampleOutputs) { lines.push(''); lines.push(exampleOutputs); } lines.push(''); } } return lines.join('\n'); } async function run() { buildProject(); const classCategoryMap = parseCategoriesFromRegister(); const toolNameCategoryMap = await mapToolNameToCategory(classCategoryMap); const items = await getToolsMetadata(); // Group tools by category using className -> category map, fallback to meta.category const groupedMap = new Map(); for (const { cls, meta } of items) { const toolName = meta.name; const category = toolNameCategoryMap.get(toolName) || meta.category || 'Other'; if (!groupedMap.has(category)) groupedMap.set(category, []); groupedMap.get(category).push({ cls, meta }); } // Sort categories with configurable preferred order and 'Other' last let preferred = ['Inspection', 'Navigation', 'Interaction', 'Content', 'Console', 'Evaluation', 'Network', 'Waiting', 'Lifecycle']; const orderFileCandidates = [ resolve(process.cwd(), 'scripts/tool-order.json'), resolve(process.cwd(), 'tool-order.json'), ]; for (const file of orderFileCandidates) { if (existsSync(file)) { try { const cfg = JSON.parse(readFileSync(file, 'utf8')); if (Array.isArray(cfg) && cfg.every(x => typeof x === 'string')) { preferred = cfg; } } catch {} break; } } const categories = Array.from(groupedMap.keys()); categories.sort((a, b) => { const ia = preferred.includes(a) ? preferred.indexOf(a) : Number.POSITIVE_INFINITY; const ib = preferred.includes(b) ? preferred.indexOf(b) : Number.POSITIVE_INFINITY; if (ia !== ib) return ia - ib; if (a === 'Other') return 1; if (b === 'Other') return -1; return a.localeCompare(b); }); // Sort tools within each category by priority, then name for (const cat of categories) { const arr = groupedMap.get(cat); arr.sort((t1, t2) => { const p1 = typeof t1.meta.priority === 'number' ? t1.meta.priority : Number.MAX_SAFE_INTEGER; const p2 = typeof t2.meta.priority === 'number' ? t2.meta.priority : Number.MAX_SAFE_INTEGER; if (p1 !== p2) return p1 - p2; return String(t1.meta.name).localeCompare(String(t2.meta.name)); }); } const grouped = categories.map(cat => [cat, groupedMap.get(cat)]); const newSection = generateCoreToolsSection(grouped); // Replace Core Tools section in README.md const readmePath = resolve(process.cwd(), 'README.md'); const readme = readFileSync(readmePath, 'utf8'); const lines = readme.split(/\r?\n/); const startIdx = lines.findIndex(l => l.trim().startsWith('## Core Tools')); if (startIdx === -1) { console.error('Could not find "## Core Tools" section in README.md'); process.exit(1); } let endIdx = lines.slice(startIdx + 1).findIndex(l => l.startsWith('## ')); if (endIdx === -1) { endIdx = lines.length - (startIdx + 1); } endIdx = startIdx + 1 + endIdx; const updated = [ ...lines.slice(0, startIdx + 1), newSection.trim(), ...lines.slice(endIdx) ].join('\n'); writeFileSync(readmePath, updated, 'utf8'); console.log('README.md Core Tools section updated.'); // Also update CLAUDE.md Available Tools section if present const claudePath = resolve(process.cwd(), 'CLAUDE.md'); try { const claude = readFileSync(claudePath, 'utf8'); const cLines = claude.split(/\r?\n/); const cStart = cLines.findIndex(l => l.trim().startsWith('## Available Tools')); if (cStart !== -1) { let cEnd = cLines.slice(cStart + 1).findIndex(l => l.startsWith('## ')); if (cEnd === -1) cEnd = cLines.length - (cStart + 1); cEnd = cStart + 1 + cEnd; const claudeSection = generateClaudeAvailableTools(grouped); const cUpdated = [ ...cLines.slice(0, cStart + 1), claudeSection.trim(), ...cLines.slice(cEnd) ].join('\n'); writeFileSync(claudePath, cUpdated, 'utf8'); console.log('CLAUDE.md Available Tools section updated.'); } } catch {} } run().catch(err => { console.error(err); process.exit(1); }); function generateClaudeAvailableTools(grouped) { const lines = []; lines.push(''); lines.push('**See `src/tools/common/registry.ts` for complete tool definitions and up-to-date descriptions.**'); lines.push(''); lines.push('### Tool Categories'); lines.push(''); for (const [category, tools] of grouped) { const names = tools.map(t => `\`${t.meta.name}\``).join(', '); lines.push(`**${category}** (${tools.length} tools):`); lines.push(`- ${names}`); lines.push(''); } return lines.join('\n'); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/antonzherdev/mcp-web-inspector'

If you have feedback or need assistance with the MCP directory API, please join our Discord server