apple-notes-mcp 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -4
- package/build/index.js +1038 -78
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -417,11 +417,11 @@ var require_codegen = __commonJS({
|
|
|
417
417
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
418
418
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
419
419
|
}
|
|
420
|
-
optimizeNames(names,
|
|
420
|
+
optimizeNames(names, constants10) {
|
|
421
421
|
if (!names[this.name.str])
|
|
422
422
|
return;
|
|
423
423
|
if (this.rhs)
|
|
424
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
424
|
+
this.rhs = optimizeExpr(this.rhs, names, constants10);
|
|
425
425
|
return this;
|
|
426
426
|
}
|
|
427
427
|
get names() {
|
|
@@ -438,10 +438,10 @@ var require_codegen = __commonJS({
|
|
|
438
438
|
render({ _n }) {
|
|
439
439
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
440
440
|
}
|
|
441
|
-
optimizeNames(names,
|
|
441
|
+
optimizeNames(names, constants10) {
|
|
442
442
|
if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
|
|
443
443
|
return;
|
|
444
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
444
|
+
this.rhs = optimizeExpr(this.rhs, names, constants10);
|
|
445
445
|
return this;
|
|
446
446
|
}
|
|
447
447
|
get names() {
|
|
@@ -502,8 +502,8 @@ var require_codegen = __commonJS({
|
|
|
502
502
|
optimizeNodes() {
|
|
503
503
|
return `${this.code}` ? this : void 0;
|
|
504
504
|
}
|
|
505
|
-
optimizeNames(names,
|
|
506
|
-
this.code = optimizeExpr(this.code, names,
|
|
505
|
+
optimizeNames(names, constants10) {
|
|
506
|
+
this.code = optimizeExpr(this.code, names, constants10);
|
|
507
507
|
return this;
|
|
508
508
|
}
|
|
509
509
|
get names() {
|
|
@@ -532,12 +532,12 @@ var require_codegen = __commonJS({
|
|
|
532
532
|
}
|
|
533
533
|
return nodes.length > 0 ? this : void 0;
|
|
534
534
|
}
|
|
535
|
-
optimizeNames(names,
|
|
535
|
+
optimizeNames(names, constants10) {
|
|
536
536
|
const { nodes } = this;
|
|
537
537
|
let i = nodes.length;
|
|
538
538
|
while (i--) {
|
|
539
539
|
const n = nodes[i];
|
|
540
|
-
if (n.optimizeNames(names,
|
|
540
|
+
if (n.optimizeNames(names, constants10))
|
|
541
541
|
continue;
|
|
542
542
|
subtractNames(names, n.names);
|
|
543
543
|
nodes.splice(i, 1);
|
|
@@ -590,12 +590,12 @@ var require_codegen = __commonJS({
|
|
|
590
590
|
return void 0;
|
|
591
591
|
return this;
|
|
592
592
|
}
|
|
593
|
-
optimizeNames(names,
|
|
593
|
+
optimizeNames(names, constants10) {
|
|
594
594
|
var _a;
|
|
595
|
-
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
596
|
-
if (!(super.optimizeNames(names,
|
|
595
|
+
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants10);
|
|
596
|
+
if (!(super.optimizeNames(names, constants10) || this.else))
|
|
597
597
|
return;
|
|
598
|
-
this.condition = optimizeExpr(this.condition, names,
|
|
598
|
+
this.condition = optimizeExpr(this.condition, names, constants10);
|
|
599
599
|
return this;
|
|
600
600
|
}
|
|
601
601
|
get names() {
|
|
@@ -618,10 +618,10 @@ var require_codegen = __commonJS({
|
|
|
618
618
|
render(opts) {
|
|
619
619
|
return `for(${this.iteration})` + super.render(opts);
|
|
620
620
|
}
|
|
621
|
-
optimizeNames(names,
|
|
622
|
-
if (!super.optimizeNames(names,
|
|
621
|
+
optimizeNames(names, constants10) {
|
|
622
|
+
if (!super.optimizeNames(names, constants10))
|
|
623
623
|
return;
|
|
624
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
624
|
+
this.iteration = optimizeExpr(this.iteration, names, constants10);
|
|
625
625
|
return this;
|
|
626
626
|
}
|
|
627
627
|
get names() {
|
|
@@ -657,10 +657,10 @@ var require_codegen = __commonJS({
|
|
|
657
657
|
render(opts) {
|
|
658
658
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
659
659
|
}
|
|
660
|
-
optimizeNames(names,
|
|
661
|
-
if (!super.optimizeNames(names,
|
|
660
|
+
optimizeNames(names, constants10) {
|
|
661
|
+
if (!super.optimizeNames(names, constants10))
|
|
662
662
|
return;
|
|
663
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
663
|
+
this.iterable = optimizeExpr(this.iterable, names, constants10);
|
|
664
664
|
return this;
|
|
665
665
|
}
|
|
666
666
|
get names() {
|
|
@@ -702,11 +702,11 @@ var require_codegen = __commonJS({
|
|
|
702
702
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
703
703
|
return this;
|
|
704
704
|
}
|
|
705
|
-
optimizeNames(names,
|
|
705
|
+
optimizeNames(names, constants10) {
|
|
706
706
|
var _a, _b;
|
|
707
|
-
super.optimizeNames(names,
|
|
708
|
-
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
709
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names,
|
|
707
|
+
super.optimizeNames(names, constants10);
|
|
708
|
+
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants10);
|
|
709
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants10);
|
|
710
710
|
return this;
|
|
711
711
|
}
|
|
712
712
|
get names() {
|
|
@@ -1007,7 +1007,7 @@ var require_codegen = __commonJS({
|
|
|
1007
1007
|
function addExprNames(names, from) {
|
|
1008
1008
|
return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
|
|
1009
1009
|
}
|
|
1010
|
-
function optimizeExpr(expr, names,
|
|
1010
|
+
function optimizeExpr(expr, names, constants10) {
|
|
1011
1011
|
if (expr instanceof code_1.Name)
|
|
1012
1012
|
return replaceName(expr);
|
|
1013
1013
|
if (!canOptimize(expr))
|
|
@@ -1022,14 +1022,14 @@ var require_codegen = __commonJS({
|
|
|
1022
1022
|
return items;
|
|
1023
1023
|
}, []));
|
|
1024
1024
|
function replaceName(n) {
|
|
1025
|
-
const c =
|
|
1025
|
+
const c = constants10[n.str];
|
|
1026
1026
|
if (c === void 0 || names[n.str] !== 1)
|
|
1027
1027
|
return n;
|
|
1028
1028
|
delete names[n.str];
|
|
1029
1029
|
return c;
|
|
1030
1030
|
}
|
|
1031
1031
|
function canOptimize(e) {
|
|
1032
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 &&
|
|
1032
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants10[c.str] !== void 0);
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
1035
|
function subtractNames(names, from) {
|
|
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
|
|
|
2991
2991
|
const schOrFunc = root.refs[ref];
|
|
2992
2992
|
if (schOrFunc)
|
|
2993
2993
|
return schOrFunc;
|
|
2994
|
-
let _sch =
|
|
2994
|
+
let _sch = resolve10.call(this, root, ref);
|
|
2995
2995
|
if (_sch === void 0) {
|
|
2996
2996
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2997
2997
|
const { schemaId } = this.opts;
|
|
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
|
|
|
3018
3018
|
function sameSchemaEnv(s1, s2) {
|
|
3019
3019
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3020
3020
|
}
|
|
3021
|
-
function
|
|
3021
|
+
function resolve10(root, ref) {
|
|
3022
3022
|
let sch;
|
|
3023
3023
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3024
3024
|
ref = sch;
|
|
@@ -3848,7 +3848,7 @@ var require_fast_uri = __commonJS({
|
|
|
3848
3848
|
}
|
|
3849
3849
|
return uri;
|
|
3850
3850
|
}
|
|
3851
|
-
function
|
|
3851
|
+
function resolve10(baseURI, relativeURI, options) {
|
|
3852
3852
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3853
3853
|
const {
|
|
3854
3854
|
parsed: baseParsed,
|
|
@@ -4217,7 +4217,7 @@ var require_fast_uri = __commonJS({
|
|
|
4217
4217
|
var fastUri = {
|
|
4218
4218
|
SCHEMES,
|
|
4219
4219
|
normalize,
|
|
4220
|
-
resolve:
|
|
4220
|
+
resolve: resolve10,
|
|
4221
4221
|
resolveComponent,
|
|
4222
4222
|
equal,
|
|
4223
4223
|
serialize,
|
|
@@ -7434,7 +7434,7 @@ var require_DOMException = __commonJS({
|
|
|
7434
7434
|
"INVALID_NODE_TYPE_ERR (24): the supplied node is invalid or has an invalid ancestor for this operation",
|
|
7435
7435
|
"DATA_CLONE_ERR (25): the object can not be cloned."
|
|
7436
7436
|
];
|
|
7437
|
-
var
|
|
7437
|
+
var constants10 = {
|
|
7438
7438
|
INDEX_SIZE_ERR,
|
|
7439
7439
|
DOMSTRING_SIZE_ERR: 2,
|
|
7440
7440
|
// historical
|
|
@@ -7473,8 +7473,8 @@ var require_DOMException = __commonJS({
|
|
|
7473
7473
|
this.name = names[code];
|
|
7474
7474
|
}
|
|
7475
7475
|
DOMException.prototype.__proto__ = Error.prototype;
|
|
7476
|
-
for (c in
|
|
7477
|
-
v = { value:
|
|
7476
|
+
for (c in constants10) {
|
|
7477
|
+
v = { value: constants10[c] };
|
|
7478
7478
|
Object.defineProperty(DOMException, c, v);
|
|
7479
7479
|
Object.defineProperty(DOMException.prototype, c, v);
|
|
7480
7480
|
}
|
|
@@ -24477,14 +24477,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
24477
24477
|
} else if (node.nodeType === 1) {
|
|
24478
24478
|
replacement = replacementForNode.call(self, node);
|
|
24479
24479
|
}
|
|
24480
|
-
return
|
|
24480
|
+
return join33(output, replacement);
|
|
24481
24481
|
}, "");
|
|
24482
24482
|
}
|
|
24483
24483
|
function postProcess(output) {
|
|
24484
24484
|
var self = this;
|
|
24485
24485
|
this.rules.forEach(function(rule) {
|
|
24486
24486
|
if (typeof rule.append === "function") {
|
|
24487
|
-
output =
|
|
24487
|
+
output = join33(output, rule.append(self.options));
|
|
24488
24488
|
}
|
|
24489
24489
|
});
|
|
24490
24490
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -24496,7 +24496,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
24496
24496
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
24497
24497
|
return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
|
|
24498
24498
|
}
|
|
24499
|
-
function
|
|
24499
|
+
function join33(output, replacement) {
|
|
24500
24500
|
var s1 = trimTrailingNewlines(output);
|
|
24501
24501
|
var s2 = trimLeadingNewlines(replacement);
|
|
24502
24502
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -36620,7 +36620,7 @@ var Protocol = class {
|
|
|
36620
36620
|
return;
|
|
36621
36621
|
}
|
|
36622
36622
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
36623
|
-
await new Promise((
|
|
36623
|
+
await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
|
|
36624
36624
|
options?.signal?.throwIfAborted();
|
|
36625
36625
|
}
|
|
36626
36626
|
} catch (error2) {
|
|
@@ -36637,7 +36637,7 @@ var Protocol = class {
|
|
|
36637
36637
|
*/
|
|
36638
36638
|
request(request, resultSchema, options) {
|
|
36639
36639
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
36640
|
-
return new Promise((
|
|
36640
|
+
return new Promise((resolve10, reject) => {
|
|
36641
36641
|
const earlyReject = (error2) => {
|
|
36642
36642
|
reject(error2);
|
|
36643
36643
|
};
|
|
@@ -36715,7 +36715,7 @@ var Protocol = class {
|
|
|
36715
36715
|
if (!parseResult.success) {
|
|
36716
36716
|
reject(parseResult.error);
|
|
36717
36717
|
} else {
|
|
36718
|
-
|
|
36718
|
+
resolve10(parseResult.data);
|
|
36719
36719
|
}
|
|
36720
36720
|
} catch (error2) {
|
|
36721
36721
|
reject(error2);
|
|
@@ -36976,12 +36976,12 @@ var Protocol = class {
|
|
|
36976
36976
|
}
|
|
36977
36977
|
} catch {
|
|
36978
36978
|
}
|
|
36979
|
-
return new Promise((
|
|
36979
|
+
return new Promise((resolve10, reject) => {
|
|
36980
36980
|
if (signal.aborted) {
|
|
36981
36981
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
36982
36982
|
return;
|
|
36983
36983
|
}
|
|
36984
|
-
const timeoutId = setTimeout(
|
|
36984
|
+
const timeoutId = setTimeout(resolve10, interval);
|
|
36985
36985
|
signal.addEventListener("abort", () => {
|
|
36986
36986
|
clearTimeout(timeoutId);
|
|
36987
36987
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -38294,7 +38294,7 @@ var McpServer = class {
|
|
|
38294
38294
|
let task = createTaskResult.task;
|
|
38295
38295
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
38296
38296
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
38297
|
-
await new Promise((
|
|
38297
|
+
await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
|
|
38298
38298
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
38299
38299
|
if (!updatedTask) {
|
|
38300
38300
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -38982,12 +38982,12 @@ var StdioServerTransport = class {
|
|
|
38982
38982
|
this.onclose?.();
|
|
38983
38983
|
}
|
|
38984
38984
|
send(message) {
|
|
38985
|
-
return new Promise((
|
|
38985
|
+
return new Promise((resolve10) => {
|
|
38986
38986
|
const json2 = serializeMessage(message);
|
|
38987
38987
|
if (this._stdout.write(json2)) {
|
|
38988
|
-
|
|
38988
|
+
resolve10();
|
|
38989
38989
|
} else {
|
|
38990
|
-
this._stdout.once("drain",
|
|
38990
|
+
this._stdout.once("drain", resolve10);
|
|
38991
38991
|
}
|
|
38992
38992
|
});
|
|
38993
38993
|
}
|
|
@@ -47866,13 +47866,13 @@ var escapeSegment = escapeFolderName;
|
|
|
47866
47866
|
function resolveFolders(rows) {
|
|
47867
47867
|
const byPk = new Map(rows.map((row) => [row.pk, row]));
|
|
47868
47868
|
const resolved = /* @__PURE__ */ new Map();
|
|
47869
|
-
const
|
|
47869
|
+
const resolve10 = (pk, seen) => {
|
|
47870
47870
|
const cached2 = resolved.get(pk);
|
|
47871
47871
|
if (cached2) return cached2;
|
|
47872
47872
|
const row = byPk.get(pk);
|
|
47873
47873
|
if (!row || seen.has(pk)) return void 0;
|
|
47874
47874
|
seen.add(pk);
|
|
47875
|
-
const parent = row.parent !== null ?
|
|
47875
|
+
const parent = row.parent !== null ? resolve10(row.parent, seen) : void 0;
|
|
47876
47876
|
const name = row.name ?? "";
|
|
47877
47877
|
const path10 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
|
|
47878
47878
|
const plainPath = parent ? `${parent.plainPath}/${name}` : name;
|
|
@@ -47886,7 +47886,7 @@ function resolveFolders(rows) {
|
|
|
47886
47886
|
resolved.set(pk, info);
|
|
47887
47887
|
return info;
|
|
47888
47888
|
};
|
|
47889
|
-
for (const row of rows)
|
|
47889
|
+
for (const row of rows) resolve10(row.pk, /* @__PURE__ */ new Set());
|
|
47890
47890
|
return resolved;
|
|
47891
47891
|
}
|
|
47892
47892
|
var coreDataMs = (seconds) => seconds === null || !Number.isFinite(seconds) ? void 0 : CORE_DATA_EPOCH_MS2 + seconds * 1e3;
|
|
@@ -52889,16 +52889,16 @@ var NOTE_PLACEHOLDERS = [
|
|
|
52889
52889
|
"exportStem"
|
|
52890
52890
|
];
|
|
52891
52891
|
var PLACEHOLDER_MODIFIERS = ["raw", "yaml"];
|
|
52892
|
-
var wrap2 = (before, after = "",
|
|
52892
|
+
var wrap2 = (before, after = "", join33) => ({
|
|
52893
52893
|
mode: "wrap",
|
|
52894
52894
|
before,
|
|
52895
52895
|
after,
|
|
52896
|
-
...
|
|
52896
|
+
...join33 ? { join: join33 } : {}
|
|
52897
52897
|
});
|
|
52898
|
-
var pattern = (value,
|
|
52898
|
+
var pattern = (value, join33) => ({
|
|
52899
52899
|
mode: "pattern",
|
|
52900
52900
|
value,
|
|
52901
|
-
...
|
|
52901
|
+
...join33 ? { join: join33 } : {}
|
|
52902
52902
|
});
|
|
52903
52903
|
var STANDARD = {
|
|
52904
52904
|
schemaVersion: 1,
|
|
@@ -56826,7 +56826,7 @@ function parseXml(source, limits) {
|
|
|
56826
56826
|
(own ??= /* @__PURE__ */ new Map()).set(prefix, a.value);
|
|
56827
56827
|
}
|
|
56828
56828
|
const scope2 = own ? { own, parent: parentScope } : parentScope ?? { own: null, parent: null };
|
|
56829
|
-
const
|
|
56829
|
+
const resolve10 = (qualified, isAttribute) => {
|
|
56830
56830
|
const colon = qualified.indexOf(":");
|
|
56831
56831
|
if (colon < 0)
|
|
56832
56832
|
return {
|
|
@@ -56840,12 +56840,12 @@ function parseXml(source, limits) {
|
|
|
56840
56840
|
if (ns === void 0) fail(`Undeclared namespace prefix "${prefix.slice(0, 40)}"`);
|
|
56841
56841
|
return { local, ns };
|
|
56842
56842
|
};
|
|
56843
|
-
const resolved =
|
|
56843
|
+
const resolved = resolve10(name, false);
|
|
56844
56844
|
const element = {
|
|
56845
56845
|
name,
|
|
56846
56846
|
local: resolved.local,
|
|
56847
56847
|
ns: resolved.ns,
|
|
56848
|
-
attributes: rawAttributes.filter((a) => a.name !== "xmlns" && !a.name.startsWith("xmlns:")).map((a) => ({ name: a.name, value: a.value, ...
|
|
56848
|
+
attributes: rawAttributes.filter((a) => a.name !== "xmlns" && !a.name.startsWith("xmlns:")).map((a) => ({ name: a.name, value: a.value, ...resolve10(a.name, true) })),
|
|
56849
56849
|
children: [],
|
|
56850
56850
|
text: "",
|
|
56851
56851
|
line: tagLine
|
|
@@ -60797,24 +60797,24 @@ async function startTemplateEditor(options = {}) {
|
|
|
60797
60797
|
socket.on("close", () => sockets.delete(socket));
|
|
60798
60798
|
});
|
|
60799
60799
|
let resolveClosed;
|
|
60800
|
-
const closed = new Promise((
|
|
60800
|
+
const closed = new Promise((resolve10) => resolveClosed = resolve10);
|
|
60801
60801
|
let closing;
|
|
60802
60802
|
const close = () => {
|
|
60803
|
-
closing ??= new Promise((
|
|
60803
|
+
closing ??= new Promise((resolve10) => {
|
|
60804
60804
|
if (idleTimer) clearTimeout(idleTimer);
|
|
60805
60805
|
server2.close(() => {
|
|
60806
60806
|
resolveClosed(closeReason);
|
|
60807
|
-
|
|
60807
|
+
resolve10();
|
|
60808
60808
|
});
|
|
60809
60809
|
for (const socket of sockets) socket.destroy();
|
|
60810
60810
|
});
|
|
60811
60811
|
return closing;
|
|
60812
60812
|
};
|
|
60813
|
-
await new Promise((
|
|
60813
|
+
await new Promise((resolve10, reject) => {
|
|
60814
60814
|
server2.once("error", reject);
|
|
60815
60815
|
server2.listen({ host, port: options.port ?? 0, exclusive: true }, () => {
|
|
60816
60816
|
server2.off("error", reject);
|
|
60817
|
-
|
|
60817
|
+
resolve10();
|
|
60818
60818
|
});
|
|
60819
60819
|
});
|
|
60820
60820
|
const port = server2.address().port;
|
|
@@ -60946,6 +60946,767 @@ ${TEMPLATES_USAGE}`);
|
|
|
60946
60946
|
return 0;
|
|
60947
60947
|
}
|
|
60948
60948
|
|
|
60949
|
+
// src/services/anchorServer.ts
|
|
60950
|
+
import { createServer as createServer2 } from "node:http";
|
|
60951
|
+
|
|
60952
|
+
// src/utils/paragraphAnchors.ts
|
|
60953
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
60954
|
+
var ANCHOR_ID_PATTERN = /^pa_[0-9a-f]{24}$/;
|
|
60955
|
+
var DEFAULT_MIN_CONFIDENCE = 0.6;
|
|
60956
|
+
var textFingerprint = (normalized2) => createHash8("sha256").update(normalized2, "utf8").digest("hex").slice(0, 32);
|
|
60957
|
+
function textSimilarity(a, b) {
|
|
60958
|
+
if (a === b) return 1;
|
|
60959
|
+
if (!a || !b) return 0;
|
|
60960
|
+
const size = Math.min(a.length, b.length) < 3 ? 2 : 3;
|
|
60961
|
+
const grams = (s) => {
|
|
60962
|
+
const out = /* @__PURE__ */ new Map();
|
|
60963
|
+
const padded = ` ${s} `;
|
|
60964
|
+
for (let i = 0; i + size <= padded.length; i++) {
|
|
60965
|
+
const g = padded.slice(i, i + size);
|
|
60966
|
+
out.set(g, (out.get(g) ?? 0) + 1);
|
|
60967
|
+
}
|
|
60968
|
+
return out;
|
|
60969
|
+
};
|
|
60970
|
+
const ga = grams(a);
|
|
60971
|
+
const gb = grams(b);
|
|
60972
|
+
let shared = 0;
|
|
60973
|
+
let total = 0;
|
|
60974
|
+
for (const count of ga.values()) total += count;
|
|
60975
|
+
for (const [g, count] of gb) {
|
|
60976
|
+
total += count;
|
|
60977
|
+
shared += Math.min(count, ga.get(g) ?? 0);
|
|
60978
|
+
}
|
|
60979
|
+
return total ? 2 * shared / total : 0;
|
|
60980
|
+
}
|
|
60981
|
+
var round2 = (value) => Math.round(value * 100) / 100;
|
|
60982
|
+
function anchorFor(note, paragraph, { anchorId, now }) {
|
|
60983
|
+
if (!note.identifier)
|
|
60984
|
+
throw new CodedError("The note has no stored identifier, so no anchor can be recorded", {
|
|
60985
|
+
code: "unsupported"
|
|
60986
|
+
});
|
|
60987
|
+
const i = note.paragraphs.indexOf(paragraph);
|
|
60988
|
+
if (i < 0) throw new Error("The paragraph does not belong to this note");
|
|
60989
|
+
const fp = (p) => p ? textFingerprint(normalizeParagraphText(p.text)) : null;
|
|
60990
|
+
const text2 = normalizeParagraphText(paragraph.text);
|
|
60991
|
+
return {
|
|
60992
|
+
anchorId,
|
|
60993
|
+
noteIdentifier: note.identifier.toUpperCase(),
|
|
60994
|
+
noteId: note.id,
|
|
60995
|
+
paragraphId: paragraph.paragraphId,
|
|
60996
|
+
paragraphIdStatus: paragraph.paragraphIdStatus,
|
|
60997
|
+
text: text2,
|
|
60998
|
+
fingerprint: textFingerprint(text2),
|
|
60999
|
+
prevFingerprint: fp(note.paragraphs[i - 1]),
|
|
61000
|
+
nextFingerprint: fp(note.paragraphs[i + 1]),
|
|
61001
|
+
blockIndex: paragraph.blockIndex,
|
|
61002
|
+
style: paragraph.style,
|
|
61003
|
+
createdAt: now.toISOString()
|
|
61004
|
+
};
|
|
61005
|
+
}
|
|
61006
|
+
function matchAnchor(anchor, paragraphs, { minConfidence = DEFAULT_MIN_CONFIDENCE } = {}) {
|
|
61007
|
+
const result = findAnchor(anchor, paragraphs);
|
|
61008
|
+
return result.status === "matched" && result.confidence < minConfidence ? { ...result, status: "low-confidence" } : result;
|
|
61009
|
+
}
|
|
61010
|
+
function findAnchor(anchor, paragraphs) {
|
|
61011
|
+
const normalized2 = paragraphs.map((p) => normalizeParagraphText(p.text));
|
|
61012
|
+
const fps = normalized2.map(textFingerprint);
|
|
61013
|
+
const neighbours = (i) => Number((fps[i - 1] ?? null) === anchor.prevFingerprint) + Number((fps[i + 1] ?? null) === anchor.nextFingerprint);
|
|
61014
|
+
const all = paragraphs.map((_, i) => i);
|
|
61015
|
+
const byNeighbours = (candidates) => {
|
|
61016
|
+
const scored2 = candidates.map((i) => ({ i, n: neighbours(i) }));
|
|
61017
|
+
const best = Math.max(...scored2.map((s) => s.n));
|
|
61018
|
+
const top = scored2.filter((s) => s.n === best);
|
|
61019
|
+
return best > 0 && top.length === 1 ? top[0] : void 0;
|
|
61020
|
+
};
|
|
61021
|
+
if (anchor.paragraphId) {
|
|
61022
|
+
const owners = all.filter((i) => paragraphs[i].paragraphId === anchor.paragraphId);
|
|
61023
|
+
if (owners.length === 1 && paragraphs[owners[0]].paragraphIdStatus === "unique") {
|
|
61024
|
+
const i = owners[0];
|
|
61025
|
+
const same = fps[i] === anchor.fingerprint;
|
|
61026
|
+
const confidence = same ? 1 : 0.8 + 0.15 * Math.max(textSimilarity(anchor.text, normalized2[i]), neighbours(i) / 2);
|
|
61027
|
+
return { status: "matched", index: i, method: "paragraph-id", confidence: round2(confidence) };
|
|
61028
|
+
}
|
|
61029
|
+
const exact2 = owners.filter((i) => fps[i] === anchor.fingerprint);
|
|
61030
|
+
if (exact2.length === 1)
|
|
61031
|
+
return { status: "matched", index: exact2[0], method: "paragraph-id", confidence: 0.95 };
|
|
61032
|
+
if (exact2.length > 1) {
|
|
61033
|
+
const pick2 = byNeighbours(exact2);
|
|
61034
|
+
if (pick2)
|
|
61035
|
+
return { status: "matched", index: pick2.i, method: "paragraph-id", confidence: 0.9 };
|
|
61036
|
+
}
|
|
61037
|
+
}
|
|
61038
|
+
const exact = all.filter((i) => fps[i] === anchor.fingerprint);
|
|
61039
|
+
if (exact.length === 1) {
|
|
61040
|
+
const confidence = neighbours(exact[0]) > 0 ? 0.95 : 0.85;
|
|
61041
|
+
return { status: "matched", index: exact[0], method: "exact-text", confidence };
|
|
61042
|
+
}
|
|
61043
|
+
if (exact.length > 1) {
|
|
61044
|
+
const pick2 = byNeighbours(exact);
|
|
61045
|
+
if (pick2) return { status: "matched", index: pick2.i, method: "exact-text", confidence: 0.8 };
|
|
61046
|
+
return { status: "ambiguous", confidence: 0, candidates: exact.length };
|
|
61047
|
+
}
|
|
61048
|
+
const scored = all.map((i) => ({ i, sim: textSimilarity(anchor.text, normalized2[i]), n: neighbours(i) })).filter(({ sim, n }) => n === 2 && sim >= 0.5 || n === 1 && sim >= 0.8).map((s) => ({ ...s, score: 0.4 * s.sim + 0.2 * s.n })).sort((a, b) => b.score - a.score);
|
|
61049
|
+
if (!scored.length) return { status: "not-found", confidence: 0 };
|
|
61050
|
+
if (scored.length > 1 && scored[0].score - scored[1].score < 0.1)
|
|
61051
|
+
return {
|
|
61052
|
+
status: "ambiguous",
|
|
61053
|
+
confidence: 0,
|
|
61054
|
+
candidates: scored.filter((s) => scored[0].score - s.score < 0.1).length
|
|
61055
|
+
};
|
|
61056
|
+
return {
|
|
61057
|
+
status: "matched",
|
|
61058
|
+
index: scored[0].i,
|
|
61059
|
+
method: "text-and-neighbours",
|
|
61060
|
+
confidence: round2(scored[0].score)
|
|
61061
|
+
};
|
|
61062
|
+
}
|
|
61063
|
+
function resolutionFor(anchor, note, options = {}) {
|
|
61064
|
+
const base = {
|
|
61065
|
+
anchorId: anchor.anchorId,
|
|
61066
|
+
noteId: note.id,
|
|
61067
|
+
needsReminting: false,
|
|
61068
|
+
resolved: false
|
|
61069
|
+
};
|
|
61070
|
+
const result = matchAnchor(anchor, note.paragraphs, options);
|
|
61071
|
+
if (result.status === "ambiguous")
|
|
61072
|
+
return {
|
|
61073
|
+
...base,
|
|
61074
|
+
status: "ambiguous",
|
|
61075
|
+
confidence: 0,
|
|
61076
|
+
candidates: result.candidates,
|
|
61077
|
+
message: `${result.candidates} paragraphs match the anchor equally well; refusing to guess`
|
|
61078
|
+
};
|
|
61079
|
+
if (result.status === "not-found")
|
|
61080
|
+
return {
|
|
61081
|
+
...base,
|
|
61082
|
+
status: "not-found",
|
|
61083
|
+
confidence: 0,
|
|
61084
|
+
message: "The anchored paragraph is no longer in the note (deleted, or edited beyond recognition)"
|
|
61085
|
+
};
|
|
61086
|
+
const p = note.paragraphs[result.index];
|
|
61087
|
+
const match = {
|
|
61088
|
+
blockIndex: p.blockIndex,
|
|
61089
|
+
text: p.text,
|
|
61090
|
+
paragraphId: p.paragraphId,
|
|
61091
|
+
paragraphIdStatus: p.paragraphIdStatus,
|
|
61092
|
+
...p.sharedWith !== void 0 ? { sharedWith: p.sharedWith } : {}
|
|
61093
|
+
};
|
|
61094
|
+
const changes = {
|
|
61095
|
+
textChanged: textFingerprint(normalizeParagraphText(p.text)) !== anchor.fingerprint,
|
|
61096
|
+
blockIndexChanged: p.blockIndex !== anchor.blockIndex,
|
|
61097
|
+
paragraphIdChanged: p.paragraphId !== anchor.paragraphId
|
|
61098
|
+
};
|
|
61099
|
+
const found = { ...base, method: result.method, confidence: result.confidence, match, changes };
|
|
61100
|
+
if (result.status === "low-confidence")
|
|
61101
|
+
return {
|
|
61102
|
+
...found,
|
|
61103
|
+
status: "low-confidence",
|
|
61104
|
+
message: `The best candidate scored ${result.confidence}, below the minimum; refusing to link it`
|
|
61105
|
+
};
|
|
61106
|
+
if (p.paragraphIdStatus === "unique" && note.identifier)
|
|
61107
|
+
return {
|
|
61108
|
+
...found,
|
|
61109
|
+
status: "resolved",
|
|
61110
|
+
resolved: true,
|
|
61111
|
+
url: paragraphUrl(note.identifier, p.paragraphId),
|
|
61112
|
+
message: `Resolved by ${result.method} (confidence ${result.confidence})`
|
|
61113
|
+
};
|
|
61114
|
+
return {
|
|
61115
|
+
...found,
|
|
61116
|
+
status: "needs-reminting",
|
|
61117
|
+
needsReminting: true,
|
|
61118
|
+
message: p.paragraphIdStatus === "shared" ? `Found the paragraph by ${result.method}, but its ID is shared with ${p.sharedWith} other paragraph(s); it needs a new paragraph ID before it can be linked` : `Found the paragraph by ${result.method}, but it has no paragraph ID; it needs one before it can be linked`
|
|
61119
|
+
};
|
|
61120
|
+
}
|
|
61121
|
+
function noteByIdentifierSql(columns) {
|
|
61122
|
+
return `SELECT json_object('pk', n.Z_PK, 'active', CASE WHEN ${activeNoteSql(columns, "n", "f")} THEN 1 ELSE 0 END, 'store', (SELECT Z_UUID FROM Z_METADATA LIMIT 1)) FROM ZICCLOUDSYNCINGOBJECT n LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = n.ZFOLDER WHERE n.Z_ENT = ${entity("ICNote")} AND n.ZIDENTIFIER IN (CAST(@upper AS TEXT), CAST(@lower AS TEXT)) ORDER BY n.Z_PK;`;
|
|
61123
|
+
}
|
|
61124
|
+
function resolveAnchorDetailed(anchor, { dbPath: dbPath2 = NOTES_DB_PATH8, minConfidence } = {}) {
|
|
61125
|
+
const none = { anchorId: anchor.anchorId, resolved: false, needsReminting: false, confidence: 0 };
|
|
61126
|
+
const columns = readColumns(dbPath2);
|
|
61127
|
+
const rows = parseJsonLines(
|
|
61128
|
+
runReadOnlySql(dbPath2, noteByIdentifierSql(columns), {
|
|
61129
|
+
upper: { blob: Buffer.from(anchor.noteIdentifier.toUpperCase(), "utf8") },
|
|
61130
|
+
lower: { blob: Buffer.from(anchor.noteIdentifier.toLowerCase(), "utf8") }
|
|
61131
|
+
})
|
|
61132
|
+
);
|
|
61133
|
+
if (!rows.length || !rows[0].store)
|
|
61134
|
+
return {
|
|
61135
|
+
resolution: {
|
|
61136
|
+
...none,
|
|
61137
|
+
status: "note-not-found",
|
|
61138
|
+
message: `No note with identifier ${anchor.noteIdentifier} is in the Notes database`
|
|
61139
|
+
}
|
|
61140
|
+
};
|
|
61141
|
+
if (rows.length > 1)
|
|
61142
|
+
return {
|
|
61143
|
+
resolution: {
|
|
61144
|
+
...none,
|
|
61145
|
+
status: "ambiguous",
|
|
61146
|
+
candidates: rows.length,
|
|
61147
|
+
message: `${rows.length} notes carry identifier ${anchor.noteIdentifier}; refusing to guess`
|
|
61148
|
+
}
|
|
61149
|
+
};
|
|
61150
|
+
const noteId3 = noteIdFor(rows[0].store, rows[0].pk);
|
|
61151
|
+
if (!rows[0].active)
|
|
61152
|
+
return {
|
|
61153
|
+
resolution: {
|
|
61154
|
+
...none,
|
|
61155
|
+
noteId: noteId3,
|
|
61156
|
+
status: "note-deleted",
|
|
61157
|
+
message: "The note is in Recently Deleted or awaiting deletion"
|
|
61158
|
+
}
|
|
61159
|
+
};
|
|
61160
|
+
let note;
|
|
61161
|
+
try {
|
|
61162
|
+
note = readNoteParagraphs({ id: noteId3 }, { dbPath: dbPath2 });
|
|
61163
|
+
} catch (error2) {
|
|
61164
|
+
if (error2 instanceof ParagraphLinkError)
|
|
61165
|
+
return {
|
|
61166
|
+
resolution: {
|
|
61167
|
+
...none,
|
|
61168
|
+
noteId: noteId3,
|
|
61169
|
+
status: error2.reason === "not-found" ? "note-not-found" : "note-unreadable",
|
|
61170
|
+
message: error2.message
|
|
61171
|
+
}
|
|
61172
|
+
};
|
|
61173
|
+
throw error2;
|
|
61174
|
+
}
|
|
61175
|
+
return { resolution: resolutionFor(anchor, note, { minConfidence }), note };
|
|
61176
|
+
}
|
|
61177
|
+
|
|
61178
|
+
// src/services/anchorServer.ts
|
|
61179
|
+
var MIN_TOKEN_LENGTH = 32;
|
|
61180
|
+
var FAILURE_WINDOW_MS = 6e4;
|
|
61181
|
+
var MAX_TRACKED_PEERS = 256;
|
|
61182
|
+
var STATUS_CODE = {
|
|
61183
|
+
resolved: 302,
|
|
61184
|
+
"needs-reminting": 409,
|
|
61185
|
+
ambiguous: 409,
|
|
61186
|
+
"low-confidence": 409,
|
|
61187
|
+
"not-found": 404,
|
|
61188
|
+
"note-not-found": 404,
|
|
61189
|
+
"note-deleted": 404,
|
|
61190
|
+
"note-unreadable": 409
|
|
61191
|
+
};
|
|
61192
|
+
function createAnchorServer(options) {
|
|
61193
|
+
if (options.token.length < MIN_TOKEN_LENGTH)
|
|
61194
|
+
throw new Error(`The resolver token must be at least ${MIN_TOKEN_LENGTH} characters`);
|
|
61195
|
+
const log = options.log ?? ((line) => process.stderr.write(line + "\n"));
|
|
61196
|
+
const maxFailures = options.maxAuthFailures ?? 20;
|
|
61197
|
+
const failuresByPeer = /* @__PURE__ */ new Map();
|
|
61198
|
+
const recentFailures = (peer, now) => {
|
|
61199
|
+
const recent = (failuresByPeer.get(peer) ?? []).filter((t) => now - t < FAILURE_WINDOW_MS);
|
|
61200
|
+
if (recent.length) failuresByPeer.set(peer, recent);
|
|
61201
|
+
else failuresByPeer.delete(peer);
|
|
61202
|
+
return recent;
|
|
61203
|
+
};
|
|
61204
|
+
const server2 = createServer2((req, res) => {
|
|
61205
|
+
const send = (status, body, headers = {}) => {
|
|
61206
|
+
res.writeHead(status, {
|
|
61207
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
61208
|
+
"Cache-Control": "no-store",
|
|
61209
|
+
"Referrer-Policy": "no-referrer",
|
|
61210
|
+
"X-Content-Type-Options": "nosniff",
|
|
61211
|
+
...headers
|
|
61212
|
+
});
|
|
61213
|
+
res.end(req.method === "HEAD" ? void 0 : body + "\n");
|
|
61214
|
+
const path10 = (req.url ?? "").split("?")[0].slice(0, 64);
|
|
61215
|
+
log(`${req.method} ${path10} ${status}`);
|
|
61216
|
+
};
|
|
61217
|
+
const now = Date.now();
|
|
61218
|
+
const peer = req.socket.remoteAddress ?? "unknown";
|
|
61219
|
+
if (failuresByPeer.size > MAX_TRACKED_PEERS)
|
|
61220
|
+
for (const other of [...failuresByPeer.keys()]) recentFailures(other, now);
|
|
61221
|
+
if (recentFailures(peer, now).length >= maxFailures)
|
|
61222
|
+
return send(429, "Too many failed requests; wait a minute.");
|
|
61223
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
61224
|
+
return send(405, "Method not allowed.", { Allow: "GET, HEAD" });
|
|
61225
|
+
const address = server2.address();
|
|
61226
|
+
const port = address && typeof address === "object" ? address.port : options.port;
|
|
61227
|
+
const hosts = /* @__PURE__ */ new Set([hostAuthority(options.host, port)]);
|
|
61228
|
+
if (options.host === "127.0.0.1") hosts.add(`localhost:${port}`);
|
|
61229
|
+
if (!hosts.has((req.headers.host ?? "").toLowerCase()))
|
|
61230
|
+
return send(403, "Unexpected Host header.");
|
|
61231
|
+
let url;
|
|
61232
|
+
try {
|
|
61233
|
+
url = new URL(req.url ?? "/", "http://resolver.invalid");
|
|
61234
|
+
} catch {
|
|
61235
|
+
return send(400, "Bad request.");
|
|
61236
|
+
}
|
|
61237
|
+
if (!tokenMatches(url.searchParams.get("token") ?? bearerToken(req), options.token)) {
|
|
61238
|
+
failuresByPeer.set(peer, [...recentFailures(peer, now), now]);
|
|
61239
|
+
return send(401, "Missing or wrong token.");
|
|
61240
|
+
}
|
|
61241
|
+
const match = /^\/a\/([^/]+)$/.exec(url.pathname);
|
|
61242
|
+
if (!match || !ANCHOR_ID_PATTERN.test(match[1])) return send(404, "Not found.");
|
|
61243
|
+
let resolution;
|
|
61244
|
+
try {
|
|
61245
|
+
resolution = options.resolve(match[1]);
|
|
61246
|
+
} catch (error2) {
|
|
61247
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
61248
|
+
return send(503, `Could not read the Notes database: ${message}`);
|
|
61249
|
+
}
|
|
61250
|
+
if (!resolution) return send(404, "No such anchor.");
|
|
61251
|
+
if (resolution.status === "resolved" && resolution.url)
|
|
61252
|
+
return send(302, `Redirecting to ${resolution.url}`, { Location: resolution.url });
|
|
61253
|
+
return send(STATUS_CODE[resolution.status], `${resolution.status}: ${resolution.message}`);
|
|
61254
|
+
});
|
|
61255
|
+
server2.requestTimeout = 3e4;
|
|
61256
|
+
server2.headersTimeout = 1e4;
|
|
61257
|
+
return server2;
|
|
61258
|
+
}
|
|
61259
|
+
function startAnchorServer(options) {
|
|
61260
|
+
const server2 = createAnchorServer(options);
|
|
61261
|
+
return new Promise((resolve10, reject) => {
|
|
61262
|
+
server2.once("error", reject);
|
|
61263
|
+
server2.listen(options.port, options.host, () => {
|
|
61264
|
+
server2.off("error", reject);
|
|
61265
|
+
const address = server2.address();
|
|
61266
|
+
const port = address && typeof address === "object" ? address.port : options.port;
|
|
61267
|
+
resolve10({
|
|
61268
|
+
server: server2,
|
|
61269
|
+
baseUrl: `http://${hostAuthority(options.host, port)}`,
|
|
61270
|
+
close: () => new Promise((done) => {
|
|
61271
|
+
server2.closeAllConnections();
|
|
61272
|
+
server2.close(() => done());
|
|
61273
|
+
})
|
|
61274
|
+
});
|
|
61275
|
+
});
|
|
61276
|
+
});
|
|
61277
|
+
}
|
|
61278
|
+
var ANCHORS_USAGE = `Usage: apple-notes-mcp anchors serve [--port N] [--tailnet]
|
|
61279
|
+
|
|
61280
|
+
Serves GET /a/<anchor-id>?token=<token>, redirecting to the paragraph's
|
|
61281
|
+
current applenotes:// link. Binds 127.0.0.1 unless --tailnet is given, in
|
|
61282
|
+
which case it binds this Mac's Tailscale address (it never changes Tailscale
|
|
61283
|
+
or firewall settings). The token is APPLE_NOTES_MCP_ANCHORS_TOKEN (at least
|
|
61284
|
+
${MIN_TOKEN_LENGTH} characters) or a random one printed at startup. The process
|
|
61285
|
+
that runs it needs Full Disk Access. Stop it with Ctrl-C.`;
|
|
61286
|
+
function parseAnchorsArgs(argv) {
|
|
61287
|
+
const args = { port: 0, tailnet: false, help: false };
|
|
61288
|
+
if (!argv.length || argv[0] === "--help" || argv[0] === "-h") return { ...args, help: true };
|
|
61289
|
+
if (argv[0] !== "serve") throw new Error(`Unknown anchors command "${argv[0]}"`);
|
|
61290
|
+
for (let i = 1; i < argv.length; i++) {
|
|
61291
|
+
const arg = argv[i];
|
|
61292
|
+
if (arg === "--tailnet") args.tailnet = true;
|
|
61293
|
+
else if (arg === "--help" || arg === "-h") args.help = true;
|
|
61294
|
+
else if (arg === "--port") {
|
|
61295
|
+
const value = argv[++i];
|
|
61296
|
+
if (!value || !/^\d{1,5}$/.test(value) || Number(value) > 65535)
|
|
61297
|
+
throw new Error("--port needs a number from 0 to 65535");
|
|
61298
|
+
args.port = Number(value);
|
|
61299
|
+
} else throw new Error(`Unknown option "${arg}"`);
|
|
61300
|
+
}
|
|
61301
|
+
return args;
|
|
61302
|
+
}
|
|
61303
|
+
async function runAnchorsCli(argv, {
|
|
61304
|
+
env = process.env,
|
|
61305
|
+
resolve: resolve10,
|
|
61306
|
+
out = (text2) => process.stdout.write(text2),
|
|
61307
|
+
interfaces,
|
|
61308
|
+
signals = process
|
|
61309
|
+
}) {
|
|
61310
|
+
let args;
|
|
61311
|
+
try {
|
|
61312
|
+
args = parseAnchorsArgs(argv);
|
|
61313
|
+
} catch (error2) {
|
|
61314
|
+
out(`${error2.message}
|
|
61315
|
+
|
|
61316
|
+
${ANCHORS_USAGE}
|
|
61317
|
+
`);
|
|
61318
|
+
return 2;
|
|
61319
|
+
}
|
|
61320
|
+
if (args.help) {
|
|
61321
|
+
out(ANCHORS_USAGE + "\n");
|
|
61322
|
+
return 0;
|
|
61323
|
+
}
|
|
61324
|
+
const host = args.tailnet ? findTailnetAddress(interfaces)?.address : "127.0.0.1";
|
|
61325
|
+
if (!host) {
|
|
61326
|
+
out("No Tailscale address (100.64.0.0/10) found on this Mac; is Tailscale connected?\n");
|
|
61327
|
+
return 1;
|
|
61328
|
+
}
|
|
61329
|
+
const fromEnv = env.APPLE_NOTES_MCP_ANCHORS_TOKEN?.trim();
|
|
61330
|
+
if (fromEnv !== void 0 && fromEnv !== "" && fromEnv.length < MIN_TOKEN_LENGTH) {
|
|
61331
|
+
out(`APPLE_NOTES_MCP_ANCHORS_TOKEN must be at least ${MIN_TOKEN_LENGTH} characters.
|
|
61332
|
+
`);
|
|
61333
|
+
return 1;
|
|
61334
|
+
}
|
|
61335
|
+
const token = fromEnv || newServerToken();
|
|
61336
|
+
let started;
|
|
61337
|
+
try {
|
|
61338
|
+
started = await startAnchorServer({ host, port: args.port, token, resolve: resolve10 });
|
|
61339
|
+
} catch (error2) {
|
|
61340
|
+
out(`Could not listen on ${host}:${args.port}: ${error2.message}
|
|
61341
|
+
`);
|
|
61342
|
+
return 1;
|
|
61343
|
+
}
|
|
61344
|
+
out(
|
|
61345
|
+
`Paragraph anchor resolver listening on ${started.baseUrl}
|
|
61346
|
+
Links: ${started.baseUrl}/a/<anchor-id>?token=<token>
|
|
61347
|
+
` + (fromEnv ? "Token: from APPLE_NOTES_MCP_ANCHORS_TOKEN\n" : `Token (this run only): ${token}
|
|
61348
|
+
`) + "Press Ctrl-C to stop.\n"
|
|
61349
|
+
);
|
|
61350
|
+
await new Promise((done) => {
|
|
61351
|
+
signals.once("SIGINT", () => done());
|
|
61352
|
+
signals.once("SIGTERM", () => done());
|
|
61353
|
+
});
|
|
61354
|
+
await started.close();
|
|
61355
|
+
return 0;
|
|
61356
|
+
}
|
|
61357
|
+
|
|
61358
|
+
// src/services/anchorRegistry.ts
|
|
61359
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
61360
|
+
import {
|
|
61361
|
+
closeSync as closeSync9,
|
|
61362
|
+
constants as constants9,
|
|
61363
|
+
fstatSync as fstatSync9,
|
|
61364
|
+
fsyncSync,
|
|
61365
|
+
lstatSync as lstatSync6,
|
|
61366
|
+
mkdirSync as mkdirSync11,
|
|
61367
|
+
openSync as openSync9,
|
|
61368
|
+
readSync as readSync7,
|
|
61369
|
+
renameSync as renameSync5,
|
|
61370
|
+
statSync as statSync6,
|
|
61371
|
+
unlinkSync as unlinkSync5,
|
|
61372
|
+
writeSync as writeSync6
|
|
61373
|
+
} from "node:fs";
|
|
61374
|
+
import { homedir as homedir23 } from "node:os";
|
|
61375
|
+
import { dirname as dirname10, isAbsolute as isAbsolute6, join as join32, resolve as resolve9 } from "node:path";
|
|
61376
|
+
var MAX_ANCHORS = 2e4;
|
|
61377
|
+
var MAX_REGISTRY_BYTES = 32 * 1024 * 1024;
|
|
61378
|
+
var LOCK_WAIT_MS = 3e3;
|
|
61379
|
+
var LOCK_STALE_MS = 3e4;
|
|
61380
|
+
var ENVELOPE = {
|
|
61381
|
+
"unsafe-path": "validation_error",
|
|
61382
|
+
"corrupt-registry": "operation_failed",
|
|
61383
|
+
"registry-full": "validation_error",
|
|
61384
|
+
"registry-busy": "operation_failed",
|
|
61385
|
+
"anchor-not-found": "not_found",
|
|
61386
|
+
"invalid-anchor-id": "validation_error"
|
|
61387
|
+
};
|
|
61388
|
+
var AnchorRegistryError = class extends CodedError {
|
|
61389
|
+
constructor(reason, message) {
|
|
61390
|
+
super(message, { code: ENVELOPE[reason], reason });
|
|
61391
|
+
this.reason = reason;
|
|
61392
|
+
this.name = "AnchorRegistryError";
|
|
61393
|
+
}
|
|
61394
|
+
reason;
|
|
61395
|
+
};
|
|
61396
|
+
function anchorRegistryPath(env = process.env) {
|
|
61397
|
+
const override = env.APPLE_NOTES_MCP_ANCHOR_FILE?.trim();
|
|
61398
|
+
if (override) {
|
|
61399
|
+
if (!isAbsolute6(override))
|
|
61400
|
+
throw new AnchorRegistryError(
|
|
61401
|
+
"unsafe-path",
|
|
61402
|
+
"APPLE_NOTES_MCP_ANCHOR_FILE must be an absolute path."
|
|
61403
|
+
);
|
|
61404
|
+
return resolve9(override);
|
|
61405
|
+
}
|
|
61406
|
+
return join32(homedir23(), "Library/Application Support/apple-notes-mcp/paragraph-anchors.json");
|
|
61407
|
+
}
|
|
61408
|
+
var STRING_OR_NULL = (v) => v === null || typeof v === "string";
|
|
61409
|
+
function isAnchor(v) {
|
|
61410
|
+
if (!v || typeof v !== "object") return false;
|
|
61411
|
+
const a = v;
|
|
61412
|
+
return typeof a.anchorId === "string" && ANCHOR_ID_PATTERN.test(a.anchorId) && typeof a.noteIdentifier === "string" && STRING_OR_NULL(a.noteId) && STRING_OR_NULL(a.paragraphId) && ["unique", "shared", "missing"].includes(a.paragraphIdStatus) && typeof a.text === "string" && typeof a.fingerprint === "string" && STRING_OR_NULL(a.prevFingerprint) && STRING_OR_NULL(a.nextFingerprint) && Number.isInteger(a.blockIndex) && typeof a.style === "string" && typeof a.createdAt === "string" && (a.updatedAt === void 0 || typeof a.updatedAt === "string");
|
|
61413
|
+
}
|
|
61414
|
+
var sleep2 = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
61415
|
+
var AnchorRegistry = class _AnchorRegistry {
|
|
61416
|
+
constructor(path10 = anchorRegistryPath(), lockWaitMs = LOCK_WAIT_MS) {
|
|
61417
|
+
this.path = path10;
|
|
61418
|
+
this.lockWaitMs = lockWaitMs;
|
|
61419
|
+
}
|
|
61420
|
+
path;
|
|
61421
|
+
lockWaitMs;
|
|
61422
|
+
/** A new random anchor id. */
|
|
61423
|
+
static newId() {
|
|
61424
|
+
return `pa_${randomBytes5(12).toString("hex")}`;
|
|
61425
|
+
}
|
|
61426
|
+
ensureDir() {
|
|
61427
|
+
const dir = dirname10(this.path);
|
|
61428
|
+
try {
|
|
61429
|
+
if (!lstatSync6(dir).isDirectory())
|
|
61430
|
+
throw new AnchorRegistryError(
|
|
61431
|
+
"unsafe-path",
|
|
61432
|
+
`${dir} is not a directory (symlinks are refused).`
|
|
61433
|
+
);
|
|
61434
|
+
} catch (error2) {
|
|
61435
|
+
if (error2 instanceof AnchorRegistryError) throw error2;
|
|
61436
|
+
mkdirSync11(dir, { recursive: true, mode: 448 });
|
|
61437
|
+
}
|
|
61438
|
+
}
|
|
61439
|
+
/** Every anchor in the file, in the order recorded. A missing file is an empty registry. */
|
|
61440
|
+
load() {
|
|
61441
|
+
let fd;
|
|
61442
|
+
try {
|
|
61443
|
+
fd = openSync9(this.path, constants9.O_RDONLY | constants9.O_NOFOLLOW);
|
|
61444
|
+
} catch (error2) {
|
|
61445
|
+
if (error2.code === "ENOENT") return [];
|
|
61446
|
+
throw new AnchorRegistryError(
|
|
61447
|
+
"unsafe-path",
|
|
61448
|
+
`Refusing to read ${this.path}: not a regular file.`
|
|
61449
|
+
);
|
|
61450
|
+
}
|
|
61451
|
+
let text2;
|
|
61452
|
+
try {
|
|
61453
|
+
const stat = fstatSync9(fd);
|
|
61454
|
+
if (!stat.isFile())
|
|
61455
|
+
throw new AnchorRegistryError(
|
|
61456
|
+
"unsafe-path",
|
|
61457
|
+
`Refusing to read ${this.path}: not a regular file.`
|
|
61458
|
+
);
|
|
61459
|
+
if (stat.size > MAX_REGISTRY_BYTES)
|
|
61460
|
+
throw new AnchorRegistryError(
|
|
61461
|
+
"corrupt-registry",
|
|
61462
|
+
`${this.path} is ${stat.size} bytes; the limit is ${MAX_REGISTRY_BYTES}.`
|
|
61463
|
+
);
|
|
61464
|
+
const data = Buffer.alloc(stat.size);
|
|
61465
|
+
let read = 0;
|
|
61466
|
+
while (read < stat.size) {
|
|
61467
|
+
const n = readSync7(fd, data, read, stat.size - read, read);
|
|
61468
|
+
if (n <= 0) break;
|
|
61469
|
+
read += n;
|
|
61470
|
+
}
|
|
61471
|
+
text2 = data.subarray(0, read).toString("utf8");
|
|
61472
|
+
} finally {
|
|
61473
|
+
closeSync9(fd);
|
|
61474
|
+
}
|
|
61475
|
+
if (text2.trim() === "") return [];
|
|
61476
|
+
let parsed;
|
|
61477
|
+
try {
|
|
61478
|
+
parsed = JSON.parse(text2);
|
|
61479
|
+
} catch {
|
|
61480
|
+
throw new AnchorRegistryError(
|
|
61481
|
+
"corrupt-registry",
|
|
61482
|
+
`${this.path} is not valid JSON; it was left unchanged. Move it aside to start a new registry.`
|
|
61483
|
+
);
|
|
61484
|
+
}
|
|
61485
|
+
const anchors = parsed?.anchors;
|
|
61486
|
+
if (parsed?.version !== 1 || !Array.isArray(anchors) || !anchors.every(isAnchor))
|
|
61487
|
+
throw new AnchorRegistryError(
|
|
61488
|
+
"corrupt-registry",
|
|
61489
|
+
`${this.path} is not a version 1 anchor registry; it was left unchanged.`
|
|
61490
|
+
);
|
|
61491
|
+
return anchors;
|
|
61492
|
+
}
|
|
61493
|
+
/** One anchor, or an `anchor-not-found` error. */
|
|
61494
|
+
get(anchorId) {
|
|
61495
|
+
if (!ANCHOR_ID_PATTERN.test(anchorId))
|
|
61496
|
+
throw new AnchorRegistryError(
|
|
61497
|
+
"invalid-anchor-id",
|
|
61498
|
+
`Invalid anchor id "${anchorId}": expected pa_ and 24 hex digits.`
|
|
61499
|
+
);
|
|
61500
|
+
const anchor = this.load().find((a) => a.anchorId === anchorId);
|
|
61501
|
+
if (!anchor) throw new AnchorRegistryError("anchor-not-found", `No anchor ${anchorId}.`);
|
|
61502
|
+
return anchor;
|
|
61503
|
+
}
|
|
61504
|
+
/**
|
|
61505
|
+
* Apply `change` to the current anchors under the lock and save the result.
|
|
61506
|
+
* Returns what `change` returns.
|
|
61507
|
+
*/
|
|
61508
|
+
update(change) {
|
|
61509
|
+
this.ensureDir();
|
|
61510
|
+
const release5 = this.lock();
|
|
61511
|
+
try {
|
|
61512
|
+
const { anchors, result } = change(this.load());
|
|
61513
|
+
if (anchors.length > MAX_ANCHORS)
|
|
61514
|
+
throw new AnchorRegistryError(
|
|
61515
|
+
"registry-full",
|
|
61516
|
+
`The registry holds at most ${MAX_ANCHORS} anchors; prune stale anchors first.`
|
|
61517
|
+
);
|
|
61518
|
+
this.write(anchors);
|
|
61519
|
+
return result;
|
|
61520
|
+
} finally {
|
|
61521
|
+
release5();
|
|
61522
|
+
}
|
|
61523
|
+
}
|
|
61524
|
+
write(anchors) {
|
|
61525
|
+
try {
|
|
61526
|
+
if (!lstatSync6(this.path).isFile())
|
|
61527
|
+
throw new AnchorRegistryError(
|
|
61528
|
+
"unsafe-path",
|
|
61529
|
+
`Refusing to replace ${this.path}: it is not a regular file.`
|
|
61530
|
+
);
|
|
61531
|
+
} catch (error2) {
|
|
61532
|
+
if (error2 instanceof AnchorRegistryError) throw error2;
|
|
61533
|
+
}
|
|
61534
|
+
const temp = join32(
|
|
61535
|
+
dirname10(this.path),
|
|
61536
|
+
`.paragraph-anchors.${randomBytes5(6).toString("hex")}.tmp`
|
|
61537
|
+
);
|
|
61538
|
+
const fd = openSync9(
|
|
61539
|
+
temp,
|
|
61540
|
+
constants9.O_WRONLY | constants9.O_CREAT | constants9.O_EXCL | constants9.O_NOFOLLOW,
|
|
61541
|
+
384
|
|
61542
|
+
);
|
|
61543
|
+
try {
|
|
61544
|
+
const data = Buffer.from(JSON.stringify({ version: 1, anchors }, null, 1) + "\n", "utf8");
|
|
61545
|
+
let written = 0;
|
|
61546
|
+
while (written < data.length) written += writeSync6(fd, data, written);
|
|
61547
|
+
fsyncSync(fd);
|
|
61548
|
+
} catch (error2) {
|
|
61549
|
+
closeSync9(fd);
|
|
61550
|
+
unlinkSync5(temp);
|
|
61551
|
+
throw error2;
|
|
61552
|
+
}
|
|
61553
|
+
closeSync9(fd);
|
|
61554
|
+
try {
|
|
61555
|
+
renameSync5(temp, this.path);
|
|
61556
|
+
} catch (error2) {
|
|
61557
|
+
unlinkSync5(temp);
|
|
61558
|
+
throw error2;
|
|
61559
|
+
}
|
|
61560
|
+
}
|
|
61561
|
+
/** Take the lock file, waiting briefly; a lock older than 30 s is treated as abandoned. */
|
|
61562
|
+
lock() {
|
|
61563
|
+
const lockPath = `${this.path}.lock`;
|
|
61564
|
+
const deadline = Date.now() + this.lockWaitMs;
|
|
61565
|
+
for (; ; ) {
|
|
61566
|
+
try {
|
|
61567
|
+
const fd = openSync9(
|
|
61568
|
+
lockPath,
|
|
61569
|
+
constants9.O_WRONLY | constants9.O_CREAT | constants9.O_EXCL | constants9.O_NOFOLLOW,
|
|
61570
|
+
384
|
|
61571
|
+
);
|
|
61572
|
+
writeSync6(fd, String(process.pid));
|
|
61573
|
+
closeSync9(fd);
|
|
61574
|
+
return () => {
|
|
61575
|
+
try {
|
|
61576
|
+
unlinkSync5(lockPath);
|
|
61577
|
+
} catch {
|
|
61578
|
+
}
|
|
61579
|
+
};
|
|
61580
|
+
} catch (error2) {
|
|
61581
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
61582
|
+
}
|
|
61583
|
+
try {
|
|
61584
|
+
if (Date.now() - statSync6(lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
61585
|
+
unlinkSync5(lockPath);
|
|
61586
|
+
continue;
|
|
61587
|
+
}
|
|
61588
|
+
} catch {
|
|
61589
|
+
continue;
|
|
61590
|
+
}
|
|
61591
|
+
if (Date.now() > deadline)
|
|
61592
|
+
throw new AnchorRegistryError(
|
|
61593
|
+
"registry-busy",
|
|
61594
|
+
`Another process holds ${lockPath}; try again shortly.`
|
|
61595
|
+
);
|
|
61596
|
+
sleep2(25);
|
|
61597
|
+
}
|
|
61598
|
+
}
|
|
61599
|
+
/**
|
|
61600
|
+
* Record anchors, reusing an existing anchor for the same note, paragraph ID,
|
|
61601
|
+
* text and block index. Each new anchor gets a fresh id (a candidate's own
|
|
61602
|
+
* anchorId is ignored). Returns each anchor with whether it was new.
|
|
61603
|
+
*/
|
|
61604
|
+
record(candidates) {
|
|
61605
|
+
return this.update((anchors) => {
|
|
61606
|
+
const out = [];
|
|
61607
|
+
const next = [...anchors];
|
|
61608
|
+
for (const candidate of candidates) {
|
|
61609
|
+
const existing = next.find(
|
|
61610
|
+
(a) => a.noteIdentifier === candidate.noteIdentifier && a.paragraphId === candidate.paragraphId && a.fingerprint === candidate.fingerprint && a.blockIndex === candidate.blockIndex
|
|
61611
|
+
);
|
|
61612
|
+
if (existing) {
|
|
61613
|
+
out.push({ anchor: existing, created: false });
|
|
61614
|
+
continue;
|
|
61615
|
+
}
|
|
61616
|
+
const anchor = { ...candidate, anchorId: _AnchorRegistry.newId() };
|
|
61617
|
+
next.push(anchor);
|
|
61618
|
+
out.push({ anchor, created: true });
|
|
61619
|
+
}
|
|
61620
|
+
return { anchors: next, result: out };
|
|
61621
|
+
});
|
|
61622
|
+
}
|
|
61623
|
+
/** Replace one anchor's fields (not its id or creation time). */
|
|
61624
|
+
replace(anchor) {
|
|
61625
|
+
return this.update((anchors) => {
|
|
61626
|
+
const i = anchors.findIndex((a) => a.anchorId === anchor.anchorId);
|
|
61627
|
+
if (i < 0) throw new AnchorRegistryError("anchor-not-found", `No anchor ${anchor.anchorId}.`);
|
|
61628
|
+
const saved = { ...anchor, createdAt: anchors[i].createdAt };
|
|
61629
|
+
const next = [...anchors];
|
|
61630
|
+
next[i] = saved;
|
|
61631
|
+
return { anchors: next, result: saved };
|
|
61632
|
+
});
|
|
61633
|
+
}
|
|
61634
|
+
/** Remove anchors by id; returns the ids that were present. */
|
|
61635
|
+
remove(anchorIds) {
|
|
61636
|
+
const wanted = new Set(anchorIds);
|
|
61637
|
+
return this.update((anchors) => ({
|
|
61638
|
+
anchors: anchors.filter((a) => !wanted.has(a.anchorId)),
|
|
61639
|
+
result: anchors.filter((a) => wanted.has(a.anchorId)).map((a) => a.anchorId)
|
|
61640
|
+
}));
|
|
61641
|
+
}
|
|
61642
|
+
};
|
|
61643
|
+
|
|
61644
|
+
// src/services/paragraphAnchorOps.ts
|
|
61645
|
+
var MAX_ANCHORS_PER_CALL = 500;
|
|
61646
|
+
function recordParagraphAnchors(note, paragraphs, registry2 = new AnchorRegistry(), now = /* @__PURE__ */ new Date()) {
|
|
61647
|
+
const candidates = paragraphs.slice(0, MAX_ANCHORS_PER_CALL).map((p) => anchorFor(note, p, { anchorId: "", now }));
|
|
61648
|
+
return candidates.length ? registry2.record(candidates) : [];
|
|
61649
|
+
}
|
|
61650
|
+
var REFRESH_MIN_CONFIDENCE = 0.8;
|
|
61651
|
+
function resolveStoredAnchor(anchorId, {
|
|
61652
|
+
registry: registry2 = new AnchorRegistry(),
|
|
61653
|
+
dbPath: dbPath2,
|
|
61654
|
+
minConfidence,
|
|
61655
|
+
refresh = false,
|
|
61656
|
+
now = () => /* @__PURE__ */ new Date()
|
|
61657
|
+
} = {}) {
|
|
61658
|
+
const anchor = registry2.get(anchorId);
|
|
61659
|
+
const { resolution, note } = resolveAnchorDetailed(anchor, { dbPath: dbPath2, minConfidence });
|
|
61660
|
+
const out = { ...resolution };
|
|
61661
|
+
if (refresh) {
|
|
61662
|
+
if (!out.match || !note || !["resolved", "needs-reminting"].includes(out.status))
|
|
61663
|
+
out.refreshSkipped = "nothing was matched";
|
|
61664
|
+
else if (out.confidence < REFRESH_MIN_CONFIDENCE)
|
|
61665
|
+
out.refreshSkipped = `confidence ${out.confidence} is below ${REFRESH_MIN_CONFIDENCE}`;
|
|
61666
|
+
else {
|
|
61667
|
+
const paragraph = note.paragraphs.find((p) => p.blockIndex === out.match.blockIndex);
|
|
61668
|
+
const fresh = anchorFor(note, paragraph, { anchorId, now: now() });
|
|
61669
|
+
registry2.replace({ ...fresh, updatedAt: fresh.createdAt });
|
|
61670
|
+
out.refreshed = true;
|
|
61671
|
+
}
|
|
61672
|
+
}
|
|
61673
|
+
return out;
|
|
61674
|
+
}
|
|
61675
|
+
var DEFAULT_PRUNE_STATUSES = ["not-found", "note-not-found"];
|
|
61676
|
+
function pruneParagraphAnchors({
|
|
61677
|
+
registry: registry2 = new AnchorRegistry(),
|
|
61678
|
+
dbPath: dbPath2,
|
|
61679
|
+
anchorIds,
|
|
61680
|
+
noteIdentifier,
|
|
61681
|
+
statuses = DEFAULT_PRUNE_STATUSES,
|
|
61682
|
+
dryRun = true
|
|
61683
|
+
} = {}) {
|
|
61684
|
+
let anchors = registry2.load();
|
|
61685
|
+
if (noteIdentifier)
|
|
61686
|
+
anchors = anchors.filter((a) => a.noteIdentifier === noteIdentifier.toUpperCase());
|
|
61687
|
+
let stale;
|
|
61688
|
+
if (anchorIds) {
|
|
61689
|
+
const wanted = new Set(anchorIds);
|
|
61690
|
+
stale = anchors.filter((a) => wanted.has(a.anchorId)).map((a) => ({ anchorId: a.anchorId, status: "listed", message: "Listed by id" }));
|
|
61691
|
+
} else {
|
|
61692
|
+
const wanted = new Set(statuses);
|
|
61693
|
+
stale = [];
|
|
61694
|
+
for (const anchor of anchors) {
|
|
61695
|
+
const r = resolveAnchorDetailed(anchor, { dbPath: dbPath2 }).resolution;
|
|
61696
|
+
if (wanted.has(r.status))
|
|
61697
|
+
stale.push({ anchorId: anchor.anchorId, status: r.status, message: r.message });
|
|
61698
|
+
}
|
|
61699
|
+
}
|
|
61700
|
+
const removed = dryRun || !stale.length ? [] : registry2.remove(stale.map((s) => s.anchorId));
|
|
61701
|
+
return { dryRun, examined: anchorIds ? stale.length : anchors.length, stale, removed };
|
|
61702
|
+
}
|
|
61703
|
+
function registryLookup(registry2 = new AnchorRegistry(), dbPath2) {
|
|
61704
|
+
return (anchorId) => {
|
|
61705
|
+
const anchor = registry2.load().find((a) => a.anchorId === anchorId);
|
|
61706
|
+
return anchor ? resolveAnchorDetailed(anchor, { dbPath: dbPath2 }).resolution : void 0;
|
|
61707
|
+
};
|
|
61708
|
+
}
|
|
61709
|
+
|
|
60949
61710
|
// src/index.ts
|
|
60950
61711
|
loadFileConfig();
|
|
60951
61712
|
var require2 = createRequire(import.meta.url);
|
|
@@ -60994,6 +61755,11 @@ if (process.argv[2] === "setup") {
|
|
|
60994
61755
|
if (process.argv[2] === "templates") {
|
|
60995
61756
|
process.exit(await runTemplatesCommand(process.argv.slice(3)));
|
|
60996
61757
|
}
|
|
61758
|
+
if (process.argv[2] === "anchors") {
|
|
61759
|
+
process.exit(
|
|
61760
|
+
await runAnchorsCli(process.argv.slice(3), { resolve: registryLookup(new AnchorRegistry()) })
|
|
61761
|
+
);
|
|
61762
|
+
}
|
|
60997
61763
|
var server = new McpServer({
|
|
60998
61764
|
name: "apple-notes",
|
|
60999
61765
|
version: version2,
|
|
@@ -62100,6 +62866,9 @@ registerTool(
|
|
|
62100
62866
|
);
|
|
62101
62867
|
}, "Error reading note blocks")
|
|
62102
62868
|
);
|
|
62869
|
+
var recordAnchorsInput = external_exports.boolean().optional().describe(
|
|
62870
|
+
"Also record a paragraph anchor for each returned paragraph (at most 500 per call) so resolve-paragraph-anchor can find it after edits; each row gains anchorId (default false; writes only the local anchor registry)"
|
|
62871
|
+
);
|
|
62103
62872
|
var paragraphNoteSelector = {
|
|
62104
62873
|
id: noteIdInput.optional().describe(`Exact note ID (${NOTE_ID_FORMS}); give id or title`),
|
|
62105
62874
|
title: external_exports.string().min(1).max(MAX.TITLE).optional().describe("Exact note title; must match one note unless folder narrows it"),
|
|
@@ -62108,41 +62877,57 @@ var paragraphNoteSelector = {
|
|
|
62108
62877
|
registerTool(
|
|
62109
62878
|
"list-note-paragraphs",
|
|
62110
62879
|
{
|
|
62111
|
-
description: "Use when: you need a note's paragraphs with their style and stored paragraph ID, for example to choose one to link to.\nReturns: one page of non-empty paragraphs in body order, each with blockIndex (as in get-note-blocks), text, style, paragraphId, paragraphIdStatus (unique, shared, missing) and, only when unique, a direct applenotes:// url that opens that paragraph; plus counts per status and page info (call again with offset set to page.nextOffset while page.hasMore is true).\nDo not use when: you need inline formatting (get-note-blocks).\nSafety:
|
|
62880
|
+
description: "Use when: you need a note's paragraphs with their style and stored paragraph ID, for example to choose one to link to.\nReturns: one page of non-empty paragraphs in body order, each with blockIndex (as in get-note-blocks), text, style, paragraphId, paragraphIdStatus (unique, shared, missing) and, only when unique, a direct applenotes:// url that opens that paragraph; plus counts per status and page info (call again with offset set to page.nextOffset while page.hasMore is true).\nDo not use when: you need inline formatting (get-note-blocks).\nSafety: never changes Notes; reads the NoteStore database directly and requires Full Disk Access. Without recordAnchors it writes nothing; with recordAnchors it also writes the local anchor registry file (mode 0600), so the tool is not marked read-only. Paragraph IDs repeat often (Notes copies them when a paragraph is split), so shared IDs get no url. Title lookups ignore Recently Deleted. Password-protected notes are refused.",
|
|
62112
62881
|
inputSchema: {
|
|
62113
62882
|
...paragraphNoteSelector,
|
|
62114
62883
|
linkableOnly: external_exports.boolean().optional().describe("Return only paragraphs that have a direct url (default false)"),
|
|
62115
62884
|
offset: external_exports.number().int().min(0).optional().describe("Index of the first paragraph to return (default 0); use page.nextOffset"),
|
|
62116
|
-
limit: external_exports.number().int().min(1).max(5e3).optional().describe("Maximum paragraphs to return (default 500, max 5000)")
|
|
62885
|
+
limit: external_exports.number().int().min(1).max(5e3).optional().describe("Maximum paragraphs to return (default 500, max 5000)"),
|
|
62886
|
+
recordAnchors: recordAnchorsInput
|
|
62117
62887
|
},
|
|
62118
62888
|
outputSchema: {
|
|
62119
62889
|
id: external_exports.string().optional(),
|
|
62120
62890
|
identifier: external_exports.string().nullable().optional(),
|
|
62121
62891
|
counts: external_exports.record(external_exports.unknown()).optional(),
|
|
62122
62892
|
paragraphs: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
62123
|
-
page: external_exports.record(external_exports.unknown()).optional()
|
|
62893
|
+
page: external_exports.record(external_exports.unknown()).optional(),
|
|
62894
|
+
anchorsRecorded: external_exports.number().optional()
|
|
62124
62895
|
},
|
|
62125
|
-
annotations: { readOnlyHint: true }
|
|
62896
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
62126
62897
|
},
|
|
62127
|
-
withErrorHandling(({ id: id2, title, folder, linkableOnly, offset, limit }) => {
|
|
62898
|
+
withErrorHandling(({ id: id2, title, folder, linkableOnly, offset, limit, recordAnchors }) => {
|
|
62128
62899
|
const note = readNoteParagraphs({ id: id2, title, folder });
|
|
62129
62900
|
const page = pageParagraphs(note.paragraphs, {
|
|
62130
62901
|
offset,
|
|
62131
|
-
limit,
|
|
62902
|
+
limit: recordAnchors ? Math.min(limit ?? 500, MAX_ANCHORS_PER_CALL) : limit,
|
|
62132
62903
|
linkableOnly,
|
|
62133
62904
|
maxBytes: blocksMaxResponseBytes()
|
|
62134
62905
|
});
|
|
62906
|
+
let anchorsRecorded;
|
|
62907
|
+
let paragraphs = page.paragraphs;
|
|
62908
|
+
if (recordAnchors) {
|
|
62909
|
+
const recorded = recordParagraphAnchors(note, page.paragraphs);
|
|
62910
|
+
anchorsRecorded = recorded.filter((r) => r.created).length;
|
|
62911
|
+
paragraphs = page.paragraphs.map((p, i) => ({ ...p, anchorId: recorded[i].anchor.anchorId }));
|
|
62912
|
+
}
|
|
62135
62913
|
const { unique, shared, missing } = note.counts;
|
|
62136
62914
|
return successResponse(
|
|
62137
|
-
`${note.paragraphs.length} paragraphs: ${unique} linkable, ${shared} with a shared ID, ${missing} without an ID; returned ${page.page.returned} from offset ${page.page.offset}` + (page.page.hasMore ? `; more at offset ${page.page.nextOffset}` : "") + ".",
|
|
62138
|
-
{
|
|
62915
|
+
`${note.paragraphs.length} paragraphs: ${unique} linkable, ${shared} with a shared ID, ${missing} without an ID; returned ${page.page.returned} from offset ${page.page.offset}` + (page.page.hasMore ? `; more at offset ${page.page.nextOffset}` : "") + (anchorsRecorded !== void 0 ? `; ${anchorsRecorded} new anchor(s) recorded` : "") + ".",
|
|
62916
|
+
{
|
|
62917
|
+
id: note.id,
|
|
62918
|
+
identifier: note.identifier,
|
|
62919
|
+
counts: note.counts,
|
|
62920
|
+
paragraphs,
|
|
62921
|
+
page: page.page,
|
|
62922
|
+
...anchorsRecorded !== void 0 ? { anchorsRecorded } : {}
|
|
62923
|
+
}
|
|
62139
62924
|
);
|
|
62140
62925
|
}, "Error listing paragraphs")
|
|
62141
62926
|
);
|
|
62142
62927
|
registerTool(
|
|
62143
62928
|
"get-paragraph-link",
|
|
62144
62929
|
{
|
|
62145
|
-
description: "Use when: you need a link that opens Notes at one paragraph (for example a heading) of a note.\nReturns: a direct applenotes://showNote?identifier=<note>¶graphID=<paragraph> url and the selected paragraph, only when that paragraph's stored ID is present and appears in no other paragraph of the note. Otherwise an error whose structuredContent.reason says why: paragraph-id-shared, paragraph-id-missing, no-match, ambiguous-paragraph (pass occurrence or a longer snippet), occurrence-out-of-range, ambiguous-note, encrypted.\nDo not use when: you want a link to the whole note (get-note-link).\nSafety:
|
|
62930
|
+
description: "Use when: you need a link that opens Notes at one paragraph (for example a heading) of a note.\nReturns: a direct applenotes://showNote?identifier=<note>¶graphID=<paragraph> url and the selected paragraph, only when that paragraph's stored ID is present and appears in no other paragraph of the note. Otherwise an error whose structuredContent.reason says why: paragraph-id-shared, paragraph-id-missing, no-match, ambiguous-paragraph (pass occurrence or a longer snippet), occurrence-out-of-range, ambiguous-note, encrypted.\nDo not use when: you want a link to the whole note (get-note-link).\nSafety: never changes Notes or a paragraph ID, so a paragraph without a unique ID cannot be linked. Requires Full Disk Access. Without recordAnchor it writes nothing; with recordAnchor it also writes the local anchor registry file (mode 0600), so the tool is not marked read-only. A later edit in Notes can replace the ID and break the link.",
|
|
62146
62931
|
inputSchema: {
|
|
62147
62932
|
...paragraphNoteSelector,
|
|
62148
62933
|
contains: external_exports.string().min(1).max(MAX.CONTENT).optional().describe(
|
|
@@ -62150,26 +62935,201 @@ registerTool(
|
|
|
62150
62935
|
),
|
|
62151
62936
|
match: external_exports.string().min(1).max(MAX.CONTENT).optional().describe("The whole paragraph text, compared the same way"),
|
|
62152
62937
|
blockIndex: external_exports.number().int().min(0).optional().describe("The paragraph's blockIndex from list-note-paragraphs"),
|
|
62153
|
-
occurrence: external_exports.number().int().min(1).optional().describe("Which match to use (1-based) when contains or match hits several paragraphs")
|
|
62938
|
+
occurrence: external_exports.number().int().min(1).optional().describe("Which match to use (1-based) when contains or match hits several paragraphs"),
|
|
62939
|
+
recordAnchor: external_exports.boolean().optional().describe(
|
|
62940
|
+
"Also record a paragraph anchor for the linked paragraph so resolve-paragraph-anchor can find it after edits (default false; writes only the local anchor registry)"
|
|
62941
|
+
)
|
|
62154
62942
|
},
|
|
62155
62943
|
outputSchema: {
|
|
62156
62944
|
url: external_exports.string().optional(),
|
|
62157
62945
|
id: external_exports.string().optional(),
|
|
62158
62946
|
identifier: external_exports.string().nullable().optional(),
|
|
62159
|
-
paragraph: external_exports.record(external_exports.unknown()).optional()
|
|
62947
|
+
paragraph: external_exports.record(external_exports.unknown()).optional(),
|
|
62948
|
+
anchorId: external_exports.string().optional()
|
|
62160
62949
|
},
|
|
62161
|
-
annotations: { readOnlyHint: true }
|
|
62950
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
62951
|
+
},
|
|
62952
|
+
withErrorHandling(
|
|
62953
|
+
({ id: id2, title, folder, contains, match, blockIndex, occurrence, recordAnchor }) => {
|
|
62954
|
+
const note = readNoteParagraphs({ id: id2, title, folder });
|
|
62955
|
+
const result = paragraphLink(note, { contains, match, blockIndex, occurrence });
|
|
62956
|
+
const anchorId = recordAnchor ? recordParagraphAnchors(note, [result.paragraph])[0].anchor.anchorId : void 0;
|
|
62957
|
+
return successResponse(
|
|
62958
|
+
`Paragraph link: ${result.url}` + (anchorId ? `
|
|
62959
|
+
Anchor: ${anchorId}` : ""),
|
|
62960
|
+
{
|
|
62961
|
+
url: result.url,
|
|
62962
|
+
id: note.id,
|
|
62963
|
+
identifier: note.identifier,
|
|
62964
|
+
paragraph: { ...result.paragraph },
|
|
62965
|
+
...anchorId ? { anchorId } : {}
|
|
62966
|
+
}
|
|
62967
|
+
);
|
|
62968
|
+
},
|
|
62969
|
+
"No paragraph link"
|
|
62970
|
+
)
|
|
62971
|
+
);
|
|
62972
|
+
var anchorIdInput = external_exports.string().regex(ANCHOR_ID_PATTERN, "Expected an anchor id: pa_ and 24 hex digits").describe(
|
|
62973
|
+
"Anchor id (pa_ and 24 hex digits) from create-paragraph-anchor or list-paragraph-anchors"
|
|
62974
|
+
);
|
|
62975
|
+
var noteIdentifierInput = external_exports.string().regex(/^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$/, "Expected a Notes UUID").optional();
|
|
62976
|
+
registerTool(
|
|
62977
|
+
"create-paragraph-anchor",
|
|
62978
|
+
{
|
|
62979
|
+
description: "Use when: you want a reference to one paragraph that can be found again after the note is edited, for example before sharing a paragraph link that should keep working.\nReturns: the anchor (anchorId, note identifier, paragraph ID and its status, normalized text, fingerprints of the paragraph and its neighbours, block index, created time), whether it is new, and the paragraph's current direct url when its ID is unique.\nDo not use when: you only need a link right now (get-paragraph-link).\nSafety: reads the NoteStore database (Full Disk Access) and never changes Notes. Writes one local file, the anchor registry (APPLE_NOTES_MCP_ANCHOR_FILE, default ~/Library/Application Support/apple-notes-mcp/paragraph-anchors.json, mode 0600), which stores the paragraph's text. Unlike get-paragraph-link, a paragraph with a shared or missing ID can be anchored. Recording the same paragraph again returns the existing anchor.",
|
|
62980
|
+
inputSchema: {
|
|
62981
|
+
...paragraphNoteSelector,
|
|
62982
|
+
contains: external_exports.string().min(1).max(MAX.CONTENT).optional().describe("Snippet of the paragraph; give one of contains, match, blockIndex"),
|
|
62983
|
+
match: external_exports.string().min(1).max(MAX.CONTENT).optional().describe("The whole paragraph text"),
|
|
62984
|
+
blockIndex: external_exports.number().int().min(0).optional().describe("The paragraph's blockIndex from list-note-paragraphs"),
|
|
62985
|
+
occurrence: external_exports.number().int().min(1).optional().describe("Which match to use (1-based) when contains or match hits several paragraphs")
|
|
62986
|
+
},
|
|
62987
|
+
outputSchema: {
|
|
62988
|
+
anchor: external_exports.record(external_exports.unknown()).optional(),
|
|
62989
|
+
created: external_exports.boolean().optional(),
|
|
62990
|
+
url: external_exports.string().optional()
|
|
62991
|
+
},
|
|
62992
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
62162
62993
|
},
|
|
62163
62994
|
withErrorHandling(({ id: id2, title, folder, contains, match, blockIndex, occurrence }) => {
|
|
62164
62995
|
const note = readNoteParagraphs({ id: id2, title, folder });
|
|
62165
|
-
const
|
|
62166
|
-
|
|
62167
|
-
|
|
62168
|
-
|
|
62169
|
-
|
|
62170
|
-
|
|
62996
|
+
const paragraph = selectParagraph(note.paragraphs, { contains, match, blockIndex, occurrence });
|
|
62997
|
+
const [{ anchor, created }] = recordParagraphAnchors(note, [paragraph]);
|
|
62998
|
+
return successResponse(
|
|
62999
|
+
`${created ? "Recorded" : "Reusing"} anchor ${anchor.anchorId} for block ${anchor.blockIndex} (paragraph ID ${paragraph.paragraphIdStatus}).`,
|
|
63000
|
+
{ anchor: { ...anchor }, created, ...paragraph.url ? { url: paragraph.url } : {} }
|
|
63001
|
+
);
|
|
63002
|
+
}, "Error recording paragraph anchor")
|
|
63003
|
+
);
|
|
63004
|
+
registerTool(
|
|
63005
|
+
"resolve-paragraph-anchor",
|
|
63006
|
+
{
|
|
63007
|
+
description: "Use when: you have an anchorId and need the paragraph's current link, or want to check that an anchored paragraph still exists after edits.\nReturns: status (resolved, needs-reminting, ambiguous, low-confidence, not-found, note-not-found, note-deleted, note-unreadable), method (paragraph-id, exact-text, text-and-neighbours), confidence from 0 to 1, the matched block (blockIndex, text, paragraphId, paragraphIdStatus) and what changed. url is present only when status is resolved.\nDo not use when: you have no anchor yet (create-paragraph-anchor, or get-paragraph-link with recordAnchor).\nSafety: reads the NoteStore database (Full Disk Access); never changes Notes. Fails closed: equally good candidates give ambiguous and no url. needs-reminting means the paragraph was found but its stored ID is shared or missing, so no safe link exists for it; this server cannot give it a new ID. refresh rewrites the stored anchor (local registry only) after a match with confidence 0.8 or more.",
|
|
63008
|
+
inputSchema: {
|
|
63009
|
+
anchorId: anchorIdInput,
|
|
63010
|
+
minConfidence: external_exports.number().min(0).max(1).optional().describe(`Lowest confidence accepted as a match (default ${DEFAULT_MIN_CONFIDENCE})`),
|
|
63011
|
+
refresh: external_exports.boolean().optional().describe(
|
|
63012
|
+
"Update the stored anchor to the matched paragraph as it is now (default false; only at confidence 0.8 or more)"
|
|
63013
|
+
)
|
|
63014
|
+
},
|
|
63015
|
+
outputSchema: {
|
|
63016
|
+
anchorId: external_exports.string().optional(),
|
|
63017
|
+
status: external_exports.string().optional(),
|
|
63018
|
+
resolved: external_exports.boolean().optional(),
|
|
63019
|
+
url: external_exports.string().optional(),
|
|
63020
|
+
needsReminting: external_exports.boolean().optional(),
|
|
63021
|
+
method: external_exports.string().optional(),
|
|
63022
|
+
confidence: external_exports.number().optional(),
|
|
63023
|
+
match: external_exports.record(external_exports.unknown()).optional(),
|
|
63024
|
+
changes: external_exports.record(external_exports.unknown()).optional(),
|
|
63025
|
+
candidates: external_exports.number().optional(),
|
|
63026
|
+
noteId: external_exports.string().optional(),
|
|
63027
|
+
message: external_exports.string().optional(),
|
|
63028
|
+
refreshed: external_exports.boolean().optional(),
|
|
63029
|
+
refreshSkipped: external_exports.string().optional()
|
|
63030
|
+
},
|
|
63031
|
+
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
63032
|
+
},
|
|
63033
|
+
withErrorHandling(({ anchorId, minConfidence, refresh }) => {
|
|
63034
|
+
const result = resolveStoredAnchor(anchorId, { minConfidence, refresh });
|
|
63035
|
+
return successResponse(
|
|
63036
|
+
`${result.status}: ${result.message}` + (result.url ? `
|
|
63037
|
+
Link: ${result.url}` : ""),
|
|
63038
|
+
{ ...result }
|
|
63039
|
+
);
|
|
63040
|
+
}, "Error resolving paragraph anchor")
|
|
63041
|
+
);
|
|
63042
|
+
registerTool(
|
|
63043
|
+
"list-paragraph-anchors",
|
|
63044
|
+
{
|
|
63045
|
+
description: "Use when: you need the recorded paragraph anchors, all of them or those of one note.\nReturns: one page of anchors in the order recorded (anchorId, noteIdentifier, paragraphId, text, blockIndex, created time), the total, and the registry path.\nDo not use when: you need an anchor's current location (resolve-paragraph-anchor).\nSafety: read-only; reads only the local anchor registry, never Notes.",
|
|
63046
|
+
inputSchema: {
|
|
63047
|
+
noteIdentifier: noteIdentifierInput.describe(
|
|
63048
|
+
"Only anchors in the note with this Notes UUID (its identifier)"
|
|
63049
|
+
),
|
|
63050
|
+
offset: external_exports.number().int().min(0).optional().describe("First anchor to return (default 0)"),
|
|
63051
|
+
limit: external_exports.number().int().min(1).max(1e3).optional().describe("Maximum anchors to return (default 100, max 1000)")
|
|
63052
|
+
},
|
|
63053
|
+
outputSchema: {
|
|
63054
|
+
anchors: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
63055
|
+
total: external_exports.number().optional(),
|
|
63056
|
+
registry: external_exports.string().optional(),
|
|
63057
|
+
page: external_exports.record(external_exports.unknown()).optional()
|
|
63058
|
+
},
|
|
63059
|
+
annotations: { readOnlyHint: true }
|
|
63060
|
+
},
|
|
63061
|
+
withErrorHandling(({ noteIdentifier, offset = 0, limit = 100 }) => {
|
|
63062
|
+
const registry2 = new AnchorRegistry();
|
|
63063
|
+
let anchors = registry2.load();
|
|
63064
|
+
if (noteIdentifier)
|
|
63065
|
+
anchors = anchors.filter((a) => a.noteIdentifier === noteIdentifier.toUpperCase());
|
|
63066
|
+
const slice = anchors.slice(offset, offset + limit);
|
|
63067
|
+
const next = offset + slice.length;
|
|
63068
|
+
return successResponse(`${anchors.length} anchor(s); returned ${slice.length}.`, {
|
|
63069
|
+
anchors: slice.map((a) => ({ ...a })),
|
|
63070
|
+
total: anchors.length,
|
|
63071
|
+
registry: registry2.path,
|
|
63072
|
+
page: {
|
|
63073
|
+
offset,
|
|
63074
|
+
returned: slice.length,
|
|
63075
|
+
hasMore: next < anchors.length,
|
|
63076
|
+
...next < anchors.length ? { nextOffset: next } : {}
|
|
63077
|
+
}
|
|
62171
63078
|
});
|
|
62172
|
-
}, "
|
|
63079
|
+
}, "Error listing paragraph anchors")
|
|
63080
|
+
);
|
|
63081
|
+
registerTool(
|
|
63082
|
+
"get-paragraph-anchor",
|
|
63083
|
+
{
|
|
63084
|
+
description: "Use when: inspecting one recorded anchor as stored.\nReturns: the stored anchor record.\nDo not use when: you need the paragraph's current location or link (resolve-paragraph-anchor).\nSafety: read-only; reads only the local anchor registry, never Notes.",
|
|
63085
|
+
inputSchema: { anchorId: anchorIdInput },
|
|
63086
|
+
outputSchema: { anchor: external_exports.record(external_exports.unknown()).optional() },
|
|
63087
|
+
annotations: { readOnlyHint: true }
|
|
63088
|
+
},
|
|
63089
|
+
withErrorHandling(({ anchorId }) => {
|
|
63090
|
+
const anchor = new AnchorRegistry().get(anchorId);
|
|
63091
|
+
return successResponse(
|
|
63092
|
+
`Anchor ${anchor.anchorId}: note ${anchor.noteIdentifier}, block ${anchor.blockIndex}, recorded ${anchor.createdAt}.`,
|
|
63093
|
+
{ anchor: { ...anchor } }
|
|
63094
|
+
);
|
|
63095
|
+
}, "Error reading paragraph anchor")
|
|
63096
|
+
);
|
|
63097
|
+
registerTool(
|
|
63098
|
+
"prune-paragraph-anchors",
|
|
63099
|
+
{
|
|
63100
|
+
description: "Use when: cleaning up anchors that no longer resolve, or removing specific anchors.\nReturns: the stale anchors found (anchorId, status, message), how many were examined, and the ids removed (none on a dry run).\nDo not use when: an anchor is only ambiguous or needs reminting; it still points at something.\nSafety: dryRun defaults to true, so nothing is removed until you call again with dryRun false. Removes entries from the local anchor registry only; never changes Notes. Without anchorIds it resolves every anchor in scope (Full Disk Access) and treats not-found and note-not-found as stale unless statuses says otherwise. A note in Recently Deleted gives note-deleted, which is not pruned by default because the note can be restored.",
|
|
63101
|
+
inputSchema: {
|
|
63102
|
+
anchorIds: external_exports.array(anchorIdInput).min(1).max(1e3).optional().describe("Remove exactly these anchors instead of resolving to find stale ones"),
|
|
63103
|
+
noteIdentifier: noteIdentifierInput.describe("Only anchors in the note with this Notes UUID"),
|
|
63104
|
+
statuses: external_exports.array(
|
|
63105
|
+
external_exports.enum([
|
|
63106
|
+
"not-found",
|
|
63107
|
+
"note-not-found",
|
|
63108
|
+
"note-deleted",
|
|
63109
|
+
"note-unreadable",
|
|
63110
|
+
"ambiguous",
|
|
63111
|
+
"low-confidence"
|
|
63112
|
+
])
|
|
63113
|
+
).min(1).optional().describe(
|
|
63114
|
+
`Resolution statuses treated as stale (default ${DEFAULT_PRUNE_STATUSES.join(", ")})`
|
|
63115
|
+
),
|
|
63116
|
+
dryRun: external_exports.boolean().optional().describe("Report without removing (default true)")
|
|
63117
|
+
},
|
|
63118
|
+
outputSchema: {
|
|
63119
|
+
dryRun: external_exports.boolean().optional(),
|
|
63120
|
+
examined: external_exports.number().optional(),
|
|
63121
|
+
stale: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
63122
|
+
removed: external_exports.array(external_exports.string()).optional()
|
|
63123
|
+
},
|
|
63124
|
+
annotations: { readOnlyHint: false, destructiveHint: true }
|
|
63125
|
+
},
|
|
63126
|
+
withErrorHandling(({ anchorIds, noteIdentifier, statuses, dryRun }) => {
|
|
63127
|
+
const result = pruneParagraphAnchors({ anchorIds, noteIdentifier, statuses, dryRun });
|
|
63128
|
+
return successResponse(
|
|
63129
|
+
result.dryRun ? `Dry run: ${result.stale.length} of ${result.examined} anchor(s) would be removed.` : `Removed ${result.removed.length} of ${result.examined} anchor(s).`,
|
|
63130
|
+
{ ...result }
|
|
63131
|
+
);
|
|
63132
|
+
}, "Error pruning paragraph anchors")
|
|
62173
63133
|
);
|
|
62174
63134
|
registerTool(
|
|
62175
63135
|
"get-note-structure",
|