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.
Files changed (3) hide show
  1. package/README.md +164 -4
  2. package/build/index.js +1038 -78
  3. 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, constants9) {
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, constants9);
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, constants9) {
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, constants9);
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, constants9) {
506
- this.code = optimizeExpr(this.code, names, constants9);
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, constants9) {
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, constants9))
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, constants9) {
593
+ optimizeNames(names, constants10) {
594
594
  var _a;
595
- this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants9);
596
- if (!(super.optimizeNames(names, constants9) || this.else))
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, constants9);
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, constants9) {
622
- if (!super.optimizeNames(names, constants9))
621
+ optimizeNames(names, constants10) {
622
+ if (!super.optimizeNames(names, constants10))
623
623
  return;
624
- this.iteration = optimizeExpr(this.iteration, names, constants9);
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, constants9) {
661
- if (!super.optimizeNames(names, constants9))
660
+ optimizeNames(names, constants10) {
661
+ if (!super.optimizeNames(names, constants10))
662
662
  return;
663
- this.iterable = optimizeExpr(this.iterable, names, constants9);
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, constants9) {
705
+ optimizeNames(names, constants10) {
706
706
  var _a, _b;
707
- super.optimizeNames(names, constants9);
708
- (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants9);
709
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants9);
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, constants9) {
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 = constants9[n.str];
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 && constants9[c.str] !== void 0);
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 = resolve9.call(this, root, ref);
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 resolve9(root, ref) {
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 resolve9(baseURI, relativeURI, options) {
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: resolve9,
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 constants9 = {
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 constants9) {
7477
- v = { value: constants9[c] };
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 join32(output, replacement);
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 = join32(output, rule.append(self.options));
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 join32(output, replacement) {
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((resolve9) => setTimeout(resolve9, pollInterval));
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((resolve9, reject) => {
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
- resolve9(parseResult.data);
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((resolve9, reject) => {
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(resolve9, interval);
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((resolve9) => setTimeout(resolve9, pollInterval));
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((resolve9) => {
38985
+ return new Promise((resolve10) => {
38986
38986
  const json2 = serializeMessage(message);
38987
38987
  if (this._stdout.write(json2)) {
38988
- resolve9();
38988
+ resolve10();
38989
38989
  } else {
38990
- this._stdout.once("drain", resolve9);
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 resolve9 = (pk, seen) => {
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 ? resolve9(row.parent, seen) : void 0;
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) resolve9(row.pk, /* @__PURE__ */ new Set());
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 = "", join32) => ({
52892
+ var wrap2 = (before, after = "", join33) => ({
52893
52893
  mode: "wrap",
52894
52894
  before,
52895
52895
  after,
52896
- ...join32 ? { join: join32 } : {}
52896
+ ...join33 ? { join: join33 } : {}
52897
52897
  });
52898
- var pattern = (value, join32) => ({
52898
+ var pattern = (value, join33) => ({
52899
52899
  mode: "pattern",
52900
52900
  value,
52901
- ...join32 ? { join: join32 } : {}
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 resolve9 = (qualified, isAttribute) => {
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 = resolve9(name, false);
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, ...resolve9(a.name, true) })),
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((resolve9) => resolveClosed = resolve9);
60800
+ const closed = new Promise((resolve10) => resolveClosed = resolve10);
60801
60801
  let closing;
60802
60802
  const close = () => {
60803
- closing ??= new Promise((resolve9) => {
60803
+ closing ??= new Promise((resolve10) => {
60804
60804
  if (idleTimer) clearTimeout(idleTimer);
60805
60805
  server2.close(() => {
60806
60806
  resolveClosed(closeReason);
60807
- resolve9();
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((resolve9, reject) => {
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
- resolve9();
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: read-only; reads the NoteStore database directly and requires Full Disk Access. 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.",
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
- { id: note.id, identifier: note.identifier, counts: note.counts, ...page }
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>&paragraphID=<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: read-only; never creates or changes a paragraph ID, so a paragraph without a unique ID cannot be linked. Requires Full Disk Access. A later edit in Notes can replace the ID and break the link.",
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>&paragraphID=<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 result = paragraphLink(note, { contains, match, blockIndex, occurrence });
62166
- return successResponse(`Paragraph link: ${result.url}`, {
62167
- url: result.url,
62168
- id: note.id,
62169
- identifier: note.identifier,
62170
- paragraph: { ...result.paragraph }
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
- }, "No paragraph link")
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",