apple-notes-mcp 2.15.5 → 2.15.7

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 +62 -11
  2. package/build/index.js +348 -190
  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.
@@ -1358,21 +1376,28 @@ actual destination folder ID matches the requested folder.
1358
1376
 
1359
1377
  #### Folder scope guards
1360
1378
 
1361
- `update-note`, `append-to-note`, `delete-note`, and `move-note` accept three
1362
- optional folder preconditions. Use exact folder ids from `list-folders`.
1379
+ `update-note`, `append-to-note`, `delete-note`, `move-note`, and
1380
+ `batch-move-notes` accept three optional folder preconditions. Use exact
1381
+ folder ids from `list-folders`.
1363
1382
 
1364
1383
  - `ifFolderId`: the note must currently be in exactly this folder.
1365
1384
  - `ifAncestorFolderId`: the note must be inside this folder or any of its
1366
1385
  subfolders.
1367
1386
  - `forbiddenAncestorFolderIds` (up to 50): the note must not be inside any of
1368
- these folders or their subfolders. For `move-note`, the destination must not
1369
- be either.
1387
+ these folders or their subfolders. For `move-note` and `batch-move-notes`,
1388
+ the destination must not be either.
1370
1389
 
1371
1390
  The checks read Notes.app's live folders inside the same AppleScript as the
1372
- write, immediately before it, so a note moved after you reviewed it is left
1373
- alone and the call fails with `Scope guard failed: …`. The one exception is a
1374
- native append to a protected note (it runs through Shortcuts): there the check
1375
- is a separate read just before the append, so it is not atomic.
1391
+ write, immediately before it. If the folder observed by the check fails a
1392
+ precondition, the write is refused with `Scope guard failed: …`. The check and
1393
+ write are separate Notes operations; another actor can move the note between
1394
+ them. A native append to a protected note runs through Shortcuts, so its
1395
+ precheck runs in a separate AppleScript before the append.
1396
+
1397
+ For `batch-move-notes`, the same preconditions apply to every supplied note ID.
1398
+ Each note is checked independently immediately before its move. A scope mismatch
1399
+ leaves that note unchanged and appears in its failure result; the remaining
1400
+ items continue. A batch can therefore move some notes and refuse others.
1376
1401
 
1377
1402
  **Example - retire a note only while it is still in the inbox:**
1378
1403
  ```json
@@ -1897,9 +1922,35 @@ Moves multiple notes to a folder.
1897
1922
  | `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
1898
1923
  | `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder). A smart folder is refused for the whole call before any note moves |
1899
1924
  | `account` | string | No | Account containing the folder |
1925
+ | `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Shared folder preconditions applied independently to each note; see [Folder scope guards](#folder-scope-guards). Forbidden folders also apply to the destination |
1900
1926
 
1901
1927
  **Returns:** Summary of successes and failures. Each success is reported only
1902
1928
  after the note's actual container folder ID matches the destination folder ID.
1929
+ If every note fails, the call returns its existing error response; the exact
1930
+ note IDs and reasons remain in the failure text.
1931
+
1932
+ Failure rows carry `committed: false, indeterminate: false` only when that
1933
+ item was refused before its move. Move/readback failures and an unknown
1934
+ whole-script outcome carry `indeterminate: true`, with `committed` omitted.
1935
+ If every item fails, the batch reports `committed: false` only when every row
1936
+ is a proven pre-move refusal. Otherwise its error result is indeterminate;
1937
+ read each exact note ID before retrying, even if another row has a scope refusal.
1938
+
1939
+ **Example - move only notes that are still in the reviewed inbox:**
1940
+ ```json
1941
+ {
1942
+ "ids": ["x-coredata://ABC123/ICNote/p456", "x-coredata://ABC123/ICNote/p457"],
1943
+ "folder": "Archive",
1944
+ "ifFolderId": "x-coredata://ABC123/ICFolder/p12"
1945
+ }
1946
+ ```
1947
+
1948
+ The source-folder check runs immediately before each move in the same
1949
+ AppleScript. If the check finds that a note is no longer in that inbox, its
1950
+ result reports `Scope guard failed: …` while a note still there can move.
1951
+ The check does not lock the note against another actor moving it before the
1952
+ move. The batch is not a transaction, and successful moves are not rolled
1953
+ back after a refusal.
1903
1954
 
1904
1955
  ---
1905
1956
 
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);
@@ -43149,8 +43149,9 @@ function parseScopeFailure(output) {
43149
43149
  const trimmed = output.trim();
43150
43150
  return trimmed.startsWith(`${SCOPE_MARKER}:`) ? trimmed.slice(SCOPE_MARKER.length + 1) : null;
43151
43151
  }
43152
- function scopeConflictMessage(reason) {
43153
- return `Scope guard failed: ${reason}. Nothing was changed; read the note's current folder and review before retrying.`;
43152
+ function scopeConflictMessage(reason, operation) {
43153
+ const refusal = operation === "batch-move" ? "This note's move was refused" : "Nothing was changed";
43154
+ return `Scope guard failed: ${reason}. ${refusal}; read the note's current folder and review before retrying.`;
43154
43155
  }
43155
43156
 
43156
43157
  // src/services/appleNotesManager.ts
@@ -43499,8 +43500,9 @@ function buildAccountScopedScript(scope2, command2) {
43499
43500
  end tell
43500
43501
  `;
43501
43502
  }
43502
- function buildAppLevelScript(command2) {
43503
+ function buildAppLevelScript(command2, handlers = "") {
43503
43504
  return `
43505
+ ${handlers}
43504
43506
  tell application "Notes"
43505
43507
  ${command2}
43506
43508
  end tell
@@ -45881,8 +45883,8 @@ var AppleNotesManager = class {
45881
45883
  /**
45882
45884
  * Result of a batch operation on a single item.
45883
45885
  */
45884
- createBatchResult(id2, success, error2) {
45885
- return error2 ? { id: id2, success, error: error2 } : { id: id2, success };
45886
+ createBatchResult(id2, success, error2, certainty) {
45887
+ return { ...error2 ? { id: id2, success, error: error2 } : { id: id2, success }, ...certainty };
45886
45888
  }
45887
45889
  /**
45888
45890
  * Maps a per-item status token emitted by a batch AppleScript loop to a
@@ -45890,23 +45892,39 @@ var AppleNotesManager = class {
45890
45892
  * per-note implementation. See {@link batchMoveNotes}.
45891
45893
  */
45892
45894
  mapBatchStatus(id2, status, op) {
45895
+ const beforeMove = op === "move" ? { committed: false, indeterminate: false } : void 0;
45896
+ const uncertainMove = op === "move" ? { indeterminate: true } : void 0;
45897
+ const scopeFailure = parseScopeFailure(status ?? "");
45898
+ if (scopeFailure !== null)
45899
+ return this.createBatchResult(
45900
+ id2,
45901
+ false,
45902
+ scopeConflictMessage(scopeFailure, op === "move" ? "batch-move" : void 0),
45903
+ beforeMove
45904
+ );
45893
45905
  switch (status) {
45894
45906
  case "ok":
45895
45907
  return this.createBatchResult(id2, true);
45896
45908
  case "pw":
45897
- return this.createBatchResult(id2, false, "Note is password-protected");
45909
+ return this.createBatchResult(id2, false, "Note is password-protected", beforeMove);
45898
45910
  case "missing":
45899
- return this.createBatchResult(id2, false, "Note not found");
45911
+ return this.createBatchResult(id2, false, "Note not found", beforeMove);
45900
45912
  case "fail":
45901
45913
  return this.createBatchResult(
45902
45914
  id2,
45903
45915
  false,
45904
- op === "delete" ? "Deletion failed" : "Move failed"
45916
+ op === "delete" ? "Deletion failed" : "Move failed",
45917
+ uncertainMove
45905
45918
  );
45906
45919
  case "wrongfolder":
45907
- return this.createBatchResult(id2, false, "Destination folder verification failed");
45920
+ return this.createBatchResult(
45921
+ id2,
45922
+ false,
45923
+ "Destination folder verification failed",
45924
+ uncertainMove
45925
+ );
45908
45926
  default:
45909
- return this.createBatchResult(id2, false, "Unknown error");
45927
+ return this.createBatchResult(id2, false, "Unknown error", uncertainMove);
45910
45928
  }
45911
45929
  }
45912
45930
  /**
@@ -45918,6 +45936,7 @@ var AppleNotesManager = class {
45918
45936
  * @param ids - Array of CoreData URL identifiers for notes to move
45919
45937
  * @param folder - Destination folder name
45920
45938
  * @param account - Account containing the folder (defaults to Notes.app's default account)
45939
+ * @param scope - Shared folder preconditions checked independently before each move
45921
45940
  * @returns Array of results with id, success status, and optional error message
45922
45941
  *
45923
45942
  * @example
@@ -45928,8 +45947,26 @@ var AppleNotesManager = class {
45928
45947
  * );
45929
45948
  * ```
45930
45949
  */
45931
- batchMoveNotes(ids, folder, account) {
45950
+ batchMoveNotes(ids, folder, account, scope2) {
45932
45951
  if (ids.length === 0) return [];
45952
+ const scopeChecks = buildScopeGuardScript("noteRef", scope2, "destFolder");
45953
+ const moveHandler = `
45954
+ script batchMoveWorker
45955
+ on moveBatchNote(noteRef, destFolder, theId)
45956
+ tell application "Notes"
45957
+ ${scopeChecks}
45958
+ move noteRef to destFolder
45959
+ set movedNoteRef to note id theId
45960
+ set actualFolder to container of movedNoteRef
45961
+ if (id of actualFolder) is (id of destFolder) then
45962
+ return "ok"
45963
+ else
45964
+ return "wrongfolder"
45965
+ end if
45966
+ end tell
45967
+ end moveBatchNote
45968
+ end script
45969
+ `;
45933
45970
  const targetAccount = this.resolveAccount(account);
45934
45971
  const destFolderSetup = buildLiveFolderResolution(folder, "destFolder", {
45935
45972
  excludeFolderIds: this.smartFolderIds()
@@ -45943,13 +45980,15 @@ var AppleNotesManager = class {
45943
45980
  results[i] = this.createBatchResult(
45944
45981
  id2,
45945
45982
  false,
45946
- e instanceof Error ? e.message : "Invalid note ID"
45983
+ e instanceof Error ? e.message : "Invalid note ID",
45984
+ { committed: false, indeterminate: false }
45947
45985
  );
45948
45986
  }
45949
45987
  });
45950
45988
  if (runnable.length > 0) {
45951
45989
  const idList2 = runnable.map((r) => `"${r.safe}"`).join(", ");
45952
- const script = buildAppLevelScript(`
45990
+ const script = buildAppLevelScript(
45991
+ `
45953
45992
  ${buildAccountResolution(targetAccount)}
45954
45993
  ${destFolderSetup}
45955
45994
  set out to ""
@@ -45970,14 +46009,8 @@ var AppleNotesManager = class {
45970
46009
  set out to out & "pw" & ${AS_RECORD_SEP}
45971
46010
  else
45972
46011
  try
45973
- move noteRef to destFolder
45974
- set movedNoteRef to note id theId
45975
- set actualFolder to container of movedNoteRef
45976
- if (id of actualFolder) is (id of destFolder) then
45977
- set out to out & "ok" & ${AS_RECORD_SEP}
45978
- else
45979
- set out to out & "wrongfolder" & ${AS_RECORD_SEP}
45980
- end if
46012
+ set itemStatus to batchMoveWorker's moveBatchNote(noteRef, destFolder, theId)
46013
+ set out to out & itemStatus & ${AS_RECORD_SEP}
45981
46014
  on error
45982
46015
  set out to out & "fail" & ${AS_RECORD_SEP}
45983
46016
  end try
@@ -45985,7 +46018,9 @@ var AppleNotesManager = class {
45985
46018
  end if
45986
46019
  end repeat
45987
46020
  return out
45988
- `);
46021
+ `,
46022
+ moveHandler
46023
+ );
45989
46024
  const res = executeMutationAppleScript(script);
45990
46025
  if (!res.success) {
45991
46026
  throwIfAccountResolutionFailed(res.error);
@@ -45994,11 +46029,12 @@ var AppleNotesManager = class {
45994
46029
  results[r.index] = this.createBatchResult(
45995
46030
  ids[r.index],
45996
46031
  false,
45997
- res.error ?? "Batch move failed"
46032
+ res.error ?? "Batch move failed",
46033
+ { indeterminate: true }
45998
46034
  );
45999
46035
  }
46000
46036
  } else {
46001
- const statuses = res.output.split(RECORD_SEP).map((s) => s.trim()).filter((s) => s.length > 0);
46037
+ const statuses = res.output.split(RECORD_SEP).map((s) => s.trim());
46002
46038
  runnable.forEach((r, k) => {
46003
46039
  results[r.index] = this.mapBatchStatus(ids[r.index], statuses[k], "move");
46004
46040
  });
@@ -47039,11 +47075,11 @@ function decodeInline(fields) {
47039
47075
  }
47040
47076
  if (font) {
47041
47077
  const name = stringOf(font, 1);
47042
- const size = fixed32Float(first(font, 2));
47043
- if (name || size !== void 0)
47078
+ const size2 = fixed32Float(first(font, 2));
47079
+ if (name || size2 !== void 0)
47044
47080
  inline.font = {
47045
47081
  ...name ? { name } : {},
47046
- ...size !== void 0 && Number.isFinite(size) ? { size } : {}
47082
+ ...size2 !== void 0 && Number.isFinite(size2) ? { size: size2 } : {}
47047
47083
  };
47048
47084
  }
47049
47085
  const attachment = sub3(fields, 12);
@@ -47236,8 +47272,8 @@ function pageNoteBlocks(doc, { offset = 0, limit = 500, maxBytes = blocksMaxResp
47236
47272
  let bytes = 0;
47237
47273
  for (let i = start; i < total && blocks.length < limit; i++) {
47238
47274
  let block = doc.blocks[i];
47239
- let size = Buffer.byteLength(JSON.stringify(block));
47240
- if (bytes + size > maxBytes) {
47275
+ let size2 = Buffer.byteLength(JSON.stringify(block));
47276
+ if (bytes + size2 > maxBytes) {
47241
47277
  if (blocks.length) break;
47242
47278
  block = {
47243
47279
  ...block,
@@ -47245,10 +47281,10 @@ function pageNoteBlocks(doc, { offset = 0, limit = 500, maxBytes = blocksMaxResp
47245
47281
  textOmitted: true,
47246
47282
  runs: block.runs.map((run) => ({ ...run, text: "" }))
47247
47283
  };
47248
- size = Buffer.byteLength(JSON.stringify(block));
47284
+ size2 = Buffer.byteLength(JSON.stringify(block));
47249
47285
  }
47250
47286
  blocks.push(block);
47251
- bytes += size;
47287
+ bytes += size2;
47252
47288
  }
47253
47289
  const next = start + blocks.length;
47254
47290
  return {
@@ -50065,7 +50101,7 @@ async function callPublicHelperAsync(action, fields = {}, deps = defaultPublicHe
50065
50101
  stdio: ["pipe", "pipe", "ignore"]
50066
50102
  });
50067
50103
  const chunks = [];
50068
- let size = 0;
50104
+ let size2 = 0;
50069
50105
  let failure2 = null;
50070
50106
  const stop = (error2) => {
50071
50107
  failure2 ??= error2;
@@ -50082,8 +50118,8 @@ async function callPublicHelperAsync(action, fields = {}, deps = defaultPublicHe
50082
50118
  signal?.removeEventListener("abort", onAbort);
50083
50119
  };
50084
50120
  child2.stdout?.on("data", (chunk) => {
50085
- size += chunk.length;
50086
- if (size > MAX_OUTPUT_BYTES2)
50121
+ size2 += chunk.length;
50122
+ if (size2 > MAX_OUTPUT_BYTES2)
50087
50123
  stop(new PublicHelperError("invalid_response", "The helper response is too large."));
50088
50124
  else chunks.push(chunk);
50089
50125
  });
@@ -51181,6 +51217,199 @@ function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS, pen
51181
51217
  };
51182
51218
  }
51183
51219
 
51220
+ // src/utils/structuredText.ts
51221
+ import { isDeepStrictEqual } from "node:util";
51222
+ var STRUCTURED_TEXT_PREFIX = "structuredContent: ";
51223
+ var SHOWN_ABOVE = "[shown in full above]";
51224
+ var MIN_ELIDED_CHARS = 64;
51225
+ var MAX_MIRROR_CHARS = 16384;
51226
+ var MAX_FIELD_CHARS = 1024;
51227
+ function textOf(content) {
51228
+ return content.map(
51229
+ (item) => item && typeof item === "object" && item.type === "text" ? String(item.text ?? "") : ""
51230
+ ).join("\n");
51231
+ }
51232
+ function elide(value, text2) {
51233
+ if (typeof value === "string") {
51234
+ return value.length >= MIN_ELIDED_CHARS && text2.includes(value) ? SHOWN_ABOVE : value;
51235
+ }
51236
+ if (Array.isArray(value)) return value.map((item) => elide(item, text2));
51237
+ if (value && typeof value === "object") {
51238
+ const out = {};
51239
+ for (const [key, item] of Object.entries(value)) {
51240
+ if (item !== void 0) out[key] = elide(item, text2);
51241
+ }
51242
+ return out;
51243
+ }
51244
+ return value;
51245
+ }
51246
+ function textIsTheJson(content, structured) {
51247
+ const plain = JSON.parse(JSON.stringify(structured));
51248
+ return content.some((item) => {
51249
+ const text2 = item?.text;
51250
+ if (item?.type !== "text" || typeof text2 !== "string") return false;
51251
+ const trimmed = text2.trim();
51252
+ if (!trimmed.startsWith("{")) return false;
51253
+ try {
51254
+ return isDeepStrictEqual(JSON.parse(trimmed), plain);
51255
+ } catch {
51256
+ return false;
51257
+ }
51258
+ });
51259
+ }
51260
+ function structuredTextLine(structured, text2) {
51261
+ const elided = elide(structured, text2);
51262
+ let json2 = JSON.stringify(elided);
51263
+ if (json2.length > MAX_MIRROR_CHARS) {
51264
+ const kept = {};
51265
+ const omitted = [];
51266
+ for (const [key, value] of Object.entries(elided)) {
51267
+ if (JSON.stringify(value).length > MAX_FIELD_CHARS) omitted.push(key);
51268
+ else kept[key] = value;
51269
+ }
51270
+ json2 = JSON.stringify({ ...kept, _omitted: omitted });
51271
+ }
51272
+ return STRUCTURED_TEXT_PREFIX + json2;
51273
+ }
51274
+ function withStructuredText(result) {
51275
+ const r = result;
51276
+ if (!r || typeof r !== "object") return result;
51277
+ const structured = r.structuredContent;
51278
+ if (!structured || typeof structured !== "object" || Array.isArray(structured)) return result;
51279
+ const content = Array.isArray(r.content) ? r.content : [];
51280
+ if (textIsTheJson(content, structured)) return result;
51281
+ const line = structuredTextLine(structured, textOf(content));
51282
+ return { ...r, content: [...content, { type: "text", text: line }] };
51283
+ }
51284
+ function installStructuredText(server2) {
51285
+ const target = server2;
51286
+ const register = target.registerTool;
51287
+ if (typeof register === "function") {
51288
+ target.registerTool = function(name, config2, cb) {
51289
+ const wrapped = typeof cb === "function" ? async (...args) => withStructuredText(await cb(...args)) : cb;
51290
+ return register.call(this, name, config2, wrapped);
51291
+ };
51292
+ }
51293
+ const createError = target.createToolError;
51294
+ if (typeof createError === "function") {
51295
+ target.createToolError = (message) => withStructuredText(createError.call(target, message));
51296
+ }
51297
+ }
51298
+
51299
+ // src/services/tableResponse.ts
51300
+ var DEFAULT_TABLE_RESPONSE_BYTES = 4 * 1024 * 1024;
51301
+ var MAX_TABLE_RESPONSE_BYTES = 8 * 1024 * 1024;
51302
+ var MAX_TABLES_PER_PAGE = 500;
51303
+ var MIN_TABLE_RESPONSE_BYTES = 1024;
51304
+ function validateTablePageOptions(options) {
51305
+ const {
51306
+ offset = 0,
51307
+ limit = MAX_TABLES_PER_PAGE,
51308
+ maxBytes = DEFAULT_TABLE_RESPONSE_BYTES
51309
+ } = options;
51310
+ 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) {
51311
+ throw new CodedError("Invalid table page offset, limit or maxBytes.", {
51312
+ code: "validation_error"
51313
+ });
51314
+ }
51315
+ return { offset, limit, maxBytes };
51316
+ }
51317
+ function response(id2, result, tables, options, stoppedAtSizeLimit) {
51318
+ const tableCount = result.tables.length;
51319
+ const next = options.offset + tables.length;
51320
+ const hasMore = next < tableCount;
51321
+ const complete = result.tableCellsComplete && tables.every((table) => table.complete);
51322
+ const markdown = tables.map(
51323
+ (table) => table.markdown ?? `[table ${table.index} could not be ${table.contentOmitted ? "returned" : "decoded"}: ${table.reason}]`
51324
+ ).join("\n\n");
51325
+ 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)"}:
51326
+
51327
+ ${markdown}`;
51328
+ if (hasMore)
51329
+ summary += `
51330
+
51331
+ More tables remain: call get-note-tables with offset ${next} and the same limit and maxBytes.`;
51332
+ return {
51333
+ content: [{ type: "text", text: summary }],
51334
+ structuredContent: {
51335
+ id: id2,
51336
+ tables,
51337
+ tableCount,
51338
+ tableCellsComplete: complete,
51339
+ markdown,
51340
+ page: {
51341
+ ...options,
51342
+ totalAvailable: tableCount,
51343
+ returned: tables.length,
51344
+ ...hasMore ? { nextOffset: next } : {},
51345
+ hasMore,
51346
+ stoppedAtSizeLimit
51347
+ }
51348
+ }
51349
+ };
51350
+ }
51351
+ var size = (value) => Buffer.byteLength(JSON.stringify(withStructuredText(value)));
51352
+ function tablePageResponse(id2, result, request = {}) {
51353
+ const options = validateTablePageOptions(request);
51354
+ const available = Math.max(0, Math.min(options.limit, result.tables.length - options.offset));
51355
+ const fits = (count) => {
51356
+ const candidate = result.tables.slice(options.offset, options.offset + count);
51357
+ const couldStopLater = count < available;
51358
+ const candidateBytes = Math.max(
51359
+ size(response(id2, result, candidate, options, false)),
51360
+ couldStopLater ? size(response(id2, result, candidate, options, true)) : 0
51361
+ );
51362
+ return candidateBytes <= options.maxBytes;
51363
+ };
51364
+ let accepted = 0;
51365
+ let trial = 1;
51366
+ while (trial <= available) {
51367
+ if (fits(trial)) {
51368
+ accepted = trial;
51369
+ if (trial === available) break;
51370
+ trial = Math.min(available, trial * 2);
51371
+ continue;
51372
+ }
51373
+ let low = accepted + 1;
51374
+ let high = trial - 1;
51375
+ while (low <= high) {
51376
+ const middle = Math.floor((low + high) / 2);
51377
+ if (fits(middle)) {
51378
+ accepted = middle;
51379
+ low = middle + 1;
51380
+ } else {
51381
+ high = middle - 1;
51382
+ }
51383
+ }
51384
+ break;
51385
+ }
51386
+ let tables = result.tables.slice(options.offset, options.offset + accepted);
51387
+ const stoppedAtSizeLimit = accepted < available;
51388
+ if (stoppedAtSizeLimit && accepted === 0) {
51389
+ const table = result.tables[options.offset];
51390
+ tables = [
51391
+ {
51392
+ index: table.index,
51393
+ id: table.id,
51394
+ ...table.attachmentId !== void 0 ? { attachmentId: table.attachmentId } : {},
51395
+ ...table.rowCount !== void 0 ? { rowCount: table.rowCount } : {},
51396
+ ...table.columnCount !== void 0 ? { columnCount: table.columnCount } : {},
51397
+ complete: false,
51398
+ contentOmitted: true,
51399
+ reason: "Table content exceeds this response size limit. Increase maxBytes or export the note to a file."
51400
+ }
51401
+ ];
51402
+ }
51403
+ const output = response(id2, result, tables, options, stoppedAtSizeLimit);
51404
+ if (size(output) > options.maxBytes) {
51405
+ throw new CodedError(
51406
+ "Table metadata is too large for this response limit; increase maxBytes.",
51407
+ { code: "validation_error" }
51408
+ );
51409
+ }
51410
+ return output;
51411
+ }
51412
+
51184
51413
  // src/utils/noteParagraphs.ts
51185
51414
  import { gunzipSync as gunzipSync9 } from "node:zlib";
51186
51415
  var ENVELOPE_CODE = {
@@ -51392,10 +51621,10 @@ function pageParagraphs(paragraphs, { offset = 0, limit = 500, maxBytes = 4 * 10
51392
51621
  const out = [];
51393
51622
  let bytes = 0;
51394
51623
  for (let i = start; i < list.length && out.length < limit; i++) {
51395
- const size = Buffer.byteLength(JSON.stringify(list[i]));
51396
- if (out.length && bytes + size > maxBytes) break;
51624
+ const size2 = Buffer.byteLength(JSON.stringify(list[i]));
51625
+ if (out.length && bytes + size2 > maxBytes) break;
51397
51626
  out.push(list[i]);
51398
- bytes += size;
51627
+ bytes += size2;
51399
51628
  }
51400
51629
  const next = start + out.length;
51401
51630
  return {
@@ -51931,10 +52160,10 @@ function listNoteLinks(options = {}) {
51931
52160
  const page = [];
51932
52161
  let bytes = 0;
51933
52162
  for (let i = start; i < links.length && page.length < limit; i++) {
51934
- const size = Buffer.byteLength(JSON.stringify(links[i]));
51935
- if (page.length && bytes + size > maxBytes) break;
52163
+ const size2 = Buffer.byteLength(JSON.stringify(links[i]));
52164
+ if (page.length && bytes + size2 > maxBytes) break;
51936
52165
  page.push(links[i]);
51937
- bytes += size;
52166
+ bytes += size2;
51938
52167
  }
51939
52168
  const next = start + page.length;
51940
52169
  return {
@@ -52545,8 +52774,8 @@ var DataUrlWriter = class {
52545
52774
  if (!this.fits(data.length)) return { error: "too-large" };
52546
52775
  return this.embed(`generated:${key}`, data, mime);
52547
52776
  }
52548
- fits(size) {
52549
- return size <= this.maxBytes && this.embeddedBytes + size <= this.totalBytes;
52777
+ fits(size2) {
52778
+ return size2 <= this.maxBytes && this.embeddedBytes + size2 <= this.totalBytes;
52550
52779
  }
52551
52780
  embed(key, data, mime) {
52552
52781
  const result = { url: `data:${mime};base64,${data.toString("base64")}`, mime };
@@ -55307,85 +55536,6 @@ function exportWithTemplate(request, deps, chosen, notes, skipped, { output, ass
55307
55536
  return { ...receipt, markdown };
55308
55537
  }
55309
55538
 
55310
- // src/utils/structuredText.ts
55311
- import { isDeepStrictEqual } from "node:util";
55312
- var STRUCTURED_TEXT_PREFIX = "structuredContent: ";
55313
- var SHOWN_ABOVE = "[shown in full above]";
55314
- var MIN_ELIDED_CHARS = 64;
55315
- var MAX_MIRROR_CHARS = 16384;
55316
- var MAX_FIELD_CHARS = 1024;
55317
- function textOf(content) {
55318
- return content.map(
55319
- (item) => item && typeof item === "object" && item.type === "text" ? String(item.text ?? "") : ""
55320
- ).join("\n");
55321
- }
55322
- function elide(value, text2) {
55323
- if (typeof value === "string") {
55324
- return value.length >= MIN_ELIDED_CHARS && text2.includes(value) ? SHOWN_ABOVE : value;
55325
- }
55326
- if (Array.isArray(value)) return value.map((item) => elide(item, text2));
55327
- if (value && typeof value === "object") {
55328
- const out = {};
55329
- for (const [key, item] of Object.entries(value)) {
55330
- if (item !== void 0) out[key] = elide(item, text2);
55331
- }
55332
- return out;
55333
- }
55334
- return value;
55335
- }
55336
- function textIsTheJson(content, structured) {
55337
- const plain = JSON.parse(JSON.stringify(structured));
55338
- return content.some((item) => {
55339
- const text2 = item?.text;
55340
- if (item?.type !== "text" || typeof text2 !== "string") return false;
55341
- const trimmed = text2.trim();
55342
- if (!trimmed.startsWith("{")) return false;
55343
- try {
55344
- return isDeepStrictEqual(JSON.parse(trimmed), plain);
55345
- } catch {
55346
- return false;
55347
- }
55348
- });
55349
- }
55350
- function structuredTextLine(structured, text2) {
55351
- const elided = elide(structured, text2);
55352
- let json2 = JSON.stringify(elided);
55353
- if (json2.length > MAX_MIRROR_CHARS) {
55354
- const kept = {};
55355
- const omitted = [];
55356
- for (const [key, value] of Object.entries(elided)) {
55357
- if (JSON.stringify(value).length > MAX_FIELD_CHARS) omitted.push(key);
55358
- else kept[key] = value;
55359
- }
55360
- json2 = JSON.stringify({ ...kept, _omitted: omitted });
55361
- }
55362
- return STRUCTURED_TEXT_PREFIX + json2;
55363
- }
55364
- function withStructuredText(result) {
55365
- const r = result;
55366
- if (!r || typeof r !== "object") return result;
55367
- const structured = r.structuredContent;
55368
- if (!structured || typeof structured !== "object" || Array.isArray(structured)) return result;
55369
- const content = Array.isArray(r.content) ? r.content : [];
55370
- if (textIsTheJson(content, structured)) return result;
55371
- const line = structuredTextLine(structured, textOf(content));
55372
- return { ...r, content: [...content, { type: "text", text: line }] };
55373
- }
55374
- function installStructuredText(server2) {
55375
- const target = server2;
55376
- const register = target.registerTool;
55377
- if (typeof register === "function") {
55378
- target.registerTool = function(name, config2, cb) {
55379
- const wrapped = typeof cb === "function" ? async (...args) => withStructuredText(await cb(...args)) : cb;
55380
- return register.call(this, name, config2, wrapped);
55381
- };
55382
- }
55383
- const createError = target.createToolError;
55384
- if (typeof createError === "function") {
55385
- target.createToolError = (message) => withStructuredText(createError.call(target, message));
55386
- }
55387
- }
55388
-
55389
55539
  // src/tools/directOperations.ts
55390
55540
  import { createHash as createHash4 } from "node:crypto";
55391
55541
  import {
@@ -55677,12 +55827,12 @@ function localAttachment(path10) {
55677
55827
  }
55678
55828
  }
55679
55829
  var MAX_ADD_ATTACHMENT_BYTES = 64 * 1024 * 1024;
55680
- function fileMatches(path10, size, expected) {
55830
+ function fileMatches(path10, size2, expected) {
55681
55831
  let descriptor;
55682
55832
  try {
55683
55833
  descriptor = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
55684
55834
  const stat = fstatSync7(descriptor);
55685
- if (!stat.isFile() || stat.size !== size) return false;
55835
+ if (!stat.isFile() || stat.size !== size2) return false;
55686
55836
  const hash = createHash4("sha256");
55687
55837
  const chunk = Buffer.allocUnsafe(1024 * 1024);
55688
55838
  let total = 0;
@@ -55690,10 +55840,10 @@ function fileMatches(path10, size, expected) {
55690
55840
  const read = readSync5(descriptor, chunk, 0, chunk.length, null);
55691
55841
  if (read === 0) break;
55692
55842
  total += read;
55693
- if (total > size) return false;
55843
+ if (total > size2) return false;
55694
55844
  hash.update(chunk.subarray(0, read));
55695
55845
  }
55696
- return total === size && hash.digest("hex") === expected;
55846
+ return total === size2 && hash.digest("hex") === expected;
55697
55847
  } catch {
55698
55848
  return false;
55699
55849
  } finally {
@@ -62842,10 +62992,10 @@ async function readEditorJson(req) {
62842
62992
  if (!/^application\/json(?:\s*;|$)/i.test(type))
62843
62993
  throw new EditorHttpError(415, "unsupported-media-type", "Send JSON (application/json).");
62844
62994
  const chunks = [];
62845
- let size = 0;
62995
+ let size2 = 0;
62846
62996
  for await (const chunk of req) {
62847
- size += chunk.length;
62848
- if (size > MAX_EDITOR_BODY_BYTES)
62997
+ size2 += chunk.length;
62998
+ if (size2 > MAX_EDITOR_BODY_BYTES)
62849
62999
  throw new EditorHttpError(413, "too-large", "Request body too large.");
62850
63000
  chunks.push(chunk);
62851
63001
  }
@@ -63196,12 +63346,12 @@ var textFingerprint = (normalized2) => createHash8("sha256").update(normalized2,
63196
63346
  function textSimilarity(a, b) {
63197
63347
  if (a === b) return 1;
63198
63348
  if (!a || !b) return 0;
63199
- const size = Math.min(a.length, b.length) < 3 ? 2 : 3;
63349
+ const size2 = Math.min(a.length, b.length) < 3 ? 2 : 3;
63200
63350
  const grams = (s) => {
63201
63351
  const out = /* @__PURE__ */ new Map();
63202
63352
  const padded = ` ${s} `;
63203
- for (let i = 0; i + size <= padded.length; i++) {
63204
- const g = padded.slice(i, i + size);
63353
+ for (let i = 0; i + size2 <= padded.length; i++) {
63354
+ const g = padded.slice(i, i + size2);
63205
63355
  out.set(g, (out.get(g) ?? 0) + 1);
63206
63356
  }
63207
63357
  return out;
@@ -64118,7 +64268,7 @@ var scopeGuardInputs = {
64118
64268
  maxItems: MAX_FORBIDDEN_FOLDERS
64119
64269
  }
64120
64270
  ).optional().describe(
64121
- "Precondition: the note must not be inside any of these folders or their subfolders (for move-note, neither may the destination). Re-checked immediately before the write."
64271
+ "Precondition: the note must not be inside any of these folders or their subfolders (for move-note and batch-move-notes, neither may the destination). Re-checked immediately before the write."
64122
64272
  )
64123
64273
  };
64124
64274
  function scopeFrom(args) {
@@ -65041,18 +65191,27 @@ registerTool(
65041
65191
  registerTool(
65042
65192
  "get-note-tables",
65043
65193
  {
65044
- 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.",
65045
- inputSchema: { id: noteIdInput },
65194
+ 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.",
65195
+ inputSchema: {
65196
+ id: noteIdInput,
65197
+ 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"),
65198
+ 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})`),
65199
+ maxBytes: external_exports.number().int().min(MIN_TABLE_RESPONSE_BYTES).max(MAX_TABLE_RESPONSE_BYTES).optional().describe(
65200
+ `Maximum UTF-8 bytes of the serialized tool response (default ${DEFAULT_TABLE_RESPONSE_BYTES}, max ${MAX_TABLE_RESPONSE_BYTES})`
65201
+ )
65202
+ },
65046
65203
  outputSchema: {
65047
65204
  id: external_exports.string().optional(),
65048
65205
  tables: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
65049
65206
  tableCount: external_exports.number().optional(),
65050
65207
  tableCellsComplete: external_exports.boolean().optional(),
65051
- markdown: external_exports.string().optional()
65208
+ markdown: external_exports.string().optional(),
65209
+ page: external_exports.record(external_exports.unknown()).optional()
65052
65210
  },
65053
65211
  annotations: { readOnlyHint: true }
65054
65212
  },
65055
- withErrorHandling(({ id: id2 }) => {
65213
+ withErrorHandling(({ id: id2, offset, limit, maxBytes }) => {
65214
+ const options = validateTablePageOptions({ offset, limit, maxBytes });
65056
65215
  const { metadata, message } = getNoteMetadata(id2);
65057
65216
  if (!metadata) return errorResponse(message || `Failed to read note "${id2}"`);
65058
65217
  if (metadata.passwordProtected) {
@@ -65061,17 +65220,7 @@ registerTool(
65061
65220
  );
65062
65221
  }
65063
65222
  const result = notesManager.getNoteTablesById(id2);
65064
- const count = result.tables.length;
65065
- const summary = count === 0 ? "This note has no native tables." : `${count} table(s)${result.tableCellsComplete ? "" : " (some content could not be decoded; see tables[].reason)"}:
65066
-
65067
- ${result.markdown}`;
65068
- return successResponse(summary, {
65069
- id: id2,
65070
- tables: result.tables,
65071
- tableCount: count,
65072
- tableCellsComplete: result.tableCellsComplete,
65073
- markdown: result.markdown
65074
- });
65223
+ return tablePageResponse(id2, result, options);
65075
65224
  }, "Error reading note tables")
65076
65225
  );
65077
65226
  registerTool(
@@ -65850,7 +65999,7 @@ registerTool(
65850
65999
  readLinks: (noteId3) => readRichNote(noteId3).links,
65851
66000
  append: (request) => {
65852
66001
  let route = "applescript";
65853
- const response = guardedAppend(
66002
+ const response2 = guardedAppend(
65854
66003
  {
65855
66004
  ...request,
65856
66005
  format: "html",
@@ -65858,11 +66007,11 @@ registerTool(
65858
66007
  },
65859
66008
  (r) => route = r
65860
66009
  );
65861
- if (response.isError) {
65862
- const envelope = response.structuredContent;
65863
- throw envelope?.code ? new CodedError(response.content[0].text, envelope) : new Error(response.content[0].text);
66010
+ if (response2.isError) {
66011
+ const envelope = response2.structuredContent;
66012
+ throw envelope?.code ? new CodedError(response2.content[0].text, envelope) : new Error(response2.content[0].text);
65864
66013
  }
65865
- return { route, contentHash: String(response.structuredContent?.contentHash ?? "") };
66014
+ return { route, contentHash: String(response2.structuredContent?.contentHash ?? "") };
65866
66015
  }
65867
66016
  }
65868
66017
  );
@@ -66678,8 +66827,9 @@ registerTool(
66678
66827
  registerTool(
66679
66828
  "batch-move-notes",
66680
66829
  {
66681
- description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder); a smart folder is refused for the whole call (code unsupported) before any note moves.",
66830
+ description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: optional folder preconditions apply independently to every note, checked live immediately before its move in the same AppleScript; a scope mismatch leaves that note unchanged and other items continue. Each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder); a smart folder is refused for the whole call (code unsupported) before any note moves.",
66682
66831
  inputSchema: {
66832
+ ...scopeGuardInputs,
66683
66833
  ids: noteIdArrayInput.describe(
66684
66834
  `Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
66685
66835
  ),
@@ -66698,11 +66848,12 @@ registerTool(
66698
66848
  results: external_exports.array(external_exports.object({}).passthrough()).optional()
66699
66849
  }
66700
66850
  },
66701
- withErrorHandling(({ ids, folder, account }) => {
66851
+ withErrorHandling((args) => {
66852
+ const { ids, folder, account } = args;
66702
66853
  if (ids.length === 0) {
66703
66854
  return errorResponse("No note IDs provided");
66704
66855
  }
66705
- const results = notesManager.batchMoveNotes(ids, folder, account);
66856
+ const results = notesManager.batchMoveNotes(ids, folder, account, scopeFrom(args));
66706
66857
  const succeeded = results.filter((r) => r.success).length;
66707
66858
  const failed = results.filter((r) => !r.success).length;
66708
66859
  const lines = [`Batch move to "${folder}": ${succeeded} succeeded, ${failed} failed`];
@@ -66712,13 +66863,20 @@ registerTool(
66712
66863
  lines.push(` - ${result.id}: ${result.error}`);
66713
66864
  }
66714
66865
  }
66715
- return succeeded > 0 ? successResponse(lines.join("\n"), {
66716
- ok: failed === 0,
66717
- folder,
66718
- succeeded,
66719
- failed,
66720
- results
66721
- }) : errorResponse(lines.join("\n"));
66866
+ if (succeeded > 0)
66867
+ return successResponse(lines.join("\n"), {
66868
+ ok: failed === 0,
66869
+ folder,
66870
+ succeeded,
66871
+ failed,
66872
+ results
66873
+ });
66874
+ const allRefusedBeforeMove = results.length === ids.length && results.every((result) => result.committed === false && result.indeterminate === false);
66875
+ if (!allRefusedBeforeMove)
66876
+ lines.push("\nBatch move outcome is uncertain; read each exact note ID before retrying.");
66877
+ const message = lines.join("\n");
66878
+ const envelope = allRefusedBeforeMove ? { ...classifyError(message), committed: false, indeterminate: false } : { code: "verification_failed", indeterminate: true };
66879
+ return errorResponse(message, new CodedError(message, envelope));
66722
66880
  }, "Error performing batch move")
66723
66881
  );
66724
66882
  registerTool(
@@ -67003,7 +67161,7 @@ registerTool(
67003
67161
  `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.`
67004
67162
  );
67005
67163
  }
67006
- const response = {
67164
+ const response2 = {
67007
67165
  content: [
67008
67166
  {
67009
67167
  type: "text",
@@ -67018,13 +67176,13 @@ Full JSON export:`
67018
67176
  ],
67019
67177
  structuredContent: { ...exportData }
67020
67178
  };
67021
- const responseBytes = Buffer.byteLength(JSON.stringify(response));
67179
+ const responseBytes = Buffer.byteLength(JSON.stringify(response2));
67022
67180
  if (responseBytes > maxResponseBytes) {
67023
67181
  return errorResponse(
67024
67182
  `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.`
67025
67183
  );
67026
67184
  }
67027
- return response;
67185
+ return response2;
67028
67186
  }, "Error exporting notes")
67029
67187
  );
67030
67188
  registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.15.5",
3
+ "version": "2.15.7",
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",