hail-hydra-cc 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/files/commands/hydra/stats.md +62 -121
- package/files/hooks/hydra-statusline.js +35 -1
- package/files/hooks/hydra-token-math.js +163 -0
- package/package.json +1 -1
- package/src/files.js +1 -0
|
@@ -8,18 +8,9 @@ allowed-tools: Bash, Read
|
|
|
8
8
|
Read the active Claude Code session log and compute actual token usage and
|
|
9
9
|
savings. NO AI estimation — pure JSONL parsing.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`~/.claude/projects/{project-slug}/{session-id}.jsonl` (or
|
|
15
|
-
`$CLAUDE_CONFIG_DIR/projects/...` if overridden). The slug is the absolute
|
|
16
|
-
project path with path separators (`/`, `\`, `:`) replaced by `-` and any
|
|
17
|
-
leading `-` stripped.
|
|
18
|
-
|
|
19
|
-
Each assistant turn line includes a `message.usage` object with
|
|
20
|
-
`input_tokens`, `output_tokens`, `cache_read_input_tokens`,
|
|
21
|
-
`cache_creation_input_tokens`, and the `model` ID. We aggregate by model
|
|
22
|
-
tier and price it.
|
|
11
|
+
Math + JSONL parsing live in the shared helper at
|
|
12
|
+
`~/.claude/hooks/hydra-token-math.js`. Statusline and `/hydra:stats` both
|
|
13
|
+
call it so numbers stay consistent.
|
|
23
14
|
|
|
24
15
|
## Pricing (per 1M tokens, verified 2026-05 for Claude 4.x)
|
|
25
16
|
|
|
@@ -29,112 +20,54 @@ tier and price it.
|
|
|
29
20
|
| Sonnet | $3 | $15 | 10% of input |
|
|
30
21
|
| Opus | $5 | $25 | 10% of input |
|
|
31
22
|
|
|
32
|
-
Edit the `
|
|
23
|
+
Edit the `PRICING` map in `hydra-token-math.js` if Anthropic publishes new prices.
|
|
33
24
|
|
|
34
25
|
## Run
|
|
35
26
|
|
|
36
|
-
Execute this single Node command (works on Windows, macOS, Linux):
|
|
37
|
-
|
|
38
27
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
# Strikethrough capability detection — env-heuristic only.
|
|
29
|
+
USE_STRIKETHROUGH=0
|
|
30
|
+
[ "$TERM_PROGRAM" = "Apple_Terminal" ] && USE_STRIKETHROUGH=1
|
|
31
|
+
[ "$TERM_PROGRAM" = "iTerm.app" ] && USE_STRIKETHROUGH=1
|
|
32
|
+
[ "$TERM_PROGRAM" = "vscode" ] && USE_STRIKETHROUGH=1
|
|
33
|
+
[ -n "$KITTY_WINDOW_ID" ] && USE_STRIKETHROUGH=1
|
|
34
|
+
[ "$TERM" = "alacritty" ] && USE_STRIKETHROUGH=1
|
|
35
|
+
[ -n "$WEZTERM_PANE" ] && USE_STRIKETHROUGH=1
|
|
36
|
+
[ -n "$WT_SESSION" ] && USE_STRIKETHROUGH=1
|
|
37
|
+
# Known-incompatible terminals (force fallback, overrides green-list)
|
|
38
|
+
[ -n "$MSYSTEM" ] && USE_STRIKETHROUGH=0
|
|
39
|
+
[ -n "$CYGWIN" ] && USE_STRIKETHROUGH=0
|
|
40
|
+
echo "$TERM" | grep -q "cygwin" && USE_STRIKETHROUGH=0
|
|
41
|
+
# User override
|
|
42
|
+
[ "$HYDRA_STRIKETHROUGH" = "0" ] && USE_STRIKETHROUGH=0
|
|
43
|
+
[ "$HYDRA_STRIKETHROUGH" = "1" ] && USE_STRIKETHROUGH=1
|
|
44
|
+
|
|
45
|
+
HYDRA_USE_STRIKETHROUGH="$USE_STRIKETHROUGH" node -e "
|
|
41
46
|
const path = require('path');
|
|
42
47
|
const os = require('os');
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log('
|
|
48
|
+
const helperPath = path.join(os.homedir(), '.claude', 'hooks', 'hydra-token-math.js');
|
|
49
|
+
let tokenMath;
|
|
50
|
+
try {
|
|
51
|
+
tokenMath = require(helperPath);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.log('hydra-token-math.js not installed at ' + helperPath);
|
|
54
|
+
console.log('Run: hail-hydra-cc to (re)install Hydra hooks.');
|
|
49
55
|
process.exit(0);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const slug = cwd.replace(/[\\\\/:]/g, '-').replace(/^-+/, '');
|
|
55
|
-
|
|
56
|
-
// Try exact match first, then case-insensitive substring fallback
|
|
57
|
-
let sessionDir = path.join(projectsDir, slug);
|
|
58
|
-
if (!fs.existsSync(sessionDir)) {
|
|
59
|
-
const all = fs.readdirSync(projectsDir);
|
|
60
|
-
const match = all.find(d => d.toLowerCase() === slug.toLowerCase())
|
|
61
|
-
|| all.find(d => d.toLowerCase().endsWith(path.basename(cwd).toLowerCase()));
|
|
62
|
-
if (match) sessionDir = path.join(projectsDir, match);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!fs.existsSync(sessionDir)) {
|
|
58
|
+
const summary = tokenMath.computeSummary();
|
|
59
|
+
if (!summary.available) {
|
|
66
60
|
console.log('No session data for this project yet.');
|
|
67
|
-
console.log('Looked in: ' + sessionDir);
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const files = fs.readdirSync(sessionDir)
|
|
72
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
73
|
-
.map(f => ({ f, mtime: fs.statSync(path.join(sessionDir, f)).mtimeMs }))
|
|
74
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
75
|
-
|
|
76
|
-
if (files.length === 0) {
|
|
77
|
-
console.log('No session JSONL files found in ' + sessionDir);
|
|
78
61
|
process.exit(0);
|
|
79
62
|
}
|
|
80
63
|
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const stats = {
|
|
91
|
-
haiku: { input: 0, output: 0, cache_read: 0, cache_create: 0, turns: 0 },
|
|
92
|
-
sonnet: { input: 0, output: 0, cache_read: 0, cache_create: 0, turns: 0 },
|
|
93
|
-
opus: { input: 0, output: 0, cache_read: 0, cache_create: 0, turns: 0 }
|
|
94
|
-
};
|
|
95
|
-
const unknownModels = new Set();
|
|
96
|
-
let totalAssistantTurns = 0;
|
|
97
|
-
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
try {
|
|
100
|
-
const obj = JSON.parse(line);
|
|
101
|
-
if (obj.type !== 'assistant' || !obj.message || !obj.message.usage) continue;
|
|
102
|
-
const model = obj.message.model || '';
|
|
103
|
-
const usage = obj.message.usage;
|
|
104
|
-
let tier = null;
|
|
105
|
-
if (model.startsWith('claude-haiku')) tier = 'haiku';
|
|
106
|
-
else if (model.startsWith('claude-sonnet')) tier = 'sonnet';
|
|
107
|
-
else if (model.startsWith('claude-opus')) tier = 'opus';
|
|
108
|
-
if (!tier) { if (model) unknownModels.add(model); continue; }
|
|
109
|
-
stats[tier].input += usage.input_tokens || 0;
|
|
110
|
-
stats[tier].output += usage.output_tokens || 0;
|
|
111
|
-
stats[tier].cache_read += usage.cache_read_input_tokens || 0;
|
|
112
|
-
stats[tier].cache_create += usage.cache_creation_input_tokens || 0;
|
|
113
|
-
stats[tier].turns += 1;
|
|
114
|
-
totalAssistantTurns += 1;
|
|
115
|
-
} catch (e) { /* skip malformed */ }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function cost(s, p) {
|
|
119
|
-
const inputCost = ((s.input + s.cache_create) * p.input + s.cache_read * p.input * 0.1) / 1_000_000;
|
|
120
|
-
const outputCost = (s.output * p.output) / 1_000_000;
|
|
121
|
-
return inputCost + outputCost;
|
|
122
|
-
}
|
|
123
|
-
const haikuCost = cost(stats.haiku, pricing['claude-haiku-4']);
|
|
124
|
-
const sonnetCost = cost(stats.sonnet, pricing['claude-sonnet-4']);
|
|
125
|
-
const opusCost = cost(stats.opus, pricing['claude-opus-4']);
|
|
126
|
-
const actualCost = haikuCost + sonnetCost + opusCost;
|
|
127
|
-
|
|
128
|
-
function asOpus(s) {
|
|
129
|
-
const p = pricing['claude-opus-4'];
|
|
130
|
-
return ((s.input + s.cache_create) * p.input + s.cache_read * p.input * 0.1 + s.output * p.output) / 1_000_000;
|
|
131
|
-
}
|
|
132
|
-
const hypotheticalCost = asOpus(stats.haiku) + asOpus(stats.sonnet) + asOpus(stats.opus);
|
|
133
|
-
const savedUSD = hypotheticalCost - actualCost;
|
|
134
|
-
const savedPct = hypotheticalCost > 0 ? (savedUSD / hypotheticalCost * 100) : 0;
|
|
135
|
-
|
|
136
|
-
const totalDelegations = stats.haiku.turns + stats.sonnet.turns;
|
|
137
|
-
const delegationRate = totalAssistantTurns > 0 ? (totalDelegations / totalAssistantTurns * 100) : 0;
|
|
64
|
+
const useStrike = process.env.HYDRA_USE_STRIKETHROUGH === '1';
|
|
65
|
+
const STRIKE = useStrike ? '\x1b[9m' : '';
|
|
66
|
+
const STRIKE_OFF = useStrike ? '\x1b[29m' : '';
|
|
67
|
+
const GREEN = '\x1b[32m';
|
|
68
|
+
const BOLD = '\x1b[1m';
|
|
69
|
+
const DIM = '\x1b[2m';
|
|
70
|
+
const RESET = '\x1b[0m';
|
|
138
71
|
|
|
139
72
|
function fmt(n) {
|
|
140
73
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + 'M';
|
|
@@ -142,31 +75,41 @@ function fmt(n) {
|
|
|
142
75
|
return n.toString();
|
|
143
76
|
}
|
|
144
77
|
|
|
78
|
+
const { stats, totalTurns, haikuCost, sonnetCost, opusCost,
|
|
79
|
+
actualCost, hypotheticalCost, savedUSD, savedPct,
|
|
80
|
+
delegatedTurns, delegationRate, sessionFile, unknownModels } = summary;
|
|
81
|
+
|
|
145
82
|
const bar = '━'.repeat(40);
|
|
146
83
|
console.log('');
|
|
147
84
|
console.log('🐉 Hydra Stats');
|
|
148
85
|
console.log(bar);
|
|
149
86
|
console.log('Session: ' + path.basename(sessionFile));
|
|
150
|
-
console.log('Turns: ' +
|
|
87
|
+
console.log('Turns: ' + totalTurns);
|
|
151
88
|
console.log(bar);
|
|
152
89
|
console.log('');
|
|
153
|
-
console.log('🟢 Haiku (' + stats.haiku.turns + ' turns): ' + fmt(stats.haiku.input + stats.haiku.cache_create) + ' in / ' + fmt(stats.haiku.output) + ' out →
|
|
154
|
-
console.log('🔵 Sonnet (' + stats.sonnet.turns + ' turns): ' + fmt(stats.sonnet.input + stats.sonnet.cache_create) + ' in / ' + fmt(stats.sonnet.output) + ' out →
|
|
155
|
-
console.log('🟣 Opus (' + stats.opus.turns + ' turns): ' + fmt(stats.opus.input + stats.opus.cache_create) + ' in / ' + fmt(stats.opus.output) + ' out →
|
|
90
|
+
console.log('🟢 Haiku (' + stats.haiku.turns + ' turns): ' + fmt(stats.haiku.input + stats.haiku.cache_create) + ' in / ' + fmt(stats.haiku.output) + ' out → \$' + haikuCost.toFixed(3));
|
|
91
|
+
console.log('🔵 Sonnet (' + stats.sonnet.turns + ' turns): ' + fmt(stats.sonnet.input + stats.sonnet.cache_create) + ' in / ' + fmt(stats.sonnet.output) + ' out → \$' + sonnetCost.toFixed(3));
|
|
92
|
+
console.log('🟣 Opus (' + stats.opus.turns + ' turns): ' + fmt(stats.opus.input + stats.opus.cache_create) + ' in / ' + fmt(stats.opus.output) + ' out → \$' + opusCost.toFixed(3));
|
|
156
93
|
console.log(bar);
|
|
157
94
|
console.log('');
|
|
158
|
-
console.log('Delegation rate: ' + delegationRate.toFixed(1) + '% (' +
|
|
159
|
-
|
|
160
|
-
|
|
95
|
+
console.log('Delegation rate: ' + delegationRate.toFixed(1) + '% (' + delegatedTurns + '/' + totalTurns + ' turns)');
|
|
96
|
+
|
|
97
|
+
if (useStrike) {
|
|
98
|
+
console.log('Was: ' + DIM + STRIKE + '\$' + hypotheticalCost.toFixed(3) + STRIKE_OFF + RESET);
|
|
99
|
+
console.log('Now: ' + BOLD + GREEN + '\$' + actualCost.toFixed(3) + RESET);
|
|
100
|
+
} else {
|
|
101
|
+
console.log('Actual cost: \$' + actualCost.toFixed(3));
|
|
102
|
+
console.log('All-Opus baseline: \$' + hypotheticalCost.toFixed(3));
|
|
103
|
+
}
|
|
161
104
|
console.log(bar);
|
|
162
|
-
console.log('💰 Saved:
|
|
105
|
+
console.log('💰 ' + GREEN + 'Saved: \$' + savedUSD.toFixed(3) + ' (' + savedPct.toFixed(1) + '%)' + RESET);
|
|
163
106
|
console.log(bar);
|
|
164
107
|
console.log('');
|
|
165
108
|
console.log('Reads Claude Code session JSONL directly. No AI estimation.');
|
|
166
|
-
if (unknownModels.size > 0) {
|
|
109
|
+
if (unknownModels && unknownModels.size > 0) {
|
|
167
110
|
console.log('');
|
|
168
111
|
console.log('⚠️ Unknown models (not counted): ' + Array.from(unknownModels).join(', '));
|
|
169
|
-
console.log(' Update
|
|
112
|
+
console.log(' Update PRICING map in ~/.claude/hooks/hydra-token-math.js');
|
|
170
113
|
}
|
|
171
114
|
"
|
|
172
115
|
```
|
|
@@ -177,9 +120,7 @@ Print the output exactly as the script emits. Do not summarize or reformat.
|
|
|
177
120
|
|
|
178
121
|
## Notes
|
|
179
122
|
|
|
180
|
-
- `All-Opus baseline` is
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
- Cache-read pricing is 10% of input price (Anthropic prompt-caching rate
|
|
185
|
-
for Claude 4.x as of 2026-05).
|
|
123
|
+
- `All-Opus baseline` (or `Was:`) is hypothetical cost if every turn had been Opus.
|
|
124
|
+
- Stats are session-scoped.
|
|
125
|
+
- Cache-read pricing is 10% of input price (Anthropic prompt-caching rate, Claude 4.x, 2026-05).
|
|
126
|
+
- Strikethrough auto-detected from terminal env. Override: `HYDRA_STRIKETHROUGH=0` or `=1`.
|
|
@@ -50,6 +50,16 @@ process.stdin.on('end', () => {
|
|
|
50
50
|
// === Session Cost ===
|
|
51
51
|
const cost = (data.cost?.total_cost_usd || 0).toFixed(2);
|
|
52
52
|
|
|
53
|
+
// === Savings vs all-Opus baseline (cached, silent on failure) ===
|
|
54
|
+
let savingsStr = '';
|
|
55
|
+
try {
|
|
56
|
+
const tokenMath = require('./hydra-token-math');
|
|
57
|
+
const summary = tokenMath.computeSummaryCached();
|
|
58
|
+
if (summary.available && summary.savedUSD >= 0.01) {
|
|
59
|
+
savingsStr = ` \x1b[32m↓$${summary.savedUSD.toFixed(2)}\x1b[0m`;
|
|
60
|
+
}
|
|
61
|
+
} catch (e) { /* silent fallback */ }
|
|
62
|
+
|
|
53
63
|
// === Working Directory ===
|
|
54
64
|
const dirName = path.basename(data.workspace?.current_dir || data.cwd || '');
|
|
55
65
|
|
|
@@ -69,7 +79,7 @@ process.stdin.on('end', () => {
|
|
|
69
79
|
'\x1b[32m\uD83D\uDC32\x1b[0m', // Green dragon emoji (🐉)
|
|
70
80
|
`${dim}${model}${reset}`, // Dim model name
|
|
71
81
|
ctxDisplay, // Color-coded context bar
|
|
72
|
-
`${dim}$${cost}${reset}`,
|
|
82
|
+
`${dim}$${cost}${reset}${savingsStr}`, // Dim cost + green ↓savings
|
|
73
83
|
`${dim}${dirName}${reset}`, // Dim directory
|
|
74
84
|
];
|
|
75
85
|
|
|
@@ -85,6 +95,30 @@ process.stdin.on('end', () => {
|
|
|
85
95
|
parts.push(`\x1b[31m\u26A0 Auto-compact at 85%\x1b[0m`);
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
// === Sentinel Pending Warning ===
|
|
99
|
+
// Check if code changes were made but sentinel hasn't run yet
|
|
100
|
+
let sentinelWarning = '';
|
|
101
|
+
try {
|
|
102
|
+
const sentinelDir = path.join(os.tmpdir(), 'hydra-sentinel');
|
|
103
|
+
const sessionId = data.session_id || 'unknown';
|
|
104
|
+
const sentinelFlag = path.join(sentinelDir, `${sessionId}-pending.json`);
|
|
105
|
+
const pendingData = JSON.parse(fs.readFileSync(sentinelFlag, 'utf8'));
|
|
106
|
+
|
|
107
|
+
// Only show if flag is recent (within last 10 minutes)
|
|
108
|
+
// and has files pending
|
|
109
|
+
const age = Date.now() - (pendingData.updated_at || 0);
|
|
110
|
+
if (pendingData.files?.length > 0 && age < 600000) {
|
|
111
|
+
const count = pendingData.files.length;
|
|
112
|
+
sentinelWarning = ` \x1b[31m\u26A0 Sentinel pending (${count} files)\x1b[0m`;
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// No flag file — sentinel is clean or hasn't been needed
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (sentinelWarning) {
|
|
119
|
+
parts.push(sentinelWarning);
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
process.stdout.write(parts.join(' \u2502 '));
|
|
89
123
|
|
|
90
124
|
} catch (e) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Shared helper: parse Claude Code session JSONL, compute per-tier usage,
|
|
4
|
+
// actual cost, and savings vs all-Opus baseline.
|
|
5
|
+
//
|
|
6
|
+
// Used by:
|
|
7
|
+
// - hooks/hydra-statusline.js (cached, every statusline refresh)
|
|
8
|
+
// - commands/hydra/stats.md (fresh, on /hydra:stats invocation)
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const PRICING = {
|
|
15
|
+
'claude-haiku-4': { input: 1, output: 5 },
|
|
16
|
+
'claude-sonnet-4': { input: 3, output: 15 },
|
|
17
|
+
'claude-opus-4': { input: 5, output: 25 }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function getPrice(model) {
|
|
21
|
+
for (const prefix in PRICING) {
|
|
22
|
+
if (model.startsWith(prefix)) return PRICING[prefix];
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getTier(model) {
|
|
28
|
+
if (model.startsWith('claude-haiku')) return 'haiku';
|
|
29
|
+
if (model.startsWith('claude-sonnet')) return 'sonnet';
|
|
30
|
+
if (model.startsWith('claude-opus')) return 'opus';
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findActiveSessionFile() {
|
|
35
|
+
try {
|
|
36
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
37
|
+
const projectsDir = path.join(configDir, 'projects');
|
|
38
|
+
if (!fs.existsSync(projectsDir)) return null;
|
|
39
|
+
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
const slug = cwd.replace(/[\/\\:]/g, '-').replace(/^-+/, '');
|
|
42
|
+
|
|
43
|
+
let sessionDir = path.join(projectsDir, slug);
|
|
44
|
+
if (!fs.existsSync(sessionDir)) {
|
|
45
|
+
const all = fs.readdirSync(projectsDir);
|
|
46
|
+
const match = all.find(d => d.toLowerCase() === slug.toLowerCase())
|
|
47
|
+
|| all.find(d => d.toLowerCase().endsWith(path.basename(cwd).toLowerCase()));
|
|
48
|
+
if (!match) return null;
|
|
49
|
+
sessionDir = path.join(projectsDir, match);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const files = fs.readdirSync(sessionDir)
|
|
53
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
54
|
+
.map(f => ({ p: path.join(sessionDir, f), m: fs.statSync(path.join(sessionDir, f)).mtimeMs }))
|
|
55
|
+
.sort((a, b) => b.m - a.m);
|
|
56
|
+
|
|
57
|
+
return files.length > 0 ? files[0].p : null;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseSession(sessionFile) {
|
|
64
|
+
const stats = {
|
|
65
|
+
haiku: { input: 0, output: 0, cache_read: 0, cache_create: 0, turns: 0 },
|
|
66
|
+
sonnet: { input: 0, output: 0, cache_read: 0, cache_create: 0, turns: 0 },
|
|
67
|
+
opus: { input: 0, output: 0, cache_read: 0, cache_create: 0, turns: 0 }
|
|
68
|
+
};
|
|
69
|
+
const unknownModels = new Set();
|
|
70
|
+
let totalTurns = 0;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
try {
|
|
76
|
+
const obj = JSON.parse(line);
|
|
77
|
+
if (obj.type !== 'assistant' || !obj.message || !obj.message.usage) continue;
|
|
78
|
+
const model = obj.message.model || '';
|
|
79
|
+
const tier = getTier(model);
|
|
80
|
+
if (!tier) { if (model) unknownModels.add(model); continue; }
|
|
81
|
+
const u = obj.message.usage;
|
|
82
|
+
stats[tier].input += u.input_tokens || 0;
|
|
83
|
+
stats[tier].output += u.output_tokens || 0;
|
|
84
|
+
stats[tier].cache_read += u.cache_read_input_tokens || 0;
|
|
85
|
+
stats[tier].cache_create += u.cache_creation_input_tokens || 0;
|
|
86
|
+
stats[tier].turns += 1;
|
|
87
|
+
totalTurns += 1;
|
|
88
|
+
} catch (e) { /* skip malformed line */ }
|
|
89
|
+
}
|
|
90
|
+
} catch (e) { /* file unreadable */ }
|
|
91
|
+
|
|
92
|
+
return { stats, totalTurns, unknownModels };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tierCost(s, p) {
|
|
96
|
+
if (!p) return 0;
|
|
97
|
+
const inputCost = ((s.input + s.cache_create) * p.input) / 1_000_000;
|
|
98
|
+
const cacheReadCost = (s.cache_read * p.input * 0.1) / 1_000_000;
|
|
99
|
+
const outputCost = (s.output * p.output) / 1_000_000;
|
|
100
|
+
return inputCost + cacheReadCost + outputCost;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function asOpusCost(s) {
|
|
104
|
+
return tierCost(s, PRICING['claude-opus-4']);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function computeSummary() {
|
|
108
|
+
const sessionFile = findActiveSessionFile();
|
|
109
|
+
if (!sessionFile) return { available: false };
|
|
110
|
+
|
|
111
|
+
const { stats, totalTurns, unknownModels } = parseSession(sessionFile);
|
|
112
|
+
if (totalTurns === 0) return { available: false };
|
|
113
|
+
|
|
114
|
+
const haikuCost = tierCost(stats.haiku, PRICING['claude-haiku-4']);
|
|
115
|
+
const sonnetCost = tierCost(stats.sonnet, PRICING['claude-sonnet-4']);
|
|
116
|
+
const opusCost = tierCost(stats.opus, PRICING['claude-opus-4']);
|
|
117
|
+
const actualCost = haikuCost + sonnetCost + opusCost;
|
|
118
|
+
|
|
119
|
+
const hypotheticalCost =
|
|
120
|
+
asOpusCost(stats.haiku) + asOpusCost(stats.sonnet) + asOpusCost(stats.opus);
|
|
121
|
+
|
|
122
|
+
const savedUSD = Math.max(0, hypotheticalCost - actualCost);
|
|
123
|
+
const savedPct = hypotheticalCost > 0 ? (savedUSD / hypotheticalCost) * 100 : 0;
|
|
124
|
+
|
|
125
|
+
const delegatedTurns = stats.haiku.turns + stats.sonnet.turns;
|
|
126
|
+
const delegationRate = totalTurns > 0 ? (delegatedTurns / totalTurns) * 100 : 0;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
available: true,
|
|
130
|
+
sessionFile,
|
|
131
|
+
totalTurns,
|
|
132
|
+
stats,
|
|
133
|
+
haikuCost, sonnetCost, opusCost,
|
|
134
|
+
actualCost, hypotheticalCost,
|
|
135
|
+
savedUSD, savedPct,
|
|
136
|
+
delegatedTurns, delegationRate,
|
|
137
|
+
unknownModels
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let _cache = null;
|
|
142
|
+
let _cacheExpiry = 0;
|
|
143
|
+
const CACHE_TTL_MS = 15000;
|
|
144
|
+
|
|
145
|
+
function computeSummaryCached() {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (_cache && now < _cacheExpiry) return _cache;
|
|
148
|
+
_cache = computeSummary();
|
|
149
|
+
_cacheExpiry = now + CACHE_TTL_MS;
|
|
150
|
+
return _cache;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
computeSummary,
|
|
155
|
+
computeSummaryCached,
|
|
156
|
+
parseSession,
|
|
157
|
+
findActiveSessionFile,
|
|
158
|
+
tierCost,
|
|
159
|
+
asOpusCost,
|
|
160
|
+
getTier,
|
|
161
|
+
getPrice,
|
|
162
|
+
PRICING
|
|
163
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hail-hydra-cc",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Multi-agent orchestration framework for Claude Code. Routes tasks to specialized Haiku/Sonnet subagents while Opus orchestrates — inspired by speculative decoding. ~50% API cost reduction.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hail-hydra-cc": "bin/cli.js"
|
package/src/files.js
CHANGED
|
@@ -94,6 +94,7 @@ const commands = {
|
|
|
94
94
|
const hooks = {
|
|
95
95
|
'hydra-check-update': readBundled('hooks/hydra-check-update.js'),
|
|
96
96
|
'hydra-statusline': readBundled('hooks/hydra-statusline.js'),
|
|
97
|
+
'hydra-token-math': readBundled('hooks/hydra-token-math.js'),
|
|
97
98
|
'hydra-auto-guard': readBundled('hooks/hydra-auto-guard.js'),
|
|
98
99
|
'hydra-notify': readBundled('hooks/hydra-notify.js'),
|
|
99
100
|
};
|