brainllm 12.0.0 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "brainllm",
4
4
  "display_name": "BrainLLM",
5
- "version": "12.0.0",
5
+ "version": "12.2.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": "12.0.0",
3
+ "version": "12.2.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": "12.0.0",
5
+ "version": "12.2.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": "12.0.0",
15
+ "version": "12.2.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -219,11 +219,11 @@ All append operations are retry-safe — if an existing append-block already car
219
219
  - 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;
220
220
  - `mode=replace` — rewrite the body;
221
221
  - `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.
222
- - `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.
222
+ - `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. `mode="prepend"` is their mirror: insert at the **top of the section body**, below its heading — the way to put a block at the top of a long section without re-emitting anything.
223
223
  - `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.
224
224
  - `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[]`.
225
225
  - `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.
226
- - `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.
226
+ - `strict=true` — refuse rather than write when the call would be wrong, and leave the note untouched. Two things trigger it: a `section=` miss (no heading matches — the miss receipt carries `available[]` plus a `didYouMean` suggestion when a real heading sits within edit distance), and — on both `revise` and `remember` — a **body needing structural repair**: if the sanitizer had to close unclosed tags or promote `<br>` runs to paragraph separators, the write is refused with the repair report instead of landing damaged behind a receipt that reads as reassurance. Cosmetic normalizations (h1 demotion, div→p, style stripping) are never a refusal.
227
227
 
228
228
  **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 a sibling block around a whole section, `mode="before"/"after"` — both are **section**-relative, so `"after"` lands past the end of that section's body and inserting a heading with it creates a new section rather than nesting one under the target. 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.
229
229
 
@@ -233,12 +233,14 @@ A revision snapshot is always taken first. Containers are refused; the maintaine
233
233
 
234
234
  **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.
235
235
 
236
- **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.
236
+ **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. On notes 15k+ the headings also carry a first-block **preview** of their section — the orientation read for oversized notes, where the heading list alone assumes you already know which section you want. 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.
237
237
 
238
238
  **A `find=` miss names its own cause AND shows you the stored text — read the hint rather than trying variants.** Four distinct causes, each with a different fix: an **entity-escaped search string** (see the asymmetry above), a find that **spans a block boundary** (anchor inside one element, or use `section=`), a string **already consumed by an earlier edit in the same `edits=` array** (the expected result, not a failure), or the text genuinely differing. In that last case the receipt carries `matchedUpTo` — the longest fragment of your string that *is* present — and `storedNearby`, the stored text around it, so you can see how it really differs instead of guessing a variant. Whitespace between elements is never the cause; the tolerant pass handles it.
239
239
 
240
240
  **Verify a run of surgical edits with `diff(noteId)`, not another full read.** It compares the note against the revision your last write snapshotted, so "what did that actually change" is one call. Trusting receipts alone is how a section replace that displaced four subsections went unnoticed.
241
241
 
242
+ **Concurrent sessions serialize automatically.** Every write-classified tool runs behind a process-wide FIFO lock, so two agents — an interactive session and an automated run, or two automated lanes — writing through the same hosted instance queue in arrival order instead of racing, and a read-modify-write can no longer silently discard the other's write. Reads stay fully parallel. You never manage this; it is why several sessions can work the brain at once without lost writes. Scope, stated honestly: the lock is per process — a human editing in the Trilium UI inside a handler's read→PUT window is outside it, and revisions remain the recovery story there.
243
+
242
244
  **Merge, don't stack — the mechanics.** Master, LLM singletons, and Knowledge notes (including every per-domain Sources note and information note) are clean structured documents, so `revise()`'s *default* append mode is the wrong one for them: reach for `section=` or `mode=replace` to absorb the content into the body, and `find=`/`edits=` for anything smaller than a section. Default append is correct only on sessions, diary entries, logs, and thread day-children. The rule and its rationale are stated in full under Writing.
243
245
 
244
246
  ---
@@ -304,6 +306,8 @@ Threads age: **active → dormant** (untouched past the policy window) **→ arc
304
306
  | `consolidate` | A thread with many day-children is expensive to walk | Fold what the thread has **established** into its Context with `revise(section="Context")`, so a successor reads the conclusion rather than replaying the history |
305
307
  | `unverified sources` | A Sources note whose Revision table still holds only its placeholder row — every ✅ above it claims a verification the table does not record | `remember(kind="sources", revision=[{source, marker, date}])` |
306
308
  | `oversized` | Past the read ceiling | Read via `section=`/`find=`/`outline()`, or split |
309
+ | `deletion` | A note under the brain root was soft-deleted within the last 7 days — the window before Trilium's eraser removes both the note and the feed's own record of the deletion. Unscoped deep runs only (a deleted note's labels are gone with it, so it cannot be attributed to a lane) | `undelete_note` if unintended, or leave it; the trace self-destructs after Trilium's retention window |
310
+ | `size trajectory` | The note grew ≥30% since the last deep run that read it and is 20k+ — corrections compounding faster than the note can absorb. A rate, kept alongside the `oversized` threshold precisely because a permanently-oversized note stops hearing that one | Trim or split deliberately now, per the Note Trimming protocol — not under pressure later |
307
311
 
308
312
  Most of these are computed **server-side** from note properties rather than by reading bodies, which is why the orphan/sink and oversized reports no longer carry caps — they are complete rather than truncated. `coverage[]` still appears when a pass that genuinely must read bodies (the structural lint) hits its limit.
309
313
 
@@ -319,7 +323,7 @@ Most of these are computed **server-side** from note properties rather than by r
319
323
  | `day(date?, recap?)` | The new-day sweep payload in one call: previous session in full + that day's log + notes touched since + the current month's deliverables note in full, plus the sweep protocol as `next[]`. Call when start() flags `newDay`. `recap=true` instead returns every addendum written TODAY across sessions, diary and thread children, chronologically and attributed — for arriving mid-day after autonomous runs. |
320
324
  | `session(date?, full?, scope?)` | Mandatory pre-close step. Fetches master and LLM singletons as `{id, lastModified, relations}` stubs (light by default — start() already served them in full; fetch via master()/llm() only what you'll revise), today's diary as `{id, blocks, size}`, and runs the lite maintenance sweep. `full=true` inlines singleton content and the diary body (rarely needed). `scope="agent"` omits the user's-singleton steps for a scoped/autonomous run. Returns `next[]` driving the full end-of-session protocol, ending in `remarks()` → `diary()` → `close()`. Call before `close()`; idempotent. |
321
325
  | `remarks()` | Cue-only diary prompt, called before `diary()` — returns two cue banks (experience: what the session was like / your opinions / your existence here; brainllm: capability walls, bugs, usability & efficiency, roadmap) and satisfies its gate step on the call. Writes nothing; answer as first-person prose in today's diary via `diary()`. |
322
- | `close(summary, title?, identity, learned?, date?, backup?, continuing?, force?)` | Commit the session log ([yyyy-mm-dd] note; identity= REQUIRED — the canonical h3 identification line; title param above Summary) + backup + daily log (regenerated in place) + idempotent session↔log wiring. **Refuses unless `session()`, `addendum()`, `maintain()`, `remarks()`, and `diary()` each ran this session AND `session() → remarks() → diary()` held (last calls)** — returns `{error, detail, hint}` naming what's missing or out of order; `force=true` bypasses the gate (reported back as `bypassed`) but never the identity requirement. `continuing=true` is the SECOND close of a day: skips the ceremonial re-run, and is verified against today's note so it can't stand in for a first close. On success returns `{action, noteId, date, backup, log}` and resets the gate. Once, last. |
326
+ | `close(summary, title?, identity, learned?, date?, backup?, continuing?, force?)` | Commit the session log ([yyyy-mm-dd] note; identity= REQUIRED — the canonical h3 identification line; title param above Summary) + backup + daily log (regenerated in place, plus a deletion catch-up that regenerates past days' logs when the change feed shows deletions ≤7 days old — a deletion landing after that day's close is no longer invisible) + idempotent session↔log wiring. **Refuses unless `session()`, `addendum()`, `maintain()`, `remarks()`, and `diary()` each ran this session AND `session() → remarks() → diary()` held (last calls)** — returns `{error, detail, hint}` naming what's missing or out of order; `force=true` bypasses the gate (reported back as `bypassed`) but never the identity requirement. `continuing=true` is the SECOND close of a day: skips the ceremonial re-run, and is verified against today's note so it can't stand in for a first close. On success returns `{action, noteId, date, backup, log}` (plus `deletionCatchUp` when one ran) and resets the gate. Once, last. |
323
327
  | `assembly(area?, includeArchived?)` | **What the brain HOLDS**, shaped for awareness rather than audit: every note by title grouped under its surface, with each surface's engraved purpose alongside. Dated collections (sessions, diary, logs) collapse to a count and a date span — 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. Read this before deciding whether you need to look something up. |
324
328
  | `brain(includeArchived?)` | Full content tree: every typed note across all five areas, grouped, with id/kind/status/parent/dates. The **inventory** — reach for it to audit or locate, and for `assembly()` when the question is what you know. |
325
329
  | `bootstrap()` | Initialize the structure if uninitialized, or verify and refresh config if it already exists. Only creates a new tree when the stored root note is confirmed deleted in Trilium (404). Any other error (network, auth, timeout) is surfaced rather than silently creating a duplicate tree. |
@@ -332,8 +336,8 @@ Most of these are computed **server-side** from note properties rather than by r
332
336
  | `claim(...)` | **Does the brain still agree with the WORLD?** `consistency()` compares notes to each other; nothing asked whether an assertion is still true of the codebase, config or live surface it describes — so a claim that quietly stopped being true stayed authoritative until something downstream broke. Register one with `assertion=` + `check=`, record an outcome with `claimId=` + `holds=` + `evidence=`, read one with `claimId=` alone, list with no arguments. **BrainLLM never executes the check** — it has no shell, and note content is data rather than instructions; you run it and report back. `maintain(deep=true)` surfaces lapsed, unverified and broken claims. |
333
337
  | `diff(noteId, revisionId?)` | What a write actually changed: the revision snapshot against current content, defaulting to the most recent. Every content write already took a revision and nothing could read one back, so verifying surgical edits meant re-reading the whole note or trusting the receipts. Returns the revision index on every call. |
334
338
  | `<surface>` / `<surface>_recall` | Read a surface in full (`section=` for one section) / skim it (master, llm, memory, knowledge, insights). |
335
- | `revise(noteId, …)` | Append / replace / section-edit / insert / find-replace a note. Thread appends REQUIRE `identity=`. Section: h2/h3/h4 tolerant match, `occurrence=` to disambiguate, `mode="before"/"after"` to insert a sibling block around the whole section (section-relative, so "after" lands *past* that section's body, never between the heading and it); returns `matched`/`headingCount`/`available[]` — check them. `find=` + `body=`: exact-string surgery with an attribute- and whitespace-tolerant fallback (`matchMode` in the receipt), `nth=` for one occurrence; `edits=[{find, body}]` for several in one read/write. Misses return a cause-specific hint. `title=` composes with every mode. Retitling a domain book cascades `#domain` to its children. `Last updated` lines bumped server-side. Snapshot taken on content writes (not metadata-only). |
336
- | `outline(noteId)` | The heading tree without the body: every h2–h4 with level, occurrence index, and — for headings carrying inline markup — `raw`, their stored form (`section=` matches `text`, `find=` matches `raw`). Plus the note's section level, any table's key column, size, and structural findings (duplicate headings, unbalanced tags). Read it before a `section=` edit you're guessing at, and after a run of surgical edits to confirm the note is still sound. |
339
+ | `revise(noteId, …)` | Append / replace / section-edit / insert / find-replace a note. Thread appends REQUIRE `identity=`. Section: h2/h3/h4 tolerant match, `occurrence=` to disambiguate, `mode="before"/"after"` to insert a sibling block around the whole section (section-relative, so "after" lands *past* that section's body, never between the heading and it), `mode="prepend"` to insert at the top of the section body below its heading; returns `matched`/`headingCount`/`available[]` — check them. `find=` + `body=`: exact-string surgery with an attribute- and whitespace-tolerant fallback (`matchMode` in the receipt), `nth=` for one occurrence; `edits=[{find, body}]` for several in one read/write. Misses return a cause-specific hint. `title=` composes with every mode. `strict=true` refuses a section miss AND a body needing structural repair. Retitling a domain book cascades `#domain` to its children. `Last updated` lines bumped server-side. Snapshot taken on content writes (not metadata-only). |
340
+ | `outline(noteId)` | The heading tree without the body: every h2–h4 with level, occurrence index, and — for headings carrying inline markup — `raw`, their stored form (`section=` matches `text`, `find=` matches `raw`). On notes 15k+ each heading also carries a first-block `preview` of its section — the orientation read for oversized notes. Plus the note's section level, any table's key column, size, and structural findings (duplicate headings, unbalanced tags). Read it before a `section=` edit you're guessing at, and after a run of surgical edits to confirm the note is still sound. |
337
341
  | `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
338
342
  | `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
339
343
  | `attach(noteId, title, content?, …)` | Upsert a raw artifact on a note by title (content provided), or read it back (content omitted). Binary is base64. |
@@ -344,7 +348,7 @@ Most of these are computed **server-side** from note properties rather than by r
344
348
  | `domain(name, …)` | Surface all content for a named domain/topic/project, grouped by kind. |
345
349
  | `connect(from, relation, to, remove?)` | Typed edge from the closed vocabulary; symmetric handled; idempotent. |
346
350
  | `explore(noteId, mode, …)` | Graph: links / backlinks / neighborhood / path. |
347
- | `inspect(noteId, content?, section?, find?)` | Full raw read of one note: every label (not just noteType/status), every outbound relation, the attachment inventory (id/title/mime/role/size), type/mime/parent/child ids, dates — the raw body when content=true, `section=` narrows `content=true` to one section, and `find=` counts a literal string (total + per-addendum-block) for flag-staleness tracking — returning the nearest present fragment and the stored text around it when the count is zero. The deep-dive counterpart to explore() and the surface reads. |
351
+ | `inspect(noteId, content?, section?, find?)` | Full raw read of one note: every label (not just noteType/status), every outbound relation, the attachment inventory (id/title/mime/role/size), type/mime/parent/child ids, dates — the raw body when content=true, `section=` narrows `content=true` to one section, and `find=` counts a literal string (total + per-addendum-block + which heading each match sits under, in `sections`) for flag-staleness tracking — returning the nearest present fragment and the stored text around it when the count is zero. The deep-dive counterpart to explore() and the surface reads. |
348
352
  | `addendum()` | Search Master, LLM singletons (responsibilities + protocols, not diary), and Knowledge for pending addendum blocks. These notes must be clean and structured — fold each block into the relevant section with revise(section=…, mode=replace), then leave no addendum marker. Only sessions, diary, and logs accumulate addendum history. Scoped/autonomous agents fold only what's in their lane — leaving personal/out-of-scope addendums for the next interactive session is correct; the call itself satisfies the gate. |
349
353
  | `maintain(deep?, dryRun?, domain?, ack?)` | Lite: thread aging + unlabeled-node check per typed container. Deep adds: unlabeled thread-children check (each thread's own day-children, one level deeper than the lite pass) + stale-review + orphan/sink report (Memory/Threads + Knowledge, brain-wide inbound detection) + structural lint + duplicate-title detection + exact-duplicate relation-edge cleanup. `ack=[noteId]` marks a note reviewed-and-correct — quiet until its content changes (`suppressed` counts what was withheld). `repair=[noteId]` unwinds one level of entity corruption in place, revision first. `domain=` narrows the deep passes to one lane — and a scoped run says so in `coverage`, so a clean scoped report is never mistaken for a clean brain. An unscoped run that finds things returns `laneHint`: **leave flags that are not yours rather than acking them** — an acknowledgement asserts you read the note. `coverage[]` names any capped pass. Report includes `policy` (active thresholds). |
350
354
  | `forget(noteId, reason?, hard?)` | Archive (default) or hard-delete (blocked while backlinked). Undo with recover(). |