hail-hydra-cc 2.4.0 → 2.4.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,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,128 +1,131 @@
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
- });
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 Indicator: 3 states (pending / clean / quiet) ===
99
+ try {
100
+ const sessionId = data.session_id || 'unknown';
101
+ const sentinelDir = path.join(os.tmpdir(), 'hydra-sentinel');
102
+ const pendingFile = path.join(sentinelDir, `${sessionId}-pending.json`);
103
+ const scanMarker = path.join(sentinelDir, `${sessionId}-last-scan`);
104
+
105
+ const pendingExists = fs.existsSync(pendingFile);
106
+ const markerExists = fs.existsSync(scanMarker);
107
+
108
+ if (pendingExists) {
109
+ const pendingData = JSON.parse(fs.readFileSync(pendingFile, 'utf8'));
110
+ const count = pendingData.files?.length || 0;
111
+ const age = Date.now() - (pendingData.updated_at || 0);
112
+ if (count > 0 && age < 600000) {
113
+ parts.push(`\x1b[33m\u26A0 Sentinel pending (${count} file${count === 1 ? '' : 's'})\x1b[0m`);
114
+ }
115
+ } else if (markerExists) {
116
+ const markerMs = parseInt(fs.readFileSync(scanMarker, 'utf8').trim(), 10) * 1000;
117
+ if (Date.now() - markerMs < 60000) {
118
+ parts.push(`\x1b[32m\u2705 Sentinel clean\x1b[0m`);
119
+ }
120
+ }
121
+ } catch (e) {
122
+ // No flag — silent quiet state
123
+ }
124
+
125
+ process.stdout.write(parts.join(' \u2502 '));
126
+
127
+ } catch (e) {
128
+ // Fallback if JSON parse fails
129
+ process.stdout.write('\uD83D\uDC32 Hydra');
130
+ }
131
+ });