hail-hydra-cc 2.3.0 → 2.3.2

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.
@@ -1,54 +1,54 @@
1
- #!/usr/bin/env node
2
-
3
- // Hydra Auto-Guard Hook — PostToolUse (matcher: Write|Edit|MultiEdit)
4
- // Records changed file paths to a temp file for hydra-guard to scan later.
5
- // Does NOT run the scan itself — that would slow down every edit.
6
- // Overhead: <1ms per edit. Files deduped by path.
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const os = require('os');
11
-
12
- let input = '';
13
- process.stdin.on('data', (chunk) => (input += chunk));
14
- process.stdin.on('end', () => {
15
- try {
16
- const data = JSON.parse(input);
17
-
18
- // Extract the file path from the tool input
19
- const filePath = data.tool_input?.file_path ||
20
- data.tool_input?.path ||
21
- null;
22
-
23
- if (!filePath) {
24
- process.exit(0);
25
- }
26
-
27
- // Append to session-scoped changed files list
28
- const sessionId = data.session_id || 'unknown';
29
- const trackingDir = path.join(os.tmpdir(), 'hydra-guard');
30
- const trackingFile = path.join(trackingDir, `${sessionId}.txt`);
31
-
32
- // Ensure directory exists
33
- if (!fs.existsSync(trackingDir)) {
34
- fs.mkdirSync(trackingDir, { recursive: true });
35
- }
36
-
37
- // Read existing tracked files
38
- let existing = '';
39
- try {
40
- existing = fs.readFileSync(trackingFile, 'utf8');
41
- } catch (e) {
42
- // File doesn't exist yet — that's fine
43
- }
44
-
45
- // Only append if not already tracked (dedup)
46
- if (!existing.split('\n').includes(filePath)) {
47
- fs.appendFileSync(trackingFile, filePath + '\n');
48
- }
49
-
50
- } catch (e) {
51
- // Silently fail — NEVER block Claude Code
52
- }
53
- process.exit(0);
54
- });
1
+ #!/usr/bin/env node
2
+
3
+ // Hydra Auto-Guard Hook — PostToolUse (matcher: Write|Edit|MultiEdit)
4
+ // Records changed file paths to a temp file for hydra-guard to scan later.
5
+ // Does NOT run the scan itself — that would slow down every edit.
6
+ // Overhead: <1ms per edit. Files deduped by path.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ let input = '';
13
+ process.stdin.on('data', (chunk) => (input += chunk));
14
+ process.stdin.on('end', () => {
15
+ try {
16
+ const data = JSON.parse(input);
17
+
18
+ // Extract the file path from the tool input
19
+ const filePath = data.tool_input?.file_path ||
20
+ data.tool_input?.path ||
21
+ null;
22
+
23
+ if (!filePath) {
24
+ process.exit(0);
25
+ }
26
+
27
+ // Append to session-scoped changed files list
28
+ const sessionId = data.session_id || 'unknown';
29
+ const trackingDir = path.join(os.tmpdir(), 'hydra-guard');
30
+ const trackingFile = path.join(trackingDir, `${sessionId}.txt`);
31
+
32
+ // Ensure directory exists
33
+ if (!fs.existsSync(trackingDir)) {
34
+ fs.mkdirSync(trackingDir, { recursive: true });
35
+ }
36
+
37
+ // Read existing tracked files
38
+ let existing = '';
39
+ try {
40
+ existing = fs.readFileSync(trackingFile, 'utf8');
41
+ } catch (e) {
42
+ // File doesn't exist yet — that's fine
43
+ }
44
+
45
+ // Only append if not already tracked (dedup)
46
+ if (!existing.split('\n').includes(filePath)) {
47
+ fs.appendFileSync(trackingFile, filePath + '\n');
48
+ }
49
+
50
+ } catch (e) {
51
+ // Silently fail — NEVER block Claude Code
52
+ }
53
+ process.exit(0);
54
+ });
@@ -1,99 +1,99 @@
1
- #!/usr/bin/env node
2
-
3
- // Hydra Update Checker — SessionStart hook
4
- // Spawns a DETACHED background process to check npm for updates.
5
- // Writes result to ~/.claude/cache/hydra-update-check.json
6
- // The statusline hook reads this cache file.
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const os = require('os');
11
- const { spawn } = require('child_process');
12
-
13
- const homeDir = os.homedir();
14
- const cacheDir = path.join(homeDir, '.claude', 'cache');
15
- const cacheFile = path.join(cacheDir, 'hydra-update-check.json');
16
-
17
- // VERSION file locations (check project first, then global)
18
- const projectVersionFile = path.join(process.cwd(), '.claude', 'hydra', 'VERSION');
19
- const globalVersionFile = path.join(homeDir, '.claude', 'hydra', 'VERSION');
20
-
21
- // Ensure cache directory exists
22
- if (!fs.existsSync(cacheDir)) {
23
- fs.mkdirSync(cacheDir, { recursive: true });
24
- }
25
-
26
- // Skip check if we checked within the last hour
27
- try {
28
- const existing = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
29
- const age = Date.now() - (existing.checked_at || 0);
30
- if (age < 3600000) { // 1 hour
31
- process.exit(0);
32
- }
33
- } catch (e) {
34
- // No cache or invalid — proceed with check
35
- }
36
-
37
- // Read stdin to prevent EPIPE (Claude Code pipes JSON to all hooks)
38
- let stdinData = '';
39
- process.stdin.on('data', (chunk) => (stdinData += chunk));
40
- process.stdin.on('end', () => {
41
- // Spawn background process (MUST be detached to not block Claude Code)
42
- const child = spawn(process.execPath, ['-e', `
43
- const fs = require('fs');
44
- const { execSync } = require('child_process');
45
-
46
- const cacheFile = ${JSON.stringify(cacheFile)};
47
- const projectVersionFile = ${JSON.stringify(projectVersionFile)};
48
- const globalVersionFile = ${JSON.stringify(globalVersionFile)};
49
-
50
- try {
51
- // Read installed version
52
- let installed = 'unknown';
53
- try {
54
- installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
55
- } catch (e) {
56
- try {
57
- installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
58
- } catch (e2) {}
59
- }
60
-
61
- // Fetch latest version from npm (with timeout)
62
- const latest = execSync('npm view hail-hydra-cc version', {
63
- encoding: 'utf8',
64
- timeout: 10000,
65
- windowsHide: true,
66
- stdio: ['pipe', 'pipe', 'pipe']
67
- }).trim();
68
-
69
- // Compare and write cache
70
- const updateAvailable = installed !== 'unknown' && latest !== installed;
71
-
72
- const result = {
73
- installed: installed,
74
- latest: latest,
75
- update_available: updateAvailable,
76
- checked_at: Date.now()
77
- };
78
-
79
- fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2));
80
- } catch (e) {
81
- // Network error or npm not available — write a "no check" result
82
- const result = {
83
- installed: 'unknown',
84
- latest: 'unknown',
85
- update_available: false,
86
- checked_at: Date.now(),
87
- error: e.message
88
- };
89
- fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2));
90
- }
91
- `], {
92
- stdio: 'ignore',
93
- windowsHide: true,
94
- detached: true // CRITICAL: prevents blocking Claude Code input on Windows
95
- });
96
-
97
- child.unref();
98
- process.exit(0);
99
- });
1
+ #!/usr/bin/env node
2
+
3
+ // Hydra Update Checker — SessionStart hook
4
+ // Spawns a DETACHED background process to check npm for updates.
5
+ // Writes result to ~/.claude/cache/hydra-update-check.json
6
+ // The statusline hook reads this cache file.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const { spawn } = require('child_process');
12
+
13
+ const homeDir = os.homedir();
14
+ const cacheDir = path.join(homeDir, '.claude', 'cache');
15
+ const cacheFile = path.join(cacheDir, 'hydra-update-check.json');
16
+
17
+ // VERSION file locations (check project first, then global)
18
+ const projectVersionFile = path.join(process.cwd(), '.claude', 'hydra', 'VERSION');
19
+ const globalVersionFile = path.join(homeDir, '.claude', 'hydra', 'VERSION');
20
+
21
+ // Ensure cache directory exists
22
+ if (!fs.existsSync(cacheDir)) {
23
+ fs.mkdirSync(cacheDir, { recursive: true });
24
+ }
25
+
26
+ // Skip check if we checked within the last hour
27
+ try {
28
+ const existing = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
29
+ const age = Date.now() - (existing.checked_at || 0);
30
+ if (age < 3600000) { // 1 hour
31
+ process.exit(0);
32
+ }
33
+ } catch (e) {
34
+ // No cache or invalid — proceed with check
35
+ }
36
+
37
+ // Read stdin to prevent EPIPE (Claude Code pipes JSON to all hooks)
38
+ let stdinData = '';
39
+ process.stdin.on('data', (chunk) => (stdinData += chunk));
40
+ process.stdin.on('end', () => {
41
+ // Spawn background process (MUST be detached to not block Claude Code)
42
+ const child = spawn(process.execPath, ['-e', `
43
+ const fs = require('fs');
44
+ const { execSync } = require('child_process');
45
+
46
+ const cacheFile = ${JSON.stringify(cacheFile)};
47
+ const projectVersionFile = ${JSON.stringify(projectVersionFile)};
48
+ const globalVersionFile = ${JSON.stringify(globalVersionFile)};
49
+
50
+ try {
51
+ // Read installed version
52
+ let installed = 'unknown';
53
+ try {
54
+ installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
55
+ } catch (e) {
56
+ try {
57
+ installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
58
+ } catch (e2) {}
59
+ }
60
+
61
+ // Fetch latest version from npm (with timeout)
62
+ const latest = execSync('npm view hail-hydra-cc version', {
63
+ encoding: 'utf8',
64
+ timeout: 10000,
65
+ windowsHide: true,
66
+ stdio: ['pipe', 'pipe', 'pipe']
67
+ }).trim();
68
+
69
+ // Compare and write cache
70
+ const updateAvailable = installed !== 'unknown' && latest !== installed;
71
+
72
+ const result = {
73
+ installed: installed,
74
+ latest: latest,
75
+ update_available: updateAvailable,
76
+ checked_at: Date.now()
77
+ };
78
+
79
+ fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2));
80
+ } catch (e) {
81
+ // Network error or npm not available — write a "no check" result
82
+ const result = {
83
+ installed: 'unknown',
84
+ latest: 'unknown',
85
+ update_available: false,
86
+ checked_at: Date.now(),
87
+ error: e.message
88
+ };
89
+ fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2));
90
+ }
91
+ `], {
92
+ stdio: 'ignore',
93
+ windowsHide: true,
94
+ detached: true // CRITICAL: prevents blocking Claude Code input on Windows
95
+ });
96
+
97
+ child.unref();
98
+ process.exit(0);
99
+ });
@@ -1,94 +1,128 @@
1
- #!/usr/bin/env node
2
-
3
- // Hydra StatusLine — persistent status bar at bottom of Claude Code
4
- // Receives session JSON via stdin, outputs one formatted line to stdout.
5
- //
6
- // Display format:
7
- // 🐉 │ Opus │ Ctx: 37% ████░░░░░░ │ $0.42 │ my-project │ ⚡ Update available
8
- //
9
- // Context bar is color-coded:
10
- // Green (0-49%) → Yellow (50-79%) → Red (80%+)
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const os = require('os');
15
-
16
- const cacheFile = path.join(os.homedir(), '.claude', 'cache', 'hydra-update-check.json');
17
-
18
- let input = '';
19
- process.stdin.on('data', (chunk) => (input += chunk));
20
- process.stdin.on('end', () => {
21
- try {
22
- const data = JSON.parse(input);
23
-
24
- // === Model ===
25
- const model = data.model?.display_name || 'Unknown';
26
-
27
- // === Context Usage ===
28
- // Use precomputed used_percentage from Claude Code (most reliable)
29
- const ctxPct = Math.round(data.context_window?.used_percentage || 0);
30
-
31
- // Build visual context bar (10 chars wide)
32
- const filled = Math.round(ctxPct / 10);
33
- const empty = 10 - filled;
34
- const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
35
-
36
- // Color-code: Green <50%, Yellow 50-79%, Red 80%+
37
- let ctxColor;
38
- if (ctxPct < 50) {
39
- ctxColor = '\x1b[32m'; // Green
40
- } else if (ctxPct < 80) {
41
- ctxColor = '\x1b[33m'; // Yellow
42
- } else {
43
- ctxColor = '\x1b[31m'; // Red
44
- }
45
- const reset = '\x1b[0m';
46
- const dim = '\x1b[2m';
47
-
48
- const ctxDisplay = `${ctxColor}Ctx: ${ctxPct}% ${bar}${reset}`;
49
-
50
- // === Session Cost ===
51
- const cost = (data.cost?.total_cost_usd || 0).toFixed(2);
52
-
53
- // === Working Directory ===
54
- const dirName = path.basename(data.workspace?.current_dir || data.cwd || '');
55
-
56
- // === Update Check (read from cache) ===
57
- let updateNotice = '';
58
- try {
59
- const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
60
- if (cache.update_available) {
61
- updateNotice = ` \x1b[33m\u26A1 v${cache.latest} available${reset}`;
62
- }
63
- } catch (e) {
64
- // No cache skip update notice
65
- }
66
-
67
- // === Compose Status Line ===
68
- const parts = [
69
- '\x1b[32m\uD83D\uDC32\x1b[0m', // Green dragon emoji (🐉)
70
- `${dim}${model}${reset}`, // Dim model name
71
- ctxDisplay, // Color-coded context bar
72
- `${dim}$${cost}${reset}`, // Dim cost
73
- `${dim}${dirName}${reset}`, // Dim directory
74
- ];
75
-
76
- // Append update notice if available
77
- if (updateNotice) {
78
- parts.push(updateNotice);
79
- }
80
-
81
- // Compaction warning — only show at 70%+ context usage
82
- if (ctxPct >= 80) {
83
- parts.push(`\x1b[31m\u26A0 Compacting soon!\x1b[0m`);
84
- } else if (ctxPct >= 70) {
85
- parts.push(`\x1b[31m\u26A0 Auto-compact at 85%\x1b[0m`);
86
- }
87
-
88
- process.stdout.write(parts.join(' \u2502 '));
89
-
90
- } catch (e) {
91
- // Fallback if JSON parse fails
92
- process.stdout.write('\uD83D\uDC32 Hydra');
93
- }
94
- });
1
+ #!/usr/bin/env node
2
+
3
+ // Hydra StatusLine — persistent status bar at bottom of Claude Code
4
+ // Receives session JSON via stdin, outputs one formatted line to stdout.
5
+ //
6
+ // Display format:
7
+ // 🐉 │ Opus │ Ctx: 37% ████░░░░░░ │ $0.42 │ my-project │ ⚡ Update available
8
+ //
9
+ // Context bar is color-coded:
10
+ // Green (0-49%) → Yellow (50-79%) → Red (80%+)
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ const cacheFile = path.join(os.homedir(), '.claude', 'cache', 'hydra-update-check.json');
17
+
18
+ let input = '';
19
+ process.stdin.on('data', (chunk) => (input += chunk));
20
+ process.stdin.on('end', () => {
21
+ try {
22
+ const data = JSON.parse(input);
23
+
24
+ // === Model ===
25
+ const model = data.model?.display_name || 'Unknown';
26
+
27
+ // === Context Usage ===
28
+ // Use precomputed used_percentage from Claude Code (most reliable)
29
+ const ctxPct = Math.round(data.context_window?.used_percentage || 0);
30
+
31
+ // Build visual context bar (10 chars wide)
32
+ const filled = Math.round(ctxPct / 10);
33
+ const empty = 10 - filled;
34
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
35
+
36
+ // Color-code: Green <50%, Yellow 50-79%, Red 80%+
37
+ let ctxColor;
38
+ if (ctxPct < 50) {
39
+ ctxColor = '\x1b[32m'; // Green
40
+ } else if (ctxPct < 80) {
41
+ ctxColor = '\x1b[33m'; // Yellow
42
+ } else {
43
+ ctxColor = '\x1b[31m'; // Red
44
+ }
45
+ const reset = '\x1b[0m';
46
+ const dim = '\x1b[2m';
47
+
48
+ const ctxDisplay = `${ctxColor}Ctx: ${ctxPct}% ${bar}${reset}`;
49
+
50
+ // === Session Cost ===
51
+ const cost = (data.cost?.total_cost_usd || 0).toFixed(2);
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
+
63
+ // === Working Directory ===
64
+ const dirName = path.basename(data.workspace?.current_dir || data.cwd || '');
65
+
66
+ // === Update Check (read from cache) ===
67
+ let updateNotice = '';
68
+ try {
69
+ const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
70
+ if (cache.update_available) {
71
+ updateNotice = ` \x1b[33m\u26A1 v${cache.latest} available${reset}`;
72
+ }
73
+ } catch (e) {
74
+ // No cache — skip update notice
75
+ }
76
+
77
+ // === Compose Status Line ===
78
+ const parts = [
79
+ '\x1b[32m\uD83D\uDC32\x1b[0m', // Green dragon emoji (🐉)
80
+ `${dim}${model}${reset}`, // Dim model name
81
+ ctxDisplay, // Color-coded context bar
82
+ `${dim}$${cost}${reset}${savingsStr}`, // Dim cost + green ↓savings
83
+ `${dim}${dirName}${reset}`, // Dim directory
84
+ ];
85
+
86
+ // Append update notice if available
87
+ if (updateNotice) {
88
+ parts.push(updateNotice);
89
+ }
90
+
91
+ // Compaction warning only show at 70%+ context usage
92
+ if (ctxPct >= 80) {
93
+ parts.push(`\x1b[31m\u26A0 Compacting soon!\x1b[0m`);
94
+ } else if (ctxPct >= 70) {
95
+ parts.push(`\x1b[31m\u26A0 Auto-compact at 85%\x1b[0m`);
96
+ }
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
+
122
+ process.stdout.write(parts.join(' \u2502 '));
123
+
124
+ } catch (e) {
125
+ // Fallback if JSON parse fails
126
+ process.stdout.write('\uD83D\uDC32 Hydra');
127
+ }
128
+ });