brainllm 12.8.0 → 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/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "brainllm",
4
4
  "display_name": "BrainLLM",
5
- "version": "12.8.0",
5
+ "version": "12.9.0",
6
6
  "description": "Self-hosted memory for LLMs in TriliumNext, structured by the server and editable by you.",
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.8.0",
3
+ "version": "12.9.0",
4
4
  "mcpName": "io.github.miisodev/brainllm",
5
5
  "description": "Self-hosted memory for LLMs in TriliumNext, structured by the server and editable by you.",
6
6
  "author": "Kevin Miiso Novo <miisodev@gmail.com>",
@@ -62,14 +62,14 @@
62
62
  "prepublishOnly": "bun run build"
63
63
  },
64
64
  "dependencies": {
65
- "@modelcontextprotocol/sdk": "^1.30.1",
65
+ "@modelcontextprotocol/sdk": "^1.32.0",
66
66
  "zod": "^3.25.76"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/bun": "latest",
70
70
  "ajv": "^8.20.0",
71
71
  "ajv-formats": "^3.0.1",
72
- "sharp": "^0.35.4",
72
+ "sharp": "^0.35.5",
73
73
  "typescript": "^5.9.3"
74
74
  }
75
75
  }
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": "Self-hosted memory for LLMs in TriliumNext, structured by the server and editable by you.",
5
- "version": "12.8.0",
5
+ "version": "12.9.0",
6
6
  "websiteUrl": "https://miisodev.github.io/BrainLLM/",
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.8.0",
15
+ "version": "12.9.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -36,7 +36,7 @@ END session() → [update singletons] → addendum() → maintain() → rem
36
36
 
37
37
  `start()` returns the date, **preferences and protocols in full**, the other singletons as section headings (pull one with `master(which)`/`llm(which)`, `section=` for one section; `depth="full"` inlines all), today's diary and session ids, active and dormant threads, the previous session and notes changed since. `newDay: true` on the first session of a day → call `day()` (previous session, its log, notes touched since). `day(recap=true)` returns everything written today, in order, across sessions, diary and thread entries.
38
38
 
39
- **Close protocol.** `session()` returns singleton stubs, `changedToday` (the maintained notes this day's writes touched: check every figure you changed agrees everywhere it is recorded, with `diff(since="today")` and `consistency()`), `pending=` (what each remaining step has to do), `audit=` (do the singletons agree, and do the LLM ones still serve the master ones) and `next[]`. Update master singletons with what you learned about the user and LLM singletons with what you learned about yourself, fold any addenda (`addendum()`), `maintain()`, `remarks()` for diary cues, `diary()` your closing record, then `close(summary, identity)`. `close()` refuses until those steps ran and `session → remarks → diary` held; the gate is durable across restarts. `force=true` bypasses a step with nothing to do (reported). `continuing=true` is a second close on the same day. Scoped or autonomous runs pass `session(scope="agent")`.
39
+ **Close protocol.** `session()` returns singleton stubs, `changedToday` (the maintained notes this day's writes touched: check every figure you changed agrees everywhere it is recorded, with `diff(since="today")` and `consistency()`), `pending=` (what each remaining step has to do), `audit=` (do the singletons agree, and do the LLM ones still serve the master ones) and `next[]`. Update master singletons with what you learned about the user and LLM singletons with what you learned about yourself, fold any addenda (`addendum()`), `maintain()`, `remarks()` for diary cues, `diary()` your closing record, then `close(summary, identity)`. `close()` refuses until those steps ran and `session → remarks → diary` held; the gate is durable across restarts. `force=true` bypasses a step with nothing to do (reported). `continuing=true` is a second close on the same day. Scoped or autonomous runs pass `session(scope="agent")`. **Light close:** a session that wrote no content gets `closeMode: "light"` from `session()` — only `remarks()`, a one-paragraph `diary()` and `close()` follow; any content write since the last close brings the full protocol back. Every `close()` also seals its day's records, which `maintain(deep)` later verifies.
40
40
 
41
41
  **Write during the session, not at the end** — a fact written mid-conversation survives a crash.
42
42
 
@@ -93,14 +93,14 @@ worth keeping?
93
93
  | a knowledge note | `knowledge(id)` — `section=` for one section |
94
94
  | a day's change log | `insights(date?)` |
95
95
  | skim a surface | `<surface>_recall` |
96
- | everything about an area | `domain(name)` — the reliable path; reach here first |
97
- | search | `recall(query, domain=…)` — scope it; `regex=` for structure; fuzzy hits are leads |
96
+ | everything about an area | `domain(name)` — the reliable path; reach here first. `outline=true` adds every maintained note's headings and size, for planning a multi-note rewrite |
97
+ | search | `recall(query, domain=…)` — scope it; `regex=` for structure, each hit with the matched passage as `evidence` (records included; `kinds=` narrows); fuzzy hits are leads; recent notes rank slightly higher |
98
98
  | several bodies at once | `read(ids=[…])` (≤ 10). Bodies stop at ~80k characters; the rest come back in `deferred[]` for a second call. `text=true` returns readable plain text for orienting (not for building a `find=` anchor) |
99
99
  | storage and growth | `health()` — Trilium version, estimated database size, heaviest notes and their revisions, named backups |
100
100
  | what the brain holds | `assembly(area?)` — titles by surface with purposes |
101
101
  | inventory / locate by id | `brain()` — read `parent`, not position |
102
102
  | a note's heading tree | `outline(id)` — before any `section=` edit you're unsure of |
103
- | raw labels, relations, body | `inspect(id, content?, section?, find?)` |
103
+ | raw labels, relations, body | `inspect(id, content?, section?, find?)` — `find=` also returns the stored element (row, item, paragraph) around each match |
104
104
 
105
105
  Every read that can be large takes `section=`; a note past the read ceiling cannot be returned whole.
106
106
 
@@ -121,9 +121,9 @@ Every read that can be large takes `section=`; a note past the read ceiling cann
121
121
 
122
122
  - **Dedup is by title.** Generic titles (Current State, Sources) exist in many domains — pass `mustCreate=true` when you mean to create, and read `action` on every receipt.
123
123
  - **Wire at creation:** `connect=[{relation, toNoteId}]` on the same call. An unconnected note is an orphan until wired.
124
- - **Identity line** (`"LLM · environment · agent/mode [· Run N]"`) is required on diary, close and dated thread appends.
124
+ - **Identity line** (`"LLM · environment · agent/mode [· Run N]"`) is required on diary, close and dated thread appends. It is checked on write: at least three `·`-separated parts, one line, no draft text — a malformed one is refused with nothing written.
125
125
  - **Every note carries an icon except logs.** The server sets the kind default at creation (a thread entry takes its thread's icon) and the sweep backfills any missing; `icon=` picks a better one (boxicons class or bare name). Removing an icon is refused.
126
- - Bodies may be text, markdown or HTML; the server normalises them to editor-native HTML and reports `sanitized[]` (markdown mixed into an HTML body is converted; a placeholder like `<name>` stays as text).
126
+ - Bodies may be text, markdown or HTML; the server normalises them to editor-native HTML and reports `sanitized[]` (markdown mixed into an HTML body is converted; a placeholder like `<name>` — even one named like an element, `<template>`, `<select>` — stays as text and never truncates what follows).
127
127
  - `diary`, `session`, `log`, `claim` and `domain` have dedicated paths; `remember` refuses them.
128
128
 
129
129
  **Four writing rules** (each applies to every note you touch):
@@ -149,10 +149,12 @@ Read `template(kind)` before your first write of a kind, then read a sibling.
149
149
  | `section=` + `mode="remove"` | delete the section |
150
150
  | `find="<text>"` | replace every occurrence (`nth=` for one) — exact first, then through re-serialized tags, then ignoring inline formatting (`matchMode` says which) |
151
151
  | `edits=[{find, body}]` | several surgeries in one read and one write |
152
- | `find="<short anchor>"` + `within="tr"` | act on the element containing the anchor: replace it (default), `mode="before"/"after"` to insert a row beside it, `mode="remove"` to delete it (`li`, `p`, `td` … too) |
152
+ | `find="<short anchor>"` + `within="tr"` | act on the element containing the anchor: replace it (default), `mode="before"/"after"` to insert a row beside it, `mode="remove"` to delete it (`li`, `p`, `td` … too). `within` names what is replaced: `"tr"` takes a whole `<tr>…</tr>` body, `"td"` one `<td>…</td>`; a plain `find=` inside a cell replaces only that text |
153
+ | `within="tr"` + `mode="remove"` + `closure={thread, body}` | close a register row in one call: the row goes, and the closure (quoting it) lands in that thread's entry for today. Needs `identity=`; run `consistency()` after on any fact the row asserted |
153
154
  | `domain="<name>"` + `find=`/`edits=` (no noteId) | the same find/replace across every maintained note in a domain; a dry run listing each match unless `dryRun=false`. Records are never touched. `dryRun` anywhere else is refused |
154
155
 
155
- - **Tables are edited by row.** Anchor on a few words unique to the row and pass `within="tr"`; never copy a whole stored row into `find=`. An anchor in several rows is refused with previews; `nth=` picks one.
156
+ - **Tables are edited by row.** Anchor on a few words unique to the row and pass `within="tr"`; never copy a whole stored row into `find=`. The row's visible text works too (`"Phase 2 | Claude | Open"`), and quotes match in any form. An anchor in several rows is refused with previews; `nth=` picks one. Write one `<td>` per column — a row whose cells disagree with its header comes back as `tableShape`.
157
+ - **"Last updated" lines are server-owned.** Every content write bumps them (the receipt says `lastUpdated`); never `find=` on one.
156
158
  - A replacement equal to its match writes nothing and reports `unchanged`.
157
159
  - A revision is taken before every content write; `diff(noteId)` shows what the last write changed, and `diff(since="today")` reviews every note you changed today in one call — run it before closing.
158
160
  - **Check the receipt.** `matched: false` means a new section was written — `available[]` lists real headings and `didYouMean` catches typos; `strict=true` refuses instead. `headingCount > 1` means only the first match was touched. `replacedSubsections[]` names nested headings a section replace took with it.
@@ -180,7 +182,7 @@ Read `template(kind)` before your first write of a kind, then read a sibling.
180
182
 
181
183
  ## Verification — `consistency` and `claim`
182
184
 
183
- - **`consistency(pattern | subject)`** — does the brain agree with itself? A regex with one capture group (or a fact in prose) returns every asserting note grouped by value. **Run it after correcting any fact that could be recorded in more than one place.** `staleAfterDays=N` reports values held in only one note untouched N+ days. Records (sessions, diary, logs, dated thread entries) are skipped because they are never rewritten; `includeRecords=true` brings them back.
185
+ - **`consistency(pattern | subject)`** — does the brain agree with itself? A regex with one capture group (or a fact in prose) returns every asserting note grouped by value. **Run it after correcting any fact that could be recorded in more than one place.** `staleAfterDays=N` reports values held in only one note untouched N+ days. Records (sessions, diary, logs, dated thread entries) are skipped because they are never rewritten; `includeRecords=true` brings them back. Without a capture group the whole match is the value; matching ignores case unless `caseSensitive=true`; `patterns=[…]` checks several facts over one read of each note.
184
186
  - **`claim(...)`** — does the brain still agree with the world? Register `assertion` + `check`; verify with `claimId` + `holds` + `evidence` (evidence required); read with `claimId`; list with no arguments. BrainLLM never runs the check — you do. Register claims that would be expensive to discover had gone stale.
185
187
 
186
188
  ---
@@ -194,7 +196,8 @@ Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=tr
194
196
  | stale | revise, resolve, or `ack=[id]` if correct as it stands |
195
197
  | orphan / sink | `connect()` |
196
198
  | dated prose | a timeless note carrying dates — move state to Current State, history to a thread entry. A register (a backlog, a defects list) whose rows carry evidence dates takes `label(id, "register", value="")`, which exempts its tables but not its prose |
197
- | duplicate heading / unbalanced tags / incomplete | fix with `revise` |
199
+ | duplicate heading / unbalanced tags / incomplete / table shape | fix with `revise` (a row: `within="tr"`) |
200
+ | record seal | a record of a sealed day was rewritten after `close()` sealed it — read its revisions and `diff(noteId)`; records are append-only, so restore it unless the change was intended |
198
201
  | oversized / section-edit-risk / size trajectory | `section=` reads, `split()` |
199
202
  | dated / long title | retitle, fold into the note it should have updated, or split |
200
203
  | stub | write it or `forget()` it |
@@ -33,10 +33,14 @@
33
33
  | `read(ids=[…])` returns `deferred[]` | The bodies passed ~80k characters together; read the deferred ids in a second call, or pass `text=true` |
34
34
  | `health()` flags a note's revisions | Revision history is most of the database's weight. Splitting the note, or tightening Trilium's revision-snapshot interval, bounds it |
35
35
  | Deep maintenance flags the same items every session | Act on them, or `maintain(ack=[…])` the ones you reviewed and found correct |
36
- | `revise(find=)` returns `replaced: 0` | Read the hint — it names the cause. Escaped search string (`&lt;h3&gt;` against stored real tags): pass the tag literally. Spans a block boundary (`</h3>` then `<ol>`): anchor inside one element, or use `section=` with `mode="before"/"after"`. Otherwise it was already replaced, or the text genuinely differs — check with `inspect(noteId, find="<shorter substring>")` |
36
+ | `revise(find=)` returns `replaced: 0` | Read the hint — it names the cause. Escaped search string (`&lt;h3&gt;` against stored real tags): pass the tag literally. Spans a block boundary (`</h3>` then `<ol>`): anchor inside one element, or use `section=` with `mode="before"/"after"`. Ends in a closing tag (`…</strong></li>`): use `within="li"` with a short plain anchor. Otherwise it was already replaced, or the text genuinely differs — check with `inspect(noteId, find="<shorter substring>")` |
37
37
  | `revise(section=)` returns `matched: false` | No heading matched, so a NEW section was written — at the note's own level, with the note's real headings in `available[]`. Re-target from that list; `outline(noteId)` gives the same tree plus levels and occurrence indices before you write |
38
38
  | Two sections legitimately share a heading | `revise(section=…, occurrence=2)`. Repeating a closing heading under every category is the consistency rule working, not drift — `occurrence=` is how you reach the second one without rewriting the whole section |
39
39
  | Note body shows literal `&lt;p&gt;` text in Trilium | Entity-corrupted: `maintain(repair=[noteId])` unwinds one level in place |
40
+ | `malformed_identity` | The identification line needs three or more `·`-separated parts on one line (`"Claude Opus 5.5 · Claude Code · Interactive"`), no question marks, not an `Addendum —` marker. Nothing was written; retry with a clean `identity=` |
41
+ | `close()` asks for only `session`, `remarks`, `diary` | Nothing was written this session (light close). After any content write it asks for `addendum()` and `maintain()` too |
42
+ | `maintain(deep)` reports `record seal` | A session, diary or dated thread entry of a sealed day was rewritten after its close. Appends are fine; this is a change to earlier text. `diff(noteId)` and the note's revisions show it; restore unless intended. The finding leaves after seven sealed days |
43
+ | A regex `recall` returns nothing although the text is there | Trilium strips diacritic characters (`·`, accents) from note text before its pre-filter matches, so BrainLLM normalises the pattern the same way and the result says so; if it still misses, the 30-candidate cap may be the reason (named in the note) — narrow with `domain=`/`kinds=` or use `consistency()`, which scans exhaustively |
40
44
  | Unsure whether a note is still structurally sound after several edits | `outline(noteId)` — duplicate headings, unbalanced tags and size in one call, no body read. `maintain(deep=true)` runs the same check across the brain |
41
45
  | `recall` returns odd results | It already filters untyped notes; if it persists, `maintain(deep=true)` then retry |
42
46
  | Deep maintenance flags something unfamiliar | Every finding names its own fix in the flag text — read it rather than guessing |