#!/usr/bin/env node /** * token-hotspots CLI — emit ranked token hotspots and Opus 4.7 pattern findings * for a target repo path. * * Usage: * node token-hotspots-cli.mjs [path] [--json] [--output-file ] [--global] * [--with-telemetry-recipe] [--accurate-tokens] * * Exit codes: 0=ok, 3=unrecoverable error. * Zero external dependencies. */ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { writeFile, readFile, stat } from 'node:fs/promises'; import { discoverConfigFiles } from './lib/file-discovery.mjs'; import { resetCounter } from './lib/output.mjs'; import { scan } from './token-hotspots.mjs'; import * as tokenizerApi from './lib/tokenizer-api.mjs'; import { humanizeFindings } from './lib/humanizer.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const TELEMETRY_RECIPE_PATH = resolve(__dirname, '..', 'knowledge', 'cache-telemetry-recipe.md'); const ACCURATE_TOKENS_SAMPLE_SIZE = 3; async function calibrateAgainstApi(hotspots, apiKey) { const sampled = hotspots.slice(0, ACCURATE_TOKENS_SAMPLE_SIZE); let actualTokens = 0; for (const hotspot of sampled) { if (!hotspot?.path) continue; let content; try { content = await readFile(hotspot.path, 'utf-8'); } catch { continue; } const result = await tokenizerApi.callCountTokensApi(content, apiKey); actualTokens += result.input_tokens; } return { actual_tokens: actualTokens, source: 'count_tokens_api', sampled_hotspots: sampled.length, }; } async function main() { const args = process.argv.slice(2); let targetPath = '.'; let outputFile = null; let jsonMode = false; let rawMode = false; let includeGlobal = false; let withTelemetryRecipe = false; let accurateTokens = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--json') jsonMode = true; else if (args[i] === '--raw') rawMode = true; else if (args[i] === '--global') includeGlobal = true; else if (args[i] === '--with-telemetry-recipe') withTelemetryRecipe = true; else if (args[i] === '--accurate-tokens') accurateTokens = true; else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i]; else if (!args[i].startsWith('-')) targetPath = args[i]; } const absPath = resolve(targetPath); try { const s = await stat(absPath); if (!s.isDirectory()) { process.stderr.write(`Error: ${absPath} is not a directory\n`); process.exit(3); } } catch { process.stderr.write(`Error: path does not exist: ${absPath}\n`); process.exit(3); } resetCounter(); const discovery = await discoverConfigFiles(absPath, { includeGlobal }); const result = await scan(absPath, discovery); const payload = { scanner: result.scanner, status: result.status, files_scanned: result.files_scanned, duration_ms: result.duration_ms, total_estimated_tokens: result.total_estimated_tokens, hotspots: result.hotspots, findings: result.findings, counts: result.counts, }; if (withTelemetryRecipe) { payload.telemetry_recipe_path = TELEMETRY_RECIPE_PATH; } if (accurateTokens) { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey || apiKey.length === 0) { process.stderr.write('ANTHROPIC_API_KEY not set — skipping API calibration\n'); payload.calibration = { skipped: 'no-api-key' }; } else { try { payload.calibration = await calibrateAgainstApi(result.hotspots || [], apiKey); } catch (err) { // Error message is already key-masked by tokenizer-api.mjs. process.stderr.write(`Calibration error: ${err.message}\n`); payload.calibration = { skipped: 'api-error', error: err.message }; } } } // Default mode humanizes payload.findings (NOT result.findings). // --json and --raw bypass for v5.0.0 byte-equal output. if (!jsonMode && !rawMode) { payload.findings = humanizeFindings(payload.findings); } const json = JSON.stringify(payload, null, 2); if (outputFile) { await writeFile(outputFile, json, 'utf-8'); } if (jsonMode || rawMode || !outputFile) { process.stdout.write(json + '\n'); } } const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname); if (isDirectRun) { main().catch(err => { process.stderr.write(`Fatal: ${err.message}\n`); process.exit(3); }); }