apple-notes-mcp 2.6.0 → 2.6.2

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 (2) hide show
  1. package/build/index.js +137 -75
  2. package/package.json +4 -2
package/build/index.js CHANGED
@@ -38706,12 +38706,14 @@ function isTimeoutError(error2) {
38706
38706
  }
38707
38707
  return false;
38708
38708
  }
38709
+ var BULK_LIST_MUTATION_ERROR = "Notes changed during listing";
38709
38710
  var RETRYABLE_ERROR_PATTERNS = [
38710
38711
  /timed? out/i,
38711
38712
  /not responding/i,
38712
38713
  /connection.*invalid/i,
38713
38714
  /lost connection/i,
38714
- /busy/i
38715
+ /busy/i,
38716
+ /changed during listing/i
38715
38717
  ];
38716
38718
  function isRetryableError(errorMessage) {
38717
38719
  return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
@@ -38770,6 +38772,13 @@ var ERROR_MAPPINGS = [
38770
38772
  pattern: /password protected|locked note/i,
38771
38773
  message: "Note is password-protected. Unlock it in Notes.app first."
38772
38774
  },
38775
+ // Mid-listing library mutation detected by a bulk-list count guard (#86).
38776
+ // The message must keep the phrase "changed during listing" so
38777
+ // RETRYABLE_ERROR_PATTERNS still matches it after this mapping.
38778
+ {
38779
+ pattern: /changed during listing/i,
38780
+ message: "Notes changed during listing (an iCloud sync may have landed mid-read). The operation is retried automatically; run it again if this persists."
38781
+ },
38773
38782
  // Syntax/script errors (usually programming bugs)
38774
38783
  {
38775
38784
  pattern: /syntax error|expected/i,
@@ -39284,6 +39293,12 @@ function buildAppleScriptDateVar(date3, varName = "thresholdDate") {
39284
39293
  const timeInSeconds = date3.getHours() * 3600 + date3.getMinutes() * 60 + date3.getSeconds();
39285
39294
  return [
39286
39295
  `set ${varName} to current date`,
39296
+ // Reset day to 1 BEFORE assigning month: AppleScript date components roll
39297
+ // over, so setting month to (say) June while the variable still holds the
39298
+ // 31st inherited from `current date` produces July 1 (June 31 doesn't
39299
+ // exist), and the day assignment below then lands in the wrong month.
39300
+ // Day 1 exists in every month, so year/month can never roll over. (#86)
39301
+ `set day of ${varName} to 1`,
39287
39302
  `set year of ${varName} to ${year}`,
39288
39303
  `set month of ${varName} to ${month}`,
39289
39304
  `set day of ${varName} to ${day}`,
@@ -39917,6 +39932,101 @@ var AppleNotesManager = class {
39917
39932
  }
39918
39933
  return true;
39919
39934
  }
39935
+ /**
39936
+ * Builds the AppleScript body for a bulk note listing.
39937
+ *
39938
+ * Names, ids, and (when date-filtering) modification dates are fetched as
39939
+ * whole-list Apple Events instead of two events per note; per-note round
39940
+ * trips scale linearly and push large libraries past client tool timeouts
39941
+ * (#86). The lists are separate snapshots of a live, syncing collection, so
39942
+ * every script guards that they are the same length before zipping them by
39943
+ * index — a mid-listing mutation would otherwise silently mispair names and
39944
+ * ids (grow) or read past the end of a list (shrink). On mismatch the
39945
+ * script raises BULK_LIST_MUTATION_ERROR, which executeAppleScript treats
39946
+ * as retryable, re-running the whole script on a fresh snapshot. A length
39947
+ * check cannot see an exactly-offsetting delete+create landing in the
39948
+ * milliseconds between two fetches; that residual window is accepted —
39949
+ * closing it would cost an extra whole-list fetch per listing.
39950
+ *
39951
+ * @param folderRef - Optional AppleScript folder reference to scope to
39952
+ * @param dateSetup - AppleScript defining thresholdDate; enables date filtering
39953
+ * @param sliceLimit - Fetch only the first N notes (mutually exclusive with
39954
+ * dateSetup: a date filter must scan every note's date). The script then
39955
+ * returns the total note count as a leading record so the caller can
39956
+ * detect a dedup shortfall and fall back to a full fetch.
39957
+ */
39958
+ buildBulkListCommand(opts) {
39959
+ const { folderRef, dateSetup, sliceLimit } = opts;
39960
+ const fullSource = folderRef ? `notes of ${folderRef}` : "notes";
39961
+ const countGuard = (listVar) => `if (count of ${listVar}) is not (count of noteNames) then error "${BULK_LIST_MUTATION_ERROR}"`;
39962
+ if (sliceLimit !== void 0) {
39963
+ const slicedSource = folderRef ? `(notes 1 thru fetchCount of ${folderRef})` : `(notes 1 thru fetchCount)`;
39964
+ return `
39965
+ set totalCount to count of ${fullSource}
39966
+ set fetchCount to ${sliceLimit}
39967
+ if fetchCount > totalCount then set fetchCount to totalCount
39968
+ set resultList to {}
39969
+ if fetchCount > 0 then
39970
+ try
39971
+ set noteNames to name of ${slicedSource}
39972
+ set noteIds to id of ${slicedSource}
39973
+ on error errMsg number errNum
39974
+ if errNum is -1719 or errNum is -1728 then
39975
+ error "${BULK_LIST_MUTATION_ERROR}"
39976
+ else
39977
+ error errMsg number errNum
39978
+ end if
39979
+ end try
39980
+ ${countGuard("noteIds")}
39981
+ repeat with i from 1 to count of noteNames
39982
+ set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)
39983
+ end repeat
39984
+ end if
39985
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
39986
+ return (totalCount as text) & ${AS_RECORD_SEP} & (resultList as text)
39987
+ `;
39988
+ }
39989
+ const dateFetch = dateSetup ? `set noteDates to modification date of ${fullSource}
39990
+ ` : "";
39991
+ const dateCountGuard = dateSetup ? `${countGuard("noteDates")}
39992
+ ` : "";
39993
+ const dateGuardOpen = dateSetup ? `if (item i of noteDates) >= thresholdDate then
39994
+ ` : "";
39995
+ const dateGuardClose = dateSetup ? `
39996
+ end if` : "";
39997
+ return `
39998
+ ${dateSetup ?? ""}set noteNames to name of ${fullSource}
39999
+ set noteIds to id of ${fullSource}
40000
+ ${dateFetch}${countGuard("noteIds")}
40001
+ ${dateCountGuard}set resultList to {}
40002
+ repeat with i from 1 to count of noteNames
40003
+ ${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)${dateGuardClose}
40004
+ end repeat
40005
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40006
+ return resultList as text
40007
+ `;
40008
+ }
40009
+ /**
40010
+ * Parses bulk listing output into deduplicated note titles.
40011
+ *
40012
+ * Duplicate CoreData references are deduped by id; the limit is applied
40013
+ * after dedup so duplicates never count against it.
40014
+ */
40015
+ parseBulkListOutput(output, safeLimit) {
40016
+ if (!output.trim()) return [];
40017
+ const seenIds = /* @__PURE__ */ new Set();
40018
+ const titles = [];
40019
+ for (const item of output.split(RECORD_SEP)) {
40020
+ const [title, id] = item.split(FIELD_SEP);
40021
+ if (!title?.trim()) continue;
40022
+ const noteId = id?.trim() || generateFallbackId();
40023
+ if (seenIds.has(noteId)) continue;
40024
+ seenIds.add(noteId);
40025
+ titles.push(title.trim());
40026
+ if (safeLimit !== void 0 && titles.length >= safeLimit) break;
40027
+ }
40028
+ return titles;
40029
+ }
39920
40030
  /**
39921
40031
  * Lists all notes in an account, optionally filtered by folder, date, and limit.
39922
40032
  *
@@ -39929,89 +40039,41 @@ var AppleNotesManager = class {
39929
40039
  listNotes(account, folder, modifiedSince, limit) {
39930
40040
  const targetAccount = this.resolveAccount(account);
39931
40041
  const safeLimit = limit !== void 0 && limit > 0 ? Math.floor(limit) : void 0;
39932
- if (modifiedSince || safeLimit !== void 0) {
39933
- const baseNotesSource = folder ? `notes of ${buildFolderReference(folder)}` : "notes";
39934
- let dateSetup = "";
39935
- let notesSource = baseNotesSource;
39936
- if (modifiedSince) {
39937
- const date3 = new Date(modifiedSince);
39938
- if (!isNaN(date3.getTime())) {
39939
- dateSetup = buildAppleScriptDateVar(date3) + "\n";
39940
- notesSource = `(${baseNotesSource} whose modification date >= thresholdDate)`;
39941
- }
39942
- }
39943
- const limitCheck = safeLimit !== void 0 ? `
39944
- if (count of resultList) >= ${safeLimit} then exit repeat` : "";
39945
- const listCommand2 = `
39946
- ${dateSetup}set resultList to {}
39947
- set seenIds to {}
39948
- repeat with n in ${notesSource}
39949
- try
39950
- set noteName to name of n
39951
- set noteId to id of n
39952
- if seenIds does not contain noteId then
39953
- set end of seenIds to noteId
39954
- set end of resultList to noteName & ${AS_FIELD_SEP} & noteId${limitCheck}
39955
- end if
39956
- end try
39957
- end repeat
39958
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
39959
- return resultList as text
39960
- `;
39961
- const script2 = buildAccountScopedScript({ account: targetAccount }, listCommand2);
40042
+ const folderRef = folder ? buildFolderReference(folder) : void 0;
40043
+ let dateSetup;
40044
+ if (modifiedSince) {
40045
+ const date3 = new Date(modifiedSince);
40046
+ if (!isNaN(date3.getTime())) {
40047
+ dateSetup = buildAppleScriptDateVar(date3) + "\n";
40048
+ }
40049
+ }
40050
+ if (safeLimit !== void 0 && !dateSetup) {
40051
+ const script2 = buildAccountScopedScript(
40052
+ { account: targetAccount },
40053
+ this.buildBulkListCommand({ folderRef, sliceLimit: safeLimit })
40054
+ );
39962
40055
  const result2 = executeAppleScript(script2);
39963
40056
  if (!result2.success) {
39964
40057
  throw new Error(`Failed to list notes: ${result2.error ?? "unknown error"}`);
39965
40058
  }
39966
- if (!result2.output.trim()) {
39967
- return [];
39968
- }
39969
- const seenIds2 = /* @__PURE__ */ new Set();
39970
- const titles2 = [];
39971
- for (const item of result2.output.split(RECORD_SEP)) {
39972
- const [title, id] = item.split(FIELD_SEP);
39973
- if (!title?.trim()) continue;
39974
- const noteId = id?.trim() || generateFallbackId();
39975
- if (seenIds2.has(noteId)) continue;
39976
- seenIds2.add(noteId);
39977
- titles2.push(title.trim());
40059
+ const sepIdx = result2.output.indexOf(RECORD_SEP);
40060
+ const header = sepIdx === -1 ? result2.output : result2.output.slice(0, sepIdx);
40061
+ const totalCount = Number.parseInt(header.trim(), 10);
40062
+ const records = sepIdx === -1 ? "" : result2.output.slice(sepIdx + 1);
40063
+ const titles = this.parseBulkListOutput(records, safeLimit);
40064
+ if (!Number.isNaN(totalCount) && (titles.length >= safeLimit || totalCount <= safeLimit)) {
40065
+ return titles;
39978
40066
  }
39979
- return titles2;
39980
40067
  }
39981
- const notesRef = folder ? `notes of ${buildFolderReference(folder)}` : `notes`;
39982
- const listCommand = `
39983
- set resultList to {}
39984
- set seenIds to {}
39985
- repeat with n in ${notesRef}
39986
- try
39987
- set noteName to name of n
39988
- set noteId to id of n
39989
- if seenIds does not contain noteId then
39990
- set end of seenIds to noteId
39991
- set end of resultList to noteName & ${AS_FIELD_SEP} & noteId
39992
- end if
39993
- end try
39994
- end repeat
39995
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
39996
- return resultList as text
39997
- `;
39998
- const script = buildAccountScopedScript({ account: targetAccount }, listCommand);
40068
+ const script = buildAccountScopedScript(
40069
+ { account: targetAccount },
40070
+ this.buildBulkListCommand({ folderRef, dateSetup })
40071
+ );
39999
40072
  const result = executeAppleScript(script);
40000
40073
  if (!result.success) {
40001
40074
  throw new Error(`Failed to list notes: ${result.error ?? "unknown error"}`);
40002
40075
  }
40003
- if (!result.output.trim()) return [];
40004
- const seenIds = /* @__PURE__ */ new Set();
40005
- const titles = [];
40006
- for (const item of result.output.split(RECORD_SEP)) {
40007
- const [title, id] = item.split(FIELD_SEP);
40008
- if (!title?.trim()) continue;
40009
- const noteId = id?.trim() || generateFallbackId();
40010
- if (seenIds.has(noteId)) continue;
40011
- seenIds.add(noteId);
40012
- titles.push(title.trim());
40013
- }
40014
- return titles;
40076
+ return this.parseBulkListOutput(result.output, safeLimit);
40015
40077
  }
40016
40078
  /**
40017
40079
  * Lists all shared (collaborative) notes across all accounts.
@@ -41635,7 +41697,7 @@ function detectChecklistAttempt(content) {
41635
41697
  function htmlToText(html) {
41636
41698
  return html.replace(/<[^>]*>/g, " ").replace(/&#x?[0-9a-f]+;/gi, " ").replace(/&[a-z]+;/gi, " ");
41637
41699
  }
41638
- var HASHTAG_RE = /(?<![\p{L}\p{N}_])#([\p{L}\p{N}_]*\p{L}[\p{L}\p{N}_]*)/gu;
41700
+ var HASHTAG_RE = new RegExp("(?<![\\p{L}\\p{N}_])#([\\p{L}\\p{N}_]*\\p{L}[\\p{L}\\p{N}_]*)", "gu");
41639
41701
  function parseHashtags(body) {
41640
41702
  if (!body) return [];
41641
41703
  const text = htmlToText(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
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",
@@ -74,7 +74,8 @@
74
74
  ]
75
75
  },
76
76
  "scripts": {
77
- "build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=build/index.js --banner:js=\"import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);\"",
77
+ "preinstall": "node -e \"const fs=require('fs');const ua=process.env.npm_config_user_agent||'';if(fs.existsSync('.git')&&!ua.startsWith('pnpm')){console.error('\\nThis repo uses pnpm. npm/yarn resolve dependencies off-lockfile and the committed bundle will mismatch CI.\\n\\n corepack enable && pnpm install --frozen-lockfile\\n');process.exit(1)}\"",
78
+ "build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=build/index.js --banner:js=\"import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);\"",
78
79
  "start": "node build/index.js",
79
80
  "dev": "tsc --watch",
80
81
  "test": "vitest run",
@@ -87,6 +88,7 @@
87
88
  "format": "prettier --write src",
88
89
  "format:check": "prettier --check src",
89
90
  "typecheck": "tsc --noEmit",
91
+ "sync:skills": "node scripts/sync-skills.mjs",
90
92
  "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin .agents/plugins codex .hermes-plugin .antigravity-plugin"
91
93
  }
92
94
  }