brainllm 12.5.0 → 12.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +3 -1
- package/SECURITY.md +2 -2
- package/dist/index.js +348 -55
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +4 -3
- package/skills/brainllm/references/domains.md +2 -0
- package/skills/brainllm/references/fullmode.md +1 -1
- package/skills/brainllm/references/troubleshooting.md +0 -1
package/.env.example
CHANGED
|
@@ -25,6 +25,13 @@ TRILIUM_PASSWORD=
|
|
|
25
25
|
# Leave unset to use the host clock — correct when BrainLLM runs on your machine.
|
|
26
26
|
# BRAINLLM_TZ=
|
|
27
27
|
|
|
28
|
+
# ── Deleted-note retention ────────────────────────────────────────────────────
|
|
29
|
+
# Days Trilium keeps a deleted note before erasing it (Options → Other → "Erase
|
|
30
|
+
# notes after"). BrainLLM reports deletions only within this window, and ETAPI
|
|
31
|
+
# cannot read the option, so set this when your instance differs from Trilium's
|
|
32
|
+
# default of 7. A whole number from 1 to 3650; overrides brainllm.json's policy.
|
|
33
|
+
# BRAINLLM_DELETION_CATCHUP_DAYS=7
|
|
34
|
+
|
|
28
35
|
# ── HTTP connector mode (Railway / remote) ────────────────────────────────────
|
|
29
36
|
# When PORT is set the server starts an HTTP/SSE connector instead of stdio.
|
|
30
37
|
# Railway sets PORT automatically; do NOT set it for local Claude Code usage.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A persistent, graph-structured second brain for Claude and any MCP client — bu
|
|
|
10
10
|
|
|
11
11
|
[**brainllm site**](https://miisodev.github.io/BrainLLM/) · [How it works](https://miisodev.github.io/BrainLLM/how-it-works.html) · [Use cases](https://miisodev.github.io/BrainLLM/use-cases.html) · [Docs](https://miisodev.github.io/BrainLLM/docs.html)
|
|
12
12
|
|
|
13
|
-
[](https://github.com/miisodev/BrainLLM/releases)
|
|
14
14
|
[](https://github.com/miisodev/BrainLLM/actions/workflows/ci.yml)
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](https://bun.sh)
|
|
@@ -296,6 +296,7 @@ Without a volume, leave `BRAINLLM_CONFIG` unset — auto-discovery re-finds the
|
|
|
296
296
|
| `TRILIUM_PASSWORD` | ◐ | Alternative to the token: BrainLLM mints one on first start and caches it beside `brainllm.json`, reusing it across restarts. Also used by `bun run init` |
|
|
297
297
|
| `BRAINLLM_MODE` | — | `core` (default) or `full` (adds the raw ETAPI surface) |
|
|
298
298
|
| `BRAINLLM_TZ` | — | IANA timezone (e.g. `Africa/Johannesburg`) so dates stamp in *your* day on hosted deploys; unset = host clock |
|
|
299
|
+
| `BRAINLLM_DELETION_CATCHUP_DAYS` | — | How many days your Trilium keeps deleted notes (Options → Other → "Erase notes after"). Deletions are reported only within this window, and ETAPI cannot read the option. Default `7`, Trilium's own default; overrides `policy.deletionCatchupDays` |
|
|
299
300
|
| `PORT` | — | Presence switches to HTTP-connector mode |
|
|
300
301
|
| `MCP_AUTH_TOKEN` | — | Static bearer token accepted on `/mcp` — what Claude Code and `mcp-remote` send |
|
|
301
302
|
| `BRAINLLM_OWNER_PASSWORD` | — | Enables the OAuth 2.1 / CIMD endpoints, required for claude.ai. The password you type on the consent screen |
|
|
@@ -322,6 +323,7 @@ A handful of values in this repo reflect the author's own machine. None are secr
|
|
|
322
323
|
| What | Where | Make it yours |
|
|
323
324
|
|---|---|---|
|
|
324
325
|
| **Timezone** | `BRAINLLM_TZ` in `.env` | Your IANA zone — or unset for the host clock |
|
|
326
|
+
| **Deleted-note retention** | `BRAINLLM_DELETION_CATCHUP_DAYS` in `.env` | Only if your Trilium erases deleted notes after something other than 7 days |
|
|
325
327
|
| **Config path** | `BRAINLLM_CONFIG` env var | Only needed on persistent-volume deploys (see above) |
|
|
326
328
|
| **Bundle path** | your MCP client config | The real absolute path to `dist/index.js` on your machine |
|
|
327
329
|
| **Author · repo · funding** | `package.json`, `.github/FUNDING.yml`, the badges above | Your own details if you fork; the funding links support the original author |
|
package/SECURITY.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -21254,6 +21254,19 @@ function saveCachedToken(token) {
|
|
|
21254
21254
|
return null;
|
|
21255
21255
|
}
|
|
21256
21256
|
}
|
|
21257
|
+
var MAX_CATCHUP_DAYS = 3650;
|
|
21258
|
+
function envDeletionCatchupDays(env = process.env) {
|
|
21259
|
+
const raw = env.BRAINLLM_DELETION_CATCHUP_DAYS?.trim();
|
|
21260
|
+
if (!raw)
|
|
21261
|
+
return {};
|
|
21262
|
+
const days = Number(raw);
|
|
21263
|
+
if (!/^\d+$/.test(raw) || days < 1 || days > MAX_CATCHUP_DAYS)
|
|
21264
|
+
return { invalid: raw };
|
|
21265
|
+
return { days };
|
|
21266
|
+
}
|
|
21267
|
+
function deletionCatchupDays(cfg, env = process.env) {
|
|
21268
|
+
return envDeletionCatchupDays(env).days ?? cfg.policy?.deletionCatchupDays ?? DEFAULT_POLICY.deletionCatchupDays;
|
|
21269
|
+
}
|
|
21257
21270
|
function loadConfig() {
|
|
21258
21271
|
const path = configFilePath();
|
|
21259
21272
|
if (!existsSync(path))
|
|
@@ -21425,13 +21438,141 @@ function normalizeIcon(raw) {
|
|
|
21425
21438
|
const name = s.replace(/^bx[sl]?-/, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
21426
21439
|
return name ? `bx bx-${name}` : "";
|
|
21427
21440
|
}
|
|
21428
|
-
var HTML_TAG = /<\/?[a-z][a-z0-9-]*(\s[^<>]*)?>/
|
|
21441
|
+
var HTML_TAG = /<\/?([a-z][a-z0-9-]*)(\s[^<>]*)?>/gi;
|
|
21442
|
+
var HTML_ELEMENTS = new Set([
|
|
21443
|
+
"a",
|
|
21444
|
+
"abbr",
|
|
21445
|
+
"address",
|
|
21446
|
+
"area",
|
|
21447
|
+
"article",
|
|
21448
|
+
"aside",
|
|
21449
|
+
"audio",
|
|
21450
|
+
"b",
|
|
21451
|
+
"base",
|
|
21452
|
+
"bdi",
|
|
21453
|
+
"bdo",
|
|
21454
|
+
"blockquote",
|
|
21455
|
+
"body",
|
|
21456
|
+
"br",
|
|
21457
|
+
"button",
|
|
21458
|
+
"canvas",
|
|
21459
|
+
"caption",
|
|
21460
|
+
"center",
|
|
21461
|
+
"cite",
|
|
21462
|
+
"code",
|
|
21463
|
+
"col",
|
|
21464
|
+
"colgroup",
|
|
21465
|
+
"data",
|
|
21466
|
+
"datalist",
|
|
21467
|
+
"dd",
|
|
21468
|
+
"del",
|
|
21469
|
+
"details",
|
|
21470
|
+
"dfn",
|
|
21471
|
+
"dialog",
|
|
21472
|
+
"div",
|
|
21473
|
+
"dl",
|
|
21474
|
+
"dt",
|
|
21475
|
+
"em",
|
|
21476
|
+
"embed",
|
|
21477
|
+
"fieldset",
|
|
21478
|
+
"figcaption",
|
|
21479
|
+
"figure",
|
|
21480
|
+
"font",
|
|
21481
|
+
"footer",
|
|
21482
|
+
"form",
|
|
21483
|
+
"h1",
|
|
21484
|
+
"h2",
|
|
21485
|
+
"h3",
|
|
21486
|
+
"h4",
|
|
21487
|
+
"h5",
|
|
21488
|
+
"h6",
|
|
21489
|
+
"head",
|
|
21490
|
+
"header",
|
|
21491
|
+
"hgroup",
|
|
21492
|
+
"hr",
|
|
21493
|
+
"html",
|
|
21494
|
+
"i",
|
|
21495
|
+
"iframe",
|
|
21496
|
+
"img",
|
|
21497
|
+
"input",
|
|
21498
|
+
"ins",
|
|
21499
|
+
"kbd",
|
|
21500
|
+
"label",
|
|
21501
|
+
"legend",
|
|
21502
|
+
"li",
|
|
21503
|
+
"link",
|
|
21504
|
+
"main",
|
|
21505
|
+
"map",
|
|
21506
|
+
"mark",
|
|
21507
|
+
"math",
|
|
21508
|
+
"menu",
|
|
21509
|
+
"meta",
|
|
21510
|
+
"meter",
|
|
21511
|
+
"nav",
|
|
21512
|
+
"noscript",
|
|
21513
|
+
"object",
|
|
21514
|
+
"ol",
|
|
21515
|
+
"optgroup",
|
|
21516
|
+
"option",
|
|
21517
|
+
"output",
|
|
21518
|
+
"p",
|
|
21519
|
+
"param",
|
|
21520
|
+
"picture",
|
|
21521
|
+
"pre",
|
|
21522
|
+
"progress",
|
|
21523
|
+
"q",
|
|
21524
|
+
"rp",
|
|
21525
|
+
"rt",
|
|
21526
|
+
"ruby",
|
|
21527
|
+
"s",
|
|
21528
|
+
"samp",
|
|
21529
|
+
"script",
|
|
21530
|
+
"search",
|
|
21531
|
+
"section",
|
|
21532
|
+
"select",
|
|
21533
|
+
"slot",
|
|
21534
|
+
"small",
|
|
21535
|
+
"source",
|
|
21536
|
+
"span",
|
|
21537
|
+
"strike",
|
|
21538
|
+
"strong",
|
|
21539
|
+
"style",
|
|
21540
|
+
"sub",
|
|
21541
|
+
"summary",
|
|
21542
|
+
"sup",
|
|
21543
|
+
"svg",
|
|
21544
|
+
"table",
|
|
21545
|
+
"tbody",
|
|
21546
|
+
"td",
|
|
21547
|
+
"template",
|
|
21548
|
+
"textarea",
|
|
21549
|
+
"tfoot",
|
|
21550
|
+
"th",
|
|
21551
|
+
"thead",
|
|
21552
|
+
"time",
|
|
21553
|
+
"title",
|
|
21554
|
+
"tr",
|
|
21555
|
+
"track",
|
|
21556
|
+
"tt",
|
|
21557
|
+
"u",
|
|
21558
|
+
"ul",
|
|
21559
|
+
"var",
|
|
21560
|
+
"video",
|
|
21561
|
+
"wbr",
|
|
21562
|
+
"applet"
|
|
21563
|
+
]);
|
|
21564
|
+
function isHtmlElement(name) {
|
|
21565
|
+
return HTML_ELEMENTS.has(name.toLowerCase());
|
|
21566
|
+
}
|
|
21429
21567
|
function looksLikeHtml(body) {
|
|
21430
|
-
|
|
21568
|
+
for (const m of body.matchAll(HTML_TAG))
|
|
21569
|
+
if (isHtmlElement(m[1]))
|
|
21570
|
+
return true;
|
|
21571
|
+
return false;
|
|
21431
21572
|
}
|
|
21432
21573
|
var ENCODED_TAG = /<\/?[a-z][a-z0-9-]*(?:\s[^&<>]*?)?\s*\/?>/i;
|
|
21433
21574
|
function looksLikeEncodedHtml(body) {
|
|
21434
|
-
return !
|
|
21575
|
+
return !looksLikeHtml(body) && ENCODED_TAG.test(body);
|
|
21435
21576
|
}
|
|
21436
21577
|
function decodeEncodedHtml(body) {
|
|
21437
21578
|
let s = body;
|
|
@@ -21492,15 +21633,102 @@ function nearestHeading(requested, available) {
|
|
|
21492
21633
|
function inlineMd(escaped) {
|
|
21493
21634
|
return escaped.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2">$1</a>');
|
|
21494
21635
|
}
|
|
21495
|
-
function
|
|
21636
|
+
function renderMarkup(raw) {
|
|
21637
|
+
const body = raw.replace(/\u0000/g, "");
|
|
21496
21638
|
if (!body.trim())
|
|
21497
|
-
return "<p></p>";
|
|
21639
|
+
return { html: "<p></p>", convertedRuns: 0 };
|
|
21498
21640
|
if (looksLikeHtml(body))
|
|
21499
|
-
return body;
|
|
21641
|
+
return mixedToHtml(body);
|
|
21500
21642
|
if (looksLikeEncodedHtml(body))
|
|
21501
|
-
return decodeEncodedHtml(body);
|
|
21643
|
+
return { html: decodeEncodedHtml(body), convertedRuns: 0 };
|
|
21644
|
+
return { html: markdownToHtml(body), convertedRuns: 0 };
|
|
21645
|
+
}
|
|
21646
|
+
var BLOCK_ELEMENTS = new Set([
|
|
21647
|
+
"address",
|
|
21648
|
+
"article",
|
|
21649
|
+
"aside",
|
|
21650
|
+
"blockquote",
|
|
21651
|
+
"dd",
|
|
21652
|
+
"details",
|
|
21653
|
+
"div",
|
|
21654
|
+
"dl",
|
|
21655
|
+
"dt",
|
|
21656
|
+
"figcaption",
|
|
21657
|
+
"figure",
|
|
21658
|
+
"footer",
|
|
21659
|
+
"h1",
|
|
21660
|
+
"h2",
|
|
21661
|
+
"h3",
|
|
21662
|
+
"h4",
|
|
21663
|
+
"h5",
|
|
21664
|
+
"h6",
|
|
21665
|
+
"header",
|
|
21666
|
+
"hr",
|
|
21667
|
+
"li",
|
|
21668
|
+
"main",
|
|
21669
|
+
"nav",
|
|
21670
|
+
"ol",
|
|
21671
|
+
"p",
|
|
21672
|
+
"pre",
|
|
21673
|
+
"section",
|
|
21674
|
+
"summary",
|
|
21675
|
+
"table",
|
|
21676
|
+
"tbody",
|
|
21677
|
+
"td",
|
|
21678
|
+
"tfoot",
|
|
21679
|
+
"th",
|
|
21680
|
+
"thead",
|
|
21681
|
+
"tr",
|
|
21682
|
+
"ul"
|
|
21683
|
+
]);
|
|
21684
|
+
var VOID_BLOCKS = new Set(["hr"]);
|
|
21685
|
+
var LEADING_BLOCK_TAG = /^\s*<([a-z][a-z0-9]*)(?=[\s>/])/i;
|
|
21686
|
+
var BLOCK_TAG = /<(\/?)([a-z][a-z0-9]*)(?=[\s>/])[^>]*?(\/?)>/gi;
|
|
21687
|
+
function mixedToHtml(body) {
|
|
21502
21688
|
const lines = body.replace(/\r\n/g, `
|
|
21503
21689
|
`).split(`
|
|
21690
|
+
`);
|
|
21691
|
+
const out = [];
|
|
21692
|
+
let run = [];
|
|
21693
|
+
let depth = 0;
|
|
21694
|
+
let convertedRuns = 0;
|
|
21695
|
+
const flushRun = () => {
|
|
21696
|
+
if (!run.length)
|
|
21697
|
+
return;
|
|
21698
|
+
const text = run.join(`
|
|
21699
|
+
`);
|
|
21700
|
+
if (text.trim()) {
|
|
21701
|
+
out.push(markdownToHtml(text, true));
|
|
21702
|
+
convertedRuns++;
|
|
21703
|
+
} else {
|
|
21704
|
+
out.push(text);
|
|
21705
|
+
}
|
|
21706
|
+
run = [];
|
|
21707
|
+
};
|
|
21708
|
+
for (const line of lines) {
|
|
21709
|
+
const lead = LEADING_BLOCK_TAG.exec(line)?.[1]?.toLowerCase();
|
|
21710
|
+
if (depth === 0 && !(lead && BLOCK_ELEMENTS.has(lead))) {
|
|
21711
|
+
run.push(line);
|
|
21712
|
+
continue;
|
|
21713
|
+
}
|
|
21714
|
+
flushRun();
|
|
21715
|
+
out.push(line);
|
|
21716
|
+
for (const [, close, name, selfClose] of line.matchAll(BLOCK_TAG)) {
|
|
21717
|
+
const tag = name.toLowerCase();
|
|
21718
|
+
if (!BLOCK_ELEMENTS.has(tag) || VOID_BLOCKS.has(tag) || selfClose)
|
|
21719
|
+
continue;
|
|
21720
|
+
depth = Math.max(0, depth + (close ? -1 : 1));
|
|
21721
|
+
}
|
|
21722
|
+
}
|
|
21723
|
+
flushRun();
|
|
21724
|
+
return { html: out.join(`
|
|
21725
|
+
`), convertedRuns };
|
|
21726
|
+
}
|
|
21727
|
+
function markdownToHtml(body, inlineHtml = false) {
|
|
21728
|
+
const kept = [];
|
|
21729
|
+
const source = inlineHtml ? body.replace(HTML_TAG, (tag, name) => isHtmlElement(name) ? `\x00${kept.push(tag) - 1}\x00` : tag) : body;
|
|
21730
|
+
const lines = source.replace(/\r\n/g, `
|
|
21731
|
+
`).split(`
|
|
21504
21732
|
`);
|
|
21505
21733
|
const out = [];
|
|
21506
21734
|
let para = [];
|
|
@@ -21598,8 +21826,12 @@ function toHtml(body) {
|
|
|
21598
21826
|
flushTable();
|
|
21599
21827
|
flushPara();
|
|
21600
21828
|
flushList();
|
|
21601
|
-
|
|
21829
|
+
const html = out.join(`
|
|
21602
21830
|
`) || "<p></p>";
|
|
21831
|
+
if (!kept.length)
|
|
21832
|
+
return html;
|
|
21833
|
+
const restore = (token) => kept[Number(token)] ?? "";
|
|
21834
|
+
return html.replace(/(<pre><code>[\s\S]*?<\/code><\/pre>)|\u0000(\d+)\u0000/g, (_m, pre, i) => pre ? pre.replace(/\u0000(\d+)\u0000/g, (_x, j) => escapeHtml(restore(j), false)) : restore(i));
|
|
21603
21835
|
}
|
|
21604
21836
|
function toText(html, maxLength = 300) {
|
|
21605
21837
|
const text = decodeEntities(html.replace(/<(style|script)[\s\S]*?<\/\1>/gi, " ").replace(/<(h[1-6]|p|li|div|tr|br)[^>]*>/gi, `
|
|
@@ -21800,17 +22032,27 @@ function safeHtmlUrl(value) {
|
|
|
21800
22032
|
return null;
|
|
21801
22033
|
return decoded;
|
|
21802
22034
|
}
|
|
22035
|
+
function keptAsText(full, warnings) {
|
|
22036
|
+
warnings.push(`Kept ${full} as text \u2014 not an HTML element`);
|
|
22037
|
+
return escapeHtml(full, false);
|
|
22038
|
+
}
|
|
21803
22039
|
function sanitizeTag(full, warnings) {
|
|
21804
22040
|
if (full.startsWith("<!--"))
|
|
21805
22041
|
return "";
|
|
21806
22042
|
const close = /^<\s*\/\s*([a-z][a-z0-9-]*)\s*>$/i.exec(full);
|
|
21807
|
-
if (close)
|
|
21808
|
-
|
|
22043
|
+
if (close) {
|
|
22044
|
+
const name = close[1].toLowerCase();
|
|
22045
|
+
if (SAFE_TAGS.has(name))
|
|
22046
|
+
return `</${name}>`;
|
|
22047
|
+
return isHtmlElement(name) ? "" : keptAsText(full, warnings);
|
|
22048
|
+
}
|
|
21809
22049
|
const open = /^<\s*([a-z][a-z0-9-]*)(\s[\s\S]*?)?\/?\s*>$/i.exec(full);
|
|
21810
22050
|
if (!open)
|
|
21811
22051
|
return "";
|
|
21812
22052
|
const tag = open[1].toLowerCase();
|
|
21813
22053
|
if (!SAFE_TAGS.has(tag)) {
|
|
22054
|
+
if (!isHtmlElement(tag))
|
|
22055
|
+
return keptAsText(full, warnings);
|
|
21814
22056
|
warnings.push(`Stripped unsupported <${tag}> element`);
|
|
21815
22057
|
return "";
|
|
21816
22058
|
}
|
|
@@ -21927,16 +22169,16 @@ function sanitizeHtml(html) {
|
|
|
21927
22169
|
}
|
|
21928
22170
|
function renderBody(body) {
|
|
21929
22171
|
const wasEncoded = looksLikeEncodedHtml(body);
|
|
21930
|
-
const
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
};
|
|
22172
|
+
const { html, convertedRuns } = renderMarkup(body);
|
|
22173
|
+
const result = sanitizeHtml(html);
|
|
22174
|
+
const notes = [];
|
|
22175
|
+
if (wasEncoded) {
|
|
22176
|
+
notes.push("Entity-encoded markup decoded to real HTML \u2014 pass tags as <p>\u2026</p>, not <p>\u2026</p>; the escaped form would otherwise be stored as literal text");
|
|
22177
|
+
}
|
|
22178
|
+
if (convertedRuns) {
|
|
22179
|
+
notes.push(`Converted ${convertedRuns} text run(s) outside HTML blocks from markdown to HTML \u2014 send a body as all markdown or all HTML to control its shape exactly`);
|
|
22180
|
+
}
|
|
22181
|
+
return notes.length ? { html: result.html, warnings: [...notes, ...result.warnings] } : result;
|
|
21940
22182
|
}
|
|
21941
22183
|
function repairedStructure(warnings) {
|
|
21942
22184
|
return warnings.filter((w) => w.startsWith("Closed unclosed block tag") || w.startsWith("<br> normalized to paragraph separators"));
|
|
@@ -22894,7 +23136,7 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
22894
23136
|
dormantAfterDays: policy.dormantAfterDays,
|
|
22895
23137
|
archiveDormantAfterDays: policy.archiveDormantAfterDays,
|
|
22896
23138
|
staleAfterDays: policy.staleAfterDays,
|
|
22897
|
-
deletionCatchupDays:
|
|
23139
|
+
deletionCatchupDays: deletionCatchupDays(cfg)
|
|
22898
23140
|
}
|
|
22899
23141
|
};
|
|
22900
23142
|
if (!cfg.root)
|
|
@@ -23043,7 +23285,7 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23043
23285
|
}
|
|
23044
23286
|
if (deep && !domainSlug) {
|
|
23045
23287
|
const feed = await trilium.getNoteHistory(cfg.root).catch(() => []);
|
|
23046
|
-
const deletionsWindow = isoDaysAgo(
|
|
23288
|
+
const deletionsWindow = isoDaysAgo(deletionCatchupDays(cfg));
|
|
23047
23289
|
const recent = feed.filter((h) => h.current_isDeleted && h.date.slice(0, 10) >= deletionsWindow);
|
|
23048
23290
|
report.scanned += recent.length;
|
|
23049
23291
|
for (const [i, h] of recent.entries()) {
|
|
@@ -23177,7 +23419,7 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23177
23419
|
{ id: cfg.knowledge.domains, kind: "domain", label: "Knowledge/Domains" }
|
|
23178
23420
|
];
|
|
23179
23421
|
for (const { id, kind, label: containerLabel } of dupeContainers) {
|
|
23180
|
-
if (!id)
|
|
23422
|
+
if (!id || domainSlug)
|
|
23181
23423
|
continue;
|
|
23182
23424
|
const all = await trilium.searchNotes(`#noteType=${kind}`, { ancestorNoteId: id, fastSearch: true, limit: 500, includeArchivedNotes: true }).catch(() => ({ results: [] }));
|
|
23183
23425
|
report.scanned += all.results.length;
|
|
@@ -23197,7 +23439,8 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23197
23439
|
}
|
|
23198
23440
|
if (cfg.knowledge.domains) {
|
|
23199
23441
|
for (const domainKind of ["information", "sources"]) {
|
|
23200
|
-
const
|
|
23442
|
+
const found = await trilium.searchNotes(`#noteType=${domainKind}`, { ancestorNoteId: cfg.knowledge.domains, fastSearch: true, limit: 500, includeArchivedNotes: true }).catch(() => ({ results: [] }));
|
|
23443
|
+
const all = { results: found.results.filter(inScope) };
|
|
23201
23444
|
report.scanned += all.results.length;
|
|
23202
23445
|
const byDomainTitle = new Map;
|
|
23203
23446
|
for (const n of all.results) {
|
|
@@ -23369,7 +23612,7 @@ async function hygienePasses(trilium, cfg, report, ctx) {
|
|
|
23369
23612
|
for (const n of heavy) {
|
|
23370
23613
|
if (!eligible(n) || ownedLabel(n, "threadShape") === "collection")
|
|
23371
23614
|
continue;
|
|
23372
|
-
report.flagged.push(`consolidate: ${n.title} [${n.noteId}] \u2014 ${n.childNoteIds.length} day-children.
|
|
23615
|
+
report.flagged.push(`consolidate: ${n.title} [${n.noteId}] \u2014 ${n.childNoteIds.length} day-children. Move what the thread has established into its domain: timeless knowledge into the information notes, measured state into Current State. The book keeps only its goal and the entries stay as the record, so a successor reads the conclusion instead of walking the history.`);
|
|
23373
23616
|
}
|
|
23374
23617
|
}
|
|
23375
23618
|
if (cfg.knowledge.domains) {
|
|
@@ -23613,9 +23856,8 @@ async function generateDailyLog(trilium, cfg, date) {
|
|
|
23613
23856
|
await trilium.addLabel(logNoteId, "created", date);
|
|
23614
23857
|
return { date, noteId: logNoteId, ...counts, action: "created" };
|
|
23615
23858
|
}
|
|
23616
|
-
var DELETION_CATCHUP_DAYS = 7;
|
|
23617
23859
|
async function catchUpDeletions(trilium, cfg, today) {
|
|
23618
|
-
const windowDays = cfg
|
|
23860
|
+
const windowDays = deletionCatchupDays(cfg);
|
|
23619
23861
|
const empty = { windowDays, coverage: "full", deletionsFound: 0, regenerated: [] };
|
|
23620
23862
|
if (!cfg.root || !cfg.insights.logs)
|
|
23621
23863
|
return empty;
|
|
@@ -23656,6 +23898,11 @@ async function catchUpDeletions(trilium, cfg, today) {
|
|
|
23656
23898
|
var txt = (obj) => ({
|
|
23657
23899
|
content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }]
|
|
23658
23900
|
});
|
|
23901
|
+
var idParams = {
|
|
23902
|
+
id: stringType().optional().describe("The note's id (noteId is accepted as an alias)"),
|
|
23903
|
+
noteId: stringType().optional().describe("Alias of id")
|
|
23904
|
+
};
|
|
23905
|
+
var missingId = (tool) => txt({ error: "missing_id", detail: `${tool}() needs the note's id.`, hint: "Pass id=<noteId> (noteId= works too)." });
|
|
23659
23906
|
var labelOf = (n, name) => n.attributes.find((a) => isOwnedAttribute(n, a) && a.type === "label" && a.name === name)?.value;
|
|
23660
23907
|
async function skim(trilium, ancestorNoteId, opts = {}) {
|
|
23661
23908
|
const { query, kind, limit = 20, includeArchived = false } = opts;
|
|
@@ -23771,14 +24018,16 @@ function registerLlmTools(server, trilium, brainRef) {
|
|
|
23771
24018
|
const b = () => brainRef.config;
|
|
23772
24019
|
server.tool("llm", `Read an LLM singleton \u2014 responsibilities, protocols or selfcorrection \u2014 or a diary entry by id. Self-correction holds general rules learned from mistakes; read it before work that has gone wrong before (audits, large edits, completeness claims). section="<heading>" reads one section.`, {
|
|
23773
24020
|
which: enumType(["responsibilities", "protocols", "selfcorrection", "diary"]),
|
|
23774
|
-
id: stringType().optional().describe("diary only: the entry id from llm_recall"),
|
|
24021
|
+
id: stringType().optional().describe("diary only: the entry id from llm_recall (noteId is accepted as an alias)"),
|
|
24022
|
+
noteId: stringType().optional().describe("Alias of id"),
|
|
23775
24023
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23776
24024
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
23777
|
-
}, async ({ which, id, section, occurrence }) => {
|
|
24025
|
+
}, async ({ which, id, noteId: idAlias, section, occurrence }) => {
|
|
23778
24026
|
if (which === "diary") {
|
|
23779
|
-
|
|
24027
|
+
const entry = id ?? idAlias;
|
|
24028
|
+
if (!entry)
|
|
23780
24029
|
throw new Error("Reading a diary entry needs its id \u2014 use llm_recall to find one.");
|
|
23781
|
-
return txt(await readFull(trilium,
|
|
24030
|
+
return txt(await readFull(trilium, entry, { section, occurrence }));
|
|
23782
24031
|
}
|
|
23783
24032
|
const noteId = b().llm[which];
|
|
23784
24033
|
if (!noteId) {
|
|
@@ -23804,18 +24053,25 @@ function registerLlmTools(server, trilium, brainRef) {
|
|
|
23804
24053
|
}
|
|
23805
24054
|
|
|
23806
24055
|
// src/tools-memory.ts
|
|
24056
|
+
var MEMORY_INDEX_DEFAULT = 30;
|
|
24057
|
+
var MEMORY_INDEX_CHAR_BUDGET = 24000;
|
|
23807
24058
|
function registerMemoryTools(server, trilium, brainRef) {
|
|
23808
24059
|
const b = () => brainRef.config;
|
|
23809
24060
|
server.tool("memory", `Read a Memory note by id \u2014 a thread or a session. A dated thread returns its Context and
|
|
23810
|
-
Resolution plus an index of its [yyyy-mm-dd] children (newest first
|
|
23811
|
-
|
|
24061
|
+
Resolution plus an index of its [yyyy-mm-dd] children (newest first: the latest 30, fewer if their
|
|
24062
|
+
index passes ~24k characters, unless limit= says otherwise; with the total count), each listed by its addendum blocks;
|
|
24063
|
+
date="yyyy-mm-dd" resolves straight to one day. A collection thread returns its
|
|
23812
24064
|
Context plus its titled entries (alphabetical, with a lead). Read a child with memory(<child id>).
|
|
23813
24065
|
section="<heading>" reads one section.`, {
|
|
23814
|
-
|
|
24066
|
+
...idParams,
|
|
23815
24067
|
date: stringType().optional().describe("Thread books only: resolve directly to this day's child note (yyyy-mm-dd)"),
|
|
24068
|
+
limit: numberType().int().positive().optional().describe("Dated threads: how many of the newest day-children to index (default 30)"),
|
|
23816
24069
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23817
24070
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
23818
|
-
}, async ({ id, date, section, occurrence }) => {
|
|
24071
|
+
}, async ({ id: idArg, noteId, date, limit, section, occurrence }) => {
|
|
24072
|
+
const id = idArg ?? noteId;
|
|
24073
|
+
if (!id)
|
|
24074
|
+
return missingId("memory");
|
|
23819
24075
|
const note = await trilium.getNote(id).catch(() => null);
|
|
23820
24076
|
if (!note || labelOf(note, "noteType") !== "thread")
|
|
23821
24077
|
return txt(await readFull(trilium, id, { section, occurrence }));
|
|
@@ -23836,14 +24092,16 @@ section="<heading>" reads one section.`, {
|
|
|
23836
24092
|
return txt(await readFull(trilium, child.results[0].noteId, { section, occurrence }));
|
|
23837
24093
|
return txt({ id, title: note.title, kind: "thread", note: `No entry for ${date}.` });
|
|
23838
24094
|
}
|
|
24095
|
+
const shown = limit ?? MEMORY_INDEX_DEFAULT;
|
|
24096
|
+
const totalChildren = note.childNoteIds.length;
|
|
23839
24097
|
const children = await trilium.searchNotes("#noteType=threadEntry", {
|
|
23840
24098
|
ancestorNoteId: id,
|
|
23841
24099
|
fastSearch: true,
|
|
23842
|
-
limit:
|
|
24100
|
+
limit: shown,
|
|
23843
24101
|
orderBy: "dateCreated",
|
|
23844
24102
|
orderDirection: "desc"
|
|
23845
24103
|
}).catch(() => ({ results: [] }));
|
|
23846
|
-
const
|
|
24104
|
+
const indexed = await Promise.all(children.results.map(async (c) => {
|
|
23847
24105
|
const content = await trilium.getNoteContent(c.noteId).catch(() => "");
|
|
23848
24106
|
const blocks = addendumIndex(content);
|
|
23849
24107
|
return {
|
|
@@ -23852,8 +24110,22 @@ section="<heading>" reads one section.`, {
|
|
|
23852
24110
|
...blocks.length ? { blocks } : { preview: toText(content, 160) }
|
|
23853
24111
|
};
|
|
23854
24112
|
}));
|
|
24113
|
+
const entries = [];
|
|
24114
|
+
let used = 0;
|
|
24115
|
+
for (const e of indexed) {
|
|
24116
|
+
const size = JSON.stringify(e).length;
|
|
24117
|
+
if (limit === undefined && entries.length && used + size > MEMORY_INDEX_CHAR_BUDGET)
|
|
24118
|
+
break;
|
|
24119
|
+
entries.push(e);
|
|
24120
|
+
used += size;
|
|
24121
|
+
}
|
|
23855
24122
|
const full = await readFull(trilium, id, { section, occurrence });
|
|
23856
|
-
return txt({
|
|
24123
|
+
return txt({
|
|
24124
|
+
...full,
|
|
24125
|
+
totalChildren,
|
|
24126
|
+
children: entries,
|
|
24127
|
+
...totalChildren > entries.length ? { more: `Showing the newest ${entries.length} of ${totalChildren} day-children. Pass limit= for more, or date= for one day.` } : {}
|
|
24128
|
+
});
|
|
23857
24129
|
});
|
|
23858
24130
|
server.tool("memory_recall", "Skim Memory: active threads and recent sessions (ids + previews). An optional query filters threads by title.", { query: stringType().optional(), limit: numberType().optional() }, async ({ query, limit }) => {
|
|
23859
24131
|
const cfg = b();
|
|
@@ -23884,10 +24156,15 @@ section="<heading>" reads one section.`, {
|
|
|
23884
24156
|
function registerKnowledgeTools(server, trilium, brainRef) {
|
|
23885
24157
|
const b = () => brainRef.config;
|
|
23886
24158
|
server.tool("knowledge", `Read a knowledge note by id \u2014 user knowledge, a domain information note or a Sources note. section="<heading>" reads one section (outline() lists them); use it on anything large.`, {
|
|
23887
|
-
|
|
24159
|
+
...idParams,
|
|
23888
24160
|
section: stringType().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
|
|
23889
24161
|
occurrence: numberType().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
|
|
23890
|
-
}, async ({ id, section, occurrence }) =>
|
|
24162
|
+
}, async ({ id, noteId, section, occurrence }) => {
|
|
24163
|
+
const target = id ?? noteId;
|
|
24164
|
+
if (!target)
|
|
24165
|
+
return missingId("knowledge");
|
|
24166
|
+
return txt(await readFull(trilium, target, { section, occurrence }));
|
|
24167
|
+
});
|
|
23891
24168
|
server.tool("knowledge_recall", "Skim Knowledge. With a domain: that domain's Sources + information notes. Without: user-knowledge notes plus the list of domains.", { query: stringType().optional(), domain: stringType().optional() }, async ({ query, domain }) => {
|
|
23892
24169
|
const cfg = b();
|
|
23893
24170
|
if (domain) {
|
|
@@ -23921,7 +24198,7 @@ section="<heading>" reads one section instead of the whole log.`, {
|
|
|
23921
24198
|
return txt({ date: d, note: "No log for this day." });
|
|
23922
24199
|
return txt(await readFull(trilium, found.results[0].noteId, { section, occurrence }));
|
|
23923
24200
|
});
|
|
23924
|
-
server.tool("insights_recall", "Skim recent change logs (
|
|
24201
|
+
server.tool("insights_recall", "Skim the most recent daily change logs (dates + previews), newest first. limit sets how many days (default 14). For one day's full log use insights(date).", { limit: numberType().optional() }, async ({ limit }) => txt(await skim(trilium, b().insights.logs, { kind: "log", limit: limit ?? 14 })));
|
|
23925
24202
|
}
|
|
23926
24203
|
|
|
23927
24204
|
// src/tools.ts
|
|
@@ -23970,6 +24247,20 @@ function registerTools(server, trilium, brainRef) {
|
|
|
23970
24247
|
const found = await trilium.searchNotes(`#noteType=session #created='${date}'`, { ancestorNoteId: cfg.memory.sessions, fastSearch: true, limit: 1 });
|
|
23971
24248
|
return found.results[0]?.noteId ?? null;
|
|
23972
24249
|
};
|
|
24250
|
+
const ensureGateNote = async (date) => {
|
|
24251
|
+
const existing = await gateNote(date);
|
|
24252
|
+
if (existing)
|
|
24253
|
+
return existing;
|
|
24254
|
+
const cfg = b();
|
|
24255
|
+
if (!cfg.memory.sessions)
|
|
24256
|
+
return null;
|
|
24257
|
+
const created = await trilium.createNote(cfg.memory.sessions, `[${date}]`, contentFor("session", { date, body: "" }));
|
|
24258
|
+
const noteId = created.note.noteId;
|
|
24259
|
+
await trilium.addLabel(noteId, "noteType", "session");
|
|
24260
|
+
await trilium.addLabel(noteId, "created", date);
|
|
24261
|
+
await ensureIcon(trilium, noteId);
|
|
24262
|
+
return noteId;
|
|
24263
|
+
};
|
|
23973
24264
|
const parseGate = (raw) => {
|
|
23974
24265
|
const out = new Map;
|
|
23975
24266
|
for (const pair of (raw ?? "").split(",")) {
|
|
@@ -23982,9 +24273,9 @@ function registerTools(server, trilium, brainRef) {
|
|
|
23982
24273
|
const serializeGate = (m) => [...m.entries()].map(([step, seq]) => `${step}:${seq}`).join(",");
|
|
23983
24274
|
const markStep = async (step, date) => {
|
|
23984
24275
|
const d = checkedDate(date);
|
|
23985
|
-
const noteId = await
|
|
24276
|
+
const noteId = await ensureGateNote(d);
|
|
23986
24277
|
if (!noteId)
|
|
23987
|
-
throw new Error(`Cannot record pre-close step "${step}":
|
|
24278
|
+
throw new Error(`Cannot record pre-close step "${step}": the brain has no Sessions container \u2014 run bootstrap()`);
|
|
23988
24279
|
const note = await trilium.getNote(noteId);
|
|
23989
24280
|
const stored = parseGate(labelOf2(note, "gate"));
|
|
23990
24281
|
const next = Math.max(preCloseSeq, ...[...stored.values()], 0) + 1;
|
|
@@ -24092,7 +24383,7 @@ ${block}` }));
|
|
|
24092
24383
|
deleted: [],
|
|
24093
24384
|
flagged: [`sweep failed: ${e}`],
|
|
24094
24385
|
dryRun: false,
|
|
24095
|
-
policy: { dormantAfterDays: cfg.policy.dormantAfterDays, archiveDormantAfterDays: cfg.policy.archiveDormantAfterDays, staleAfterDays: cfg.policy.staleAfterDays, deletionCatchupDays: cfg
|
|
24386
|
+
policy: { dormantAfterDays: cfg.policy.dormantAfterDays, archiveDormantAfterDays: cfg.policy.archiveDormantAfterDays, staleAfterDays: cfg.policy.staleAfterDays, deletionCatchupDays: deletionCatchupDays(cfg) }
|
|
24096
24387
|
})),
|
|
24097
24388
|
buildDigest(trilium, cfg, { depth: depth ?? "digest" })
|
|
24098
24389
|
]);
|
|
@@ -24129,11 +24420,7 @@ ${block}` }));
|
|
|
24129
24420
|
sessionPreview = toText(content, 200);
|
|
24130
24421
|
newDay = !/<h2(?:\s[^>]*)?>\s*Addendum/i.test(content);
|
|
24131
24422
|
} else {
|
|
24132
|
-
|
|
24133
|
-
sessionNoteId = created.note.noteId;
|
|
24134
|
-
await trilium.addLabel(sessionNoteId, "noteType", "session");
|
|
24135
|
-
await trilium.addLabel(sessionNoteId, "created", todayStr);
|
|
24136
|
-
await ensureIcon(trilium, sessionNoteId);
|
|
24423
|
+
sessionNoteId = await ensureGateNote(todayStr);
|
|
24137
24424
|
newDay = true;
|
|
24138
24425
|
}
|
|
24139
24426
|
} catch {}
|
|
@@ -25055,7 +25342,7 @@ ${block}`), d);
|
|
|
25055
25342
|
|
|
25056
25343
|
Modes: default append (a dated addendum \u2014 right only for records; a dated thread's append lands in today's child), mode=replace (whole body), section="<heading>" (replace that section's whole body, h2\u2192h3\u2192h4, occurrence= for repeats), section= + mode=before|after (insert a sibling block around the whole section), mode=prepend (top of the section body), mode=remove (delete the section). find="<exact stored text>" replaces every occurrence (nth= for one); edits=[{find, body}] applies several in one write. title= composes with every mode; retitling a domain book cascades its #domain slug.
|
|
25057
25344
|
|
|
25058
|
-
Check the receipt: matched=false means a NEW section was written (available= lists real headings; strict=true refuses instead). A section replace swaps everything under the heading \u2014 use find= for anything smaller. find= matches stored HTML (tags literal), not rendered text.`, {
|
|
25345
|
+
Check the receipt: matched=false means a NEW section was written (available= lists real headings; strict=true refuses instead). A section replace swaps everything under the heading \u2014 use find= for anything smaller. find= matches stored HTML (tags literal), not rendered text, and is taken literally: a Windows path's backslash is one backslash, never doubled.`, {
|
|
25059
25346
|
noteId: stringType().describe("Note to update"),
|
|
25060
25347
|
body: stringType().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
|
|
25061
25348
|
title: stringType().optional().describe("New title (normalized server-side)"),
|
|
@@ -26218,7 +26505,7 @@ assertion + check \u2192 register (deduped by assertion); claimId + holds + evid
|
|
|
26218
26505
|
...found.length === 0 ? { note: "No notes with pending addendums." } : { hint: "Fold each addendum into its note: read the block, then revise(noteId, section='<heading>', body='<merged content>', mode='replace') to absorb it into the right section. These notes must be left clean \u2014 no addendum markers. Addendum-style history belongs only in sessions, diary, and logs. Scoped/autonomous agents: fold only what's in your lane \u2014 leaving personal or out-of-scope addendums for the next interactive session is correct, and calling this tool is what satisfies the pre-close gate." }
|
|
26219
26506
|
});
|
|
26220
26507
|
});
|
|
26221
|
-
server.tool("maintain", `Brain hygiene. Lite (automatic in start/close): ages threads active \u2192 dormant \u2192 archived and checks labels. deep=true adds stale notes, orphans and sinks, structural lint (duplicate headings, unbalanced tags, missing required sections, dated prose in timeless notes, oversized notes), duplicate titles, lapsed or broken claims and hygiene passes. dryRun previews. ack=[ids] silences a note you reviewed until its content changes. domain= narrows deep passes to one lane. repair=[ids] unwinds entity double-escaping in place. coverage names any capped pass.`, {
|
|
26508
|
+
server.tool("maintain", `Brain hygiene. Lite (automatic in start/close): ages threads active \u2192 dormant \u2192 archived and checks labels. deep=true adds stale notes, orphans and sinks, structural lint (duplicate headings, unbalanced tags, missing required sections, dated prose in timeless notes, oversized notes), duplicate titles, lapsed or broken claims and hygiene passes. dryRun previews. ack=[ids] silences a note you reviewed until its content changes. domain= narrows deep passes to one lane. repair=[ids] unwinds entity double-escaping in place. coverage names any capped pass: structural lint reads at most 40 notes per run and rotates through the rest day by day, so a whole-brain lint spans several runs.`, {
|
|
26222
26509
|
deep: booleanType().optional().describe("Deep pass: stale-review + orphan/sink + structural lint + duplicate titles across Memory/Threads and Knowledge (default: false)"),
|
|
26223
26510
|
dryRun: booleanType().optional().describe("Report what would change without changing it"),
|
|
26224
26511
|
domain: stringType().optional().describe("Narrow the deep passes to one domain's notes (slugged server-side) \u2014 for scoped agents working a single lane"),
|
|
@@ -27142,7 +27429,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27142
27429
|
const note = await trilium.getNote(noteId);
|
|
27143
27430
|
return txt4(await trilium.getNoteContentResult(noteId, note.type));
|
|
27144
27431
|
});
|
|
27145
|
-
server.tool("create_note",
|
|
27432
|
+
server.tool("create_note", 'Create a note at an explicit parent. Types: text/code/book/canvas/mermaid/relationMap/render/search/file/image. The note gets no #noteType, icon or placement, so core tools cannot see it until it is typed with label(id, "noteType", <kind>); use remember() for a memory.', {
|
|
27146
27433
|
parentNoteId: stringType(),
|
|
27147
27434
|
title: stringType(),
|
|
27148
27435
|
content: stringType(),
|
|
@@ -27159,7 +27446,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27159
27446
|
throw error;
|
|
27160
27447
|
}
|
|
27161
27448
|
});
|
|
27162
|
-
server.tool("update_note_content", "Replace a note's full content. Binary input must use encoding=base64; text remains UTF-8.", { noteId: stringType(), content: stringType(), mime: stringType().optional(), encoding: enumType(["auto", "text", "base64"]).optional() }, async ({ noteId, content, mime, encoding }) => {
|
|
27449
|
+
server.tool("update_note_content", "Replace a note's full content. Binary input must use encoding=base64; text remains UTF-8. No revision is taken and nothing is sanitised: call create_revision first to keep the prior content recoverable, and prefer revise() for any BrainLLM note.", { noteId: stringType(), content: stringType(), mime: stringType().optional(), encoding: enumType(["auto", "text", "base64"]).optional() }, async ({ noteId, content, mime, encoding }) => {
|
|
27163
27450
|
const note = await trilium.getNote(noteId);
|
|
27164
27451
|
const targetMime = mime ?? note.mime;
|
|
27165
27452
|
try {
|
|
@@ -27183,7 +27470,7 @@ note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is giv
|
|
|
27183
27470
|
const note = await trilium.patchNote(noteId, fields);
|
|
27184
27471
|
return txt4({ noteId: note.noteId, title: note.title, type: note.type });
|
|
27185
27472
|
});
|
|
27186
|
-
server.tool("delete_note", "Hard-delete a note (
|
|
27473
|
+
server.tool("delete_note", "Hard-delete a single note. Irreversible. Refused (blocked: true) while any relation points at it (backlinks_exist), while it has children or more than one parent (blast_radius), or if the backlink check itself fails. Remove or retarget those first; for a recoverable removal use forget().", { noteId: stringType() }, async ({ noteId }) => {
|
|
27187
27474
|
const note = await trilium.getNote(noteId);
|
|
27188
27475
|
let backlinks;
|
|
27189
27476
|
try {
|
|
@@ -28514,6 +28801,12 @@ if (!brain) {
|
|
|
28514
28801
|
console.error("[brainllm] BrainLLM not found in Trilium. Run the bootstrap tool to initialize.");
|
|
28515
28802
|
}
|
|
28516
28803
|
}
|
|
28804
|
+
{
|
|
28805
|
+
const { invalid } = envDeletionCatchupDays();
|
|
28806
|
+
if (invalid !== undefined) {
|
|
28807
|
+
console.error(`[brainllm] BRAINLLM_DELETION_CATCHUP_DAYS="${invalid}" ignored \u2014 expected a whole number of days from 1 to 3650. Using ${deletionCatchupDays(brain ?? EMPTY_BRAINLLM)}.`);
|
|
28808
|
+
}
|
|
28809
|
+
}
|
|
28517
28810
|
var brainRef = { config: brain ?? EMPTY_BRAINLLM };
|
|
28518
28811
|
var port = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
|
|
28519
28812
|
var authToken = process.env.MCP_AUTH_TOKEN;
|
|
@@ -28536,7 +28829,7 @@ function createServer(origin = null) {
|
|
|
28536
28829
|
const s = new McpServer({
|
|
28537
28830
|
name: "BrainLLM",
|
|
28538
28831
|
title: "BrainLLM",
|
|
28539
|
-
version: "12.
|
|
28832
|
+
version: "12.6.1",
|
|
28540
28833
|
icons: brandingIcons(origin)
|
|
28541
28834
|
});
|
|
28542
28835
|
registerTools(s, trilium, brainRef);
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "brainllm",
|
|
4
4
|
"display_name": "BrainLLM",
|
|
5
|
-
"version": "12.
|
|
5
|
+
"version": "12.6.1",
|
|
6
6
|
"description": "Turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
|
|
7
7
|
"long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
|
|
8
8
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainllm",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.6.1",
|
|
4
4
|
"mcpName": "io.github.miisodev/brainllm",
|
|
5
5
|
"description": "MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
|
|
6
6
|
"author": "Kevin Miiso Novo <miisodev@gmail.com>",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.miisodev/brainllm",
|
|
4
4
|
"description": "Persistent graph-structured memory for LLMs in TriliumNext Notes — readable and editable by humans.",
|
|
5
|
-
"version": "12.
|
|
5
|
+
"version": "12.6.1",
|
|
6
6
|
"websiteUrl": "https://github.com/miisodev/BrainLLM#readme",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/miisodev/BrainLLM",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "brainllm",
|
|
15
|
-
"version": "12.
|
|
15
|
+
"version": "12.6.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/skills/brainllm/SKILL.md
CHANGED
|
@@ -80,7 +80,7 @@ worth keeping?
|
|
|
80
80
|
└─ passing remark or already known from training ─→ don't capture
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
**Domains
|
|
83
|
+
**Domains are your own working knowledge of a subject as it currently is.** Their scope, depth and size are your call, set by what keeps you confident enough to act and advise well; refresh one whenever reality moves. The structure keeps that knowledge tidy and checkable, and never caps it: one maintained **Sources** note plus one information note per sub-category, including **Current State**, revised in place. Every claim traces to a Sources entry (❇️ discovered, ✅ used); `remember(kind="sources", revision=[{source, marker, date}])` upserts the Revision row. Creating a domain creates its Sources note. Read `references/domains.md` before creating a domain, adding a sub-category or refreshing one.
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
@@ -89,7 +89,7 @@ worth keeping?
|
|
|
89
89
|
| Need | Tool |
|
|
90
90
|
|---|---|
|
|
91
91
|
| a singleton | `master(which)` / `llm(which)` — `section=` for one section |
|
|
92
|
-
| a thread or session | `memory(id)` — dated index or collection entries; `date=` for one day |
|
|
92
|
+
| a thread or session | `memory(id)` — dated index (newest days, up to 30 and ~24k characters; `limit=` for more) or collection entries; `date=` for one day |
|
|
93
93
|
| a knowledge note | `knowledge(id)` — `section=` for one section |
|
|
94
94
|
| a day's change log | `insights(date?)` |
|
|
95
95
|
| skim a surface | `<surface>_recall` |
|
|
@@ -122,7 +122,7 @@ Every read that can be large takes `section=`; a note past the read ceiling cann
|
|
|
122
122
|
- **Wire at creation:** `connect=[{relation, toNoteId}]` on the same call. An unconnected note is an orphan until wired.
|
|
123
123
|
- **Identity line** (`"LLM · environment · agent/mode [· Run N]"`) is required on diary, close and dated thread appends.
|
|
124
124
|
- **Every note carries an icon except logs.** The server sets the kind default at creation (a thread entry takes its thread's icon) and the sweep backfills any missing; `icon=` picks a better one (boxicons class or bare name). Removing an icon is refused.
|
|
125
|
-
- Bodies may be text, markdown or HTML; the server normalises them and reports `sanitized[]
|
|
125
|
+
- Bodies may be text, markdown or HTML; the server normalises them to editor-native HTML and reports `sanitized[]` (markdown mixed into an HTML body is converted; a placeholder like `<name>` stays as text).
|
|
126
126
|
- `diary`, `session`, `log`, `claim` and `domain` have dedicated paths; `remember` refuses them.
|
|
127
127
|
|
|
128
128
|
**Four writing rules** (each applies to every note you touch):
|
|
@@ -214,6 +214,7 @@ Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=tr
|
|
|
214
214
|
| timeouts or connection errors | the Trilium instance at `TRILIUM_BASE_URL` is unreachable; tools don't need restarting |
|
|
215
215
|
| `start()` → `uninitialized` | `bootstrap()` |
|
|
216
216
|
| dates look off on a hosted deploy | set `BRAINLLM_TZ` (IANA) |
|
|
217
|
+
| deletions older than a week go unreported | Trilium keeps deleted notes longer than its 7-day default: set `BRAINLLM_DELETION_CATCHUP_DAYS` to match |
|
|
217
218
|
| an informational `{error, detail, hint}` | read `hint` and retry with corrected arguments |
|
|
218
219
|
|
|
219
220
|
## References
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Read this before creating a domain, adding a sub-category note, or refreshing domain content, then follow the one protocol that matches. Never skip the sources gate and never manufacture knowledge.
|
|
4
4
|
|
|
5
|
+
**A domain is your own working knowledge of a subject as it currently is.** What it covers, how deep it goes and how large it grows are your call. The measure is confidence: enough that a session reading the domain can act and advise well without re-deriving the subject, and no more. The structure below does not cap that knowledge. It keeps it tidy and makes every claim checkable against a source. When reality moves, refresh the domain rather than waiting to be asked.
|
|
6
|
+
|
|
5
7
|
## The shape of a domain
|
|
6
8
|
|
|
7
9
|
A domain is **one Sources note plus one information note per sub-category**, each a maintained document revised in place.
|
|
@@ -20,7 +20,7 @@ BrainLLM memories are typed text notes; `code` / `file` / `image` notes aren't a
|
|
|
20
20
|
- **Code / structured text** → embed it as a fenced block in an `information` note via core `remember` (fully conformant and full-text searchable), or
|
|
21
21
|
- **Binary (image, PDF, file)** → core `attach()` it onto the relevant typed note (`role=image` / `file`) — pass standard base64 with `encoding="base64"`; read back with `attach(noteId, title)`, which returns the base64 envelope and MIME.
|
|
22
22
|
|
|
23
|
-
Create a standalone `type=code` / `file` note only when you specifically need Trilium's native handling of that type. If you do: label it (`add_label noteType <closest kind>`) so `recall` can see it, give it the same `domain` / `topic` labels as its anchor, and `connect` it to a typed note — otherwise it's an orphan
|
|
23
|
+
Create a standalone `type=code` / `file` note only when you specifically need Trilium's native handling of that type. If you do: label it (`add_label noteType <closest kind>`) so `recall` can see it, give it the same `domain` / `topic` labels as its anchor, and `connect` it to a typed note — otherwise it's an untyped orphan, exactly what `maintain(deep=true)` flags.
|
|
24
24
|
|
|
25
25
|
### Use-case → tool
|
|
26
26
|
|
|
@@ -47,4 +47,3 @@
|
|
|
47
47
|
| `bootstrap` created a duplicate brain tree | Caused by a transient network/auth error during the existence check — the old catch-all fell through to fresh create. Fixed: only a confirmed 404 (root deleted) now triggers a new tree. To recover: identify the newer duplicate by `dateCreated`, then `forget(rootId, hard=true)` its entire subtree or delete it directly in Trilium. |
|
|
48
48
|
| Hosted deploy: `ENOENT` on startup / auto-discovery fails with a file-path error | `BRAINLLM_CONFIG` is set to a directory path instead of a file path (e.g. `/home/node/trilium-data` instead of `/vol/brainllm.json`). Mount a persistent volume on the BrainLLM MCP service (not the Trilium service) and set `BRAINLLM_CONFIG` to a file path inside it, or remove `BRAINLLM_CONFIG` entirely to rely on auto-discovery each startup. |
|
|
49
49
|
| `revise`/`remember` on a thread returns `collection_thread` | It is a collection thread: add entries with `remember(kind="threadEntry", thread=, title=)`, edit them with `revise(<entry id>)` |
|
|
50
|
-
| `content is not standard RFC 4648 base64` on a text note | Fixed in V12.5: a blank MIME type is now text. On an older server, rewrite the note with `update_note_content(mime="text/html", encoding="text")` |
|