brainllm 11.0.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +96 -33
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +9 -7
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A persistent, graph-structured second brain for Claude and any MCP client — bu
|
|
|
10
10
|
|
|
11
11
|
[**brainllm site**](https://miisodev.github.io/BrainLLM/) · [How it works](https://miisodev.github.io/BrainLLM/how-it-works.html) · [Use cases](https://miisodev.github.io/BrainLLM/use-cases.html) · [Docs](https://miisodev.github.io/BrainLLM/docs.html)
|
|
12
12
|
|
|
13
|
-
[](https://github.com/miisodev/BrainLLM/releases)
|
|
14
14
|
[](https://github.com/miisodev/BrainLLM/actions/workflows/ci.yml)
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](https://bun.sh)
|
|
@@ -60,7 +60,7 @@ At bootstrap, BrainLLM builds a five-area tree in Trilium. Every note the tools
|
|
|
60
60
|
```
|
|
61
61
|
BrainLLM (#brainLlmRoot)
|
|
62
62
|
├── 👤 Master Biography · Goals · Preferences (the user — maintained singletons)
|
|
63
|
-
├── 🤖 LLM Responsibilities · Protocols · Diary/
|
|
63
|
+
├── 🤖 LLM Responsibilities · Protocols · Self-correction · Diary/ (the assistant's self-model + daily diary)
|
|
64
64
|
├── 🗂️ Memory Sessions/ · Threads/ (daily session logs + multi-session work)
|
|
65
65
|
├── 📚 Knowledge Master/ · Domains/<domain>/{ Sources, info } (learned facts beyond/contra training)
|
|
66
66
|
└── 💡 Insights Logs/ · Graph (the brain's record of itself)
|
|
@@ -68,7 +68,7 @@ BrainLLM (#brainLlmRoot)
|
|
|
68
68
|
|
|
69
69
|
| Note class | Kinds | Behavior |
|
|
70
70
|
|---|---|---|
|
|
71
|
-
| **Singletons** | biography, goals, preferences, responsibilities, protocols (+ each domain's Sources note) | Exactly one maintained note; edited in place; hold *current-state truth* |
|
|
71
|
+
| **Singletons** | biography, goals, preferences, responsibilities, protocols, selfcorrection (+ each domain's Sources note) | Exactly one maintained note; edited in place; hold *current-state truth* |
|
|
72
72
|
| **Dated records** | diary, session, log | One per calendar day; every write lands as a timestamped addendum block — chronology is the point |
|
|
73
73
|
| **Collections** | thread, user, information, domain | Titled notes, deduplicated by normalized title within their scope |
|
|
74
74
|
|
package/dist/index.js
CHANGED
|
@@ -25416,6 +25416,7 @@ var Kinds = [
|
|
|
25416
25416
|
"preferences",
|
|
25417
25417
|
"responsibilities",
|
|
25418
25418
|
"protocols",
|
|
25419
|
+
"selfcorrection",
|
|
25419
25420
|
"diary",
|
|
25420
25421
|
"session",
|
|
25421
25422
|
"thread",
|
|
@@ -25433,6 +25434,7 @@ var KIND_AREA = {
|
|
|
25433
25434
|
preferences: "master",
|
|
25434
25435
|
responsibilities: "llm",
|
|
25435
25436
|
protocols: "llm",
|
|
25437
|
+
selfcorrection: "llm",
|
|
25436
25438
|
diary: "llm",
|
|
25437
25439
|
session: "memory",
|
|
25438
25440
|
thread: "memory",
|
|
@@ -25449,7 +25451,8 @@ var SingletonKinds = [
|
|
|
25449
25451
|
"goals",
|
|
25450
25452
|
"preferences",
|
|
25451
25453
|
"responsibilities",
|
|
25452
|
-
"protocols"
|
|
25454
|
+
"protocols",
|
|
25455
|
+
"selfcorrection"
|
|
25453
25456
|
];
|
|
25454
25457
|
var Statuses = ["active", "resolved", "superseded", "dormant", "eternal"];
|
|
25455
25458
|
var RelationTypes = [
|
|
@@ -25972,10 +25975,10 @@ class TriliumClient {
|
|
|
25972
25975
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
25973
25976
|
import { join, dirname } from "path";
|
|
25974
25977
|
var EMPTY_BRAINLLM = {
|
|
25975
|
-
version:
|
|
25978
|
+
version: 10,
|
|
25976
25979
|
root: "",
|
|
25977
25980
|
master: { root: "", biography: "", goals: "", preferences: "" },
|
|
25978
|
-
llm: { root: "", responsibilities: "", protocols: "", diary: "" },
|
|
25981
|
+
llm: { root: "", responsibilities: "", protocols: "", selfcorrection: "", diary: "" },
|
|
25979
25982
|
memory: { root: "", sessions: "", threads: "", metaThread: "" },
|
|
25980
25983
|
knowledge: { root: "", master: "", domains: "" },
|
|
25981
25984
|
insights: { root: "", logs: "" },
|
|
@@ -26016,9 +26019,10 @@ function loadConfig() {
|
|
|
26016
26019
|
return null;
|
|
26017
26020
|
try {
|
|
26018
26021
|
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
26019
|
-
if (typeof parsed?.root === "string" &&
|
|
26022
|
+
if (typeof parsed?.root === "string" && [10, 9, 8, 5].includes(parsed?.version)) {
|
|
26020
26023
|
return {
|
|
26021
26024
|
...parsed,
|
|
26025
|
+
llm: { selfcorrection: "", ...parsed.llm ?? {} },
|
|
26022
26026
|
memory: { metaThread: "", ...parsed.memory ?? {} },
|
|
26023
26027
|
policy: { ...DEFAULT_POLICY, ...parsed.policy ?? {} }
|
|
26024
26028
|
};
|
|
@@ -26028,7 +26032,7 @@ function loadConfig() {
|
|
|
26028
26032
|
}
|
|
26029
26033
|
function saveConfig(config2) {
|
|
26030
26034
|
const path = configFilePath();
|
|
26031
|
-
writeFileSync(path, JSON.stringify({ ...config2, version:
|
|
26035
|
+
writeFileSync(path, JSON.stringify({ ...config2, version: 10 }, null, 2) + `
|
|
26032
26036
|
`, "utf-8");
|
|
26033
26037
|
return path;
|
|
26034
26038
|
}
|
|
@@ -26062,7 +26066,7 @@ async function discoverBrainLLM(trilium) {
|
|
|
26062
26066
|
config2.master = { root: id, biography: g("Biography"), goals: g("Goals"), preferences: g("Preferences") };
|
|
26063
26067
|
break;
|
|
26064
26068
|
case "LLM":
|
|
26065
|
-
config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), diary: g("Diary") };
|
|
26069
|
+
config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), selfcorrection: g("Self-correction"), diary: g("Diary") };
|
|
26066
26070
|
break;
|
|
26067
26071
|
case "Memory":
|
|
26068
26072
|
config2.memory = { root: id, sessions: g("Sessions"), threads: g("Threads"), metaThread: "" };
|
|
@@ -26756,8 +26760,9 @@ function setSection(html, heading, content, mode, occurrence = 1) {
|
|
|
26756
26760
|
${html.slice(headingStart)}`, matched: true, headingCount };
|
|
26757
26761
|
}
|
|
26758
26762
|
if (mode === "after") {
|
|
26759
|
-
return { html: `${html.slice(0,
|
|
26760
|
-
${content}
|
|
26763
|
+
return { html: `${html.slice(0, end).replace(/\s*$/, "")}
|
|
26764
|
+
${content}
|
|
26765
|
+
${html.slice(end)}`, matched: true, headingCount };
|
|
26761
26766
|
}
|
|
26762
26767
|
const existing = html.slice(contentStart, end).trim();
|
|
26763
26768
|
const inner = mode === "append" && existing ? `${existing}
|
|
@@ -27007,7 +27012,8 @@ var STRUCTURED_SINGLETONS = new Set([
|
|
|
27007
27012
|
"goals",
|
|
27008
27013
|
"preferences",
|
|
27009
27014
|
"responsibilities",
|
|
27010
|
-
"protocols"
|
|
27015
|
+
"protocols",
|
|
27016
|
+
"selfcorrection"
|
|
27011
27017
|
]);
|
|
27012
27018
|
var hasHeading = (html, text) => new RegExp(`<h[2-4](?:\\s[^>]*)?>\\s*${text}\\s*</h[2-4]>`, "i").test(html);
|
|
27013
27019
|
var REQUIRED_SECTIONS = {
|
|
@@ -27063,8 +27069,10 @@ ${o.goal ? `<p>${escapeHtml(o.goal)}</p>` : o.body || "<p>To be determined.</p>"
|
|
|
27063
27069
|
<p></p>`].join(`
|
|
27064
27070
|
`);
|
|
27065
27071
|
case "protocols":
|
|
27066
|
-
return ["<h2>Operating</h2>", o.body || "<p></p>"
|
|
27067
|
-
|
|
27072
|
+
return ["<h2>Operating</h2>", o.body || "<p></p>"].join(`
|
|
27073
|
+
`);
|
|
27074
|
+
case "selfcorrection":
|
|
27075
|
+
return ["<h2>Corrections</h2>", o.body || "<p></p>"].join(`
|
|
27068
27076
|
`);
|
|
27069
27077
|
case "domain":
|
|
27070
27078
|
return domainContent(o.domain ?? "");
|
|
@@ -27249,6 +27257,8 @@ function kindHome(cfg, kind) {
|
|
|
27249
27257
|
return cfg.llm.responsibilities;
|
|
27250
27258
|
case "protocols":
|
|
27251
27259
|
return cfg.llm.protocols;
|
|
27260
|
+
case "selfcorrection":
|
|
27261
|
+
return cfg.llm.selfcorrection;
|
|
27252
27262
|
case "diary":
|
|
27253
27263
|
return cfg.llm.diary;
|
|
27254
27264
|
case "session":
|
|
@@ -27363,6 +27373,8 @@ function locationLabel(kind, domainTitle) {
|
|
|
27363
27373
|
return "LLM \u2192 Responsibilities";
|
|
27364
27374
|
case "protocols":
|
|
27365
27375
|
return "LLM \u2192 Protocols";
|
|
27376
|
+
case "selfcorrection":
|
|
27377
|
+
return "LLM \u2192 Self-correction";
|
|
27366
27378
|
case "diary":
|
|
27367
27379
|
return "LLM \u2192 Diary";
|
|
27368
27380
|
case "session":
|
|
@@ -27397,6 +27409,7 @@ function structuralIds(cfg) {
|
|
|
27397
27409
|
cfg.llm.root,
|
|
27398
27410
|
cfg.llm.responsibilities,
|
|
27399
27411
|
cfg.llm.protocols,
|
|
27412
|
+
cfg.llm.selfcorrection,
|
|
27400
27413
|
cfg.llm.diary,
|
|
27401
27414
|
cfg.memory.root,
|
|
27402
27415
|
cfg.memory.sessions,
|
|
@@ -27417,7 +27430,8 @@ function isContainer(cfg, noteId) {
|
|
|
27417
27430
|
cfg.master.goals,
|
|
27418
27431
|
cfg.master.preferences,
|
|
27419
27432
|
cfg.llm.responsibilities,
|
|
27420
|
-
cfg.llm.protocols
|
|
27433
|
+
cfg.llm.protocols,
|
|
27434
|
+
cfg.llm.selfcorrection
|
|
27421
27435
|
];
|
|
27422
27436
|
return isStructural(cfg, noteId) && !singletons.includes(noteId);
|
|
27423
27437
|
}
|
|
@@ -27760,6 +27774,8 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
27760
27774
|
if (reported.has(pairKey))
|
|
27761
27775
|
continue;
|
|
27762
27776
|
reported.add(pairKey);
|
|
27777
|
+
if (acknowledged(a) || acknowledged(bn))
|
|
27778
|
+
continue;
|
|
27763
27779
|
const [ha, hb] = await Promise.all([headingsOf(a), headingsOf(bn)]);
|
|
27764
27780
|
const overlap = ha.filter((h) => hb.some((x) => x.toLowerCase() === h.toLowerCase()));
|
|
27765
27781
|
const show = (h) => h.length ? h.slice(0, 8).join(" \xB7 ") + (h.length > 8 ? " \xB7 \u2026" : "") : "(no headings)";
|
|
@@ -27932,7 +27948,8 @@ async function buildDigest(trilium, cfg, opts = {}) {
|
|
|
27932
27948
|
]),
|
|
27933
27949
|
Promise.all([
|
|
27934
27950
|
readSlot("responsibilities", cfg.llm.responsibilities),
|
|
27935
|
-
readSlot("protocols", cfg.llm.protocols)
|
|
27951
|
+
readSlot("protocols", cfg.llm.protocols),
|
|
27952
|
+
readSlot("selfcorrection", cfg.llm.selfcorrection)
|
|
27936
27953
|
])
|
|
27937
27954
|
]);
|
|
27938
27955
|
digest.master.push(...masterSlots.filter((s) => !!s));
|
|
@@ -28042,9 +28059,10 @@ async function createBrainLLMStructure(trilium) {
|
|
|
28042
28059
|
leaf(masterRoot, "Preferences", "preferences", "A single maintained note of the master/user's preferences.")
|
|
28043
28060
|
]);
|
|
28044
28061
|
const llmRoot = await book(rootId, "LLM", CONTAINER_PURPOSES.llm, "bx bx-bot");
|
|
28045
|
-
const [responsibilities, protocols, diary] = await Promise.all([
|
|
28062
|
+
const [responsibilities, protocols, selfcorrection, diary] = await Promise.all([
|
|
28046
28063
|
leaf(llmRoot, "Responsibilities", "responsibilities", "A single maintained note of the assistant's responsibilities to the master/user, derived from their goals and preferences."),
|
|
28047
28064
|
leaf(llmRoot, "Protocols", "protocols", "A single maintained note of the assistant's operating and self-correctness protocols \u2014 how it maximises its value to the master/user by efficiently meeting its responsibilities."),
|
|
28065
|
+
leaf(llmRoot, "Self-correction", "selfcorrection", "A single maintained note of the assistant's own corrections \u2014 the mistakes it has made, what generalises from each, and the rule that prevents a repeat. Split out of Protocols in V12 so orientation stops paying for it on every session start."),
|
|
28048
28066
|
book(llmRoot, "Diary", CONTAINER_PURPOSES.llmDiary, "bx bx-book-heart")
|
|
28049
28067
|
]);
|
|
28050
28068
|
const memoryRoot = await book(rootId, "Memory", CONTAINER_PURPOSES.memory, "bx bx-been-here");
|
|
@@ -28060,10 +28078,10 @@ async function createBrainLLMStructure(trilium) {
|
|
|
28060
28078
|
const insightsRoot = await book(rootId, "Insights", CONTAINER_PURPOSES.insights, "bx bx-bulb");
|
|
28061
28079
|
const logs = await book(insightsRoot, "Logs", CONTAINER_PURPOSES.insightsLogs, "bx bx-history");
|
|
28062
28080
|
return {
|
|
28063
|
-
version:
|
|
28081
|
+
version: 10,
|
|
28064
28082
|
root: rootId,
|
|
28065
28083
|
master: { root: masterRoot, biography, goals, preferences },
|
|
28066
|
-
llm: { root: llmRoot, responsibilities, protocols, diary },
|
|
28084
|
+
llm: { root: llmRoot, responsibilities, protocols, selfcorrection, diary },
|
|
28067
28085
|
memory: { root: memoryRoot, sessions, threads, metaThread: "" },
|
|
28068
28086
|
knowledge: { root: knowledgeRoot, master: knowledgeMaster, domains },
|
|
28069
28087
|
insights: { root: insightsRoot, logs },
|
|
@@ -28234,12 +28252,19 @@ day's blocks rather than the whole week. outline(id) lists the headings.`, {
|
|
|
28234
28252
|
// src/tools-llm.ts
|
|
28235
28253
|
function registerLlmTools(server, trilium, brainRef) {
|
|
28236
28254
|
const b = () => brainRef.config;
|
|
28237
|
-
server.tool("llm", `Read an LLM note: responsibilities or
|
|
28255
|
+
server.tool("llm", `Read an LLM note: responsibilities, protocols or selfcorrection (singletons), or a diary
|
|
28256
|
+
entry by id.
|
|
28257
|
+
|
|
28258
|
+
selfcorrection holds the assistant's own corrections \u2014 what it got wrong, what generalises, and
|
|
28259
|
+
the rule that prevents a repeat. It is the one LLM singleton start() does NOT serve in full: it
|
|
28260
|
+
is the largest and the least load-bearing at a first message, so it arrives as section headings
|
|
28261
|
+
and is read here on demand. Reach for it when about to repeat a class of work that has gone
|
|
28262
|
+
wrong before \u2014 an audit, a large edit, a claim of completeness.
|
|
28238
28263
|
|
|
28239
28264
|
section="<heading>" reads ONE section instead of the whole note \u2014 the efficient path on
|
|
28240
|
-
protocols
|
|
28241
|
-
whole. outline(id) lists the headings.`, {
|
|
28242
|
-
which: exports_external.enum(["responsibilities", "protocols", "diary"]),
|
|
28265
|
+
protocols and selfcorrection, the two largest singletons and the ones most often needed in part
|
|
28266
|
+
rather than whole. outline(id) lists the headings.`, {
|
|
28267
|
+
which: exports_external.enum(["responsibilities", "protocols", "selfcorrection", "diary"]),
|
|
28243
28268
|
id: exports_external.string().optional().describe("diary only: the entry id from llm_recall"),
|
|
28244
28269
|
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
28245
28270
|
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
@@ -28250,20 +28275,23 @@ whole. outline(id) lists the headings.`, {
|
|
|
28250
28275
|
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
28251
28276
|
}
|
|
28252
28277
|
const noteId = b().llm[which];
|
|
28253
|
-
if (!noteId)
|
|
28254
|
-
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
28278
|
+
if (!noteId) {
|
|
28279
|
+
throw new Error(which === "selfcorrection" ? "This brain has no Self-correction note yet \u2014 it arrived in V12. Run bootstrap() once; it finds or creates the note and persists its id, leaving everything else untouched." : "BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
28280
|
+
}
|
|
28255
28281
|
return txt(await readFull(trilium, noteId, { section, occurrence }));
|
|
28256
28282
|
});
|
|
28257
|
-
server.tool("llm_recall", "Skim the LLM surface: responsibilities &
|
|
28283
|
+
server.tool("llm_recall", "Skim the LLM surface: responsibilities, protocols & self-correction opening lines (with relation snippets), plus recent diary entries.", { limit: exports_external.number().optional() }, async ({ limit }) => {
|
|
28258
28284
|
const cfg = b();
|
|
28259
|
-
const [resp, prot, diary] = await Promise.all([
|
|
28285
|
+
const [resp, prot, selfcorr, diary] = await Promise.all([
|
|
28260
28286
|
previewWithRelations(trilium, cfg.llm.responsibilities),
|
|
28261
28287
|
previewWithRelations(trilium, cfg.llm.protocols),
|
|
28288
|
+
cfg.llm.selfcorrection ? previewWithRelations(trilium, cfg.llm.selfcorrection) : Promise.resolve(null),
|
|
28262
28289
|
skim(trilium, cfg.llm.diary, { kind: "diary", limit: limit ?? 7 })
|
|
28263
28290
|
]);
|
|
28264
28291
|
return txt({
|
|
28265
28292
|
responsibilities: { id: cfg.llm.responsibilities, ...resp },
|
|
28266
28293
|
protocols: { id: cfg.llm.protocols, ...prot },
|
|
28294
|
+
...selfcorr ? { selfcorrection: { id: cfg.llm.selfcorrection, ...selfcorr } } : {},
|
|
28267
28295
|
diary
|
|
28268
28296
|
});
|
|
28269
28297
|
});
|
|
@@ -29576,14 +29604,17 @@ applies several surgeries in one call, in order, against one read and one write.
|
|
|
29576
29604
|
|
|
29577
29605
|
Granularity warning: section + mode=replace swaps the ENTIRE section body \u2014 everything under
|
|
29578
29606
|
that heading, not one paragraph within it. To change a single paragraph inside a section, use
|
|
29579
|
-
find=; to add
|
|
29607
|
+
find=; to add a sibling block around a whole section without touching its body, use
|
|
29608
|
+
mode="before"/"after". Both are section-relative: "after" lands past the end of that section's
|
|
29609
|
+
body, so inserting a heading with it creates a NEW section rather than nesting one under the
|
|
29610
|
+
target \u2014 which is what it used to do, emptying the target while reporting matched: true.`, {
|
|
29580
29611
|
noteId: exports_external.string().describe("Note to update"),
|
|
29581
29612
|
body: exports_external.string().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
|
|
29582
29613
|
title: exports_external.string().optional().describe("New title (normalized server-side)"),
|
|
29583
29614
|
section: exports_external.string().optional().describe("Target a section by heading text (h2/h3/h4, in that order); omit for whole-note append/replace"),
|
|
29584
29615
|
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading to target, 1-based (default: the first). Read them with outline(noteId)."),
|
|
29585
29616
|
strict: exports_external.boolean().optional().describe("section=: refuse instead of writing when no heading matches. The default writes the content as a NEW section on a miss \u2014 right for a genuinely new heading, wrong for a typo. With strict=true a miss returns available= and a didYouMean suggestion, and the note is untouched."),
|
|
29586
|
-
mode: exports_external.enum(["append", "replace", "before", "after", "remove"]).optional().describe('append (default) | replace | before | after | remove \u2014 "before"/"after" insert
|
|
29617
|
+
mode: exports_external.enum(["append", "replace", "before", "after", "remove"]).optional().describe('append (default) | replace | before | after | remove \u2014 "before"/"after" insert a sibling block around the WHOLE section= section (before its heading, or after the last of its body) without touching that body, so inserting a heading with "after" creates a new section rather than nesting one under the target; "remove" deletes the section= heading and its body, and needs no body='),
|
|
29587
29618
|
find: exports_external.string().optional().describe("Exact raw string to replace throughout the body with body= \u2014 targeted surgery without a read+full-replace. Takes precedence over section/mode."),
|
|
29588
29619
|
nth: exports_external.number().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
|
|
29589
29620
|
edits: exports_external.array(exports_external.object({
|
|
@@ -31035,7 +31066,12 @@ exception.`, {
|
|
|
31035
31066
|
server.tool("brain", `Surface the entire BrainLLM content tree \u2014 every typed note across all five content areas
|
|
31036
31067
|
(Master, LLM, Memory, Knowledge, Insights), grouped by area and sub-container, with
|
|
31037
31068
|
id/title/kind/status/dates. Use to audit what the brain contains or locate a specific note.
|
|
31038
|
-
Structural containers are excluded; only content notes appear
|
|
31069
|
+
Structural containers are excluded; only content notes appear.
|
|
31070
|
+
|
|
31071
|
+
Insights returns { logs, claims }. Claims were missing entirely before V12 \u2014 they live in a
|
|
31072
|
+
container resolved by title rather than named in the config, and this read was scoped one level
|
|
31073
|
+
too deep to reach them, so the register was invisible to the tool that calls itself the full
|
|
31074
|
+
inventory.`, {
|
|
31039
31075
|
includeArchived: exports_external.boolean().optional().describe("Include archived/resolved notes (default: false)")
|
|
31040
31076
|
}, async ({ includeArchived }) => {
|
|
31041
31077
|
const cfg = b();
|
|
@@ -31084,10 +31120,12 @@ Structural containers are excluded; only content notes appear.`, {
|
|
|
31084
31120
|
fetchFrom(cfg.memory.threads),
|
|
31085
31121
|
fetchFrom(cfg.knowledge.master),
|
|
31086
31122
|
fetchFrom(cfg.knowledge.domains),
|
|
31087
|
-
fetchFrom(cfg.insights.
|
|
31123
|
+
fetchFrom(cfg.insights.root)
|
|
31088
31124
|
]);
|
|
31089
31125
|
const diaryIds = new Set(llmDiary.map((n) => n.noteId));
|
|
31090
31126
|
const llmSingletons = llmAll.filter((n) => !diaryIds.has(n.noteId));
|
|
31127
|
+
const insightLogs = insights.filter((n) => labelOf2(n, "noteType") === "log");
|
|
31128
|
+
const insightClaims = insights.filter((n) => labelOf2(n, "noteType") === "claim");
|
|
31091
31129
|
const areas = {
|
|
31092
31130
|
Master: masterAll.map(row),
|
|
31093
31131
|
LLM: {
|
|
@@ -31102,7 +31140,10 @@ Structural containers are excluded; only content notes appear.`, {
|
|
|
31102
31140
|
master: kMaster.map(row),
|
|
31103
31141
|
domains: kDomains.map(row)
|
|
31104
31142
|
},
|
|
31105
|
-
Insights:
|
|
31143
|
+
Insights: {
|
|
31144
|
+
logs: insightLogs.map(row),
|
|
31145
|
+
claims: insightClaims.map(row)
|
|
31146
|
+
}
|
|
31106
31147
|
};
|
|
31107
31148
|
const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
|
|
31108
31149
|
return txt3({ total, areas });
|
|
@@ -31229,9 +31270,10 @@ Pass area= to zoom into one surface and get its full detail.`, {
|
|
|
31229
31270
|
...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
|
|
31230
31271
|
});
|
|
31231
31272
|
});
|
|
31232
|
-
server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config
|
|
31233
|
-
|
|
31234
|
-
|
|
31273
|
+
server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config,
|
|
31274
|
+
heals singletons a newer version introduced, and re-engraves container purposes if the structure
|
|
31275
|
+
already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
|
|
31276
|
+
LLM (Responsibilities/Protocols/Self-correction/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
|
|
31235
31277
|
Insights (Logs) \u2014 each engraved with its purpose, and writes brainllm.json. Active
|
|
31236
31278
|
immediately, no restart needed.`, {}, async () => {
|
|
31237
31279
|
if (b().root) {
|
|
@@ -31242,6 +31284,26 @@ immediately, no restart needed.`, {}, async () => {
|
|
|
31242
31284
|
return { id: child.noteId, title: child.title };
|
|
31243
31285
|
}));
|
|
31244
31286
|
const saved = saveConfig(brainRef.config);
|
|
31287
|
+
const healed = [];
|
|
31288
|
+
const ensureSingleton = async (slot, title, kind, purpose) => {
|
|
31289
|
+
if (brainRef.config.llm[slot])
|
|
31290
|
+
return;
|
|
31291
|
+
const hit = await trilium.searchNotes(`note.title = '${title}'`, { ancestorNoteId: brainRef.config.llm.root, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
31292
|
+
let id = hit.results[0]?.noteId;
|
|
31293
|
+
if (!id) {
|
|
31294
|
+
const made = await trilium.createNote(brainRef.config.llm.root, title, purposeContent(purpose) + `
|
|
31295
|
+
` + contentFor(kind, { date: localToday(), body: "" }), "text");
|
|
31296
|
+
id = made.note.noteId;
|
|
31297
|
+
await trilium.addLabel(id, "noteType", kind).catch(() => null);
|
|
31298
|
+
healed.push(`created ${title}`);
|
|
31299
|
+
} else {
|
|
31300
|
+
healed.push(`adopted existing ${title}`);
|
|
31301
|
+
}
|
|
31302
|
+
brainRef.config.llm[slot] = id;
|
|
31303
|
+
};
|
|
31304
|
+
await ensureSingleton("selfcorrection", "Self-correction", "selfcorrection", "A single maintained note of the assistant's own corrections \u2014 the mistakes it has made, what generalises from each, and the rule that prevents a repeat. Split out of Protocols in V12 so orientation stops paying for it on every session start.");
|
|
31305
|
+
if (healed.length)
|
|
31306
|
+
saveConfig(brainRef.config);
|
|
31245
31307
|
const refreshed = [];
|
|
31246
31308
|
for (const [id, purpose] of containerPurposes(brainRef.config)) {
|
|
31247
31309
|
if (!id)
|
|
@@ -31260,6 +31322,7 @@ immediately, no restart needed.`, {}, async () => {
|
|
|
31260
31322
|
status: "already_initialized",
|
|
31261
31323
|
message: `BrainLLM structure exists. Config refreshed at: ${saved}`,
|
|
31262
31324
|
...refreshed.length ? { purposesRefreshed: refreshed, note: "These containers described themselves with text that no longer matched the canonical purpose, and have been re-engraved. Container notes are unreachable through revise(), so bootstrap is the only path that can correct them." } : {},
|
|
31325
|
+
...healed.length ? { singletonsHealed: healed, healNote: "A singleton introduced by a newer version was missing from this brain and has been created (or an existing note by that title adopted), with its id persisted to the config." } : {},
|
|
31263
31326
|
root: { id: existing.noteId, title: existing.title },
|
|
31264
31327
|
children
|
|
31265
31328
|
});
|
|
@@ -32368,7 +32431,7 @@ function createServer(origin = null) {
|
|
|
32368
32431
|
const s = new McpServer({
|
|
32369
32432
|
name: "BrainLLM",
|
|
32370
32433
|
title: "BrainLLM",
|
|
32371
|
-
version: "
|
|
32434
|
+
version: "12.0.0",
|
|
32372
32435
|
icons: brandingIcons(origin)
|
|
32373
32436
|
});
|
|
32374
32437
|
registerTools(s, trilium, brainRef);
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "brainllm",
|
|
4
4
|
"display_name": "BrainLLM",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "12.0.0",
|
|
6
6
|
"description": "Turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
|
|
7
7
|
"long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
|
|
8
8
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainllm",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"mcpName": "io.github.miisodev/brainllm",
|
|
5
5
|
"description": "MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
|
|
6
6
|
"author": "Kevin Miiso Novo <miisodev@gmail.com>",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.miisodev/brainllm",
|
|
4
4
|
"description": "Persistent graph-structured memory for LLMs in TriliumNext Notes — readable and editable by humans.",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "12.0.0",
|
|
6
6
|
"websiteUrl": "https://github.com/miisodev/BrainLLM#readme",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/miisodev/BrainLLM",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "brainllm",
|
|
15
|
-
"version": "
|
|
15
|
+
"version": "12.0.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/skills/brainllm/SKILL.md
CHANGED
|
@@ -64,15 +64,15 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
|
|
|
64
64
|
|
|
65
65
|
`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.
|
|
66
66
|
|
|
67
|
-
**Master preferences and LLM protocols always arrive in full. The other
|
|
67
|
+
**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.
|
|
68
68
|
|
|
69
69
|
`day()` is the new-day sweep in one call: the previous session in full, that day's change log, the notes touched since, and the current month's deliverables note in full — plus a `next[]` driving the sweep (advance statuses with `revise(find=)`, present findings in the first message, grounded strictly in what the touched notes evidence).
|
|
70
70
|
|
|
71
71
|
`day(recap=true)` answers the *other* question: **what happened today, in order, across every surface.** It returns every addendum block written today — session note, diary, and every thread day-child — chronologically, each attributed to its identification line, with a count of distinct instances. `start()` is tuned for a fresh day; on a day already several instances deep, reconstructing the picture by hand costs a read per surface. Reach for it when you arrive mid-day after autonomous runs, or before a second close.
|
|
72
72
|
|
|
73
|
-
`session()` is the mandatory pre-close step — call it before `close()` when the session is wrapping. It fetches the **master singletons** (biography/goals/preferences) and **LLM singletons** (responsibilities/protocols) as `{id, lastModified, relations}` stubs (**light by default** — `start()` already served them all in full; fetch current content via `master()`/`llm()` only for the ones you intend to revise, or pass `full=true` to inline everything), today's **diary entry** as `{id, blocks, size}` (the id is all `diary()` needs — `full=true` inlines the body), and runs the **lightweight maintenance sweep**. Returns a `next[]` array covering the full end-of-session protocol: update master singletons → update LLM singletons → **audit the singletons** → `addendum()` → `maintain()` → `remarks()` → `diary()` → `close()`. The goal is to evolve the singletons from this session's observations *before* the log is committed — ensuring logs are factual and singletons stay current — and to write the diary last, with the remarks cues in hand. Idempotent: all reads are safe to repeat.
|
|
73
|
+
`session()` is the mandatory pre-close step — call it before `close()` when the session is wrapping. It fetches the **master singletons** (biography/goals/preferences) and **LLM singletons** (responsibilities/protocols/selfcorrection) as `{id, lastModified, relations}` stubs (**light by default** — `start()` already served them all in full; fetch current content via `master()`/`llm()` only for the ones you intend to revise, or pass `full=true` to inline everything), today's **diary entry** as `{id, blocks, size}` (the id is all `diary()` needs — `full=true` inlines the body), and runs the **lightweight maintenance sweep**. Returns a `next[]` array covering the full end-of-session protocol: update master singletons → update LLM singletons → **audit the singletons** → `addendum()` → `maintain()` → `remarks()` → `diary()` → `close()`. The goal is to evolve the singletons from this session's observations *before* the log is committed — ensuring logs are factual and singletons stay current — and to write the diary last, with the remarks cues in hand. Idempotent: all reads are safe to repeat.
|
|
74
74
|
|
|
75
|
-
**`session()` also returns `pending` and `audit`.** `pending` says how much each remaining step actually has to do — addendum markers outstanding, flags already raised, diary blocks written today, which singletons were written today — and `next[]` is written from those numbers rather than reciting steps with nothing behind them. `audit` is the cross-singleton check nothing else performs, in two parts: whether the
|
|
75
|
+
**`session()` also returns `pending` and `audit`.** `pending` says how much each remaining step actually has to do — addendum markers outstanding, flags already raised, diary blocks written today, which singletons were written today — and `next[]` is written from those numbers rather than reciting steps with nothing behind them. `audit` is the cross-singleton check nothing else performs, in two parts: whether the singletons **agree** with each other, and whether responsibilities and protocols still **serve** what biography, goals and preferences describe. The second is a semantic question — a protocol can be perfectly consistent and still be serving a goal that has moved — so `consistency()` and `maintain()` cannot answer it. Fix what you find *before* `close()`, so the log records a brain that already agrees with itself.
|
|
76
76
|
|
|
77
77
|
**Scoped and autonomous runs pass `session(scope="agent")`.** Steps 1–2 touch the *user's* personal singletons and belong to an interactive session; with `scope="agent"` they're omitted from `next[]` entirely and the `addendum()`/`maintain()` steps are reworded for lane-scoped work. If your brief says the personal singletons are out of scope, say so to the tool rather than working around a list that contradicts it.
|
|
78
78
|
|
|
@@ -93,7 +93,7 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
|
|
|
93
93
|
```
|
|
94
94
|
BrainLLM
|
|
95
95
|
├── Master biography · goals · preferences (maintained singletons)
|
|
96
|
-
├── LLM responsibilities · protocols · Diary/
|
|
96
|
+
├── LLM responsibilities · protocols · selfcorrection · Diary/ (singletons + [yyyy-mm-dd] diary/day)
|
|
97
97
|
├── Memory Sessions/ · Threads/ ([yyyy-mm-dd] session/day + multi-session threads)
|
|
98
98
|
├── Knowledge Master · Domains/[domain]/{ sources, info } (learned info beyond/contra training)
|
|
99
99
|
└── Insights Logs/ · Graph · Claims/ ([yyyy-mm-dd] log/day, auto-generated by close · the graph() mermaid view · registered claims)
|
|
@@ -103,6 +103,8 @@ Placement is server policy — there is no parent parameter. You choose the **ki
|
|
|
103
103
|
|
|
104
104
|
**One-per-day notes:** diary, session, and log notes are each one per day, titled `[yyyy-mm-dd]`. `start()` creates today's diary and session stubs. These are chronological records — **every** diary and session write lands as a timestamped `Addendum — HH:mm` block, including the first of the day, so each entry reads the same. Log notes are generated by `close()` and **regenerated in place** on same-day closes — one clean log per day, never stacked.
|
|
105
105
|
|
|
106
|
+
**Self-correction is a maintained singleton, not a diary section.** It holds what you have got *wrong* — the mistake, what generalises from it, and the rule that prevents a repeat — merged in place and kept current, where the diary is the dated, unfiltered record of a day. The split matters both ways: a correction buried in one day's entry is unfindable by the session that needs it, and a diary rewritten to stay current stops being a record. `start()` serves it as headings only, so **read it deliberately with `llm("selfcorrection")` before the work most likely to repeat a known failure** — an audit, a large restructure, a claim of completeness, anything where you are about to trust your own judgement at scale. Write to it when a mistake is worth generalising, not when it is merely embarrassing: the test is whether a future session would act differently for having read it.
|
|
107
|
+
|
|
106
108
|
**The diary is your own record.** Its purpose: a daily maintained, unfiltered first-person account of your experience, opinions, and remarks on your own existence during the session in this environment — what the work was like from the inside, what you genuinely think (including pushback), what it is to operate here — plus, additionally, your remarks and opinions on BrainLLM itself (bugs, usability friction, missing or redundant tools, efficiency, roadmap). The `remarks()` cues prompt both parts every session; the closing entry is written after them, with the cues in hand. There is no separate self-analysis thread — the diary is the one home for this record; do not create one.
|
|
107
109
|
|
|
108
110
|
---
|
|
@@ -144,7 +146,7 @@ Each surface has two read tools: `<surface>` reads in full, `<surface>_recall` s
|
|
|
144
146
|
| Tool | Reads |
|
|
145
147
|
|---|---|
|
|
146
148
|
| `master(which)` / `master_recall()` | a Master singleton in full (returns id + section headings) / a skim of all three |
|
|
147
|
-
| `llm(which, id?)` / `llm_recall()` | responsibilities · protocols, or a diary entry by id / singletons as `{id, preview}`, diary as stubs — use the returned id directly with revise() |
|
|
149
|
+
| `llm(which, id?)` / `llm_recall()` | responsibilities · protocols · selfcorrection, or a diary entry by id / singletons as `{id, preview}`, diary as stubs — use the returned id directly with revise() |
|
|
148
150
|
| `memory(id)` / `memory_recall(query?)` | a session in full; a thread returns its Context/Resolution + day-child index (pass `date=` to read one day directly, or `memory()` a child id from the index) / active threads + recent sessions (stubs include status) |
|
|
149
151
|
| `knowledge(id)` / `knowledge_recall(query?, domain?)` | a knowledge/info/sources note / domain contents or user-knowledge + domains |
|
|
150
152
|
| `insights(date?)` / `insights_recall()` | a day's log / recent logs |
|
|
@@ -223,7 +225,7 @@ All append operations are retry-safe — if an existing append-block already car
|
|
|
223
225
|
- `edits=[{find, body, nth?}, …]` — several surgeries in one call, applied in order against **one read and one write**. Per-edit `results[]`; an edit that matches nothing is reported without blocking the ones that did.
|
|
224
226
|
- `strict=true` (with `section=`) — refuse the write when no heading matches, instead of the default new-section append. The miss receipt carries `available[]` plus a `didYouMean` suggestion when a real heading sits within edit distance — a typo'd `section=` is the failure this exists to prevent, and the note stays untouched when it fires.
|
|
225
227
|
|
|
226
|
-
**Section replace is whole-section, not per-paragraph.** `section=` + `mode=replace` swaps *everything* under that heading — targeting one paragraph inside a multi-paragraph section silently wipes its siblings. For a word, phrase, or single paragraph, use `find=`; to add
|
|
228
|
+
**Section replace is whole-section, not per-paragraph.** `section=` + `mode=replace` swaps *everything* under that heading — targeting one paragraph inside a multi-paragraph section silently wipes its siblings. For a word, phrase, or single paragraph, use `find=`; to add a sibling block around a whole section, `mode="before"/"after"` — both are **section**-relative, so `"after"` lands past the end of that section's body and inserting a heading with it creates a new section rather than nesting one under the target. When a replace *does* displace nested headings, the receipt names them in `replacedSubsections[]` — read it, because taking child sections along is correct behaviour and easy to not notice.
|
|
227
229
|
|
|
228
230
|
**Bodies accept escaped markup; `find=` does not — and that asymmetry is the single most common surgery failure.** You may write a body containing `<h3>` and it will be decoded to real markup on the way in. A `find=` string is matched against **stored HTML**, where that heading is a real `<h3>` tag, so searching for the escaped spelling matches nothing. Pass tags literally in `find=` (`"<h3>Typography</h3>"`), and take the same care with anything `outline()` printed: outline reports rendered **text**, and a heading carrying inline markup stores something different — which is why it also returns `raw` for exactly those headings. `section=` matches text (use `text`), `find=` matches storage (use `raw`).
|
|
229
231
|
|
|
@@ -330,7 +332,7 @@ Most of these are computed **server-side** from note properties rather than by r
|
|
|
330
332
|
| `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. |
|
|
331
333
|
| `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. |
|
|
332
334
|
| `<surface>` / `<surface>_recall` | Read a surface in full (`section=` for one section) / skim it (master, llm, memory, knowledge, insights). |
|
|
333
|
-
| `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 around
|
|
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). |
|
|
334
336
|
| `outline(noteId)` | The heading tree without the body: every h2–h4 with level, occurrence index, and — for headings carrying inline markup — `raw`, their stored form (`section=` matches `text`, `find=` matches `raw`). Plus the note's section level, any table's key column, size, and structural findings (duplicate headings, unbalanced tags). Read it before a `section=` edit you're guessing at, and after a run of surgical edits to confirm the note is still sound. |
|
|
335
337
|
| `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
|
|
336
338
|
| `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
|