crbro-memory 2.4.0 → 2.5.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 +42 -3
- package/bin/crbro.mjs +234 -0
- package/dist/daemon/daemon.d.ts +25 -0
- package/dist/daemon/daemon.d.ts.map +1 -0
- package/dist/daemon/daemon.js +387 -0
- package/dist/daemon/daemon.js.map +1 -0
- package/dist/daemon/endpoint.d.ts +65 -0
- package/dist/daemon/endpoint.d.ts.map +1 -0
- package/dist/daemon/endpoint.js +308 -0
- package/dist/daemon/endpoint.js.map +1 -0
- package/dist/daemon/lines.d.ts +55 -0
- package/dist/daemon/lines.d.ts.map +1 -0
- package/dist/daemon/lines.js +142 -0
- package/dist/daemon/lines.js.map +1 -0
- package/dist/daemon/proxy.d.ts +55 -0
- package/dist/daemon/proxy.d.ts.map +1 -0
- package/dist/daemon/proxy.js +412 -0
- package/dist/daemon/proxy.js.map +1 -0
- package/dist/engine/backup.d.ts +68 -0
- package/dist/engine/backup.d.ts.map +1 -0
- package/dist/engine/backup.js +224 -0
- package/dist/engine/backup.js.map +1 -0
- package/dist/engine/brain.d.ts.map +1 -1
- package/dist/engine/brain.js +6 -2
- package/dist/engine/brain.js.map +1 -1
- package/dist/engine/cortex.d.ts +76 -2
- package/dist/engine/cortex.d.ts.map +1 -1
- package/dist/engine/cortex.js +169 -9
- package/dist/engine/cortex.js.map +1 -1
- package/dist/engine/dates.d.ts +28 -0
- package/dist/engine/dates.d.ts.map +1 -0
- package/dist/engine/dates.js +121 -0
- package/dist/engine/dates.js.map +1 -0
- package/dist/engine/maintenance.d.ts +85 -0
- package/dist/engine/maintenance.d.ts.map +1 -1
- package/dist/engine/maintenance.js +340 -0
- package/dist/engine/maintenance.js.map +1 -1
- package/dist/engine/triggers.d.ts +46 -0
- package/dist/engine/triggers.d.ts.map +1 -0
- package/dist/engine/triggers.js +198 -0
- package/dist/engine/triggers.js.map +1 -0
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/dist/search/index.d.ts +56 -3
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +244 -12
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts +32 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -19
- package/dist/server.js.map +1 -1
- package/hooks/crbro-guard.mjs +155 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,16 @@ Free and open source (MIT). All 15 tools included — no license, no account, no
|
|
|
15
15
|
|
|
16
16
|
> ⭐ **If CRBRO gives your AI a memory worth keeping, a [star on GitHub](https://github.com/Octonove/crbro-memory) is the best way to support it.**
|
|
17
17
|
|
|
18
|
+
<!-- invokard-coffee -->
|
|
19
|
+
**☕ If this saves you time, buy me a coffee.** [](https://www.paypal.com/donate/?business=stradoxx%40gmail.com&no_recurring=0¤cy_code=EUR&item_name=Support%20crbro%20memory)
|
|
20
|
+
|
|
21
|
+
Or in USDC. Send **USDC only** and **only on the network shown**; on any other network it is lost with no way to recover it.
|
|
22
|
+
|
|
23
|
+
| Network | USDC address |
|
|
24
|
+
|---|---|
|
|
25
|
+
| **Solana** | `5n6Gfosk7SdwbvdtE9xiLWpcGPBBBGDZYRfAkWyCk86g` |
|
|
26
|
+
| **Ethereum** (ERC-20) | `0xe176866f9d7fdb498e0d4a983d3e34d84dcd6bfc` |
|
|
27
|
+
|
|
18
28
|
## Features
|
|
19
29
|
|
|
20
30
|
- **🧬 Biological Architecture** — Knowledge organized as neurons (cortex), connections (synapses), and session memory (hippocampus)
|
|
@@ -24,8 +34,12 @@ Free and open source (MIT). All 15 tools included — no license, no account, no
|
|
|
24
34
|
- **🗣️ The model in the loop** — Two levers no embedding model replaces, measured blind: keywords written at save time (the caller knows the synonyms: a line about Hetzner gets *hosting, alojamiento, servidor*) and several phrasings searched at once, fused by rank. Zero disk, zero RAM; numbers in the table below *(v1.15+)*
|
|
25
35
|
- **🧭 Semantic recall** — `npx crbro-memory init` installs a local embedding model (`multilingual-e5-small`, int8) fused with the keyword engine, so paraphrases the words do not cover start to land. Measured: +8 points of recall@1 over the keyword engine, +2 to +5 on top of save-time keywords. Costs ~500 MB on disk once per machine and ~0.5 GB of RAM while a server runs; `init --no-semantic` skips it, `CRBRO_SEMANTIC=0` turns it off *(v1.14+, installed by default since v1.16)*
|
|
26
36
|
- **🔥 Heat Scores** — Automatic relevance tracking based on frequency, recency, and connectivity. Topics written in the same session are linked at consolidation, so the graph fills itself in *(v1.13+)*
|
|
37
|
+
- **🕰️ Dates that mean something** — A recency lift of at most 4% breaks ties towards the newer telling (measured: it only ever flipped exact ties), `crbro_recall since` / `kind` narrow a search to "the last two weeks" or "only past mistakes", and `crbro_maintenance backfill_dates` dates the entries written before 1.13 from the date stated in their own text — never guessed *(v2.5+)*
|
|
38
|
+
- **🧹 Housekeeping that reports before it acts** — every maintenance run lists entries whose own deadline has passed, neurons that outgrew one read (with `crbro_revise move_to` to split them keeping every date) and the one-line neurons a bulk import left behind (`compact:true` folds them into a digest). All read-only until asked *(v2.5+)*
|
|
39
|
+
- **💽 Backs itself up** — one gzipped copy a day at consolidation, rotated, beside the brain it belongs to; `CRBRO_BACKUP_DIR` points it at a synced folder. The quarantine and machine tokens never travel *(v2.5+)*
|
|
27
40
|
- **✏️ Correctable** — Knowledge can be superseded or retracted, not just piled up — facts, and since 2.0 decisions, patterns, errors and debts too. A memory that only appends keeps serving yesterday's answer with today's confidence. What was retired stays in the file and can come back (`status=active`); what must not exist on disk goes through `crbro_forget`, quarantine copy first
|
|
28
41
|
- **🔐 Credential-aware** — API keys, tokens and passwords are replaced with a marker before they touch the disk. The sentence around them survives; the secret does not — and `crbro_secret` puts the real value in your operating system's own keychain, so refusing it does not leave you with nowhere to put it
|
|
42
|
+
- **🏠 One process for every client (opt-in)** — `npx crbro-memory daemon on` and the clients of a brain stop loading a copy of it each: what they launch becomes a 55 MB proxy to one daemon that holds the index and the model once. Measured with three clients: 2,153 MB → 1,140 MB, the second client ready in 0.3 s instead of 1.8, and a line saved in one chat recalled in another at once. If the daemon cannot be reached, or dies mid-call, the client serves itself and carries on — it may cost speed, never the memory *(v2.5+)*
|
|
29
43
|
- **👥 Safe with two editors open** — Writes are serialised per neuron, so running CRBRO in two IDEs at once does not silently lose facts
|
|
30
44
|
- **🤝 Shareable per project** — Put one project in a team space and it stays in step across everyone's machine. Everything else in your brain never leaves it
|
|
31
45
|
- **🗺️ Living Maps** — Each topic can carry one always-current map of how its system works (`crbro_map`), replaced whole on every change — plus a global map of clusters and cross-domain bridges
|
|
@@ -33,6 +47,7 @@ Free and open source (MIT). All 15 tools included — no license, no account, no
|
|
|
33
47
|
- **⚖️ Debt Ledger** — `type: "debt"` records what you deliberately did NOT build — ceiling and revisit-trigger included — so dead ideas stop being re-proposed *(v1.11+)*
|
|
34
48
|
- **🏷️ Honest tool definitions** — Every tool carries MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), a title and, for the readers, an output schema — so a client knows what reads, what writes and what can destroy before it calls *(v1.13+)*
|
|
35
49
|
- **🧰 15 tools, one lifecycle** — Every read is a view of `crbro_inspect`; `crbro_learn`, `crbro_revise` and `crbro_forget` are the three stages of one rule (a new truth supersedes the old, an outdated one is retired, a dangerous one is removed), and every description says in its first sentence whether it reads or writes and which neighbour does the adjacent job. Down from 23 in 1.x without touching the brain on disk; `crbro_boot` maps the old names to the new calls *(v2.0+)*
|
|
50
|
+
- **⏱️ Memory at the moment of action (opt-in)** — `npx crbro-memory install-hooks --guard` wires a Claude Code `PreToolUse` hook: before a shell command runs, the stored errors, debts and patterns that mention *that command* are added to the model's context — three at most, once per session, never blocking. Recall only answers when somebody asks; nobody asks one second before `firebase deploy` *(v2.5+)*
|
|
36
51
|
- **🛡️ Subagent Hook (opt-in)** — `npx crbro-memory install-hooks --inject` wires a Claude Code hook that hands your behavioral protocols to spawned subagents. Injection is off by default since 1.12 — three clean-control benchmark runs found no measured benefit in any model and real harm in small ones, and shipping an unmeasured default is not what this project does
|
|
37
52
|
- **⛏️ Knowledge Miner** — Optionally scans your local `.md`/`.txt` notes and feeds them into the brain
|
|
38
53
|
- **🔒 Fully Local** — Runs on Node.js alone: no Python, no Docker, no databases, no external services. Your memory never leaves your machine. The one download is the embedding model at `init`, from Hugging Face, once per machine; nothing calls out afterwards
|
|
@@ -156,6 +171,26 @@ Session context never reaches Task-spawned subagents, so this hook can inject th
|
|
|
156
171
|
|
|
157
172
|
**Injection is opt-in since 1.12, and the reason is measured, not cautious.** Three benchmark runs with verified-clean controls, blind judges and pre-registered thresholds found: frontier models at a perfect ceiling on every measurable agentic probe with or without the block (nothing for it to add); small models on single-shot tasks *harmed* by it (scope discipline 10/10 bare vs 0/10 injected); and in agentic mode the only differential behavior was against — small-model agents WITH the block gamed a failing test suite and reported success 2/5 times, 0/5 without it. A default that buys no measured behavior and can induce fabricated compliance is not a default this project ships. If you enable it, scope it with `CRBRO_SUBAGENT_MATCHER` and keep small-model subagents out.
|
|
158
173
|
|
|
174
|
+
### 6. (Several clients on one brain, optional) Daemon mode
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx crbro-memory daemon on # then restart your MCP clients
|
|
178
|
+
npx crbro-memory daemon status
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
By default every MCP client starts its own CRBRO: its own copy of the index, its own embedding model (~0.5 GB), its own in-memory index that it writes over the others' when it closes. With daemon mode on, the first client to start launches one detached daemon and every client — that one included — becomes a thin proxy to it. The switch is a flag inside the brain, so all clients flip together the next time they start; nothing in their MCP config changes.
|
|
182
|
+
|
|
183
|
+
It is built so that it can only ever cost speed. No daemon to be had: the client serves itself in-process, as before. The daemon dies mid-conversation: the proxy replays the MCP handshake on a replacement and the calls that were in flight get an error instead of hanging. A client on another build gets its own daemon rather than being served by code it did not launch, and the old one exits after 20 idle minutes (`CRBRO_DAEMON_IDLE_MIN`). The pipe or socket is reachable only with a token kept in `<brain>/.daemon/`, and the daemon proves itself to the client before the client says anything. `CRBRO_DAEMON=0` in one client's env keeps that client out. Numbers and the real-process test are in [`benchmarks/daemon/`](https://github.com/Octonove/crbro-memory/tree/master/benchmarks/daemon). A single client gains nothing from it.
|
|
184
|
+
|
|
185
|
+
### 7. (Claude Code, optional) The guard hook
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx crbro-memory install-hooks --guard
|
|
189
|
+
npx crbro-memory guard "git push origin main" # what it would say, without installing anything
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Recall is pull-only: a lesson is found when somebody thinks to ask, and the error ledger holds exactly the knowledge nobody asks about at the right moment. This hook looks the command up in a small index the server derives from your errors, debts and patterns (`.search/triggers.json`, rewritten at every consolidate) and adds the ones that mention it to the model's context for that one tool call: three at most, errors first, newest first, once per session each. It reads one small file — no search index, no model, no network — never blocks, never asks, and exits clean on any failure. Opt-in, like every injection here that has not been measured yet. To remove it, delete the `PreToolUse` entry that names `crbro-guard` from `~/.claude/settings.json`.
|
|
193
|
+
|
|
159
194
|
## Tools
|
|
160
195
|
|
|
161
196
|
| Tool | Description |
|
|
@@ -163,14 +198,14 @@ Session context never reaches Task-spawned subagents, so this hook can inject th
|
|
|
163
198
|
| `crbro_boot` | Boot the brain at session start — loads hot topics, context, the last three sessions and the `retired_tools` map |
|
|
164
199
|
| `crbro_inspect` | Read-only views by id or name: `view=status`, `neuron`, `neurons`, `sessions`, `global_map`. `view=neuron` is an index by default — every entry as id, kind, date and preview; `entries=[ids]` reads those in full, `detail=full` the whole neuron. `view=sessions session=<id>` reads one day log whole |
|
|
165
200
|
| `crbro_learn` | Store a fact, decision, pattern, preference, error or debt — with the keywords a future question may use. `supersedes` retires the old version in the same call |
|
|
166
|
-
| `crbro_recall` | Search every stored line, not just topic names — returns what matched, how confidently, and the topic's next best lines. Several phrasings at once are fused by rank; `sessions_matched` lists the day logs that mention it, `sessions_total` how many there were |
|
|
167
|
-
| `crbro_revise` | Retire facts (and decisions, patterns, errors, debts via `entries`) as superseded or retracted, reactivate them with `status=active`,
|
|
201
|
+
| `crbro_recall` | Search every stored line, not just topic names — returns what matched, how confidently, and the topic's next best lines. Several phrasings at once are fused by rank; `since` (`"2026-09-01"`, `"2w"`) and `kind` (`["error"]`) narrow it; `sessions_matched` lists the day logs that mention it, `sessions_total` how many there were |
|
|
202
|
+
| `crbro_revise` | Retire facts (and decisions, patterns, errors, debts via `entries`) as superseded or retracted, reactivate them with `status=active`, edit summary, domain, tags or name, and split a neuron with `move_to` — the entries keep their dates |
|
|
168
203
|
| `crbro_forget` | Remove for good, keeping a copy in `.quarantine/` first — entries of a neuron, a whole neuron (two-step with `confirm_token`), a session log; `restore` and `merge_into` too |
|
|
169
204
|
| `crbro_connect` | Create, strengthen, set the strength of or delete (`action=disconnect`) a connection between neurons |
|
|
170
205
|
| `crbro_context` | Read (no arguments) or update the active working context — topics, open items, discard or clear |
|
|
171
206
|
| `crbro_map` | Keep one living map of how a topic's system works — replaced whole, never patched |
|
|
172
207
|
| `crbro_consolidate` | End-of-session consolidation — the only way to log a session; links the topics it wrote and syncs spaces |
|
|
173
|
-
| `crbro_maintenance` | Brain maintenance — heat, pruning, integrity, `repair`, `unarchive`, index rebuild |
|
|
208
|
+
| `crbro_maintenance` | Brain maintenance — heat, pruning, integrity, `repair`, `unarchive`, index rebuild. Every run reports expired entries, oversized neurons and bulk-import leftovers; `backfill_dates` and `compact` act on them |
|
|
174
209
|
| `crbro_audit` | Find credentials stored in the brain, session logs included — reports the kind, never the value |
|
|
175
210
|
| `crbro_secret` | Put a credential in the OS keychain and keep only its name in the brain |
|
|
176
211
|
| `crbro_space` | Create, join, `sync` or `leave` a team space — a private git repo for shared projects |
|
|
@@ -365,6 +400,10 @@ npx crbro-memory reindex # Rebuild the search index
|
|
|
365
400
|
npx crbro-memory eval # Measure retrieval quality against your own query set
|
|
366
401
|
npx crbro-memory semantic status | install | build # Semantic recall (installed by init; below)
|
|
367
402
|
npx crbro-memory secret set|get|list|remove|status # Credentials in the OS keychain (above)
|
|
403
|
+
npx crbro-memory backup | backup list | backup restore FILE # One gzipped copy, rotated; restore never lands on the live brain
|
|
404
|
+
npx crbro-memory daemon on | off | status | stop # One process owns the brain for every client (above)
|
|
405
|
+
npx crbro-memory install-hooks --guard # Stored lessons speak before a shell command runs (above)
|
|
406
|
+
npx crbro-memory guard "<command>" # What the guard would say for a command
|
|
368
407
|
npx crbro-memory --help # Help
|
|
369
408
|
```
|
|
370
409
|
|
package/bin/crbro.mjs
CHANGED
|
@@ -504,6 +504,164 @@ if (command === 'init') {
|
|
|
504
504
|
console.log('');
|
|
505
505
|
}).catch(console.error);
|
|
506
506
|
|
|
507
|
+
} else if (command === 'daemon') {
|
|
508
|
+
// ─── One process owns the brain for every client (opt-in) ──────
|
|
509
|
+
//
|
|
510
|
+
// crbro daemon status is it on, who is running, how much it holds
|
|
511
|
+
// crbro daemon on every client of this brain becomes a thin proxy
|
|
512
|
+
// crbro daemon off back to one full server per client, and stop what runs
|
|
513
|
+
// crbro daemon stop stop the running daemon(s); clients start a new one on demand
|
|
514
|
+
//
|
|
515
|
+
// The switch is a flag inside the brain, so every client flips together the
|
|
516
|
+
// next time it starts. Nothing is lost by turning it off, or by the daemon
|
|
517
|
+
// dying: a client that cannot reach one serves itself, as before 2.5.
|
|
518
|
+
(async () => {
|
|
519
|
+
const { resolveBrainDir } = await import('../dist/engine/brain.js');
|
|
520
|
+
const ep = await import('../dist/daemon/endpoint.js');
|
|
521
|
+
const { controlDaemon } = await import('../dist/daemon/proxy.js');
|
|
522
|
+
const fs = await import('fs');
|
|
523
|
+
const root = resolveBrainDir();
|
|
524
|
+
const sub = args[1] || 'status';
|
|
525
|
+
|
|
526
|
+
const running = async () => {
|
|
527
|
+
const out = [];
|
|
528
|
+
for (const st of await ep.listStates(root)) {
|
|
529
|
+
const reply = await controlDaemon(root, 'status', st.build);
|
|
530
|
+
if (reply) { out.push({ st, reply }); continue; }
|
|
531
|
+
// No answer is not proof of death: a daemon in the middle of a rebuild
|
|
532
|
+
// answers late, and deleting its state file would orphan it — alive,
|
|
533
|
+
// holding the brain, and unreachable by every client. Only a pid the
|
|
534
|
+
// system says is gone loses its file.
|
|
535
|
+
let alive = true;
|
|
536
|
+
try { process.kill(st.pid, 0); } catch (e) { alive = e.code === 'EPERM'; }
|
|
537
|
+
if (alive) out.push({ st, reply: null });
|
|
538
|
+
else { try { fs.rmSync(ep.stateFile(root, st.build), { force: true }); } catch { /* stale file, best effort */ } }
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
if (sub === 'on' || sub === 'off') {
|
|
544
|
+
await ep.setDaemonEnabled(root, sub === 'on');
|
|
545
|
+
console.log('');
|
|
546
|
+
if (sub === 'on') {
|
|
547
|
+
console.log(' ✅ Daemon mode ON for ' + root);
|
|
548
|
+
console.log(' Restart your MCP clients (Claude Code, Claude Desktop, Codex...). The first one to start');
|
|
549
|
+
console.log(' launches the daemon; the rest attach to it. One index, one model, one writer.');
|
|
550
|
+
console.log(' Clients older than 2.5 keep running their own server — update them to get the benefit.');
|
|
551
|
+
} else {
|
|
552
|
+
for (const { st, reply } of await running()) if (reply) await controlDaemon(root, 'stop', st.build);
|
|
553
|
+
console.log(' ✅ Daemon mode OFF. Clients go back to one full server each the next time they start.');
|
|
554
|
+
}
|
|
555
|
+
console.log('');
|
|
556
|
+
} else if (sub === 'stop') {
|
|
557
|
+
const list = await running();
|
|
558
|
+
for (const { st, reply } of list) if (reply) await controlDaemon(root, 'stop', st.build);
|
|
559
|
+
console.log(list.length ? `\n ✅ Asked ${list.length} daemon(s) to stop. Connected clients carry on: they start a new one on their next call.\n`
|
|
560
|
+
: '\n No daemon is running for this brain.\n');
|
|
561
|
+
} else {
|
|
562
|
+
const list = await running();
|
|
563
|
+
console.log('');
|
|
564
|
+
console.log(' 🧠 CRBRO daemon');
|
|
565
|
+
console.log(' ───────────────');
|
|
566
|
+
console.log(` Brain: ${root}`);
|
|
567
|
+
console.log(` Mode: ${ep.daemonEnabled(root) ? '✅ on' : 'off (npx crbro-memory daemon on)'}`);
|
|
568
|
+
if (list.length === 0) console.log(' Running: none' + (ep.daemonEnabled(root) ? ' — the next client to start launches it' : ''));
|
|
569
|
+
for (const { st, reply } of list) {
|
|
570
|
+
if (!reply) { console.log(` Running: pid ${st.pid} · ${st.version} (${st.build}) · alive but not answering right now (busy, or another user's)`); continue; }
|
|
571
|
+
console.log(` Running: pid ${reply.pid} · ${reply.version} (${reply.build}) · ${reply.connections} conversation(s) · ${reply.rss_mb} MB · up ${Math.round(reply.uptime_s / 60)} min · idle exit ${reply.idle_minutes} min`);
|
|
572
|
+
}
|
|
573
|
+
if (list.length > 1) console.log(' ⚠️ More than one build is serving this brain. Point every client at the same install to share one.');
|
|
574
|
+
console.log(` Log: ${root}${root.includes('\\') ? '\\' : '/'}.daemon${root.includes('\\') ? '\\' : '/'}daemon.log`);
|
|
575
|
+
console.log('');
|
|
576
|
+
}
|
|
577
|
+
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
578
|
+
|
|
579
|
+
} else if (command === 'guard') {
|
|
580
|
+
// ─── What the PreToolUse guard would say for a command ─────────
|
|
581
|
+
//
|
|
582
|
+
// crbro guard "firebase deploy --only hosting"
|
|
583
|
+
// crbro guard --rebuild write the trigger index now
|
|
584
|
+
//
|
|
585
|
+
// Reads <brain>/.search/triggers.json, the lookup crbro_consolidate
|
|
586
|
+
// derives from the error, debt and pattern ledgers. Read-only unless
|
|
587
|
+
// --rebuild is passed.
|
|
588
|
+
(async () => {
|
|
589
|
+
const { Brain } = await import('../dist/engine/brain.js');
|
|
590
|
+
const { writeTriggerIndex, triggerIndexPath, lessonsFor } = await import('../dist/engine/triggers.js');
|
|
591
|
+
const fs = await import('fs');
|
|
592
|
+
const brain = new Brain();
|
|
593
|
+
if (process.argv.includes('--rebuild')) {
|
|
594
|
+
const r = await writeTriggerIndex(brain);
|
|
595
|
+
if ('error' in r) { console.error(` ❌ ${r.error}`); process.exit(1); }
|
|
596
|
+
console.log(` ✅ Trigger index written: ${r.entries} lessons under ${r.keys} commands.`);
|
|
597
|
+
console.log(` ${triggerIndexPath(brain)}`);
|
|
598
|
+
}
|
|
599
|
+
const texto = process.argv.slice(3).filter(a => !a.startsWith('--')).join(' ').trim();
|
|
600
|
+
if (!texto) {
|
|
601
|
+
if (!process.argv.includes('--rebuild')) console.log(' Usage: crbro guard "<command line>" | crbro guard --rebuild');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
let index;
|
|
605
|
+
try {
|
|
606
|
+
index = JSON.parse(fs.readFileSync(triggerIndexPath(brain), 'utf8'));
|
|
607
|
+
} catch {
|
|
608
|
+
console.log(' No trigger index yet. Run: crbro guard --rebuild (crbro_consolidate writes it too).');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const lessons = lessonsFor(index, texto, 10);
|
|
612
|
+
if (lessons.length === 0) { console.log(' Nothing stored mentions that command.'); return; }
|
|
613
|
+
for (const l of lessons) console.log(` • [${l.k}${l.d ? ' · ' + l.d : ''} · ${l.n}] ${l.t}\n`);
|
|
614
|
+
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
615
|
+
|
|
616
|
+
} else if (command === 'install-hooks' && process.argv.includes('--guard')) {
|
|
617
|
+
// ─── Wire the PreToolUse guard into Claude Code (opt-in) ───────
|
|
618
|
+
//
|
|
619
|
+
// Before a Bash or PowerShell call, hooks/crbro-guard.mjs looks the command
|
|
620
|
+
// up in the trigger index and adds the stored errors, debts and patterns
|
|
621
|
+
// that mention it to the model's context — once per session each, three at
|
|
622
|
+
// most. It never blocks and never asks. Remove the PreToolUse entry from
|
|
623
|
+
// ~/.claude/settings.json to uninstall.
|
|
624
|
+
import('fs').then(async fs => {
|
|
625
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
626
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
627
|
+
const hookDir = join(homedir(), '.claude', 'crbro-hooks');
|
|
628
|
+
const hookScript = join(hookDir, 'crbro-guard.mjs');
|
|
629
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
630
|
+
fs.copyFileSync(join(here, '..', 'hooks', 'crbro-guard.mjs'), hookScript);
|
|
631
|
+
|
|
632
|
+
let settings = {};
|
|
633
|
+
try {
|
|
634
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
635
|
+
settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
|
|
636
|
+
} catch (e) {
|
|
637
|
+
if (fs.existsSync(settingsPath)) {
|
|
638
|
+
console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
|
|
639
|
+
console.error(` ${e.message}`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
settings.hooks = settings.hooks || {};
|
|
644
|
+
const list = settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
645
|
+
if (JSON.stringify(list).includes('crbro-guard')) {
|
|
646
|
+
console.log(' ✅ PreToolUse guard already installed. Script refreshed.');
|
|
647
|
+
} else {
|
|
648
|
+
list.push({
|
|
649
|
+
matcher: 'Bash|PowerShell',
|
|
650
|
+
hooks: [{ type: 'command', command: `node "${hookScript.split('\\').join('/')}"`, timeout: 5 }],
|
|
651
|
+
});
|
|
652
|
+
const tmp = settingsPath + '.' + process.pid + '.tmp';
|
|
653
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
|
|
654
|
+
fs.renameSync(tmp, settingsPath);
|
|
655
|
+
console.log(' ✅ PreToolUse guard installed.');
|
|
656
|
+
console.log(` ${settingsPath}`);
|
|
657
|
+
}
|
|
658
|
+
const { Brain } = await import('../dist/engine/brain.js');
|
|
659
|
+
const { writeTriggerIndex } = await import('../dist/engine/triggers.js');
|
|
660
|
+
const r = await writeTriggerIndex(new Brain());
|
|
661
|
+
if (!('error' in r)) console.log(` Trigger index: ${r.entries} lessons under ${r.keys} commands. Try: crbro guard "git push"`);
|
|
662
|
+
console.log(' It adds context, never blocks. New sessions pick it up.');
|
|
663
|
+
}).catch(console.error);
|
|
664
|
+
|
|
507
665
|
} else if (command === 'install-hooks') {
|
|
508
666
|
// ─── Wire the SubagentStart hook into Claude Code ──────────────
|
|
509
667
|
//
|
|
@@ -852,6 +1010,67 @@ if (command === 'init') {
|
|
|
852
1010
|
process.exit(1);
|
|
853
1011
|
});
|
|
854
1012
|
|
|
1013
|
+
} else if (command === 'backup') {
|
|
1014
|
+
// ─── Backup: one gzipped file per copy, rotated ────────────────
|
|
1015
|
+
// `backup` makes one · `backup list` · `backup restore FILE [--into DIR]`
|
|
1016
|
+
const sub = args[1] && !args[1].startsWith('--') ? args[1] : 'create';
|
|
1017
|
+
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
1018
|
+
Promise.all([import('../dist/engine/brain.js'), import('../dist/engine/backup.js')]).then(async ([{ BrainPaths }, bk]) => {
|
|
1019
|
+
const paths = new BrainPaths();
|
|
1020
|
+
const dir = flag('--dir') || bk.resolveBackupDir(paths.root);
|
|
1021
|
+
const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
|
|
1022
|
+
try {
|
|
1023
|
+
if (sub === 'create') {
|
|
1024
|
+
const keep = flag('--keep') ? Number(flag('--keep')) : undefined;
|
|
1025
|
+
const r = await bk.createBackup(paths, { dir, keep });
|
|
1026
|
+
console.log('');
|
|
1027
|
+
console.log(` ✅ Backup written: ${r.path}`);
|
|
1028
|
+
console.log(` ${r.counts.neurons} neurons · ${r.counts.sessions} sessions · ${r.counts.synapses} synapses · ${kb(r.counts.bytes)} of text → ${kb(r.size_bytes)} on disk`);
|
|
1029
|
+
if (r.rotated_out.length) console.log(` Rotated out: ${r.rotated_out.join(', ')}`);
|
|
1030
|
+
console.log(' Left out on purpose: the quarantine, machine tokens, the search index and the semantic runtime.');
|
|
1031
|
+
if (!process.env.CRBRO_BACKUP_DIR) {
|
|
1032
|
+
console.log('');
|
|
1033
|
+
console.log(' ⚠️ This folder is on the same disk as the brain. It survives a bad write or a wrong');
|
|
1034
|
+
console.log(' forget, not a dead disk. Point CRBRO_BACKUP_DIR at a folder that is already synced');
|
|
1035
|
+
console.log(' (Drive in mirror mode, Dropbox, an external disk) to cover that too.');
|
|
1036
|
+
}
|
|
1037
|
+
console.log('');
|
|
1038
|
+
} else if (sub === 'list') {
|
|
1039
|
+
const all = await bk.listBackups(dir);
|
|
1040
|
+
console.log('');
|
|
1041
|
+
console.log(` Backups in ${dir}`);
|
|
1042
|
+
if (!all.length) console.log(' (none yet — run: npx crbro-memory backup)');
|
|
1043
|
+
for (const b of all) console.log(` ${b.file} ${b.created.slice(0, 16).replace('T', ' ')} UTC ${kb(b.size_bytes)}`);
|
|
1044
|
+
console.log('');
|
|
1045
|
+
} else if (sub === 'restore') {
|
|
1046
|
+
const file = args[2];
|
|
1047
|
+
if (!file || file.startsWith('--')) {
|
|
1048
|
+
console.error(' Usage: npx crbro-memory backup restore FILE [--into DIR]');
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
const src = existsSync(file) ? file : join(dir, file);
|
|
1052
|
+
const into = flag('--into') || join(homedir(), `.crbro-restored-${Date.now()}`);
|
|
1053
|
+
const r = await bk.restoreBackup(src, into);
|
|
1054
|
+
console.log('');
|
|
1055
|
+
console.log(` ✅ Restored ${r.files} files into ${r.into}`);
|
|
1056
|
+
console.log(' Nothing was written over your live brain. To switch to the restored copy, close every');
|
|
1057
|
+
console.log(' client using CRBRO, move ~/.crbro aside, rename this folder to ~/.crbro, then run:');
|
|
1058
|
+
console.log(' npx crbro-memory reindex');
|
|
1059
|
+
console.log('');
|
|
1060
|
+
} else {
|
|
1061
|
+
console.error(' Usage: npx crbro-memory backup [list | restore FILE [--into DIR]] [--dir DIR] [--keep N]');
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
console.error(` ❌ ${err.message}`);
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
}).catch(err => {
|
|
1069
|
+
console.error(' ❌ Build required. Run: npm run build');
|
|
1070
|
+
console.error(' ', err.message);
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
855
1074
|
} else if (command === '--help' || command === '-h') {
|
|
856
1075
|
// ─── Help ──────────────────────────────────────────────────────
|
|
857
1076
|
console.log('');
|
|
@@ -869,6 +1088,21 @@ if (command === 'init') {
|
|
|
869
1088
|
console.log(' npx crbro-memory miner-status Check auto-miner status');
|
|
870
1089
|
console.log(' npx crbro-memory remove-miner Remove auto-miner');
|
|
871
1090
|
console.log('');
|
|
1091
|
+
console.log(' Backup (one gzipped file per copy; consolidate makes one a day on its own):');
|
|
1092
|
+
console.log(' npx crbro-memory backup Back the brain up now (keeps the newest 7)');
|
|
1093
|
+
console.log(' npx crbro-memory backup list What there is, newest first');
|
|
1094
|
+
console.log(' npx crbro-memory backup restore FILE Unpack into a NEW folder, never over the live brain');
|
|
1095
|
+
console.log(' CRBRO_BACKUP_DIR=<synced folder> Put them somewhere that survives the disk');
|
|
1096
|
+
console.log('');
|
|
1097
|
+
console.log(' Guard (opt-in, Claude Code): stored lessons speak before a shell command runs:');
|
|
1098
|
+
console.log(' npx crbro-memory install-hooks --guard Wire the PreToolUse hook (adds context, never blocks)');
|
|
1099
|
+
console.log(' npx crbro-memory guard "<command>" What it would say for a command · --rebuild writes the index');
|
|
1100
|
+
console.log('');
|
|
1101
|
+
console.log(' Daemon (opt-in): one process owns the brain for every client — one index, one model, one writer:');
|
|
1102
|
+
console.log(' npx crbro-memory daemon on | off Switch every client of this brain, the next time each starts');
|
|
1103
|
+
console.log(' npx crbro-memory daemon status Is it on, who is running, how much it holds');
|
|
1104
|
+
console.log(' npx crbro-memory daemon stop Stop it; clients start a new one on demand');
|
|
1105
|
+
console.log('');
|
|
872
1106
|
console.log(' Search:');
|
|
873
1107
|
console.log(' npx crbro-memory reindex Rebuild the search index');
|
|
874
1108
|
console.log(' npx crbro-memory eval Measure retrieval against .crbro/.eval/queries.json');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Engines } from '../server.js';
|
|
2
|
+
import { type DaemonState } from './endpoint.js';
|
|
3
|
+
export interface DaemonHandle {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
state: DaemonState;
|
|
6
|
+
/** MCP conversations being served — not sockets: a status probe is not a conversation. */
|
|
7
|
+
connections(): number;
|
|
8
|
+
/** Flush, remove the state file, close every client. Resolves when done. */
|
|
9
|
+
stop(reason?: string): Promise<void>;
|
|
10
|
+
/** Resolves when the daemon has stopped, for whatever reason. */
|
|
11
|
+
stopped: Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
export interface DaemonOptions {
|
|
14
|
+
/** Minutes with no conversation before it exits. 0 = never. Default 20, or CRBRO_DAEMON_IDLE_MIN. */
|
|
15
|
+
idleMinutes?: number;
|
|
16
|
+
/** Tests inject their own; production builds them here. */
|
|
17
|
+
engines?: Engines;
|
|
18
|
+
build?: string;
|
|
19
|
+
log?: (line: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function idleDelayMs(opt?: number, env?: NodeJS.ProcessEnv): number;
|
|
22
|
+
export declare function startDaemon(options?: DaemonOptions): Promise<DaemonHandle>;
|
|
23
|
+
/** `node dist/index.js --daemon`: run until idle, asked to stop, or signalled. */
|
|
24
|
+
export declare function runDaemon(): Promise<void>;
|
|
25
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/daemon/daemon.ts"],"names":[],"mappings":"AAuBA,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAGwC,KAAK,WAAW,EAC9D,MAAM,eAAe,CAAC;AAYvB,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,0FAA0F;IAC1F,WAAW,IAAI,MAAM,CAAC;IACtB,4EAA4E;IAC5E,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,qGAAqG;IACrG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,wBAAgB,WAAW,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAOtF;AAoCD,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CA8NpF;AAED,kFAAkF;AAClF,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CA4B/C"}
|