apple-notes-mcp 2.12.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/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
@@ -60219,6 +60219,1494 @@ function registerPrivateHelperTools(server2, manager, depsFactory = () => defaul
60219
60219
  );
60220
60220
  }
60221
60221
 
60222
+ // src/utils/localServer.ts
60223
+ import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
60224
+ import { networkInterfaces } from "node:os";
60225
+ function isTailnetIPv4(address) {
60226
+ const match = /^100\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(address);
60227
+ if (!match) return false;
60228
+ const [second, third, fourth] = match.slice(1).map(Number);
60229
+ return second >= 64 && second <= 127 && third <= 255 && fourth <= 255;
60230
+ }
60231
+ function findTailnetAddress(interfaces = networkInterfaces()) {
60232
+ const candidates = [];
60233
+ for (const [name, infos] of Object.entries(interfaces)) {
60234
+ for (const info of infos ?? []) {
60235
+ if (info.family === "IPv4" && !info.internal && isTailnetIPv4(info.address))
60236
+ candidates.push({ address: info.address, interface: name });
60237
+ }
60238
+ }
60239
+ return candidates.find((c) => c.interface.startsWith("utun")) ?? candidates[0];
60240
+ }
60241
+ function newServerToken() {
60242
+ return randomBytes3(32).toString("hex");
60243
+ }
60244
+ function bearerToken(req) {
60245
+ const auth = req.headers.authorization;
60246
+ return auth?.startsWith("Bearer ") ? auth.slice(7) : void 0;
60247
+ }
60248
+ function tokenMatches(given, token) {
60249
+ if (!given) return false;
60250
+ const a = Buffer.from(given, "utf8");
60251
+ const b = Buffer.from(token, "utf8");
60252
+ return a.length === b.length && timingSafeEqual(a, b);
60253
+ }
60254
+ function hostAuthority(host, port) {
60255
+ return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
60256
+ }
60257
+ function crossOriginRefusal(req, origin, requireOrigin = false) {
60258
+ const site = req.headers["sec-fetch-site"];
60259
+ if (site && site !== "same-origin" && site !== "none") return "cross-site";
60260
+ const requestOrigin = req.headers.origin;
60261
+ if (requestOrigin !== void 0 && requestOrigin !== origin) return "foreign-origin";
60262
+ if (requireOrigin && requestOrigin !== origin) return "missing-origin";
60263
+ return void 0;
60264
+ }
60265
+
60266
+ // src/services/templateEditor.ts
60267
+ import { randomBytes as randomBytes4 } from "node:crypto";
60268
+ import { createServer } from "node:http";
60269
+
60270
+ // src/utils/templateSamples.ts
60271
+ function sampleBlock(text2, style = "body", extra = {}, runs = [{ text: text2 }]) {
60272
+ return {
60273
+ index: 0,
60274
+ start: 0,
60275
+ length: runs.reduce((sum, run) => sum + run.text.length, 0),
60276
+ text: runs.map((run) => run.text).join(""),
60277
+ style,
60278
+ styleType: null,
60279
+ indent: 0,
60280
+ alignment: "left",
60281
+ blockQuote: false,
60282
+ ...extra,
60283
+ runs: runs.map((run) => ({ start: 0, length: run.text.length, ...run })),
60284
+ attachments: []
60285
+ };
60286
+ }
60287
+ var sampleAttachmentRun = (id2, uti) => ({
60288
+ text: "\uFFFC",
60289
+ attachment: { id: id2, uti }
60290
+ });
60291
+ function sampleAttachment(id2, uti, extra = {}) {
60292
+ return { id: id2, pk: 0, uti, kind: classifyUti(uti), children: [], ...extra };
60293
+ }
60294
+ function sampleNote(pk, blocks, attachments = []) {
60295
+ let offset = 0;
60296
+ const indexed = blocks.map((b, index) => {
60297
+ const start = offset;
60298
+ offset += b.length + 1;
60299
+ let at = start;
60300
+ const runs = b.runs.map((run) => {
60301
+ const shifted = { ...run, start: at };
60302
+ at += run.length;
60303
+ return shifted;
60304
+ });
60305
+ const markers2 = runs.flatMap(
60306
+ (run) => run.attachment ? [{ ...run.attachment, start: run.start, blockIndex: index }] : []
60307
+ );
60308
+ return { ...b, index, start, runs, attachments: markers2 };
60309
+ });
60310
+ const markers = indexed.flatMap((b) => b.attachments);
60311
+ return {
60312
+ id: `x-coredata://SAMPLE/ICNote/p${pk}`,
60313
+ title: indexed[0]?.text ?? "",
60314
+ doc: {
60315
+ text: indexed.map((b) => b.text).join("\n"),
60316
+ textLength: offset,
60317
+ blocks: indexed,
60318
+ attachments: markers,
60319
+ undecodedFields: { attributeRun: {}, paragraphStyle: {} },
60320
+ summary: summarize(indexed, markers)
60321
+ },
60322
+ attachments: new Map(attachments.map((a) => [a.id, a])),
60323
+ ordered: attachments.map((a, i) => ({ ...a, pk: i + 1 }))
60324
+ };
60325
+ }
60326
+ function sampleMeta(uuid2) {
60327
+ return {
60328
+ uuid: uuid2,
60329
+ created: "2026-01-05T14:30:00.000Z",
60330
+ modified: "2026-02-10T09:15:00.000Z",
60331
+ folder: "Sample Folder",
60332
+ account: "iCloud"
60333
+ };
60334
+ }
60335
+ function templateSamples() {
60336
+ return [
60337
+ {
60338
+ id: "structure",
60339
+ label: "Headings, lists and checklists",
60340
+ meta: sampleMeta("00000000-0000-4000-8000-000000000001"),
60341
+ note: sampleNote(1, [
60342
+ sampleBlock("Weekly Plan", "title"),
60343
+ sampleBlock("Goals", "heading"),
60344
+ sampleBlock("Details", "subheading"),
60345
+ sampleBlock("A plain paragraph with *characters* that need escaping."),
60346
+ sampleBlock("First step", "numbered"),
60347
+ sampleBlock("Second step", "numbered"),
60348
+ sampleBlock("Nested step", "numbered", { indent: 1 }),
60349
+ sampleBlock("A bullet", "bulleted"),
60350
+ sampleBlock("A dash", "dashed"),
60351
+ sampleBlock("Nested bullet", "bulleted", { indent: 1 }),
60352
+ sampleBlock("Done task", "checklist", { checklist: { id: "sample-a", done: true } }),
60353
+ sampleBlock("Open task", "checklist", { checklist: { id: "sample-b", done: false } }),
60354
+ sampleBlock("A quoted line", "body", { blockQuote: true }),
60355
+ sampleBlock("const answer = 42;", "monospaced"),
60356
+ sampleBlock('console.log("sample");', "monospaced")
60357
+ ])
60358
+ },
60359
+ {
60360
+ id: "inline",
60361
+ label: "Inline formatting and links",
60362
+ meta: sampleMeta("00000000-0000-4000-8000-000000000002"),
60363
+ note: sampleNote(2, [
60364
+ sampleBlock("Formatting Sample", "title"),
60365
+ sampleBlock("", "body", {}, [
60366
+ { text: "Bold", bold: true },
60367
+ { text: ", " },
60368
+ { text: "italic", italic: true },
60369
+ { text: ", " },
60370
+ { text: "both", bold: true, italic: true },
60371
+ { text: ", " },
60372
+ { text: "struck", strikethrough: true },
60373
+ { text: ", " },
60374
+ { text: "underlined", underline: true },
60375
+ { text: "." }
60376
+ ]),
60377
+ sampleBlock("", "body", {}, [
60378
+ { text: "Highlighted", highlight: "purple" },
60379
+ { text: ", x" },
60380
+ { text: "2", superscript: true },
60381
+ { text: ", H" },
60382
+ { text: "2", subscript: true },
60383
+ { text: "O, and " },
60384
+ { text: "red text", color: "#FF3B30" },
60385
+ { text: "." }
60386
+ ]),
60387
+ sampleBlock("", "body", {}, [
60388
+ { text: "A " },
60389
+ { text: "web link", link: "https://example.com/page", linkSafe: true },
60390
+ { text: " and a " },
60391
+ { text: "bold link", link: "https://example.com/bold", linkSafe: true, bold: true },
60392
+ { text: "." }
60393
+ ])
60394
+ ])
60395
+ },
60396
+ {
60397
+ id: "attachments",
60398
+ label: "Attachments and tags",
60399
+ meta: sampleMeta("00000000-0000-4000-8000-000000000003"),
60400
+ note: sampleNote(
60401
+ 3,
60402
+ [
60403
+ sampleBlock("Trip Notes", "title"),
60404
+ sampleBlock("", "body", {}, [
60405
+ { text: "Tagged " },
60406
+ sampleAttachmentRun("SAMPLE-TAG", "com.apple.notes.inlinetextattachment.hashtag"),
60407
+ { text: " for later." }
60408
+ ]),
60409
+ sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-IMG", "public.jpeg")]),
60410
+ sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-PDF", "com.adobe.pdf")]),
60411
+ sampleBlock("", "body", {}, [
60412
+ sampleAttachmentRun("SAMPLE-DIV", "com.apple.notes.inlinetextattachment.dividerline")
60413
+ ]),
60414
+ sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-URL", "public.url")]),
60415
+ sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-IMGURL", "public.url")])
60416
+ ],
60417
+ [
60418
+ sampleAttachment("SAMPLE-TAG", "com.apple.notes.inlinetextattachment.hashtag", {
60419
+ altText: "#travel"
60420
+ }),
60421
+ sampleAttachment("SAMPLE-IMG", "public.jpeg", { title: "harbor.jpg" }),
60422
+ sampleAttachment("SAMPLE-PDF", "com.adobe.pdf", { title: "itinerary.pdf" }),
60423
+ sampleAttachment("SAMPLE-DIV", "com.apple.notes.inlinetextattachment.dividerline"),
60424
+ sampleAttachment("SAMPLE-URL", "public.url", {
60425
+ title: "Example Domain",
60426
+ url: "https://example.com/"
60427
+ }),
60428
+ sampleAttachment("SAMPLE-IMGURL", "public.url", {
60429
+ title: "Harbor at dusk",
60430
+ url: "https://example.com/photos/harbor.jpg"
60431
+ })
60432
+ ]
60433
+ )
60434
+ }
60435
+ ];
60436
+ }
60437
+
60438
+ // src/services/templateEditorPage.ts
60439
+ var EDITOR_STYLE = `
60440
+ :root { color-scheme: light dark; --bg: #fafafa; --fg: #1d1d1f; --muted: #6e6e73;
60441
+ --panel: #ffffff; --border: #d2d2d7; --accent: #0a66d8; --bad: #c1272d; --ok: #1f7a3a; }
60442
+ @media (prefers-color-scheme: dark) { :root { --bg: #1c1c1e; --fg: #f2f2f7; --muted: #a1a1a6;
60443
+ --panel: #2c2c2e; --border: #3a3a3c; --accent: #4c9bff; --bad: #ff6b6b; --ok: #4cd07d; } }
60444
+ * { box-sizing: border-box; }
60445
+ body { margin: 0; font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
60446
+ background: var(--bg); color: var(--fg); }
60447
+ header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; padding: 10px 16px;
60448
+ border-bottom: 1px solid var(--border); background: var(--panel); }
60449
+ header h1 { font-size: 15px; margin: 0 12px 0 0; }
60450
+ label { display: inline-flex; gap: 6px; align-items: center; color: var(--muted); }
60451
+ select, input, button, textarea { font: inherit; color: var(--fg); background: var(--bg);
60452
+ border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; }
60453
+ button { background: var(--accent); color: #fff; border-color: var(--accent); cursor: pointer; }
60454
+ main { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px 16px;
60455
+ height: calc(100vh - 110px); min-height: 360px; }
60456
+ @media (max-width: 800px) { main { grid-template-columns: 1fr; height: auto; } }
60457
+ section { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
60458
+ h2 { font-size: 13px; margin: 0 0 6px; color: var(--muted); font-weight: 600; }
60459
+ textarea { flex: 1; min-height: 300px; width: 100%; resize: none;
60460
+ font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
60461
+ pre { flex: 1; margin: 0; overflow: auto; padding: 8px; background: var(--panel);
60462
+ border: 1px solid var(--border); border-radius: 6px; min-height: 300px;
60463
+ font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
60464
+ ul { margin: 6px 0 0; padding-left: 18px; max-height: 120px; overflow: auto; }
60465
+ .bad { color: var(--bad); } .ok { color: var(--ok); } .muted { color: var(--muted); }
60466
+ footer { padding: 0 16px 12px; }
60467
+ `;
60468
+ var EDITOR_SCRIPT = `
60469
+ "use strict";
60470
+ const token = new URLSearchParams(location.search).get("token") || "";
60471
+ const $ = (id) => document.getElementById(id);
60472
+ const api = async (path, body) => {
60473
+ const res = await fetch(path, body === undefined
60474
+ ? { headers: { Authorization: "Bearer " + token } }
60475
+ : { method: "POST", headers: { Authorization: "Bearer " + token,
60476
+ "Content-Type": "application/json" }, body: JSON.stringify(body) });
60477
+ const data = await res.json().catch(() => ({}));
60478
+ if (!res.ok) { const e = new Error((data.error && data.error.message) || res.statusText);
60479
+ e.detail = data.error; throw e; }
60480
+ return data;
60481
+ };
60482
+ const setText = (id, text, cls) => { const el = $(id); el.textContent = text;
60483
+ el.className = cls || ""; };
60484
+ const list = (id, items) => { const ul = $(id); ul.replaceChildren();
60485
+ for (const item of items) { const li = document.createElement("li");
60486
+ li.textContent = item; ul.append(li); } };
60487
+ let timer;
60488
+ async function refresh() {
60489
+ try {
60490
+ const out = await api("/api/preview", { template: $("json").value, sample: $("sample").value });
60491
+ if (out.valid) setText("status", "Valid template.", "ok");
60492
+ else setText("status", out.errors.length + " problem(s):", "bad");
60493
+ list("errors", out.errors.map((e) => e.path + ": " + e.message));
60494
+ list("warnings", out.warnings.map((w) => w.code + (w.attachmentId ? " (" + w.attachmentId + ")" : "")));
60495
+ $("preview").textContent = out.valid ? out.markdown : "";
60496
+ } catch (e) { setText("status", "Preview failed: " + e.message, "bad"); }
60497
+ }
60498
+ const schedule = () => { clearTimeout(timer); timer = setTimeout(refresh, 250); };
60499
+ async function load(name) {
60500
+ try {
60501
+ const out = await api("/api/template?name=" + encodeURIComponent(name));
60502
+ $("json").value = JSON.stringify(out.template, null, 2) + "\\n";
60503
+ $("name").value = out.source === "saved" ? out.name : "";
60504
+ refresh();
60505
+ } catch (e) { setText("status", "Could not open " + name + ": " + e.message, "bad"); }
60506
+ }
60507
+ async function save() {
60508
+ try {
60509
+ const out = await api("/api/save", { name: $("name").value.trim(),
60510
+ template: $("json").value, force: $("force").checked });
60511
+ setText("saved", (out.replaced ? "Replaced " : "Saved ") + out.name + " at " + out.path, "ok");
60512
+ $("force").checked = false;
60513
+ await fillPicker(out.name);
60514
+ } catch (e) {
60515
+ const errors = (e.detail && e.detail.errors) || [];
60516
+ setText("saved", e.message + (errors.length ? " " + errors.map((x) => x.path + ": " + x.message).join("; ") : ""), "bad");
60517
+ }
60518
+ }
60519
+ async function fillPicker(selected) {
60520
+ const state = await api("/api/state");
60521
+ const picker = $("picker"); picker.replaceChildren();
60522
+ const group = (label, names) => { const g = document.createElement("optgroup");
60523
+ g.label = label; for (const n of names) { const o = document.createElement("option");
60524
+ o.value = n; o.textContent = n; g.append(o); } picker.append(g); };
60525
+ group("Built-in", state.builtins);
60526
+ if (state.saved.length) group("Saved", state.saved);
60527
+ picker.value = selected;
60528
+ setText("dir", "Library: " + state.dir, "muted");
60529
+ return state;
60530
+ }
60531
+ (async () => {
60532
+ try {
60533
+ const state = await fillPicker("");
60534
+ const sample = $("sample");
60535
+ for (const s of state.samples) { const o = document.createElement("option");
60536
+ o.value = s.id; o.textContent = s.label; sample.append(o); }
60537
+ $("picker").value = state.initial.name;
60538
+ $("json").value = JSON.stringify(state.initial.template, null, 2) + "\\n";
60539
+ $("name").value = state.initial.source === "saved" ? state.initial.name : "";
60540
+ $("json").addEventListener("input", schedule);
60541
+ sample.addEventListener("change", refresh);
60542
+ $("picker").addEventListener("change", () => load($("picker").value));
60543
+ $("save").addEventListener("click", save);
60544
+ refresh();
60545
+ } catch (e) { setText("status", "Could not start: " + e.message, "bad"); }
60546
+ })();
60547
+ `;
60548
+ var EDITOR_BODY = `
60549
+ <header>
60550
+ <h1>Markdown template editor</h1>
60551
+ <label>Open <select id="picker"></select></label>
60552
+ <label>Preview <select id="sample"></select></label>
60553
+ <span id="dir" class="muted"></span>
60554
+ </header>
60555
+ <main>
60556
+ <section>
60557
+ <h2>Template JSON</h2>
60558
+ <textarea id="json" spellcheck="false" aria-label="Template JSON"></textarea>
60559
+ <div id="status" role="status"></div>
60560
+ <ul id="errors" class="bad"></ul>
60561
+ </section>
60562
+ <section>
60563
+ <h2>Markdown preview</h2>
60564
+ <pre id="preview" aria-label="Markdown preview"></pre>
60565
+ <ul id="warnings" class="muted" aria-label="Warnings"></ul>
60566
+ </section>
60567
+ </main>
60568
+ <footer>
60569
+ <label>Save as <input id="name" placeholder="my-template" maxlength="64" autocomplete="off"></label>
60570
+ <label><input id="force" type="checkbox"> Replace an existing template</label>
60571
+ <button id="save" type="button">Save</button>
60572
+ <span id="saved" role="status"></span>
60573
+ </footer>
60574
+ `;
60575
+ function templateEditorPage(nonce) {
60576
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="referrer" content="no-referrer"><title>Template Editor</title><style nonce="${nonce}">${EDITOR_STYLE}</style></head><body>${EDITOR_BODY}<script nonce="${nonce}">${EDITOR_SCRIPT}</script></body></html>`;
60577
+ }
60578
+
60579
+ // src/services/templateEditor.ts
60580
+ var DEFAULT_EDITOR_IDLE_MS = 30 * 60 * 1e3;
60581
+ var MAX_EDITOR_BODY_BYTES = MAX_TEMPLATE_BYTES * 2 + 4096;
60582
+ var EditorHttpError = class extends Error {
60583
+ constructor(status, code, message, errors) {
60584
+ super(message);
60585
+ this.status = status;
60586
+ this.code = code;
60587
+ this.errors = errors;
60588
+ }
60589
+ status;
60590
+ code;
60591
+ errors;
60592
+ };
60593
+ function editorPresentedToken(req, url) {
60594
+ return bearerToken(req)?.trim() ?? url.searchParams.get("token") ?? void 0;
60595
+ }
60596
+ var CROSS_ORIGIN_MESSAGE = {
60597
+ "cross-site": "Cross-site requests are refused.",
60598
+ "foreign-origin": "Cross-origin requests are refused.",
60599
+ "missing-origin": "POST requires the editor's own Origin."
60600
+ };
60601
+ async function readEditorJson(req) {
60602
+ const type = req.headers["content-type"] ?? "";
60603
+ if (!/^application\/json(?:\s*;|$)/i.test(type))
60604
+ throw new EditorHttpError(415, "unsupported-media-type", "Send JSON (application/json).");
60605
+ const chunks = [];
60606
+ let size = 0;
60607
+ for await (const chunk of req) {
60608
+ size += chunk.length;
60609
+ if (size > MAX_EDITOR_BODY_BYTES)
60610
+ throw new EditorHttpError(413, "too-large", "Request body too large.");
60611
+ chunks.push(chunk);
60612
+ }
60613
+ let parsed;
60614
+ try {
60615
+ parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
60616
+ } catch {
60617
+ throw new EditorHttpError(400, "bad-request", "Request body is not valid JSON.");
60618
+ }
60619
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
60620
+ throw new EditorHttpError(400, "bad-request", "Request body must be a JSON object.");
60621
+ return parsed;
60622
+ }
60623
+ var editorStringField = (body, key) => {
60624
+ const value = body[key];
60625
+ if (typeof value !== "string")
60626
+ throw new EditorHttpError(400, "bad-request", `"${key}" must be a string.`);
60627
+ return value;
60628
+ };
60629
+ async function startTemplateEditor(options = {}) {
60630
+ const host = options.host ?? "127.0.0.1";
60631
+ const idleMs = options.idleMs ?? DEFAULT_EDITOR_IDLE_MS;
60632
+ const store = options.store ?? new TemplateStore();
60633
+ const token = options.token ?? newServerToken();
60634
+ const samples = templateSamples();
60635
+ const initialName = options.name ?? "standard-markdown";
60636
+ const initial = isBuiltinTemplate(initialName) ? builtinTemplate(initialName) : store.get(initialName);
60637
+ const initialSource = isBuiltinTemplate(initialName) ? "builtin" : "saved";
60638
+ let origin = "";
60639
+ let hostHeader = "";
60640
+ let idleTimer;
60641
+ let closeReason = "closed";
60642
+ const sockets = /* @__PURE__ */ new Set();
60643
+ const loadTemplate = (name) => {
60644
+ if (isBuiltinTemplate(name))
60645
+ return { name, source: "builtin", template: builtinTemplate(name) };
60646
+ return { name, source: "saved", template: store.get(name) };
60647
+ };
60648
+ const state = () => {
60649
+ const listing = store.list();
60650
+ return {
60651
+ builtins: BUILTIN_TEMPLATE_NAMES,
60652
+ saved: listing.templates.map((t) => t.name),
60653
+ dir: listing.dir,
60654
+ samples: [
60655
+ ...samples.map((s) => ({ id: s.id, label: s.label })),
60656
+ ...options.note ? [{ id: "note", label: `Note: ${options.note.note.title}` }] : []
60657
+ ],
60658
+ initial: { name: initialName, source: initialSource, template: initial }
60659
+ };
60660
+ };
60661
+ const preview = (body) => {
60662
+ const text2 = editorStringField(body, "template");
60663
+ const sampleId = typeof body.sample === "string" ? body.sample : samples[0].id;
60664
+ const source = sampleId === "note" && options.note ? options.note : samples.find((s) => s.id === sampleId);
60665
+ if (!source) throw new EditorHttpError(400, "bad-request", `Unknown sample "${sampleId}".`);
60666
+ let portable;
60667
+ try {
60668
+ portable = parseTemplate(text2);
60669
+ } catch (error2) {
60670
+ if (error2 instanceof TemplateValidationError)
60671
+ return { valid: false, errors: error2.errors, markdown: "", warnings: [] };
60672
+ throw error2;
60673
+ }
60674
+ const result = renderNotesWithTemplate(
60675
+ [source.note],
60676
+ { stats: emptyStats() },
60677
+ {
60678
+ template: resolveTemplate(portable),
60679
+ exportStem: "preview",
60680
+ metaFor: () => source.meta
60681
+ }
60682
+ );
60683
+ return { valid: true, errors: [], markdown: result.markdown, warnings: result.warnings };
60684
+ };
60685
+ const save = (body) => {
60686
+ const name = editorStringField(body, "name");
60687
+ const text2 = editorStringField(body, "template");
60688
+ const force = body.force === true;
60689
+ try {
60690
+ const saved = store.save(name, text2, { force });
60691
+ return { name, path: saved.path, bytes: saved.bytes, replaced: saved.replaced };
60692
+ } catch (error2) {
60693
+ if (error2 instanceof TemplateValidationError)
60694
+ throw new EditorHttpError(
60695
+ 422,
60696
+ "invalid-template",
60697
+ "The template is invalid.",
60698
+ error2.errors
60699
+ );
60700
+ if (error2 instanceof TemplateStoreError && error2.code === "template-exists")
60701
+ throw new EditorHttpError(
60702
+ 409,
60703
+ error2.code,
60704
+ `A template named "${name}" already exists. Tick "Replace an existing template" to replace it.`
60705
+ );
60706
+ if (error2 instanceof TemplateStoreError)
60707
+ throw new EditorHttpError(400, error2.code, error2.message);
60708
+ throw error2;
60709
+ }
60710
+ };
60711
+ const send = (res, status, type, body, extra = {}) => {
60712
+ res.writeHead(status, {
60713
+ "Content-Type": type,
60714
+ "Cache-Control": "no-store",
60715
+ "Referrer-Policy": "no-referrer",
60716
+ "X-Content-Type-Options": "nosniff",
60717
+ "X-Frame-Options": "DENY",
60718
+ "Cross-Origin-Resource-Policy": "same-origin",
60719
+ "Cross-Origin-Opener-Policy": "same-origin",
60720
+ ...extra
60721
+ });
60722
+ res.end(body);
60723
+ };
60724
+ const sendJson = (res, status, value) => send(res, status, "application/json; charset=utf-8", JSON.stringify(value));
60725
+ const touch = () => {
60726
+ if (idleTimer) clearTimeout(idleTimer);
60727
+ if (idleMs > 0)
60728
+ idleTimer = setTimeout(() => {
60729
+ closeReason = "idle";
60730
+ void close();
60731
+ }, idleMs);
60732
+ };
60733
+ const handle = async (req, res) => {
60734
+ if (req.headers.host !== hostHeader)
60735
+ throw new EditorHttpError(421, "wrong-host", "Unexpected Host header.");
60736
+ const url = new URL(req.url ?? "/", origin);
60737
+ if (!tokenMatches(editorPresentedToken(req, url), token))
60738
+ throw new EditorHttpError(401, "unauthorized", "Missing or wrong token.");
60739
+ const refusal = crossOriginRefusal(req, origin, req.method === "POST");
60740
+ if (refusal) throw new EditorHttpError(403, "cross-origin", CROSS_ORIGIN_MESSAGE[refusal]);
60741
+ touch();
60742
+ const route = `${req.method} ${url.pathname}`;
60743
+ switch (route) {
60744
+ case "GET /": {
60745
+ const nonce = randomBytes4(16).toString("base64");
60746
+ return send(res, 200, "text/html; charset=utf-8", templateEditorPage(nonce), {
60747
+ "Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`
60748
+ });
60749
+ }
60750
+ case "GET /api/state":
60751
+ return sendJson(res, 200, state());
60752
+ case "GET /api/template": {
60753
+ const name = url.searchParams.get("name") ?? "";
60754
+ try {
60755
+ return sendJson(res, 200, loadTemplate(name));
60756
+ } catch (error2) {
60757
+ if (error2 instanceof TemplateStoreError)
60758
+ throw new EditorHttpError(404, error2.code, error2.message);
60759
+ if (error2 instanceof TemplateValidationError)
60760
+ throw new EditorHttpError(
60761
+ 422,
60762
+ "invalid-template",
60763
+ "The saved template is invalid.",
60764
+ error2.errors
60765
+ );
60766
+ throw error2;
60767
+ }
60768
+ }
60769
+ case "POST /api/preview":
60770
+ return sendJson(res, 200, preview(await readEditorJson(req)));
60771
+ case "POST /api/save":
60772
+ return sendJson(res, 200, save(await readEditorJson(req)));
60773
+ default:
60774
+ throw new EditorHttpError(404, "not-found", "Not found.");
60775
+ }
60776
+ };
60777
+ const server2 = createServer((req, res) => {
60778
+ handle(req, res).catch((error2) => {
60779
+ const http = error2 instanceof EditorHttpError ? error2 : new EditorHttpError(
60780
+ 500,
60781
+ "internal-error",
60782
+ "The editor could not complete the request."
60783
+ );
60784
+ if (!res.headersSent)
60785
+ sendJson(res, http.status, {
60786
+ error: {
60787
+ code: http.code,
60788
+ message: http.message,
60789
+ ...http.errors ? { errors: http.errors } : {}
60790
+ }
60791
+ });
60792
+ else res.end();
60793
+ });
60794
+ });
60795
+ server2.on("connection", (socket) => {
60796
+ sockets.add(socket);
60797
+ socket.on("close", () => sockets.delete(socket));
60798
+ });
60799
+ let resolveClosed;
60800
+ const closed = new Promise((resolve10) => resolveClosed = resolve10);
60801
+ let closing;
60802
+ const close = () => {
60803
+ closing ??= new Promise((resolve10) => {
60804
+ if (idleTimer) clearTimeout(idleTimer);
60805
+ server2.close(() => {
60806
+ resolveClosed(closeReason);
60807
+ resolve10();
60808
+ });
60809
+ for (const socket of sockets) socket.destroy();
60810
+ });
60811
+ return closing;
60812
+ };
60813
+ await new Promise((resolve10, reject) => {
60814
+ server2.once("error", reject);
60815
+ server2.listen({ host, port: options.port ?? 0, exclusive: true }, () => {
60816
+ server2.off("error", reject);
60817
+ resolve10();
60818
+ });
60819
+ });
60820
+ const port = server2.address().port;
60821
+ const authority = hostAuthority(host, port);
60822
+ origin = `http://${authority}`;
60823
+ hostHeader = authority;
60824
+ touch();
60825
+ return { url: `${origin}/?token=${token}`, host, port, token, closed, close };
60826
+ }
60827
+
60828
+ // src/services/templateEditorCli.ts
60829
+ var TEMPLATES_USAGE = `Usage: apple-notes-mcp templates edit [name] [options]
60830
+
60831
+ Open a local web editor for Markdown export templates. The editor validates
60832
+ and previews as you type, against built-in sample notes, and saves into the
60833
+ template library (create-only unless you tick "replace").
60834
+
60835
+ name Template to open: standard-markdown, obsidian, or a saved name
60836
+ --port N Port to listen on (default: a free port)
60837
+ --idle-minutes N Stop after N minutes without a request (default 30; 0: never)
60838
+ --note ID Also preview one real note (x-coredata://.../ICNote/pN), read-only
60839
+ --tailnet Listen on this Mac's Tailscale address instead of 127.0.0.1,
60840
+ so other devices on your tailnet can open the editor.
60841
+ Anyone on the tailnet who has the printed URL can read and save
60842
+ templates, and with --note can read the full body of that note.
60843
+
60844
+ Every request needs the per-run token in the printed URL. Press Ctrl-C to stop.
60845
+ `;
60846
+ function parseTemplatesArgs(argv) {
60847
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) return "help";
60848
+ const [command, ...rest] = argv;
60849
+ if (command !== "edit") throw new Error(`Unknown templates command "${command}".`);
60850
+ const args = {
60851
+ idleMinutes: DEFAULT_EDITOR_IDLE_MS / 6e4,
60852
+ tailnet: false
60853
+ };
60854
+ const number3 = (flag3, value, max) => {
60855
+ if (value === void 0 || !/^\d+$/.test(value) || Number(value) > max)
60856
+ throw new Error(`${flag3} needs a whole number from 0 to ${max}.`);
60857
+ return Number(value);
60858
+ };
60859
+ for (let i = 0; i < rest.length; i++) {
60860
+ const arg = rest[i];
60861
+ if (arg === "--port") args.port = number3(arg, rest[++i], 65535);
60862
+ else if (arg === "--idle-minutes") args.idleMinutes = number3(arg, rest[++i], 24 * 60);
60863
+ else if (arg === "--note") {
60864
+ const id2 = rest[++i];
60865
+ if (!id2 || !/^x-coredata:\/\/[^/\s]+\/ICNote\/p[0-9]{1,18}$/.test(id2))
60866
+ throw new Error("--note needs a note id such as x-coredata://\u2026/ICNote/p123.");
60867
+ args.noteId = id2;
60868
+ } else if (arg === "--tailnet") args.tailnet = true;
60869
+ else if (arg.startsWith("-")) throw new Error(`Unknown option ${arg}.`);
60870
+ else if (args.name === void 0) args.name = arg;
60871
+ else throw new Error(`Unexpected argument "${arg}".`);
60872
+ }
60873
+ return args;
60874
+ }
60875
+ var editorSignals = (stop) => {
60876
+ process.once("SIGINT", stop);
60877
+ process.once("SIGTERM", stop);
60878
+ return () => {
60879
+ process.off("SIGINT", stop);
60880
+ process.off("SIGTERM", stop);
60881
+ };
60882
+ };
60883
+ function tailnetWarning(host, withNote) {
60884
+ const lines = [
60885
+ `Listening on the tailnet address ${host}. Any device on your tailnet that has this URL can read and save templates. Keep the URL private.`,
60886
+ withNote ? "--note is set: the full body of that note is shown in the preview, so anyone with this URL can read the entire note. Stop the editor when you are done." : "--note exposes the full note body to anyone with this URL, so do not combine it with --tailnet unless you want that."
60887
+ ];
60888
+ return `${lines.join("\n")}
60889
+ `;
60890
+ }
60891
+ async function runTemplatesCommand(argv, deps = {}) {
60892
+ const out = deps.out ?? ((text2) => process.stdout.write(text2));
60893
+ const err = deps.err ?? ((text2) => process.stderr.write(text2));
60894
+ let args;
60895
+ try {
60896
+ args = parseTemplatesArgs(argv);
60897
+ } catch (error2) {
60898
+ err(`${error2.message}
60899
+
60900
+ ${TEMPLATES_USAGE}`);
60901
+ return 2;
60902
+ }
60903
+ if (args === "help") {
60904
+ out(TEMPLATES_USAGE);
60905
+ return 0;
60906
+ }
60907
+ let host = "127.0.0.1";
60908
+ if (args.tailnet) {
60909
+ const found = (deps.tailnetAddress ?? findTailnetAddress)();
60910
+ if (!found) {
60911
+ err(
60912
+ "No Tailscale address found (no 100.64.0.0/10 IPv4 address on this Mac). Connect Tailscale first, or run without --tailnet.\n"
60913
+ );
60914
+ return 1;
60915
+ }
60916
+ host = found.address;
60917
+ }
60918
+ let handle;
60919
+ try {
60920
+ const note = args.noteId ? (deps.readNote ?? ((id2) => ({ note: readExportNote(id2), meta: readExportNoteMeta(id2) })))(args.noteId) : void 0;
60921
+ handle = await (deps.start ?? startTemplateEditor)({
60922
+ host,
60923
+ port: args.port,
60924
+ idleMs: args.idleMinutes * 6e4,
60925
+ name: args.name,
60926
+ note
60927
+ });
60928
+ } catch (error2) {
60929
+ err(`Could not start the template editor: ${error2.message}
60930
+ `);
60931
+ return 1;
60932
+ }
60933
+ out(`Template editor: ${handle.url}
60934
+ `);
60935
+ if (args.tailnet) err(tailnetWarning(handle.host, args.noteId !== void 0));
60936
+ err(
60937
+ args.idleMinutes > 0 ? `Press Ctrl-C to stop. Stops by itself after ${args.idleMinutes} idle minute(s).
60938
+ ` : "Press Ctrl-C to stop.\n"
60939
+ );
60940
+ const unregister = (deps.onSignal ?? editorSignals)(() => void handle.close());
60941
+ const reason = await handle.closed;
60942
+ unregister();
60943
+ err(
60944
+ reason === "idle" ? "Template editor stopped after the idle timeout.\n" : "Template editor stopped.\n"
60945
+ );
60946
+ return 0;
60947
+ }
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
+
60222
61710
  // src/index.ts
60223
61711
  loadFileConfig();
60224
61712
  var require2 = createRequire(import.meta.url);
@@ -60264,6 +61752,14 @@ if (process.argv[2] === "setup") {
60264
61752
  process.stdout.write(formatShortcutSetup(report) + "\n");
60265
61753
  process.exit(report.ready || !report.checkOnly ? 0 : 1);
60266
61754
  }
61755
+ if (process.argv[2] === "templates") {
61756
+ process.exit(await runTemplatesCommand(process.argv.slice(3)));
61757
+ }
61758
+ if (process.argv[2] === "anchors") {
61759
+ process.exit(
61760
+ await runAnchorsCli(process.argv.slice(3), { resolve: registryLookup(new AnchorRegistry()) })
61761
+ );
61762
+ }
60267
61763
  var server = new McpServer({
60268
61764
  name: "apple-notes",
60269
61765
  version: version2,
@@ -61370,6 +62866,9 @@ registerTool(
61370
62866
  );
61371
62867
  }, "Error reading note blocks")
61372
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
+ );
61373
62872
  var paragraphNoteSelector = {
61374
62873
  id: noteIdInput.optional().describe(`Exact note ID (${NOTE_ID_FORMS}); give id or title`),
61375
62874
  title: external_exports.string().min(1).max(MAX.TITLE).optional().describe("Exact note title; must match one note unless folder narrows it"),
@@ -61378,41 +62877,57 @@ var paragraphNoteSelector = {
61378
62877
  registerTool(
61379
62878
  "list-note-paragraphs",
61380
62879
  {
61381
- 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.",
61382
62881
  inputSchema: {
61383
62882
  ...paragraphNoteSelector,
61384
62883
  linkableOnly: external_exports.boolean().optional().describe("Return only paragraphs that have a direct url (default false)"),
61385
62884
  offset: external_exports.number().int().min(0).optional().describe("Index of the first paragraph to return (default 0); use page.nextOffset"),
61386
- 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
61387
62887
  },
61388
62888
  outputSchema: {
61389
62889
  id: external_exports.string().optional(),
61390
62890
  identifier: external_exports.string().nullable().optional(),
61391
62891
  counts: external_exports.record(external_exports.unknown()).optional(),
61392
62892
  paragraphs: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
61393
- page: external_exports.record(external_exports.unknown()).optional()
62893
+ page: external_exports.record(external_exports.unknown()).optional(),
62894
+ anchorsRecorded: external_exports.number().optional()
61394
62895
  },
61395
- annotations: { readOnlyHint: true }
62896
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
61396
62897
  },
61397
- withErrorHandling(({ id: id2, title, folder, linkableOnly, offset, limit }) => {
62898
+ withErrorHandling(({ id: id2, title, folder, linkableOnly, offset, limit, recordAnchors }) => {
61398
62899
  const note = readNoteParagraphs({ id: id2, title, folder });
61399
62900
  const page = pageParagraphs(note.paragraphs, {
61400
62901
  offset,
61401
- limit,
62902
+ limit: recordAnchors ? Math.min(limit ?? 500, MAX_ANCHORS_PER_CALL) : limit,
61402
62903
  linkableOnly,
61403
62904
  maxBytes: blocksMaxResponseBytes()
61404
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
+ }
61405
62913
  const { unique, shared, missing } = note.counts;
61406
62914
  return successResponse(
61407
- `${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}` : "") + ".",
61408
- { 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
+ }
61409
62924
  );
61410
62925
  }, "Error listing paragraphs")
61411
62926
  );
61412
62927
  registerTool(
61413
62928
  "get-paragraph-link",
61414
62929
  {
61415
- 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.",
61416
62931
  inputSchema: {
61417
62932
  ...paragraphNoteSelector,
61418
62933
  contains: external_exports.string().min(1).max(MAX.CONTENT).optional().describe(
@@ -61420,26 +62935,201 @@ registerTool(
61420
62935
  ),
61421
62936
  match: external_exports.string().min(1).max(MAX.CONTENT).optional().describe("The whole paragraph text, compared the same way"),
61422
62937
  blockIndex: external_exports.number().int().min(0).optional().describe("The paragraph's blockIndex from list-note-paragraphs"),
61423
- 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
+ )
61424
62942
  },
61425
62943
  outputSchema: {
61426
62944
  url: external_exports.string().optional(),
61427
62945
  id: external_exports.string().optional(),
61428
62946
  identifier: external_exports.string().nullable().optional(),
61429
- paragraph: external_exports.record(external_exports.unknown()).optional()
62947
+ paragraph: external_exports.record(external_exports.unknown()).optional(),
62948
+ anchorId: external_exports.string().optional()
61430
62949
  },
61431
- 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 }
61432
62993
  },
61433
62994
  withErrorHandling(({ id: id2, title, folder, contains, match, blockIndex, occurrence }) => {
61434
62995
  const note = readNoteParagraphs({ id: id2, title, folder });
61435
- const result = paragraphLink(note, { contains, match, blockIndex, occurrence });
61436
- return successResponse(`Paragraph link: ${result.url}`, {
61437
- url: result.url,
61438
- id: note.id,
61439
- identifier: note.identifier,
61440
- 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
+ }
61441
63078
  });
61442
- }, "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")
61443
63133
  );
61444
63134
  registerTool(
61445
63135
  "get-note-structure",