brainllm 12.8.0 → 12.9.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 +1 -1
- package/README.md +4 -2
- package/SECURITY.md +4 -2
- package/dist/index.js +707 -183
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +13 -10
- package/skills/brainllm/references/troubleshooting.md +5 -1
package/dist/index.js
CHANGED
|
@@ -17351,6 +17351,9 @@ class Protocol {
|
|
|
17351
17351
|
this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
|
|
17352
17352
|
const handleTaskResult = async () => {
|
|
17353
17353
|
const taskId = request.params.taskId;
|
|
17354
|
+
if (!await this._taskStore.getTask(taskId, extra.sessionId)) {
|
|
17355
|
+
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
|
|
17356
|
+
}
|
|
17354
17357
|
if (this._taskMessageQueue) {
|
|
17355
17358
|
let queuedMessage;
|
|
17356
17359
|
while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) {
|
|
@@ -17381,12 +17384,12 @@ class Protocol {
|
|
|
17381
17384
|
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
|
|
17382
17385
|
}
|
|
17383
17386
|
if (!isTerminal(task.status)) {
|
|
17384
|
-
await this._waitForTaskUpdate(taskId, extra.signal);
|
|
17387
|
+
await this._waitForTaskUpdate(taskId, extra.signal, extra.sessionId);
|
|
17385
17388
|
return await handleTaskResult();
|
|
17386
17389
|
}
|
|
17387
17390
|
if (isTerminal(task.status)) {
|
|
17388
17391
|
const result = await this._taskStore.getTaskResult(taskId, extra.sessionId);
|
|
17389
|
-
this._clearTaskQueue(taskId);
|
|
17392
|
+
this._clearTaskQueue(taskId, extra.sessionId);
|
|
17390
17393
|
return {
|
|
17391
17394
|
...result,
|
|
17392
17395
|
_meta: {
|
|
@@ -17423,7 +17426,7 @@ class Protocol {
|
|
|
17423
17426
|
throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
|
|
17424
17427
|
}
|
|
17425
17428
|
await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId);
|
|
17426
|
-
this._clearTaskQueue(request.params.taskId);
|
|
17429
|
+
this._clearTaskQueue(request.params.taskId, extra.sessionId);
|
|
17427
17430
|
const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
|
|
17428
17431
|
if (!cancelledTask) {
|
|
17429
17432
|
throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
|
|
@@ -17546,6 +17549,19 @@ class Protocol {
|
|
|
17546
17549
|
const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
|
|
17547
17550
|
const capturedTransport = this._transport;
|
|
17548
17551
|
const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
|
|
17552
|
+
const sessionId = capturedTransport?.sessionId;
|
|
17553
|
+
const store = this._taskStore;
|
|
17554
|
+
let relatedTaskFound = true;
|
|
17555
|
+
let relatedTaskLookup;
|
|
17556
|
+
if (relatedTaskId && store && this._taskMessageQueue && sessionId !== undefined) {
|
|
17557
|
+
relatedTaskFound = false;
|
|
17558
|
+
relatedTaskLookup = (async () => {
|
|
17559
|
+
if (!await store.getTask(relatedTaskId, sessionId)) {
|
|
17560
|
+
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${relatedTaskId}`);
|
|
17561
|
+
}
|
|
17562
|
+
relatedTaskFound = true;
|
|
17563
|
+
})();
|
|
17564
|
+
}
|
|
17549
17565
|
if (handler === undefined) {
|
|
17550
17566
|
const errorResponse = {
|
|
17551
17567
|
jsonrpc: "2.0",
|
|
@@ -17555,7 +17571,10 @@ class Protocol {
|
|
|
17555
17571
|
message: "Method not found"
|
|
17556
17572
|
}
|
|
17557
17573
|
};
|
|
17558
|
-
if (relatedTaskId &&
|
|
17574
|
+
if (relatedTaskId && relatedTaskLookup) {
|
|
17575
|
+
const queuedError = { type: "error", message: errorResponse, timestamp: Date.now() };
|
|
17576
|
+
relatedTaskLookup.then(() => this._enqueueTaskMessage(relatedTaskId, queuedError, sessionId), () => capturedTransport?.send(errorResponse)).catch((error) => this._onerror(new Error(`Failed to send an error response: ${error}`)));
|
|
17577
|
+
} else if (relatedTaskId && this._taskMessageQueue) {
|
|
17559
17578
|
this._enqueueTaskMessage(relatedTaskId, {
|
|
17560
17579
|
type: "error",
|
|
17561
17580
|
message: errorResponse,
|
|
@@ -17606,7 +17625,10 @@ class Protocol {
|
|
|
17606
17625
|
closeSSEStream: extra?.closeSSEStream,
|
|
17607
17626
|
closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
|
|
17608
17627
|
};
|
|
17609
|
-
Promise.resolve().then(() => {
|
|
17628
|
+
(relatedTaskLookup ?? Promise.resolve()).then(() => {
|
|
17629
|
+
if (relatedTaskLookup && abortController.signal.aborted) {
|
|
17630
|
+
throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
|
|
17631
|
+
}
|
|
17610
17632
|
if (taskCreationParams) {
|
|
17611
17633
|
this.assertTaskHandlerCapability(request.method);
|
|
17612
17634
|
}
|
|
@@ -17641,7 +17663,7 @@ class Protocol {
|
|
|
17641
17663
|
...error["data"] !== undefined && { data: error["data"] }
|
|
17642
17664
|
}
|
|
17643
17665
|
};
|
|
17644
|
-
if (relatedTaskId && this._taskMessageQueue) {
|
|
17666
|
+
if (relatedTaskId && this._taskMessageQueue && relatedTaskFound) {
|
|
17645
17667
|
await this._enqueueTaskMessage(relatedTaskId, {
|
|
17646
17668
|
type: "error",
|
|
17647
17669
|
message: errorResponse,
|
|
@@ -18029,7 +18051,7 @@ class Protocol {
|
|
|
18029
18051
|
throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
|
|
18030
18052
|
}
|
|
18031
18053
|
const maxQueueSize = this._options?.maxTaskQueueSize;
|
|
18032
|
-
await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
|
|
18054
|
+
await this._taskMessageQueue.enqueue(taskId, message, sessionId ?? this._transport?.sessionId, maxQueueSize);
|
|
18033
18055
|
}
|
|
18034
18056
|
async _clearTaskQueue(taskId, sessionId) {
|
|
18035
18057
|
if (this._taskMessageQueue) {
|
|
@@ -18048,10 +18070,10 @@ class Protocol {
|
|
|
18048
18070
|
}
|
|
18049
18071
|
}
|
|
18050
18072
|
}
|
|
18051
|
-
async _waitForTaskUpdate(taskId, signal) {
|
|
18073
|
+
async _waitForTaskUpdate(taskId, signal, sessionId) {
|
|
18052
18074
|
let interval = this._options?.defaultTaskPollInterval ?? 1000;
|
|
18053
18075
|
try {
|
|
18054
|
-
const task = await this._taskStore?.getTask(taskId);
|
|
18076
|
+
const task = await this._taskStore?.getTask(taskId, sessionId);
|
|
18055
18077
|
if (task?.pollInterval) {
|
|
18056
18078
|
interval = task.pollInterval;
|
|
18057
18079
|
}
|
|
@@ -18718,6 +18740,43 @@ class ExperimentalMcpServerTasks {
|
|
|
18718
18740
|
}
|
|
18719
18741
|
}
|
|
18720
18742
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
|
|
18743
|
+
function toolInputElementCount(value, max) {
|
|
18744
|
+
let count = 0;
|
|
18745
|
+
const stack = [value];
|
|
18746
|
+
while (stack.length > 0) {
|
|
18747
|
+
const node = stack.pop();
|
|
18748
|
+
if (node === null || typeof node !== "object")
|
|
18749
|
+
continue;
|
|
18750
|
+
if (Array.isArray(node)) {
|
|
18751
|
+
for (const child of node) {
|
|
18752
|
+
if (++count > max)
|
|
18753
|
+
return count;
|
|
18754
|
+
if (child !== null && typeof child === "object")
|
|
18755
|
+
stack.push(child);
|
|
18756
|
+
}
|
|
18757
|
+
} else {
|
|
18758
|
+
for (const key in node) {
|
|
18759
|
+
if (!Object.prototype.hasOwnProperty.call(node, key))
|
|
18760
|
+
continue;
|
|
18761
|
+
if (++count > max)
|
|
18762
|
+
return count;
|
|
18763
|
+
const child = node[key];
|
|
18764
|
+
if (child !== null && typeof child === "object")
|
|
18765
|
+
stack.push(child);
|
|
18766
|
+
}
|
|
18767
|
+
}
|
|
18768
|
+
}
|
|
18769
|
+
return count;
|
|
18770
|
+
}
|
|
18771
|
+
function resolveMaxToolInputElements(value) {
|
|
18772
|
+
if (value === undefined || value === Infinity)
|
|
18773
|
+
return;
|
|
18774
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 1) {
|
|
18775
|
+
throw new RangeError(`maxToolInputElements must be a number of at least 1, or Infinity, got ${String(value)}`);
|
|
18776
|
+
}
|
|
18777
|
+
return value;
|
|
18778
|
+
}
|
|
18779
|
+
|
|
18721
18780
|
class McpServer {
|
|
18722
18781
|
constructor(serverInfo, options) {
|
|
18723
18782
|
this._registeredResources = {};
|
|
@@ -18729,6 +18788,7 @@ class McpServer {
|
|
|
18729
18788
|
this._resourceHandlersInitialized = false;
|
|
18730
18789
|
this._promptHandlersInitialized = false;
|
|
18731
18790
|
this.server = new Server(serverInfo, options);
|
|
18791
|
+
this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
|
|
18732
18792
|
}
|
|
18733
18793
|
get experimental() {
|
|
18734
18794
|
if (!this._experimental) {
|
|
@@ -18835,12 +18895,15 @@ class McpServer {
|
|
|
18835
18895
|
};
|
|
18836
18896
|
}
|
|
18837
18897
|
async validateToolInput(tool, args, toolName) {
|
|
18898
|
+
if (this._maxToolInputElements !== undefined && toolInputElementCount(args, this._maxToolInputElements) > this._maxToolInputElements) {
|
|
18899
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${toolName}: arguments contain more than the maximum of ${this._maxToolInputElements} elements`);
|
|
18900
|
+
}
|
|
18838
18901
|
if (!tool.inputSchema) {
|
|
18839
18902
|
return;
|
|
18840
18903
|
}
|
|
18841
18904
|
const inputObj = normalizeObjectSchema(tool.inputSchema);
|
|
18842
18905
|
const schemaToParse = inputObj ?? tool.inputSchema;
|
|
18843
|
-
const parseResult = await safeParseAsync2(schemaToParse, args);
|
|
18906
|
+
const parseResult = await safeParseAsync2(schemaToParse, args ?? {});
|
|
18844
18907
|
if (!parseResult.success) {
|
|
18845
18908
|
const error = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
18846
18909
|
const errorMessage = getParseErrorMessage(error);
|
|
@@ -19065,7 +19128,7 @@ class McpServer {
|
|
|
19065
19128
|
}
|
|
19066
19129
|
if (prompt.argsSchema) {
|
|
19067
19130
|
const argsObj = normalizeObjectSchema(prompt.argsSchema);
|
|
19068
|
-
const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
|
|
19131
|
+
const parseResult = await safeParseAsync2(argsObj, request.params.arguments ?? {});
|
|
19069
19132
|
if (!parseResult.success) {
|
|
19070
19133
|
const error = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
19071
19134
|
const errorMessage = getParseErrorMessage(error);
|
|
@@ -21591,6 +21654,16 @@ function looksLikeHtml(body) {
|
|
|
21591
21654
|
return true;
|
|
21592
21655
|
return false;
|
|
21593
21656
|
}
|
|
21657
|
+
function isStructuralTag(name) {
|
|
21658
|
+
const tag = name.toLowerCase();
|
|
21659
|
+
return SAFE_TAGS.has(tag) || BLOCK_ELEMENTS.has(tag);
|
|
21660
|
+
}
|
|
21661
|
+
function looksLikeStructuralHtml(body) {
|
|
21662
|
+
for (const m of body.matchAll(HTML_TAG))
|
|
21663
|
+
if (isStructuralTag(m[1]))
|
|
21664
|
+
return true;
|
|
21665
|
+
return false;
|
|
21666
|
+
}
|
|
21594
21667
|
var ENCODED_TAG = /<\/?[a-z][a-z0-9-]*(?:\s[^&<>]*?)?\s*\/?>/i;
|
|
21595
21668
|
function looksLikeEncodedHtml(body) {
|
|
21596
21669
|
return !looksLikeHtml(body) && ENCODED_TAG.test(body);
|
|
@@ -21658,7 +21731,7 @@ function renderMarkup(raw) {
|
|
|
21658
21731
|
const body = raw.replace(/\u0000/g, "");
|
|
21659
21732
|
if (!body.trim())
|
|
21660
21733
|
return { html: "<p></p>", convertedRuns: 0 };
|
|
21661
|
-
if (
|
|
21734
|
+
if (looksLikeStructuralHtml(body))
|
|
21662
21735
|
return mixedToHtml(body);
|
|
21663
21736
|
if (looksLikeEncodedHtml(body))
|
|
21664
21737
|
return { html: decodeEncodedHtml(body), convertedRuns: 0 };
|
|
@@ -21747,7 +21820,7 @@ function mixedToHtml(body) {
|
|
|
21747
21820
|
}
|
|
21748
21821
|
function markdownToHtml(body, inlineHtml = false) {
|
|
21749
21822
|
const kept = [];
|
|
21750
|
-
const source = inlineHtml ? body.replace(HTML_TAG, (tag, name) =>
|
|
21823
|
+
const source = inlineHtml ? body.replace(HTML_TAG, (tag, name) => isStructuralTag(name) ? `\x00${kept.push(tag) - 1}\x00` : tag) : body;
|
|
21751
21824
|
const lines = source.replace(/\r\n/g, `
|
|
21752
21825
|
`).split(`
|
|
21753
21826
|
`);
|
|
@@ -21978,6 +22051,9 @@ function stripTagsWithMap(html) {
|
|
|
21978
22051
|
function escapeQueryValue(s) {
|
|
21979
22052
|
return s.replace(/["\\]/g, " ").replace(/\s+/g, " ").trim();
|
|
21980
22053
|
}
|
|
22054
|
+
function triliumSearchForm(pattern) {
|
|
22055
|
+
return pattern.normalize("NFD").replace(/\p{Diacritic}/gu, (c) => c.charCodeAt(0) < 128 ? c : "");
|
|
22056
|
+
}
|
|
21981
22057
|
function escapeQueryRegex(s) {
|
|
21982
22058
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
21983
22059
|
}
|
|
@@ -22171,12 +22247,21 @@ function sanitizeHtml(html) {
|
|
|
22171
22247
|
n++;
|
|
22172
22248
|
return "";
|
|
22173
22249
|
});
|
|
22174
|
-
|
|
22250
|
+
let dropped = 0;
|
|
22251
|
+
s = s.replace(/<(script|style|noscript|iframe)(\s[^>]*)?>[\s\S]*$/gi, (m) => {
|
|
22175
22252
|
n++;
|
|
22253
|
+
dropped += m.length;
|
|
22176
22254
|
return "";
|
|
22177
22255
|
});
|
|
22178
22256
|
if (n)
|
|
22179
|
-
warnings.push(`Stripped ${n} forbidden element block(s) \u2014 active/content-bearing tags are not allowed`);
|
|
22257
|
+
warnings.push(`Stripped ${n} forbidden element block(s) \u2014 active/content-bearing tags are not allowed${dropped ? ` (${dropped} characters removed with an unclosed code element)` : ""}`);
|
|
22258
|
+
let opened = 0;
|
|
22259
|
+
s = s.replace(/<(form|object|applet|select|textarea|button|template|svg|math|canvas)(\s[^>]*)?>/gi, (m) => {
|
|
22260
|
+
opened++;
|
|
22261
|
+
return escapeHtml(m, false);
|
|
22262
|
+
});
|
|
22263
|
+
if (opened)
|
|
22264
|
+
warnings.push(`Kept ${opened} unclosed forbidden tag(s) as text \u2014 written as a placeholder, nothing after it was removed`);
|
|
22180
22265
|
const comments = s.match(/<!--[\s\S]*?-->/g)?.length ?? 0;
|
|
22181
22266
|
if (comments) {
|
|
22182
22267
|
s = s.replace(/<!--[\s\S]*?-->/g, "");
|
|
@@ -22288,19 +22373,44 @@ function tolerantFindRegex(find) {
|
|
|
22288
22373
|
function spansBlockBoundary(find) {
|
|
22289
22374
|
return /<\/[a-zA-Z][a-zA-Z0-9-]*\s*>\s*<[a-zA-Z]/.test(find);
|
|
22290
22375
|
}
|
|
22376
|
+
function closedBlockTag(find) {
|
|
22377
|
+
const m = /<\/(tr|td|th|li|p|blockquote|figure|table|ul|ol)\s*>/i.exec(find);
|
|
22378
|
+
return m ? m[1].toLowerCase() : null;
|
|
22379
|
+
}
|
|
22380
|
+
function visibleTextSpans(source, needle, tag) {
|
|
22381
|
+
const norm = (s) => decodeEntities(s).replace(/\\(["'])/g, "$1").replace(/[\u201C\u201D]/g, '"').replace(/[\u2018\u2019]/g, "'").replace(/\s*\|\s*/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
22382
|
+
const want = norm(needle.replace(/<[^>]+>/g, " "));
|
|
22383
|
+
if (want.length < 2)
|
|
22384
|
+
return [];
|
|
22385
|
+
const out = [];
|
|
22386
|
+
const rx = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi");
|
|
22387
|
+
for (const m of source.matchAll(rx)) {
|
|
22388
|
+
if (norm(m[1].replace(/<[^>]+>/g, " ")).includes(want)) {
|
|
22389
|
+
const open = /^<[^>]*>/.exec(m[0])[0];
|
|
22390
|
+
out.push({ start: m.index + open.length, length: 0 });
|
|
22391
|
+
}
|
|
22392
|
+
}
|
|
22393
|
+
return out;
|
|
22394
|
+
}
|
|
22291
22395
|
var INLINE_TAGS = "strong|em|b|i|u|s|del|ins|code|a|span|mark|sub|sup|kbd|small";
|
|
22292
22396
|
var INLINE_TAG_RX = new RegExp(`<\\/?(?:${INLINE_TAGS})(?:\\s[^<>]*)?\\/?>`, "gi");
|
|
22397
|
+
var DOUBLE_QUOTE = '(?:"|"|"|\u201C|\u201D|“|”)';
|
|
22398
|
+
var SINGLE_QUOTE = "(?:'|'|'|\u2018|\u2019|‘|’)";
|
|
22293
22399
|
var ENTITY_FORMS = {
|
|
22294
22400
|
"&": "&(?:amp;)?",
|
|
22295
22401
|
"<": "<",
|
|
22296
22402
|
">": ">",
|
|
22297
|
-
'"':
|
|
22298
|
-
"
|
|
22403
|
+
'"': DOUBLE_QUOTE,
|
|
22404
|
+
"\u201C": DOUBLE_QUOTE,
|
|
22405
|
+
"\u201D": DOUBLE_QUOTE,
|
|
22406
|
+
"'": SINGLE_QUOTE,
|
|
22407
|
+
"\u2018": SINGLE_QUOTE,
|
|
22408
|
+
"\u2019": SINGLE_QUOTE
|
|
22299
22409
|
};
|
|
22300
22410
|
function inlineTolerantSpans(source, needle) {
|
|
22301
22411
|
if (/<\/?(?!(?:strong|em|b|i|u|s|del|ins|code|a|span|mark|sub|sup|kbd|small)\b)[a-zA-Z]/.test(needle))
|
|
22302
22412
|
return [];
|
|
22303
|
-
const text = decodeEntities(needle.replace(INLINE_TAG_RX, "")).trim();
|
|
22413
|
+
const text = decodeEntities(needle.replace(INLINE_TAG_RX, "")).replace(/\\(["'])/g, "$1").trim();
|
|
22304
22414
|
if (text.length < 2)
|
|
22305
22415
|
return [];
|
|
22306
22416
|
const gap = `(?:<\\/?(?:${INLINE_TAGS})(?:\\s[^<>]*)?\\/?>)*`;
|
|
@@ -22379,11 +22489,26 @@ function bumpLastUpdated(html, date) {
|
|
|
22379
22489
|
return { html: html.replace(re, `$1${formatted}`), bumped: true };
|
|
22380
22490
|
}
|
|
22381
22491
|
function leadingIdentification(html) {
|
|
22492
|
+
return leadingIdentityText(html)?.includes("\xB7") ?? false;
|
|
22493
|
+
}
|
|
22494
|
+
function leadingIdentityText(html) {
|
|
22382
22495
|
const m = /^(?:\s|<p>(?:\s| )*<\/p>)*<h3(?:\s[^>]*)?>([\s\S]*?)<\/h3>/i.exec(html);
|
|
22383
|
-
|
|
22384
|
-
|
|
22385
|
-
|
|
22386
|
-
|
|
22496
|
+
return m ? decodeEntities(m[1].replace(/<[^>]+>/g, "")).trim() : null;
|
|
22497
|
+
}
|
|
22498
|
+
function identityProblem(text) {
|
|
22499
|
+
const line = text.trim();
|
|
22500
|
+
if (/[\r\n]/.test(line))
|
|
22501
|
+
return "it spans several lines";
|
|
22502
|
+
if (line.length > 160)
|
|
22503
|
+
return `it is ${line.length} characters long (160 at most)`;
|
|
22504
|
+
if (/^addendum\b/i.test(line))
|
|
22505
|
+
return "it is an addendum marker, not an identity";
|
|
22506
|
+
if (/\?/.test(line))
|
|
22507
|
+
return "it carries a question mark, which reads as a draft";
|
|
22508
|
+
const parts = line.split("\xB7").map((p) => p.trim());
|
|
22509
|
+
if (parts.length < 3 || parts.some((p) => !p))
|
|
22510
|
+
return `it has ${parts.filter(Boolean).length} "\xB7"-separated part(s); it needs at least three`;
|
|
22511
|
+
return null;
|
|
22387
22512
|
}
|
|
22388
22513
|
function duplicateHeadings(html) {
|
|
22389
22514
|
const MARKER = /<h2(?:\s[^>]*)?>\s*(?:Addendum|Withdrawn|Recovered|Reopened)\s*(?:\u2014|\u2013|-)[^<]*<\/h2>/gi;
|
|
@@ -22680,11 +22805,47 @@ function unbalancedTags(html) {
|
|
|
22680
22805
|
}
|
|
22681
22806
|
return [...new Set(stack)];
|
|
22682
22807
|
}
|
|
22808
|
+
function tableShapeIssues(html) {
|
|
22809
|
+
const issues = [];
|
|
22810
|
+
const spanOf = (attrs, name) => Number(new RegExp(`${name}\\s*=\\s*"?(\\d+)`, "i").exec(attrs ?? "")?.[1] ?? 1);
|
|
22811
|
+
for (const table of html.matchAll(/<table\b[^>]*>([\s\S]*?)<\/table>/gi)) {
|
|
22812
|
+
const rows = [...table[1].matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map((r) => [...r[1].matchAll(/<(t[hd])\b([^>]*)>([\s\S]*?)<\/\1>/gi)].map((c) => ({
|
|
22813
|
+
span: spanOf(c[2], "colspan"),
|
|
22814
|
+
rows: spanOf(c[2], "rowspan"),
|
|
22815
|
+
text: decodeEntities(c[3].replace(/<[^>]+>/g, "")).trim()
|
|
22816
|
+
})));
|
|
22817
|
+
if (rows.length < 2)
|
|
22818
|
+
continue;
|
|
22819
|
+
const width = rows[0].reduce((sum, c) => sum + c.span, 0);
|
|
22820
|
+
let spanning = [];
|
|
22821
|
+
for (const [i, cells] of rows.entries()) {
|
|
22822
|
+
const inherited = spanning.reduce((sum, s) => sum + s.columns, 0);
|
|
22823
|
+
if (i > 0) {
|
|
22824
|
+
const label = (cells[0]?.text ?? "").slice(0, 60) || "(empty row)";
|
|
22825
|
+
const count = cells.reduce((sum, c) => sum + c.span, 0) + inherited;
|
|
22826
|
+
if (count !== width) {
|
|
22827
|
+
issues.push(`row "${label}" has ${count} cell(s), its header has ${width}`);
|
|
22828
|
+
} else {
|
|
22829
|
+
const piped = cells.find((c) => (c.text.match(/\s\|\s/g)?.length ?? 0) >= Math.max(1, width - 2) && width > 1);
|
|
22830
|
+
if (piped && cells.filter((c) => c.text).length < width) {
|
|
22831
|
+
issues.push(`row "${label}" carries pipe-separated values inside one cell \u2014 send each value as its own <td>`);
|
|
22832
|
+
}
|
|
22833
|
+
}
|
|
22834
|
+
}
|
|
22835
|
+
spanning = [
|
|
22836
|
+
...spanning.map((s) => ({ ...s, rowsLeft: s.rowsLeft - 1 })).filter((s) => s.rowsLeft > 0),
|
|
22837
|
+
...cells.filter((c) => c.rows > 1).map((c) => ({ columns: c.span, rowsLeft: c.rows - 1 }))
|
|
22838
|
+
];
|
|
22839
|
+
}
|
|
22840
|
+
}
|
|
22841
|
+
return issues;
|
|
22842
|
+
}
|
|
22683
22843
|
var LARGE_NOTE_CHARS = 40000;
|
|
22684
22844
|
function structureReport(html) {
|
|
22685
22845
|
return {
|
|
22686
22846
|
duplicateHeadings: duplicateHeadings(html),
|
|
22687
22847
|
unbalancedTags: unbalancedTags(html),
|
|
22848
|
+
tableShape: tableShapeIssues(html),
|
|
22688
22849
|
size: html.length
|
|
22689
22850
|
};
|
|
22690
22851
|
}
|
|
@@ -23518,6 +23679,9 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23518
23679
|
if (structure.unbalancedTags.length) {
|
|
23519
23680
|
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)`);
|
|
23520
23681
|
}
|
|
23682
|
+
if (structure.tableShape.length) {
|
|
23683
|
+
report.flagged.push(`table shape: ${n.title} [${n.noteId}] \u2014 ${structure.tableShape.slice(0, 3).join("; ")}${structure.tableShape.length > 3 ? ` (+${structure.tableShape.length - 3} more)` : ""}. Rewrite the row with one <td> per column: revise(find=<anchor>, within="tr")`);
|
|
23684
|
+
}
|
|
23521
23685
|
const profile = sectionProfile(content);
|
|
23522
23686
|
const biggest = profile.largest[0];
|
|
23523
23687
|
const crowded = profile.count >= SECTION_EDIT_RISK_COUNT;
|
|
@@ -23590,13 +23754,15 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23590
23754
|
byDomain.get(domSlug).push(n);
|
|
23591
23755
|
}
|
|
23592
23756
|
const headingCache = new Map;
|
|
23593
|
-
const
|
|
23757
|
+
const shapeOf = async (n) => {
|
|
23594
23758
|
if (!headingCache.has(n.noteId)) {
|
|
23595
23759
|
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
23596
|
-
|
|
23760
|
+
const words = new Set(content.replace(/<[^>]+>/g, " ").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3));
|
|
23761
|
+
headingCache.set(n.noteId, { headings: headingOutline(content).filter((h) => h.level <= 3).map((h) => h.text), words });
|
|
23597
23762
|
}
|
|
23598
23763
|
return headingCache.get(n.noteId);
|
|
23599
23764
|
};
|
|
23765
|
+
const GENERIC_HEADING = /^(last updated\b.*|overview|context|goal|sources|revision|resolution|notes?|summary)$/i;
|
|
23600
23766
|
for (const [domSlug, notes] of byDomain) {
|
|
23601
23767
|
const reported = new Set;
|
|
23602
23768
|
for (let i = 0;i < notes.length; i++) {
|
|
@@ -23613,10 +23779,19 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23613
23779
|
reported.add(pairKey);
|
|
23614
23780
|
if (acknowledged(a) || acknowledged(bn))
|
|
23615
23781
|
continue;
|
|
23616
|
-
const [
|
|
23617
|
-
const
|
|
23782
|
+
const [sa, sb] = await Promise.all([shapeOf(a), shapeOf(bn)]);
|
|
23783
|
+
const ha = sa.headings, hb = sb.headings;
|
|
23784
|
+
const overlap = ha.filter((h) => !GENERIC_HEADING.test(h) && hb.some((x) => x.toLowerCase() === h.toLowerCase()));
|
|
23785
|
+
let common = 0;
|
|
23786
|
+
for (const w of sa.words)
|
|
23787
|
+
if (sb.words.has(w))
|
|
23788
|
+
common++;
|
|
23789
|
+
const union = sa.words.size + sb.words.size - common;
|
|
23790
|
+
const similarity = union ? common / union : 0;
|
|
23791
|
+
if (!overlap.length && similarity < 0.45)
|
|
23792
|
+
continue;
|
|
23618
23793
|
const show = (h) => h.length ? h.slice(0, 8).join(" \xB7 ") + (h.length > 8 ? " \xB7 \u2026" : "") : "(no headings)";
|
|
23619
|
-
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,
|
|
23794
|
+
report.flagged.push(`near-duplicate subject: '${a.title}' [${a.noteId}] and '${bn.title}' [${bn.noteId}] in Domain/${domSlug} ` + `share '${shared.join("', '")}' and ${Math.round(similarity * 100)}% of their vocabulary. 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, but most of their wording is shared \u2014 read both; if they are distinct, maintain(ack=["${a.noteId}"]) silences this.`));
|
|
23620
23795
|
}
|
|
23621
23796
|
}
|
|
23622
23797
|
}
|
|
@@ -24016,6 +24191,96 @@ async function catchUpDeletions(trilium, cfg, today) {
|
|
|
24016
24191
|
return { windowDays, coverage: "full", deletionsFound: total, regenerated };
|
|
24017
24192
|
}
|
|
24018
24193
|
|
|
24194
|
+
// src/seal.ts
|
|
24195
|
+
import { createHash } from "crypto";
|
|
24196
|
+
var GENESIS = "genesis";
|
|
24197
|
+
function sealText(html) {
|
|
24198
|
+
return decodeEntities(html.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
24199
|
+
}
|
|
24200
|
+
function sha(text) {
|
|
24201
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
24202
|
+
}
|
|
24203
|
+
function sealRecord(text) {
|
|
24204
|
+
return [text.length, sha(text).slice(0, 16)];
|
|
24205
|
+
}
|
|
24206
|
+
function dayDigest(prev, date, records) {
|
|
24207
|
+
const canonical = Object.keys(records).sort().map((id) => `${id}:${records[id][0]}:${records[id][1]}`).join(",");
|
|
24208
|
+
return sha(`${prev}
|
|
24209
|
+
${date}
|
|
24210
|
+
${canonical}`);
|
|
24211
|
+
}
|
|
24212
|
+
function recordIntact(current, sealed) {
|
|
24213
|
+
if (current.length < sealed[0])
|
|
24214
|
+
return false;
|
|
24215
|
+
return sha(current.slice(0, sealed[0])).slice(0, 16) === sealed[1];
|
|
24216
|
+
}
|
|
24217
|
+
async function dayRecords(trilium, cfg, date) {
|
|
24218
|
+
const find = (ancestor, kind) => ancestor ? trilium.searchNotes(`#noteType=${kind} #created='${date}'`, { ancestorNoteId: ancestor, fastSearch: true, limit: 200, includeArchivedNotes: true }).then((r) => r.results).catch(() => []) : Promise.resolve([]);
|
|
24219
|
+
const [sessions, diary, entries] = await Promise.all([
|
|
24220
|
+
find(cfg.memory.sessions, "session"),
|
|
24221
|
+
find(cfg.llm.diary, "diary"),
|
|
24222
|
+
find(cfg.memory.threads, "threadEntry")
|
|
24223
|
+
]);
|
|
24224
|
+
return [...sessions, ...diary, ...entries.filter((n) => n.title.trim() === `[${date}]`)];
|
|
24225
|
+
}
|
|
24226
|
+
async function sealedLogs(trilium, cfg) {
|
|
24227
|
+
if (!cfg.insights.logs)
|
|
24228
|
+
return [];
|
|
24229
|
+
const res = await trilium.searchNotes("#noteType=log #seal", { ancestorNoteId: cfg.insights.logs, fastSearch: true, limit: 1000, includeArchivedNotes: true }).catch(() => ({ results: [] }));
|
|
24230
|
+
return res.results.filter((n) => ownedLabel(n, "seal")).sort((a, b) => (ownedLabel(a, "created") ?? "").localeCompare(ownedLabel(b, "created") ?? ""));
|
|
24231
|
+
}
|
|
24232
|
+
async function sealDay(trilium, cfg, date, logNoteId) {
|
|
24233
|
+
const earlier = (await sealedLogs(trilium, cfg)).filter((n) => (ownedLabel(n, "created") ?? "") < date);
|
|
24234
|
+
const prev = earlier.length ? ownedLabel(earlier[earlier.length - 1], "seal") : GENESIS;
|
|
24235
|
+
const records = {};
|
|
24236
|
+
for (const n of await dayRecords(trilium, cfg, date)) {
|
|
24237
|
+
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
24238
|
+
if (content)
|
|
24239
|
+
records[n.noteId] = sealRecord(sealText(content));
|
|
24240
|
+
}
|
|
24241
|
+
const digest = dayDigest(prev, date, records);
|
|
24242
|
+
await trilium.updateLabelValue(logNoteId, "sealRecords", JSON.stringify(records));
|
|
24243
|
+
await trilium.updateLabelValue(logNoteId, "sealPrev", prev);
|
|
24244
|
+
await trilium.updateLabelValue(logNoteId, "seal", digest);
|
|
24245
|
+
return { date, digest, records: Object.keys(records).length };
|
|
24246
|
+
}
|
|
24247
|
+
async function verifySeals(trilium, cfg, today, days = 7) {
|
|
24248
|
+
const logs = (await sealedLogs(trilium, cfg)).filter((n) => (ownedLabel(n, "created") ?? "") < today);
|
|
24249
|
+
const findings = [];
|
|
24250
|
+
const window = logs.slice(-days);
|
|
24251
|
+
for (const log of window) {
|
|
24252
|
+
const date = ownedLabel(log, "created") ?? "";
|
|
24253
|
+
const digest = ownedLabel(log, "seal") ?? "";
|
|
24254
|
+
const prev = ownedLabel(log, "sealPrev") ?? "";
|
|
24255
|
+
const i = logs.indexOf(log);
|
|
24256
|
+
const expectedPrev = i > 0 ? ownedLabel(logs[i - 1], "seal") ?? "" : prev;
|
|
24257
|
+
if (prev !== expectedPrev) {
|
|
24258
|
+
findings.push({ date, noteId: log.noteId, problem: `the chain breaks here \u2014 this seal links to ${prev.slice(0, 12)}\u2026, but the previous sealed day's digest is ${expectedPrev.slice(0, 12)}\u2026` });
|
|
24259
|
+
}
|
|
24260
|
+
let records;
|
|
24261
|
+
try {
|
|
24262
|
+
records = JSON.parse(ownedLabel(log, "sealRecords") ?? "{}");
|
|
24263
|
+
} catch {
|
|
24264
|
+
findings.push({ date, noteId: log.noteId, problem: "the sealed record list is unreadable" });
|
|
24265
|
+
continue;
|
|
24266
|
+
}
|
|
24267
|
+
if (dayDigest(prev, date, records) !== digest) {
|
|
24268
|
+
findings.push({ date, noteId: log.noteId, problem: "the sealed record list no longer matches its digest \u2014 the seal labels were edited" });
|
|
24269
|
+
}
|
|
24270
|
+
for (const [noteId, sealed] of Object.entries(records)) {
|
|
24271
|
+
const content = await trilium.getNoteContent(noteId).catch(() => null);
|
|
24272
|
+
if (content === null) {
|
|
24273
|
+
findings.push({ date, noteId, problem: "a sealed record is gone (deleted or unreadable)" });
|
|
24274
|
+
continue;
|
|
24275
|
+
}
|
|
24276
|
+
if (!recordIntact(sealText(content), sealed)) {
|
|
24277
|
+
findings.push({ date, noteId, problem: "a sealed record was changed after its day was sealed \u2014 its sealed text is no longer the start of its body. diff(noteId) and the note's revisions show what changed" });
|
|
24278
|
+
}
|
|
24279
|
+
}
|
|
24280
|
+
}
|
|
24281
|
+
return { checked: window.length, findings };
|
|
24282
|
+
}
|
|
24283
|
+
|
|
24019
24284
|
// src/diffing.ts
|
|
24020
24285
|
function revisionsSince(revisions, cutoff) {
|
|
24021
24286
|
return revisions.filter((r) => (r.dateCreated ?? "").slice(0, 16) >= cutoff);
|
|
@@ -24617,6 +24882,8 @@ function registerTools(server, trilium, brainRef) {
|
|
|
24617
24882
|
const preCloseSteps = new Map;
|
|
24618
24883
|
let preCloseSeq = 0;
|
|
24619
24884
|
const REQUIRED_PRECLOSE_STEPS = ["session", "addendum", "maintain", "remarks", "diary"];
|
|
24885
|
+
const LIGHT_PRECLOSE_STEPS = ["session", "remarks", "diary"];
|
|
24886
|
+
let wroteSinceClose = false;
|
|
24620
24887
|
const gateNote = async (date) => {
|
|
24621
24888
|
const cfg = b();
|
|
24622
24889
|
if (!cfg.memory.sessions)
|
|
@@ -24675,6 +24942,7 @@ function registerTools(server, trilium, brainRef) {
|
|
|
24675
24942
|
await trilium.updateLabelValue(noteId, "gate", "");
|
|
24676
24943
|
preCloseSteps.clear();
|
|
24677
24944
|
preCloseSeq = 0;
|
|
24945
|
+
wroteSinceClose = false;
|
|
24678
24946
|
};
|
|
24679
24947
|
const RECORD_KINDS = new Set(["session", "diary", "log", "threadEntry"]);
|
|
24680
24948
|
const isRecordNote = (n) => {
|
|
@@ -24684,16 +24952,30 @@ function registerTools(server, trilium, brainRef) {
|
|
|
24684
24952
|
return kind === "threadEntry" && /^\[\d{4}-\d{2}-\d{2}\]$/.test(n.title.trim());
|
|
24685
24953
|
};
|
|
24686
24954
|
const structuralFindings = (kind, html) => {
|
|
24687
|
-
if (!html
|
|
24955
|
+
if (!html)
|
|
24688
24956
|
return {};
|
|
24957
|
+
const tableShape = tableShapeIssues(html);
|
|
24958
|
+
const shape = tableShape.length ? { tableShape, tableHint: 'A table row disagrees with its header \u2014 rewrite it with one <td> per column (revise(find=<anchor>, within="tr")).' } : {};
|
|
24959
|
+
if (kind && RECORD_KINDS.has(kind))
|
|
24960
|
+
return shape;
|
|
24689
24961
|
const dupes = duplicateHeadings(html);
|
|
24690
24962
|
if (!dupes.length)
|
|
24691
|
-
return
|
|
24963
|
+
return shape;
|
|
24692
24964
|
return {
|
|
24693
24965
|
duplicateHeadings: dupes,
|
|
24694
|
-
structureHint: "The note now carries duplicated section headings \u2014 merge them with revise(section=\u2026, mode=replace), or target one specifically with occurrence=."
|
|
24966
|
+
structureHint: "The note now carries duplicated section headings \u2014 merge them with revise(section=\u2026, mode=replace), or target one specifically with occurrence=.",
|
|
24967
|
+
...shape
|
|
24695
24968
|
};
|
|
24696
24969
|
};
|
|
24970
|
+
const identityRefusal = (identity, html) => {
|
|
24971
|
+
const text = leadingIdentification(html) ? leadingIdentityText(html) : identity;
|
|
24972
|
+
if (!text)
|
|
24973
|
+
return null;
|
|
24974
|
+
const problem = identityProblem(text);
|
|
24975
|
+
if (!problem)
|
|
24976
|
+
return null;
|
|
24977
|
+
return err("malformed_identity", `The identification line "${text.slice(0, 100)}" is malformed: ${problem}. Nothing was written.`, 'Pass identity="LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]", e.g. "Claude Opus 5.5 \xB7 Claude Code \xB7 Interactive".');
|
|
24978
|
+
};
|
|
24697
24979
|
const applyIcon = async (noteId, icon) => {
|
|
24698
24980
|
const cls = icon ? normalizeIcon(icon) : "";
|
|
24699
24981
|
if (!cls) {
|
|
@@ -24846,7 +25128,7 @@ ${block}` }));
|
|
|
24846
25128
|
});
|
|
24847
25129
|
server.tool("session", `Pre-close review. Returns the six singletons as {id, lastModified} stubs (full=true inlines them), today's diary as {id, blocks, size}, the maintained notes changed today with their write counts, runs the lite maintenance sweep, and returns pending= (what each remaining close step has to do), audit= (do the singletons agree, and do the LLM singletons still serve the master ones) and next[]. Records the session step of the close gate. Idempotent.
|
|
24848
25130
|
|
|
24849
|
-
The close sequence next[] lays out: singleton updates from the session, then addendum(), maintain(), remarks(), diary() and close(). close() checks that addendum, maintain, remarks and diary ran, in the order session \u2192 remarks \u2192 diary. scope="agent" omits the singleton updates for scoped or autonomous runs.`, {
|
|
25131
|
+
The close sequence next[] lays out: singleton updates from the session, then addendum(), maintain(), remarks(), diary() and close(). close() checks that addendum, maintain, remarks and diary ran, in the order session \u2192 remarks \u2192 diary. scope="agent" omits the singleton updates for scoped or autonomous runs. A session that wrote no content gets a light close: session() returns only the short next[] (remarks, a one-paragraph diary, close) and close() does not require addendum() or maintain().`, {
|
|
24850
25132
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
24851
25133
|
full: booleanType().optional().describe("Inline every singleton's content and the diary body (default: stubs only)"),
|
|
24852
25134
|
scope: enumType(["interactive", "agent"]).optional().describe(`"agent" for a scoped/autonomous run whose brief excludes the user's personal singletons \u2014 omits those steps from next[] (default: "interactive")`),
|
|
@@ -24856,6 +25138,19 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
24856
25138
|
const cfg = b();
|
|
24857
25139
|
if (!cfg.master.root || !cfg.llm.root)
|
|
24858
25140
|
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
25141
|
+
if (!(await readGate(d)).has("write")) {
|
|
25142
|
+
await markStep("session", d);
|
|
25143
|
+
return txt3({
|
|
25144
|
+
date: d,
|
|
25145
|
+
closeMode: "light",
|
|
25146
|
+
note: "No content was written this session, so addendum() and maintain() are not required and there is nothing to audit. If you learned something durable about the user or yourself, revise that singleton first \u2014 that makes it a full close, and session() will say so when re-run.",
|
|
25147
|
+
next: [
|
|
25148
|
+
"Call remarks() \u2014 one short cue for a short session.",
|
|
25149
|
+
"Call diary() \u2014 one short paragraph: what the session was and anything worth the team knowing.",
|
|
25150
|
+
"Call close() \u2014 commit the session log."
|
|
25151
|
+
]
|
|
25152
|
+
});
|
|
25153
|
+
}
|
|
24859
25154
|
const fetchSingleton = async (id) => {
|
|
24860
25155
|
if (!full) {
|
|
24861
25156
|
const note = await trilium.getNote(id);
|
|
@@ -24957,6 +25252,13 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
24957
25252
|
if (!cfg.root)
|
|
24958
25253
|
return txt3({ status: "uninitialized", action: "Run bootstrap first." });
|
|
24959
25254
|
await markStep("remarks");
|
|
25255
|
+
if (!(await readGate(localToday())).has("write")) {
|
|
25256
|
+
return txt3({
|
|
25257
|
+
closeMode: "light",
|
|
25258
|
+
cue: "One short paragraph, first person: what this session was, how it went, and anything about BrainLLM worth the team knowing. Skip what has nothing real to say.",
|
|
25259
|
+
next: ["Write it via diary(body, identity), then call close()."]
|
|
25260
|
+
});
|
|
25261
|
+
}
|
|
24960
25262
|
return txt3({
|
|
24961
25263
|
cues: {
|
|
24962
25264
|
experience: [
|
|
@@ -24977,7 +25279,7 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
24977
25279
|
]
|
|
24978
25280
|
});
|
|
24979
25281
|
});
|
|
24980
|
-
server.tool("close", `Writes the session log, normally the last call of a session. Refuses unless session(), addendum(), maintain(), remarks() and diary() ran and session \u2192 remarks \u2192 diary held (force=true bypasses a step with genuinely nothing to do; bypassed steps are reported). identity= is required. Writes a timestamped block to today's [yyyy-mm-dd] session note (title= becomes its heading), regenerates the daily log, backs up the database and resets the gate. continuing=true is a second close the same day.`, {
|
|
25282
|
+
server.tool("close", `Writes the session log, normally the last call of a session. Refuses unless session(), addendum(), maintain(), remarks() and diary() ran and session \u2192 remarks \u2192 diary held; a session that wrote no content (light close) needs only session(), remarks() and diary() (force=true bypasses a step with genuinely nothing to do; bypassed steps are reported). identity= is required. Writes a timestamped block to today's [yyyy-mm-dd] session note (title= becomes its heading), regenerates the daily log, backs up the database and resets the gate. continuing=true is a second close the same day.`, {
|
|
24981
25283
|
summary: stringType().describe("What happened this session \u2014 factual, concise prose"),
|
|
24982
25284
|
title: stringType().optional().describe("Short session title \u2014 appears as an <h2> heading above Summary"),
|
|
24983
25285
|
identity: stringType().describe(`Identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" \u2014 rendered as the block's h3 (required)`),
|
|
@@ -25000,9 +25302,11 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
25000
25302
|
}
|
|
25001
25303
|
}
|
|
25002
25304
|
const gate = await readGate(gateDate);
|
|
25003
|
-
const
|
|
25305
|
+
const light = !gate.has("write");
|
|
25306
|
+
const required = light ? LIGHT_PRECLOSE_STEPS : REQUIRED_PRECLOSE_STEPS;
|
|
25307
|
+
const missing = continued ? [] : required.filter((step) => !gate.has(step));
|
|
25004
25308
|
if (missing.length && !force) {
|
|
25005
|
-
return err("preclose_incomplete", `close() refused \u2014 these pre-close steps haven't run yet this session: ${missing.join(", ")}
|
|
25309
|
+
return err("preclose_incomplete", `close() refused \u2014 these pre-close steps haven't run yet this session: ${missing.join(", ")}.${light ? " (No content was written this session, so addendum() and maintain() are not required.)" : ""}`, `Call ${missing.map((s) => `${s}()`).join(", ")} first, or pass force=true if one of them genuinely has nothing to log.`);
|
|
25006
25310
|
}
|
|
25007
25311
|
const seq = (step) => gate.get(step) ?? 0;
|
|
25008
25312
|
const orderOk = continued || missing.length > 0 || seq("session") < seq("remarks") && seq("remarks") < seq("diary");
|
|
@@ -25018,6 +25322,9 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
25018
25322
|
if (!identity && !leadingIdentification(summaryHtml)) {
|
|
25019
25323
|
return err("missing_identity", 'Session 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>" on close() \u2014 the server renders it as the block's h3.`);
|
|
25020
25324
|
}
|
|
25325
|
+
const badIdentity = identityRefusal(identity, summaryHtml);
|
|
25326
|
+
if (badIdentity)
|
|
25327
|
+
return badIdentity;
|
|
25021
25328
|
const identityBlock = identity && !leadingIdentification(summaryHtml) ? `<h3>${escapeHtml(identity)}</h3>
|
|
25022
25329
|
` : "";
|
|
25023
25330
|
const titleBlock = title ? `<h2>${escapeHtml(title)}</h2>
|
|
@@ -25072,6 +25379,7 @@ ${contentBlock}` }));
|
|
|
25072
25379
|
if (!hasEdge(logNote, noteId))
|
|
25073
25380
|
await trilium.addRelation(logReport.noteId, "references", noteId).catch(() => null);
|
|
25074
25381
|
}
|
|
25382
|
+
const sealReport = logReport?.noteId ? await sealDay(trilium, cfg, d, logReport.noteId).catch(() => null) : null;
|
|
25075
25383
|
let backupStatus = "disabled";
|
|
25076
25384
|
let backupName = rollingBackupName(d);
|
|
25077
25385
|
if (backup !== false) {
|
|
@@ -25093,6 +25401,8 @@ ${contentBlock}` }));
|
|
|
25093
25401
|
...caughtUp?.coverage === "unknown" ? { deletionCatchUp: "unknown \u2014 Trilium's deletion history could not be read; the configured catch-up window is not fully verified" } : caughtUp && caughtUp.deletionsFound ? { deletionCatchUp: `${caughtUp.deletionsFound} deletion(s) caught up \u2014 logs regenerated for: ${caughtUp.regenerated.join(", ")}` } : {},
|
|
25094
25402
|
...iconSet ? { icon: iconSet } : {},
|
|
25095
25403
|
...continued ? { continuing: true } : {},
|
|
25404
|
+
...sealReport ? { seal: `${sealReport.records} record(s) of ${d} sealed (${sealReport.digest.slice(0, 12)}\u2026), chained to the previous sealed day` } : { seal: "not written \u2014 the day's log note was unavailable" },
|
|
25405
|
+
...light && !continued ? { closeMode: "light \u2014 no content was written this session, so addendum() and maintain() were not required" } : {},
|
|
25096
25406
|
...missing.length || !orderOk ? { bypassed: [...missing, ...!orderOk ? ["ordering(session\u2192remarks\u2192diary)"] : []] } : {},
|
|
25097
25407
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25098
25408
|
});
|
|
@@ -25190,6 +25500,9 @@ ${contentBlock}` }));
|
|
|
25190
25500
|
if (!identity && !leadingIdentification(sanitized.html)) {
|
|
25191
25501
|
return err("missing_identity", 'Diary 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>" on diary() \u2014 the server renders it as the block's h3.`);
|
|
25192
25502
|
}
|
|
25503
|
+
const badIdentity = identityRefusal(identity, sanitized.html);
|
|
25504
|
+
if (badIdentity)
|
|
25505
|
+
return badIdentity;
|
|
25193
25506
|
const html = identity && !leadingIdentification(sanitized.html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
25194
25507
|
${sanitized.html}` : sanitized.html;
|
|
25195
25508
|
const found = await trilium.searchNotes(`#noteType=diary #created='${d}'`, { ancestorNoteId: parentId, fastSearch: true, limit: 1 });
|
|
@@ -25372,7 +25685,8 @@ Collection kinds dedup by title \u2014 mustCreate=true refuses instead of adopti
|
|
|
25372
25685
|
...createdDomain ? { createdDomain: domainTitle } : {},
|
|
25373
25686
|
...revisionChanges.length ? { revision: revisionChanges } : {},
|
|
25374
25687
|
...sourceMerge,
|
|
25375
|
-
...revisionKeys.length ? { revisionRows: revisionKeys } : {},
|
|
25688
|
+
...revisionKeys.length ? { revisionRows: revisionKeys.length } : {},
|
|
25689
|
+
...revisionChanges.some((c) => c.startsWith("added:")) && revisionKeys.length ? { revisionKeys, revisionKeysNote: "A new Revision row was added. If it was meant to update an existing source, its key must match one of these exactly." } : {},
|
|
25376
25690
|
...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." } : {},
|
|
25377
25691
|
...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." } : {},
|
|
25378
25692
|
...connected.length ? { connected } : {},
|
|
@@ -25470,6 +25784,9 @@ Collection kinds dedup by title \u2014 mustCreate=true refuses instead of adopti
|
|
|
25470
25784
|
if (kind === "thread" && !identity && !leadingIdentification(html)) {
|
|
25471
25785
|
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.`);
|
|
25472
25786
|
}
|
|
25787
|
+
const badIdentity = identityRefusal(identity, html);
|
|
25788
|
+
if (badIdentity)
|
|
25789
|
+
return badIdentity;
|
|
25473
25790
|
const block = identity && !leadingIdentification(html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
25474
25791
|
${html}` : html;
|
|
25475
25792
|
if (kind === "thread") {
|
|
@@ -25489,7 +25806,7 @@ ${html}` : html;
|
|
|
25489
25806
|
const connected = await wireRequested(existing.noteId);
|
|
25490
25807
|
const iconSet = await applyIcon(existing.noteId, icon);
|
|
25491
25808
|
const relations = relationSnippet(existing);
|
|
25492
|
-
return txt3({ action: "updated", noteId: existing.noteId, entryId: entry.noteId, entryAction: entry.action, kind, title: existing.title, ...connected.length ? { connected } : {}, ...iconSet ? { icon: iconSet } : {}, ...relations ? { relations } : {}, ...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {} });
|
|
25809
|
+
return txt3({ action: "updated", noteId: existing.noteId, entryId: entry.noteId, entryAction: entry.action, entry: `today's [${d}] entry in this thread; revise(entryId) edits it`, kind, title: existing.title, ...connected.length ? { connected } : {}, ...iconSet ? { icon: iconSet } : {}, ...relations ? { relations } : {}, ...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {} });
|
|
25493
25810
|
}
|
|
25494
25811
|
const current = await trilium.getNoteContent(existing.noteId);
|
|
25495
25812
|
if (isDuplicateAppend(current, block))
|
|
@@ -25553,7 +25870,7 @@ ${block}`), d);
|
|
|
25553
25870
|
...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {}
|
|
25554
25871
|
});
|
|
25555
25872
|
});
|
|
25556
|
-
server.tool("recall", `Ranked search across the whole brain \u2014 label, title and full-text strategies merged, with kind and status. Pass domain= whenever you know the area. orderBy/orderDirection sort by date; fastSearch= scans titles and labels only; regex= matches note bodies with a real regular expression; a fuzzy pass runs automatically when exact results are thin (hits marked fuzzy: true are leads, not answers). includeArchived= includes archived notes. A thin result is evidence about the query, not the brain.`, {
|
|
25873
|
+
server.tool("recall", `Ranked search across the whole brain \u2014 label, title and full-text strategies merged, with kind and status. Pass domain= whenever you know the area. orderBy/orderDirection sort by date; fastSearch= scans titles and labels only; regex= matches note bodies with a real regular expression; a fuzzy pass runs automatically when exact results are thin (hits marked fuzzy: true are leads, not answers). Recently changed notes rank slightly higher at equal relevance. Regex hits carry evidence: the matched passage; records are included unless kinds= narrows. includeArchived= includes archived notes. A thin result is evidence about the query, not the brain.`, {
|
|
25557
25874
|
query: stringType().describe("What to find \u2014 natural phrasing is fine"),
|
|
25558
25875
|
kinds: arrayType(enumType(Kinds)).optional().describe("Restrict to these kinds"),
|
|
25559
25876
|
domain: stringType().optional().describe("Restrict to a knowledge domain"),
|
|
@@ -25616,10 +25933,21 @@ ${block}`), d);
|
|
|
25616
25933
|
} catch (e) {
|
|
25617
25934
|
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
25618
25935
|
}
|
|
25619
|
-
const
|
|
25936
|
+
const backendPattern = triliumSearchForm(regex);
|
|
25937
|
+
const widened = backendPattern !== regex;
|
|
25938
|
+
const candidates = (await run(`note.content %= '${escapeQueryRegex(backendPattern)}'`)).filter(filterNote);
|
|
25620
25939
|
const confirmed = [];
|
|
25621
25940
|
let rejected = 0;
|
|
25622
25941
|
let tagSpanning = 0;
|
|
25942
|
+
let recordHits = 0;
|
|
25943
|
+
const evidenceIn = (text) => {
|
|
25944
|
+
const m = new RegExp(re.source, "i").exec(text);
|
|
25945
|
+
if (!m)
|
|
25946
|
+
return "";
|
|
25947
|
+
const from = Math.max(0, m.index - 80);
|
|
25948
|
+
const to = Math.min(text.length, m.index + m[0].length + 80);
|
|
25949
|
+
return `${from > 0 ? "\u2026" : ""}${text.slice(from, to).replace(/\s+/g, " ").trim()}${to < text.length ? "\u2026" : ""}`;
|
|
25950
|
+
};
|
|
25623
25951
|
for (const n of candidates) {
|
|
25624
25952
|
if (confirmed.length >= max)
|
|
25625
25953
|
break;
|
|
@@ -25629,18 +25957,28 @@ ${block}`), d);
|
|
|
25629
25957
|
continue;
|
|
25630
25958
|
}
|
|
25631
25959
|
const rawHit = re.test(content);
|
|
25632
|
-
const
|
|
25960
|
+
const projected = stripTagsWithMap(content).text;
|
|
25961
|
+
const projectedHit = re.test(projected);
|
|
25633
25962
|
if (rawHit || projectedHit) {
|
|
25634
|
-
if (projectedHit)
|
|
25963
|
+
if (projectedHit && !rawHit)
|
|
25635
25964
|
tagSpanning++;
|
|
25636
|
-
|
|
25965
|
+
const kind = labelOf2(n, "noteType");
|
|
25966
|
+
if (kind === "session" || kind === "diary" || kind === "log" || kind === "threadEntry" && /^\[\d{4}-\d{2}-\d{2}\]$/.test(n.title.trim()))
|
|
25967
|
+
recordHits++;
|
|
25968
|
+
confirmed.push({ note: n, evidence: decodeEntities(projectedHit ? evidenceIn(projected) : evidenceIn(toText(content, 1e5))) });
|
|
25637
25969
|
} else
|
|
25638
25970
|
rejected++;
|
|
25639
25971
|
}
|
|
25640
|
-
const results = await Promise.all(confirmed.map(
|
|
25972
|
+
const results = await Promise.all(confirmed.map(async ({ note, evidence }, i) => {
|
|
25973
|
+
const { snippet: _snippet, ...base } = await buildResult(note, i);
|
|
25974
|
+
return { ...base, ...evidence ? { evidence } : {} };
|
|
25975
|
+
}));
|
|
25641
25976
|
const notes = [
|
|
25642
25977
|
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,
|
|
25643
25978
|
tagSpanning ? `${tagSpanning} match(es) were found only after stripping markup \u2014 the phrase is split by an inline tag there.` : null,
|
|
25979
|
+
widened ? "Diacritic characters (\xB7, accents) were normalised away for Trilium's pre-filter, which strips them from note text before matching; every result was then verified against your exact pattern." : null,
|
|
25980
|
+
recordHits ? `${recordHits} result(s) are records (sessions, diary, dated thread entries) \u2014 included by default; kinds= narrows to maintained notes.` : null,
|
|
25981
|
+
candidates.length >= 30 && confirmed.length < max ? "The backend returned its 30-candidate cap, so further matches may exist \u2014 narrow with domain= or kinds=, or use consistency() for an exhaustive scan." : null,
|
|
25644
25982
|
results.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,
|
|
25645
25983
|
results.length === 0 && regex.includes("\\") ? `The pattern reached the server as /${regex}/i. In it, \\d is a digit and \\\\ is one literal backslash (a Windows path separator); a backslash doubled once too often matches a literal backslash instead of escaping. If that is not the pattern you meant, adjust the escaping and retry.` : null
|
|
25646
25984
|
].filter(Boolean);
|
|
@@ -25701,7 +26039,12 @@ ${block}`), d);
|
|
|
25701
26039
|
fuzzyIds.add(n.noteId);
|
|
25702
26040
|
add(byFuzzy, 0.5);
|
|
25703
26041
|
}
|
|
25704
|
-
const
|
|
26042
|
+
const now = Date.now();
|
|
26043
|
+
const recency = (n) => {
|
|
26044
|
+
const age = Math.max(0, (now - new Date(n.dateModified.replace(" ", "T")).getTime()) / 86400000);
|
|
26045
|
+
return Number.isFinite(age) ? 0.9 * Math.pow(0.5, age / 14) : 0;
|
|
26046
|
+
};
|
|
26047
|
+
const ranked = [...scores.values()].filter(({ note }) => filterNote(note)).map((s) => ({ ...s, rank: s.score + recency(s.note) })).sort((a, b2) => b2.rank - a.rank || (a.note.dateModified < b2.note.dateModified ? 1 : -1)).slice(0, max);
|
|
25705
26048
|
const results = await Promise.all(ranked.map(async ({ note }, i) => {
|
|
25706
26049
|
const row = await buildResult(note, i);
|
|
25707
26050
|
return fuzzyIds.has(note.noteId) ? { ...row, fuzzy: true } : row;
|
|
@@ -25713,10 +26056,11 @@ ${block}`), d);
|
|
|
25713
26056
|
...results.length === 0 ? noMatch : {}
|
|
25714
26057
|
});
|
|
25715
26058
|
});
|
|
25716
|
-
server.tool("domain", `Everything for a domain, topic or project: its knowledge book (if any) plus every note carrying a matching #domain or #topic slug, grouped by kind, each with idle days and a stale flag. The reliable retrieval path for an area; use recall() for keyword search.`, {
|
|
26059
|
+
server.tool("domain", `Everything for a domain, topic or project: its knowledge book (if any) plus every note carrying a matching #domain or #topic slug, grouped by kind, each with idle days and a stale flag; outline=true adds every maintained note's headings and size. The reliable retrieval path for an area; use recall() for keyword search.`, {
|
|
25717
26060
|
name: stringType().describe("Domain, topic, or project name"),
|
|
25718
|
-
includeArchived: booleanType().optional().describe("Include archived/resolved items (default: false)")
|
|
25719
|
-
|
|
26061
|
+
includeArchived: booleanType().optional().describe("Include archived/resolved items (default: false)"),
|
|
26062
|
+
outline: booleanType().optional().describe("Also return each maintained note's headings (h2\u2013h4) and size \u2014 plan a multi-note rewrite in one call instead of one outline() per note")
|
|
26063
|
+
}, async ({ name, includeArchived, outline }) => {
|
|
25720
26064
|
const cfg = b();
|
|
25721
26065
|
const slug = slugify(name);
|
|
25722
26066
|
const CAP = 500;
|
|
@@ -25743,6 +26087,19 @@ ${block}`), d);
|
|
|
25743
26087
|
const staleAfter = cfg.policy.staleAfterDays;
|
|
25744
26088
|
const idleSince = (iso) => Math.max(0, Math.floor((Date.now() - new Date(iso.replace(" ", "T")).getTime()) / 86400000));
|
|
25745
26089
|
const RECORD_ROWS = new Set(["session", "diary", "log", "threadEntry"]);
|
|
26090
|
+
const outlines = new Map;
|
|
26091
|
+
if (outline) {
|
|
26092
|
+
await Promise.all(all.filter((n) => {
|
|
26093
|
+
const k = ownedLabel(n, "noteType");
|
|
26094
|
+
return k && k !== "domain" && !RECORD_ROWS.has(k);
|
|
26095
|
+
}).map(async (n) => {
|
|
26096
|
+
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
26097
|
+
outlines.set(n.noteId, {
|
|
26098
|
+
headings: headingOutline(content).filter((h) => !/^Last updated\b/i.test(h.text)).map((h) => `${"#".repeat(h.level - 1)} ${h.text}`),
|
|
26099
|
+
size: content.length
|
|
26100
|
+
});
|
|
26101
|
+
}));
|
|
26102
|
+
}
|
|
25746
26103
|
const groups = {};
|
|
25747
26104
|
let staleCount = 0;
|
|
25748
26105
|
for (const n of all) {
|
|
@@ -25766,7 +26123,8 @@ ${block}`), d);
|
|
|
25766
26123
|
...stale ? { stale: true } : {},
|
|
25767
26124
|
...hasLabel(n, "mandate") ? { mandate: true } : {},
|
|
25768
26125
|
...hasLabel(n, "archived") ? { archived: true } : {},
|
|
25769
|
-
...relations ? { relations } : {}
|
|
26126
|
+
...relations ? { relations } : {},
|
|
26127
|
+
...outlines.has(n.noteId) ? outlines.get(n.noteId) : {}
|
|
25770
26128
|
});
|
|
25771
26129
|
}
|
|
25772
26130
|
const total = all.filter((n) => {
|
|
@@ -25910,7 +26268,7 @@ ${block}`), d);
|
|
|
25910
26268
|
};
|
|
25911
26269
|
server.tool("revise", `Edit a note by id. A revision is taken first; "Last updated" lines are bumped on content writes.
|
|
25912
26270
|
|
|
25913
|
-
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. find= + within="tr" (or li, p, td, \u2026) acts on the element containing a short anchor: replace it, insert a row before/after it, or remove it, so a table row never needs its full stored markup. A replacement equal to its match writes nothing and reports unchanged. title= composes with every mode; retitling a domain book cascades its #domain slug.
|
|
26271
|
+
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. find= + within="tr" (or li, p, td, \u2026) acts on the element containing a short anchor: replace it, insert a row before/after it, or remove it, so a table row never needs its full stored markup. within= names the element acted on: within="tr" replaces the whole row (body is a full <tr>\u2026</tr>), within="td" one cell (body is a <td>\u2026</td>), and a plain find= inside a cell replaces only that text. Keep anchors short; quotes match in any form, and within= also matches an element by its visible text ("Phase 2 | Claude | Open" for a row). closure={thread, body} with within="tr" and mode="remove" closes a register row: the row is removed and the closure appended to that thread's entry for today, in one call. "Last updated" lines are server-owned: every content write bumps them, so never find= on one. A replacement equal to its match writes nothing and reports unchanged. title= composes with every mode; retitling a domain book cascades its #domain slug.
|
|
25914
26272
|
|
|
25915
26273
|
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.`, {
|
|
25916
26274
|
noteId: stringType().optional().describe("Note to update (omit only with domain=)"),
|
|
@@ -25925,6 +26283,10 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
25925
26283
|
find: stringType().optional().describe("Exact raw string to replace throughout the body with body= \u2014 targeted surgery without a read+full-replace. Takes precedence over section/mode."),
|
|
25926
26284
|
nth: numberType().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them). With within=: the Nth containing element"),
|
|
25927
26285
|
within: enumType(WITHIN_TAGS).optional().describe('With find=: act on the <tag> element that contains the find text (a short unique anchor) \u2014 mode="replace" (default) swaps it for body=, "before"/"after" insert body= as a sibling (add a table row), "remove" deletes it'),
|
|
26286
|
+
closure: objectType({
|
|
26287
|
+
thread: stringType().describe('The dated thread to record the closure in: its id or title (e.g. "Escalations")'),
|
|
26288
|
+
body: stringType().describe("The closure: what settled it, who ruled, and where its durable substance went")
|
|
26289
|
+
}).optional().describe(`With within= and mode="remove": close a register row in one call. The row is removed and body= is appended to the thread's entry for today, quoting the removed row. Needs identity=. Run consistency() afterwards on any fact the row asserted elsewhere`),
|
|
25928
26290
|
edits: arrayType(objectType({
|
|
25929
26291
|
find: stringType().describe("Exact raw string to replace"),
|
|
25930
26292
|
body: stringType().describe("Raw replacement string"),
|
|
@@ -25933,7 +26295,9 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
25933
26295
|
identity: stringType().optional().describe('Appending to a dated thread: the identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" (required)'),
|
|
25934
26296
|
icon: stringType().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name; normalized server-side'),
|
|
25935
26297
|
date: stringType().optional().describe("ISO date (default: today)")
|
|
25936
|
-
}, async ({ noteId: noteIdArg, domain, dryRun, body, title, section, occurrence, mode, find, nth, within, edits, identity, icon, date, strict }) => {
|
|
26298
|
+
}, async ({ noteId: noteIdArg, domain, dryRun, body, title, section, occurrence, mode, find, nth, within, closure, edits, identity, icon, date, strict }) => {
|
|
26299
|
+
if (closure && !(within && mode === "remove"))
|
|
26300
|
+
return err("conflicting_params", 'closure= closes a register row and needs within= with mode="remove"; nothing was written.', 'e.g. revise(noteId, find="<row anchor>", within="tr", mode="remove", closure={thread: "Escalations", body: "Closed on \u2026"}, identity="\u2026")');
|
|
25937
26301
|
if (domain !== undefined) {
|
|
25938
26302
|
if (noteIdArg)
|
|
25939
26303
|
return err("conflicting_params", "Pass noteId= or domain=, not both.", "domain= edits every note in the domain; noteId= edits one.");
|
|
@@ -25997,11 +26361,16 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
25997
26361
|
const missHint = (needle, source, alreadyConsumed) => {
|
|
25998
26362
|
if (alreadyConsumed)
|
|
25999
26363
|
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.`;
|
|
26364
|
+
if (/Last updated/i.test(needle))
|
|
26365
|
+
return `Not found \u2014 "Last updated" lines are server-owned and every content write bumps them to today, so this one most likely already moved. Leave it out of find=; the server keeps it current.`;
|
|
26000
26366
|
if (looksEntityEscaped(needle))
|
|
26001
26367
|
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>".`;
|
|
26002
26368
|
if (spansBlockBoundary(needle))
|
|
26003
26369
|
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).`;
|
|
26004
|
-
|
|
26370
|
+
const closed = closedBlockTag(needle);
|
|
26371
|
+
if (closed && !within)
|
|
26372
|
+
return `Not found \u2014 the string ends inside a closing </${closed}>, and where the editor stored that tag decides whether it matches. Use within="${closed}" with a short plain-text anchor from inside the element instead: revise(noteId, find="<a few words>", within="${closed}", body="<${closed}>\u2026</${closed}>").`;
|
|
26373
|
+
return `Not found in the note body (exact, attribute-tolerant, ignoring inline tags${within ? `, or by the visible text of a <${within}>` : ""}) \u2014 already replaced on a retry, or the text genuinely differs.${within ? "" : ' To act on a whole table row by its visible text, use within="tr".'}`;
|
|
26005
26374
|
};
|
|
26006
26375
|
const missContext = (needle, source) => {
|
|
26007
26376
|
const near = nearestContext(source, needle);
|
|
@@ -26021,7 +26390,9 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26021
26390
|
exact.push({ start: at, length: find.length });
|
|
26022
26391
|
const rx = exact.length ? null : tolerantFindRegex(find);
|
|
26023
26392
|
const tolerantHits = rx ? [...current.matchAll(rx)].map((m) => ({ start: m.index, length: m[0].length })) : [];
|
|
26024
|
-
const
|
|
26393
|
+
const inlineHits = exact.length || tolerantHits.length ? [] : inlineTolerantSpans(current, find);
|
|
26394
|
+
const visibleHits = exact.length || tolerantHits.length || inlineHits.length ? [] : visibleTextSpans(current, find, within);
|
|
26395
|
+
const hits = exact.length ? exact : tolerantHits.length ? tolerantHits : inlineHits.length ? inlineHits : visibleHits;
|
|
26025
26396
|
const res = editWithin(current, hits, within, action, body ?? "", nth);
|
|
26026
26397
|
if (!res.ok) {
|
|
26027
26398
|
const hint = res.reason === "no_match" ? missHint(find, current, false) : res.reason === "no_element" ? `The anchor matched ${res.matches} time(s), but never inside a <${within}>. Check the tag, or drop within= to edit the text itself.` : `The anchor sits in ${res.matches} different <${within}> elements. Lengthen it until it is unique, or pass nth= (1-based, in document order) \u2014 elements[] previews them.`;
|
|
@@ -26039,6 +26410,26 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26039
26410
|
}
|
|
26040
26411
|
if (res.html === current)
|
|
26041
26412
|
return txt3({ ok: true, noteId, mode: `within:${within}:${action}`, unchanged: true, date: d, note: "The body equals the element it would replace, so nothing was written and no revision was taken." });
|
|
26413
|
+
let closureThread = null;
|
|
26414
|
+
let closureBlock = "";
|
|
26415
|
+
if (closure) {
|
|
26416
|
+
closureThread = /^[A-Za-z0-9]{12}$/.test(closure.thread) ? await trilium.getNote(closure.thread).catch(() => null) : await findExisting("thread", closure.thread);
|
|
26417
|
+
if (!closureThread || ownedLabel(closureThread, "noteType") !== "thread")
|
|
26418
|
+
return err("not_found", `closure.thread "${closure.thread}" is not a thread; nothing was written.`, `Pass the thread's id or exact title, e.g. "Escalations".`);
|
|
26419
|
+
if (isCollectionThread(closureThread))
|
|
26420
|
+
return collectionAppendRefusal(closureThread);
|
|
26421
|
+
const rendered = renderBody(closure.body).html;
|
|
26422
|
+
const removedText = toText(res.element, 400);
|
|
26423
|
+
const closureHtml = `<p><strong>Closed:</strong> ${escapeHtml(removedText)}</p>
|
|
26424
|
+
${rendered}`;
|
|
26425
|
+
if (!identity)
|
|
26426
|
+
return err("missing_identity", "closure= appends to a dated thread, which needs identity=; nothing was written.", 'Pass identity="LLM \xB7 environment \xB7 agent/mode".');
|
|
26427
|
+
const badIdentity = identityRefusal(identity, closureHtml);
|
|
26428
|
+
if (badIdentity)
|
|
26429
|
+
return badIdentity;
|
|
26430
|
+
closureBlock = `<h3>${escapeHtml(identity)}</h3>
|
|
26431
|
+
${closureHtml}`;
|
|
26432
|
+
}
|
|
26042
26433
|
await trilium.createRevision(noteId).catch(() => null);
|
|
26043
26434
|
const withinResult = sanitizeHtml(res.html);
|
|
26044
26435
|
const stampedWithin = bumpLastUpdated(withinResult.html, d);
|
|
@@ -26049,16 +26440,32 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26049
26440
|
if (labelOf2(note, "status") === "dormant")
|
|
26050
26441
|
await trilium.updateLabelValue(noteId, "status", "active");
|
|
26051
26442
|
const relsWithin = relationSnippet(note);
|
|
26443
|
+
let closureReceipt = null;
|
|
26444
|
+
if (closureThread) {
|
|
26445
|
+
const entry = await appendThreadEntry(closureThread.noteId, closureBlock, d);
|
|
26446
|
+
await trilium.updateLabelValue(closureThread.noteId, "updated", d);
|
|
26447
|
+
if (labelOf2(closureThread, "status") === "dormant")
|
|
26448
|
+
await trilium.updateLabelValue(closureThread.noteId, "status", "active");
|
|
26449
|
+
closureReceipt = {
|
|
26450
|
+
threadId: closureThread.noteId,
|
|
26451
|
+
thread: closureThread.title,
|
|
26452
|
+
entryId: entry.noteId,
|
|
26453
|
+
action: entry.action,
|
|
26454
|
+
next: "Run consistency() on any fact the closed row asserted elsewhere, so no stale sentence survives it."
|
|
26455
|
+
};
|
|
26456
|
+
}
|
|
26052
26457
|
return txt3({
|
|
26053
26458
|
ok: true,
|
|
26054
26459
|
noteId,
|
|
26055
26460
|
mode: `within:${within}:${action}`,
|
|
26056
26461
|
date: d,
|
|
26057
26462
|
element: toText(res.element, 200),
|
|
26463
|
+
...closureReceipt ? { closure: closureReceipt } : {},
|
|
26058
26464
|
...titledWithin.retitled ? { retitled: titledWithin.retitled } : {},
|
|
26059
26465
|
...iconWithin ? { icon: iconWithin } : {},
|
|
26060
26466
|
...relsWithin ? { relations: relsWithin } : {},
|
|
26061
26467
|
...structuralFindings(noteKind, stampedWithin.html),
|
|
26468
|
+
...stampedWithin.bumped ? { lastUpdated: "bumped to today \u2014 the line is server-owned" } : {},
|
|
26062
26469
|
...withinResult.warnings.length ? { sanitized: withinResult.warnings } : {}
|
|
26063
26470
|
});
|
|
26064
26471
|
}
|
|
@@ -26138,6 +26545,7 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26138
26545
|
...iconApplied ? { icon: iconApplied } : {},
|
|
26139
26546
|
...rels ? { relations: rels } : {},
|
|
26140
26547
|
...structure,
|
|
26548
|
+
...stamped.bumped ? { lastUpdated: "bumped to today \u2014 the line is server-owned" } : {},
|
|
26141
26549
|
...replacedResult.warnings.length ? { sanitized: replacedResult.warnings } : {}
|
|
26142
26550
|
});
|
|
26143
26551
|
}
|
|
@@ -26230,6 +26638,9 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26230
26638
|
if (!identity && !leadingIdentification(html)) {
|
|
26231
26639
|
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.`);
|
|
26232
26640
|
}
|
|
26641
|
+
const badIdentity = identityRefusal(identity, html);
|
|
26642
|
+
if (badIdentity)
|
|
26643
|
+
return badIdentity;
|
|
26233
26644
|
const block = identity && !leadingIdentification(html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
26234
26645
|
${html}` : html;
|
|
26235
26646
|
threadEntryResult = await appendThreadEntry(noteId, block, d);
|
|
@@ -26268,7 +26679,7 @@ ${block}`), d).html;
|
|
|
26268
26679
|
} : {},
|
|
26269
26680
|
...sectionMiss,
|
|
26270
26681
|
...sectionHint ? { hint: sectionHint } : {},
|
|
26271
|
-
...threadEntryResult ? { entryId: threadEntryResult.noteId, entryAction: threadEntryResult.action } : {},
|
|
26682
|
+
...threadEntryResult ? { entryId: threadEntryResult.noteId, entryAction: threadEntryResult.action, entry: `today's [${d}] entry in this thread; revise(entryId) edits it` } : {},
|
|
26272
26683
|
...titled.retitled ? { retitled: titled.retitled } : {},
|
|
26273
26684
|
...titled.cascaded ? { domainCascade: `#domain updated on ${titled.cascaded} descendant note(s)` } : {},
|
|
26274
26685
|
...structuralFindings(noteKind, finalContent),
|
|
@@ -26540,8 +26951,10 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26540
26951
|
}
|
|
26541
26952
|
}
|
|
26542
26953
|
});
|
|
26543
|
-
server.tool("consistency", `Does the brain agree with itself? pattern= is a regex with ONE capture group naming the value that should match across notes, e.g. "(\\\\d+) mailboxes"; the result groups every asserting note by value with agreement unanimous or DISAGREEMENT. subject="<fact in prose>" finds notes asserting about a subject however phrased. staleAfterDays=N also reports values held in exactly one note untouched N+ days. Matches stored HTML and tag-stripped text; escape backslashes; scope with domain=/kinds=. Records (sessions, diary, logs, dated thread entries) are skipped by default because they are never rewritten; includeRecords=true or a kinds= naming them brings them back. Scans every in-scope note (fast=true uses Trilium's lossy pre-filter). Run it after correcting any fact recorded in more than one place.`, {
|
|
26544
|
-
pattern: stringType().optional().describe("Regex over note bodies. One capture group = the value that should agree across notes. Omit when using subject= (prose mode)."),
|
|
26954
|
+
server.tool("consistency", `Does the brain agree with itself? pattern= is a regex with ONE capture group naming the value that should match across notes, e.g. "(\\\\d+) mailboxes"; the result groups every asserting note by value with agreement unanimous or DISAGREEMENT. subject="<fact in prose>" finds notes asserting about a subject however phrased. staleAfterDays=N also reports values held in exactly one note untouched N+ days. Matches stored HTML and tag-stripped text; escape backslashes; scope with domain=/kinds=. Records (sessions, diary, logs, dated thread entries) are skipped by default because they are never rewritten; includeRecords=true or a kinds= naming them brings them back. Scans every in-scope note (fast=true uses Trilium's lossy pre-filter). Matching ignores case unless caseSensitive=true (a leading (?i) is accepted). Without a capture group the whole match is the value compared. patterns=[\u2026] checks several facts in one call over one read of each note. Run it after correcting any fact recorded in more than one place.`, {
|
|
26955
|
+
pattern: stringType().optional().describe("Regex over note bodies. One capture group = the value that should agree across notes; with none, the whole match is the value. Omit when using subject= (prose mode)."),
|
|
26956
|
+
patterns: arrayType(stringType()).max(10).optional().describe("Several patterns checked in one call (each reported as its own result, in order); every note is read once"),
|
|
26957
|
+
caseSensitive: booleanType().optional().describe('Match case exactly (default false: "Blueprints" and "blueprints" both match)'),
|
|
26545
26958
|
subject: stringType().optional().describe("A fact in prose \u2014 returns notes asserting about it however phrased (instead of pattern=)"),
|
|
26546
26959
|
staleAfterDays: numberType().optional().describe("With pattern: also report values held in exactly one note untouched N+ days (staleSingles)"),
|
|
26547
26960
|
domain: stringType().optional().describe("Restrict to one knowledge domain"),
|
|
@@ -26550,146 +26963,175 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26550
26963
|
includeArchived: booleanType().optional().describe("Include archived notes (default false)"),
|
|
26551
26964
|
limit: numberType().optional().describe("Max notes to examine (default 60)"),
|
|
26552
26965
|
fast: booleanType().optional().describe("Use Trilium's faster but lossy %= pre-filter (default: scan every in-scope note)")
|
|
26553
|
-
}, async (
|
|
26554
|
-
const
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26560
|
-
|
|
26561
|
-
|
|
26562
|
-
if (!wantsRecords && isRecordNote(n)) {
|
|
26563
|
-
recordsSkipped++;
|
|
26564
|
-
return false;
|
|
26565
|
-
}
|
|
26566
|
-
return true;
|
|
26966
|
+
}, async (args) => {
|
|
26967
|
+
const contentCache = new Map;
|
|
26968
|
+
const contentOf = (id) => {
|
|
26969
|
+
let p = contentCache.get(id);
|
|
26970
|
+
if (!p) {
|
|
26971
|
+
p = trilium.getNoteContent(id).catch(() => "");
|
|
26972
|
+
contentCache.set(id, p);
|
|
26973
|
+
}
|
|
26974
|
+
return p;
|
|
26567
26975
|
};
|
|
26568
|
-
|
|
26569
|
-
|
|
26570
|
-
|
|
26571
|
-
|
|
26976
|
+
if (args.patterns?.length) {
|
|
26977
|
+
if (args.pattern || args.subject)
|
|
26978
|
+
return err("conflicting_params", "Pass patterns=[\u2026] alone, not with pattern= or subject=.", "Put every pattern in the patterns array.");
|
|
26979
|
+
const results = [];
|
|
26980
|
+
for (const p of args.patterns) {
|
|
26981
|
+
const r = await checkOne({ ...args, pattern: p, patterns: undefined });
|
|
26982
|
+
results.push(JSON.parse(r.content[0].text));
|
|
26983
|
+
}
|
|
26984
|
+
const disagreeing = results.filter((r) => r.agreement === "DISAGREEMENT").length;
|
|
26985
|
+
return txt3({ mode: "multi", patterns: args.patterns.length, disagreeing, results });
|
|
26986
|
+
}
|
|
26987
|
+
return checkOne(args);
|
|
26988
|
+
async function checkOne({ pattern, subject, staleAfterDays, domain, kinds, includeRecords, includeArchived, limit, fast, caseSensitive }) {
|
|
26989
|
+
const wantsRecords = includeRecords === true || (kinds ?? []).some((k) => RECORD_KINDS.has(k));
|
|
26990
|
+
let recordsSkipped = 0;
|
|
26991
|
+
const inScope = (n) => {
|
|
26992
|
+
const kind = ownedLabel(n, "noteType");
|
|
26993
|
+
if (!kind)
|
|
26994
|
+
return false;
|
|
26995
|
+
if (kinds?.length && !kinds.includes(kind))
|
|
26996
|
+
return false;
|
|
26997
|
+
if (!wantsRecords && isRecordNote(n)) {
|
|
26998
|
+
recordsSkipped++;
|
|
26999
|
+
return false;
|
|
27000
|
+
}
|
|
27001
|
+
return true;
|
|
27002
|
+
};
|
|
27003
|
+
const recordsNote = () => recordsSkipped ? { recordsSkipped, recordsNote: `${recordsSkipped} record(s) (sessions, diary, logs, dated thread entries) were skipped: they are never rewritten. includeRecords=true matches them too.` } : {};
|
|
27004
|
+
if (!pattern && !subject)
|
|
27005
|
+
return err("missing_param", "consistency() needs either a regex pattern or a prose subject.", 'Pass pattern="(\\\\d+) users" to compare a captured value, or subject="<a fact in prose>" to find every note asserting about that subject however phrased.');
|
|
27006
|
+
if (subject && !pattern) {
|
|
27007
|
+
const max = limit ?? 60;
|
|
27008
|
+
const tokens = queryTokens(subject, 8);
|
|
27009
|
+
if (!tokens.length)
|
|
27010
|
+
return err("no_tokens", "The subject produced no significant content words.", 'A subject of all stop-words ("what was it") cannot find anything \u2014 name the thing you re-measured.');
|
|
27011
|
+
const clauses = ["#noteType"];
|
|
27012
|
+
if (domain)
|
|
27013
|
+
clauses.push(`#domain='${slugify(domain)}'`);
|
|
27014
|
+
const notes = await trilium.searchNotes(clauses.join(" AND "), { ancestorNoteId: b().root, limit: wantsRecords ? max : max * 4, includeArchivedNotes: includeArchived ?? false }).then((r) => r.results).catch(() => []);
|
|
27015
|
+
const scoped = notes.filter(inScope).slice(0, max);
|
|
27016
|
+
const threshold = Math.max(1, Math.ceil(tokens.length / 2));
|
|
27017
|
+
const hits = [];
|
|
27018
|
+
for (const n of scoped) {
|
|
27019
|
+
const content = await contentOf(n.noteId);
|
|
27020
|
+
if (!content)
|
|
27021
|
+
continue;
|
|
27022
|
+
const text = stripTagsWithMap(content).text.toLowerCase();
|
|
27023
|
+
const matchedTokens = tokens.filter((t) => text.includes(t));
|
|
27024
|
+
if (matchedTokens.length < threshold)
|
|
27025
|
+
continue;
|
|
27026
|
+
hits.push({ id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "", matchedTokens, snippet: toText(content, 200) });
|
|
27027
|
+
}
|
|
27028
|
+
return txt3({
|
|
27029
|
+
mode: "subject",
|
|
27030
|
+
subject,
|
|
27031
|
+
tokens,
|
|
27032
|
+
threshold: `${threshold} of ${tokens.length} token(s)`,
|
|
27033
|
+
notesExamined: scoped.length,
|
|
27034
|
+
notes: hits,
|
|
27035
|
+
total: hits.length,
|
|
27036
|
+
...domain ? { domain: slugify(domain) } : {},
|
|
27037
|
+
...recordsNote(),
|
|
27038
|
+
note: hits.length ? `${hits.length} note(s) assert something about "${subject}". Open each to read the claim \u2014 or narrow with domain= / kinds=.` : `No note in scope matched ${threshold}+ of the subject's tokens. That is evidence about the phrasing, not the brain \u2014 this mode is deliberately phrase-agnostic, so if you expected a hit, the fact may not be recorded here at all.`
|
|
27039
|
+
});
|
|
27040
|
+
}
|
|
27041
|
+
if (!pattern)
|
|
27042
|
+
return err("missing_param", "No regex pattern given for consistency.", 'Pass pattern="(\\\\d+) users" \u2014 or subject="<a fact in prose>" for the prose-subject mode.');
|
|
27043
|
+
let re;
|
|
27044
|
+
const inlineInsensitive = /^\(\?i\)/.test(pattern);
|
|
27045
|
+
const source = inlineInsensitive ? pattern.slice(4) : pattern;
|
|
27046
|
+
const flags = caseSensitive && !inlineInsensitive ? "g" : "gi";
|
|
27047
|
+
try {
|
|
27048
|
+
re = new RegExp(source, flags);
|
|
27049
|
+
} catch (e) {
|
|
27050
|
+
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
27051
|
+
}
|
|
27052
|
+
const groupCount = new RegExp(`${source}|`).exec("").length - 1;
|
|
26572
27053
|
const max = limit ?? 60;
|
|
26573
|
-
const
|
|
26574
|
-
if (!tokens.length)
|
|
26575
|
-
return err("no_tokens", "The subject produced no significant content words.", 'A subject of all stop-words ("what was it") cannot find anything \u2014 name the thing you re-measured.');
|
|
26576
|
-
const clauses = ["#noteType"];
|
|
27054
|
+
const clauses = fast ? [`note.content %= '${escapeQueryRegex(pattern)}'`] : ["#noteType"];
|
|
26577
27055
|
if (domain)
|
|
26578
27056
|
clauses.push(`#domain='${slugify(domain)}'`);
|
|
26579
27057
|
const notes = await trilium.searchNotes(clauses.join(" AND "), { ancestorNoteId: b().root, limit: wantsRecords ? max : max * 4, includeArchivedNotes: includeArchived ?? false }).then((r) => r.results).catch(() => []);
|
|
26580
27058
|
const scoped = notes.filter(inScope).slice(0, max);
|
|
26581
|
-
const
|
|
26582
|
-
const
|
|
27059
|
+
const byValue = new Map;
|
|
27060
|
+
const noCapture = [];
|
|
27061
|
+
let hasCaptureGroup = false;
|
|
27062
|
+
const scan = (haystack) => {
|
|
27063
|
+
const values = [];
|
|
27064
|
+
let matched = false;
|
|
27065
|
+
re.lastIndex = 0;
|
|
27066
|
+
let m;
|
|
27067
|
+
while ((m = re.exec(haystack)) !== null) {
|
|
27068
|
+
matched = true;
|
|
27069
|
+
const captured = groupCount === 0 ? m[0] : m.slice(1).find((g) => g !== undefined);
|
|
27070
|
+
if (captured !== undefined && captured !== "")
|
|
27071
|
+
values.push(toText(captured, 120).trim() || captured.trim());
|
|
27072
|
+
if (m[0] === "")
|
|
27073
|
+
re.lastIndex++;
|
|
27074
|
+
}
|
|
27075
|
+
return { values, matched };
|
|
27076
|
+
};
|
|
27077
|
+
let tagSpanning = 0;
|
|
27078
|
+
const idleSince = (iso) => Math.max(0, Math.floor((Date.now() - new Date(iso.replace(" ", "T")).getTime()) / 86400000));
|
|
26583
27079
|
for (const n of scoped) {
|
|
26584
|
-
const content = await
|
|
27080
|
+
const content = await contentOf(n.noteId);
|
|
26585
27081
|
if (!content)
|
|
26586
27082
|
continue;
|
|
26587
|
-
const
|
|
26588
|
-
const
|
|
26589
|
-
|
|
26590
|
-
|
|
26591
|
-
|
|
27083
|
+
const stub = { id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "", idleDays: idleSince(n.dateModified) };
|
|
27084
|
+
const raw = scan(content);
|
|
27085
|
+
const projected = scan(stripTagsWithMap(content).text);
|
|
27086
|
+
if (projected.matched && !raw.matched)
|
|
27087
|
+
tagSpanning++;
|
|
27088
|
+
const seen = new Set;
|
|
27089
|
+
for (const value of [...raw.values, ...projected.values]) {
|
|
27090
|
+
hasCaptureGroup = true;
|
|
27091
|
+
if (seen.has(value))
|
|
27092
|
+
continue;
|
|
27093
|
+
seen.add(value);
|
|
27094
|
+
if (!byValue.has(value))
|
|
27095
|
+
byValue.set(value, []);
|
|
27096
|
+
byValue.get(value).push(stub);
|
|
27097
|
+
}
|
|
27098
|
+
if ((raw.matched || projected.matched) && !seen.size)
|
|
27099
|
+
noCapture.push(stub);
|
|
26592
27100
|
}
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
26598
|
-
|
|
26599
|
-
|
|
26600
|
-
|
|
26601
|
-
|
|
26602
|
-
|
|
26603
|
-
|
|
26604
|
-
});
|
|
26605
|
-
}
|
|
26606
|
-
if (!pattern)
|
|
26607
|
-
return err("missing_param", "No regex pattern given for consistency.", 'Pass pattern="(\\\\d+) users" \u2014 or subject="<a fact in prose>" for the prose-subject mode.');
|
|
26608
|
-
let re;
|
|
26609
|
-
try {
|
|
26610
|
-
re = new RegExp(pattern, "gi");
|
|
26611
|
-
} catch (e) {
|
|
26612
|
-
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
26613
|
-
}
|
|
26614
|
-
const max = limit ?? 60;
|
|
26615
|
-
const clauses = fast ? [`note.content %= '${escapeQueryRegex(pattern)}'`] : ["#noteType"];
|
|
26616
|
-
if (domain)
|
|
26617
|
-
clauses.push(`#domain='${slugify(domain)}'`);
|
|
26618
|
-
const notes = await trilium.searchNotes(clauses.join(" AND "), { ancestorNoteId: b().root, limit: wantsRecords ? max : max * 4, includeArchivedNotes: includeArchived ?? false }).then((r) => r.results).catch(() => []);
|
|
26619
|
-
const scoped = notes.filter(inScope).slice(0, max);
|
|
26620
|
-
const byValue = new Map;
|
|
26621
|
-
const noCapture = [];
|
|
26622
|
-
let hasCaptureGroup = false;
|
|
26623
|
-
const scan = (haystack) => {
|
|
26624
|
-
const values = [];
|
|
26625
|
-
let matched = false;
|
|
26626
|
-
re.lastIndex = 0;
|
|
26627
|
-
let m;
|
|
26628
|
-
while ((m = re.exec(haystack)) !== null) {
|
|
26629
|
-
matched = true;
|
|
26630
|
-
const captured = m.slice(1).find((g) => g !== undefined);
|
|
26631
|
-
if (captured !== undefined)
|
|
26632
|
-
values.push(toText(captured, 120).trim() || captured.trim());
|
|
26633
|
-
if (m[0] === "")
|
|
26634
|
-
re.lastIndex++;
|
|
26635
|
-
}
|
|
26636
|
-
return { values, matched };
|
|
26637
|
-
};
|
|
26638
|
-
let tagSpanning = 0;
|
|
26639
|
-
const idleSince = (iso) => Math.max(0, Math.floor((Date.now() - new Date(iso.replace(" ", "T")).getTime()) / 86400000));
|
|
26640
|
-
for (const n of scoped) {
|
|
26641
|
-
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
26642
|
-
if (!content)
|
|
26643
|
-
continue;
|
|
26644
|
-
const stub = { id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "", idleDays: idleSince(n.dateModified) };
|
|
26645
|
-
const raw = scan(content);
|
|
26646
|
-
const projected = scan(stripTagsWithMap(content).text);
|
|
26647
|
-
if (projected.matched && !raw.matched)
|
|
26648
|
-
tagSpanning++;
|
|
26649
|
-
const seen = new Set;
|
|
26650
|
-
for (const value of [...raw.values, ...projected.values]) {
|
|
26651
|
-
hasCaptureGroup = true;
|
|
26652
|
-
if (seen.has(value))
|
|
26653
|
-
continue;
|
|
26654
|
-
seen.add(value);
|
|
26655
|
-
if (!byValue.has(value))
|
|
26656
|
-
byValue.set(value, []);
|
|
26657
|
-
byValue.get(value).push(stub);
|
|
27101
|
+
if (!hasCaptureGroup) {
|
|
27102
|
+
return txt3({
|
|
27103
|
+
mode: "presence",
|
|
27104
|
+
pattern,
|
|
27105
|
+
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
27106
|
+
notesExamined: scoped.length,
|
|
27107
|
+
...recordsNote(),
|
|
27108
|
+
notes: noCapture,
|
|
27109
|
+
total: noCapture.length,
|
|
27110
|
+
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.'
|
|
27111
|
+
});
|
|
26658
27112
|
}
|
|
26659
|
-
|
|
26660
|
-
|
|
26661
|
-
|
|
26662
|
-
if (!hasCaptureGroup) {
|
|
27113
|
+
const groups = [...byValue.entries()].map(([value, notes]) => ({ value, count: notes.length, notes })).sort((a, b) => b.count - a.count);
|
|
27114
|
+
const staleSingles = staleAfterDays ? groups.filter((g) => g.count === 1 && g.notes[0].idleDays >= staleAfterDays).map((g) => ({ value: g.value, note: { id: g.notes[0].id, title: g.notes[0].title, kind: g.notes[0].kind }, idleDays: g.notes[0].idleDays })) : [];
|
|
27115
|
+
const agrees = groups.length <= 1;
|
|
26663
27116
|
return txt3({
|
|
26664
|
-
mode: "
|
|
27117
|
+
mode: "consistency",
|
|
26665
27118
|
pattern,
|
|
27119
|
+
...groupCount === 0 ? { valueFrom: "the whole match (the pattern has no capture group)" } : {},
|
|
27120
|
+
...caseSensitive && !inlineInsensitive ? { caseSensitive: true } : {},
|
|
26666
27121
|
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
27122
|
+
...domain ? { domain: slugify(domain) } : {},
|
|
26667
27123
|
notesExamined: scoped.length,
|
|
26668
27124
|
...recordsNote(),
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
27125
|
+
...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.` } : {},
|
|
27126
|
+
distinctValues: groups.length,
|
|
27127
|
+
agreement: groups.length === 0 ? "no-data" : agrees ? "unanimous" : "DISAGREEMENT",
|
|
27128
|
+
groups,
|
|
27129
|
+
...staleAfterDays ? { staleAfterDays } : {},
|
|
27130
|
+
...staleSingles.length ? { staleSingles, staleSinglesNote: `${staleSingles.length} figure(s) appear in exactly one note, untouched ${staleAfterDays}d+ \u2014 a single copy cannot disagree with itself, so re-measure these against the world rather than against other notes.` } : {},
|
|
27131
|
+
...noCapture.length ? { matchedWithoutValue: noCapture } : {},
|
|
27132
|
+
note: agrees ? groups.length === 0 ? "No note asserted a value for that pattern. That is evidence about the pattern, not about the brain \u2014 check it against a note you know contains the fact." : `All ${groups[0].count} note(s) agree on "${groups[0].value}".` : `${groups.length} DIFFERENT values are asserted across ${scoped.length} notes. Establish which is true from evidence, correct every note that disagrees, and wire ~corrects from the note that overturns the old claim \u2014 revising in place leaves no trace the wrong value was ever believed.`
|
|
26672
27133
|
});
|
|
26673
27134
|
}
|
|
26674
|
-
const groups = [...byValue.entries()].map(([value, notes]) => ({ value, count: notes.length, notes })).sort((a, b) => b.count - a.count);
|
|
26675
|
-
const staleSingles = staleAfterDays ? groups.filter((g) => g.count === 1 && g.notes[0].idleDays >= staleAfterDays).map((g) => ({ value: g.value, note: { id: g.notes[0].id, title: g.notes[0].title, kind: g.notes[0].kind }, idleDays: g.notes[0].idleDays })) : [];
|
|
26676
|
-
const agrees = groups.length <= 1;
|
|
26677
|
-
return txt3({
|
|
26678
|
-
mode: "consistency",
|
|
26679
|
-
pattern,
|
|
26680
|
-
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
26681
|
-
...domain ? { domain: slugify(domain) } : {},
|
|
26682
|
-
notesExamined: scoped.length,
|
|
26683
|
-
...recordsNote(),
|
|
26684
|
-
...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.` } : {},
|
|
26685
|
-
distinctValues: groups.length,
|
|
26686
|
-
agreement: groups.length === 0 ? "no-data" : agrees ? "unanimous" : "DISAGREEMENT",
|
|
26687
|
-
groups,
|
|
26688
|
-
...staleAfterDays ? { staleAfterDays } : {},
|
|
26689
|
-
...staleSingles.length ? { staleSingles, staleSinglesNote: `${staleSingles.length} figure(s) appear in exactly one note, untouched ${staleAfterDays}d+ \u2014 a single copy cannot disagree with itself, so re-measure these against the world rather than against other notes.` } : {},
|
|
26690
|
-
...noCapture.length ? { matchedWithoutValue: noCapture } : {},
|
|
26691
|
-
note: agrees ? groups.length === 0 ? "No note asserted a value for that pattern. That is evidence about the pattern, not about the brain \u2014 check it against a note you know contains the fact." : `All ${groups[0].count} note(s) agree on "${groups[0].value}".` : `${groups.length} DIFFERENT values are asserted across ${scoped.length} notes. Establish which is true from evidence, correct every note that disagrees, and wire ~corrects from the note that overturns the old claim \u2014 revising in place leaves no trace the wrong value was ever believed.`
|
|
26692
|
-
});
|
|
26693
27135
|
});
|
|
26694
27136
|
server.tool("outline", `A note's heading tree without its body: each h2\u2013h4 with level and occurrence index (and raw stored text where inline markup differs), table key columns, size, and structural findings. Read it before a section= edit you're not sure of. Notes over 15k also get a first-block preview per section.`, {
|
|
26695
27137
|
noteId: stringType().describe("Note to outline")
|
|
@@ -26738,7 +27180,7 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26738
27180
|
...headings.length === 0 ? { note: "No h2\u2013h4 headings \u2014 section= would append a new section rather than match anything." } : {}
|
|
26739
27181
|
});
|
|
26740
27182
|
});
|
|
26741
|
-
server.tool("inspect", `Everything about one note: every label, relation and attachment, type/mime, parent and child ids, dates. content=true adds the raw body (section= narrows it). find="<literal>" counts occurrences (total, per addendum block, per section) and on a miss shows the nearest stored text. Read-only, safe on any note.`, {
|
|
27183
|
+
server.tool("inspect", `Everything about one note: every label, relation and attachment, type/mime, parent and child ids, dates. content=true adds the raw body (section= narrows it). find="<literal>" counts occurrences (total, per addendum block, per section), returns the stored element around each one (up to 10: its row, list item or paragraph, ready to anchor a revise), and on a miss shows the nearest stored text. Read-only, safe on any note.`, {
|
|
26742
27184
|
noteId: stringType().describe("Note to inspect"),
|
|
26743
27185
|
content: booleanType().optional().describe("Include the note's raw body content (default: false)"),
|
|
26744
27186
|
section: stringType().optional().describe("With content=true, return only this heading's section rather than the whole body"),
|
|
@@ -26811,11 +27253,39 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26811
27253
|
}
|
|
26812
27254
|
const total = countIn(rawBody);
|
|
26813
27255
|
const near = total === 0 ? nearestContext(rawBody, find) : null;
|
|
27256
|
+
const matches = [];
|
|
27257
|
+
const seenSpans = new Set;
|
|
27258
|
+
for (let at = rawBody.indexOf(find);at !== -1 && matches.length < 10; at = rawBody.indexOf(find, at + Math.max(1, find.length))) {
|
|
27259
|
+
let el = null;
|
|
27260
|
+
let tag = "";
|
|
27261
|
+
for (const t of ["tr", "li", "p", "blockquote", "td"]) {
|
|
27262
|
+
el = containingElement(rawBody, t, at, at + find.length);
|
|
27263
|
+
if (el) {
|
|
27264
|
+
tag = t;
|
|
27265
|
+
break;
|
|
27266
|
+
}
|
|
27267
|
+
}
|
|
27268
|
+
const start = el?.start ?? Math.max(0, at - 120);
|
|
27269
|
+
const end = el?.end ?? Math.min(rawBody.length, at + find.length + 120);
|
|
27270
|
+
if (seenSpans.has(start))
|
|
27271
|
+
continue;
|
|
27272
|
+
seenSpans.add(start);
|
|
27273
|
+
let sectionName = "(head)";
|
|
27274
|
+
for (const h of headings) {
|
|
27275
|
+
if (h.index < at)
|
|
27276
|
+
sectionName = h.text;
|
|
27277
|
+
else
|
|
27278
|
+
break;
|
|
27279
|
+
}
|
|
27280
|
+
const stored = rawBody.slice(start, end);
|
|
27281
|
+
matches.push({ section: sectionName, element: tag || "text", stored: stored.length > 600 ? `${stored.slice(0, 600)}\u2026` : stored });
|
|
27282
|
+
}
|
|
26814
27283
|
findReport = {
|
|
26815
27284
|
find,
|
|
26816
27285
|
total,
|
|
26817
27286
|
blocks,
|
|
26818
27287
|
...sections.length ? { sections } : {},
|
|
27288
|
+
...matches.length ? { matches } : {},
|
|
26819
27289
|
...near ? { matchedUpTo: near.fragment, storedNearby: near.context } : {},
|
|
26820
27290
|
...total === 0 && !near ? { hint: "Not present, and no fragment of it is either \u2014 the string is unrelated to this note's content." } : {}
|
|
26821
27291
|
};
|
|
@@ -27166,7 +27636,7 @@ assertion + check \u2192 register (deduped by assertion); claimId + holds + evid
|
|
|
27166
27636
|
...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." }
|
|
27167
27637
|
});
|
|
27168
27638
|
});
|
|
27169
|
-
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.`, {
|
|
27639
|
+
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, table rows that disagree with their header, record seals (whether any record of the last seven sealed days was rewritten after close() sealed it) 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.`, {
|
|
27170
27640
|
deep: booleanType().optional().describe("Deep pass: stale-review + orphan/sink + structural lint + duplicate titles across Memory/Threads and Knowledge (default: false)"),
|
|
27171
27641
|
dryRun: booleanType().optional().describe("Report what would change without changing it"),
|
|
27172
27642
|
domain: stringType().optional().describe("Narrow the deep passes to one domain's notes (slugged server-side) \u2014 for scoped agents working a single lane"),
|
|
@@ -27180,6 +27650,15 @@ assertion + check \u2192 register (deduped by assertion); claimId + holds + evid
|
|
|
27180
27650
|
...ack?.length ? { ack } : {},
|
|
27181
27651
|
...repair?.length ? { repair } : {}
|
|
27182
27652
|
});
|
|
27653
|
+
if (deep && !domain) {
|
|
27654
|
+
const seals = await verifySeals(trilium, b(), localToday()).catch(() => null);
|
|
27655
|
+
if (seals) {
|
|
27656
|
+
for (const f of seals.findings) {
|
|
27657
|
+
report.flagged.push(`record seal: ${f.date}${f.noteId ? ` [${f.noteId}]` : ""} \u2014 ${f.problem}. Records are append-only; read the revisions to see whether the change was intended. The finding ages out with the seven-day window.`);
|
|
27658
|
+
}
|
|
27659
|
+
report.seals = seals.checked ? `${seals.checked} sealed day(s) verified, ${seals.findings.length} finding(s)` : "no sealed days yet \u2014 the first close of 12.9.0 seals its day";
|
|
27660
|
+
}
|
|
27661
|
+
}
|
|
27183
27662
|
if (deep && !dryRun && b().sizes && Object.keys(b().sizes).length) {
|
|
27184
27663
|
saveConfig(b());
|
|
27185
27664
|
}
|
|
@@ -27743,6 +28222,37 @@ and every thread day-child, in time order with its identification line.`, {
|
|
|
27743
28222
|
registerMemoryTools(server, trilium, brainRef);
|
|
27744
28223
|
registerKnowledgeTools(server, trilium, brainRef);
|
|
27745
28224
|
registerInsightsTools(server, trilium, brainRef);
|
|
28225
|
+
const registry = server._registeredTools ?? {};
|
|
28226
|
+
for (const name of CONTENT_WRITE_TOOLS) {
|
|
28227
|
+
const tool = registry[name];
|
|
28228
|
+
const inner = tool?.handler;
|
|
28229
|
+
if (!tool || typeof inner !== "function")
|
|
28230
|
+
continue;
|
|
28231
|
+
tool.handler = async (...args) => {
|
|
28232
|
+
const result = await inner(...args);
|
|
28233
|
+
if (!wroteSinceClose && wroteContent(result)) {
|
|
28234
|
+
await markStep("write").catch(() => null);
|
|
28235
|
+
wroteSinceClose = true;
|
|
28236
|
+
}
|
|
28237
|
+
return result;
|
|
28238
|
+
};
|
|
28239
|
+
}
|
|
28240
|
+
}
|
|
28241
|
+
var CONTENT_WRITE_TOOLS = ["remember", "revise", "split", "resolve", "withdraw", "recover", "forget", "label", "connect", "attach", "detach", "claim"];
|
|
28242
|
+
function wroteContent(result) {
|
|
28243
|
+
const text = result?.content?.[0]?.text;
|
|
28244
|
+
if (typeof text !== "string")
|
|
28245
|
+
return true;
|
|
28246
|
+
try {
|
|
28247
|
+
const r = JSON.parse(text);
|
|
28248
|
+
if (r.error || r.ok === false || r.unchanged === true || r.action === "already_written" || r.mode === "already_written")
|
|
28249
|
+
return false;
|
|
28250
|
+
if (r.replaced === 0 && !r.closure)
|
|
28251
|
+
return false;
|
|
28252
|
+
return true;
|
|
28253
|
+
} catch {
|
|
28254
|
+
return true;
|
|
28255
|
+
}
|
|
27746
28256
|
}
|
|
27747
28257
|
|
|
27748
28258
|
// src/annotations.ts
|
|
@@ -28083,10 +28593,24 @@ var asArray = (v) => {
|
|
|
28083
28593
|
return v;
|
|
28084
28594
|
}
|
|
28085
28595
|
};
|
|
28596
|
+
var asObject = (v) => {
|
|
28597
|
+
if (typeof v !== "string")
|
|
28598
|
+
return v;
|
|
28599
|
+
const t = v.trim();
|
|
28600
|
+
if (!t.startsWith("{"))
|
|
28601
|
+
return v;
|
|
28602
|
+
try {
|
|
28603
|
+
const parsed = JSON.parse(t);
|
|
28604
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : v;
|
|
28605
|
+
} catch {
|
|
28606
|
+
return v;
|
|
28607
|
+
}
|
|
28608
|
+
};
|
|
28086
28609
|
var COERCERS = {
|
|
28087
28610
|
ZodBoolean: asBoolean,
|
|
28088
28611
|
ZodNumber: asNumber,
|
|
28089
|
-
ZodArray: asArray
|
|
28612
|
+
ZodArray: asArray,
|
|
28613
|
+
ZodObject: asObject
|
|
28090
28614
|
};
|
|
28091
28615
|
function coerceToolArgs(server) {
|
|
28092
28616
|
const registry = server._registeredTools;
|
|
@@ -28613,7 +29137,7 @@ data: ${JSON.stringify(message)}
|
|
|
28613
29137
|
}
|
|
28614
29138
|
|
|
28615
29139
|
// src/oauth.ts
|
|
28616
|
-
import { createHmac, randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
29140
|
+
import { createHmac, randomBytes, createHash as createHash2, timingSafeEqual } from "crypto";
|
|
28617
29141
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
28618
29142
|
|
|
28619
29143
|
// src/network-security.ts
|
|
@@ -29537,7 +30061,7 @@ var tokenError = (error, description, status = 400) => new Response(JSON.stringi
|
|
|
29537
30061
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
29538
30062
|
});
|
|
29539
30063
|
function refreshKey(token) {
|
|
29540
|
-
return "sha256:" +
|
|
30064
|
+
return "sha256:" + createHash2("sha256").update(token).digest("hex");
|
|
29541
30065
|
}
|
|
29542
30066
|
function issueTokens(base, clientId, resource, scope) {
|
|
29543
30067
|
const store = loadStore();
|
|
@@ -29583,7 +30107,7 @@ async function handleToken(req, base) {
|
|
|
29583
30107
|
return tokenError("invalid_grant", "redirect_uri mismatch.");
|
|
29584
30108
|
if (!verifier)
|
|
29585
30109
|
return tokenError("invalid_request", "code_verifier is required.");
|
|
29586
|
-
const computed =
|
|
30110
|
+
const computed = createHash2("sha256").update(verifier).digest("base64url");
|
|
29587
30111
|
if (computed !== record.codeChallenge)
|
|
29588
30112
|
return tokenError("invalid_grant", "PKCE verification failed.");
|
|
29589
30113
|
return new Response(JSON.stringify(issueTokens(base, record.clientId, record.resource, record.scope)), {
|
|
@@ -29690,7 +30214,7 @@ function createServer(origin = null) {
|
|
|
29690
30214
|
const s = new McpServer({
|
|
29691
30215
|
name: BRAND2.name,
|
|
29692
30216
|
title: BRAND2.name,
|
|
29693
|
-
version: "12.
|
|
30217
|
+
version: "12.9.1",
|
|
29694
30218
|
description: BRAND2.description,
|
|
29695
30219
|
websiteUrl: BRAND2.website,
|
|
29696
30220
|
icons: brandIcons(origin)
|