brainllm 12.1.0 → 12.3.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 +6 -4
- package/dist/index.js +1857 -1783
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
- package/skills/brainllm/SKILL.md +25 -11
- package/skills/brainllm/references/domains.md +6 -0
- package/skills/brainllm/references/taxonomy.md +1 -0
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.
|
|
5
|
+
"version": "12.3.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.
|
|
3
|
+
"version": "12.3.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.
|
|
5
|
+
"version": "12.3.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
|
-
|
|
15
|
+
"version": "12.3.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"name": "BRAINLLM_MODE",
|
|
42
|
-
"description": "core (default) exposes the
|
|
42
|
+
"description": "core (default) exposes the 44 brain-aware tools; full additionally exposes the 33 raw ETAPI tools.",
|
|
43
43
|
"isRequired": false,
|
|
44
44
|
"format": "string",
|
|
45
45
|
"default": "core"
|
package/skills/brainllm/SKILL.md
CHANGED
|
@@ -30,9 +30,11 @@ DURING remember(...) ← the moment something worth keeping a
|
|
|
30
30
|
<surface> / _recall ← read a surface in full, or skim it (section= reads ONE section — the read twin of revise(section=))
|
|
31
31
|
domain(name) ← surface all content for a domain/topic/project — the RELIABLE retrieval path; reach here first
|
|
32
32
|
recall(...) ← ranked search; pass domain= whenever you know the area
|
|
33
|
-
consistency(pattern)
|
|
33
|
+
consistency(pattern|subject) ← does the brain agree with itself? pattern= to compare a captured value; subject= to find notes asserting about a fact however phrased; staleAfterDays= to surface single-copy figures that rot silently. Run after correcting a fact recorded in >1 note
|
|
34
|
+
read(ids=[…]) ← batched multi-note read — several bodies, one round trip
|
|
34
35
|
claim(...) ← does the brain still agree with the WORLD? register a checkable assertion; verify on a schedule
|
|
35
36
|
revise(...) ← edit a note (section= for whole sections, find=/edits= for surgery, mode=before/after to insert, mode=remove to delete)
|
|
37
|
+
split(noteId, sections, into) ← the trim affordance: lift whole sections into a new note, leaving a pointer — the write half of the oversized-note problem
|
|
36
38
|
outline(noteId) ← the heading tree + structural check — read BEFORE a section= edit you're guessing at
|
|
37
39
|
diff(noteId) ← what your last write actually changed, against the revision it snapshotted
|
|
38
40
|
resolve(...) ← close a thread with its outcome
|
|
@@ -155,6 +157,8 @@ Each surface has two read tools: `<surface>` reads in full, `<surface>_recall` s
|
|
|
155
157
|
|
|
156
158
|
`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.
|
|
157
159
|
|
|
160
|
+
**`read(ids=[…])` is the batched read — several bodies, one round trip.** The orientation step is a fan-out (a dozen notes, several huge) and N separate calls is N chances to time out mid-read; `read()` collapses it into one call, up to 10 ids, returning each body verbatim regardless of kind. Reach for it when you need whole bodies across kinds; prefer the kinded `knowledge()`/`memory()`/`llm()` for a single note, and a `section=` read for one huge note — `read()` returns whole bodies.
|
|
161
|
+
|
|
158
162
|
`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.
|
|
159
163
|
|
|
160
164
|
**`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.
|
|
@@ -219,11 +223,11 @@ All append operations are retry-safe — if an existing append-block already car
|
|
|
219
223
|
- 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
224
|
- `mode=replace` — rewrite the body;
|
|
221
225
|
- `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.
|
|
226
|
+
- `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
227
|
- `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
228
|
- `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
229
|
- `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`
|
|
230
|
+
- `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
231
|
|
|
228
232
|
**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
233
|
|
|
@@ -233,14 +237,18 @@ A revision snapshot is always taken first. Containers are refused; the maintaine
|
|
|
233
237
|
|
|
234
238
|
**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
239
|
|
|
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.
|
|
240
|
+
**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
241
|
|
|
238
242
|
**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
243
|
|
|
240
244
|
**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
245
|
|
|
246
|
+
**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.
|
|
247
|
+
|
|
242
248
|
**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
249
|
|
|
250
|
+
**When a note is past the read ceiling, the right move is `split()`, not more surgery.** A note too big to return whole and flagged by `maintain()`'s oversized check has usually grown two or three subjects under one title — that is a structural problem surgery can't fix, only a split can. `split(noteId, sections=[…], into="<title>")` lifts whole sections (heading + body, nested sub-sections with their parent) into a new note under the same parent, typed and labelled like the source, and leaves a pointer plus a `~references` edge in the source. Splitting on a semantic seam (a live queue from a do-not-re-raise index, one sub-category from another) turns two months of brittle `find=` surgery into one call. Outline the note first to name the seams; sections that don't match come back in `missed=` and stay put.
|
|
251
|
+
|
|
244
252
|
---
|
|
245
253
|
|
|
246
254
|
## Completing — `resolve`
|
|
@@ -269,7 +277,9 @@ Pick the most specific verb that's true; `relatesTo` is the last resort. `worksW
|
|
|
269
277
|
|
|
270
278
|
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.
|
|
271
279
|
|
|
272
|
-
**`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.
|
|
280
|
+
**`consistency(pattern | subject, domain?, kinds?, staleAfterDays?)` — 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.
|
|
281
|
+
|
|
282
|
+
**Two additions make it the tool to reach for when you re-measure a fact.** `subject="<prose>"` is the no-regex-guessing mode: give it the fact in plain words and it returns every note asserting about that subject, however phrased — tokenized and matched on significant content words, so the word-order a hand-built pattern anchored on no longer hides the answer. `staleAfterDays=N` reports the **single-copy** figures: values asserted in exactly one note whose sole note was untouched N+ days. Those are the ones that rot silently — a single copy can only agree with itself, so `consistency()`'s verdict of "unanimous" is no evidence of currentness; the `staleSingles` list is where re-measurement is owed.
|
|
273
283
|
|
|
274
284
|
**`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.
|
|
275
285
|
|
|
@@ -304,6 +314,8 @@ Threads age: **active → dormant** (untouched past the policy window) **→ arc
|
|
|
304
314
|
| `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
315
|
| `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
316
|
| `oversized` | Past the read ceiling | Read via `section=`/`find=`/`outline()`, or split |
|
|
317
|
+
| `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 |
|
|
318
|
+
| `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
319
|
|
|
308
320
|
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
321
|
|
|
@@ -319,21 +331,23 @@ Most of these are computed **server-side** from note properties rather than by r
|
|
|
319
331
|
| `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
332
|
| `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
333
|
| `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. |
|
|
334
|
+
| `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 inside the configurable window — `deletionCatchupDays`, default 7, Trilium's eraser retention — so 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
335
|
| `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
336
|
| `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
337
|
| `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. |
|
|
326
|
-
| `remember(kind, …)` | Write a note — routed, formatted, deduped server-side. New threads REQUIRE `goal=` (queried from the user); thread appends REQUIRE `identity=`; thread bodies must not carry a Resolution. `connect=[{relation, toNoteId}]` wires relations in the same call; a new connectable note without them returns an orphan-prevention hint. Sources/information receipts include `domainId`. `icon=` sets the display icon. Rejects diary/session/log/domain — each has a dedicated path. |
|
|
338
|
+
| `remember(kind, …)` | Write a note — routed, formatted, deduped server-side. New threads REQUIRE `goal=` (queried from the user); thread appends REQUIRE `identity=`; thread bodies must not carry a Resolution. `connect=[{relation, toNoteId}]` wires relations in the same call; a new connectable note without them returns an orphan-prevention hint. Sources/information receipts include `domainId`. `icon=` sets the display icon. Rejects diary/session/log/domain — each has a dedicated path. `mandate=true` (kind=information) marks a note as a standing brief — adds a `#mandate` flag surfaced in domain() and knowledge_recall, so a scoped autonomous session can find "the thing I must obey" without reading every note's prose to tell it apart from current-state fact. |
|
|
327
339
|
| `diary(body, identity, icon?, date?)` | Write to today's [yyyy-mm-dd] diary — every write lands as a timestamped `Addendum — HH:mm` block, including the first of the day; identity= ENFORCED (the canonical h3 identification line). The FINAL gate step — close() counts it only when its last call came after session() and remarks(); mid-session writes welcome. |
|
|
328
340
|
| `template(kind)` | The canonical structure for a content kind: skeleton + top-to-bottom structure + the rules writes are held to. Read before the first write of a kind — then read an existing sibling for the conventions the skeleton can't encode (heading depth, table shape, title style). Template for the schema, sibling for the shape. |
|
|
329
341
|
| `graph(noteId?, depth?, includeArchived?)` | The graph view — Mermaid flowchart of the relation graph (whole brain, or a note's neighborhood), returned AND upserted into the maintained Insights/Graph mermaid note. On-demand only — reflects the brain as of the call, not auto-refreshed after later writes; a scoped call replaces the note's content, it doesn't merge. |
|
|
330
342
|
| `recall(query, …)` | BrainLLM-wide ranked search. **Scope it — pass `domain=` whenever you know the area.** Title matches score per-token with an exact-title bonus, but an unscoped query across a large brain still ranks full-text noise alongside the note you want. `orderBy` / `orderDirection` for temporal ordering; `fastSearch` for title/label-only; `regex=` for a body pattern instead of keywords; `fuzzy=` to force the typo-tolerant pass on or off. A thin or odd result is evidence about the **query**, not the brain. |
|
|
331
|
-
| `
|
|
343
|
+
| `read(ids=[…])` | **Batched multi-note read — several bodies, one round trip.** The orientation step is structurally a fan-out (a dozen notes, some huge) and N separate surface calls is N chances to time out mid-read; this collapses it into one. Up to 10 ids; returns `{count, notes:[{id, title, kind, content, relations?}]}`. Pure read, no kind restrictions. For a single note prefer the kinded read (knowledge()/memory()/llm()); for one huge note a sectioned read is still cheaper — read() returns whole bodies. |
|
|
344
|
+
| `consistency(pattern \| subject, domain?, kinds?, staleAfterDays?)` | **Does the brain agree with itself?** Two modes. *Pattern:* give a regex with one capture group naming the value that should match across notes — `consistency("(\\d+) Titan mailboxes")` — and it returns every note asserting a value, grouped, with `agreement: "unanimous"` or `"DISAGREEMENT"`. *Subject:* give a fact in prose — `consistency(subject="the two scheduled agents")` — and get back every note asserting about it, however phrased, no regex guessing; the tool tokenizes the prose and matches a majority of its significant words, so the phrasing that defeated a hand-built pattern is found. `staleAfterDays=N` adds the **single-copy** report: figures asserted in exactly one note whose sole note was untouched N+ days — the ones that rot silently, because a single copy can only agree with itself. This is the check nothing else performs: `recall()` ranks by relevance and `maintain()` checks structure, so a correction applied to one note leaves its siblings silently wrong. **Run it after correcting any fact that could be recorded in more than one place.** Matched against **both** the stored HTML and a tag-stripped projection, so a phrase split by an inline `<strong>` or `<code>` is found and so is a pattern anchored on tags. Scans every in-scope note by default — Trilium's `%=` pre-filter reads a striptags'd copy and silently drops notes, and on a contradiction sweep a falsely clean result is worse than a slow one; `fast=true` opts back into it. |
|
|
332
345
|
| `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
346
|
| `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
347
|
| `<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
|
-
| `
|
|
348
|
+
| `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). |
|
|
349
|
+
| `split(noteId, sections=[…], into="<title>")` | **The trim affordance — the write half of the oversized-note problem.** Lifts whole sections (heading + body, nested sub-sections go with their parent) out of one note into a new note created under the same parent, typed and labelled like the source, and leaves a pointer in the source plus a `~references` edge. A note past the read ceiling becomes two readable notes instead of one unusable one. Sections are matched by heading TEXT (the same contract `section=` uses); repeated headings are consumed first-match-per-request; sections that don't exist come back in `missed=` and stay in place. A revision is taken of the source first. Refused on containers and singletons — for content notes. |
|
|
350
|
+
| `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
351
|
| `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
|
|
338
352
|
| `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
|
|
339
353
|
| `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 +358,7 @@ Most of these are computed **server-side** from note properties rather than by r
|
|
|
344
358
|
| `domain(name, …)` | Surface all content for a named domain/topic/project, grouped by kind. |
|
|
345
359
|
| `connect(from, relation, to, remove?)` | Typed edge from the closed vocabulary; symmetric handled; idempotent. |
|
|
346
360
|
| `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. |
|
|
361
|
+
| `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
362
|
| `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
363
|
| `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
364
|
| `forget(noteId, reason?, hard?)` | Archive (default) or hard-delete (blocked while backlinked). Undo with recover(). |
|
|
@@ -58,3 +58,9 @@ A domain's Knowledge surface is exactly: **one maintained Sources note, plus a s
|
|
|
58
58
|
Chronological, run-by-run history belongs in Memory/Threads — a thread is the right place for "here's what Run N found"; a domain information note is the right place for "here's what's actually true about this sub-category," kept current by revision, not accumulation. If you find yourself naming a new information note after today's date or a run number, stop — that finding either updates an existing note (revise it) or doesn't belong in Domains at all.
|
|
59
59
|
|
|
60
60
|
The title rule is the same here as everywhere: **four words maximum**, no dates, no run numbers. Sub-category titles are where it's most often stretched, and a title that won't fit is telling you something — a sub-category needing eight words to name is usually two sub-categories, so split the content rather than the title. Merge behaviour follows from the same place: Sources and information notes are maintained current-state documents, so new findings fold into the relevant section (`section=`, `find=`) and never arrive as a dated addendum block.
|
|
61
|
+
|
|
62
|
+
## Standing briefs vs current-state facts — the mandate marker
|
|
63
|
+
|
|
64
|
+
A domain holds two things that read alike and behave differently: a **standing brief** (instructions a future session must follow — the operating mandate of a venture) and a **current-state fact** (what's true right now). Both are `kind="information"`, and until now telling them apart meant reading each note's prose. A scoped autonomous session that needs "the thing I must obey" should not have to skim every sub-category note to find it.
|
|
65
|
+
|
|
66
|
+
Mark a brief with `remember(kind="information", domain=…, title=…, body=…, mandate=true)` — it adds a `#mandate` flag, surfaced as `mandate: true` in `domain()` group rows and in `knowledge_recall` stubs. A session looking for its brief reads the domain and the mandate-flagged note is the one carrying instructions rather than facts. `label(noteId, "mandate")` / `label(noteId, "mandate", remove=true)` sets or clears the marker on an existing note. One note per mandate, titled by the brief it is ("Founder Brief", "Operating Mandate") — the four-word rule still applies.
|
|
@@ -44,6 +44,7 @@ Written by the server — you never set `#noteType`, `#status`, `#created`, `#up
|
|
|
44
44
|
| `#topic` | slugged, repeatable | Subject tags (`ai-tooling`, `infra`) — capitalization normalized server-side |
|
|
45
45
|
| `#domain` | slugged | Knowledge domain — book auto-created on first use |
|
|
46
46
|
| `#archived` | (flag) | Excludes the note from default `recall()`; content preserved in place |
|
|
47
|
+
| `#mandate` | (flag) | Information notes only — marks a **standing brief** (instructions a future session must follow) as distinct from current-state fact. Set via `remember(…, mandate=true)` or `label(id, "mandate")`; surfaced in `domain()` and `knowledge_recall`. See `references/domains.md` |
|
|
47
48
|
| `#reviewed` | content blob id | Set by `maintain(ack=[…])` — this note's current content was reviewed and its maintenance findings accepted. Suppresses them until the body actually changes, then all of them return |
|
|
48
49
|
| `#brainLlmRoot` | (flag) | Marks the brain root — used by auto-discovery |
|
|
49
50
|
| `#iconClass` | `bx …` | Display icon — set via `icon=` on `remember`/`revise`/`diary`/`close` (class or bare name, normalized) |
|