brainllm 12.4.2 → 12.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +6 -3
- package/SECURITY.md +2 -2
- package/dist/index.js +606 -602
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +144 -308
- package/skills/brainllm/references/domains.md +30 -46
- package/skills/brainllm/references/fullmode.md +1 -1
- package/skills/brainllm/references/taxonomy.md +3 -2
- package/skills/brainllm/references/troubleshooting.md +4 -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";
|
|
@@ -21203,6 +21254,19 @@ function saveCachedToken(token) {
|
|
|
21203
21254
|
return null;
|
|
21204
21255
|
}
|
|
21205
21256
|
}
|
|
21257
|
+
var MAX_CATCHUP_DAYS = 3650;
|
|
21258
|
+
function envDeletionCatchupDays(env = process.env) {
|
|
21259
|
+
const raw = env.BRAINLLM_DELETION_CATCHUP_DAYS?.trim();
|
|
21260
|
+
if (!raw)
|
|
21261
|
+
return {};
|
|
21262
|
+
const days = Number(raw);
|
|
21263
|
+
if (!/^\d+$/.test(raw) || days < 1 || days > MAX_CATCHUP_DAYS)
|
|
21264
|
+
return { invalid: raw };
|
|
21265
|
+
return { days };
|
|
21266
|
+
}
|
|
21267
|
+
function deletionCatchupDays(cfg, env = process.env) {
|
|
21268
|
+
return envDeletionCatchupDays(env).days ?? cfg.policy?.deletionCatchupDays ?? DEFAULT_POLICY.deletionCatchupDays;
|
|
21269
|
+
}
|
|
21206
21270
|
function loadConfig() {
|
|
21207
21271
|
const path = configFilePath();
|
|
21208
21272
|
if (!existsSync(path))
|
|
@@ -21374,13 +21438,141 @@ function normalizeIcon(raw) {
|
|
|
21374
21438
|
const name = s.replace(/^bx[sl]?-/, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
21375
21439
|
return name ? `bx bx-${name}` : "";
|
|
21376
21440
|
}
|
|
21377
|
-
var HTML_TAG = /<\/?[a-z][a-z0-9-]*(\s[^<>]*)?>/
|
|
21441
|
+
var HTML_TAG = /<\/?([a-z][a-z0-9-]*)(\s[^<>]*)?>/gi;
|
|
21442
|
+
var HTML_ELEMENTS = new Set([
|
|
21443
|
+
"a",
|
|
21444
|
+
"abbr",
|
|
21445
|
+
"address",
|
|
21446
|
+
"area",
|
|
21447
|
+
"article",
|
|
21448
|
+
"aside",
|
|
21449
|
+
"audio",
|
|
21450
|
+
"b",
|
|
21451
|
+
"base",
|
|
21452
|
+
"bdi",
|
|
21453
|
+
"bdo",
|
|
21454
|
+
"blockquote",
|
|
21455
|
+
"body",
|
|
21456
|
+
"br",
|
|
21457
|
+
"button",
|
|
21458
|
+
"canvas",
|
|
21459
|
+
"caption",
|
|
21460
|
+
"center",
|
|
21461
|
+
"cite",
|
|
21462
|
+
"code",
|
|
21463
|
+
"col",
|
|
21464
|
+
"colgroup",
|
|
21465
|
+
"data",
|
|
21466
|
+
"datalist",
|
|
21467
|
+
"dd",
|
|
21468
|
+
"del",
|
|
21469
|
+
"details",
|
|
21470
|
+
"dfn",
|
|
21471
|
+
"dialog",
|
|
21472
|
+
"div",
|
|
21473
|
+
"dl",
|
|
21474
|
+
"dt",
|
|
21475
|
+
"em",
|
|
21476
|
+
"embed",
|
|
21477
|
+
"fieldset",
|
|
21478
|
+
"figcaption",
|
|
21479
|
+
"figure",
|
|
21480
|
+
"font",
|
|
21481
|
+
"footer",
|
|
21482
|
+
"form",
|
|
21483
|
+
"h1",
|
|
21484
|
+
"h2",
|
|
21485
|
+
"h3",
|
|
21486
|
+
"h4",
|
|
21487
|
+
"h5",
|
|
21488
|
+
"h6",
|
|
21489
|
+
"head",
|
|
21490
|
+
"header",
|
|
21491
|
+
"hgroup",
|
|
21492
|
+
"hr",
|
|
21493
|
+
"html",
|
|
21494
|
+
"i",
|
|
21495
|
+
"iframe",
|
|
21496
|
+
"img",
|
|
21497
|
+
"input",
|
|
21498
|
+
"ins",
|
|
21499
|
+
"kbd",
|
|
21500
|
+
"label",
|
|
21501
|
+
"legend",
|
|
21502
|
+
"li",
|
|
21503
|
+
"link",
|
|
21504
|
+
"main",
|
|
21505
|
+
"map",
|
|
21506
|
+
"mark",
|
|
21507
|
+
"math",
|
|
21508
|
+
"menu",
|
|
21509
|
+
"meta",
|
|
21510
|
+
"meter",
|
|
21511
|
+
"nav",
|
|
21512
|
+
"noscript",
|
|
21513
|
+
"object",
|
|
21514
|
+
"ol",
|
|
21515
|
+
"optgroup",
|
|
21516
|
+
"option",
|
|
21517
|
+
"output",
|
|
21518
|
+
"p",
|
|
21519
|
+
"param",
|
|
21520
|
+
"picture",
|
|
21521
|
+
"pre",
|
|
21522
|
+
"progress",
|
|
21523
|
+
"q",
|
|
21524
|
+
"rp",
|
|
21525
|
+
"rt",
|
|
21526
|
+
"ruby",
|
|
21527
|
+
"s",
|
|
21528
|
+
"samp",
|
|
21529
|
+
"script",
|
|
21530
|
+
"search",
|
|
21531
|
+
"section",
|
|
21532
|
+
"select",
|
|
21533
|
+
"slot",
|
|
21534
|
+
"small",
|
|
21535
|
+
"source",
|
|
21536
|
+
"span",
|
|
21537
|
+
"strike",
|
|
21538
|
+
"strong",
|
|
21539
|
+
"style",
|
|
21540
|
+
"sub",
|
|
21541
|
+
"summary",
|
|
21542
|
+
"sup",
|
|
21543
|
+
"svg",
|
|
21544
|
+
"table",
|
|
21545
|
+
"tbody",
|
|
21546
|
+
"td",
|
|
21547
|
+
"template",
|
|
21548
|
+
"textarea",
|
|
21549
|
+
"tfoot",
|
|
21550
|
+
"th",
|
|
21551
|
+
"thead",
|
|
21552
|
+
"time",
|
|
21553
|
+
"title",
|
|
21554
|
+
"tr",
|
|
21555
|
+
"track",
|
|
21556
|
+
"tt",
|
|
21557
|
+
"u",
|
|
21558
|
+
"ul",
|
|
21559
|
+
"var",
|
|
21560
|
+
"video",
|
|
21561
|
+
"wbr",
|
|
21562
|
+
"applet"
|
|
21563
|
+
]);
|
|
21564
|
+
function isHtmlElement(name) {
|
|
21565
|
+
return HTML_ELEMENTS.has(name.toLowerCase());
|
|
21566
|
+
}
|
|
21378
21567
|
function looksLikeHtml(body) {
|
|
21379
|
-
|
|
21568
|
+
for (const m of body.matchAll(HTML_TAG))
|
|
21569
|
+
if (isHtmlElement(m[1]))
|
|
21570
|
+
return true;
|
|
21571
|
+
return false;
|
|
21380
21572
|
}
|
|
21381
21573
|
var ENCODED_TAG = /<\/?[a-z][a-z0-9-]*(?:\s[^&<>]*?)?\s*\/?>/i;
|
|
21382
21574
|
function looksLikeEncodedHtml(body) {
|
|
21383
|
-
return !
|
|
21575
|
+
return !looksLikeHtml(body) && ENCODED_TAG.test(body);
|
|
21384
21576
|
}
|
|
21385
21577
|
function decodeEncodedHtml(body) {
|
|
21386
21578
|
let s = body;
|
|
@@ -21441,15 +21633,102 @@ function nearestHeading(requested, available) {
|
|
|
21441
21633
|
function inlineMd(escaped) {
|
|
21442
21634
|
return escaped.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2">$1</a>');
|
|
21443
21635
|
}
|
|
21444
|
-
function
|
|
21636
|
+
function renderMarkup(raw) {
|
|
21637
|
+
const body = raw.replace(/\u0000/g, "");
|
|
21445
21638
|
if (!body.trim())
|
|
21446
|
-
return "<p></p>";
|
|
21639
|
+
return { html: "<p></p>", convertedRuns: 0 };
|
|
21447
21640
|
if (looksLikeHtml(body))
|
|
21448
|
-
return body;
|
|
21641
|
+
return mixedToHtml(body);
|
|
21449
21642
|
if (looksLikeEncodedHtml(body))
|
|
21450
|
-
return decodeEncodedHtml(body);
|
|
21643
|
+
return { html: decodeEncodedHtml(body), convertedRuns: 0 };
|
|
21644
|
+
return { html: markdownToHtml(body), convertedRuns: 0 };
|
|
21645
|
+
}
|
|
21646
|
+
var BLOCK_ELEMENTS = new Set([
|
|
21647
|
+
"address",
|
|
21648
|
+
"article",
|
|
21649
|
+
"aside",
|
|
21650
|
+
"blockquote",
|
|
21651
|
+
"dd",
|
|
21652
|
+
"details",
|
|
21653
|
+
"div",
|
|
21654
|
+
"dl",
|
|
21655
|
+
"dt",
|
|
21656
|
+
"figcaption",
|
|
21657
|
+
"figure",
|
|
21658
|
+
"footer",
|
|
21659
|
+
"h1",
|
|
21660
|
+
"h2",
|
|
21661
|
+
"h3",
|
|
21662
|
+
"h4",
|
|
21663
|
+
"h5",
|
|
21664
|
+
"h6",
|
|
21665
|
+
"header",
|
|
21666
|
+
"hr",
|
|
21667
|
+
"li",
|
|
21668
|
+
"main",
|
|
21669
|
+
"nav",
|
|
21670
|
+
"ol",
|
|
21671
|
+
"p",
|
|
21672
|
+
"pre",
|
|
21673
|
+
"section",
|
|
21674
|
+
"summary",
|
|
21675
|
+
"table",
|
|
21676
|
+
"tbody",
|
|
21677
|
+
"td",
|
|
21678
|
+
"tfoot",
|
|
21679
|
+
"th",
|
|
21680
|
+
"thead",
|
|
21681
|
+
"tr",
|
|
21682
|
+
"ul"
|
|
21683
|
+
]);
|
|
21684
|
+
var VOID_BLOCKS = new Set(["hr"]);
|
|
21685
|
+
var LEADING_BLOCK_TAG = /^\s*<([a-z][a-z0-9]*)(?=[\s>/])/i;
|
|
21686
|
+
var BLOCK_TAG = /<(\/?)([a-z][a-z0-9]*)(?=[\s>/])[^>]*?(\/?)>/gi;
|
|
21687
|
+
function mixedToHtml(body) {
|
|
21451
21688
|
const lines = body.replace(/\r\n/g, `
|
|
21452
21689
|
`).split(`
|
|
21690
|
+
`);
|
|
21691
|
+
const out = [];
|
|
21692
|
+
let run = [];
|
|
21693
|
+
let depth = 0;
|
|
21694
|
+
let convertedRuns = 0;
|
|
21695
|
+
const flushRun = () => {
|
|
21696
|
+
if (!run.length)
|
|
21697
|
+
return;
|
|
21698
|
+
const text = run.join(`
|
|
21699
|
+
`);
|
|
21700
|
+
if (text.trim()) {
|
|
21701
|
+
out.push(markdownToHtml(text, true));
|
|
21702
|
+
convertedRuns++;
|
|
21703
|
+
} else {
|
|
21704
|
+
out.push(text);
|
|
21705
|
+
}
|
|
21706
|
+
run = [];
|
|
21707
|
+
};
|
|
21708
|
+
for (const line of lines) {
|
|
21709
|
+
const lead = LEADING_BLOCK_TAG.exec(line)?.[1]?.toLowerCase();
|
|
21710
|
+
if (depth === 0 && !(lead && BLOCK_ELEMENTS.has(lead))) {
|
|
21711
|
+
run.push(line);
|
|
21712
|
+
continue;
|
|
21713
|
+
}
|
|
21714
|
+
flushRun();
|
|
21715
|
+
out.push(line);
|
|
21716
|
+
for (const [, close, name, selfClose] of line.matchAll(BLOCK_TAG)) {
|
|
21717
|
+
const tag = name.toLowerCase();
|
|
21718
|
+
if (!BLOCK_ELEMENTS.has(tag) || VOID_BLOCKS.has(tag) || selfClose)
|
|
21719
|
+
continue;
|
|
21720
|
+
depth = Math.max(0, depth + (close ? -1 : 1));
|
|
21721
|
+
}
|
|
21722
|
+
}
|
|
21723
|
+
flushRun();
|
|
21724
|
+
return { html: out.join(`
|
|
21725
|
+
`), convertedRuns };
|
|
21726
|
+
}
|
|
21727
|
+
function markdownToHtml(body, inlineHtml = false) {
|
|
21728
|
+
const kept = [];
|
|
21729
|
+
const source = inlineHtml ? body.replace(HTML_TAG, (tag, name) => isHtmlElement(name) ? `\x00${kept.push(tag) - 1}\x00` : tag) : body;
|
|
21730
|
+
const lines = source.replace(/\r\n/g, `
|
|
21731
|
+
`).split(`
|
|
21453
21732
|
`);
|
|
21454
21733
|
const out = [];
|
|
21455
21734
|
let para = [];
|
|
@@ -21547,8 +21826,12 @@ function toHtml(body) {
|
|
|
21547
21826
|
flushTable();
|
|
21548
21827
|
flushPara();
|
|
21549
21828
|
flushList();
|
|
21550
|
-
|
|
21829
|
+
const html = out.join(`
|
|
21551
21830
|
`) || "<p></p>";
|
|
21831
|
+
if (!kept.length)
|
|
21832
|
+
return html;
|
|
21833
|
+
const restore = (token) => kept[Number(token)] ?? "";
|
|
21834
|
+
return html.replace(/(<pre><code>[\s\S]*?<\/code><\/pre>)|\u0000(\d+)\u0000/g, (_m, pre, i) => pre ? pre.replace(/\u0000(\d+)\u0000/g, (_x, j) => escapeHtml(restore(j), false)) : restore(i));
|
|
21552
21835
|
}
|
|
21553
21836
|
function toText(html, maxLength = 300) {
|
|
21554
21837
|
const text = decodeEntities(html.replace(/<(style|script)[\s\S]*?<\/\1>/gi, " ").replace(/<(h[1-6]|p|li|div|tr|br)[^>]*>/gi, `
|
|
@@ -21749,17 +22032,27 @@ function safeHtmlUrl(value) {
|
|
|
21749
22032
|
return null;
|
|
21750
22033
|
return decoded;
|
|
21751
22034
|
}
|
|
22035
|
+
function keptAsText(full, warnings) {
|
|
22036
|
+
warnings.push(`Kept ${full} as text \u2014 not an HTML element`);
|
|
22037
|
+
return escapeHtml(full, false);
|
|
22038
|
+
}
|
|
21752
22039
|
function sanitizeTag(full, warnings) {
|
|
21753
22040
|
if (full.startsWith("<!--"))
|
|
21754
22041
|
return "";
|
|
21755
22042
|
const close = /^<\s*\/\s*([a-z][a-z0-9-]*)\s*>$/i.exec(full);
|
|
21756
|
-
if (close)
|
|
21757
|
-
|
|
22043
|
+
if (close) {
|
|
22044
|
+
const name = close[1].toLowerCase();
|
|
22045
|
+
if (SAFE_TAGS.has(name))
|
|
22046
|
+
return `</${name}>`;
|
|
22047
|
+
return isHtmlElement(name) ? "" : keptAsText(full, warnings);
|
|
22048
|
+
}
|
|
21758
22049
|
const open = /^<\s*([a-z][a-z0-9-]*)(\s[\s\S]*?)?\/?\s*>$/i.exec(full);
|
|
21759
22050
|
if (!open)
|
|
21760
22051
|
return "";
|
|
21761
22052
|
const tag = open[1].toLowerCase();
|
|
21762
22053
|
if (!SAFE_TAGS.has(tag)) {
|
|
22054
|
+
if (!isHtmlElement(tag))
|
|
22055
|
+
return keptAsText(full, warnings);
|
|
21763
22056
|
warnings.push(`Stripped unsupported <${tag}> element`);
|
|
21764
22057
|
return "";
|
|
21765
22058
|
}
|
|
@@ -21876,16 +22169,16 @@ function sanitizeHtml(html) {
|
|
|
21876
22169
|
}
|
|
21877
22170
|
function renderBody(body) {
|
|
21878
22171
|
const wasEncoded = looksLikeEncodedHtml(body);
|
|
21879
|
-
const
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
};
|
|
22172
|
+
const { html, convertedRuns } = renderMarkup(body);
|
|
22173
|
+
const result = sanitizeHtml(html);
|
|
22174
|
+
const notes = [];
|
|
22175
|
+
if (wasEncoded) {
|
|
22176
|
+
notes.push("Entity-encoded markup decoded to real HTML \u2014 pass tags as <p>\u2026</p>, not <p>\u2026</p>; the escaped form would otherwise be stored as literal text");
|
|
22177
|
+
}
|
|
22178
|
+
if (convertedRuns) {
|
|
22179
|
+
notes.push(`Converted ${convertedRuns} text run(s) outside HTML blocks from markdown to HTML \u2014 send a body as all markdown or all HTML to control its shape exactly`);
|
|
22180
|
+
}
|
|
22181
|
+
return notes.length ? { html: result.html, warnings: [...notes, ...result.warnings] } : result;
|
|
21889
22182
|
}
|
|
21890
22183
|
function repairedStructure(warnings) {
|
|
21891
22184
|
return warnings.filter((w) => w.startsWith("Closed unclosed block tag") || w.startsWith("<br> normalized to paragraph separators"));
|
|
@@ -22399,11 +22692,6 @@ var hasHeading = (html, text) => new RegExp(`<h[2-4](?:\\s[^>]*)?>\\s*${text}\\s
|
|
|
22399
22692
|
var REQUIRED_SECTIONS = {
|
|
22400
22693
|
thread: ["Context", "Resolution"],
|
|
22401
22694
|
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
22695
|
selfcorrection: ["Corrections"],
|
|
22408
22696
|
claim: ["Check", "Verifications"]
|
|
22409
22697
|
};
|
|
@@ -22504,115 +22792,102 @@ function domainContent(name) {
|
|
|
22504
22792
|
function purposeContent(purpose) {
|
|
22505
22793
|
return `<p><em>${escapeHtml(purpose)}</em></p>`;
|
|
22506
22794
|
}
|
|
22795
|
+
var TIMELESS = [
|
|
22796
|
+
"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",
|
|
22797
|
+
"One sentence is enough if one sentence says it; drop what is rarely relevant to the work",
|
|
22798
|
+
"Merged in place (revise section=/find=), never appended as dated addendum blocks"
|
|
22799
|
+
];
|
|
22507
22800
|
var STRUCTURE_RULES = {
|
|
22508
22801
|
singleton: {
|
|
22509
22802
|
structure: [
|
|
22510
22803
|
"`Last updated - <date>` (h4) \u2014 server-maintained on every write",
|
|
22511
|
-
"Minimal h3 sections; tables for reference data;
|
|
22804
|
+
"Minimal h3 sections; tables for reference data; short bold-lead rules for rule/duty lists"
|
|
22512
22805
|
],
|
|
22513
22806
|
rules: [
|
|
22514
|
-
|
|
22515
|
-
"
|
|
22807
|
+
...TIMELESS,
|
|
22808
|
+
"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
22809
|
]
|
|
22517
22810
|
},
|
|
22518
22811
|
diary: {
|
|
22519
22812
|
structure: [
|
|
22520
22813
|
"`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)"
|
|
22814
|
+
"Identification line (h3): `LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]` \u2014 pass identity=",
|
|
22815
|
+
"**Experience** (h4), then **BrainLLM** (h4)"
|
|
22524
22816
|
],
|
|
22525
22817
|
rules: [
|
|
22526
|
-
"One note per day
|
|
22527
|
-
"
|
|
22528
|
-
"The closing entry is written at the close-protocol step, after remarks()"
|
|
22818
|
+
"One note per day; every write is a timestamped addendum block",
|
|
22819
|
+
"identity= is enforced; the closing entry is written after remarks()"
|
|
22529
22820
|
]
|
|
22530
22821
|
},
|
|
22531
22822
|
session: {
|
|
22532
22823
|
structure: [
|
|
22533
22824
|
"`Addendum \u2014 HH:mm` (h2, server-written)",
|
|
22534
|
-
"Identification line (h3)
|
|
22535
|
-
"**Summary
|
|
22536
|
-
"**Learned** \u2014 durable bullets [+ further detail sections as warranted]"
|
|
22825
|
+
"Identification line (h3) \u2014 pass identity= on close()",
|
|
22826
|
+
"**Summary**, then **Learned**"
|
|
22537
22827
|
],
|
|
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
|
-
]
|
|
22828
|
+
rules: ["Written by close() only; one note per day; identity= is enforced"]
|
|
22542
22829
|
},
|
|
22543
22830
|
thread: {
|
|
22544
22831
|
structure: [
|
|
22545
|
-
"A book note
|
|
22546
|
-
"
|
|
22547
|
-
|
|
22832
|
+
"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()",
|
|
22833
|
+
"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",
|
|
22834
|
+
'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
22835
|
],
|
|
22549
22836
|
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"
|
|
22837
|
+
"Creating a thread requires goal= (ask the user)",
|
|
22838
|
+
"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",
|
|
22839
|
+
`Dated: appends land in today's child (identity= required). Collection: add with remember(kind="threadEntry", thread=, title=), change with revise(<entry id>)`,
|
|
22840
|
+
"Only resolve() writes the Resolution"
|
|
22556
22841
|
]
|
|
22557
22842
|
},
|
|
22558
22843
|
threadEntry: {
|
|
22559
22844
|
structure: [
|
|
22560
|
-
"
|
|
22561
|
-
"
|
|
22845
|
+
"Dated: header (threadEntry \xB7 <date>), then `Addendum \u2014 HH:mm` blocks with an identification line",
|
|
22846
|
+
"Collection: a titled maintained document \u2014 minimal h2/h3 sections"
|
|
22562
22847
|
],
|
|
22563
22848
|
rules: [
|
|
22564
|
-
"
|
|
22565
|
-
"
|
|
22849
|
+
"Dated entries are created by appending to the thread and are records (never rewritten)",
|
|
22850
|
+
"Collection entries dedup by title within their thread and are edited in place"
|
|
22566
22851
|
]
|
|
22567
22852
|
},
|
|
22568
22853
|
sources: {
|
|
22569
22854
|
structure: [
|
|
22570
|
-
"
|
|
22571
|
-
"
|
|
22572
|
-
"**
|
|
22573
|
-
"**Revision** (h2): a Source | Marker | Date table, one row per source, current-state only"
|
|
22855
|
+
"Header (sources \xB7 domain) \u2192 `Last updated` (h4)",
|
|
22856
|
+
"**Sources** (h2): the \u2747\uFE0F/\u2705 legend, then every source listed individually with its emoji, grouped under h3s",
|
|
22857
|
+
"**Revision** (h2): Source | Marker | Date, one row per source"
|
|
22574
22858
|
],
|
|
22575
22859
|
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"
|
|
22860
|
+
'One per domain, created with the domain; merged via remember(kind="sources"), never stacked',
|
|
22861
|
+
"Revision rows upsert by source name via revision=[{source, marker, date}]; the name must match the Sources list exactly"
|
|
22580
22862
|
]
|
|
22581
22863
|
},
|
|
22582
22864
|
information: {
|
|
22583
22865
|
structure: [
|
|
22584
|
-
"
|
|
22585
|
-
"
|
|
22586
|
-
"Minimal h3 sections \u2014 current-state truth, revised in place"
|
|
22866
|
+
"Header (information \xB7 domain \xB7 date) \u2192 `Last updated` line (server-maintained)",
|
|
22867
|
+
"Minimal h3 sections"
|
|
22587
22868
|
],
|
|
22588
22869
|
rules: [
|
|
22589
|
-
|
|
22590
|
-
"
|
|
22870
|
+
...TIMELESS,
|
|
22871
|
+
"Exception: the note titled **Current State** holds the domain's measured state \u2014 latest value only, with the date it was measured",
|
|
22872
|
+
"One note per sub-category; no dates or run numbers in titles (\u2264 4 words); every claim traces to the Sources note"
|
|
22591
22873
|
]
|
|
22592
22874
|
},
|
|
22593
22875
|
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"]
|
|
22876
|
+
structure: ["Header (user \xB7 date) \u2192 `Last updated` (h4) \u2192 minimal h3 sections, tables for reference data"],
|
|
22877
|
+
rules: [...TIMELESS, "Titles \u2264 4 words"]
|
|
22600
22878
|
},
|
|
22601
22879
|
log: {
|
|
22602
|
-
structure: ["
|
|
22603
|
-
rules: ["
|
|
22880
|
+
structure: ["Header (log \xB7 date) \u2192 **Created** / **Updated** / **Deleted** (h2) lists"],
|
|
22881
|
+
rules: ["Generated by close(); regenerated in place"]
|
|
22604
22882
|
},
|
|
22605
22883
|
claim: {
|
|
22606
22884
|
structure: [
|
|
22607
|
-
"**Check** (h2) \u2014 how to verify
|
|
22608
|
-
"**Verifications** (h2) \u2014 one dated HOLDS/BROKEN line per check, with
|
|
22885
|
+
"**Check** (h2) \u2014 how to verify, as inert text (command, query, path, URL)",
|
|
22886
|
+
"**Verifications** (h2) \u2014 one dated HOLDS/BROKEN line per check, with evidence"
|
|
22609
22887
|
],
|
|
22610
22888
|
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"
|
|
22889
|
+
"Only claim() writes claims; the title is the assertion and the dedup key",
|
|
22890
|
+
"BrainLLM never executes the check \u2014 the agent runs it and reports back with evidence"
|
|
22616
22891
|
]
|
|
22617
22892
|
}
|
|
22618
22893
|
};
|
|
@@ -22836,6 +23111,13 @@ function isoDaysAgo(days) {
|
|
|
22836
23111
|
function reviewKey(n) {
|
|
22837
23112
|
return n.blobId ?? n.dateModified;
|
|
22838
23113
|
}
|
|
23114
|
+
var TIMELESS_KINDS = new Set(["thread", "user", "information", "domain"]);
|
|
23115
|
+
var DATED_PROSE_LIMIT = 4;
|
|
23116
|
+
var MONTHS = "Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?";
|
|
23117
|
+
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");
|
|
23118
|
+
function datedReferences(html) {
|
|
23119
|
+
return (toText(html, Number.MAX_SAFE_INTEGER).match(DATED_REF) ?? []).length;
|
|
23120
|
+
}
|
|
22839
23121
|
var STRUCTURE_LINT_LIMIT = 40;
|
|
22840
23122
|
var SECTION_EDIT_RISK_COUNT = 25;
|
|
22841
23123
|
var SECTION_EDIT_RISK_CHARS = 8000;
|
|
@@ -22854,7 +23136,7 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22854
23136
|
dormantAfterDays: policy.dormantAfterDays,
|
|
22855
23137
|
archiveDormantAfterDays: policy.archiveDormantAfterDays,
|
|
22856
23138
|
staleAfterDays: policy.staleAfterDays,
|
|
22857
|
-
deletionCatchupDays:
|
|
23139
|
+
deletionCatchupDays: deletionCatchupDays(cfg)
|
|
22858
23140
|
}
|
|
22859
23141
|
};
|
|
22860
23142
|
if (!cfg.root)
|
|
@@ -22902,6 +23184,23 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22902
23184
|
}
|
|
22903
23185
|
report.transitions.push(`archived: ${n.title} (thread, dormant past grace)`);
|
|
22904
23186
|
}
|
|
23187
|
+
const iconless = await trilium.searchNotes("#!iconClass #noteType != log", { ancestorNoteId: cfg.root, includeArchivedNotes: true, limit: 1000 }).then((r) => r.results).catch(() => []);
|
|
23188
|
+
if (iconless.length) {
|
|
23189
|
+
report.scanned += iconless.length;
|
|
23190
|
+
const entryLast = [...iconless].sort((a, b) => Number(ownedLabel(a, "noteType") === "threadEntry") - Number(ownedLabel(b, "noteType") === "threadEntry"));
|
|
23191
|
+
let set = 0;
|
|
23192
|
+
for (const n of entryLast) {
|
|
23193
|
+
if (dryRun) {
|
|
23194
|
+
if (defaultIcon(n))
|
|
23195
|
+
set++;
|
|
23196
|
+
continue;
|
|
23197
|
+
}
|
|
23198
|
+
if (await ensureIcon(trilium, n.noteId, n))
|
|
23199
|
+
set++;
|
|
23200
|
+
}
|
|
23201
|
+
if (set)
|
|
23202
|
+
report.fixed.push(`icons: ${dryRun ? "would set" : "set"} a default icon on ${set} note${set === 1 ? "" : "s"} that had none`);
|
|
23203
|
+
}
|
|
22905
23204
|
const typedContainers = [
|
|
22906
23205
|
{ id: cfg.memory.threads, kind: "thread", label: "Threads" },
|
|
22907
23206
|
{ id: cfg.memory.sessions, kind: "session", label: "Sessions" },
|
|
@@ -22986,7 +23285,7 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22986
23285
|
}
|
|
22987
23286
|
if (deep && !domainSlug) {
|
|
22988
23287
|
const feed = await trilium.getNoteHistory(cfg.root).catch(() => []);
|
|
22989
|
-
const deletionsWindow = isoDaysAgo(
|
|
23288
|
+
const deletionsWindow = isoDaysAgo(deletionCatchupDays(cfg));
|
|
22990
23289
|
const recent = feed.filter((h) => h.current_isDeleted && h.date.slice(0, 10) >= deletionsWindow);
|
|
22991
23290
|
report.scanned += recent.length;
|
|
22992
23291
|
for (const [i, h] of recent.entries()) {
|
|
@@ -23079,6 +23378,14 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23079
23378
|
if (structure.duplicateHeadings.length) {
|
|
23080
23379
|
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
23380
|
}
|
|
23381
|
+
const kindHere = ownedLabel(n, "noteType");
|
|
23382
|
+
const isCurrentState = kindHere === "information" && /^current state$/i.test(n.title.trim());
|
|
23383
|
+
if (kindHere && TIMELESS_KINDS.has(kindHere) && !isCurrentState) {
|
|
23384
|
+
const dated = datedReferences(content);
|
|
23385
|
+
if (dated >= DATED_PROSE_LIMIT) {
|
|
23386
|
+
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.`);
|
|
23387
|
+
}
|
|
23388
|
+
}
|
|
23082
23389
|
const lintKind = ownedLabel(n, "noteType");
|
|
23083
23390
|
if (lintKind) {
|
|
23084
23391
|
const missing = missingSections(lintKind, content);
|
|
@@ -23302,9 +23609,9 @@ async function hygienePasses(trilium, cfg, report, ctx) {
|
|
|
23302
23609
|
if (cfg.memory.threads) {
|
|
23303
23610
|
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
23611
|
for (const n of heavy) {
|
|
23305
|
-
if (!eligible(n))
|
|
23612
|
+
if (!eligible(n) || ownedLabel(n, "threadShape") === "collection")
|
|
23306
23613
|
continue;
|
|
23307
|
-
report.flagged.push(`consolidate: ${n.title} [${n.noteId}] \u2014 ${n.childNoteIds.length} day-children.
|
|
23614
|
+
report.flagged.push(`consolidate: ${n.title} [${n.noteId}] \u2014 ${n.childNoteIds.length} day-children. Move what the thread has established into its domain: timeless knowledge into the information notes, measured state into Current State. The book keeps only its goal and the entries stay as the record, so a successor reads the conclusion instead of walking the history.`);
|
|
23308
23615
|
}
|
|
23309
23616
|
}
|
|
23310
23617
|
if (cfg.knowledge.domains) {
|
|
@@ -23548,9 +23855,8 @@ async function generateDailyLog(trilium, cfg, date) {
|
|
|
23548
23855
|
await trilium.addLabel(logNoteId, "created", date);
|
|
23549
23856
|
return { date, noteId: logNoteId, ...counts, action: "created" };
|
|
23550
23857
|
}
|
|
23551
|
-
var DELETION_CATCHUP_DAYS = 7;
|
|
23552
23858
|
async function catchUpDeletions(trilium, cfg, today) {
|
|
23553
|
-
const windowDays = cfg
|
|
23859
|
+
const windowDays = deletionCatchupDays(cfg);
|
|
23554
23860
|
const empty = { windowDays, coverage: "full", deletionsFound: 0, regenerated: [] };
|
|
23555
23861
|
if (!cfg.root || !cfg.insights.logs)
|
|
23556
23862
|
return empty;
|
|
@@ -23591,6 +23897,11 @@ async function catchUpDeletions(trilium, cfg, today) {
|
|
|
23591
23897
|
var txt = (obj) => ({
|
|
23592
23898
|
content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }]
|
|
23593
23899
|
});
|
|
23900
|
+
var idParams = {
|
|
23901
|
+
id: stringType().optional().describe("The note's id (noteId is accepted as an alias)"),
|
|
23902
|
+
noteId: stringType().optional().describe("Alias of id")
|
|
23903
|
+
};
|
|
23904
|
+
var missingId = (tool) => txt({ error: "missing_id", detail: `${tool}() needs the note's id.`, hint: "Pass id=<noteId> (noteId= works too)." });
|
|
23594
23905
|
var labelOf = (n, name) => n.attributes.find((a) => isOwnedAttribute(n, a) && a.type === "label" && a.name === name)?.value;
|
|
23595
23906
|
async function skim(trilium, ancestorNoteId, opts = {}) {
|
|
23596
23907
|
const { query, kind, limit = 20, includeArchived = false } = opts;
|
|
@@ -23675,12 +23986,7 @@ var txt2 = (obj) => ({
|
|
|
23675
23986
|
var AREAS = ["biography", "goals", "preferences"];
|
|
23676
23987
|
function registerMasterTools(server, trilium, brainRef) {
|
|
23677
23988
|
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.`, {
|
|
23989
|
+
server.tool("master", `Read a master singleton \u2014 biography, goals or preferences \u2014 with its id and relations. section="<heading>" reads one section.`, {
|
|
23684
23990
|
which: enumType(AREAS),
|
|
23685
23991
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23686
23992
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
@@ -23709,27 +24015,18 @@ day's blocks rather than the whole week. outline(id) lists the headings.`, {
|
|
|
23709
24015
|
// src/tools-llm.ts
|
|
23710
24016
|
function registerLlmTools(server, trilium, brainRef) {
|
|
23711
24017
|
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.`, {
|
|
24018
|
+
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
24019
|
which: enumType(["responsibilities", "protocols", "selfcorrection", "diary"]),
|
|
23725
|
-
id: stringType().optional().describe("diary only: the entry id from llm_recall"),
|
|
24020
|
+
id: stringType().optional().describe("diary only: the entry id from llm_recall (noteId is accepted as an alias)"),
|
|
24021
|
+
noteId: stringType().optional().describe("Alias of id"),
|
|
23726
24022
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23727
24023
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
23728
|
-
}, async ({ which, id, section, occurrence }) => {
|
|
24024
|
+
}, async ({ which, id, noteId: idAlias, section, occurrence }) => {
|
|
23729
24025
|
if (which === "diary") {
|
|
23730
|
-
|
|
24026
|
+
const entry = id ?? idAlias;
|
|
24027
|
+
if (!entry)
|
|
23731
24028
|
throw new Error("Reading a diary entry needs its id \u2014 use llm_recall to find one.");
|
|
23732
|
-
return txt(await readFull(trilium,
|
|
24029
|
+
return txt(await readFull(trilium, entry, { section, occurrence }));
|
|
23733
24030
|
}
|
|
23734
24031
|
const noteId = b().llm[which];
|
|
23735
24032
|
if (!noteId) {
|
|
@@ -23755,40 +24052,55 @@ rather than whole. outline(id) lists the headings.`, {
|
|
|
23755
24052
|
}
|
|
23756
24053
|
|
|
23757
24054
|
// src/tools-memory.ts
|
|
24055
|
+
var MEMORY_INDEX_DEFAULT = 30;
|
|
24056
|
+
var MEMORY_INDEX_CHAR_BUDGET = 24000;
|
|
23758
24057
|
function registerMemoryTools(server, trilium, brainRef) {
|
|
23759
24058
|
const b = () => brainRef.config;
|
|
23760
|
-
server.tool("memory", `Read a Memory note
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
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.`, {
|
|
23770
|
-
id: stringType(),
|
|
24059
|
+
server.tool("memory", `Read a Memory note by id \u2014 a thread or a session. A dated thread returns its Context and
|
|
24060
|
+
Resolution plus an index of its [yyyy-mm-dd] children (newest first: the latest 30, fewer if their
|
|
24061
|
+
index passes ~24k characters, unless limit= says otherwise; with the total count), each listed by its addendum blocks;
|
|
24062
|
+
date="yyyy-mm-dd" resolves straight to one day. A collection thread returns its
|
|
24063
|
+
Context plus its titled entries (alphabetical, with a lead). Read a child with memory(<child id>).
|
|
24064
|
+
section="<heading>" reads one section.`, {
|
|
24065
|
+
...idParams,
|
|
23771
24066
|
date: stringType().optional().describe("Thread books only: resolve directly to this day's child note (yyyy-mm-dd)"),
|
|
24067
|
+
limit: numberType().int().positive().optional().describe("Dated threads: how many of the newest day-children to index (default 30)"),
|
|
23772
24068
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23773
24069
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
23774
|
-
}, async ({ id, date, section, occurrence }) => {
|
|
24070
|
+
}, async ({ id: idArg, noteId, date, limit, section, occurrence }) => {
|
|
24071
|
+
const id = idArg ?? noteId;
|
|
24072
|
+
if (!id)
|
|
24073
|
+
return missingId("memory");
|
|
23775
24074
|
const note = await trilium.getNote(id).catch(() => null);
|
|
23776
24075
|
if (!note || labelOf(note, "noteType") !== "thread")
|
|
23777
24076
|
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
24077
|
+
if (isCollectionThread(note)) {
|
|
24078
|
+
const kids = await trilium.searchNotes("#noteType=threadEntry", { ancestorNoteId: id, fastSearch: true, limit: 500 }).catch(() => ({ results: [] }));
|
|
24079
|
+
const entries = await Promise.all(kids.results.filter((c) => c.parentNoteIds.includes(id)).sort((a, z) => a.title.localeCompare(z.title)).map(async (c) => ({
|
|
24080
|
+
id: c.noteId,
|
|
24081
|
+
title: c.title,
|
|
24082
|
+
updated: labelOf(c, "updated") ?? c.dateModified.slice(0, 10),
|
|
24083
|
+
preview: toText(await trilium.getNoteContent(c.noteId).catch(() => ""), 160)
|
|
24084
|
+
})));
|
|
24085
|
+
const full = await readFull(trilium, id, { section, occurrence });
|
|
24086
|
+
return txt({ ...full, shape: "collection", entries });
|
|
24087
|
+
}
|
|
23778
24088
|
if (date) {
|
|
23779
24089
|
const child = await trilium.searchNotes(`#noteType=threadEntry #created='${date}'`, { ancestorNoteId: id, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
23780
24090
|
if (child.results[0])
|
|
23781
24091
|
return txt(await readFull(trilium, child.results[0].noteId, { section, occurrence }));
|
|
23782
24092
|
return txt({ id, title: note.title, kind: "thread", note: `No entry for ${date}.` });
|
|
23783
24093
|
}
|
|
24094
|
+
const shown = limit ?? MEMORY_INDEX_DEFAULT;
|
|
24095
|
+
const totalChildren = note.childNoteIds.length;
|
|
23784
24096
|
const children = await trilium.searchNotes("#noteType=threadEntry", {
|
|
23785
24097
|
ancestorNoteId: id,
|
|
23786
24098
|
fastSearch: true,
|
|
23787
|
-
limit:
|
|
24099
|
+
limit: shown,
|
|
23788
24100
|
orderBy: "dateCreated",
|
|
23789
24101
|
orderDirection: "desc"
|
|
23790
24102
|
}).catch(() => ({ results: [] }));
|
|
23791
|
-
const
|
|
24103
|
+
const indexed = await Promise.all(children.results.map(async (c) => {
|
|
23792
24104
|
const content = await trilium.getNoteContent(c.noteId).catch(() => "");
|
|
23793
24105
|
const blocks = addendumIndex(content);
|
|
23794
24106
|
return {
|
|
@@ -23797,8 +24109,22 @@ of the whole body \u2014 use it on long sessions and consolidated thread Context
|
|
|
23797
24109
|
...blocks.length ? { blocks } : { preview: toText(content, 160) }
|
|
23798
24110
|
};
|
|
23799
24111
|
}));
|
|
24112
|
+
const entries = [];
|
|
24113
|
+
let used = 0;
|
|
24114
|
+
for (const e of indexed) {
|
|
24115
|
+
const size = JSON.stringify(e).length;
|
|
24116
|
+
if (limit === undefined && entries.length && used + size > MEMORY_INDEX_CHAR_BUDGET)
|
|
24117
|
+
break;
|
|
24118
|
+
entries.push(e);
|
|
24119
|
+
used += size;
|
|
24120
|
+
}
|
|
23800
24121
|
const full = await readFull(trilium, id, { section, occurrence });
|
|
23801
|
-
return txt({
|
|
24122
|
+
return txt({
|
|
24123
|
+
...full,
|
|
24124
|
+
totalChildren,
|
|
24125
|
+
children: entries,
|
|
24126
|
+
...totalChildren > entries.length ? { more: `Showing the newest ${entries.length} of ${totalChildren} day-children. Pass limit= for more, or date= for one day.` } : {}
|
|
24127
|
+
});
|
|
23802
24128
|
});
|
|
23803
24129
|
server.tool("memory_recall", "Skim Memory: active threads and recent sessions (ids + previews). An optional query filters threads by title.", { query: stringType().optional(), limit: numberType().optional() }, async ({ query, limit }) => {
|
|
23804
24130
|
const cfg = b();
|
|
@@ -23828,17 +24154,16 @@ of the whole body \u2014 use it on long sessions and consolidated thread Context
|
|
|
23828
24154
|
// src/tools-knowledge.ts
|
|
23829
24155
|
function registerKnowledgeTools(server, trilium, brainRef) {
|
|
23830
24156
|
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.`, {
|
|
23838
|
-
id: stringType(),
|
|
24157
|
+
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.`, {
|
|
24158
|
+
...idParams,
|
|
23839
24159
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23840
24160
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
23841
|
-
}, async ({ id, section, occurrence }) =>
|
|
24161
|
+
}, async ({ id, noteId, section, occurrence }) => {
|
|
24162
|
+
const target = id ?? noteId;
|
|
24163
|
+
if (!target)
|
|
24164
|
+
return missingId("knowledge");
|
|
24165
|
+
return txt(await readFull(trilium, target, { section, occurrence }));
|
|
24166
|
+
});
|
|
23842
24167
|
server.tool("knowledge_recall", "Skim Knowledge. With a domain: that domain's Sources + information notes. Without: user-knowledge notes plus the list of domains.", { query: stringType().optional(), domain: stringType().optional() }, async ({ query, domain }) => {
|
|
23843
24168
|
const cfg = b();
|
|
23844
24169
|
if (domain) {
|
|
@@ -23872,7 +24197,7 @@ section="<heading>" reads one section instead of the whole log.`, {
|
|
|
23872
24197
|
return txt({ date: d, note: "No log for this day." });
|
|
23873
24198
|
return txt(await readFull(trilium, found.results[0].noteId, { section, occurrence }));
|
|
23874
24199
|
});
|
|
23875
|
-
server.tool("insights_recall", "Skim recent change logs (
|
|
24200
|
+
server.tool("insights_recall", "Skim the most recent daily change logs (dates + previews), newest first. limit sets how many days (default 14). For one day's full log use insights(date).", { limit: numberType().optional() }, async ({ limit }) => txt(await skim(trilium, b().insights.logs, { kind: "log", limit: limit ?? 14 })));
|
|
23876
24201
|
}
|
|
23877
24202
|
|
|
23878
24203
|
// src/tools.ts
|
|
@@ -23921,6 +24246,20 @@ function registerTools(server, trilium, brainRef) {
|
|
|
23921
24246
|
const found = await trilium.searchNotes(`#noteType=session #created='${date}'`, { ancestorNoteId: cfg.memory.sessions, fastSearch: true, limit: 1 });
|
|
23922
24247
|
return found.results[0]?.noteId ?? null;
|
|
23923
24248
|
};
|
|
24249
|
+
const ensureGateNote = async (date) => {
|
|
24250
|
+
const existing = await gateNote(date);
|
|
24251
|
+
if (existing)
|
|
24252
|
+
return existing;
|
|
24253
|
+
const cfg = b();
|
|
24254
|
+
if (!cfg.memory.sessions)
|
|
24255
|
+
return null;
|
|
24256
|
+
const created = await trilium.createNote(cfg.memory.sessions, `[${date}]`, contentFor("session", { date, body: "" }));
|
|
24257
|
+
const noteId = created.note.noteId;
|
|
24258
|
+
await trilium.addLabel(noteId, "noteType", "session");
|
|
24259
|
+
await trilium.addLabel(noteId, "created", date);
|
|
24260
|
+
await ensureIcon(trilium, noteId);
|
|
24261
|
+
return noteId;
|
|
24262
|
+
};
|
|
23924
24263
|
const parseGate = (raw) => {
|
|
23925
24264
|
const out = new Map;
|
|
23926
24265
|
for (const pair of (raw ?? "").split(",")) {
|
|
@@ -23933,9 +24272,9 @@ function registerTools(server, trilium, brainRef) {
|
|
|
23933
24272
|
const serializeGate = (m) => [...m.entries()].map(([step, seq]) => `${step}:${seq}`).join(",");
|
|
23934
24273
|
const markStep = async (step, date) => {
|
|
23935
24274
|
const d = checkedDate(date);
|
|
23936
|
-
const noteId = await
|
|
24275
|
+
const noteId = await ensureGateNote(d);
|
|
23937
24276
|
if (!noteId)
|
|
23938
|
-
throw new Error(`Cannot record pre-close step "${step}":
|
|
24277
|
+
throw new Error(`Cannot record pre-close step "${step}": the brain has no Sessions container \u2014 run bootstrap()`);
|
|
23939
24278
|
const note = await trilium.getNote(noteId);
|
|
23940
24279
|
const stored = parseGate(labelOf2(note, "gate"));
|
|
23941
24280
|
const next = Math.max(preCloseSeq, ...[...stored.values()], 0) + 1;
|
|
@@ -23972,11 +24311,11 @@ function registerTools(server, trilium, brainRef) {
|
|
|
23972
24311
|
};
|
|
23973
24312
|
};
|
|
23974
24313
|
const applyIcon = async (noteId, icon) => {
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
|
|
23978
|
-
if (!cls)
|
|
24314
|
+
const cls = icon ? normalizeIcon(icon) : "";
|
|
24315
|
+
if (!cls) {
|
|
24316
|
+
await ensureIcon(trilium, noteId);
|
|
23979
24317
|
return;
|
|
24318
|
+
}
|
|
23980
24319
|
await trilium.updateLabelValue(noteId, "iconClass", cls).catch(() => null);
|
|
23981
24320
|
return cls;
|
|
23982
24321
|
};
|
|
@@ -24022,26 +24361,13 @@ ${block}` }));
|
|
|
24022
24361
|
const noteId = created.note.noteId;
|
|
24023
24362
|
await trilium.addLabel(noteId, "noteType", "threadEntry");
|
|
24024
24363
|
await trilium.addLabel(noteId, "created", d);
|
|
24364
|
+
await ensureIcon(trilium, noteId);
|
|
24025
24365
|
return { noteId, action: "created" };
|
|
24026
24366
|
}
|
|
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.`, {
|
|
24367
|
+
function collectionAppendRefusal(book) {
|
|
24368
|
+
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.`);
|
|
24369
|
+
}
|
|
24370
|
+
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
24371
|
depth: enumType(["digest", "full"]).optional().describe('Singleton detail: "digest" (default \u2014 section headings + preview + size) or "full" (every singleton inline; token-heavy)')
|
|
24046
24372
|
}, async ({ depth }) => {
|
|
24047
24373
|
const cfg = b();
|
|
@@ -24056,7 +24382,7 @@ sessions need one section, not six documents.`, {
|
|
|
24056
24382
|
deleted: [],
|
|
24057
24383
|
flagged: [`sweep failed: ${e}`],
|
|
24058
24384
|
dryRun: false,
|
|
24059
|
-
policy: { dormantAfterDays: cfg.policy.dormantAfterDays, archiveDormantAfterDays: cfg.policy.archiveDormantAfterDays, staleAfterDays: cfg.policy.staleAfterDays, deletionCatchupDays: cfg
|
|
24385
|
+
policy: { dormantAfterDays: cfg.policy.dormantAfterDays, archiveDormantAfterDays: cfg.policy.archiveDormantAfterDays, staleAfterDays: cfg.policy.staleAfterDays, deletionCatchupDays: deletionCatchupDays(cfg) }
|
|
24060
24386
|
})),
|
|
24061
24387
|
buildDigest(trilium, cfg, { depth: depth ?? "digest" })
|
|
24062
24388
|
]);
|
|
@@ -24074,6 +24400,7 @@ sessions need one section, not six documents.`, {
|
|
|
24074
24400
|
diaryNoteId = created.note.noteId;
|
|
24075
24401
|
await trilium.addLabel(diaryNoteId, "noteType", "diary");
|
|
24076
24402
|
await trilium.addLabel(diaryNoteId, "created", todayStr);
|
|
24403
|
+
await ensureIcon(trilium, diaryNoteId);
|
|
24077
24404
|
}
|
|
24078
24405
|
if (diaryNoteId) {
|
|
24079
24406
|
const content = await trilium.getNoteContent(diaryNoteId).catch(() => "");
|
|
@@ -24092,10 +24419,7 @@ sessions need one section, not six documents.`, {
|
|
|
24092
24419
|
sessionPreview = toText(content, 200);
|
|
24093
24420
|
newDay = !/<h2(?:\s[^>]*)?>\s*Addendum/i.test(content);
|
|
24094
24421
|
} else {
|
|
24095
|
-
|
|
24096
|
-
sessionNoteId = created.note.noteId;
|
|
24097
|
-
await trilium.addLabel(sessionNoteId, "noteType", "session");
|
|
24098
|
-
await trilium.addLabel(sessionNoteId, "created", todayStr);
|
|
24422
|
+
sessionNoteId = await ensureGateNote(todayStr);
|
|
24099
24423
|
newDay = true;
|
|
24100
24424
|
}
|
|
24101
24425
|
} catch {}
|
|
@@ -24131,45 +24455,16 @@ sessions need one section, not six documents.`, {
|
|
|
24131
24455
|
dormantThreads: digest.reviewQueue.length ? { note: "These threads went dormant from inactivity. Mention them if relevant; revise() or resolve() to act.", items: digest.reviewQueue } : [],
|
|
24132
24456
|
lastSession: digest.lastSession ?? null,
|
|
24133
24457
|
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
|
|
24458
|
+
...newDay ? { newDay: true, newDayHint: "First session of the day \u2014 call day() for the sweep payload (previous session + log + changes in one call)." } : {},
|
|
24135
24459
|
...(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
24460
|
hygiene: { scanned: hygiene.scanned, fixed: hygiene.fixed.length, transitions: hygiene.transitions, flagged: hygiene.flagged, ...hygiene.suppressed ? { suppressed: hygiene.suppressed } : {} }
|
|
24137
24461
|
});
|
|
24138
24462
|
});
|
|
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.`, {
|
|
24463
|
+
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.
|
|
24464
|
+
|
|
24465
|
+
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
24466
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
24172
|
-
full: booleanType().optional().describe("
|
|
24467
|
+
full: booleanType().optional().describe("Inline every singleton's content and the diary body (default: stubs only)"),
|
|
24173
24468
|
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
24469
|
light: booleanType().optional().describe("Deprecated \u2014 light is now the default; accepted for compatibility and ignored")
|
|
24175
24470
|
}, async ({ date, full, scope }) => {
|
|
@@ -24261,18 +24556,7 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
24261
24556
|
]
|
|
24262
24557
|
});
|
|
24263
24558
|
});
|
|
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 () => {
|
|
24559
|
+
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
24560
|
const cfg = b();
|
|
24277
24561
|
if (!cfg.root)
|
|
24278
24562
|
return txt3({ status: "uninitialized", action: "Run bootstrap first." });
|
|
@@ -24297,28 +24581,10 @@ outright rather than padding it; two honest paragraphs beat eight forced ones.`,
|
|
|
24297
24581
|
]
|
|
24298
24582
|
});
|
|
24299
24583
|
});
|
|
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.`, {
|
|
24584
|
+
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
24585
|
summary: stringType().describe("What happened this session \u2014 factual, concise prose"),
|
|
24320
24586
|
title: stringType().optional().describe("Short session title \u2014 appears as an <h2> heading above Summary"),
|
|
24321
|
-
identity: stringType().describe(`Identification line
|
|
24587
|
+
identity: stringType().describe(`Identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" \u2014 rendered as the block's h3 (required)`),
|
|
24322
24588
|
learned: arrayType(stringType()).optional().describe("Durable things learned (also remember() them as knowledge)"),
|
|
24323
24589
|
icon: stringType().optional().describe("Display icon for the session note \u2014 a boxicons class or bare name; normalized server-side"),
|
|
24324
24590
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
@@ -24435,9 +24701,7 @@ ${contentBlock}` }));
|
|
|
24435
24701
|
...warnings.length ? { sanitized: warnings } : {}
|
|
24436
24702
|
});
|
|
24437
24703
|
});
|
|
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.`, {
|
|
24704
|
+
server.tool("backup", `Named database snapshot. close() already backs up; use this before a large restructure.`, {
|
|
24441
24705
|
name: stringType().optional().describe("Backup name without extension (default: brainllm-{today}). Use a descriptive name for milestones.")
|
|
24442
24706
|
}, async ({ name }) => {
|
|
24443
24707
|
const d = today();
|
|
@@ -24449,19 +24713,9 @@ close() already triggers a backup automatically \u2014 use this for on-demand mi
|
|
|
24449
24713
|
return txt3({ ok: false, backup: backupName, backupStatus: "failed", error: error instanceof Error ? error.message : String(error), date: d });
|
|
24450
24714
|
}
|
|
24451
24715
|
});
|
|
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.`, {
|
|
24716
|
+
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
24717
|
body: stringType().describe("What to record \u2014 first-person prose, honest and unfiltered"),
|
|
24464
|
-
identity: stringType().describe(`Identification line
|
|
24718
|
+
identity: stringType().describe(`Identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" \u2014 rendered as the block's h3 (required)`),
|
|
24465
24719
|
icon: stringType().optional().describe("Display icon for the day's entry \u2014 a boxicons class or bare name; normalized server-side"),
|
|
24466
24720
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)")
|
|
24467
24721
|
}, async ({ body, identity, icon, date }) => {
|
|
@@ -24506,36 +24760,28 @@ ${html}` }));
|
|
|
24506
24760
|
await markStep("diary", d);
|
|
24507
24761
|
return txt3({ action: "created", noteId, date: d, location: locationLabel("diary"), ...iconSet ? { icon: iconSet } : {}, ...warnings.length ? { sanitized: warnings } : {} });
|
|
24508
24762
|
});
|
|
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.`, {
|
|
24763
|
+
server.tool("remember", `Store something the moment it matters; the server owns placement, labels and dedup.
|
|
24764
|
+
|
|
24765
|
+
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).
|
|
24766
|
+
|
|
24767
|
+
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
24768
|
kind: enumType(Kinds).describe("What kind of memory this is"),
|
|
24525
24769
|
title: stringType().optional().describe("Title \u2014 collection kinds (thread/knowledge/information sub-category); ignored for singletons & sources"),
|
|
24526
24770
|
body: stringType().optional().describe("Content: plain text, markdown, or HTML"),
|
|
24527
24771
|
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
|
-
|
|
24772
|
+
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)'),
|
|
24773
|
+
thread: stringType().optional().describe(`kind="threadEntry" only: the collection thread's id \u2014 adds a titled child to it`),
|
|
24774
|
+
identity: stringType().optional().describe('Appending to a dated thread: the identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" (required)'),
|
|
24529
24775
|
domain: stringType().optional().describe("knowledge: the domain name for information/sources (auto-created complete with its Sources note)"),
|
|
24530
24776
|
revision: arrayType(objectType({
|
|
24531
24777
|
source: stringType().describe("Must exactly match how the source is introduced in the Sources list \u2014 this is the upsert key"),
|
|
24532
24778
|
marker: stringType().describe('"\u2747\uFE0F" (discovered/credible) or "\u2705" (used)'),
|
|
24533
24779
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
24534
|
-
})).optional().describe("kind=sources
|
|
24780
|
+
})).optional().describe("kind=sources: upsert Revision rows by source name"),
|
|
24535
24781
|
topics: arrayType(stringType()).optional().describe("Topic tags \u2014 slugged server-side"),
|
|
24536
24782
|
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
|
|
24783
|
+
mandate: booleanType().optional().describe("kind=information: flag the note as a standing brief a session must follow (#mandate)"),
|
|
24784
|
+
mustCreate: booleanType().optional().describe("Refuse instead of adopting a note that already has this title"),
|
|
24539
24785
|
strict: booleanType().optional().describe("Refuse the write when the body needs structural repair (unclosed tags, <br> runs) instead of accepting the repaired form"),
|
|
24540
24786
|
connect: arrayType(objectType({
|
|
24541
24787
|
relation: enumType(RelationTypes),
|
|
@@ -24543,7 +24789,7 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
24543
24789
|
})).optional().describe("Relations to wire from this note in the same call \u2014 same semantics as connect() (idempotent, worksWith wired both ways)"),
|
|
24544
24790
|
icon: stringType().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name ("brain"); normalized server-side'),
|
|
24545
24791
|
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 }) => {
|
|
24792
|
+
}, async ({ kind, title, body, goal, shape, thread, identity, domain, revision, topics, supersedes, mandate, mustCreate, strict, connect: connectRels, icon, date }) => {
|
|
24547
24793
|
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
24794
|
const opts = { domain, topics, date, ...mandate ? { mandate: true } : {} };
|
|
24549
24795
|
const d = checkedDate(date);
|
|
@@ -24733,8 +24979,26 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
24733
24979
|
return err("rejected_kind", "Log notes are auto-generated by close() and cannot be written manually.");
|
|
24734
24980
|
if (kind === "domain")
|
|
24735
24981
|
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
|
-
|
|
24982
|
+
if (kind === "threadEntry") {
|
|
24983
|
+
const book = thread ? await trilium.getNote(thread).catch(() => null) : null;
|
|
24984
|
+
if (!book || ownedLabel(book, "noteType") !== "thread" || !isCollectionThread(book))
|
|
24985
|
+
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).');
|
|
24986
|
+
const { title: entryTitle } = normalizeTitle(title ?? "");
|
|
24987
|
+
if (!entryTitle)
|
|
24988
|
+
return err("missing_param", "A collection entry needs a title.", 'Add title="<entry name>".');
|
|
24989
|
+
const siblings = await trilium.searchNotes("#noteType=threadEntry", { ancestorNoteId: book.noteId, fastSearch: true, limit: 500 }).then((r) => r.results).catch(() => []);
|
|
24990
|
+
const clash = siblings.find((s) => s.parentNoteIds.includes(book.noteId) && sameTitle(s.title, entryTitle));
|
|
24991
|
+
if (clash)
|
|
24992
|
+
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.`);
|
|
24993
|
+
const created = await trilium.createNote(book.noteId, entryTitle, contentFor("threadEntry", { date: d, body: html }));
|
|
24994
|
+
const eid = created.note.noteId;
|
|
24995
|
+
await trilium.addLabel(eid, "noteType", "threadEntry");
|
|
24996
|
+
await trilium.addLabel(eid, "created", d);
|
|
24997
|
+
await trilium.updateLabelValue(book.noteId, "updated", d);
|
|
24998
|
+
const connected = await wireRequested(eid);
|
|
24999
|
+
const iconSet = await applyIcon(eid, icon);
|
|
25000
|
+
return txt3({ action: "created", noteId: eid, kind, title: entryTitle, thread: book.title, ...connected.length ? { connected } : {}, ...iconSet ? { icon: iconSet } : {}, ...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {} });
|
|
25001
|
+
}
|
|
24738
25002
|
const { title: cleanTitle } = normalizeTitle(title ?? "");
|
|
24739
25003
|
if (!cleanTitle)
|
|
24740
25004
|
return err("missing_param", `kind="${kind}" requires a title.`, 'Add title="<note title>" to your call.');
|
|
@@ -24748,6 +25012,8 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
24748
25012
|
const block = identity && !leadingIdentification(html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
24749
25013
|
${html}` : html;
|
|
24750
25014
|
if (kind === "thread") {
|
|
25015
|
+
if (isCollectionThread(existing))
|
|
25016
|
+
return collectionAppendRefusal(existing);
|
|
24751
25017
|
const entry = await appendThreadEntry(existing.noteId, block, d);
|
|
24752
25018
|
if (entry.action === "already_written") {
|
|
24753
25019
|
return txt3({ action: "already_written", noteId: existing.noteId, entryId: entry.noteId, kind, title: existing.title });
|
|
@@ -24794,6 +25060,8 @@ ${block}`), d);
|
|
|
24794
25060
|
for (const l of labelPlan(kind, opts, d)) {
|
|
24795
25061
|
await trilium.addLabel(nid, l.name, l.value, l.inheritable ?? false);
|
|
24796
25062
|
}
|
|
25063
|
+
if (kind === "thread" && shape === "collection")
|
|
25064
|
+
await trilium.addLabel(nid, "threadShape", "collection");
|
|
24797
25065
|
const wired = [];
|
|
24798
25066
|
const extraRelations = [];
|
|
24799
25067
|
if (supersedes) {
|
|
@@ -24824,23 +25092,7 @@ ${block}`), d);
|
|
|
24824
25092
|
...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {}
|
|
24825
25093
|
});
|
|
24826
25094
|
});
|
|
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.`, {
|
|
25095
|
+
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
25096
|
query: stringType().describe("What to find \u2014 natural phrasing is fine"),
|
|
24845
25097
|
kinds: arrayType(enumType(Kinds)).optional().describe("Restrict to these kinds"),
|
|
24846
25098
|
domain: stringType().optional().describe("Restrict to a knowledge domain"),
|
|
@@ -24999,11 +25251,7 @@ a doubly-escaped tag", "which cite a 2026 date"). Backslashes must be escaped.`,
|
|
|
24999
25251
|
...results.length === 0 ? noMatch : {}
|
|
25000
25252
|
});
|
|
25001
25253
|
});
|
|
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.`, {
|
|
25254
|
+
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
25255
|
name: stringType().describe("Domain, topic, or project name"),
|
|
25008
25256
|
includeArchived: booleanType().optional().describe("Include archived/resolved items (default: false)")
|
|
25009
25257
|
}, async ({ name, includeArchived }) => {
|
|
@@ -25067,16 +25315,7 @@ Use recall() for keyword or full-text search instead.`, {
|
|
|
25067
25315
|
...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
25316
|
});
|
|
25069
25317
|
});
|
|
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.`, {
|
|
25318
|
+
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
25319
|
ids: arrayType(stringType()).min(1).max(10).describe("Note ids to read \u2014 one body per id, one round trip (cap 10)")
|
|
25081
25320
|
}, async ({ ids }) => {
|
|
25082
25321
|
const notes = await Promise.all(ids.map(async (id) => {
|
|
@@ -25098,42 +25337,18 @@ efficient path \u2014 read() returns whole bodies.`, {
|
|
|
25098
25337
|
}));
|
|
25099
25338
|
return txt3({ count: notes.length, notes });
|
|
25100
25339
|
});
|
|
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.`, {
|
|
25340
|
+
server.tool("revise", `Edit a note by id. A revision is taken first; "Last updated" lines are bumped on content writes.
|
|
25341
|
+
|
|
25342
|
+
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.
|
|
25343
|
+
|
|
25344
|
+
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, and is taken literally: a Windows path's backslash is one backslash, never doubled.`, {
|
|
25130
25345
|
noteId: stringType().describe("Note to update"),
|
|
25131
25346
|
body: stringType().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
|
|
25132
25347
|
title: stringType().optional().describe("New title (normalized server-side)"),
|
|
25133
25348
|
section: stringType().optional().describe("Target a section by heading text (h2/h3/h4, in that order); omit for whole-note append/replace"),
|
|
25134
25349
|
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(
|
|
25350
|
+
strict: booleanType().optional().describe("Refuse (note untouched) on a section= miss or a body needing structural repair, instead of writing a new section"),
|
|
25351
|
+
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
25352
|
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
25353
|
nth: numberType().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
|
|
25139
25354
|
edits: arrayType(objectType({
|
|
@@ -25141,7 +25356,7 @@ target \u2014 which is what it used to do, emptying the target while reporting m
|
|
|
25141
25356
|
body: stringType().describe("Raw replacement string"),
|
|
25142
25357
|
nth: numberType().int().positive().optional().describe("Replace only the Nth occurrence")
|
|
25143
25358
|
})).optional().describe("Several find/replace surgeries applied in order against one read and one write. Mutually exclusive with find=."),
|
|
25144
|
-
identity: stringType().optional().describe(
|
|
25359
|
+
identity: stringType().optional().describe('Appending to a dated thread: the identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" (required)'),
|
|
25145
25360
|
icon: stringType().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name; normalized server-side'),
|
|
25146
25361
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
25147
25362
|
}, async ({ noteId, body, title, section, occurrence, mode, find, nth, edits, identity, icon, date, strict }) => {
|
|
@@ -25382,6 +25597,8 @@ target \u2014 which is what it used to do, emptying the target while reporting m
|
|
|
25382
25597
|
finalContent = bumpLastUpdated(html, d).html;
|
|
25383
25598
|
await trilium.updateNoteContent(noteId, finalContent);
|
|
25384
25599
|
} else if (noteKind === "thread") {
|
|
25600
|
+
if (isCollectionThread(note))
|
|
25601
|
+
return collectionAppendRefusal(note);
|
|
25385
25602
|
if (!identity && !leadingIdentification(html)) {
|
|
25386
25603
|
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
25604
|
}
|
|
@@ -25432,9 +25649,7 @@ ${block}`), d).html;
|
|
|
25432
25649
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25433
25650
|
});
|
|
25434
25651
|
});
|
|
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.`, {
|
|
25652
|
+
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
25653
|
noteId: stringType().describe("The thread / note to complete"),
|
|
25439
25654
|
outcome: stringType().describe("The resolution \u2014 substantive, standalone prose"),
|
|
25440
25655
|
status: enumType(["resolved", "superseded"]).optional().describe("Terminal status (default: resolved)"),
|
|
@@ -25470,23 +25685,7 @@ status, and archive it in place (it stays where it is, excluded from default rec
|
|
|
25470
25685
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25471
25686
|
});
|
|
25472
25687
|
});
|
|
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.`, {
|
|
25688
|
+
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
25689
|
noteId: stringType().describe("Note to split \u2014 the source whose sections are moving out"),
|
|
25491
25690
|
sections: arrayType(stringType()).min(1).max(20).describe("Section headings (text) to move out \u2014 nested sub-sections go with their parent"),
|
|
25492
25691
|
into: stringType().describe("Title of the new note that receives the moved sections"),
|
|
@@ -25544,9 +25743,7 @@ ${result.html}`);
|
|
|
25544
25743
|
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
25744
|
});
|
|
25546
25745
|
});
|
|
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.`, {
|
|
25746
|
+
server.tool("withdraw", `Return an archived or resolved thread to active and note the withdrawal on the book. Use when closed work resurfaces.`, {
|
|
25550
25747
|
noteId: stringType().describe("The archived/resolved thread to withdraw"),
|
|
25551
25748
|
reason: stringType().optional().describe("Why it was withdrawn \u2014 written as an addendum"),
|
|
25552
25749
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
@@ -25583,13 +25780,7 @@ Use when a resolved or dormant thread resurfaces as live work.`, {
|
|
|
25583
25780
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25584
25781
|
});
|
|
25585
25782
|
});
|
|
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.`, {
|
|
25783
|
+
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
25784
|
noteId: stringType().describe("Note to edit"),
|
|
25594
25785
|
name: stringType().describe("Label name, no # prefix (e.g. status, domain, topic, created)"),
|
|
25595
25786
|
value: stringType().optional().describe("New value \u2014 required unless remove=true"),
|
|
@@ -25597,6 +25788,11 @@ setting updated itself.`, {
|
|
|
25597
25788
|
}, async ({ noteId, name, value, remove }) => {
|
|
25598
25789
|
if (isContainer(b(), noteId))
|
|
25599
25790
|
return err("protected_note", `Note ${noteId} is a container \u2014 its labels cannot be edited directly.`);
|
|
25791
|
+
if (name === "iconClass" && remove) {
|
|
25792
|
+
const n = await trilium.getNote(noteId).catch(() => null);
|
|
25793
|
+
if (n && !ICON_EXEMPT.has(ownedLabel(n, "noteType") ?? ""))
|
|
25794
|
+
return err("protected_label", "Every note except a log carries an icon.", 'Change it instead: label(noteId, "iconClass", value="bx bx-<name>").');
|
|
25795
|
+
}
|
|
25600
25796
|
const noteForGuard = name === "noteType" ? await trilium.getNote(noteId).catch(() => null) : null;
|
|
25601
25797
|
if (name === "noteType") {
|
|
25602
25798
|
if (!noteForGuard)
|
|
@@ -25716,38 +25912,15 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
25716
25912
|
}
|
|
25717
25913
|
}
|
|
25718
25914
|
});
|
|
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.`, {
|
|
25915
|
+
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
25916
|
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:
|
|
25917
|
+
subject: stringType().optional().describe("A fact in prose \u2014 returns notes asserting about it however phrased (instead of pattern=)"),
|
|
25918
|
+
staleAfterDays: numberType().optional().describe("With pattern: also report values held in exactly one note untouched N+ days (staleSingles)"),
|
|
25746
25919
|
domain: stringType().optional().describe("Restrict to one knowledge domain"),
|
|
25747
25920
|
kinds: arrayType(enumType(Kinds)).optional().describe("Restrict to these kinds"),
|
|
25748
25921
|
includeArchived: booleanType().optional().describe("Include archived notes (default false)"),
|
|
25749
25922
|
limit: numberType().optional().describe("Max notes to examine (default 60)"),
|
|
25750
|
-
fast: booleanType().optional().describe("
|
|
25923
|
+
fast: booleanType().optional().describe("Use Trilium's faster but lossy %= pre-filter (default: scan every in-scope note)")
|
|
25751
25924
|
}, async ({ pattern, subject, staleAfterDays, domain, kinds, includeArchived, limit, fast }) => {
|
|
25752
25925
|
if (!pattern && !subject)
|
|
25753
25926
|
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 +26054,7 @@ anyway when scope is wide and speed matters more than completeness.`, {
|
|
|
25881
26054
|
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
26055
|
});
|
|
25883
26056
|
});
|
|
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.`, {
|
|
26057
|
+
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
26058
|
noteId: stringType().describe("Note to outline")
|
|
25901
26059
|
}, async ({ noteId }) => {
|
|
25902
26060
|
const note = await trilium.getNote(noteId);
|
|
@@ -25943,22 +26101,7 @@ read for oversized notes, where outline() alone assumes you already know which s
|
|
|
25943
26101
|
...headings.length === 0 ? { note: "No h2\u2013h4 headings \u2014 section= would append a new section rather than match anything." } : {}
|
|
25944
26102
|
});
|
|
25945
26103
|
});
|
|
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.`, {
|
|
26104
|
+
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
26105
|
noteId: stringType().describe("Note to inspect"),
|
|
25963
26106
|
content: booleanType().optional().describe("Include the note's raw body content (default: false)"),
|
|
25964
26107
|
section: stringType().optional().describe("With content=true, return only this heading's section rather than the whole body"),
|
|
@@ -26071,26 +26214,9 @@ miss is diagnosed in the same call rather than in three more.`, {
|
|
|
26071
26214
|
await trilium.addLabel(created.note.noteId, "iconClass", "bx bx-check-shield").catch(() => null);
|
|
26072
26215
|
return created.note.noteId;
|
|
26073
26216
|
};
|
|
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.`, {
|
|
26217
|
+
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.
|
|
26218
|
+
|
|
26219
|
+
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
26220
|
assertion: stringType().optional().describe('The claim in plain words, e.g. "the parse pipeline runs before validation" \u2014 also the dedup key'),
|
|
26095
26221
|
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
26222
|
noteId: stringType().optional().describe("The note this claim is made in \u2014 wired ~derivedFrom so the claim and its source stay linked"),
|
|
@@ -26202,20 +26328,7 @@ broken ones, so staleness arrives as a maintenance finding instead of a surprise
|
|
|
26202
26328
|
...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
26329
|
});
|
|
26204
26330
|
});
|
|
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.`, {
|
|
26331
|
+
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
26332
|
noteId: stringType().describe("Note to diff"),
|
|
26220
26333
|
revisionId: stringType().optional().describe("Compare against this revision (default: the most recent one)"),
|
|
26221
26334
|
context: numberType().int().min(0).max(10).optional().describe("Unchanged lines to show around each change (default: 1)")
|
|
@@ -26275,15 +26388,7 @@ HTML, so a formatting-only change is a real difference and shows as one.`, {
|
|
|
26275
26388
|
revisions: index
|
|
26276
26389
|
});
|
|
26277
26390
|
});
|
|
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().`, {
|
|
26391
|
+
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
26392
|
noteId: stringType().describe("Owning note"),
|
|
26288
26393
|
title: stringType().describe("Attachment title \u2014 the upsert/read key on this note"),
|
|
26289
26394
|
content: stringType().optional().describe("Content to write (text, or base64 for binary). Omit to read the attachment instead."),
|
|
@@ -26329,9 +26434,7 @@ inspect(noteId); remove with detach().`, {
|
|
|
26329
26434
|
throw error;
|
|
26330
26435
|
}
|
|
26331
26436
|
});
|
|
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.`, {
|
|
26437
|
+
server.tool("detach", `Remove an attachment by attachmentId, or by noteId + title. Permanent; retry-safe.`, {
|
|
26335
26438
|
attachmentId: stringType().optional().describe("The attachment to remove"),
|
|
26336
26439
|
noteId: stringType().optional().describe("Owning note \u2014 used with title when the id isn't at hand"),
|
|
26337
26440
|
title: stringType().optional().describe("Attachment title on noteId")
|
|
@@ -26355,16 +26458,7 @@ an already-removed target returns cleanly instead of erroring.`, {
|
|
|
26355
26458
|
}
|
|
26356
26459
|
return txt3({ ok: true, removed: id, ...noteId ? { noteId, title } : {} });
|
|
26357
26460
|
});
|
|
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 () => {
|
|
26461
|
+
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
26462
|
const cfg = b();
|
|
26369
26463
|
if (!cfg.root)
|
|
26370
26464
|
return txt3({ error: "BrainLLM not bootstrapped \u2014 run bootstrap." });
|
|
@@ -26410,33 +26504,7 @@ Returns note IDs, titles, kinds, and content snippets so you can identify what t
|
|
|
26410
26504
|
...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
26505
|
});
|
|
26412
26506
|
});
|
|
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.`, {
|
|
26507
|
+
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: structural lint reads at most 40 notes per run and rotates through the rest day by day, so a whole-brain lint spans several runs.`, {
|
|
26440
26508
|
deep: booleanType().optional().describe("Deep pass: stale-review + orphan/sink + structural lint + duplicate titles across Memory/Threads and Knowledge (default: false)"),
|
|
26441
26509
|
dryRun: booleanType().optional().describe("Report what would change without changing it"),
|
|
26442
26510
|
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 +26524,7 @@ coverage names any pass that hit a cap, so a short list is never mistaken for a
|
|
|
26456
26524
|
await markStep("maintain");
|
|
26457
26525
|
return txt3(report);
|
|
26458
26526
|
});
|
|
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().`, {
|
|
26527
|
+
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
26528
|
noteId: stringType().describe("Note to forget"),
|
|
26464
26529
|
reason: stringType().optional().describe("Why \u2014 recorded in the note before archiving"),
|
|
26465
26530
|
hard: booleanType().optional().describe("Permanently delete instead of archive")
|
|
@@ -26505,11 +26570,7 @@ re-wire with connect() first). To undo an archive, use recover().`, {
|
|
|
26505
26570
|
const relations = relationSnippet(note);
|
|
26506
26571
|
return txt3({ ok: true, archived: noteId, title: note.title, ...relations ? { relations } : {} });
|
|
26507
26572
|
});
|
|
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.`, {
|
|
26573
|
+
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
26574
|
noteId: stringType().describe("The archived or resolved note to restore"),
|
|
26514
26575
|
reason: stringType().optional().describe("Why it was recovered \u2014 written as an addendum"),
|
|
26515
26576
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
@@ -26543,17 +26604,7 @@ prior snapshot. For notes deleted from Trilium entirely (not just archived), use
|
|
|
26543
26604
|
...warnings.length ? { sanitized: warnings } : {}
|
|
26544
26605
|
});
|
|
26545
26606
|
});
|
|
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.`, {
|
|
26607
|
+
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
26608
|
kind: enumType(Kinds).describe("The content kind to serve the canonical structure for")
|
|
26558
26609
|
}, async ({ kind }) => {
|
|
26559
26610
|
const rule = structureRuleFor(kind);
|
|
@@ -26568,22 +26619,17 @@ its siblings, and a note that satisfies the skeleton can still be the odd one ou
|
|
|
26568
26619
|
...rule ? { structure: rule.structure, rules: rule.rules } : { note: "No bespoke structure for this kind \u2014 server meta line + body." },
|
|
26569
26620
|
skeleton,
|
|
26570
26621
|
conventions: [
|
|
26571
|
-
"Headings h2\u2013h4 only
|
|
26572
|
-
"
|
|
26573
|
-
"
|
|
26574
|
-
"
|
|
26575
|
-
"
|
|
26622
|
+
"Headings h2\u2013h4 only (h1 is the title); only headings that earn their place \u2014 depth comes from tables, lists and emphasis.",
|
|
26623
|
+
"Titles: at most 4 words, no dates or run numbers (they defeat dedup-by-title).",
|
|
26624
|
+
"Timeless kinds carry no state, version or decision history: state goes to the domain's Current State note, history to thread entries and sessions.",
|
|
26625
|
+
"One sentence is enough if it says the thing; drop what is rarely relevant.",
|
|
26626
|
+
"Merge, don't stack: only sessions, diary, logs and dated thread entries are append-only records.",
|
|
26627
|
+
"Match your siblings' structure; improve a pattern everywhere or nowhere.",
|
|
26628
|
+
"Every note carries an icon except logs: the kind default is set for you; pass icon= to choose a fitting one."
|
|
26576
26629
|
]
|
|
26577
26630
|
});
|
|
26578
26631
|
});
|
|
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.`, {
|
|
26632
|
+
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
26633
|
noteId: stringType().optional().describe("Center the graph on this note's neighborhood instead of the whole brain"),
|
|
26588
26634
|
depth: numberType().optional().describe("Neighborhood hops when noteId is given (default: 2)"),
|
|
26589
26635
|
includeArchived: booleanType().optional().describe("Include archived notes (default: false)")
|
|
@@ -26678,19 +26724,12 @@ whole-brain view.`, {
|
|
|
26678
26724
|
mermaid
|
|
26679
26725
|
});
|
|
26680
26726
|
});
|
|
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.`, {
|
|
26727
|
+
server.tool("day", `The new-day sweep in one call: whether today is fresh (no addendum blocks in today's session
|
|
26728
|
+
note), the previous session in full, that day's log, and the notes touched since. Present what the
|
|
26729
|
+
touched notes evidence in the first message.
|
|
26730
|
+
|
|
26731
|
+
recap=true instead returns every addendum block written today across the session note, the diary
|
|
26732
|
+
and every thread day-child, in time order with its identification line.`, {
|
|
26694
26733
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
26695
26734
|
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
26735
|
}, async ({ date, recap }) => {
|
|
@@ -26778,37 +26817,19 @@ exception.`, {
|
|
|
26778
26817
|
}));
|
|
26779
26818
|
} catch {}
|
|
26780
26819
|
}
|
|
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
26820
|
return txt3({
|
|
26789
26821
|
date: todayStr,
|
|
26790
26822
|
newDay,
|
|
26791
|
-
month: monthName,
|
|
26792
26823
|
lastSession,
|
|
26793
26824
|
previousLog,
|
|
26794
26825
|
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
26826
|
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."
|
|
26827
|
+
"Skim lastSession, previousLog and changes for evidenced outputs \u2014 never plausibility.",
|
|
26828
|
+
"Present what moved in the first message."
|
|
26800
26829
|
]
|
|
26801
26830
|
});
|
|
26802
26831
|
});
|
|
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.`, {
|
|
26832
|
+
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
26833
|
includeArchived: booleanType().optional().describe("Include archived/resolved notes (default: false)")
|
|
26813
26834
|
}, async ({ includeArchived }) => {
|
|
26814
26835
|
const cfg = b();
|
|
@@ -26885,25 +26906,7 @@ inventory.`, {
|
|
|
26885
26906
|
const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
|
|
26886
26907
|
return txt3({ total, areas });
|
|
26887
26908
|
});
|
|
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.`, {
|
|
26909
|
+
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
26910
|
area: enumType(["master", "llm", "memory", "knowledge", "insights"]).optional().describe("Zoom into one surface instead of all five"),
|
|
26908
26911
|
includeArchived: booleanType().optional().describe("Include archived/resolved notes (default: false)")
|
|
26909
26912
|
}, async ({ area, includeArchived }) => {
|
|
@@ -27007,12 +27010,7 @@ Pass area= to zoom into one surface and get its full detail.`, {
|
|
|
27007
27010
|
...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
|
|
27008
27011
|
});
|
|
27009
27012
|
});
|
|
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 () => {
|
|
27013
|
+
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
27014
|
if (b().root) {
|
|
27017
27015
|
try {
|
|
27018
27016
|
const existing = await trilium.getNote(b().root);
|
|
@@ -27430,7 +27428,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27430
27428
|
const note = await trilium.getNote(noteId);
|
|
27431
27429
|
return txt4(await trilium.getNoteContentResult(noteId, note.type));
|
|
27432
27430
|
});
|
|
27433
|
-
server.tool("create_note",
|
|
27431
|
+
server.tool("create_note", 'Create a note at an explicit parent. Types: text/code/book/canvas/mermaid/relationMap/render/search/file/image. The note gets no #noteType, icon or placement, so core tools cannot see it until it is typed with label(id, "noteType", <kind>); use remember() for a memory.', {
|
|
27434
27432
|
parentNoteId: stringType(),
|
|
27435
27433
|
title: stringType(),
|
|
27436
27434
|
content: stringType(),
|
|
@@ -27447,7 +27445,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27447
27445
|
throw error;
|
|
27448
27446
|
}
|
|
27449
27447
|
});
|
|
27450
|
-
server.tool("update_note_content", "Replace a note's full content. Binary input must use encoding=base64; text remains UTF-8.", { noteId: stringType(), content: stringType(), mime: stringType().optional(), encoding: enumType(["auto", "text", "base64"]).optional() }, async ({ noteId, content, mime, encoding }) => {
|
|
27448
|
+
server.tool("update_note_content", "Replace a note's full content. Binary input must use encoding=base64; text remains UTF-8. No revision is taken and nothing is sanitised: call create_revision first to keep the prior content recoverable, and prefer revise() for any BrainLLM note.", { noteId: stringType(), content: stringType(), mime: stringType().optional(), encoding: enumType(["auto", "text", "base64"]).optional() }, async ({ noteId, content, mime, encoding }) => {
|
|
27451
27449
|
const note = await trilium.getNote(noteId);
|
|
27452
27450
|
const targetMime = mime ?? note.mime;
|
|
27453
27451
|
try {
|
|
@@ -27471,7 +27469,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27471
27469
|
const note = await trilium.patchNote(noteId, fields);
|
|
27472
27470
|
return txt4({ noteId: note.noteId, title: note.title, type: note.type });
|
|
27473
27471
|
});
|
|
27474
|
-
server.tool("delete_note", "Hard-delete a note (
|
|
27472
|
+
server.tool("delete_note", "Hard-delete a single note. Irreversible. Refused (blocked: true) while any relation points at it (backlinks_exist), while it has children or more than one parent (blast_radius), or if the backlink check itself fails. Remove or retarget those first; for a recoverable removal use forget().", { noteId: stringType() }, async ({ noteId }) => {
|
|
27475
27473
|
const note = await trilium.getNote(noteId);
|
|
27476
27474
|
let backlinks;
|
|
27477
27475
|
try {
|
|
@@ -27488,7 +27486,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27488
27486
|
await trilium.deleteNote(noteId);
|
|
27489
27487
|
return txt4({ ok: true, deleted: noteId });
|
|
27490
27488
|
});
|
|
27491
|
-
server.tool("undelete_note",
|
|
27489
|
+
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
27490
|
await trilium.undeleteNote(noteId);
|
|
27493
27491
|
const cfg = b();
|
|
27494
27492
|
const note = await trilium.getNote(noteId);
|
|
@@ -27660,7 +27658,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27660
27658
|
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
27659
|
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
27660
|
server.tool("get_app_info", "Trilium server version, DB version, runtime metadata.", {}, async () => txt4(await trilium.getAppInfo()));
|
|
27663
|
-
server.tool("create_backup",
|
|
27661
|
+
server.tool("create_backup", `Named Trilium database backup (raw). The resulting file path is not exposed.`, {
|
|
27664
27662
|
name: stringType().optional().describe("Backup file name without .db extension (default: brainllm-{today})"),
|
|
27665
27663
|
date: stringType().optional().describe("ISO date used in the default name when name is omitted (default: today)")
|
|
27666
27664
|
}, async ({ name, date }) => {
|
|
@@ -28802,6 +28800,12 @@ if (!brain) {
|
|
|
28802
28800
|
console.error("[brainllm] BrainLLM not found in Trilium. Run the bootstrap tool to initialize.");
|
|
28803
28801
|
}
|
|
28804
28802
|
}
|
|
28803
|
+
{
|
|
28804
|
+
const { invalid } = envDeletionCatchupDays();
|
|
28805
|
+
if (invalid !== undefined) {
|
|
28806
|
+
console.error(`[brainllm] BRAINLLM_DELETION_CATCHUP_DAYS="${invalid}" ignored \u2014 expected a whole number of days from 1 to 3650. Using ${deletionCatchupDays(brain ?? EMPTY_BRAINLLM)}.`);
|
|
28807
|
+
}
|
|
28808
|
+
}
|
|
28805
28809
|
var brainRef = { config: brain ?? EMPTY_BRAINLLM };
|
|
28806
28810
|
var port = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
|
|
28807
28811
|
var authToken = process.env.MCP_AUTH_TOKEN;
|
|
@@ -28824,7 +28828,7 @@ function createServer(origin = null) {
|
|
|
28824
28828
|
const s = new McpServer({
|
|
28825
28829
|
name: "BrainLLM",
|
|
28826
28830
|
title: "BrainLLM",
|
|
28827
|
-
version: "12.
|
|
28831
|
+
version: "12.6.0",
|
|
28828
28832
|
icons: brandingIcons(origin)
|
|
28829
28833
|
});
|
|
28830
28834
|
registerTools(s, trilium, brainRef);
|