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.
- package/README.md +99 -99
- package/bin/cli.js +105 -105
- package/files/SKILL.md +1172 -1172
- package/files/agents/hydra-analyst.md +1 -1
- package/files/agents/hydra-coder.md +2 -2
- package/files/agents/hydra-git.md +1 -1
- package/files/agents/hydra-guard.md +3 -3
- package/files/agents/hydra-runner.md +1 -1
- package/files/agents/hydra-scout.md +1 -1
- package/files/agents/hydra-scribe.md +1 -1
- package/files/agents/hydra-sentinel-scan.md +19 -1
- package/files/agents/hydra-sentinel.md +19 -1
- package/files/commands/hydra/config.md +37 -37
- package/files/commands/hydra/guard.md +71 -71
- package/files/commands/hydra/help.md +47 -47
- package/files/commands/hydra/quiet.md +16 -16
- package/files/commands/hydra/status.md +85 -85
- package/files/commands/hydra/verbose.md +29 -29
- package/files/hooks/hydra-auto-guard.js +130 -124
- package/files/hooks/hydra-check-update.js +99 -99
- package/files/hooks/hydra-statusline.js +131 -128
- package/files/hooks/hydra-token-math.js +179 -163
- package/files/references/model-capabilities.md +164 -164
- package/files/references/routing-guide.md +303 -303
- package/package.json +58 -58
- package/src/files.js +110 -110
- package/src/installer.js +401 -401
- package/src/prompts.js +80 -80
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
});
|