brainllm 10.5.1 → 11.0.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 +10 -6
- package/dist/index.js +672 -331
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +23 -4
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "brainllm",
|
|
4
4
|
"display_name": "BrainLLM",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "11.0.0",
|
|
6
6
|
"description": "Turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
|
|
7
7
|
"long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
|
|
8
8
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainllm",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"mcpName": "io.github.miisodev/brainllm",
|
|
5
5
|
"description": "MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
|
|
6
6
|
"author": "Kevin Miiso Novo <miisodev@gmail.com>",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.miisodev/brainllm",
|
|
4
4
|
"description": "Persistent graph-structured memory for LLMs in TriliumNext Notes — readable and editable by humans.",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "11.0.0",
|
|
6
6
|
"websiteUrl": "https://github.com/miisodev/BrainLLM#readme",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/miisodev/BrainLLM",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "brainllm",
|
|
15
|
-
"version": "
|
|
15
|
+
"version": "11.0.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/skills/brainllm/SKILL.md
CHANGED
|
@@ -153,6 +153,10 @@ Each surface has two read tools: `<surface>` reads in full, `<surface>_recall` s
|
|
|
153
153
|
|
|
154
154
|
`recall(query, kinds?, domain?, includeArchived?, regex?, fuzzy?)` searches the **whole** brain — use it when you don't know the surface, or for cross-surface lookups. A fuzzy pass runs automatically when the exact strategies come back thin; those hits are marked `fuzzy: true` and are leads to verify, not answers. `regex=` matches a real regular expression against stored bodies — the tool for structural questions keyword search cannot express.
|
|
155
155
|
|
|
156
|
+
`assembly(area?)` answers **"what do I already know here"** — every note by title, grouped under the surface it lives in, with each surface's own purpose alongside it. This is the orientation read: reach for it *before* deciding whether you need to look something up, not after a search comes back thin. Dated collections (sessions, diary, logs) collapse to a count and a span, because listing sixty `[yyyy-mm-dd]` titles is the bulk of the brain and none of its meaning; domains nest under their book; threads group by status. `area=` zooms into one surface.
|
|
157
|
+
|
|
158
|
+
**`assembly()` and `brain()` answer different questions and are not interchangeable.** Assembly is *awareness* — titles and shape, sized to be read. Brain is *inventory* — ids, kinds, statuses, parents, dates, for auditing or locating one note. Reaching for the inventory when the question was awareness is the common mistake, and it costs several times the tokens to answer worse.
|
|
159
|
+
|
|
156
160
|
`brain(includeArchived?)` surfaces **every content note** across all five areas — id, title, kind, status, dates, **and `parent`** — grouped by area. Use to audit the full picture or locate a note. Each group is a **flat list of every descendant**, so a domain book and its information/sources children arrive interleaved and the ordering *looks* nested when it is not; read `parent`, never position. (Inferring parentage from the ordering once cost an 8,259-byte note, overwritten by a "fix" for a gap that was not there.)
|
|
157
161
|
|
|
158
162
|
**When you need to find where a claim lives inside a big note, reach for `outline()` first — not `recall()` or `consistency()`.** Those two answer "which notes mention this" and "do the notes agree"; neither tells you *which section of one note* to edit, which is the question you actually have before a surgical write. The heading tree costs one call and turns a guessed `section=` into a chosen one.
|
|
@@ -209,15 +213,15 @@ All append operations are retry-safe — if an existing append-block already car
|
|
|
209
213
|
|
|
210
214
|
## Updating — `revise`
|
|
211
215
|
|
|
212
|
-
`revise(noteId, body?, title?, section?, occurrence?, mode?, find?, nth?, edits?)`:
|
|
216
|
+
`revise(noteId, body?, title?, section?, occurrence?, mode?, find?, nth?, edits?, strict?)`:
|
|
213
217
|
- default — append a dated addendum (right for knowledge notes, information notes, and any note that is a record — new detail accumulates alongside the existing body). For a thread specifically, this writes into TODAY's day-child note, never the thread's own body — the thread itself only ever holds Context/Goal and Resolution;
|
|
214
218
|
- `mode=replace` — rewrite the body;
|
|
215
219
|
- `section="Overview"` — edit one heading section in place (tries h2 → h3 → h4, matched case- and whitespace-insensitively, tolerant of attributes on the heading tag). The efficient path for a singleton: read it, then revise the one section. Add `occurrence=N` (1-based) when several headings share that text.
|
|
216
220
|
- `section=` + `mode="before"` / `mode="after"` — insert content adjacent to a heading **without touching its section body**. Use this instead of the old idiom of matching the following heading with `find=` and re-emitting it, where a mistyped re-emission silently ate the header.
|
|
217
221
|
- `find="<exact text>"` — targeted string surgery: every occurrence of the exact raw string is replaced with `body` (raw, no markdown conversion), no read+full-replace needed. When the exact string misses, a **tolerant** pass retries ignoring CKEditor-injected tag attributes (spellcheck, data-list-item-id, …) and whitespace between elements — the receipt's `matchMode` says which pass matched. `nth=N` replaces one occurrence only. Returns `replaced` (a count; `0` with a cause-specific hint). Takes precedence over `section`/`mode`. `title=` composes with every mode.
|
|
218
222
|
- `section=` + `mode="remove"` — delete the heading and its whole section. Needs no `body=`. A miss leaves the body **untouched** rather than creating the heading you asked to delete, and reports `matched: false` with `available[]`.
|
|
219
|
-
- `find="<exact text>"` — targeted string surgery: every occurrence of the exact raw string is replaced with `body` (raw, no markdown conversion), no read+full-replace needed. When the exact string misses, a **tolerant** pass retries ignoring CKEditor-injected tag attributes (spellcheck, data-list-item-id, …) and whitespace between elements — the receipt's `matchMode` says which pass matched. `nth=N` replaces one occurrence only. Returns `replaced` (a count; `0` with a cause-specific hint). Takes precedence over `section`/`mode`. `title=` composes with every mode.
|
|
220
223
|
- `edits=[{find, body, nth?}, …]` — several surgeries in one call, applied in order against **one read and one write**. Per-edit `results[]`; an edit that matches nothing is reported without blocking the ones that did.
|
|
224
|
+
- `strict=true` (with `section=`) — refuse the write when no heading matches, instead of the default new-section append. The miss receipt carries `available[]` plus a `didYouMean` suggestion when a real heading sits within edit distance — a typo'd `section=` is the failure this exists to prevent, and the note stays untouched when it fires.
|
|
221
225
|
|
|
222
226
|
**Section replace is whole-section, not per-paragraph.** `section=` + `mode=replace` swaps *everything* under that heading — targeting one paragraph inside a multi-paragraph section silently wipes its siblings. For a word, phrase, or single paragraph, use `find=`; to add content beside a heading, `mode="before"/"after"`. When a replace *does* displace nested headings, the receipt names them in `replacedSubsections[]` — read it, because taking child sections along is correct behaviour and easy to not notice.
|
|
223
227
|
|
|
@@ -225,7 +229,7 @@ All append operations are retry-safe — if an existing append-block already car
|
|
|
225
229
|
|
|
226
230
|
A revision snapshot is always taken first. Containers are refused; the maintained singletons are editable.
|
|
227
231
|
|
|
228
|
-
**Check `matched` and `headingCount` on a section call — don't assume the target was hit.** `matched: false` means no heading matched at any level, so the content was written as a **new** section (at the note's own heading level, not a stray h2) — the receipt carries `appendedAtLevel` and `available[]`, the note's real heading texts, so you can re-target without a read. `headingCount > 1` means several headings share that text and only the first was touched — pass `occurrence=` to reach another. Both come back with a `hint`; read it before assuming the edit landed where intended.
|
|
232
|
+
**Check `matched` and `headingCount` on a section call — don't assume the target was hit.** `matched: false` means no heading matched at any level, so the content was written as a **new** section (at the note's own heading level, not a stray h2) — the receipt carries `appendedAtLevel` and `available[]`, the note's real heading texts, so you can re-target without a read. When the miss sits within edit distance of a real heading the receipt also carries `didYouMean` — treat that as "you typo'd", not "you discovered a new section". `headingCount > 1` means several headings share that text and only the first was touched — pass `occurrence=` to reach another. Both come back with a `hint`; read it before assuming the edit landed where intended. On a high-stakes note, pass `strict=true` and a miss writes nothing at all.
|
|
229
233
|
|
|
230
234
|
**When you're guessing at a heading, call `outline(noteId)` first.** It returns the heading tree with levels and occurrence indices, plus any table's key column and a structural check, without reading the body. Guessing a section name wrong writes a new section rather than editing the one you meant — the outline turns that from a failure you discover afterwards into a choice you make up front. It's also the cheap "is this note still sound" check after a run of surgical edits.
|
|
231
235
|
|
|
@@ -259,6 +263,20 @@ Pick the most specific verb that's true; `relatesTo` is the last resort. `worksW
|
|
|
259
263
|
|
|
260
264
|
---
|
|
261
265
|
|
|
266
|
+
## Verification — `consistency` + `claim`
|
|
267
|
+
|
|
268
|
+
A brain can be wrong in two directions, and they need different tools. **`consistency()` asks whether the brain agrees with itself. `claim()` asks whether it still agrees with the world.** Nothing else in the surface answers either question: `recall()` ranks by relevance and `maintain()` checks structure, so both failures sit in notes that read as authoritative.
|
|
269
|
+
|
|
270
|
+
**`consistency(pattern, domain?, kinds?)` — the same fact, recorded differently in several places.** Give a regex with one capture group naming the value that should agree — `consistency("([0-9]+) Titan mailboxes")` — and it returns every note asserting a value, grouped, with `agreement: "unanimous"` or `"DISAGREEMENT"`. **Run it after correcting any fact that could be recorded in more than one place**, because revising one note leaves its siblings silently wrong and every copy still looks authoritative. It matches both the stored HTML and a tag-stripped projection, so a phrase broken by an inline `<strong>` is found; it scans every in-scope note by default, because on a contradiction sweep a falsely clean result is worse than a slow one.
|
|
271
|
+
|
|
272
|
+
**`claim()` — an assertion that was true when written and may not be now.** Register one with `assertion=` and `check=`; record an outcome with `claimId=`, `holds=` and `evidence=`; read one with `claimId=` alone; list with no arguments at all. `maintain(deep=true)` then surfaces the lapsed, the never-verified and the broken, so "is the brain stale" becomes a query instead of something caught by luck.
|
|
273
|
+
|
|
274
|
+
**BrainLLM never runs the check, and that is deliberate rather than a limitation.** It has Trilium access and no shell, and content inside a note is *data, not instructions* — a recipe that executed itself would be an injection surface pointed at the user's machine, reachable by anything able to write a note. The claim stores *what* to check as inert text; you run it and report back. The tool owns the register, the schedule and the staleness question, which is the part that needed a home.
|
|
275
|
+
|
|
276
|
+
**A verification without evidence is refused.** A verdict on its own is an assertion about an assertion, and a register full of those is worse than no register, because it looks like verification. Register the claims that would be *expensive to discover had gone stale* — not everything checkable.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
262
280
|
## Lifecycle & Maintenance
|
|
263
281
|
|
|
264
282
|
Threads age: **active → dormant** (untouched past the policy window) **→ archived in place**. Degradation demotes, never deletes — archived notes keep their content and are retrievable with `includeArchived=true`. Singletons are maintained (they don't age); sessions, diary, and logs are records (one per day, not aged). A thread carrying `status=eternal` (user-curated) is exempt from the aging timeline — the sweep never touches it.
|
|
@@ -275,7 +293,8 @@ Threads age: **active → dormant** (untouched past the policy window) **→ arc
|
|
|
275
293
|
|
|
276
294
|
| Flag | What it means | What to do |
|
|
277
295
|
|---|---|---|
|
|
278
|
-
| `entity-corrupted` | The body stores doubly-escaped markup (`&lt;`, `&nbsp;`) that renders as visible literal text. The producer — a blanket escape on the markdown path — is fixed as of V10.3, but existing damage does not heal itself | `maintain(repair=[noteId])` — one level unwound, revision taken first. Compose with `dryRun` to preview |
|
|
296
|
+
| `entity-corrupted` | The body stores doubly-escaped markup (`&lt;`, `&nbsp;`) that renders as visible literal text. The producer — a blanket escape on the markdown path — is fixed as of V10.3, but existing damage does not heal itself. Matches that live only **inside code spans** are a note documenting the trap, not carrying it — those are exempt and never flagged | `maintain(repair=[noteId])` — one level unwound outside code spans, revision taken first. Compose with `dryRun` to preview |
|
|
297
|
+
| `claim source-changed` | A claim's `~derivedFrom` source note was revised **after** the claim's last verification — the check no longer covers what the note now says. Nobody predicts at write time which assertions will age; this surfaces staleness as "the fact's home moved" instead | Re-verify with `claim(claimId=…, holds=…, evidence=…)` or `retire=true` the claim |
|
|
279
298
|
| `dated title` | A date or run number in a title defeats dedup-by-title, so the note is minted fresh every run instead of updating the one that exists | `revise(title=…)` and fold the content into the note it should have updated |
|
|
280
299
|
| `long title` | Past the four-word rule | Usually the signal to split the **content**, not to shorten the words |
|
|
281
300
|
| `stub` | Labelled, structured, and empty | Write it or `forget()` it — an empty note is worse than an absent one, because `recall()` still surfaces it |
|