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.
@@ -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
- ## How It Works
12
-
13
- Claude Code writes every conversation turn to a JSONL file at
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 `pricing` map below if Anthropic publishes new prices.
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
- node -e "
40
- const fs = require('fs');
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
- const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
45
- const projectsDir = path.join(configDir, 'projects');
46
-
47
- if (!fs.existsSync(projectsDir)) {
48
- console.log('No Claude Code projects directory found at ' + projectsDir);
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
- // Slug = absolute cwd with /, \\, : replaced by -, leading - stripped
53
- const cwd = process.cwd();
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 sessionFile = path.join(sessionDir, files[0].f);
82
- const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
83
-
84
- const pricing = {
85
- 'claude-haiku-4': { input: 1, output: 5 },
86
- 'claude-sonnet-4': { input: 3, output: 15 },
87
- 'claude-opus-4': { input: 5, output: 25 }
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: ' + totalAssistantTurns);
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 → $' + haikuCost.toFixed(3));
154
- console.log('🔵 Sonnet (' + stats.sonnet.turns + ' turns): ' + fmt(stats.sonnet.input + stats.sonnet.cache_create) + ' in / ' + fmt(stats.sonnet.output) + ' out → $' + sonnetCost.toFixed(3));
155
- console.log('🟣 Opus (' + stats.opus.turns + ' turns): ' + fmt(stats.opus.input + stats.opus.cache_create) + ' in / ' + fmt(stats.opus.output) + ' out → $' + opusCost.toFixed(3));
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) + '% (' + totalDelegations + '/' + totalAssistantTurns + ' turns)');
159
- console.log('Actual cost: $' + actualCost.toFixed(3));
160
- console.log('All-Opus baseline: $' + hypotheticalCost.toFixed(3));
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: $' + savedUSD.toFixed(3) + ' (' + savedPct.toFixed(1) + '%)');
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 pricing map in ~/.claude/commands/hydra/stats.md');
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 the hypothetical cost if every turn (including
181
- Haiku and Sonnet ones) had been Opus. The savings show what Hydra's model
182
- routing actually saved this session.
183
- - Stats are session-scoped. Future versions may add `--all` and `--since`.
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}`, // Dim cost
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.0",
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
  };