UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

201 lines (181 loc) 5.69 kB
#!/usr/bin/env node /** * Token Counter Helper * * Count tokens in files or text using tiktoken (cl100k_base encoding). * This is the ONLY way to count tokens in the Genie framework. * * Usage: * node count-tokens.js <file-path> # Output: just token count number * node count-tokens.js <file-path> --json # Output: JSON with metadata * echo "some text" | node count-tokens.js # Output: just token count * node count-tokens.js --before=file --after=file # Compare (always JSON) * * Output (default): * Plain number: 1234 * * Output (--json): * JSON: { tokens: N, lines: N, bytes: N, encoding: "cl100k_base" } */ const fs = require('fs'); const path = require('path'); /** * Count tokens using tiktoken */ function countTokens(text) { try { const { getEncoding } = require('js-tiktoken'); const encName = process.env.TOKEN_ENCODING || 'cl100k_base'; const encoder = getEncoding(encName); const tokens = encoder.encode(text).length; try { encoder.free && encoder.free(); } catch {} return { tokens, encoding: encName, method: 'tiktoken' }; } catch (err) { // Fallback to word count approximation (should not happen if tiktoken installed) console.error(`Warning: tiktoken failed, using word count fallback: ${err.message}`); const approx = (text || '').trim().split(/\s+/).filter(Boolean).length; return { tokens: approx, encoding: 'approx-words', method: 'fallback' }; } } /** * Get file stats (lines, bytes) */ function getStats(text) { const lines = (text.match(/\n/g) || []).length + 1; const bytes = Buffer.byteLength(text, 'utf8'); return { lines, bytes }; } /** * Count tokens in file */ function countFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf-8'); const { tokens, encoding, method } = countTokens(content); const { lines, bytes } = getStats(content); return { file: path.basename(filePath), path: filePath, tokens, lines, bytes, encoding, method, }; } catch (err) { return { error: err.message, file: filePath, }; } } /** * Compare before/after token counts */ function compareFiles(beforePath, afterPath) { const before = countFile(beforePath); const after = countFile(afterPath); if (before.error || after.error) { return { error: 'Failed to read files' }; } const diff = after.tokens - before.tokens; const percentChange = ((diff / before.tokens) * 100).toFixed(1); const saved = diff < 0; return { before: { file: before.file, tokens: before.tokens, lines: before.lines, bytes: before.bytes, }, after: { file: after.file, tokens: after.tokens, lines: after.lines, bytes: after.bytes, }, diff: { tokens: diff, percent: percentChange, saved: saved, message: saved ? `Saved ${Math.abs(diff)} tokens (${Math.abs(percentChange)}% reduction)` : `Added ${diff} tokens (${percentChange}% increase)`, }, encoding: before.encoding, }; } /** * Main */ function main() { const args = process.argv.slice(2); const jsonMode = args.includes('--json'); const fileArgs = args.filter(a => !a.startsWith('--')); // Check for comparison mode (--before=file --after=file) const beforeArg = args.find(a => a.startsWith('--before=')); const afterArg = args.find(a => a.startsWith('--after=')); if (beforeArg && afterArg) { const beforePath = beforeArg.split('=')[1]; const afterPath = afterArg.split('=')[1]; const result = compareFiles(beforePath, afterPath); console.log(JSON.stringify(result, null, 2)); process.exit(result.error ? 1 : 0); } // Single file mode if (fileArgs.length > 0) { const filePath = fileArgs[0]; if (!fs.existsSync(filePath)) { console.error(jsonMode ? JSON.stringify({ error: `File not found: ${filePath}` }) : `Error: File not found: ${filePath}`); process.exit(1); } const result = countFile(filePath); if (result.error) { console.error(jsonMode ? JSON.stringify(result) : `Error: ${result.error}`); process.exit(1); } console.log(jsonMode ? JSON.stringify(result, null, 2) : result.tokens.toString()); process.exit(0); } // Stdin mode if (!process.stdin.isTTY) { let input = ''; process.stdin.setEncoding('utf-8'); process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', () => { const { tokens, encoding, method } = countTokens(input); const { lines, bytes } = getStats(input); if (jsonMode) { console.log(JSON.stringify({ tokens, lines, bytes, encoding, method, }, null, 2)); } else { console.log(tokens.toString()); } }); return; } // No input, show usage console.error(` Usage: node count-tokens.js <file-path> # Output: just token count (number) node count-tokens.js <file-path> --json # Output: JSON with metadata echo "text" | node count-tokens.js # Output: just token count node count-tokens.js --before=old.md --after=new.md # Compare files (always JSON) Output (default): Plain number (e.g., "1234") Output (--json): JSON with token count, lines, bytes, encoding Examples: node count-tokens.js README.md # 1234 node count-tokens.js README.md --json # { "tokens": 1234, ... } node count-tokens.js --before=before.md --after=after.md cat myfile.md | node count-tokens.js `); process.exit(1); } main();