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.
- package/README.md +99 -99
- package/bin/cli.js +105 -105
- package/files/SKILL.md +1217 -1174
- package/files/agents/hydra-analyst.md +159 -145
- package/files/agents/hydra-coder.md +137 -123
- package/files/agents/hydra-git.md +148 -130
- package/files/agents/hydra-guard.md +153 -135
- package/files/agents/hydra-preflight.md +22 -0
- package/files/agents/hydra-runner.md +107 -93
- package/files/agents/hydra-scout.md +241 -227
- package/files/agents/hydra-scribe.md +98 -84
- package/files/agents/hydra-sentinel-scan.md +242 -236
- package/files/agents/hydra-sentinel.md +210 -192
- package/files/commands/hydra/config.md +37 -37
- package/files/commands/hydra/guard.md +71 -71
- package/files/commands/hydra/help.md +47 -46
- package/files/commands/hydra/quiet.md +16 -16
- package/files/commands/hydra/stats.md +62 -121
- package/files/commands/hydra/status.md +85 -85
- package/files/commands/hydra/stfu.md +21 -0
- package/files/commands/hydra/verbose.md +29 -29
- package/files/hooks/hydra-auto-guard.js +54 -54
- package/files/hooks/hydra-check-update.js +99 -99
- package/files/hooks/hydra-statusline.js +128 -94
- package/files/hooks/hydra-token-math.js +163 -0
- package/files/references/model-capabilities.md +164 -164
- package/files/references/routing-guide.md +303 -303
- package/files/skills/stfu-agents/SKILL.md +59 -0
- package/package.json +1 -1
- package/src/files.js +106 -105
- package/src/installer.js +393 -393
- package/src/prompts.js +80 -80
|
@@ -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
|
-
// ===
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
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
|
+
});
|