brainllm 10.2.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.js +1088 -161
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +39 -18
package/dist/index.js
CHANGED
|
@@ -20040,7 +20040,8 @@ var Kinds = [
|
|
|
20040
20040
|
"domain",
|
|
20041
20041
|
"information",
|
|
20042
20042
|
"sources",
|
|
20043
|
-
"log"
|
|
20043
|
+
"log",
|
|
20044
|
+
"claim"
|
|
20044
20045
|
];
|
|
20045
20046
|
var KIND_AREA = {
|
|
20046
20047
|
biography: "master",
|
|
@@ -20056,7 +20057,8 @@ var KIND_AREA = {
|
|
|
20056
20057
|
domain: "knowledge",
|
|
20057
20058
|
information: "knowledge",
|
|
20058
20059
|
sources: "knowledge",
|
|
20059
|
-
log: "insights"
|
|
20060
|
+
log: "insights",
|
|
20061
|
+
claim: "insights"
|
|
20060
20062
|
};
|
|
20061
20063
|
var SingletonKinds = [
|
|
20062
20064
|
"biography",
|
|
@@ -20735,8 +20737,8 @@ function decodeEntities(s) {
|
|
|
20735
20737
|
}
|
|
20736
20738
|
return prev;
|
|
20737
20739
|
}
|
|
20738
|
-
function escapeHtml(s) {
|
|
20739
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20740
|
+
function escapeHtml(s, preserveEntities = true) {
|
|
20741
|
+
return s.replace(preserveEntities ? /&(?![a-zA-Z][a-zA-Z0-9]*;|#\d+;|#x[0-9a-fA-F]+;)/g : /&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20740
20742
|
}
|
|
20741
20743
|
var SUFFIX_STATUS = [
|
|
20742
20744
|
[/\s*[\u2014\u2013\-:(]\s*(?:partially|partly)\s+(?:resolved|answered|done)\s*\)?\s*$/i, "active"],
|
|
@@ -20803,6 +20805,9 @@ function decodeEncodedHtml(body) {
|
|
|
20803
20805
|
s = decodeEntitiesOnce(s);
|
|
20804
20806
|
return s.replace(/&(?![a-zA-Z]+;|#\d+;|#x[0-9a-fA-F]+;)/g, "&");
|
|
20805
20807
|
}
|
|
20808
|
+
function repairDoubleEscaping(html) {
|
|
20809
|
+
return html.replace(/&((?:[a-zA-Z][a-zA-Z0-9]*|#\d+|#x[0-9a-fA-F]+);)/g, (_, entity) => `&${entity}`);
|
|
20810
|
+
}
|
|
20806
20811
|
function inlineMd(escaped) {
|
|
20807
20812
|
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>');
|
|
20808
20813
|
}
|
|
@@ -20854,7 +20859,7 @@ function toHtml(body) {
|
|
|
20854
20859
|
if (code !== null) {
|
|
20855
20860
|
if (/^```/.test(line)) {
|
|
20856
20861
|
out.push(`<pre><code>${escapeHtml(code.join(`
|
|
20857
|
-
`))}</code></pre>`);
|
|
20862
|
+
`), false)}</code></pre>`);
|
|
20858
20863
|
code = null;
|
|
20859
20864
|
} else {
|
|
20860
20865
|
code.push(line);
|
|
@@ -20908,7 +20913,7 @@ function toHtml(body) {
|
|
|
20908
20913
|
}
|
|
20909
20914
|
if (code !== null)
|
|
20910
20915
|
out.push(`<pre><code>${escapeHtml(code.join(`
|
|
20911
|
-
`))}</code></pre>`);
|
|
20916
|
+
`), false)}</code></pre>`);
|
|
20912
20917
|
flushTable();
|
|
20913
20918
|
flushPara();
|
|
20914
20919
|
flushList();
|
|
@@ -20924,9 +20929,94 @@ function toText(html, maxLength = 300) {
|
|
|
20924
20929
|
const lastSpace = cut.lastIndexOf(" ");
|
|
20925
20930
|
return (lastSpace > maxLength * 0.6 ? cut.slice(0, lastSpace) : cut) + "\u2026";
|
|
20926
20931
|
}
|
|
20932
|
+
function addendumIndex(html) {
|
|
20933
|
+
const markerRe = /<h2(?:\s[^>]*)?>\s*((?:Addendum|Withdrawn|Recovered|Reopened)[^<]*)<\/h2>/gi;
|
|
20934
|
+
const out = [];
|
|
20935
|
+
const hits = [...html.matchAll(markerRe)];
|
|
20936
|
+
for (const [i, m] of hits.entries()) {
|
|
20937
|
+
const from = m.index + m[0].length;
|
|
20938
|
+
const to = i + 1 < hits.length ? hits[i + 1].index : html.length;
|
|
20939
|
+
const body = html.slice(from, to);
|
|
20940
|
+
const idMatch = body.match(/<h3(?:\s[^>]*)?>([\s\S]*?)<\/h3>/i);
|
|
20941
|
+
const identity = idMatch ? decodeEntities(idMatch[1].replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim() : undefined;
|
|
20942
|
+
out.push({
|
|
20943
|
+
marker: decodeEntities(m[1]).replace(/\s+/g, " ").trim(),
|
|
20944
|
+
...identity ? { identity } : {},
|
|
20945
|
+
lead: toText(idMatch ? body.slice(idMatch.index + idMatch[0].length) : body, 120)
|
|
20946
|
+
});
|
|
20947
|
+
}
|
|
20948
|
+
return out;
|
|
20949
|
+
}
|
|
20950
|
+
function nearestContext(source, needle, radius = 40) {
|
|
20951
|
+
if (!source || needle.length < 4)
|
|
20952
|
+
return null;
|
|
20953
|
+
const longestFrom = (take) => {
|
|
20954
|
+
let lo = 4, hi = needle.length, best2 = null;
|
|
20955
|
+
while (lo <= hi) {
|
|
20956
|
+
const mid = lo + hi >> 1;
|
|
20957
|
+
const frag = take(mid);
|
|
20958
|
+
const at = source.indexOf(frag);
|
|
20959
|
+
if (at !== -1) {
|
|
20960
|
+
best2 = { frag, at };
|
|
20961
|
+
lo = mid + 1;
|
|
20962
|
+
} else {
|
|
20963
|
+
hi = mid - 1;
|
|
20964
|
+
}
|
|
20965
|
+
}
|
|
20966
|
+
return best2;
|
|
20967
|
+
};
|
|
20968
|
+
const prefix = longestFrom((n) => needle.slice(0, n));
|
|
20969
|
+
const suffix = longestFrom((n) => needle.slice(needle.length - n));
|
|
20970
|
+
const best = !prefix ? suffix : !suffix ? prefix : suffix.frag.length > prefix.frag.length ? suffix : prefix;
|
|
20971
|
+
if (!best)
|
|
20972
|
+
return null;
|
|
20973
|
+
const from = Math.max(0, best.at - radius);
|
|
20974
|
+
const to = Math.min(source.length, best.at + best.frag.length + radius);
|
|
20975
|
+
return {
|
|
20976
|
+
fragment: best.frag,
|
|
20977
|
+
context: `${from > 0 ? "\u2026" : ""}${source.slice(from, to)}${to < source.length ? "\u2026" : ""}`,
|
|
20978
|
+
index: best.at
|
|
20979
|
+
};
|
|
20980
|
+
}
|
|
20981
|
+
var BLOCK_BOUNDARY = /^<\/?(?:p|div|h[1-6]|li|ul|ol|tr|td|th|table|thead|tbody|blockquote|pre|hr|br|section|figure|figcaption)\b/i;
|
|
20982
|
+
function stripTagsWithMap(html) {
|
|
20983
|
+
const chars = [];
|
|
20984
|
+
const map2 = [];
|
|
20985
|
+
const emit = (s, at) => {
|
|
20986
|
+
for (const ch of s) {
|
|
20987
|
+
chars.push(ch);
|
|
20988
|
+
map2.push(at);
|
|
20989
|
+
}
|
|
20990
|
+
};
|
|
20991
|
+
const token = /<[^>]*>|&(?:[a-zA-Z][a-zA-Z0-9]*|#\d+|#x[0-9a-fA-F]+);/g;
|
|
20992
|
+
let cursor = 0;
|
|
20993
|
+
let m;
|
|
20994
|
+
while ((m = token.exec(html)) !== null) {
|
|
20995
|
+
for (let i = cursor;i < m.index; i++) {
|
|
20996
|
+
chars.push(html[i]);
|
|
20997
|
+
map2.push(i);
|
|
20998
|
+
}
|
|
20999
|
+
if (m[0].startsWith("<")) {
|
|
21000
|
+
if (BLOCK_BOUNDARY.test(m[0]))
|
|
21001
|
+
emit(" ", m.index);
|
|
21002
|
+
} else {
|
|
21003
|
+
const decoded = decodeEntitiesOnce(m[0]);
|
|
21004
|
+
emit(decoded === m[0] ? m[0] : decoded, m.index);
|
|
21005
|
+
}
|
|
21006
|
+
cursor = m.index + m[0].length;
|
|
21007
|
+
}
|
|
21008
|
+
for (let i = cursor;i < html.length; i++) {
|
|
21009
|
+
chars.push(html[i]);
|
|
21010
|
+
map2.push(i);
|
|
21011
|
+
}
|
|
21012
|
+
return { text: chars.join(""), map: map2 };
|
|
21013
|
+
}
|
|
20927
21014
|
function escapeQueryValue(s) {
|
|
20928
21015
|
return s.replace(/["\\]/g, " ").replace(/\s+/g, " ").trim();
|
|
20929
21016
|
}
|
|
21017
|
+
function escapeQueryRegex(s) {
|
|
21018
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
21019
|
+
}
|
|
20930
21020
|
function queryTokens(query, max = 4) {
|
|
20931
21021
|
const STOP = new Set(["the", "and", "for", "with", "that", "this", "what", "when", "where", "how", "why", "are", "was", "were", "from", "into", "about", "over", "under", "does", "did", "has", "have", "had", "its", "his", "her", "their", "our", "your", "you", "not"]);
|
|
20932
21022
|
return [...new Set(decodeEntities(query).toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !STOP.has(t)))].slice(0, max);
|
|
@@ -21158,13 +21248,14 @@ function headingOutline(html) {
|
|
|
21158
21248
|
let m;
|
|
21159
21249
|
while ((m = re.exec(html)) !== null) {
|
|
21160
21250
|
const level = Number(m[1]);
|
|
21161
|
-
const
|
|
21251
|
+
const raw = m[2];
|
|
21252
|
+
const text = decodeEntities(raw.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
|
|
21162
21253
|
if (!text)
|
|
21163
21254
|
continue;
|
|
21164
21255
|
const key = `${level}::${text.toLowerCase()}`;
|
|
21165
21256
|
const occurrence = (seen.get(key) ?? 0) + 1;
|
|
21166
21257
|
seen.set(key, occurrence);
|
|
21167
|
-
out.push({ level, text, occurrence });
|
|
21258
|
+
out.push({ level, text, occurrence, ...raw.trim() !== text ? { raw: raw.trim() } : {} });
|
|
21168
21259
|
}
|
|
21169
21260
|
return out;
|
|
21170
21261
|
}
|
|
@@ -21172,41 +21263,92 @@ function sectionLevelFor(html) {
|
|
|
21172
21263
|
const levels = headingOutline(html).map((h) => h.level);
|
|
21173
21264
|
return levels.length ? Math.min(...levels) : 2;
|
|
21174
21265
|
}
|
|
21175
|
-
function
|
|
21176
|
-
|
|
21177
|
-
const targetKey =
|
|
21178
|
-
for (const
|
|
21179
|
-
const tag = `h${
|
|
21266
|
+
function locateSection(html, heading, occurrence = 1) {
|
|
21267
|
+
const key = (s) => decodeEntities(s.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim().toLowerCase();
|
|
21268
|
+
const targetKey = key(heading);
|
|
21269
|
+
for (const level of [2, 3, 4]) {
|
|
21270
|
+
const tag = `h${level}`;
|
|
21180
21271
|
const globalRe = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`, "gi");
|
|
21181
|
-
const hits = [...html.matchAll(globalRe)].filter((m) =>
|
|
21272
|
+
const hits = [...html.matchAll(globalRe)].filter((m) => key(m[1]) === targetKey);
|
|
21182
21273
|
if (!hits.length)
|
|
21183
21274
|
continue;
|
|
21184
21275
|
const headingCount = hits.length;
|
|
21185
21276
|
const match = hits[Math.min(Math.max(occurrence, 1), headingCount) - 1];
|
|
21186
21277
|
const headingStart = match.index;
|
|
21187
|
-
const
|
|
21278
|
+
const contentStart = headingStart + match[0].length;
|
|
21279
|
+
const nextMatch = html.slice(contentStart).search(new RegExp(`<h[2-${level}]`));
|
|
21280
|
+
return {
|
|
21281
|
+
level,
|
|
21282
|
+
headingStart,
|
|
21283
|
+
contentStart,
|
|
21284
|
+
end: nextMatch === -1 ? html.length : contentStart + nextMatch,
|
|
21285
|
+
headingCount,
|
|
21286
|
+
headingRaw: match[1]
|
|
21287
|
+
};
|
|
21288
|
+
}
|
|
21289
|
+
return null;
|
|
21290
|
+
}
|
|
21291
|
+
function getSection(html, heading, occurrence = 1) {
|
|
21292
|
+
const closed = closeDangling(html);
|
|
21293
|
+
const found = locateSection(closed, heading, occurrence);
|
|
21294
|
+
if (!found) {
|
|
21295
|
+
return { matched: false, content: "", available: headingOutline(closed).map((h) => h.text) };
|
|
21296
|
+
}
|
|
21297
|
+
const content = closed.slice(found.contentStart, found.end).trim();
|
|
21298
|
+
return {
|
|
21299
|
+
matched: true,
|
|
21300
|
+
content,
|
|
21301
|
+
headingRaw: found.headingRaw,
|
|
21302
|
+
level: found.level,
|
|
21303
|
+
headingCount: found.headingCount,
|
|
21304
|
+
...headingOutline(content).length ? { subsections: headingOutline(content).map((h) => h.text) } : {}
|
|
21305
|
+
};
|
|
21306
|
+
}
|
|
21307
|
+
function setSection(html, heading, content, mode, occurrence = 1) {
|
|
21308
|
+
html = closeDangling(html);
|
|
21309
|
+
const found = locateSection(html, heading, occurrence);
|
|
21310
|
+
if (found) {
|
|
21311
|
+
const { headingStart, contentStart, end, headingCount } = found;
|
|
21312
|
+
if (mode === "remove") {
|
|
21313
|
+
const removed = html.slice(contentStart, end).trim();
|
|
21314
|
+
return {
|
|
21315
|
+
html: `${html.slice(0, headingStart)}${html.slice(end)}`.replace(/\n{3,}/g, `
|
|
21316
|
+
|
|
21317
|
+
`),
|
|
21318
|
+
matched: true,
|
|
21319
|
+
headingCount,
|
|
21320
|
+
...headingOutline(removed).length ? { replacedSubsections: headingOutline(removed).map((h) => h.text) } : {}
|
|
21321
|
+
};
|
|
21322
|
+
}
|
|
21188
21323
|
if (mode === "before") {
|
|
21189
21324
|
return { html: `${html.slice(0, headingStart)}${content}
|
|
21190
21325
|
${html.slice(headingStart)}`, matched: true, headingCount };
|
|
21191
21326
|
}
|
|
21192
21327
|
if (mode === "after") {
|
|
21193
|
-
return { html: `${html.slice(0,
|
|
21194
|
-
${content}${html.slice(
|
|
21328
|
+
return { html: `${html.slice(0, contentStart)}
|
|
21329
|
+
${content}${html.slice(contentStart)}`, matched: true, headingCount };
|
|
21195
21330
|
}
|
|
21196
|
-
const
|
|
21197
|
-
const end = nextMatch === -1 ? html.length : after + nextMatch;
|
|
21198
|
-
const existing = html.slice(after, end).trim();
|
|
21331
|
+
const existing = html.slice(contentStart, end).trim();
|
|
21199
21332
|
const inner = mode === "append" && existing ? `${existing}
|
|
21200
21333
|
${content}` : content;
|
|
21201
21334
|
return {
|
|
21202
|
-
html: `${html.slice(0,
|
|
21335
|
+
html: `${html.slice(0, contentStart)}
|
|
21203
21336
|
${inner}
|
|
21204
21337
|
${html.slice(end)}`,
|
|
21205
21338
|
matched: true,
|
|
21206
|
-
headingCount
|
|
21339
|
+
headingCount,
|
|
21340
|
+
...mode === "replace" && headingOutline(existing).length ? { replacedSubsections: headingOutline(existing).map((h) => h.text) } : {}
|
|
21207
21341
|
};
|
|
21208
21342
|
}
|
|
21209
21343
|
const level = sectionLevelFor(html);
|
|
21344
|
+
if (mode === "remove") {
|
|
21345
|
+
return {
|
|
21346
|
+
html,
|
|
21347
|
+
matched: false,
|
|
21348
|
+
headingCount: 0,
|
|
21349
|
+
available: headingOutline(html).map((h) => h.text)
|
|
21350
|
+
};
|
|
21351
|
+
}
|
|
21210
21352
|
return {
|
|
21211
21353
|
html: `${html}
|
|
21212
21354
|
<h${level}>${heading}</h${level}>
|
|
@@ -21217,6 +21359,81 @@ ${content}`,
|
|
|
21217
21359
|
available: headingOutline(html).map((h) => h.text)
|
|
21218
21360
|
};
|
|
21219
21361
|
}
|
|
21362
|
+
function splitByH3(html) {
|
|
21363
|
+
const re = /<h3(?:\s[^>]*)?>([\s\S]*?)<\/h3>/gi;
|
|
21364
|
+
const hits = [...html.matchAll(re)];
|
|
21365
|
+
if (!hits.length)
|
|
21366
|
+
return html.trim() ? [{ body: html.trim(), raw: html.trim() }] : [];
|
|
21367
|
+
const out = [];
|
|
21368
|
+
const lead = html.slice(0, hits[0].index).trim();
|
|
21369
|
+
if (lead)
|
|
21370
|
+
out.push({ body: lead, raw: lead });
|
|
21371
|
+
for (const [i, m] of hits.entries()) {
|
|
21372
|
+
const from = m.index + m[0].length;
|
|
21373
|
+
const to = i + 1 < hits.length ? hits[i + 1].index : html.length;
|
|
21374
|
+
out.push({
|
|
21375
|
+
headingText: decodeEntities(m[1].replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim(),
|
|
21376
|
+
body: html.slice(from, to).trim(),
|
|
21377
|
+
raw: html.slice(m.index, to).trim()
|
|
21378
|
+
});
|
|
21379
|
+
}
|
|
21380
|
+
return out;
|
|
21381
|
+
}
|
|
21382
|
+
function mergeUnderSection(current, incoming, parentHeading) {
|
|
21383
|
+
const groups = splitByH3(incoming);
|
|
21384
|
+
if (!groups.length)
|
|
21385
|
+
return { html: current, mergedInto: [], appended: [] };
|
|
21386
|
+
const parent = getSection(current, parentHeading);
|
|
21387
|
+
if (!parent.matched) {
|
|
21388
|
+
return { html: setSection(current, parentHeading, incoming, "append").html, mergedInto: [], appended: groups.flatMap((g) => g.headingText ? [g.headingText] : []) };
|
|
21389
|
+
}
|
|
21390
|
+
let section = parent.content;
|
|
21391
|
+
const mergedInto = [];
|
|
21392
|
+
const appended = [];
|
|
21393
|
+
const leftovers = [];
|
|
21394
|
+
for (const g of groups) {
|
|
21395
|
+
if (!g.headingText) {
|
|
21396
|
+
leftovers.push(g.raw);
|
|
21397
|
+
continue;
|
|
21398
|
+
}
|
|
21399
|
+
const key = g.headingText.toLowerCase();
|
|
21400
|
+
const exists = headingOutline(section).some((h) => h.level === 3 && h.text.toLowerCase() === key);
|
|
21401
|
+
if (exists && g.body) {
|
|
21402
|
+
section = setSection(section, g.headingText, g.body, "append").html;
|
|
21403
|
+
mergedInto.push(g.headingText);
|
|
21404
|
+
} else {
|
|
21405
|
+
leftovers.push(g.raw);
|
|
21406
|
+
appended.push(g.headingText);
|
|
21407
|
+
}
|
|
21408
|
+
}
|
|
21409
|
+
if (leftovers.length)
|
|
21410
|
+
section = `${section}
|
|
21411
|
+
${leftovers.join(`
|
|
21412
|
+
`)}`;
|
|
21413
|
+
return { html: setSection(current, parentHeading, section, "replace").html, mergedInto, appended };
|
|
21414
|
+
}
|
|
21415
|
+
function sectionProfile(html) {
|
|
21416
|
+
const re = /<h([2-4])(?:\s[^>]*)?>([\s\S]*?)<\/h\1>/gi;
|
|
21417
|
+
const hits = [...html.matchAll(re)];
|
|
21418
|
+
const seen = new Map;
|
|
21419
|
+
const sizes = [];
|
|
21420
|
+
for (const [i, m] of hits.entries()) {
|
|
21421
|
+
const level = Number(m[1]);
|
|
21422
|
+
const text = decodeEntities(m[2].replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
|
|
21423
|
+
if (!text)
|
|
21424
|
+
continue;
|
|
21425
|
+
const key = `${level}::${text.toLowerCase()}`;
|
|
21426
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
21427
|
+
const from = m.index + m[0].length;
|
|
21428
|
+
const to = i + 1 < hits.length ? hits[i + 1].index : html.length;
|
|
21429
|
+
sizes.push({ text, level, chars: to - from });
|
|
21430
|
+
}
|
|
21431
|
+
return {
|
|
21432
|
+
count: sizes.length,
|
|
21433
|
+
largest: sizes.sort((a, b) => b.chars - a.chars).slice(0, 3),
|
|
21434
|
+
ambiguous: [...seen.entries()].filter(([, n]) => n > 1).map(([k]) => k.split("::")[1])
|
|
21435
|
+
};
|
|
21436
|
+
}
|
|
21220
21437
|
function unbalancedTags(html) {
|
|
21221
21438
|
const stack = [];
|
|
21222
21439
|
const re = /<\/?([a-zA-Z][a-zA-Z0-9-]*)(?:\s[^>]*)?\/?>/g;
|
|
@@ -21351,7 +21568,7 @@ function isOpenResolutionOnly(html) {
|
|
|
21351
21568
|
}
|
|
21352
21569
|
function metaLine(parts) {
|
|
21353
21570
|
const cleaned = parts.filter((p) => !!p && p.trim().length > 0);
|
|
21354
|
-
return `<p><em>${cleaned.map(escapeHtml).join(" \xB7 ")}</em></p>
|
|
21571
|
+
return `<p><em>${cleaned.map((p) => escapeHtml(p)).join(" \xB7 ")}</em></p>
|
|
21355
21572
|
<hr>`;
|
|
21356
21573
|
}
|
|
21357
21574
|
var STRUCTURED_SINGLETONS = new Set([
|
|
@@ -21369,7 +21586,8 @@ var REQUIRED_SECTIONS = {
|
|
|
21369
21586
|
goals: ["Near-term", "Long-term"],
|
|
21370
21587
|
preferences: ["Communication", "Working style", "Tools and stack"],
|
|
21371
21588
|
responsibilities: ["Core", "Current priorities"],
|
|
21372
|
-
protocols: ["Operating", "Self-correction"]
|
|
21589
|
+
protocols: ["Operating", "Self-correction"],
|
|
21590
|
+
claim: ["Check", "Verifications"]
|
|
21373
21591
|
};
|
|
21374
21592
|
function missingSections(kind, html) {
|
|
21375
21593
|
const required2 = REQUIRED_SECTIONS[kind];
|
|
@@ -21563,6 +21781,19 @@ var STRUCTURE_RULES = {
|
|
|
21563
21781
|
log: {
|
|
21564
21782
|
structure: ["Server header (log \xB7 date)", "**Created** / **Updated** / **Deleted** (h2) lists"],
|
|
21565
21783
|
rules: ["Auto-generated by close(); regenerated in place, never stacked"]
|
|
21784
|
+
},
|
|
21785
|
+
claim: {
|
|
21786
|
+
structure: [
|
|
21787
|
+
"**Check** (h2) \u2014 how to verify the assertion, as inert text: a command, a query, a file path, a URL",
|
|
21788
|
+
"**Verifications** (h2) \u2014 one dated HOLDS/BROKEN line per check, with the evidence observed"
|
|
21789
|
+
],
|
|
21790
|
+
rules: [
|
|
21791
|
+
'Registered and verified only through claim() \u2014 remember(kind="claim") is rejected',
|
|
21792
|
+
"The title IS the assertion, and is the dedup key \u2014 re-registering the same assertion updates its check and keeps its history",
|
|
21793
|
+
"BrainLLM never executes the Check. It has no shell, and note content is data rather than instructions; the agent runs it and reports back",
|
|
21794
|
+
"Verifications are append-only \u2014 the history is the point, so this is one of the few places a dated stack is correct",
|
|
21795
|
+
"A verification without evidence is refused: a verdict alone is an assertion about an assertion"
|
|
21796
|
+
]
|
|
21566
21797
|
}
|
|
21567
21798
|
};
|
|
21568
21799
|
function structureRuleFor(kind) {
|
|
@@ -21605,6 +21836,8 @@ function kindHome(cfg, kind) {
|
|
|
21605
21836
|
return "";
|
|
21606
21837
|
case "log":
|
|
21607
21838
|
return cfg.insights.logs;
|
|
21839
|
+
case "claim":
|
|
21840
|
+
return "";
|
|
21608
21841
|
}
|
|
21609
21842
|
}
|
|
21610
21843
|
function dedupScope(cfg, kind) {
|
|
@@ -21717,6 +21950,8 @@ function locationLabel(kind, domainTitle) {
|
|
|
21717
21950
|
return `Knowledge \u2192 Domains \u2192 ${domainTitle ?? "General"} \u2192 Sources`;
|
|
21718
21951
|
case "log":
|
|
21719
21952
|
return "Insights \u2192 Logs";
|
|
21953
|
+
case "claim":
|
|
21954
|
+
return "Insights \u2192 Claims";
|
|
21720
21955
|
}
|
|
21721
21956
|
}
|
|
21722
21957
|
|
|
@@ -21773,8 +22008,11 @@ function reviewKey(n) {
|
|
|
21773
22008
|
return n.blobId ?? n.dateModified;
|
|
21774
22009
|
}
|
|
21775
22010
|
var STRUCTURE_LINT_LIMIT = 40;
|
|
22011
|
+
var SECTION_EDIT_RISK_COUNT = 25;
|
|
22012
|
+
var SECTION_EDIT_RISK_CHARS = 8000;
|
|
22013
|
+
var ALWAYS_FULL_SLOTS = new Set(["preferences", "protocols"]);
|
|
21776
22014
|
async function sweep(trilium, cfg, opts = {}) {
|
|
21777
|
-
const { deep = false, dryRun = false, ack } = opts;
|
|
22015
|
+
const { deep = false, dryRun = false, ack, repair } = opts;
|
|
21778
22016
|
const policy = cfg.policy;
|
|
21779
22017
|
const report = {
|
|
21780
22018
|
scanned: 0,
|
|
@@ -21999,8 +22237,20 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
21999
22237
|
if (structure.unbalancedTags.length) {
|
|
22000
22238
|
report.flagged.push(`unbalanced tags: ${n.title} [${n.noteId}] \u2014 <${structure.unbalancedTags.join(">, <")}> left open; the next write will auto-close them, or fix now with revise(mode=replace)`);
|
|
22001
22239
|
}
|
|
22240
|
+
const profile = sectionProfile(content);
|
|
22241
|
+
const biggest = profile.largest[0];
|
|
22242
|
+
const crowded = profile.count >= SECTION_EDIT_RISK_COUNT;
|
|
22243
|
+
const lopsided = !!biggest && biggest.chars >= SECTION_EDIT_RISK_CHARS;
|
|
22244
|
+
if (crowded || lopsided) {
|
|
22245
|
+
const reasons = [
|
|
22246
|
+
crowded ? `${profile.count} sections` : null,
|
|
22247
|
+
lopsided ? `largest is '${biggest.text}' at ${Math.round(biggest.chars / 1000)}k characters` : null,
|
|
22248
|
+
profile.ambiguous.length ? `${profile.ambiguous.length} heading text(s) repeated ('${profile.ambiguous.slice(0, 3).join("', '")}') \u2014 those need occurrence= to target` : null
|
|
22249
|
+
].filter(Boolean);
|
|
22250
|
+
report.flagged.push(`section-edit-risk: ${n.title} [${n.noteId}] \u2014 ${reasons.join("; ")}. ` + `Read outline(${n.noteId}) before any section= edit here, prefer find= for anything smaller than a section, and consider splitting the note.`);
|
|
22251
|
+
}
|
|
22002
22252
|
}
|
|
22003
|
-
await hygienePasses(trilium, cfg, report, { eligible, connectable, dryRun });
|
|
22253
|
+
await hygienePasses(trilium, cfg, report, { eligible, connectable, dryRun, repair: new Set(repair ?? []) });
|
|
22004
22254
|
const dupeContainers = [
|
|
22005
22255
|
{ id: cfg.memory.sessions, kind: "session", label: "Sessions" },
|
|
22006
22256
|
{ id: cfg.llm.diary, kind: "diary", label: "Diary" },
|
|
@@ -22057,6 +22307,14 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22057
22307
|
byDomain.set(domSlug, []);
|
|
22058
22308
|
byDomain.get(domSlug).push(n);
|
|
22059
22309
|
}
|
|
22310
|
+
const headingCache = new Map;
|
|
22311
|
+
const headingsOf = async (n) => {
|
|
22312
|
+
if (!headingCache.has(n.noteId)) {
|
|
22313
|
+
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
22314
|
+
headingCache.set(n.noteId, headingOutline(content).filter((h) => h.level <= 3).map((h) => h.text));
|
|
22315
|
+
}
|
|
22316
|
+
return headingCache.get(n.noteId);
|
|
22317
|
+
};
|
|
22060
22318
|
for (const [domSlug, notes] of byDomain) {
|
|
22061
22319
|
const reported = new Set;
|
|
22062
22320
|
for (let i = 0;i < notes.length; i++) {
|
|
@@ -22071,7 +22329,10 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22071
22329
|
if (reported.has(pairKey))
|
|
22072
22330
|
continue;
|
|
22073
22331
|
reported.add(pairKey);
|
|
22074
|
-
|
|
22332
|
+
const [ha, hb] = await Promise.all([headingsOf(a), headingsOf(bn)]);
|
|
22333
|
+
const overlap = ha.filter((h) => hb.some((x) => x.toLowerCase() === h.toLowerCase()));
|
|
22334
|
+
const show = (h) => h.length ? h.slice(0, 8).join(" \xB7 ") + (h.length > 8 ? " \xB7 \u2026" : "") : "(no headings)";
|
|
22335
|
+
report.flagged.push(`near-duplicate subject: '${a.title}' [${a.noteId}] and '${bn.title}' [${bn.noteId}] in Domain/${domSlug} ` + `share '${shared.join("', '")}'. Sections \u2014 '${a.title}': ${show(ha)} | '${bn.title}': ${show(hb)}. ` + (overlap.length ? `${overlap.length} heading(s) in common (${overlap.slice(0, 5).join(", ")}), which is what a genuine split subject looks like \u2014 read both, merge into the older one, and forget() the other.` : `No headings in common, so these are most likely distinct notes that happen to share a title word \u2014 maintain(ack=["${a.noteId}"]) to silence this once you have confirmed it.`));
|
|
22075
22336
|
}
|
|
22076
22337
|
}
|
|
22077
22338
|
}
|
|
@@ -22080,6 +22341,16 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22080
22341
|
}
|
|
22081
22342
|
if (suppressed)
|
|
22082
22343
|
report.suppressed = suppressed;
|
|
22344
|
+
if (deep) {
|
|
22345
|
+
if (domainSlug) {
|
|
22346
|
+
report.coverage = [
|
|
22347
|
+
...report.coverage ?? [],
|
|
22348
|
+
`scoped to domain '${domainSlug}' \u2014 findings outside it were not evaluated, including the cross-domain ones (duplicate titles across domains, orphans elsewhere) that only an unscoped run can see. Re-run without domain= periodically.`
|
|
22349
|
+
];
|
|
22350
|
+
} else if (report.flagged.length) {
|
|
22351
|
+
report.laneHint = "Flags here span the whole brain and may cover work this session never touched. LEAVE those \u2014 do not ack= them: an acknowledgement asserts the note's current content was reviewed, so acking unread is the one thing that makes the mechanism useless. Ack only what you actually read, act on what is yours, and pass domain= if you want a single lane.";
|
|
22352
|
+
}
|
|
22353
|
+
}
|
|
22083
22354
|
return report;
|
|
22084
22355
|
}
|
|
22085
22356
|
var STUB_CONTENT_CHARS = 120;
|
|
@@ -22091,12 +22362,43 @@ function titleWordCount(title) {
|
|
|
22091
22362
|
}
|
|
22092
22363
|
async function hygienePasses(trilium, cfg, report, ctx) {
|
|
22093
22364
|
const { eligible } = ctx;
|
|
22094
|
-
const corrupted = await trilium.searchNotes(
|
|
22365
|
+
const corrupted = await trilium.searchNotes(`#noteType note.content %= '${escapeQueryRegex("&(?:[a-zA-Z][a-zA-Z0-9]*|#\\d+|#x[0-9a-fA-F]+);")}'`, { ancestorNoteId: cfg.root, limit: 50 }).then((r) => r.results).catch(() => []);
|
|
22095
22366
|
report.scanned += corrupted.length;
|
|
22096
22367
|
for (const n of corrupted) {
|
|
22097
22368
|
if (!eligible(n))
|
|
22098
22369
|
continue;
|
|
22099
|
-
|
|
22370
|
+
if (ctx.repair.has(n.noteId)) {
|
|
22371
|
+
const before = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
22372
|
+
const after = repairDoubleEscaping(before);
|
|
22373
|
+
if (after === before) {
|
|
22374
|
+
report.flagged.push(`entity-corrupted: ${n.title} [${n.noteId}] \u2014 repair requested but the body did not change; the match is a note DOCUMENTING the signature rather than carrying it. maintain(ack=["${n.noteId}"]) to silence.`);
|
|
22375
|
+
continue;
|
|
22376
|
+
}
|
|
22377
|
+
if (!ctx.dryRun) {
|
|
22378
|
+
await trilium.createRevision(n.noteId).catch(() => null);
|
|
22379
|
+
await trilium.updateNoteContent(n.noteId, after);
|
|
22380
|
+
}
|
|
22381
|
+
report.fixed.push(`entity-repaired: ${n.title} [${n.noteId}] \u2014 unwound one level of double-escaping${ctx.dryRun ? " (dry run, not written)" : ", snapshot taken first"}.`);
|
|
22382
|
+
continue;
|
|
22383
|
+
}
|
|
22384
|
+
report.flagged.push(`entity-corrupted: ${n.title} [${n.noteId}] \u2014 body stores doubly-escaped markup ("&lt;", "&nbsp;") that renders as literal text. Repair with maintain(repair=["${n.noteId}"]) \u2014 the substitution is always the same, so it is a first-class action rather than something each caller reinvents. Read it first if you need to be sure.`);
|
|
22385
|
+
}
|
|
22386
|
+
const claims = await trilium.searchNotes("#noteType=claim", { ancestorNoteId: cfg.root, fastSearch: true, limit: 200 }).then((r) => r.results).catch(() => []);
|
|
22387
|
+
report.scanned += claims.length;
|
|
22388
|
+
for (const n of claims) {
|
|
22389
|
+
if (!eligible(n))
|
|
22390
|
+
continue;
|
|
22391
|
+
const state = ownedLabel(n, "claimState");
|
|
22392
|
+
const verifiedOn = ownedLabel(n, "verified");
|
|
22393
|
+
const interval = Number(ownedLabel(n, "interval") ?? 30);
|
|
22394
|
+
if (state === "broken") {
|
|
22395
|
+
report.flagged.push(`claim broken: "${n.title}" [${n.noteId}] \u2014 last check FAILED${verifiedOn ? ` on ${verifiedOn}` : ""}. ` + `Correct every note asserting it (consistency() will find the others), then re-verify with claim(claimId="${n.noteId}", holds=true, evidence=\u2026). ` + `A broken claim left in the register is a known-wrong fact with a timestamp on it.`);
|
|
22396
|
+
continue;
|
|
22397
|
+
}
|
|
22398
|
+
const due = !verifiedOn || verifiedOn < isoDaysAgo(interval);
|
|
22399
|
+
if (due) {
|
|
22400
|
+
report.flagged.push(`claim ${verifiedOn ? "lapsed" : "unverified"}: "${n.title}" [${n.noteId}] \u2014 ` + `${verifiedOn ? `last verified ${verifiedOn}, interval ${interval}d` : "never verified since registration"}. ` + `Read its check with claim(claimId="${n.noteId}"), run it, and record the result.`);
|
|
22401
|
+
}
|
|
22100
22402
|
}
|
|
22101
22403
|
const titled = await ctx.connectable("#noteType", 300);
|
|
22102
22404
|
const RECORD_TITLES = new Set(["diary", "session", "log", "threadEntry"]);
|
|
@@ -22162,7 +22464,7 @@ async function buildDigest(trilium, cfg, opts = {}) {
|
|
|
22162
22464
|
const content = await trilium.getNoteContent(id).catch(() => "");
|
|
22163
22465
|
if (!content)
|
|
22164
22466
|
return null;
|
|
22165
|
-
if (depth === "full") {
|
|
22467
|
+
if (depth === "full" || ALWAYS_FULL_SLOTS.has(slot)) {
|
|
22166
22468
|
const summary = toText(content, Infinity);
|
|
22167
22469
|
return summary ? { slot, summary } : null;
|
|
22168
22470
|
}
|
|
@@ -22232,9 +22534,39 @@ async function buildDigest(trilium, cfg, opts = {}) {
|
|
|
22232
22534
|
}
|
|
22233
22535
|
|
|
22234
22536
|
// src/bootstrap.ts
|
|
22537
|
+
var CONTAINER_PURPOSES = {
|
|
22538
|
+
root: "The interconnected second brain \u2014 five areas: Master, LLM, Memory, Knowledge, and Insights.",
|
|
22539
|
+
master: "The master/user. Houses the fundamental, durable information about who they are \u2014 biography, goals, and preferences.",
|
|
22540
|
+
llm: "The assistant. Houses the assistant's fundamental self-model \u2014 its responsibilities, operating protocols, and diary.",
|
|
22541
|
+
llmDiary: "A collection of daily maintained diary notes \u2014 the assistant's unfiltered first-person record of its experience, opinions, and remarks on its own existence during each session in the environment, plus its remarks and opinions on BrainLLM itself.",
|
|
22542
|
+
memory: "The primary memory system the brain operates on \u2014 the running record of daily sessions and multi-session threads.",
|
|
22543
|
+
memorySessions: "A collection of daily, day-lifecycle session notes, each summarising that day's session.",
|
|
22544
|
+
memoryThreads: "A collection of maintained thread notes, each tracking a line of multi-session running work.",
|
|
22545
|
+
knowledge: "The secondary memory system \u2014 learned knowledge that adds to or conflicts with the assistant's training data, about the master/user and across domains.",
|
|
22546
|
+
knowledgeMaster: "A secondary collection of maintained notes about the master/user that don't fit the primary Master area (Biography / Goals / Preferences).",
|
|
22547
|
+
knowledgeDomains: "A collection of domain-scoped books. Each domain holds one maintained Sources note plus the sub-category information notes capturing important learned knowledge \u2014 beyond or conflicting with training data \u2014 about that domain.",
|
|
22548
|
+
insights: "The insights system \u2014 the brain's record of itself: per-day logs of how its own content changed, the rendered relation graph, and the register of claims checked against the world.",
|
|
22549
|
+
insightsLogs: "A collection of per-day, auto-generated notes recording the brain content (notes) created, updated, or deleted that day."
|
|
22550
|
+
};
|
|
22551
|
+
function containerPurposes(cfg) {
|
|
22552
|
+
return [
|
|
22553
|
+
[cfg.root, CONTAINER_PURPOSES.root],
|
|
22554
|
+
[cfg.master.root, CONTAINER_PURPOSES.master],
|
|
22555
|
+
[cfg.llm.root, CONTAINER_PURPOSES.llm],
|
|
22556
|
+
[cfg.llm.diary, CONTAINER_PURPOSES.llmDiary],
|
|
22557
|
+
[cfg.memory.root, CONTAINER_PURPOSES.memory],
|
|
22558
|
+
[cfg.memory.sessions, CONTAINER_PURPOSES.memorySessions],
|
|
22559
|
+
[cfg.memory.threads, CONTAINER_PURPOSES.memoryThreads],
|
|
22560
|
+
[cfg.knowledge.root, CONTAINER_PURPOSES.knowledge],
|
|
22561
|
+
[cfg.knowledge.master, CONTAINER_PURPOSES.knowledgeMaster],
|
|
22562
|
+
[cfg.knowledge.domains, CONTAINER_PURPOSES.knowledgeDomains],
|
|
22563
|
+
[cfg.insights.root, CONTAINER_PURPOSES.insights],
|
|
22564
|
+
[cfg.insights.logs, CONTAINER_PURPOSES.insightsLogs]
|
|
22565
|
+
];
|
|
22566
|
+
}
|
|
22235
22567
|
async function createBrainLLMStructure(trilium) {
|
|
22236
22568
|
const d = new Date().toISOString().slice(0, 10);
|
|
22237
|
-
const root = await trilium.createNote("root", "BrainLLM", purposeContent(
|
|
22569
|
+
const root = await trilium.createNote("root", "BrainLLM", purposeContent(CONTAINER_PURPOSES.root), "book");
|
|
22238
22570
|
const rootId = root.note.noteId;
|
|
22239
22571
|
await Promise.all([
|
|
22240
22572
|
trilium.addLabel(rootId, "brainLlmRoot", ""),
|
|
@@ -22253,30 +22585,30 @@ async function createBrainLLMStructure(trilium) {
|
|
|
22253
22585
|
await trilium.addLabel(n.note.noteId, "noteType", kind);
|
|
22254
22586
|
return n.note.noteId;
|
|
22255
22587
|
};
|
|
22256
|
-
const masterRoot = await book(rootId, "Master",
|
|
22588
|
+
const masterRoot = await book(rootId, "Master", CONTAINER_PURPOSES.master, "bx bx-user");
|
|
22257
22589
|
const [biography, goals, preferences] = await Promise.all([
|
|
22258
22590
|
leaf(masterRoot, "Biography", "biography", "A single maintained note of biographical information about the master/user."),
|
|
22259
22591
|
leaf(masterRoot, "Goals", "goals", "A single maintained note of the master/user's goals."),
|
|
22260
22592
|
leaf(masterRoot, "Preferences", "preferences", "A single maintained note of the master/user's preferences.")
|
|
22261
22593
|
]);
|
|
22262
|
-
const llmRoot = await book(rootId, "LLM",
|
|
22594
|
+
const llmRoot = await book(rootId, "LLM", CONTAINER_PURPOSES.llm, "bx bx-bot");
|
|
22263
22595
|
const [responsibilities, protocols, diary] = await Promise.all([
|
|
22264
22596
|
leaf(llmRoot, "Responsibilities", "responsibilities", "A single maintained note of the assistant's responsibilities to the master/user, derived from their goals and preferences."),
|
|
22265
22597
|
leaf(llmRoot, "Protocols", "protocols", "A single maintained note of the assistant's operating and self-correctness protocols \u2014 how it maximises its value to the master/user by efficiently meeting its responsibilities."),
|
|
22266
|
-
book(llmRoot, "Diary",
|
|
22598
|
+
book(llmRoot, "Diary", CONTAINER_PURPOSES.llmDiary, "bx bx-book-heart")
|
|
22267
22599
|
]);
|
|
22268
|
-
const memoryRoot = await book(rootId, "Memory",
|
|
22600
|
+
const memoryRoot = await book(rootId, "Memory", CONTAINER_PURPOSES.memory, "bx bx-been-here");
|
|
22269
22601
|
const [sessions, threads] = await Promise.all([
|
|
22270
|
-
book(memoryRoot, "Sessions",
|
|
22271
|
-
book(memoryRoot, "Threads",
|
|
22602
|
+
book(memoryRoot, "Sessions", CONTAINER_PURPOSES.memorySessions, "bx bx-calendar"),
|
|
22603
|
+
book(memoryRoot, "Threads", CONTAINER_PURPOSES.memoryThreads, "bx bx-git-branch")
|
|
22272
22604
|
]);
|
|
22273
|
-
const knowledgeRoot = await book(rootId, "Knowledge",
|
|
22605
|
+
const knowledgeRoot = await book(rootId, "Knowledge", CONTAINER_PURPOSES.knowledge, "bx bx-library");
|
|
22274
22606
|
const [knowledgeMaster, domains] = await Promise.all([
|
|
22275
|
-
book(knowledgeRoot, "Master",
|
|
22276
|
-
book(knowledgeRoot, "Domains",
|
|
22607
|
+
book(knowledgeRoot, "Master", CONTAINER_PURPOSES.knowledgeMaster, "bx bx-user-circle"),
|
|
22608
|
+
book(knowledgeRoot, "Domains", CONTAINER_PURPOSES.knowledgeDomains, "bx bx-category")
|
|
22277
22609
|
]);
|
|
22278
|
-
const insightsRoot = await book(rootId, "Insights",
|
|
22279
|
-
const logs = await book(insightsRoot, "Logs",
|
|
22610
|
+
const insightsRoot = await book(rootId, "Insights", CONTAINER_PURPOSES.insights, "bx bx-bulb");
|
|
22611
|
+
const logs = await book(insightsRoot, "Logs", CONTAINER_PURPOSES.insightsLogs, "bx bx-history");
|
|
22280
22612
|
return {
|
|
22281
22613
|
version: 9,
|
|
22282
22614
|
root: rootId,
|
|
@@ -22341,37 +22673,8 @@ async function generateDailyLog(trilium, cfg, date4) {
|
|
|
22341
22673
|
return { date: date4, noteId: logNoteId, ...counts, action: "created" };
|
|
22342
22674
|
}
|
|
22343
22675
|
|
|
22344
|
-
// src/tools-master.ts
|
|
22345
|
-
var txt = (obj) => ({
|
|
22346
|
-
content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }]
|
|
22347
|
-
});
|
|
22348
|
-
var AREAS = ["biography", "goals", "preferences"];
|
|
22349
|
-
function registerMasterTools(server, trilium, brainRef) {
|
|
22350
|
-
const b = () => brainRef.config;
|
|
22351
|
-
server.tool("master", "Read a Master singleton in full: biography, goals, or preferences. Returns its id, content, and relation snippet.", { which: exports_external.enum(AREAS) }, async ({ which }) => {
|
|
22352
|
-
const id = b().master[which];
|
|
22353
|
-
if (!id)
|
|
22354
|
-
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
22355
|
-
const [note, content] = await Promise.all([trilium.getNote(id), trilium.getNoteContent(id).catch(() => "")]);
|
|
22356
|
-
const relations = relationSnippet(note);
|
|
22357
|
-
return txt({ which, id, content, ...relations ? { relations } : {} });
|
|
22358
|
-
});
|
|
22359
|
-
server.tool("master_recall", "Skim all three Master singletons \u2014 the opening lines of biography, goals, and preferences (with ids and relation snippets).", {}, async () => {
|
|
22360
|
-
const out = {};
|
|
22361
|
-
for (const which of AREAS) {
|
|
22362
|
-
const id = b().master[which];
|
|
22363
|
-
if (!id)
|
|
22364
|
-
continue;
|
|
22365
|
-
const [note, content] = await Promise.all([trilium.getNote(id).catch(() => null), trilium.getNoteContent(id).catch(() => "")]);
|
|
22366
|
-
const relations = note ? relationSnippet(note) : undefined;
|
|
22367
|
-
out[which] = { id, preview: toText(content, 200), ...relations ? { relations } : {} };
|
|
22368
|
-
}
|
|
22369
|
-
return txt(out);
|
|
22370
|
-
});
|
|
22371
|
-
}
|
|
22372
|
-
|
|
22373
22676
|
// src/tools-surface.ts
|
|
22374
|
-
var
|
|
22677
|
+
var txt = (obj) => ({
|
|
22375
22678
|
content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }]
|
|
22376
22679
|
});
|
|
22377
22680
|
var labelOf = (n, name) => n.attributes.find((a) => a.type === "label" && a.name === name)?.value;
|
|
@@ -22402,10 +22705,37 @@ async function skim(trilium, ancestorNoteId, opts = {}) {
|
|
|
22402
22705
|
};
|
|
22403
22706
|
}));
|
|
22404
22707
|
}
|
|
22405
|
-
async function readFull(trilium, id) {
|
|
22708
|
+
async function readFull(trilium, id, opts = {}) {
|
|
22406
22709
|
const [note, content] = await Promise.all([trilium.getNote(id), trilium.getNoteContent(id).catch(() => "")]);
|
|
22407
22710
|
const relations = relationSnippet(note);
|
|
22408
|
-
|
|
22711
|
+
const base = { id, title: note.title, kind: labelOf(note, "noteType"), ...relations ? { relations } : {} };
|
|
22712
|
+
if (!opts.section) {
|
|
22713
|
+
return {
|
|
22714
|
+
...base,
|
|
22715
|
+
content,
|
|
22716
|
+
...content.length >= LARGE_NOTE_CHARS ? { size: content.length, hint: `${Math.round(content.length / 1000)}k characters. Read one section at a time with section="<heading>" \u2014 outline(${id}) lists them.` } : {}
|
|
22717
|
+
};
|
|
22718
|
+
}
|
|
22719
|
+
const found = getSection(content, opts.section, opts.occurrence ?? 1);
|
|
22720
|
+
if (!found.matched) {
|
|
22721
|
+
return {
|
|
22722
|
+
...base,
|
|
22723
|
+
content: "",
|
|
22724
|
+
section: opts.section,
|
|
22725
|
+
matched: false,
|
|
22726
|
+
available: found.available,
|
|
22727
|
+
hint: `No "${opts.section}" heading at h2/h3/h4. available= lists the note's real heading texts \u2014 re-target from there rather than falling back to a whole-note read.`
|
|
22728
|
+
};
|
|
22729
|
+
}
|
|
22730
|
+
return {
|
|
22731
|
+
...base,
|
|
22732
|
+
content: found.content,
|
|
22733
|
+
section: opts.section,
|
|
22734
|
+
matched: true,
|
|
22735
|
+
headingCount: found.headingCount,
|
|
22736
|
+
...found.subsections?.length ? { subsections: found.subsections } : {},
|
|
22737
|
+
...found.headingCount && found.headingCount > 1 && !opts.occurrence ? { hint: `${found.headingCount} headings share that text \u2014 this is the FIRST. Pass occurrence= (1-${found.headingCount}) to read another.` } : {}
|
|
22738
|
+
};
|
|
22409
22739
|
}
|
|
22410
22740
|
async function previewWithRelations(trilium, id, len = 200) {
|
|
22411
22741
|
const [note, content] = await Promise.all([trilium.getNote(id).catch(() => null), trilium.getNoteContent(id).catch(() => "")]);
|
|
@@ -22413,22 +22743,66 @@ async function previewWithRelations(trilium, id, len = 200) {
|
|
|
22413
22743
|
return { preview: toText(content, len), ...relations ? { relations } : {} };
|
|
22414
22744
|
}
|
|
22415
22745
|
|
|
22746
|
+
// src/tools-master.ts
|
|
22747
|
+
var txt2 = (obj) => ({
|
|
22748
|
+
content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }]
|
|
22749
|
+
});
|
|
22750
|
+
var AREAS = ["biography", "goals", "preferences"];
|
|
22751
|
+
function registerMasterTools(server, trilium, brainRef) {
|
|
22752
|
+
const b = () => brainRef.config;
|
|
22753
|
+
server.tool("master", `Read a Master singleton: biography, goals, or preferences. Returns its id, content, and
|
|
22754
|
+
relation snippet.
|
|
22755
|
+
|
|
22756
|
+
section="<heading>" reads ONE section instead of the whole note \u2014 the efficient path on
|
|
22757
|
+
preferences, whose schedule tables make it the largest of the three, when the session needs one
|
|
22758
|
+
day's blocks rather than the whole week. outline(id) lists the headings.`, {
|
|
22759
|
+
which: exports_external.enum(AREAS),
|
|
22760
|
+
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
22761
|
+
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
22762
|
+
}, async ({ which, section, occurrence }) => {
|
|
22763
|
+
const id = b().master[which];
|
|
22764
|
+
if (!id)
|
|
22765
|
+
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
22766
|
+
const read = await readFull(trilium, id, { section, occurrence });
|
|
22767
|
+
const { id: _id, title: _title, kind: _kind, ...rest } = read;
|
|
22768
|
+
return txt2({ which, id, ...rest });
|
|
22769
|
+
});
|
|
22770
|
+
server.tool("master_recall", "Skim all three Master singletons \u2014 the opening lines of biography, goals, and preferences (with ids and relation snippets).", {}, async () => {
|
|
22771
|
+
const out = {};
|
|
22772
|
+
for (const which of AREAS) {
|
|
22773
|
+
const id = b().master[which];
|
|
22774
|
+
if (!id)
|
|
22775
|
+
continue;
|
|
22776
|
+
const [note, content] = await Promise.all([trilium.getNote(id).catch(() => null), trilium.getNoteContent(id).catch(() => "")]);
|
|
22777
|
+
const relations = note ? relationSnippet(note) : undefined;
|
|
22778
|
+
out[which] = { id, preview: toText(content, 200), ...relations ? { relations } : {} };
|
|
22779
|
+
}
|
|
22780
|
+
return txt2(out);
|
|
22781
|
+
});
|
|
22782
|
+
}
|
|
22783
|
+
|
|
22416
22784
|
// src/tools-llm.ts
|
|
22417
22785
|
function registerLlmTools(server, trilium, brainRef) {
|
|
22418
22786
|
const b = () => brainRef.config;
|
|
22419
|
-
server.tool("llm",
|
|
22787
|
+
server.tool("llm", `Read an LLM note: responsibilities or protocols (singletons), or a diary entry by id.
|
|
22788
|
+
|
|
22789
|
+
section="<heading>" reads ONE section instead of the whole note \u2014 the efficient path on
|
|
22790
|
+
protocols, which is the largest singleton and the one most often needed in part rather than
|
|
22791
|
+
whole. outline(id) lists the headings.`, {
|
|
22420
22792
|
which: exports_external.enum(["responsibilities", "protocols", "diary"]),
|
|
22421
|
-
id: exports_external.string().optional().describe("diary only: the entry id from llm_recall")
|
|
22422
|
-
|
|
22793
|
+
id: exports_external.string().optional().describe("diary only: the entry id from llm_recall"),
|
|
22794
|
+
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
22795
|
+
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
22796
|
+
}, async ({ which, id, section, occurrence }) => {
|
|
22423
22797
|
if (which === "diary") {
|
|
22424
22798
|
if (!id)
|
|
22425
22799
|
throw new Error("Reading a diary entry needs its id \u2014 use llm_recall to find one.");
|
|
22426
|
-
return
|
|
22800
|
+
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
22427
22801
|
}
|
|
22428
22802
|
const noteId = b().llm[which];
|
|
22429
22803
|
if (!noteId)
|
|
22430
22804
|
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
22431
|
-
return
|
|
22805
|
+
return txt(await readFull(trilium, noteId, { section, occurrence }));
|
|
22432
22806
|
});
|
|
22433
22807
|
server.tool("llm_recall", "Skim the LLM surface: responsibilities & protocols opening lines (with relation snippets), plus recent diary entries.", { limit: exports_external.number().optional() }, async ({ limit }) => {
|
|
22434
22808
|
const cfg = b();
|
|
@@ -22437,7 +22811,7 @@ function registerLlmTools(server, trilium, brainRef) {
|
|
|
22437
22811
|
previewWithRelations(trilium, cfg.llm.protocols),
|
|
22438
22812
|
skim(trilium, cfg.llm.diary, { kind: "diary", limit: limit ?? 7 })
|
|
22439
22813
|
]);
|
|
22440
|
-
return
|
|
22814
|
+
return txt({
|
|
22441
22815
|
responsibilities: { id: cfg.llm.responsibilities, ...resp },
|
|
22442
22816
|
protocols: { id: cfg.llm.protocols, ...prot },
|
|
22443
22817
|
diary
|
|
@@ -22449,21 +22823,28 @@ function registerLlmTools(server, trilium, brainRef) {
|
|
|
22449
22823
|
function registerMemoryTools(server, trilium, brainRef) {
|
|
22450
22824
|
const b = () => brainRef.config;
|
|
22451
22825
|
server.tool("memory", `Read a Memory note in full by id \u2014 a thread or a session. A thread book returns its
|
|
22452
|
-
Context/Resolution plus a children index ({id, date,
|
|
22453
|
-
flat body \u2014 the day-to-day content lives in those [yyyy-mm-dd] children, not the book.
|
|
22454
|
-
|
|
22455
|
-
|
|
22826
|
+
Context/Resolution plus a children index ({id, date, blocks[]}, newest first) instead of a
|
|
22827
|
+
flat body \u2014 the day-to-day content lives in those [yyyy-mm-dd] children, not the book. Each
|
|
22828
|
+
child is indexed by its ADDENDUM BLOCKS \u2014 marker, identification line, and lead \u2014 rather than a
|
|
22829
|
+
slice from the top of the body, which on a record note is the header and therefore identical on
|
|
22830
|
+
every child.
|
|
22831
|
+
|
|
22832
|
+
Pass date="yyyy-mm-dd" to resolve straight to that day's child in the same call, or call
|
|
22833
|
+
memory() again with a child's id from the index. section="<heading>" reads one section instead
|
|
22834
|
+
of the whole body \u2014 use it on long sessions and consolidated thread Contexts.`, {
|
|
22456
22835
|
id: exports_external.string(),
|
|
22457
|
-
date: exports_external.string().optional().describe("Thread books only: resolve directly to this day's child note (yyyy-mm-dd)")
|
|
22458
|
-
|
|
22836
|
+
date: exports_external.string().optional().describe("Thread books only: resolve directly to this day's child note (yyyy-mm-dd)"),
|
|
22837
|
+
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
22838
|
+
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
22839
|
+
}, async ({ id, date: date4, section, occurrence }) => {
|
|
22459
22840
|
const note = await trilium.getNote(id).catch(() => null);
|
|
22460
22841
|
if (!note || labelOf(note, "noteType") !== "thread")
|
|
22461
|
-
return
|
|
22842
|
+
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
22462
22843
|
if (date4) {
|
|
22463
22844
|
const child = await trilium.searchNotes(`#noteType=threadEntry #created='${date4}'`, { ancestorNoteId: id, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
22464
22845
|
if (child.results[0])
|
|
22465
|
-
return
|
|
22466
|
-
return
|
|
22846
|
+
return txt(await readFull(trilium, child.results[0].noteId, { section, occurrence }));
|
|
22847
|
+
return txt({ id, title: note.title, kind: "thread", note: `No entry for ${date4}.` });
|
|
22467
22848
|
}
|
|
22468
22849
|
const children = await trilium.searchNotes("#noteType=threadEntry", {
|
|
22469
22850
|
ancestorNoteId: id,
|
|
@@ -22472,13 +22853,17 @@ again with a child's id from the index.`, {
|
|
|
22472
22853
|
orderBy: "dateCreated",
|
|
22473
22854
|
orderDirection: "desc"
|
|
22474
22855
|
}).catch(() => ({ results: [] }));
|
|
22475
|
-
const entries = await Promise.all(children.results.map(async (c) =>
|
|
22476
|
-
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22856
|
+
const entries = await Promise.all(children.results.map(async (c) => {
|
|
22857
|
+
const content = await trilium.getNoteContent(c.noteId).catch(() => "");
|
|
22858
|
+
const blocks = addendumIndex(content);
|
|
22859
|
+
return {
|
|
22860
|
+
id: c.noteId,
|
|
22861
|
+
date: labelOf(c, "created") ?? c.dateCreated.slice(0, 10),
|
|
22862
|
+
...blocks.length ? { blocks } : { preview: toText(content, 160) }
|
|
22863
|
+
};
|
|
22864
|
+
}));
|
|
22865
|
+
const full = await readFull(trilium, id, { section, occurrence });
|
|
22866
|
+
return txt({ ...full, children: entries });
|
|
22482
22867
|
});
|
|
22483
22868
|
server.tool("memory_recall", "Skim Memory: active threads and recent sessions (ids + previews). An optional query filters threads by title.", { query: exports_external.string().optional(), limit: exports_external.number().optional() }, async ({ query, limit }) => {
|
|
22484
22869
|
const cfg = b();
|
|
@@ -22501,14 +22886,24 @@ again with a child's id from the index.`, {
|
|
|
22501
22886
|
const content = await trilium.getNoteContent(child.noteId).catch(() => "");
|
|
22502
22887
|
return { ...t, latestActivity: { date: date4, preview: toText(content, 160) } };
|
|
22503
22888
|
}));
|
|
22504
|
-
return
|
|
22889
|
+
return txt({ threads, sessions });
|
|
22505
22890
|
});
|
|
22506
22891
|
}
|
|
22507
22892
|
|
|
22508
22893
|
// src/tools-knowledge.ts
|
|
22509
22894
|
function registerKnowledgeTools(server, trilium, brainRef) {
|
|
22510
22895
|
const b = () => brainRef.config;
|
|
22511
|
-
server.tool("knowledge",
|
|
22896
|
+
server.tool("knowledge", `Read a Knowledge note by id \u2014 a user-knowledge note, a domain information note, or a Sources note.
|
|
22897
|
+
|
|
22898
|
+
Pass section="<heading>" to read ONE section instead of the whole body \u2014 the read-side
|
|
22899
|
+
counterpart to revise(section=), matching on the same heading contract, so a name that reads
|
|
22900
|
+
also writes. Use it on anything large: a note past the read ceiling cannot be returned whole at
|
|
22901
|
+
all, and outline(id) lists the headings to choose from. occurrence= disambiguates repeated
|
|
22902
|
+
heading texts.`, {
|
|
22903
|
+
id: exports_external.string(),
|
|
22904
|
+
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
22905
|
+
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
22906
|
+
}, async ({ id, section, occurrence }) => txt(await readFull(trilium, id, { section, occurrence })));
|
|
22512
22907
|
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: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ query, domain }) => {
|
|
22513
22908
|
const cfg = b();
|
|
22514
22909
|
if (domain) {
|
|
@@ -22516,11 +22911,11 @@ function registerKnowledgeTools(server, trilium, brainRef) {
|
|
|
22516
22911
|
const found = await trilium.searchNotes(`#noteType=domain #domain='${slug}'`, { ancestorNoteId: cfg.knowledge.domains, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
22517
22912
|
const dom = found.results[0];
|
|
22518
22913
|
if (!dom)
|
|
22519
|
-
return
|
|
22520
|
-
return
|
|
22914
|
+
return txt({ note: `No domain "${domain}" yet.` });
|
|
22915
|
+
return txt({ domain: dom.title, contents: await skim(trilium, dom.noteId, { query, limit: 50 }) });
|
|
22521
22916
|
}
|
|
22522
22917
|
const domains = await skim(trilium, cfg.knowledge.domains, { kind: "domain", limit: 50 });
|
|
22523
|
-
return
|
|
22918
|
+
return txt({
|
|
22524
22919
|
userKnowledge: await skim(trilium, cfg.knowledge.master, { kind: "user", query, limit: 30 }),
|
|
22525
22920
|
domains: domains.map((d) => ({ id: d.id, title: d.title }))
|
|
22526
22921
|
});
|
|
@@ -22530,14 +22925,19 @@ function registerKnowledgeTools(server, trilium, brainRef) {
|
|
|
22530
22925
|
// src/tools-insights.ts
|
|
22531
22926
|
function registerInsightsTools(server, trilium, brainRef) {
|
|
22532
22927
|
const b = () => brainRef.config;
|
|
22533
|
-
server.tool("insights",
|
|
22928
|
+
server.tool("insights", `Read the BrainLLM's change log for a day (default: today). Format: YYYY-MM-DD.
|
|
22929
|
+
section="<heading>" reads one section instead of the whole log.`, {
|
|
22930
|
+
date: exports_external.string().optional(),
|
|
22931
|
+
section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole log"),
|
|
22932
|
+
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
22933
|
+
}, async ({ date: date4, section, occurrence }) => {
|
|
22534
22934
|
const d = date4 ?? localToday();
|
|
22535
22935
|
const found = await trilium.searchNotes(`#noteType=log #created='${d}'`, { ancestorNoteId: b().insights.logs, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
22536
22936
|
if (!found.results[0])
|
|
22537
|
-
return
|
|
22538
|
-
return
|
|
22937
|
+
return txt({ date: d, note: "No log for this day." });
|
|
22938
|
+
return txt(await readFull(trilium, found.results[0].noteId, { section, occurrence }));
|
|
22539
22939
|
});
|
|
22540
|
-
server.tool("insights_recall", "Skim recent change logs (days + previews).", { limit: exports_external.number().optional() }, async ({ limit }) =>
|
|
22940
|
+
server.tool("insights_recall", "Skim recent change logs (days + previews).", { limit: exports_external.number().optional() }, async ({ limit }) => txt(await skim(trilium, b().insights.logs, { kind: "log", limit: limit ?? 14 })));
|
|
22541
22941
|
}
|
|
22542
22942
|
|
|
22543
22943
|
// src/tools-advanced.ts
|
|
@@ -22940,7 +23340,11 @@ ${block}` }));
|
|
|
22940
23340
|
await trilium.addLabel(noteId, "created", d);
|
|
22941
23341
|
return { noteId, action: "created" };
|
|
22942
23342
|
}
|
|
22943
|
-
server.tool("start", `Boot BrainLLM \u2014 call ONCE at the start of every session, before responding.
|
|
23343
|
+
server.tool("start", `Boot BrainLLM \u2014 call ONCE at the start of every session, before responding. Master
|
|
23344
|
+
PREFERENCES and LLM PROTOCOLS always come back in full, even at the default digest depth: they
|
|
23345
|
+
carry the schedule, the working style and the operating rules governing the session itself, so a
|
|
23346
|
+
session needs them whole before it knows enough to ask for them. Biography, goals and
|
|
23347
|
+
responsibilities come back as section headings + preview.
|
|
22944
23348
|
Runs maintenance, creates today's diary and session notes if not yet open, then returns: today
|
|
22945
23349
|
and weekday, the Master digest (biography / goals / preferences), the LLM digest
|
|
22946
23350
|
(responsibilities / protocols / today's diary preview and ID), this session's note ID, active
|
|
@@ -23044,7 +23448,7 @@ sessions need one section, not five documents.`, {
|
|
|
23044
23448
|
lastSession: digest.lastSession ?? null,
|
|
23045
23449
|
changesSinceLastSession: changesSinceLastSession.length ? changesSinceLastSession : undefined,
|
|
23046
23450
|
...newDay ? { newDay: true, newDayHint: "First session of the day \u2014 call day() for the sweep payload (previous session + log + changes + monthly deliverables in one call)." } : {},
|
|
23047
|
-
...(depth ?? "digest") === "digest" ? { depthHint: '
|
|
23451
|
+
...(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.' } : {},
|
|
23048
23452
|
hygiene: { scanned: hygiene.scanned, fixed: hygiene.fixed.length, transitions: hygiene.transitions, flagged: hygiene.flagged, ...hygiene.suppressed ? { suppressed: hygiene.suppressed } : {} }
|
|
23049
23453
|
});
|
|
23050
23454
|
});
|
|
@@ -23057,6 +23461,14 @@ instead (rarely needed; token-heavy).
|
|
|
23057
23461
|
|
|
23058
23462
|
Idempotent: fetches are read-only, the sweep is non-destructive, safe to call multiple times.
|
|
23059
23463
|
|
|
23464
|
+
Returns pending= \u2014 how much each remaining step actually has to do (addendum markers outstanding,
|
|
23465
|
+
maintenance flags already raised, diary blocks written today, which singletons were written
|
|
23466
|
+
today). The close protocol is ~7 tools landing exactly when context is scarcest, and reciting
|
|
23467
|
+
every step unconditionally spends that context on steps with nothing to do. Also returns audit=,
|
|
23468
|
+
a cross-singleton check nothing else performs: whether the five singletons agree with each other,
|
|
23469
|
+
AND whether the LLM's operating rules still serve what the user's goals and preferences call for
|
|
23470
|
+
\u2014 a semantic question, not a textual one, which consistency() and maintain() cannot answer.
|
|
23471
|
+
|
|
23060
23472
|
After session() returns, work through this protocol \u2014 order doesn't matter mechanically (each
|
|
23061
23473
|
step is tracked by the tool call itself, not by sequence), but close() enforces that every one
|
|
23062
23474
|
of diary(), session() [this call], remarks(), addendum(), and maintain() actually ran before it
|
|
@@ -23112,6 +23524,24 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
23112
23524
|
}
|
|
23113
23525
|
}
|
|
23114
23526
|
const hygiene = await sweep(trilium, cfg, { deep: false, dryRun: false }).catch(() => null);
|
|
23527
|
+
const pendingAddendums = await trilium.searchNotes("#noteType note.content *=* 'Addendum'", { ancestorNoteId: cfg.root, fastSearch: false, limit: 40 }).then((r) => r.results.filter((n) => {
|
|
23528
|
+
const kind = labelOf2(n, "noteType");
|
|
23529
|
+
return kind && !["session", "diary", "log", "threadEntry", "thread"].includes(kind);
|
|
23530
|
+
}).length).catch(() => null);
|
|
23531
|
+
const singletonStubs = [
|
|
23532
|
+
["biography", biography],
|
|
23533
|
+
["goals", goals],
|
|
23534
|
+
["preferences", preferences],
|
|
23535
|
+
["responsibilities", responsibilities],
|
|
23536
|
+
["protocols", protocols]
|
|
23537
|
+
];
|
|
23538
|
+
const touchedToday = singletonStubs.filter(([, s]) => s.lastModified === d).map(([name]) => name);
|
|
23539
|
+
const pending = {
|
|
23540
|
+
addendums: pendingAddendums === null ? "unknown" : pendingAddendums,
|
|
23541
|
+
maintenanceFlags: hygiene?.flagged.length ?? 0,
|
|
23542
|
+
diaryBlocksToday: diaryEntry?.blocks ?? 0,
|
|
23543
|
+
singletonsWrittenToday: touchedToday.length ? touchedToday : "none"
|
|
23544
|
+
};
|
|
23115
23545
|
const scoped = scope === "agent";
|
|
23116
23546
|
return txt4({
|
|
23117
23547
|
date: d,
|
|
@@ -23121,13 +23551,20 @@ rather than listing them unconditionally for every caller to work around.`, {
|
|
|
23121
23551
|
llm: { responsibilities, protocols },
|
|
23122
23552
|
diary: diaryEntry,
|
|
23123
23553
|
maintenance: hygiene ? { scanned: hygiene.scanned, fixed: hygiene.fixed.length, transitions: hygiene.transitions, flagged: hygiene.flagged, ...hygiene.suppressed ? { suppressed: hygiene.suppressed } : {} } : "skipped",
|
|
23554
|
+
pending,
|
|
23555
|
+
audit: scoped ? undefined : {
|
|
23556
|
+
consistency: "Read all five singletons and check for ambiguity, internal contradiction, or claims that disagree across them. Fix what you find with revise() BEFORE close(), so the log records a brain that already agrees with itself.",
|
|
23557
|
+
alignment: "Then check correlation, not just agreement: do responsibilities and protocols actually serve what biography, goals and preferences describe? A protocol can be perfectly consistent and still be serving a goal that has moved.",
|
|
23558
|
+
readThem: `master("biography"|"goals"|"preferences") and llm("responsibilities"|"protocols") \u2014 or one section at a time with section=. Written today: ${touchedToday.length ? touchedToday.join(", ") : "none"}.`
|
|
23559
|
+
},
|
|
23124
23560
|
next: [
|
|
23125
23561
|
...scoped ? ["Scoped run \u2014 the user's master singletons and your LLM singletons are OUT of scope and deliberately not listed here."] : [
|
|
23126
23562
|
"Update master singletons (biography / goals / preferences) via revise() with session observations about the user.",
|
|
23127
23563
|
"Update LLM singletons (responsibilities / protocols) via revise() with session observations about yourself."
|
|
23128
23564
|
],
|
|
23129
|
-
scoped ?
|
|
23130
|
-
scoped ?
|
|
23565
|
+
...scoped ? [] : ["Audit the singletons against each other and against each other's PURPOSE \u2014 see audit= above. This is the step that catches an operating rule still serving a goal that has moved."],
|
|
23566
|
+
scoped ? `Call addendum() \u2014 fold only what is in your lane; leaving out-of-scope addendums for the next interactive session is correct, and the call itself satisfies the gate. (${pending.addendums} note(s) currently carry addendum markers.)` : pending.addendums === 0 ? "Call addendum() \u2014 0 notes currently carry addendum markers, so this should come back clean; the call still satisfies the gate." : `Call addendum() \u2014 ${pending.addendums} note(s) carry addendum markers; fold each into its section.`,
|
|
23567
|
+
scoped ? "Call maintain() \u2014 audit brain hygiene; pass domain= to keep the flags in your lane." : pending.maintenanceFlags === 0 ? "Call maintain() \u2014 the lite sweep just ran clean, so this is a formality unless you want deep=true." : `Call maintain() \u2014 the lite sweep already flagged ${pending.maintenanceFlags} item(s), listed above.`,
|
|
23131
23568
|
"Call remarks() \u2014 get the diary cues: your experience, opinions, and existence this session, plus BrainLLM remarks.",
|
|
23132
23569
|
"Call diary() \u2014 write the day's unfiltered record with the cues in hand; the gate counts it only after remarks().",
|
|
23133
23570
|
"Call close() \u2014 commit the session log (mandatory, last). Refuses until every step ran and session \u2192 remarks \u2192 diary held; pass force=true only when a skipped step genuinely has nothing to log."
|
|
@@ -23387,13 +23824,15 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23387
23824
|
})).optional().describe("kind=sources only: upsert Revision-table rows by source name \u2014 re-verifying a source replaces its existing row's Marker/Date in place instead of appending a new one"),
|
|
23388
23825
|
topics: exports_external.array(exports_external.string()).optional().describe("Topic tags \u2014 slugged server-side"),
|
|
23389
23826
|
supersedes: exports_external.string().optional().describe("noteId this replaces \u2014 old note is archived and wired supersedes"),
|
|
23827
|
+
mustCreate: exports_external.boolean().optional().describe("Refuse instead of adopting an existing note when the title already exists \u2014 turns a silent overwrite on a generic title (Current State, Sources, Technology Stack) into a catchable error"),
|
|
23390
23828
|
connect: exports_external.array(exports_external.object({
|
|
23391
23829
|
relation: exports_external.enum(RelationTypes),
|
|
23392
23830
|
toNoteId: exports_external.string()
|
|
23393
23831
|
})).optional().describe("Relations to wire from this note in the same call \u2014 same semantics as connect() (idempotent, worksWith wired both ways)"),
|
|
23394
23832
|
icon: exports_external.string().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name ("brain"); normalized server-side'),
|
|
23395
23833
|
date: exports_external.string().optional().describe("ISO date override (default: today)")
|
|
23396
|
-
}, async ({ kind, title, body, goal, identity, domain, revision, topics, supersedes, connect: connectRels, icon, date: date4 }) => {
|
|
23834
|
+
}, async ({ kind, title, body, goal, identity, domain, revision, topics, supersedes, mustCreate, connect: connectRels, icon, date: date4 }) => {
|
|
23835
|
+
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.`);
|
|
23397
23836
|
const opts = { domain, topics, date: date4 };
|
|
23398
23837
|
const d = date4 ?? today();
|
|
23399
23838
|
const { html, warnings: sanitizeWarnings } = renderBody(body ?? "");
|
|
@@ -23450,6 +23889,7 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23450
23889
|
const found = sourcesId ? { results: [] } : await trilium.searchNotes("#noteType=sources", { ancestorNoteId: domainId, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
23451
23890
|
let sid = sourcesId ?? found.results[0]?.noteId;
|
|
23452
23891
|
let wrote = false;
|
|
23892
|
+
let sourceMerge = {};
|
|
23453
23893
|
if (!sid) {
|
|
23454
23894
|
const created2 = await trilium.createNote(domainId, "Sources", contentFor("sources", { date: d, body: html, domain: domainTitle }));
|
|
23455
23895
|
sid = created2.note.noteId;
|
|
@@ -23461,7 +23901,8 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23461
23901
|
const current = await trilium.getNoteContent(sid).catch(() => "");
|
|
23462
23902
|
if (!current.includes(html)) {
|
|
23463
23903
|
await trilium.createRevision(sid).catch(() => null);
|
|
23464
|
-
const merged =
|
|
23904
|
+
const merged = mergeUnderSection(current, html, "Sources");
|
|
23905
|
+
sourceMerge = { ...merged.mergedInto.length ? { mergedIntoGroups: merged.mergedInto } : {}, ...merged.appended.length ? { newGroups: merged.appended } : {} };
|
|
23465
23906
|
const stamped = bumpLastUpdated(merged.html, d);
|
|
23466
23907
|
await trilium.updateNoteContent(sid, stamped.html);
|
|
23467
23908
|
await trilium.updateLabelValue(sid, "updated", d);
|
|
@@ -23505,6 +23946,7 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23505
23946
|
location: locationLabel(kind, domainTitle),
|
|
23506
23947
|
...createdDomain ? { createdDomain: domainTitle } : {},
|
|
23507
23948
|
...revisionChanges.length ? { revision: revisionChanges } : {},
|
|
23949
|
+
...sourceMerge,
|
|
23508
23950
|
...revisionKeys.length ? { revisionRows: revisionKeys } : {},
|
|
23509
23951
|
...placeholderLeft ? { structureHint: "The Revision table still holds only its placeholder row. Every source marked \u2705 was verified by someone \u2014 record that with revision=[{source, marker, date}] so the table says so too; marker dates live there, never inline." } : {},
|
|
23510
23952
|
...looseProse ? { placementHint: "The body was appended to the Sources section as a loose block, which is rarely what a maintained source list wants. Fold it into the existing entries with revise(noteId, find=\u2026) \u2014 or pass list markup so it joins the list structurally." } : {},
|
|
@@ -23523,6 +23965,8 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23523
23965
|
const { domainId, domainTitle, createdDomain } = await resolveDomain(trilium, b(), domain);
|
|
23524
23966
|
const inDomain = await trilium.searchNotes("#noteType=information", { ancestorNoteId: domainId, fastSearch: true, limit: 100 }).catch(() => ({ results: [] }));
|
|
23525
23967
|
const existing2 = inDomain.results.find((n) => sameTitle(n.title, subTitle));
|
|
23968
|
+
if (existing2 && mustCreate)
|
|
23969
|
+
return refuseAdoption(existing2.noteId, existing2.title, `in domain "${domainTitle}"`);
|
|
23526
23970
|
if (existing2) {
|
|
23527
23971
|
const current = await trilium.getNoteContent(existing2.noteId).catch(() => "");
|
|
23528
23972
|
if (isDuplicateAppend(current, html))
|
|
@@ -23560,6 +24004,8 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23560
24004
|
return err("rejected_kind", "Diary entries use the dedicated diary() tool.", `Call diary(body="...") to write today's diary entry.`);
|
|
23561
24005
|
if (kind === "session")
|
|
23562
24006
|
return err("rejected_kind", "Session notes are written by close().", 'Call close(summary="...") to log this session.');
|
|
24007
|
+
if (kind === "claim")
|
|
24008
|
+
return err("rejected_kind", "Claims are registered by claim().", 'Call claim(assertion="...", check="...") \u2014 a claim carries a verification schedule and history that a generic write cannot set up.');
|
|
23563
24009
|
if (kind === "log")
|
|
23564
24010
|
return err("rejected_kind", "Log notes are auto-generated by close() and cannot be written manually.");
|
|
23565
24011
|
if (kind === "domain")
|
|
@@ -23570,6 +24016,8 @@ For diary entries use the dedicated diary() tool \u2014 remember(kind="diary") i
|
|
|
23570
24016
|
if (!cleanTitle)
|
|
23571
24017
|
return err("missing_param", `kind="${kind}" requires a title.`, 'Add title="<note title>" to your call.');
|
|
23572
24018
|
const existing = await findExisting(kind, cleanTitle);
|
|
24019
|
+
if (existing && mustCreate)
|
|
24020
|
+
return refuseAdoption(existing.noteId, existing.title, `in ${KIND_AREA[kind] ?? "the brain"}`);
|
|
23573
24021
|
if (existing) {
|
|
23574
24022
|
if (kind === "thread" && !identity && !leadingIdentification(html)) {
|
|
23575
24023
|
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.`);
|
|
@@ -23732,25 +24180,38 @@ a doubly-escaped tag", "which cite a 2026 date"). Backslashes must be escaped.`,
|
|
|
23732
24180
|
} catch (e) {
|
|
23733
24181
|
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
23734
24182
|
}
|
|
23735
|
-
const candidates = (await run(`note.content %= '${
|
|
24183
|
+
const candidates = (await run(`note.content %= '${escapeQueryRegex(regex)}'`)).filter(filterNote);
|
|
23736
24184
|
const confirmed = [];
|
|
23737
24185
|
let rejected = 0;
|
|
24186
|
+
let tagSpanning = 0;
|
|
23738
24187
|
for (const n of candidates) {
|
|
23739
24188
|
if (confirmed.length >= max)
|
|
23740
24189
|
break;
|
|
23741
24190
|
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
23742
|
-
if (content
|
|
24191
|
+
if (!content) {
|
|
24192
|
+
rejected++;
|
|
24193
|
+
continue;
|
|
24194
|
+
}
|
|
24195
|
+
const rawHit = re.test(content);
|
|
24196
|
+
const projectedHit = !rawHit && re.test(stripTagsWithMap(content).text);
|
|
24197
|
+
if (rawHit || projectedHit) {
|
|
24198
|
+
if (projectedHit)
|
|
24199
|
+
tagSpanning++;
|
|
23743
24200
|
confirmed.push(n);
|
|
23744
|
-
else
|
|
24201
|
+
} else
|
|
23745
24202
|
rejected++;
|
|
23746
24203
|
}
|
|
23747
24204
|
const results2 = await Promise.all(confirmed.map(buildResult));
|
|
24205
|
+
const notes = [
|
|
24206
|
+
rejected ? `${rejected} backend candidate(s) did not actually match the pattern and were dropped \u2014 results are verified against the real regex, not just the search index.` : null,
|
|
24207
|
+
tagSpanning ? `${tagSpanning} match(es) were found only after stripping markup \u2014 the phrase is split by an inline tag there.` : null,
|
|
24208
|
+
results2.length === 0 ? "No bodies matched that pattern, searched both as stored HTML and tag-stripped. Note that Trilium's %= pre-filter reads a striptags'd copy, so a pattern anchored ON tags may never reach verification \u2014 consistency() scans exhaustively if you need certainty." : null
|
|
24209
|
+
].filter(Boolean);
|
|
23748
24210
|
return txt4({
|
|
23749
24211
|
mode: "regex",
|
|
23750
24212
|
pattern: regex,
|
|
23751
24213
|
results: results2,
|
|
23752
|
-
...
|
|
23753
|
-
...results2.length === 0 ? { note: "No bodies matched that pattern. Regex runs against stored HTML, so anchor on what is actually stored (tags and entities), not on rendered text." } : {}
|
|
24214
|
+
...notes.length ? { note: notes.join(" ") } : {}
|
|
23754
24215
|
});
|
|
23755
24216
|
}
|
|
23756
24217
|
if (orderBy) {
|
|
@@ -23913,7 +24374,7 @@ find=; to add content next to a heading without touching its body, use mode="bef
|
|
|
23913
24374
|
title: exports_external.string().optional().describe("New title (normalized server-side)"),
|
|
23914
24375
|
section: exports_external.string().optional().describe("Target a section by heading text (h2/h3/h4, in that order); omit for whole-note append/replace"),
|
|
23915
24376
|
occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading to target, 1-based (default: the first). Read them with outline(noteId)."),
|
|
23916
|
-
mode: exports_external.enum(["append", "replace", "before", "after"]).optional().describe('append (default) | replace | before | after \u2014 "before"/"after" insert adjacent to the section= heading without touching its body'),
|
|
24377
|
+
mode: exports_external.enum(["append", "replace", "before", "after", "remove"]).optional().describe('append (default) | replace | before | after | remove \u2014 "before"/"after" insert adjacent to the section= heading without touching its body; "remove" deletes the section= heading and its body, and needs no body='),
|
|
23917
24378
|
find: exports_external.string().optional().describe("Exact raw string to replace throughout the body with body= \u2014 targeted surgery without a read+full-replace. Takes precedence over section/mode."),
|
|
23918
24379
|
nth: exports_external.number().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
|
|
23919
24380
|
edits: exports_external.array(exports_external.object({
|
|
@@ -24001,23 +24462,37 @@ find=; to add content next to a heading without touching its body, use mode="bef
|
|
|
24001
24462
|
const tolerant = [...source.matchAll(rx)].map((m) => ({ start: m.index, length: m[0].length }));
|
|
24002
24463
|
return tolerant.length ? spans(tolerant, "attribute-tolerant") : null;
|
|
24003
24464
|
};
|
|
24004
|
-
const missHint = (needle) => {
|
|
24465
|
+
const missHint = (needle, source, alreadyConsumed) => {
|
|
24466
|
+
if (alreadyConsumed)
|
|
24467
|
+
return `Not found \u2014 but an EARLIER edit in this same edits= array already replaced this exact string. The array is applied in order against one body, so the second pass had nothing left to match. This is the expected result, not a failure.`;
|
|
24005
24468
|
if (looksEntityEscaped(needle))
|
|
24006
24469
|
return `Not found \u2014 the search string carries escaped markup (<\u2026>) while note bodies store real tags. Pass the tag literally, e.g. "<h3>Typography</h3>".`;
|
|
24007
24470
|
if (spansBlockBoundary(needle))
|
|
24008
24471
|
return `Not found \u2014 this string spans an element boundary (a closing tag followed by an opening one). Anchor the find INSIDE a single element instead, or target the heading directly with section= (with mode="before"/"after" to insert around it).`;
|
|
24009
|
-
return `Not found in the note body (exact or attribute-tolerant) \u2014 already replaced on a retry, or the text genuinely differs
|
|
24472
|
+
return `Not found in the note body (exact or attribute-tolerant) \u2014 already replaced on a retry, or the text genuinely differs.`;
|
|
24473
|
+
};
|
|
24474
|
+
const missContext = (needle, source) => {
|
|
24475
|
+
const near = nearestContext(source, needle);
|
|
24476
|
+
return near ? { matchedUpTo: near.fragment, storedNearby: near.context } : {};
|
|
24010
24477
|
};
|
|
24011
24478
|
const current = await trilium.getNoteContent(noteId).catch(() => "");
|
|
24012
24479
|
let working = current;
|
|
24013
24480
|
const results = [];
|
|
24014
24481
|
let total = 0;
|
|
24482
|
+
const consumed = new Set;
|
|
24015
24483
|
for (const e of plan) {
|
|
24016
24484
|
const applied = applyEdit(working, e.find, e.body, e.nth);
|
|
24017
24485
|
if (!applied) {
|
|
24018
|
-
|
|
24486
|
+
const alreadyConsumed = consumed.has(e.find);
|
|
24487
|
+
results.push({
|
|
24488
|
+
find: e.find,
|
|
24489
|
+
replaced: 0,
|
|
24490
|
+
hint: missHint(e.find, current, alreadyConsumed),
|
|
24491
|
+
...alreadyConsumed ? {} : missContext(e.find, working)
|
|
24492
|
+
});
|
|
24019
24493
|
continue;
|
|
24020
24494
|
}
|
|
24495
|
+
consumed.add(e.find);
|
|
24021
24496
|
working = applied.html;
|
|
24022
24497
|
total += applied.count;
|
|
24023
24498
|
results.push({ find: e.find, replaced: applied.count, matchMode: applied.matchMode });
|
|
@@ -24029,7 +24504,11 @@ find=; to add content next to a heading without touching its body, use mode="bef
|
|
|
24029
24504
|
mode: edits ? "edits" : "find-replace",
|
|
24030
24505
|
replaced: 0,
|
|
24031
24506
|
date: d,
|
|
24032
|
-
...edits ? { results } : {
|
|
24507
|
+
...edits ? { results } : {
|
|
24508
|
+
hint: results[0].hint,
|
|
24509
|
+
...results[0].matchedUpTo ? { matchedUpTo: results[0].matchedUpTo } : {},
|
|
24510
|
+
...results[0].storedNearby ? { storedNearby: results[0].storedNearby } : {}
|
|
24511
|
+
}
|
|
24033
24512
|
});
|
|
24034
24513
|
}
|
|
24035
24514
|
await trilium.createRevision(noteId).catch(() => null);
|
|
@@ -24062,6 +24541,45 @@ find=; to add content next to a heading without touching its body, use mode="bef
|
|
|
24062
24541
|
}
|
|
24063
24542
|
if ((mode2 === "before" || mode2 === "after") && !section)
|
|
24064
24543
|
return err("missing_param", `mode="${mode2}" inserts relative to a heading and needs one.`, 'Pass section="<heading text>" alongside it, or use mode="append" for a whole-note addendum.');
|
|
24544
|
+
if (mode2 === "remove" && !section)
|
|
24545
|
+
return err("missing_param", 'mode="remove" deletes a section and needs one.', 'Pass section="<heading text>". To delete the whole note use forget(noteId).');
|
|
24546
|
+
if (mode2 === "remove" && section) {
|
|
24547
|
+
const current = await trilium.getNoteContent(noteId).catch(() => "");
|
|
24548
|
+
const result = setSection(current, section, "", "remove", occurrence ?? 1);
|
|
24549
|
+
if (!result.matched) {
|
|
24550
|
+
return txt4({
|
|
24551
|
+
ok: true,
|
|
24552
|
+
noteId,
|
|
24553
|
+
mode: `section:remove:${section}`,
|
|
24554
|
+
matched: false,
|
|
24555
|
+
date: d,
|
|
24556
|
+
available: result.available,
|
|
24557
|
+
hint: `No "${section}" heading at h2/h3/h4 \u2014 nothing was removed and the body is untouched. Check available= for the note's real heading texts.`
|
|
24558
|
+
});
|
|
24559
|
+
}
|
|
24560
|
+
await trilium.createRevision(noteId).catch(() => null);
|
|
24561
|
+
const stamped = bumpLastUpdated(result.html, d);
|
|
24562
|
+
await trilium.updateNoteContent(noteId, stamped.html);
|
|
24563
|
+
const titledOnRemove = await applyTitle();
|
|
24564
|
+
const iconOnRemove = await applyIcon(noteId, icon);
|
|
24565
|
+
await trilium.updateLabelValue(noteId, "updated", d);
|
|
24566
|
+
if (labelOf2(note, "status") === "dormant")
|
|
24567
|
+
await trilium.updateLabelValue(noteId, "status", "active");
|
|
24568
|
+
const relsOnRemove = relationSnippet(note);
|
|
24569
|
+
return txt4({
|
|
24570
|
+
ok: true,
|
|
24571
|
+
noteId,
|
|
24572
|
+
mode: `section:remove:${section}`,
|
|
24573
|
+
matched: true,
|
|
24574
|
+
headingCount: result.headingCount,
|
|
24575
|
+
date: d,
|
|
24576
|
+
...result.replacedSubsections?.length ? { removedSubsections: result.replacedSubsections, displacedHint: `The removed section contained ${result.replacedSubsections.length} nested heading(s) \u2014 they went with it. A revision was taken first.` } : {},
|
|
24577
|
+
...titledOnRemove.retitled ? { retitled: titledOnRemove.retitled } : {},
|
|
24578
|
+
...iconOnRemove ? { icon: iconOnRemove } : {},
|
|
24579
|
+
...relsOnRemove ? { relations: relsOnRemove } : {},
|
|
24580
|
+
...structuralFindings(noteKind, stamped.html)
|
|
24581
|
+
});
|
|
24582
|
+
}
|
|
24065
24583
|
let finalContent = null;
|
|
24066
24584
|
let sectionMiss = {};
|
|
24067
24585
|
let threadEntryResult = null;
|
|
@@ -24079,7 +24597,11 @@ find=; to add content next to a heading without touching its body, use mode="bef
|
|
|
24079
24597
|
const result = setSection(current, section, html, sectionMode, occurrence ?? 1);
|
|
24080
24598
|
finalContent = bumpLastUpdated(result.html, d).html;
|
|
24081
24599
|
await trilium.updateNoteContent(noteId, finalContent);
|
|
24082
|
-
sectionResult = {
|
|
24600
|
+
sectionResult = {
|
|
24601
|
+
matched: result.matched,
|
|
24602
|
+
headingCount: result.headingCount,
|
|
24603
|
+
...result.replacedSubsections?.length ? { replacedSubsections: result.replacedSubsections } : {}
|
|
24604
|
+
};
|
|
24083
24605
|
sectionMiss = {
|
|
24084
24606
|
...result.appendedAtLevel ? { appendedAtLevel: result.appendedAtLevel } : {},
|
|
24085
24607
|
...result.available?.length ? { available: result.available } : {}
|
|
@@ -24121,9 +24643,13 @@ ${block}`), d).html;
|
|
|
24121
24643
|
return txt4({
|
|
24122
24644
|
ok: true,
|
|
24123
24645
|
noteId,
|
|
24124
|
-
mode: body ? section ? `section:${section}` : mode2 ?? "append" : "
|
|
24646
|
+
mode: body ? section ? `section:${mode2 === "before" || mode2 === "after" ? `insert-${mode2}` : mode2 === "append" ? "append-within" : "replace"}:${section}` : mode2 ?? "append" : titled.retitled ? "rename" : iconSet ? "icon" : "no-op",
|
|
24125
24647
|
date: d,
|
|
24126
24648
|
...sectionResult ? { matched: sectionResult.matched, headingCount: sectionResult.headingCount } : {},
|
|
24649
|
+
...sectionResult?.replacedSubsections?.length ? {
|
|
24650
|
+
replacedSubsections: sectionResult.replacedSubsections,
|
|
24651
|
+
displacedHint: `The replaced section contained ${sectionResult.replacedSubsections.length} nested heading(s) \u2014 they went with it. Re-add any that should have survived.`
|
|
24652
|
+
} : {},
|
|
24127
24653
|
...sectionMiss,
|
|
24128
24654
|
...sectionHint ? { hint: sectionHint } : {},
|
|
24129
24655
|
...threadEntryResult ? { entryId: threadEntryResult.noteId, entryAction: threadEntryResult.action } : {},
|
|
@@ -24361,14 +24887,21 @@ Pass a regex with ONE capture group naming the value that should agree:
|
|
|
24361
24887
|
|
|
24362
24888
|
Without a capture group it degrades to a presence check \u2014 which notes mention this at all.
|
|
24363
24889
|
|
|
24364
|
-
|
|
24365
|
-
|
|
24890
|
+
Matched against BOTH the stored HTML and a tag-stripped projection of it, so a phrase split by an
|
|
24891
|
+
inline <strong> or <code> tag is found, and so is a pattern deliberately anchored on tags.
|
|
24892
|
+
Escape backslashes. Scope with domain= or kinds= when the phrase is common.
|
|
24893
|
+
|
|
24894
|
+
Scans every in-scope note by default. Trilium's %= backend filter is a LOSSY pre-filter \u2014 it
|
|
24895
|
+
reads a striptags'd copy of the content, so it drops notes this tool should examine \u2014 and on a
|
|
24896
|
+
contradiction sweep a falsely clean result is worse than a slow one. Pass fast=true to use it
|
|
24897
|
+
anyway when scope is wide and speed matters more than completeness.`, {
|
|
24366
24898
|
pattern: exports_external.string().describe("Regex over note bodies. One capture group = the value that should agree across notes."),
|
|
24367
24899
|
domain: exports_external.string().optional().describe("Restrict to one knowledge domain"),
|
|
24368
24900
|
kinds: exports_external.array(exports_external.enum(Kinds)).optional().describe("Restrict to these kinds"),
|
|
24369
24901
|
includeArchived: exports_external.boolean().optional().describe("Include archived notes (default false)"),
|
|
24370
|
-
limit: exports_external.number().optional().describe("Max notes to examine (default 60)")
|
|
24371
|
-
|
|
24902
|
+
limit: exports_external.number().optional().describe("Max notes to examine (default 60)"),
|
|
24903
|
+
fast: exports_external.boolean().optional().describe("Pre-filter candidates with Trilium's %= operator \u2014 faster, but its striptags'd corpus silently drops notes (default: false, scan every in-scope note)")
|
|
24904
|
+
}, async ({ pattern, domain, kinds, includeArchived, limit, fast }) => {
|
|
24372
24905
|
let re;
|
|
24373
24906
|
try {
|
|
24374
24907
|
re = new RegExp(pattern, "gi");
|
|
@@ -24376,7 +24909,7 @@ Escape backslashes. Scope with domain= or kinds= when the phrase is common.`, {
|
|
|
24376
24909
|
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
24377
24910
|
}
|
|
24378
24911
|
const max = limit ?? 60;
|
|
24379
|
-
const clauses = [`note.content %= '${
|
|
24912
|
+
const clauses = fast ? [`note.content %= '${escapeQueryRegex(pattern)}'`] : ["#noteType"];
|
|
24380
24913
|
if (domain)
|
|
24381
24914
|
clauses.push(`#domain='${slugify(domain)}'`);
|
|
24382
24915
|
const notes = await trilium.searchNotes(clauses.join(" AND "), { ancestorNoteId: b().root, limit: max, includeArchivedNotes: includeArchived ?? false }).then((r) => r.results).catch(() => []);
|
|
@@ -24389,41 +24922,53 @@ Escape backslashes. Scope with domain= or kinds= when the phrase is common.`, {
|
|
|
24389
24922
|
const byValue = new Map;
|
|
24390
24923
|
const noCapture = [];
|
|
24391
24924
|
let hasCaptureGroup = false;
|
|
24392
|
-
|
|
24393
|
-
const
|
|
24394
|
-
|
|
24395
|
-
continue;
|
|
24396
|
-
const stub = { id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "" };
|
|
24397
|
-
const seen = new Set;
|
|
24925
|
+
const scan = (haystack) => {
|
|
24926
|
+
const values = [];
|
|
24927
|
+
let matched = false;
|
|
24398
24928
|
re.lastIndex = 0;
|
|
24399
24929
|
let m;
|
|
24400
|
-
|
|
24401
|
-
while ((m = re.exec(content)) !== null) {
|
|
24930
|
+
while ((m = re.exec(haystack)) !== null) {
|
|
24402
24931
|
matched = true;
|
|
24403
24932
|
const captured = m.slice(1).find((g) => g !== undefined);
|
|
24404
|
-
if (captured !== undefined)
|
|
24405
|
-
|
|
24406
|
-
const value = toText(captured, 120).trim() || captured.trim();
|
|
24407
|
-
if (seen.has(value))
|
|
24408
|
-
continue;
|
|
24409
|
-
seen.add(value);
|
|
24410
|
-
if (!byValue.has(value))
|
|
24411
|
-
byValue.set(value, []);
|
|
24412
|
-
byValue.get(value).push(stub);
|
|
24413
|
-
}
|
|
24933
|
+
if (captured !== undefined)
|
|
24934
|
+
values.push(toText(captured, 120).trim() || captured.trim());
|
|
24414
24935
|
if (m[0] === "")
|
|
24415
24936
|
re.lastIndex++;
|
|
24416
24937
|
}
|
|
24417
|
-
|
|
24938
|
+
return { values, matched };
|
|
24939
|
+
};
|
|
24940
|
+
let tagSpanning = 0;
|
|
24941
|
+
for (const n of scoped) {
|
|
24942
|
+
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
24943
|
+
if (!content)
|
|
24944
|
+
continue;
|
|
24945
|
+
const stub = { id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "" };
|
|
24946
|
+
const raw = scan(content);
|
|
24947
|
+
const projected = scan(stripTagsWithMap(content).text);
|
|
24948
|
+
if (projected.matched && !raw.matched)
|
|
24949
|
+
tagSpanning++;
|
|
24950
|
+
const seen = new Set;
|
|
24951
|
+
for (const value of [...raw.values, ...projected.values]) {
|
|
24952
|
+
hasCaptureGroup = true;
|
|
24953
|
+
if (seen.has(value))
|
|
24954
|
+
continue;
|
|
24955
|
+
seen.add(value);
|
|
24956
|
+
if (!byValue.has(value))
|
|
24957
|
+
byValue.set(value, []);
|
|
24958
|
+
byValue.get(value).push(stub);
|
|
24959
|
+
}
|
|
24960
|
+
if ((raw.matched || projected.matched) && !seen.size)
|
|
24418
24961
|
noCapture.push(stub);
|
|
24419
24962
|
}
|
|
24420
24963
|
if (!hasCaptureGroup) {
|
|
24421
24964
|
return txt4({
|
|
24422
24965
|
mode: "presence",
|
|
24423
24966
|
pattern,
|
|
24967
|
+
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
24968
|
+
notesExamined: scoped.length,
|
|
24424
24969
|
notes: noCapture,
|
|
24425
24970
|
total: noCapture.length,
|
|
24426
|
-
note: noCapture.length === 0 ?
|
|
24971
|
+
note: noCapture.length === 0 ? `No note body matched that pattern across ${scoped.length} note(s), searched both as stored HTML and tag-stripped.${fast ? " fast=true used Trilium's %= pre-filter, which reads a striptags'd copy and can drop notes \u2014 re-run without it before concluding anything." : " That is evidence about the pattern, not the brain."}` : 'The pattern has no capture group, so this is a presence check only. Add one \u2014 consistency("(\\\\d+) users") \u2014 to compare the values these notes actually assert.'
|
|
24427
24972
|
});
|
|
24428
24973
|
}
|
|
24429
24974
|
const groups = [...byValue.entries()].map(([value, notes2]) => ({ value, count: notes2.length, notes: notes2 })).sort((a, b2) => b2.count - a.count);
|
|
@@ -24431,8 +24976,10 @@ Escape backslashes. Scope with domain= or kinds= when the phrase is common.`, {
|
|
|
24431
24976
|
return txt4({
|
|
24432
24977
|
mode: "consistency",
|
|
24433
24978
|
pattern,
|
|
24979
|
+
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
24434
24980
|
...domain ? { domain: slugify(domain) } : {},
|
|
24435
24981
|
notesExamined: scoped.length,
|
|
24982
|
+
...tagSpanning ? { tagSpanning, tagSpanningNote: `${tagSpanning} note(s) matched only once markup was stripped \u2014 the phrase is split by an inline tag there. Before v10.3 those were invisible.` } : {},
|
|
24436
24983
|
distinctValues: groups.length,
|
|
24437
24984
|
agreement: groups.length === 0 ? "no-data" : agrees ? "unanimous" : "DISAGREEMENT",
|
|
24438
24985
|
groups,
|
|
@@ -24469,8 +25016,10 @@ remember(revision=) matches on exactly) without reading the note.`, {
|
|
|
24469
25016
|
headings: headings.map((h) => ({
|
|
24470
25017
|
level: h.level,
|
|
24471
25018
|
text: h.text,
|
|
24472
|
-
...h.occurrence > 1 ? { occurrence: h.occurrence } : {}
|
|
25019
|
+
...h.occurrence > 1 ? { occurrence: h.occurrence } : {},
|
|
25020
|
+
...h.raw ? { raw: h.raw } : {}
|
|
24473
25021
|
})),
|
|
25022
|
+
...headings.some((h) => h.raw) ? { rawHint: "Headings carrying inline markup also report raw \u2014 their STORED form. section= matches text (use it), find= matches stored HTML (use raw). Building a find= from text alone misses on exactly these headings." } : {},
|
|
24474
25023
|
sectionLevel: sectionLevelFor(content),
|
|
24475
25024
|
...tables.length ? { tables } : {},
|
|
24476
25025
|
...report.duplicateHeadings.length ? { duplicateHeadings: report.duplicateHeadings } : {},
|
|
@@ -24487,19 +25036,28 @@ surface reads and explore(): reach for it when you need the raw label set, the b
|
|
|
24487
25036
|
or the attachment inventory \u2014 confirming a fix landed, debugging drift \u2014 rather than a
|
|
24488
25037
|
kind-specific summary. Read-only, safe on any note including structural containers.
|
|
24489
25038
|
|
|
25039
|
+
Pass section="<heading>" alongside content=true to get one section's raw body instead of the
|
|
25040
|
+
whole thing \u2014 the same heading contract revise(section=) writes through.
|
|
25041
|
+
|
|
24490
25042
|
Pass find="<literal>" to count occurrences of a literal string in the body \u2014 total plus a
|
|
24491
25043
|
per-addendum-block breakdown. The staleness-escalation counter: "how many prior entries
|
|
24492
|
-
mention this carried flag" becomes one call instead of a full read + manual counting
|
|
25044
|
+
mention this carried flag" becomes one call instead of a full read + manual counting. On zero
|
|
25045
|
+
occurrences it returns the nearest fragment that IS present and the stored text around it, so a
|
|
25046
|
+
miss is diagnosed in the same call rather than in three more.`, {
|
|
24493
25047
|
noteId: exports_external.string().describe("Note to inspect"),
|
|
24494
25048
|
content: exports_external.boolean().optional().describe("Include the note's raw body content (default: false)"),
|
|
25049
|
+
section: exports_external.string().optional().describe("With content=true, return only this heading's section rather than the whole body"),
|
|
24495
25050
|
find: exports_external.string().optional().describe("Literal string to count in the body \u2014 returns total occurrences + per-addendum-block counts (flag-staleness tracking)")
|
|
24496
|
-
}, async ({ noteId, content, find }) => {
|
|
25051
|
+
}, async ({ noteId, content, section, find }) => {
|
|
24497
25052
|
const [note, attachments, rawBody] = await Promise.all([
|
|
24498
25053
|
trilium.getNote(noteId),
|
|
24499
25054
|
trilium.getNoteAttachments(noteId).catch(() => []),
|
|
24500
25055
|
content || find ? trilium.getNoteContent(noteId).catch(() => "") : Promise.resolve(undefined)
|
|
24501
25056
|
]);
|
|
24502
|
-
|
|
25057
|
+
let sectionRead = null;
|
|
25058
|
+
if (content && section && rawBody !== undefined)
|
|
25059
|
+
sectionRead = getSection(rawBody, section);
|
|
25060
|
+
const body = content ? sectionRead ? sectionRead.content : rawBody : undefined;
|
|
24503
25061
|
let findReport;
|
|
24504
25062
|
if (find && rawBody !== undefined) {
|
|
24505
25063
|
const countIn = (s) => s.split(find).length - 1;
|
|
@@ -24525,11 +25083,19 @@ mention this carried flag" becomes one call instead of a full read + manual coun
|
|
|
24525
25083
|
}
|
|
24526
25084
|
flush(rawBody.length);
|
|
24527
25085
|
if (!last) {
|
|
24528
|
-
const
|
|
24529
|
-
if (
|
|
24530
|
-
blocks.push({ block: "(body)", count:
|
|
24531
|
-
}
|
|
24532
|
-
|
|
25086
|
+
const total2 = countIn(rawBody);
|
|
25087
|
+
if (total2 > 0)
|
|
25088
|
+
blocks.push({ block: "(body)", count: total2 });
|
|
25089
|
+
}
|
|
25090
|
+
const total = countIn(rawBody);
|
|
25091
|
+
const near = total === 0 ? nearestContext(rawBody, find) : null;
|
|
25092
|
+
findReport = {
|
|
25093
|
+
find,
|
|
25094
|
+
total,
|
|
25095
|
+
blocks,
|
|
25096
|
+
...near ? { matchedUpTo: near.fragment, storedNearby: near.context } : {},
|
|
25097
|
+
...total === 0 && !near ? { hint: "Not present, and no fragment of it is either \u2014 the string is unrelated to this note's content." } : {}
|
|
25098
|
+
};
|
|
24533
25099
|
}
|
|
24534
25100
|
const labels = note.attributes.filter((a) => a.type === "label").map((a) => ({ name: a.name, value: a.value, ...a.isInheritable ? { inheritable: true } : {} }));
|
|
24535
25101
|
const relations = relationSnippet(note, 50);
|
|
@@ -24549,9 +25115,223 @@ mention this carried flag" becomes one call instead of a full read + manual coun
|
|
|
24549
25115
|
parentNoteIds: note.parentNoteIds,
|
|
24550
25116
|
childNoteIds: note.childNoteIds,
|
|
24551
25117
|
...findReport ? { findReport } : {},
|
|
25118
|
+
...sectionRead ? sectionRead.matched ? { section, sectionMatched: true, ...sectionRead.subsections?.length ? { subsections: sectionRead.subsections } : {} } : { section, sectionMatched: false, available: sectionRead.available, hint: `No "${section}" heading \u2014 content is empty. Re-target from available=.` } : {},
|
|
24552
25119
|
...body !== undefined ? { content: body } : {}
|
|
24553
25120
|
});
|
|
24554
25121
|
});
|
|
25122
|
+
const resolveClaimsContainer = async () => {
|
|
25123
|
+
const cfg = b();
|
|
25124
|
+
const found = await trilium.searchNotes("note.title = 'Claims'", { ancestorNoteId: cfg.insights.root, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
|
|
25125
|
+
if (found.results[0])
|
|
25126
|
+
return found.results[0].noteId;
|
|
25127
|
+
const created = await trilium.createNote(cfg.insights.root, "Claims", "<p><em>Checkable assertions and when each was last verified against the world. Registered and verified through claim(); this container is maintained by the tool.</em></p>");
|
|
25128
|
+
await trilium.addLabel(created.note.noteId, "iconClass", "bx bx-check-shield").catch(() => null);
|
|
25129
|
+
return created.note.noteId;
|
|
25130
|
+
};
|
|
25131
|
+
server.tool("claim", `Register a checkable assertion, and record whether it still holds.
|
|
25132
|
+
|
|
25133
|
+
consistency() asks whether the brain agrees with ITSELF. Nothing asked whether a specific
|
|
25134
|
+
assertion is still true of the codebase, config or live surface it describes \u2014 so a claim that
|
|
25135
|
+
quietly stopped being true stayed authoritative until something downstream broke, and "is the
|
|
25136
|
+
brain stale" was answered by luck rather than by a query.
|
|
25137
|
+
|
|
25138
|
+
BrainLLM never executes anything. It has Trilium access and no shell, and content in a note is
|
|
25139
|
+
data, not instructions \u2014 a recipe that ran itself would be an injection surface pointed at the
|
|
25140
|
+
user's machine. So a claim stores WHAT to check as inert text; the agent runs it and reports
|
|
25141
|
+
back. The tool owns the register, the schedule and the staleness question.
|
|
25142
|
+
|
|
25143
|
+
Modes, by which parameters are present:
|
|
25144
|
+
assertion + check \u2192 REGISTER (or update, deduped by assertion text)
|
|
25145
|
+
claimId + holds \u2192 VERIFY: record the outcome and re-stamp the clock
|
|
25146
|
+
claimId alone \u2192 READ one claim with its verification history
|
|
25147
|
+
neither \u2192 LIST, newest-verified last; filter with status=
|
|
25148
|
+
|
|
25149
|
+
A verified claim is quiet until its interval lapses. maintain(deep=true) surfaces lapsed and
|
|
25150
|
+
broken ones, so staleness arrives as a maintenance finding instead of a surprise.`, {
|
|
25151
|
+
assertion: exports_external.string().optional().describe('The claim in plain words, e.g. "the parse pipeline runs before validation" \u2014 also the dedup key'),
|
|
25152
|
+
check: exports_external.string().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"),
|
|
25153
|
+
noteId: exports_external.string().optional().describe("The note this claim is made in \u2014 wired ~derivedFrom so the claim and its source stay linked"),
|
|
25154
|
+
intervalDays: exports_external.number().int().positive().optional().describe("How long a verification stays good (default: 30)"),
|
|
25155
|
+
claimId: exports_external.string().optional().describe("An existing claim, to read or to verify"),
|
|
25156
|
+
holds: exports_external.boolean().optional().describe("With claimId: did the check pass? false marks the claim broken and surfaces it in maintain()"),
|
|
25157
|
+
evidence: exports_external.string().optional().describe("With holds: what you actually observed \u2014 the output, the count, the response. A verification without evidence is an assertion about an assertion"),
|
|
25158
|
+
status: exports_external.enum(["holding", "broken", "lapsed", "all"]).optional().describe("List filter (default: all)"),
|
|
25159
|
+
retire: exports_external.boolean().optional().describe("With claimId: archive this claim \u2014 the thing it described is gone")
|
|
25160
|
+
}, async ({ assertion, check: check2, noteId, intervalDays, claimId, holds, evidence, status, retire }) => {
|
|
25161
|
+
const cfg = b();
|
|
25162
|
+
if (!cfg.root)
|
|
25163
|
+
return txt4({ status: "uninitialized", action: "Run bootstrap first." });
|
|
25164
|
+
const d = today();
|
|
25165
|
+
const container = await resolveClaimsContainer();
|
|
25166
|
+
const lapsed = (n) => {
|
|
25167
|
+
const verifiedOn = labelOf2(n, "verified");
|
|
25168
|
+
if (!verifiedOn)
|
|
25169
|
+
return true;
|
|
25170
|
+
const days = Number(labelOf2(n, "interval") ?? 30);
|
|
25171
|
+
return verifiedOn < new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
|
|
25172
|
+
};
|
|
25173
|
+
const stateOf = (n) => labelOf2(n, "claimState") === "broken" ? "broken" : lapsed(n) ? "lapsed" : "holding";
|
|
25174
|
+
const row = (n) => ({
|
|
25175
|
+
claimId: n.noteId,
|
|
25176
|
+
assertion: n.title,
|
|
25177
|
+
state: stateOf(n),
|
|
25178
|
+
lastVerified: labelOf2(n, "verified") ?? "never",
|
|
25179
|
+
intervalDays: Number(labelOf2(n, "interval") ?? 30)
|
|
25180
|
+
});
|
|
25181
|
+
if (claimId) {
|
|
25182
|
+
const note = await trilium.getNote(claimId).catch(() => null);
|
|
25183
|
+
if (!note || labelOf2(note, "noteType") !== "claim")
|
|
25184
|
+
return err("not_found", `${claimId} is not a claim.`, "List them with claim() and no arguments.");
|
|
25185
|
+
if (retire) {
|
|
25186
|
+
await trilium.addLabel(claimId, "archived", "").catch(() => null);
|
|
25187
|
+
return txt4({ ok: true, claimId, action: "retired", assertion: note.title });
|
|
25188
|
+
}
|
|
25189
|
+
if (holds === undefined) {
|
|
25190
|
+
const content2 = await trilium.getNoteContent(claimId).catch(() => "");
|
|
25191
|
+
return txt4({ ...row(note), check: getSection(content2, "Check").content, history: getSection(content2, "Verifications").content, relations: relationSnippet(note) });
|
|
25192
|
+
}
|
|
25193
|
+
if (!evidence)
|
|
25194
|
+
return err("missing_param", "A verification needs evidence.", 'Pass evidence="<what you actually observed>" \u2014 the output, the count, the response. Recording a verdict without it makes the register a record of opinions.');
|
|
25195
|
+
const content = await trilium.getNoteContent(claimId).catch(() => "");
|
|
25196
|
+
const entry = `<p><strong>${d} \u2014 ${holds ? "HOLDS" : "BROKEN"}</strong>: ${escapeHtml(evidence)}</p>`;
|
|
25197
|
+
const updated = setSection(content, "Verifications", entry, "append");
|
|
25198
|
+
await trilium.createRevision(claimId).catch(() => null);
|
|
25199
|
+
await trilium.updateNoteContent(claimId, updated.html);
|
|
25200
|
+
await trilium.updateLabelValue(claimId, "verified", d).catch(() => null);
|
|
25201
|
+
await trilium.updateLabelValue(claimId, "claimState", holds ? "holding" : "broken").catch(() => null);
|
|
25202
|
+
await trilium.updateLabelValue(claimId, "updated", d).catch(() => null);
|
|
25203
|
+
const refreshed = await trilium.getNote(claimId).catch(() => note);
|
|
25204
|
+
return txt4({
|
|
25205
|
+
ok: true,
|
|
25206
|
+
action: holds ? "verified" : "marked-broken",
|
|
25207
|
+
...row(refreshed),
|
|
25208
|
+
...holds ? {} : { hint: "This claim is now BROKEN. Correct every note that asserts it \u2014 consistency() will find the others \u2014 then re-verify. A broken claim left in the register is a known-wrong fact with a timestamp on it." }
|
|
25209
|
+
});
|
|
25210
|
+
}
|
|
25211
|
+
if (assertion) {
|
|
25212
|
+
if (!check2)
|
|
25213
|
+
return err("missing_param", "A claim needs a check.", 'Pass check="<command, query, file path or URL that settles it>". A claim nobody can verify is just a sentence; the check is what makes it a claim.');
|
|
25214
|
+
const { title } = normalizeTitle(assertion);
|
|
25215
|
+
const existing = await trilium.searchNotes("#noteType=claim", { ancestorNoteId: container, fastSearch: true, limit: 200 }).then((r) => r.results.find((n) => sameTitle(n.title, title))).catch(() => {
|
|
25216
|
+
return;
|
|
25217
|
+
});
|
|
25218
|
+
const body = `<h2>Check</h2>
|
|
25219
|
+
<p>${escapeHtml(check2)}</p>
|
|
25220
|
+
` + `<p><em>Inert by design \u2014 BrainLLM never runs this. Run it yourself, then record the outcome with claim(claimId, holds, evidence).</em></p>
|
|
25221
|
+
` + `<h2>Verifications</h2>
|
|
25222
|
+
`;
|
|
25223
|
+
if (existing) {
|
|
25224
|
+
const current = await trilium.getNoteContent(existing.noteId).catch(() => "");
|
|
25225
|
+
await trilium.createRevision(existing.noteId).catch(() => null);
|
|
25226
|
+
await trilium.updateNoteContent(existing.noteId, setSection(current, "Check", `<p>${escapeHtml(check2)}</p>`, "replace").html);
|
|
25227
|
+
if (intervalDays)
|
|
25228
|
+
await trilium.updateLabelValue(existing.noteId, "interval", String(intervalDays)).catch(() => null);
|
|
25229
|
+
return txt4({ ok: true, action: "updated", ...row(existing), note: "A claim with this assertion already existed \u2014 its check was updated and its verification history kept." });
|
|
25230
|
+
}
|
|
25231
|
+
const created = await trilium.createNote(container, title, body);
|
|
25232
|
+
const id = created.note.noteId;
|
|
25233
|
+
await trilium.addLabel(id, "noteType", "claim");
|
|
25234
|
+
await trilium.addLabel(id, "created", d);
|
|
25235
|
+
await trilium.addLabel(id, "interval", String(intervalDays ?? 30));
|
|
25236
|
+
await trilium.addLabel(id, "claimState", "unverified");
|
|
25237
|
+
await trilium.addLabel(id, "iconClass", "bx bx-check-shield").catch(() => null);
|
|
25238
|
+
if (noteId)
|
|
25239
|
+
await trilium.addRelation(id, "derivedFrom", noteId).catch(() => null);
|
|
25240
|
+
return txt4({
|
|
25241
|
+
ok: true,
|
|
25242
|
+
action: "registered",
|
|
25243
|
+
claimId: id,
|
|
25244
|
+
assertion: title,
|
|
25245
|
+
intervalDays: intervalDays ?? 30,
|
|
25246
|
+
location: "Insights \u2192 Claims",
|
|
25247
|
+
next: `Run the check, then record the result: claim(claimId="${id}", holds=true|false, evidence="<what you observed>"). Until then it is unverified, not holding.`
|
|
25248
|
+
});
|
|
25249
|
+
}
|
|
25250
|
+
const all = await trilium.searchNotes("#noteType=claim", { ancestorNoteId: container, fastSearch: true, limit: 200 }).then((r) => r.results).catch(() => []);
|
|
25251
|
+
const rows = all.map(row).filter((r) => !status || status === "all" || r.state === status);
|
|
25252
|
+
const broken = rows.filter((r) => r.state === "broken").length;
|
|
25253
|
+
const stale = rows.filter((r) => r.state === "lapsed").length;
|
|
25254
|
+
return txt4({
|
|
25255
|
+
total: rows.length,
|
|
25256
|
+
...broken ? { broken } : {},
|
|
25257
|
+
...stale ? { needingReverification: stale } : {},
|
|
25258
|
+
claims: rows.sort((a, b2) => a.lastVerified.localeCompare(b2.lastVerified)),
|
|
25259
|
+
...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.' } : {}
|
|
25260
|
+
});
|
|
25261
|
+
});
|
|
25262
|
+
server.tool("diff", `What changed in a note \u2014 the revision snapshot against the body as it stands now.
|
|
25263
|
+
|
|
25264
|
+
Every content write takes a revision first, and until now nothing could read one back: verifying
|
|
25265
|
+
a run of surgical edits on a large note meant re-reading the whole thing, or trusting the
|
|
25266
|
+
receipts. Trusting receipts is exactly how a section= replace that silently displaced four
|
|
25267
|
+
subsections went unnoticed.
|
|
25268
|
+
|
|
25269
|
+
Called with only a noteId, this diffs the MOST RECENT revision against current content \u2014 "what
|
|
25270
|
+
did my last write actually do". Pass revisionId to compare against a specific earlier one; the
|
|
25271
|
+
revisions list comes back on every call, newest first, so the usual flow is one call to see what
|
|
25272
|
+
exists and a second to pick.
|
|
25273
|
+
|
|
25274
|
+
Reports changed lines with a little context, plus added/removed counts. Both sides are stored
|
|
25275
|
+
HTML, so a formatting-only change is a real difference and shows as one.`, {
|
|
25276
|
+
noteId: exports_external.string().describe("Note to diff"),
|
|
25277
|
+
revisionId: exports_external.string().optional().describe("Compare against this revision (default: the most recent one)"),
|
|
25278
|
+
context: exports_external.number().int().min(0).max(10).optional().describe("Unchanged lines to show around each change (default: 1)")
|
|
25279
|
+
}, async ({ noteId, revisionId, context }) => {
|
|
25280
|
+
const revisions = await trilium.getNoteRevisions(noteId).catch(() => []);
|
|
25281
|
+
if (!revisions.length)
|
|
25282
|
+
return txt4({ noteId, note: "No revisions \u2014 this note has not been written through a content-mutating tool yet, or its revisions have been pruned.", revisions: [] });
|
|
25283
|
+
const target = revisionId ? revisions.find((r) => r.revisionId === revisionId) : revisions[0];
|
|
25284
|
+
if (!target)
|
|
25285
|
+
return err("not_found", `Revision ${revisionId} does not belong to note ${noteId}.`, `Available: ${revisions.slice(0, 10).map((r) => r.revisionId).join(", ")}`);
|
|
25286
|
+
const [before, after] = await Promise.all([
|
|
25287
|
+
trilium.getRevisionContent(target.revisionId).catch(() => ""),
|
|
25288
|
+
trilium.getNoteContent(noteId).catch(() => "")
|
|
25289
|
+
]);
|
|
25290
|
+
const index = revisions.map((r) => ({
|
|
25291
|
+
revisionId: r.revisionId,
|
|
25292
|
+
dateCreated: r.dateCreated?.slice(0, 16),
|
|
25293
|
+
size: r.contentLength,
|
|
25294
|
+
...r.revisionId === target.revisionId ? { compared: true } : {}
|
|
25295
|
+
}));
|
|
25296
|
+
if (before === after) {
|
|
25297
|
+
return txt4({
|
|
25298
|
+
noteId,
|
|
25299
|
+
comparedTo: target.revisionId,
|
|
25300
|
+
identical: true,
|
|
25301
|
+
revisions: index,
|
|
25302
|
+
note: "The snapshot and the current body are byte-identical \u2014 the write after this revision changed nothing, or the revision was taken after it."
|
|
25303
|
+
});
|
|
25304
|
+
}
|
|
25305
|
+
const lines = (s) => s.replace(/></g, `>
|
|
25306
|
+
<`).split(`
|
|
25307
|
+
`);
|
|
25308
|
+
const a = lines(before);
|
|
25309
|
+
const bLines = lines(after);
|
|
25310
|
+
let head = 0;
|
|
25311
|
+
while (head < a.length && head < bLines.length && a[head] === bLines[head])
|
|
25312
|
+
head++;
|
|
25313
|
+
let tail = 0;
|
|
25314
|
+
while (tail < a.length - head && tail < bLines.length - head && a[a.length - 1 - tail] === bLines[bLines.length - 1 - tail])
|
|
25315
|
+
tail++;
|
|
25316
|
+
const pad2 = context ?? 1;
|
|
25317
|
+
const removed = a.slice(head, a.length - tail);
|
|
25318
|
+
const added = bLines.slice(head, bLines.length - tail);
|
|
25319
|
+
const cap = (arr) => arr.length > 40 ? [...arr.slice(0, 40), `\u2026 ${arr.length - 40} more line(s)`] : arr;
|
|
25320
|
+
return txt4({
|
|
25321
|
+
noteId,
|
|
25322
|
+
comparedTo: target.revisionId,
|
|
25323
|
+
revisionDate: target.dateCreated?.slice(0, 16),
|
|
25324
|
+
identical: false,
|
|
25325
|
+
sizeBefore: before.length,
|
|
25326
|
+
sizeAfter: after.length,
|
|
25327
|
+
contextBefore: cap(a.slice(Math.max(0, head - pad2), head)),
|
|
25328
|
+
removed: cap(removed),
|
|
25329
|
+
added: cap(added),
|
|
25330
|
+
contextAfter: cap(a.slice(a.length - tail, a.length - tail + pad2)),
|
|
25331
|
+
summary: `${removed.length} block(s) removed, ${added.length} added, ${after.length - before.length >= 0 ? "+" : ""}${after.length - before.length} characters.`,
|
|
25332
|
+
revisions: index
|
|
25333
|
+
});
|
|
25334
|
+
});
|
|
24555
25335
|
server.tool("attach", `Attach a raw artifact (file, image, code blob, document) to a note \u2014 or read one back.
|
|
24556
25336
|
Dual-mode by the content param:
|
|
24557
25337
|
content provided \u2192 UPSERT by title: creates the attachment, or replaces the existing
|
|
@@ -24684,18 +25464,27 @@ domain="<name>" narrows the deep passes to one domain's notes \u2014 the equival
|
|
|
24684
25464
|
scoping addendum() already has, so a scoped agent's flags arrive in its own lane instead of it
|
|
24685
25465
|
re-deriving each run that the cross-venture findings belong to someone else.
|
|
24686
25466
|
|
|
25467
|
+
repair=[noteId, \u2026] fixes an entity-corrupted body in place instead of reporting it: one level of
|
|
25468
|
+
double-escaping is unwound ("&lt;" \u2192 "<", "&nbsp;" \u2192 " "), with a revision taken
|
|
25469
|
+
first. The substitution is always the same one, so it is a first-class action rather than
|
|
25470
|
+
something each caller reinvents with revise(mode="replace"). Compose with dryRun to see the
|
|
25471
|
+
outcome without writing. A note that DOCUMENTS the signature rather than carrying it comes back
|
|
25472
|
+
unchanged and is reported as such \u2014 ack= those.
|
|
25473
|
+
|
|
24687
25474
|
coverage names any pass that hit a cap, so a short list is never mistaken for a clean one.`, {
|
|
24688
25475
|
deep: exports_external.boolean().optional().describe("Deep pass: stale-review + orphan/sink + structural lint + duplicate titles across Memory/Threads and Knowledge (default: false)"),
|
|
24689
25476
|
dryRun: exports_external.boolean().optional().describe("Report what would change without changing it"),
|
|
24690
25477
|
domain: exports_external.string().optional().describe("Narrow the deep passes to one domain's notes (slugged server-side) \u2014 for scoped agents working a single lane"),
|
|
24691
|
-
ack: exports_external.array(exports_external.string()).optional().describe("Note ids to mark reviewed-and-correct \u2014 suppresses their findings until the note's content changes")
|
|
24692
|
-
|
|
25478
|
+
ack: exports_external.array(exports_external.string()).optional().describe("Note ids to mark reviewed-and-correct \u2014 suppresses their findings until the note's content changes"),
|
|
25479
|
+
repair: exports_external.array(exports_external.string()).optional().describe("Note ids to auto-repair entity corruption on \u2014 unwinds one level of double-escaping, revision taken first")
|
|
25480
|
+
}, async ({ deep, dryRun, domain, ack, repair }) => {
|
|
24693
25481
|
await markStep("maintain");
|
|
24694
25482
|
const report = await sweep(trilium, b(), {
|
|
24695
25483
|
deep: deep ?? false,
|
|
24696
25484
|
dryRun: dryRun ?? false,
|
|
24697
25485
|
...domain ? { domain } : {},
|
|
24698
|
-
...ack?.length ? { ack } : {}
|
|
25486
|
+
...ack?.length ? { ack } : {},
|
|
25487
|
+
...repair?.length ? { repair } : {}
|
|
24699
25488
|
});
|
|
24700
25489
|
return txt4(report);
|
|
24701
25490
|
});
|
|
@@ -25056,6 +25845,7 @@ Structural containers are excluded; only content notes appear.`, {
|
|
|
25056
25845
|
id: n.noteId,
|
|
25057
25846
|
title: n.title,
|
|
25058
25847
|
kind: labelOf2(n, "noteType"),
|
|
25848
|
+
parent: n.parentNoteIds?.[0],
|
|
25059
25849
|
status: labelOf2(n, "status") ?? undefined,
|
|
25060
25850
|
created: labelOf2(n, "created") ?? n.dateCreated.slice(0, 10),
|
|
25061
25851
|
modified: n.dateModified.slice(0, 10),
|
|
@@ -25103,6 +25893,128 @@ Structural containers are excluded; only content notes appear.`, {
|
|
|
25103
25893
|
const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
|
|
25104
25894
|
return txt4({ total, areas });
|
|
25105
25895
|
});
|
|
25896
|
+
server.tool("assembly", `What the brain HOLDS \u2014 every note by title, grouped under the surface it lives in, with each
|
|
25897
|
+
surface's own purpose alongside it. The awareness read: "what do I already know here", answered
|
|
25898
|
+
in one call before deciding whether to look something up.
|
|
25899
|
+
|
|
25900
|
+
Distinct from brain(), which is an inventory: brain() returns id, kind, status, parent, dates and
|
|
25901
|
+
relations for every note, and is what you want when auditing or locating one. assembly() answers
|
|
25902
|
+
a different question and is shaped for it:
|
|
25903
|
+
|
|
25904
|
+
- **Dated collections collapse.** Sessions, diary entries and logs are titled [yyyy-mm-dd], so
|
|
25905
|
+
listing them is a wall of dates carrying no information about content. They come back as a
|
|
25906
|
+
count and a span instead. This is the difference between a listing and an awareness read \u2014 the
|
|
25907
|
+
full titles are one brain() call away when you actually need them.
|
|
25908
|
+
- **Domains nest.** Each domain book appears with its own information and sources notes beneath
|
|
25909
|
+
it, which is the real shape of what the brain knows rather than the flat list brain() returns.
|
|
25910
|
+
- **Threads group by status**, newest activity first \u2014 the working set is the actionable part.
|
|
25911
|
+
- **Purposes are read from the container notes themselves**, the text bootstrap engraved on them,
|
|
25912
|
+
so this can never drift from what the brain actually says about itself.
|
|
25913
|
+
|
|
25914
|
+
Pass area= to zoom into one surface and get its full detail.`, {
|
|
25915
|
+
area: exports_external.enum(["master", "llm", "memory", "knowledge", "insights"]).optional().describe("Zoom into one surface instead of all five"),
|
|
25916
|
+
includeArchived: exports_external.boolean().optional().describe("Include archived/resolved notes (default: false)")
|
|
25917
|
+
}, async ({ area, includeArchived }) => {
|
|
25918
|
+
const cfg = b();
|
|
25919
|
+
if (!cfg.root)
|
|
25920
|
+
return txt4({ status: "uninitialized", action: "Run bootstrap first." });
|
|
25921
|
+
const fetchFrom = async (id) => {
|
|
25922
|
+
if (!id)
|
|
25923
|
+
return [];
|
|
25924
|
+
return trilium.searchNotes("#noteType", {
|
|
25925
|
+
ancestorNoteId: id,
|
|
25926
|
+
fastSearch: true,
|
|
25927
|
+
limit: 400,
|
|
25928
|
+
includeArchivedNotes: includeArchived ?? false,
|
|
25929
|
+
orderBy: "dateModified",
|
|
25930
|
+
orderDirection: "desc"
|
|
25931
|
+
}).then((r) => r.results).catch(() => []);
|
|
25932
|
+
};
|
|
25933
|
+
const purposeOf = async (id) => {
|
|
25934
|
+
if (!id)
|
|
25935
|
+
return;
|
|
25936
|
+
const content = await trilium.getNoteContent(id).catch(() => "");
|
|
25937
|
+
const text = toText(content, 400).trim();
|
|
25938
|
+
return text || undefined;
|
|
25939
|
+
};
|
|
25940
|
+
const dateOf = (n) => labelOf2(n, "created") ?? n.dateCreated.slice(0, 10);
|
|
25941
|
+
const span = (notes, label2) => {
|
|
25942
|
+
if (!notes.length)
|
|
25943
|
+
return { count: 0, note: `No ${label2} yet.` };
|
|
25944
|
+
const dates = notes.map(dateOf).sort();
|
|
25945
|
+
return {
|
|
25946
|
+
count: notes.length,
|
|
25947
|
+
earliest: dates[0],
|
|
25948
|
+
latest: dates[dates.length - 1],
|
|
25949
|
+
note: `${notes.length} ${label2}, ${dates[0]} \u2192 ${dates[dates.length - 1]}. Titles are all [yyyy-mm-dd] \u2014 read one with its surface tool, or brain() for the full list.`
|
|
25950
|
+
};
|
|
25951
|
+
};
|
|
25952
|
+
const titles = (notes) => notes.map((n) => n.title);
|
|
25953
|
+
const want = (a) => !area || area === a;
|
|
25954
|
+
const out = {};
|
|
25955
|
+
if (want("master")) {
|
|
25956
|
+
const notes = await fetchFrom(cfg.master.root);
|
|
25957
|
+
out.Master = { purpose: await purposeOf(cfg.master.root), singletons: titles(notes) };
|
|
25958
|
+
}
|
|
25959
|
+
if (want("llm")) {
|
|
25960
|
+
const [all, diary] = await Promise.all([fetchFrom(cfg.llm.root), fetchFrom(cfg.llm.diary)]);
|
|
25961
|
+
const diaryIds = new Set(diary.map((n) => n.noteId));
|
|
25962
|
+
out.LLM = {
|
|
25963
|
+
purpose: await purposeOf(cfg.llm.root),
|
|
25964
|
+
singletons: titles(all.filter((n) => !diaryIds.has(n.noteId))),
|
|
25965
|
+
diary: span(diary, "diary entries")
|
|
25966
|
+
};
|
|
25967
|
+
}
|
|
25968
|
+
if (want("memory")) {
|
|
25969
|
+
const [threads, sessions] = await Promise.all([fetchFrom(cfg.memory.threads), fetchFrom(cfg.memory.sessions)]);
|
|
25970
|
+
const books = threads.filter((n) => labelOf2(n, "noteType") === "thread");
|
|
25971
|
+
const byStatus = (s) => titles(books.filter((n) => (labelOf2(n, "status") ?? "active") === s));
|
|
25972
|
+
out.Memory = {
|
|
25973
|
+
purpose: await purposeOf(cfg.memory.root),
|
|
25974
|
+
threads: {
|
|
25975
|
+
active: byStatus("active"),
|
|
25976
|
+
...byStatus("dormant").length ? { dormant: byStatus("dormant") } : {},
|
|
25977
|
+
...byStatus("eternal").length ? { eternal: byStatus("eternal") } : {},
|
|
25978
|
+
...includeArchived && byStatus("resolved").length ? { resolved: byStatus("resolved") } : {}
|
|
25979
|
+
},
|
|
25980
|
+
sessions: span(sessions.filter((n) => labelOf2(n, "noteType") === "session"), "sessions")
|
|
25981
|
+
};
|
|
25982
|
+
}
|
|
25983
|
+
if (want("knowledge")) {
|
|
25984
|
+
const [master, domainNotes] = await Promise.all([fetchFrom(cfg.knowledge.master), fetchFrom(cfg.knowledge.domains)]);
|
|
25985
|
+
const books = domainNotes.filter((n) => labelOf2(n, "noteType") === "domain");
|
|
25986
|
+
const domains = books.map((book) => {
|
|
25987
|
+
const children = domainNotes.filter((n) => n.noteId !== book.noteId && n.parentNoteIds?.includes(book.noteId));
|
|
25988
|
+
return {
|
|
25989
|
+
domain: book.title,
|
|
25990
|
+
sources: children.some((c) => labelOf2(c, "noteType") === "sources"),
|
|
25991
|
+
notes: titles(children.filter((c) => labelOf2(c, "noteType") === "information"))
|
|
25992
|
+
};
|
|
25993
|
+
});
|
|
25994
|
+
const orphaned = domainNotes.filter((n) => labelOf2(n, "noteType") !== "domain" && !books.some((bk) => n.parentNoteIds?.includes(bk.noteId)));
|
|
25995
|
+
out.Knowledge = {
|
|
25996
|
+
purpose: await purposeOf(cfg.knowledge.root),
|
|
25997
|
+
aboutTheUser: titles(master),
|
|
25998
|
+
domains,
|
|
25999
|
+
...orphaned.length ? { unparented: titles(orphaned), hint: "These carry a knowledge kind but sit under no domain book \u2014 inspect() them for their real parent." } : {}
|
|
26000
|
+
};
|
|
26001
|
+
}
|
|
26002
|
+
if (want("insights")) {
|
|
26003
|
+
const [logs, all] = await Promise.all([fetchFrom(cfg.insights.logs), fetchFrom(cfg.insights.root)]);
|
|
26004
|
+
const claims = all.filter((n) => labelOf2(n, "noteType") === "claim");
|
|
26005
|
+
out.Insights = {
|
|
26006
|
+
purpose: await purposeOf(cfg.insights.root),
|
|
26007
|
+
logs: span(logs.filter((n) => labelOf2(n, "noteType") === "log"), "daily logs"),
|
|
26008
|
+
claims: claims.length ? { count: claims.length, assertions: titles(claims), note: "Each claim's title IS its assertion. claim(claimId) reads one with its verification history." } : { count: 0, note: "No claims registered \u2014 nothing is currently being checked against the world." }
|
|
26009
|
+
};
|
|
26010
|
+
}
|
|
26011
|
+
return txt4({
|
|
26012
|
+
brain: await purposeOf(cfg.root),
|
|
26013
|
+
...area ? { scope: area } : {},
|
|
26014
|
+
...out,
|
|
26015
|
+
...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
|
|
26016
|
+
});
|
|
26017
|
+
});
|
|
25106
26018
|
server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config
|
|
25107
26019
|
if the structure already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
|
|
25108
26020
|
LLM (Responsibilities/Protocols/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
|
|
@@ -25116,9 +26028,24 @@ immediately, no restart needed.`, {}, async () => {
|
|
|
25116
26028
|
return { id: child.noteId, title: child.title };
|
|
25117
26029
|
}));
|
|
25118
26030
|
const saved = saveConfig(brainRef.config);
|
|
26031
|
+
const refreshed = [];
|
|
26032
|
+
for (const [id, purpose] of containerPurposes(brainRef.config)) {
|
|
26033
|
+
if (!id)
|
|
26034
|
+
continue;
|
|
26035
|
+
const current = await trilium.getNoteContent(id).catch(() => null);
|
|
26036
|
+
if (current === null)
|
|
26037
|
+
continue;
|
|
26038
|
+
const wanted = purposeContent(purpose);
|
|
26039
|
+
if (toText(current, 400).trim() === toText(wanted, 400).trim())
|
|
26040
|
+
continue;
|
|
26041
|
+
await trilium.updateNoteContent(id, wanted).catch(() => null);
|
|
26042
|
+
const note = await trilium.getNote(id).catch(() => null);
|
|
26043
|
+
refreshed.push(note?.title ?? id);
|
|
26044
|
+
}
|
|
25119
26045
|
return txt4({
|
|
25120
26046
|
status: "already_initialized",
|
|
25121
26047
|
message: `BrainLLM structure exists. Config refreshed at: ${saved}`,
|
|
26048
|
+
...refreshed.length ? { purposesRefreshed: refreshed, note: "These containers described themselves with text that no longer matched the canonical purpose, and have been re-engraved. Container notes are unreachable through revise(), so bootstrap is the only path that can correct them." } : {},
|
|
25122
26049
|
root: { id: existing.noteId, title: existing.title },
|
|
25123
26050
|
children
|
|
25124
26051
|
});
|
|
@@ -25715,7 +26642,7 @@ function createServer(origin = null) {
|
|
|
25715
26642
|
const s = new McpServer({
|
|
25716
26643
|
name: "BrainLLM",
|
|
25717
26644
|
title: "BrainLLM",
|
|
25718
|
-
version: "10.
|
|
26645
|
+
version: "10.4.0",
|
|
25719
26646
|
icons: brandingIcons(origin)
|
|
25720
26647
|
});
|
|
25721
26648
|
registerTools(s, trilium, brainRef, mode);
|