ruvnet-brain 0.5.0-dev
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/LICENSE +26 -0
- package/README.md +234 -0
- package/bin/install.mjs +932 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stuart Kerr (Isovision.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Note: The RuvNet building-block repositories indexed by this brain are the work
|
|
24
|
+
of Reuven Cohen (rUv) and are governed by their own licenses in the ruvnet
|
|
25
|
+
organization (https://github.com/ruvnet). This project indexes and cites that
|
|
26
|
+
source; it does not relicense it.
|
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
# π§ RuvNet Brain
|
|
6
|
+
|
|
7
|
+
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack β delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
8
|
+
|
|
9
|
+
[](https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev)
|
|
10
|
+
[](https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev)
|
|
11
|
+
[](https://ruvnet-brain.vercel.app)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](#testing--proof)
|
|
14
|
+
|
|
15
|
+
<sub>Built by **[Stuart Kerr](https://isovision.ai)** at [Isovision.ai](https://isovision.ai) Β· free & fair use, to help everyone leverage the high end of agentic coding.</sub>
|
|
16
|
+
|
|
17
|
+
### [βΆ Come see it visually explained](https://ruvnet-brain.vercel.app)
|
|
18
|
+
|
|
19
|
+
<sub>An interactive, animated walkthrough of what you can actually build β **click the preview** to open it.</sub>
|
|
20
|
+
|
|
21
|
+
[](https://ruvnet-brain.vercel.app)
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## The one thing to understand first
|
|
28
|
+
|
|
29
|
+
**Reuven Cohen (rUv) builds about nine months ahead of the state of the art.** His RuvNet building blocks β Ruflo, RuVector, AgentDB, agentic-flow, SPARC and ~20 more β are the working prototypes of what becomes mainstream AI tooling three quarters later. This is the actual front edge.
|
|
30
|
+
|
|
31
|
+
But **Claude was trained on _classical_ software development.** Point it at rUv's stack and it doesn't recognize the work: it drifts, it _doubts real capabilities_ (βruflo can't edit files,β βRuvNet has no vector DB β use Pineconeβ), and it quietly falls back to the patterns it knows (pgvector, LangChain, a hand-rolled cosine loop). A newcomer gets the **worst of both worlds** β a revolutionary toolset with no instruction manual, _and_ an assistant that talks them out of using it.
|
|
32
|
+
|
|
33
|
+
> **RuvNet Brain is the missing instruction manual.** It reads rUv's real source, hands Claude the _answer key_, and removes Claude's permission to make things up about the stack. Install it once, aim it at any repo, and a newcomer can build ~9 months ahead β without being rUv.
|
|
34
|
+
|
|
35
|
+
The novelty is **enforcement, not retrieval.** Plain RAG only decides what to _add_ to context; it never stops a model from overriding good context with a stronger prior. This ships a `UserPromptSubmit` hook that injects a grounding directive on every RuvNet-relevant turn, consumed structurally by the harness β so grounding is **non-optional**. **RAG decides what to add; this decides what the model isn't allowed to make up.**
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## The problem it solves
|
|
40
|
+
|
|
41
|
+
You know the failure mode. You ask Claude to build with Ruflo or RuVector, and instead of reading rUv's actual code it reaches for its training priors: _βlet's just use pgvector,β βI'll hand-roll cosine similarity,β βI don't think ruflo can edit files.β_ It skims, it guesses, it doubts tools that work perfectly well β and the result drifts **off** the very stack you chose.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
<details><summary>ASCII version (for AI / accessibility)</summary>
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
WITHOUT the brain β drift | WITH RuvNet Brain β grounded
|
|
49
|
+
|
|
|
50
|
+
You: build with Ruflo/RuVector | You: the same request
|
|
51
|
+
| | |
|
|
52
|
+
v | v
|
|
53
|
+
Claude falls back to priors | Enforcement hook grounds the turn
|
|
54
|
+
| | |
|
|
55
|
+
v | v
|
|
56
|
+
"just use pgvector" | search_ruvnet -> cited rUv source
|
|
57
|
+
hand-rolls cosine similarity | (whole files, labeled repo + path)
|
|
58
|
+
"ruflo can't edit files" | |
|
|
59
|
+
| | v
|
|
60
|
+
v | Builds ON the stack (RVF/HNSW, swarms)
|
|
61
|
+
Drifts OFF the stack |
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
</details>
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Install β one line
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx github:stuinfla/ruvnet-brain
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That single command runs the whole setup, narrating _what it's doing and why_ at each step: it downloads the brain (~512 MB) from the [v0.5.0-dev Release](https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev), unpacks it to `~/.cache/ruvnet-brain/kb`, installs its local reader (no cloud calls, no API keys), and wires the Claude Code plugin β the `search_ruvnet` MCP tool + the `UserPromptSubmit` grounding hook β at user scope. It's safe to re-run, and it stays current: `node ~/.cache/ruvnet-brain/kb/forge-update.mjs --apply` pulls the latest Release. You install once; you don't keep re-downloading.
|
|
75
|
+
|
|
76
|
+
> **Honest note:** not on npm yet β `npx github:stuinfla/ruvnet-brain` is the works-now path (it pulls the brain from the Release, which exists). Once published, the same install becomes `npx ruvnet-brain`.
|
|
77
|
+
|
|
78
|
+
<details><summary>Manual install (what the one-liner automates)</summary>
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
claude plugin marketplace add stuinfla/ruvnet-brain
|
|
82
|
+
claude plugin install ruvnet-brain@ruvnet-brain --scope user
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Registers the `search_ruvnet` MCP tool, the grounding skill, and the `UserPromptSubmit` enforcement hook β globally, at user scope. The plugin expects the brain at `~/.cache/ruvnet-brain/kb` (or point `RUVNET_BRAIN_KB` at your own copy). The first install may show a one-time trust prompt for the hook.
|
|
86
|
+
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
**Then just ask.** _βHow does Ruflo orchestrate agent swarms, and what implements it?β_ The hook grounds the turn, Claude calls `search_ruvnet`, and it answers from cited source β down to the function body.
|
|
90
|
+
|
|
91
|
+

|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## β¨ What's new in v0.5.0-dev β it now knows the stack _down to the code_
|
|
96
|
+
|
|
97
|
+
Earlier versions knew the **docs and architecture**. v0.5.0-dev re-indexes the code-rich repos to **full function bodies**, against each repo's real source layout β so βhow is this actually implemented?β returns the implementation, not a summary:
|
|
98
|
+
|
|
99
|
+
| Repo | Full-body code passages | |
|
|
100
|
+
|---|---:|---|
|
|
101
|
+
| `agentic-flow` | 296 β **984** | model-routing, ReasoningBank |
|
|
102
|
+
| `ruv-fann` | 52 β **779** | ruv-swarm, cuda-wasm, neuro-divergent |
|
|
103
|
+
| `qudag` | β **531** | post-quantum crypto core, exchange, MCP |
|
|
104
|
+
| `daa` | β **266** | orchestrator, economy, rules, swarm |
|
|
105
|
+
| `safla` | 0 β **136** | the metacognitive / self-modification layer |
|
|
106
|
+
|
|
107
|
+
Plus: the **βtake the wheelβ behavioral pipeline** (below), a **4-level behavioral test harness**, a build-integrity fix (empty files no longer pollute the index), and the private-store fence that keeps unpublished repos out of the public bundle.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
The expensive work happens **once, at build time**: every covered repo is deep-walked (whole files, full function bodies, plus a symbol index), embedded into **two** vector variants (MiniLM-384 for edge/portability, bge-768 for depth) stored on-disk in **RVF / HNSW**, and distilled into a concepts + capability layer of per-repo primers and cards. That's **90,842 source chunks**. At **query time**, `search_ruvnet` searches every repo's store at once, pools the hits, and runs them through **one cross-encoder rerank** on a common scale β so the truly relevant file wins regardless of which repo it lives in β then returns whole source files, each labeled by repo and path.
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
- **Per-repo RVF stores** β each repo gets its own HNSW graph; the tool queries across them and normalizes.
|
|
118
|
+
- **Dual embeddings + cross-encoder** β MiniLM-384 for edge/portability, bge-768 for depth; a single cross-encoder rerank puts every candidate on one comparable scale. (The cross-encoder β a third model reading query+passage together β is the real quality lever, not a fusion of the two embedders.)
|
|
119
|
+
- **Concepts / capability layer** β per-repo primers and capability cards let the model ground _capability_ claims and route a described need to the right repo, not just do file lookups.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## How it changes Claude's behavior β it takes the wheel
|
|
124
|
+
|
|
125
|
+
Grounding is **enforced, not suggested.** On a RuvNet-relevant prompt the `UserPromptSubmit` hook injects a directive into context; Claude calls `search_ruvnet`, gets whole source files labeled by repo and path, and answers _from_ them. Because the hook's stdout is consumed by the harness every turn, it's structural β Claude can't quietly skip it.
|
|
126
|
+
|
|
127
|
+

|
|
128
|
+
|
|
129
|
+
And on a **build request**, the brain doesn't wait to be told each step β it runs the process **the way Ruv would**:
|
|
130
|
+
|
|
131
|
+
> **Propose the architecture first** β one go/no-go β then execute end-to-end: **SPARC** (Spec β Pseudocode β Architecture β Refinement β Completion) Β· model the domain (**DDD**) and capture **ADRs** Β· spin up **parallel Ruflo swarms** Β· persist decisions to **AgentDB** Β· treat design as a build step (**frontend-design + AI image generation**, never βworking but uglyβ) Β· **test β score 1β100 β loop to β₯98** Β· and it asks for an **API key** when a step needs one instead of silently skipping it.
|
|
132
|
+
|
|
133
|
+
The downstream effect: Claude **prefers RuvNet building blocks over generic defaults** (RVF/HNSW over pgvector/Pinecone, Ruflo swarms over hand-rolled orchestration) and drives the whole assess β build β verify β score loop rather than answering one question at a time.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Capability-confidence routing
|
|
138
|
+
|
|
139
|
+
The brain answers **both** kinds of questions. **Name the repo or ask something specific** and it resolves to the right repo (**47/48, 98%**). **Describe a _need_ without naming the repo** β the way a newcomer would β and it still lands the right repo (**26/28, 93%**). That newcomer path used to be the weak spot (**33% before the fix**); adding **capability cards** β a capability-phrased passage per building block β closed the gap.
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## What it covers
|
|
146
|
+
|
|
147
|
+
~21 of rUv's **building-block** repos in the [ruvnet](https://github.com/ruvnet) org β the reusable pieces you'd actually compose into a system β each deep-walked, embedded in both variants, symbol-indexed, and given a capability card.
|
|
148
|
+
|
|
149
|
+

|
|
150
|
+
|
|
151
|
+
| Repo | What it gives you | Repo | What it gives you |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| [`ruflo`](https://github.com/ruvnet/ruflo) | Agent orchestration / swarms | [`ruvector`](https://github.com/ruvnet/ruvector) | RVF + on-disk HNSW vectors |
|
|
154
|
+
| [`agentdb`](https://github.com/ruvnet/agentdb) | Agent memory + graph/Cypher | [`rulake`](https://github.com/ruvnet/rulake) | Vector cache layer |
|
|
155
|
+
| [`ruview`](https://github.com/ruvnet/ruview) | Camera-free WiFi/CSI sensing (presence, pose, vitals) | [`agentic-flow`](https://github.com/ruvnet/agentic-flow) | Cheap multi-provider model routing |
|
|
156
|
+
| [`agenticow`](https://github.com/ruvnet/agenticow) | Copy-on-write agent memory (branch 1M vectors in ~162 B) | [`sparc`](https://github.com/ruvnet/sparc) | 5-phase build methodology |
|
|
157
|
+
| [`qudag`](https://github.com/ruvnet/qudag) | Quantum-resistant DAG messaging | [`safla`](https://github.com/ruvnet/safla) | Self-aware feedback loop |
|
|
158
|
+
| [`ruv-fann`](https://github.com/ruvnet/ruv-FANN) | Fast neural nets (Rust/WASM) + ruv-swarm | [`daa`](https://github.com/ruvnet/daa) | Decentralized autonomous agents |
|
|
159
|
+
| [`synthlang`](https://github.com/ruvnet/synthlang) | Prompt compression (~75% token cut) | [`rupixel`](https://github.com/ruvnet/rupixel) | On-device visual embeddings |
|
|
160
|
+
| [`dspy.ts`](https://github.com/ruvnet/dspy.ts) | DSPy-style programmable LLM pipelines in TS | [`fact`](https://github.com/ruvnet/fact) | Fast-Access Cached Tools + circuit breaker |
|
|
161
|
+
| [`cve-bench`](https://github.com/ruvnet/cve-bench) | Security-fix benchmark | [`agent-harness-generator`](https://github.com/ruvnet/agent-harness-generator) | Harness scaffolding / metaharness |
|
|
162
|
+
| [`rvm`](https://github.com/ruvnet/rvm) | Proof-gated capability microhypervisor | [`rUv-dev`](https://github.com/ruvnet/rUv-dev) Β· [`open-claude-code`](https://github.com/ruvnet/open-claude-code) | Dev workflow + agent tooling |
|
|
163
|
+
|
|
164
|
+
> **Not in the public brain:** rUv's private **Cognitum One** repos (seed, v0-appliance, platform-docs) are fenced out of the download by design β verified zero-leak in every build. **Helix** (rUv's local-first health app) is a finished product, not a building block, so it's out too.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Testing & proof
|
|
169
|
+
|
|
170
|
+
Everything below is **re-runnable** β the proof is the output of a command, not a claim.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bash scripts/gate.sh # the pass/fail routing gate
|
|
174
|
+
node scripts/behavioral-l1-l4.mjs # the 4-level behavioral harness
|
|
175
|
+
node plugin/test/run-tests.mjs # full plugin QA over real JSON-RPC
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Test | Result | What it proves |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| **Named / specific routing** | **47 / 48 (98%)** | name the tool β right repo |
|
|
181
|
+
| **Described-need routing** | **26 / 28 (93%)** | describe the need, no name β right repo (was 33% before capability cards) |
|
|
182
|
+
| **Context-scenario routing** | **7 / 8 (88%)** | full-scenario prompts route correctly |
|
|
183
|
+
| **L1βL4 behavioral harness** | **all pass** | route Β· deep-recall (returns _code_) Β· implement (cites the API) Β· orchestrate (the hook drives the full pipeline) |
|
|
184
|
+
| **Plugin QA** | **26 / 26** | manifests, hook firing, MCP `initialize`/`tools/list`, capability battery |
|
|
185
|
+
| **Clean-room install** | **3 / 3** | download the published 512 MB bundle fresh β unzip β query β grounded, cited answers |
|
|
186
|
+
|
|
187
|
+
Two honest residuals, not hidden: one described question (_βroute to cheaper models to cut costβ_) still leans `ruflo` over `agentic-flow` (orchestration/cost overlap); one unnamed _βmethodologyβ_ question routes to `synthlang` instead of `sparc`. Proof reports land in [`PROOF.md`](PROOF.md), [`DESCRIBED-PROOF.md`](DESCRIBED-PROOF.md), and [`HELIX-DEMO-NOHELIX.md`](HELIX-DEMO-NOHELIX.md).
|
|
188
|
+
|
|
189
|
+
**Query it directly (CLI):**
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
cd kb
|
|
193
|
+
export KB_MODEL_CACHE=/path/to/models-cache
|
|
194
|
+
node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector search?" --k 3
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> Cross-repo answer quality **requires the cross-encoder**, which requires `npm i` in the bundle. Without it, search falls back to raw vectors and ranks poorly.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Honest status
|
|
202
|
+
|
|
203
|
+
This is **`v0.5.0-dev`** β we don't claim βdone,β βcomplete,β or βzero hallucinations.β Where it stands:
|
|
204
|
+
|
|
205
|
+
- β
**The grounding brain is real and proven** β ~21 building-block repos, 90,842 chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
|
|
206
|
+
- β
**Code-level depth** β the code-rich repos are indexed to full function bodies; βhow is it implemented?β returns the implementation. Verified in the shipped bundle (clean-room 3/3).
|
|
207
|
+
- β
**Routing holds** β named 47/48, described 26/28, scenario 7/8; behavioral L1βL4 all pass; private stores fenced out of the public bundle (zero-leak verified).
|
|
208
|
+
- β οΈ **Two routing residuals** (above) β surfaced, not hidden.
|
|
209
|
+
- β³ **Not yet a public one-line npm install** β `npx github:stuinfla/ruvnet-brain` is the works-now path; `npx ruvnet-brain` comes with the npm publish.
|
|
210
|
+
- β³ **The fully-autonomous engineering loop** ([ADR-0008](docs/adr/)) β the behavioral hook _drives_ the assess β SPARC β ADR/DDD β QA β score loop; a generalized always-loops-to-β₯98 engine is the next phase.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## What's in the box
|
|
215
|
+
|
|
216
|
+
- `kb/` β the brain: per-repo `.rvf` + `.big.rvf` stores, full-passage sidecars, symbol indexes, per-repo primers, the concepts store, and the `forge-*` query tools (CLI + MCP `search_ruvnet`).
|
|
217
|
+
- `plugin/` β the Claude Code plugin (MCP server, grounding skill, `UserPromptSubmit` enforcement hook, marketplace manifest, test suite).
|
|
218
|
+
- `scripts/` β `gate.sh` (routing gate), `behavioral-l1-l4.mjs` (behavioral harness), `prove.mjs`, `build-bundle.mjs`, `brain-stamp.mjs`.
|
|
219
|
+
- `docs/` β [`VISION.md`](docs/VISION.md) (the why), [`adr/`](docs/adr/) (locked decisions incl. ADR-0008), [`DDD.md`](docs/DDD.md).
|
|
220
|
+
- `explainer/` β the source of the [live explainer](https://ruvnet-brain.vercel.app).
|
|
221
|
+
- `SPEC.md` Β· `PROGRESS.md` β the master spec and the living, timestamped build log.
|
|
222
|
+
|
|
223
|
+
The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev), not git β a fresh clone is lightweight; `npx` fetches the 512 MB bundle.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Links
|
|
228
|
+
|
|
229
|
+
- **βΆ Live explainer:** https://ruvnet-brain.vercel.app
|
|
230
|
+
- **Download / Release:** https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev
|
|
231
|
+
- **rUv's RuvNet org:** https://github.com/ruvnet
|
|
232
|
+
- **Built by:** [Stuart Kerr β Isovision.ai](https://isovision.ai)
|
|
233
|
+
|
|
234
|
+
<div align="center"><sub>MIT licensed Β· free & fair use Β· grounded in rUv's real source, cited every time.</sub></div>
|
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/install.mjs β the one-command installer for RuvNet Brain.
|
|
3
|
+
//
|
|
4
|
+
// npx github:stuinfla/ruvnet-brain # works today (fetches the brain from the Release)
|
|
5
|
+
// npx ruvnet-brain # once published to npm
|
|
6
|
+
// node bin/install.mjs --local # from a repo clone that already has dist/ruvnet-brain.zip
|
|
7
|
+
//
|
|
8
|
+
// Goal: a newcomer runs ONE command and ends up with (a) the brain on disk and (b) the Claude Code
|
|
9
|
+
// plugin wired at user scope β narrating "what I'm doing and why" at every step (the product's ethos).
|
|
10
|
+
//
|
|
11
|
+
// Design rules: dependency-free (Node built-ins + shelling to unzip/npm/claude only), idempotent
|
|
12
|
+
// (safe to re-run), and never a silent half-state (every failure explains the next step).
|
|
13
|
+
|
|
14
|
+
import https from 'node:https';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import readline from 'node:readline';
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
24
|
+
|
|
25
|
+
const REPO = 'stuinfla/ruvnet-brain';
|
|
26
|
+
const RELEASE_API = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
27
|
+
const ASSET_NAME = 'ruvnet-brain.zip';
|
|
28
|
+
// Known-good fallback used when we can't reach GitHub (offline / rate-limited / no releases).
|
|
29
|
+
// Default behavior is "get the latest"; this is only the safety net.
|
|
30
|
+
const RELEASE_VERSION = 'v0.5.0-dev';
|
|
31
|
+
const fallbackUrl = (tag) => `https://github.com/${REPO}/releases/download/${tag}/${ASSET_NAME}`;
|
|
32
|
+
const APPROX_SIZE = '~512MB';
|
|
33
|
+
|
|
34
|
+
const argv = process.argv.slice(2);
|
|
35
|
+
const FLAG_LOCAL = argv.includes('--local');
|
|
36
|
+
const FLAG_FORCE = argv.includes('--force');
|
|
37
|
+
const FLAG_HELP = argv.includes('--help') || argv.includes('-h');
|
|
38
|
+
const FLAG_DOCTOR = argv.includes('--doctor');
|
|
39
|
+
const FLAG_NO_VERIFY = argv.includes('--no-verify');
|
|
40
|
+
const FLAG_PIN = argv.includes('--pin'); // skip the latest-check, use the bundled default
|
|
41
|
+
const FLAG_DEMO = argv.includes('--demo'); // guided, real (non-fabricated) walkthrough of the brain in action
|
|
42
|
+
// ββ onboarding-experience flags (all optional; every offer is safe to decline) ββ
|
|
43
|
+
const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
|
|
44
|
+
const FLAG_WITH_STACK = argv.includes('--with-stack'); // add missing Ruflo/RuVector without prompting
|
|
45
|
+
const FLAG_NO_STACK = argv.includes('--no-stack'); // skip the toolkit offer entirely
|
|
46
|
+
const FLAG_ENHANCE_CLAUDE_MD = argv.includes('--enhance-claude-md'); // add the CLAUDE.md section without prompting
|
|
47
|
+
const FLAG_NO_ENHANCE = argv.includes('--no-enhance'); // skip the CLAUDE.md offer entirely
|
|
48
|
+
// --version <tag> forces a specific Release tag (e.g. --version v0.4.0-dev)
|
|
49
|
+
const versionIdx = argv.indexOf('--version');
|
|
50
|
+
const FORCED_VERSION =
|
|
51
|
+
versionIdx !== -1 && argv[versionIdx + 1] && !argv[versionIdx + 1].startsWith('-')
|
|
52
|
+
? argv[versionIdx + 1]
|
|
53
|
+
: null;
|
|
54
|
+
|
|
55
|
+
// ββ tiny narrating logger β every step says WHAT and WHY βββββββββββββββββββββββββββββββββββββββββ
|
|
56
|
+
const c = {
|
|
57
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
58
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
59
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
60
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
61
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
62
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
63
|
+
};
|
|
64
|
+
let stepNo = 0;
|
|
65
|
+
function step(what, why) {
|
|
66
|
+
stepNo += 1;
|
|
67
|
+
console.log(`\n${c.cyan(`[${stepNo}]`)} ${c.bold(what)}`);
|
|
68
|
+
if (why) console.log(` ${c.dim('why: ' + why)}`);
|
|
69
|
+
}
|
|
70
|
+
const info = (s) => console.log(` ${s}`);
|
|
71
|
+
const ok = (s) => console.log(` ${c.green('β')} ${s}`);
|
|
72
|
+
const warn = (s) => console.log(` ${c.yellow('!')} ${s}`);
|
|
73
|
+
|
|
74
|
+
// Prints an unmistakable "this is RuvNet Brain, and it's running right now" banner β so a user
|
|
75
|
+
// never has to wonder whether the right tool is doing the work. Used by the installer, --doctor,
|
|
76
|
+
// and --demo alike, so every entry point identifies itself the same way.
|
|
77
|
+
function printBanner(subtitle) {
|
|
78
|
+
const line = 'β'.repeat(64);
|
|
79
|
+
console.log(`\n${c.cyan(line)}`);
|
|
80
|
+
console.log(` π§ ${c.bold(c.cyan('RuvNet Brain'))} ${c.dim('β')} ${c.bold(subtitle)}`);
|
|
81
|
+
console.log(`${c.cyan(line)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function die(msg, hint) {
|
|
85
|
+
console.error(`\n${c.red('β install stopped:')} ${msg}`);
|
|
86
|
+
if (hint) console.error(`\n${hint}`);
|
|
87
|
+
console.error(
|
|
88
|
+
`\nNothing is left half-installed β fix the above and re-run the same command (it's safe to re-run).`,
|
|
89
|
+
);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ββ shell helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
94
|
+
// On Windows, npm/claude/claude-flow/ruflo etc. are `.cmd` shims, not `.exe` binaries. Node's
|
|
95
|
+
// spawnSync uses CreateProcess directly (no shell:true) which CANNOT launch .cmd/.bat files β it
|
|
96
|
+
// fails with ENOENT even when the command works fine in any real terminal. shell:true routes the
|
|
97
|
+
// call through cmd.exe, which resolves .cmd shims the same way an interactive shell would.
|
|
98
|
+
const IS_WIN = process.platform === 'win32';
|
|
99
|
+
// Existence check via the OS's own PATH resolver β `where` on Windows, POSIX `command -v` elsewhere β
|
|
100
|
+
// NEVER by invoking the tool itself. Probing with `<cmd> --version` (the previous approach) is
|
|
101
|
+
// fundamentally fragile: plenty of real, correctly-installed tools don't support that exact flag and
|
|
102
|
+
// exit non-zero β e.g. Info-ZIP unzip exits 10 on `--version` (verified: this happens even on macOS's
|
|
103
|
+
// own bundled unzip, not just Debian's β same codebase), and Windows PowerShell 5.1's powershell.exe
|
|
104
|
+
// parses `--version` as a PowerShell expression and exits 1. Both read as "not installed" when the
|
|
105
|
+
// tool plainly is. Asking the OS "is this on PATH?" sidesteps every tool's own argument parsing.
|
|
106
|
+
function have(cmd) {
|
|
107
|
+
const probe = IS_WIN
|
|
108
|
+
? spawnSync('where', [cmd], { stdio: 'ignore' })
|
|
109
|
+
: spawnSync('sh', ['-c', `command -v -- ${cmd}`], { stdio: 'ignore' });
|
|
110
|
+
return !probe.error && probe.status === 0;
|
|
111
|
+
}
|
|
112
|
+
function run(cmd, args, opts = {}) {
|
|
113
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit', shell: IS_WIN, ...opts });
|
|
114
|
+
if (r.error) throw r.error;
|
|
115
|
+
if (r.status !== 0) throw new Error(`\`${cmd} ${args.join(' ')}\` exited with code ${r.status}`);
|
|
116
|
+
}
|
|
117
|
+
function tryRun(cmd, args, opts = {}) {
|
|
118
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit', shell: IS_WIN, ...opts });
|
|
119
|
+
return !r.error && r.status === 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ββ download with redirect-following + progress ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
123
|
+
function download(url, dest, redirects = 0) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
if (redirects > 10) return reject(new Error('too many redirects'));
|
|
126
|
+
const req = https.get(
|
|
127
|
+
url,
|
|
128
|
+
{ headers: { 'User-Agent': 'ruvnet-brain-installer', Accept: 'application/octet-stream' } },
|
|
129
|
+
(res) => {
|
|
130
|
+
const { statusCode = 0, headers } = res;
|
|
131
|
+
if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location) {
|
|
132
|
+
res.resume(); // drain so the socket frees up
|
|
133
|
+
const next = new URL(headers.location, url).toString();
|
|
134
|
+
return resolve(download(next, dest, redirects + 1));
|
|
135
|
+
}
|
|
136
|
+
if (statusCode !== 200) {
|
|
137
|
+
res.resume();
|
|
138
|
+
return reject(new Error(`server returned HTTP ${statusCode}`));
|
|
139
|
+
}
|
|
140
|
+
const total = Number(headers['content-length'] || 0);
|
|
141
|
+
let received = 0;
|
|
142
|
+
let lastShown = -1;
|
|
143
|
+
const out = fs.createWriteStream(dest);
|
|
144
|
+
res.on('data', (chunk) => {
|
|
145
|
+
received += chunk.length;
|
|
146
|
+
const mb = (received / 1e6).toFixed(0);
|
|
147
|
+
if (total) {
|
|
148
|
+
const pct = Math.floor((received / total) * 100);
|
|
149
|
+
if (pct !== lastShown && pct % 5 === 0) {
|
|
150
|
+
process.stdout.write(`\r β¦${pct}% (${mb}MB / ${(total / 1e6).toFixed(0)}MB)`);
|
|
151
|
+
lastShown = pct;
|
|
152
|
+
}
|
|
153
|
+
} else if (mb % 20 === 0 && Number(mb) !== lastShown) {
|
|
154
|
+
process.stdout.write(`\r β¦${mb}MB`);
|
|
155
|
+
lastShown = Number(mb);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
res.pipe(out);
|
|
159
|
+
out.on('finish', () => out.close(() => {
|
|
160
|
+
process.stdout.write('\n');
|
|
161
|
+
resolve();
|
|
162
|
+
}));
|
|
163
|
+
out.on('error', (e) => reject(e));
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
req.on('error', (e) => reject(e));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ββ fetch a small JSON payload (redirect-following, dependency-free) ββββββββββββββββββββββββββββββ
|
|
171
|
+
function fetchJson(url, redirects = 0) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
if (redirects > 10) return reject(new Error('too many redirects'));
|
|
174
|
+
const req = https.get(
|
|
175
|
+
url,
|
|
176
|
+
{
|
|
177
|
+
headers: {
|
|
178
|
+
// GitHub's API requires a User-Agent; the Accept header pins the v3 JSON schema.
|
|
179
|
+
'User-Agent': 'ruvnet-brain-installer',
|
|
180
|
+
Accept: 'application/vnd.github+json',
|
|
181
|
+
},
|
|
182
|
+
timeout: 15000,
|
|
183
|
+
},
|
|
184
|
+
(res) => {
|
|
185
|
+
const { statusCode = 0, headers } = res;
|
|
186
|
+
if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location) {
|
|
187
|
+
res.resume();
|
|
188
|
+
const next = new URL(headers.location, url).toString();
|
|
189
|
+
return resolve(fetchJson(next, redirects + 1));
|
|
190
|
+
}
|
|
191
|
+
if (statusCode !== 200) {
|
|
192
|
+
res.resume();
|
|
193
|
+
return reject(new Error(`GitHub API returned HTTP ${statusCode}`));
|
|
194
|
+
}
|
|
195
|
+
let body = '';
|
|
196
|
+
res.setEncoding('utf8');
|
|
197
|
+
res.on('data', (chunk) => (body += chunk));
|
|
198
|
+
res.on('end', () => {
|
|
199
|
+
try {
|
|
200
|
+
resolve(JSON.parse(body));
|
|
201
|
+
} catch (e) {
|
|
202
|
+
reject(new Error(`GitHub API response was not valid JSON: ${e.message}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
req.on('timeout', () => req.destroy(new Error('GitHub API request timed out')));
|
|
208
|
+
req.on('error', (e) => reject(e));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ββ step: resolve which Release to download (latest by default; safe fallback) βββββββββββββββββββ
|
|
213
|
+
// Default behavior: ask GitHub for the LATEST Release and use its ruvnet-brain.zip asset.
|
|
214
|
+
// --version <tag> forces a tag; --pin skips the network check and uses the bundled known-good tag.
|
|
215
|
+
// Any failure (offline / rate-limited / no releases) FALLS BACK to the pinned known-good Release,
|
|
216
|
+
// narrated clearly so the user knows exactly what happened.
|
|
217
|
+
async function resolveRelease() {
|
|
218
|
+
step(
|
|
219
|
+
'Finding the latest brain to install',
|
|
220
|
+
'so a stranger always gets the most current brain β not whatever was hardcoded when this script shipped',
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (FLAG_PIN) {
|
|
224
|
+
info(`--pin set: skipping the latest-check and using the bundled known-good ${c.bold(RELEASE_VERSION)}`);
|
|
225
|
+
return { tag: RELEASE_VERSION, url: fallbackUrl(RELEASE_VERSION), source: 'pinned' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (FORCED_VERSION) {
|
|
229
|
+
info(`--version set: forcing Release ${c.bold(FORCED_VERSION)} (no latest-check)`);
|
|
230
|
+
return { tag: FORCED_VERSION, url: fallbackUrl(FORCED_VERSION), source: 'forced' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
info(`checking ${RELEASE_API} β¦`);
|
|
235
|
+
const rel = await fetchJson(RELEASE_API);
|
|
236
|
+
const tag = rel && rel.tag_name;
|
|
237
|
+
if (!tag) throw new Error('latest Release has no tag_name');
|
|
238
|
+
const asset = Array.isArray(rel.assets) ? rel.assets.find((a) => a.name === ASSET_NAME) : null;
|
|
239
|
+
const url = asset && asset.browser_download_url ? asset.browser_download_url : fallbackUrl(tag);
|
|
240
|
+
if (!asset) {
|
|
241
|
+
warn(`latest Release ${tag} has no ${ASSET_NAME} asset listed β using the conventional download URL`);
|
|
242
|
+
}
|
|
243
|
+
ok(`latest Release is ${c.bold(tag)}`);
|
|
244
|
+
return { tag, url, source: 'latest' };
|
|
245
|
+
} catch (e) {
|
|
246
|
+
warn(`couldn't check for the latest version (${e.message})`);
|
|
247
|
+
info(`using the known-good ${c.bold(RELEASE_VERSION)} instead β the install is still safe and complete`);
|
|
248
|
+
return { tag: RELEASE_VERSION, url: fallbackUrl(RELEASE_VERSION), source: 'fallback' };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ββ step: resolve the cache dir ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
253
|
+
function resolveCacheDir() {
|
|
254
|
+
const custom = process.env.RUVNET_BRAIN_KB;
|
|
255
|
+
const cacheDir = custom || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
256
|
+
step(
|
|
257
|
+
'Choosing where the brain will live',
|
|
258
|
+
'the Claude Code plugin looks here by default, so put it where it expects',
|
|
259
|
+
);
|
|
260
|
+
info(`brain dir: ${c.bold(cacheDir)}`);
|
|
261
|
+
if (custom) info(`(from your RUVNET_BRAIN_KB override)`);
|
|
262
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
263
|
+
return { cacheDir, isCustom: Boolean(custom) };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ββ step: obtain the bundle (local or download) ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
267
|
+
async function obtainBundle(release) {
|
|
268
|
+
const localZip = path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip');
|
|
269
|
+
const haveLocal = fs.existsSync(localZip);
|
|
270
|
+
|
|
271
|
+
if (FLAG_LOCAL && !haveLocal) {
|
|
272
|
+
die(
|
|
273
|
+
`--local was passed but ${localZip} does not exist.`,
|
|
274
|
+
`Build it first with: ${c.bold('node scripts/build-bundle.mjs')} (then re-run with --local),\nor drop --local to download the published brain instead.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (haveLocal || FLAG_LOCAL) {
|
|
279
|
+
step('Using the local brain bundle', 'you are running from the repo, so no download is needed');
|
|
280
|
+
info(`source: ${localZip}`);
|
|
281
|
+
return { zipPath: localZip, downloaded: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const downloadUrl = (release && release.url) || fallbackUrl(RELEASE_VERSION);
|
|
285
|
+
step(
|
|
286
|
+
`Downloading the brain (${APPROX_SIZE})`,
|
|
287
|
+
'the brain is the embedded source of ~18 RuvNet repos β too big for git, so it ships as a Release',
|
|
288
|
+
);
|
|
289
|
+
info(`version: ${c.bold((release && release.tag) || RELEASE_VERSION)}`);
|
|
290
|
+
info(`from: ${downloadUrl}`);
|
|
291
|
+
const tmp = path.join(os.tmpdir(), `ruvnet-brain-${process.pid}.zip`);
|
|
292
|
+
try {
|
|
293
|
+
console.log(` downloading the brain (${APPROX_SIZE})β¦`);
|
|
294
|
+
await download(downloadUrl, tmp);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
297
|
+
die(
|
|
298
|
+
`couldn't download the brain (${e.message}).`,
|
|
299
|
+
`Check your connection, then re-run. Or, if you have a repo clone, build the bundle locally\n(${c.bold('node scripts/build-bundle.mjs')}) and run ${c.bold('node bin/install.mjs --local')}.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
ok(`downloaded to ${tmp}`);
|
|
303
|
+
return { zipPath: tmp, downloaded: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ββ step: unzip into the cache dir (flattening the top-level ruvnet-brain/ folder) βββββββββββββββ
|
|
307
|
+
function unzipInto(zipPath, cacheDir) {
|
|
308
|
+
step(
|
|
309
|
+
'Unpacking the brain into place',
|
|
310
|
+
'so the plugin finds forge-mcp-all.mjs and the vector stores right where it looks',
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const hasUnzip = have('unzip');
|
|
314
|
+
// Windows fallback: PowerShell's Expand-Archive is available on all modern Windows systems.
|
|
315
|
+
const psExe = !hasUnzip ? (['pwsh', 'powershell'].find(have) || null) : null;
|
|
316
|
+
|
|
317
|
+
if (!hasUnzip && !psExe) {
|
|
318
|
+
die(
|
|
319
|
+
`no zip extraction tool is available on this machine.`,
|
|
320
|
+
[
|
|
321
|
+
`Install one and re-run:`,
|
|
322
|
+
` β’ macOS: \`unzip\` is already built in β check your PATH`,
|
|
323
|
+
` β’ Debian/Ubuntu: ${c.bold('sudo apt-get install -y unzip')}`,
|
|
324
|
+
` β’ Fedora/RHEL: ${c.bold('sudo dnf install -y unzip')}`,
|
|
325
|
+
` β’ Windows: open a PowerShell window and re-run (Expand-Archive is built in)`,
|
|
326
|
+
].join('\n'),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The zip extracts to a top-level `ruvnet-brain/` folder. Extract into the cache dir, then lift
|
|
331
|
+
// its CONTENTS up one level so that cacheDir/forge-mcp-all.mjs exists (idempotent: -o overwrites).
|
|
332
|
+
try {
|
|
333
|
+
if (hasUnzip) {
|
|
334
|
+
run('unzip', ['-q', '-o', zipPath, '-d', cacheDir]);
|
|
335
|
+
} else {
|
|
336
|
+
// Windows: PowerShell's Expand-Archive handles .zip natively with -Force for overwrite.
|
|
337
|
+
// shell:false here (pwsh/powershell are real .exe files, not .cmd shims) β routing this
|
|
338
|
+
// through cmd.exe would re-tokenize the already-quoted -Command string and break it.
|
|
339
|
+
// -ExecutionPolicy Bypass: Expand-Archive ships as a script module (.psm1); on a locked-down
|
|
340
|
+
// machine (Restricted/AllSigned policy β common in sandboxes) importing it fails with
|
|
341
|
+
// "running scripts is disabled on this system" even though the exe itself runs fine. Bypass
|
|
342
|
+
// only affects this one child process, not any persistent machine setting.
|
|
343
|
+
run(psExe, [
|
|
344
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command',
|
|
345
|
+
`Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${cacheDir}" -Force`,
|
|
346
|
+
], { shell: false });
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {
|
|
349
|
+
die(`extraction failed (${e.message}).`, `The archive may be incomplete β re-run to download a fresh copy.`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const nested = path.join(cacheDir, 'ruvnet-brain');
|
|
353
|
+
if (fs.existsSync(path.join(nested, 'forge-mcp-all.mjs'))) {
|
|
354
|
+
for (const entry of fs.readdirSync(nested)) {
|
|
355
|
+
const from = path.join(nested, entry);
|
|
356
|
+
const to = path.join(cacheDir, entry);
|
|
357
|
+
fs.rmSync(to, { recursive: true, force: true }); // idempotent overwrite
|
|
358
|
+
fs.renameSync(from, to); // same filesystem β cheap rename
|
|
359
|
+
}
|
|
360
|
+
fs.rmdirSync(nested);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs'))) {
|
|
364
|
+
die(
|
|
365
|
+
`the brain unpacked but forge-mcp-all.mjs is missing from ${cacheDir}.`,
|
|
366
|
+
`The archive layout may have changed. Re-run, or report this at https://github.com/stuinfla/ruvnet-brain/issues`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
ok(`brain unpacked to ${cacheDir}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ββ step: install the reader deps ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
373
|
+
function installReader(cacheDir) {
|
|
374
|
+
step(
|
|
375
|
+
'Installing the local reader',
|
|
376
|
+
'the brain reads its vectors with @ruvector/rvf and reranks with a local model β no cloud calls',
|
|
377
|
+
);
|
|
378
|
+
if (!have('npm')) {
|
|
379
|
+
die(`\`npm\` isn't available, but the brain needs it for its reader.`, `Install Node.js (which includes npm) and re-run.`);
|
|
380
|
+
}
|
|
381
|
+
info('installing the local readerβ¦');
|
|
382
|
+
try {
|
|
383
|
+
run('npm', ['i', '--no-audit', '--no-fund', '--loglevel=error'], {
|
|
384
|
+
cwd: cacheDir,
|
|
385
|
+
// silence npm's "new version available" update-notifier so the narration stays clean
|
|
386
|
+
env: { ...process.env, npm_config_update_notifier: 'false', npm_config_fund: 'false' },
|
|
387
|
+
});
|
|
388
|
+
} catch (e) {
|
|
389
|
+
die(`the reader install failed (${e.message}).`, `Re-run after checking your network / npm setup.`);
|
|
390
|
+
}
|
|
391
|
+
ok('reader installed');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ββ step: wire the Claude Code plugin ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
395
|
+
function wirePlugin() {
|
|
396
|
+
step(
|
|
397
|
+
'Wiring the Claude Code plugin',
|
|
398
|
+
'this registers search_ruvnet + the grounding hook so Claude uses the brain automatically',
|
|
399
|
+
);
|
|
400
|
+
const manualMarketplace = 'claude plugin marketplace add stuinfla/ruvnet-brain';
|
|
401
|
+
const manualInstall = 'claude plugin install ruvnet-brain@ruvnet-brain --scope user';
|
|
402
|
+
|
|
403
|
+
if (!have('claude')) {
|
|
404
|
+
warn(`I couldn't run the \`claude\` command from this shell.`);
|
|
405
|
+
info(`That's normal if you use Claude Code as the ${c.bold('VS Code extension')} or ${c.bold('desktop app')} β the`);
|
|
406
|
+
info(`command just isn't on your terminal's PATH. ${c.green('The brain itself is fully downloaded.')}`);
|
|
407
|
+
info(`Finish wiring in ~20s β paste these two into ${c.bold("Claude Code's integrated terminal")} (or any shell with \`claude\`):`);
|
|
408
|
+
info(` ${c.bold(manualMarketplace)}`);
|
|
409
|
+
info(` ${c.bold(manualInstall)}`);
|
|
410
|
+
info(`Then reopen Claude Code once. ${c.dim('Or simply open Claude Code and ask: βfinish setting up the RuvNet Brain.β')}`);
|
|
411
|
+
return { wired: false, manualMarketplace, manualInstall };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const addedMarket = tryRun('claude', ['plugin', 'marketplace', 'add', 'stuinfla/ruvnet-brain']);
|
|
415
|
+
if (!addedMarket) warn(`couldn't add the marketplace automatically (it may already be added β that's fine).`);
|
|
416
|
+
|
|
417
|
+
const installed = tryRun('claude', ['plugin', 'install', 'ruvnet-brain@ruvnet-brain', '--scope', 'user']);
|
|
418
|
+
if (installed) {
|
|
419
|
+
ok('plugin installed at user scope (global, alongside Ruflo / RuVector)');
|
|
420
|
+
return { wired: true, manualMarketplace, manualInstall };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
warn(`couldn't install the plugin automatically. Run these two commands yourself:`);
|
|
424
|
+
info(` ${c.bold(manualMarketplace)}`);
|
|
425
|
+
info(` ${c.bold(manualInstall)}`);
|
|
426
|
+
return { wired: false, manualMarketplace, manualInstall };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ββ step: verify the install is REAL (counts β never take "installed" on faith) ββββββββββββββββββ
|
|
430
|
+
function verifyInstall(cacheDir) {
|
|
431
|
+
step(
|
|
432
|
+
'Verifying the brain is real and reachable',
|
|
433
|
+
"you should never have to trust the word \"installed\" β here's the proof on disk",
|
|
434
|
+
);
|
|
435
|
+
let repos = 0;
|
|
436
|
+
try {
|
|
437
|
+
repos = fs
|
|
438
|
+
.readdirSync(cacheDir)
|
|
439
|
+
.filter((f) => f.endsWith('.rvf') && !f.endsWith('.big.rvf')).length;
|
|
440
|
+
} catch {
|
|
441
|
+
/* ignore */
|
|
442
|
+
}
|
|
443
|
+
if (repos > 0) ok(`${repos} RuvNet repos indexed (vector stores present on disk)`);
|
|
444
|
+
else warn(`no .rvf stores found in ${cacheDir} β the brain may be incomplete (re-run with --force)`);
|
|
445
|
+
|
|
446
|
+
const reader = fs.existsSync(path.join(cacheDir, 'node_modules'));
|
|
447
|
+
if (reader) ok('local reader installed (vector reads happen offline β no cloud, no API key)');
|
|
448
|
+
else warn('reader deps missing β re-run the installer');
|
|
449
|
+
|
|
450
|
+
const mcp = fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs'));
|
|
451
|
+
if (mcp) ok('search_ruvnet server present (this is what Claude calls to ground answers)');
|
|
452
|
+
else warn('forge-mcp-all.mjs missing β the brain unpacked incompletely');
|
|
453
|
+
|
|
454
|
+
return { repos, reader, mcp };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ββ step: warm the model + prove grounding with one real question (best-effort, never fatal) ββββββ
|
|
458
|
+
function smokeQuery(cacheDir) {
|
|
459
|
+
const ask = path.join(cacheDir, 'forge-ask-all.mjs');
|
|
460
|
+
if (!fs.existsSync(ask)) return { ran: false };
|
|
461
|
+
step(
|
|
462
|
+
'Asking the brain a real question',
|
|
463
|
+
'this warms the local model so your first real answer is instant β and proves grounding works end to end',
|
|
464
|
+
);
|
|
465
|
+
const Q = 'How should I store embeddings in this project without running a server?';
|
|
466
|
+
info(`Q: ${c.cyan(`"${Q}"`)}`);
|
|
467
|
+
info(c.dim('(first run downloads a small local model once β this can take a minute)'));
|
|
468
|
+
let r;
|
|
469
|
+
try {
|
|
470
|
+
// Relative filename + matching cwd (NOT the absolute `ask` path) β forge-ask-all.mjs only runs
|
|
471
|
+
// its CLI main() when `path.resolve(process.argv[1]) === path.resolve(__filename)`; passed as an
|
|
472
|
+
// absolute path via spawnSync (no shell involved) that identity check silently fails on this
|
|
473
|
+
// machine, so main() never runs β exit 0, zero stdout, zero stderr, no exception. Looks like a
|
|
474
|
+
// clean success; is actually a total no-op. Verified: switching to a relative name + cwd fixes it.
|
|
475
|
+
r = spawnSync('node', ['forge-ask-all.mjs', '--dir', cacheDir, '--q', Q, '--k', '1'], {
|
|
476
|
+
cwd: cacheDir,
|
|
477
|
+
encoding: 'utf8',
|
|
478
|
+
timeout: 240000,
|
|
479
|
+
env: process.env,
|
|
480
|
+
});
|
|
481
|
+
} catch {
|
|
482
|
+
warn("skipped the live test (couldn't launch the reader) β it'll warm on your first real question");
|
|
483
|
+
return { ran: false };
|
|
484
|
+
}
|
|
485
|
+
const out = `${r.stdout || ''}`;
|
|
486
|
+
if (r.status === 0 && /\brvf\b|ruvector|hnsw|single[- ]file|no server/i.test(out)) {
|
|
487
|
+
ok("the brain answered from rUv's real source β grounding confirmed β¦");
|
|
488
|
+
return { ran: true, grounded: true };
|
|
489
|
+
}
|
|
490
|
+
warn(
|
|
491
|
+
"skipped the live test (first-run model download or offline) β the brain is installed; it'll warm on your first real question",
|
|
492
|
+
);
|
|
493
|
+
return { ran: true, grounded: false };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ββ `--demo`: a guided, REAL walkthrough β proves grounding live, never fabricates output βββββββββ
|
|
497
|
+
const DEMO_QUESTIONS = [
|
|
498
|
+
{
|
|
499
|
+
q: 'How should I store embeddings in this project without running a server?',
|
|
500
|
+
why: 'shows the brain reaching for RuVector/RVF β a single local file β instead of Pinecone or pgvector',
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
q: 'How do I orchestrate multiple agents working on a task in parallel?',
|
|
504
|
+
why: 'shows it grounding in Ruflo (the real orchestration engine) instead of guessing at a generic pattern',
|
|
505
|
+
},
|
|
506
|
+
];
|
|
507
|
+
function runDemo() {
|
|
508
|
+
printBanner('demo');
|
|
509
|
+
const cacheDir = process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
510
|
+
const ask = path.join(cacheDir, 'forge-ask-all.mjs');
|
|
511
|
+
if (!fs.existsSync(ask)) {
|
|
512
|
+
warn(`no brain found at ${cacheDir}.`);
|
|
513
|
+
info(`Install it first: ${c.bold('npx github:stuinfla/ruvnet-brain')}`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
console.log(c.dim(`\nThis asks your installed brain ${DEMO_QUESTIONS.length} real questions and shows you the`));
|
|
517
|
+
console.log(c.dim(`actual, unedited answers β grounded in rUv's real source, cited by file path. Nothing here`));
|
|
518
|
+
console.log(c.dim(`is scripted or faked; it's the same brain your Claude Code sessions use.\n`));
|
|
519
|
+
|
|
520
|
+
for (const [i, { q, why }] of DEMO_QUESTIONS.entries()) {
|
|
521
|
+
step(`Question ${i + 1} of ${DEMO_QUESTIONS.length}`, why);
|
|
522
|
+
info(`${c.cyan('Q:')} "${q}"`);
|
|
523
|
+
let r;
|
|
524
|
+
try {
|
|
525
|
+
// Relative filename + matching cwd β see the identity-check note in smokeQuery() above.
|
|
526
|
+
r = spawnSync('node', ['forge-ask-all.mjs', '--dir', cacheDir, '--q', q, '--k', '1'], {
|
|
527
|
+
cwd: cacheDir, encoding: 'utf8', timeout: 150000, env: process.env,
|
|
528
|
+
});
|
|
529
|
+
} catch (e) {
|
|
530
|
+
warn(`couldn't run this question (${e.message}) β skipping`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const out = `${r.stdout || ''}`.trim();
|
|
534
|
+
if (r.status !== 0 || !out) {
|
|
535
|
+
warn(`no answer came back β the local model may still be warming up (run this again in a moment)`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
// Show the top hit's actual citation (repo/path/title + the start of its real cited text) β
|
|
539
|
+
// skip past the noisy per-repo-hit-count header so the meaningful, confidence-building part
|
|
540
|
+
// (WHICH file this came from, and its real words) isn't crowded out. This is a TRIM of the
|
|
541
|
+
// real output, never a rewrite of it.
|
|
542
|
+
const hitStart = out.indexOf('\n#1');
|
|
543
|
+
const firstResult = (hitStart >= 0 ? out.slice(hitStart + 1) : out).split(/\n={10,}\n/)[0];
|
|
544
|
+
const trimmed = firstResult.length > 500 ? `${firstResult.slice(0, 500)}\nβ¦` : firstResult;
|
|
545
|
+
console.log(`\n${c.dim(trimmed.split('\n').map((l) => ` ${l}`).join('\n'))}\n`);
|
|
546
|
+
ok('grounded in real source β cited above, not guessed');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
console.log(`\n${c.green('β'.repeat(64))}`);
|
|
550
|
+
console.log(` ${c.bold('That\'s it β no cloud calls, no API key, just your local brain.')}`);
|
|
551
|
+
console.log(`${c.green('β'.repeat(64))}`);
|
|
552
|
+
console.log(`\n Now try it for real: open Claude Code in any project and ask it something about`);
|
|
553
|
+
console.log(` RuVector, Ruflo, AgentDB, or SPARC β it'll ground the same way, automatically.`);
|
|
554
|
+
console.log(` Run this demo again any time: ${c.bold('npx github:stuinfla/ruvnet-brain --demo')}`);
|
|
555
|
+
console.log(` Full health check: ${c.bold('npx github:stuinfla/ruvnet-brain --doctor')}\n`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ββ `--doctor`: a standalone health check the user can run any time βββββββββββββββββββββββββββββββ
|
|
559
|
+
function doctor() {
|
|
560
|
+
printBanner('doctor');
|
|
561
|
+
console.log(c.dim('Checking every part of the install and reporting green/red.\n'));
|
|
562
|
+
const cacheDir = process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
563
|
+
info(`brain dir: ${c.bold(cacheDir)}`);
|
|
564
|
+
const present = fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs'));
|
|
565
|
+
if (!present) {
|
|
566
|
+
warn('brain not found here β run the installer first: npx github:stuinfla/ruvnet-brain');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
have('node') ? ok('node present') : warn('node missing');
|
|
570
|
+
have('npm') ? ok('npm present') : warn('npm missing');
|
|
571
|
+
have('claude') ? ok('claude CLI present') : warn('claude CLI missing (plugin wiring needs it)');
|
|
572
|
+
have('unzip') || have('pwsh') || have('powershell')
|
|
573
|
+
? ok('zip extraction available (unzip or PowerShell Expand-Archive)')
|
|
574
|
+
: warn('no zip tool found β unzip or PowerShell needed for re-install');
|
|
575
|
+
have('git')
|
|
576
|
+
? ok('git present (not required by this installer, but handy)')
|
|
577
|
+
: info('git not found β that\'s fine, this installer never needs it');
|
|
578
|
+
const env = detectEnvironment();
|
|
579
|
+
env.ruflo
|
|
580
|
+
? ok('Ruflo present β orchestration / swarms / SPARC available')
|
|
581
|
+
: warn('Ruflo not found β answers still work. To build: npm install -g claude-flow@alpha (or /plugin add ruvnet/claude-flow)');
|
|
582
|
+
env.ruvector
|
|
583
|
+
? ok('RuVector present β vector CLI / MCP available')
|
|
584
|
+
: warn('RuVector not found β answers still work. To add: claude mcp add ruvector --scope user -- npx -y ruvector mcp start');
|
|
585
|
+
const v = verifyInstall(cacheDir);
|
|
586
|
+
smokeQuery(cacheDir);
|
|
587
|
+
const allGreen = v.repos > 0 && v.reader && v.mcp;
|
|
588
|
+
console.log(
|
|
589
|
+
`\n ${allGreen ? c.green('β Healthy.') : c.yellow('! Needs attention.')} ${
|
|
590
|
+
allGreen ? 'The brain is installed and reachable.' : 'Re-run the installer to fix the warnings above.'
|
|
591
|
+
}`,
|
|
592
|
+
);
|
|
593
|
+
if (allGreen) {
|
|
594
|
+
console.log(`\n ${c.bold('What this means for you:')}`);
|
|
595
|
+
console.log(` β’ ${c.bold('It works in EVERY project')} β user-level (global). Open Claude Code in any repo or VS Code`);
|
|
596
|
+
console.log(` window and it's there. ${c.bold('No reinstall per project. No second download.')} One brain, shared.`);
|
|
597
|
+
console.log(` β’ ${c.bold('Nothing to git-ignore')} in your projects β it drops zero files into your working repos.`);
|
|
598
|
+
console.log(` β’ ${c.bold('To use it:')} just ask Claude about rUv's stack (RuVector, Ruflo, AgentDB, SPARCβ¦) β it`);
|
|
599
|
+
console.log(` grounds the answer automatically and takes the lead on builds. You don't invoke anything.`);
|
|
600
|
+
console.log(` β’ ${c.bold("To know it's on:")} a fresh session greets you with "π§ RuvNet Brain active". Or run this`);
|
|
601
|
+
console.log(` ${c.bold('--doctor')} command any time.`);
|
|
602
|
+
}
|
|
603
|
+
console.log(
|
|
604
|
+
c.dim('\n Heads-up: a window that was ALREADY open when you installed needs a restart to pick it up;\n newly-opened windows are fine.\n'),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ββ tiny interactive yes/no β SAFE in non-TTY (returns the default; never blocks a piped install) ββ
|
|
609
|
+
function ask(question, def = false) {
|
|
610
|
+
if (FLAG_YES) return Promise.resolve(true);
|
|
611
|
+
if (!process.stdin.isTTY) return Promise.resolve(def);
|
|
612
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
613
|
+
const suffix = def ? c.dim('[Y/n]') : c.dim('[y/N]');
|
|
614
|
+
return new Promise((resolve) => {
|
|
615
|
+
rl.question(` ${c.cyan('?')} ${question} ${suffix} `, (a) => {
|
|
616
|
+
rl.close();
|
|
617
|
+
const s = String(a).trim().toLowerCase();
|
|
618
|
+
if (s === '') return resolve(def);
|
|
619
|
+
resolve(s === 'y' || s === 'yes');
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// `claude mcp add <name> --scope user` writes to the top-level `mcpServers` object in ~/.claude.json
|
|
625
|
+
// (NOT ~/.claude/settings.json, and NOT the per-project `projects[cwd].mcpServers` used by the
|
|
626
|
+
// default "local" scope). Checking the wrong file/scope is why a successfully-added server can still
|
|
627
|
+
// show up as "not found" on the next run.
|
|
628
|
+
function hasUserScopeMcpServer(name) {
|
|
629
|
+
try {
|
|
630
|
+
const p = path.join(os.homedir(), '.claude.json');
|
|
631
|
+
if (!fs.existsSync(p)) return false;
|
|
632
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
633
|
+
return Boolean(j.mcpServers && Object.keys(j.mcpServers).some((k) => k.toLowerCase() === name.toLowerCase()));
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ββ detect the user's environment + rUv toolkit (never mutates anything) ββββββββββββββββββββββββββ
|
|
640
|
+
function detectEnvironment() {
|
|
641
|
+
const nodeMajor = (() => { const m = /^v(\d+)/.exec(process.version); return m ? Number(m[1]) : 0; })();
|
|
642
|
+
const env = {
|
|
643
|
+
node: process.version,
|
|
644
|
+
nodeOK: nodeMajor >= 18,
|
|
645
|
+
platform: process.platform,
|
|
646
|
+
arch: process.arch,
|
|
647
|
+
claude: have('claude'),
|
|
648
|
+
ruflo: have('ruflo') || have('claude-flow'),
|
|
649
|
+
ruvector: have('ruvector') || hasUserScopeMcpServer('ruvector'),
|
|
650
|
+
};
|
|
651
|
+
// Also honor a toolkit that's wired into the Claude config even if the CLI isn't on PATH (npx users).
|
|
652
|
+
try {
|
|
653
|
+
const settings = path.join(os.homedir(), '.claude', 'settings.json');
|
|
654
|
+
if (fs.existsSync(settings)) {
|
|
655
|
+
const s = fs.readFileSync(settings, 'utf8');
|
|
656
|
+
if (/claude-flow|\bruflo\b/i.test(s)) env.ruflo = true;
|
|
657
|
+
}
|
|
658
|
+
} catch { /* ignore β detection is best-effort */ }
|
|
659
|
+
return env;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ββ step: is the rUv toolkit here? the brain ANSWERS alone; it BUILDS best with Ruflo + RuVector ββ
|
|
663
|
+
async function offerStack(env) {
|
|
664
|
+
step(
|
|
665
|
+
'Checking your rUv toolkit',
|
|
666
|
+
"the brain answers on its own with zero setup β but it can also BUILD, and that shines when the tools it recommends are here",
|
|
667
|
+
);
|
|
668
|
+
const mark = (ok) => (ok ? c.green('β present') : c.yellow('β not found'));
|
|
669
|
+
info(`platform: ${c.bold(`${env.platform}/${env.arch}`)} Β· node ${c.bold(env.node)}`);
|
|
670
|
+
info(`Ruflo (claude-flow Β· swarms / SPARC / orchestration): ${mark(env.ruflo)}`);
|
|
671
|
+
info(`RuVector (vector engine Β· CLI + MCP): ${mark(env.ruvector)}`);
|
|
672
|
+
|
|
673
|
+
if (env.ruflo && env.ruvector) {
|
|
674
|
+
ok('your full rUv toolkit is here β the brain can orchestrate end-to-end, not just answer');
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Grounded canonical commands (verified from rUv's own source: ruflo INSTALLATION.md + ruvector mcp-server.js).
|
|
679
|
+
const missing = [];
|
|
680
|
+
if (!env.ruflo)
|
|
681
|
+
missing.push({
|
|
682
|
+
what: 'Ruflo (orchestration + swarms)',
|
|
683
|
+
shell: env.claude ? ['npm', ['install', '-g', 'claude-flow@alpha']] : null,
|
|
684
|
+
say: 'npm install -g claude-flow@alpha',
|
|
685
|
+
plugin: '/plugin add ruvnet/claude-flow',
|
|
686
|
+
verify: () => have('claude-flow') || have('ruflo'),
|
|
687
|
+
});
|
|
688
|
+
if (!env.ruvector)
|
|
689
|
+
missing.push({
|
|
690
|
+
what: 'RuVector (vectors / RVF)',
|
|
691
|
+
// --scope user: same reason the plugin itself installs at user scope β the brain is "one
|
|
692
|
+
// toolkit, every project", and the default "local" scope ties the server to whatever
|
|
693
|
+
// directory happens to be the cwd when this runs.
|
|
694
|
+
shell: env.claude ? ['claude', ['mcp', 'add', 'ruvector', '--scope', 'user', '--', 'npx', '-y', 'ruvector', 'mcp', 'start']] : null,
|
|
695
|
+
say: 'claude mcp add ruvector --scope user -- npx -y ruvector mcp start',
|
|
696
|
+
plugin: null,
|
|
697
|
+
verify: () => hasUserScopeMcpServer('ruvector'),
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
info('');
|
|
701
|
+
info(c.dim("The brain is FULLY working right now for grounded answers β these two optional tools add the \"build it\" muscle:"));
|
|
702
|
+
|
|
703
|
+
const printCmds = () =>
|
|
704
|
+
missing.forEach((m) => {
|
|
705
|
+
info(` ${c.bold(m.what)}: ${c.cyan(m.say)}`);
|
|
706
|
+
if (m.plugin) info(` ${c.dim(`or in Claude Code: ${m.plugin}`)}`);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (FLAG_NO_STACK) {
|
|
710
|
+
info('(--no-stack: skipping. Add them any time with:)');
|
|
711
|
+
printCmds();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const yes = FLAG_WITH_STACK || (await ask('Add the missing rUv tools now so the brain can build, not just answer?', false));
|
|
716
|
+
if (!yes) {
|
|
717
|
+
info("No problem β nothing's missing for answers. To add the build tools any time:");
|
|
718
|
+
printCmds();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
for (const m of missing) {
|
|
723
|
+
if (m.shell) {
|
|
724
|
+
info(`installing ${m.what} β¦ ${c.dim(m.say)}`);
|
|
725
|
+
const ran = tryRun(m.shell[0], m.shell[1]);
|
|
726
|
+
// Don't trust the exit code alone β e.g. `claude mcp add` exits non-zero on "already exists",
|
|
727
|
+
// which is functionally success. Re-check the real state (m.verify) before warning.
|
|
728
|
+
if (ran || (m.verify && m.verify())) ok(`${m.what} added`);
|
|
729
|
+
else warn(`couldn't auto-add ${m.what}; run it yourself: ${c.bold(m.say)}${m.plugin ? ` (or ${m.plugin})` : ''}`);
|
|
730
|
+
} else {
|
|
731
|
+
info(`${m.what}: run ${c.bold(m.say)}${m.plugin ? ` ${c.dim(`(or ${m.plugin} in Claude Code)`)}` : ''}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (missing.some((m) => m.plugin))
|
|
735
|
+
info(c.dim('Tip: the Claude Code plugin path (/plugin add ruvnet/claude-flow) also wires Ruflo\'s MCP + slash commands.'));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ββ step: teach the user's Claude to lean on the brain (opt-in, idempotent, marker-guarded) βββββββ
|
|
739
|
+
const CLAUDE_MD_START = '<!-- ruvnet-brain:start -->';
|
|
740
|
+
const CLAUDE_MD_END = '<!-- ruvnet-brain:end -->';
|
|
741
|
+
const CLAUDE_MD_BLOCK = `${CLAUDE_MD_START}
|
|
742
|
+
## RuvNet Brain (installed Β· user-level Β· works in every project)
|
|
743
|
+
This machine has the RuvNet Brain. When a task touches rUv's stack β RuVector/RVF, Ruflo (claude-flow),
|
|
744
|
+
AgentDB, SPARC, agentic-flow, agenticow, QuDAG, SAFLA, RuLake β GROUND answers in real source via the
|
|
745
|
+
\`search_ruvnet\` MCP tool BEFORE asserting what any tool can do; training priors on this stack are stale.
|
|
746
|
+
Prefer rUv-native primitives (RVF over Pinecone/pgvector, Ruflo over LangChain, AgentDB over ad-hoc memory).
|
|
747
|
+
Not sure it's active? Run \`npx github:stuinfla/ruvnet-brain --doctor\`.
|
|
748
|
+
${CLAUDE_MD_END}`;
|
|
749
|
+
|
|
750
|
+
async function offerClaudeMd() {
|
|
751
|
+
if (FLAG_NO_ENHANCE) return;
|
|
752
|
+
const p = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
753
|
+
let existing = '';
|
|
754
|
+
try { existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : ''; } catch { /* ignore */ }
|
|
755
|
+
if (existing.includes(CLAUDE_MD_START)) { return; } // already enhanced β idempotent, stay silent
|
|
756
|
+
|
|
757
|
+
step(
|
|
758
|
+
'Teaching your Claude to lean on the brain',
|
|
759
|
+
'a short note in your global CLAUDE.md so every session β in any project β knows to use it',
|
|
760
|
+
);
|
|
761
|
+
const yes =
|
|
762
|
+
FLAG_ENHANCE_CLAUDE_MD ||
|
|
763
|
+
(await ask(`Add a short RuvNet-Brain section to ${existing ? 'your' : 'a new'} ~/.claude/CLAUDE.md?`, false));
|
|
764
|
+
if (!yes) {
|
|
765
|
+
info('skipped β the plugin hooks already enforce grounding every turn; this was just extra reinforcement');
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
770
|
+
const next = existing ? `${existing.replace(/\s*$/, '')}\n\n${CLAUDE_MD_BLOCK}\n` : `${CLAUDE_MD_BLOCK}\n`;
|
|
771
|
+
fs.writeFileSync(p, next);
|
|
772
|
+
ok(`added a RuvNet-Brain section to ${p} ${c.dim('(marker-guarded β safe to re-run)')}`);
|
|
773
|
+
} catch (e) {
|
|
774
|
+
warn(`couldn't update CLAUDE.md (${e.message}) β not important; the plugin hooks still enforce grounding`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ββ final success block ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
779
|
+
function success({ cacheDir, isCustom, plugin, env }) {
|
|
780
|
+
const line = 'β'.repeat(64);
|
|
781
|
+
console.log(`\n${c.green(line)}`);
|
|
782
|
+
console.log(`${c.green(c.bold(' RuvNet Brain is installed.'))}`);
|
|
783
|
+
console.log(`${c.green(line)}`);
|
|
784
|
+
console.log(`\n What you now have:`);
|
|
785
|
+
console.log(` β’ the brain (embedded source of ~18 RuvNet repos) at:`);
|
|
786
|
+
console.log(` ${c.bold(cacheDir)}`);
|
|
787
|
+
console.log(
|
|
788
|
+
` β’ the Claude Code plugin ${plugin.wired ? c.green('wired at user scope') : c.yellow('(finish the 2 commands above)')} β search_ruvnet + grounding hook`,
|
|
789
|
+
);
|
|
790
|
+
if (env) {
|
|
791
|
+
const t = (okv) => (okv ? c.green('β present') : c.yellow('not added (optional)'));
|
|
792
|
+
console.log(` β’ rUv build toolkit β Ruflo: ${t(env.ruflo)} Β· RuVector: ${t(env.ruvector)} ${c.dim('answers work without them')}`);
|
|
793
|
+
}
|
|
794
|
+
if (isCustom) {
|
|
795
|
+
console.log(`\n ${c.yellow('Heads up:')} you installed to a custom dir, so make this export permanent`);
|
|
796
|
+
console.log(` (add it to your shell profile) so the plugin can find the brain:`);
|
|
797
|
+
console.log(` ${c.bold(`export RUVNET_BRAIN_KB="${cacheDir}"`)}`);
|
|
798
|
+
}
|
|
799
|
+
// ββ where + how it runs β the confidence answers, stated up front ββ
|
|
800
|
+
console.log(`\n ${c.bold('Where it runs:')} ${c.bold('everywhere you use Claude Code')} β CLI, VS Code, JetBrains, the desktop app.`);
|
|
801
|
+
console.log(` It's ${c.bold('user-level (global)')}: open ANY repo or folder and it's already there. Nothing per project,`);
|
|
802
|
+
console.log(` nothing to copy in, nothing to git-ignore. ${c.dim('(Runs locally β it is not active in the claude.ai web app.)')}`);
|
|
803
|
+
console.log(`\n ${c.bold('How it runs:')} ${c.bold('automatically')} β you never call or configure anything. Ask normally; on rUv-stack work it`);
|
|
804
|
+
console.log(` grounds in real source and cites it, and if you drift to a classical default it steps in with the rUv option.`);
|
|
805
|
+
console.log(`\n ${c.bold('Keep it fresh:')} re-run ${c.bold('npx github:stuinfla/ruvnet-brain')} any time β it always pulls the latest brain.`);
|
|
806
|
+
|
|
807
|
+
// ββ one important expectation: the hook activates on the NEXT session ββ
|
|
808
|
+
console.log(`\n ${c.yellow(c.bold('One thing to know:'))} the grounding hook turns on at your ${c.bold('next')} Claude Code session.`);
|
|
809
|
+
console.log(` ${c.dim('If Claude Code is open right now, quit and reopen it β then the brain is live on every prompt.')}`);
|
|
810
|
+
|
|
811
|
+
// ββ what to do now ββ
|
|
812
|
+
console.log(`\n ${c.bold('What to do now:')}`);
|
|
813
|
+
console.log(` ${c.cyan('1.')} Open Claude Code in ${c.bold('any')} project (your own, or a fresh repo β nothing to copy in).`);
|
|
814
|
+
console.log(` ${c.cyan('2.')} Just ask, normally. Try: ${c.bold('"Set up vector search here the way rUv would."')}`);
|
|
815
|
+
console.log(` ${c.cyan('3.')} Watch what changes (below).`);
|
|
816
|
+
|
|
817
|
+
// ββ how you'll KNOW it's working ββ
|
|
818
|
+
console.log(`\n ${c.bold('How you\'ll know it\'s working:')}`);
|
|
819
|
+
console.log(` ${c.green('β')} Claude reaches for ${c.bold('RuVector / RVF, Ruflo, AgentDB')} instead of Pinecone / pgvector / LangChain.`);
|
|
820
|
+
console.log(` ${c.green('β')} It ${c.bold('cites real source paths')} (it calls ${c.bold('search_ruvnet')}) instead of guessing.`);
|
|
821
|
+
console.log(` ${c.green('β')} If you start to drift to a generic default, the brain ${c.bold('steps in')} and points you back.`);
|
|
822
|
+
|
|
823
|
+
// ββ honest expectations ββ
|
|
824
|
+
console.log(`\n ${c.bold('What to expect (honestly):')}`);
|
|
825
|
+
console.log(` β’ On rUv-stack work (vectors, swarms, agent memory, SPARC) it grounds ${c.bold('every time')}.`);
|
|
826
|
+
console.log(` β’ On unrelated work, it stays quiet β Claude behaves normally. It only speaks up when it should.`);
|
|
827
|
+
console.log(` β’ Not sure it's on? Run ${c.bold('npx github:stuinfla/ruvnet-brain --doctor')} any time for a health check.
|
|
828
|
+
β’ Want to see it answer, live, right now? ${c.bold('npx github:stuinfla/ruvnet-brain --demo')} β 2 real questions, real cited answers.`);
|
|
829
|
+
|
|
830
|
+
console.log(`\n ${c.bold('Set it up your way:')} this default is ${c.bold('global')} β live in every VS Code project automatically,`);
|
|
831
|
+
console.log(` which is what most people want. Want it different (project-only, moved, with the build stack added,`);
|
|
832
|
+
console.log(` auto-updating nightly)? ${c.bold('Just tell Claude')} once it's on β the brain is smart enough to reconfigure`);
|
|
833
|
+
console.log(` itself. You never have to learn its internals.`);
|
|
834
|
+
|
|
835
|
+
console.log(`\n ${c.dim('You can\'t break anything β the plugin is disable-able and only acts on RuvNet-shaped work.')}`);
|
|
836
|
+
console.log('');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function showHelp() {
|
|
840
|
+
console.log(`
|
|
841
|
+
RuvNet Brain installer
|
|
842
|
+
|
|
843
|
+
By default this installs the LATEST published Release (it asks GitHub which one that is),
|
|
844
|
+
and falls back to a known-good version if GitHub can't be reached.
|
|
845
|
+
|
|
846
|
+
Usage:
|
|
847
|
+
npx github:stuinfla/ruvnet-brain Install the LATEST brain + Claude Code plugin
|
|
848
|
+
npx github:stuinfla/ruvnet-brain --doctor Health-check an existing install (green/red per part)
|
|
849
|
+
npx github:stuinfla/ruvnet-brain --demo Guided walkthrough β 2 real questions, real cited answers
|
|
850
|
+
node bin/install.mjs --version <tag> Install a specific Release tag (e.g. --version v0.4.0-dev)
|
|
851
|
+
node bin/install.mjs --pin Skip the latest-check; use the bundled known-good version
|
|
852
|
+
node bin/install.mjs --local Install from a repo clone's dist/ruvnet-brain.zip
|
|
853
|
+
node bin/install.mjs --force Re-fetch and reinstall even if already present
|
|
854
|
+
node bin/install.mjs --no-verify Skip the post-install verify + warm-up smoke test
|
|
855
|
+
node bin/install.mjs --with-stack Also add missing Ruflo / RuVector (no prompt)
|
|
856
|
+
node bin/install.mjs --no-stack Don't offer to add Ruflo / RuVector
|
|
857
|
+
node bin/install.mjs --enhance-claude-md Add a RuvNet-Brain section to ~/.claude/CLAUDE.md (no prompt)
|
|
858
|
+
node bin/install.mjs --no-enhance Don't offer the CLAUDE.md section
|
|
859
|
+
node bin/install.mjs --yes, -y Accept every optional offer (good for scripted installs)
|
|
860
|
+
|
|
861
|
+
Env:
|
|
862
|
+
RUVNET_BRAIN_KB Override where the brain is stored (default ~/.cache/ruvnet-brain/kb)
|
|
863
|
+
|
|
864
|
+
It is safe to re-run at any time. After installing, restart Claude Code so the grounding hook loads.
|
|
865
|
+
`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ββ main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
869
|
+
(async () => {
|
|
870
|
+
if (FLAG_HELP) return showHelp();
|
|
871
|
+
if (FLAG_DOCTOR) return doctor();
|
|
872
|
+
if (FLAG_DEMO) return runDemo();
|
|
873
|
+
|
|
874
|
+
printBanner('installer');
|
|
875
|
+
console.log(c.dim("I'll set up the brain and the Claude Code plugin, explaining each step as I go.\n"));
|
|
876
|
+
|
|
877
|
+
// ββ environment guard: fail early and CLEARLY on unsupported Node, not cryptically mid-install ββ
|
|
878
|
+
// Node itself can't be silently auto-upgraded from inside a script it's currently running as β
|
|
879
|
+
// that's the running process replacing its own runtime, which is invasive and can go wrong in
|
|
880
|
+
// platform-specific ways (permissions, competing version managers). The safe, correct move is to
|
|
881
|
+
// hand back the exact right one-liner for the platform actually in front of us.
|
|
882
|
+
{
|
|
883
|
+
const m = /^v(\d+)/.exec(process.version);
|
|
884
|
+
const major = m ? Number(m[1]) : 0;
|
|
885
|
+
if (major && major < 18) {
|
|
886
|
+
const fix = IS_WIN
|
|
887
|
+
? `Update Node (pick one):\n β’ winget install OpenJS.NodeJS.LTS\n β’ or download the LTS installer: https://nodejs.org`
|
|
888
|
+
: process.platform === 'darwin'
|
|
889
|
+
? `Update Node (pick one):\n β’ brew install node\n β’ or: nvm install 20 && nvm use 20 (https://nodejs.org)`
|
|
890
|
+
: `Update Node (pick one):\n β’ nvm install 20 && nvm use 20\n β’ or your distro's Node 20+ package (see https://nodejs.org)`;
|
|
891
|
+
die(`RuvNet Brain needs Node 18 or newer β you're on ${process.version}.`, `${fix}\nThen re-run this same command β everything else is ready.`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const { cacheDir, isCustom } = resolveCacheDir();
|
|
896
|
+
|
|
897
|
+
const alreadyInstalled = fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs'));
|
|
898
|
+
if (alreadyInstalled && !FLAG_FORCE) {
|
|
899
|
+
step(
|
|
900
|
+
'Brain already present β skipping the download',
|
|
901
|
+
"it's already unpacked here; I'll just make sure the reader and plugin are wired (use --force to refetch)",
|
|
902
|
+
);
|
|
903
|
+
ok(`found an existing brain at ${cacheDir}`);
|
|
904
|
+
} else {
|
|
905
|
+
// Resolve which Release to fetch BEFORE downloading. Skipped entirely on the --local path
|
|
906
|
+
// (obtainBundle short-circuits to the repo's dist/ zip and never touches the network).
|
|
907
|
+
const localZipPresent =
|
|
908
|
+
FLAG_LOCAL || fs.existsSync(path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip'));
|
|
909
|
+
const release = localZipPresent ? null : await resolveRelease();
|
|
910
|
+
const { zipPath, downloaded } = await obtainBundle(release);
|
|
911
|
+
unzipInto(zipPath, cacheDir);
|
|
912
|
+
if (downloaded) {
|
|
913
|
+
try { fs.rmSync(zipPath, { force: true }); } catch { /* leave temp behind, not fatal */ }
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
installReader(cacheDir);
|
|
918
|
+
const plugin = wirePlugin();
|
|
919
|
+
if (!FLAG_NO_VERIFY) {
|
|
920
|
+
verifyInstall(cacheDir);
|
|
921
|
+
smokeQuery(cacheDir);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ββ onboarding: detect the toolkit + make offers (all optional, all non-fatal) ββ
|
|
925
|
+
const env = detectEnvironment();
|
|
926
|
+
try { await offerStack(env); } catch (e) { warn(`(toolkit check skipped: ${e && e.message})`); }
|
|
927
|
+
try { await offerClaudeMd(); } catch { /* non-fatal β never let an offer break the install */ }
|
|
928
|
+
|
|
929
|
+
success({ cacheDir, isCustom, plugin, env });
|
|
930
|
+
})().catch((e) => {
|
|
931
|
+
die(e && e.message ? e.message : String(e));
|
|
932
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ruvnet-brain",
|
|
3
|
+
"version": "0.5.0-dev",
|
|
4
|
+
"description": "One-command installer for RuvNet Brain β a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ruvnet-brain": "bin/install.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/install.mjs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ruvnet",
|
|
19
|
+
"ruv",
|
|
20
|
+
"ruflo",
|
|
21
|
+
"ruvector",
|
|
22
|
+
"rvf",
|
|
23
|
+
"agentdb",
|
|
24
|
+
"grounding",
|
|
25
|
+
"brain",
|
|
26
|
+
"mcp",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"installer"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/stuinfla/ruvnet-brain.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/stuinfla/ruvnet-brain#readme",
|
|
36
|
+
"bugs": "https://github.com/stuinfla/ruvnet-brain/issues",
|
|
37
|
+
"author": "Stuart Kerr"
|
|
38
|
+
}
|