hail-hydra-cc 1.2.0 → 1.2.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.
- package/README.md +16 -3
- package/bin/cli.js +2 -2
- package/files/SKILL.md +8 -11
- package/files/commands/hydra/config.md +37 -0
- package/files/commands/hydra/guard.md +71 -0
- package/files/commands/hydra/help.md +41 -0
- package/files/commands/hydra/quiet.md +14 -0
- package/files/commands/hydra/status.md +72 -0
- package/files/commands/hydra/update.md +35 -0
- package/files/commands/hydra/verbose.md +27 -0
- package/files/hooks/hydra-auto-guard.js +54 -0
- package/files/hooks/hydra-check-update.js +99 -0
- package/files/hooks/hydra-statusline.js +87 -0
- package/package.json +1 -1
- package/src/display.js +66 -14
- package/src/files.js +17 -1
- package/src/installer.js +144 -10
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ npx hail-hydra-cc --help # Show help
|
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
~/.claude/ (or ./.claude/ for local)
|
|
47
|
-
├── agents/
|
|
47
|
+
├── agents/ # 7 agent definitions
|
|
48
48
|
│ ├── hydra-scout.md
|
|
49
49
|
│ ├── hydra-runner.md
|
|
50
50
|
│ ├── hydra-scribe.md
|
|
@@ -52,8 +52,21 @@ npx hail-hydra-cc --help # Show help
|
|
|
52
52
|
│ ├── hydra-git.md
|
|
53
53
|
│ ├── hydra-coder.md
|
|
54
54
|
│ └── hydra-analyst.md
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
├── commands/hydra/ # 7 slash commands
|
|
56
|
+
│ ├── help.md # /hydra:help
|
|
57
|
+
│ ├── status.md # /hydra:status
|
|
58
|
+
│ ├── update.md # /hydra:update
|
|
59
|
+
│ ├── config.md # /hydra:config
|
|
60
|
+
│ ├── guard.md # /hydra:guard
|
|
61
|
+
│ ├── quiet.md # /hydra:quiet
|
|
62
|
+
│ └── verbose.md # /hydra:verbose
|
|
63
|
+
├── hooks/ # 3 lifecycle hooks
|
|
64
|
+
│ ├── hydra-check-update.js # SessionStart — version check
|
|
65
|
+
│ ├── hydra-statusline.js # StatusLine — status bar
|
|
66
|
+
│ └── hydra-auto-guard.js # PostToolUse — file tracker
|
|
67
|
+
└── skills/hydra/ # Skill (Claude Code discoverable via /skills)
|
|
68
|
+
├── SKILL.md # Orchestrator instructions
|
|
69
|
+
├── VERSION # Installed version
|
|
57
70
|
└── references/
|
|
58
71
|
├── routing-guide.md
|
|
59
72
|
└── model-capabilities.md
|
package/bin/cli.js
CHANGED
|
@@ -39,8 +39,8 @@ Examples:
|
|
|
39
39
|
|
|
40
40
|
What gets installed:
|
|
41
41
|
~/.claude/agents/ 7 Hydra agent .md files
|
|
42
|
-
~/.claude/hydra/SKILL.md Core framework instructions
|
|
43
|
-
~/.claude/hydra/references/ Model capabilities & routing guides
|
|
42
|
+
~/.claude/skills/hydra/SKILL.md Core framework instructions
|
|
43
|
+
~/.claude/skills/hydra/references/ Model capabilities & routing guides
|
|
44
44
|
`);
|
|
45
45
|
|
|
46
46
|
// ── Main action ───────────────────────────────────────────────────────────────
|
package/files/SKILL.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hydra
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
as "always on" infrastructure, like an OS scheduler. If you are running as Opus and about
|
|
11
|
-
to do work that a faster model could handle, delegate it. "Cut off one head, two more
|
|
12
|
-
shall take its place" — except here, every head is working FOR you.
|
|
4
|
+
Multi-agent orchestration framework for Claude Code. Automatically delegates
|
|
5
|
+
tasks to cheaper, faster sub-agents (Haiku 4.5, Sonnet 4.6) while maintaining
|
|
6
|
+
Opus-level quality through verification. Use when working on any coding task —
|
|
7
|
+
Hydra activates automatically to route file exploration, test running,
|
|
8
|
+
documentation, code writing, debugging, security scanning, and git operations
|
|
9
|
+
to the optimal agent. Saves ~50% on API costs.
|
|
13
10
|
---
|
|
14
11
|
|
|
15
12
|
# 🐉 Hydra — Multi-Headed Speculative Execution
|
|
@@ -718,8 +715,8 @@ Hydra's heads live in `agents/`. Install them where Claude Code discovers subage
|
|
|
718
715
|
## Configuration
|
|
719
716
|
|
|
720
717
|
At session start, check for a Hydra configuration file at:
|
|
721
|
-
1. `.claude/hydra/hydra.config.md` (project-level, takes precedence)
|
|
722
|
-
2. `~/.claude/hydra/hydra.config.md` (user-level, fallback)
|
|
718
|
+
1. `.claude/skills/hydra/config/hydra.config.md` (project-level, takes precedence)
|
|
719
|
+
2. `~/.claude/skills/hydra/config/hydra.config.md` (user-level, fallback)
|
|
723
720
|
|
|
724
721
|
If found, apply the settings. If not found, use defaults:
|
|
725
722
|
- **mode**: balanced
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show current Hydra configuration and how to customize it
|
|
3
|
+
allowed-tools: Read, Bash
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hydra Config
|
|
7
|
+
|
|
8
|
+
Show the current Hydra configuration:
|
|
9
|
+
|
|
10
|
+
1. Check for project-level config first:
|
|
11
|
+
```bash
|
|
12
|
+
cat .claude/skills/hydra/config/hydra.config.md 2>/dev/null
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. If not found, check global config:
|
|
16
|
+
```bash
|
|
17
|
+
cat ~/.claude/skills/hydra/config/hydra.config.md 2>/dev/null
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
3. If neither found, show defaults:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
🐉 Hydra Configuration (defaults — no config file found)
|
|
24
|
+
─────────────────────────────
|
|
25
|
+
Mode: balanced
|
|
26
|
+
Dispatch Log: on
|
|
27
|
+
Auto-Guard: on
|
|
28
|
+
Model Overrides: none
|
|
29
|
+
─────────────────────────────
|
|
30
|
+
To customize, create a config file:
|
|
31
|
+
Global: ~/.claude/skills/hydra/config/hydra.config.md
|
|
32
|
+
Project: .claude/skills/hydra/config/hydra.config.md
|
|
33
|
+
|
|
34
|
+
Run /hydra:status to see current agent assignments.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
4. If a config file IS found, display its contents and note where it was loaded from.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Manually run the Hydra security and quality scan on specified files or directories
|
|
3
|
+
allowed-tools: Read, Grep, Glob, Bash
|
|
4
|
+
model: haiku
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Hydra Guard — Manual Security Scan
|
|
8
|
+
|
|
9
|
+
Run a focused security and quality scan on the specified files.
|
|
10
|
+
|
|
11
|
+
**Target**: $ARGUMENTS
|
|
12
|
+
|
|
13
|
+
If no arguments provided, scan all files changed since the last commit:
|
|
14
|
+
```bash
|
|
15
|
+
git diff --name-only HEAD 2>/dev/null || echo "Not a git repository or no changes"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Scan Checklist
|
|
19
|
+
|
|
20
|
+
For each target file, check for:
|
|
21
|
+
|
|
22
|
+
### CRITICAL (security)
|
|
23
|
+
- Hardcoded secrets, API keys, tokens, passwords (patterns: `sk-`, `ghp_`, `AKIA`, `password =`, `secret =`, `token =`, `api_key =`)
|
|
24
|
+
- SQL injection vulnerabilities (string concatenation in queries)
|
|
25
|
+
- XSS vulnerabilities (unescaped user input in HTML output)
|
|
26
|
+
- Unsafe deserialization (`eval()`, `pickle.loads()`, `unserialize()`)
|
|
27
|
+
- Path traversal (`../` in file operations without validation)
|
|
28
|
+
- Command injection (user input passed to shell commands)
|
|
29
|
+
|
|
30
|
+
### WARNING (quality)
|
|
31
|
+
- `console.log` / `print()` debug leftovers
|
|
32
|
+
- TODO/FIXME/HACK comments
|
|
33
|
+
- Unused imports (obvious cases only)
|
|
34
|
+
- Missing error handling on async operations (no try/catch, no .catch())
|
|
35
|
+
- Empty catch blocks
|
|
36
|
+
- Hardcoded URLs or magic numbers without constants
|
|
37
|
+
|
|
38
|
+
### INFO (style)
|
|
39
|
+
- Functions longer than 100 lines
|
|
40
|
+
- Deeply nested conditionals (3+ levels)
|
|
41
|
+
- Inconsistent naming conventions within the same file
|
|
42
|
+
|
|
43
|
+
## Output Format
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
🐉 Hydra Guard — Security & Quality Report
|
|
47
|
+
═══════════════════════════════════════════
|
|
48
|
+
Files scanned: 3
|
|
49
|
+
|
|
50
|
+
🔴 CRITICAL (2 findings)
|
|
51
|
+
src/auth.py:45 Hardcoded API key: OPENAI_KEY = "sk-..."
|
|
52
|
+
src/db.py:78 SQL injection: f-string in query construction
|
|
53
|
+
|
|
54
|
+
⚠️ WARNING (3 findings)
|
|
55
|
+
src/auth.py:12 console.log left in production code
|
|
56
|
+
src/utils.py:89 Empty catch block — errors silently swallowed
|
|
57
|
+
src/api.py:34 TODO: "fix this later" (line 34)
|
|
58
|
+
|
|
59
|
+
ℹ️ INFO (1 finding)
|
|
60
|
+
src/handler.py:1 Function process_request is 142 lines long
|
|
61
|
+
|
|
62
|
+
Summary: 2 critical · 3 warnings · 1 info
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If no issues found:
|
|
66
|
+
```
|
|
67
|
+
🐉 Hydra Guard — All Clear ✅
|
|
68
|
+
Files scanned: 3 | No issues found.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Important**: This is a FAST scan, not a deep audit. For thorough security review, use hydra-analyst instead.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show all available Hydra commands, agents, and a quick reference guide
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Hydra Help
|
|
6
|
+
|
|
7
|
+
Display the following help reference directly — do NOT search files or run commands:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
🐉 Hydra Framework — Quick Reference
|
|
11
|
+
═══════════════════════════════════════
|
|
12
|
+
COMMANDS
|
|
13
|
+
/hydra:help Show this help screen
|
|
14
|
+
/hydra:status Show installed agents, version, config
|
|
15
|
+
/hydra:update Update Hydra to the latest version
|
|
16
|
+
/hydra:config Show current configuration
|
|
17
|
+
/hydra:guard Run security scan on files (usage: /hydra:guard src/auth.py)
|
|
18
|
+
/hydra:quiet Suppress dispatch logs for this session
|
|
19
|
+
/hydra:verbose Enable verbose dispatch logs with timing
|
|
20
|
+
|
|
21
|
+
AGENTS
|
|
22
|
+
🟢 hydra-scout (Haiku 4.5) — Explore codebase, find files, map structure
|
|
23
|
+
🟢 hydra-runner (Haiku 4.5) — Run tests, linters, build commands
|
|
24
|
+
🟢 hydra-scribe (Haiku 4.5) — Write docs, comments, READMEs
|
|
25
|
+
🟢 hydra-guard (Haiku 4.5) — Security scan, quality gate
|
|
26
|
+
🟢 hydra-git (Haiku 4.5) — Git operations, commits, branches
|
|
27
|
+
🔵 hydra-coder (Sonnet 4.6) — Write and edit code
|
|
28
|
+
🔵 hydra-analyst (Sonnet 4.6) — Debug, diagnose, review
|
|
29
|
+
|
|
30
|
+
HOW IT WORKS
|
|
31
|
+
The Opus 4.6 orchestrator automatically delegates tasks to cheaper,
|
|
32
|
+
faster agents (Haiku 4.5 and Sonnet 4.6) — saving ~50% on API costs
|
|
33
|
+
while maintaining Opus-level quality through verification.
|
|
34
|
+
|
|
35
|
+
You don't need to do anything. Just work normally.
|
|
36
|
+
Hydra operates invisibly unless you check the dispatch log.
|
|
37
|
+
|
|
38
|
+
LINKS
|
|
39
|
+
GitHub: https://github.com/AR6420/Hail_Hydra
|
|
40
|
+
npm: https://www.npmjs.com/package/hail-hydra-cc
|
|
41
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Suppress Hydra dispatch logs for the rest of this session
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Hydra Quiet Mode
|
|
6
|
+
|
|
7
|
+
Acknowledge this command and remember for the rest of this session:
|
|
8
|
+
|
|
9
|
+
**Do NOT display the Hydra Dispatch Log footer after tasks.**
|
|
10
|
+
|
|
11
|
+
Respond with:
|
|
12
|
+
"🐉 Quiet mode enabled. Dispatch logs suppressed for this session. Use /hydra:verbose to re-enable."
|
|
13
|
+
|
|
14
|
+
Continue operating Hydra normally (delegation, verification, auto-guard) — just don't show the dispatch log table.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show Hydra framework status — installed agents, version, config, and update availability
|
|
3
|
+
allowed-tools: Bash, Read, Glob
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hydra Status
|
|
7
|
+
|
|
8
|
+
Show a comprehensive status report for the Hydra framework.
|
|
9
|
+
|
|
10
|
+
## 1. Version Info
|
|
11
|
+
```bash
|
|
12
|
+
INSTALLED=$(cat ~/.claude/skills/hydra/VERSION 2>/dev/null || echo "unknown")
|
|
13
|
+
echo "Installed: $INSTALLED"
|
|
14
|
+
LATEST=$(npm view hail-hydra-cc version 2>/dev/null || echo "unknown")
|
|
15
|
+
echo "Latest: $LATEST"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 2. Installed Agents
|
|
19
|
+
```bash
|
|
20
|
+
echo "=== Global Agents ==="
|
|
21
|
+
ls -1 ~/.claude/agents/hydra-*.md 2>/dev/null || echo "None found"
|
|
22
|
+
echo "=== Local Agents ==="
|
|
23
|
+
ls -1 .claude/agents/hydra-*.md 2>/dev/null || echo "None found"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 3. Installed Commands
|
|
27
|
+
```bash
|
|
28
|
+
echo "=== Global Commands ==="
|
|
29
|
+
ls -1 ~/.claude/commands/hydra/*.md 2>/dev/null || echo "None found"
|
|
30
|
+
echo "=== Local Commands ==="
|
|
31
|
+
ls -1 .claude/commands/hydra/*.md 2>/dev/null || echo "None found"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 4. Hooks
|
|
35
|
+
```bash
|
|
36
|
+
ls -1 ~/.claude/hooks/hydra-*.js 2>/dev/null || echo "None found"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 5. Configuration
|
|
40
|
+
```bash
|
|
41
|
+
cat ~/.claude/skills/hydra/config/hydra.config.md 2>/dev/null || \
|
|
42
|
+
cat .claude/skills/hydra/config/hydra.config.md 2>/dev/null || \
|
|
43
|
+
echo "No config file found (using defaults)"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Display Format
|
|
47
|
+
|
|
48
|
+
Present results as a clean status card:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
🐉 Hydra Framework Status
|
|
52
|
+
──────────────────────────────
|
|
53
|
+
Version: 1.0.0 (latest: 1.0.0 ✅) OR (update available: 1.1.0 ⚡)
|
|
54
|
+
Install: Global (~/.claude/)
|
|
55
|
+
Agents (7):
|
|
56
|
+
🟢 hydra-scout (Haiku 4.5) ✅
|
|
57
|
+
🟢 hydra-runner (Haiku 4.5) ✅
|
|
58
|
+
🟢 hydra-scribe (Haiku 4.5) ✅
|
|
59
|
+
🟢 hydra-guard (Haiku 4.5) ✅
|
|
60
|
+
🟢 hydra-git (Haiku 4.5) ✅
|
|
61
|
+
🔵 hydra-coder (Sonnet 4.6) ✅
|
|
62
|
+
🔵 hydra-analyst (Sonnet 4.6) ✅
|
|
63
|
+
Commands (7): update, status, help, config, guard, quiet, verbose
|
|
64
|
+
Hooks (3): check-update ✅, statusline ✅, auto-guard ✅
|
|
65
|
+
Config: balanced mode, dispatch log on, auto-guard on
|
|
66
|
+
──────────────────────────────
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If an update is available, add:
|
|
70
|
+
```
|
|
71
|
+
⚡ Update available! Run /hydra:update to get the latest version.
|
|
72
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Update the Hydra framework to the latest version from npm
|
|
3
|
+
allowed-tools: Bash
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hydra Update
|
|
7
|
+
|
|
8
|
+
Run the following steps to update Hydra to the latest version:
|
|
9
|
+
|
|
10
|
+
1. First, check the current installed version:
|
|
11
|
+
```bash
|
|
12
|
+
cat ~/.claude/skills/hydra/VERSION 2>/dev/null || echo "VERSION file not found"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. Check the latest available version on npm:
|
|
16
|
+
```bash
|
|
17
|
+
npm view hail-hydra-cc version 2>/dev/null || echo "Package not found on npm"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
3. If an update is available (versions differ), run the installer:
|
|
21
|
+
```bash
|
|
22
|
+
npx hail-hydra-cc@latest --global
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
4. After installation completes, verify the new version:
|
|
26
|
+
```bash
|
|
27
|
+
cat ~/.claude/skills/hydra/VERSION
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
5. Report to the user:
|
|
31
|
+
- If updated: "🐉 Hydra updated from [old] → [new]. All heads refreshed."
|
|
32
|
+
- If already current: "🐉 Hydra is already at the latest version ([version])."
|
|
33
|
+
- If error: Show the error and suggest running `npx hail-hydra-cc@latest --global` manually in their terminal.
|
|
34
|
+
|
|
35
|
+
**Important**: After updating, the user should restart Claude Code to reload the updated agent files, commands, and hooks.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Enable verbose Hydra dispatch logs with timing and token estimates
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Hydra Verbose Mode
|
|
6
|
+
|
|
7
|
+
Acknowledge this command and remember for the rest of this session:
|
|
8
|
+
|
|
9
|
+
**Display DETAILED Hydra Dispatch Logs after every task that involves delegation.**
|
|
10
|
+
|
|
11
|
+
The verbose log includes extra columns for timing:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
🐉 Hydra Dispatch Log (verbose)
|
|
15
|
+
| Step | Agent | Task | Time | Verdict |
|
|
16
|
+
|------|---------------------|-----------------------|-------|-------------|
|
|
17
|
+
| 1 | hydra-scout (Haiku 4.5) | Explored auth module | 3.2s | ✅ Accepted |
|
|
18
|
+
| 2 | hydra-coder (Sonnet 4.6) | Fixed null check | 8.1s | ✅ Accepted |
|
|
19
|
+
| 3 | hydra-guard (Haiku 4.5) | Security scan | 1.4s | ✅ Passed |
|
|
20
|
+
| 4 | hydra-runner (Haiku 4.5) | Ran test suite | 4.7s | ✅ Accepted |
|
|
21
|
+
|
|
22
|
+
Delegation: 4/4 (100%) | Accepted: 4 | Adjusted: 0 | Rejected: 0
|
|
23
|
+
Total delegation time: 17.4s | Waves: 2
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Respond with:
|
|
27
|
+
"🐉 Verbose mode enabled. Dispatch logs will include timing details. Use /hydra:quiet to suppress."
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
process.stdout.write(parts.join(' \u2502 '));
|
|
82
|
+
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Fallback if JSON parse fails
|
|
85
|
+
process.stdout.write('\uD83D\uDC32 Hydra');
|
|
86
|
+
}
|
|
87
|
+
});
|
package/package.json
CHANGED
package/src/display.js
CHANGED
|
@@ -37,17 +37,36 @@ function showFileInstalled(displayName, success, errorMsg) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function showInstallComplete() {
|
|
40
|
+
function showInstallComplete(statusLineConfigured = true) {
|
|
41
41
|
console.log();
|
|
42
|
-
console.log(chalk.cyan.bold('
|
|
42
|
+
console.log(chalk.cyan.bold(' \uD83D\uDC09 Hail Hydra! Framework deployed and ready.'));
|
|
43
|
+
console.log(chalk.gray(' ' + '\u2500'.repeat(45)));
|
|
44
|
+
console.log(chalk.green(` \u2714 7 agents installed`));
|
|
45
|
+
console.log(chalk.green(` \u2714 7 slash commands installed`));
|
|
46
|
+
console.log(chalk.green(` \u2714 3 hooks registered`));
|
|
47
|
+
if (statusLineConfigured) {
|
|
48
|
+
console.log(chalk.green(` \u2714 StatusLine configured`));
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.yellow(` \u26a0 StatusLine skipped (existing config preserved)`));
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.green(` \u2714 Version tracked (${VERSION})`));
|
|
43
53
|
console.log();
|
|
44
|
-
console.log(chalk.gray('
|
|
54
|
+
console.log(chalk.gray(' Quick start: /hydra:help'));
|
|
55
|
+
console.log(chalk.gray(' Check status: /hydra:status'));
|
|
45
56
|
console.log(chalk.gray(' GitHub: https://github.com/AR6420/Hail_Hydra'));
|
|
46
57
|
console.log();
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
function showStatusTable(globalStatus, localStatus) {
|
|
50
|
-
const
|
|
61
|
+
const os = require('os');
|
|
62
|
+
const path = require('path');
|
|
63
|
+
const fs = require('fs');
|
|
64
|
+
|
|
65
|
+
function fileExists(p) {
|
|
66
|
+
try { fs.accessSync(p, fs.constants.F_OK); return true; } catch { return false; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const divider = chalk.gray(' ' + '\u2500'.repeat(50));
|
|
51
70
|
|
|
52
71
|
console.log();
|
|
53
72
|
console.log(chalk.bold(' Installation Status'));
|
|
@@ -65,7 +84,9 @@ function showStatusTable(globalStatus, localStatus) {
|
|
|
65
84
|
const anyInstalled =
|
|
66
85
|
status.agents.some((a) => a.installed) ||
|
|
67
86
|
status.skill ||
|
|
68
|
-
status.references.some((r) => r.installed)
|
|
87
|
+
status.references.some((r) => r.installed) ||
|
|
88
|
+
(status.commands && status.commands.some((c) => c.installed)) ||
|
|
89
|
+
status.version;
|
|
69
90
|
|
|
70
91
|
if (!anyInstalled) {
|
|
71
92
|
console.log(chalk.gray(' (not installed)'));
|
|
@@ -73,29 +94,60 @@ function showStatusTable(globalStatus, localStatus) {
|
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
for (const agent of status.agents) {
|
|
76
|
-
const dot = agent.model === 'Haiku' ? chalk.green('
|
|
77
|
-
const name = chalk.bold(agent.display.split('
|
|
78
|
-
const role = chalk.gray('
|
|
97
|
+
const dot = agent.model === 'Haiku' ? chalk.green('\uD83D\uDFE2') : chalk.blue('\uD83D\uDD35');
|
|
98
|
+
const name = chalk.bold(agent.display.split('\u2014')[0].trim());
|
|
99
|
+
const role = chalk.gray('\u2014 ' + agent.display.split('\u2014')[1].trim());
|
|
79
100
|
if (agent.installed) {
|
|
80
|
-
console.log(` ${dot} ${chalk.green('
|
|
101
|
+
console.log(` ${dot} ${chalk.green('\u2714')} ${name} ${role}`);
|
|
81
102
|
} else {
|
|
82
|
-
console.log(` ${dot} ${chalk.gray('
|
|
103
|
+
console.log(` ${dot} ${chalk.gray('\u2716')} ${chalk.gray(name)} ${chalk.gray(role + ' (not installed)')}`);
|
|
83
104
|
}
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
if (status.skill) {
|
|
87
|
-
console.log(chalk.green('
|
|
108
|
+
console.log(chalk.green(' \u2714 SKILL.md'));
|
|
88
109
|
} else {
|
|
89
|
-
console.log(chalk.gray('
|
|
110
|
+
console.log(chalk.gray(' \u2716 SKILL.md (not installed)'));
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
for (const ref of status.references) {
|
|
93
114
|
if (ref.installed) {
|
|
94
|
-
console.log(chalk.green(`
|
|
115
|
+
console.log(chalk.green(` \u2714 ${ref.name}`));
|
|
95
116
|
} else {
|
|
96
|
-
console.log(chalk.gray(`
|
|
117
|
+
console.log(chalk.gray(` \u2716 ${ref.name} (not installed)`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Commands
|
|
122
|
+
if (status.commands) {
|
|
123
|
+
for (const cmd of status.commands) {
|
|
124
|
+
if (cmd.installed) {
|
|
125
|
+
console.log(chalk.green(` \u2714 ${cmd.name}`));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(chalk.gray(` \u2716 ${cmd.name} (not installed)`));
|
|
128
|
+
}
|
|
97
129
|
}
|
|
98
130
|
}
|
|
131
|
+
|
|
132
|
+
// Version
|
|
133
|
+
if (status.version) {
|
|
134
|
+
console.log(chalk.green(` \u2714 hydra/VERSION (${status.version})`));
|
|
135
|
+
} else {
|
|
136
|
+
console.log(chalk.gray(' \u2716 hydra/VERSION (not installed)'));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Global hooks (always ~/.claude/hooks/)
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.bold(' Global Hooks (~/.claude/hooks/)'));
|
|
143
|
+
const hookKeys = ['hydra-check-update', 'hydra-statusline', 'hydra-auto-guard'];
|
|
144
|
+
for (const key of hookKeys) {
|
|
145
|
+
const dest = path.join(os.homedir(), '.claude', 'hooks', `${key}.js`);
|
|
146
|
+
if (fileExists(dest)) {
|
|
147
|
+
console.log(chalk.green(` \u2714 ${key}.js`));
|
|
148
|
+
} else {
|
|
149
|
+
console.log(chalk.gray(` \u2716 ${key}.js (not installed)`));
|
|
150
|
+
}
|
|
99
151
|
}
|
|
100
152
|
|
|
101
153
|
console.log();
|
package/src/files.js
CHANGED
|
@@ -62,4 +62,20 @@ const references = {
|
|
|
62
62
|
'model-capabilities': readBundled('references/model-capabilities.md'),
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
const commands = {
|
|
66
|
+
'update': readBundled('commands/hydra/update.md'),
|
|
67
|
+
'status': readBundled('commands/hydra/status.md'),
|
|
68
|
+
'help': readBundled('commands/hydra/help.md'),
|
|
69
|
+
'config': readBundled('commands/hydra/config.md'),
|
|
70
|
+
'guard': readBundled('commands/hydra/guard.md'),
|
|
71
|
+
'quiet': readBundled('commands/hydra/quiet.md'),
|
|
72
|
+
'verbose': readBundled('commands/hydra/verbose.md'),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hooks = {
|
|
76
|
+
'hydra-check-update': readBundled('hooks/hydra-check-update.js'),
|
|
77
|
+
'hydra-statusline': readBundled('hooks/hydra-statusline.js'),
|
|
78
|
+
'hydra-auto-guard': readBundled('hooks/hydra-auto-guard.js'),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
module.exports = { agents, skill, references, commands, hooks };
|
package/src/installer.js
CHANGED
|
@@ -5,8 +5,8 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
|
|
8
|
-
const { agents, skill, references } = require('./files');
|
|
9
|
-
const { showInstallHeader, showFileInstalled, showInstallComplete, showStatusTable } = require('./display');
|
|
8
|
+
const { agents, skill, references, commands, hooks } = require('./files');
|
|
9
|
+
const { showInstallHeader, showFileInstalled, showInstallComplete, showStatusTable, VERSION } = require('./display');
|
|
10
10
|
|
|
11
11
|
// ── Install locations ────────────────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -28,9 +28,9 @@ function buildManifest(base) {
|
|
|
28
28
|
const skillEntry = {
|
|
29
29
|
type: 'skill',
|
|
30
30
|
key: 'SKILL.md',
|
|
31
|
-
display: 'SKILL.md',
|
|
31
|
+
display: 'skills/hydra/SKILL.md',
|
|
32
32
|
content: skill,
|
|
33
|
-
dest: path.join(base, 'hydra', 'SKILL.md'),
|
|
33
|
+
dest: path.join(base, 'skills', 'hydra', 'SKILL.md'),
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const refEntries = Object.entries(references).map(([key, content]) => ({
|
|
@@ -38,10 +38,26 @@ function buildManifest(base) {
|
|
|
38
38
|
key,
|
|
39
39
|
display: `references/${key}.md`,
|
|
40
40
|
content,
|
|
41
|
-
dest: path.join(base, 'hydra', 'references', `${key}.md`),
|
|
41
|
+
dest: path.join(base, 'skills', 'hydra', 'references', `${key}.md`),
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const commandEntries = Object.entries(commands).map(([key, content]) => ({
|
|
45
|
+
type: 'command',
|
|
46
|
+
key,
|
|
47
|
+
display: `commands/hydra/${key}.md`,
|
|
48
|
+
content,
|
|
49
|
+
dest: path.join(base, 'commands', 'hydra', `${key}.md`),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const versionEntry = {
|
|
53
|
+
type: 'version',
|
|
54
|
+
key: 'VERSION',
|
|
55
|
+
display: 'skills/hydra/VERSION',
|
|
56
|
+
content: VERSION,
|
|
57
|
+
dest: path.join(base, 'skills', 'hydra', 'VERSION'),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return [...agentEntries, skillEntry, ...refEntries, ...commandEntries, versionEntry];
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -68,6 +84,94 @@ function hasAnyInstalled(base) {
|
|
|
68
84
|
return buildManifest(base).some((entry) => fileExists(entry.dest));
|
|
69
85
|
}
|
|
70
86
|
|
|
87
|
+
// ── Hooks & settings ─────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function installHooks() {
|
|
90
|
+
const hooksDir = path.join(GLOBAL_BASE, 'hooks');
|
|
91
|
+
ensureDir(hooksDir);
|
|
92
|
+
|
|
93
|
+
for (const [key, content] of Object.entries(hooks)) {
|
|
94
|
+
const dest = path.join(hooksDir, `${key}.js`);
|
|
95
|
+
try {
|
|
96
|
+
writeFileSafe(dest, content);
|
|
97
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
98
|
+
showFileInstalled(`hooks/${key}.js`, true);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
showFileInstalled(`hooks/${key}.js`, false, err.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function registerHooksInSettings() {
|
|
106
|
+
const settingsFile = path.join(GLOBAL_BASE, 'settings.json');
|
|
107
|
+
|
|
108
|
+
let settings = {};
|
|
109
|
+
try {
|
|
110
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
111
|
+
} catch {}
|
|
112
|
+
|
|
113
|
+
if (!settings.hooks) settings.hooks = {};
|
|
114
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
115
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
116
|
+
|
|
117
|
+
const isHydraHook = (entry) =>
|
|
118
|
+
Array.isArray(entry.hooks) && entry.hooks.some(h => h.command && h.command.includes('hydra-'));
|
|
119
|
+
|
|
120
|
+
// Remove stale Hydra entries (clean reinstall)
|
|
121
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(x => !isHydraHook(x));
|
|
122
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(x => !isHydraHook(x));
|
|
123
|
+
|
|
124
|
+
settings.hooks.SessionStart.push({
|
|
125
|
+
hooks: [{ type: 'command', command: 'node ~/.claude/hooks/hydra-check-update.js' }]
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
settings.hooks.PostToolUse.push({
|
|
129
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
130
|
+
hooks: [{ type: 'command', command: 'node ~/.claude/hooks/hydra-auto-guard.js' }]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let statusLineConfigured = false;
|
|
134
|
+
if (!settings.statusLine || (settings.statusLine.command && settings.statusLine.command.includes('hydra-'))) {
|
|
135
|
+
settings.statusLine = {
|
|
136
|
+
type: 'command',
|
|
137
|
+
command: 'node ~/.claude/hooks/hydra-statusline.js',
|
|
138
|
+
padding: 0,
|
|
139
|
+
};
|
|
140
|
+
statusLineConfigured = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
writeFileSafe(settingsFile, JSON.stringify(settings, null, 2));
|
|
144
|
+
return { statusLineConfigured };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function deregisterHooks() {
|
|
148
|
+
const settingsFile = path.join(GLOBAL_BASE, 'settings.json');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
let settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
152
|
+
|
|
153
|
+
const isHydraHook = (entry) =>
|
|
154
|
+
Array.isArray(entry.hooks) && entry.hooks.some(h => h.command && h.command.includes('hydra-'));
|
|
155
|
+
|
|
156
|
+
if (settings.hooks?.SessionStart) {
|
|
157
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(x => !isHydraHook(x));
|
|
158
|
+
if (!settings.hooks.SessionStart.length) delete settings.hooks.SessionStart;
|
|
159
|
+
}
|
|
160
|
+
if (settings.hooks?.PostToolUse) {
|
|
161
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(x => !isHydraHook(x));
|
|
162
|
+
if (!settings.hooks.PostToolUse.length) delete settings.hooks.PostToolUse;
|
|
163
|
+
}
|
|
164
|
+
if (settings.hooks && !Object.keys(settings.hooks).length) delete settings.hooks;
|
|
165
|
+
|
|
166
|
+
if (settings.statusLine?.command?.includes('hydra-')) delete settings.statusLine;
|
|
167
|
+
|
|
168
|
+
writeFileSafe(settingsFile, JSON.stringify(settings, null, 2));
|
|
169
|
+
console.log(chalk.green(' \u2714 Hooks deregistered from settings.json'));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.log(chalk.yellow(` \u26a0 Could not update settings.json: ${err.message}`));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
71
175
|
// ── Core install ─────────────────────────────────────────────────────────────
|
|
72
176
|
|
|
73
177
|
function installToBase(base, label) {
|
|
@@ -125,10 +229,14 @@ async function runInstall(scope) {
|
|
|
125
229
|
if (results.some((r) => !r.success)) anyFailed = true;
|
|
126
230
|
}
|
|
127
231
|
|
|
232
|
+
// Hooks and settings are always global (once, not per-base)
|
|
233
|
+
installHooks();
|
|
234
|
+
const { statusLineConfigured } = registerHooksInSettings();
|
|
235
|
+
|
|
128
236
|
if (!anyFailed) {
|
|
129
|
-
showInstallComplete();
|
|
237
|
+
showInstallComplete(statusLineConfigured);
|
|
130
238
|
} else {
|
|
131
|
-
console.log(chalk.yellow('
|
|
239
|
+
console.log(chalk.yellow(' \u26a0 Some files failed to install. Check errors above.\n'));
|
|
132
240
|
}
|
|
133
241
|
}
|
|
134
242
|
|
|
@@ -189,11 +297,28 @@ async function runUninstall({ interactive = true } = {}) {
|
|
|
189
297
|
}
|
|
190
298
|
}
|
|
191
299
|
|
|
300
|
+
// Remove hook .js files from ~/.claude/hooks/
|
|
301
|
+
for (const key of Object.keys(hooks)) {
|
|
302
|
+
const dest = path.join(GLOBAL_BASE, 'hooks', `${key}.js`);
|
|
303
|
+
if (fileExists(dest)) {
|
|
304
|
+
try { fs.unlinkSync(dest); console.log(chalk.green(` \u2714 Removed hooks/${key}.js`)); }
|
|
305
|
+
catch (err) { console.log(chalk.red(` \u2716 Failed: hooks/${key}.js \u2014 ${err.message}`)); }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Remove cache file
|
|
310
|
+
const cacheFile = path.join(GLOBAL_BASE, 'cache', 'hydra-update-check.json');
|
|
311
|
+
if (fileExists(cacheFile)) {
|
|
312
|
+
try { fs.unlinkSync(cacheFile); } catch {}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
deregisterHooks();
|
|
316
|
+
|
|
192
317
|
console.log();
|
|
193
318
|
if (failed === 0) {
|
|
194
|
-
console.log(chalk.cyan.bold('
|
|
319
|
+
console.log(chalk.cyan.bold(' \uD83D\uDC09 All heads severed. Hydra sleeps.\n'));
|
|
195
320
|
} else {
|
|
196
|
-
console.log(chalk.yellow(`
|
|
321
|
+
console.log(chalk.yellow(` \u26a0 ${removed} removed, ${failed} failed.\n`));
|
|
197
322
|
}
|
|
198
323
|
}
|
|
199
324
|
|
|
@@ -214,10 +339,19 @@ function getStatus(base) {
|
|
|
214
339
|
installed: fileExists(e.dest),
|
|
215
340
|
}));
|
|
216
341
|
|
|
342
|
+
const commandEntries = manifest.filter((e) => e.type === 'command').map((e) => ({
|
|
343
|
+
name: e.display,
|
|
344
|
+
installed: fileExists(e.dest),
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
const versionEntry = manifest.find((e) => e.type === 'version');
|
|
348
|
+
|
|
217
349
|
return {
|
|
218
350
|
agents: agentEntries,
|
|
219
351
|
skill: skillEntry ? fileExists(skillEntry.dest) : false,
|
|
220
352
|
references: refEntries,
|
|
353
|
+
commands: commandEntries,
|
|
354
|
+
version: versionEntry ? (fileExists(versionEntry.dest) ? fs.readFileSync(versionEntry.dest, 'utf8').trim() : null) : null,
|
|
221
355
|
};
|
|
222
356
|
}
|
|
223
357
|
|