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.
- package/README.md +13 -0
- package/build/index.js +576 -487
- 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 =
|
|
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
|
|
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
|
|
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:
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
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((
|
|
38973
|
+
return new Promise((resolve3) => {
|
|
38974
38974
|
const json2 = serializeMessage(message);
|
|
38975
38975
|
if (this._stdout.write(json2)) {
|
|
38976
|
-
|
|
38976
|
+
resolve3();
|
|
38977
38977
|
} else {
|
|
38978
|
-
this._stdout.once("drain",
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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(/ | /gi, " ").replace(/"/gi, '"').replace(/'|'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/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
|
|
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
|
|
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(/ | /gi, " ").replace(/"/gi, '"').replace(/'|'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/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
|
|
43492
|
+
mkdtempSync as mkdtempSync4,
|
|
43022
43493
|
openSync,
|
|
43023
43494
|
readFileSync as readFileSync3,
|
|
43024
|
-
rmSync as
|
|
43025
|
-
writeFileSync
|
|
43495
|
+
rmSync as rmSync4,
|
|
43496
|
+
writeFileSync as writeFileSync3
|
|
43026
43497
|
} from "node:fs";
|
|
43027
|
-
import { tmpdir as
|
|
43028
|
-
import { basename, isAbsolute as isAbsolute2, join as
|
|
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 =
|
|
43143
|
-
const temporaryFile =
|
|
43613
|
+
const directory = mkdtempSync4(join10(tmpdir4(), "notes-attachment-add-"));
|
|
43614
|
+
const temporaryFile = join10(directory, basename(path4));
|
|
43144
43615
|
try {
|
|
43145
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 ? "
|
|
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