apple-notes-mcp 2.15.6 → 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 +41 -8
  2. package/build/index.js +80 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1376,21 +1376,28 @@ actual destination folder ID matches the requested folder.
1376
1376
 
1377
1377
  #### Folder scope guards
1378
1378
 
1379
- `update-note`, `append-to-note`, `delete-note`, and `move-note` accept three
1380
- 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`.
1381
1382
 
1382
1383
  - `ifFolderId`: the note must currently be in exactly this folder.
1383
1384
  - `ifAncestorFolderId`: the note must be inside this folder or any of its
1384
1385
  subfolders.
1385
1386
  - `forbiddenAncestorFolderIds` (up to 50): the note must not be inside any of
1386
- these folders or their subfolders. For `move-note`, the destination must not
1387
- be either.
1387
+ these folders or their subfolders. For `move-note` and `batch-move-notes`,
1388
+ the destination must not be either.
1388
1389
 
1389
1390
  The checks read Notes.app's live folders inside the same AppleScript as the
1390
- write, immediately before it, so a note moved after you reviewed it is left
1391
- alone and the call fails with `Scope guard failed: …`. The one exception is a
1392
- native append to a protected note (it runs through Shortcuts): there the check
1393
- 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.
1394
1401
 
1395
1402
  **Example - retire a note only while it is still in the inbox:**
1396
1403
  ```json
@@ -1915,9 +1922,35 @@ Moves multiple notes to a folder.
1915
1922
  | `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
1916
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 |
1917
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 |
1918
1926
 
1919
1927
  **Returns:** Summary of successes and failures. Each success is reported only
1920
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.
1921
1954
 
1922
1955
  ---
1923
1956
 
package/build/index.js CHANGED
@@ -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
  });
@@ -64232,7 +64268,7 @@ var scopeGuardInputs = {
64232
64268
  maxItems: MAX_FORBIDDEN_FOLDERS
64233
64269
  }
64234
64270
  ).optional().describe(
64235
- "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."
64236
64272
  )
64237
64273
  };
64238
64274
  function scopeFrom(args) {
@@ -66791,8 +66827,9 @@ registerTool(
66791
66827
  registerTool(
66792
66828
  "batch-move-notes",
66793
66829
  {
66794
- 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.",
66795
66831
  inputSchema: {
66832
+ ...scopeGuardInputs,
66796
66833
  ids: noteIdArrayInput.describe(
66797
66834
  `Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
66798
66835
  ),
@@ -66811,11 +66848,12 @@ registerTool(
66811
66848
  results: external_exports.array(external_exports.object({}).passthrough()).optional()
66812
66849
  }
66813
66850
  },
66814
- withErrorHandling(({ ids, folder, account }) => {
66851
+ withErrorHandling((args) => {
66852
+ const { ids, folder, account } = args;
66815
66853
  if (ids.length === 0) {
66816
66854
  return errorResponse("No note IDs provided");
66817
66855
  }
66818
- const results = notesManager.batchMoveNotes(ids, folder, account);
66856
+ const results = notesManager.batchMoveNotes(ids, folder, account, scopeFrom(args));
66819
66857
  const succeeded = results.filter((r) => r.success).length;
66820
66858
  const failed = results.filter((r) => !r.success).length;
66821
66859
  const lines = [`Batch move to "${folder}": ${succeeded} succeeded, ${failed} failed`];
@@ -66825,13 +66863,20 @@ registerTool(
66825
66863
  lines.push(` - ${result.id}: ${result.error}`);
66826
66864
  }
66827
66865
  }
66828
- return succeeded > 0 ? successResponse(lines.join("\n"), {
66829
- ok: failed === 0,
66830
- folder,
66831
- succeeded,
66832
- failed,
66833
- results
66834
- }) : 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));
66835
66880
  }, "Error performing batch move")
66836
66881
  );
66837
66882
  registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.15.6",
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",