brainllm 11.0.0 → 12.1.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 +116 -48
- 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,8 +25451,12 @@ var SingletonKinds = [
|
|
|
25449
25451
|
"goals",
|
|
25450
25452
|
"preferences",
|
|
25451
25453
|
"responsibilities",
|
|
25452
|
-
"protocols"
|
|
25454
|
+
"protocols",
|
|
25455
|
+
"selfcorrection"
|
|
25453
25456
|
];
|
|
25457
|
+
function isSingletonKind(k) {
|
|
25458
|
+
return SingletonKinds.includes(k);
|
|
25459
|
+
}
|
|
25454
25460
|
var Statuses = ["active", "resolved", "superseded", "dormant", "eternal"];
|
|
25455
25461
|
var RelationTypes = [
|
|
25456
25462
|
"relatesTo",
|
|
@@ -25972,10 +25978,10 @@ class TriliumClient {
|
|
|
25972
25978
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
25973
25979
|
import { join, dirname } from "path";
|
|
25974
25980
|
var EMPTY_BRAINLLM = {
|
|
25975
|
-
version:
|
|
25981
|
+
version: 10,
|
|
25976
25982
|
root: "",
|
|
25977
25983
|
master: { root: "", biography: "", goals: "", preferences: "" },
|
|
25978
|
-
llm: { root: "", responsibilities: "", protocols: "", diary: "" },
|
|
25984
|
+
llm: { root: "", responsibilities: "", protocols: "", selfcorrection: "", diary: "" },
|
|
25979
25985
|
memory: { root: "", sessions: "", threads: "", metaThread: "" },
|
|
25980
25986
|
knowledge: { root: "", master: "", domains: "" },
|
|
25981
25987
|
insights: { root: "", logs: "" },
|
|
@@ -26016,9 +26022,10 @@ function loadConfig() {
|
|
|
26016
26022
|
return null;
|
|
26017
26023
|
try {
|
|
26018
26024
|
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
26019
|
-
if (typeof parsed?.root === "string" &&
|
|
26025
|
+
if (typeof parsed?.root === "string" && [10, 9, 8, 5].includes(parsed?.version)) {
|
|
26020
26026
|
return {
|
|
26021
26027
|
...parsed,
|
|
26028
|
+
llm: { selfcorrection: "", ...parsed.llm ?? {} },
|
|
26022
26029
|
memory: { metaThread: "", ...parsed.memory ?? {} },
|
|
26023
26030
|
policy: { ...DEFAULT_POLICY, ...parsed.policy ?? {} }
|
|
26024
26031
|
};
|
|
@@ -26028,7 +26035,7 @@ function loadConfig() {
|
|
|
26028
26035
|
}
|
|
26029
26036
|
function saveConfig(config2) {
|
|
26030
26037
|
const path = configFilePath();
|
|
26031
|
-
writeFileSync(path, JSON.stringify({ ...config2, version:
|
|
26038
|
+
writeFileSync(path, JSON.stringify({ ...config2, version: 10 }, null, 2) + `
|
|
26032
26039
|
`, "utf-8");
|
|
26033
26040
|
return path;
|
|
26034
26041
|
}
|
|
@@ -26062,7 +26069,7 @@ async function discoverBrainLLM(trilium) {
|
|
|
26062
26069
|
config2.master = { root: id, biography: g("Biography"), goals: g("Goals"), preferences: g("Preferences") };
|
|
26063
26070
|
break;
|
|
26064
26071
|
case "LLM":
|
|
26065
|
-
config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), diary: g("Diary") };
|
|
26072
|
+
config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), selfcorrection: g("Self-correction"), diary: g("Diary") };
|
|
26066
26073
|
break;
|
|
26067
26074
|
case "Memory":
|
|
26068
26075
|
config2.memory = { root: id, sessions: g("Sessions"), threads: g("Threads"), metaThread: "" };
|
|
@@ -26756,8 +26763,9 @@ function setSection(html, heading, content, mode, occurrence = 1) {
|
|
|
26756
26763
|
${html.slice(headingStart)}`, matched: true, headingCount };
|
|
26757
26764
|
}
|
|
26758
26765
|
if (mode === "after") {
|
|
26759
|
-
return { html: `${html.slice(0,
|
|
26760
|
-
${content}
|
|
26766
|
+
return { html: `${html.slice(0, end).replace(/\s*$/, "")}
|
|
26767
|
+
${content}
|
|
26768
|
+
${html.slice(end)}`, matched: true, headingCount };
|
|
26761
26769
|
}
|
|
26762
26770
|
const existing = html.slice(contentStart, end).trim();
|
|
26763
26771
|
const inner = mode === "append" && existing ? `${existing}
|
|
@@ -27007,7 +27015,8 @@ var STRUCTURED_SINGLETONS = new Set([
|
|
|
27007
27015
|
"goals",
|
|
27008
27016
|
"preferences",
|
|
27009
27017
|
"responsibilities",
|
|
27010
|
-
"protocols"
|
|
27018
|
+
"protocols",
|
|
27019
|
+
"selfcorrection"
|
|
27011
27020
|
]);
|
|
27012
27021
|
var hasHeading = (html, text) => new RegExp(`<h[2-4](?:\\s[^>]*)?>\\s*${text}\\s*</h[2-4]>`, "i").test(html);
|
|
27013
27022
|
var REQUIRED_SECTIONS = {
|
|
@@ -27063,8 +27072,10 @@ ${o.goal ? `<p>${escapeHtml(o.goal)}</p>` : o.body || "<p>To be determined.</p>"
|
|
|
27063
27072
|
<p></p>`].join(`
|
|
27064
27073
|
`);
|
|
27065
27074
|
case "protocols":
|
|
27066
|
-
return ["<h2>Operating</h2>", o.body || "<p></p>"
|
|
27067
|
-
|
|
27075
|
+
return ["<h2>Operating</h2>", o.body || "<p></p>"].join(`
|
|
27076
|
+
`);
|
|
27077
|
+
case "selfcorrection":
|
|
27078
|
+
return ["<h2>Corrections</h2>", o.body || "<p></p>"].join(`
|
|
27068
27079
|
`);
|
|
27069
27080
|
case "domain":
|
|
27070
27081
|
return domainContent(o.domain ?? "");
|
|
@@ -27235,7 +27246,7 @@ function structureRuleFor(kind) {
|
|
|
27235
27246
|
|
|
27236
27247
|
// src/router.ts
|
|
27237
27248
|
function isSingleton(kind) {
|
|
27238
|
-
return
|
|
27249
|
+
return isSingletonKind(kind);
|
|
27239
27250
|
}
|
|
27240
27251
|
function kindHome(cfg, kind) {
|
|
27241
27252
|
switch (kind) {
|
|
@@ -27249,6 +27260,8 @@ function kindHome(cfg, kind) {
|
|
|
27249
27260
|
return cfg.llm.responsibilities;
|
|
27250
27261
|
case "protocols":
|
|
27251
27262
|
return cfg.llm.protocols;
|
|
27263
|
+
case "selfcorrection":
|
|
27264
|
+
return cfg.llm.selfcorrection;
|
|
27252
27265
|
case "diary":
|
|
27253
27266
|
return cfg.llm.diary;
|
|
27254
27267
|
case "session":
|
|
@@ -27363,6 +27376,8 @@ function locationLabel(kind, domainTitle) {
|
|
|
27363
27376
|
return "LLM \u2192 Responsibilities";
|
|
27364
27377
|
case "protocols":
|
|
27365
27378
|
return "LLM \u2192 Protocols";
|
|
27379
|
+
case "selfcorrection":
|
|
27380
|
+
return "LLM \u2192 Self-correction";
|
|
27366
27381
|
case "diary":
|
|
27367
27382
|
return "LLM \u2192 Diary";
|
|
27368
27383
|
case "session":
|
|
@@ -27397,6 +27412,7 @@ function structuralIds(cfg) {
|
|
|
27397
27412
|
cfg.llm.root,
|
|
27398
27413
|
cfg.llm.responsibilities,
|
|
27399
27414
|
cfg.llm.protocols,
|
|
27415
|
+
cfg.llm.selfcorrection,
|
|
27400
27416
|
cfg.llm.diary,
|
|
27401
27417
|
cfg.memory.root,
|
|
27402
27418
|
cfg.memory.sessions,
|
|
@@ -27417,7 +27433,8 @@ function isContainer(cfg, noteId) {
|
|
|
27417
27433
|
cfg.master.goals,
|
|
27418
27434
|
cfg.master.preferences,
|
|
27419
27435
|
cfg.llm.responsibilities,
|
|
27420
|
-
cfg.llm.protocols
|
|
27436
|
+
cfg.llm.protocols,
|
|
27437
|
+
cfg.llm.selfcorrection
|
|
27421
27438
|
];
|
|
27422
27439
|
return isStructural(cfg, noteId) && !singletons.includes(noteId);
|
|
27423
27440
|
}
|
|
@@ -27760,6 +27777,8 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
27760
27777
|
if (reported.has(pairKey))
|
|
27761
27778
|
continue;
|
|
27762
27779
|
reported.add(pairKey);
|
|
27780
|
+
if (acknowledged(a) || acknowledged(bn))
|
|
27781
|
+
continue;
|
|
27763
27782
|
const [ha, hb] = await Promise.all([headingsOf(a), headingsOf(bn)]);
|
|
27764
27783
|
const overlap = ha.filter((h) => hb.some((x) => x.toLowerCase() === h.toLowerCase()));
|
|
27765
27784
|
const show = (h) => h.length ? h.slice(0, 8).join(" \xB7 ") + (h.length > 8 ? " \xB7 \u2026" : "") : "(no headings)";
|
|
@@ -27932,7 +27951,8 @@ async function buildDigest(trilium, cfg, opts = {}) {
|
|
|
27932
27951
|
]),
|
|
27933
27952
|
Promise.all([
|
|
27934
27953
|
readSlot("responsibilities", cfg.llm.responsibilities),
|
|
27935
|
-
readSlot("protocols", cfg.llm.protocols)
|
|
27954
|
+
readSlot("protocols", cfg.llm.protocols),
|
|
27955
|
+
readSlot("selfcorrection", cfg.llm.selfcorrection)
|
|
27936
27956
|
])
|
|
27937
27957
|
]);
|
|
27938
27958
|
digest.master.push(...masterSlots.filter((s) => !!s));
|
|
@@ -28042,9 +28062,10 @@ async function createBrainLLMStructure(trilium) {
|
|
|
28042
28062
|
leaf(masterRoot, "Preferences", "preferences", "A single maintained note of the master/user's preferences.")
|
|
28043
28063
|
]);
|
|
28044
28064
|
const llmRoot = await book(rootId, "LLM", CONTAINER_PURPOSES.llm, "bx bx-bot");
|
|
28045
|
-
const [responsibilities, protocols, diary] = await Promise.all([
|
|
28065
|
+
const [responsibilities, protocols, selfcorrection, diary] = await Promise.all([
|
|
28046
28066
|
leaf(llmRoot, "Responsibilities", "responsibilities", "A single maintained note of the assistant's responsibilities to the master/user, derived from their goals and preferences."),
|
|
28047
28067
|
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."),
|
|
28068
|
+
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
28069
|
book(llmRoot, "Diary", CONTAINER_PURPOSES.llmDiary, "bx bx-book-heart")
|
|
28049
28070
|
]);
|
|
28050
28071
|
const memoryRoot = await book(rootId, "Memory", CONTAINER_PURPOSES.memory, "bx bx-been-here");
|
|
@@ -28060,10 +28081,10 @@ async function createBrainLLMStructure(trilium) {
|
|
|
28060
28081
|
const insightsRoot = await book(rootId, "Insights", CONTAINER_PURPOSES.insights, "bx bx-bulb");
|
|
28061
28082
|
const logs = await book(insightsRoot, "Logs", CONTAINER_PURPOSES.insightsLogs, "bx bx-history");
|
|
28062
28083
|
return {
|
|
28063
|
-
version:
|
|
28084
|
+
version: 10,
|
|
28064
28085
|
root: rootId,
|
|
28065
28086
|
master: { root: masterRoot, biography, goals, preferences },
|
|
28066
|
-
llm: { root: llmRoot, responsibilities, protocols, diary },
|
|
28087
|
+
llm: { root: llmRoot, responsibilities, protocols, selfcorrection, diary },
|
|
28067
28088
|
memory: { root: memoryRoot, sessions, threads, metaThread: "" },
|
|
28068
28089
|
knowledge: { root: knowledgeRoot, master: knowledgeMaster, domains },
|
|
28069
28090
|
insights: { root: insightsRoot, logs },
|
|
@@ -28234,12 +28255,19 @@ day's blocks rather than the whole week. outline(id) lists the headings.`, {
|
|
|
28234
28255
|
// src/tools-llm.ts
|
|
28235
28256
|
function registerLlmTools(server, trilium, brainRef) {
|
|
28236
28257
|
const b = () => brainRef.config;
|
|
28237
|
-
server.tool("llm", `Read an LLM note: responsibilities or
|
|
28258
|
+
server.tool("llm", `Read an LLM note: responsibilities, protocols or selfcorrection (singletons), or a diary
|
|
28259
|
+
entry by id.
|
|
28260
|
+
|
|
28261
|
+
selfcorrection holds the assistant's own corrections \u2014 what it got wrong, what generalises, and
|
|
28262
|
+
the rule that prevents a repeat. It is the one LLM singleton start() does NOT serve in full: it
|
|
28263
|
+
is the largest and the least load-bearing at a first message, so it arrives as section headings
|
|
28264
|
+
and is read here on demand. Reach for it when about to repeat a class of work that has gone
|
|
28265
|
+
wrong before \u2014 an audit, a large edit, a claim of completeness.
|
|
28238
28266
|
|
|
28239
28267
|
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"]),
|
|
28268
|
+
protocols and selfcorrection, the two largest singletons and the ones most often needed in part
|
|
28269
|
+
rather than whole. outline(id) lists the headings.`, {
|
|
28270
|
+
which: exports_external.enum(["responsibilities", "protocols", "selfcorrection", "diary"]),
|
|
28243
28271
|
id: exports_external.string().optional().describe("diary only: the entry id from llm_recall"),
|
|
28244
28272
|
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
28245
28273
|
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
@@ -28250,20 +28278,23 @@ whole. outline(id) lists the headings.`, {
|
|
|
28250
28278
|
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
28251
28279
|
}
|
|
28252
28280
|
const noteId = b().llm[which];
|
|
28253
|
-
if (!noteId)
|
|
28254
|
-
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
28281
|
+
if (!noteId) {
|
|
28282
|
+
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.");
|
|
28283
|
+
}
|
|
28255
28284
|
return txt(await readFull(trilium, noteId, { section, occurrence }));
|
|
28256
28285
|
});
|
|
28257
|
-
server.tool("llm_recall", "Skim the LLM surface: responsibilities &
|
|
28286
|
+
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
28287
|
const cfg = b();
|
|
28259
|
-
const [resp, prot, diary] = await Promise.all([
|
|
28288
|
+
const [resp, prot, selfcorr, diary] = await Promise.all([
|
|
28260
28289
|
previewWithRelations(trilium, cfg.llm.responsibilities),
|
|
28261
28290
|
previewWithRelations(trilium, cfg.llm.protocols),
|
|
28291
|
+
cfg.llm.selfcorrection ? previewWithRelations(trilium, cfg.llm.selfcorrection) : Promise.resolve(null),
|
|
28262
28292
|
skim(trilium, cfg.llm.diary, { kind: "diary", limit: limit ?? 7 })
|
|
28263
28293
|
]);
|
|
28264
28294
|
return txt({
|
|
28265
28295
|
responsibilities: { id: cfg.llm.responsibilities, ...resp },
|
|
28266
28296
|
protocols: { id: cfg.llm.protocols, ...prot },
|
|
28297
|
+
...selfcorr ? { selfcorrection: { id: cfg.llm.selfcorrection, ...selfcorr } } : {},
|
|
28267
28298
|
diary
|
|
28268
28299
|
});
|
|
28269
28300
|
});
|
|
@@ -28711,12 +28742,13 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
28711
28742
|
const relations = relationSnippet(note);
|
|
28712
28743
|
return { id, lastModified: note.dateModified.slice(0, 10), content, ...relations ? { relations } : {} };
|
|
28713
28744
|
};
|
|
28714
|
-
const [biography, goals, preferences, responsibilities, protocols] = await Promise.all([
|
|
28745
|
+
const [biography, goals, preferences, responsibilities, protocols, selfcorrection] = await Promise.all([
|
|
28715
28746
|
fetchSingleton(cfg.master.biography),
|
|
28716
28747
|
fetchSingleton(cfg.master.goals),
|
|
28717
28748
|
fetchSingleton(cfg.master.preferences),
|
|
28718
28749
|
fetchSingleton(cfg.llm.responsibilities),
|
|
28719
|
-
fetchSingleton(cfg.llm.protocols)
|
|
28750
|
+
fetchSingleton(cfg.llm.protocols),
|
|
28751
|
+
cfg.llm.selfcorrection ? fetchSingleton(cfg.llm.selfcorrection) : Promise.resolve(null)
|
|
28720
28752
|
]);
|
|
28721
28753
|
let diaryEntry = null;
|
|
28722
28754
|
if (cfg.llm.diary) {
|
|
@@ -28736,14 +28768,15 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
28736
28768
|
const contents = await Promise.all(candidates.map((n) => trilium.getNoteContent(n.noteId).catch(() => "")));
|
|
28737
28769
|
return contents.filter(hasAddendumMarker).length;
|
|
28738
28770
|
}).catch(() => null);
|
|
28739
|
-
const singletonStubs =
|
|
28740
|
-
|
|
28741
|
-
|
|
28742
|
-
|
|
28743
|
-
|
|
28744
|
-
|
|
28745
|
-
|
|
28746
|
-
|
|
28771
|
+
const singletonStubs = {
|
|
28772
|
+
biography,
|
|
28773
|
+
goals,
|
|
28774
|
+
preferences,
|
|
28775
|
+
responsibilities,
|
|
28776
|
+
protocols,
|
|
28777
|
+
selfcorrection
|
|
28778
|
+
};
|
|
28779
|
+
const touchedToday = Object.keys(singletonStubs).filter((k) => singletonStubs[k]?.lastModified === d);
|
|
28747
28780
|
const pending = {
|
|
28748
28781
|
addendums: pendingAddendums === null ? "unknown" : pendingAddendums,
|
|
28749
28782
|
maintenanceFlags: hygiene?.flagged.length ?? 0,
|
|
@@ -28756,19 +28789,19 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
28756
28789
|
...scoped ? { scope: "agent" } : {},
|
|
28757
28790
|
...!full ? { mode: "light", note: "Singleton content and the diary body are omitted (default) \u2014 fetch via master()/llm() only where lastModified indicates a revision is needed; start() already served all singletons in full, and diary() needs only the id." } : {},
|
|
28758
28791
|
master: { biography, goals, preferences },
|
|
28759
|
-
llm: { responsibilities, protocols },
|
|
28792
|
+
llm: { responsibilities, protocols, ...selfcorrection ? { selfcorrection } : {} },
|
|
28760
28793
|
diary: diaryEntry,
|
|
28761
28794
|
maintenance: hygiene ? { scanned: hygiene.scanned, fixed: hygiene.fixed.length, transitions: hygiene.transitions, flagged: hygiene.flagged, ...hygiene.suppressed ? { suppressed: hygiene.suppressed } : {} } : "skipped",
|
|
28762
28795
|
pending,
|
|
28763
28796
|
audit: scoped ? undefined : {
|
|
28764
|
-
consistency: "Read
|
|
28797
|
+
consistency: "Read every singleton and check for ambiguity, internal contradiction, or claims that disagree across them. Fix what you find with revise() BEFORE close(), so the log records a brain that already agrees with itself.",
|
|
28765
28798
|
alignment: "Then check correlation, not just agreement: do responsibilities and protocols actually serve what biography, goals and preferences describe? A protocol can be perfectly consistent and still be serving a goal that has moved.",
|
|
28766
|
-
readThem: `master("biography"|"goals"|"preferences") and llm("responsibilities"|"protocols") \u2014 or one section at a time with section=. Written today: ${touchedToday.length ? touchedToday.join(", ") : "none"}.`
|
|
28799
|
+
readThem: `master("biography"|"goals"|"preferences") and llm("responsibilities"|"protocols"|"selfcorrection") \u2014 or one section at a time with section=. Written today: ${touchedToday.length ? touchedToday.join(", ") : "none"}.`
|
|
28767
28800
|
},
|
|
28768
28801
|
next: [
|
|
28769
28802
|
...scoped ? ["Scoped run \u2014 the user's master singletons and your LLM singletons are OUT of scope and deliberately not listed here."] : [
|
|
28770
28803
|
"Update master singletons (biography / goals / preferences) via revise() with session observations about the user.",
|
|
28771
|
-
"Update LLM singletons (responsibilities / protocols) via revise() with session observations about yourself."
|
|
28804
|
+
"Update LLM singletons (responsibilities / protocols / selfcorrection) via revise() with session observations about yourself \u2014 selfcorrection takes what you got WRONG and what generalises from it, not what you did."
|
|
28772
28805
|
],
|
|
28773
28806
|
...scoped ? [] : ["Audit the singletons against each other and against each other's PURPOSE \u2014 see audit= above. This is the step that catches an operating rule still serving a goal that has moved."],
|
|
28774
28807
|
scoped ? `Call addendum() \u2014 fold only what is in your lane; leaving out-of-scope addendums for the next interactive session is correct, and the call itself satisfies the gate. (${pending.addendums} note(s) currently carry addendum markers.)` : pending.addendums === 0 ? "Call addendum() \u2014 0 notes currently carry addendum markers, so this should come back clean; the call still satisfies the gate." : `Call addendum() \u2014 ${pending.addendums} note(s) carry addendum markers; fold each into its section.`,
|
|
@@ -29576,14 +29609,17 @@ applies several surgeries in one call, in order, against one read and one write.
|
|
|
29576
29609
|
|
|
29577
29610
|
Granularity warning: section + mode=replace swaps the ENTIRE section body \u2014 everything under
|
|
29578
29611
|
that heading, not one paragraph within it. To change a single paragraph inside a section, use
|
|
29579
|
-
find=; to add
|
|
29612
|
+
find=; to add a sibling block around a whole section without touching its body, use
|
|
29613
|
+
mode="before"/"after". Both are section-relative: "after" lands past the end of that section's
|
|
29614
|
+
body, so inserting a heading with it creates a NEW section rather than nesting one under the
|
|
29615
|
+
target \u2014 which is what it used to do, emptying the target while reporting matched: true.`, {
|
|
29580
29616
|
noteId: exports_external.string().describe("Note to update"),
|
|
29581
29617
|
body: exports_external.string().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
|
|
29582
29618
|
title: exports_external.string().optional().describe("New title (normalized server-side)"),
|
|
29583
29619
|
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
29620
|
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
29621
|
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
|
|
29622
|
+
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
29623
|
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
29624
|
nth: exports_external.number().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
|
|
29589
29625
|
edits: exports_external.array(exports_external.object({
|
|
@@ -31035,7 +31071,12 @@ exception.`, {
|
|
|
31035
31071
|
server.tool("brain", `Surface the entire BrainLLM content tree \u2014 every typed note across all five content areas
|
|
31036
31072
|
(Master, LLM, Memory, Knowledge, Insights), grouped by area and sub-container, with
|
|
31037
31073
|
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
|
|
31074
|
+
Structural containers are excluded; only content notes appear.
|
|
31075
|
+
|
|
31076
|
+
Insights returns { logs, claims }. Claims were missing entirely before V12 \u2014 they live in a
|
|
31077
|
+
container resolved by title rather than named in the config, and this read was scoped one level
|
|
31078
|
+
too deep to reach them, so the register was invisible to the tool that calls itself the full
|
|
31079
|
+
inventory.`, {
|
|
31039
31080
|
includeArchived: exports_external.boolean().optional().describe("Include archived/resolved notes (default: false)")
|
|
31040
31081
|
}, async ({ includeArchived }) => {
|
|
31041
31082
|
const cfg = b();
|
|
@@ -31084,10 +31125,12 @@ Structural containers are excluded; only content notes appear.`, {
|
|
|
31084
31125
|
fetchFrom(cfg.memory.threads),
|
|
31085
31126
|
fetchFrom(cfg.knowledge.master),
|
|
31086
31127
|
fetchFrom(cfg.knowledge.domains),
|
|
31087
|
-
fetchFrom(cfg.insights.
|
|
31128
|
+
fetchFrom(cfg.insights.root)
|
|
31088
31129
|
]);
|
|
31089
31130
|
const diaryIds = new Set(llmDiary.map((n) => n.noteId));
|
|
31090
31131
|
const llmSingletons = llmAll.filter((n) => !diaryIds.has(n.noteId));
|
|
31132
|
+
const insightLogs = insights.filter((n) => labelOf2(n, "noteType") === "log");
|
|
31133
|
+
const insightClaims = insights.filter((n) => labelOf2(n, "noteType") === "claim");
|
|
31091
31134
|
const areas = {
|
|
31092
31135
|
Master: masterAll.map(row),
|
|
31093
31136
|
LLM: {
|
|
@@ -31102,7 +31145,10 @@ Structural containers are excluded; only content notes appear.`, {
|
|
|
31102
31145
|
master: kMaster.map(row),
|
|
31103
31146
|
domains: kDomains.map(row)
|
|
31104
31147
|
},
|
|
31105
|
-
Insights:
|
|
31148
|
+
Insights: {
|
|
31149
|
+
logs: insightLogs.map(row),
|
|
31150
|
+
claims: insightClaims.map(row)
|
|
31151
|
+
}
|
|
31106
31152
|
};
|
|
31107
31153
|
const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
|
|
31108
31154
|
return txt3({ total, areas });
|
|
@@ -31229,9 +31275,10 @@ Pass area= to zoom into one surface and get its full detail.`, {
|
|
|
31229
31275
|
...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
|
|
31230
31276
|
});
|
|
31231
31277
|
});
|
|
31232
|
-
server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config
|
|
31233
|
-
|
|
31234
|
-
|
|
31278
|
+
server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config,
|
|
31279
|
+
heals singletons a newer version introduced, and re-engraves container purposes if the structure
|
|
31280
|
+
already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
|
|
31281
|
+
LLM (Responsibilities/Protocols/Self-correction/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
|
|
31235
31282
|
Insights (Logs) \u2014 each engraved with its purpose, and writes brainllm.json. Active
|
|
31236
31283
|
immediately, no restart needed.`, {}, async () => {
|
|
31237
31284
|
if (b().root) {
|
|
@@ -31242,6 +31289,26 @@ immediately, no restart needed.`, {}, async () => {
|
|
|
31242
31289
|
return { id: child.noteId, title: child.title };
|
|
31243
31290
|
}));
|
|
31244
31291
|
const saved = saveConfig(brainRef.config);
|
|
31292
|
+
const healed = [];
|
|
31293
|
+
const ensureSingleton = async (slot, title, kind, purpose) => {
|
|
31294
|
+
if (brainRef.config.llm[slot])
|
|
31295
|
+
return;
|
|
31296
|
+
const hit = await trilium.searchNotes(`note.title = '${title}'`, { ancestorNoteId: brainRef.config.llm.root, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
31297
|
+
let id = hit.results[0]?.noteId;
|
|
31298
|
+
if (!id) {
|
|
31299
|
+
const made = await trilium.createNote(brainRef.config.llm.root, title, purposeContent(purpose) + `
|
|
31300
|
+
` + contentFor(kind, { date: localToday(), body: "" }), "text");
|
|
31301
|
+
id = made.note.noteId;
|
|
31302
|
+
await trilium.addLabel(id, "noteType", kind).catch(() => null);
|
|
31303
|
+
healed.push(`created ${title}`);
|
|
31304
|
+
} else {
|
|
31305
|
+
healed.push(`adopted existing ${title}`);
|
|
31306
|
+
}
|
|
31307
|
+
brainRef.config.llm[slot] = id;
|
|
31308
|
+
};
|
|
31309
|
+
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.");
|
|
31310
|
+
if (healed.length)
|
|
31311
|
+
saveConfig(brainRef.config);
|
|
31245
31312
|
const refreshed = [];
|
|
31246
31313
|
for (const [id, purpose] of containerPurposes(brainRef.config)) {
|
|
31247
31314
|
if (!id)
|
|
@@ -31260,6 +31327,7 @@ immediately, no restart needed.`, {}, async () => {
|
|
|
31260
31327
|
status: "already_initialized",
|
|
31261
31328
|
message: `BrainLLM structure exists. Config refreshed at: ${saved}`,
|
|
31262
31329
|
...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." } : {},
|
|
31330
|
+
...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
31331
|
root: { id: existing.noteId, title: existing.title },
|
|
31264
31332
|
children
|
|
31265
31333
|
});
|
|
@@ -32368,7 +32436,7 @@ function createServer(origin = null) {
|
|
|
32368
32436
|
const s = new McpServer({
|
|
32369
32437
|
name: "BrainLLM",
|
|
32370
32438
|
title: "BrainLLM",
|
|
32371
|
-
version: "
|
|
32439
|
+
version: "12.1.0",
|
|
32372
32440
|
icons: brandingIcons(origin)
|
|
32373
32441
|
});
|
|
32374
32442
|
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.1.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.1.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.1.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.1.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). |
|