brainllm 12.3.0 → 12.4.1
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 +9 -1
- package/README.md +37 -17
- package/SECURITY.md +33 -0
- package/dist/index.js +2645 -6261
- package/manifest.json +14 -9
- package/package.json +9 -5
- package/server.json +21 -2
- package/skills/brainllm/SKILL.md +9 -9
- package/skills/brainllm/references/fullmode.md +12 -12
- package/skills/brainllm/references/taxonomy.md +2 -2
- package/skills/brainllm/references/troubleshooting.md +1 -1
package/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"manifest_version": "0.3",
|
|
3
3
|
"name": "brainllm",
|
|
4
4
|
"display_name": "BrainLLM",
|
|
5
|
-
"version": "12.
|
|
5
|
+
"version": "12.4.1",
|
|
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": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"env": {
|
|
29
29
|
"TRILIUM_BASE_URL": "${user_config.trilium_base_url}",
|
|
30
30
|
"TRILIUM_ETAPI_TOKEN": "${user_config.trilium_etapi_token}",
|
|
31
|
+
"TRILIUM_PASSWORD": "${user_config.trilium_password}",
|
|
31
32
|
"BRAINLLM_MODE": "${user_config.brainllm_mode}",
|
|
32
33
|
"BRAINLLM_TZ": "${user_config.brainllm_tz}"
|
|
33
34
|
}
|
|
@@ -44,9 +45,16 @@
|
|
|
44
45
|
},
|
|
45
46
|
"trilium_etapi_token": {
|
|
46
47
|
"type": "string",
|
|
47
|
-
"title": "Trilium ETAPI token",
|
|
48
|
-
"description": "
|
|
49
|
-
"required":
|
|
48
|
+
"title": "Trilium ETAPI token (optional)",
|
|
49
|
+
"description": "Preferred: create one in Trilium under Options -> ETAPI. Leave blank when using the password alternative.",
|
|
50
|
+
"required": false,
|
|
51
|
+
"sensitive": true
|
|
52
|
+
},
|
|
53
|
+
"trilium_password": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"title": "Trilium password (alternative)",
|
|
56
|
+
"description": "Used only when no ETAPI token is supplied; BrainLLM mints and caches a token on first start.",
|
|
57
|
+
"required": false,
|
|
50
58
|
"sensitive": true
|
|
51
59
|
},
|
|
52
60
|
"brainllm_mode": {
|
|
@@ -64,9 +72,6 @@
|
|
|
64
72
|
}
|
|
65
73
|
},
|
|
66
74
|
"compatibility": {
|
|
67
|
-
"platforms": ["darwin", "win32", "linux"]
|
|
68
|
-
"runtimes": {
|
|
69
|
-
"bun": ">=1.0.0"
|
|
70
|
-
}
|
|
75
|
+
"platforms": ["darwin", "win32", "linux"]
|
|
71
76
|
}
|
|
72
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainllm",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.1",
|
|
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>",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"manifest.json",
|
|
42
42
|
"server.json",
|
|
43
43
|
"README.md",
|
|
44
|
+
"SECURITY.md",
|
|
44
45
|
"LICENSE"
|
|
45
46
|
],
|
|
46
47
|
"engines": {
|
|
@@ -51,20 +52,23 @@
|
|
|
51
52
|
"build": "bun build src/index.ts --outfile dist/index.js --target bun",
|
|
52
53
|
"start": "bun run dist/index.js",
|
|
53
54
|
"test": "bun run src/test.ts",
|
|
55
|
+
"test:unit": "bun test",
|
|
56
|
+
"test:http": "bun run scripts/http-smoke.ts",
|
|
54
57
|
"init": "bun run src/init.ts",
|
|
55
58
|
"icons": "bun run scripts/icons.ts",
|
|
56
59
|
"validate:release": "bun run scripts/validate-release.ts",
|
|
60
|
+
"validate:manifest": "npx --yes @anthropic-ai/mcpb@2.1.2 validate manifest.json",
|
|
57
61
|
"prepublishOnly": "bun run build"
|
|
58
62
|
},
|
|
59
63
|
"dependencies": {
|
|
60
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
61
|
-
"zod": "^3.
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
65
|
+
"zod": "^3.25.76"
|
|
62
66
|
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@types/bun": "latest",
|
|
65
69
|
"ajv": "^8.20.0",
|
|
66
70
|
"ajv-formats": "^3.0.1",
|
|
67
|
-
"sharp": "^0.35.
|
|
68
|
-
"typescript": "^5.
|
|
71
|
+
"sharp": "^0.35.4",
|
|
72
|
+
"typescript": "^5.9.3"
|
|
69
73
|
}
|
|
70
74
|
}
|
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.4.1",
|
|
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.
|
|
15
|
+
"version": "12.4.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -57,11 +57,30 @@
|
|
|
57
57
|
"isSecret": true,
|
|
58
58
|
"format": "string"
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"name": "BRAINLLM_OAUTH_SECRET",
|
|
62
|
+
"description": "Optional 32+ character deployment secret for signing OAuth tokens; rotate it to invalidate old tokens.",
|
|
63
|
+
"isRequired": false,
|
|
64
|
+
"isSecret": true,
|
|
65
|
+
"format": "string"
|
|
66
|
+
},
|
|
60
67
|
{
|
|
61
68
|
"name": "BRAINLLM_PUBLIC_URL",
|
|
62
69
|
"description": "Override for the public origin when a proxy rewrites Host. Must exactly match the URL entered in the client.",
|
|
63
70
|
"isRequired": false,
|
|
64
71
|
"format": "string"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "BRAINLLM_ALLOW_UNAUTHENTICATED_HTTP",
|
|
75
|
+
"description": "Explicit trusted-network-only escape hatch for HTTP mode. Never enable on a public interface.",
|
|
76
|
+
"isRequired": false,
|
|
77
|
+
"format": "boolean"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "BRAINLLM_TRUST_PROXY",
|
|
81
|
+
"description": "Trust forwarded host/protocol headers for OAuth discovery only when a trusted reverse proxy sets them.",
|
|
82
|
+
"isRequired": false,
|
|
83
|
+
"format": "boolean"
|
|
65
84
|
}
|
|
66
85
|
]
|
|
67
86
|
}
|
package/skills/brainllm/SKILL.md
CHANGED
|
@@ -49,7 +49,7 @@ DURING remember(...) ← the moment something worth keeping a
|
|
|
49
49
|
SESSION END session() ← mandatory pre-close; singleton stubs + diary + sweep (light by default); follow next[]
|
|
50
50
|
[revise() master singletons with session observations about the user]
|
|
51
51
|
[revise() LLM singletons with session observations about yourself]
|
|
52
|
-
addendum() ← find and
|
|
52
|
+
addendum() ← find pending blocks and durably mark the gate
|
|
53
53
|
maintain() ← audit and fix brain hygiene
|
|
54
54
|
remarks() ← the diary cues: experience/opinions/existence + BrainLLM remarks
|
|
55
55
|
diary(body, identity) ← the day's closing record, written with the cues in hand — gate counts it only after remarks()
|
|
@@ -64,7 +64,7 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
|
|
|
64
64
|
inspect(noteId, content?, section?, find?) ← every label/relation/attachment; content=true adds the body (section= narrows it); find= counts a literal flag and shows the nearest text on a miss
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`start()` runs maintenance, creates today's diary and session stubs if they don't exist yet, then returns: **today + weekday**, the **Master digest** (biography / goals / preferences), the **LLM digest** (responsibilities / protocols, plus today's diary note with its ID in the `llm` array as `{slot:"diary", id, preview}`), **this session's note** as `{id, preview}`, **activeThreads** (with idle ages), **dormantThreads** for review, the **lastSession** summary (always the *previous* session, never today's own note), and — on the first session of a day — **`newDay: true`** with a hint to call `day()`. Don't re-derive any of this with extra calls.
|
|
67
|
+
`start()` runs maintenance, creates today's diary and session stubs if they don't exist yet, then returns: **today + weekday**, the **Master digest** (biography / goals / preferences), the **LLM digest** (responsibilities / protocols / self-correction, plus today's diary note with its ID in the `llm` array as `{slot:"diary", id, preview}`), **this session's note** as `{id, preview}`, **activeThreads** (with idle ages), **dormantThreads** for review, the **lastSession** summary (always the *previous* session, never today's own note), and — on the first session of a day — **`newDay: true`** with a hint to call `day()`. Don't re-derive any of this with extra calls.
|
|
68
68
|
|
|
69
69
|
**Master preferences and LLM protocols always arrive in full. The other four arrive as section headings.** Preferences carries the schedule and working style; protocols carries the rules governing the session itself — a session needs both whole to orient correctly from its *first* message, before it knows enough to decide it needed them, which is exactly when a digest costs a second round-trip. Biography, goals, responsibilities and self-correction come back as `{slot, sections[], preview, size}`: enough to know what the brain holds, and rarely load-bearing turn to turn. **Self-correction is the largest of them and deliberately not served whole** — it answers "have I got this wrong before?", a question worth asking before an audit or a large edit and not on every first message. Pull one with `master(which)` / `llm(which)`, or a single section with their `section=` parameter. `start(depth="full")` serves all six inline — right for a strategy review or a singleton rewrite, wasteful for a one-line question.
|
|
70
70
|
|
|
@@ -82,9 +82,9 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
|
|
|
82
82
|
|
|
83
83
|
**The gate is durable.** Progress is written to today's session note as a `#gate` label, not held in process memory, so it survives a server restart mid-session and behaves identically across stdio and HTTP. It clears on a successful `close()`. Practical consequence: if a session is interrupted and resumed, the steps you already ran still count — re-run only what is actually missing, and trust `close()`'s error to name it.
|
|
84
84
|
|
|
85
|
-
`remarks()` is the diary-cue step before `diary()` — a **cue-only tool** returning two banks of questions: **experience** (primary — what this session was like from the inside, your genuine opinions including pushback, and observations on being what you are in this environment) and **brainllm** (additional — capability walls, bugs, usability/efficiency, roadmap). It
|
|
85
|
+
`remarks()` is the diary-cue step before `diary()` — a **cue-only content tool** returning two banks of questions: **experience** (primary — what this session was like from the inside, your genuine opinions including pushback, and observations on being what you are in this environment) and **brainllm** (additional — capability walls, bugs, usability/efficiency, roadmap). It durably marks the pre-close gate; answer the cues as **unfiltered first-person prose in today's diary via `diary()`**, experience first, BrainLLM remarks after. Skip a cue outright rather than padding it; two honest paragraphs beat eight forced ones.
|
|
86
86
|
|
|
87
|
-
`close(summary, title?, identity?, learned?, ...)` commits the session log — call it **once, last**, after completing the `session()` protocol (including `remarks()`). Idempotent per date — a
|
|
87
|
+
`close(summary, title?, identity?, learned?, ...)` commits the session log — call it **once, last**, after completing the `session()` protocol (including `remarks()`). Idempotent per date — an exact retry is duplicate-guarded, while a genuinely new same-day continuation appends an addendum. The session note title is always `[yyyy-mm-dd]`; pass `identity=` ("LLM · environment · agent/mode [· Run N]") and the server injects the canonical identification line as the block's h3; the `title` param appears as an `<h2>` heading above Summary. Triggers a DB backup, regenerates the daily log **in place**, and links session↔log with `~references` (idempotent). Returns `{action, noteId, date, backup, backupStatus, log}`; ETAPI proves the logical backup request completed but not its resulting filename.
|
|
88
88
|
|
|
89
89
|
**Write during the session, not at the end.** A fact remembered mid-conversation survives a crash; one you planned to write at the end does not.
|
|
90
90
|
|
|
@@ -330,8 +330,8 @@ Most of these are computed **server-side** from note properties rather than by r
|
|
|
330
330
|
| `start(depth?)` | Orient: master + LLM singletons as SECTION HEADINGS (+ preview and size) + diary id + session id + active/dormant threads + lastSession (always the previous session) + changesSinceLastSession + `newDay` flag on the first session of a day. Pull a section with master(which)/llm(which); `depth="full"` inlines every singleton and is token-heavy. Creates today's diary + session stubs. Once, first. |
|
|
331
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. |
|
|
332
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. |
|
|
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.
|
|
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. |
|
|
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 durably satisfies its gate step on the call. It writes only that gate marker; answer as first-person prose in today's diary via `diary()`. |
|
|
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, backupStatus, log}` (plus `deletionCatchUp` when one ran) and resets the gate. Once, last. |
|
|
335
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. |
|
|
336
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. |
|
|
337
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. |
|
|
@@ -350,16 +350,16 @@ Most of these are computed **server-side** from note properties rather than by r
|
|
|
350
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. |
|
|
351
351
|
| `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
|
|
352
352
|
| `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
|
|
353
|
-
| `attach(noteId, title, content?,
|
|
353
|
+
| `attach(noteId, title, content?, mime?, role?, encoding?)` | Upsert a raw artifact on a note by title (content provided), or read it back (content omitted). Binary is standard base64 with `encoding="base64"`; reads return a base64 envelope and MIME. |
|
|
354
354
|
| `detach(attachmentId? \| noteId+title)` | Remove an attachment — permanent; already-removed returns cleanly. |
|
|
355
355
|
| `recover(noteId, reason?, …)` | Restore any archived or resolved note: removes #archived, clears #closed, resets status. The canonical undo for forget(). |
|
|
356
356
|
| `label(noteId, name, value?, remove?)` | Guarded direct label edit/removal — refused on containers, status validated against the closed vocabulary, domain/topic auto-slugged. `noteType` cannot be **changed** or removed, but it **can be repaired**: on a note that has none, `label(id, "noteType", "<kind>")` types it and applies the rest of its label plan. That is the core path back from an untyped note — dedup cannot see one, so `remember()` would mint a duplicate beside it instead of adopting it. |
|
|
357
|
-
| `backup(name?)` | On-demand DB backup (close() already backs up; use this for milestone snapshots). |
|
|
357
|
+
| `backup(name?)` | On-demand DB backup (close() already backs up; use this for milestone snapshots). Returns the logical name and `backupStatus`; ETAPI does not expose the resulting `.db`/`.tnbackup` path. |
|
|
358
358
|
| `domain(name, …)` | Surface all content for a named domain/topic/project, grouped by kind. |
|
|
359
359
|
| `connect(from, relation, to, remove?)` | Typed edge from the closed vocabulary; symmetric handled; idempotent. |
|
|
360
360
|
| `explore(noteId, mode, …)` | Graph: links / backlinks / neighborhood / path. |
|
|
361
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. |
|
|
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. |
|
|
362
|
+
| `addendum()` | Search Master, LLM singletons (responsibilities + protocols + self-correction, not diary), and Knowledge for pending addendum blocks. The call durably marks the pre-close gate; fold each block into the relevant section with revise(section=…, mode=replace), then leave no addendum marker. 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. |
|
|
363
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). |
|
|
364
364
|
| `forget(noteId, reason?, hard?)` | Archive (default) or hard-delete (blocked while backlinked). Undo with recover(). |
|
|
365
365
|
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
Full-mode tools place nothing, label nothing — so when you reach past the core surface you take on what it normally guarantees. Three things keep raw edits from silently corrupting the brain:
|
|
12
12
|
|
|
13
13
|
- **A note is only a "memory" once it carries `#noteType`.** `recall` and every `<surface>` read filter out untyped notes, so a note you `create_note` without labelling is invisible to them. For a new memory, use core `remember` — it places the note, writes `#noteType` + `#created`/`#updated`, and dedups by title. Reach for `create_note` only for shapes core can't make (a `code` / `canvas` / `mermaid` note, a deliberate placement), then replicate the labels yourself: `add_label noteType <kind>`.
|
|
14
|
-
- **Overwrites don't snapshot; labels don't dedup.** `update_note_content` replaces the body with no revision — `create_revision` first when the content matters. `add_label` always adds (it can leave you with two `#status` labels); change an existing one with `update_attribute`. `delete_note` deletes the whole subtree when it's the last branch — prefer core `forget`, which archives and checks backlinks.
|
|
14
|
+
- **Overwrites don't snapshot; labels don't dedup.** `update_note_content` replaces the body with no revision — `create_revision` first when the content matters. Binary writes use standard base64 with `encoding="base64"`; the client decodes and sends raw bytes. `add_label` always adds (it can leave you with two `#status` labels); change an existing one with `update_attribute`. `delete_note` deletes the whole subtree when it's the last branch — prefer core `forget`, which archives and checks backlinks.
|
|
15
15
|
- **Find structure by its marker, not a hardcoded id.** There is no `get_brain_config`. To locate a container, `search_notes("#brainLlmRoot")` for the root, then `get_note` and walk `children` to the area / book you need; or lift a `parents` id from any note a surface read already returned.
|
|
16
16
|
|
|
17
17
|
### Raw artifacts (code, images, files)
|
|
18
18
|
|
|
19
19
|
BrainLLM memories are typed text notes; `code` / `file` / `image` notes aren't a `#noteType` kind. Keep raw artifacts *attached to* or *embedded in* a typed note rather than free-floating:
|
|
20
20
|
- **Code / structured text** → embed it as a fenced block in an `information` note via core `remember` (fully conformant and full-text searchable), or
|
|
21
|
-
- **Binary (image, PDF, file)** → core `attach()` it onto the relevant typed note (`role=image` / `file`) —
|
|
21
|
+
- **Binary (image, PDF, file)** → core `attach()` it onto the relevant typed note (`role=image` / `file`) — pass standard base64 with `encoding="base64"`; read back with `attach(noteId, title)`, which returns the base64 envelope and MIME.
|
|
22
22
|
|
|
23
23
|
Create a standalone `type=code` / `file` note only when you specifically need Trilium's native handling of that type. If you do: label it (`add_label noteType <closest kind>`) so `recall` can see it, give it the same `domain` / `topic` labels as its anchor, and `connect` it to a typed note — otherwise it's an orphan with a blueprint-less type, exactly what `maintain(deep=true)` flags.
|
|
24
24
|
|
|
@@ -33,7 +33,7 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
|
|
|
33
33
|
| Recover content clobbered by a bad write | `get_revisions` → `get_revision_content` |
|
|
34
34
|
| Recover a Trilium-hard-deleted note | `note_history` (check `canBeUndeleted`) → `undelete_note` |
|
|
35
35
|
| Fix or remove a stray label | core `label(noteId, name, value?, remove?)` — guarded, validates `status`, slugs `domain`/`topic`, refuses on containers |
|
|
36
|
-
| Retarget an existing relation
|
|
36
|
+
| Retarget an existing relation | Delete and re-add it; relation targets are immutable through `update_attribute` |
|
|
37
37
|
| Place one note under a second parent | `clone_note` (shared content, not a copy) |
|
|
38
38
|
| A Trilium journal day / week / month / year note | `get_day_note` / `get_week_note` / … |
|
|
39
39
|
|
|
@@ -45,9 +45,9 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
|
|
|
45
45
|
|---|---|
|
|
46
46
|
| `search_notes` | `(query, ancestorNoteId?, limit?, orderBy?, orderDirection?, fastSearch?, includeArchived?, debug?)` — raw Trilium query language; unscoped unless `ancestorNoteId` given |
|
|
47
47
|
| `get_note` | `(noteId)` — metadata + attributes + parent/child ids + dates |
|
|
48
|
-
| `get_note_content` | `(noteId)` — raw content |
|
|
49
|
-
| `create_note` | `(parentNoteId, title, content, type?, mime?)` — `
|
|
50
|
-
| `update_note_content` | `(noteId, content)` — full replace |
|
|
48
|
+
| `get_note_content` | `(noteId)` — raw content; binary returns `{encoding:"base64", mime, content}` |
|
|
49
|
+
| `create_note` | `(parentNoteId, title, content, type?, mime?, encoding?)` — `encoding` ∈ auto·text·base64; binary creation is an empty create followed by a raw PUT |
|
|
50
|
+
| `update_note_content` | `(noteId, content, mime?, encoding?)` — full replace; binary uses standard base64 and raw bytes |
|
|
51
51
|
| `patch_note` | `(noteId, title?, type?, mime?)` — metadata only |
|
|
52
52
|
| `delete_note` | `(noteId)` — hard-delete (subtree if last branch) |
|
|
53
53
|
| `undelete_note` | `(noteId)` — recover a Trilium-deleted note from Trilium's trash (`canBeUndeleted` must be true per `note_history`). Distinct from core `recover()` which restores BrainLLM-archived notes. |
|
|
@@ -60,7 +60,7 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
|
|
|
60
60
|
| `get_attribute` | `(attributeId)` |
|
|
61
61
|
| `add_label` | `(noteId, name, value?, isInheritable?)` — adds a `#label` (no dedup) |
|
|
62
62
|
| `add_relation` | `(fromNoteId, relationName, toNoteId, isInheritable?)` — any name (`connect` enforces the closed vocab) |
|
|
63
|
-
| `update_attribute` | `(attributeId, value?, position?)` |
|
|
63
|
+
| `update_attribute` | `(attributeId, value?, position?)` — label value/position only; relation targets are immutable, and relation-position compatibility errors are explicit |
|
|
64
64
|
| `delete_attribute` | `(attributeId)` |
|
|
65
65
|
|
|
66
66
|
## Branches (placement)
|
|
@@ -78,16 +78,16 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
|
|
|
78
78
|
|---|---|
|
|
79
79
|
| `create_revision` | `(noteId)` |
|
|
80
80
|
| `get_revisions` | `(noteId)` — list, newest first |
|
|
81
|
-
| `get_revision_content` | `(revisionId)` |
|
|
81
|
+
| `get_revision_content` | `(revisionId)` — text or base64 envelope for binary snapshots |
|
|
82
82
|
|
|
83
83
|
## Attachments
|
|
84
84
|
|
|
85
85
|
| Tool | Signature |
|
|
86
86
|
|---|---|
|
|
87
87
|
| `get_attachments` | `(noteId)` |
|
|
88
|
-
| `get_attachment_content` | `(attachmentId)` |
|
|
89
|
-
| `create_attachment` | `(ownerId, title, mime, content, role?)` — `role` ∈ file·image |
|
|
90
|
-
| `update_attachment` | `(attachmentId, title?, mime?, content?)` —
|
|
88
|
+
| `get_attachment_content` | `(attachmentId)` — text or base64 envelope for binary attachments |
|
|
89
|
+
| `create_attachment` | `(ownerId, title, mime, content, role?, encoding?)` — `role` ∈ file·image; binary uses base64/raw bytes |
|
|
90
|
+
| `update_attachment` | `(attachmentId, title?, mime?, content?, encoding?)` — metadata first, then raw content; binary uses base64 |
|
|
91
91
|
| `delete_attachment` | `(attachmentId)` |
|
|
92
92
|
|
|
93
93
|
## Calendar (Trilium journal)
|
|
@@ -105,4 +105,4 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
|
|
|
105
105
|
| Tool | Signature |
|
|
106
106
|
|---|---|
|
|
107
107
|
| `get_app_info` | `()` — Trilium version, DB version, runtime metadata |
|
|
108
|
-
| `create_backup` | `(name?, date?)` — named DB backup
|
|
108
|
+
| `create_backup` | `(name?, date?)` — named DB backup request; ETAPI reports logical completion, not the resulting filename (`.db`/`.tnbackup` is deployment-dependent) |
|
|
@@ -32,11 +32,11 @@ Closed — `connect()` rejects anything not on this list. Pick the most specific
|
|
|
32
32
|
|
|
33
33
|
## Label Conventions
|
|
34
34
|
|
|
35
|
-
Written by the server — you never set `#noteType`, `#status`, `#created`, `#updated`, `#closed`, `#archived` manually through the normal write path (`remember`/`revise`/`resolve`/`withdraw`/`recover`). The one sanctioned exception is `label(noteId, name, value?, remove?)` — a guarded direct edit for fixing a stray or drifted value; `#noteType` is refused
|
|
35
|
+
Written by the server — you never set `#noteType`, `#status`, `#created`, `#updated`, `#closed`, `#archived` manually through the normal write path (`remember`/`revise`/`resolve`/`withdraw`/`recover`). The one sanctioned exception is `label(noteId, name, value?, remove?)` — a guarded direct edit for fixing a stray or drifted value; an existing `#noteType` is refused, while `label()` can repair a note whose kind is missing. Documented here so you can read and filter on them in `recall()` and `search_notes()`.
|
|
36
36
|
|
|
37
37
|
| Label | Values | Purpose |
|
|
38
38
|
|---|---|---|
|
|
39
|
-
| `#noteType` | `biography` `goals` `preferences` `responsibilities` `protocols` `diary` `session` `thread` `threadEntry` `user` `domain` `information` `sources` `log` | Kind — exactly one per note, set at creation
|
|
39
|
+
| `#noteType` | `biography` `goals` `preferences` `responsibilities` `protocols` `selfcorrection` `diary` `session` `thread` `threadEntry` `user` `domain` `information` `sources` `claim` `log` | Kind — exactly one per note, set at creation and immutable once present; a missing kind may be repaired explicitly. `threadEntry` is never created directly — only as a thread's day-child; `claim` is managed by `claim()` |
|
|
40
40
|
| `#status` | `active` `dormant` `resolved` `superseded` `eternal` | Lifecycle state — threads age; `resolve()` sets terminal. `eternal` = lifecycle-exempt, never aged by the sweep — for user-curated permanent threads |
|
|
41
41
|
| `#created` | ISO date | Set at creation (the user's local day) |
|
|
42
42
|
| `#updated` | ISO date | Updated on every write |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Situation | What happens / what to do |
|
|
4
4
|
|---|---|
|
|
5
5
|
| BrainLLM not initialized | `start` returns `status: "uninitialized"` → run `bootstrap` (idempotent, safe anytime) |
|
|
6
|
-
| Second `close` same day |
|
|
6
|
+
| Second `close` same day | An exact retry is duplicate-guarded; a genuinely new continuation appends an addendum to today's session note by design. Pass `continuing=true` so you don't re-run the whole pre-close protocol for a session whose brain writes are already done; it's verified against today's note, so it can't stand in for a first close |
|
|
7
7
|
| `remember()` says `action: "updated"` unexpectedly | A same-kind note with that title existed; content was appended there. If it was genuinely a different subject, `remember()` again with a distinguishing title |
|
|
8
8
|
| User contradicts a stored fact about themselves | `master(which)` to read it, then `revise(id, section=…)` with the correction — the Master singletons hold current-state truth, not history |
|
|
9
9
|
| A stored fact was wrong from the start | `revise(mode="replace")` — a revision snapshot is taken automatically, nothing is lost |
|