brainllm 12.8.0 → 12.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/README.md +4 -2
- package/SECURITY.md +4 -2
- package/dist/index.js +698 -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,38 @@ function unbalancedTags(html) {
|
|
|
22680
22805
|
}
|
|
22681
22806
|
return [...new Set(stack)];
|
|
22682
22807
|
}
|
|
22808
|
+
function tableShapeIssues(html) {
|
|
22809
|
+
const issues = [];
|
|
22810
|
+
const cellSpan = (attrs) => Number(/colspan\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: cellSpan(c[2]),
|
|
22814
|
+
text: decodeEntities(c[3].replace(/<[^>]+>/g, "")).trim()
|
|
22815
|
+
})));
|
|
22816
|
+
if (rows.length < 2)
|
|
22817
|
+
continue;
|
|
22818
|
+
const width = rows[0].reduce((sum, c) => sum + c.span, 0);
|
|
22819
|
+
for (const cells of rows.slice(1)) {
|
|
22820
|
+
const label = (cells[0]?.text ?? "").slice(0, 60) || "(empty row)";
|
|
22821
|
+
const count = cells.reduce((sum, c) => sum + c.span, 0);
|
|
22822
|
+
if (count !== width) {
|
|
22823
|
+
issues.push(`row "${label}" has ${count} cell(s), its header has ${width}`);
|
|
22824
|
+
continue;
|
|
22825
|
+
}
|
|
22826
|
+
const piped = cells.find((c) => (c.text.match(/\s\|\s/g)?.length ?? 0) >= Math.max(1, width - 2) && width > 1);
|
|
22827
|
+
if (piped && cells.filter((c) => c.text).length < width) {
|
|
22828
|
+
issues.push(`row "${label}" carries pipe-separated values inside one cell \u2014 send each value as its own <td>`);
|
|
22829
|
+
}
|
|
22830
|
+
}
|
|
22831
|
+
}
|
|
22832
|
+
return issues;
|
|
22833
|
+
}
|
|
22683
22834
|
var LARGE_NOTE_CHARS = 40000;
|
|
22684
22835
|
function structureReport(html) {
|
|
22685
22836
|
return {
|
|
22686
22837
|
duplicateHeadings: duplicateHeadings(html),
|
|
22687
22838
|
unbalancedTags: unbalancedTags(html),
|
|
22839
|
+
tableShape: tableShapeIssues(html),
|
|
22688
22840
|
size: html.length
|
|
22689
22841
|
};
|
|
22690
22842
|
}
|
|
@@ -23518,6 +23670,9 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23518
23670
|
if (structure.unbalancedTags.length) {
|
|
23519
23671
|
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
23672
|
}
|
|
23673
|
+
if (structure.tableShape.length) {
|
|
23674
|
+
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")`);
|
|
23675
|
+
}
|
|
23521
23676
|
const profile = sectionProfile(content);
|
|
23522
23677
|
const biggest = profile.largest[0];
|
|
23523
23678
|
const crowded = profile.count >= SECTION_EDIT_RISK_COUNT;
|
|
@@ -23590,13 +23745,15 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23590
23745
|
byDomain.get(domSlug).push(n);
|
|
23591
23746
|
}
|
|
23592
23747
|
const headingCache = new Map;
|
|
23593
|
-
const
|
|
23748
|
+
const shapeOf = async (n) => {
|
|
23594
23749
|
if (!headingCache.has(n.noteId)) {
|
|
23595
23750
|
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
23596
|
-
|
|
23751
|
+
const words = new Set(content.replace(/<[^>]+>/g, " ").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3));
|
|
23752
|
+
headingCache.set(n.noteId, { headings: headingOutline(content).filter((h) => h.level <= 3).map((h) => h.text), words });
|
|
23597
23753
|
}
|
|
23598
23754
|
return headingCache.get(n.noteId);
|
|
23599
23755
|
};
|
|
23756
|
+
const GENERIC_HEADING = /^(last updated\b.*|overview|context|goal|sources|revision|resolution|notes?|summary)$/i;
|
|
23600
23757
|
for (const [domSlug, notes] of byDomain) {
|
|
23601
23758
|
const reported = new Set;
|
|
23602
23759
|
for (let i = 0;i < notes.length; i++) {
|
|
@@ -23613,10 +23770,19 @@ async function sweep(trilium, cfg, opts = {}) {
|
|
|
23613
23770
|
reported.add(pairKey);
|
|
23614
23771
|
if (acknowledged(a) || acknowledged(bn))
|
|
23615
23772
|
continue;
|
|
23616
|
-
const [
|
|
23617
|
-
const
|
|
23773
|
+
const [sa, sb] = await Promise.all([shapeOf(a), shapeOf(bn)]);
|
|
23774
|
+
const ha = sa.headings, hb = sb.headings;
|
|
23775
|
+
const overlap = ha.filter((h) => !GENERIC_HEADING.test(h) && hb.some((x) => x.toLowerCase() === h.toLowerCase()));
|
|
23776
|
+
let common = 0;
|
|
23777
|
+
for (const w of sa.words)
|
|
23778
|
+
if (sb.words.has(w))
|
|
23779
|
+
common++;
|
|
23780
|
+
const union = sa.words.size + sb.words.size - common;
|
|
23781
|
+
const similarity = union ? common / union : 0;
|
|
23782
|
+
if (!overlap.length && similarity < 0.45)
|
|
23783
|
+
continue;
|
|
23618
23784
|
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,
|
|
23785
|
+
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
23786
|
}
|
|
23621
23787
|
}
|
|
23622
23788
|
}
|
|
@@ -24016,6 +24182,96 @@ async function catchUpDeletions(trilium, cfg, today) {
|
|
|
24016
24182
|
return { windowDays, coverage: "full", deletionsFound: total, regenerated };
|
|
24017
24183
|
}
|
|
24018
24184
|
|
|
24185
|
+
// src/seal.ts
|
|
24186
|
+
import { createHash } from "crypto";
|
|
24187
|
+
var GENESIS = "genesis";
|
|
24188
|
+
function sealText(html) {
|
|
24189
|
+
return decodeEntities(html.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
24190
|
+
}
|
|
24191
|
+
function sha(text) {
|
|
24192
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
24193
|
+
}
|
|
24194
|
+
function sealRecord(text) {
|
|
24195
|
+
return [text.length, sha(text).slice(0, 16)];
|
|
24196
|
+
}
|
|
24197
|
+
function dayDigest(prev, date, records) {
|
|
24198
|
+
const canonical = Object.keys(records).sort().map((id) => `${id}:${records[id][0]}:${records[id][1]}`).join(",");
|
|
24199
|
+
return sha(`${prev}
|
|
24200
|
+
${date}
|
|
24201
|
+
${canonical}`);
|
|
24202
|
+
}
|
|
24203
|
+
function recordIntact(current, sealed) {
|
|
24204
|
+
if (current.length < sealed[0])
|
|
24205
|
+
return false;
|
|
24206
|
+
return sha(current.slice(0, sealed[0])).slice(0, 16) === sealed[1];
|
|
24207
|
+
}
|
|
24208
|
+
async function dayRecords(trilium, cfg, date) {
|
|
24209
|
+
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([]);
|
|
24210
|
+
const [sessions, diary, entries] = await Promise.all([
|
|
24211
|
+
find(cfg.memory.sessions, "session"),
|
|
24212
|
+
find(cfg.llm.diary, "diary"),
|
|
24213
|
+
find(cfg.memory.threads, "threadEntry")
|
|
24214
|
+
]);
|
|
24215
|
+
return [...sessions, ...diary, ...entries.filter((n) => n.title.trim() === `[${date}]`)];
|
|
24216
|
+
}
|
|
24217
|
+
async function sealedLogs(trilium, cfg) {
|
|
24218
|
+
if (!cfg.insights.logs)
|
|
24219
|
+
return [];
|
|
24220
|
+
const res = await trilium.searchNotes("#noteType=log #seal", { ancestorNoteId: cfg.insights.logs, fastSearch: true, limit: 1000, includeArchivedNotes: true }).catch(() => ({ results: [] }));
|
|
24221
|
+
return res.results.filter((n) => ownedLabel(n, "seal")).sort((a, b) => (ownedLabel(a, "created") ?? "").localeCompare(ownedLabel(b, "created") ?? ""));
|
|
24222
|
+
}
|
|
24223
|
+
async function sealDay(trilium, cfg, date, logNoteId) {
|
|
24224
|
+
const earlier = (await sealedLogs(trilium, cfg)).filter((n) => (ownedLabel(n, "created") ?? "") < date);
|
|
24225
|
+
const prev = earlier.length ? ownedLabel(earlier[earlier.length - 1], "seal") : GENESIS;
|
|
24226
|
+
const records = {};
|
|
24227
|
+
for (const n of await dayRecords(trilium, cfg, date)) {
|
|
24228
|
+
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
24229
|
+
if (content)
|
|
24230
|
+
records[n.noteId] = sealRecord(sealText(content));
|
|
24231
|
+
}
|
|
24232
|
+
const digest = dayDigest(prev, date, records);
|
|
24233
|
+
await trilium.updateLabelValue(logNoteId, "sealRecords", JSON.stringify(records));
|
|
24234
|
+
await trilium.updateLabelValue(logNoteId, "sealPrev", prev);
|
|
24235
|
+
await trilium.updateLabelValue(logNoteId, "seal", digest);
|
|
24236
|
+
return { date, digest, records: Object.keys(records).length };
|
|
24237
|
+
}
|
|
24238
|
+
async function verifySeals(trilium, cfg, today, days = 7) {
|
|
24239
|
+
const logs = (await sealedLogs(trilium, cfg)).filter((n) => (ownedLabel(n, "created") ?? "") < today);
|
|
24240
|
+
const findings = [];
|
|
24241
|
+
const window = logs.slice(-days);
|
|
24242
|
+
for (const log of window) {
|
|
24243
|
+
const date = ownedLabel(log, "created") ?? "";
|
|
24244
|
+
const digest = ownedLabel(log, "seal") ?? "";
|
|
24245
|
+
const prev = ownedLabel(log, "sealPrev") ?? "";
|
|
24246
|
+
const i = logs.indexOf(log);
|
|
24247
|
+
const expectedPrev = i > 0 ? ownedLabel(logs[i - 1], "seal") ?? "" : prev;
|
|
24248
|
+
if (prev !== expectedPrev) {
|
|
24249
|
+
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` });
|
|
24250
|
+
}
|
|
24251
|
+
let records;
|
|
24252
|
+
try {
|
|
24253
|
+
records = JSON.parse(ownedLabel(log, "sealRecords") ?? "{}");
|
|
24254
|
+
} catch {
|
|
24255
|
+
findings.push({ date, noteId: log.noteId, problem: "the sealed record list is unreadable" });
|
|
24256
|
+
continue;
|
|
24257
|
+
}
|
|
24258
|
+
if (dayDigest(prev, date, records) !== digest) {
|
|
24259
|
+
findings.push({ date, noteId: log.noteId, problem: "the sealed record list no longer matches its digest \u2014 the seal labels were edited" });
|
|
24260
|
+
}
|
|
24261
|
+
for (const [noteId, sealed] of Object.entries(records)) {
|
|
24262
|
+
const content = await trilium.getNoteContent(noteId).catch(() => null);
|
|
24263
|
+
if (content === null) {
|
|
24264
|
+
findings.push({ date, noteId, problem: "a sealed record is gone (deleted or unreadable)" });
|
|
24265
|
+
continue;
|
|
24266
|
+
}
|
|
24267
|
+
if (!recordIntact(sealText(content), sealed)) {
|
|
24268
|
+
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" });
|
|
24269
|
+
}
|
|
24270
|
+
}
|
|
24271
|
+
}
|
|
24272
|
+
return { checked: window.length, findings };
|
|
24273
|
+
}
|
|
24274
|
+
|
|
24019
24275
|
// src/diffing.ts
|
|
24020
24276
|
function revisionsSince(revisions, cutoff) {
|
|
24021
24277
|
return revisions.filter((r) => (r.dateCreated ?? "").slice(0, 16) >= cutoff);
|
|
@@ -24617,6 +24873,8 @@ function registerTools(server, trilium, brainRef) {
|
|
|
24617
24873
|
const preCloseSteps = new Map;
|
|
24618
24874
|
let preCloseSeq = 0;
|
|
24619
24875
|
const REQUIRED_PRECLOSE_STEPS = ["session", "addendum", "maintain", "remarks", "diary"];
|
|
24876
|
+
const LIGHT_PRECLOSE_STEPS = ["session", "remarks", "diary"];
|
|
24877
|
+
let wroteSinceClose = false;
|
|
24620
24878
|
const gateNote = async (date) => {
|
|
24621
24879
|
const cfg = b();
|
|
24622
24880
|
if (!cfg.memory.sessions)
|
|
@@ -24675,6 +24933,7 @@ function registerTools(server, trilium, brainRef) {
|
|
|
24675
24933
|
await trilium.updateLabelValue(noteId, "gate", "");
|
|
24676
24934
|
preCloseSteps.clear();
|
|
24677
24935
|
preCloseSeq = 0;
|
|
24936
|
+
wroteSinceClose = false;
|
|
24678
24937
|
};
|
|
24679
24938
|
const RECORD_KINDS = new Set(["session", "diary", "log", "threadEntry"]);
|
|
24680
24939
|
const isRecordNote = (n) => {
|
|
@@ -24684,16 +24943,30 @@ function registerTools(server, trilium, brainRef) {
|
|
|
24684
24943
|
return kind === "threadEntry" && /^\[\d{4}-\d{2}-\d{2}\]$/.test(n.title.trim());
|
|
24685
24944
|
};
|
|
24686
24945
|
const structuralFindings = (kind, html) => {
|
|
24687
|
-
if (!html
|
|
24946
|
+
if (!html)
|
|
24688
24947
|
return {};
|
|
24948
|
+
const tableShape = tableShapeIssues(html);
|
|
24949
|
+
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")).' } : {};
|
|
24950
|
+
if (kind && RECORD_KINDS.has(kind))
|
|
24951
|
+
return shape;
|
|
24689
24952
|
const dupes = duplicateHeadings(html);
|
|
24690
24953
|
if (!dupes.length)
|
|
24691
|
-
return
|
|
24954
|
+
return shape;
|
|
24692
24955
|
return {
|
|
24693
24956
|
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=."
|
|
24957
|
+
structureHint: "The note now carries duplicated section headings \u2014 merge them with revise(section=\u2026, mode=replace), or target one specifically with occurrence=.",
|
|
24958
|
+
...shape
|
|
24695
24959
|
};
|
|
24696
24960
|
};
|
|
24961
|
+
const identityRefusal = (identity, html) => {
|
|
24962
|
+
const text = leadingIdentification(html) ? leadingIdentityText(html) : identity;
|
|
24963
|
+
if (!text)
|
|
24964
|
+
return null;
|
|
24965
|
+
const problem = identityProblem(text);
|
|
24966
|
+
if (!problem)
|
|
24967
|
+
return null;
|
|
24968
|
+
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".');
|
|
24969
|
+
};
|
|
24697
24970
|
const applyIcon = async (noteId, icon) => {
|
|
24698
24971
|
const cls = icon ? normalizeIcon(icon) : "";
|
|
24699
24972
|
if (!cls) {
|
|
@@ -24846,7 +25119,7 @@ ${block}` }));
|
|
|
24846
25119
|
});
|
|
24847
25120
|
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
25121
|
|
|
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.`, {
|
|
25122
|
+
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
25123
|
date: stringType().optional().describe("ISO date YYYY-MM-DD (default: today)"),
|
|
24851
25124
|
full: booleanType().optional().describe("Inline every singleton's content and the diary body (default: stubs only)"),
|
|
24852
25125
|
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 +25129,19 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
24856
25129
|
const cfg = b();
|
|
24857
25130
|
if (!cfg.master.root || !cfg.llm.root)
|
|
24858
25131
|
throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
|
|
25132
|
+
if (!(await readGate(d)).has("write")) {
|
|
25133
|
+
await markStep("session", d);
|
|
25134
|
+
return txt3({
|
|
25135
|
+
date: d,
|
|
25136
|
+
closeMode: "light",
|
|
25137
|
+
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.",
|
|
25138
|
+
next: [
|
|
25139
|
+
"Call remarks() \u2014 one short cue for a short session.",
|
|
25140
|
+
"Call diary() \u2014 one short paragraph: what the session was and anything worth the team knowing.",
|
|
25141
|
+
"Call close() \u2014 commit the session log."
|
|
25142
|
+
]
|
|
25143
|
+
});
|
|
25144
|
+
}
|
|
24859
25145
|
const fetchSingleton = async (id) => {
|
|
24860
25146
|
if (!full) {
|
|
24861
25147
|
const note = await trilium.getNote(id);
|
|
@@ -24957,6 +25243,13 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
24957
25243
|
if (!cfg.root)
|
|
24958
25244
|
return txt3({ status: "uninitialized", action: "Run bootstrap first." });
|
|
24959
25245
|
await markStep("remarks");
|
|
25246
|
+
if (!(await readGate(localToday())).has("write")) {
|
|
25247
|
+
return txt3({
|
|
25248
|
+
closeMode: "light",
|
|
25249
|
+
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.",
|
|
25250
|
+
next: ["Write it via diary(body, identity), then call close()."]
|
|
25251
|
+
});
|
|
25252
|
+
}
|
|
24960
25253
|
return txt3({
|
|
24961
25254
|
cues: {
|
|
24962
25255
|
experience: [
|
|
@@ -24977,7 +25270,7 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
24977
25270
|
]
|
|
24978
25271
|
});
|
|
24979
25272
|
});
|
|
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.`, {
|
|
25273
|
+
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
25274
|
summary: stringType().describe("What happened this session \u2014 factual, concise prose"),
|
|
24982
25275
|
title: stringType().optional().describe("Short session title \u2014 appears as an <h2> heading above Summary"),
|
|
24983
25276
|
identity: stringType().describe(`Identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" \u2014 rendered as the block's h3 (required)`),
|
|
@@ -25000,9 +25293,11 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
25000
25293
|
}
|
|
25001
25294
|
}
|
|
25002
25295
|
const gate = await readGate(gateDate);
|
|
25003
|
-
const
|
|
25296
|
+
const light = !gate.has("write");
|
|
25297
|
+
const required = light ? LIGHT_PRECLOSE_STEPS : REQUIRED_PRECLOSE_STEPS;
|
|
25298
|
+
const missing = continued ? [] : required.filter((step) => !gate.has(step));
|
|
25004
25299
|
if (missing.length && !force) {
|
|
25005
|
-
return err("preclose_incomplete", `close() refused \u2014 these pre-close steps haven't run yet this session: ${missing.join(", ")}
|
|
25300
|
+
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
25301
|
}
|
|
25007
25302
|
const seq = (step) => gate.get(step) ?? 0;
|
|
25008
25303
|
const orderOk = continued || missing.length > 0 || seq("session") < seq("remarks") && seq("remarks") < seq("diary");
|
|
@@ -25018,6 +25313,9 @@ The close sequence next[] lays out: singleton updates from the session, then add
|
|
|
25018
25313
|
if (!identity && !leadingIdentification(summaryHtml)) {
|
|
25019
25314
|
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
25315
|
}
|
|
25316
|
+
const badIdentity = identityRefusal(identity, summaryHtml);
|
|
25317
|
+
if (badIdentity)
|
|
25318
|
+
return badIdentity;
|
|
25021
25319
|
const identityBlock = identity && !leadingIdentification(summaryHtml) ? `<h3>${escapeHtml(identity)}</h3>
|
|
25022
25320
|
` : "";
|
|
25023
25321
|
const titleBlock = title ? `<h2>${escapeHtml(title)}</h2>
|
|
@@ -25072,6 +25370,7 @@ ${contentBlock}` }));
|
|
|
25072
25370
|
if (!hasEdge(logNote, noteId))
|
|
25073
25371
|
await trilium.addRelation(logReport.noteId, "references", noteId).catch(() => null);
|
|
25074
25372
|
}
|
|
25373
|
+
const sealReport = logReport?.noteId ? await sealDay(trilium, cfg, d, logReport.noteId).catch(() => null) : null;
|
|
25075
25374
|
let backupStatus = "disabled";
|
|
25076
25375
|
let backupName = rollingBackupName(d);
|
|
25077
25376
|
if (backup !== false) {
|
|
@@ -25093,6 +25392,8 @@ ${contentBlock}` }));
|
|
|
25093
25392
|
...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
25393
|
...iconSet ? { icon: iconSet } : {},
|
|
25095
25394
|
...continued ? { continuing: true } : {},
|
|
25395
|
+
...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" },
|
|
25396
|
+
...light && !continued ? { closeMode: "light \u2014 no content was written this session, so addendum() and maintain() were not required" } : {},
|
|
25096
25397
|
...missing.length || !orderOk ? { bypassed: [...missing, ...!orderOk ? ["ordering(session\u2192remarks\u2192diary)"] : []] } : {},
|
|
25097
25398
|
...warnings.length ? { sanitized: warnings } : {}
|
|
25098
25399
|
});
|
|
@@ -25190,6 +25491,9 @@ ${contentBlock}` }));
|
|
|
25190
25491
|
if (!identity && !leadingIdentification(sanitized.html)) {
|
|
25191
25492
|
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
25493
|
}
|
|
25494
|
+
const badIdentity = identityRefusal(identity, sanitized.html);
|
|
25495
|
+
if (badIdentity)
|
|
25496
|
+
return badIdentity;
|
|
25193
25497
|
const html = identity && !leadingIdentification(sanitized.html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
25194
25498
|
${sanitized.html}` : sanitized.html;
|
|
25195
25499
|
const found = await trilium.searchNotes(`#noteType=diary #created='${d}'`, { ancestorNoteId: parentId, fastSearch: true, limit: 1 });
|
|
@@ -25372,7 +25676,8 @@ Collection kinds dedup by title \u2014 mustCreate=true refuses instead of adopti
|
|
|
25372
25676
|
...createdDomain ? { createdDomain: domainTitle } : {},
|
|
25373
25677
|
...revisionChanges.length ? { revision: revisionChanges } : {},
|
|
25374
25678
|
...sourceMerge,
|
|
25375
|
-
...revisionKeys.length ? { revisionRows: revisionKeys } : {},
|
|
25679
|
+
...revisionKeys.length ? { revisionRows: revisionKeys.length } : {},
|
|
25680
|
+
...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
25681
|
...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
25682
|
...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
25683
|
...connected.length ? { connected } : {},
|
|
@@ -25470,6 +25775,9 @@ Collection kinds dedup by title \u2014 mustCreate=true refuses instead of adopti
|
|
|
25470
25775
|
if (kind === "thread" && !identity && !leadingIdentification(html)) {
|
|
25471
25776
|
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
25777
|
}
|
|
25778
|
+
const badIdentity = identityRefusal(identity, html);
|
|
25779
|
+
if (badIdentity)
|
|
25780
|
+
return badIdentity;
|
|
25473
25781
|
const block = identity && !leadingIdentification(html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
25474
25782
|
${html}` : html;
|
|
25475
25783
|
if (kind === "thread") {
|
|
@@ -25489,7 +25797,7 @@ ${html}` : html;
|
|
|
25489
25797
|
const connected = await wireRequested(existing.noteId);
|
|
25490
25798
|
const iconSet = await applyIcon(existing.noteId, icon);
|
|
25491
25799
|
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 } : {} });
|
|
25800
|
+
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
25801
|
}
|
|
25494
25802
|
const current = await trilium.getNoteContent(existing.noteId);
|
|
25495
25803
|
if (isDuplicateAppend(current, block))
|
|
@@ -25553,7 +25861,7 @@ ${block}`), d);
|
|
|
25553
25861
|
...sanitizeWarnings.length ? { sanitized: sanitizeWarnings } : {}
|
|
25554
25862
|
});
|
|
25555
25863
|
});
|
|
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.`, {
|
|
25864
|
+
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
25865
|
query: stringType().describe("What to find \u2014 natural phrasing is fine"),
|
|
25558
25866
|
kinds: arrayType(enumType(Kinds)).optional().describe("Restrict to these kinds"),
|
|
25559
25867
|
domain: stringType().optional().describe("Restrict to a knowledge domain"),
|
|
@@ -25616,10 +25924,21 @@ ${block}`), d);
|
|
|
25616
25924
|
} catch (e) {
|
|
25617
25925
|
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
25618
25926
|
}
|
|
25619
|
-
const
|
|
25927
|
+
const backendPattern = triliumSearchForm(regex);
|
|
25928
|
+
const widened = backendPattern !== regex;
|
|
25929
|
+
const candidates = (await run(`note.content %= '${escapeQueryRegex(backendPattern)}'`)).filter(filterNote);
|
|
25620
25930
|
const confirmed = [];
|
|
25621
25931
|
let rejected = 0;
|
|
25622
25932
|
let tagSpanning = 0;
|
|
25933
|
+
let recordHits = 0;
|
|
25934
|
+
const evidenceIn = (text) => {
|
|
25935
|
+
const m = new RegExp(re.source, "i").exec(text);
|
|
25936
|
+
if (!m)
|
|
25937
|
+
return "";
|
|
25938
|
+
const from = Math.max(0, m.index - 80);
|
|
25939
|
+
const to = Math.min(text.length, m.index + m[0].length + 80);
|
|
25940
|
+
return `${from > 0 ? "\u2026" : ""}${text.slice(from, to).replace(/\s+/g, " ").trim()}${to < text.length ? "\u2026" : ""}`;
|
|
25941
|
+
};
|
|
25623
25942
|
for (const n of candidates) {
|
|
25624
25943
|
if (confirmed.length >= max)
|
|
25625
25944
|
break;
|
|
@@ -25629,18 +25948,28 @@ ${block}`), d);
|
|
|
25629
25948
|
continue;
|
|
25630
25949
|
}
|
|
25631
25950
|
const rawHit = re.test(content);
|
|
25632
|
-
const
|
|
25951
|
+
const projected = stripTagsWithMap(content).text;
|
|
25952
|
+
const projectedHit = re.test(projected);
|
|
25633
25953
|
if (rawHit || projectedHit) {
|
|
25634
|
-
if (projectedHit)
|
|
25954
|
+
if (projectedHit && !rawHit)
|
|
25635
25955
|
tagSpanning++;
|
|
25636
|
-
|
|
25956
|
+
const kind = labelOf2(n, "noteType");
|
|
25957
|
+
if (kind === "session" || kind === "diary" || kind === "log" || kind === "threadEntry" && /^\[\d{4}-\d{2}-\d{2}\]$/.test(n.title.trim()))
|
|
25958
|
+
recordHits++;
|
|
25959
|
+
confirmed.push({ note: n, evidence: decodeEntities(projectedHit ? evidenceIn(projected) : evidenceIn(toText(content, 1e5))) });
|
|
25637
25960
|
} else
|
|
25638
25961
|
rejected++;
|
|
25639
25962
|
}
|
|
25640
|
-
const results = await Promise.all(confirmed.map(
|
|
25963
|
+
const results = await Promise.all(confirmed.map(async ({ note, evidence }, i) => {
|
|
25964
|
+
const { snippet: _snippet, ...base } = await buildResult(note, i);
|
|
25965
|
+
return { ...base, ...evidence ? { evidence } : {} };
|
|
25966
|
+
}));
|
|
25641
25967
|
const notes = [
|
|
25642
25968
|
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
25969
|
tagSpanning ? `${tagSpanning} match(es) were found only after stripping markup \u2014 the phrase is split by an inline tag there.` : null,
|
|
25970
|
+
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,
|
|
25971
|
+
recordHits ? `${recordHits} result(s) are records (sessions, diary, dated thread entries) \u2014 included by default; kinds= narrows to maintained notes.` : null,
|
|
25972
|
+
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
25973
|
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
25974
|
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
25975
|
].filter(Boolean);
|
|
@@ -25701,7 +26030,12 @@ ${block}`), d);
|
|
|
25701
26030
|
fuzzyIds.add(n.noteId);
|
|
25702
26031
|
add(byFuzzy, 0.5);
|
|
25703
26032
|
}
|
|
25704
|
-
const
|
|
26033
|
+
const now = Date.now();
|
|
26034
|
+
const recency = (n) => {
|
|
26035
|
+
const age = Math.max(0, (now - new Date(n.dateModified.replace(" ", "T")).getTime()) / 86400000);
|
|
26036
|
+
return Number.isFinite(age) ? 0.9 * Math.pow(0.5, age / 14) : 0;
|
|
26037
|
+
};
|
|
26038
|
+
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
26039
|
const results = await Promise.all(ranked.map(async ({ note }, i) => {
|
|
25706
26040
|
const row = await buildResult(note, i);
|
|
25707
26041
|
return fuzzyIds.has(note.noteId) ? { ...row, fuzzy: true } : row;
|
|
@@ -25713,10 +26047,11 @@ ${block}`), d);
|
|
|
25713
26047
|
...results.length === 0 ? noMatch : {}
|
|
25714
26048
|
});
|
|
25715
26049
|
});
|
|
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.`, {
|
|
26050
|
+
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
26051
|
name: stringType().describe("Domain, topic, or project name"),
|
|
25718
|
-
includeArchived: booleanType().optional().describe("Include archived/resolved items (default: false)")
|
|
25719
|
-
|
|
26052
|
+
includeArchived: booleanType().optional().describe("Include archived/resolved items (default: false)"),
|
|
26053
|
+
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")
|
|
26054
|
+
}, async ({ name, includeArchived, outline }) => {
|
|
25720
26055
|
const cfg = b();
|
|
25721
26056
|
const slug = slugify(name);
|
|
25722
26057
|
const CAP = 500;
|
|
@@ -25743,6 +26078,19 @@ ${block}`), d);
|
|
|
25743
26078
|
const staleAfter = cfg.policy.staleAfterDays;
|
|
25744
26079
|
const idleSince = (iso) => Math.max(0, Math.floor((Date.now() - new Date(iso.replace(" ", "T")).getTime()) / 86400000));
|
|
25745
26080
|
const RECORD_ROWS = new Set(["session", "diary", "log", "threadEntry"]);
|
|
26081
|
+
const outlines = new Map;
|
|
26082
|
+
if (outline) {
|
|
26083
|
+
await Promise.all(all.filter((n) => {
|
|
26084
|
+
const k = ownedLabel(n, "noteType");
|
|
26085
|
+
return k && k !== "domain" && !RECORD_ROWS.has(k);
|
|
26086
|
+
}).map(async (n) => {
|
|
26087
|
+
const content = await trilium.getNoteContent(n.noteId).catch(() => "");
|
|
26088
|
+
outlines.set(n.noteId, {
|
|
26089
|
+
headings: headingOutline(content).filter((h) => !/^Last updated\b/i.test(h.text)).map((h) => `${"#".repeat(h.level - 1)} ${h.text}`),
|
|
26090
|
+
size: content.length
|
|
26091
|
+
});
|
|
26092
|
+
}));
|
|
26093
|
+
}
|
|
25746
26094
|
const groups = {};
|
|
25747
26095
|
let staleCount = 0;
|
|
25748
26096
|
for (const n of all) {
|
|
@@ -25766,7 +26114,8 @@ ${block}`), d);
|
|
|
25766
26114
|
...stale ? { stale: true } : {},
|
|
25767
26115
|
...hasLabel(n, "mandate") ? { mandate: true } : {},
|
|
25768
26116
|
...hasLabel(n, "archived") ? { archived: true } : {},
|
|
25769
|
-
...relations ? { relations } : {}
|
|
26117
|
+
...relations ? { relations } : {},
|
|
26118
|
+
...outlines.has(n.noteId) ? outlines.get(n.noteId) : {}
|
|
25770
26119
|
});
|
|
25771
26120
|
}
|
|
25772
26121
|
const total = all.filter((n) => {
|
|
@@ -25910,7 +26259,7 @@ ${block}`), d);
|
|
|
25910
26259
|
};
|
|
25911
26260
|
server.tool("revise", `Edit a note by id. A revision is taken first; "Last updated" lines are bumped on content writes.
|
|
25912
26261
|
|
|
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.
|
|
26262
|
+
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
26263
|
|
|
25915
26264
|
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
26265
|
noteId: stringType().optional().describe("Note to update (omit only with domain=)"),
|
|
@@ -25925,6 +26274,10 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
25925
26274
|
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
26275
|
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
26276
|
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'),
|
|
26277
|
+
closure: objectType({
|
|
26278
|
+
thread: stringType().describe('The dated thread to record the closure in: its id or title (e.g. "Escalations")'),
|
|
26279
|
+
body: stringType().describe("The closure: what settled it, who ruled, and where its durable substance went")
|
|
26280
|
+
}).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
26281
|
edits: arrayType(objectType({
|
|
25929
26282
|
find: stringType().describe("Exact raw string to replace"),
|
|
25930
26283
|
body: stringType().describe("Raw replacement string"),
|
|
@@ -25933,7 +26286,9 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
25933
26286
|
identity: stringType().optional().describe('Appending to a dated thread: the identification line "LLM \xB7 environment \xB7 agent/mode [\xB7 Run N]" (required)'),
|
|
25934
26287
|
icon: stringType().optional().describe('Display icon \u2014 a boxicons class ("bx bx-brain") or a bare name; normalized server-side'),
|
|
25935
26288
|
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 }) => {
|
|
26289
|
+
}, async ({ noteId: noteIdArg, domain, dryRun, body, title, section, occurrence, mode, find, nth, within, closure, edits, identity, icon, date, strict }) => {
|
|
26290
|
+
if (closure && !(within && mode === "remove"))
|
|
26291
|
+
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
26292
|
if (domain !== undefined) {
|
|
25938
26293
|
if (noteIdArg)
|
|
25939
26294
|
return err("conflicting_params", "Pass noteId= or domain=, not both.", "domain= edits every note in the domain; noteId= edits one.");
|
|
@@ -25997,11 +26352,16 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
25997
26352
|
const missHint = (needle, source, alreadyConsumed) => {
|
|
25998
26353
|
if (alreadyConsumed)
|
|
25999
26354
|
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.`;
|
|
26355
|
+
if (/Last updated/i.test(needle))
|
|
26356
|
+
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
26357
|
if (looksEntityEscaped(needle))
|
|
26001
26358
|
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
26359
|
if (spansBlockBoundary(needle))
|
|
26003
26360
|
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
|
-
|
|
26361
|
+
const closed = closedBlockTag(needle);
|
|
26362
|
+
if (closed && !within)
|
|
26363
|
+
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}>").`;
|
|
26364
|
+
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
26365
|
};
|
|
26006
26366
|
const missContext = (needle, source) => {
|
|
26007
26367
|
const near = nearestContext(source, needle);
|
|
@@ -26021,7 +26381,9 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26021
26381
|
exact.push({ start: at, length: find.length });
|
|
26022
26382
|
const rx = exact.length ? null : tolerantFindRegex(find);
|
|
26023
26383
|
const tolerantHits = rx ? [...current.matchAll(rx)].map((m) => ({ start: m.index, length: m[0].length })) : [];
|
|
26024
|
-
const
|
|
26384
|
+
const inlineHits = exact.length || tolerantHits.length ? [] : inlineTolerantSpans(current, find);
|
|
26385
|
+
const visibleHits = exact.length || tolerantHits.length || inlineHits.length ? [] : visibleTextSpans(current, find, within);
|
|
26386
|
+
const hits = exact.length ? exact : tolerantHits.length ? tolerantHits : inlineHits.length ? inlineHits : visibleHits;
|
|
26025
26387
|
const res = editWithin(current, hits, within, action, body ?? "", nth);
|
|
26026
26388
|
if (!res.ok) {
|
|
26027
26389
|
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 +26401,26 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26039
26401
|
}
|
|
26040
26402
|
if (res.html === current)
|
|
26041
26403
|
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." });
|
|
26404
|
+
let closureThread = null;
|
|
26405
|
+
let closureBlock = "";
|
|
26406
|
+
if (closure) {
|
|
26407
|
+
closureThread = /^[A-Za-z0-9]{12}$/.test(closure.thread) ? await trilium.getNote(closure.thread).catch(() => null) : await findExisting("thread", closure.thread);
|
|
26408
|
+
if (!closureThread || ownedLabel(closureThread, "noteType") !== "thread")
|
|
26409
|
+
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".`);
|
|
26410
|
+
if (isCollectionThread(closureThread))
|
|
26411
|
+
return collectionAppendRefusal(closureThread);
|
|
26412
|
+
const rendered = renderBody(closure.body).html;
|
|
26413
|
+
const removedText = toText(res.element, 400);
|
|
26414
|
+
const closureHtml = `<p><strong>Closed:</strong> ${escapeHtml(removedText)}</p>
|
|
26415
|
+
${rendered}`;
|
|
26416
|
+
if (!identity)
|
|
26417
|
+
return err("missing_identity", "closure= appends to a dated thread, which needs identity=; nothing was written.", 'Pass identity="LLM \xB7 environment \xB7 agent/mode".');
|
|
26418
|
+
const badIdentity = identityRefusal(identity, closureHtml);
|
|
26419
|
+
if (badIdentity)
|
|
26420
|
+
return badIdentity;
|
|
26421
|
+
closureBlock = `<h3>${escapeHtml(identity)}</h3>
|
|
26422
|
+
${closureHtml}`;
|
|
26423
|
+
}
|
|
26042
26424
|
await trilium.createRevision(noteId).catch(() => null);
|
|
26043
26425
|
const withinResult = sanitizeHtml(res.html);
|
|
26044
26426
|
const stampedWithin = bumpLastUpdated(withinResult.html, d);
|
|
@@ -26049,16 +26431,32 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26049
26431
|
if (labelOf2(note, "status") === "dormant")
|
|
26050
26432
|
await trilium.updateLabelValue(noteId, "status", "active");
|
|
26051
26433
|
const relsWithin = relationSnippet(note);
|
|
26434
|
+
let closureReceipt = null;
|
|
26435
|
+
if (closureThread) {
|
|
26436
|
+
const entry = await appendThreadEntry(closureThread.noteId, closureBlock, d);
|
|
26437
|
+
await trilium.updateLabelValue(closureThread.noteId, "updated", d);
|
|
26438
|
+
if (labelOf2(closureThread, "status") === "dormant")
|
|
26439
|
+
await trilium.updateLabelValue(closureThread.noteId, "status", "active");
|
|
26440
|
+
closureReceipt = {
|
|
26441
|
+
threadId: closureThread.noteId,
|
|
26442
|
+
thread: closureThread.title,
|
|
26443
|
+
entryId: entry.noteId,
|
|
26444
|
+
action: entry.action,
|
|
26445
|
+
next: "Run consistency() on any fact the closed row asserted elsewhere, so no stale sentence survives it."
|
|
26446
|
+
};
|
|
26447
|
+
}
|
|
26052
26448
|
return txt3({
|
|
26053
26449
|
ok: true,
|
|
26054
26450
|
noteId,
|
|
26055
26451
|
mode: `within:${within}:${action}`,
|
|
26056
26452
|
date: d,
|
|
26057
26453
|
element: toText(res.element, 200),
|
|
26454
|
+
...closureReceipt ? { closure: closureReceipt } : {},
|
|
26058
26455
|
...titledWithin.retitled ? { retitled: titledWithin.retitled } : {},
|
|
26059
26456
|
...iconWithin ? { icon: iconWithin } : {},
|
|
26060
26457
|
...relsWithin ? { relations: relsWithin } : {},
|
|
26061
26458
|
...structuralFindings(noteKind, stampedWithin.html),
|
|
26459
|
+
...stampedWithin.bumped ? { lastUpdated: "bumped to today \u2014 the line is server-owned" } : {},
|
|
26062
26460
|
...withinResult.warnings.length ? { sanitized: withinResult.warnings } : {}
|
|
26063
26461
|
});
|
|
26064
26462
|
}
|
|
@@ -26138,6 +26536,7 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26138
26536
|
...iconApplied ? { icon: iconApplied } : {},
|
|
26139
26537
|
...rels ? { relations: rels } : {},
|
|
26140
26538
|
...structure,
|
|
26539
|
+
...stamped.bumped ? { lastUpdated: "bumped to today \u2014 the line is server-owned" } : {},
|
|
26141
26540
|
...replacedResult.warnings.length ? { sanitized: replacedResult.warnings } : {}
|
|
26142
26541
|
});
|
|
26143
26542
|
}
|
|
@@ -26230,6 +26629,9 @@ Check the receipt: matched=false means a NEW section was written (available= lis
|
|
|
26230
26629
|
if (!identity && !leadingIdentification(html)) {
|
|
26231
26630
|
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
26631
|
}
|
|
26632
|
+
const badIdentity = identityRefusal(identity, html);
|
|
26633
|
+
if (badIdentity)
|
|
26634
|
+
return badIdentity;
|
|
26233
26635
|
const block = identity && !leadingIdentification(html) ? `<h3>${escapeHtml(identity)}</h3>
|
|
26234
26636
|
${html}` : html;
|
|
26235
26637
|
threadEntryResult = await appendThreadEntry(noteId, block, d);
|
|
@@ -26268,7 +26670,7 @@ ${block}`), d).html;
|
|
|
26268
26670
|
} : {},
|
|
26269
26671
|
...sectionMiss,
|
|
26270
26672
|
...sectionHint ? { hint: sectionHint } : {},
|
|
26271
|
-
...threadEntryResult ? { entryId: threadEntryResult.noteId, entryAction: threadEntryResult.action } : {},
|
|
26673
|
+
...threadEntryResult ? { entryId: threadEntryResult.noteId, entryAction: threadEntryResult.action, entry: `today's [${d}] entry in this thread; revise(entryId) edits it` } : {},
|
|
26272
26674
|
...titled.retitled ? { retitled: titled.retitled } : {},
|
|
26273
26675
|
...titled.cascaded ? { domainCascade: `#domain updated on ${titled.cascaded} descendant note(s)` } : {},
|
|
26274
26676
|
...structuralFindings(noteKind, finalContent),
|
|
@@ -26540,8 +26942,10 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26540
26942
|
}
|
|
26541
26943
|
}
|
|
26542
26944
|
});
|
|
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)."),
|
|
26945
|
+
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.`, {
|
|
26946
|
+
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)."),
|
|
26947
|
+
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"),
|
|
26948
|
+
caseSensitive: booleanType().optional().describe('Match case exactly (default false: "Blueprints" and "blueprints" both match)'),
|
|
26545
26949
|
subject: stringType().optional().describe("A fact in prose \u2014 returns notes asserting about it however phrased (instead of pattern=)"),
|
|
26546
26950
|
staleAfterDays: numberType().optional().describe("With pattern: also report values held in exactly one note untouched N+ days (staleSingles)"),
|
|
26547
26951
|
domain: stringType().optional().describe("Restrict to one knowledge domain"),
|
|
@@ -26550,146 +26954,175 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26550
26954
|
includeArchived: booleanType().optional().describe("Include archived notes (default false)"),
|
|
26551
26955
|
limit: numberType().optional().describe("Max notes to examine (default 60)"),
|
|
26552
26956
|
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;
|
|
26957
|
+
}, async (args) => {
|
|
26958
|
+
const contentCache = new Map;
|
|
26959
|
+
const contentOf = (id) => {
|
|
26960
|
+
let p = contentCache.get(id);
|
|
26961
|
+
if (!p) {
|
|
26962
|
+
p = trilium.getNoteContent(id).catch(() => "");
|
|
26963
|
+
contentCache.set(id, p);
|
|
26964
|
+
}
|
|
26965
|
+
return p;
|
|
26567
26966
|
};
|
|
26568
|
-
|
|
26569
|
-
|
|
26570
|
-
|
|
26571
|
-
|
|
26967
|
+
if (args.patterns?.length) {
|
|
26968
|
+
if (args.pattern || args.subject)
|
|
26969
|
+
return err("conflicting_params", "Pass patterns=[\u2026] alone, not with pattern= or subject=.", "Put every pattern in the patterns array.");
|
|
26970
|
+
const results = [];
|
|
26971
|
+
for (const p of args.patterns) {
|
|
26972
|
+
const r = await checkOne({ ...args, pattern: p, patterns: undefined });
|
|
26973
|
+
results.push(JSON.parse(r.content[0].text));
|
|
26974
|
+
}
|
|
26975
|
+
const disagreeing = results.filter((r) => r.agreement === "DISAGREEMENT").length;
|
|
26976
|
+
return txt3({ mode: "multi", patterns: args.patterns.length, disagreeing, results });
|
|
26977
|
+
}
|
|
26978
|
+
return checkOne(args);
|
|
26979
|
+
async function checkOne({ pattern, subject, staleAfterDays, domain, kinds, includeRecords, includeArchived, limit, fast, caseSensitive }) {
|
|
26980
|
+
const wantsRecords = includeRecords === true || (kinds ?? []).some((k) => RECORD_KINDS.has(k));
|
|
26981
|
+
let recordsSkipped = 0;
|
|
26982
|
+
const inScope = (n) => {
|
|
26983
|
+
const kind = ownedLabel(n, "noteType");
|
|
26984
|
+
if (!kind)
|
|
26985
|
+
return false;
|
|
26986
|
+
if (kinds?.length && !kinds.includes(kind))
|
|
26987
|
+
return false;
|
|
26988
|
+
if (!wantsRecords && isRecordNote(n)) {
|
|
26989
|
+
recordsSkipped++;
|
|
26990
|
+
return false;
|
|
26991
|
+
}
|
|
26992
|
+
return true;
|
|
26993
|
+
};
|
|
26994
|
+
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.` } : {};
|
|
26995
|
+
if (!pattern && !subject)
|
|
26996
|
+
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.');
|
|
26997
|
+
if (subject && !pattern) {
|
|
26998
|
+
const max = limit ?? 60;
|
|
26999
|
+
const tokens = queryTokens(subject, 8);
|
|
27000
|
+
if (!tokens.length)
|
|
27001
|
+
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.');
|
|
27002
|
+
const clauses = ["#noteType"];
|
|
27003
|
+
if (domain)
|
|
27004
|
+
clauses.push(`#domain='${slugify(domain)}'`);
|
|
27005
|
+
const notes = await trilium.searchNotes(clauses.join(" AND "), { ancestorNoteId: b().root, limit: wantsRecords ? max : max * 4, includeArchivedNotes: includeArchived ?? false }).then((r) => r.results).catch(() => []);
|
|
27006
|
+
const scoped = notes.filter(inScope).slice(0, max);
|
|
27007
|
+
const threshold = Math.max(1, Math.ceil(tokens.length / 2));
|
|
27008
|
+
const hits = [];
|
|
27009
|
+
for (const n of scoped) {
|
|
27010
|
+
const content = await contentOf(n.noteId);
|
|
27011
|
+
if (!content)
|
|
27012
|
+
continue;
|
|
27013
|
+
const text = stripTagsWithMap(content).text.toLowerCase();
|
|
27014
|
+
const matchedTokens = tokens.filter((t) => text.includes(t));
|
|
27015
|
+
if (matchedTokens.length < threshold)
|
|
27016
|
+
continue;
|
|
27017
|
+
hits.push({ id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "", matchedTokens, snippet: toText(content, 200) });
|
|
27018
|
+
}
|
|
27019
|
+
return txt3({
|
|
27020
|
+
mode: "subject",
|
|
27021
|
+
subject,
|
|
27022
|
+
tokens,
|
|
27023
|
+
threshold: `${threshold} of ${tokens.length} token(s)`,
|
|
27024
|
+
notesExamined: scoped.length,
|
|
27025
|
+
notes: hits,
|
|
27026
|
+
total: hits.length,
|
|
27027
|
+
...domain ? { domain: slugify(domain) } : {},
|
|
27028
|
+
...recordsNote(),
|
|
27029
|
+
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.`
|
|
27030
|
+
});
|
|
27031
|
+
}
|
|
27032
|
+
if (!pattern)
|
|
27033
|
+
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.');
|
|
27034
|
+
let re;
|
|
27035
|
+
const inlineInsensitive = /^\(\?i\)/.test(pattern);
|
|
27036
|
+
const source = inlineInsensitive ? pattern.slice(4) : pattern;
|
|
27037
|
+
const flags = caseSensitive && !inlineInsensitive ? "g" : "gi";
|
|
27038
|
+
try {
|
|
27039
|
+
re = new RegExp(source, flags);
|
|
27040
|
+
} catch (e) {
|
|
27041
|
+
return err("invalid_pattern", `Not a valid regular expression: ${e.message}`, "Escape backslashes \u2014 a JSON string needs \\\\d for \\d.");
|
|
27042
|
+
}
|
|
27043
|
+
const groupCount = new RegExp(`${source}|`).exec("").length - 1;
|
|
26572
27044
|
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"];
|
|
27045
|
+
const clauses = fast ? [`note.content %= '${escapeQueryRegex(pattern)}'`] : ["#noteType"];
|
|
26577
27046
|
if (domain)
|
|
26578
27047
|
clauses.push(`#domain='${slugify(domain)}'`);
|
|
26579
27048
|
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
27049
|
const scoped = notes.filter(inScope).slice(0, max);
|
|
26581
|
-
const
|
|
26582
|
-
const
|
|
27050
|
+
const byValue = new Map;
|
|
27051
|
+
const noCapture = [];
|
|
27052
|
+
let hasCaptureGroup = false;
|
|
27053
|
+
const scan = (haystack) => {
|
|
27054
|
+
const values = [];
|
|
27055
|
+
let matched = false;
|
|
27056
|
+
re.lastIndex = 0;
|
|
27057
|
+
let m;
|
|
27058
|
+
while ((m = re.exec(haystack)) !== null) {
|
|
27059
|
+
matched = true;
|
|
27060
|
+
const captured = groupCount === 0 ? m[0] : m.slice(1).find((g) => g !== undefined);
|
|
27061
|
+
if (captured !== undefined && captured !== "")
|
|
27062
|
+
values.push(toText(captured, 120).trim() || captured.trim());
|
|
27063
|
+
if (m[0] === "")
|
|
27064
|
+
re.lastIndex++;
|
|
27065
|
+
}
|
|
27066
|
+
return { values, matched };
|
|
27067
|
+
};
|
|
27068
|
+
let tagSpanning = 0;
|
|
27069
|
+
const idleSince = (iso) => Math.max(0, Math.floor((Date.now() - new Date(iso.replace(" ", "T")).getTime()) / 86400000));
|
|
26583
27070
|
for (const n of scoped) {
|
|
26584
|
-
const content = await
|
|
27071
|
+
const content = await contentOf(n.noteId);
|
|
26585
27072
|
if (!content)
|
|
26586
27073
|
continue;
|
|
26587
|
-
const
|
|
26588
|
-
const
|
|
26589
|
-
|
|
26590
|
-
|
|
26591
|
-
|
|
27074
|
+
const stub = { id: n.noteId, title: n.title, kind: ownedLabel(n, "noteType") ?? "", idleDays: idleSince(n.dateModified) };
|
|
27075
|
+
const raw = scan(content);
|
|
27076
|
+
const projected = scan(stripTagsWithMap(content).text);
|
|
27077
|
+
if (projected.matched && !raw.matched)
|
|
27078
|
+
tagSpanning++;
|
|
27079
|
+
const seen = new Set;
|
|
27080
|
+
for (const value of [...raw.values, ...projected.values]) {
|
|
27081
|
+
hasCaptureGroup = true;
|
|
27082
|
+
if (seen.has(value))
|
|
27083
|
+
continue;
|
|
27084
|
+
seen.add(value);
|
|
27085
|
+
if (!byValue.has(value))
|
|
27086
|
+
byValue.set(value, []);
|
|
27087
|
+
byValue.get(value).push(stub);
|
|
27088
|
+
}
|
|
27089
|
+
if ((raw.matched || projected.matched) && !seen.size)
|
|
27090
|
+
noCapture.push(stub);
|
|
26592
27091
|
}
|
|
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);
|
|
27092
|
+
if (!hasCaptureGroup) {
|
|
27093
|
+
return txt3({
|
|
27094
|
+
mode: "presence",
|
|
27095
|
+
pattern,
|
|
27096
|
+
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
27097
|
+
notesExamined: scoped.length,
|
|
27098
|
+
...recordsNote(),
|
|
27099
|
+
notes: noCapture,
|
|
27100
|
+
total: noCapture.length,
|
|
27101
|
+
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.'
|
|
27102
|
+
});
|
|
26658
27103
|
}
|
|
26659
|
-
|
|
26660
|
-
|
|
26661
|
-
|
|
26662
|
-
if (!hasCaptureGroup) {
|
|
27104
|
+
const groups = [...byValue.entries()].map(([value, notes]) => ({ value, count: notes.length, notes })).sort((a, b) => b.count - a.count);
|
|
27105
|
+
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 })) : [];
|
|
27106
|
+
const agrees = groups.length <= 1;
|
|
26663
27107
|
return txt3({
|
|
26664
|
-
mode: "
|
|
27108
|
+
mode: "consistency",
|
|
26665
27109
|
pattern,
|
|
27110
|
+
...groupCount === 0 ? { valueFrom: "the whole match (the pattern has no capture group)" } : {},
|
|
27111
|
+
...caseSensitive && !inlineInsensitive ? { caseSensitive: true } : {},
|
|
26666
27112
|
scan: fast ? "fast (%= pre-filter)" : "exhaustive",
|
|
27113
|
+
...domain ? { domain: slugify(domain) } : {},
|
|
26667
27114
|
notesExamined: scoped.length,
|
|
26668
27115
|
...recordsNote(),
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
27116
|
+
...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.` } : {},
|
|
27117
|
+
distinctValues: groups.length,
|
|
27118
|
+
agreement: groups.length === 0 ? "no-data" : agrees ? "unanimous" : "DISAGREEMENT",
|
|
27119
|
+
groups,
|
|
27120
|
+
...staleAfterDays ? { staleAfterDays } : {},
|
|
27121
|
+
...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.` } : {},
|
|
27122
|
+
...noCapture.length ? { matchedWithoutValue: noCapture } : {},
|
|
27123
|
+
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
27124
|
});
|
|
26673
27125
|
}
|
|
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
27126
|
});
|
|
26694
27127
|
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
27128
|
noteId: stringType().describe("Note to outline")
|
|
@@ -26738,7 +27171,7 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26738
27171
|
...headings.length === 0 ? { note: "No h2\u2013h4 headings \u2014 section= would append a new section rather than match anything." } : {}
|
|
26739
27172
|
});
|
|
26740
27173
|
});
|
|
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.`, {
|
|
27174
|
+
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
27175
|
noteId: stringType().describe("Note to inspect"),
|
|
26743
27176
|
content: booleanType().optional().describe("Include the note's raw body content (default: false)"),
|
|
26744
27177
|
section: stringType().optional().describe("With content=true, return only this heading's section rather than the whole body"),
|
|
@@ -26811,11 +27244,39 @@ calling twice is safe. Use remove=true to delete an edge.`, {
|
|
|
26811
27244
|
}
|
|
26812
27245
|
const total = countIn(rawBody);
|
|
26813
27246
|
const near = total === 0 ? nearestContext(rawBody, find) : null;
|
|
27247
|
+
const matches = [];
|
|
27248
|
+
const seenSpans = new Set;
|
|
27249
|
+
for (let at = rawBody.indexOf(find);at !== -1 && matches.length < 10; at = rawBody.indexOf(find, at + Math.max(1, find.length))) {
|
|
27250
|
+
let el = null;
|
|
27251
|
+
let tag = "";
|
|
27252
|
+
for (const t of ["tr", "li", "p", "blockquote", "td"]) {
|
|
27253
|
+
el = containingElement(rawBody, t, at, at + find.length);
|
|
27254
|
+
if (el) {
|
|
27255
|
+
tag = t;
|
|
27256
|
+
break;
|
|
27257
|
+
}
|
|
27258
|
+
}
|
|
27259
|
+
const start = el?.start ?? Math.max(0, at - 120);
|
|
27260
|
+
const end = el?.end ?? Math.min(rawBody.length, at + find.length + 120);
|
|
27261
|
+
if (seenSpans.has(start))
|
|
27262
|
+
continue;
|
|
27263
|
+
seenSpans.add(start);
|
|
27264
|
+
let sectionName = "(head)";
|
|
27265
|
+
for (const h of headings) {
|
|
27266
|
+
if (h.index < at)
|
|
27267
|
+
sectionName = h.text;
|
|
27268
|
+
else
|
|
27269
|
+
break;
|
|
27270
|
+
}
|
|
27271
|
+
const stored = rawBody.slice(start, end);
|
|
27272
|
+
matches.push({ section: sectionName, element: tag || "text", stored: stored.length > 600 ? `${stored.slice(0, 600)}\u2026` : stored });
|
|
27273
|
+
}
|
|
26814
27274
|
findReport = {
|
|
26815
27275
|
find,
|
|
26816
27276
|
total,
|
|
26817
27277
|
blocks,
|
|
26818
27278
|
...sections.length ? { sections } : {},
|
|
27279
|
+
...matches.length ? { matches } : {},
|
|
26819
27280
|
...near ? { matchedUpTo: near.fragment, storedNearby: near.context } : {},
|
|
26820
27281
|
...total === 0 && !near ? { hint: "Not present, and no fragment of it is either \u2014 the string is unrelated to this note's content." } : {}
|
|
26821
27282
|
};
|
|
@@ -27166,7 +27627,7 @@ assertion + check \u2192 register (deduped by assertion); claimId + holds + evid
|
|
|
27166
27627
|
...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
27628
|
});
|
|
27168
27629
|
});
|
|
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.`, {
|
|
27630
|
+
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
27631
|
deep: booleanType().optional().describe("Deep pass: stale-review + orphan/sink + structural lint + duplicate titles across Memory/Threads and Knowledge (default: false)"),
|
|
27171
27632
|
dryRun: booleanType().optional().describe("Report what would change without changing it"),
|
|
27172
27633
|
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 +27641,15 @@ assertion + check \u2192 register (deduped by assertion); claimId + holds + evid
|
|
|
27180
27641
|
...ack?.length ? { ack } : {},
|
|
27181
27642
|
...repair?.length ? { repair } : {}
|
|
27182
27643
|
});
|
|
27644
|
+
if (deep && !domain) {
|
|
27645
|
+
const seals = await verifySeals(trilium, b(), localToday()).catch(() => null);
|
|
27646
|
+
if (seals) {
|
|
27647
|
+
for (const f of seals.findings) {
|
|
27648
|
+
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.`);
|
|
27649
|
+
}
|
|
27650
|
+
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";
|
|
27651
|
+
}
|
|
27652
|
+
}
|
|
27183
27653
|
if (deep && !dryRun && b().sizes && Object.keys(b().sizes).length) {
|
|
27184
27654
|
saveConfig(b());
|
|
27185
27655
|
}
|
|
@@ -27743,6 +28213,37 @@ and every thread day-child, in time order with its identification line.`, {
|
|
|
27743
28213
|
registerMemoryTools(server, trilium, brainRef);
|
|
27744
28214
|
registerKnowledgeTools(server, trilium, brainRef);
|
|
27745
28215
|
registerInsightsTools(server, trilium, brainRef);
|
|
28216
|
+
const registry = server._registeredTools ?? {};
|
|
28217
|
+
for (const name of CONTENT_WRITE_TOOLS) {
|
|
28218
|
+
const tool = registry[name];
|
|
28219
|
+
const inner = tool?.handler;
|
|
28220
|
+
if (!tool || typeof inner !== "function")
|
|
28221
|
+
continue;
|
|
28222
|
+
tool.handler = async (...args) => {
|
|
28223
|
+
const result = await inner(...args);
|
|
28224
|
+
if (!wroteSinceClose && wroteContent(result)) {
|
|
28225
|
+
await markStep("write").catch(() => null);
|
|
28226
|
+
wroteSinceClose = true;
|
|
28227
|
+
}
|
|
28228
|
+
return result;
|
|
28229
|
+
};
|
|
28230
|
+
}
|
|
28231
|
+
}
|
|
28232
|
+
var CONTENT_WRITE_TOOLS = ["remember", "revise", "split", "resolve", "withdraw", "recover", "forget", "label", "connect", "attach", "detach", "claim"];
|
|
28233
|
+
function wroteContent(result) {
|
|
28234
|
+
const text = result?.content?.[0]?.text;
|
|
28235
|
+
if (typeof text !== "string")
|
|
28236
|
+
return true;
|
|
28237
|
+
try {
|
|
28238
|
+
const r = JSON.parse(text);
|
|
28239
|
+
if (r.error || r.ok === false || r.unchanged === true || r.action === "already_written" || r.mode === "already_written")
|
|
28240
|
+
return false;
|
|
28241
|
+
if (r.replaced === 0 && !r.closure)
|
|
28242
|
+
return false;
|
|
28243
|
+
return true;
|
|
28244
|
+
} catch {
|
|
28245
|
+
return true;
|
|
28246
|
+
}
|
|
27746
28247
|
}
|
|
27747
28248
|
|
|
27748
28249
|
// src/annotations.ts
|
|
@@ -28083,10 +28584,24 @@ var asArray = (v) => {
|
|
|
28083
28584
|
return v;
|
|
28084
28585
|
}
|
|
28085
28586
|
};
|
|
28587
|
+
var asObject = (v) => {
|
|
28588
|
+
if (typeof v !== "string")
|
|
28589
|
+
return v;
|
|
28590
|
+
const t = v.trim();
|
|
28591
|
+
if (!t.startsWith("{"))
|
|
28592
|
+
return v;
|
|
28593
|
+
try {
|
|
28594
|
+
const parsed = JSON.parse(t);
|
|
28595
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : v;
|
|
28596
|
+
} catch {
|
|
28597
|
+
return v;
|
|
28598
|
+
}
|
|
28599
|
+
};
|
|
28086
28600
|
var COERCERS = {
|
|
28087
28601
|
ZodBoolean: asBoolean,
|
|
28088
28602
|
ZodNumber: asNumber,
|
|
28089
|
-
ZodArray: asArray
|
|
28603
|
+
ZodArray: asArray,
|
|
28604
|
+
ZodObject: asObject
|
|
28090
28605
|
};
|
|
28091
28606
|
function coerceToolArgs(server) {
|
|
28092
28607
|
const registry = server._registeredTools;
|
|
@@ -28613,7 +29128,7 @@ data: ${JSON.stringify(message)}
|
|
|
28613
29128
|
}
|
|
28614
29129
|
|
|
28615
29130
|
// src/oauth.ts
|
|
28616
|
-
import { createHmac, randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
29131
|
+
import { createHmac, randomBytes, createHash as createHash2, timingSafeEqual } from "crypto";
|
|
28617
29132
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
28618
29133
|
|
|
28619
29134
|
// src/network-security.ts
|
|
@@ -29537,7 +30052,7 @@ var tokenError = (error, description, status = 400) => new Response(JSON.stringi
|
|
|
29537
30052
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
29538
30053
|
});
|
|
29539
30054
|
function refreshKey(token) {
|
|
29540
|
-
return "sha256:" +
|
|
30055
|
+
return "sha256:" + createHash2("sha256").update(token).digest("hex");
|
|
29541
30056
|
}
|
|
29542
30057
|
function issueTokens(base, clientId, resource, scope) {
|
|
29543
30058
|
const store = loadStore();
|
|
@@ -29583,7 +30098,7 @@ async function handleToken(req, base) {
|
|
|
29583
30098
|
return tokenError("invalid_grant", "redirect_uri mismatch.");
|
|
29584
30099
|
if (!verifier)
|
|
29585
30100
|
return tokenError("invalid_request", "code_verifier is required.");
|
|
29586
|
-
const computed =
|
|
30101
|
+
const computed = createHash2("sha256").update(verifier).digest("base64url");
|
|
29587
30102
|
if (computed !== record.codeChallenge)
|
|
29588
30103
|
return tokenError("invalid_grant", "PKCE verification failed.");
|
|
29589
30104
|
return new Response(JSON.stringify(issueTokens(base, record.clientId, record.resource, record.scope)), {
|
|
@@ -29690,7 +30205,7 @@ function createServer(origin = null) {
|
|
|
29690
30205
|
const s = new McpServer({
|
|
29691
30206
|
name: BRAND2.name,
|
|
29692
30207
|
title: BRAND2.name,
|
|
29693
|
-
version: "12.
|
|
30208
|
+
version: "12.9.0",
|
|
29694
30209
|
description: BRAND2.description,
|
|
29695
30210
|
websiteUrl: BRAND2.website,
|
|
29696
30211
|
icons: brandIcons(origin)
|