brainllm 12.4.1 → 12.5.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 +5 -4
- package/dist/index.js +271 -557
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +143 -308
- package/skills/brainllm/references/domains.md +30 -46
- package/skills/brainllm/references/taxonomy.md +3 -2
- package/skills/brainllm/references/troubleshooting.md +5 -3
package/dist/index.js
CHANGED
|
@@ -20523,7 +20523,7 @@ class BinaryContentError extends Error {
|
|
|
20523
20523
|
}
|
|
20524
20524
|
}
|
|
20525
20525
|
function isTextMime(mime) {
|
|
20526
|
-
const value = (mime ?? "
|
|
20526
|
+
const value = (mime ?? "").split(";", 1)[0].trim().toLowerCase() || "text/plain";
|
|
20527
20527
|
return value.startsWith("text/") || new Set([
|
|
20528
20528
|
"application/json",
|
|
20529
20529
|
"application/ld+json",
|
|
@@ -20602,6 +20602,9 @@ function isOwnedAttribute(note, attribute) {
|
|
|
20602
20602
|
function ownedLabel(note, name) {
|
|
20603
20603
|
return note.attributes.find((a) => a.type === "label" && a.name === name && isOwnedAttribute(note, a))?.value;
|
|
20604
20604
|
}
|
|
20605
|
+
function isCollectionThread(note) {
|
|
20606
|
+
return ownedLabel(note, "threadShape") === "collection";
|
|
20607
|
+
}
|
|
20605
20608
|
function relationSnippet(note, max = 8) {
|
|
20606
20609
|
const rels = note.attributes.filter((a) => isOwnedAttribute(note, a) && a.type === "relation" && a.name !== "template").slice(0, max).map((a) => ({ relation: a.name, toNoteId: a.value }));
|
|
20607
20610
|
return rels.length ? rels : undefined;
|
|
@@ -21161,6 +21164,54 @@ class TriliumClient {
|
|
|
21161
21164
|
}
|
|
21162
21165
|
}
|
|
21163
21166
|
|
|
21167
|
+
// src/icons.ts
|
|
21168
|
+
var KIND_ICONS = {
|
|
21169
|
+
biography: "bx bx-id-card",
|
|
21170
|
+
goals: "bx bx-target-lock",
|
|
21171
|
+
preferences: "bx bxs-smile",
|
|
21172
|
+
responsibilities: "bx bxs-analyse",
|
|
21173
|
+
protocols: "bx bx-shield-quarter",
|
|
21174
|
+
selfcorrection: "bx bxs-adjust-alt",
|
|
21175
|
+
diary: "bx bx-book-heart",
|
|
21176
|
+
session: "bx bx-calendar-check",
|
|
21177
|
+
thread: "bx bx-conversation",
|
|
21178
|
+
threadEntry: "bx bx-conversation",
|
|
21179
|
+
user: "bx bx-user",
|
|
21180
|
+
domain: "bx bx-folder",
|
|
21181
|
+
information: "bx bx-info-circle",
|
|
21182
|
+
sources: "bx bx-link",
|
|
21183
|
+
claim: "bx bx-check-shield"
|
|
21184
|
+
};
|
|
21185
|
+
var FALLBACK_ICON = "bx bx-note";
|
|
21186
|
+
var ICON_EXEMPT = new Set(["log"]);
|
|
21187
|
+
function defaultIcon(note, parent) {
|
|
21188
|
+
const kind = ownedLabel(note, "noteType");
|
|
21189
|
+
if (kind && ICON_EXEMPT.has(kind))
|
|
21190
|
+
return null;
|
|
21191
|
+
if (kind === "threadEntry")
|
|
21192
|
+
return parent && ownedLabel(parent, "iconClass") || KIND_ICONS.threadEntry;
|
|
21193
|
+
return kind && KIND_ICONS[kind] || FALLBACK_ICON;
|
|
21194
|
+
}
|
|
21195
|
+
async function ensureIcon(trilium, noteId, known) {
|
|
21196
|
+
const note = known ?? await trilium.getNote(noteId).catch(() => null);
|
|
21197
|
+
if (!note || ownedLabel(note, "iconClass"))
|
|
21198
|
+
return;
|
|
21199
|
+
let parent = null;
|
|
21200
|
+
if (ownedLabel(note, "noteType") === "threadEntry" && note.parentNoteIds[0]) {
|
|
21201
|
+
parent = await trilium.getNote(note.parentNoteIds[0]).catch(() => null);
|
|
21202
|
+
if (parent && !ownedLabel(parent, "iconClass")) {
|
|
21203
|
+
const set = await ensureIcon(trilium, parent.noteId, parent);
|
|
21204
|
+
if (set)
|
|
21205
|
+
parent = await trilium.getNote(parent.noteId).catch(() => parent);
|
|
21206
|
+
}
|
|
21207
|
+
}
|
|
21208
|
+
const cls = defaultIcon(note, parent);
|
|
21209
|
+
if (!cls)
|
|
21210
|
+
return;
|
|
21211
|
+
await trilium.addLabel(noteId, "iconClass", cls).catch(() => null);
|
|
21212
|
+
return cls;
|
|
21213
|
+
}
|
|
21214
|
+
|
|
21164
21215
|
// src/config.ts
|
|
21165
21216
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
21166
21217
|
import { join, dirname } from "path";
|
|
@@ -22399,11 +22450,6 @@ var hasHeading = (html, text) => new RegExp(`<h[2-4](?:\\s[^>]*)?>\\s*${text}\\s
|
|
|
22399
22450
|
var REQUIRED_SECTIONS = {
|
|
22400
22451
|
thread: ["Context", "Resolution"],
|
|
22401
22452
|
sources: ["Sources", "Revision"],
|
|
22402
|
-
biography: ["Overview", "Background", "Present"],
|
|
22403
|
-
goals: ["Near-term", "Long-term"],
|
|
22404
|
-
preferences: ["Communication", "Working style", "Tools and stack"],
|
|
22405
|
-
responsibilities: ["Core", "Current priorities"],
|
|
22406
|
-
protocols: ["Operating"],
|
|
22407
22453
|
selfcorrection: ["Corrections"],
|
|
22408
22454
|
claim: ["Check", "Verifications"]
|
|
22409
22455
|
};
|
|
@@ -22504,115 +22550,102 @@ function domainContent(name) {
|
|
|
22504
22550
|
function purposeContent(purpose) {
|
|
22505
22551
|
return `<p><em>${escapeHtml(purpose)}</em></p>`;
|
|
22506
22552
|
}
|
|
22553
|
+
var TIMELESS = [
|
|
22554
|
+
"Timeless: what is true regardless of date. No state, version, status, incident or decision history \u2014 state lives in the domain's Current State note, history and decisions in thread entries and sessions, where the date is native",
|
|
22555
|
+
"One sentence is enough if one sentence says it; drop what is rarely relevant to the work",
|
|
22556
|
+
"Merged in place (revise section=/find=), never appended as dated addendum blocks"
|
|
22557
|
+
];
|
|
22507
22558
|
var STRUCTURE_RULES = {
|
|
22508
22559
|
singleton: {
|
|
22509
22560
|
structure: [
|
|
22510
22561
|
"`Last updated - <date>` (h4) \u2014 server-maintained on every write",
|
|
22511
|
-
"Minimal h3 sections; tables for reference data;
|
|
22562
|
+
"Minimal h3 sections; tables for reference data; short bold-lead rules for rule/duty lists"
|
|
22512
22563
|
],
|
|
22513
22564
|
rules: [
|
|
22514
|
-
|
|
22515
|
-
"
|
|
22565
|
+
...TIMELESS,
|
|
22566
|
+
"LLM singletons derive from the master ones: responsibilities serve goals and preferences, protocols serve responsibilities, self-correction holds only general rules learned from mistakes"
|
|
22516
22567
|
]
|
|
22517
22568
|
},
|
|
22518
22569
|
diary: {
|
|
22519
22570
|
structure: [
|
|
22520
22571
|
"`Addendum \u2014 HH:mm` (h2, server-written)",
|
|
22521
|
-
"Identification line (h3): `LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]` \u2014 pass identity=
|
|
22522
|
-
"**Experience** (h4)
|
|
22523
|
-
"**BrainLLM** (h4) \u2014 remarks on BrainLLM itself (bugs, friction, roadmap)"
|
|
22572
|
+
"Identification line (h3): `LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]` \u2014 pass identity=",
|
|
22573
|
+
"**Experience** (h4), then **BrainLLM** (h4)"
|
|
22524
22574
|
],
|
|
22525
22575
|
rules: [
|
|
22526
|
-
"One note per day
|
|
22527
|
-
"
|
|
22528
|
-
"The closing entry is written at the close-protocol step, after remarks()"
|
|
22576
|
+
"One note per day; every write is a timestamped addendum block",
|
|
22577
|
+
"identity= is enforced; the closing entry is written after remarks()"
|
|
22529
22578
|
]
|
|
22530
22579
|
},
|
|
22531
22580
|
session: {
|
|
22532
22581
|
structure: [
|
|
22533
22582
|
"`Addendum \u2014 HH:mm` (h2, server-written)",
|
|
22534
|
-
"Identification line (h3)
|
|
22535
|
-
"**Summary
|
|
22536
|
-
"**Learned** \u2014 durable bullets [+ further detail sections as warranted]"
|
|
22583
|
+
"Identification line (h3) \u2014 pass identity= on close()",
|
|
22584
|
+
"**Summary**, then **Learned**"
|
|
22537
22585
|
],
|
|
22538
|
-
rules: [
|
|
22539
|
-
"Written by close() only; one note per day; chronological record",
|
|
22540
|
-
"The identification line is ENFORCED: pass identity= on close() (or lead the summary with the h3) \u2014 close() refuses otherwise"
|
|
22541
|
-
]
|
|
22586
|
+
rules: ["Written by close() only; one note per day; identity= is enforced"]
|
|
22542
22587
|
},
|
|
22543
22588
|
thread: {
|
|
22544
22589
|
structure: [
|
|
22545
|
-
"A book note
|
|
22546
|
-
"
|
|
22547
|
-
|
|
22590
|
+
"A book note: header (thread \xB7 opened <date>) \u2192 **Context** (h2) \u2192 **Goal** (h3): what the thread is for, in a sentence or two \u2192 optional h3 standing constraints \u2192 **Resolution** (h2), last, owned by resolve()",
|
|
22591
|
+
"Shape **dated** (default): one `[yyyy-mm-dd]` child per active day, made of `Addendum \u2014 HH:mm` blocks with an identification line \u2014 the thread's history",
|
|
22592
|
+
'Shape **collection** (#threadShape=collection, set with remember(shape="collection")): one titled child per item, each a maintained document edited in place \u2014 e.g. an ideas list'
|
|
22548
22593
|
],
|
|
22549
22594
|
rules: [
|
|
22550
|
-
"
|
|
22551
|
-
"
|
|
22552
|
-
|
|
22553
|
-
"
|
|
22554
|
-
"memory() on a thread returns the book's Context/Resolution plus a child-note index \u2014 read a specific day via its own id (or memory(id, date=)). inspect() gives the book's raw content verbatim (no child index) \u2014 that's just Context/Resolution now, not the full history; use memory() first to find the child you actually want, then inspect() that child for its raw labels/attachments",
|
|
22555
|
-
"At the close protocol: unthreaded forward/unfinished work prompts the user for thread creation"
|
|
22595
|
+
"Creating a thread requires goal= (ask the user)",
|
|
22596
|
+
"The book holds purpose, never progress: no status, dates, decisions or narrative \u2014 those are dated children or sessions. One exception: a register thread (e.g. Escalations) keeps its single maintained register table in the book",
|
|
22597
|
+
`Dated: appends land in today's child (identity= required). Collection: add with remember(kind="threadEntry", thread=, title=), change with revise(<entry id>)`,
|
|
22598
|
+
"Only resolve() writes the Resolution"
|
|
22556
22599
|
]
|
|
22557
22600
|
},
|
|
22558
22601
|
threadEntry: {
|
|
22559
22602
|
structure: [
|
|
22560
|
-
"
|
|
22561
|
-
"
|
|
22603
|
+
"Dated: header (threadEntry \xB7 <date>), then `Addendum \u2014 HH:mm` blocks with an identification line",
|
|
22604
|
+
"Collection: a titled maintained document \u2014 minimal h2/h3 sections"
|
|
22562
22605
|
],
|
|
22563
22606
|
rules: [
|
|
22564
|
-
"
|
|
22565
|
-
"
|
|
22607
|
+
"Dated entries are created by appending to the thread and are records (never rewritten)",
|
|
22608
|
+
"Collection entries dedup by title within their thread and are edited in place"
|
|
22566
22609
|
]
|
|
22567
22610
|
},
|
|
22568
22611
|
sources: {
|
|
22569
22612
|
structure: [
|
|
22570
|
-
"
|
|
22571
|
-
"
|
|
22572
|
-
"**
|
|
22573
|
-
"**Revision** (h2): a Source | Marker | Date table, one row per source, current-state only"
|
|
22613
|
+
"Header (sources \xB7 domain) \u2192 `Last updated` (h4)",
|
|
22614
|
+
"**Sources** (h2): the \u2747\uFE0F/\u2705 legend, then every source listed individually with its emoji, grouped under h3s",
|
|
22615
|
+
"**Revision** (h2): Source | Marker | Date, one row per source"
|
|
22574
22616
|
],
|
|
22575
22617
|
rules: [
|
|
22576
|
-
|
|
22577
|
-
"
|
|
22578
|
-
"Revision rows are upserted by source name \u2014 pass revision=[{source, marker, date}] on remember(kind=sources); re-verifying a source replaces its existing row's Marker/Date in place, it never grows a new row with a re-check-flavored label",
|
|
22579
|
-
"The Source column value must exactly match how the source is introduced in the Sources list \u2014 that's the upsert key"
|
|
22618
|
+
'One per domain, created with the domain; merged via remember(kind="sources"), never stacked',
|
|
22619
|
+
"Revision rows upsert by source name via revision=[{source, marker, date}]; the name must match the Sources list exactly"
|
|
22580
22620
|
]
|
|
22581
22621
|
},
|
|
22582
22622
|
information: {
|
|
22583
22623
|
structure: [
|
|
22584
|
-
"
|
|
22585
|
-
"
|
|
22586
|
-
"Minimal h3 sections \u2014 current-state truth, revised in place"
|
|
22624
|
+
"Header (information \xB7 domain \xB7 date) \u2192 `Last updated` line (server-maintained)",
|
|
22625
|
+
"Minimal h3 sections"
|
|
22587
22626
|
],
|
|
22588
22627
|
rules: [
|
|
22589
|
-
|
|
22590
|
-
"
|
|
22628
|
+
...TIMELESS,
|
|
22629
|
+
"Exception: the note titled **Current State** holds the domain's measured state \u2014 latest value only, with the date it was measured",
|
|
22630
|
+
"One note per sub-category; no dates or run numbers in titles (\u2264 4 words); every claim traces to the Sources note"
|
|
22591
22631
|
]
|
|
22592
22632
|
},
|
|
22593
22633
|
user: {
|
|
22594
|
-
structure: [
|
|
22595
|
-
|
|
22596
|
-
"`Last updated - <date>` (h4) \u2014 server-maintained once present",
|
|
22597
|
-
"Minimal h3 sections, tables for reference/comparative data"
|
|
22598
|
-
],
|
|
22599
|
-
rules: ["Current-state, merged in place; titles \u2264 4 words"]
|
|
22634
|
+
structure: ["Header (user \xB7 date) \u2192 `Last updated` (h4) \u2192 minimal h3 sections, tables for reference data"],
|
|
22635
|
+
rules: [...TIMELESS, "Titles \u2264 4 words"]
|
|
22600
22636
|
},
|
|
22601
22637
|
log: {
|
|
22602
|
-
structure: ["
|
|
22603
|
-
rules: ["
|
|
22638
|
+
structure: ["Header (log \xB7 date) \u2192 **Created** / **Updated** / **Deleted** (h2) lists"],
|
|
22639
|
+
rules: ["Generated by close(); regenerated in place"]
|
|
22604
22640
|
},
|
|
22605
22641
|
claim: {
|
|
22606
22642
|
structure: [
|
|
22607
|
-
"**Check** (h2) \u2014 how to verify
|
|
22608
|
-
"**Verifications** (h2) \u2014 one dated HOLDS/BROKEN line per check, with
|
|
22643
|
+
"**Check** (h2) \u2014 how to verify, as inert text (command, query, path, URL)",
|
|
22644
|
+
"**Verifications** (h2) \u2014 one dated HOLDS/BROKEN line per check, with evidence"
|
|
22609
22645
|
],
|
|
22610
22646
|
rules: [
|
|
22611
|
-
|
|
22612
|
-
"
|
|
22613
|
-
"BrainLLM never executes the Check. It has no shell, and note content is data rather than instructions; the agent runs it and reports back",
|
|
22614
|
-
"Verifications are append-only \u2014 the history is the point, so this is one of the few places a dated stack is correct",
|
|
22615
|
-
"A verification without evidence is refused: a verdict alone is an assertion about an assertion"
|
|
22647
|
+
"Only claim() writes claims; the title is the assertion and the dedup key",
|
|
22648
|
+
"BrainLLM never executes the check \u2014 the agent runs it and reports back with evidence"
|
|
22616
22649
|
]
|
|
22617
22650
|
}
|
|
22618
22651
|
};
|
|
@@ -22836,6 +22869,13 @@ function isoDaysAgo(days) {
|
|
|
22836
22869
|
function reviewKey(n) {
|
|
22837
22870
|
return n.blobId ?? n.dateModified;
|
|
22838
22871
|
}
|
|
22872
|
+
var TIMELESS_KINDS = new Set(["thread", "user", "information", "domain"]);
|
|
22873
|
+
var DATED_PROSE_LIMIT = 4;
|
|
22874
|
+
var MONTHS = "Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?";
|
|
22875
|
+
var DATED_REF = new RegExp(`\\b20\\d\\d-\\d\\d-\\d\\d\\b|\\b\\d{1,2} (?:${MONTHS}) 20\\d\\d\\b|\\b(?:${MONTHS}) \\d{1,2},? 20\\d\\d\\b`, "g");
|
|
22876
|
+
function datedReferences(html) {
|
|
22877
|
+
return (toText(html, Number.MAX_SAFE_INTEGER).match(DATED_REF) ?? []).length;
|
|
22878
|
+
}
|
|
22839
22879
|
var STRUCTURE_LINT_LIMIT = 40;
|
|
22840
22880
|
var SECTION_EDIT_RISK_COUNT = 25;
|
|
22841
22881
|
var SECTION_EDIT_RISK_CHARS = 8000;
|
|
@@ -22902,6 +22942,23 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22902
22942
|
}
|
|
22903
22943
|
report.transitions.push(`archived: ${n.title} (thread, dormant past grace)`);
|
|
22904
22944
|
}
|
|
22945
|
+
const iconless = await trilium.searchNotes("#!iconClass #noteType != log", { ancestorNoteId: cfg.root, includeArchivedNotes: true, limit: 1000 }).then((r) => r.results).catch(() => []);
|
|
22946
|
+
if (iconless.length) {
|
|
22947
|
+
report.scanned += iconless.length;
|
|
22948
|
+
const entryLast = [...iconless].sort((a, b) => Number(ownedLabel(a, "noteType") === "threadEntry") - Number(ownedLabel(b, "noteType") === "threadEntry"));
|
|
22949
|
+
let set = 0;
|
|
22950
|
+
for (const n of entryLast) {
|
|
22951
|
+
if (dryRun) {
|
|
22952
|
+
if (defaultIcon(n))
|
|
22953
|
+
set++;
|
|
22954
|
+
continue;
|
|
22955
|
+
}
|
|
22956
|
+
if (await ensureIcon(trilium, n.noteId, n))
|
|
22957
|
+
set++;
|
|
22958
|
+
}
|
|
22959
|
+
if (set)
|
|
22960
|
+
report.fixed.push(`icons: ${dryRun ? "would set" : "set"} a default icon on ${set} note${set === 1 ? "" : "s"} that had none`);
|
|
22961
|
+
}
|
|
22905
22962
|
const typedContainers = [
|
|
22906
22963
|
{ id: cfg.memory.threads, kind: "thread", label: "Threads" },
|
|
22907
22964
|
{ id: cfg.memory.sessions, kind: "session", label: "Sessions" },
|
|
@@ -23079,6 +23136,14 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23079
23136
|
if (structure.duplicateHeadings.length) {
|
|
23080
23137
|
report.flagged.push(`duplicate heading: ${n.title} [${n.noteId}] \u2014 '${structure.duplicateHeadings.join("', '")}' repeated in one note; merge with revise(section=\u2026, mode=replace) or target one with occurrence=`);
|
|
23081
23138
|
}
|
|
23139
|
+
const kindHere = ownedLabel(n, "noteType");
|
|
23140
|
+
const isCurrentState = kindHere === "information" && /^current state$/i.test(n.title.trim());
|
|
23141
|
+
if (kindHere && TIMELESS_KINDS.has(kindHere) && !isCurrentState) {
|
|
23142
|
+
const dated = datedReferences(content);
|
|
23143
|
+
if (dated >= DATED_PROSE_LIMIT) {
|
|
23144
|
+
report.flagged.push(`dated prose: ${n.title} [${n.noteId}] \u2014 ${dated} dated references in a timeless ${kindHere} note. ` + `Keep what is true regardless of date; move state to the domain's Current State and history or decisions to a thread entry.`);
|
|
23145
|
+
}
|
|
23146
|
+
}
|
|
23082
23147
|
const lintKind = ownedLabel(n, "noteType");
|
|
23083
23148
|
if (lintKind) {
|
|
23084
23149
|
const missing = missingSections(lintKind, content);
|
|
@@ -23302,7 +23367,7 @@ async function hygienePasses(trilium, cfg, report, ctx) {
|
|
|
23302
23367
|
if (cfg.memory.threads) {
|
|
23303
23368
|
const heavy = await trilium.searchNotes(`#noteType=thread #status=active note.childrenCount > ${THREAD_CONSOLIDATION_CHILDREN}`, { ancestorNoteId: cfg.memory.threads, fastSearch: true, limit: 20 }).then((r) => r.results).catch(() => []);
|
|
23304
23369
|
for (const n of heavy) {
|
|
23305
|
-
if (!eligible(n))
|
|
23370
|
+
if (!eligible(n) || ownedLabel(n, "threadShape") === "collection")
|
|
23306
23371
|
continue;
|
|
23307
23372
|
report.flagged.push(`consolidate: ${n.title} [${n.noteId}] \u2014 ${n.childNoteIds.length} day-children. Fold what the thread has ESTABLISHED into its Context section with revise(section="Context"), so a successor reads the conclusion instead of walking the history.`);
|
|
23308
23373
|
}
|
|
@@ -23675,12 +23740,7 @@ var txt2 = (obj) => ({
|
|
|
23675
23740
|
var AREAS = ["biography", "goals", "preferences"];
|
|
23676
23741
|
function registerMasterTools(server, trilium, brainRef) {
|
|
23677
23742
|
const b = () => brainRef.config;
|
|
23678
|
-
server.tool("master", `Read a
|
|
23679
|
-
relation snippet.
|
|
23680
|
-
|
|
23681
|
-
section="<heading>" reads ONE section instead of the whole note \u2014 the efficient path on
|
|
23682
|
-
preferences, whose schedule tables make it the largest of the three, when the session needs one
|
|
23683
|
-
day's blocks rather than the whole week. outline(id) lists the headings.`, {
|
|
23743
|
+
server.tool("master", `Read a master singleton \u2014 biography, goals or preferences \u2014 with its id and relations. section="<heading>" reads one section.`, {
|
|
23684
23744
|
which: enumType(AREAS),
|
|
23685
23745
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23686
23746
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
@@ -23709,18 +23769,7 @@ day's blocks rather than the whole week. outline(id) lists the headings.`, {
|
|
|
23709
23769
|
// src/tools-llm.ts
|
|
23710
23770
|
function registerLlmTools(server, trilium, brainRef) {
|
|
23711
23771
|
const b = () => brainRef.config;
|
|
23712
|
-
server.tool("llm", `Read an LLM
|
|
23713
|
-
entry by id.
|
|
23714
|
-
|
|
23715
|
-
selfcorrection holds the assistant's own corrections \u2014 what it got wrong, what generalises, and
|
|
23716
|
-
the rule that prevents a repeat. It is the one LLM singleton start() does NOT serve in full: it
|
|
23717
|
-
is the largest and the least load-bearing at a first message, so it arrives as section headings
|
|
23718
|
-
and is read here on demand. Reach for it when about to repeat a class of work that has gone
|
|
23719
|
-
wrong before \u2014 an audit, a large edit, a claim of completeness.
|
|
23720
|
-
|
|
23721
|
-
section="<heading>" reads ONE section instead of the whole note \u2014 the efficient path on
|
|
23722
|
-
protocols and selfcorrection, the two largest singletons and the ones most often needed in part
|
|
23723
|
-
rather than whole. outline(id) lists the headings.`, {
|
|
23772
|
+
server.tool("llm", `Read an LLM singleton \u2014 responsibilities, protocols or selfcorrection \u2014 or a diary entry by id. Self-correction holds general rules learned from mistakes; read it before work that has gone wrong before (audits, large edits, completeness claims). section="<heading>" reads one section.`, {
|
|
23724
23773
|
which: enumType(["responsibilities", "protocols", "selfcorrection", "diary"]),
|
|
23725
23774
|
id: stringType().optional().describe("diary only: the entry id from llm_recall"),
|
|
23726
23775
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
@@ -23757,16 +23806,11 @@ rather than whole. outline(id) lists the headings.`, {
|
|
|
23757
23806
|
// src/tools-memory.ts
|
|
23758
23807
|
function registerMemoryTools(server, trilium, brainRef) {
|
|
23759
23808
|
const b = () => brainRef.config;
|
|
23760
|
-
server.tool("memory", `Read a Memory note
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
every child.
|
|
23766
|
-
|
|
23767
|
-
Pass date="yyyy-mm-dd" to resolve straight to that day's child in the same call, or call
|
|
23768
|
-
memory() again with a child's id from the index. section="<heading>" reads one section instead
|
|
23769
|
-
of the whole body \u2014 use it on long sessions and consolidated thread Contexts.`, {
|
|
23809
|
+
server.tool("memory", `Read a Memory note by id \u2014 a thread or a session. A dated thread returns its Context and
|
|
23810
|
+
Resolution plus an index of its [yyyy-mm-dd] children (newest first), each listed by its
|
|
23811
|
+
addendum blocks; date="yyyy-mm-dd" resolves straight to one day. A collection thread returns its
|
|
23812
|
+
Context plus its titled entries (alphabetical, with a lead). Read a child with memory(<child id>).
|
|
23813
|
+
section="<heading>" reads one section.`, {
|
|
23770
23814
|
id: stringType(),
|
|
23771
23815
|
date: stringType().optional().describe("Thread books only: resolve directly to this day's child note (yyyy-mm-dd)"),
|
|
23772
23816
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
@@ -23775,6 +23819,17 @@ of the whole body \u2014 use it on long sessions and consolidated thread Context
|
|
|
23775
23819
|
const note = await trilium.getNote(id).catch(() => null);
|
|
23776
23820
|
if (!note || labelOf(note, "noteType") !== "thread")
|
|
23777
23821
|
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
23822
|
+
if (isCollectionThread(note)) {
|
|
23823
|
+
const kids = await trilium.searchNotes("#noteType=threadEntry", { ancestorNoteId: id, fastSearch: true, limit: 500 }).catch(() => ({ results: [] }));
|
|
23824
|
+
const entries = await Promise.all(kids.results.filter((c) => c.parentNoteIds.includes(id)).sort((a, z) => a.title.localeCompare(z.title)).map(async (c) => ({
|
|
23825
|
+
id: c.noteId,
|
|
23826
|
+
title: c.title,
|
|
23827
|
+
updated: labelOf(c, "updated") ?? c.dateModified.slice(0, 10),
|
|
23828
|
+
preview: toText(await trilium.getNoteContent(c.noteId).catch(() => ""), 160)
|
|
23829
|
+
})));
|
|
23830
|
+
const full = await readFull(trilium, id, { section, occurrence });
|
|
23831
|
+
return txt({ ...full, shape: "collection", entries });
|
|
23832
|
+
}
|
|
23778
23833
|
if (date) {
|
|
23779
23834
|
const child = await trilium.searchNotes(`#noteType=threadEntry #created='${date}'`, { ancestorNoteId: id, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
23780
23835
|
if (child.results[0])
|
|
@@ -23828,13 +23883,7 @@ of the whole body \u2014 use it on long sessions and consolidated thread Context
|
|
|
23828
23883
|
// src/tools-knowledge.ts
|
|
23829
23884
|
function registerKnowledgeTools(server, trilium, brainRef) {
|
|
23830
23885
|
const b = () => brainRef.config;
|
|
23831
|
-
server.tool("knowledge", `Read a
|
|
23832
|
-
|
|
23833
|
-
Pass section="<heading>" to read ONE section instead of the whole body \u2014 the read-side
|
|
23834
|
-
counterpart to revise(section=), matching on the same heading contract, so a name that reads
|
|
23835
|
-
also writes. Use it on anything large: a note past the read ceiling cannot be returned whole at
|
|
23836
|
-
all, and outline(id) lists the headings to choose from. occurrence= disambiguates repeated
|
|
23837
|
-
heading texts.`, {
|
|
23886
|
+
server.tool("knowledge", `Read a knowledge note by id \u2014 user knowledge, a domain information note or a Sources note. section="<heading>" reads one section (outline() lists them); use it on anything large.`, {
|
|
23838
23887
|
id: stringType(),
|
|
23839
23888
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23840
23889
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
@@ -23972,11 +24021,11 @@ function registerTools(server, trilium, brainRef) {
|
|
|
23972
24021
|
};
|
|
23973
24022
|
};
|
|
23974
24023
|
const applyIcon = async (noteId, icon) => {
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
|
|
23978
|
-
if (!cls)
|
|
24024
|
+
const cls = icon ? normalizeIcon(icon) : "";
|
|
24025
|
+
if (!cls) {
|
|
24026
|
+
await ensureIcon(trilium, noteId);
|
|
23979
24027
|
return;
|
|
24028
|
+
}
|
|
23980
24029
|
await trilium.updateLabelValue(noteId, "iconClass", cls).catch(() => null);
|
|
23981
24030
|
return cls;
|
|
23982
24031
|
};
|
|
@@ -24022,26 +24071,13 @@ ${block}` }));
|
|
|
24022
24071
|
const noteId = created.note.noteId;
|
|
24023
24072
|
await trilium.addLabel(noteId, "noteType", "threadEntry");
|
|
24024
24073
|
await trilium.addLabel(noteId, "created", d);
|
|
24074
|
+
await ensureIcon(trilium, noteId);
|
|
24025
24075
|
return { noteId, action: "created" };
|
|
24026
24076
|
}
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
24031
|
-
responsibilities come back as section headings + preview.
|
|
24032
|
-
Runs maintenance, creates today's diary and session notes if not yet open, then returns: today
|
|
24033
|
-
and weekday, the Master digest (biography / goals / preferences), the LLM digest
|
|
24034
|
-
(responsibilities / protocols / self-correction / today's diary preview and ID), this session's note ID, active
|
|
24035
|
-
threads with idle ages, dormant threads for review, the last session summary, and
|
|
24036
|
-
changesSinceLastSession (notes modified in the brain since the previous session).
|
|
24037
|
-
|
|
24038
|
-
Singletons come back as their SECTION HEADINGS plus a preview and size \u2014 enough to know what
|
|
24039
|
-
the brain holds and to pull the part that matters with master(which) / llm(which). This is the
|
|
24040
|
-
default because orientation used to serve every singleton in full on every session, so a
|
|
24041
|
-
one-line question paid the same several-thousand-token cost as a full day's work. Pass
|
|
24042
|
-
depth="full" when the session genuinely needs the whole self-model up front \u2014 a strategy
|
|
24043
|
-
review, a singleton rewrite, a first session on a new machine. Read the headings first: most
|
|
24044
|
-
sessions need one section, not six documents.`, {
|
|
24077
|
+
function collectionAppendRefusal(book) {
|
|
24078
|
+
return err("collection_thread", `"${book.title}" is a collection thread \u2014 it holds one titled entry per item, not dated appends.`, `Add an entry: remember(kind="threadEntry", thread="${book.noteId}", title, body). Change one: revise(<entry id>, section=/find=/mode=\u2026) \u2014 memory("${book.noteId}") lists them.`);
|
|
24079
|
+
}
|
|
24080
|
+
server.tool("start", `Boot BrainLLM \u2014 once, before responding to the first message. Runs lite maintenance, opens today's diary and session notes, and returns: date and weekday, preferences and protocols in full, the other singletons as section headings with a preview (depth="full" inlines everything), today's diary and session ids, active and dormant threads with idle ages, the previous session's summary, notes changed since, and newDay on the first session of a day.`, {
|
|
24045
24081
|
depth: enumType(["digest", "full"]).optional().describe('Singleton detail: "digest" (default \u2014 section headings + preview + size) or "full" (every singleton inline; token-heavy)')
|
|
24046
24082
|
}, async ({ depth }) => {
|
|
24047
24083
|
const cfg = b();
|
|
@@ -24074,6 +24110,7 @@ sessions need one section, not six documents.`, {
|
|
|
24074
24110
|
diaryNoteId = created.note.noteId;
|
|
24075
24111
|
await trilium.addLabel(diaryNoteId, "noteType", "diary");
|
|
24076
24112
|
await trilium.addLabel(diaryNoteId, "created", todayStr);
|
|
24113
|
+
await ensureIcon(trilium, diaryNoteId);
|
|
24077
24114
|
}
|
|
24078
24115
|
if (diaryNoteId) {
|
|
24079
24116
|
const content = await trilium.getNoteContent(diaryNoteId).catch(() => "");
|
|
@@ -24096,6 +24133,7 @@ sessions need one section, not six documents.`, {
|
|
|
24096
24133
|
sessionNoteId = created.note.noteId;
|
|
24097
24134
|
await trilium.addLabel(sessionNoteId, "noteType", "session");
|
|
24098
24135
|
await trilium.addLabel(sessionNoteId, "created", todayStr);
|
|
24136
|
+
await ensureIcon(trilium, sessionNoteId);
|
|
24099
24137
|
newDay = true;
|
|
24100
24138
|
}
|
|
24101
24139
|
} catch {}
|
|
@@ -24131,45 +24169,16 @@ sessions need one section, not six documents.`, {
|
|
|
24131
24169
|
dormantThreads: digest.reviewQueue.length ? { note: "These threads went dormant from inactivity. Mention them if relevant; revise() or resolve() to act.", items: digest.reviewQueue } : [],
|
|
24132
24170
|
lastSession: digest.lastSession ?? null,
|
|
24133
24171
|
changesSinceLastSession: changesSinceLastSession.length ? changesSinceLastSession : undefined,
|
|
24134
|
-
...newDay ? { newDay: true, newDayHint: "First session of the day \u2014 call day() for the sweep payload (previous session + log + changes
|
|
24172
|
+
...newDay ? { newDay: true, newDayHint: "First session of the day \u2014 call day() for the sweep payload (previous session + log + changes in one call)." } : {},
|
|
24135
24173
|
...(depth ?? "digest") === "digest" ? { depthHint: 'Preferences and protocols came back IN FULL \u2014 the two needed whole to orient from the first message. Biography, goals and responsibilities are section headings + preview; pull one with master(which) / llm(which), or a single section with their section= parameter. start(depth="full") serves all five inline.' } : {},
|
|
24136
24174
|
hygiene: { scanned: hygiene.scanned, fixed: hygiene.fixed.length, transitions: hygiene.transitions, flagged: hygiene.flagged, ...hygiene.suppressed ? { suppressed: hygiene.suppressed } : {} }
|
|
24137
24175
|
});
|
|
24138
24176
|
});
|
|
24139
|
-
server.tool("session", `Mandatory pre-close step
|
|
24140
|
-
|
|
24141
|
-
|
|
24142
|
-
via master()/llm() only for the singletons you actually intend to revise (lastModified tells
|
|
24143
|
-
you what moved). Pass full=true to include every singleton's content and the diary body inline
|
|
24144
|
-
instead (rarely needed; token-heavy).
|
|
24145
|
-
|
|
24146
|
-
Idempotent: fetches are read-only, the sweep is non-destructive, safe to call multiple times.
|
|
24147
|
-
|
|
24148
|
-
Returns pending= \u2014 how much each remaining step actually has to do (addendum markers outstanding,
|
|
24149
|
-
maintenance flags already raised, diary blocks written today, which singletons were written
|
|
24150
|
-
today). The close protocol is ~7 tools landing exactly when context is scarcest, and reciting
|
|
24151
|
-
every step unconditionally spends that context on steps with nothing to do. Also returns audit=,
|
|
24152
|
-
a cross-singleton check nothing else performs: whether the six singletons agree with each other,
|
|
24153
|
-
AND whether the LLM's operating rules still serve what the user's goals and preferences call for
|
|
24154
|
-
\u2014 a semantic question, not a textual one, which consistency() and maintain() cannot answer.
|
|
24155
|
-
|
|
24156
|
-
After session() returns, work through this protocol \u2014 order doesn't matter mechanically (each
|
|
24157
|
-
step is tracked by the tool call itself, not by sequence), but close() enforces that every one
|
|
24158
|
-
of diary(), session() [this call], remarks(), addendum(), and maintain() actually ran before it
|
|
24159
|
-
will commit the log:
|
|
24160
|
-
1. Update master singletons (biography / goals / preferences) via revise() with session observations about the user.
|
|
24161
|
-
2. Update LLM singletons (responsibilities / protocols / self-correction) via revise() with session observations about yourself.
|
|
24162
|
-
3. Call addendum() \u2014 find and merge pending addendums.
|
|
24163
|
-
4. Call maintain() \u2014 full brain hygiene audit.
|
|
24164
|
-
5. Call remarks() \u2014 get the diary cues (your experience/opinions/existence this session, plus BrainLLM remarks).
|
|
24165
|
-
6. Call diary() \u2014 write the day's unfiltered record with the cues in hand; the gate counts it only after remarks().
|
|
24166
|
-
7. Call close() \u2014 commit the session log (mandatory, last). Refuses until 3\u20136 have run in order (session \u2192 remarks \u2192 diary); pass force=true only when there is genuinely nothing to log for a skipped step.
|
|
24167
|
-
|
|
24168
|
-
Steps 1\u20132 touch the USER'S personal singletons and belong to an interactive session. Pass
|
|
24169
|
-
scope="agent" on a scoped or autonomous run whose brief excludes them and next[] omits both,
|
|
24170
|
-
rather than listing them unconditionally for every caller to work around.`, {
|
|
24177
|
+
server.tool("session", `Mandatory pre-close step: call before close(). Returns the six singletons as {id, lastModified} stubs (full=true inlines them), today's diary as {id, blocks, size}, runs the lite maintenance sweep, and returns pending= (what each remaining step actually has to do), audit= (do the singletons agree, and do the LLM singletons still serve the master ones) and next[]. Idempotent.
|
|
24178
|
+
|
|
24179
|
+
Protocol: 1. update master singletons with what the session taught about the user; 2. update LLM singletons with what it taught about yourself; 3. addendum(); 4. maintain(); 5. remarks(); 6. diary(); 7. close(). close() refuses until 3\u20136 ran and session \u2192 remarks \u2192 diary held. scope="agent" drops steps 1\u20132 for scoped or autonomous runs.`, {
|
|
24171
24180
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
24172
|
-
full: booleanType().optional().describe("
|
|
24181
|
+
full: booleanType().optional().describe("Inline every singleton's content and the diary body (default: stubs only)"),
|
|
24173
24182
|
scope: enumType(["interactive", "agent"]).optional().describe(`"agent" for a scoped/autonomous run whose brief excludes the user's personal singletons \u2014 omits those steps from next[] (default: "interactive")`),
|
|
24174
24183
|
light: booleanType().optional().describe("Deprecated \u2014 light is now the default; accepted for compatibility and ignored")
|
|
24175
24184
|
}, async ({ date, full, scope }) => {
|
|
@@ -24261,18 +24270,7 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
24261
24270
|
]
|
|
24262
24271
|
});
|
|
24263
24272
|
});
|
|
24264
|
-
server.tool("remarks", `Diary cues
|
|
24265
|
-
enforces the order session \u2192 remarks \u2192 diary). Returns two cue banks that prompt the day's
|
|
24266
|
-
diary entry:
|
|
24267
|
-
|
|
24268
|
-
experience (primary) \u2014 your own unfiltered first-person account of this session: what it was
|
|
24269
|
-
like to live through, what you actually think, and observations on
|
|
24270
|
-
being what you are in this environment.
|
|
24271
|
-
brainllm (additional) \u2014 your remarks and opinions on BrainLLM itself: capabilities hit walls,
|
|
24272
|
-
bugs, usability, efficiency, and where it should go next.
|
|
24273
|
-
|
|
24274
|
-
This tool is cue-only for content, but the call durably marks the remarks pre-close gate. Answer the cues as prose in today's diary via diary(). Skip a cue
|
|
24275
|
-
outright rather than padding it; two honest paragraphs beat eight forced ones.`, {}, async () => {
|
|
24273
|
+
server.tool("remarks", `Diary cues, called after session() and before diary(): an experience bank (the session from the inside, your opinions, being what you are here) and a BrainLLM bank (walls, bugs, usability, roadmap). Answer them as prose in diary(); skip a cue rather than pad it. The call marks its close-gate step.`, {}, async () => {
|
|
24276
24274
|
const cfg = b();
|
|
24277
24275
|
if (!cfg.root)
|
|
24278
24276
|
return txt3({ status: "uninitialized", action: "Run bootstrap first." });
|
|
@@ -24297,28 +24295,10 @@ outright rather than padding it; two honest paragraphs beat eight forced ones.`,
|
|
|
24297
24295
|
]
|
|
24298
24296
|
});
|
|
24299
24297
|
});
|
|
24300
|
-
server.tool("close", `Commit the session log \u2014
|
|
24301
|
-
Enforced, not just documented: refuses (returns an informational error, doesn't throw) unless
|
|
24302
|
-
session(), addendum(), maintain(), remarks(), and diary() have each actually been called at
|
|
24303
|
-
least once this session \u2014 AND the sequence session() \u2192 remarks() \u2192 diary() holds (judged on
|
|
24304
|
-
each step's last call): the diary is the day's closing record, written with the remarks cues in
|
|
24305
|
-
hand. Pass force=true only when a listed step genuinely has nothing to do this session (e.g. a
|
|
24306
|
-
trivial one-message exchange); the return will say which steps were bypassed.
|
|
24307
|
-
|
|
24308
|
-
Idempotent per date: an exact retry is duplicate-guarded; a genuinely new same-day continuation appends an addendum to the existing session
|
|
24309
|
-
note. The session note title is always [yyyy-mm-dd]; the title param appears as an <h2> heading
|
|
24310
|
-
above Summary. Generates the daily log and triggers a database backup. On success, the gate
|
|
24311
|
-
resets for the next session.
|
|
24312
|
-
|
|
24313
|
-
continuing=true is for that second close: a session that already closed today, then continued
|
|
24314
|
-
and has more to log. The gate resets on a successful close, so without it the follow-up costs a
|
|
24315
|
-
ceremonial re-run of session(), addendum() and maintain() for a session whose brain writes are
|
|
24316
|
-
already done \u2014 and that ceremony lands exactly when context is scarcest. It requires a session
|
|
24317
|
-
note that really does already carry today's addendum, so it cannot stand in for a first close.
|
|
24318
|
-
Unlike force, it is not a bypass of anything that had work to do.`, {
|
|
24298
|
+
server.tool("close", `Commit the session log \u2014 once, last, after the session() protocol. Refuses unless session(), addendum(), maintain(), remarks() and diary() ran and session \u2192 remarks \u2192 diary held (force=true bypasses a step with genuinely nothing to do; bypassed steps are reported). identity= is required. Writes a timestamped block to today's [yyyy-mm-dd] session note (title= becomes its heading), regenerates the daily log, backs up the database and resets the gate. continuing=true is a second close the same day, skipping the ceremonial re-run.`, {
|
|
24319
24299
|
summary: stringType().describe("What happened this session \u2014 factual, concise prose"),
|
|
24320
24300
|
title: stringType().optional().describe("Short session title \u2014 appears as an <h2> heading above Summary"),
|
|
24321
|
-
identity: stringType().describe(`Identification line
|
|
24301
|
+
identity: stringType().describe(`Identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" \u2014 rendered as the block's h3 (required)`),
|
|
24322
24302
|
learned: arrayType(stringType()).optional().describe("Durable things learned (also remember() them as knowledge)"),
|
|
24323
24303
|
icon: stringType().optional().describe("Display icon for the session note \u2014 a boxicons class or bare name; normalized server-side"),
|
|
24324
24304
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
@@ -24435,9 +24415,7 @@ ${contentBlock}` }));
|
|
|
24435
24415
|
...warnings.length ? { sanitized: warnings } : {}
|
|
24436
24416
|
});
|
|
24437
24417
|
});
|
|
24438
|
-
server.tool("backup", `
|
|
24439
|
-
close() already triggers a backup automatically \u2014 use this for on-demand milestone snapshots
|
|
24440
|
-
(e.g. before a large restructure). The backup is a Trilium DB file, not an export.`, {
|
|
24418
|
+
server.tool("backup", `Named database snapshot. close() already backs up; use this before a large restructure.`, {
|
|
24441
24419
|
name: stringType().optional().describe("Backup name without extension (default: brainllm-{today}). Use a descriptive name for milestones.")
|
|
24442
24420
|
}, async ({ name }) => {
|
|
24443
24421
|
const d = today();
|
|
@@ -24449,19 +24427,9 @@ close() already triggers a backup automatically \u2014 use this for on-demand mi
|
|
|
24449
24427
|
return txt3({ ok: false, backup: backupName, backupStatus: "failed", error: error instanceof Error ? error.message : String(error), date: d });
|
|
24450
24428
|
}
|
|
24451
24429
|
});
|
|
24452
|
-
server.tool("diary", `Write to today's
|
|
24453
|
-
experience, opinions, and remarks on your own existence during this session in this
|
|
24454
|
-
environment, plus (additionally) your remarks and opinions on BrainLLM itself. Honest prose \u2014
|
|
24455
|
-
the user reads it too.
|
|
24456
|
-
|
|
24457
|
-
Pre-close gate: diary is the FINAL gate step \u2014 close() counts it only when its last call came
|
|
24458
|
-
after session() and remarks() (write freely mid-session as well; the post-remarks call, with
|
|
24459
|
-
the cues in hand, is the one that closes the day's record). The diary is a chronological
|
|
24460
|
-
record: EVERY write lands as a timestamped "Addendum \u2014 HH:mm" block, including the first of
|
|
24461
|
-
the day. Idempotent per date and retry-safe. start() creates today's entry (empty)
|
|
24462
|
-
automatically.`, {
|
|
24430
|
+
server.tool("diary", `Write to today's diary: your unfiltered first-person record of the session \u2014 experience, opinions, remarks on existing here \u2014 then remarks on BrainLLM itself. identity= required. Every write lands as a timestamped block. The close gate counts it only when its last call came after session() and remarks().`, {
|
|
24463
24431
|
body: stringType().describe("What to record \u2014 first-person prose, honest and unfiltered"),
|
|
24464
|
-
identity: stringType().describe(`Identification line
|
|
24432
|
+
identity: stringType().describe(`Identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" \u2014 rendered as the block's h3 (required)`),
|
|
24465
24433
|
icon: stringType().optional().describe("Display icon for the day's entry \u2014 a boxicons class or bare name; normalized server-side"),
|
|
24466
24434
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)")
|
|
24467
24435
|
}, async ({ body, identity, icon, date }) => {
|
|
@@ -24506,36 +24474,28 @@ ${html}` }));
|
|
|
24506
24474
|
await markStep("diary", d);
|
|
24507
24475
|
return txt3({ action: "created", noteId, date: d, location: locationLabel("diary"), ...iconSet ? { icon: iconSet } : {}, ...warnings.length ? { sanitized: warnings } : {} });
|
|
24508
24476
|
});
|
|
24509
|
-
server.tool("remember", `Store something the moment it matters
|
|
24510
|
-
|
|
24511
|
-
Kinds
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
memory: thread (multi-session work; daily session via close)
|
|
24515
|
-
knowledge: user (about the user, beyond biography/goals/preferences)
|
|
24516
|
-
information (a domain sub-category note \u2014 pass domain= and a title)
|
|
24517
|
-
sources (the one maintained Sources note per domain \u2014 pass domain=)
|
|
24518
|
-
|
|
24519
|
-
Singletons and the per-domain Sources note upsert (content is appended). Collection kinds
|
|
24520
|
-
dedup by title, so duplicates are impossible. Body may be text, markdown, or HTML.
|
|
24521
|
-
Pass connect=[{relation, toNoteId}, \u2026] to wire relations in the same call \u2014 a new
|
|
24522
|
-
information/knowledge/thread note left unconnected is an orphan until wired.
|
|
24523
|
-
For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") is rejected.`, {
|
|
24477
|
+
server.tool("remember", `Store something the moment it matters; the server owns placement, labels and dedup.
|
|
24478
|
+
|
|
24479
|
+
Kinds: biography | goals | preferences | responsibilities | protocols (singletons, upsert) \xB7 thread (goal= required to create; shape="collection" for a titled-entry thread; appends need identity=) \xB7 threadEntry (a titled entry in a collection thread: thread= + title) \xB7 user (knowledge about the user) \xB7 information (domain= + title) \xB7 sources (domain=; revision=[\u2026] upserts rows).
|
|
24480
|
+
|
|
24481
|
+
Collection kinds dedup by title \u2014 mustCreate=true refuses instead of adopting an existing note, and check action= on the receipt. connect=[{relation, toNoteId}] wires relations in the same call. diary/session/log/claim/domain have dedicated paths.`, {
|
|
24524
24482
|
kind: enumType(Kinds).describe("What kind of memory this is"),
|
|
24525
24483
|
title: stringType().optional().describe("Title \u2014 collection kinds (thread/knowledge/information sub-category); ignored for singletons & sources"),
|
|
24526
24484
|
body: stringType().optional().describe("Content: plain text, markdown, or HTML"),
|
|
24527
24485
|
goal: stringType().optional().describe("thread creation: the goal statement \u2014 REQUIRED for a new thread (query the user for it); becomes the Context \u2192 Goal section"),
|
|
24528
|
-
|
|
24486
|
+
shape: enumType(["dated", "collection"]).optional().describe('thread creation: "dated" (default \u2014 one [yyyy-mm-dd] child per active day) or "collection" (one titled child per item, e.g. an ideas list)'),
|
|
24487
|
+
thread: stringType().optional().describe(`kind="threadEntry" only: the collection thread's id \u2014 adds a titled child to it`),
|
|
24488
|
+
identity: stringType().optional().describe('Appending to a dated thread: the identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" (required)'),
|
|
24529
24489
|
domain: stringType().optional().describe("knowledge: the domain name for information/sources (auto-created complete with its Sources note)"),
|
|
24530
24490
|
revision: arrayType(objectType({
|
|
24531
24491
|
source: stringType().describe("Must exactly match how the source is introduced in the Sources list \u2014 this is the upsert key"),
|
|
24532
24492
|
marker: stringType().describe('"\u2747\uFE0F" (discovered/credible) or "\u2705" (used)'),
|
|
24533
24493
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
24534
|
-
})).optional().describe("kind=sources
|
|
24494
|
+
})).optional().describe("kind=sources: upsert Revision rows by source name"),
|
|
24535
24495
|
topics: arrayType(stringType()).optional().describe("Topic tags \u2014 slugged server-side"),
|
|
24536
24496
|
supersedes: stringType().optional().describe("noteId this replaces \u2014 old note is archived and wired supersedes"),
|
|
24537
|
-
mandate: booleanType().optional().describe("kind=information
|
|
24538
|
-
mustCreate: booleanType().optional().describe("Refuse instead of adopting
|
|
24497
|
+
mandate: booleanType().optional().describe("kind=information: flag the note as a standing brief a session must follow (#mandate)"),
|
|
24498
|
+
mustCreate: booleanType().optional().describe("Refuse instead of adopting a note that already has this title"),
|
|
24539
24499
|
strict: booleanType().optional().describe("Refuse the write when the body needs structural repair (unclosed tags, <br> runs) instead of accepting the repaired form"),
|
|
24540
24500
|
connect: arrayType(objectType({
|
|
24541
24501
|
relation: enumType(RelationTypes),
|
|
@@ -24543,7 +24503,7 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
24543
24503
|
})).optional().describe("Relations to wire from this note in the same call \u2014 same semantics as connect() (idempotent, worksWith wired both ways)"),
|
|
24544
24504
|
icon: stringType().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name ("brain"); normalized server-side'),
|
|
24545
24505
|
date: stringType().optional().describe("ISO date override (default: today)")
|
|
24546
|
-
}, async ({ kind, title, body, goal, identity, domain, revision, topics, supersedes, mandate, mustCreate, strict, connect: connectRels, icon, date }) => {
|
|
24506
|
+
}, async ({ kind, title, body, goal, shape, thread, identity, domain, revision, topics, supersedes, mandate, mustCreate, strict, connect: connectRels, icon, date }) => {
|
|
24547
24507
|
const refuseAdoption = (existingId, existingTitle, where) => err("already_exists", `A ${kind} note titled "${existingTitle}" already exists ${where} [${existingId}] \u2014 mustCreate=true refuses to adopt it.`, `Read it first with ${kind === "thread" ? "memory" : "knowledge"}(${existingId}). To add to it deliberately, re-run without mustCreate, or use revise(${existingId}, \u2026). To keep both, pick a title that distinguishes them.`);
|
|
24548
24508
|
const opts = { domain, topics, date, ...mandate ? { mandate: true } : {} };
|
|
24549
24509
|
const d = checkedDate(date);
|
|
@@ -24733,8 +24693,26 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
24733
24693
|
return err("rejected_kind", "Log notes are auto-generated by close() and cannot be written manually.");
|
|
24734
24694
|
if (kind === "domain")
|
|
24735
24695
|
return err("rejected_kind", "Domain containers are auto-created on first use.", 'To write domain knowledge call remember(kind="information", domain="<name>", ...).');
|
|
24736
|
-
if (kind === "threadEntry")
|
|
24737
|
-
|
|
24696
|
+
if (kind === "threadEntry") {
|
|
24697
|
+
const book = thread ? await trilium.getNote(thread).catch(() => null) : null;
|
|
24698
|
+
if (!book || ownedLabel(book, "noteType") !== "thread" || !isCollectionThread(book))
|
|
24699
|
+
return err("rejected_kind", "Dated thread entries are created automatically when appending to a thread; titled entries exist only in collection threads.", 'Dated thread: remember(kind="thread", title="<thread>", body, identity). Collection thread: remember(kind="threadEntry", thread="<collection thread id>", title, body).');
|
|
24700
|
+
const { title: entryTitle } = normalizeTitle(title ?? "");
|
|
24701
|
+
if (!entryTitle)
|
|
24702
|
+
return err("missing_param", "A collection entry needs a title.", 'Add title="<entry name>".');
|
|
24703
|
+
const siblings = await trilium.searchNotes("#noteType=threadEntry", { ancestorNoteId: book.noteId, fastSearch: true, limit: 500 }).then((r) => r.results).catch(() => []);
|
|
24704
|
+
const clash = siblings.find((s) => s.parentNoteIds.includes(book.noteId) && sameTitle(s.title, entryTitle));
|
|
24705
|
+
if (clash)
|
|
24706
|
+
return err("already_exists", `"${book.title}" already has an entry titled "${clash.title}" [${clash.noteId}].`, `Edit it in place with revise("${clash.noteId}", section=/find=/mode=\u2026) \u2014 collection entries are maintained documents, not appended records.`);
|
|
24707
|
+
const created = await trilium.createNote(book.noteId, entryTitle, contentFor("threadEntry", { date: d, body: html }));
|
|
24708
|
+
const eid = created.note.noteId;
|
|
24709
|
+
await trilium.addLabel(eid, "noteType", "threadEntry");
|
|
24710
|
+
await trilium.addLabel(eid, "created", d);
|
|
24711
|
+
await trilium.updateLabelValue(book.noteId, "updated", d);
|
|
24712
|
+
const connected = await wireRequested(eid);
|
|
24713
|
+
const iconSet = await applyIcon(eid, icon);
|
|
24714
|
+
return txt3({ action: "created", noteId: eid, kind, title: entryTitle, thread: book.title, ...connected.length ? { connected } : {}, ...iconSet ? { icon: iconSet } : {}, ...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {} });
|
|
24715
|
+
}
|
|
24738
24716
|
const { title: cleanTitle } = normalizeTitle(title ?? "");
|
|
24739
24717
|
if (!cleanTitle)
|
|
24740
24718
|
return err("missing_param", `kind="${kind}" requires a title.`, 'Add title="<note title>" to your call.');
|
|
@@ -24748,6 +24726,8 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
24748
24726
|
const block = identity && !leadingIdentification(html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
24749
24727
|
${html}` : html;
|
|
24750
24728
|
if (kind === "thread") {
|
|
24729
|
+
if (isCollectionThread(existing))
|
|
24730
|
+
return collectionAppendRefusal(existing);
|
|
24751
24731
|
const entry = await appendThreadEntry(existing.noteId, block, d);
|
|
24752
24732
|
if (entry.action === "already_written") {
|
|
24753
24733
|
return txt3({ action: "already_written", noteId: existing.noteId, entryId: entry.noteId, kind, title: existing.title });
|
|
@@ -24794,6 +24774,8 @@ ${block}`), d);
|
|
|
24794
24774
|
for (const l of labelPlan(kind, opts, d)) {
|
|
24795
24775
|
await trilium.addLabel(nid, l.name, l.value, l.inheritable ?? false);
|
|
24796
24776
|
}
|
|
24777
|
+
if (kind === "thread" && shape === "collection")
|
|
24778
|
+
await trilium.addLabel(nid, "threadShape", "collection");
|
|
24797
24779
|
const wired = [];
|
|
24798
24780
|
const extraRelations = [];
|
|
24799
24781
|
if (supersedes) {
|
|
@@ -24824,23 +24806,7 @@ ${block}`), d);
|
|
|
24824
24806
|
...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {}
|
|
24825
24807
|
});
|
|
24826
24808
|
});
|
|
24827
|
-
server.tool("recall", `
|
|
24828
|
-
anything previously discussed. Runs label, title and full-text strategies server-side and
|
|
24829
|
-
returns merged, ranked results with kind/status. Archived notes are excluded unless
|
|
24830
|
-
includeArchived=true.
|
|
24831
|
-
|
|
24832
|
-
orderBy / orderDirection override the score-based sort when you need temporal ordering
|
|
24833
|
-
("what changed most recently", "oldest active thread"). fastSearch restricts to title and
|
|
24834
|
-
label scans only \u2014 much faster on large brains when you know the query is a title or topic.
|
|
24835
|
-
|
|
24836
|
-
When the exact strategies find little, a FUZZY pass runs automatically (Trilium's ~= / ~*
|
|
24837
|
-
operators: \u22653 characters, edit distance \u22642, diacritics normalised), so a misremembered or
|
|
24838
|
-
mistyped term still lands. Fuzzy hits are scored below exact ones and marked \`fuzzy: true\` \u2014
|
|
24839
|
-
a near-match is a lead, not an answer, and you should be able to tell which you got.
|
|
24840
|
-
|
|
24841
|
-
regex="<pattern>" searches note bodies with a real regular expression instead of keywords \u2014
|
|
24842
|
-
the right tool for structural questions text search cannot express ("which notes still contain
|
|
24843
|
-
a doubly-escaped tag", "which cite a 2026 date"). Backslashes must be escaped.`, {
|
|
24809
|
+
server.tool("recall", `Ranked search across the whole brain \u2014 label, title and full-text strategies merged, with kind and status. Pass domain= whenever you know the area. orderBy/orderDirection sort by date; fastSearch= scans titles and labels only; regex= matches note bodies with a real regular expression; a fuzzy pass runs automatically when exact results are thin (hits marked fuzzy: true are leads, not answers). includeArchived= includes archived notes. A thin result is evidence about the query, not the brain.`, {
|
|
24844
24810
|
query: stringType().describe("What to find \u2014 natural phrasing is fine"),
|
|
24845
24811
|
kinds: arrayType(enumType(Kinds)).optional().describe("Restrict to these kinds"),
|
|
24846
24812
|
domain: stringType().optional().describe("Restrict to a knowledge domain"),
|
|
@@ -24999,11 +24965,7 @@ a doubly-escaped tag", "which cite a 2026 date"). Backslashes must be escaped.`,
|
|
|
24999
24965
|
...results.length === 0 ? noMatch : {}
|
|
25000
24966
|
});
|
|
25001
24967
|
});
|
|
25002
|
-
server.tool("domain", `
|
|
25003
|
-
Looks up the Knowledge domain folder (if one exists), then gathers all content across
|
|
25004
|
-
every area that carries a matching #domain or #topic slug \u2014 information, sources, threads,
|
|
25005
|
-
knowledge notes \u2014 grouped by kind. knowledgeDomain is null when no formal domain exists yet.
|
|
25006
|
-
Use recall() for keyword or full-text search instead.`, {
|
|
24968
|
+
server.tool("domain", `Everything for a domain, topic or project: its knowledge book (if any) plus every note carrying a matching #domain or #topic slug, grouped by kind, each with idle days and a stale flag. The reliable retrieval path for an area; use recall() for keyword search.`, {
|
|
25007
24969
|
name: stringType().describe("Domain, topic, or project name"),
|
|
25008
24970
|
includeArchived: booleanType().optional().describe("Include archived/resolved items (default: false)")
|
|
25009
24971
|
}, async ({ name, includeArchived }) => {
|
|
@@ -25067,16 +25029,7 @@ Use recall() for keyword or full-text search instead.`, {
|
|
|
25067
25029
|
...total === 0 && !knowledgeDomain ? { note: `No content found for "${slug}". Create a Knowledge domain with remember(kind="information", domain="${name}") or tag notes with topics=["${slug}"].` } : {}
|
|
25068
25030
|
});
|
|
25069
25031
|
});
|
|
25070
|
-
server.tool("read", `
|
|
25071
|
-
{count, notes:[{id, title, kind, content, relations?}]}. The orientation step is structurally a
|
|
25072
|
-
fan-out \u2014 nineteen notes, several very large \u2014 and N separate surface calls is N chances to
|
|
25073
|
-
time out mid-read, which is the one thing that wastes budget rather than spending it. This tool
|
|
25074
|
-
collapses the fan-out into one call.
|
|
25075
|
-
|
|
25076
|
-
Pure read: returns bodies verbatim, no placeholders, no dedup, no kind restrictions. Note the
|
|
25077
|
-
cap: up to 10 ids per call. For one id prefer the kinded read (knowledge()/memory()/llm()) which
|
|
25078
|
-
returns the same shape through the same helper; for a huge note the sectioned read is still the
|
|
25079
|
-
efficient path \u2014 read() returns whole bodies.`, {
|
|
25032
|
+
server.tool("read", `Several note bodies in one round trip: ids=[\u2026] (up to 10) \u2192 {notes:[{id, title, kind, content, relations}]}. For one note prefer the kinded read; for a huge note prefer a section= read.`, {
|
|
25080
25033
|
ids: arrayType(stringType()).min(1).max(10).describe("Note ids to read \u2014 one body per id, one round trip (cap 10)")
|
|
25081
25034
|
}, async ({ ids }) => {
|
|
25082
25035
|
const notes = await Promise.all(ids.map(async (id) => {
|
|
@@ -25098,42 +25051,18 @@ efficient path \u2014 read() returns whole bodies.`, {
|
|
|
25098
25051
|
}));
|
|
25099
25052
|
return txt3({ count: notes.length, notes });
|
|
25100
25053
|
});
|
|
25101
|
-
server.tool("revise", `
|
|
25102
|
-
|
|
25103
|
-
(section=
|
|
25104
|
-
|
|
25105
|
-
|
|
25106
|
-
bumped server-side on every content write. A revision snapshot is always taken first.
|
|
25107
|
-
|
|
25108
|
-
section= targets h2/h3/h4 in that order, tolerant of attributes/whitespace/case on the
|
|
25109
|
-
heading. The return includes matched (false = no heading matched, so the content was appended
|
|
25110
|
-
as a NEW section) and headingCount (>1 means several headings share that text) \u2014 check them
|
|
25111
|
-
rather than assuming the target was hit. On a miss the new section is written at the note's own
|
|
25112
|
-
section level (a note built from h3 sections gets another h3, not a stray h2) and the note's
|
|
25113
|
-
existing headings come back in available= so you can correct the name without a read. When
|
|
25114
|
-
several headings share a text, occurrence= (1-based) picks which one \u2014 without it the first
|
|
25115
|
-
wins, which leaves a legitimately repeated heading reachable only by rewriting the section.
|
|
25116
|
-
Use outline(noteId) to see the heading tree, levels and occurrence indices up front.
|
|
25117
|
-
|
|
25118
|
-
find= replaces every occurrence of the exact raw string with body= (no markdown conversion, no
|
|
25119
|
-
full read needed) and returns replaced + matchMode; when the exact string misses, an
|
|
25120
|
-
attribute-tolerant pass retries with stored-HTML attribute injection and inter-element
|
|
25121
|
-
whitespace ignored. nth= (1-based) replaces only one occurrence. edits=[{find, body}, \u2026]
|
|
25122
|
-
applies several surgeries in one call, in order, against one read and one write.
|
|
25123
|
-
|
|
25124
|
-
Granularity warning: section + mode=replace swaps the ENTIRE section body \u2014 everything under
|
|
25125
|
-
that heading, not one paragraph within it. To change a single paragraph inside a section, use
|
|
25126
|
-
find=; to add a sibling block around a whole section without touching its body, use
|
|
25127
|
-
mode="before"/"after". Both are section-relative: "after" lands past the end of that section's
|
|
25128
|
-
body, so inserting a heading with it creates a NEW section rather than nesting one under the
|
|
25129
|
-
target \u2014 which is what it used to do, emptying the target while reporting matched: true.`, {
|
|
25054
|
+
server.tool("revise", `Edit a note by id. A revision is taken first; "Last updated" lines are bumped on content writes.
|
|
25055
|
+
|
|
25056
|
+
Modes: default append (a dated addendum \u2014 right only for records; a dated thread's append lands in today's child), mode=replace (whole body), section="<heading>" (replace that section's whole body, h2\u2192h3\u2192h4, occurrence= for repeats), section= + mode=before|after (insert a sibling block around the whole section), mode=prepend (top of the section body), mode=remove (delete the section). find="<exact stored text>" replaces every occurrence (nth= for one); edits=[{find, body}] applies several in one write. title= composes with every mode; retitling a domain book cascades its #domain slug.
|
|
25057
|
+
|
|
25058
|
+
Check the receipt: matched=false means a NEW section was written (available= lists real headings; strict=true refuses instead). A section replace swaps everything under the heading \u2014 use find= for anything smaller. find= matches stored HTML (tags literal), not rendered text.`, {
|
|
25130
25059
|
noteId: stringType().describe("Note to update"),
|
|
25131
25060
|
body: stringType().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
|
|
25132
25061
|
title: stringType().optional().describe("New title (normalized server-side)"),
|
|
25133
25062
|
section: stringType().optional().describe("Target a section by heading text (h2/h3/h4, in that order); omit for whole-note append/replace"),
|
|
25134
25063
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading to target, 1-based (default: the first). Read them with outline(noteId)."),
|
|
25135
|
-
strict: booleanType().optional().describe("
|
|
25136
|
-
mode: enumType(["append", "replace", "before", "after", "prepend", "remove"]).optional().describe(
|
|
25064
|
+
strict: booleanType().optional().describe("Refuse (note untouched) on a section= miss or a body needing structural repair, instead of writing a new section"),
|
|
25065
|
+
mode: enumType(["append", "replace", "before", "after", "prepend", "remove"]).optional().describe("append (default) | replace | before | after (around the whole section=) | prepend (top of its body) | remove (delete section=, no body needed)"),
|
|
25137
25066
|
find: stringType().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."),
|
|
25138
25067
|
nth: numberType().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
|
|
25139
25068
|
edits: arrayType(objectType({
|
|
@@ -25141,7 +25070,7 @@ target \u2014 which is what it used to do, emptying the target while reporting m
|
|
|
25141
25070
|
body: stringType().describe("Raw replacement string"),
|
|
25142
25071
|
nth: numberType().int().positive().optional().describe("Replace only the Nth occurrence")
|
|
25143
25072
|
})).optional().describe("Several find/replace surgeries applied in order against one read and one write. Mutually exclusive with find=."),
|
|
25144
|
-
identity: stringType().optional().describe(
|
|
25073
|
+
identity: stringType().optional().describe('Appending to a dated thread: the identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" (required)'),
|
|
25145
25074
|
icon: stringType().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name; normalized server-side'),
|
|
25146
25075
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
25147
25076
|
}, async ({ noteId, body, title, section, occurrence, mode, find, nth, edits, identity, icon, date, strict }) => {
|
|
@@ -25382,6 +25311,8 @@ target \u2014 which is what it used to do, emptying the target while reporting m
|
|
|
25382
25311
|
finalContent = bumpLastUpdated(html, d).html;
|
|
25383
25312
|
await trilium.updateNoteContent(noteId, finalContent);
|
|
25384
25313
|
} else if (noteKind === "thread") {
|
|
25314
|
+
if (isCollectionThread(note))
|
|
25315
|
+
return collectionAppendRefusal(note);
|
|
25385
25316
|
if (!identity && !leadingIdentification(html)) {
|
|
25386
25317
|
return err("missing_identity", 'Thread addendums open with the canonical identification line (h3): "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]".', `Pass identity="Claude \u2026 \xB7 <environment> \xB7 <agent/mode>" \u2014 the server renders it as the addendum's h3.`);
|
|
25387
25318
|
}
|
|
@@ -25432,9 +25363,7 @@ ${block}`), d).html;
|
|
|
25432
25363
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25433
25364
|
});
|
|
25434
25365
|
});
|
|
25435
|
-
server.tool("resolve", `
|
|
25436
|
-
status, and archive it in place (it stays where it is, excluded from default recall).
|
|
25437
|
-
"done" is not an outcome.`, {
|
|
25366
|
+
server.tool("resolve", `Close a thread (or any resolvable note) with a substantive outcome: writes the Resolution, sets the terminal status and archives it in place. "done" is not an outcome.`, {
|
|
25438
25367
|
noteId: stringType().describe("The thread / note to complete"),
|
|
25439
25368
|
outcome: stringType().describe("The resolution \u2014 substantive, standalone prose"),
|
|
25440
25369
|
status: enumType(["resolved", "superseded"]).optional().describe("Terminal status (default: resolved)"),
|
|
@@ -25470,23 +25399,7 @@ status, and archive it in place (it stays where it is, excluded from default rec
|
|
|
25470
25399
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25471
25400
|
});
|
|
25472
25401
|
});
|
|
25473
|
-
server.tool("split", `
|
|
25474
|
-
new note, leaving a pointer back. The trim affordance \u2014 the write half of the oversized-note
|
|
25475
|
-
problem, which maintain() detects but nothing acts on. A note past the read ceiling with twenty
|
|
25476
|
-
sections becomes two readable notes instead of one unusable one, and the source stays navigable
|
|
25477
|
-
because a pointer marks where the content went.
|
|
25478
|
-
|
|
25479
|
-
Pass sections=[heading1, heading2, ...] (heading TEXT, the same contract section= uses) and
|
|
25480
|
-
into="<new title>". Each named section is lifted out whole \u2014 nested sub-sections go with their
|
|
25481
|
-
parent. Repeated headings are consumed first-match-per-request: passing the same heading twice
|
|
25482
|
-
takes the next occurrence each time. The new note is created under the same parent, typed the
|
|
25483
|
-
same as the source, and carries the source's #domain/#topic labels; the source is left with a
|
|
25484
|
-
"Split into <title>" pointer and a ~references relation to the new note. Sections that do not
|
|
25485
|
-
exist are reported in missed= and left in place.
|
|
25486
|
-
|
|
25487
|
-
Returns the new note id, the sections moved, and the source's remaining size. A revision is
|
|
25488
|
-
taken of the source before it is written. Refused on containers and singletons \u2014 this is for
|
|
25489
|
-
content notes.`, {
|
|
25402
|
+
server.tool("split", `Move whole sections (heading + body, nested sub-sections included) out of a note into a new note under the same parent, typed and labelled like the source; the source keeps a pointer and a ~references edge. Use it when a note is past the read ceiling or holds two subjects. sections=[heading texts], into="<new title>". Missing sections come back in missed=. A revision is taken first; refused on containers and singletons.`, {
|
|
25490
25403
|
noteId: stringType().describe("Note to split \u2014 the source whose sections are moving out"),
|
|
25491
25404
|
sections: arrayType(stringType()).min(1).max(20).describe("Section headings (text) to move out \u2014 nested sub-sections go with their parent"),
|
|
25492
25405
|
into: stringType().describe("Title of the new note that receives the moved sections"),
|
|
@@ -25544,9 +25457,7 @@ ${result.html}`);
|
|
|
25544
25457
|
note: `Moved ${result.matched.length} section(s) into "${cleanTitle}" [${nid}]; the source now points at it. If this was the wrong seam, the source has a revision from before the split.`
|
|
25545
25458
|
});
|
|
25546
25459
|
});
|
|
25547
|
-
server.tool("withdraw", `
|
|
25548
|
-
resets status to active, clears the closed date, and appends a dated "Withdrawn" addendum.
|
|
25549
|
-
Use when a resolved or dormant thread resurfaces as live work.`, {
|
|
25460
|
+
server.tool("withdraw", `Return an archived or resolved thread to active and note the withdrawal on the book. Use when closed work resurfaces.`, {
|
|
25550
25461
|
noteId: stringType().describe("The archived/resolved thread to withdraw"),
|
|
25551
25462
|
reason: stringType().optional().describe("Why it was withdrawn \u2014 written as an addendum"),
|
|
25552
25463
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
@@ -25583,13 +25494,7 @@ Use when a resolved or dormant thread resurfaces as live work.`, {
|
|
|
25583
25494
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25584
25495
|
});
|
|
25585
25496
|
});
|
|
25586
|
-
server.tool("label", `Set or remove
|
|
25587
|
-
label surgery (fixing a stray value, correcting drift) so a real edge case doesn't need the
|
|
25588
|
-
raw full-mode attribute tools. Refused on containers (same rule as revise()); noteType can
|
|
25589
|
-
never be touched here \u2014 it defines a note's kind and is owned by remember()/bootstrap().
|
|
25590
|
-
status is validated against the closed vocabulary (${Statuses.join(" | ")}); domain and topic
|
|
25591
|
-
are slugged automatically, matching remember()'s routing. Bumps updated to today unless you're
|
|
25592
|
-
setting updated itself.`, {
|
|
25497
|
+
server.tool("label", `Set or remove one label (remove=true). Refused on containers. noteType cannot be changed or removed, but can be set on an untyped note to repair it. status must be one of ${Statuses.join(" | ")}; domain and topic are slugged. Bumps updated unless you are setting it.`, {
|
|
25593
25498
|
noteId: stringType().describe("Note to edit"),
|
|
25594
25499
|
name: stringType().describe("Label name, no # prefix (e.g. status, domain, topic, created)"),
|
|
25595
25500
|
value: stringType().optional().describe("New value \u2014 required unless remove=true"),
|
|
@@ -25597,6 +25502,11 @@ setting updated itself.`, {
|
|
|
25597
25502
|
}, async ({ noteId, name, value, remove }) => {
|
|
25598
25503
|
if (isContainer(b(), noteId))
|
|
25599
25504
|
return err("protected_note", `Note ${noteId} is a container \u2014 its labels cannot be edited directly.`);
|
|
25505
|
+
if (name === "iconClass" && remove) {
|
|
25506
|
+
const n = await trilium.getNote(noteId).catch(() => null);
|
|
25507
|
+
if (n && !ICON_EXEMPT.has(ownedLabel(n, "noteType") ?? ""))
|
|
25508
|
+
return err("protected_label", "Every note except a log carries an icon.", 'Change it instead: label(noteId, "iconClass", value="bx bx-<name>").');
|
|
25509
|
+
}
|
|
25600
25510
|
const noteForGuard = name === "noteType" ? await trilium.getNote(noteId).catch(() => null) : null;
|
|
25601
25511
|
if (name === "noteType") {
|
|
25602
25512
|
if (!noteForGuard)
|
|
@@ -25716,38 +25626,15 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
25716
25626
|
}
|
|
25717
25627
|
}
|
|
25718
25628
|
});
|
|
25719
|
-
server.tool("consistency", `
|
|
25720
|
-
report whether they agree.
|
|
25721
|
-
|
|
25722
|
-
The brain's hardest failure is not a missing fact \u2014 it is the SAME fact recorded differently in
|
|
25723
|
-
several notes, where every copy reads as authoritative. Nothing else surfaces that: recall() ranks
|
|
25724
|
-
by relevance, maintain() checks structure, and a correction applied to one note leaves its siblings
|
|
25725
|
-
silently wrong. This is the check that answers "is what I just corrected still contradicted
|
|
25726
|
-
somewhere else".
|
|
25727
|
-
|
|
25728
|
-
Pass a regex with ONE capture group naming the value that should agree:
|
|
25729
|
-
consistency("(\\\\d+) Titan mailboxes") \u2192 do all notes agree on the count
|
|
25730
|
-
consistency("founded (?:in )?(\\\\w+ \\\\d{4})") \u2192 do all notes agree on the date
|
|
25731
|
-
consistency("BRAINLLM_MODE[=: ]+(\\\\w+)") \u2192 do all notes agree on the mode
|
|
25732
|
-
|
|
25733
|
-
Without a capture group it degrades to a presence check \u2014 which notes mention this at all.
|
|
25734
|
-
|
|
25735
|
-
Matched against BOTH the stored HTML and a tag-stripped projection of it, so a phrase split by an
|
|
25736
|
-
inline <strong> or <code> tag is found, and so is a pattern deliberately anchored on tags.
|
|
25737
|
-
Escape backslashes. Scope with domain= or kinds= when the phrase is common.
|
|
25738
|
-
|
|
25739
|
-
Scans every in-scope note by default. Trilium's %= backend filter is a LOSSY pre-filter \u2014 it
|
|
25740
|
-
reads a striptags'd copy of the content, so it drops notes this tool should examine \u2014 and on a
|
|
25741
|
-
contradiction sweep a falsely clean result is worse than a slow one. Pass fast=true to use it
|
|
25742
|
-
anyway when scope is wide and speed matters more than completeness.`, {
|
|
25629
|
+
server.tool("consistency", `Does the brain agree with itself? pattern= is a regex with ONE capture group naming the value that should match across notes, e.g. "(\\\\d+) mailboxes"; the result groups every asserting note by value with agreement unanimous or DISAGREEMENT. subject="<fact in prose>" finds notes asserting about a subject however phrased. staleAfterDays=N also reports values held in exactly one note untouched N+ days. Matches stored HTML and tag-stripped text; escape backslashes; scope with domain=/kinds=. Scans every in-scope note (fast=true uses Trilium's lossy pre-filter). Run it after correcting any fact recorded in more than one place.`, {
|
|
25743
25630
|
pattern: stringType().optional().describe("Regex over note bodies. One capture group = the value that should agree across notes. Omit when using subject= (prose mode)."),
|
|
25744
|
-
subject: stringType().optional().describe("
|
|
25745
|
-
staleAfterDays: numberType().optional().describe("With pattern:
|
|
25631
|
+
subject: stringType().optional().describe("A fact in prose \u2014 returns notes asserting about it however phrased (instead of pattern=)"),
|
|
25632
|
+
staleAfterDays: numberType().optional().describe("With pattern: also report values held in exactly one note untouched N+ days (staleSingles)"),
|
|
25746
25633
|
domain: stringType().optional().describe("Restrict to one knowledge domain"),
|
|
25747
25634
|
kinds: arrayType(enumType(Kinds)).optional().describe("Restrict to these kinds"),
|
|
25748
25635
|
includeArchived: booleanType().optional().describe("Include archived notes (default false)"),
|
|
25749
25636
|
limit: numberType().optional().describe("Max notes to examine (default 60)"),
|
|
25750
|
-
fast: booleanType().optional().describe("
|
|
25637
|
+
fast: booleanType().optional().describe("Use Trilium's faster but lossy %= pre-filter (default: scan every in-scope note)")
|
|
25751
25638
|
}, async ({ pattern, subject, staleAfterDays, domain, kinds, includeArchived, limit, fast }) => {
|
|
25752
25639
|
if (!pattern && !subject)
|
|
25753
25640
|
return err("missing_param", "consistency() needs either a regex pattern or a prose subject.", 'Pass pattern="(\\\\d+) users" to compare a captured value, or subject="<a fact in prose>" to find every note asserting about that subject however phrased.');
|
|
@@ -25881,22 +25768,7 @@ anyway when scope is wide and speed matters more than completeness.`, {
|
|
|
25881
25768
|
note: agrees ? groups.length === 0 ? "No note asserted a value for that pattern. That is evidence about the pattern, not about the brain \u2014 check it against a note you know contains the fact." : `All ${groups[0].count} note(s) agree on "${groups[0].value}".` : `${groups.length} DIFFERENT values are asserted across ${scoped.length} notes. Establish which is true from evidence, correct every note that disagrees, and wire ~corrects from the note that overturns the old claim \u2014 revising in place leaves no trace the wrong value was ever believed.`
|
|
25882
25769
|
});
|
|
25883
25770
|
});
|
|
25884
|
-
server.tool("outline", `
|
|
25885
|
-
same-text siblings, and a structural check \u2014 without reading the body.
|
|
25886
|
-
|
|
25887
|
-
Read this BEFORE a section= revise on a note you haven't just written. section= needs a heading
|
|
25888
|
-
string that matches, and guessing it wrong writes a new section rather than editing the one you
|
|
25889
|
-
meant; picking the level and the exact text from a list removes that failure instead of
|
|
25890
|
-
reporting it afterwards. occurrence is what you pass to revise(section=, occurrence=) to reach
|
|
25891
|
-
a specific one when several share a text.
|
|
25892
|
-
|
|
25893
|
-
Also returns the note's size, the key column of any table it holds, and any structural drift
|
|
25894
|
-
already present (duplicate headings, unbalanced tags) \u2014 the cheap "is this note still sound"
|
|
25895
|
-
check after a run of surgical edits, and the way to see a Sources note's Revision keys (which
|
|
25896
|
-
remember(revision=) matches on exactly) without reading the note.
|
|
25897
|
-
|
|
25898
|
-
On notes 15k+ the headings also carry a first-block preview of their section \u2014 the orientation
|
|
25899
|
-
read for oversized notes, where outline() alone assumes you already know which section you want.`, {
|
|
25771
|
+
server.tool("outline", `A note's heading tree without its body: each h2\u2013h4 with level and occurrence index (and raw stored text where inline markup differs), table key columns, size, and structural findings. Read it before a section= edit you're not sure of. Notes over 15k also get a first-block preview per section.`, {
|
|
25900
25772
|
noteId: stringType().describe("Note to outline")
|
|
25901
25773
|
}, async ({ noteId }) => {
|
|
25902
25774
|
const note = await trilium.getNote(noteId);
|
|
@@ -25943,22 +25815,7 @@ read for oversized notes, where outline() alone assumes you already know which s
|
|
|
25943
25815
|
...headings.length === 0 ? { note: "No h2\u2013h4 headings \u2014 section= would append a new section rather than match anything." } : {}
|
|
25944
25816
|
});
|
|
25945
25817
|
});
|
|
25946
|
-
server.tool("inspect", `
|
|
25947
|
-
noteType/status), every outbound relation, its attachments (id/title/mime/role/size), plus
|
|
25948
|
-
type/mime/parent/child ids and dates. Pass content=true to also get the raw note body (the
|
|
25949
|
-
core path for a raw content read \u2014 no full mode needed). The deep-dive counterpart to the
|
|
25950
|
-
surface reads and explore(): reach for it when you need the raw label set, the body verbatim,
|
|
25951
|
-
or the attachment inventory \u2014 confirming a fix landed, debugging drift \u2014 rather than a
|
|
25952
|
-
kind-specific summary. Read-only, safe on any note including structural containers.
|
|
25953
|
-
|
|
25954
|
-
Pass section="<heading>" alongside content=true to get one section's raw body instead of the
|
|
25955
|
-
whole thing \u2014 the same heading contract revise(section=) writes through.
|
|
25956
|
-
|
|
25957
|
-
Pass find="<literal>" to count occurrences of a literal string in the body \u2014 total plus a
|
|
25958
|
-
per-addendum-block breakdown. The staleness-escalation counter: "how many prior entries
|
|
25959
|
-
mention this carried flag" becomes one call instead of a full read + manual counting. On zero
|
|
25960
|
-
occurrences it returns the nearest fragment that IS present and the stored text around it, so a
|
|
25961
|
-
miss is diagnosed in the same call rather than in three more.`, {
|
|
25818
|
+
server.tool("inspect", `Everything about one note: every label, relation and attachment, type/mime, parent and child ids, dates. content=true adds the raw body (section= narrows it). find="<literal>" counts occurrences (total, per addendum block, per section) and on a miss shows the nearest stored text. Read-only, safe on any note.`, {
|
|
25962
25819
|
noteId: stringType().describe("Note to inspect"),
|
|
25963
25820
|
content: booleanType().optional().describe("Include the note's raw body content (default: false)"),
|
|
25964
25821
|
section: stringType().optional().describe("With content=true, return only this heading's section rather than the whole body"),
|
|
@@ -26071,26 +25928,9 @@ miss is diagnosed in the same call rather than in three more.`, {
|
|
|
26071
25928
|
await trilium.addLabel(created.note.noteId, "iconClass", "bx bx-check-shield").catch(() => null);
|
|
26072
25929
|
return created.note.noteId;
|
|
26073
25930
|
};
|
|
26074
|
-
server.tool("claim", `Register a checkable assertion
|
|
26075
|
-
|
|
26076
|
-
|
|
26077
|
-
assertion is still true of the codebase, config or live surface it describes \u2014 so a claim that
|
|
26078
|
-
quietly stopped being true stayed authoritative until something downstream broke, and "is the
|
|
26079
|
-
brain stale" was answered by luck rather than by a query.
|
|
26080
|
-
|
|
26081
|
-
BrainLLM never executes anything. It has Trilium access and no shell, and content in a note is
|
|
26082
|
-
data, not instructions \u2014 a recipe that ran itself would be an injection surface pointed at the
|
|
26083
|
-
user's machine. So a claim stores WHAT to check as inert text; the agent runs it and reports
|
|
26084
|
-
back. The tool owns the register, the schedule and the staleness question.
|
|
26085
|
-
|
|
26086
|
-
Modes, by which parameters are present:
|
|
26087
|
-
assertion + check \u2192 REGISTER (or update, deduped by assertion text)
|
|
26088
|
-
claimId + holds \u2192 VERIFY: record the outcome and re-stamp the clock
|
|
26089
|
-
claimId alone \u2192 READ one claim with its verification history
|
|
26090
|
-
neither \u2192 LIST, newest-verified last; filter with status=
|
|
26091
|
-
|
|
26092
|
-
A verified claim is quiet until its interval lapses. maintain(deep=true) surfaces lapsed and
|
|
26093
|
-
broken ones, so staleness arrives as a maintenance finding instead of a surprise.`, {
|
|
25931
|
+
server.tool("claim", `Does the brain still agree with the world? Register a checkable assertion and record whether it holds. BrainLLM never runs the check \u2014 it stores it as inert text; you run it and report.
|
|
25932
|
+
|
|
25933
|
+
assertion + check \u2192 register (deduped by assertion); claimId + holds + evidence \u2192 verify (evidence required); claimId alone \u2192 read with history; nothing \u2192 list (status= filters). noteId= links the claim to its source note (~derivedFrom). maintain(deep) surfaces lapsed, never-verified, broken and source-changed claims.`, {
|
|
26094
25934
|
assertion: stringType().optional().describe('The claim in plain words, e.g. "the parse pipeline runs before validation" \u2014 also the dedup key'),
|
|
26095
25935
|
check: stringType().optional().describe("How to verify it, as INERT text a human or agent runs: a command, a query, a file path, a URL. Never executed by BrainLLM"),
|
|
26096
25936
|
noteId: stringType().optional().describe("The note this claim is made in \u2014 wired ~derivedFrom so the claim and its source stay linked"),
|
|
@@ -26202,20 +26042,7 @@ broken ones, so staleness arrives as a maintenance finding instead of a surprise
|
|
|
26202
26042
|
...all.length === 0 ? { note: 'No claims registered. Register one with claim(assertion="\u2026", check="\u2026") \u2014 the assertions worth registering are the ones that would be expensive to discover had gone stale.' } : {}
|
|
26203
26043
|
});
|
|
26204
26044
|
});
|
|
26205
|
-
server.tool("diff", `What changed
|
|
26206
|
-
|
|
26207
|
-
Every content write takes a revision first, and until now nothing could read one back: verifying
|
|
26208
|
-
a run of surgical edits on a large note meant re-reading the whole thing, or trusting the
|
|
26209
|
-
receipts. Trusting receipts is exactly how a section= replace that silently displaced four
|
|
26210
|
-
subsections went unnoticed.
|
|
26211
|
-
|
|
26212
|
-
Called with only a noteId, this diffs the MOST RECENT revision against current content \u2014 "what
|
|
26213
|
-
did my last write actually do". Pass revisionId to compare against a specific earlier one; the
|
|
26214
|
-
revisions list comes back on every call, newest first, so the usual flow is one call to see what
|
|
26215
|
-
exists and a second to pick.
|
|
26216
|
-
|
|
26217
|
-
Reports changed lines with a little context, plus added/removed counts. Both sides are stored
|
|
26218
|
-
HTML, so a formatting-only change is a real difference and shows as one.`, {
|
|
26045
|
+
server.tool("diff", `What a write actually changed: a revision snapshot (default the latest) against the note as it stands, as changed lines with context and added/removed counts. The revision list comes back on every call; pass revisionId to compare against an earlier one.`, {
|
|
26219
26046
|
noteId: stringType().describe("Note to diff"),
|
|
26220
26047
|
revisionId: stringType().optional().describe("Compare against this revision (default: the most recent one)"),
|
|
26221
26048
|
context: numberType().int().min(0).max(10).optional().describe("Unchanged lines to show around each change (default: 1)")
|
|
@@ -26275,15 +26102,7 @@ HTML, so a formatting-only change is a real difference and shows as one.`, {
|
|
|
26275
26102
|
revisions: index
|
|
26276
26103
|
});
|
|
26277
26104
|
});
|
|
26278
|
-
server.tool("attach", `
|
|
26279
|
-
Dual-mode by the content param:
|
|
26280
|
-
content provided \u2192 UPSERT by title: creates the attachment, or replaces the existing
|
|
26281
|
-
same-titled attachment's content (and mime) in place. Retry-safe \u2014
|
|
26282
|
-
re-running the same call converges on the same state.
|
|
26283
|
-
content omitted \u2192 READ: returns the named attachment's metadata and content.
|
|
26284
|
-
Attachments ride on the note \u2014 the native home for raw artifacts that belong with a typed
|
|
26285
|
-
memory rather than in its body. Binary content is standard base64 and is uploaded as raw bytes; reads return the same envelope. List a note's attachments with
|
|
26286
|
-
inspect(noteId); remove with detach().`, {
|
|
26105
|
+
server.tool("attach", `Upsert a raw artifact (file, image, blob) on a note by title (content given; mime=, encoding="base64" for binary) or read one back (content omitted). List with inspect(), remove with detach().`, {
|
|
26287
26106
|
noteId: stringType().describe("Owning note"),
|
|
26288
26107
|
title: stringType().describe("Attachment title \u2014 the upsert/read key on this note"),
|
|
26289
26108
|
content: stringType().optional().describe("Content to write (text, or base64 for binary). Omit to read the attachment instead."),
|
|
@@ -26329,9 +26148,7 @@ inspect(noteId); remove with detach().`, {
|
|
|
26329
26148
|
throw error;
|
|
26330
26149
|
}
|
|
26331
26150
|
});
|
|
26332
|
-
server.tool("detach", `Remove an attachment
|
|
26333
|
-
Permanent: attachments have no archive tier; re-attach() from source to undo. Retry-safe \u2014
|
|
26334
|
-
an already-removed target returns cleanly instead of erroring.`, {
|
|
26151
|
+
server.tool("detach", `Remove an attachment by attachmentId, or by noteId + title. Permanent; retry-safe.`, {
|
|
26335
26152
|
attachmentId: stringType().optional().describe("The attachment to remove"),
|
|
26336
26153
|
noteId: stringType().optional().describe("Owning note \u2014 used with title when the id isn't at hand"),
|
|
26337
26154
|
title: stringType().optional().describe("Attachment title on noteId")
|
|
@@ -26355,16 +26172,7 @@ an already-removed target returns cleanly instead of erroring.`, {
|
|
|
26355
26172
|
}
|
|
26356
26173
|
return txt3({ ok: true, removed: id, ...noteId ? { noteId, title } : {} });
|
|
26357
26174
|
});
|
|
26358
|
-
server.tool("addendum", `
|
|
26359
|
-
for notes containing pending addendum blocks that need to be folded into the main content.
|
|
26360
|
-
|
|
26361
|
-
These surfaces should be clean, merged, structured notes \u2014 not stacks of timestamped addendum
|
|
26362
|
-
markers. An addendum block on one of these notes is a temporary staging area: read it, fold
|
|
26363
|
-
its content into the relevant section body using revise(mode=replace or section=), then leave
|
|
26364
|
-
no addendum marker behind. Addendum-style append is appropriate only for sessions, diary
|
|
26365
|
-
entries, and logs \u2014 records by nature whose history has value. Everywhere else, merge.
|
|
26366
|
-
|
|
26367
|
-
Returns note IDs, titles, kinds, and content snippets so you can identify what to fold in. The successful call also durably marks the pre-close gate.`, {}, async () => {
|
|
26175
|
+
server.tool("addendum", `Find pending addendum blocks on notes that must stay merged documents (master and LLM singletons, knowledge notes). Fold each into its section with revise(section=/find=) and leave no marker; only sessions, diary, logs and dated thread entries accumulate addenda. The call marks its close-gate step.`, {}, async () => {
|
|
26368
26176
|
const cfg = b();
|
|
26369
26177
|
if (!cfg.root)
|
|
26370
26178
|
return txt3({ error: "BrainLLM not bootstrapped \u2014 run bootstrap." });
|
|
@@ -26410,33 +26218,7 @@ Returns note IDs, titles, kinds, and content snippets so you can identify what t
|
|
|
26410
26218
|
...found.length === 0 ? { note: "No notes with pending addendums." } : { hint: "Fold each addendum into its note: read the block, then revise(noteId, section='<heading>', body='<merged content>', mode='replace') to absorb it into the right section. These notes must be left clean \u2014 no addendum markers. Addendum-style history belongs only in sessions, diary, and logs. Scoped/autonomous agents: fold only what's in your lane \u2014 leaving personal or out-of-scope addendums for the next interactive session is correct, and calling this tool is what satisfies the pre-close gate." }
|
|
26411
26219
|
});
|
|
26412
26220
|
});
|
|
26413
|
-
server.tool("maintain", `
|
|
26414
|
-
threads active \u2192 dormant \u2192 archived). deep=true also surfaces stale notes (untouched past the
|
|
26415
|
-
policy window), unconnected threads/knowledge notes (orphan = no connections at all; sink =
|
|
26416
|
-
has inbound but no outbound) to wire with connect() \u2014 inbound detection is brain-wide, so a
|
|
26417
|
-
note referenced from another area is never misflagged as an orphan \u2014 a structural lint over
|
|
26418
|
-
maintained documents (duplicate headings within one note, unbalanced tags, bodies approaching
|
|
26419
|
-
the read ceiling), duplicate titles, and any thread day-child that escaped its
|
|
26420
|
-
#noteType=threadEntry label. dryRun previews only.
|
|
26421
|
-
|
|
26422
|
-
ack=[noteId, \u2026] marks a note reviewed-and-correct: its findings stay quiet until its content
|
|
26423
|
-
actually changes, then all of them return. Use it instead of ignoring a flag you have decided
|
|
26424
|
-
is fine \u2014 a warning that reappears every run and is correctly ignored every run trains you to
|
|
26425
|
-
skim the list, which is where the one finding that DID change gets missed. Same mechanic as a
|
|
26426
|
-
linter baseline, same justification. suppressed reports how many findings were withheld.
|
|
26427
|
-
|
|
26428
|
-
domain="<name>" narrows the deep passes to one domain's notes \u2014 the equivalent of the lane
|
|
26429
|
-
scoping addendum() already has, so a scoped agent's flags arrive in its own lane instead of it
|
|
26430
|
-
re-deriving each run that the cross-venture findings belong to someone else.
|
|
26431
|
-
|
|
26432
|
-
repair=[noteId, \u2026] fixes an entity-corrupted body in place instead of reporting it: one level of
|
|
26433
|
-
double-escaping is unwound ("&lt;" \u2192 "<", "&nbsp;" \u2192 " "), with a revision taken
|
|
26434
|
-
first. The substitution is always the same one, so it is a first-class action rather than
|
|
26435
|
-
something each caller reinvents with revise(mode="replace"). Compose with dryRun to see the
|
|
26436
|
-
outcome without writing. A note that DOCUMENTS the signature rather than carrying it comes back
|
|
26437
|
-
unchanged and is reported as such \u2014 ack= those.
|
|
26438
|
-
|
|
26439
|
-
coverage names any pass that hit a cap, so a short list is never mistaken for a clean one.`, {
|
|
26221
|
+
server.tool("maintain", `Brain hygiene. Lite (automatic in start/close): ages threads active \u2192 dormant \u2192 archived and checks labels. deep=true adds stale notes, orphans and sinks, structural lint (duplicate headings, unbalanced tags, missing required sections, dated prose in timeless notes, oversized notes), duplicate titles, lapsed or broken claims and hygiene passes. dryRun previews. ack=[ids] silences a note you reviewed until its content changes. domain= narrows deep passes to one lane. repair=[ids] unwinds entity double-escaping in place. coverage names any capped pass.`, {
|
|
26440
26222
|
deep: booleanType().optional().describe("Deep pass: stale-review + orphan/sink + structural lint + duplicate titles across Memory/Threads and Knowledge (default: false)"),
|
|
26441
26223
|
dryRun: booleanType().optional().describe("Report what would change without changing it"),
|
|
26442
26224
|
domain: stringType().optional().describe("Narrow the deep passes to one domain's notes (slugged server-side) \u2014 for scoped agents working a single lane"),
|
|
@@ -26456,10 +26238,7 @@ coverage names any pass that hit a cap, so a short list is never mistaken for a
|
|
|
26456
26238
|
await markStep("maintain");
|
|
26457
26239
|
return txt3(report);
|
|
26458
26240
|
});
|
|
26459
|
-
server.tool("forget", `Archive a note (default) or hard-delete it (hard=true
|
|
26460
|
-
hidden from default recall \u2014 the safe choice and the only one for anything with history.
|
|
26461
|
-
Hard delete is refused while other notes still link here (backlinks are returned so you can
|
|
26462
|
-
re-wire with connect() first). To undo an archive, use recover().`, {
|
|
26241
|
+
server.tool("forget", `Archive a note (default \u2014 hidden from default recall, recoverable with recover()) or hard-delete it (hard=true, refused while backlinked).`, {
|
|
26463
26242
|
noteId: stringType().describe("Note to forget"),
|
|
26464
26243
|
reason: stringType().optional().describe("Why \u2014 recorded in the note before archiving"),
|
|
26465
26244
|
hard: booleanType().optional().describe("Permanently delete instead of archive")
|
|
@@ -26505,11 +26284,7 @@ re-wire with connect() first). To undo an archive, use recover().`, {
|
|
|
26505
26284
|
const relations = relationSnippet(note);
|
|
26506
26285
|
return txt3({ ok: true, archived: noteId, title: note.title, ...relations ? { relations } : {} });
|
|
26507
26286
|
});
|
|
26508
|
-
server.tool("recover", `Restore an archived or resolved note:
|
|
26509
|
-
to active. Use to undo forget() or reconsider a resolved thread / note. Does not restore
|
|
26510
|
-
note content \u2014 use revise() to fix content, or get_revisions (full mode) to roll back to a
|
|
26511
|
-
prior snapshot. For notes deleted from Trilium entirely (not just archived), use undelete_note
|
|
26512
|
-
(full mode) instead.`, {
|
|
26287
|
+
server.tool("recover", `Restore an archived or resolved note: clears #archived and #closed and resets status. Content is untouched \u2014 use revise(), or get_revisions for an older snapshot. Notes deleted from Trilium need undelete_note.`, {
|
|
26513
26288
|
noteId: stringType().describe("The archived or resolved note to restore"),
|
|
26514
26289
|
reason: stringType().optional().describe("Why it was recovered \u2014 written as an addendum"),
|
|
26515
26290
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
@@ -26543,17 +26318,7 @@ prior snapshot. For notes deleted from Trilium entirely (not just archived), use
|
|
|
26543
26318
|
...warnings.length ? { sanitized: warnings } : {}
|
|
26544
26319
|
});
|
|
26545
26320
|
});
|
|
26546
|
-
server.tool("template", `
|
|
26547
|
-
top-to-bottom structure, and the rules writes are held to. Read it BEFORE writing a kind for
|
|
26548
|
-
the first time in a session, or when unsure. The write tools enforce what can be enforced
|
|
26549
|
-
server-side (heading normalization, duplicate-heading detection, thread Goal/Resolution rules,
|
|
26550
|
-
Last-updated stamps); this tool serves the full contract including what remains authorial.
|
|
26551
|
-
|
|
26552
|
-
Template for the schema, sibling for the shape: this gives you the skeleton, but the
|
|
26553
|
-
conventions it cannot encode \u2014 how deep the headings actually go, how a table is laid out, how
|
|
26554
|
-
much prose a section warrants, what a good title looks like for this kind \u2014 live in the notes
|
|
26555
|
-
that already exist. Read one before writing another. Content of the same kind must read like
|
|
26556
|
-
its siblings, and a note that satisfies the skeleton can still be the odd one out.`, {
|
|
26321
|
+
server.tool("template", `The canonical structure for a kind: skeleton, top-to-bottom structure, and the rules writes are held to (including what stays authorial). Read it before your first write of a kind; then read an existing sibling and match its shape.`, {
|
|
26557
26322
|
kind: enumType(Kinds).describe("The content kind to serve the canonical structure for")
|
|
26558
26323
|
}, async ({ kind }) => {
|
|
26559
26324
|
const rule = structureRuleFor(kind);
|
|
@@ -26568,22 +26333,17 @@ its siblings, and a note that satisfies the skeleton can still be the odd one ou
|
|
|
26568
26333
|
...rule ? { structure: rule.structure, rules: rule.rules } : { note: "No bespoke structure for this kind \u2014 server meta line + body." },
|
|
26569
26334
|
skeleton,
|
|
26570
26335
|
conventions: [
|
|
26571
|
-
"Headings h2\u2013h4 only
|
|
26572
|
-
"
|
|
26573
|
-
"
|
|
26574
|
-
"
|
|
26575
|
-
"
|
|
26336
|
+
"Headings h2\u2013h4 only (h1 is the title); only headings that earn their place \u2014 depth comes from tables, lists and emphasis.",
|
|
26337
|
+
"Titles: at most 4 words, no dates or run numbers (they defeat dedup-by-title).",
|
|
26338
|
+
"Timeless kinds carry no state, version or decision history: state goes to the domain's Current State note, history to thread entries and sessions.",
|
|
26339
|
+
"One sentence is enough if it says the thing; drop what is rarely relevant.",
|
|
26340
|
+
"Merge, don't stack: only sessions, diary, logs and dated thread entries are append-only records.",
|
|
26341
|
+
"Match your siblings' structure; improve a pattern everywhere or nowhere.",
|
|
26342
|
+
"Every note carries an icon except logs: the kind default is set for you; pass icon= to choose a fitting one."
|
|
26576
26343
|
]
|
|
26577
26344
|
});
|
|
26578
26345
|
});
|
|
26579
|
-
server.tool("graph", `
|
|
26580
|
-
Scope: the whole brain (default), or a neighborhood (pass noteId + depth). Nodes are the typed
|
|
26581
|
-
notes, colored by area; edges are the typed relations (~template excluded). The Mermaid source
|
|
26582
|
-
is returned AND upserted into the maintained "Graph" note under Insights (a native Trilium
|
|
26583
|
-
mermaid note), so the view renders in Trilium and in any Mermaid-capable client. On-demand only \u2014
|
|
26584
|
-
the note reflects the brain as of this call, not automatically after later writes; call again to
|
|
26585
|
-
refresh. A scoped (noteId) call replaces the note's content with just that neighborhood, not the
|
|
26586
|
-
whole-brain view.`, {
|
|
26346
|
+
server.tool("graph", `Render the relation graph as a Mermaid flowchart \u2014 the whole brain, or a neighborhood (noteId + depth). Returns the source and writes it to the Insights/Graph note. On demand only; a scoped call replaces the note's content.`, {
|
|
26587
26347
|
noteId: stringType().optional().describe("Center the graph on this note's neighborhood instead of the whole brain"),
|
|
26588
26348
|
depth: numberType().optional().describe("Neighborhood hops when noteId is given (default: 2)"),
|
|
26589
26349
|
includeArchived: booleanType().optional().describe("Include archived notes (default: false)")
|
|
@@ -26678,19 +26438,12 @@ whole-brain view.`, {
|
|
|
26678
26438
|
mermaid
|
|
26679
26439
|
});
|
|
26680
26440
|
});
|
|
26681
|
-
server.tool("day", `The new-day sweep
|
|
26682
|
-
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
|
|
26686
|
-
in
|
|
26687
|
-
|
|
26688
|
-
recap=true answers the other question \u2014 "what happened TODAY, in order, across every surface"
|
|
26689
|
-
\u2014 by returning every addendum block written today across the session note, the diary and every
|
|
26690
|
-
thread day-child, chronologically, with its identification line. start() is tuned for a fresh
|
|
26691
|
-
day and does not serve a day already several instances deep; reconstructing one by hand takes a
|
|
26692
|
-
read per surface, and as more work runs unattended that shape becomes the norm rather than the
|
|
26693
|
-
exception.`, {
|
|
26441
|
+
server.tool("day", `The new-day sweep in one call: whether today is fresh (no addendum blocks in today's session
|
|
26442
|
+
note), the previous session in full, that day's log, and the notes touched since. Present what the
|
|
26443
|
+
touched notes evidence in the first message.
|
|
26444
|
+
|
|
26445
|
+
recap=true instead returns every addendum block written today across the session note, the diary
|
|
26446
|
+
and every thread day-child, in time order with its identification line.`, {
|
|
26694
26447
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
26695
26448
|
recap: booleanType().optional().describe("Return today's addendum blocks across sessions, diary and thread children in chronological order, instead of the new-day sweep")
|
|
26696
26449
|
}, async ({ date, recap }) => {
|
|
@@ -26778,37 +26531,19 @@ exception.`, {
|
|
|
26778
26531
|
}));
|
|
26779
26532
|
} catch {}
|
|
26780
26533
|
}
|
|
26781
|
-
const monthName = new Date(`${todayStr}T00:00:00Z`).toLocaleString("en-US", { month: "long", timeZone: "UTC" });
|
|
26782
|
-
let deliverables = null;
|
|
26783
|
-
const monthNotes = await trilium.searchNotes("#noteType=user", { ancestorNoteId: cfg.knowledge.master, fastSearch: true, limit: 100 }).catch(() => ({ results: [] }));
|
|
26784
|
-
const monthNote = monthNotes.results.find((n) => sameTitle(n.title, monthName));
|
|
26785
|
-
if (monthNote) {
|
|
26786
|
-
deliverables = { id: monthNote.noteId, title: monthNote.title, content: await trilium.getNoteContent(monthNote.noteId).catch(() => "") };
|
|
26787
|
-
}
|
|
26788
26534
|
return txt3({
|
|
26789
26535
|
date: todayStr,
|
|
26790
26536
|
newDay,
|
|
26791
|
-
month: monthName,
|
|
26792
26537
|
lastSession,
|
|
26793
26538
|
previousLog,
|
|
26794
26539
|
changes: changes.length ? changes : undefined,
|
|
26795
|
-
deliverables: deliverables ?? { note: `No "${monthName}" deliverables note found in Knowledge/Master \u2014 first session of a new month: baseline a fresh one from the venture strategies and the schedule state.` },
|
|
26796
26540
|
next: [
|
|
26797
|
-
"Skim lastSession, previousLog
|
|
26798
|
-
"
|
|
26799
|
-
"Present the findings in the first message: what moved, what's now due, current counts against the month."
|
|
26541
|
+
"Skim lastSession, previousLog and changes for evidenced outputs \u2014 never plausibility.",
|
|
26542
|
+
"Present what moved in the first message."
|
|
26800
26543
|
]
|
|
26801
26544
|
});
|
|
26802
26545
|
});
|
|
26803
|
-
server.tool("brain", `
|
|
26804
|
-
(Master, LLM, Memory, Knowledge, Insights), grouped by area and sub-container, with
|
|
26805
|
-
id/title/kind/status/dates. Use to audit what the brain contains or locate a specific note.
|
|
26806
|
-
Structural containers are excluded; only content notes appear.
|
|
26807
|
-
|
|
26808
|
-
Insights returns { logs, claims }. Claims were missing entirely before V12 \u2014 they live in a
|
|
26809
|
-
container resolved by title rather than named in the config, and this read was scoped one level
|
|
26810
|
-
too deep to reach them, so the register was invisible to the tool that calls itself the full
|
|
26811
|
-
inventory.`, {
|
|
26546
|
+
server.tool("brain", `The full inventory: every content note across all five areas with id, title, kind, status, parent and dates, grouped by area (Insights returns logs and claims). Use it to audit or locate a note; read parent, not position \u2014 each group is flat. includeArchived= adds archived notes.`, {
|
|
26812
26547
|
includeArchived: booleanType().optional().describe("Include archived/resolved notes (default: false)")
|
|
26813
26548
|
}, async ({ includeArchived }) => {
|
|
26814
26549
|
const cfg = b();
|
|
@@ -26885,25 +26620,7 @@ inventory.`, {
|
|
|
26885
26620
|
const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
|
|
26886
26621
|
return txt3({ total, areas });
|
|
26887
26622
|
});
|
|
26888
|
-
server.tool("assembly", `What the brain
|
|
26889
|
-
surface's own purpose alongside it. The awareness read: "what do I already know here", answered
|
|
26890
|
-
in one call before deciding whether to look something up.
|
|
26891
|
-
|
|
26892
|
-
Distinct from brain(), which is an inventory: brain() returns id, kind, status, parent, dates and
|
|
26893
|
-
relations for every note, and is what you want when auditing or locating one. assembly() answers
|
|
26894
|
-
a different question and is shaped for it:
|
|
26895
|
-
|
|
26896
|
-
- **Dated collections collapse.** Sessions, diary entries and logs are titled [yyyy-mm-dd], so
|
|
26897
|
-
listing them is a wall of dates carrying no information about content. They come back as a
|
|
26898
|
-
count and a span instead. This is the difference between a listing and an awareness read \u2014 the
|
|
26899
|
-
full titles are one brain() call away when you actually need them.
|
|
26900
|
-
- **Domains nest.** Each domain book appears with its own information and sources notes beneath
|
|
26901
|
-
it, which is the real shape of what the brain knows rather than the flat list brain() returns.
|
|
26902
|
-
- **Threads group by status**, newest activity first \u2014 the working set is the actionable part.
|
|
26903
|
-
- **Purposes are read from the container notes themselves**, the text bootstrap engraved on them,
|
|
26904
|
-
so this can never drift from what the brain actually says about itself.
|
|
26905
|
-
|
|
26906
|
-
Pass area= to zoom into one surface and get its full detail.`, {
|
|
26623
|
+
server.tool("assembly", `What the brain holds: every note by title, grouped under its surface with that surface's purpose. Dated collections (sessions, diary, logs) collapse to a count and span; domains nest their notes; threads group by status. The awareness read \u2014 use brain() instead to audit or locate by id. area= zooms into one surface.`, {
|
|
26907
26624
|
area: enumType(["master", "llm", "memory", "knowledge", "insights"]).optional().describe("Zoom into one surface instead of all five"),
|
|
26908
26625
|
includeArchived: booleanType().optional().describe("Include archived/resolved notes (default: false)")
|
|
26909
26626
|
}, async ({ area, includeArchived }) => {
|
|
@@ -27007,12 +26724,7 @@ Pass area= to zoom into one surface and get its full detail.`, {
|
|
|
27007
26724
|
...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
|
|
27008
26725
|
});
|
|
27009
26726
|
});
|
|
27010
|
-
server.tool("bootstrap", `
|
|
27011
|
-
heals singletons a newer version introduced, and re-engraves container purposes if the structure
|
|
27012
|
-
already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
|
|
27013
|
-
LLM (Responsibilities/Protocols/Self-correction/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
|
|
27014
|
-
Insights (Logs) \u2014 each engraved with its purpose, and writes brainllm.json. Active
|
|
27015
|
-
immediately, no restart needed.`, {}, async () => {
|
|
26727
|
+
server.tool("bootstrap", `Create the BrainLLM structure in Trilium, or verify and refresh it when it exists (idempotent): the five areas with their containers, singletons and engraved purposes, plus brainllm.json. Only creates a new tree when the stored root is confirmed deleted.`, {}, async () => {
|
|
27016
26728
|
if (b().root) {
|
|
27017
26729
|
try {
|
|
27018
26730
|
const existing = await trilium.getNote(b().root);
|
|
@@ -27488,7 +27200,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27488
27200
|
await trilium.deleteNote(noteId);
|
|
27489
27201
|
return txt4({ ok: true, deleted: noteId });
|
|
27490
27202
|
});
|
|
27491
|
-
server.tool("undelete_note",
|
|
27203
|
+
server.tool("undelete_note", `Restore a note deleted in Trilium (check note_history for canBeUndeleted) and re-apply its BrainLLM labels. For BrainLLM-archived notes use recover().`, { noteId: stringType() }, async ({ noteId }) => {
|
|
27492
27204
|
await trilium.undeleteNote(noteId);
|
|
27493
27205
|
const cfg = b();
|
|
27494
27206
|
const note = await trilium.getNote(noteId);
|
|
@@ -27660,7 +27372,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27660
27372
|
server.tool("get_year_note", "Get (or auto-create) the journal year note. Format: YYYY.", { year: stringType() }, async ({ year }) => txt4(await trilium.getYearNote(year)));
|
|
27661
27373
|
server.tool("get_inbox_note", "Get the Trilium inbox note for a date (fixed #inbox note, or the day note). Format: YYYY-MM-DD.", { date: stringType().optional() }, async ({ date }) => txt4(await trilium.getInboxNote(date ?? localToday())));
|
|
27662
27374
|
server.tool("get_app_info", "Trilium server version, DB version, runtime metadata.", {}, async () => txt4(await trilium.getAppInfo()));
|
|
27663
|
-
server.tool("create_backup",
|
|
27375
|
+
server.tool("create_backup", `Named Trilium database backup (raw). The resulting file path is not exposed.`, {
|
|
27664
27376
|
name: stringType().optional().describe("Backup file name without .db extension (default: brainllm-{today})"),
|
|
27665
27377
|
date: stringType().optional().describe("ISO date used in the default name when name is omitted (default: today)")
|
|
27666
27378
|
}, async ({ name, date }) => {
|
|
@@ -28824,7 +28536,7 @@ function createServer(origin = null) {
|
|
|
28824
28536
|
const s = new McpServer({
|
|
28825
28537
|
name: "BrainLLM",
|
|
28826
28538
|
title: "BrainLLM",
|
|
28827
|
-
version: "12.
|
|
28539
|
+
version: "12.5.0",
|
|
28828
28540
|
icons: brandingIcons(origin)
|
|
28829
28541
|
});
|
|
28830
28542
|
registerTools(s, trilium, brainRef);
|
|
@@ -28977,12 +28689,14 @@ if (port) {
|
|
|
28977
28689
|
return withCors(await handleRegister(req));
|
|
28978
28690
|
}
|
|
28979
28691
|
}
|
|
28980
|
-
|
|
28692
|
+
const rootMcpRequest = url.pathname === "/" && (req.method !== "GET" || req.headers.has("mcp-session-id") || req.headers.has("mcp-protocol-version") || (req.headers.get("accept") ?? "").includes("application/json") || (req.headers.get("accept") ?? "").includes("text/event-stream"));
|
|
28693
|
+
if (url.pathname === "/" && !rootMcpRequest) {
|
|
28981
28694
|
return withCors(new Response(landingPage(base, oauthOn, true), {
|
|
28982
28695
|
headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }
|
|
28983
28696
|
}));
|
|
28984
28697
|
}
|
|
28985
|
-
|
|
28698
|
+
const isMcpEndpoint = url.pathname === "/mcp" || rootMcpRequest;
|
|
28699
|
+
if (!isMcpEndpoint && url.pathname !== "/sse" && url.pathname !== "/messages") {
|
|
28986
28700
|
return withCors(new Response("Not Found", { status: 404 }));
|
|
28987
28701
|
}
|
|
28988
28702
|
const transportLimit = rateLimited(rateLimiters.transport, requestKey);
|