mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-16 01:12:13 +08:00
Integrates useful changes from #1882, #1884, #1889, #1893, #1898, #1899, and #1903: - fix rule install docs to preserve language directories - correct Ruby security command examples - harden dev-server hook command-substitution parsing - add Prisma patterns skill and catalog/package surfaces - allow first-time protected config creation while blocking existing configs - read cost metrics from Stop hook transcripts - emit suggest-compact additionalContext on stdout Co-authored-by: Jamkris <dltmdgus1412@gmail.com> Co-authored-by: Levi-Evan <levishantz@gmail.com> Co-authored-by: gaurav0107 <gauravdubey0107@gmail.com> Co-authored-by: richm-spp <richard.millar@salarypackagingplus.com.au> Co-authored-by: zomia <zomians@outlook.jp> Co-authored-by: donghyeun02 <donghyeun02@gmail.com>
158 lines
5.0 KiB
JavaScript
Executable File
158 lines
5.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Cost Tracker Hook (v2)
|
|
*
|
|
* Reads transcript_path from Stop hook stdin, sums usage across all
|
|
* assistant turns in the session JSONL, and appends one row to
|
|
* ~/.claude/metrics/costs.jsonl.
|
|
*
|
|
* Stop hook stdin payload: { session_id, transcript_path, cwd, hook_event_name, ... }
|
|
* The Stop payload does NOT include `usage` or `model` directly. The previous
|
|
* version of this hook expected those fields and silently produced zero-filled
|
|
* rows (verified: 2,340 rows captured with 0.0% non-zero token rate over 52
|
|
* days). The fix is to read the transcript file Claude Code already passes us.
|
|
*
|
|
* JSONL assistant entry shape (per Claude Code):
|
|
* { type: "assistant", message: { model, usage: { input_tokens, output_tokens,
|
|
* cache_creation_input_tokens, cache_read_input_tokens } } }
|
|
*
|
|
* Cumulative behavior: Stop fires per assistant response, not per session.
|
|
* Each row therefore represents the cumulative session total up to that point.
|
|
* To get per-session cost, take the last row per session_id. To get per-day
|
|
* spend, aggregate.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { ensureDir, appendFile, getClaudeDir } = require('../lib/utils');
|
|
const { sanitizeSessionId } = require('../lib/session-bridge');
|
|
|
|
// Approximate per-1M-token billing rates (USD).
|
|
// Cache creation: 1.25x input rate. Cache read: 0.1x input rate.
|
|
const RATE_TABLE = {
|
|
haiku: { in: 0.80, out: 4.0, cacheWrite: 1.00, cacheRead: 0.08 },
|
|
sonnet: { in: 3.00, out: 15.0, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
opus: { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 }
|
|
};
|
|
|
|
function getRates(model) {
|
|
const m = String(model || '').toLowerCase();
|
|
if (m.includes('haiku')) return RATE_TABLE.haiku;
|
|
if (m.includes('opus')) return RATE_TABLE.opus;
|
|
return RATE_TABLE.sonnet;
|
|
}
|
|
|
|
function toNumber(v) {
|
|
const n = Number(v);
|
|
return Number.isFinite(n) ? n : 0;
|
|
}
|
|
|
|
/**
|
|
* Scan the session JSONL and sum token usage across all assistant turns.
|
|
* Returns { inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, model }
|
|
* or null on read failure.
|
|
*/
|
|
function sumUsageFromTranscript(transcriptPath) {
|
|
let content;
|
|
try {
|
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
let inputTokens = 0;
|
|
let outputTokens = 0;
|
|
let cacheWriteTokens = 0;
|
|
let cacheReadTokens = 0;
|
|
let model = 'unknown';
|
|
|
|
for (const line of content.split('\n')) {
|
|
if (!line.trim()) continue;
|
|
let entry;
|
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
|
|
if (entry.type !== 'assistant') continue;
|
|
const msg = entry.message;
|
|
if (!msg || !msg.usage) continue;
|
|
|
|
const u = msg.usage;
|
|
inputTokens += toNumber(u.input_tokens);
|
|
outputTokens += toNumber(u.output_tokens);
|
|
cacheWriteTokens += toNumber(u.cache_creation_input_tokens);
|
|
cacheReadTokens += toNumber(u.cache_read_input_tokens);
|
|
|
|
if (msg.model && msg.model !== 'unknown') model = msg.model;
|
|
}
|
|
|
|
return { inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, model };
|
|
}
|
|
|
|
const MAX_STDIN = 64 * 1024;
|
|
let raw = '';
|
|
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => {
|
|
if (raw.length < MAX_STDIN) raw += chunk.substring(0, MAX_STDIN - raw.length);
|
|
});
|
|
|
|
process.stdin.on('end', () => {
|
|
try {
|
|
const input = raw.trim() ? JSON.parse(raw) : {};
|
|
|
|
const transcriptPath = (typeof input.transcript_path === 'string' && input.transcript_path)
|
|
? input.transcript_path
|
|
: process.env.CLAUDE_TRANSCRIPT_PATH || null;
|
|
|
|
const sessionId =
|
|
sanitizeSessionId(input.session_id) ||
|
|
sanitizeSessionId(process.env.ECC_SESSION_ID) ||
|
|
sanitizeSessionId(process.env.CLAUDE_SESSION_ID) ||
|
|
'default';
|
|
|
|
let usageTotals = null;
|
|
if (transcriptPath && fs.existsSync(transcriptPath)) {
|
|
usageTotals = sumUsageFromTranscript(transcriptPath);
|
|
}
|
|
|
|
const {
|
|
inputTokens = 0,
|
|
outputTokens = 0,
|
|
cacheWriteTokens = 0,
|
|
cacheReadTokens = 0,
|
|
model = 'unknown'
|
|
} = usageTotals || {};
|
|
|
|
const rates = getRates(model);
|
|
const estimatedCostUsd = Math.round((
|
|
(inputTokens / 1e6) * rates.in +
|
|
(outputTokens / 1e6) * rates.out +
|
|
(cacheWriteTokens / 1e6) * rates.cacheWrite +
|
|
(cacheReadTokens / 1e6) * rates.cacheRead
|
|
) * 1e6) / 1e6;
|
|
|
|
const metricsDir = path.join(getClaudeDir(), 'metrics');
|
|
ensureDir(metricsDir);
|
|
|
|
const row = {
|
|
timestamp: new Date().toISOString(),
|
|
session_id: sessionId,
|
|
transcript_path: transcriptPath || '',
|
|
model,
|
|
input_tokens: inputTokens,
|
|
output_tokens: outputTokens,
|
|
cache_write_tokens: cacheWriteTokens,
|
|
cache_read_tokens: cacheReadTokens,
|
|
estimated_cost_usd: estimatedCostUsd
|
|
};
|
|
|
|
appendFile(path.join(metricsDir, 'costs.jsonl'), `${JSON.stringify(row)}\n`);
|
|
} catch {
|
|
// Non-blocking — never fail the Stop hook.
|
|
}
|
|
|
|
// Pass stdin through (required by ECC hook convention).
|
|
process.stdout.write(raw);
|
|
});
|