brainllm 12.7.1 → 12.9.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/.env.example +1 -1
- package/README.md +25 -13
- package/SECURITY.md +6 -2
- package/dist/index.js +1358 -284
- package/manifest.json +4 -4
- package/package.json +7 -6
- package/server.json +5 -5
- package/skills/brainllm/SKILL.md +22 -17
- package/skills/brainllm/references/troubleshooting.md +9 -1
package/.env.example
CHANGED
|
@@ -17,7 +17,7 @@ TRILIUM_ETAPI_TOKEN=
|
|
|
17
17
|
TRILIUM_PASSWORD=
|
|
18
18
|
|
|
19
19
|
# ── Tool surface ───────────────────────────────────────────────────────────────
|
|
20
|
-
# core (default): the
|
|
20
|
+
# core (default): the 45 brain-aware tools. full: adds the 33 raw ETAPI tools.
|
|
21
21
|
# BRAINLLM_MODE=full
|
|
22
22
|
|
|
23
23
|
# ── Timezone ───────────────────────────────────────────────────────────────────
|
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
**Give Claude a memory that survives the session.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Self-hosted memory for LLMs in [TriliumNext Notes](https://github.com/TriliumNext/Trilium), structured by the server and editable by you, served over the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
10
10
|
|
|
11
|
-
[**
|
|
11
|
+
[**Site**](https://miisodev.github.io/BrainLLM/) · [Connect to Claude](https://miisodev.github.io/BrainLLM/connect.html) · [How it works](https://miisodev.github.io/BrainLLM/how-it-works.html) · [Docs](https://miisodev.github.io/BrainLLM/docs.html) · [Privacy](https://miisodev.github.io/BrainLLM/privacy.html)
|
|
12
12
|
|
|
13
|
-
[](https://github.com/miisodev/BrainLLM/releases)
|
|
14
14
|
[](https://github.com/miisodev/BrainLLM/actions/workflows/ci.yml)
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](https://bun.sh)
|
|
@@ -35,7 +35,7 @@ Anyone running an LLM as a long-running collaborator, not a one-shot Q&A tool
|
|
|
35
35
|
|
|
36
36
|
### From experiment to open source
|
|
37
37
|
|
|
38
|
-
BrainLLM began in February 2026 as a personal experiment: could an LLM operate a real, self-hosted
|
|
38
|
+
BrainLLM began in February 2026 as a personal experiment: could an LLM operate a real, self-hosted memory reliably enough to be trusted as its own memory — orienting, writing, connecting, and closing sessions without a human doing the filing? Through sustained daily, production use the answer held. The design has settled, the failure modes have been found and fixed, and the project has graduated from experiment to something **efficient and stable enough to share** — so it's now open source. It still runs the author's own sessions every day; what you're reading is the same code, not a demo. It also still carries the shape of one person's daily use — see [Adapting to your environment](#adapting-to-your-environment) for exactly what that means and what to change.
|
|
39
39
|
|
|
40
40
|
### The core principle
|
|
41
41
|
|
|
@@ -55,6 +55,8 @@ Placement, naming, labels, deduplication, relation bookkeeping, lifecycle aging,
|
|
|
55
55
|
- **Resilient plumbing** — every backend call is timeout-bounded with retry on idempotent reads; routine core writes are idempotent or duplicate-guarded, while full-mode raw overwrites remain explicit; content surgery survives the editor's own HTML rewriting; renaming a domain cascades to everything inside it; the maintenance sweep heals drift it finds.
|
|
56
56
|
- **Multi-agent by default** — all write-classified tools serialize behind a process-wide FIFO lock, so two agents (an interactive session and an automated run, say) writing through one hosted instance queue in arrival order instead of racing; reads stay fully parallel. Deletion catch-up in the daily log keeps a note deleted after its day's close from vanishing without a trace — and its window is configurable (`deletionCatchupDays` in the lifecycle policy), defaulting to Trilium's 7-day retention.
|
|
57
57
|
- **Ask in prose, split on a seam** — `consistency(subject="…")` finds every note asserting about a fact however it is phrased, no regex guessing; `consistency(pattern, staleAfterDays=N)` surfaces figures held in exactly one untouched note, the ones that rot silently because nothing disagrees with them, and skips sessions and diaries, which are history rather than claims; `diff(since="today")` reviews every note a session changed in one call; `revise(find="Phase 2", within="tr")` edits, inserts or removes a table row named by a few words inside it; `read(ids=[…])` batches a multi-note orientation into one round trip; and `split(noteId, sections=[…], into="…")` is the write half of the oversized-note problem — it lifts whole sections into a new note and leaves a pointer back. Information notes can carry a `#mandate` marker so a scoped session finds the one note it must obey without reading every note's prose.
|
|
58
|
+
- **Registers close in one call** — `revise(find, within="tr", mode="remove", closure={thread, body})` removes a register row and records its closure in that thread's entry for the day; `within=` also accepts a row as a reader sees it (`"Phase 2 | Claude | Open"`), and a table row whose cells disagree with its header is flagged at write time and by `maintain(deep)`.
|
|
59
|
+
- **Records you can prove** — sessions, diary entries and dated thread entries are append-only, and now verifiably so: every `close()` seals its day's records into a digest chained to the previous day's, and `maintain(deep)` reports any sealed record that was rewritten afterwards. Appends stay legal; edits to history do not go unnoticed.
|
|
58
60
|
|
|
59
61
|
---
|
|
60
62
|
|
|
@@ -77,7 +79,7 @@ BrainLLM (#brainLlmRoot)
|
|
|
77
79
|
| **Dated records** | diary, session, log | One per calendar day; every write lands as a timestamped addendum block — chronology is the point |
|
|
78
80
|
| **Collections** | thread, user, information, domain | Titled notes, deduplicated by normalized title within their scope |
|
|
79
81
|
|
|
80
|
-
A session follows an enforced protocol: `start()` orients (who you are, live threads, what changed since last time) → the model works, writing durable facts *as they surface* → `session()` → `addendum()` → `maintain()` → `remarks()` → `diary()` → `close()` commits the log, regenerates the daily change log, and triggers a DB backup. The pre-close gate is **enforced in code**: `close()` refuses until every step actually ran, in order — narrating "I did the steps" doesn't count, only tool calls do. The gate is *durable*, written to the session note rather than held in memory, so it survives a restart mid-session and behaves the same on stdio and HTTP.
|
|
82
|
+
A session follows an enforced protocol: `start()` orients (who you are, live threads, what changed since last time) → the model works, writing durable facts *as they surface* → `session()` → `addendum()` → `maintain()` → `remarks()` → `diary()` → `close()` commits the log, regenerates the daily change log, and triggers a DB backup. The pre-close gate is **enforced in code**: `close()` refuses until every step actually ran, in order — narrating "I did the steps" doesn't count, only tool calls do. The gate is *durable*, written to the session note rather than held in memory, so it survives a restart mid-session and behaves the same on stdio and HTTP. A session that wrote no content gets a **light close**: nothing to fold or sweep, so `close()` asks only for `session()`, `remarks()` and a short `diary()`.
|
|
81
83
|
|
|
82
84
|
Orientation is deliberately cheap. `start()` returns your singletons as **section headings** — enough for the model to know what the brain holds — and it pulls the one section that matters. Serving the entire self-model on every session meant a one-line question cost the same as a day's work; `start(depth="full")` is still there for the sessions that genuinely need it.
|
|
83
85
|
|
|
@@ -199,15 +201,15 @@ The **full mode** tools deliberately break this convention — they are named af
|
|
|
199
201
|
|
|
200
202
|
### Tool permissions
|
|
201
203
|
|
|
202
|
-
Every tool declares whether it reads, writes, or destroys, so your client can group them and you can grant permission per group instead of per call. The current full-mode split is **
|
|
204
|
+
Every tool declares whether it reads, writes, or destroys, so your client can group them and you can grant permission per group instead of per call. Every tool also carries a human-readable title ("Edit a note", "Search the brain") for permission prompts and directory listings. The current full-mode split is **34 read-only · 31 write · 13 destructive** (core mode is **23 read-only · 17 write · 5 destructive**); `revise`, `label` and `connect` count as destructive because they can overwrite or remove what was there, which lets reads run unattended while anything that touches your brain still asks.
|
|
203
205
|
|
|
204
206
|
The classification lives in one reviewable table ([`src/annotations.ts`](./src/annotations.ts)) rather than scattered across registrations, and its default is deliberately unsafe-side: a tool missing from the table is treated as a **write**, never a read, and says so at startup. Three are worth knowing because they look like reads and aren't — `start()` creates today's diary and session stubs, `session()` runs the maintenance sweep, and `graph()` writes the rendered graph note.
|
|
205
207
|
|
|
206
|
-
### Core — universal verbs (
|
|
208
|
+
### Core — universal verbs (35)
|
|
207
209
|
|
|
208
210
|
| Group | Tools |
|
|
209
211
|
|---|---|
|
|
210
|
-
| Session lifecycle | `start` · `day` · `session` · `remarks` · `close` · `backup` |
|
|
212
|
+
| Session lifecycle | `start` · `day` · `session` · `remarks` · `close` · `backup` · `health` |
|
|
211
213
|
| Writing | `remember` · `diary` · `revise` · `split` · `resolve` · `withdraw` · `recover` |
|
|
212
214
|
| Reading & search | `read` · `recall` · `domain` · `brain` · `assembly` · `outline` · `inspect` · `diff` · `template` · `consistency` |
|
|
213
215
|
| Graph | `connect` · `explore` · `graph` |
|
|
@@ -222,7 +224,7 @@ The classification lives in one reviewable table ([`src/annotations.ts`](./src/a
|
|
|
222
224
|
|
|
223
225
|
The raw ETAPI, one tool per Trilium primitive — notes, attributes, branches, revisions, attachments, calendar, system. Brain-agnostic and guard-free: an edge-case fallback for what core can't express (precise raw queries, code/canvas/mermaid notes, branch surgery, revision recovery).
|
|
224
226
|
|
|
225
|
-
The complete operational reference is [`skills/brainllm/SKILL.md`](./skills/brainllm/SKILL.md); per-topic deep dives live in [`skills/brainllm/references/`](./skills/brainllm/references/). Beyond that, the **
|
|
227
|
+
The complete operational reference is [`skills/brainllm/SKILL.md`](./skills/brainllm/SKILL.md); per-topic deep dives live in [`skills/brainllm/references/`](./skills/brainllm/references/). Beyond that, the **architecture reference** explains how BrainLLM is built and why — every module, every tool's behaviour, the sweep internals, the idempotency matrix — and is kept current with each release rather than published in this repo; see [Support the project](#support-the-project) for how to get a copy.
|
|
226
228
|
|
|
227
229
|
---
|
|
228
230
|
|
|
@@ -243,7 +245,7 @@ The included [`Dockerfile`](./Dockerfile) builds and runs the HTTP connector (tw
|
|
|
243
245
|
|
|
244
246
|
### Branding
|
|
245
247
|
|
|
246
|
-
In HTTP mode BrainLLM serves its own icons — `/icon.png`, `/icon.svg`, sized variants, and `/favicon.ico` — and advertises them in the MCP handshake, so clients show the BrainLLM mark in your connector list and beside its tool calls.
|
|
248
|
+
In HTTP mode BrainLLM serves its own icons — `/icon.png`, `/icon.svg`, sized variants, and `/favicon.ico` — and advertises them in the MCP handshake together with its title, a one-line description and its website, so clients show the BrainLLM mark in your connector list and beside its tool calls. The handshake also carries short server instructions describing the five areas and the session shape. Everything brand-shaped (name, tagline, description, the mark's geometry) is defined once in [`src/brand.ts`](./src/brand.ts), and a unit test holds `package.json`, `manifest.json` and `server.json` to the same description.
|
|
247
249
|
|
|
248
250
|
Both halves of that are load-bearing. The spec tells clients to **verify icon URIs share the server's origin**, so icons hosted anywhere else are silently dropped. And a host that serves no favicon makes clients fall back to the *registrable domain's* — a BrainLLM at `brain.yourdomain.com` would show `yourdomain.com`'s site logo, which is worse than nothing because it looks intentional.
|
|
249
251
|
|
|
@@ -253,7 +255,7 @@ To use your own artwork, replace `public/BrainLLM.svg` and run `bun run icons`
|
|
|
253
255
|
|
|
254
256
|
Claude Code and `mcp-remote` send `MCP_AUTH_TOKEN` as a header and need nothing further. **The hosted Claude surfaces — claude.ai, Claude mobile, Cowork — cannot.** Their custom-connector UI offers OAuth or nothing; there is no field for a bearer token. So BrainLLM ships its own OAuth 2.1 authorization server. It serves **both** registration mechanisms, so every OAuth-capable client works:
|
|
255
257
|
|
|
256
|
-
- **Client ID Metadata Documents (CIMD)** — the registration-free mechanism MCP
|
|
258
|
+
- **Client ID Metadata Documents (CIMD)** — the registration-free mechanism adopted in the MCP `2025-11-25` specification; this is what Claude selects.
|
|
257
259
|
- **Dynamic Client Registration (RFC 7591)** — for clients that never grew CIMD support. opencode (MCP TS SDK ≤1.29) is the forcing case: without a `registration_endpoint` it refuses to authenticate at all.
|
|
258
260
|
|
|
259
261
|
Set one variable to turn it on:
|
|
@@ -273,9 +275,11 @@ What that turns on:
|
|
|
273
275
|
| `/.well-known/oauth-protected-resource` (+ `/mcp` variant) | RFC 9728 — names the authorization server |
|
|
274
276
|
| `/.well-known/oauth-authorization-server` (+ `/mcp` variant) | RFC 8414 — advertises CIMD support, DCR, and PKCE S256 |
|
|
275
277
|
| `/authorize` | Validates the client (CIMD document or registered id), shows the consent screen |
|
|
276
|
-
| `/token` | PKCE-verified code exchange, with rotating refresh tokens |
|
|
278
|
+
| `/token` | PKCE-verified code exchange, with rotating refresh tokens stored only as SHA-256 hashes |
|
|
277
279
|
| `/register` | RFC 7591 dynamic client registration — how opencode and other DCR-only clients connect |
|
|
278
280
|
|
|
281
|
+
The consent screen names the requesting client and the address it will return to, and warns when that address is your own computer (a loopback redirect, as Claude Code uses). MCP traffic is rate-limited per bearer token, with a looser per-IP bound, so clients that share an egress range (all of Claude's hosted traffic does) never exhaust each other's budget.
|
|
282
|
+
|
|
279
283
|
Both credentials work simultaneously — a static token from Claude Code and an OAuth token from claude.ai, against the same brain. Access tokens are signed JWTs bound to your server's resource URI, so a token minted for a different MCP server is rejected; the signing secret and refresh tokens persist beside `brainllm.json`, which on a container deploy means **putting `BRAINLLM_CONFIG` on a volume** — otherwise every redeploy invalidates every token.
|
|
280
284
|
|
|
281
285
|
For config persistence across redeploys, mount a volume on the **BrainLLM service** (not the Trilium service) and set `BRAINLLM_CONFIG` to a file path inside it:
|
|
@@ -341,12 +345,15 @@ index.ts ─→ tools.ts ─┬→ trilium.ts ETAPI client: bounded/retrying
|
|
|
341
345
|
├→ journal.ts daily Insights log generation (regenerate-in-place)
|
|
342
346
|
├→ time.ts timezone-correct now/today (BRAINLLM_TZ)
|
|
343
347
|
├→ rate-limit.ts bounded public HTTP budgets
|
|
348
|
+
├→ brand.ts name, description, mark, icons, server instructions
|
|
349
|
+
├→ cap.ts the 140k-character result cap
|
|
350
|
+
├→ diffing.ts block and table-cell diffs
|
|
344
351
|
├→ network-security.ts CIMD public-network and redirect checks
|
|
345
352
|
├→ bootstrap.ts five-area tree builder
|
|
346
353
|
└→ tools-*.ts per-area surface reads · full-mode raw ETAPI
|
|
347
354
|
```
|
|
348
355
|
|
|
349
|
-
Key properties: every write is sanitized for Trilium/CKEditor 5 compatibility (mutations reported back as `sanitized[]`); a revision snapshot precedes routine content mutation; every request carries the user's local time so Trilium stamps dates in the right day; core writes are idempotent or duplicate-guarded, while raw full-mode overwrites are explicit.
|
|
356
|
+
Key properties: no tool result exceeds 140,000 characters (a larger one is cut with a note naming the narrower read); every write is sanitized for Trilium/CKEditor 5 compatibility (mutations reported back as `sanitized[]`); a revision snapshot precedes routine content mutation; every request carries the user's local time so Trilium stamps dates in the right day; core writes are idempotent or duplicate-guarded, while raw full-mode overwrites are explicit.
|
|
350
357
|
|
|
351
358
|
## Development
|
|
352
359
|
|
|
@@ -355,6 +362,7 @@ bun run dev # hot-reload dev server
|
|
|
355
362
|
bun run build # bundle to dist/index.js
|
|
356
363
|
bun run test:unit # all unit tests (Bun discovery; works on Windows and CI)
|
|
357
364
|
bun run test:http # local auth/header smoke (no Trilium required)
|
|
365
|
+
BRAINLLM_LIVE_TOKEN=... bun run test:live # read-only smoke against a deployed server, over real MCP
|
|
358
366
|
bun run test # integration tests (requires a live Trilium instance)
|
|
359
367
|
bun run validate:manifest # MCPB schema/icon validation
|
|
360
368
|
bun run init # CLI bootstrap
|
|
@@ -392,6 +400,10 @@ Not sure where to start? Issues labeled `good first issue`, doc gaps, and the tr
|
|
|
392
400
|
|
|
393
401
|
Please do not report suspected vulnerabilities in a public issue. Follow the private reporting process in [`SECURITY.md`](./SECURITY.md); never include ETAPI tokens, OAuth credentials, or private brain content in a report.
|
|
394
402
|
|
|
403
|
+
## Privacy and support
|
|
404
|
+
|
|
405
|
+
BrainLLM collects nothing: no analytics, no telemetry, no account. Your notes stay in your Trilium, and sign-in records stay on your own server. The full [privacy policy](https://miisodev.github.io/BrainLLM/privacy.html) says what is stored where. For help, open a [GitHub issue](https://github.com/miisodev/BrainLLM/issues) or email miisodev@gmail.com.
|
|
406
|
+
|
|
395
407
|
---
|
|
396
408
|
|
|
397
409
|
## Support the project
|
package/SECURITY.md
CHANGED
|
@@ -6,8 +6,8 @@ Security fixes ship on the latest release line. Older versions may remain useful
|
|
|
6
6
|
|
|
7
7
|
| Version | Supported |
|
|
8
8
|
|---|---|
|
|
9
|
-
| 12.
|
|
10
|
-
| 12.
|
|
9
|
+
| 12.9.x | ✅ |
|
|
10
|
+
| 12.8.x and earlier | ❌ |
|
|
11
11
|
|
|
12
12
|
## Reporting a vulnerability
|
|
13
13
|
|
|
@@ -26,6 +26,10 @@ Please use a private channel until a fix and coordinated disclosure path are agr
|
|
|
26
26
|
|
|
27
27
|
Run the latest tagged release rather than an unreleased branch or a mutable dependency lock. For an HTTP deployment reachable outside a trusted network, set `MCP_AUTH_TOKEN`; configure `BRAINLLM_OWNER_PASSWORD` only when hosted OAuth clients require it. Keep the Trilium instance, ETAPI token, OAuth store, and `brainllm.json` on protected storage, and restrict network access at the platform edge.
|
|
28
28
|
|
|
29
|
+
Since 12.8.0 the OAuth store keeps refresh tokens only as SHA-256 hashes (a token issued earlier is accepted once and replaced by a hashed successor on its next refresh), MCP traffic is rate-limited per bearer token with a looser per-IP bound, and the consent screen shows where an approval will be sent and warns when that is the local machine.
|
|
30
|
+
|
|
31
|
+
Since 12.9.0 the body sanitizer keeps an unclosed `form`, `select`, `template` or similar opener as escaped, inert text instead of removing everything after it; unclosed `script`, `style`, `noscript` and `iframe` still take their tail with them, and closed active blocks are still removed whole. Each close also seals the day's records in a hash chain that `maintain(deep=true)` verifies, so a rewritten session, diary or thread entry is reported rather than silently trusted.
|
|
32
|
+
|
|
29
33
|
For credential rotation, replace `MCP_AUTH_TOKEN` and the ETAPI token in the provider/Trilium, set a fresh `BRAINLLM_OAUTH_SECRET` (32+ characters) to invalidate existing OAuth tokens, and remove obsolete local `*.oauth.json` / `*.token` files. Never put recovered credentials in a repository, shared context note, Docker context, or release artifact.
|
|
30
34
|
|
|
31
35
|
## Scope
|