120 lines
3.2 KiB
JavaScript
120 lines
3.2 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const DEFAULT_BASELINE_MULTIPLIER = 2.9;
|
|
|
|
function readJsonl(filePath) {
|
|
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
|
|
return fs
|
|
.readFileSync(filePath, "utf8")
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
try {
|
|
return JSON.parse(line);
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function collectUsage(entry) {
|
|
const candidates = [
|
|
entry.usage,
|
|
entry.message && entry.message.usage,
|
|
entry.response && entry.response.usage,
|
|
entry.result && entry.result.usage,
|
|
].filter(Boolean);
|
|
|
|
return candidates.reduce(
|
|
(sum, usage) => {
|
|
sum.input += Number(usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0);
|
|
sum.output += Number(usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0);
|
|
return sum;
|
|
},
|
|
{ input: 0, output: 0 },
|
|
);
|
|
}
|
|
|
|
function compactNumber(value) {
|
|
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}m`;
|
|
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
|
|
return String(value);
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return Math.round(value).toLocaleString("en-US");
|
|
}
|
|
|
|
function buildStats(input) {
|
|
const transcriptPath = input.transcript_path || input.transcriptPath;
|
|
const entries = readJsonl(transcriptPath);
|
|
const usage = entries.reduce(
|
|
(sum, entry) => {
|
|
const item = collectUsage(entry);
|
|
sum.input += item.input;
|
|
sum.output += item.output;
|
|
return sum;
|
|
},
|
|
{ input: 0, output: 0 },
|
|
);
|
|
|
|
const turns = entries.filter((entry) => entry.type === "assistant" || entry.type === "user").length;
|
|
const baselineOutput = Math.round(usage.output * DEFAULT_BASELINE_MULTIPLIER);
|
|
const saved = Math.max(0, baselineOutput - usage.output);
|
|
const percent = baselineOutput ? Math.round((saved / baselineOutput) * 100) : 0;
|
|
|
|
return {
|
|
turns,
|
|
input: usage.input,
|
|
output: usage.output,
|
|
baselineOutput,
|
|
saved,
|
|
percent,
|
|
transcriptPath,
|
|
};
|
|
}
|
|
|
|
function writeLifetimeSuffix(input, stats) {
|
|
const cwd = input.cwd || process.cwd();
|
|
const outPath = path.join(cwd, ".agents", "hooks", ".caveman-lifetime-savings");
|
|
|
|
try {
|
|
fs.writeFileSync(outPath, ` ${compactNumber(stats.saved)}`, "utf8");
|
|
} catch {
|
|
// Stats display should still work if this optional badge write fails.
|
|
}
|
|
}
|
|
|
|
function formatStats(stats) {
|
|
if (!stats.transcriptPath) {
|
|
return "No transcript path in hook input. Cannot compute real token stats.";
|
|
}
|
|
|
|
if (!stats.input && !stats.output) {
|
|
return [
|
|
"No token usage found in current transcript.",
|
|
`Transcript: ${stats.transcriptPath}`,
|
|
].join("\n");
|
|
}
|
|
|
|
return [
|
|
`Session: ${formatNumber(stats.turns)} turns`,
|
|
`Input: ${formatNumber(stats.input)} tokens`,
|
|
`Output: ${formatNumber(stats.output)} tokens (caveman)`,
|
|
`Baseline:${formatNumber(stats.baselineOutput).padStart(10)} tokens (estimated without caveman)`,
|
|
`Saved: ${formatNumber(stats.saved).padStart(10)} tokens (~${stats.percent}%)`,
|
|
].join("\n");
|
|
}
|
|
|
|
function run(input) {
|
|
const stats = buildStats(input || {});
|
|
writeLifetimeSuffix(input || {}, stats);
|
|
return formatStats(stats);
|
|
}
|
|
|
|
module.exports = { run, buildStats, formatStats };
|
|
|