apple-notes-mcp 2.15.4 → 2.15.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 +21 -3
  2. package/build/index.js +509 -255
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -761,7 +761,7 @@ Disk Access.
761
761
 
762
762
  #### `get-note-tables`
763
763
 
764
- Reads every native table in one exact note, in body order, from the NoteStore
764
+ Reads a page of native tables in one exact note, in body order, from the NoteStore
765
765
  database. Each table is returned as GitHub-flavored Markdown and as JSON `rows`
766
766
  with stable `rowIds` and `columnIds`. Notes tables have no header row, so the
767
767
  Markdown uses the first row as the header. Pipes are escaped as `\|`,
@@ -770,8 +770,26 @@ backslashes are doubled, and line breaks inside a cell become `<br>`.
770
770
  | Parameter | Type | Required | Description |
771
771
  |-----------|------|----------|-------------|
772
772
  | `id` | string | Yes | Exact note ID (`x-coredata://…/ICNote/pNNN`) |
773
-
774
- `tableCellsComplete` is false when any table or cell could not be decoded. A
773
+ | `offset` | integer | No | Zero-based position of the first table (default 0); use `page.nextOffset` |
774
+ | `limit` | integer | No | Maximum tables per page (default and maximum 500) |
775
+ | `maxBytes` | integer | No | Full serialized response budget in UTF-8 bytes (default 4 MiB, minimum 1024 bytes, maximum 8 MiB) |
776
+
777
+ The byte budget includes both the text response and structured data, including
778
+ repeated Markdown and JSON escaping. Tables are returned whole. `tableCount`
779
+ and `page.totalAvailable` count all tables in the note; `page.returned` counts
780
+ the entries in this response. While `page.hasMore` is true, call again with
781
+ `offset: page.nextOffset` and the same `limit` and `maxBytes`. Each call reads
782
+ the note again, so finish paging before editing it.
783
+
784
+ If one table exceeds an otherwise empty page, its entry contains identifying
785
+ metadata, `complete: false`, `contentOmitted: true`, and a `reason`, without
786
+ partial rows or row/column IDs. The next offset advances past that entry. To
787
+ retrieve its content, retry that offset with a larger `maxBytes`, or export the
788
+ note to a file. If even the metadata exceeds the budget, the tool returns a
789
+ validation error. `page.stoppedAtSizeLimit` reports a byte-budget stop.
790
+
791
+ `tableCellsComplete` is false when any table or cell could not be decoded, or
792
+ when the current page omits a table's content for its byte budget. A
775
793
  cell holding an embedded object is `null` in `rows`, listed in
776
794
  `incompleteCells`, and shown as `[undecoded cell]` in Markdown. A table whose
777
795
  data cannot be decoded at all has `complete: false`, a `reason`, and no rows.
package/build/index.js CHANGED
@@ -27885,8 +27885,8 @@ var ZodSet = class _ZodSet extends ZodType {
27885
27885
  maxSize: { value: maxSize, message: errorUtil.toString(message) }
27886
27886
  });
27887
27887
  }
27888
- size(size, message) {
27889
- return this.min(size, message).max(size, message);
27888
+ size(size2, message) {
27889
+ return this.min(size2, message).max(size2, message);
27890
27890
  }
27891
27891
  nonempty(message) {
27892
27892
  return this.min(1, message);
@@ -36551,7 +36551,7 @@ var Protocol = class {
36551
36551
  if (abortController.signal.aborted) {
36552
36552
  return;
36553
36553
  }
36554
- const response = {
36554
+ const response2 = {
36555
36555
  result,
36556
36556
  jsonrpc: "2.0",
36557
36557
  id: request.id
@@ -36559,11 +36559,11 @@ var Protocol = class {
36559
36559
  if (relatedTaskId && this._taskMessageQueue) {
36560
36560
  await this._enqueueTaskMessage(relatedTaskId, {
36561
36561
  type: "response",
36562
- message: response,
36562
+ message: response2,
36563
36563
  timestamp: Date.now()
36564
36564
  }, capturedTransport?.sessionId);
36565
36565
  } else {
36566
- await capturedTransport?.send(response);
36566
+ await capturedTransport?.send(response2);
36567
36567
  }
36568
36568
  }, async (error2) => {
36569
36569
  if (abortController.signal.aborted) {
@@ -36616,29 +36616,29 @@ var Protocol = class {
36616
36616
  }
36617
36617
  handler(params);
36618
36618
  }
36619
- _onresponse(response) {
36620
- const messageId = Number(response.id);
36619
+ _onresponse(response2) {
36620
+ const messageId = Number(response2.id);
36621
36621
  const resolver = this._requestResolvers.get(messageId);
36622
36622
  if (resolver) {
36623
36623
  this._requestResolvers.delete(messageId);
36624
- if (isJSONRPCResultResponse(response)) {
36625
- resolver(response);
36624
+ if (isJSONRPCResultResponse(response2)) {
36625
+ resolver(response2);
36626
36626
  } else {
36627
- const error2 = new McpError(response.error.code, response.error.message, response.error.data);
36627
+ const error2 = new McpError(response2.error.code, response2.error.message, response2.error.data);
36628
36628
  resolver(error2);
36629
36629
  }
36630
36630
  return;
36631
36631
  }
36632
36632
  const handler = this._responseHandlers.get(messageId);
36633
36633
  if (handler === void 0) {
36634
- this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response)}`));
36634
+ this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response2)}`));
36635
36635
  return;
36636
36636
  }
36637
36637
  this._responseHandlers.delete(messageId);
36638
36638
  this._cleanupTimeout(messageId);
36639
36639
  let isTaskResponse = false;
36640
- if (isJSONRPCResultResponse(response) && response.result && typeof response.result === "object") {
36641
- const result = response.result;
36640
+ if (isJSONRPCResultResponse(response2) && response2.result && typeof response2.result === "object") {
36641
+ const result = response2.result;
36642
36642
  if (result.task && typeof result.task === "object") {
36643
36643
  const task = result.task;
36644
36644
  if (typeof task.taskId === "string") {
@@ -36650,10 +36650,10 @@ var Protocol = class {
36650
36650
  if (!isTaskResponse) {
36651
36651
  this._progressHandlers.delete(messageId);
36652
36652
  }
36653
- if (isJSONRPCResultResponse(response)) {
36654
- handler(response);
36653
+ if (isJSONRPCResultResponse(response2)) {
36654
+ handler(response2);
36655
36655
  } else {
36656
- const error2 = McpError.fromError(response.error.code, response.error.message, response.error.data);
36656
+ const error2 = McpError.fromError(response2.error.code, response2.error.message, response2.error.data);
36657
36657
  handler(error2);
36658
36658
  }
36659
36659
  }
@@ -36825,15 +36825,15 @@ var Protocol = class {
36825
36825
  const error2 = reason instanceof McpError ? reason : new McpError(ErrorCode.RequestTimeout, String(reason));
36826
36826
  reject(error2);
36827
36827
  };
36828
- this._responseHandlers.set(messageId, (response) => {
36828
+ this._responseHandlers.set(messageId, (response2) => {
36829
36829
  if (options?.signal?.aborted) {
36830
36830
  return;
36831
36831
  }
36832
- if (response instanceof Error) {
36833
- return reject(response);
36832
+ if (response2 instanceof Error) {
36833
+ return reject(response2);
36834
36834
  }
36835
36835
  try {
36836
- const parseResult = safeParse2(resultSchema, response.result);
36836
+ const parseResult = safeParse2(resultSchema, response2.result);
36837
36837
  if (!parseResult.success) {
36838
36838
  reject(parseResult.error);
36839
36839
  } else {
@@ -36851,10 +36851,10 @@ var Protocol = class {
36851
36851
  this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
36852
36852
  const relatedTaskId = relatedTask?.taskId;
36853
36853
  if (relatedTaskId) {
36854
- const responseResolver = (response) => {
36854
+ const responseResolver = (response2) => {
36855
36855
  const handler = this._responseHandlers.get(messageId);
36856
36856
  if (handler) {
36857
- handler(response);
36857
+ handler(response2);
36858
36858
  } else {
36859
36859
  this._onerror(new Error(`Response handler missing for side-channeled request ${messageId}`));
36860
36860
  }
@@ -41977,10 +41977,10 @@ function readFileBase64(p) {
41977
41977
  return readFileSync(p).toString("base64");
41978
41978
  }
41979
41979
  function readFileBase64Capped(p, maxBytes = maxAttachmentBytes()) {
41980
- const size = fileSize(p);
41981
- if (size > maxBytes) {
41980
+ const size2 = fileSize(p);
41981
+ if (size2 > maxBytes) {
41982
41982
  throw new Error(
41983
- `Attachment is ${size} bytes, exceeding the ${maxBytes}-byte fetch limit (APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES). Use save-attachment to export it to disk instead.`
41983
+ `Attachment is ${size2} bytes, exceeding the ${maxBytes}-byte fetch limit (APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES). Use save-attachment to export it to disk instead.`
41984
41984
  );
41985
41985
  }
41986
41986
  return readFileBase64(p);
@@ -47039,11 +47039,11 @@ function decodeInline(fields) {
47039
47039
  }
47040
47040
  if (font) {
47041
47041
  const name = stringOf(font, 1);
47042
- const size = fixed32Float(first(font, 2));
47043
- if (name || size !== void 0)
47042
+ const size2 = fixed32Float(first(font, 2));
47043
+ if (name || size2 !== void 0)
47044
47044
  inline.font = {
47045
47045
  ...name ? { name } : {},
47046
- ...size !== void 0 && Number.isFinite(size) ? { size } : {}
47046
+ ...size2 !== void 0 && Number.isFinite(size2) ? { size: size2 } : {}
47047
47047
  };
47048
47048
  }
47049
47049
  const attachment = sub3(fields, 12);
@@ -47236,8 +47236,8 @@ function pageNoteBlocks(doc, { offset = 0, limit = 500, maxBytes = blocksMaxResp
47236
47236
  let bytes = 0;
47237
47237
  for (let i = start; i < total && blocks.length < limit; i++) {
47238
47238
  let block = doc.blocks[i];
47239
- let size = Buffer.byteLength(JSON.stringify(block));
47240
- if (bytes + size > maxBytes) {
47239
+ let size2 = Buffer.byteLength(JSON.stringify(block));
47240
+ if (bytes + size2 > maxBytes) {
47241
47241
  if (blocks.length) break;
47242
47242
  block = {
47243
47243
  ...block,
@@ -47245,10 +47245,10 @@ function pageNoteBlocks(doc, { offset = 0, limit = 500, maxBytes = blocksMaxResp
47245
47245
  textOmitted: true,
47246
47246
  runs: block.runs.map((run) => ({ ...run, text: "" }))
47247
47247
  };
47248
- size = Buffer.byteLength(JSON.stringify(block));
47248
+ size2 = Buffer.byteLength(JSON.stringify(block));
47249
47249
  }
47250
47250
  blocks.push(block);
47251
- bytes += size;
47251
+ bytes += size2;
47252
47252
  }
47253
47253
  const next = start + blocks.length;
47254
47254
  return {
@@ -48713,33 +48713,11 @@ import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync2, rmSync as
48713
48713
  import { tmpdir as tmpdir3 } from "node:os";
48714
48714
  import { join as join17 } from "node:path";
48715
48715
 
48716
- // src/services/shortcutConsent.ts
48717
- function shortcutConsentHint(shortcut) {
48718
- const target = shortcut ? `"${shortcut}"` : "the bridge Shortcut";
48719
- return `This may be an unanswered first-run Shortcuts consent prompt, which a background run cannot display: run ${target} once in the foreground in Shortcuts.app and choose Always Allow.`;
48720
- }
48721
-
48722
- // src/services/nativeTags.ts
48723
- var NATIVE_TAGS_SHORTCUT = "Apple Notes MCP - Native Tags";
48724
- function normalizeNativeTags(tags) {
48725
- if (!tags.length || tags.length > 100) throw new Error("Provide between 1 and 100 tags");
48726
- return [
48727
- ...new Set(
48728
- tags.map((value) => {
48729
- const tag = value.normalize("NFC").replace(/^#/, "");
48730
- if (tag.length > 100 || !new RegExp("^(?=.*\\p{L})[\\p{L}\\p{N}_-]+$", "u").test(tag))
48731
- throw new Error(
48732
- "Tags must contain a letter and only letters, digits, hyphens or underscores"
48733
- );
48734
- return tag;
48735
- })
48736
- )
48737
- ];
48738
- }
48716
+ // src/utils/richContentPreservation.ts
48739
48717
  var TAG_OBJECT_TYPE = "com.apple.notes.inlinetextattachment.hashtag";
48740
48718
  function requirePreservationMetadata(rich) {
48741
- const unavailable = () => {
48742
- throw new Error("Native object, checklist or formatting preservation metadata is unavailable");
48719
+ const unavailable = (message = "Native object, checklist or formatting preservation metadata is unavailable") => {
48720
+ throw new Error(message);
48743
48721
  };
48744
48722
  const { objects, objectData, checklistItems, styleRuns, nativeTagObjectIds } = rich;
48745
48723
  if (!objects || !objectData || !checklistItems || !styleRuns || !nativeTagObjectIds || rich.nativeObjectDataComplete !== true)
@@ -48774,16 +48752,173 @@ function requirePreservationMetadata(rich) {
48774
48752
  return unavailable();
48775
48753
  }
48776
48754
  let position = 0;
48755
+ const firstReferences = /* @__PURE__ */ new Map();
48777
48756
  for (const run of styleRuns) {
48778
48757
  if (run.start !== position || !Number.isInteger(run.length) || run.length < 0 || typeof run.signature !== "string" || run.nativeSemantics?.complete !== true || run.length === 0 && (run.nativeSemantics.unknown || run.nativeSemantics.structuredParagraph || run.nativeSemantics.links || run.nativeSemantics.objects.length > 0))
48779
48758
  return unavailable();
48759
+ const references = run.nativeSemantics.objects;
48760
+ if (references.length > 1 || rich.text.slice(run.start, run.start + run.length).includes("\uFFFC") && references.length !== 1)
48761
+ return unavailable("Native object placement preservation metadata is unavailable");
48762
+ for (const reference of references) {
48763
+ if (!objectData.some((data) => data.id === reference.id && data.type === reference.type))
48764
+ return unavailable("Native object placement preservation metadata is unavailable");
48765
+ if (!firstReferences.has(reference.id))
48766
+ firstReferences.set(reference.id, {
48767
+ start: run.start,
48768
+ length: run.length,
48769
+ type: reference.type
48770
+ });
48771
+ }
48780
48772
  position += run.length;
48781
48773
  }
48782
48774
  if (position !== rich.text.length) return unavailable();
48775
+ if (objects.some((object3) => {
48776
+ const reference = firstReferences.get(object3.id);
48777
+ return !reference || reference.start !== object3.start || reference.length !== object3.length || reference.type !== object3.type;
48778
+ }))
48779
+ return unavailable("Native object placement preservation metadata is unavailable");
48783
48780
  for (const link of rich.links)
48784
48781
  if (!Number.isInteger(link.start) || !Number.isInteger(link.length) || link.start < 0 || link.length < 1 || link.start + link.length > rich.text.length || rich.text.slice(link.start, link.start + link.length) !== link.text)
48785
48782
  return unavailable();
48786
48783
  }
48784
+ function assertZeroLengthFormatting(before, after, spans) {
48785
+ const oldEmpty = before.styleRuns.filter((run) => run.length === 0).map((run) => {
48786
+ const span = spans.find(
48787
+ (span2) => run.start >= span2.before && run.start <= span2.before + span2.length
48788
+ );
48789
+ if (!span) throw new Error("Zero-length formatting falls inside the requested removal");
48790
+ return { ...run, start: span.after + run.start - span.before };
48791
+ });
48792
+ const newEmpty = after.styleRuns.filter(
48793
+ (run) => run.length === 0 && spans.some((span) => run.start >= span.after && run.start <= span.after + span.length)
48794
+ );
48795
+ if (JSON.stringify(oldEmpty) !== JSON.stringify(newEmpty))
48796
+ throw new Error("Existing zero-length formatting metadata changed");
48797
+ }
48798
+ function retainedSpans(before, after, change) {
48799
+ if (change.kind !== "remove-tag") {
48800
+ if (change.kind === "append" ? !after.text.startsWith(before.text) : after.text !== before.text)
48801
+ throw new Error("Existing note text or whitespace was not preserved");
48802
+ return {
48803
+ spans: [{ before: 0, after: 0, length: before.text.length }],
48804
+ removed: /* @__PURE__ */ new Set()
48805
+ };
48806
+ }
48807
+ const removed = new Set(before.nativeTagObjectIds[change.tag]);
48808
+ if (!removed.size || after.nativeTags.includes(change.tag))
48809
+ throw new Error("Native tag removal was not verified");
48810
+ const spans = [];
48811
+ let position = 0, output = "";
48812
+ for (const object3 of before.objects.filter((object4) => removed.has(object4.id))) {
48813
+ const text2 = before.text.slice(object3.start, object3.start + object3.length);
48814
+ if (text2 !== "\uFFFC" && text2 !== `#${change.tag}`)
48815
+ throw new Error("Native tag removal range is unsupported");
48816
+ const prefix = before.text.slice(position, object3.start);
48817
+ spans.push({ before: position, after: output.length, length: prefix.length });
48818
+ output += prefix;
48819
+ position = object3.start + object3.length;
48820
+ }
48821
+ spans.push({ before: position, after: output.length, length: before.text.length - position });
48822
+ output += before.text.slice(position);
48823
+ if (after.text !== output)
48824
+ throw new Error("Existing note text or whitespace was not preserved around the removed tag");
48825
+ return { spans, removed };
48826
+ }
48827
+ function assertRetainedRichContent(before, after, change = { kind: "unchanged" }) {
48828
+ requirePreservationMetadata(before);
48829
+ requirePreservationMetadata(after);
48830
+ const { spans, removed } = retainedSpans(before, after, change);
48831
+ const rangeStart = (start, length) => {
48832
+ const span = spans.find(
48833
+ (span2) => start >= span2.before && start + length <= span2.before + span2.length
48834
+ );
48835
+ return span && span.after + start - span.before;
48836
+ };
48837
+ const retainedIds = before.nativeObjectIds.filter((id2) => !removed.has(id2));
48838
+ if (retainedIds.some((id2) => !after.nativeObjectIds.includes(id2)) || [...removed].some((id2) => after.nativeObjectIds.includes(id2)) || change.kind !== "append" && after.nativeObjectIds.length !== retainedIds.length || change.kind === "append" && after.objects.some(
48839
+ (object3) => !retainedIds.includes(object3.id) && object3.start < before.text.length
48840
+ ))
48841
+ throw new Error("Existing native object identity or placement changed");
48842
+ for (const object3 of before.objects.filter((object4) => !removed.has(object4.id))) {
48843
+ const actual = after.objects.find((current) => current.id === object3.id);
48844
+ if (actual.type !== object3.type || actual.length !== object3.length || actual.start !== rangeStart(object3.start, object3.length))
48845
+ throw new Error("Existing native object range changed");
48846
+ const oldData = before.objectData.find((row) => row.id === object3.id), newData = after.objectData.find((row) => row.id === object3.id);
48847
+ if (oldData.pk !== newData.pk || oldData.type !== newData.type || oldData.mergeable !== newData.mergeable || oldData.view !== newData.view || oldData.altText !== newData.altText)
48848
+ throw new Error("Existing native object payload changed");
48849
+ }
48850
+ const tags = before.nativeTags.filter(
48851
+ (tag) => change.kind !== "remove-tag" || tag !== change.tag
48852
+ );
48853
+ if (tags.some(
48854
+ (tag) => JSON.stringify(before.nativeTagObjectIds[tag]) !== JSON.stringify(after.nativeTagObjectIds[tag])
48855
+ ) || change.kind !== "append" && after.nativeTags.length !== tags.length)
48856
+ throw new Error("Existing native tag identities changed");
48857
+ const links = change.kind === "append" ? after.links.filter((link) => link.start < before.text.length) : after.links;
48858
+ if (links.length !== before.links.length || before.links.some((link, index) => {
48859
+ const actual = links[index];
48860
+ return actual.text !== link.text || actual.url !== link.url || actual.length !== link.length || actual.start !== rangeStart(link.start, link.length);
48861
+ }))
48862
+ throw new Error("Existing links or their ranges were not preserved");
48863
+ const items = change.kind === "append" ? after.checklistItems.filter((item) => item.start < before.text.length) : after.checklistItems;
48864
+ if (items.length !== before.checklistItems.length || before.checklistItems.some((item, index) => {
48865
+ const actual = items[index];
48866
+ const retained = spans.flatMap((span) => {
48867
+ const start2 = Math.max(item.start, span.before), end = Math.min(item.start + item.text.length, span.before + span.length);
48868
+ return start2 < end ? [before.text.slice(start2, end)] : [];
48869
+ }).join("");
48870
+ const startSpan = spans.find(
48871
+ (span) => item.start >= span.before && item.start <= span.before + span.length
48872
+ );
48873
+ const start = startSpan && startSpan.after + item.start - startSpan.before;
48874
+ return actual.id !== item.id || actual.text !== retained || actual.done !== item.done || actual.start !== start;
48875
+ }))
48876
+ throw new Error("Existing checklist item identity, state or range changed");
48877
+ for (const span of spans) {
48878
+ let offset = 0, oldIndex = 0, newIndex = 0;
48879
+ while (offset < span.length) {
48880
+ const oldPosition = span.before + offset, newPosition = span.after + offset;
48881
+ while (before.styleRuns[oldIndex].start + before.styleRuns[oldIndex].length <= oldPosition)
48882
+ oldIndex++;
48883
+ while (after.styleRuns[newIndex].start + after.styleRuns[newIndex].length <= newPosition)
48884
+ newIndex++;
48885
+ const oldRun = before.styleRuns[oldIndex], newRun = after.styleRuns[newIndex];
48886
+ if (oldRun.signature !== newRun.signature || oldRun.paragraphStyle !== newRun.paragraphStyle || oldRun.blockQuote !== newRun.blockQuote || oldRun.highlight !== newRun.highlight)
48887
+ throw new Error("Existing rich formatting changed");
48888
+ offset += Math.min(
48889
+ span.length - offset,
48890
+ oldRun.start + oldRun.length - oldPosition,
48891
+ newRun.start + newRun.length - newPosition
48892
+ );
48893
+ }
48894
+ }
48895
+ assertZeroLengthFormatting(before, after, spans);
48896
+ }
48897
+
48898
+ // src/services/shortcutConsent.ts
48899
+ function shortcutConsentHint(shortcut) {
48900
+ const target = shortcut ? `"${shortcut}"` : "the bridge Shortcut";
48901
+ return `This may be an unanswered first-run Shortcuts consent prompt, which a background run cannot display: run ${target} once in the foreground in Shortcuts.app and choose Always Allow.`;
48902
+ }
48903
+
48904
+ // src/services/nativeTags.ts
48905
+ var NATIVE_TAGS_SHORTCUT = "Apple Notes MCP - Native Tags";
48906
+ function normalizeNativeTags(tags) {
48907
+ if (!tags.length || tags.length > 100) throw new Error("Provide between 1 and 100 tags");
48908
+ return [
48909
+ ...new Set(
48910
+ tags.map((value) => {
48911
+ const tag = value.normalize("NFC").replace(/^#/, "");
48912
+ if (tag.length > 100 || !new RegExp("^(?=.*\\p{L})[\\p{L}\\p{N}_-]+$", "u").test(tag))
48913
+ throw new Error(
48914
+ "Tags must contain a letter and only letters, digits, hyphens or underscores"
48915
+ );
48916
+ return tag;
48917
+ })
48918
+ )
48919
+ ];
48920
+ }
48921
+ var TAG_OBJECT_TYPE2 = "com.apple.notes.inlinetextattachment.hashtag";
48787
48922
  function plainNativeRange(rich, start, length) {
48788
48923
  return rich.styleRuns.every((run) => {
48789
48924
  if (run.start >= start + length || run.start + run.length <= start) return true;
@@ -48799,7 +48934,7 @@ function preservedTextSpans(before, after, missing) {
48799
48934
  ([tag, ids]) => ids.map((id2) => [id2, tag])
48800
48935
  )
48801
48936
  );
48802
- if (!added.length || added.some((o) => o.type !== TAG_OBJECT_TYPE || !missing.includes(tagsById.get(o.id))) || missing.some((tag) => !added.some((o) => tagsById.get(o.id) === tag)))
48937
+ if (!added.length || added.some((o) => o.type !== TAG_OBJECT_TYPE2 || !missing.includes(tagsById.get(o.id))) || missing.some((tag) => !added.some((o) => tagsById.get(o.id) === tag)))
48803
48938
  return void 0;
48804
48939
  const spans = [];
48805
48940
  let oldPosition = 0, newPosition = 0, appended = false;
@@ -48856,6 +48991,11 @@ function preservesAddedSemantics(before, after, spans) {
48856
48991
  return true;
48857
48992
  }
48858
48993
  function preservesNativeContent(before, after, spans) {
48994
+ try {
48995
+ assertZeroLengthFormatting(before, after, spans);
48996
+ } catch {
48997
+ return false;
48998
+ }
48859
48999
  const rangeStart = (start, length) => {
48860
49000
  const span = spans.find((s) => start >= s.before && start + length <= s.before + s.length);
48861
49001
  return span && span.after + start - span.before;
@@ -48906,6 +49046,16 @@ function preservesNativeContent(before, after, spans) {
48906
49046
  }
48907
49047
  return true;
48908
49048
  }
49049
+ function nativeTagAdditionPreserved(before, after, tags) {
49050
+ requirePreservationMetadata(before);
49051
+ requirePreservationMetadata(after);
49052
+ const missing = tags.filter((tag) => !before.nativeTags.includes(tag));
49053
+ const allTags = [.../* @__PURE__ */ new Set([...before.nativeTags, ...tags])];
49054
+ const spans = preservedTextSpans(before, after, missing);
49055
+ return after.nativeTags.length === allTags.length && allTags.every((tag) => after.nativeTags.includes(tag)) && before.nativeObjectIds.every((id2) => after.nativeObjectIds.includes(id2)) && before.hasChecklist === after.hasChecklist && Boolean(
49056
+ spans && preservesAddedSemantics(before, after, spans) && preservesNativeContent(before, after, spans)
49057
+ );
49058
+ }
48909
49059
  function addNativeTags(request, deps) {
48910
49060
  if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(request.id))
48911
49061
  throw new Error("An exact CoreData note ID is required");
@@ -48947,9 +49097,7 @@ function addNativeTags(request, deps) {
48947
49097
  } catch (error2) {
48948
49098
  throw verificationFailure(error2 instanceof Error ? error2.message : String(error2));
48949
49099
  }
48950
- const allTags = [.../* @__PURE__ */ new Set([...before.rich.nativeTags, ...tags])];
48951
- const spans = preservedTextSpans(before.rich, after.rich, missing);
48952
- if (after.title !== before.title || after.rich.nativeTags.length !== allTags.length || allTags.some((tag) => !after.rich.nativeTags.includes(tag)) || before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)) || before.rich.hasChecklist !== after.rich.hasChecklist || !spans || !preservesAddedSemantics(before.rich, after.rich, spans) || !preservesNativeContent(before.rich, after.rich, spans)) {
49100
+ if (after.title !== before.title || !nativeTagAdditionPreserved(before.rich, after.rich, tags)) {
48953
49101
  throw verificationFailure();
48954
49102
  }
48955
49103
  return {
@@ -49389,53 +49537,25 @@ function backgroundDependencies(manager) {
49389
49537
  function assertPreserved(before, after, options = {}) {
49390
49538
  if (before.id !== after.id || before.title !== after.title)
49391
49539
  throw new Error("Note identity changed");
49392
- const tidy = (s) => s.replace(/\r\n/g, "\n").replace(/[\s\ufffc]+$/gu, "");
49393
- let oldText = tidy(before.rich.text), newText = tidy(after.rich.text);
49394
- if (options.tagChange) {
49395
- const escaped = options.tagChange.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49396
- const strip = (s) => s.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "").replace(/[\s\ufffc]/gu, "");
49397
- oldText = strip(oldText);
49398
- newText = strip(newText);
49399
- }
49400
- if (options.append ? !newText.startsWith(oldText) : newText !== oldText)
49401
- throw new Error("Existing note text was not preserved");
49402
- const links = options.append ? after.rich.links.slice(0, before.rich.links.length) : after.rich.links;
49403
- if (linkSignature(before.rich.links) !== linkSignature(links))
49404
- throw new Error("Existing links were not preserved");
49405
- if (!options.tagChange && before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)))
49406
- throw new Error("Existing native object was lost");
49407
- if (JSON.stringify(before.checklist) !== JSON.stringify(
49408
- options.append ? after.checklist.slice(0, before.checklist.length) : after.checklist
49409
- ))
49410
- throw new Error("Existing checklist items changed");
49411
- for (const item of before.rich.checklistItems || []) {
49412
- const actual = after.rich.checklistItems?.find((current) => current.id === item.id);
49413
- if (!actual || actual.text !== item.text || actual.done !== item.done)
49414
- throw new Error("Existing checklist item identity or state changed");
49415
- }
49416
- if (before.rich.nativeTags.filter((t) => t !== options.tagChange).some((t) => !after.rich.nativeTags.includes(t)))
49417
- throw new Error("Existing native tag was lost");
49418
- const allowedRemoved = options.tagChange ? before.rich.nativeTagObjectIds?.[options.tagChange] || [] : [];
49419
- for (const object3 of before.rich.objectData || []) {
49420
- if (allowedRemoved.includes(object3.id)) continue;
49421
- const actual = after.rich.objectData?.find((o) => o.id === object3.id);
49422
- if (!actual || actual.mergeable !== object3.mergeable || actual.view !== object3.view)
49423
- throw new Error("Existing native object content or presentation changed");
49424
- }
49425
- if (!options.tagChange && before.rich.styleRuns && after.rich.styleRuns) {
49426
- const end = before.rich.text.trimEnd().length;
49427
- for (const oldRun of before.rich.styleRuns)
49428
- for (const newRun of after.rich.styleRuns) {
49429
- const overlapStart = Math.max(oldRun.start, newRun.start);
49430
- const overlapEnd = Math.min(
49431
- end,
49432
- oldRun.start + oldRun.length,
49433
- newRun.start + newRun.length
49434
- );
49435
- if (overlapStart < overlapEnd && /[^\s\ufffc]/u.test(before.rich.text.slice(overlapStart, overlapEnd)) && oldRun.signature !== newRun.signature)
49436
- throw new Error("Existing rich formatting changed");
49437
- }
49540
+ if (options.tagChange && !options.removeTag) {
49541
+ if (!nativeTagAdditionPreserved(before.rich, after.rich, [options.tagChange]))
49542
+ throw new Error("Native tag addition or retained content was not verified");
49543
+ } else {
49544
+ try {
49545
+ assertRetainedRichContent(
49546
+ before.rich,
49547
+ after.rich,
49548
+ options.tagChange ? { kind: "remove-tag", tag: options.tagChange } : options.append ? { kind: "append" } : { kind: "unchanged" }
49549
+ );
49550
+ } catch (error2) {
49551
+ if (error2 instanceof Error && error2.message === "Existing note text or whitespace was not preserved")
49552
+ throw new Error("Existing note text was not preserved", { cause: error2 });
49553
+ throw error2;
49554
+ }
49438
49555
  }
49556
+ for (const snapshot of [before, after])
49557
+ if (JSON.stringify(snapshot.checklist) !== JSON.stringify(snapshot.rich.checklistItems.map(({ text: text2, done }) => ({ text: text2, done }))))
49558
+ throw new Error("Native checklist readers disagree about item text or state");
49439
49559
  }
49440
49560
  function describeTransportFailure(error2) {
49441
49561
  const detail = error2;
@@ -49449,12 +49569,37 @@ function textBelowTitle(text2, title) {
49449
49569
  if (firstLine.trim() !== title.trim()) return text2;
49450
49570
  return lineEnd < 0 ? "" : text2.slice(lineEnd + 1);
49451
49571
  }
49572
+ function assertChecklistEndAppendAvailable(rich) {
49573
+ requirePreservationMetadata(rich);
49574
+ const items = [...rich.checklistItems].sort((a, b) => a.start - b.start);
49575
+ if (!items.length) return;
49576
+ const last = items.at(-1);
49577
+ if (items.some(
49578
+ (item, index) => item.start > 0 && rich.text[item.start - 1] !== "\n" || index > 0 && item.start !== items[index - 1].start + items[index - 1].text.length + 1
49579
+ ) || !/^\n?$/u.test(rich.text.slice(last.start + last.text.length)))
49580
+ throw new CodedError(
49581
+ "Checklist end-append requires one existing checklist group at the end of the note. Notes may insert into an earlier group when text or native objects follow it, or when groups are separate. Nothing changed",
49582
+ { code: "validation_error", committed: false, indeterminate: false }
49583
+ );
49584
+ }
49585
+ function requireBackgroundPreflightMetadata(rich) {
49586
+ try {
49587
+ requirePreservationMetadata(rich);
49588
+ } catch (error2) {
49589
+ throw new CodedError(error2 instanceof Error ? error2.message : String(error2), {
49590
+ code: "validation_error",
49591
+ committed: false,
49592
+ indeterminate: false
49593
+ });
49594
+ }
49595
+ }
49452
49596
  function mutateBackground(request, operation, data, verify, deps) {
49453
49597
  if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
49454
49598
  throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
49455
49599
  const before = deps.read(request.id);
49456
49600
  if (before.hash !== request.expectedContentHash)
49457
49601
  throw new Error("Note revision changed; read it again");
49602
+ requireBackgroundPreflightMetadata(before.rich);
49458
49603
  if (!before.rich.text.includes(request.scopeText))
49459
49604
  throw new Error("Scope is absent from exact note");
49460
49605
  if (!textBelowTitle(before.rich.text, before.title).includes(request.scopeText))
@@ -49465,8 +49610,10 @@ function mutateBackground(request, operation, data, verify, deps) {
49465
49610
  const candidates = deps.candidates(before.title, request.scopeText);
49466
49611
  if (candidates.length !== 1 || candidates[0] !== request.id)
49467
49612
  throw new Error("Ambiguous note selection; nothing changed");
49468
- if (deps.read(request.id).hash !== before.hash)
49469
- throw new Error("Note revision changed during preflight");
49613
+ const current = deps.read(request.id);
49614
+ if (current.hash !== before.hash) throw new Error("Note revision changed during preflight");
49615
+ requireBackgroundPreflightMetadata(current.rich);
49616
+ if (operation === "create-checklist-item") assertChecklistEndAppendAvailable(current.rich);
49470
49617
  let transportUncertain = false;
49471
49618
  let transportMessage = "";
49472
49619
  let transportError;
@@ -49570,7 +49717,7 @@ function setNativeTag(manager, request) {
49570
49717
  request.present ? "add-tag" : "remove-tag",
49571
49718
  { tag },
49572
49719
  (before, after) => {
49573
- assertPreserved(before, after, { tagChange: tag });
49720
+ assertPreserved(before, after, { tagChange: tag, removeTag: !request.present });
49574
49721
  if (after.rich.nativeTags.includes(tag) !== request.present)
49575
49722
  throw new Error("Native tag state not verified");
49576
49723
  const removed = before.rich.nativeObjectIds.filter(
@@ -49918,7 +50065,7 @@ async function callPublicHelperAsync(action, fields = {}, deps = defaultPublicHe
49918
50065
  stdio: ["pipe", "pipe", "ignore"]
49919
50066
  });
49920
50067
  const chunks = [];
49921
- let size = 0;
50068
+ let size2 = 0;
49922
50069
  let failure2 = null;
49923
50070
  const stop = (error2) => {
49924
50071
  failure2 ??= error2;
@@ -49935,8 +50082,8 @@ async function callPublicHelperAsync(action, fields = {}, deps = defaultPublicHe
49935
50082
  signal?.removeEventListener("abort", onAbort);
49936
50083
  };
49937
50084
  child2.stdout?.on("data", (chunk) => {
49938
- size += chunk.length;
49939
- if (size > MAX_OUTPUT_BYTES2)
50085
+ size2 += chunk.length;
50086
+ if (size2 > MAX_OUTPUT_BYTES2)
49940
50087
  stop(new PublicHelperError("invalid_response", "The helper response is too large."));
49941
50088
  else chunks.push(chunk);
49942
50089
  });
@@ -51034,6 +51181,199 @@ function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS, pen
51034
51181
  };
51035
51182
  }
51036
51183
 
51184
+ // src/utils/structuredText.ts
51185
+ import { isDeepStrictEqual } from "node:util";
51186
+ var STRUCTURED_TEXT_PREFIX = "structuredContent: ";
51187
+ var SHOWN_ABOVE = "[shown in full above]";
51188
+ var MIN_ELIDED_CHARS = 64;
51189
+ var MAX_MIRROR_CHARS = 16384;
51190
+ var MAX_FIELD_CHARS = 1024;
51191
+ function textOf(content) {
51192
+ return content.map(
51193
+ (item) => item && typeof item === "object" && item.type === "text" ? String(item.text ?? "") : ""
51194
+ ).join("\n");
51195
+ }
51196
+ function elide(value, text2) {
51197
+ if (typeof value === "string") {
51198
+ return value.length >= MIN_ELIDED_CHARS && text2.includes(value) ? SHOWN_ABOVE : value;
51199
+ }
51200
+ if (Array.isArray(value)) return value.map((item) => elide(item, text2));
51201
+ if (value && typeof value === "object") {
51202
+ const out = {};
51203
+ for (const [key, item] of Object.entries(value)) {
51204
+ if (item !== void 0) out[key] = elide(item, text2);
51205
+ }
51206
+ return out;
51207
+ }
51208
+ return value;
51209
+ }
51210
+ function textIsTheJson(content, structured) {
51211
+ const plain = JSON.parse(JSON.stringify(structured));
51212
+ return content.some((item) => {
51213
+ const text2 = item?.text;
51214
+ if (item?.type !== "text" || typeof text2 !== "string") return false;
51215
+ const trimmed = text2.trim();
51216
+ if (!trimmed.startsWith("{")) return false;
51217
+ try {
51218
+ return isDeepStrictEqual(JSON.parse(trimmed), plain);
51219
+ } catch {
51220
+ return false;
51221
+ }
51222
+ });
51223
+ }
51224
+ function structuredTextLine(structured, text2) {
51225
+ const elided = elide(structured, text2);
51226
+ let json2 = JSON.stringify(elided);
51227
+ if (json2.length > MAX_MIRROR_CHARS) {
51228
+ const kept = {};
51229
+ const omitted = [];
51230
+ for (const [key, value] of Object.entries(elided)) {
51231
+ if (JSON.stringify(value).length > MAX_FIELD_CHARS) omitted.push(key);
51232
+ else kept[key] = value;
51233
+ }
51234
+ json2 = JSON.stringify({ ...kept, _omitted: omitted });
51235
+ }
51236
+ return STRUCTURED_TEXT_PREFIX + json2;
51237
+ }
51238
+ function withStructuredText(result) {
51239
+ const r = result;
51240
+ if (!r || typeof r !== "object") return result;
51241
+ const structured = r.structuredContent;
51242
+ if (!structured || typeof structured !== "object" || Array.isArray(structured)) return result;
51243
+ const content = Array.isArray(r.content) ? r.content : [];
51244
+ if (textIsTheJson(content, structured)) return result;
51245
+ const line = structuredTextLine(structured, textOf(content));
51246
+ return { ...r, content: [...content, { type: "text", text: line }] };
51247
+ }
51248
+ function installStructuredText(server2) {
51249
+ const target = server2;
51250
+ const register = target.registerTool;
51251
+ if (typeof register === "function") {
51252
+ target.registerTool = function(name, config2, cb) {
51253
+ const wrapped = typeof cb === "function" ? async (...args) => withStructuredText(await cb(...args)) : cb;
51254
+ return register.call(this, name, config2, wrapped);
51255
+ };
51256
+ }
51257
+ const createError = target.createToolError;
51258
+ if (typeof createError === "function") {
51259
+ target.createToolError = (message) => withStructuredText(createError.call(target, message));
51260
+ }
51261
+ }
51262
+
51263
+ // src/services/tableResponse.ts
51264
+ var DEFAULT_TABLE_RESPONSE_BYTES = 4 * 1024 * 1024;
51265
+ var MAX_TABLE_RESPONSE_BYTES = 8 * 1024 * 1024;
51266
+ var MAX_TABLES_PER_PAGE = 500;
51267
+ var MIN_TABLE_RESPONSE_BYTES = 1024;
51268
+ function validateTablePageOptions(options) {
51269
+ const {
51270
+ offset = 0,
51271
+ limit = MAX_TABLES_PER_PAGE,
51272
+ maxBytes = DEFAULT_TABLE_RESPONSE_BYTES
51273
+ } = options;
51274
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > MAX_TABLES_PER_PAGE || !Number.isSafeInteger(maxBytes) || maxBytes < MIN_TABLE_RESPONSE_BYTES || maxBytes > MAX_TABLE_RESPONSE_BYTES) {
51275
+ throw new CodedError("Invalid table page offset, limit or maxBytes.", {
51276
+ code: "validation_error"
51277
+ });
51278
+ }
51279
+ return { offset, limit, maxBytes };
51280
+ }
51281
+ function response(id2, result, tables, options, stoppedAtSizeLimit) {
51282
+ const tableCount = result.tables.length;
51283
+ const next = options.offset + tables.length;
51284
+ const hasMore = next < tableCount;
51285
+ const complete = result.tableCellsComplete && tables.every((table) => table.complete);
51286
+ const markdown = tables.map(
51287
+ (table) => table.markdown ?? `[table ${table.index} could not be ${table.contentOmitted ? "returned" : "decoded"}: ${table.reason}]`
51288
+ ).join("\n\n");
51289
+ let summary = tableCount === 0 ? "This note has no native tables." : `${tables.length === tableCount ? tableCount : `${tables.length} of ${tableCount}`} table(s)${complete ? "" : " (some content could not be returned; see tables[].reason)"}:
51290
+
51291
+ ${markdown}`;
51292
+ if (hasMore)
51293
+ summary += `
51294
+
51295
+ More tables remain: call get-note-tables with offset ${next} and the same limit and maxBytes.`;
51296
+ return {
51297
+ content: [{ type: "text", text: summary }],
51298
+ structuredContent: {
51299
+ id: id2,
51300
+ tables,
51301
+ tableCount,
51302
+ tableCellsComplete: complete,
51303
+ markdown,
51304
+ page: {
51305
+ ...options,
51306
+ totalAvailable: tableCount,
51307
+ returned: tables.length,
51308
+ ...hasMore ? { nextOffset: next } : {},
51309
+ hasMore,
51310
+ stoppedAtSizeLimit
51311
+ }
51312
+ }
51313
+ };
51314
+ }
51315
+ var size = (value) => Buffer.byteLength(JSON.stringify(withStructuredText(value)));
51316
+ function tablePageResponse(id2, result, request = {}) {
51317
+ const options = validateTablePageOptions(request);
51318
+ const available = Math.max(0, Math.min(options.limit, result.tables.length - options.offset));
51319
+ const fits = (count) => {
51320
+ const candidate = result.tables.slice(options.offset, options.offset + count);
51321
+ const couldStopLater = count < available;
51322
+ const candidateBytes = Math.max(
51323
+ size(response(id2, result, candidate, options, false)),
51324
+ couldStopLater ? size(response(id2, result, candidate, options, true)) : 0
51325
+ );
51326
+ return candidateBytes <= options.maxBytes;
51327
+ };
51328
+ let accepted = 0;
51329
+ let trial = 1;
51330
+ while (trial <= available) {
51331
+ if (fits(trial)) {
51332
+ accepted = trial;
51333
+ if (trial === available) break;
51334
+ trial = Math.min(available, trial * 2);
51335
+ continue;
51336
+ }
51337
+ let low = accepted + 1;
51338
+ let high = trial - 1;
51339
+ while (low <= high) {
51340
+ const middle = Math.floor((low + high) / 2);
51341
+ if (fits(middle)) {
51342
+ accepted = middle;
51343
+ low = middle + 1;
51344
+ } else {
51345
+ high = middle - 1;
51346
+ }
51347
+ }
51348
+ break;
51349
+ }
51350
+ let tables = result.tables.slice(options.offset, options.offset + accepted);
51351
+ const stoppedAtSizeLimit = accepted < available;
51352
+ if (stoppedAtSizeLimit && accepted === 0) {
51353
+ const table = result.tables[options.offset];
51354
+ tables = [
51355
+ {
51356
+ index: table.index,
51357
+ id: table.id,
51358
+ ...table.attachmentId !== void 0 ? { attachmentId: table.attachmentId } : {},
51359
+ ...table.rowCount !== void 0 ? { rowCount: table.rowCount } : {},
51360
+ ...table.columnCount !== void 0 ? { columnCount: table.columnCount } : {},
51361
+ complete: false,
51362
+ contentOmitted: true,
51363
+ reason: "Table content exceeds this response size limit. Increase maxBytes or export the note to a file."
51364
+ }
51365
+ ];
51366
+ }
51367
+ const output = response(id2, result, tables, options, stoppedAtSizeLimit);
51368
+ if (size(output) > options.maxBytes) {
51369
+ throw new CodedError(
51370
+ "Table metadata is too large for this response limit; increase maxBytes.",
51371
+ { code: "validation_error" }
51372
+ );
51373
+ }
51374
+ return output;
51375
+ }
51376
+
51037
51377
  // src/utils/noteParagraphs.ts
51038
51378
  import { gunzipSync as gunzipSync9 } from "node:zlib";
51039
51379
  var ENVELOPE_CODE = {
@@ -51245,10 +51585,10 @@ function pageParagraphs(paragraphs, { offset = 0, limit = 500, maxBytes = 4 * 10
51245
51585
  const out = [];
51246
51586
  let bytes = 0;
51247
51587
  for (let i = start; i < list.length && out.length < limit; i++) {
51248
- const size = Buffer.byteLength(JSON.stringify(list[i]));
51249
- if (out.length && bytes + size > maxBytes) break;
51588
+ const size2 = Buffer.byteLength(JSON.stringify(list[i]));
51589
+ if (out.length && bytes + size2 > maxBytes) break;
51250
51590
  out.push(list[i]);
51251
- bytes += size;
51591
+ bytes += size2;
51252
51592
  }
51253
51593
  const next = start + out.length;
51254
51594
  return {
@@ -51784,10 +52124,10 @@ function listNoteLinks(options = {}) {
51784
52124
  const page = [];
51785
52125
  let bytes = 0;
51786
52126
  for (let i = start; i < links.length && page.length < limit; i++) {
51787
- const size = Buffer.byteLength(JSON.stringify(links[i]));
51788
- if (page.length && bytes + size > maxBytes) break;
52127
+ const size2 = Buffer.byteLength(JSON.stringify(links[i]));
52128
+ if (page.length && bytes + size2 > maxBytes) break;
51789
52129
  page.push(links[i]);
51790
- bytes += size;
52130
+ bytes += size2;
51791
52131
  }
51792
52132
  const next = start + page.length;
51793
52133
  return {
@@ -52398,8 +52738,8 @@ var DataUrlWriter = class {
52398
52738
  if (!this.fits(data.length)) return { error: "too-large" };
52399
52739
  return this.embed(`generated:${key}`, data, mime);
52400
52740
  }
52401
- fits(size) {
52402
- return size <= this.maxBytes && this.embeddedBytes + size <= this.totalBytes;
52741
+ fits(size2) {
52742
+ return size2 <= this.maxBytes && this.embeddedBytes + size2 <= this.totalBytes;
52403
52743
  }
52404
52744
  embed(key, data, mime) {
52405
52745
  const result = { url: `data:${mime};base64,${data.toString("base64")}`, mime };
@@ -55160,85 +55500,6 @@ function exportWithTemplate(request, deps, chosen, notes, skipped, { output, ass
55160
55500
  return { ...receipt, markdown };
55161
55501
  }
55162
55502
 
55163
- // src/utils/structuredText.ts
55164
- import { isDeepStrictEqual } from "node:util";
55165
- var STRUCTURED_TEXT_PREFIX = "structuredContent: ";
55166
- var SHOWN_ABOVE = "[shown in full above]";
55167
- var MIN_ELIDED_CHARS = 64;
55168
- var MAX_MIRROR_CHARS = 16384;
55169
- var MAX_FIELD_CHARS = 1024;
55170
- function textOf(content) {
55171
- return content.map(
55172
- (item) => item && typeof item === "object" && item.type === "text" ? String(item.text ?? "") : ""
55173
- ).join("\n");
55174
- }
55175
- function elide(value, text2) {
55176
- if (typeof value === "string") {
55177
- return value.length >= MIN_ELIDED_CHARS && text2.includes(value) ? SHOWN_ABOVE : value;
55178
- }
55179
- if (Array.isArray(value)) return value.map((item) => elide(item, text2));
55180
- if (value && typeof value === "object") {
55181
- const out = {};
55182
- for (const [key, item] of Object.entries(value)) {
55183
- if (item !== void 0) out[key] = elide(item, text2);
55184
- }
55185
- return out;
55186
- }
55187
- return value;
55188
- }
55189
- function textIsTheJson(content, structured) {
55190
- const plain = JSON.parse(JSON.stringify(structured));
55191
- return content.some((item) => {
55192
- const text2 = item?.text;
55193
- if (item?.type !== "text" || typeof text2 !== "string") return false;
55194
- const trimmed = text2.trim();
55195
- if (!trimmed.startsWith("{")) return false;
55196
- try {
55197
- return isDeepStrictEqual(JSON.parse(trimmed), plain);
55198
- } catch {
55199
- return false;
55200
- }
55201
- });
55202
- }
55203
- function structuredTextLine(structured, text2) {
55204
- const elided = elide(structured, text2);
55205
- let json2 = JSON.stringify(elided);
55206
- if (json2.length > MAX_MIRROR_CHARS) {
55207
- const kept = {};
55208
- const omitted = [];
55209
- for (const [key, value] of Object.entries(elided)) {
55210
- if (JSON.stringify(value).length > MAX_FIELD_CHARS) omitted.push(key);
55211
- else kept[key] = value;
55212
- }
55213
- json2 = JSON.stringify({ ...kept, _omitted: omitted });
55214
- }
55215
- return STRUCTURED_TEXT_PREFIX + json2;
55216
- }
55217
- function withStructuredText(result) {
55218
- const r = result;
55219
- if (!r || typeof r !== "object") return result;
55220
- const structured = r.structuredContent;
55221
- if (!structured || typeof structured !== "object" || Array.isArray(structured)) return result;
55222
- const content = Array.isArray(r.content) ? r.content : [];
55223
- if (textIsTheJson(content, structured)) return result;
55224
- const line = structuredTextLine(structured, textOf(content));
55225
- return { ...r, content: [...content, { type: "text", text: line }] };
55226
- }
55227
- function installStructuredText(server2) {
55228
- const target = server2;
55229
- const register = target.registerTool;
55230
- if (typeof register === "function") {
55231
- target.registerTool = function(name, config2, cb) {
55232
- const wrapped = typeof cb === "function" ? async (...args) => withStructuredText(await cb(...args)) : cb;
55233
- return register.call(this, name, config2, wrapped);
55234
- };
55235
- }
55236
- const createError = target.createToolError;
55237
- if (typeof createError === "function") {
55238
- target.createToolError = (message) => withStructuredText(createError.call(target, message));
55239
- }
55240
- }
55241
-
55242
55503
  // src/tools/directOperations.ts
55243
55504
  import { createHash as createHash4 } from "node:crypto";
55244
55505
  import {
@@ -55509,30 +55770,13 @@ function readSnapshot(manager, id2) {
55509
55770
  const rich = readRichNote(id2);
55510
55771
  if (enriched.revision !== rich.revision)
55511
55772
  throw new Error("Native metadata changed during read; read the note again");
55773
+ requirePreservationMetadata(rich);
55512
55774
  return { id: id2, title: note.title, body, rich, hash: richContentHash(body, enriched) };
55513
55775
  }
55514
55776
  function assertExistingContentPreserved(before, after) {
55515
55777
  if (before.id !== after.id || before.title !== after.title)
55516
55778
  throw new Error("Note identity changed");
55517
- const tidy = (text2) => text2.replace(/\r\n/g, "\n").replace(/[\s\ufffc]+$/gu, "");
55518
- if (!tidy(after.rich.text).startsWith(tidy(before.rich.text)))
55519
- throw new Error("Existing note text was not preserved");
55520
- if (linkSignature(before.rich.links) !== linkSignature(after.rich.links.slice(0, before.rich.links.length)))
55521
- throw new Error("Existing links were not preserved");
55522
- if (before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)))
55523
- throw new Error("Existing native object was lost");
55524
- if (before.rich.nativeTags.some((tag) => !after.rich.nativeTags.includes(tag)))
55525
- throw new Error("Existing native tag was lost");
55526
- for (const item of before.rich.checklistItems || []) {
55527
- const actual = after.rich.checklistItems?.find((current) => current.id === item.id);
55528
- if (!actual || actual.text !== item.text || actual.done !== item.done)
55529
- throw new Error("Existing checklist item identity or state changed");
55530
- }
55531
- for (const object3 of before.rich.objectData || []) {
55532
- const actual = after.rich.objectData?.find((current) => current.id === object3.id);
55533
- if (!actual || actual.mergeable !== object3.mergeable || actual.view !== object3.view)
55534
- throw new Error("Existing native object content or presentation changed");
55535
- }
55779
+ assertRetainedRichContent(before.rich, after.rich, { kind: "append" });
55536
55780
  }
55537
55781
  function localAttachment(path10) {
55538
55782
  if (!isAbsolute4(path10)) throw new Error("An absolute local file path is required");
@@ -55547,12 +55791,12 @@ function localAttachment(path10) {
55547
55791
  }
55548
55792
  }
55549
55793
  var MAX_ADD_ATTACHMENT_BYTES = 64 * 1024 * 1024;
55550
- function fileMatches(path10, size, expected) {
55794
+ function fileMatches(path10, size2, expected) {
55551
55795
  let descriptor;
55552
55796
  try {
55553
55797
  descriptor = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
55554
55798
  const stat = fstatSync7(descriptor);
55555
- if (!stat.isFile() || stat.size !== size) return false;
55799
+ if (!stat.isFile() || stat.size !== size2) return false;
55556
55800
  const hash = createHash4("sha256");
55557
55801
  const chunk = Buffer.allocUnsafe(1024 * 1024);
55558
55802
  let total = 0;
@@ -55560,10 +55804,10 @@ function fileMatches(path10, size, expected) {
55560
55804
  const read = readSync5(descriptor, chunk, 0, chunk.length, null);
55561
55805
  if (read === 0) break;
55562
55806
  total += read;
55563
- if (total > size) return false;
55807
+ if (total > size2) return false;
55564
55808
  hash.update(chunk.subarray(0, read));
55565
55809
  }
55566
- return total === size && hash.digest("hex") === expected;
55810
+ return total === size2 && hash.digest("hex") === expected;
55567
55811
  } catch {
55568
55812
  return false;
55569
55813
  } finally {
@@ -55909,8 +56153,19 @@ function attachFile(manager, args, checked, progress = { insertionStarted: false
55909
56153
  transportWarning: "Transport was uncertain; exact bytes and prior content were verified"
55910
56154
  } : {}
55911
56155
  };
56156
+ } catch (error2) {
56157
+ if (progress.insertionStarted)
56158
+ throw new CodedError(
56159
+ `Attachment outcome uncertain; read the exact note before any retry: ${error2 instanceof Error ? error2.message : String(error2)}`,
56160
+ { code: "verification_failed", indeterminate: true }
56161
+ );
56162
+ throw error2;
55912
56163
  } finally {
55913
- rmSync7(directory, { recursive: true, force: true });
56164
+ try {
56165
+ rmSync7(directory, { recursive: true, force: true });
56166
+ } catch {
56167
+ console.error("Could not remove private temporary attachment files");
56168
+ }
55914
56169
  }
55915
56170
  }
55916
56171
 
@@ -59994,7 +60249,7 @@ function registerNativeOperations(server2, manager) {
59994
60249
  );
59995
60250
  tool(
59996
60251
  "create-checklist-item",
59997
- "Use when: appending one real unchecked Notes checklist item.\nReturns: the created checklist identity after exact-note readback.\nDo not use when: plain text is sufficient or more than one item is requested.\nSafety: preserves existing text, links, native objects, checklist identities, and formatting.",
60252
+ "Use when: appending one real unchecked Notes checklist item.\nReturns: the created checklist identity after exact-note readback.\nDo not use when: plain text is sufficient or more than one item is requested.\nSafety: preserves existing text, links, native objects, checklist identities, and formatting. If a checklist already exists, requires one contiguous group at the end of the note; otherwise refuses before running the bridge.",
59998
60253
  {
59999
60254
  ...common,
60000
60255
  text: external_exports.string().min(1).max(1e4).refine((s) => !/[\r\n\0]/u.test(s), "One line per checklist item")
@@ -60018,7 +60273,7 @@ function registerNativeOperations(server2, manager) {
60018
60273
  );
60019
60274
  tool(
60020
60275
  "create-checklist-items",
60021
- "Use when: appending several real unchecked Notes checklist items to one note, in order.\nReturns: each landed item's native identity and text, the final order check, and the new revision; on a stop, which items landed, the item whose outcome is uncertain, and the items not attempted.\nDo not use when: one item is enough (create-checklist-item) or plain text is sufficient.\nSafety: runs the verified single-item bridge once per item, chaining each verified revision into the next; checks after every item that exactly one new unchecked item with that text and a new identity appeared and that earlier items kept theirs, and stops at the first uncertain result without retrying. Each item takes a few seconds, so a full batch can run about a minute; if the call times out on the client side, read the note before retrying and send only items that are not present.",
60276
+ "Use when: appending several real unchecked Notes checklist items to one note, in order.\nReturns: each landed item's native identity and text, the final order check, and the new revision; on a stop, which items landed, the item whose outcome is uncertain, and the items not attempted.\nDo not use when: one item is enough (create-checklist-item) or plain text is sufficient.\nSafety: runs the verified single-item bridge once per item, chaining each verified revision into the next; requires any existing checklist to be one contiguous group at the end of the note before dispatch. Checks after every item that exactly one new unchecked item with that text and a new identity appeared and that earlier items kept theirs, and stops at the first uncertain result without retrying. Each item takes a few seconds, so a full batch can run about a minute; if the call times out on the client side, read the note before retrying and send only items that are not present.",
60022
60277
  {
60023
60278
  ...common,
60024
60279
  items: external_exports.array(
@@ -62701,10 +62956,10 @@ async function readEditorJson(req) {
62701
62956
  if (!/^application\/json(?:\s*;|$)/i.test(type))
62702
62957
  throw new EditorHttpError(415, "unsupported-media-type", "Send JSON (application/json).");
62703
62958
  const chunks = [];
62704
- let size = 0;
62959
+ let size2 = 0;
62705
62960
  for await (const chunk of req) {
62706
- size += chunk.length;
62707
- if (size > MAX_EDITOR_BODY_BYTES)
62961
+ size2 += chunk.length;
62962
+ if (size2 > MAX_EDITOR_BODY_BYTES)
62708
62963
  throw new EditorHttpError(413, "too-large", "Request body too large.");
62709
62964
  chunks.push(chunk);
62710
62965
  }
@@ -63055,12 +63310,12 @@ var textFingerprint = (normalized2) => createHash8("sha256").update(normalized2,
63055
63310
  function textSimilarity(a, b) {
63056
63311
  if (a === b) return 1;
63057
63312
  if (!a || !b) return 0;
63058
- const size = Math.min(a.length, b.length) < 3 ? 2 : 3;
63313
+ const size2 = Math.min(a.length, b.length) < 3 ? 2 : 3;
63059
63314
  const grams = (s) => {
63060
63315
  const out = /* @__PURE__ */ new Map();
63061
63316
  const padded = ` ${s} `;
63062
- for (let i = 0; i + size <= padded.length; i++) {
63063
- const g = padded.slice(i, i + size);
63317
+ for (let i = 0; i + size2 <= padded.length; i++) {
63318
+ const g = padded.slice(i, i + size2);
63064
63319
  out.set(g, (out.get(g) ?? 0) + 1);
63065
63320
  }
63066
63321
  return out;
@@ -64900,18 +65155,27 @@ registerTool(
64900
65155
  registerTool(
64901
65156
  "get-note-tables",
64902
65157
  {
64903
- description: "Use when: reading the native tables in one exact note as data or Markdown.\nReturns: every table in body order as GitHub-flavored Markdown (first row as header) plus JSON rows with stable row/column ids, and tableCellsComplete.\nDo not use when: you need the whole note (get-note-markdown / get-note-content) or native object ranges and checklist ids (get-native-objects).\nSafety: read-only; reads the NoteStore database and requires Full Disk Access. A cell that cannot be decoded is null in rows, listed in incompleteCells, and marked [undecoded cell] in Markdown; it is never guessed. Cell text only: links and styling inside cells are not rendered.",
64904
- inputSchema: { id: noteIdInput },
65158
+ description: "Use when: reading the native tables in one exact note as data or Markdown.\nReturns: one page of whole tables in body order as GitHub-flavored Markdown (first row as header) plus JSON rows with stable row/column ids, tableCellsComplete, and page info. Default limit 500 and maxBytes 4 MiB; while page.hasMore is true, call again with offset set to page.nextOffset. The byte budget includes the full serialized tool response, including repeated Markdown.\nDo not use when: you need the whole note (get-note-markdown / get-note-content) or native object ranges and checklist ids (get-native-objects).\nSafety: read-only; reads the NoteStore database and requires Full Disk Access. A cell that cannot be decoded is null in rows, listed in incompleteCells, and marked [undecoded cell] in Markdown; it is never guessed. A table larger than an empty page returns metadata only with complete: false and contentOmitted: true; increase maxBytes (max 8 MiB) or export the note to a file. Cell text only: links and styling inside cells are not rendered.",
65159
+ inputSchema: {
65160
+ id: noteIdInput,
65161
+ offset: external_exports.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional().describe("Zero-based position of the first table (default 0); use page.nextOffset"),
65162
+ limit: external_exports.number().int().min(1).max(MAX_TABLES_PER_PAGE).optional().describe(`Maximum tables to return (default and max ${MAX_TABLES_PER_PAGE})`),
65163
+ maxBytes: external_exports.number().int().min(MIN_TABLE_RESPONSE_BYTES).max(MAX_TABLE_RESPONSE_BYTES).optional().describe(
65164
+ `Maximum UTF-8 bytes of the serialized tool response (default ${DEFAULT_TABLE_RESPONSE_BYTES}, max ${MAX_TABLE_RESPONSE_BYTES})`
65165
+ )
65166
+ },
64905
65167
  outputSchema: {
64906
65168
  id: external_exports.string().optional(),
64907
65169
  tables: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
64908
65170
  tableCount: external_exports.number().optional(),
64909
65171
  tableCellsComplete: external_exports.boolean().optional(),
64910
- markdown: external_exports.string().optional()
65172
+ markdown: external_exports.string().optional(),
65173
+ page: external_exports.record(external_exports.unknown()).optional()
64911
65174
  },
64912
65175
  annotations: { readOnlyHint: true }
64913
65176
  },
64914
- withErrorHandling(({ id: id2 }) => {
65177
+ withErrorHandling(({ id: id2, offset, limit, maxBytes }) => {
65178
+ const options = validateTablePageOptions({ offset, limit, maxBytes });
64915
65179
  const { metadata, message } = getNoteMetadata(id2);
64916
65180
  if (!metadata) return errorResponse(message || `Failed to read note "${id2}"`);
64917
65181
  if (metadata.passwordProtected) {
@@ -64920,17 +65184,7 @@ registerTool(
64920
65184
  );
64921
65185
  }
64922
65186
  const result = notesManager.getNoteTablesById(id2);
64923
- const count = result.tables.length;
64924
- const summary = count === 0 ? "This note has no native tables." : `${count} table(s)${result.tableCellsComplete ? "" : " (some content could not be decoded; see tables[].reason)"}:
64925
-
64926
- ${result.markdown}`;
64927
- return successResponse(summary, {
64928
- id: id2,
64929
- tables: result.tables,
64930
- tableCount: count,
64931
- tableCellsComplete: result.tableCellsComplete,
64932
- markdown: result.markdown
64933
- });
65187
+ return tablePageResponse(id2, result, options);
64934
65188
  }, "Error reading note tables")
64935
65189
  );
64936
65190
  registerTool(
@@ -65709,7 +65963,7 @@ registerTool(
65709
65963
  readLinks: (noteId3) => readRichNote(noteId3).links,
65710
65964
  append: (request) => {
65711
65965
  let route = "applescript";
65712
- const response = guardedAppend(
65966
+ const response2 = guardedAppend(
65713
65967
  {
65714
65968
  ...request,
65715
65969
  format: "html",
@@ -65717,11 +65971,11 @@ registerTool(
65717
65971
  },
65718
65972
  (r) => route = r
65719
65973
  );
65720
- if (response.isError) {
65721
- const envelope = response.structuredContent;
65722
- throw envelope?.code ? new CodedError(response.content[0].text, envelope) : new Error(response.content[0].text);
65974
+ if (response2.isError) {
65975
+ const envelope = response2.structuredContent;
65976
+ throw envelope?.code ? new CodedError(response2.content[0].text, envelope) : new Error(response2.content[0].text);
65723
65977
  }
65724
- return { route, contentHash: String(response.structuredContent?.contentHash ?? "") };
65978
+ return { route, contentHash: String(response2.structuredContent?.contentHash ?? "") };
65725
65979
  }
65726
65980
  }
65727
65981
  );
@@ -66862,7 +67116,7 @@ registerTool(
66862
67116
  `Too large to return whole, so oversized inline images were replaced (strippedImages) or the body was left out (contentOmitted): ${degraded.map((n) => n.id).join(", ")}. Read these with get-note-content, and their files with list-attachments and save-attachment.`
66863
67117
  );
66864
67118
  }
66865
- const response = {
67119
+ const response2 = {
66866
67120
  content: [
66867
67121
  {
66868
67122
  type: "text",
@@ -66877,13 +67131,13 @@ Full JSON export:`
66877
67131
  ],
66878
67132
  structuredContent: { ...exportData }
66879
67133
  };
66880
- const responseBytes = Buffer.byteLength(JSON.stringify(response));
67134
+ const responseBytes = Buffer.byteLength(JSON.stringify(response2));
66881
67135
  if (responseBytes > maxResponseBytes) {
66882
67136
  return errorResponse(
66883
67137
  `Error exporting notes: this page is ${formatMegabytes(responseBytes)}, over the ${formatMegabytes(maxResponseBytes)} response limit, so it was not sent. Call export-notes-json again with offset ${page.offset} and a smaller limit (for example ${Math.max(1, Math.floor(page.returned / 2))}), or raise APPLE_NOTES_MCP_EXPORT_MAX_BYTES if your MCP client accepts larger messages.`
66884
67138
  );
66885
67139
  }
66886
- return response;
67140
+ return response2;
66887
67141
  }, "Error exporting notes")
66888
67142
  );
66889
67143
  registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.15.4",
3
+ "version": "2.15.6",
4
4
  "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",