apple-notes-mcp 2.9.12 → 2.9.13

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 +5 -3
  2. package/build/index.js +169 -89
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -257,7 +257,7 @@ Creates a new note in Apple Notes.
257
257
  | `content` | string | One of `content`/`contentPath` | The body content of the note (do not repeat the title here) |
258
258
  | `contentPath` | string | One of `content`/`contentPath` | Absolute path of a local UTF-8 file to use as the body instead of `content`. Allowed in the same places [`save-attachment`](#save-attachment) may write (home, temp, `/Volumes`); symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped |
259
259
  | `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags). Refused with `format: "markdown"` |
260
- | `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). Defaults to account root |
260
+ | `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). A smart folder is refused (see [`move-note`](#move-note)). Defaults to account root |
261
261
  | `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
262
262
  | `format` | string | No | Content format: `"plaintext"` (default), `"html"`, or `"markdown"`. In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities. `"markdown"` produces real Title/Heading/Subheading styles through a Shortcut; see [Markdown notes](#markdown-notes) |
263
263
  | `markdownRoute` | string | No | With `format: "markdown"` only: `"shortcut"` (default) or `"html"`. See [Markdown through HTML](#markdown-through-html) |
@@ -1032,6 +1032,8 @@ such a note for good, do it in Notes.app.
1032
1032
 
1033
1033
  Moves a note to a different folder. The note is relocated in place via Notes.app's native `move`, so its id, creation date, and all embedded attachments (files, images, scans, PDFs, audio) are preserved. The destination folder must already exist — create it first with [`create-folder`](#create-folder).
1034
1034
 
1035
+ A smart folder is never a destination: it only gathers notes by its rules, and Notes.app would move the note to Recently Deleted or store a created note where no folder shows it. `create-note`, `create-note-with-attachment`, `move-note`, `batch-move-notes`, and `create-folder` refuse one before writing anything, with `code: "unsupported"`, `committed: false`, and `reason: "smart_folder_destination"`. An ordinary folder that shares a smart folder's name is still found. Detection reads the NoteStore database, so it needs Full Disk Access; without it, destinations resolve as before.
1036
+
1035
1037
  | Parameter | Type | Required | Description |
1036
1038
  |-----------|------|----------|-------------|
1037
1039
  | `id` | string | Yes | Exact CoreData note ID returned by a read or search |
@@ -1402,7 +1404,7 @@ Creates a new folder, including a whole nested hierarchy in one call.
1402
1404
 
1403
1405
  | Parameter | Type | Required | Description |
1404
1406
  |-----------|------|----------|-------------|
1405
- | `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped |
1407
+ | `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped. A segment that names a smart folder is refused before anything is created |
1406
1408
  | `account` | string | No | Account to create folder in (defaults to Notes.app's default account; exact or unique-prefix match) |
1407
1409
 
1408
1410
  **Example:**
@@ -1590,7 +1592,7 @@ Moves multiple notes to a folder.
1590
1592
  | Parameter | Type | Required | Description |
1591
1593
  |-----------|------|----------|-------------|
1592
1594
  | `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
1593
- | `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder) |
1595
+ | `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 |
1594
1596
  | `account` | string | No | Account containing the folder |
1595
1597
 
1596
1598
  **Returns:** Summary of successes and failures. Each success is reported only
package/build/index.js CHANGED
@@ -39588,6 +39588,77 @@ function getChecklistItems(noteId3) {
39588
39588
  return { items };
39589
39589
  }
39590
39590
 
39591
+ // src/utils/errorCodes.ts
39592
+ var CodedError = class extends Error {
39593
+ envelope;
39594
+ constructor(message, envelope) {
39595
+ super(message);
39596
+ this.name = "CodedError";
39597
+ this.envelope = envelope;
39598
+ }
39599
+ };
39600
+ var RULES = [
39601
+ { code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
39602
+ {
39603
+ code: "verification_failed",
39604
+ pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
39605
+ },
39606
+ {
39607
+ code: "revision_conflict",
39608
+ pattern: /changed after it was read|revision changed|(?:pinned|note) state changed|changed during (?:read|preflight)|changed; read (?:it|the note) again|rich text do not match/i
39609
+ },
39610
+ { code: "full_disk_access_missing", pattern: /Full Disk Access/i },
39611
+ {
39612
+ code: "shortcut_not_installed",
39613
+ pattern: /(?:Install|Import) the supplied|no such shortcut|Run apple-notes-mcp setup/i
39614
+ },
39615
+ {
39616
+ code: "not_found",
39617
+ pattern: /not found|does not exist|\bNo \w+ found\b|does not contain any checklist items|Can[’']t get (?:note|folder|account|attachment)|Cannot find note|Scope is absent/i
39618
+ },
39619
+ { code: "ambiguous", pattern: /ambiguous|more than one|exactly once|Duplicate note IDs/i },
39620
+ {
39621
+ code: "notes_unavailable",
39622
+ pattern: /Notes\.app is (?:not responding|busy)|Lost connection to Notes|isn't running/i
39623
+ },
39624
+ {
39625
+ code: "unsupported",
39626
+ pattern: /password-protected|Locked notes|unsupported|not supported|is blocked|blocked because|has not passed live|refuses to sign|No (?:supported )?background|only one where|Real Notes link unavailable/i
39627
+ },
39628
+ {
39629
+ code: "validation_error",
39630
+ pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
39631
+ }
39632
+ ];
39633
+ var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
39634
+ var WRITE_ACCEPTED = /\baccepted (?:the|an) \w+, but/i;
39635
+ function classifyError(message, cause) {
39636
+ if (cause instanceof CodedError) return { ...cause.envelope };
39637
+ const text2 = cause instanceof Error && cause.message && !message.includes(cause.message) ? `${message}
39638
+ ${cause.message}` : message;
39639
+ const timeoutCause = typeof cause === "object" && cause !== null && cause.code === "ETIMEDOUT";
39640
+ let code = "operation_failed";
39641
+ if (timeoutCause) code = "timeout_indeterminate";
39642
+ else if (isPermissionDenied(text2)) code = "permission_denied";
39643
+ else code = RULES.find((rule) => rule.pattern.test(text2))?.code ?? "operation_failed";
39644
+ const envelope = { code };
39645
+ if (code === "timeout_indeterminate" || code === "verification_failed") {
39646
+ envelope.indeterminate = true;
39647
+ if (WRITE_ACCEPTED.test(text2)) envelope.committed = true;
39648
+ } else if (code === "revision_conflict" || NOTHING_WRITTEN.test(text2)) {
39649
+ envelope.committed = false;
39650
+ envelope.indeterminate = false;
39651
+ }
39652
+ return envelope;
39653
+ }
39654
+ function errorResult(message, cause) {
39655
+ return {
39656
+ content: [{ type: "text", text: message }],
39657
+ structuredContent: classifyError(message, cause),
39658
+ isError: true
39659
+ };
39660
+ }
39661
+
39591
39662
  // src/utils/inlineImages.ts
39592
39663
  var DEFAULT_MAX_INLINE_IMAGE_BYTES = 256 * 1024;
39593
39664
  function maxInlineImageBytes(env = process.env) {
@@ -42358,6 +42429,24 @@ function buildFolderReference(folderPath) {
42358
42429
  }
42359
42430
  var AS_ACCOUNT_REF = "__acctRef";
42360
42431
  var LIVE_FOLDER_NOT_FOUND = "Folder not found";
42432
+ var SMART_FOLDER_DESTINATION = "SMART_FOLDER_DESTINATION";
42433
+ function readSmartFolderIds(read = readSmartFolders) {
42434
+ return (read().folders ?? []).map((folder) => folder.id).filter((id2) => FOLDER_ID_PATTERN.test(id2));
42435
+ }
42436
+ function smartFolderDestinationError(folderPath) {
42437
+ return new CodedError(
42438
+ `Refused: "${folderPath}" is a smart folder. Smart folders only gather notes by their rules and cannot hold notes or folders; choose an ordinary folder (list-folders). Nothing was changed`,
42439
+ {
42440
+ code: "unsupported",
42441
+ committed: false,
42442
+ indeterminate: false,
42443
+ reason: "smart_folder_destination"
42444
+ }
42445
+ );
42446
+ }
42447
+ function throwIfSmartFolderDestination(error2, folderPath) {
42448
+ if (error2?.includes(SMART_FOLDER_DESTINATION)) throw smartFolderDestinationError(folderPath);
42449
+ }
42361
42450
  function buildLiveFolderResolution(folderPath, varName, opts = {}) {
42362
42451
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) throw new Error("Invalid AppleScript variable");
42363
42452
  buildFolderReference(folderPath);
@@ -42366,10 +42455,26 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
42366
42455
  const cid = `${varName}_cid`;
42367
42456
  const any = `${varName}_any`;
42368
42457
  const parent = `${varName}_p`;
42369
- const notFound = `error "${LIVE_FOLDER_NOT_FOUND}: ${escapePlainStringForAppleScript(folderPath)}" number -1728`;
42370
- const lines = [];
42458
+ const quotedPath = escapePlainStringForAppleScript(folderPath);
42459
+ const notFound = `error "${LIVE_FOLDER_NOT_FOUND}: ${quotedPath}" number -1728`;
42460
+ const excluded = (opts.excludeFolderIds ?? []).filter((id2) => FOLDER_ID_PATTERN.test(id2));
42461
+ const skipIds = `${varName}_sx`;
42462
+ const skipped = `${varName}_sh`;
42463
+ const candidateTest = excluded.length ? [
42464
+ ` if ${skipIds} contains ${cid} then`,
42465
+ ` set ${skipped} to true`,
42466
+ ` else if exists folder id ${cid} then`
42467
+ ] : [` if exists folder id ${cid} then`];
42468
+ const unresolved = excluded.length ? [
42469
+ `if ${varName} is missing value then`,
42470
+ ` if ${skipped} then error "${SMART_FOLDER_DESTINATION}: ${quotedPath}" number -1728`,
42471
+ ` ${notFound}`,
42472
+ `end if`
42473
+ ] : [`if ${varName} is missing value then ${notFound}`];
42474
+ const lines = excluded.length ? [`set ${skipIds} to {${excluded.map((id2) => `"${id2}"`).join(", ")}}`] : [];
42371
42475
  parts.forEach((part, i) => {
42372
42476
  const name = escapePlainStringForAppleScript(part);
42477
+ if (excluded.length) lines.push(`set ${skipped} to false`);
42373
42478
  if (i === 0) {
42374
42479
  lines.push(
42375
42480
  `set ${varName} to missing value`,
@@ -42377,7 +42482,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
42377
42482
  `repeat with ${c} in (folders of ${AS_ACCOUNT_REF} whose name is "${name}")`,
42378
42483
  ` try`,
42379
42484
  ` set ${cid} to id of ${c}`,
42380
- ` if exists folder id ${cid} then`,
42485
+ ...candidateTest,
42381
42486
  ` if class of (container of ${c}) is not folder then`,
42382
42487
  ` set ${varName} to folder id ${cid}`,
42383
42488
  ` exit repeat`,
@@ -42397,7 +42502,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
42397
42502
  `repeat with ${c} in (folders of ${parent} whose name is "${name}")`,
42398
42503
  ` try`,
42399
42504
  ` set ${cid} to id of ${c}`,
42400
- ` if exists folder id ${cid} then`,
42505
+ ...candidateTest,
42401
42506
  ` set ${varName} to folder id ${cid}`,
42402
42507
  ` exit repeat`,
42403
42508
  ` end if`,
@@ -42405,7 +42510,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
42405
42510
  `end repeat`
42406
42511
  );
42407
42512
  }
42408
- lines.push(`if ${varName} is missing value then ${notFound}`);
42513
+ lines.push(...unresolved);
42409
42514
  });
42410
42515
  return lines.join("\n");
42411
42516
  }
@@ -42666,7 +42771,9 @@ var AppleNotesManager = class {
42666
42771
  const safeBody = escapeHtmlForAppleScript(`<h1>${htmlTitle}</h1>${bodyContent}`);
42667
42772
  let createCommand;
42668
42773
  if (folder) {
42669
- createCommand = `${buildLiveFolderResolution(folder, "__folder")}
42774
+ createCommand = `${buildLiveFolderResolution(folder, "__folder", {
42775
+ excludeFolderIds: this.smartFolderIds()
42776
+ })}
42670
42777
  make new note at __folder with properties {body:"${safeBody}"}`;
42671
42778
  } else {
42672
42779
  createCommand = `
@@ -42678,6 +42785,7 @@ var AppleNotesManager = class {
42678
42785
  const result = executeMutationAppleScript(script);
42679
42786
  if (!result.success) {
42680
42787
  throwIfAccountResolutionFailed(result.error);
42788
+ if (folder) throwIfSmartFolderDestination(result.error, folder);
42681
42789
  console.error(`Failed to create note "${title}":`, result.error);
42682
42790
  return null;
42683
42791
  }
@@ -43282,6 +43390,35 @@ var AppleNotesManager = class {
43282
43390
  return (resultList as text)${trashSuffix}
43283
43391
  `;
43284
43392
  }
43393
+ /**
43394
+ * Smart folder ids that a destination folder must never resolve to. Read
43395
+ * fresh on every write (a smart folder can be added at any time); empty
43396
+ * without Full Disk Access. See {@link readSmartFolderIds}.
43397
+ */
43398
+ smartFolderIds() {
43399
+ return readSmartFolderIds();
43400
+ }
43401
+ /**
43402
+ * Read-only preflight: throws the smart-folder refusal when `folderPath`
43403
+ * resolves in `account` only to a smart folder. For write paths that create
43404
+ * something before they reach their own destination check (the Markdown
43405
+ * bridge creates first and moves second). Other failures are left to the
43406
+ * write itself, which reports them as before.
43407
+ *
43408
+ * @param folderPath - Destination folder path (list-folders syntax)
43409
+ * @param account - Account to resolve in (defaults to Notes' default)
43410
+ */
43411
+ assertNotSmartFolderDestination(folderPath, account) {
43412
+ const excludeFolderIds = this.smartFolderIds();
43413
+ if (excludeFolderIds.length === 0) return;
43414
+ const script = buildAccountScopedScript(
43415
+ { account: this.resolveAccount(account) },
43416
+ `${buildLiveFolderResolution(folderPath, "__folder", { excludeFolderIds })}
43417
+ return id of __folder`
43418
+ );
43419
+ const result = executeAppleScript(script);
43420
+ if (!result.success) throwIfSmartFolderDestination(result.error, folderPath);
43421
+ }
43285
43422
  /**
43286
43423
  * Lists every smart folder with its decoded query, read-only.
43287
43424
  *
@@ -43823,37 +43960,40 @@ var AppleNotesManager = class {
43823
43960
  console.error(`Invalid folder name: "${name}"`);
43824
43961
  return null;
43825
43962
  }
43963
+ const smart = { rootOnly: true, excludeFolderIds: this.smartFolderIds() };
43826
43964
  for (let i = 0; i < parts.length; i++) {
43827
43965
  const currentPath = parts.slice(0, i + 1).map((p) => escapeFolderName(p)).join("/");
43828
43966
  const checkScript = buildAccountScopedScript(
43829
43967
  { account: targetAccount },
43830
- `${buildLiveFolderResolution(currentPath, "__folder", { rootOnly: true })}
43968
+ `${buildLiveFolderResolution(currentPath, "__folder", smart)}
43831
43969
  return id of __folder`
43832
43970
  );
43833
43971
  const checkResult = executeAppleScript(checkScript);
43834
43972
  if (checkResult.success) {
43835
43973
  continue;
43836
43974
  }
43975
+ throwIfSmartFolderDestination(checkResult.error, currentPath);
43837
43976
  const segmentName = escapePlainStringForAppleScript(parts[i]);
43838
43977
  let createCommand;
43839
43978
  if (i === 0) {
43840
43979
  createCommand = `make new folder with properties {name:"${segmentName}"}`;
43841
43980
  } else {
43842
43981
  const parentPath = parts.slice(0, i).map((p) => escapeFolderName(p)).join("/");
43843
- createCommand = `${buildLiveFolderResolution(parentPath, "__parent", { rootOnly: true })}
43982
+ createCommand = `${buildLiveFolderResolution(parentPath, "__parent", smart)}
43844
43983
  make new folder at __parent with properties {name:"${segmentName}"}`;
43845
43984
  }
43846
43985
  const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
43847
43986
  const result = executeMutationAppleScript(script);
43848
43987
  if (!result.success) {
43849
43988
  throwIfAccountResolutionFailed(result.error);
43989
+ throwIfSmartFolderDestination(result.error, name);
43850
43990
  console.error(`Failed to create folder "${name}":`, result.error);
43851
43991
  return null;
43852
43992
  }
43853
43993
  }
43854
43994
  const idScript = buildAccountScopedScript(
43855
43995
  { account: targetAccount },
43856
- `${buildLiveFolderResolution(name, "__folder", { rootOnly: true })}
43996
+ `${buildLiveFolderResolution(name, "__folder", smart)}
43857
43997
  return (id of __folder) & ${AS_FIELD_SEP} & (name of it)`
43858
43998
  );
43859
43999
  const idResult = executeAppleScript(idScript);
@@ -43912,7 +44052,9 @@ var AppleNotesManager = class {
43912
44052
  moveNoteById(id2, destinationFolder, account, scope2) {
43913
44053
  const targetAccount = this.resolveAccount(account);
43914
44054
  const safeId = sanitizeNoteId(id2);
43915
- const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder");
44055
+ const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder", {
44056
+ excludeFolderIds: this.smartFolderIds()
44057
+ });
43916
44058
  const moveCommand = `
43917
44059
  ${buildAccountResolution(targetAccount)}
43918
44060
  ${destFolderSetup}
@@ -43927,6 +44069,7 @@ var AppleNotesManager = class {
43927
44069
  const result = executeMutationAppleScript(script);
43928
44070
  if (!result.success) {
43929
44071
  throwIfAccountResolutionFailed(result.error);
44072
+ throwIfSmartFolderDestination(result.error, destinationFolder);
43930
44073
  console.error(
43931
44074
  `Cannot move note to "${destinationFolder}" (folder may not exist):`,
43932
44075
  result.error
@@ -44821,7 +44964,9 @@ var AppleNotesManager = class {
44821
44964
  batchMoveNotes(ids, folder, account) {
44822
44965
  if (ids.length === 0) return [];
44823
44966
  const targetAccount = this.resolveAccount(account);
44824
- const destFolderSetup = buildLiveFolderResolution(folder, "destFolder");
44967
+ const destFolderSetup = buildLiveFolderResolution(folder, "destFolder", {
44968
+ excludeFolderIds: this.smartFolderIds()
44969
+ });
44825
44970
  const results = new Array(ids.length);
44826
44971
  const runnable = [];
44827
44972
  ids.forEach((id2, i) => {
@@ -44877,6 +45022,7 @@ var AppleNotesManager = class {
44877
45022
  const res = executeMutationAppleScript(script);
44878
45023
  if (!res.success) {
44879
45024
  throwIfAccountResolutionFailed(res.error);
45025
+ throwIfSmartFolderDestination(res.error, folder);
44880
45026
  for (const r of runnable) {
44881
45027
  results[r.index] = this.createBatchResult(
44882
45028
  ids[r.index],
@@ -48358,12 +48504,15 @@ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
48358
48504
  const segments = (path10) => JSON.stringify(splitFolderPath(path10).map((part) => part.toLocaleLowerCase()));
48359
48505
  if (request.folder) {
48360
48506
  const wanted = segments(request.folder);
48361
- if (!manager.listAccounts().some(
48362
- (account) => account.defaultFolder && manager.listFolders(account.name).some((folder) => segments(folder.name) === wanted)
48507
+ const accounts = manager.listAccounts().filter((account) => account.defaultFolder);
48508
+ if (!accounts.some(
48509
+ (account) => manager.listFolders(account.name).some((folder) => segments(folder.name) === wanted)
48363
48510
  ))
48364
48511
  throw new Error(
48365
48512
  `Folder "${request.folder}" does not exist; create it with create-folder first. Nothing was created`
48366
48513
  );
48514
+ for (const account of accounts)
48515
+ manager.assertNotSmartFolderDestination(request.folder, account.name);
48367
48516
  }
48368
48517
  const defaultFolderNotes = () => new Map(
48369
48518
  manager.listAccounts().flatMap(
@@ -49031,77 +49180,6 @@ function withJsonSchema2020_12(transport2) {
49031
49180
  return transport2;
49032
49181
  }
49033
49182
 
49034
- // src/utils/errorCodes.ts
49035
- var CodedError = class extends Error {
49036
- envelope;
49037
- constructor(message, envelope) {
49038
- super(message);
49039
- this.name = "CodedError";
49040
- this.envelope = envelope;
49041
- }
49042
- };
49043
- var RULES = [
49044
- { code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
49045
- {
49046
- code: "verification_failed",
49047
- pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
49048
- },
49049
- {
49050
- code: "revision_conflict",
49051
- pattern: /changed after it was read|revision changed|(?:pinned|note) state changed|changed during (?:read|preflight)|changed; read (?:it|the note) again|rich text do not match/i
49052
- },
49053
- { code: "full_disk_access_missing", pattern: /Full Disk Access/i },
49054
- {
49055
- code: "shortcut_not_installed",
49056
- pattern: /(?:Install|Import) the supplied|no such shortcut|Run apple-notes-mcp setup/i
49057
- },
49058
- {
49059
- code: "not_found",
49060
- pattern: /not found|does not exist|\bNo \w+ found\b|does not contain any checklist items|Can[’']t get (?:note|folder|account|attachment)|Cannot find note|Scope is absent/i
49061
- },
49062
- { code: "ambiguous", pattern: /ambiguous|more than one|exactly once|Duplicate note IDs/i },
49063
- {
49064
- code: "notes_unavailable",
49065
- pattern: /Notes\.app is (?:not responding|busy)|Lost connection to Notes|isn't running/i
49066
- },
49067
- {
49068
- code: "unsupported",
49069
- pattern: /password-protected|Locked notes|unsupported|not supported|is blocked|blocked because|has not passed live|refuses to sign|No (?:supported )?background|only one where|Real Notes link unavailable/i
49070
- },
49071
- {
49072
- code: "validation_error",
49073
- pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
49074
- }
49075
- ];
49076
- var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
49077
- var WRITE_ACCEPTED = /\baccepted (?:the|an) \w+, but/i;
49078
- function classifyError(message, cause) {
49079
- if (cause instanceof CodedError) return { ...cause.envelope };
49080
- const text2 = cause instanceof Error && cause.message && !message.includes(cause.message) ? `${message}
49081
- ${cause.message}` : message;
49082
- const timeoutCause = typeof cause === "object" && cause !== null && cause.code === "ETIMEDOUT";
49083
- let code = "operation_failed";
49084
- if (timeoutCause) code = "timeout_indeterminate";
49085
- else if (isPermissionDenied(text2)) code = "permission_denied";
49086
- else code = RULES.find((rule) => rule.pattern.test(text2))?.code ?? "operation_failed";
49087
- const envelope = { code };
49088
- if (code === "timeout_indeterminate" || code === "verification_failed") {
49089
- envelope.indeterminate = true;
49090
- if (WRITE_ACCEPTED.test(text2)) envelope.committed = true;
49091
- } else if (code === "revision_conflict" || NOTHING_WRITTEN.test(text2)) {
49092
- envelope.committed = false;
49093
- envelope.indeterminate = false;
49094
- }
49095
- return envelope;
49096
- }
49097
- function errorResult(message, cause) {
49098
- return {
49099
- content: [{ type: "text", text: message }],
49100
- structuredContent: classifyError(message, cause),
49101
- isError: true
49102
- };
49103
- }
49104
-
49105
49183
  // src/utils/shutdown.ts
49106
49184
  var SHUTDOWN_DRAIN_TIMEOUT_MS = 2e3;
49107
49185
  function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
@@ -51577,7 +51655,9 @@ function registerDirectOperations(server2, manager) {
51577
51655
  {
51578
51656
  title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
51579
51657
  content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
51580
- folder: external_exports.string().max(1e3).optional().describe("Existing folder or nested path; create it first with create-folder"),
51658
+ folder: external_exports.string().max(1e3).optional().describe(
51659
+ "Existing folder or nested path; create it first with create-folder. A smart folder is refused before the note is created"
51660
+ ),
51581
51661
  account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
51582
51662
  ...attachmentInput
51583
51663
  },
@@ -56607,7 +56687,7 @@ function registerTool(name, config2, cb) {
56607
56687
  registerTool(
56608
56688
  "create-note",
56609
56689
  {
56610
- description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field). A 'folder' must already exist \u2014 create-folder first (it is idempotent), since this tool does not create it.",
56690
+ description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field). A 'folder' must already exist \u2014 create-folder first (it is idempotent), since this tool does not create it. A smart folder is refused (code unsupported); it cannot hold notes.",
56611
56691
  inputSchema: {
56612
56692
  title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
56613
56693
  content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).optional().describe(
@@ -58099,7 +58179,7 @@ registerTool(
58099
58179
  registerTool(
58100
58180
  "move-note",
58101
58181
  {
58102
- description: "Use when: moving one exact note to a different folder by id.\nReturns: confirmation and exact-ID readback.\nDo not use when: you only have a title or want to move many notes (batch-move-notes).\nNote: Notes.app's native move preserves the note id, creation date, body, and attachments. The destination folder must already exist. Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds (which also covers the destination) are re-checked inside the move AppleScript.",
58182
+ description: "Use when: moving one exact note to a different folder by id.\nReturns: confirmation and exact-ID readback.\nDo not use when: you only have a title or want to move many notes (batch-move-notes).\nNote: Notes.app's native move preserves the note id, creation date, body, and attachments. The destination folder must already exist and cannot be a smart folder (refused with code unsupported before the move). Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds (which also covers the destination) are re-checked inside the move AppleScript.",
58103
58183
  inputSchema: {
58104
58184
  id: noteIdInput,
58105
58185
  folder: external_exports.string().min(1, "Destination folder is required").max(MAX.FOLDER),
@@ -58341,7 +58421,7 @@ ${lines.join("\n")}${fullyDecoded ? "" : '\n\nSome rules were not recognized; se
58341
58421
  registerTool(
58342
58422
  "create-folder",
58343
58423
  {
58344
- description: "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nDo not use when: creating a note (create-note).",
58424
+ description: "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nNote: a path segment that names a smart folder is refused (code unsupported) before anything is created; smart folders cannot hold folders.\nDo not use when: creating a note (create-note).",
58345
58425
  inputSchema: {
58346
58426
  name: external_exports.string().min(1, "Folder name is required").max(MAX.FOLDER).describe(
58347
58427
  'Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'
@@ -58782,7 +58862,7 @@ registerTool(
58782
58862
  registerTool(
58783
58863
  "batch-move-notes",
58784
58864
  {
58785
- 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).",
58865
+ 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.",
58786
58866
  inputSchema: {
58787
58867
  ids: noteIdArrayInput.describe(
58788
58868
  `Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.12",
3
+ "version": "2.9.13",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",