hydra-nano 0.1.0 → 0.2.0
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 +84 -156
- package/package.json +13 -3
- package/skills/hydra/scripts/hooks/route-tests.mjs +42 -12
- package/skills/hydra/scripts/hooks/session-guide.mjs +21 -5
- package/skills/hydra/scripts/hydra.mjs +74 -16
- package/skills/hydra/scripts/install.mjs +20 -1
- package/skills/hydra/scripts/lib/claude_hooks.mjs +69 -4
- package/skills/hydra/scripts/lib/integrations.mjs +251 -0
- package/skills/hydra/scripts/lib/ledger.mjs +106 -7
- package/skills/hydra/scripts/lib/test_logs.mjs +10 -6
- package/skills/hydra/scripts/mcp.mjs +154 -0
package/README.md
CHANGED
|
@@ -1,194 +1,122 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/rahul05ranjan/hydra/main/assets/hydra-banner.svg" alt="Hydra: small local models that take the noisy work off your coding agent" width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/hydra-nano"><img src="https://img.shields.io/npm/v/hydra-nano?color=7c5cff&label=npm" alt="npm version"></a>
|
|
7
|
+
<a href="https://huggingface.co/rahul05ranjan/hydra-nano-git"><img src="https://img.shields.io/badge/model-Hydra%20Nano%20v2-22d3ee" alt="Hydra Nano v2 on Hugging Face"></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/runs-100%25%20local-5eead4" alt="Runs 100% local">
|
|
9
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A518-3c873a" alt="Node 18 or later">
|
|
10
|
+
<a href="https://github.com/rahul05ranjan/hydra/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
11
|
+
</p>
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
3. **read head** (`hydra-nano:read`): extracts symbol definitions (functions, classes, methods), line ranges, or structural definition outlines across TypeScript, JavaScript, Python, Rust, and Go without dumping whole multi-thousand-line files into context.
|
|
9
|
-
4. **test & logs head** (`hydra-nano:test`): runs test and compiler suites (`npm test`, `pytest`, `cargo test`, `tsc`), strips framework noise and `node_modules` stacks, and returns verbatim failure diffs and source lines with the command's exit code.
|
|
10
|
-
5. **os head** (`hydra-nano:os`): read-only system inspection (ports, processes, disk, system info) with strict secret-blocking guardrails.
|
|
13
|
+
<p align="center">
|
|
14
|
+
<b>Your coding agent reads every line of every test log, git diff and process list it runs.<br>Hydra reads them on your machine and hands back only the lines that matter.</b>
|
|
15
|
+
</p>
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
---
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
agent ── hydra ask --head git "<question>" ──────▶ git head model picks probes from a menu, code runs them
|
|
16
|
-
agent ── hydra ask --head files "<question>" ────▶ files head model picks search terms, code searches
|
|
17
|
-
agent ── hydra read <file> [symbol|--lines] ─────▶ read head pinpoints symbol/lines, extracts outline
|
|
18
|
-
agent ── hydra run -- <test/build-command> ──────▶ test head runs locally, digests failures & diffs
|
|
19
|
-
agent ◀── verbatim evidence lines ──────────────── model picks lines; the agent interprets them
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
- **Read-only and safe**: The model never writes an arbitrary command. It picks from fixed menus or digests command output.
|
|
23
|
-
- **Verbatim evidence**: Results are the command's own text lines, never a paraphrase.
|
|
24
|
-
- **Complete lists**: Git status, history, branches, remotes, stashes, tags and worktrees retain every returned record. Explicit commit counts override the model's count (maximum 50).
|
|
25
|
-
- **Pass-through**: Output under 800 characters is handed over whole with zero model calls.
|
|
26
|
-
- **Preserved exit codes**: `hydra run -- <cmd>` preserves `<cmd>`'s exit code, so agents know immediately if a build or test failed.
|
|
27
|
-
|
|
28
|
-
On Windows, OS inspection uses fixed read-only netstat, tasklist and PowerShell CIM commands, plus Node system/memory APIs. Process output is capped at 30 records and is not sorted by CPU usage. Failed probes are marked in the returned evidence. Code search ranks current source ahead of archived evaluation results while keeping those results searchable.
|
|
29
|
-
|
|
30
|
-
## Try it
|
|
31
|
-
|
|
32
|
-
Needs Node 18 or later and [llama.cpp](https://github.com/ggml-org/llama.cpp) (the default backend). [Ollama](https://ollama.com/download) is optional; select it with `HYDRA_BACKEND=ollama`.
|
|
33
|
-
|
|
34
|
-
### Run without Ollama
|
|
35
|
-
|
|
36
|
-
**Hydra Nano v2 (2026-10-14):** the installer pins the Q8_0 GGUF of a full-parameter Git fine-tune at Hub revision `8129125f` (SHA-256 `6d6542c8…`). With the current code it solves **963/1,000 sealed Git tasks (96.3%; 96.17% usage-weighted)** (927/1,000 with the code validated on Kaggle) and, on 1,000-task held-out sets, Files 98.0%, Read 96.1%, Test 94.4% and OS 100% (synthetic probe outputs). See the [model card](https://huggingface.co/rahul05ranjan/hydra-nano-git) and the receipts in [`evals/results/suite/v2-6d6542c8`](evals/results/suite/v2-6d6542c8) and [`final-git-validation`](evals/results/suite/final-git-validation/SUMMARY.json). The previous v1 artifact (87.4% sealed) stays at Hub revision `ff1c723f`.
|
|
37
|
-
|
|
38
|
-
`train/publish_release.py` publishes a validated release: it checks the checkpoint and GGUF against the frozen validation receipt, uploads them with the model cards in [`hub/`](hub), and pins `runtime-manifest.json` to the new revision. The GGUF is reproducible with llama.cpp `convert_hf_to_gguf.py` at commit `a05e58c` (`--outtype q8_0`). Safetensors files cannot be passed directly to `llama-server`.
|
|
39
|
-
|
|
40
|
-
Start the server in one terminal:
|
|
41
|
-
|
|
42
|
-
```powershell
|
|
43
|
-
llama-server -m C:\models\hydra-nano-git.gguf --alias hydra-nano:git --host 127.0.0.1 --port 8080 -c 8192
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
In another PowerShell terminal:
|
|
47
|
-
|
|
48
|
-
```powershell
|
|
49
|
-
$env:HYDRA_BACKEND = 'llamacpp'
|
|
50
|
-
$env:HYDRA_LLAMA_URL = 'http://127.0.0.1:8080'
|
|
51
|
-
$env:HYDRA_MODEL = 'hydra-nano:git'
|
|
52
|
-
node skills/hydra/scripts/hydra.mjs doctor
|
|
53
|
-
node skills/hydra/scripts/hydra.mjs ask --head git "which branch am I on?"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
On macOS/Linux, set the same variables with `export HYDRA_BACKEND=llamacpp`, `export HYDRA_LLAMA_URL=http://127.0.0.1:8080` and `export HYDRA_MODEL=hydra-nano:git`.
|
|
57
|
-
|
|
58
|
-
The URL is the server root, without `/v1`. `HYDRA_MODEL` must match the server alias; setting it sends all model-backed heads to that one model and does not create specialist weights. Without this override, Hydra requests its existing per-head model names; your server must serve those aliases. `hydra doctor` verifies the effective configured names. `hydra setup` with this backend prints instructions without installing or starting anything.
|
|
59
|
-
|
|
60
|
-
Hydra uses llama.cpp's [schema-constrained chat API](https://github.com/ggml-org/llama.cpp/tree/master/tools/server#post-v1chatcompletions-openai-compatible-chat-completions-api), disables thinking, and limits replies to 512 tokens. The model still chooses fixed read-only probes. HTTP fixture tests and native Windows Q8 inference/bootstrap checks pass; sealed accuracy on the converted runtime is tracked separately.
|
|
19
|
+
## Why Hydra
|
|
61
20
|
|
|
62
|
-
|
|
21
|
+
Frontier coding agents pay for every token of tool output: a failing test suite, a long `git log`, a `ps` dump. Most of it is noise, and all of it fills the context window.
|
|
63
22
|
|
|
64
|
-
|
|
23
|
+
Hydra puts small specialist models on your own machine between the agent and that output:
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# 2. Ask questions about any repo
|
|
71
|
-
cd <any git repository>
|
|
72
|
-
node <path>/skills/hydra/scripts/hydra.mjs ask "who changed package.json most recently and why?"
|
|
73
|
-
|
|
74
|
-
# 3. Slice files, symbols, or show structural outlines
|
|
75
|
-
node <path>/skills/hydra/scripts/hydra.mjs read src/index.ts handleRequest
|
|
76
|
-
node <path>/skills/hydra/scripts/hydra.mjs read src/server.ts --lines 20-50
|
|
77
|
-
node <path>/skills/hydra/scripts/hydra.mjs read src/config.py --outline
|
|
25
|
+
- **Less context, same answer.** A `pnpm test` run that printed ~13,807 tokens reached the agent as ~57: the failing test, the assertion diff and the source line.
|
|
26
|
+
- **Nothing leaves your machine.** The model runs locally on llama.cpp. Hydra's telemetry stays in `~/.hydra/ledger.jsonl`.
|
|
27
|
+
- **Read-only by design.** The model never writes a shell command. It picks from fixed, read-only menus; code builds and runs the command.
|
|
28
|
+
- **Verbatim, not paraphrased.** Hydra returns the command's own lines, so the agent sees evidence, not a summary.
|
|
78
29
|
|
|
79
|
-
|
|
80
|
-
node <path>/skills/hydra/scripts/hydra.mjs run -- npm test
|
|
81
|
-
node <path>/skills/hydra/scripts/hydra.mjs run -- pytest
|
|
82
|
-
|
|
83
|
-
# 5. View token savings
|
|
84
|
-
node <path>/skills/hydra/scripts/hydra.mjs stats
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Install as a skill
|
|
88
|
-
|
|
89
|
-
Everything an agent needs is inside `skills/hydra`:
|
|
30
|
+
## Quick start
|
|
90
31
|
|
|
91
32
|
```bash
|
|
92
|
-
npx
|
|
33
|
+
npx hydra-nano
|
|
93
34
|
```
|
|
94
35
|
|
|
95
|
-
|
|
36
|
+
One command installs everything: a pinned llama.cpp runtime, the ~812 MB Hydra Nano model (SHA-256 checked), the Hydra skill, and the integration for each coding tool it finds.
|
|
96
37
|
|
|
97
|
-
|
|
38
|
+
Want the `hydra` command in your own terminal too?
|
|
98
39
|
|
|
99
40
|
```bash
|
|
100
|
-
|
|
41
|
+
npm install -g hydra-nano
|
|
42
|
+
hydra doctor
|
|
101
43
|
```
|
|
102
44
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
For Claude Code the bootstrap also registers two Claude Code hooks in `~/.claude/settings.json`, because an agent that merely has the skill rarely uses it. A `PreToolUse` hook turns a direct test or build run (npm/pnpm/yarn test or build, jest, vitest, pytest, unittest, `node --test`, cargo, go, gradle, mvn, dotnet, make test) back with the `hydra run --` command to use instead; prefixing a command with `HYDRA_RAW=1` lets the raw log through. A `SessionStart` hook tells each new session to ask Hydra about git history and to keep Read and Grep for source files, where Hydra does not save tokens. Other settings are kept, the original file is saved as `settings.json.before-hydra`, and an invalid settings file is left untouched. Skip the hooks with `--no-hooks`; manage them later with `hydra hooks install|remove|status`.
|
|
106
|
-
|
|
107
|
-
The first bootstrap shares the Git model across all model-backed heads; it does not download five specialist weights. Runtime downloads and configuration stay outside agent skill folders, so skill updates retain them. Hydra starts its managed server on `127.0.0.1:11435` when needed; it stays running until `hydra stop` or a reboot. Run the installed `scripts/hydra.mjs doctor` to check readiness. `HYDRA_HOME` changes the data/config directory; explicit backend, URL and model environment variables override managed defaults.
|
|
45
|
+
Requires Node 18+. Runtime packages for macOS, Linux and Windows on x64 and arm64; verified live on macOS and Windows. Prefer GitHub? `npx github:rahul05ranjan/hydra` installs the same thing.
|
|
108
46
|
|
|
109
|
-
|
|
47
|
+
## Works with your coding tools
|
|
110
48
|
|
|
111
|
-
|
|
49
|
+
| Tool | How Hydra joins | Status |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| **Claude Code** | Skill + hooks that route test/build runs and git questions | Tested |
|
|
52
|
+
| **Codex** | Skill + MCP server + `PreToolUse` hook | MCP listed by Codex; trust the hook with `/hooks` |
|
|
53
|
+
| **OpenCode** | MCP server + plugin | MCP connects |
|
|
54
|
+
| **Cursor** | MCP server + `beforeShellExecution` hook | Preview |
|
|
55
|
+
| **Gemini CLI** | MCP server + `BeforeTool` hook | Preview |
|
|
56
|
+
| **Antigravity** | Skill + MCP server | Preview |
|
|
112
57
|
|
|
113
|
-
|
|
114
|
-
- Claude Code: `~/.claude/skills/hydra`
|
|
115
|
-
- Antigravity: `~/.gemini/config/skills/hydra`
|
|
58
|
+
Any other tool that speaks MCP can use Hydra too: point it at `hydra mcp` (stdio).
|
|
116
59
|
|
|
117
|
-
|
|
60
|
+
Hydra never overwrites your settings. Every file it edits is backed up once as `<file>.before-hydra`, only Hydra's own entries change, and `hydra agents remove` takes them out again.
|
|
118
61
|
|
|
119
|
-
|
|
62
|
+
## What it does
|
|
120
63
|
|
|
121
|
-
|
|
64
|
+
| Head | Ask it | It returns |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| **git** | "what changed in the last commit?", "who last touched `billing.js`?" | The matching commits, diff hunks or blame lines |
|
|
67
|
+
| **test** | `hydra run -- npm test` (pytest, cargo, go, tsc, gradle, ...) | Failing test names, assertion diffs and source locations, with the original exit code |
|
|
68
|
+
| **os** | "what is listening on port 3000?" | The matching ports, processes or disk lines, with secrets blocked |
|
|
69
|
+
| **read** | `hydra read src/server.ts handleRequest` | One symbol, a line range or an outline |
|
|
70
|
+
| **files** | `hydra search parseConfig` | Definition-ranked matches |
|
|
122
71
|
|
|
123
|
-
|
|
72
|
+
Through MCP, Hydra offers the three where it pays most: `hydra_git`, `hydra_run` and `hydra_os`.
|
|
124
73
|
|
|
125
|
-
|
|
74
|
+
## Benchmarks
|
|
126
75
|
|
|
127
|
-
|
|
76
|
+
**Hydra Nano v2**: a full-parameter fine-tune of a 0.8B model, one set of weights for every head, run as a Q8_0 GGUF.
|
|
128
77
|
|
|
129
|
-
|
|
78
|
+
| Set | Tasks | Solved |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Git (sealed test set, never used for training) | 1,000 | **96.3%** |
|
|
81
|
+
| Files (held out) | 1,000 | **98.0%** |
|
|
82
|
+
| Read (held out) | 1,000 | **96.1%** |
|
|
83
|
+
| Test logs (second held-out set) | 1,000 | **97.5%** |
|
|
84
|
+
| OS (held out, synthetic probe outputs) | 1,000 | **100%** |
|
|
85
|
+
| Test-log benchmark across 8 frameworks | 20 | **19/20**, 47% fewer tokens |
|
|
130
86
|
|
|
131
|
-
|
|
87
|
+
Receipts: [model card](https://huggingface.co/rahul05ranjan/hydra-nano-git) and [`evals/results`](https://github.com/rahul05ranjan/hydra/tree/main/evals/results).
|
|
132
88
|
|
|
133
|
-
|
|
89
|
+
**Where Hydra does not help, measured.** In a controlled trial of 36 Claude Code sessions extracting functions from real source code, forcing that work through Hydra used **72% more tokens** than the agent's own tools. So Hydra leaves source reading and search to the agent, and routes only the noisy work: test and build logs, git history, system inspection. Full method and numbers in [evaluations](https://github.com/rahul05ranjan/hydra/blob/main/docs/evaluations.md).
|
|
134
90
|
|
|
135
|
-
|
|
91
|
+
## Commands
|
|
136
92
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- `commit_files`: 46% ➔ 72% (+26 points)
|
|
148
|
-
|
|
149
|
-
Full results in `evals/results/git1.qwen3.5-0.8b.json`.
|
|
150
|
-
|
|
151
|
-
### 2. `evals/test_logs` (20 cross-language tasks)
|
|
152
|
-
|
|
153
|
-
Benchmarks failure extraction and noise reduction across Vitest, Jest, Pytest, Cargo, TSC, Go, node:test, and Mocha.
|
|
154
|
-
|
|
155
|
-
- **Solved**: **100% (20/20)**
|
|
156
|
-
- **Test Name Recall**: **100%**
|
|
157
|
-
- **File / Line Location Recall**: **100%**
|
|
158
|
-
- **Assertion Diff Recall**: **100%**
|
|
159
|
-
- **Cleanliness**: **100%** (zero framework or `node_modules` stack frame pollution)
|
|
160
|
-
- **Token Savings**: **49.6% net token reduction** overall, and **96.4% to 96.7%** on verbose test suites.
|
|
161
|
-
- **Runtime**: **3.2s** across all 20 tasks.
|
|
93
|
+
```bash
|
|
94
|
+
hydra ask "which branch am I on and what changed?" # git, files or os, routed by the question
|
|
95
|
+
hydra run -- pytest -q # a test or build run, digested
|
|
96
|
+
hydra os "what is using port 8080?" # read-only system inspection
|
|
97
|
+
hydra read src/app.py create_app # one symbol, a line range or an outline
|
|
98
|
+
hydra stats # tokens kept out of context
|
|
99
|
+
hydra improve --since 2026-10-17 # what the local telemetry says to improve
|
|
100
|
+
hydra agents status # Hydra in each coding tool
|
|
101
|
+
hydra doctor # is the local model ready?
|
|
102
|
+
```
|
|
162
103
|
|
|
163
|
-
|
|
104
|
+
Need the raw log for one run? Prefix it with `HYDRA_RAW=1` and the hook lets it through.
|
|
164
105
|
|
|
165
|
-
|
|
106
|
+
## Privacy and control
|
|
166
107
|
|
|
167
|
-
|
|
108
|
+
- **Local only.** The model, the runtime and the telemetry live in `~/.hydra`. Nothing is sent anywhere.
|
|
109
|
+
- **Turn telemetry off** with `HYDRA_TELEMETRY=0`.
|
|
110
|
+
- **Skip parts of the install** with `--no-agents` (no other tools), `--no-hooks` (no hooks) or `--runtime-only` (no skill).
|
|
111
|
+
- **Uninstall cleanly.** `hydra agents remove` and `hydra hooks remove` undo every integration and restore your settings; `hydra stop` stops the local server; deleting `~/.hydra` removes the runtime, the model and the telemetry.
|
|
168
112
|
|
|
169
|
-
|
|
170
|
-
- **Target Line Recall**: **100%**
|
|
171
|
-
- **Cleanliness**: **100%** (zero unrelated function/code leak)
|
|
172
|
-
- **Token Savings**: **48.1% net token reduction** overall, and **77% to 92%** on targeted line and symbol slices.
|
|
173
|
-
- **Runtime**: **0.01s** (instantaneous local extraction).
|
|
113
|
+
## Learn more
|
|
174
114
|
|
|
175
|
-
|
|
115
|
+
- [How Hydra works](https://github.com/rahul05ranjan/hydra/blob/main/docs/how-it-works.md): heads, backends (llama.cpp or Ollama), telemetry, every install path and the code layout.
|
|
116
|
+
- [Evaluations and trials](https://github.com/rahul05ranjan/hydra/blob/main/docs/evaluations.md): how each number above was measured.
|
|
117
|
+
- [What real use teaches](https://github.com/rahul05ranjan/hydra/blob/main/IMPROVEMENTS.md): the open and fixed findings from Hydra's own telemetry.
|
|
118
|
+
- [Hydra Nano on Hugging Face](https://huggingface.co/rahul05ranjan/hydra-nano-git).
|
|
176
119
|
|
|
177
|
-
##
|
|
120
|
+
## License
|
|
178
121
|
|
|
179
|
-
|
|
180
|
-
- `skills/hydra/scripts/hydra.mjs`: the CLI.
|
|
181
|
-
- `skills/hydra/scripts/lib/`:
|
|
182
|
-
- `runtime.mjs`: selectable Ollama/llama.cpp API client and per-head model configuration.
|
|
183
|
-
- `route.mjs`: route questions between heads.
|
|
184
|
-
- `git.mjs`: git head probes and plan step.
|
|
185
|
-
- `files.mjs`: search and walk head.
|
|
186
|
-
- `read.mjs`: file slice and symbol reader head.
|
|
187
|
-
- `test_logs.mjs`: test runner and build log reader head.
|
|
188
|
-
- `answer.mjs`: verbatim line selection and lead generation.
|
|
189
|
-
- `ollama.mjs`: compatibility exports for existing evaluation callers.
|
|
190
|
-
- `ledger.mjs`: local token accounting ledger.
|
|
191
|
-
- `test/`: unit test suite (`npm test`).
|
|
192
|
-
- `evals/git1/`: 1000-task sealed test set for git head.
|
|
193
|
-
- `evals/test_logs/`: 20-task cross-language benchmark for test and build logs.
|
|
194
|
-
- `evals/read/`: 20-task cross-language benchmark for file slice reading.
|
|
122
|
+
MIT. See [LICENSE](https://github.com/rahul05ranjan/hydra/blob/main/LICENSE).
|
package/package.json
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hydra-nano",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Small local models that
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Small local models that take the noisy work off your coding agent: git history, test and build digests and OS inspection run on your machine, and only the lines that matter reach Claude Code, Codex, Cursor, OpenCode, Gemini CLI or Antigravity. Skill, MCP server and hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
8
|
+
"cursor",
|
|
9
|
+
"opencode",
|
|
10
|
+
"gemini-cli",
|
|
11
|
+
"antigravity",
|
|
12
|
+
"mcp",
|
|
13
|
+
"mcp-server",
|
|
8
14
|
"agent-skill",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"coding-agent",
|
|
9
17
|
"local-llm",
|
|
10
18
|
"llama.cpp",
|
|
19
|
+
"token-saving",
|
|
20
|
+
"context-window",
|
|
11
21
|
"git",
|
|
12
22
|
"test-runner",
|
|
13
|
-
"
|
|
23
|
+
"developer-tools"
|
|
14
24
|
],
|
|
15
25
|
"license": "MIT",
|
|
16
26
|
"author": "rahul05ranjan",
|
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// keeps failing test names, assertion diffs and source locations and drops the rest
|
|
2
|
+
// Shell-command hook: a test or build run goes through `hydra run --` (or the hydra_run MCP tool),
|
|
3
|
+
// whose digest keeps failing test names, assertion diffs and source locations and drops the rest.
|
|
4
4
|
// Prefix a command with HYDRA_RAW=1 when the raw log is really needed.
|
|
5
|
+
// --format picks the tool's hook protocol:
|
|
6
|
+
// claude, codex PreToolUse; command at tool_input.command; deny via hookSpecificOutput.
|
|
7
|
+
// gemini BeforeTool on run_shell_command; deny via {"decision": "deny", "reason"}.
|
|
8
|
+
// cursor beforeShellExecution; command at command; must always answer with a permission.
|
|
9
|
+
// Each decision goes to the local ledger with the hook's own time, for `hydra improve`; a command
|
|
10
|
+
// that simply passes is recorded without its text.
|
|
5
11
|
import path from 'node:path';
|
|
6
12
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { testRun } from '../lib/claude_hooks.mjs';
|
|
13
|
+
import { rawBypass, testRun } from '../lib/claude_hooks.mjs';
|
|
14
|
+
import { record } from '../lib/ledger.mjs';
|
|
8
15
|
|
|
9
16
|
const hydra = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'hydra.mjs');
|
|
17
|
+
const flag = process.argv.indexOf('--format');
|
|
18
|
+
const format = flag >= 0 ? process.argv[flag + 1] : 'claude';
|
|
19
|
+
|
|
20
|
+
const answers = {
|
|
21
|
+
claude: { pass: null, deny: reason => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } }) },
|
|
22
|
+
gemini: { pass: { decision: 'allow' }, deny: reason => ({ decision: 'deny', reason }) },
|
|
23
|
+
cursor: { pass: { permission: 'allow' }, deny: reason => ({ permission: 'deny', agent_message: reason,
|
|
24
|
+
user_message: 'Hydra sent this test/build run through hydra run to keep the full log out of context.' }) },
|
|
25
|
+
};
|
|
26
|
+
answers.codex = answers.claude;
|
|
27
|
+
const answer = answers[format] ?? answers.claude;
|
|
28
|
+
|
|
10
29
|
let input = '';
|
|
11
30
|
process.stdin.on('data', chunk => { input += chunk; }).on('end', () => {
|
|
12
|
-
let
|
|
13
|
-
try {
|
|
31
|
+
let parsed;
|
|
32
|
+
try { parsed = JSON.parse(input) ?? {}; } catch {
|
|
33
|
+
if (answer.pass) process.stdout.write(JSON.stringify(answer.pass));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const command = String((format === 'cursor' ? parsed.command : parsed.tool_input?.command) ?? '');
|
|
37
|
+
const session = parsed.session_id ?? parsed.conversation_id;
|
|
38
|
+
const event = (decision, withCommand = true) => ({ ts: new Date().toISOString(), kind: 'hook', hook: 'route-tests',
|
|
39
|
+
...(format === 'claude' ? {} : { agent: format }), decision, ...(withCommand ? { command: command.slice(0, 300) } : {}),
|
|
40
|
+
...(session ? { session } : {}), hookMs: Math.round(process.uptime() * 1000) });
|
|
14
41
|
const run = testRun(command);
|
|
15
|
-
if (!run)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
if (!run) {
|
|
43
|
+
record(rawBypass(command) ? event('raw') : event('pass', false));
|
|
44
|
+
if (answer.pass) process.stdout.write(JSON.stringify(answer.pass));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
record({ ...event('deny'), run });
|
|
48
|
+
process.stdout.write(JSON.stringify(answer.deny(`Test/build runs go through Hydra so the full log stays out of context. Run it as:\n` +
|
|
49
|
+
` node "${hydra}" run -- ${run}\n${format === 'claude' ? '' : 'or call the hydra_run tool with that command. '}` +
|
|
50
|
+
`(Keep any cd or env prefix before it.) Hydra returns the failing tests, assertion diffs and source locations ` +
|
|
51
|
+
`verbatim, or a short pass summary. If you truly need the raw log, prefix the original command with HYDRA_RAW=1.`)));
|
|
22
52
|
});
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Session-start hook: tells the agent where Hydra pays off, so it uses it without being asked.
|
|
3
|
+
// --format claude or codex answers with hookSpecificOutput; cursor with additional_context.
|
|
4
|
+
// The session start goes to the local ledger, so `hydra improve` can tell how many sessions used Hydra.
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { record } from '../lib/ledger.mjs';
|
|
5
8
|
|
|
6
9
|
const hydra = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'hydra.mjs');
|
|
7
|
-
process.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
const flag = process.argv.indexOf('--format');
|
|
11
|
+
const format = flag >= 0 ? process.argv[flag + 1] : 'claude';
|
|
12
|
+
const mcp = format === 'claude' ? '' : ' (or the hydra_git / hydra_run MCP tools)';
|
|
13
|
+
const guide = `Hydra is installed: it runs noisy read-only commands on a small local model and returns only the lines that matter.\n` +
|
|
14
|
+
`- For git history, diff, blame or "what changed" questions, first run: node "${hydra}" ask --head git "<question>"${mcp} ` +
|
|
10
15
|
`from the repository, instead of reading git log, git diff or git show output yourself. Use plain git for writes ` +
|
|
11
16
|
`and for short checks such as git status --short.\n` +
|
|
12
17
|
`- A hook sends test and build runs through: node "${hydra}" run -- <command>\n` +
|
|
13
|
-
`- Keep using
|
|
18
|
+
`- Keep using your own file reading and search for source files; Hydra does not save tokens there.`;
|
|
19
|
+
|
|
20
|
+
let input = '';
|
|
21
|
+
process.stdin.on('data', chunk => { input += chunk; }).on('end', () => {
|
|
22
|
+
let start = {};
|
|
23
|
+
try { start = JSON.parse(input) ?? {}; } catch { /* The tool always sends JSON; record what there is. */ }
|
|
24
|
+
const session = start.session_id ?? start.conversation_id;
|
|
25
|
+
record({ ts: new Date().toISOString(), kind: 'session', ...(format === 'claude' ? {} : { agent: format }),
|
|
26
|
+
...(session ? { session } : {}), ...(start.source ? { source: start.source } : {}) });
|
|
27
|
+
process.stdout.write(JSON.stringify(format === 'cursor' ? { additional_context: guide }
|
|
28
|
+
: { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: guide } }));
|
|
29
|
+
});
|
|
@@ -10,11 +10,12 @@ import * as git from './lib/git.mjs';
|
|
|
10
10
|
import * as test_logs from './lib/test_logs.mjs';
|
|
11
11
|
import * as read from './lib/read.mjs';
|
|
12
12
|
import * as osHead from './lib/os.mjs';
|
|
13
|
-
import { record, stats, tokens } from './lib/ledger.mjs';
|
|
13
|
+
import { findings, metered, record, stats, tokens } from './lib/ledger.mjs';
|
|
14
14
|
import { chat, hasModel, BACKEND, modelFor, Unavailable } from './lib/runtime.mjs';
|
|
15
15
|
import { route } from './lib/route.mjs';
|
|
16
16
|
import { stopLocal } from './lib/local.mjs';
|
|
17
17
|
import { hookStatus, installHooks, removeHooks } from './lib/claude_hooks.mjs';
|
|
18
|
+
import * as integrations from './lib/integrations.mjs';
|
|
18
19
|
|
|
19
20
|
const HEADS = { git, files, test: test_logs, read, os: osHead };
|
|
20
21
|
|
|
@@ -25,10 +26,14 @@ const USAGE = `usage:
|
|
|
25
26
|
hydra run [--head test] -- <command...> run a command and digest test/build output
|
|
26
27
|
hydra os [question...] inspect ports, processes, disk, system
|
|
27
28
|
hydra stats token accounting for past calls
|
|
29
|
+
hydra improve [--since <date>] [--json] what the local telemetry says to improve
|
|
28
30
|
hydra doctor check that the local model is ready
|
|
29
31
|
hydra setup show runtime setup instructions
|
|
30
32
|
hydra stop stop Hydra's managed llama-server
|
|
31
|
-
hydra hooks install|remove|status Claude Code hooks that route tests and git questions here
|
|
33
|
+
hydra hooks install|remove|status Claude Code hooks that route tests and git questions here
|
|
34
|
+
hydra agents install|remove|status [tool...] Hydra's MCP server and hooks in codex, cursor, opencode,
|
|
35
|
+
gemini-cli and antigravity (default: every tool found)
|
|
36
|
+
hydra mcp run the MCP server on stdio`;
|
|
32
37
|
|
|
33
38
|
function fail(code, message) {
|
|
34
39
|
console.error(message);
|
|
@@ -47,14 +52,14 @@ async function ask(args) {
|
|
|
47
52
|
if (head === 'git' && !repo) fail(1, 'hydra: the git head needs a git repository, and this directory is not in one');
|
|
48
53
|
|
|
49
54
|
const started = Date.now();
|
|
50
|
-
const
|
|
51
|
-
const sections = await HEADS[head].gather(question, cwd,
|
|
52
|
-
const { text, rawChars } = await answer(question, sections,
|
|
55
|
+
const meter = metered(opts => chat({ ...opts, model: modelFor(head) }));
|
|
56
|
+
const sections = await HEADS[head].gather(question, cwd, meter.chat);
|
|
57
|
+
const { text, rawChars } = await answer(question, sections, meter.chat);
|
|
53
58
|
const raw = tokens(rawChars);
|
|
54
59
|
const returned = tokens(text.length);
|
|
55
60
|
const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
|
|
56
61
|
|
|
57
|
-
record({ ts: new Date().toISOString(), head, question, raw, returned, seconds });
|
|
62
|
+
record({ ts: new Date().toISOString(), head, question, raw, returned, seconds, modelCalls: meter.modelCalls, modelSeconds: meter.modelSeconds });
|
|
58
63
|
console.log(`${text}\n\n[hydra ${head} · read ~${raw} tok locally, returned ~${returned} tok · ${seconds}s]`);
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -67,12 +72,12 @@ async function runCommand(args) {
|
|
|
67
72
|
const stdinData = readFileSync(0, 'utf8');
|
|
68
73
|
if (stdinData.trim()) {
|
|
69
74
|
const started = Date.now();
|
|
70
|
-
const
|
|
71
|
-
const { text, rawChars, exitCode } = await test_logs.digest({ cmd: 'pipe', out: stdinData, exitCode: 1 },
|
|
75
|
+
const meter = metered(opts => chat({ ...opts, model: modelFor('test') }));
|
|
76
|
+
const { text, rawChars, exitCode } = await test_logs.digest({ cmd: 'pipe', out: stdinData, exitCode: 1 }, meter.chat);
|
|
72
77
|
const raw = tokens(rawChars);
|
|
73
78
|
const returned = tokens(text.length);
|
|
74
79
|
const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
|
|
75
|
-
record({ ts: new Date().toISOString(), head: 'test', question: 'pipe', raw, returned, seconds });
|
|
80
|
+
record({ ts: new Date().toISOString(), head: 'test', question: 'pipe', raw, returned, seconds, modelCalls: meter.modelCalls, modelSeconds: meter.modelSeconds });
|
|
76
81
|
console.log(`${text}\n\n[hydra test · read ~${raw} tok locally, returned ~${returned} tok · ${seconds}s]`);
|
|
77
82
|
if (exitCode !== 0) process.exit(exitCode);
|
|
78
83
|
return;
|
|
@@ -85,17 +90,19 @@ async function runCommand(args) {
|
|
|
85
90
|
const cwd = process.cwd();
|
|
86
91
|
const started = Date.now();
|
|
87
92
|
const res = await run(prog, progArgs, cwd);
|
|
88
|
-
const
|
|
93
|
+
const commandSeconds = Number(((Date.now() - started) / 1000).toFixed(1));
|
|
94
|
+
const meter = metered(opts => chat({ ...opts, model: modelFor('test') }));
|
|
89
95
|
const { text, rawChars, exitCode } = await test_logs.digest({
|
|
90
96
|
cmd: cmdArgs.join(' '),
|
|
91
97
|
out: res.out,
|
|
92
98
|
err: res.err,
|
|
93
99
|
exitCode: res.exitCode,
|
|
94
|
-
},
|
|
100
|
+
}, meter.chat);
|
|
95
101
|
const raw = tokens(rawChars);
|
|
96
102
|
const returned = tokens(text.length);
|
|
97
103
|
const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
|
|
98
|
-
record({ ts: new Date().toISOString(), head: 'test', question: cmdArgs.join(' '), raw, returned, seconds
|
|
104
|
+
record({ ts: new Date().toISOString(), head: 'test', question: cmdArgs.join(' '), raw, returned, seconds,
|
|
105
|
+
commandSeconds, exit: exitCode, modelCalls: meter.modelCalls, modelSeconds: meter.modelSeconds });
|
|
99
106
|
console.log(`${text}\n\n[hydra test · read ~${raw} tok locally, returned ~${returned} tok · ${seconds}s]`);
|
|
100
107
|
if (exitCode !== 0) process.exit(exitCode);
|
|
101
108
|
}
|
|
@@ -153,14 +160,14 @@ async function readCommand(args) {
|
|
|
153
160
|
async function osCommand(args) {
|
|
154
161
|
const question = args.join(' ').trim() || 'system overview';
|
|
155
162
|
const started = Date.now();
|
|
156
|
-
const
|
|
157
|
-
const sections = await osHead.gather(question, process.cwd(),
|
|
158
|
-
const { text, rawChars } = await answer(question, sections,
|
|
163
|
+
const meter = metered(opts => chat({ ...opts, model: modelFor('os') }));
|
|
164
|
+
const sections = await osHead.gather(question, process.cwd(), meter.chat);
|
|
165
|
+
const { text, rawChars } = await answer(question, sections, meter.chat);
|
|
159
166
|
const raw = tokens(rawChars);
|
|
160
167
|
const returned = tokens(text.length);
|
|
161
168
|
const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
|
|
162
169
|
|
|
163
|
-
record({ ts: new Date().toISOString(), head: 'os', question, raw, returned, seconds });
|
|
170
|
+
record({ ts: new Date().toISOString(), head: 'os', question, raw, returned, seconds, modelCalls: meter.modelCalls, modelSeconds: meter.modelSeconds });
|
|
164
171
|
console.log(`${text}\n\n[hydra os · read ~${raw} tok locally, returned ~${returned} tok · ${seconds}s]`);
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -218,6 +225,30 @@ Estimate: output size / 4. It counts the output an agent would have read by runn
|
|
|
218
225
|
the commands itself, and leaves out the extra turns those commands would have cost.`);
|
|
219
226
|
}
|
|
220
227
|
|
|
228
|
+
function improve(args) {
|
|
229
|
+
const flag = args.indexOf('--since');
|
|
230
|
+
const since = flag >= 0 ? args[flag + 1] : undefined;
|
|
231
|
+
if (flag >= 0 && (!since || Number.isNaN(Date.parse(since)))) fail(1, 'usage: hydra improve [--since <date>] [--json]');
|
|
232
|
+
const report = findings(undefined, { since: since && new Date(since).toISOString() });
|
|
233
|
+
if (args.includes('--json')) return console.log(JSON.stringify(report, null, 2));
|
|
234
|
+
const lines = [`since ${report.since ?? 'the start'}: ${report.calls} calls; ${report.sessions.usingHydra} of ${report.sessions.seen} recorded sessions used Hydra`];
|
|
235
|
+
for (const [head, h] of Object.entries(report.perHead)) {
|
|
236
|
+
lines.push(`${head.padEnd(6)} ${h.calls} calls, saved ~${h.saved} tok (${Math.round(h.savedShare * 100)}%), ` +
|
|
237
|
+
`${h.costMore} cost more than raw, p50 ${h.p50Seconds}s, p90 ${h.p90Seconds}s, ${h.modelCalls} model calls`);
|
|
238
|
+
for (const row of h.costMoreExamples) lines.push(` cost more: ${row.question.slice(0, 80)} (${row.raw} -> ${row.returned})`);
|
|
239
|
+
}
|
|
240
|
+
for (const [agent, a] of Object.entries(report.agents)) {
|
|
241
|
+
lines.push(`agent ${agent}: ${a.calls} calls (${a.viaMcp} through MCP), ${a.denials} runs turned back, ${a.sessions} sessions started`);
|
|
242
|
+
}
|
|
243
|
+
const { denials, followedByHydra, rawBypasses } = report.hooks;
|
|
244
|
+
const { decisions, p50Ms, p90Ms } = report.hooks;
|
|
245
|
+
lines.push(`hooks ${decisions} Bash calls checked (p50 ${p50Ms} ms, p90 ${p90Ms} ms); ${denials} test/build runs turned back, ` +
|
|
246
|
+
`${followedByHydra} followed by a Hydra run within 5 minutes`);
|
|
247
|
+
for (const command of rawBypasses) lines.push(` HYDRA_RAW=1 bypass (check for a false match): ${command.slice(0, 100)}`);
|
|
248
|
+
for (const error of report.errors) lines.push(`error ${error.ts} ${error.command}: ${error.error}`);
|
|
249
|
+
console.log(lines.join('\n'));
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
async function doctor() {
|
|
222
253
|
const checks = new Map();
|
|
223
254
|
const modelStatuses = await Promise.all(
|
|
@@ -268,12 +299,38 @@ const commands = {
|
|
|
268
299
|
read: () => readCommand(rest),
|
|
269
300
|
os: () => osCommand(rest),
|
|
270
301
|
stats: showStats,
|
|
302
|
+
improve: () => improve(rest),
|
|
271
303
|
doctor,
|
|
272
304
|
setup,
|
|
273
305
|
stop: () => console.log(stopLocal() ? 'Hydra local server stopped.' : 'No managed Hydra server is running.'),
|
|
274
306
|
hooks: () => hooksCommand(rest[0]),
|
|
307
|
+
agents: () => agentsCommand(rest),
|
|
308
|
+
mcp: () => import('./mcp.mjs').then(server => server.serve()),
|
|
275
309
|
};
|
|
276
310
|
|
|
311
|
+
function agentsCommand(args) {
|
|
312
|
+
const [action, ...words] = args;
|
|
313
|
+
const ids = words.filter(word => !word.startsWith('--'));
|
|
314
|
+
if (action === 'status') {
|
|
315
|
+
for (const row of integrations.status()) {
|
|
316
|
+
console.log(`${row.id.padEnd(12)} ${row.present ? 'found ' : 'not found'} mcp ${row.mcp ? 'yes' : 'no '}${'hooks' in row ? ` hooks ${row.hooks ? 'yes' : 'no'}` : ''}`);
|
|
317
|
+
}
|
|
318
|
+
console.log(`claude-code ${hookStatus().length ? 'hooks yes' : 'hooks no'} (skill and hooks; see hydra hooks status)`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!['install', 'remove'].includes(action)) fail(1, 'usage: hydra agents install|remove|status [codex cursor opencode gemini-cli antigravity] [--no-hooks]');
|
|
322
|
+
const scripts = action === 'install'
|
|
323
|
+
? integrations.installSharedScripts(path.dirname(fileURLToPath(import.meta.url))) : integrations.sharedScripts();
|
|
324
|
+
const targets = ids.length ? ids : action === 'install' ? integrations.detected(scripts) : Object.keys(integrations.tools(scripts));
|
|
325
|
+
if (!targets.length) return console.log('No supported coding tool found (codex, cursor, opencode, gemini-cli, antigravity).');
|
|
326
|
+
for (const result of integrations.apply(targets, action, { scripts, hooks: !words.includes('--no-hooks') })) {
|
|
327
|
+
if (result.error) { console.log(`${result.id}: ${result.error}`); continue; }
|
|
328
|
+
console.log(`${result.id}: ${result.changes.length ? result.changes.join('; ') : action === 'install' ? 'nothing changed' : 'nothing to remove'}`);
|
|
329
|
+
for (const message of result.manual) console.log(` ${message.replace(/\n/g, '\n ')}`);
|
|
330
|
+
if (result.note) console.log(` ${result.note}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
277
334
|
function hooksCommand(action) {
|
|
278
335
|
const scripts = path.dirname(fileURLToPath(import.meta.url));
|
|
279
336
|
if (action === 'install') console.log(`Hydra hooks registered in ${installHooks(scripts)}.`);
|
|
@@ -286,6 +343,7 @@ try {
|
|
|
286
343
|
if (!commands[command]) fail(1, USAGE);
|
|
287
344
|
await commands[command]();
|
|
288
345
|
} catch (e) {
|
|
346
|
+
record({ ts: new Date().toISOString(), kind: 'error', command, error: String(e?.message ?? e).slice(0, 300) });
|
|
289
347
|
if (!(e instanceof Unavailable)) throw e;
|
|
290
348
|
fail(2, `hydra unavailable: ${e.message}`);
|
|
291
349
|
}
|