apple-notes-mcp 2.8.9 → 2.8.10

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 +13 -0
  2. package/build/index.js +576 -487
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,6 +58,19 @@ This method also installs a **skill** that teaches Claude when and how to use Ap
58
58
 
59
59
  On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** to the app that launches the server to enable the database-backed tools (`get-checklist-state`, `get-note-metadata`, `get-note-link`, checklist annotations in `get-note-markdown`, and full `get-sync-status` detail); see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). The rest of the server is pure AppleScript and works without it.
60
60
 
61
+ Native tag, checklist, table, pin, and rich append operations use two packaged
62
+ Apple Shortcuts. Run the explicit setup once:
63
+
64
+ ```bash
65
+ npx -y apple-notes-mcp setup
66
+ ```
67
+
68
+ The command checks existing installations and opens only missing signed
69
+ workflows. Confirm **Add Shortcut** in each macOS window, then verify with
70
+ `npx -y apple-notes-mcp setup --check` or the MCP `doctor` tool. macOS does not
71
+ support silent Shortcut import, so merely connecting an MCP client never opens
72
+ setup windows or bypasses these confirmations.
73
+
61
74
  ### Using the Codex Marketplace
62
75
 
63
76
  The same plugin is available for Codex. Add the marketplace and install the plugin:
package/build/index.js CHANGED
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
2991
2991
  const schOrFunc = root.refs[ref];
2992
2992
  if (schOrFunc)
2993
2993
  return schOrFunc;
2994
- let _sch = resolve2.call(this, root, ref);
2994
+ let _sch = resolve3.call(this, root, ref);
2995
2995
  if (_sch === void 0) {
2996
2996
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2997
2997
  const { schemaId } = this.opts;
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
3018
3018
  function sameSchemaEnv(s1, s2) {
3019
3019
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3020
3020
  }
3021
- function resolve2(root, ref) {
3021
+ function resolve3(root, ref) {
3022
3022
  let sch;
3023
3023
  while (typeof (sch = this.refs[ref]) == "string")
3024
3024
  ref = sch;
@@ -3843,7 +3843,7 @@ var require_fast_uri = __commonJS({
3843
3843
  }
3844
3844
  return uri;
3845
3845
  }
3846
- function resolve2(baseURI, relativeURI, options) {
3846
+ function resolve3(baseURI, relativeURI, options) {
3847
3847
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3848
3848
  const {
3849
3849
  parsed: baseParsed,
@@ -4205,7 +4205,7 @@ var require_fast_uri = __commonJS({
4205
4205
  var fastUri = {
4206
4206
  SCHEMES,
4207
4207
  normalize,
4208
- resolve: resolve2,
4208
+ resolve: resolve3,
4209
4209
  resolveComponent,
4210
4210
  equal,
4211
4211
  serialize,
@@ -36608,7 +36608,7 @@ var Protocol = class {
36608
36608
  return;
36609
36609
  }
36610
36610
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
36611
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
36611
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
36612
36612
  options?.signal?.throwIfAborted();
36613
36613
  }
36614
36614
  } catch (error2) {
@@ -36625,7 +36625,7 @@ var Protocol = class {
36625
36625
  */
36626
36626
  request(request, resultSchema, options) {
36627
36627
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
36628
- return new Promise((resolve2, reject) => {
36628
+ return new Promise((resolve3, reject) => {
36629
36629
  const earlyReject = (error2) => {
36630
36630
  reject(error2);
36631
36631
  };
@@ -36703,7 +36703,7 @@ var Protocol = class {
36703
36703
  if (!parseResult.success) {
36704
36704
  reject(parseResult.error);
36705
36705
  } else {
36706
- resolve2(parseResult.data);
36706
+ resolve3(parseResult.data);
36707
36707
  }
36708
36708
  } catch (error2) {
36709
36709
  reject(error2);
@@ -36964,12 +36964,12 @@ var Protocol = class {
36964
36964
  }
36965
36965
  } catch {
36966
36966
  }
36967
- return new Promise((resolve2, reject) => {
36967
+ return new Promise((resolve3, reject) => {
36968
36968
  if (signal.aborted) {
36969
36969
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36970
36970
  return;
36971
36971
  }
36972
- const timeoutId = setTimeout(resolve2, interval);
36972
+ const timeoutId = setTimeout(resolve3, interval);
36973
36973
  signal.addEventListener("abort", () => {
36974
36974
  clearTimeout(timeoutId);
36975
36975
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -38282,7 +38282,7 @@ var McpServer = class {
38282
38282
  let task = createTaskResult.task;
38283
38283
  const pollInterval = task.pollInterval ?? 5e3;
38284
38284
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
38285
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
38285
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
38286
38286
  const updatedTask = await extra.taskStore.getTask(taskId);
38287
38287
  if (!updatedTask) {
38288
38288
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -38970,12 +38970,12 @@ var StdioServerTransport = class {
38970
38970
  this.onclose?.();
38971
38971
  }
38972
38972
  send(message) {
38973
- return new Promise((resolve2) => {
38973
+ return new Promise((resolve3) => {
38974
38974
  const json2 = serializeMessage(message);
38975
38975
  if (this._stdout.write(json2)) {
38976
- resolve2();
38976
+ resolve3();
38977
38977
  } else {
38978
- this._stdout.once("drain", resolve2);
38978
+ this._stdout.once("drain", resolve3);
38979
38979
  }
38980
38980
  });
38981
38981
  }
@@ -42621,6 +42621,469 @@ function describeSearchScope(searchContent, resultCount) {
42621
42621
 
42622
42622
  // src/tools/doctor.ts
42623
42623
  import { spawnSync } from "child_process";
42624
+
42625
+ // src/services/nativeTags.ts
42626
+ import { execFileSync as execFileSync6 } from "node:child_process";
42627
+ import { mkdtempSync as mkdtempSync2, writeFileSync, rmSync as rmSync2 } from "node:fs";
42628
+ import { tmpdir as tmpdir2 } from "node:os";
42629
+ import { join as join7 } from "node:path";
42630
+ var NATIVE_TAGS_SHORTCUT = "Apple Notes MCP - Native Tags";
42631
+ function normalizeNativeTags(tags) {
42632
+ if (!tags.length || tags.length > 100) throw new Error("Provide between 1 and 100 tags");
42633
+ return [
42634
+ ...new Set(
42635
+ tags.map((value) => {
42636
+ const tag = value.normalize("NFC").replace(/^#/, "");
42637
+ if (tag.length > 100 || !new RegExp("^(?=.*\\p{L})[\\p{L}\\p{N}_-]+$", "u").test(tag))
42638
+ throw new Error(
42639
+ "Tags must contain a letter and only letters, digits, hyphens or underscores"
42640
+ );
42641
+ return tag;
42642
+ })
42643
+ )
42644
+ ];
42645
+ }
42646
+ function addNativeTags(request, deps) {
42647
+ if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(request.id))
42648
+ throw new Error("An exact CoreData note ID is required");
42649
+ if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
42650
+ throw new Error("scopeText must be a stable, single-line project marker of 12\u2013500 characters");
42651
+ const tags = normalizeNativeTags(request.tags);
42652
+ const before = deps.read(request.id);
42653
+ if (before.contentHash !== request.expectedContentHash)
42654
+ throw new Error("Note revision changed; read it again");
42655
+ if (before.title !== request.title || !before.plaintext.includes(request.scopeText))
42656
+ throw new Error("The exact note does not match the title and project marker");
42657
+ const missing = tags.filter((tag) => !before.rich.nativeTags.includes(tag));
42658
+ if (!missing.length)
42659
+ return { nativeTags: before.rich.nativeTags, added: [], contentHash: before.contentHash };
42660
+ const candidates = deps.candidates(request.title, request.scopeText);
42661
+ if (candidates.length !== 1 || candidates[0] !== request.id)
42662
+ throw new Error(
42663
+ "Shortcuts selection is ambiguous or points to a different note; nothing was changed"
42664
+ );
42665
+ if (deps.read(request.id).contentHash !== request.expectedContentHash)
42666
+ throw new Error("Note revision changed during preflight; nothing was changed");
42667
+ let transportWarning;
42668
+ try {
42669
+ deps.run({ title: request.title, scopeText: request.scopeText, tags: missing });
42670
+ } catch (error2) {
42671
+ transportWarning = error2 instanceof Error ? error2.message : "Shortcuts completion was uncertain";
42672
+ }
42673
+ const after = deps.read(request.id);
42674
+ const allTags = [.../* @__PURE__ */ new Set([...before.rich.nativeTags, ...tags])];
42675
+ const textWithoutTags = (text) => {
42676
+ for (const tag of [...allTags].sort((a, b) => b.length - a.length)) {
42677
+ const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42678
+ text = text.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "");
42679
+ }
42680
+ return text.replace(/[\s\ufffc]/gu, "");
42681
+ };
42682
+ if (after.title !== before.title || allTags.some((tag) => !after.rich.nativeTags.includes(tag)) || textWithoutTags(before.rich.text) !== textWithoutTags(after.rich.text) || linkSignature(before.rich.links) !== linkSignature(after.rich.links) || before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)) || before.rich.hasChecklist !== after.rich.hasChecklist) {
42683
+ throw new Error(
42684
+ "Shortcuts ran, but exact-ID tags/text/links readback was not verified. Read the note before any retry"
42685
+ );
42686
+ }
42687
+ return {
42688
+ nativeTags: after.rich.nativeTags,
42689
+ added: missing,
42690
+ contentHash: after.contentHash,
42691
+ ...transportWarning ? {
42692
+ transportWarning: "Shortcuts completion was uncertain, but all requested native tags and preserved content were verified by exact ID."
42693
+ } : {}
42694
+ };
42695
+ }
42696
+ function nativeTagsStatus(shortcut = process.env.APPLE_NOTES_MCP_TAGS_SHORTCUT || NATIVE_TAGS_SHORTCUT) {
42697
+ const lines = execFileSync6("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
42698
+ encoding: "utf8",
42699
+ timeout: 15e3,
42700
+ maxBuffer: 1024 * 1024,
42701
+ stdio: ["ignore", "pipe", "pipe"]
42702
+ }).split(/\r?\n/u);
42703
+ const matches = lines.flatMap((line) => {
42704
+ const match = /^(.*) \(([0-9A-Fa-f-]{36})\)$/.exec(line);
42705
+ return match && (match[1] === shortcut || match[2].toLowerCase() === shortcut.toLowerCase()) ? [{ name: match[1], identifier: match[2] }] : [];
42706
+ });
42707
+ return {
42708
+ shortcut,
42709
+ installed: matches.length === 1,
42710
+ identifier: matches.length === 1 ? matches[0].identifier : void 0
42711
+ };
42712
+ }
42713
+ function runNativeTagsShortcut(input) {
42714
+ const status = nativeTagsStatus();
42715
+ if (!status.installed)
42716
+ throw new Error(`Import the supplied ${status.shortcut}.shortcut in Shortcuts first`);
42717
+ const directory = mkdtempSync2(join7(tmpdir2(), "apple-notes-native-tags-"));
42718
+ try {
42719
+ const path4 = join7(directory, "request.json");
42720
+ writeFileSync(path4, JSON.stringify(input), { mode: 384 });
42721
+ execFileSync6("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path4], {
42722
+ encoding: "utf8",
42723
+ timeout: 6e4,
42724
+ maxBuffer: 1024 * 1024,
42725
+ stdio: ["ignore", "pipe", "pipe"]
42726
+ });
42727
+ } catch {
42728
+ throw new Error(
42729
+ "Native tag operation did not complete cleanly (possibly waiting for macOS permission). Do not retry automatically; read the exact note and check Shortcuts"
42730
+ );
42731
+ } finally {
42732
+ rmSync2(directory, { recursive: true, force: true });
42733
+ }
42734
+ }
42735
+
42736
+ // src/services/backgroundNotes.ts
42737
+ import { execFileSync as execFileSync7 } from "node:child_process";
42738
+ import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync2, rmSync as rmSync3 } from "node:fs";
42739
+ import { tmpdir as tmpdir3 } from "node:os";
42740
+ import { join as join8 } from "node:path";
42741
+
42742
+ // src/utils/appendMarkdown.ts
42743
+ var escape2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
42744
+ function appendMarkdownHtml(markdown) {
42745
+ if (markdown.includes("\uE000") || markdown.includes("\uE001"))
42746
+ throw new Error("Unsupported reserved characters");
42747
+ if (Array.from(markdown).some((c) => c.charCodeAt(0) < 32 && !["\n", "\r", " "].includes(c)))
42748
+ throw new Error("Unsupported control characters");
42749
+ if (/[`~|]|^#{4,}\s|^\s*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(markdown))
42750
+ throw new Error(
42751
+ "Markdown append supports paragraphs, headings, flat lists, emphasis and inline links; use semantic HTML for other formatting"
42752
+ );
42753
+ const inline = (text) => {
42754
+ const links = [];
42755
+ let value = text.replace(/\[([^\]\n]+)\]\(([^()\s]+)\)/g, (_s, label, url) => {
42756
+ if (!/^(https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url))
42757
+ throw new Error("Unsupported Markdown link URL");
42758
+ links.push(`<a href="${escape2(url)}">${escape2(label)}</a>`);
42759
+ return `\uE000${links.length - 1}\uE001`;
42760
+ });
42761
+ value = escape2(value).replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/\*([^*\n]+)\*/g, "<i>$1</i>");
42762
+ if (value.includes("[") || value.includes("]") || value.includes("*"))
42763
+ throw new Error("Unsupported or unbalanced Markdown inline syntax");
42764
+ return value.replace(/\uE000(\d+)\uE001/g, (_s, i) => links[Number(i)]);
42765
+ };
42766
+ let html = "", list;
42767
+ const close = () => {
42768
+ if (list) {
42769
+ html += `</${list}>`;
42770
+ list = void 0;
42771
+ }
42772
+ };
42773
+ for (const line of markdown.replace(/\r\n/g, "\n").split("\n")) {
42774
+ if (/^\s{2,}\S/.test(line)) throw new Error("Nested lists and indented code are unsupported");
42775
+ const heading = /^(#{1,3})\s+(.+)$/.exec(line), item = /^(?:([-+*])|\d+\.)\s+(.+)$/.exec(line);
42776
+ if (item) {
42777
+ const kind = item[1] ? "ul" : "ol";
42778
+ if (list !== kind) {
42779
+ close();
42780
+ list = kind;
42781
+ html += `<${kind}>`;
42782
+ }
42783
+ html += `<li>${inline(item[2])}</li>`;
42784
+ continue;
42785
+ }
42786
+ close();
42787
+ html += heading ? `<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>` : line.trim() ? `<div>${inline(line)}</div>` : "<div><br></div>";
42788
+ }
42789
+ close();
42790
+ return html;
42791
+ }
42792
+
42793
+ // src/utils/noteRevision.ts
42794
+ var INLINE_TAG = /^<\/?(?:b|i|u|s|strike|em|strong|span|a|font|sub|sup|code|tt|small|big|mark)\b/i;
42795
+ function comparableVisibleText(html) {
42796
+ return html.replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, (tag) => INLINE_TAG.test(tag) ? "" : " ").replace(/&nbsp;|&#160;/gi, " ").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&").replace(/&#(\d+);/g, (_match, codePoint) => String.fromCodePoint(Number(codePoint))).replace(
42797
+ /&#x([0-9a-f]+);/gi,
42798
+ (_match, codePoint) => String.fromCodePoint(Number.parseInt(codePoint, 16))
42799
+ ).replace(/\s+/g, " ").trim();
42800
+ }
42801
+
42802
+ // src/services/backgroundNotes.ts
42803
+ var BACKGROUND_SHORTCUT = "Apple Notes MCP - Background Operations v5";
42804
+ var backgroundStatus = () => nativeTagsStatus(process.env.APPLE_NOTES_MCP_BACKGROUND_SHORTCUT || BACKGROUND_SHORTCUT);
42805
+ var nativeTagBridgeStatus = () => nativeTagsStatus();
42806
+ function readBackgroundSnapshot(manager, id2) {
42807
+ if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(id2))
42808
+ throw new Error("Exact note ID required");
42809
+ const note = manager.getNoteById(id2);
42810
+ if (!note) throw new Error("Note not found");
42811
+ if (note.passwordProtected) throw new Error("Locked notes are unavailable in background mode");
42812
+ const html = manager.getNoteContentById(id2);
42813
+ const enriched = enrichNoteRead(id2, html);
42814
+ const rich = readRichNote(id2);
42815
+ if (enriched.revision !== rich.revision)
42816
+ throw new Error("Note changed during read; read it again");
42817
+ return {
42818
+ id: id2,
42819
+ title: note.title,
42820
+ hash: richContentHash(html, enriched),
42821
+ html,
42822
+ rich,
42823
+ pinned: getNoteMetadata(id2).metadata?.pinned,
42824
+ checklist: getChecklistItems(id2).items || []
42825
+ };
42826
+ }
42827
+ function validateAppendContent(content, format) {
42828
+ if (!content || content.length > 1024 * 1024 || content.includes("\0"))
42829
+ throw new Error("Invalid append content (limit 1 MiB)");
42830
+ if (format === "html") {
42831
+ const allowed = /* @__PURE__ */ new Set([
42832
+ "div",
42833
+ "p",
42834
+ "br",
42835
+ "b",
42836
+ "strong",
42837
+ "i",
42838
+ "em",
42839
+ "u",
42840
+ "s",
42841
+ "del",
42842
+ "h1",
42843
+ "h2",
42844
+ "h3",
42845
+ "ul",
42846
+ "ol",
42847
+ "li",
42848
+ "a",
42849
+ "table",
42850
+ "thead",
42851
+ "tbody",
42852
+ "tr",
42853
+ "td",
42854
+ "th"
42855
+ ]);
42856
+ for (const tag of content.matchAll(/<\/?\s*([a-z][a-z0-9]*)\b([^>]*)>/gi)) {
42857
+ if (!allowed.has(tag[1].toLowerCase()))
42858
+ throw new Error(`Unsupported HTML element: ${tag[1]}`);
42859
+ let attrs = tag[2].replace(/\/$/, "").trim();
42860
+ if (tag[1].toLowerCase() === "a")
42861
+ attrs = attrs.replace(/\bhref\s*=\s*(["'])(.*?)\1/gi, (_s, _q, url) => {
42862
+ if (!/^(https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url) || Array.from(url).some((c) => c.charCodeAt(0) < 33))
42863
+ throw new Error("Unsupported link URL");
42864
+ return "";
42865
+ });
42866
+ if (attrs.trim())
42867
+ throw new Error(
42868
+ "Unsupported HTML attributes; use semantic formatting and explicit blank paragraphs"
42869
+ );
42870
+ }
42871
+ if (/<!--|<!|<\?|<[^>]*$/u.test(content)) throw new Error("Unsupported HTML markup");
42872
+ }
42873
+ if (format === "markdown" && /!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
42874
+ throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
42875
+ }
42876
+ function runBackgroundShortcut(input) {
42877
+ const status = backgroundStatus();
42878
+ if (!status.installed)
42879
+ throw new Error("Install the supplied Background Operations shortcut once");
42880
+ const directory = mkdtempSync3(join8(tmpdir3(), "apple-notes-background-"));
42881
+ try {
42882
+ const file = join8(directory, "request.json");
42883
+ writeFileSync2(
42884
+ file,
42885
+ JSON.stringify({
42886
+ operation: "",
42887
+ title: "",
42888
+ scopeText: "",
42889
+ text: "",
42890
+ objectId: "",
42891
+ objectName: "",
42892
+ change: "",
42893
+ otherTitle: "",
42894
+ otherScope: "",
42895
+ tag: "",
42896
+ size: "",
42897
+ ...input
42898
+ }),
42899
+ { mode: 384 }
42900
+ );
42901
+ execFileSync7("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
42902
+ encoding: "utf8",
42903
+ timeout: 6e4,
42904
+ maxBuffer: 1024 * 1024,
42905
+ stdio: ["ignore", "pipe", "pipe"]
42906
+ });
42907
+ } finally {
42908
+ rmSync3(directory, { recursive: true, force: true });
42909
+ }
42910
+ }
42911
+ function backgroundDependencies(manager) {
42912
+ return {
42913
+ read: (id2) => readBackgroundSnapshot(manager, id2),
42914
+ run: runBackgroundShortcut,
42915
+ candidates: (title, scope) => manager.listAccounts().flatMap(
42916
+ (account) => manager.searchNotes(title, false, account.name).filter(
42917
+ (note) => note.title === title && !note.passwordProtected && manager.getNotePlaintextById(note.id).includes(scope)
42918
+ ).map((note) => note.id)
42919
+ )
42920
+ };
42921
+ }
42922
+ function assertPreserved(before, after, options = {}) {
42923
+ if (before.id !== after.id || before.title !== after.title)
42924
+ throw new Error("Note identity changed");
42925
+ const tidy = (s) => s.replace(/\r\n/g, "\n").replace(/[\s\ufffc]+$/gu, "");
42926
+ let oldText = tidy(before.rich.text), newText = tidy(after.rich.text);
42927
+ if (options.tagChange) {
42928
+ const escaped = options.tagChange.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42929
+ const strip = (s) => s.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "").replace(/[\s\ufffc]/gu, "");
42930
+ oldText = strip(oldText);
42931
+ newText = strip(newText);
42932
+ }
42933
+ if (options.append ? !newText.startsWith(oldText) : newText !== oldText)
42934
+ throw new Error("Existing note text was not preserved");
42935
+ const links = options.append ? after.rich.links.slice(0, before.rich.links.length) : after.rich.links;
42936
+ if (linkSignature(before.rich.links) !== linkSignature(links))
42937
+ throw new Error("Existing links were not preserved");
42938
+ if (!options.tagChange && before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)))
42939
+ throw new Error("Existing native object was lost");
42940
+ if (JSON.stringify(before.checklist) !== JSON.stringify(
42941
+ options.append ? after.checklist.slice(0, before.checklist.length) : after.checklist
42942
+ ))
42943
+ throw new Error("Existing checklist items changed");
42944
+ for (const item of before.rich.checklistItems || []) {
42945
+ const actual = after.rich.checklistItems?.find((current) => current.id === item.id);
42946
+ if (!actual || actual.text !== item.text || actual.done !== item.done)
42947
+ throw new Error("Existing checklist item identity or state changed");
42948
+ }
42949
+ if (before.rich.nativeTags.filter((t) => t !== options.tagChange).some((t) => !after.rich.nativeTags.includes(t)))
42950
+ throw new Error("Existing native tag was lost");
42951
+ const allowedRemoved = options.tagChange ? before.rich.nativeTagObjectIds?.[options.tagChange] || [] : [];
42952
+ for (const object3 of before.rich.objectData || []) {
42953
+ if (allowedRemoved.includes(object3.id)) continue;
42954
+ const actual = after.rich.objectData?.find((o) => o.id === object3.id);
42955
+ if (!actual || actual.mergeable !== object3.mergeable || actual.view !== object3.view)
42956
+ throw new Error("Existing native object content or presentation changed");
42957
+ }
42958
+ if (!options.tagChange && before.rich.styleRuns && after.rich.styleRuns) {
42959
+ const end = before.rich.text.trimEnd().length;
42960
+ for (const oldRun of before.rich.styleRuns)
42961
+ for (const newRun of after.rich.styleRuns) {
42962
+ const overlapStart = Math.max(oldRun.start, newRun.start);
42963
+ const overlapEnd = Math.min(
42964
+ end,
42965
+ oldRun.start + oldRun.length,
42966
+ newRun.start + newRun.length
42967
+ );
42968
+ if (overlapStart < overlapEnd && /[^\s\ufffc]/u.test(before.rich.text.slice(overlapStart, overlapEnd)) && oldRun.signature !== newRun.signature)
42969
+ throw new Error("Existing rich formatting changed");
42970
+ }
42971
+ }
42972
+ }
42973
+ function mutateBackground(request, operation, data, verify, deps) {
42974
+ if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
42975
+ throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
42976
+ const before = deps.read(request.id);
42977
+ if (before.hash !== request.expectedContentHash)
42978
+ throw new Error("Note revision changed; read it again");
42979
+ if (!before.rich.text.includes(request.scopeText))
42980
+ throw new Error("Scope is absent from exact note");
42981
+ const candidates = deps.candidates(before.title, request.scopeText);
42982
+ if (candidates.length !== 1 || candidates[0] !== request.id)
42983
+ throw new Error("Ambiguous note selection; nothing changed");
42984
+ if (deps.read(request.id).hash !== before.hash)
42985
+ throw new Error("Note revision changed during preflight");
42986
+ let transportUncertain = false;
42987
+ let transportMessage = "";
42988
+ try {
42989
+ deps.run({ ...data, operation, title: before.title, scopeText: request.scopeText });
42990
+ } catch (error2) {
42991
+ transportUncertain = true;
42992
+ const detail = error2;
42993
+ transportMessage = detail?.code === "ETIMEDOUT" ? "Shortcuts timed out; check for an interactive parameter or permission request" : String(detail?.stderr || detail?.message || "Shortcuts failed").trim().slice(0, 500);
42994
+ }
42995
+ const after = deps.read(request.id);
42996
+ try {
42997
+ verify(before, after);
42998
+ } catch (error2) {
42999
+ throw new Error(
43000
+ `Operation outcome uncertain; read exact note before any retry: ${error2 instanceof Error ? error2.message : "readback failed"}${transportMessage ? "; " + transportMessage : ""}`
43001
+ );
43002
+ }
43003
+ return {
43004
+ ok: true,
43005
+ id: request.id,
43006
+ previousContentHash: before.hash,
43007
+ contentHash: after.hash,
43008
+ ...transportUncertain ? {
43009
+ transportWarning: "Transport was uncertain; exact-ID readback verified the requested result"
43010
+ } : {}
43011
+ };
43012
+ }
43013
+ function assertAppendedHtmlLinks(previousCount, links, html) {
43014
+ const remaining = links.slice(previousCount);
43015
+ for (const expected of htmlLinks(html)) {
43016
+ const index = remaining.findIndex(
43017
+ (link) => linkSignature([link]) === linkSignature([expected])
43018
+ );
43019
+ if (index < 0) throw new Error("Appended HTML link not verified");
43020
+ remaining.splice(index, 1);
43021
+ }
43022
+ }
43023
+ function assertAppendedVisibleText(beforeHtml, afterHtml, expected) {
43024
+ const before = comparableVisibleText(beforeHtml);
43025
+ const after = comparableVisibleText(afterHtml);
43026
+ if (!after.startsWith(before)) throw new Error("Appended text not verified");
43027
+ const suffix = after.slice(before.length).replace(/\s+/gu, " ").trim();
43028
+ const wanted = expected.replace(/\s+/gu, " ").trim();
43029
+ if (!suffix || !suffix.includes(wanted)) throw new Error("Appended text not verified");
43030
+ }
43031
+ function appendNative(manager, request) {
43032
+ validateAppendContent(request.content, request.format);
43033
+ if (request.format === "markdown")
43034
+ return appendNative(manager, {
43035
+ ...request,
43036
+ content: appendMarkdownHtml(request.content),
43037
+ format: "html"
43038
+ });
43039
+ if (request.format === "html" && /<table\b/i.test(request.content))
43040
+ throw new Error("Use create-table for verified native table insertion");
43041
+ const op = request.format === "plaintext" ? "append-text" : request.format === "html" ? "append-html" : "append-markdown";
43042
+ const text = request.format === "html" ? "<div><br></div>" + request.content : "\n\n" + request.content;
43043
+ return mutateBackground(
43044
+ request,
43045
+ op,
43046
+ { text },
43047
+ (before, after) => {
43048
+ assertPreserved(before, after, { append: true });
43049
+ const expected = request.format === "html" ? comparableVisibleText(request.content) : request.format === "plaintext" ? request.content : null;
43050
+ if (expected) assertAppendedVisibleText(before.html, after.html, expected);
43051
+ if (request.format === "html")
43052
+ assertAppendedHtmlLinks(before.rich.links.length, after.rich.links, request.content);
43053
+ },
43054
+ backgroundDependencies(manager)
43055
+ );
43056
+ }
43057
+ function setNativeTag(manager, request) {
43058
+ const tag = normalizeNativeTags([request.tag])[0];
43059
+ const initial = readBackgroundSnapshot(manager, request.id);
43060
+ if (initial.hash !== request.expectedContentHash)
43061
+ throw new Error("Note revision changed; read it again");
43062
+ if (initial.rich.nativeTags.includes(tag) === request.present)
43063
+ return { ok: true, id: request.id, contentHash: initial.hash, changed: false };
43064
+ const deps = backgroundDependencies(manager);
43065
+ if (request.present)
43066
+ deps.run = ({ title, scopeText }) => runNativeTagsShortcut({ title, scopeText, tags: [tag] });
43067
+ return mutateBackground(
43068
+ request,
43069
+ request.present ? "add-tag" : "remove-tag",
43070
+ { tag },
43071
+ (before, after) => {
43072
+ assertPreserved(before, after, { tagChange: tag });
43073
+ if (after.rich.nativeTags.includes(tag) !== request.present)
43074
+ throw new Error("Native tag state not verified");
43075
+ const removed = before.rich.nativeObjectIds.filter(
43076
+ (id2) => !after.rich.nativeObjectIds.includes(id2)
43077
+ );
43078
+ const allowed = request.present ? [] : before.rich.nativeTagObjectIds?.[tag] || [];
43079
+ if (removed.some((id2) => !allowed.includes(id2)))
43080
+ throw new Error("Unrelated native object was lost");
43081
+ },
43082
+ deps
43083
+ );
43084
+ }
43085
+
43086
+ // src/tools/doctor.ts
42624
43087
  function runDoctor(manager) {
42625
43088
  const checks = [];
42626
43089
  const hc = manager.healthCheck();
@@ -42651,6 +43114,23 @@ function runDoctor(manager) {
42651
43114
  status: fda ? "ok" : "warn",
42652
43115
  detail: fda ? "granted \u2014 the Notes database is readable (checklist state, note metadata, note links, sync detail)" : `not granted \u2014 get-checklist-state, get-note-metadata, and the checklist annotations in get-note-markdown won't work; get-note-link fails on macOS 26+ (macOS 12-15 falls back to AppleScript); get-sync-status still answers but cannot see pending uploads. Everything else is pure AppleScript and is unaffected. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it and re-run doctor. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`
42653
43116
  });
43117
+ try {
43118
+ const bridgeStatuses = [NATIVE_TAGS_SHORTCUT, BACKGROUND_SHORTCUT].map(
43119
+ (name) => nativeTagsStatus(name)
43120
+ );
43121
+ const missing = bridgeStatuses.filter((status) => !status.installed);
43122
+ checks.push({
43123
+ name: "Native write Shortcuts",
43124
+ status: missing.length ? "warn" : "ok",
43125
+ detail: missing.length ? `missing: ${missing.map((status) => status.shortcut).join(", ")}. Run apple-notes-mcp setup and approve Add Shortcut in macOS` : "both native-write bridges are installed"
43126
+ });
43127
+ } catch (error2) {
43128
+ checks.push({
43129
+ name: "Native write Shortcuts",
43130
+ status: "warn",
43131
+ detail: `could not inspect Shortcuts: ${String(error2)}. Run apple-notes-mcp setup --check`
43132
+ });
43133
+ }
42654
43134
  checks.push(checkNodeRuntimeSignature());
42655
43135
  const healthy = !checks.some((c) => c.status === "fail");
42656
43136
  return { healthy, checks };
@@ -42694,12 +43174,12 @@ function formatDoctorReport(r) {
42694
43174
 
42695
43175
  // src/services/fileConfig.ts
42696
43176
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
42697
- import { join as join7 } from "path";
43177
+ import { join as join9 } from "path";
42698
43178
  import { homedir as homedir7 } from "os";
42699
43179
  function fileConfigPath(env = process.env) {
42700
43180
  const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
42701
43181
  if (override && override.trim()) return override.trim();
42702
- return join7(homedir7(), "Library", "Application Support", "apple-notes-mcp", "config.json");
43182
+ return join9(homedir7(), "Library", "Application Support", "apple-notes-mcp", "config.json");
42703
43183
  }
42704
43184
  function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
42705
43185
  const applied = [];
@@ -42906,15 +43386,6 @@ function withJsonSchema2020_12(transport2) {
42906
43386
  return transport2;
42907
43387
  }
42908
43388
 
42909
- // src/utils/noteRevision.ts
42910
- var INLINE_TAG = /^<\/?(?:b|i|u|s|strike|em|strong|span|a|font|sub|sup|code|tt|small|big|mark)\b/i;
42911
- function comparableVisibleText(html) {
42912
- return html.replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, (tag) => INLINE_TAG.test(tag) ? "" : " ").replace(/&nbsp;|&#160;/gi, " ").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&").replace(/&#(\d+);/g, (_match, codePoint) => String.fromCodePoint(Number(codePoint))).replace(
42913
- /&#x([0-9a-f]+);/gi,
42914
- (_match, codePoint) => String.fromCodePoint(Number.parseInt(codePoint, 16))
42915
- ).replace(/\s+/g, " ").trim();
42916
- }
42917
-
42918
43389
  // src/utils/noteTables.ts
42919
43390
  import { gunzipSync as gunzipSync3 } from "node:zlib";
42920
43391
  var sub = (f, n) => {
@@ -43018,14 +43489,14 @@ import {
43018
43489
  closeSync,
43019
43490
  constants,
43020
43491
  fstatSync,
43021
- mkdtempSync as mkdtempSync2,
43492
+ mkdtempSync as mkdtempSync4,
43022
43493
  openSync,
43023
43494
  readFileSync as readFileSync3,
43024
- rmSync as rmSync2,
43025
- writeFileSync
43495
+ rmSync as rmSync4,
43496
+ writeFileSync as writeFileSync3
43026
43497
  } from "node:fs";
43027
- import { tmpdir as tmpdir2 } from "node:os";
43028
- import { basename, isAbsolute as isAbsolute2, join as join8 } from "node:path";
43498
+ import { tmpdir as tmpdir4 } from "node:os";
43499
+ import { basename, isAbsolute as isAbsolute2, join as join10 } from "node:path";
43029
43500
  var noteId = external_exports.string().regex(/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i);
43030
43501
  var revision = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
43031
43502
  function readSnapshot(manager, id2) {
@@ -43139,10 +43610,10 @@ function registerDirectOperations(server2, manager) {
43139
43610
  if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
43140
43611
  const bytes = localAttachment(path4);
43141
43612
  const beforeAttachments = manager.listAttachmentsById(id2);
43142
- const directory = mkdtempSync2(join8(tmpdir2(), "notes-attachment-add-"));
43143
- const temporaryFile = join8(directory, basename(path4));
43613
+ const directory = mkdtempSync4(join10(tmpdir4(), "notes-attachment-add-"));
43614
+ const temporaryFile = join10(directory, basename(path4));
43144
43615
  try {
43145
- writeFileSync(temporaryFile, bytes, { mode: 384 });
43616
+ writeFileSync3(temporaryFile, bytes, { mode: 384 });
43146
43617
  if (readSnapshot(manager, id2).hash !== before.hash)
43147
43618
  throw new Error("Note revision changed");
43148
43619
  let returnedId;
@@ -43185,123 +43656,12 @@ function registerDirectOperations(server2, manager) {
43185
43656
  } : {}
43186
43657
  };
43187
43658
  } finally {
43188
- rmSync2(directory, { recursive: true, force: true });
43659
+ rmSync4(directory, { recursive: true, force: true });
43189
43660
  }
43190
43661
  }
43191
43662
  );
43192
43663
  }
43193
43664
 
43194
- // src/services/nativeTags.ts
43195
- import { execFileSync as execFileSync6 } from "node:child_process";
43196
- import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync2, rmSync as rmSync3 } from "node:fs";
43197
- import { tmpdir as tmpdir3 } from "node:os";
43198
- import { join as join9 } from "node:path";
43199
- var NATIVE_TAGS_SHORTCUT = "Apple Notes MCP - Native Tags";
43200
- function normalizeNativeTags(tags) {
43201
- if (!tags.length || tags.length > 100) throw new Error("Provide between 1 and 100 tags");
43202
- return [
43203
- ...new Set(
43204
- tags.map((value) => {
43205
- const tag = value.normalize("NFC").replace(/^#/, "");
43206
- if (tag.length > 100 || !new RegExp("^(?=.*\\p{L})[\\p{L}\\p{N}_-]+$", "u").test(tag))
43207
- throw new Error(
43208
- "Tags must contain a letter and only letters, digits, hyphens or underscores"
43209
- );
43210
- return tag;
43211
- })
43212
- )
43213
- ];
43214
- }
43215
- function addNativeTags(request, deps) {
43216
- if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(request.id))
43217
- throw new Error("An exact CoreData note ID is required");
43218
- if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
43219
- throw new Error("scopeText must be a stable, single-line project marker of 12\u2013500 characters");
43220
- const tags = normalizeNativeTags(request.tags);
43221
- const before = deps.read(request.id);
43222
- if (before.contentHash !== request.expectedContentHash)
43223
- throw new Error("Note revision changed; read it again");
43224
- if (before.title !== request.title || !before.plaintext.includes(request.scopeText))
43225
- throw new Error("The exact note does not match the title and project marker");
43226
- const missing = tags.filter((tag) => !before.rich.nativeTags.includes(tag));
43227
- if (!missing.length)
43228
- return { nativeTags: before.rich.nativeTags, added: [], contentHash: before.contentHash };
43229
- const candidates = deps.candidates(request.title, request.scopeText);
43230
- if (candidates.length !== 1 || candidates[0] !== request.id)
43231
- throw new Error(
43232
- "Shortcuts selection is ambiguous or points to a different note; nothing was changed"
43233
- );
43234
- if (deps.read(request.id).contentHash !== request.expectedContentHash)
43235
- throw new Error("Note revision changed during preflight; nothing was changed");
43236
- let transportWarning;
43237
- try {
43238
- deps.run({ title: request.title, scopeText: request.scopeText, tags: missing });
43239
- } catch (error2) {
43240
- transportWarning = error2 instanceof Error ? error2.message : "Shortcuts completion was uncertain";
43241
- }
43242
- const after = deps.read(request.id);
43243
- const allTags = [.../* @__PURE__ */ new Set([...before.rich.nativeTags, ...tags])];
43244
- const textWithoutTags = (text) => {
43245
- for (const tag of [...allTags].sort((a, b) => b.length - a.length)) {
43246
- const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43247
- text = text.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "");
43248
- }
43249
- return text.replace(/[\s\ufffc]/gu, "");
43250
- };
43251
- if (after.title !== before.title || allTags.some((tag) => !after.rich.nativeTags.includes(tag)) || textWithoutTags(before.rich.text) !== textWithoutTags(after.rich.text) || linkSignature(before.rich.links) !== linkSignature(after.rich.links) || before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)) || before.rich.hasChecklist !== after.rich.hasChecklist) {
43252
- throw new Error(
43253
- "Shortcuts ran, but exact-ID tags/text/links readback was not verified. Read the note before any retry"
43254
- );
43255
- }
43256
- return {
43257
- nativeTags: after.rich.nativeTags,
43258
- added: missing,
43259
- contentHash: after.contentHash,
43260
- ...transportWarning ? {
43261
- transportWarning: "Shortcuts completion was uncertain, but all requested native tags and preserved content were verified by exact ID."
43262
- } : {}
43263
- };
43264
- }
43265
- function nativeTagsStatus(shortcut = process.env.APPLE_NOTES_MCP_TAGS_SHORTCUT || NATIVE_TAGS_SHORTCUT) {
43266
- const lines = execFileSync6("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
43267
- encoding: "utf8",
43268
- timeout: 15e3,
43269
- maxBuffer: 1024 * 1024,
43270
- stdio: ["ignore", "pipe", "pipe"]
43271
- }).split(/\r?\n/u);
43272
- const matches = lines.flatMap((line) => {
43273
- const match = /^(.*) \(([0-9A-Fa-f-]{36})\)$/.exec(line);
43274
- return match && (match[1] === shortcut || match[2].toLowerCase() === shortcut.toLowerCase()) ? [{ name: match[1], identifier: match[2] }] : [];
43275
- });
43276
- return {
43277
- shortcut,
43278
- installed: matches.length === 1,
43279
- identifier: matches.length === 1 ? matches[0].identifier : void 0
43280
- };
43281
- }
43282
- function runNativeTagsShortcut(input) {
43283
- const status = nativeTagsStatus();
43284
- if (!status.installed)
43285
- throw new Error(`Import the supplied ${status.shortcut}.shortcut in Shortcuts first`);
43286
- const directory = mkdtempSync3(join9(tmpdir3(), "apple-notes-native-tags-"));
43287
- try {
43288
- const path4 = join9(directory, "request.json");
43289
- writeFileSync2(path4, JSON.stringify(input), { mode: 384 });
43290
- execFileSync6("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path4], {
43291
- encoding: "utf8",
43292
- timeout: 6e4,
43293
- maxBuffer: 1024 * 1024,
43294
- stdio: ["ignore", "pipe", "pipe"]
43295
- });
43296
- } catch {
43297
- throw new Error(
43298
- "Native tag operation did not complete cleanly (possibly waiting for macOS permission). Do not retry automatically; read the exact note and check Shortcuts"
43299
- );
43300
- } finally {
43301
- rmSync3(directory, { recursive: true, force: true });
43302
- }
43303
- }
43304
-
43305
43665
  // src/tools/nativeTagsBridge.ts
43306
43666
  var noteId2 = external_exports.string().regex(/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i);
43307
43667
  var revision2 = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
@@ -43356,7 +43716,10 @@ function registerNativeTagsBridge(server2, manager) {
43356
43716
  "native-tags-status",
43357
43717
  "Use when: checking whether the Native Tags Shortcut is installed before a tag write.\nReturns: the configured Shortcut name, unique installed identifier, and installed state.\nDo not use when: listing tags on notes (list-native-tags).\nSafety: read-only; installation does not by itself prove Notes permission or a successful live mutation.",
43358
43718
  {},
43359
- () => nativeTagsStatus()
43719
+ () => {
43720
+ const status = nativeTagsStatus();
43721
+ return status.installed ? status : { ...status, setupCommand: "apple-notes-mcp setup" };
43722
+ }
43360
43723
  );
43361
43724
  tool(
43362
43725
  "add-native-tags",
@@ -43386,347 +43749,6 @@ function registerNativeTagsBridge(server2, manager) {
43386
43749
  );
43387
43750
  }
43388
43751
 
43389
- // src/services/backgroundNotes.ts
43390
- import { execFileSync as execFileSync7 } from "node:child_process";
43391
- import { mkdtempSync as mkdtempSync4, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "node:fs";
43392
- import { tmpdir as tmpdir4 } from "node:os";
43393
- import { join as join10 } from "node:path";
43394
-
43395
- // src/utils/appendMarkdown.ts
43396
- var escape2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
43397
- function appendMarkdownHtml(markdown) {
43398
- if (markdown.includes("\uE000") || markdown.includes("\uE001"))
43399
- throw new Error("Unsupported reserved characters");
43400
- if (Array.from(markdown).some((c) => c.charCodeAt(0) < 32 && !["\n", "\r", " "].includes(c)))
43401
- throw new Error("Unsupported control characters");
43402
- if (/[`~|]|^#{4,}\s|^\s*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(markdown))
43403
- throw new Error(
43404
- "Markdown append supports paragraphs, headings, flat lists, emphasis and inline links; use semantic HTML for other formatting"
43405
- );
43406
- const inline = (text) => {
43407
- const links = [];
43408
- let value = text.replace(/\[([^\]\n]+)\]\(([^()\s]+)\)/g, (_s, label, url) => {
43409
- if (!/^(https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url))
43410
- throw new Error("Unsupported Markdown link URL");
43411
- links.push(`<a href="${escape2(url)}">${escape2(label)}</a>`);
43412
- return `\uE000${links.length - 1}\uE001`;
43413
- });
43414
- value = escape2(value).replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/\*([^*\n]+)\*/g, "<i>$1</i>");
43415
- if (value.includes("[") || value.includes("]") || value.includes("*"))
43416
- throw new Error("Unsupported or unbalanced Markdown inline syntax");
43417
- return value.replace(/\uE000(\d+)\uE001/g, (_s, i) => links[Number(i)]);
43418
- };
43419
- let html = "", list;
43420
- const close = () => {
43421
- if (list) {
43422
- html += `</${list}>`;
43423
- list = void 0;
43424
- }
43425
- };
43426
- for (const line of markdown.replace(/\r\n/g, "\n").split("\n")) {
43427
- if (/^\s{2,}\S/.test(line)) throw new Error("Nested lists and indented code are unsupported");
43428
- const heading = /^(#{1,3})\s+(.+)$/.exec(line), item = /^(?:([-+*])|\d+\.)\s+(.+)$/.exec(line);
43429
- if (item) {
43430
- const kind = item[1] ? "ul" : "ol";
43431
- if (list !== kind) {
43432
- close();
43433
- list = kind;
43434
- html += `<${kind}>`;
43435
- }
43436
- html += `<li>${inline(item[2])}</li>`;
43437
- continue;
43438
- }
43439
- close();
43440
- html += heading ? `<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>` : line.trim() ? `<div>${inline(line)}</div>` : "<div><br></div>";
43441
- }
43442
- close();
43443
- return html;
43444
- }
43445
-
43446
- // src/services/backgroundNotes.ts
43447
- var BACKGROUND_SHORTCUT = "Apple Notes MCP - Background Operations v5";
43448
- var backgroundStatus = () => nativeTagsStatus(process.env.APPLE_NOTES_MCP_BACKGROUND_SHORTCUT || BACKGROUND_SHORTCUT);
43449
- var nativeTagBridgeStatus = () => nativeTagsStatus();
43450
- function readBackgroundSnapshot(manager, id2) {
43451
- if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(id2))
43452
- throw new Error("Exact note ID required");
43453
- const note = manager.getNoteById(id2);
43454
- if (!note) throw new Error("Note not found");
43455
- if (note.passwordProtected) throw new Error("Locked notes are unavailable in background mode");
43456
- const html = manager.getNoteContentById(id2);
43457
- const enriched = enrichNoteRead(id2, html);
43458
- const rich = readRichNote(id2);
43459
- if (enriched.revision !== rich.revision)
43460
- throw new Error("Note changed during read; read it again");
43461
- return {
43462
- id: id2,
43463
- title: note.title,
43464
- hash: richContentHash(html, enriched),
43465
- html,
43466
- rich,
43467
- pinned: getNoteMetadata(id2).metadata?.pinned,
43468
- checklist: getChecklistItems(id2).items || []
43469
- };
43470
- }
43471
- function validateAppendContent(content, format) {
43472
- if (!content || content.length > 1024 * 1024 || content.includes("\0"))
43473
- throw new Error("Invalid append content (limit 1 MiB)");
43474
- if (format === "html") {
43475
- const allowed = /* @__PURE__ */ new Set([
43476
- "div",
43477
- "p",
43478
- "br",
43479
- "b",
43480
- "strong",
43481
- "i",
43482
- "em",
43483
- "u",
43484
- "s",
43485
- "del",
43486
- "h1",
43487
- "h2",
43488
- "h3",
43489
- "ul",
43490
- "ol",
43491
- "li",
43492
- "a",
43493
- "table",
43494
- "thead",
43495
- "tbody",
43496
- "tr",
43497
- "td",
43498
- "th"
43499
- ]);
43500
- for (const tag of content.matchAll(/<\/?\s*([a-z][a-z0-9]*)\b([^>]*)>/gi)) {
43501
- if (!allowed.has(tag[1].toLowerCase()))
43502
- throw new Error(`Unsupported HTML element: ${tag[1]}`);
43503
- let attrs = tag[2].replace(/\/$/, "").trim();
43504
- if (tag[1].toLowerCase() === "a")
43505
- attrs = attrs.replace(/\bhref\s*=\s*(["'])(.*?)\1/gi, (_s, _q, url) => {
43506
- if (!/^(https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url) || Array.from(url).some((c) => c.charCodeAt(0) < 33))
43507
- throw new Error("Unsupported link URL");
43508
- return "";
43509
- });
43510
- if (attrs.trim())
43511
- throw new Error(
43512
- "Unsupported HTML attributes; use semantic formatting and explicit blank paragraphs"
43513
- );
43514
- }
43515
- if (/<!--|<!|<\?|<[^>]*$/u.test(content)) throw new Error("Unsupported HTML markup");
43516
- }
43517
- if (format === "markdown" && /!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
43518
- throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
43519
- }
43520
- function runBackgroundShortcut(input) {
43521
- const status = backgroundStatus();
43522
- if (!status.installed)
43523
- throw new Error("Install the supplied Background Operations shortcut once");
43524
- const directory = mkdtempSync4(join10(tmpdir4(), "apple-notes-background-"));
43525
- try {
43526
- const file = join10(directory, "request.json");
43527
- writeFileSync3(
43528
- file,
43529
- JSON.stringify({
43530
- operation: "",
43531
- title: "",
43532
- scopeText: "",
43533
- text: "",
43534
- objectId: "",
43535
- objectName: "",
43536
- change: "",
43537
- otherTitle: "",
43538
- otherScope: "",
43539
- tag: "",
43540
- size: "",
43541
- ...input
43542
- }),
43543
- { mode: 384 }
43544
- );
43545
- execFileSync7("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
43546
- encoding: "utf8",
43547
- timeout: 6e4,
43548
- maxBuffer: 1024 * 1024,
43549
- stdio: ["ignore", "pipe", "pipe"]
43550
- });
43551
- } finally {
43552
- rmSync4(directory, { recursive: true, force: true });
43553
- }
43554
- }
43555
- function backgroundDependencies(manager) {
43556
- return {
43557
- read: (id2) => readBackgroundSnapshot(manager, id2),
43558
- run: runBackgroundShortcut,
43559
- candidates: (title, scope) => manager.listAccounts().flatMap(
43560
- (account) => manager.searchNotes(title, false, account.name).filter(
43561
- (note) => note.title === title && !note.passwordProtected && manager.getNotePlaintextById(note.id).includes(scope)
43562
- ).map((note) => note.id)
43563
- )
43564
- };
43565
- }
43566
- function assertPreserved(before, after, options = {}) {
43567
- if (before.id !== after.id || before.title !== after.title)
43568
- throw new Error("Note identity changed");
43569
- const tidy = (s) => s.replace(/\r\n/g, "\n").replace(/[\s\ufffc]+$/gu, "");
43570
- let oldText = tidy(before.rich.text), newText = tidy(after.rich.text);
43571
- if (options.tagChange) {
43572
- const escaped = options.tagChange.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43573
- const strip = (s) => s.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "").replace(/[\s\ufffc]/gu, "");
43574
- oldText = strip(oldText);
43575
- newText = strip(newText);
43576
- }
43577
- if (options.append ? !newText.startsWith(oldText) : newText !== oldText)
43578
- throw new Error("Existing note text was not preserved");
43579
- const links = options.append ? after.rich.links.slice(0, before.rich.links.length) : after.rich.links;
43580
- if (linkSignature(before.rich.links) !== linkSignature(links))
43581
- throw new Error("Existing links were not preserved");
43582
- if (!options.tagChange && before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)))
43583
- throw new Error("Existing native object was lost");
43584
- if (JSON.stringify(before.checklist) !== JSON.stringify(
43585
- options.append ? after.checklist.slice(0, before.checklist.length) : after.checklist
43586
- ))
43587
- throw new Error("Existing checklist items changed");
43588
- for (const item of before.rich.checklistItems || []) {
43589
- const actual = after.rich.checklistItems?.find((current) => current.id === item.id);
43590
- if (!actual || actual.text !== item.text || actual.done !== item.done)
43591
- throw new Error("Existing checklist item identity or state changed");
43592
- }
43593
- if (before.rich.nativeTags.filter((t) => t !== options.tagChange).some((t) => !after.rich.nativeTags.includes(t)))
43594
- throw new Error("Existing native tag was lost");
43595
- const allowedRemoved = options.tagChange ? before.rich.nativeTagObjectIds?.[options.tagChange] || [] : [];
43596
- for (const object3 of before.rich.objectData || []) {
43597
- if (allowedRemoved.includes(object3.id)) continue;
43598
- const actual = after.rich.objectData?.find((o) => o.id === object3.id);
43599
- if (!actual || actual.mergeable !== object3.mergeable || actual.view !== object3.view)
43600
- throw new Error("Existing native object content or presentation changed");
43601
- }
43602
- if (!options.tagChange && before.rich.styleRuns && after.rich.styleRuns) {
43603
- const end = before.rich.text.trimEnd().length;
43604
- for (const oldRun of before.rich.styleRuns)
43605
- for (const newRun of after.rich.styleRuns) {
43606
- const overlapStart = Math.max(oldRun.start, newRun.start);
43607
- const overlapEnd = Math.min(
43608
- end,
43609
- oldRun.start + oldRun.length,
43610
- newRun.start + newRun.length
43611
- );
43612
- if (overlapStart < overlapEnd && /[^\s\ufffc]/u.test(before.rich.text.slice(overlapStart, overlapEnd)) && oldRun.signature !== newRun.signature)
43613
- throw new Error("Existing rich formatting changed");
43614
- }
43615
- }
43616
- }
43617
- function mutateBackground(request, operation, data, verify, deps) {
43618
- if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
43619
- throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
43620
- const before = deps.read(request.id);
43621
- if (before.hash !== request.expectedContentHash)
43622
- throw new Error("Note revision changed; read it again");
43623
- if (!before.rich.text.includes(request.scopeText))
43624
- throw new Error("Scope is absent from exact note");
43625
- const candidates = deps.candidates(before.title, request.scopeText);
43626
- if (candidates.length !== 1 || candidates[0] !== request.id)
43627
- throw new Error("Ambiguous note selection; nothing changed");
43628
- if (deps.read(request.id).hash !== before.hash)
43629
- throw new Error("Note revision changed during preflight");
43630
- let transportUncertain = false;
43631
- let transportMessage = "";
43632
- try {
43633
- deps.run({ ...data, operation, title: before.title, scopeText: request.scopeText });
43634
- } catch (error2) {
43635
- transportUncertain = true;
43636
- const detail = error2;
43637
- transportMessage = detail?.code === "ETIMEDOUT" ? "Shortcuts timed out; check for an interactive parameter or permission request" : String(detail?.stderr || detail?.message || "Shortcuts failed").trim().slice(0, 500);
43638
- }
43639
- const after = deps.read(request.id);
43640
- try {
43641
- verify(before, after);
43642
- } catch (error2) {
43643
- throw new Error(
43644
- `Operation outcome uncertain; read exact note before any retry: ${error2 instanceof Error ? error2.message : "readback failed"}${transportMessage ? "; " + transportMessage : ""}`
43645
- );
43646
- }
43647
- return {
43648
- ok: true,
43649
- id: request.id,
43650
- previousContentHash: before.hash,
43651
- contentHash: after.hash,
43652
- ...transportUncertain ? {
43653
- transportWarning: "Transport was uncertain; exact-ID readback verified the requested result"
43654
- } : {}
43655
- };
43656
- }
43657
- function assertAppendedHtmlLinks(previousCount, links, html) {
43658
- const remaining = links.slice(previousCount);
43659
- for (const expected of htmlLinks(html)) {
43660
- const index = remaining.findIndex(
43661
- (link) => linkSignature([link]) === linkSignature([expected])
43662
- );
43663
- if (index < 0) throw new Error("Appended HTML link not verified");
43664
- remaining.splice(index, 1);
43665
- }
43666
- }
43667
- function assertAppendedVisibleText(beforeHtml, afterHtml, expected) {
43668
- const before = comparableVisibleText(beforeHtml);
43669
- const after = comparableVisibleText(afterHtml);
43670
- if (!after.startsWith(before)) throw new Error("Appended text not verified");
43671
- const suffix = after.slice(before.length).replace(/\s+/gu, " ").trim();
43672
- const wanted = expected.replace(/\s+/gu, " ").trim();
43673
- if (!suffix || !suffix.includes(wanted)) throw new Error("Appended text not verified");
43674
- }
43675
- function appendNative(manager, request) {
43676
- validateAppendContent(request.content, request.format);
43677
- if (request.format === "markdown")
43678
- return appendNative(manager, {
43679
- ...request,
43680
- content: appendMarkdownHtml(request.content),
43681
- format: "html"
43682
- });
43683
- if (request.format === "html" && /<table\b/i.test(request.content))
43684
- throw new Error("Use create-table for verified native table insertion");
43685
- const op = request.format === "plaintext" ? "append-text" : request.format === "html" ? "append-html" : "append-markdown";
43686
- const text = request.format === "html" ? "<div><br></div>" + request.content : "\n\n" + request.content;
43687
- return mutateBackground(
43688
- request,
43689
- op,
43690
- { text },
43691
- (before, after) => {
43692
- assertPreserved(before, after, { append: true });
43693
- const expected = request.format === "html" ? comparableVisibleText(request.content) : request.format === "plaintext" ? request.content : null;
43694
- if (expected) assertAppendedVisibleText(before.html, after.html, expected);
43695
- if (request.format === "html")
43696
- assertAppendedHtmlLinks(before.rich.links.length, after.rich.links, request.content);
43697
- },
43698
- backgroundDependencies(manager)
43699
- );
43700
- }
43701
- function setNativeTag(manager, request) {
43702
- const tag = normalizeNativeTags([request.tag])[0];
43703
- const initial = readBackgroundSnapshot(manager, request.id);
43704
- if (initial.hash !== request.expectedContentHash)
43705
- throw new Error("Note revision changed; read it again");
43706
- if (initial.rich.nativeTags.includes(tag) === request.present)
43707
- return { ok: true, id: request.id, contentHash: initial.hash, changed: false };
43708
- const deps = backgroundDependencies(manager);
43709
- if (request.present)
43710
- deps.run = ({ title, scopeText }) => runNativeTagsShortcut({ title, scopeText, tags: [tag] });
43711
- return mutateBackground(
43712
- request,
43713
- request.present ? "add-tag" : "remove-tag",
43714
- { tag },
43715
- (before, after) => {
43716
- assertPreserved(before, after, { tagChange: tag });
43717
- if (after.rich.nativeTags.includes(tag) !== request.present)
43718
- throw new Error("Native tag state not verified");
43719
- const removed = before.rich.nativeObjectIds.filter(
43720
- (id2) => !after.rich.nativeObjectIds.includes(id2)
43721
- );
43722
- const allowed = request.present ? [] : before.rich.nativeTagObjectIds?.[tag] || [];
43723
- if (removed.some((id2) => !allowed.includes(id2)))
43724
- throw new Error("Unrelated native object was lost");
43725
- },
43726
- deps
43727
- );
43728
- }
43729
-
43730
43752
  // src/tools/nativeOperations.ts
43731
43753
  var VERIFIED_BACKGROUND = /* @__PURE__ */ new Set([
43732
43754
  "append-native",
@@ -43841,7 +43863,7 @@ function registerNativeOperations(server2, manager) {
43841
43863
  implemented: true,
43842
43864
  verified: VERIFIED_BACKGROUND.has(name),
43843
43865
  available: VERIFIED_BACKGROUND.has(name) && (!native.includes(name) || bridge.installed) && (name !== "replace-native-tag" || tagBridgeInstalled),
43844
- reason: !VERIFIED_BACKGROUND.has(name) ? UNAVAILABLE[name] || LIVE_VALIDATION_BLOCKERS[name] || "Live validation pending; install the shortcut and complete the isolated acceptance tests" : native.includes(name) && !bridge.installed ? "Install the supplied shortcut once" : name === "replace-native-tag" && !tagBridgeInstalled ? "Install the verified Native Tags bridge for the addition phase" : void 0
43866
+ reason: !VERIFIED_BACKGROUND.has(name) ? UNAVAILABLE[name] || LIVE_VALIDATION_BLOCKERS[name] || "Live validation pending; install the shortcut and complete the isolated acceptance tests" : native.includes(name) && !bridge.installed ? "Run apple-notes-mcp setup and approve Add Shortcut in macOS" : name === "replace-native-tag" && !tagBridgeInstalled ? "Run apple-notes-mcp setup to install the Native Tags bridge for the addition phase" : void 0
43845
43867
  }
43846
43868
  ])
43847
43869
  ),
@@ -44055,10 +44077,77 @@ function registerNativeOperations(server2, manager) {
44055
44077
  );
44056
44078
  }
44057
44079
 
44080
+ // src/setupShortcuts.ts
44081
+ import { spawnSync as spawnSync2 } from "node:child_process";
44082
+ import { existsSync as existsSync7 } from "node:fs";
44083
+ import { dirname as dirname2, resolve as resolve2 } from "node:path";
44084
+ import { fileURLToPath } from "node:url";
44085
+ var shortcutFiles = [
44086
+ { name: NATIVE_TAGS_SHORTCUT, file: "Apple Notes MCP - Native Tags.shortcut" },
44087
+ {
44088
+ name: BACKGROUND_SHORTCUT,
44089
+ file: "Apple Notes MCP - Background Operations v5.shortcut"
44090
+ }
44091
+ ];
44092
+ function setupShortcuts(checkOnly, dependencies = {}) {
44093
+ const status = dependencies.status || nativeTagsStatus;
44094
+ const exists = dependencies.exists || existsSync7;
44095
+ const open = dependencies.open || ((path4) => {
44096
+ const result = spawnSync2("/usr/bin/open", [path4], { encoding: "utf8" });
44097
+ return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
44098
+ });
44099
+ const baseDirectory = dependencies.baseDirectory || resolve2(dirname2(fileURLToPath(import.meta.url)), "../shortcuts");
44100
+ const items = shortcutFiles.map(({ name, file }) => {
44101
+ const path4 = resolve2(baseDirectory, file);
44102
+ let installed = false;
44103
+ let identifier;
44104
+ let error2;
44105
+ try {
44106
+ const current = status(name);
44107
+ installed = current.installed;
44108
+ identifier = current.identifier;
44109
+ } catch (cause) {
44110
+ error2 = cause instanceof Error ? cause.message : String(cause);
44111
+ }
44112
+ let opened = false;
44113
+ if (!installed && !checkOnly) {
44114
+ if (!exists(path4)) error2 = `Packaged Shortcut is missing: ${path4}`;
44115
+ else {
44116
+ const result = open(path4);
44117
+ opened = result.ok;
44118
+ if (!result.ok) error2 = result.error || `Could not open ${file}`;
44119
+ }
44120
+ }
44121
+ return { name, installed, identifier, file: path4, opened, ...error2 ? { error: error2 } : {} };
44122
+ });
44123
+ return { ready: items.every((item) => item.installed), checkOnly, items };
44124
+ }
44125
+ function formatShortcutSetup(report) {
44126
+ const lines = ["Apple Notes MCP Shortcut setup", ""];
44127
+ for (const item of report.items) {
44128
+ if (item.installed) lines.push(`\u2713 ${item.name} (${item.identifier})`);
44129
+ else if (item.opened) lines.push(`\u2192 ${item.name}: confirm \u201CAdd Shortcut\u201D in macOS`);
44130
+ else lines.push(`\u2717 ${item.name}: ${item.error || "not installed"}`);
44131
+ }
44132
+ lines.push("");
44133
+ if (report.ready) lines.push("Both Shortcut bridges are installed.");
44134
+ else if (report.checkOnly) lines.push("Run `apple-notes-mcp setup` to open missing workflows.");
44135
+ else
44136
+ lines.push(
44137
+ "After approving the macOS dialogs, run `apple-notes-mcp setup --check` or the MCP doctor tool."
44138
+ );
44139
+ return lines.join("\n");
44140
+ }
44141
+
44058
44142
  // src/index.ts
44059
44143
  loadFileConfig();
44060
44144
  var require2 = createRequire(import.meta.url);
44061
44145
  var { version: version2 } = require2("../package.json");
44146
+ if (process.argv[2] === "setup") {
44147
+ const report = setupShortcuts(process.argv.slice(3).includes("--check"));
44148
+ process.stdout.write(formatShortcutSetup(report) + "\n");
44149
+ process.exit(report.ready || !report.checkOnly ? 0 : 1);
44150
+ }
44062
44151
  var server = new McpServer({
44063
44152
  name: "apple-notes",
44064
44153
  version: version2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.9",
3
+ "version": "2.8.10",
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",