apple-notes-mcp 2.8.4 → 2.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -8
  2. package/build/index.js +699 -146
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -286,10 +286,11 @@ Retrieves the full content of a specific note.
286
286
 
287
287
  **Returns:** The HTML content of the note, its exact `id`, and a
288
288
  `contentHash`. Pass that hash back as `expectedContentHash` for a later update,
289
- append, or delete; the write is rejected if the note changed after this read.
290
- The `structuredContent` also includes `hashtags` any inline `#hashtag` tags parsed
291
- from the body. Apple Notes tags are inline hashtags, not a scriptable property;
292
- see [docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md#tags--hashtags-29). Smart Folders are not scriptable.
289
+ append, or delete; the write is rejected if the note's body or rich metadata
290
+ changed after this read. With Full Disk Access, embedded URLs omitted by
291
+ AppleScript are restored and returned in `links`. The response also reports
292
+ actual `nativeTags`, `richContentComplete`, and `writable`. Textual `hashtags`
293
+ remain a separate field and are not proof that Notes registered native tags.
293
294
 
294
295
  **⚠️ The returned body can be lossy — do not write it back verbatim.** Inline
295
296
  base64 images larger than `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` (default
@@ -302,6 +303,26 @@ attachment-bearing notes; edit them in Notes.app.
302
303
 
303
304
  ---
304
305
 
306
+ #### `get-native-objects`
307
+
308
+ Reads native object identities and ranges, checklist item IDs and state, actual
309
+ native tags, and native table data from one exact note ID. Table output includes
310
+ stable row and column identifiers. `tableCellsComplete` is false when Notes
311
+ metadata cannot be decoded completely. This tool is read-only and requires Full
312
+ Disk Access.
313
+
314
+ ---
315
+
316
+ #### `list-native-tags`
317
+
318
+ Lists actual native Notes tags used within one explicit `account` and `folder`,
319
+ mapping each tag to its matching note IDs. This differs from textual hashtag
320
+ search. The response reports `complete: false` and per-note errors when some
321
+ native metadata is unavailable. This tool is read-only and requires Full Disk
322
+ Access.
323
+
324
+ ---
325
+
305
326
  #### `get-note-plaintext`
306
327
 
307
328
  Retrieves a note's body as plain text, with no HTML markup.
@@ -385,6 +406,7 @@ Updates an existing note's content and/or title.
385
406
  | `newTitle` | string | No | New title (if changing the title; ignored when `format` is `"html"`) |
386
407
  | `newContent` | string | Yes | New content for the note body |
387
408
  | `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content replaces the entire note body as raw HTML and `newTitle` is ignored (the first HTML element serves as the title) |
409
+ | `allowLinkChanges` | boolean | No | Set to `true` only when intentionally changing or removing existing links |
388
410
 
389
411
  Title-only updates are rejected because Apple Notes titles are not unique.
390
412
 
@@ -414,8 +436,10 @@ byte-identical rich formatting.
414
436
 
415
437
  **Note:** `newContent` **replaces the entire note body** — it is not appended. To add to a note, prefer [`append-to-note`](#append-to-note), which does the read-and-concatenate for you and always round-trips the body as HTML. If you do read-modify-write by hand, note that `get-note-content` replaces oversized inline images with text placeholders (see [`get-note-content`](#get-note-content)) — writing that body back bakes the placeholders in.
416
438
 
417
- **Attachments:** `update-note` refuses to replace any note that contains an
418
- attachment. Edit those notes in Notes.app or create a separate note instead.
439
+ **Rich-content safety:** `update-note` refuses to replace a note when its rich
440
+ metadata is unavailable or it contains attachments, native tags, inline
441
+ objects, or checklists that AppleScript cannot preserve. Existing link
442
+ destinations must remain present unless `allowLinkChanges` is explicitly set.
419
443
 
420
444
  ---
421
445
 
@@ -509,8 +533,9 @@ Title-only appends are rejected.
509
533
  visible text after saving, not byte-identical rich formatting. Warns when the
510
534
  note is shared with collaborators.
511
535
 
512
- **Safety:** The append is rejected if the note changed since it was read or if
513
- the note contains an attachment.
536
+ **Safety:** The append is rejected if the note changed since it was read, rich
537
+ metadata is unavailable, or the note contains attachments or other native
538
+ objects. Existing link destinations are verified after saving.
514
539
 
515
540
  ---
516
541
 
package/build/index.js CHANGED
@@ -3358,9 +3358,9 @@ var require_utils = __commonJS({
3358
3358
  let output = "";
3359
3359
  for (let i = 0; i < input.length; i++) {
3360
3360
  if (input[i] === "%" && i + 2 < input.length) {
3361
- const hex = input.slice(i + 1, i + 3);
3362
- if (isHexPair(hex)) {
3363
- const normalizedHex = hex.toUpperCase();
3361
+ const hex2 = input.slice(i + 1, i + 3);
3362
+ if (isHexPair(hex2)) {
3363
+ const normalizedHex = hex2.toUpperCase();
3364
3364
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3365
3365
  if (decodeUnreserved && isUnreserved(decoded)) {
3366
3366
  output += decoded;
@@ -3380,9 +3380,9 @@ var require_utils = __commonJS({
3380
3380
  for (let i = 0; i < input.length; i++) {
3381
3381
  const ch = input[i];
3382
3382
  if (ch === "%" && i + 2 < input.length) {
3383
- const hex = input.slice(i + 1, i + 3);
3384
- if (isHexPair(hex)) {
3385
- const normalizedHex = hex.toUpperCase();
3383
+ const hex2 = input.slice(i + 1, i + 3);
3384
+ if (isHexPair(hex2)) {
3385
+ const normalizedHex = hex2.toUpperCase();
3386
3386
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3387
3387
  if (decoded !== "." && isUnreserved(decoded)) {
3388
3388
  output += decoded;
@@ -3422,9 +3422,9 @@ var require_utils = __commonJS({
3422
3422
  for (let i = 0; i < input.length; i++) {
3423
3423
  const ch = input[i];
3424
3424
  if (ch === "%" && i + 2 < input.length) {
3425
- const hex = input.slice(i + 1, i + 3);
3426
- if (isHexPair(hex)) {
3427
- output += "%" + hex.toUpperCase();
3425
+ const hex2 = input.slice(i + 1, i + 3);
3426
+ if (isHexPair(hex2)) {
3427
+ output += "%" + hex2.toUpperCase();
3428
3428
  i += 2;
3429
3429
  continue;
3430
3430
  }
@@ -3460,9 +3460,9 @@ var require_utils = __commonJS({
3460
3460
  for (let i = 0; i < input.length; i++) {
3461
3461
  const ch = input[i];
3462
3462
  if (ch === "%" && i + 2 < input.length) {
3463
- const hex = input.slice(i + 1, i + 3);
3464
- if (isHexPair(hex)) {
3465
- output += "%" + hex.toUpperCase();
3463
+ const hex2 = input.slice(i + 1, i + 3);
3464
+ if (isHexPair(hex2)) {
3465
+ output += "%" + hex2.toUpperCase();
3466
3466
  i += 2;
3467
3467
  continue;
3468
3468
  }
@@ -3507,9 +3507,9 @@ var require_utils = __commonJS({
3507
3507
  for (let i = 0; i < input.length; i++) {
3508
3508
  const ch = input[i];
3509
3509
  if (ch === "%" && i + 2 < input.length) {
3510
- const hex = input.slice(i + 1, i + 3);
3511
- if (isHexPair(hex)) {
3512
- const normalizedHex = hex.toUpperCase();
3510
+ const hex2 = input.slice(i + 1, i + 3);
3511
+ if (isHexPair(hex2)) {
3512
+ const normalizedHex = hex2.toUpperCase();
3513
3513
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3514
3514
  if (isUnreserved(decoded)) {
3515
3515
  output += decoded;
@@ -3547,9 +3547,9 @@ var require_utils = __commonJS({
3547
3547
  let output = "";
3548
3548
  for (let i = 0; i < input.length; i++) {
3549
3549
  if (input[i] === "%" && i + 2 < input.length) {
3550
- const hex = input.slice(i + 1, i + 3);
3551
- if (isHexPair(hex)) {
3552
- output += "%" + hex.toUpperCase();
3550
+ const hex2 = input.slice(i + 1, i + 3);
3551
+ if (isHexPair(hex2)) {
3552
+ output += "%" + hex2.toUpperCase();
3553
3553
  i += 2;
3554
3554
  continue;
3555
3555
  }
@@ -4199,8 +4199,8 @@ var require_fast_uri = __commonJS({
4199
4199
  } catch {
4200
4200
  return void 0;
4201
4201
  }
4202
- const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
4203
- return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized;
4202
+ const { normalized: normalized2, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
4203
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized2;
4204
4204
  }
4205
4205
  var fastUri = {
4206
4206
  SCHEMES,
@@ -9329,8 +9329,8 @@ var require_select = __commonJS({
9329
9329
  regex = regex.replace(name, val.source || val);
9330
9330
  return new RegExp(regex);
9331
9331
  };
9332
- var truncateUrl = function(url, num) {
9333
- return url.replace(/^(?:\w+:\/\/|\/+)/, "").replace(/(?:\/+|\/*#.*?)$/, "").split("/", num).join("/");
9332
+ var truncateUrl = function(url, num2) {
9333
+ return url.replace(/^(?:\w+:\/\/|\/+)/, "").replace(/(?:\/+|\/*#.*?)$/, "").split("/", num2).join("/");
9334
9334
  };
9335
9335
  var parseNth = function(param_, test) {
9336
9336
  var param = param_.replace(/\s+/g, ""), cap;
@@ -24465,14 +24465,14 @@ var require_turndown_cjs = __commonJS({
24465
24465
  } else if (node.nodeType === 1) {
24466
24466
  replacement = replacementForNode.call(self, node);
24467
24467
  }
24468
- return join7(output, replacement);
24468
+ return join8(output, replacement);
24469
24469
  }, "");
24470
24470
  }
24471
24471
  function postProcess(output) {
24472
24472
  var self = this;
24473
24473
  this.rules.forEach(function(rule) {
24474
24474
  if (typeof rule.append === "function") {
24475
- output = join7(output, rule.append(self.options));
24475
+ output = join8(output, rule.append(self.options));
24476
24476
  }
24477
24477
  });
24478
24478
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24484,7 +24484,7 @@ var require_turndown_cjs = __commonJS({
24484
24484
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24485
24485
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24486
24486
  }
24487
- function join7(output, replacement) {
24487
+ function join8(output, replacement) {
24488
24488
  var s1 = trimTrailingNewlines(output);
24489
24489
  var s2 = trimLeadingNewlines(replacement);
24490
24490
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -24777,8 +24777,8 @@ var ZodError = class _ZodError extends Error {
24777
24777
  constructor(issues) {
24778
24778
  super();
24779
24779
  this.issues = [];
24780
- this.addIssue = (sub) => {
24781
- this.issues = [...this.issues, sub];
24780
+ this.addIssue = (sub2) => {
24781
+ this.issues = [...this.issues, sub2];
24782
24782
  };
24783
24783
  this.addIssues = (subs = []) => {
24784
24784
  this.issues = [...this.issues, ...subs];
@@ -24845,13 +24845,13 @@ var ZodError = class _ZodError extends Error {
24845
24845
  flatten(mapper = (issue2) => issue2.message) {
24846
24846
  const fieldErrors = {};
24847
24847
  const formErrors = [];
24848
- for (const sub of this.issues) {
24849
- if (sub.path.length > 0) {
24850
- const firstEl = sub.path[0];
24848
+ for (const sub2 of this.issues) {
24849
+ if (sub2.path.length > 0) {
24850
+ const firstEl = sub2.path[0];
24851
24851
  fieldErrors[firstEl] = fieldErrors[firstEl] || [];
24852
- fieldErrors[firstEl].push(mapper(sub));
24852
+ fieldErrors[firstEl].push(mapper(sub2));
24853
24853
  } else {
24854
- formErrors.push(mapper(sub));
24854
+ formErrors.push(mapper(sub2));
24855
24855
  }
24856
24856
  }
24857
24857
  return { formErrors, fieldErrors };
@@ -29150,12 +29150,12 @@ var $ZodRealError = $constructor("$ZodError", initializer, { Parent: Error });
29150
29150
  function flattenError(error2, mapper = (issue2) => issue2.message) {
29151
29151
  const fieldErrors = {};
29152
29152
  const formErrors = [];
29153
- for (const sub of error2.issues) {
29154
- if (sub.path.length > 0) {
29155
- fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];
29156
- fieldErrors[sub.path[0]].push(mapper(sub));
29153
+ for (const sub2 of error2.issues) {
29154
+ if (sub2.path.length > 0) {
29155
+ fieldErrors[sub2.path[0]] = fieldErrors[sub2.path[0]] || [];
29156
+ fieldErrors[sub2.path[0]].push(mapper(sub2));
29157
29157
  } else {
29158
- formErrors.push(mapper(sub));
29158
+ formErrors.push(mapper(sub2));
29159
29159
  }
29160
29160
  }
29161
29161
  return { formErrors, fieldErrors };
@@ -30297,7 +30297,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
30297
30297
  });
30298
30298
  const generateFastpass = (shape) => {
30299
30299
  const doc = new Doc(["shape", "payload", "ctx"]);
30300
- const normalized = _normalized.value;
30300
+ const normalized2 = _normalized.value;
30301
30301
  const parseStr = (key) => {
30302
30302
  const k = esc(key);
30303
30303
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
@@ -30305,12 +30305,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
30305
30305
  doc.write(`const input = payload.value;`);
30306
30306
  const ids = /* @__PURE__ */ Object.create(null);
30307
30307
  let counter = 0;
30308
- for (const key of normalized.keys) {
30308
+ for (const key of normalized2.keys) {
30309
30309
  ids[key] = `key_${counter++}`;
30310
30310
  }
30311
30311
  doc.write(`const newResult = {}`);
30312
- for (const key of normalized.keys) {
30313
- if (normalized.optionalKeys.has(key)) {
30312
+ for (const key of normalized2.keys) {
30313
+ if (normalized2.optionalKeys.has(key)) {
30314
30314
  const id = ids[key];
30315
30315
  doc.write(`const ${id} = ${parseStr(key)};`);
30316
30316
  const k = esc(key);
@@ -39364,9 +39364,9 @@ function queryNoteData(noteId) {
39364
39364
  timeout: 5e3,
39365
39365
  stdio: ["pipe", "pipe", "pipe"]
39366
39366
  });
39367
- const hex = result.trim();
39368
- if (!hex) return { hex: null };
39369
- return { hex };
39367
+ const hex2 = result.trim();
39368
+ if (!hex2) return { hex: null };
39369
+ return { hex: hex2 };
39370
39370
  } catch (error2) {
39371
39371
  const message = error2 instanceof Error ? error2.message : String(error2);
39372
39372
  console.error(`Failed to query NoteStore database: ${message}`);
@@ -39376,10 +39376,10 @@ function queryNoteData(noteId) {
39376
39376
  return { hex: null };
39377
39377
  }
39378
39378
  }
39379
- function hexToBytes(hex) {
39380
- const bytes = new Uint8Array(hex.length / 2);
39381
- for (let i = 0; i < hex.length; i += 2) {
39382
- bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
39379
+ function hexToBytes(hex2) {
39380
+ const bytes = new Uint8Array(hex2.length / 2);
39381
+ for (let i = 0; i < hex2.length; i += 2) {
39382
+ bytes[i / 2] = parseInt(hex2.substring(i, i + 2), 16);
39383
39383
  }
39384
39384
  return bytes;
39385
39385
  }
@@ -39484,6 +39484,311 @@ function getChecklistItems(noteId) {
39484
39484
  return { items };
39485
39485
  }
39486
39486
 
39487
+ // src/utils/noteRichText.ts
39488
+ import { execFileSync as execFileSync3 } from "node:child_process";
39489
+ import { createHash } from "node:crypto";
39490
+ import { homedir as homedir2 } from "node:os";
39491
+ import { join as join2 } from "node:path";
39492
+ import { gunzipSync as gunzipSync2 } from "node:zlib";
39493
+ var dbPath = join2(homedir2(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
39494
+ var safeUrl = (url) => /^(?:https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url) && !Array.from(url).some((char) => char.charCodeAt(0) < 32);
39495
+ var escapeAttribute = (text) => text.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
39496
+ var normalized = (text) => text.replace(/[\s\ufffc]/gu, "");
39497
+ function styleValue(field) {
39498
+ if (!(field.value instanceof Uint8Array)) return field.value;
39499
+ if (field.fieldNumber === 2) {
39500
+ const data = field.value, kept = [];
39501
+ let offset = 0;
39502
+ try {
39503
+ while (offset < data.length) {
39504
+ const start = offset;
39505
+ let tag, length = -1;
39506
+ [tag, offset] = decodeVarint(data, offset);
39507
+ const wire = tag & 7;
39508
+ if (wire === 0) [, offset] = decodeVarint(data, offset);
39509
+ else if (wire === 1) offset += 8;
39510
+ else if (wire === 5) offset += 4;
39511
+ else if (wire === 2) {
39512
+ [length, offset] = decodeVarint(data, offset);
39513
+ offset += length;
39514
+ } else return Buffer.from(data).toString("hex");
39515
+ if (offset > data.length) return Buffer.from(data).toString("hex");
39516
+ if (!(tag >>> 3 === 9 && wire === 2 && length === 16)) kept.push(data.slice(start, offset));
39517
+ }
39518
+ return Buffer.concat(kept).toString("hex");
39519
+ } catch {
39520
+ return Buffer.from(data).toString("hex");
39521
+ }
39522
+ }
39523
+ return Buffer.from(field.value).toString("hex");
39524
+ }
39525
+ function parseRichNote(data, nativeTags = []) {
39526
+ const doc = decodeMessage(data);
39527
+ const wrapper = embeddedMessage(getField(doc, 2));
39528
+ const body = wrapper && embeddedMessage(getField(wrapper, 3));
39529
+ const text = body && stringValue(getField(body, 2));
39530
+ if (!body || text === void 0) throw new Error("Unsupported Notes document structure");
39531
+ const links = [];
39532
+ const nativeObjectIds = [];
39533
+ const objects = [];
39534
+ const checklistItems = [];
39535
+ const styleRuns = [];
39536
+ let position = 0;
39537
+ let hasNativeObjects = false;
39538
+ let hasChecklist = false;
39539
+ for (const run of getFields(body, 5)) {
39540
+ const fields = embeddedMessage(run);
39541
+ if (!fields) throw new Error("Invalid Notes attribute run");
39542
+ const length = varintValue(getField(fields, 1));
39543
+ if (length === void 0 || length < 0 || position + length > text.length)
39544
+ throw new Error("Invalid Notes run length");
39545
+ styleRuns.push({
39546
+ start: position,
39547
+ length,
39548
+ signature: JSON.stringify(
39549
+ fields.filter((f) => f.fieldNumber >= 2 && f.fieldNumber <= 12 || f.fieldNumber === 14).map((f) => [f.fieldNumber, styleValue(f)])
39550
+ )
39551
+ });
39552
+ const url = stringValue(getField(fields, 9));
39553
+ if (url) {
39554
+ if (!safeUrl(url)) throw new Error("Unsupported link scheme in note");
39555
+ const previous = links.at(-1);
39556
+ if (previous?.url === url && previous.start + previous.length === position) {
39557
+ previous.length += length;
39558
+ previous.text += text.slice(position, position + length);
39559
+ } else
39560
+ links.push({ start: position, length, text: text.slice(position, position + length), url });
39561
+ }
39562
+ hasNativeObjects ||= Boolean(getField(fields, 12));
39563
+ const attachment = embeddedMessage(getField(fields, 12));
39564
+ const attachmentId = attachment && stringValue(getField(attachment, 1));
39565
+ if (attachmentId) nativeObjectIds.push(attachmentId);
39566
+ if (attachmentId)
39567
+ objects.push({
39568
+ id: attachmentId,
39569
+ type: stringValue(getField(attachment, 2)) || "unknown",
39570
+ start: position,
39571
+ length
39572
+ });
39573
+ const paragraph = embeddedMessage(getField(fields, 2));
39574
+ hasChecklist ||= Boolean(paragraph && varintValue(getField(paragraph, 1)) === 103);
39575
+ if (paragraph && varintValue(getField(paragraph, 1)) === 103) {
39576
+ const checklist = embeddedMessage(getField(paragraph, 5));
39577
+ const rawId = checklist && getField(checklist, 1)?.value;
39578
+ const itemId = rawId instanceof Uint8Array ? Buffer.from(rawId).toString("hex") : "";
39579
+ const start = text.lastIndexOf("\n", position - 1) + 1;
39580
+ if (itemId && !checklistItems.some((item) => item.id === itemId))
39581
+ checklistItems.push({
39582
+ id: itemId,
39583
+ start,
39584
+ text: text.slice(
39585
+ start,
39586
+ text.indexOf("\n", start) === -1 ? text.length : text.indexOf("\n", start)
39587
+ ),
39588
+ done: checklist ? varintValue(getField(checklist, 2)) === 1 : false
39589
+ });
39590
+ }
39591
+ position += length;
39592
+ }
39593
+ if (position !== text.length) throw new Error("Incomplete Notes attribute runs");
39594
+ return {
39595
+ text,
39596
+ links,
39597
+ nativeTags: hasNativeObjects ? nativeTags : [],
39598
+ nativeObjectIds,
39599
+ hasNativeObjects,
39600
+ hasChecklist,
39601
+ revision: createHash("sha256").update(data).digest("hex"),
39602
+ objects,
39603
+ checklistItems,
39604
+ styleRuns
39605
+ };
39606
+ }
39607
+ function readRichNote(id) {
39608
+ const pk = /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p([0-9]+)$/i.exec(id)?.[1];
39609
+ if (!pk) throw new Error("Invalid exact note ID");
39610
+ const sql = `BEGIN; SELECT hex(ZDATA) FROM ZICNOTEDATA WHERE ZNOTE=${pk}; SELECT json_group_object(ZIDENTIFIER,ZALTTEXT) FROM ZICCLOUDSYNCINGOBJECT WHERE ZNOTE1=${pk} AND ZTYPEUTI1='com.apple.notes.inlinetextattachment.hashtag'; SELECT json_group_array(json_object('id',ZIDENTIFIER,'pk',Z_PK,'type',COALESCE(ZTYPEUTI1,ZTYPEUTI),'mergeable',hex(COALESCE(ZMERGEABLEDATA1,ZMERGEABLEDATA)),'view',ZATTACHMENTVIEWTYPE)) FROM ZICCLOUDSYNCINGOBJECT WHERE ZNOTE1=${pk} OR ZNOTE=${pk}; COMMIT;`;
39611
+ const rows = execFileSync3("/usr/bin/sqlite3", ["-readonly", dbPath, sql], {
39612
+ encoding: "utf8",
39613
+ timeout: 5e3,
39614
+ maxBuffer: 64 * 1024 * 1024,
39615
+ stdio: ["pipe", "pipe", "pipe"]
39616
+ }).trim().split("\n");
39617
+ if (!rows[0] || !/^[0-9a-f]+$/i.test(rows[0])) throw new Error("No Notes document data");
39618
+ const tags = JSON.parse(rows[1] || "{}");
39619
+ if (!tags || Array.isArray(tags) || typeof tags !== "object" || Object.values(tags).some((tag) => typeof tag !== "string"))
39620
+ throw new Error("Invalid native tags");
39621
+ const rich = parseRichNote(
39622
+ gunzipSync2(Buffer.from(rows[0], "hex"), { maxOutputLength: 32 * 1024 * 1024 })
39623
+ );
39624
+ const tagMap = tags;
39625
+ const objectData = JSON.parse(rows[2] || "[]");
39626
+ if (!Array.isArray(objectData) || objectData.some(
39627
+ (row) => !row || typeof row.id !== "string" || !Number.isInteger(row.pk) || typeof row.mergeable !== "string" || !/^[0-9a-f]*$/i.test(row.mergeable)
39628
+ ))
39629
+ throw new Error("Invalid native object metadata");
39630
+ rich.objectData = objectData.filter((row) => rich.nativeObjectIds.includes(row.id)).sort((a, b) => a.id.localeCompare(b.id));
39631
+ rich.revision = createHash("sha256").update(rich.revision).update(JSON.stringify(rich.objectData)).digest("hex");
39632
+ rich.nativeTagObjectIds = {};
39633
+ for (const id2 of rich.nativeObjectIds)
39634
+ if (tagMap[id2]) {
39635
+ const tag = tagMap[id2].replace(/^#/, "");
39636
+ (rich.nativeTagObjectIds[tag] ||= []).push(id2);
39637
+ }
39638
+ rich.nativeTags = [
39639
+ ...new Set(
39640
+ rich.nativeObjectIds.flatMap((id2) => tagMap[id2] ? [tagMap[id2].replace(/^#/, "")] : [])
39641
+ )
39642
+ ];
39643
+ return rich;
39644
+ }
39645
+ function decodeEntity(value) {
39646
+ const named = {
39647
+ amp: "&",
39648
+ lt: "<",
39649
+ gt: ">",
39650
+ quot: '"',
39651
+ apos: "'",
39652
+ nbsp: " "
39653
+ };
39654
+ if (!value.startsWith("&") || value === "&") return value;
39655
+ const name = value.slice(1).replace(/;$/, "");
39656
+ if (name.startsWith("#")) {
39657
+ const cp = name[1]?.toLowerCase() === "x" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10);
39658
+ if (!Number.isInteger(cp) || cp < 0 || cp > 1114111) throw new Error("Invalid HTML entity");
39659
+ return String.fromCodePoint(cp);
39660
+ }
39661
+ if (!(name in named)) throw new Error("Unsupported HTML entity");
39662
+ return named[name];
39663
+ }
39664
+ function visibleCharacters(html) {
39665
+ const chars = [];
39666
+ let token = 0;
39667
+ for (const part of html.matchAll(/<[^>]*>|[^<]+/g)) {
39668
+ token++;
39669
+ if (part[0].startsWith("<")) continue;
39670
+ for (const item of part[0].matchAll(
39671
+ /&(?:#[0-9]+;?|#x[0-9a-f]+;?|(?:amp|lt|gt|quot|apos|nbsp)(?:;|(?![a-z0-9=])))|[\s\S]/gi
39672
+ )) {
39673
+ const value = decodeEntity(item[0]);
39674
+ for (let i = 0; i < value.length; i++) {
39675
+ if (/\s/u.test(value[i])) continue;
39676
+ chars.push({
39677
+ value: value[i],
39678
+ start: part.index + item.index,
39679
+ end: part.index + item.index + item[0].length,
39680
+ token
39681
+ });
39682
+ }
39683
+ }
39684
+ }
39685
+ return chars;
39686
+ }
39687
+ function restoreNoteLinks(html, rich) {
39688
+ const base = html.replace(/<\/?a\b[^>]*>/gi, "");
39689
+ const chars = visibleCharacters(base);
39690
+ if (chars.map((c) => c.value).join("") !== normalized(rich.text))
39691
+ throw new Error("Notes HTML and rich text do not match; retry after sync");
39692
+ const positions = [];
39693
+ for (let i = 0; i < rich.text.length; i++)
39694
+ if (!/[\s\ufffc]/u.test(rich.text[i])) positions.push(i);
39695
+ const inserts = [];
39696
+ for (const link of rich.links) {
39697
+ if (!safeUrl(link.url)) throw new Error("Unsupported link scheme in note");
39698
+ let span;
39699
+ for (let i = 0; i < chars.length; i++) {
39700
+ if (positions[i] < link.start || positions[i] >= link.start + link.length) continue;
39701
+ const c = chars[i];
39702
+ if (span?.token === c.token) span.end = c.end;
39703
+ else {
39704
+ if (span) inserts.push(span);
39705
+ span = { start: c.start, end: c.end, token: c.token, url: link.url };
39706
+ }
39707
+ }
39708
+ if (span) inserts.push(span);
39709
+ }
39710
+ let result = base;
39711
+ for (const span of inserts.sort((a, b) => b.start - a.start))
39712
+ result = result.slice(0, span.start) + `<a href="${escapeAttribute(span.url)}">` + result.slice(span.start, span.end) + "</a>" + result.slice(span.end);
39713
+ return result;
39714
+ }
39715
+ function enrichNoteRead(id, rawBody) {
39716
+ let metadata;
39717
+ try {
39718
+ const rich = readRichNote(id);
39719
+ metadata = rich;
39720
+ const content = restoreNoteLinks(rawBody, rich);
39721
+ const writable = !rich.hasNativeObjects && !rich.hasChecklist;
39722
+ return {
39723
+ content,
39724
+ links: rich.links,
39725
+ nativeTags: rich.nativeTags,
39726
+ complete: writable,
39727
+ writable,
39728
+ revision: rich.revision,
39729
+ ...!writable ? {
39730
+ warning: "Native tags, inline objects or checklists are present. Their state is not writable through AppleScript; full-body edits are blocked to preserve them."
39731
+ } : {}
39732
+ };
39733
+ } catch {
39734
+ return {
39735
+ content: rawBody,
39736
+ links: metadata?.links ?? [],
39737
+ nativeTags: metadata?.nativeTags ?? [],
39738
+ complete: false,
39739
+ writable: false,
39740
+ revision: metadata?.revision ?? "unavailable",
39741
+ warning: "Rich Notes metadata could not be read or matched. Links/native tags may be missing from this view. Full-body edits are blocked; check Full Disk Access and retry after sync."
39742
+ };
39743
+ }
39744
+ }
39745
+ function richContentHash(rawBody, rich) {
39746
+ return `sha256:${createHash("sha256").update(rawBody).update("\0").update(rich.revision).digest("hex")}`;
39747
+ }
39748
+ function htmlLinks(html) {
39749
+ const links = [];
39750
+ for (const match of html.matchAll(
39751
+ /<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi
39752
+ )) {
39753
+ const url = (match[1] ?? match[2]).replace(/&(?:#[0-9]+|#x[0-9a-f]+|[a-z]+);/gi, decodeEntity);
39754
+ if (!safeUrl(url)) throw new Error("Unsupported link scheme");
39755
+ links.push({
39756
+ text: visibleCharacters(match[3]).map((c) => c.value).join(""),
39757
+ url
39758
+ });
39759
+ }
39760
+ return links;
39761
+ }
39762
+ function linkSignature(links) {
39763
+ return JSON.stringify(
39764
+ links.flatMap(
39765
+ (link) => normalized(link.text).split("").map((char) => [char, link.url])
39766
+ )
39767
+ );
39768
+ }
39769
+ function assertLinkedWrite(rich, content, format, allowLinkChanges = false) {
39770
+ if (!rich.writable) throw new Error(rich.warning || "Rich note cannot be safely rewritten");
39771
+ if (rich.links.length && format !== "html" && !allowLinkChanges)
39772
+ throw new Error(
39773
+ "This note has links. Use format=html and preserve the linked HTML returned by get-note-content."
39774
+ );
39775
+ const next = format === "html" ? htmlLinks(content) : [];
39776
+ if (allowLinkChanges) return;
39777
+ const needed = /* @__PURE__ */ new Map();
39778
+ for (const link of rich.links) {
39779
+ const key = linkSignature([link]);
39780
+ needed.set(key, (needed.get(key) || 0) + 1);
39781
+ }
39782
+ const incoming = linkSignature(next);
39783
+ for (const [key, count] of needed) {
39784
+ const sequence = key.slice(1, -1);
39785
+ if (incoming.split(sequence).length - 1 < count)
39786
+ throw new Error(
39787
+ "Update would remove or change an existing link. Preserve its label and URL from get-note-content."
39788
+ );
39789
+ }
39790
+ }
39791
+
39487
39792
  // src/utils/attachmentFs.ts
39488
39793
  import {
39489
39794
  existsSync as existsSync2,
@@ -39495,11 +39800,11 @@ import {
39495
39800
  rmSync,
39496
39801
  statSync
39497
39802
  } from "fs";
39498
- import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
39499
- import { homedir as homedir2, tmpdir } from "os";
39803
+ import { dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
39804
+ import { homedir as homedir3, tmpdir } from "os";
39500
39805
  function allowedSaveRoots() {
39501
39806
  return [
39502
- resolve(homedir2()),
39807
+ resolve(homedir3()),
39503
39808
  resolve(tmpdir()),
39504
39809
  "/Volumes",
39505
39810
  "/private/var/folders",
@@ -39573,7 +39878,7 @@ function assertSafeSavePath(p, roots = allowedSaveRoots()) {
39573
39878
  if (suffix.split(sep).includes("..")) {
39574
39879
  throw new Error(`Refusing to write outside allowed locations (home, temp, /Volumes): "${abs}"`);
39575
39880
  }
39576
- const canonicalDest = suffix ? join2(canonicalAncestor, suffix) : canonicalAncestor;
39881
+ const canonicalDest = suffix ? join3(canonicalAncestor, suffix) : canonicalAncestor;
39577
39882
  const allowed = canonicalRoots(roots);
39578
39883
  if (!isWithinRoots(canonicalAncestor, allowed) || !isWithinRoots(canonicalDest, allowed)) {
39579
39884
  throw new Error(
@@ -39623,8 +39928,8 @@ function cleanupTempDir(dir) {
39623
39928
  // src/services/appleNotesManager.ts
39624
39929
  var import_turndown = __toESM(require_turndown_cjs(), 1);
39625
39930
  import { existsSync as existsSync3 } from "fs";
39626
- import { homedir as homedir3 } from "os";
39627
- import { join as join3 } from "path";
39931
+ import { homedir as homedir4 } from "os";
39932
+ import { join as join4 } from "path";
39628
39933
  var FIELD_SEP = "";
39629
39934
  var RECORD_SEP = "";
39630
39935
  var AS_FIELD_SEP = "(ASCII character 31)";
@@ -39702,8 +40007,8 @@ function parseAppleScriptDate(appleScriptDate) {
39702
40007
  return isNaN(dt.getTime()) ? /* @__PURE__ */ new Date() : dt;
39703
40008
  }
39704
40009
  const withoutPrefix = s.replace(/^date\s+/, "");
39705
- const normalized = withoutPrefix.replace(" at ", " ");
39706
- const parsed = new Date(normalized);
40010
+ const normalized2 = withoutPrefix.replace(" at ", " ");
40011
+ const parsed = new Date(normalized2);
39707
40012
  return isNaN(parsed.getTime()) ? /* @__PURE__ */ new Date() : parsed;
39708
40013
  }
39709
40014
  function buildAppleScriptDateVar(date3, varName = "thresholdDate") {
@@ -39825,11 +40130,11 @@ function getNoteLinkFromDB(coreDataId) {
39825
40130
  const match = coreDataId.match(/\/p(\d+)$/);
39826
40131
  if (!match) return null;
39827
40132
  const pk = parseInt(match[1], 10);
39828
- const dbPath = join3(homedir3(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
39829
- if (!existsSync3(dbPath)) return null;
40133
+ const dbPath2 = join4(homedir4(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
40134
+ if (!existsSync3(dbPath2)) return null;
39830
40135
  try {
39831
40136
  const { DatabaseSync } = __require("node:sqlite");
39832
- const db = new DatabaseSync(dbPath, { readOnly: true });
40137
+ const db = new DatabaseSync(dbPath2, { readOnly: true });
39833
40138
  try {
39834
40139
  const row = db.prepare("SELECT ZIDENTIFIER FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ?").get(pk);
39835
40140
  const identifier = row?.ZIDENTIFIER;
@@ -40320,8 +40625,13 @@ var AppleNotesManager = class {
40320
40625
  * race that would exist if JavaScript checked the note and then issued a
40321
40626
  * separate unconditional `set body` command.
40322
40627
  */
40323
- updateNoteByIdIfUnchanged(id, currentTitle, expectedBody, newTitle, newContent, format = "plaintext") {
40628
+ updateNoteByIdIfUnchanged(id, currentTitle, expectedBody, newTitle, newContent, format = "plaintext", expectedRichRevision) {
40324
40629
  const safeId = sanitizeNoteId(id);
40630
+ if (expectedRichRevision) {
40631
+ const rich = readRichNote(id);
40632
+ if (rich.revision !== expectedRichRevision) return { status: "conflict" };
40633
+ if (rich.hasNativeObjects || rich.hasChecklist) return { status: "attachments" };
40634
+ }
40325
40635
  if (newTitle) validateLength(newTitle, MAX_TITLE_LENGTH, "Note title");
40326
40636
  validateLength(newContent, MAX_CONTENT_LENGTH, "Note content");
40327
40637
  validateLength(expectedBody, MAX_CONTENT_LENGTH, "Expected note content");
@@ -41887,8 +42197,9 @@ var AppleNotesManager = class {
41887
42197
  getNoteMarkdown(title, account) {
41888
42198
  const html = this.getNoteContent(title, account);
41889
42199
  if (!html) return "";
41890
- let markdown = this.htmlToMarkdown(html);
41891
42200
  const note = this.getNoteDetails(title, account);
42201
+ const rich = note?.id ? enrichNoteRead(note.id, html) : void 0;
42202
+ let markdown = this.htmlToMarkdown(rich?.content || html);
41892
42203
  if (note?.id) {
41893
42204
  const result = getChecklistItems(note.id);
41894
42205
  if (result.items) {
@@ -41913,7 +42224,8 @@ var AppleNotesManager = class {
41913
42224
  getNoteMarkdownById(id) {
41914
42225
  const html = this.getNoteContentById(id);
41915
42226
  if (!html) return "";
41916
- let markdown = this.htmlToMarkdown(html);
42227
+ const rich = enrichNoteRead(id, html);
42228
+ let markdown = this.htmlToMarkdown(rich.content);
41917
42229
  const result = getChecklistItems(id);
41918
42230
  if (result.items) {
41919
42231
  markdown = this.enrichMarkdownWithChecklists(markdown, result.items);
@@ -41923,7 +42235,7 @@ var AppleNotesManager = class {
41923
42235
  };
41924
42236
 
41925
42237
  // src/utils/syncDetection.ts
41926
- import { execFileSync as execFileSync3 } from "child_process";
42238
+ import { execFileSync as execFileSync4 } from "child_process";
41927
42239
  import * as fs2 from "fs";
41928
42240
  import * as path2 from "path";
41929
42241
  import * as os2 from "os";
@@ -41968,7 +42280,7 @@ function getSyncStatus(useCache = true) {
41968
42280
  WHERE object.ZCLOUDSTATE = state.Z_PK
41969
42281
  );
41970
42282
  `;
41971
- const result = execFileSync3(
42283
+ const result = execFileSync4(
41972
42284
  "sqlite3",
41973
42285
  ["-readonly", NOTES_DB_PATH2, query.replace(/\n/g, " ")],
41974
42286
  {
@@ -42027,7 +42339,7 @@ function withSyncAwarenessSync(operation, fn) {
42027
42339
  }
42028
42340
 
42029
42341
  // src/utils/noteMetadata.ts
42030
- import { execFileSync as execFileSync4 } from "child_process";
42342
+ import { execFileSync as execFileSync5 } from "child_process";
42031
42343
  import * as fs3 from "fs";
42032
42344
  import * as path3 from "path";
42033
42345
  import * as os3 from "os";
@@ -42048,7 +42360,7 @@ var COLUMN_MAP = [
42048
42360
  { key: "smartFolderQuery", column: "ZSMARTFOLDERQUERYJSON", type: "text" }
42049
42361
  ];
42050
42362
  function runSqlite(query) {
42051
- return execFileSync4("sqlite3", ["-readonly", NOTES_DB_PATH3, query], {
42363
+ return execFileSync5("sqlite3", ["-readonly", NOTES_DB_PATH3, query], {
42052
42364
  encoding: "utf8",
42053
42365
  timeout: 5e3,
42054
42366
  stdio: ["pipe", "pipe", "pipe"]
@@ -42199,7 +42511,7 @@ function decodeHtmlEntities(text) {
42199
42511
  }
42200
42512
  return String.fromCodePoint(codePoint);
42201
42513
  };
42202
- return text.replace(/&#x([0-9a-f]+);?/gi, (match, hex) => decodeCodePoint(match, hex, 16)).replace(/&#([0-9]+);?/g, (match, decimal) => decodeCodePoint(match, decimal, 10)).replace(/&nbsp(?:;|(?![0-9a-z]))/gi, " ").replace(/&quot(?:;|(?![0-9a-z]))/gi, '"').replace(/&apos(?:;|(?![0-9a-z]))/gi, "'").replace(/&lt(?:;|(?![0-9a-z]))/gi, "<").replace(/&gt(?:;|(?![0-9a-z]))/gi, ">").replace(/&amp(?:;|(?![0-9a-z]))/gi, "&");
42514
+ return text.replace(/&#x([0-9a-f]+);?/gi, (match, hex2) => decodeCodePoint(match, hex2, 16)).replace(/&#([0-9]+);?/g, (match, decimal) => decodeCodePoint(match, decimal, 10)).replace(/&nbsp(?:;|(?![0-9a-z]))/gi, " ").replace(/&quot(?:;|(?![0-9a-z]))/gi, '"').replace(/&apos(?:;|(?![0-9a-z]))/gi, "'").replace(/&lt(?:;|(?![0-9a-z]))/gi, "<").replace(/&gt(?:;|(?![0-9a-z]))/gi, ">").replace(/&amp(?:;|(?![0-9a-z]))/gi, "&");
42203
42515
  }
42204
42516
  function firstVisibleHtmlLine(html) {
42205
42517
  let text = html;
@@ -42319,12 +42631,12 @@ function formatDoctorReport(r) {
42319
42631
 
42320
42632
  // src/services/fileConfig.ts
42321
42633
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
42322
- import { join as join6 } from "path";
42323
- import { homedir as homedir6 } from "os";
42634
+ import { join as join7 } from "path";
42635
+ import { homedir as homedir7 } from "os";
42324
42636
  function fileConfigPath(env = process.env) {
42325
42637
  const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
42326
42638
  if (override && override.trim()) return override.trim();
42327
- return join6(homedir6(), "Library", "Application Support", "apple-notes-mcp", "config.json");
42639
+ return join7(homedir7(), "Library", "Application Support", "apple-notes-mcp", "config.json");
42328
42640
  }
42329
42641
  function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
42330
42642
  const applied = [];
@@ -42532,10 +42844,6 @@ function withJsonSchema2020_12(transport2) {
42532
42844
  }
42533
42845
 
42534
42846
  // src/utils/noteRevision.ts
42535
- import { createHash } from "node:crypto";
42536
- function hashNoteContent(content) {
42537
- return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
42538
- }
42539
42847
  var INLINE_TAG = /^<\/?(?:b|i|u|s|strike|em|strong|span|a|font|sub|sup|code|tt|small|big|mark)\b/i;
42540
42848
  function comparableVisibleText(html) {
42541
42849
  return html.replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, (tag) => INLINE_TAG.test(tag) ? "" : " ").replace(/&nbsp;|&#160;/gi, " ").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&").replace(/&#(\d+);/g, (_match, codePoint) => String.fromCodePoint(Number(codePoint))).replace(
@@ -42544,6 +42852,103 @@ function comparableVisibleText(html) {
42544
42852
  ).replace(/\s+/g, " ").trim();
42545
42853
  }
42546
42854
 
42855
+ // src/utils/noteTables.ts
42856
+ import { gunzipSync as gunzipSync3 } from "node:zlib";
42857
+ var sub = (f, n) => {
42858
+ const value = embeddedMessage(getField(f, n));
42859
+ if (!value) throw new Error(`Missing table field ${n}`);
42860
+ return value;
42861
+ };
42862
+ var num = (f, n) => {
42863
+ const v = varintValue(getField(f, n));
42864
+ if (v === void 0) throw new Error(`Missing table index ${n}`);
42865
+ return v;
42866
+ };
42867
+ var many = (f, n) => getFields(f, n).map((v) => {
42868
+ const m = embeddedMessage(v);
42869
+ if (!m) throw new Error("Invalid table entry");
42870
+ return m;
42871
+ });
42872
+ var hex = (f) => {
42873
+ if (!(f?.value instanceof Uint8Array)) throw new Error("Missing table UUID");
42874
+ return Buffer.from(f.value).toString("hex");
42875
+ };
42876
+ function parseNoteTable(compressed) {
42877
+ const root = decodeMessage(gunzipSync3(compressed, { maxOutputLength: 16 * 1024 * 1024 }));
42878
+ const data = sub(sub(root, 2), 3), entries = many(data, 3);
42879
+ if (entries.length > 1e5) throw new Error("Table too large");
42880
+ const keys = getFields(data, 4).map(stringValue), types = getFields(data, 5).map(stringValue), uuids = getFields(data, 6).map(hex);
42881
+ const entry = (index) => {
42882
+ if (!entries[index]) throw new Error("Invalid table reference");
42883
+ return entries[index];
42884
+ };
42885
+ const uuidIndex = (index) => num(sub(many(sub(entry(index), 13), 3)[0], 2), 2);
42886
+ const roots = entries.filter((e) => {
42887
+ const map = embeddedMessage(getField(e, 13));
42888
+ return map && types[num(map, 1)] === "com.apple.notes.ICTable";
42889
+ });
42890
+ if (roots.length !== 1) throw new Error("Ambiguous native table root");
42891
+ const refs = new Map(
42892
+ many(sub(roots[0], 13), 3).filter((m) => ["crRows", "crColumns", "cellColumns"].includes(keys[num(m, 1)] || "")).map((m) => [keys[num(m, 1)], num(sub(m, 2), 6)])
42893
+ );
42894
+ const ordered = (key) => {
42895
+ const ref = refs.get(key);
42896
+ if (ref === void 0) throw new Error("Missing table dimension");
42897
+ const ordering = sub(sub(entry(ref), 16), 1), array2 = sub(ordering, 1);
42898
+ const ids = many(array2, 2).map((a) => hex(getField(a, 2)));
42899
+ const map = /* @__PURE__ */ new Map();
42900
+ ids.forEach((id, i) => {
42901
+ const index = uuids.indexOf(id);
42902
+ if (index < 0) throw new Error("Missing dimension UUID");
42903
+ map.set(index, i);
42904
+ });
42905
+ const aliases = many(sub(ordering, 2), 1).map((pair) => [
42906
+ uuidIndex(num(sub(pair, 1), 6)),
42907
+ uuidIndex(num(sub(pair, 2), 6))
42908
+ ]);
42909
+ for (let pass = 0; pass < aliases.length + 1; pass++) {
42910
+ let changed = false;
42911
+ for (const [key2, value] of aliases)
42912
+ if (map.has(key2) && !map.has(value)) {
42913
+ map.set(value, map.get(key2));
42914
+ changed = true;
42915
+ }
42916
+ if (!changed) break;
42917
+ }
42918
+ return { ids, map };
42919
+ };
42920
+ const rows = ordered("crRows"), columns = ordered("crColumns");
42921
+ if (!rows.ids.length || !columns.ids.length || rows.ids.length * columns.ids.length > 1e5)
42922
+ throw new Error("Unsupported table size");
42923
+ const values = rows.ids.map(() => columns.ids.map(() => ""));
42924
+ const cellRef = refs.get("cellColumns");
42925
+ if (cellRef === void 0) throw new Error("Missing table cells");
42926
+ for (const column of many(sub(entry(cellRef), 6), 1)) {
42927
+ const ci = columns.map.get(uuidIndex(num(sub(column, 1), 6)));
42928
+ const cells = entry(num(sub(column, 2), 6));
42929
+ for (const row of many(sub(cells, 6), 1)) {
42930
+ const ri = rows.map.get(uuidIndex(num(sub(row, 1), 6)));
42931
+ if (ri === void 0 || ci === void 0) continue;
42932
+ const note = sub(entry(num(sub(row, 2), 6)), 10);
42933
+ const text = stringValue(getField(note, 2));
42934
+ if (text === void 0 || text.includes("\uFFFC"))
42935
+ throw new Error("Embedded or unsupported table cell");
42936
+ values[ri][ci] = text.replace(/\n$/u, "");
42937
+ }
42938
+ }
42939
+ const rtl = entries.some((e) => {
42940
+ const map = embeddedMessage(getField(e, 13));
42941
+ return map && many(map, 3).some(
42942
+ (m) => stringValue(getField(sub(m, 2), 4)) === "CRTableColumnDirectionRightToLeft"
42943
+ );
42944
+ });
42945
+ if (rtl) {
42946
+ for (const row of values) row.reverse();
42947
+ columns.ids.reverse();
42948
+ }
42949
+ return { rows: values, rowIds: rows.ids, columnIds: columns.ids };
42950
+ }
42951
+
42547
42952
  // src/index.ts
42548
42953
  loadFileConfig();
42549
42954
  var require2 = createRequire(import.meta.url);
@@ -42611,7 +43016,8 @@ function readExactNoteSnapshot(id) {
42611
43016
  }
42612
43017
  const body = notesManager.getNoteContentById(id);
42613
43018
  if (!body) return { error: `Failed to read content of note "${note.title}"` };
42614
- return { note, body, contentHash: hashNoteContent(body) };
43019
+ const rich = enrichNoteRead(id, body);
43020
+ return { note, body, rich, contentHash: richContentHash(body, rich) };
42615
43021
  }
42616
43022
  function revisionConflictMessage(title) {
42617
43023
  return `Note "${title}" changed after it was read. Read it again and review the newer version before retrying.`;
@@ -42641,7 +43047,7 @@ registerTool(
42641
43047
  ),
42642
43048
  format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
42643
43049
  tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
42644
- "Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags inside the content body instead (Notes.app turns those into real tags)."
43050
+ "Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags in the body for searchable text; this does not create native tag objects. Native tags need the Notes Shortcuts action."
42645
43051
  ),
42646
43052
  folder: external_exports.string().max(MAX.FOLDER).optional().describe(
42647
43053
  "Folder to create the note in (supports nested paths like 'Work/Clients'). The folder must already exist \u2014 this tool does not create it; call create-folder first, which is idempotent and creates intermediate segments."
@@ -42675,7 +43081,7 @@ registerTool(
42675
43081
  `A note may have been created, but its exact ID could not be verified. Do not retry automatically. Returned ID: ${note.id}`
42676
43082
  );
42677
43083
  }
42678
- const contentHash = hashNoteContent(createdBody);
43084
+ const contentHash = richContentHash(createdBody, enrichNoteRead(note.id, createdBody));
42679
43085
  const checklistWarning = detectChecklistAttempt(content) ?? "";
42680
43086
  return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`, {
42681
43087
  ok: true,
@@ -42764,7 +43170,7 @@ ${noteList}${truncationNote}${syncNote}`,
42764
43170
  registerTool(
42765
43171
  "get-note-content",
42766
43172
  {
42767
- description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, and strippedImages/truncated when the body was capped.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
43173
+ description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
42768
43174
  inputSchema: {
42769
43175
  id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
42770
43176
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
@@ -42778,6 +43184,13 @@ registerTool(
42778
43184
  content: external_exports.string().optional(),
42779
43185
  contentHash: external_exports.string().optional(),
42780
43186
  hashtags: external_exports.array(external_exports.string()).optional(),
43187
+ nativeTags: external_exports.array(external_exports.string()).optional(),
43188
+ links: external_exports.array(
43189
+ external_exports.object({ start: external_exports.number(), length: external_exports.number(), text: external_exports.string(), url: external_exports.string() })
43190
+ ).optional(),
43191
+ richContentComplete: external_exports.boolean().optional(),
43192
+ writable: external_exports.boolean().optional(),
43193
+ warning: external_exports.string().optional(),
42781
43194
  /** Number of oversized inline images replaced with text placeholders. */
42782
43195
  strippedImages: external_exports.number().optional(),
42783
43196
  /** True when content is lossy — see strippedImages. Never write a truncated body back. */
@@ -42799,15 +43212,21 @@ registerTool(
42799
43212
  if (!rawContent2) {
42800
43213
  return errorResponse(`Failed to read content of note "${note2.title}"`);
42801
43214
  }
42802
- const stripped2 = stripLargeInlineImages(rawContent2);
43215
+ const rich2 = enrichNoteRead(id, rawContent2);
43216
+ const stripped2 = stripLargeInlineImages(rich2.content);
42803
43217
  const content2 = stripped2.html;
42804
43218
  const hashtags2 = parseHashtags(content2);
42805
- const warning2 = strippedImagesWarning(stripped2);
43219
+ const warning2 = [strippedImagesWarning(stripped2), rich2.warning].filter(Boolean).join("\n\n");
42806
43220
  return successResponse(warning2 ? content2 + warning2 : content2, {
42807
43221
  id,
42808
43222
  title: note2.title,
42809
43223
  content: content2,
42810
- contentHash: hashNoteContent(rawContent2),
43224
+ contentHash: richContentHash(rawContent2, rich2),
43225
+ links: rich2.links,
43226
+ nativeTags: rich2.nativeTags,
43227
+ richContentComplete: rich2.complete,
43228
+ writable: rich2.writable && stripped2.strippedCount === 0,
43229
+ warning: rich2.warning,
42811
43230
  hashtags: hashtags2,
42812
43231
  strippedImages: stripped2.strippedCount,
42813
43232
  truncated: stripped2.strippedCount > 0
@@ -42829,15 +43248,21 @@ registerTool(
42829
43248
  if (!rawContent) {
42830
43249
  return errorResponse(`Failed to read content of note "${title}"`);
42831
43250
  }
42832
- const stripped = stripLargeInlineImages(rawContent);
43251
+ const rich = enrichNoteRead(note.id, rawContent);
43252
+ const stripped = stripLargeInlineImages(rich.content);
42833
43253
  const content = stripped.html;
42834
43254
  const hashtags = parseHashtags(content);
42835
- const warning = strippedImagesWarning(stripped);
43255
+ const warning = [strippedImagesWarning(stripped), rich.warning].filter(Boolean).join("\n\n");
42836
43256
  return successResponse(warning ? content + warning : content, {
42837
43257
  id: note.id,
42838
43258
  title,
42839
43259
  content,
42840
- contentHash: hashNoteContent(rawContent),
43260
+ contentHash: richContentHash(rawContent, rich),
43261
+ links: rich.links,
43262
+ nativeTags: rich.nativeTags,
43263
+ richContentComplete: rich.complete,
43264
+ writable: rich.writable && stripped.strippedCount === 0,
43265
+ warning: rich.warning,
42841
43266
  hashtags,
42842
43267
  strippedImages: stripped.strippedCount,
42843
43268
  truncated: stripped.strippedCount > 0
@@ -43078,13 +43503,110 @@ registerTool(
43078
43503
  return successResponse(`Shown account with ID "${id}" in Notes.app`, { id, separately });
43079
43504
  }, "Error showing account")
43080
43505
  );
43506
+ registerTool(
43507
+ "get-native-objects",
43508
+ {
43509
+ description: "Use when: inspecting native objects, checklist identities, or tables in one exact note.\nReturns: native object IDs and ranges, checklist IDs and state, actual native tags, decoded tables, and the current rich content hash.\nDo not use when: you only need the note body (get-note-content) or AppleScript attachment metadata (list-attachments).\nSafety: read-only; requires Full Disk Access and reports incomplete table metadata instead of guessing.",
43510
+ inputSchema: { id: noteIdInput },
43511
+ outputSchema: {
43512
+ id: external_exports.string().optional(),
43513
+ contentHash: external_exports.string().optional(),
43514
+ objects: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
43515
+ checklistItems: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
43516
+ nativeTags: external_exports.array(external_exports.string()).optional(),
43517
+ tables: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
43518
+ tableCellsComplete: external_exports.boolean().optional()
43519
+ },
43520
+ annotations: { readOnlyHint: true }
43521
+ },
43522
+ withErrorHandling(({ id }) => {
43523
+ const note = notesManager.getNoteById(id);
43524
+ if (!note) return errorResponse(`Note with ID "${id}" not found`);
43525
+ const body = notesManager.getNoteContentById(id);
43526
+ if (!body) return errorResponse(`Failed to read content of note "${note.title}"`);
43527
+ const rich = readRichNote(id);
43528
+ const tables = (rich.objectData || []).filter((object3) => object3.type?.includes("table")).map((object3) => {
43529
+ try {
43530
+ return {
43531
+ id: object3.id,
43532
+ attachmentId: id.replace(/ICNote\/p\d+$/, `ICAttachment/p${object3.pk}`),
43533
+ complete: true,
43534
+ ...parseNoteTable(Buffer.from(object3.mergeable, "hex"))
43535
+ };
43536
+ } catch (error2) {
43537
+ return { id: object3.id, complete: false, reason: String(error2) };
43538
+ }
43539
+ });
43540
+ for (const object3 of rich.objects || []) {
43541
+ if (object3.type.includes("table") && !tables.some((table) => table.id === object3.id)) {
43542
+ tables.push({
43543
+ id: object3.id,
43544
+ complete: false,
43545
+ reason: "Native table metadata is unavailable"
43546
+ });
43547
+ }
43548
+ }
43549
+ const richRead = {
43550
+ content: body,
43551
+ links: rich.links,
43552
+ nativeTags: rich.nativeTags,
43553
+ complete: true,
43554
+ writable: !rich.hasNativeObjects && !rich.hasChecklist,
43555
+ revision: rich.revision
43556
+ };
43557
+ return successResponse("Native objects read from the exact note", {
43558
+ id,
43559
+ contentHash: richContentHash(body, richRead),
43560
+ objects: rich.objects,
43561
+ checklistItems: rich.checklistItems,
43562
+ nativeTags: rich.nativeTags,
43563
+ tables,
43564
+ tableCellsComplete: tables.every((table) => table.complete)
43565
+ });
43566
+ }, "Error reading native objects")
43567
+ );
43568
+ registerTool(
43569
+ "list-native-tags",
43570
+ {
43571
+ description: "Use when: listing actual native Notes tags used in one explicit account and folder.\nReturns: each native tag mapped to exact matching note IDs, plus completeness and per-note errors.\nDo not use when: searching textual #hashtags in note bodies (search-notes).\nSafety: read-only; requires Full Disk Access and discloses partial reads.",
43572
+ inputSchema: {
43573
+ account: external_exports.string().min(1).max(MAX.ACCOUNT),
43574
+ folder: external_exports.string().min(1).max(MAX.FOLDER)
43575
+ },
43576
+ outputSchema: {
43577
+ tags: external_exports.record(external_exports.array(external_exports.string())).optional(),
43578
+ complete: external_exports.boolean().optional(),
43579
+ errors: external_exports.record(external_exports.string()).optional()
43580
+ },
43581
+ annotations: { readOnlyHint: true }
43582
+ },
43583
+ withErrorHandling(({ account, folder }) => {
43584
+ const tags = {};
43585
+ const errors = {};
43586
+ for (const note of notesManager.listNoteRefs(account, folder)) {
43587
+ try {
43588
+ for (const tag of readRichNote(note.id).nativeTags) (tags[tag] ||= []).push(note.id);
43589
+ } catch {
43590
+ errors[note.id] = "Native metadata unavailable";
43591
+ }
43592
+ }
43593
+ return successResponse("Native tags read from the requested folder", {
43594
+ tags,
43595
+ complete: Object.keys(errors).length === 0,
43596
+ errors
43597
+ });
43598
+ }, "Error listing native tags")
43599
+ );
43081
43600
  registerTool(
43082
43601
  "update-note",
43083
43602
  {
43084
- description: "Use when: replacing the body of one exact Apple Note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or the note has attachments.\nSafety: requires the exact note id and expectedContentHash from get-note-content. The server atomically rejects stale content and attachment-bearing notes, then reads the same id back after saving. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
43603
+ description: "Use when: replacing the body of one exact Apple Note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or the note has attachments.\nSafety: requires the exact note id and expectedContentHash from get-note-content. The server checks rich metadata revision, atomically checks the AppleScript body, blocks native objects/checklists, and verifies actual link destinations after saving. Preserve returned HTML links unless allowLinkChanges is explicitly requested. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
43085
43604
  inputSchema: {
43086
43605
  id: noteIdInput,
43087
43606
  expectedContentHash: expectedContentHashInput,
43607
+ allowLinkChanges: external_exports.boolean().optional().default(false).describe(
43608
+ "Set true only when the user explicitly intends to remove, relabel or change existing links. Defaults to preserving all links."
43609
+ ),
43088
43610
  newTitle: external_exports.string().max(MAX.TITLE).optional().describe(
43089
43611
  "New title for plaintext updates. Ignored when format is 'html'; include the visible title as the first line of newContent instead."
43090
43612
  ),
@@ -43103,72 +43625,90 @@ registerTool(
43103
43625
  verifiedVisibleText: external_exports.boolean().optional()
43104
43626
  }
43105
43627
  },
43106
- withErrorHandling(({ id, expectedContentHash, newTitle, newContent, format = "plaintext" }) => {
43107
- const snapshot = readExactNoteSnapshot(id);
43108
- if ("error" in snapshot) return errorResponse(snapshot.error);
43109
- if (snapshot.contentHash !== expectedContentHash) {
43110
- return errorResponse(revisionConflictMessage(snapshot.note.title));
43111
- }
43112
- const attachments = notesManager.listAttachmentsById(id);
43113
- if (attachments.length > 0) {
43114
- return errorResponse(
43115
- `Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
43116
- );
43117
- }
43118
- const result = notesManager.updateNoteByIdIfUnchanged(
43628
+ withErrorHandling(
43629
+ ({
43119
43630
  id,
43120
- snapshot.note.title,
43121
- snapshot.body,
43631
+ expectedContentHash,
43122
43632
  newTitle,
43123
43633
  newContent,
43124
- format
43125
- );
43126
- if (result.status === "conflict") {
43127
- return errorResponse(revisionConflictMessage(snapshot.note.title));
43128
- }
43129
- if (result.status === "attachments") {
43130
- return errorResponse(
43131
- `Note "${snapshot.note.title}" gained an attachment before saving. No content was replaced.`
43634
+ format = "plaintext",
43635
+ allowLinkChanges = false
43636
+ }) => {
43637
+ const snapshot = readExactNoteSnapshot(id);
43638
+ if ("error" in snapshot) return errorResponse(snapshot.error);
43639
+ if (snapshot.contentHash !== expectedContentHash) {
43640
+ return errorResponse(revisionConflictMessage(snapshot.note.title));
43641
+ }
43642
+ assertLinkedWrite(snapshot.rich, newContent, format, allowLinkChanges);
43643
+ const attachments = notesManager.listAttachmentsById(id);
43644
+ if (attachments.length > 0) {
43645
+ return errorResponse(
43646
+ `Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
43647
+ );
43648
+ }
43649
+ const result = notesManager.updateNoteByIdIfUnchanged(
43650
+ id,
43651
+ snapshot.note.title,
43652
+ snapshot.body,
43653
+ newTitle,
43654
+ newContent,
43655
+ format,
43656
+ snapshot.rich.revision
43132
43657
  );
43133
- }
43134
- if (result.status !== "updated") {
43135
- return errorResponse(
43136
- `The update result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
43658
+ if (result.status === "conflict") {
43659
+ return errorResponse(revisionConflictMessage(snapshot.note.title));
43660
+ }
43661
+ if (result.status === "attachments") {
43662
+ return errorResponse(
43663
+ `Note "${snapshot.note.title}" gained an attachment before saving. No content was replaced.`
43664
+ );
43665
+ }
43666
+ if (result.status !== "updated") {
43667
+ return errorResponse(
43668
+ `The update result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
43669
+ );
43670
+ }
43671
+ const readback = notesManager.getNoteContentById(id);
43672
+ const richReadback = enrichNoteRead(id, readback || "");
43673
+ const contentHash = readback ? richContentHash(readback, richReadback) : "";
43674
+ if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
43675
+ return errorResponse(
43676
+ "The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
43677
+ );
43678
+ }
43679
+ if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
43680
+ return errorResponse(
43681
+ `The note accepted an update, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
43682
+ );
43683
+ }
43684
+ const displayTitle = resolveUpdateResponseTitle(
43685
+ snapshot.note.title,
43686
+ newTitle,
43687
+ format,
43688
+ newContent
43137
43689
  );
43138
- }
43139
- const readback = notesManager.getNoteContentById(id);
43140
- const contentHash = readback ? hashNoteContent(readback) : "";
43141
- if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
43142
- return errorResponse(
43143
- `The note accepted an update, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
43690
+ const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
43691
+ const checklistWarning = detectChecklistAttempt(newContent) ?? "";
43692
+ return successResponse(
43693
+ `Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
43694
+ {
43695
+ ok: true,
43696
+ id,
43697
+ title: displayTitle,
43698
+ shared: snapshot.note.shared ?? false,
43699
+ previousContentHash: expectedContentHash,
43700
+ contentHash,
43701
+ verifiedVisibleText: true
43702
+ }
43144
43703
  );
43145
- }
43146
- const displayTitle = resolveUpdateResponseTitle(
43147
- snapshot.note.title,
43148
- newTitle,
43149
- format,
43150
- newContent
43151
- );
43152
- const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
43153
- const checklistWarning = detectChecklistAttempt(newContent) ?? "";
43154
- return successResponse(
43155
- `Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
43156
- {
43157
- ok: true,
43158
- id,
43159
- title: displayTitle,
43160
- shared: snapshot.note.shared ?? false,
43161
- previousContentHash: expectedContentHash,
43162
- contentHash,
43163
- verifiedVisibleText: true
43164
- }
43165
- );
43166
- }, "Error updating note")
43704
+ },
43705
+ "Error updating note"
43706
+ )
43167
43707
  );
43168
43708
  registerTool(
43169
43709
  "append-to-note",
43170
43710
  {
43171
- description: "Use when: adding content to one exact note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or it has attachments.\nSafety: append still rewrites the full HTML body, so it uses the same exact-ID, revision, attachment, and readback guards as update-note. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
43711
+ description: "Use when: adding content to one exact note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or it has attachments or native objects.\nSafety: append rewrites the HTML body, so it uses exact-ID, rich revision, native-object, attachment, link, and readback guards. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
43172
43712
  inputSchema: {
43173
43713
  id: noteIdInput,
43174
43714
  expectedContentHash: expectedContentHashInput,
@@ -43216,15 +43756,21 @@ registerTool(
43216
43756
  if (snapshot.contentHash !== expectedContentHash) {
43217
43757
  return errorResponse(revisionConflictMessage(snapshot.note.title));
43218
43758
  }
43759
+ if (!snapshot.rich.writable) {
43760
+ return errorResponse(
43761
+ snapshot.rich.warning || `Note "${snapshot.note.title}" contains native objects that cannot be preserved by a full-body append.`
43762
+ );
43763
+ }
43219
43764
  const attachments = notesManager.listAttachmentsById(id);
43220
43765
  if (attachments.length > 0) {
43221
43766
  return errorResponse(
43222
43767
  `Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
43223
43768
  );
43224
43769
  }
43225
- const firstDivEnd = snapshot.body.indexOf("</div>");
43226
- const titleDiv = firstDivEnd !== -1 ? snapshot.body.slice(0, firstDivEnd + 6) : "";
43227
- const bodyHtml = firstDivEnd !== -1 ? snapshot.body.slice(firstDivEnd + 6) : snapshot.body;
43770
+ assertLinkedWrite(snapshot.rich, snapshot.rich.content, "html");
43771
+ const firstDivEnd = snapshot.rich.content.indexOf("</div>");
43772
+ const titleDiv = firstDivEnd !== -1 ? snapshot.rich.content.slice(0, firstDivEnd + 6) : "";
43773
+ const bodyHtml = firstDivEnd !== -1 ? snapshot.rich.content.slice(firstDivEnd + 6) : snapshot.rich.content;
43228
43774
  const newBlock = contentToHtml(content);
43229
43775
  const sepHtml = separatorToHtml(separator);
43230
43776
  const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
@@ -43234,7 +43780,8 @@ registerTool(
43234
43780
  snapshot.body,
43235
43781
  void 0,
43236
43782
  combinedBody,
43237
- "html"
43783
+ "html",
43784
+ snapshot.rich.revision
43238
43785
  );
43239
43786
  if (result.status === "conflict") {
43240
43787
  return errorResponse(revisionConflictMessage(snapshot.note.title));
@@ -43250,7 +43797,13 @@ registerTool(
43250
43797
  );
43251
43798
  }
43252
43799
  const readback = notesManager.getNoteContentById(id);
43253
- const contentHash = readback ? hashNoteContent(readback) : "";
43800
+ const richReadback = enrichNoteRead(id, readback || "");
43801
+ const contentHash = readback ? richContentHash(readback, richReadback) : "";
43802
+ if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
43803
+ return errorResponse(
43804
+ "The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
43805
+ );
43806
+ }
43254
43807
  if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
43255
43808
  return errorResponse(
43256
43809
  `The note accepted an append, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",