@mgcrea/mcp-apple-notes 1.22.1 → 1.23.0

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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { D as BUILD_INFO, S as NOTES_SURFACE, a as loadConfig, r as createServer } from "./server-BuEKyXZw.js";
2
+ import { D as BUILD_INFO, S as NOTES_SURFACE, a as loadConfig, r as createServer } from "./server-Dewhz4d9.js";
3
3
  import { runStdioServer } from "@mgcrea/mcp-apple-core";
4
4
  //#region src/cli.ts
5
5
  const LOG_PREFIX = "apple-notes-mcp";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as NoteLockedError, D as BUILD_INFO, E as NotesNotRunningError, S as NOTES_SURFACE, T as NotesBusyError, _ as encodeRef, a as loadConfig, b as locateStore, c as introspect, d as collectStrings, f as extractNoteText, g as decodeRef, h as REF_VERSION, i as registerTools, l as openStore, m as readVarint, n as SERVER_VERSION, o as AppleNotesClient, p as parseFields, r as createServer, s as NoteStore, t as SERVER_NAME, u as NOTE_TEXT_PATH, v as refFromPrimaryKey, w as NoteNotFoundError, x as AppleNotesError, y as defaultStorePath } from "./server-BuEKyXZw.js";
1
+ import { C as NoteLockedError, D as BUILD_INFO, E as NotesNotRunningError, S as NOTES_SURFACE, T as NotesBusyError, _ as encodeRef, a as loadConfig, b as locateStore, c as introspect, d as collectStrings, f as extractNoteText, g as decodeRef, h as REF_VERSION, i as registerTools, l as openStore, m as readVarint, n as SERVER_VERSION, o as AppleNotesClient, p as parseFields, r as createServer, s as NoteStore, t as SERVER_NAME, u as NOTE_TEXT_PATH, v as refFromPrimaryKey, w as NoteNotFoundError, x as AppleNotesError, y as defaultStorePath } from "./server-Dewhz4d9.js";
2
2
  export { AppleNotesClient, AppleNotesError, BUILD_INFO, NOTES_SURFACE, NOTE_TEXT_PATH, NoteLockedError, NoteNotFoundError, NoteStore, NotesBusyError, NotesNotRunningError, REF_VERSION, SERVER_NAME, SERVER_VERSION, collectStrings, createServer, decodeRef, defaultStorePath, encodeRef, extractNoteText, introspect, loadConfig, locateStore, openStore, parseFields, readVarint, refFromPrimaryKey, registerTools };
@@ -13,8 +13,8 @@ const pkg = readPackageIdentity(new URL("../package.json", import.meta.url), {
13
13
  const BUILD_INFO = {
14
14
  name: pkg.name,
15
15
  version: pkg.version,
16
- gitCommit: "153c3ea",
17
- gitCommitDate: "2026-09-16T14:44:21+02:00"
16
+ gitCommit: "254e371",
17
+ gitCommitDate: "2026-09-18T23:50:15+02:00"
18
18
  };
19
19
  //#endregion
20
20
  //#region src/client/errors.ts
@@ -1907,4 +1907,4 @@ const createServer = (opts) => {
1907
1907
  //#endregion
1908
1908
  export { NoteLockedError as C, BUILD_INFO as D, NotesNotRunningError as E, NOTES_SURFACE as S, NotesBusyError as T, encodeRef as _, loadConfig as a, locateStore as b, introspect as c, collectStrings as d, extractNoteText as f, decodeRef as g, REF_VERSION as h, registerTools as i, openStore as l, readVarint as m, SERVER_VERSION as n, AppleNotesClient as o, parseFields as p, createServer as r, NoteStore as s, SERVER_NAME as t, NOTE_TEXT_PATH as u, refFromPrimaryKey as v, NoteNotFoundError as w, AppleNotesError as x, defaultStorePath as y };
1909
1909
 
1910
- //# sourceMappingURL=server-BuEKyXZw.js.map
1910
+ //# sourceMappingURL=server-Dewhz4d9.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-BuEKyXZw.js","names":["#db","#where","#select","#toRow","#logger","#storeTried","#store","#allowed","#indexCanAnswer","#fromIndex","#bulkNotes","#allBodies","#bodies","#truncate","#findMedia"],"sources":["../src/build-info.ts","../src/client/errors.ts","../src/client/locate.ts","../src/client/jxa/core.ts","../src/client/jxa/read.ts","../src/client/jxa/repair.ts","../src/client/jxa/write.ts","../src/client/ref.ts","../src/client/protobuf.ts","../src/client/store.ts","../src/client/notes.ts","../src/config.ts","../src/guide.ts","../src/prompts.ts","../src/tools/util.ts","../src/tools/diagnostics.ts","../src/tools/accounts.ts","../src/tools/actions.ts","../src/tools/notes.ts","../src/tools/index.ts","../src/server.ts"],"sourcesContent":["// Build-time / runtime identity for the running server. `name`/`version` are\n// read from package.json at startup (always accurate); `gitCommit` /\n// `gitCommitDate` are injected by tsdown's `define` substitution at build time\n// and fall back to \"unknown\" when running from source (e.g. vitest).\n\nimport { readPackageIdentity, type BuildInfo } from \"@mgcrea/mcp-apple-core\";\n\n// oxlint-disable no-underscore-dangle -- bundler-injected build-time constants.\ndeclare const __GIT_COMMIT__: string;\ndeclare const __GIT_COMMIT_DATE__: string;\n\nconst pkg = readPackageIdentity(new URL(\"../package.json\", import.meta.url), {\n name: \"@mgcrea/mcp-apple-notes\",\n version: \"0.0.0\",\n});\n\nexport type { BuildInfo };\n\nexport const BUILD_INFO: BuildInfo = {\n name: pkg.name,\n version: pkg.version,\n gitCommit: typeof __GIT_COMMIT__ === \"string\" ? __GIT_COMMIT__ : \"unknown\",\n gitCommitDate: typeof __GIT_COMMIT_DATE__ === \"string\" ? __GIT_COMMIT_DATE__ : \"unknown\",\n};\n","/**\n * Notes' error surface. The taxonomy lives in `@mgcrea/mcp-apple-core`; what\n * belongs here is the identity those messages are written against, and the\n * errors that are genuinely about notes.\n */\n\nimport { AppleAutomationError, type SurfaceContext } from \"@mgcrea/mcp-apple-core\";\n\n/** Named in every user-facing error and in the env vars they mention. */\nexport const NOTES_SURFACE: SurfaceContext = { appName: \"Notes\", envPrefix: \"APPLE_NOTES\" };\n\nexport {\n AppleAutomationError as AppleNotesError,\n AppBusyError as NotesBusyError,\n AppNotRunningError as NotesNotRunningError,\n IndexUnavailableError,\n OsascriptTimeoutError,\n PlatformError,\n PreconditionError,\n ProtocolError,\n SchemaDriftError,\n TccDeniedError,\n WritesDisabledError,\n} from \"@mgcrea/mcp-apple-core\";\n\n/** A NoteRef no longer resolves — the note was deleted, or the index is stale. */\nexport class NoteNotFoundError extends AppleAutomationError {\n override readonly name = \"NoteNotFoundError\";\n\n constructor(ref: string) {\n super(\n `No note for ref \"${ref}\". It was probably deleted since the search ran. ` +\n `Re-run the search to get a current ref.`,\n );\n }\n}\n\n/**\n * The note is password-protected and Notes will not hand over its text.\n *\n * The file lane cannot help either: locked notes are AES ciphertext at rest,\n * which is why `ZICNOTEDATA` carries `ZCRYPTOINITIALIZATIONVECTOR` and\n * `ZCRYPTOTAG` columns. Saying so is more useful than an empty body.\n */\nexport class NoteLockedError extends AppleAutomationError {\n override readonly name = \"NoteLockedError\";\n\n constructor(ref: string) {\n super(\n `Note \"${ref}\" is password-protected. Unlock it in Notes and retry — its text is ` +\n `encrypted at rest, so no amount of permission makes it readable from here.`,\n );\n }\n}\n","import { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { describeStore, type StoreFacts } from \"@mgcrea/mcp-apple-core\";\n\n/**\n * Find Notes' store.\n *\n * Simpler than Mail's, and measurably so: Mail's root is version-numbered\n * (`~/Library/Mail/V10`) *and* TCC-hidden, so it needs a four-branch strategy\n * ladder that asks Mail to name its own directory. Notes has one constant path,\n * confirmed on macOS 26.6.\n *\n * The path is TCC-protected, so `statSync` succeeds on it while `open` and\n * `access` are denied — existence and readability are different questions, and\n * only readability answers whether Full Disk Access is granted.\n */\n\n/** `~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite`. */\nexport const GROUP_CONTAINER = \"group.com.apple.notes\";\n\nexport type LocateResult = StoreFacts & {\n storePath: string;\n /** Where attachment bytes live, one directory per account. */\n mediaRoot: string;\n reason: string | null;\n};\n\nexport const defaultStorePath = (home: string = homedir()): string =>\n join(home, \"Library\", \"Group Containers\", GROUP_CONTAINER, \"NoteStore.sqlite\");\n\nexport const locateStore = (\n opts: { storePath?: string | undefined; home?: string } = {},\n): LocateResult => {\n const storePath = opts.storePath ?? defaultStorePath(opts.home);\n const facts = describeStore(storePath);\n\n const reason = facts.exists\n ? facts.readable\n ? null\n : \"NoteStore.sqlite exists but cannot be read. Grant Full Disk Access to the app running \" +\n \"this server (System Settings > Privacy & Security > Full Disk Access) and restart it. \" +\n \"Granting it to Notes.app does nothing — the reader needs the permission.\"\n : `No NoteStore.sqlite at ${storePath}. Has Notes ever been set up on this account?`;\n\n return { ...facts, storePath, mediaRoot: join(dirname(storePath), \"Accounts\"), reason };\n};\n","/**\n * JXA script fragments.\n *\n * Every script here is a static constant. None of them may contain a template\n * interpolation — `assertStaticScript` rejects any script containing `${`, and\n * that includes JavaScript template literals written *inside* the JXA source.\n * Use string concatenation in JXA code.\n *\n * Every script follows the same contract:\n * - it reads its parameters from `JSON.parse(argv[0])`\n * - it returns `JSON.stringify({ok: true, data})` on success\n * - it returns `JSON.stringify({ok: false, error: {code, message}})` on an\n * application-level failure, still exiting 0\n * so that a non-zero exit always means infrastructure rather than \"no such note\".\n *\n * ## Two rules the measurements impose\n *\n * 1. **Never read a property per note in a loop.** One note's `plaintext` costs\n * ~116ms, so a 921-note library would take ~107 seconds. Every read here is a\n * bulk array fetch (`N.notes.plaintext()`), which is one Apple Event whatever\n * the size.\n * 2. **Never use `whose` for text search.** `notes.whose({plaintext: ...})` took\n * 671ms against 97ms for pulling everything and filtering in JS — 6.9x slower,\n * because the specifier evaluates per note across the Apple Event bridge while\n * the bulk form is a single round trip.\n */\n\n/**\n * Shared prelude.\n *\n * The liveness gate matters more than it looks. `Application(\"Notes\").notes`\n * *launches* Notes if it isn't running — which steals focus as a side effect of\n * a read. `NSRunningApplication` answers the same question without launching\n * anything and without a second TCC prompt.\n *\n * Deliberately NOT shared with Mail's prelude: 45% of that file is a UI-scripting\n * workaround for Mail's forced blockquote citation, which every script there\n * carries whether it composes mail or lists accounts. Notes needs none of it.\n */\nexport const PRELUDE = `\nObjC.import(\"AppKit\");\n\nfunction isNotesRunning() {\n var apps = $.NSRunningApplication.runningApplicationsWithBundleIdentifier(\"com.apple.Notes\");\n return apps.count > 0;\n}\n\nfunction ok(data) { return JSON.stringify({ ok: true, data: data }); }\nfunction err(code, message) { return JSON.stringify({ ok: false, error: { code: code, message: String(message) } }); }\n\nfunction iso(d) {\n try { return d ? d.toISOString() : null; } catch (e) { return null; }\n}\n\n/** Read one property defensively: Notes throws on properties it cannot supply. */\nfunction prop(fn, fallback) {\n try {\n var v = fn();\n return v === undefined ? fallback : v;\n } catch (e) {\n return fallback;\n }\n}\n\n/**\n * Zip parallel bulk arrays into records.\n *\n * Bulk fetches are the only affordable read shape, and they come back as\n * separate arrays that line up by index. Rebuilding records here keeps that\n * assumption in one place.\n */\nfunction zip(keys, columns, count) {\n var out = [];\n for (var i = 0; i < count; i++) {\n var row = {};\n for (var k = 0; k < keys.length; k++) {\n var col = columns[k];\n row[keys[k]] = col && i < col.length ? col[i] : null;\n }\n out.push(row);\n }\n return out;\n}\n\n/** Walk the folder tree. Folders nest in the dictionary, so this recurses. */\nfunction folderTree(container, accountId, depth, out) {\n if (depth > 8) return out;\n var subs = [];\n try { subs = container.folders(); } catch (e) { return out; }\n for (var i = 0; i < subs.length; i++) {\n var f = subs[i];\n var id = prop(function () { return String(f.id()); }, null);\n out.push({\n id: id,\n name: prop(function () { return String(f.name()); }, null),\n accountId: accountId,\n depth: depth,\n shared: prop(function () { return f.shared(); }, false),\n noteCount: prop(function () { return f.notes().length; }, null)\n });\n folderTree(f, accountId, depth + 1, out);\n }\n return out;\n}\n`;\n\n/**\n * Wrap a script body in the prelude and the liveness gate.\n *\n * `allowLaunch` is opt-in per script, and only tools whose whole purpose is to\n * make Notes do something set it. A read that silently launches Notes is a read\n * with a side effect.\n */\nexport const script = (body: string, opts: { allowLaunch?: boolean } = {}): string => `\n${PRELUDE}\nfunction run(argv) {\n var p = JSON.parse(argv[0] || \"{}\");\n if (!isNotesRunning() && !${opts.allowLaunch ? \"true\" : \"false\"}) {\n return err(\"APP_NOT_RUNNING\", \"Notes is not running.\");\n }\n try {\n var N = Application(\"Notes\");\n ${body}\n } catch (e) {\n var msg = String(e && e.message ? e.message : e);\n if (msg.indexOf(\"-1743\") !== -1) return err(\"NOT_AUTHORIZED\", msg);\n return err(\"SCRIPT_ERROR\", msg);\n }\n}\n`;\n","import { script } from \"./core.js\";\n\n/** Accounts, with the folder count each one holds. */\nexport const LIST_ACCOUNTS = script(`\n var accounts = N.accounts();\n var out = [];\n for (var i = 0; i < accounts.length; i++) {\n var a = accounts[i];\n out.push({\n id: prop(function () { return String(a.id()); }, null),\n name: prop(function () { return String(a.name()); }, null),\n defaultFolder: prop(function () { return String(a.defaultFolder.name()); }, null),\n folderCount: prop(function () { return a.folders().length; }, 0),\n noteCount: prop(function () { return a.notes().length; }, 0)\n });\n }\n return ok(out);\n`);\n\n/** The folder tree. Folders nest, so this is depth-first rather than a flat list. */\nexport const LIST_FOLDERS = script(`\n var accounts = N.accounts();\n var out = [];\n for (var i = 0; i < accounts.length; i++) {\n var a = accounts[i];\n var accountId = prop(function () { return String(a.id()); }, null);\n var accountName = prop(function () { return String(a.name()); }, null);\n var folders = folderTree(a, accountId, 1, []);\n for (var j = 0; j < folders.length; j++) {\n folders[j].accountName = accountName;\n out.push(folders[j]);\n }\n }\n return ok(out);\n`);\n\n/**\n * Every note's metadata, as bulk arrays.\n *\n * One Apple Event per property for the whole library — ~0.1ms per note — versus\n * ~116ms per note for reading properties one note at a time. Folder membership\n * is resolved with one bulk call per *folder*, which is a handful of round trips\n * rather than one per note.\n */\nexport const BULK_NOTES = script(`\n var ids = N.notes.id();\n var count = ids.length;\n var rows = zip(\n [\"id\", \"name\", \"modified\", \"created\", \"locked\", \"shared\"],\n [\n ids,\n prop(function () { return N.notes.name(); }, []),\n prop(function () { return N.notes.modificationDate(); }, []),\n prop(function () { return N.notes.creationDate(); }, []),\n prop(function () { return N.notes.passwordProtected(); }, []),\n prop(function () { return N.notes.shared(); }, [])\n ],\n count\n );\n\n // Note -> folder, one bulk call per folder rather than one per note.\n var folderOf = {};\n var accounts = N.accounts();\n for (var i = 0; i < accounts.length; i++) {\n var a = accounts[i];\n var accountId = prop(function () { return String(a.id()); }, null);\n var accountName = prop(function () { return String(a.name()); }, null);\n var folders = folderTree(a, accountId, 1, []);\n for (var j = 0; j < folders.length; j++) {\n var f = folders[j];\n var noteIds = [];\n try {\n var handle = N.folders.byId(f.id);\n noteIds = handle.notes.id();\n } catch (e) { noteIds = []; }\n for (var k = 0; k < noteIds.length; k++) {\n folderOf[String(noteIds[k])] = { folderId: f.id, folder: f.name, accountId: accountId, account: accountName };\n }\n }\n }\n\n for (var r = 0; r < rows.length; r++) {\n var row = rows[r];\n row.id = String(row.id);\n row.modified = iso(row.modified);\n row.created = iso(row.created);\n var where = folderOf[row.id];\n row.folder = where ? where.folder : null;\n row.folderId = where ? where.folderId : null;\n row.account = where ? where.account : null;\n row.accountId = where ? where.accountId : null;\n }\n return ok({ count: count, notes: rows });\n`);\n\n/**\n * Every note's id and plaintext, in one round trip.\n *\n * This is the search lane when the index is unavailable: pull everything, filter\n * in JS. Measured at 97ms over 921 notes and 250KB — against 671ms for the\n * equivalent `whose` specifier.\n */\nexport const BULK_PLAINTEXT = script(`\n return ok({ ids: N.notes.id(), texts: N.notes.plaintext() });\n`);\n\n/** Bodies for a specific set of notes, by id. Used after the index narrows things down. */\nexport const GET_NOTE_BODIES = script(`\n var out = [];\n for (var i = 0; i < p.ids.length; i++) {\n var id = String(p.ids[i]);\n var note = null;\n try { note = N.notes.byId(id); } catch (e) { note = null; }\n if (!note) { out.push({ id: id, found: false }); continue; }\n var locked = prop(function () { return note.passwordProtected(); }, false);\n out.push({\n id: id,\n found: true,\n locked: locked,\n name: prop(function () { return String(note.name()); }, null),\n plaintext: locked ? null : prop(function () { return String(note.plaintext()); }, null),\n body: locked ? null : prop(function () { return String(note.body()); }, null),\n modified: iso(prop(function () { return note.modificationDate(); }, null)),\n created: iso(prop(function () { return note.creationDate(); }, null))\n });\n }\n return ok(out);\n`);\n\n/** Attachment metadata for one note. There is no filesystem path in the dictionary. */\nexport const LIST_ATTACHMENTS = script(`\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n var atts = prop(function () { return note.attachments(); }, []);\n var out = [];\n for (var i = 0; i < atts.length; i++) {\n var a = atts[i];\n out.push({\n id: prop(function () { return String(a.id()); }, null),\n name: prop(function () { return String(a.name()); }, null),\n contentIdentifier: prop(function () { return String(a.contentIdentifier()); }, null),\n url: prop(function () { return String(a.URL()); }, null),\n created: iso(prop(function () { return a.creationDate(); }, null)),\n modified: iso(prop(function () { return a.modificationDate(); }, null))\n });\n }\n return ok(out);\n`);\n","/**\n * Repair for Notes' lossy `body` round trip.\n *\n * `UPDATE_NOTE`'s append mode reads `note.body()` and assigns the result back,\n * so whatever the getter mangles is written back mangled — onto content the\n * caller never touched. Two independent losses, both reproduced against Notes\n * on macOS 15:\n *\n * 1. **Entities come back unterminated.** The text `ps.\"projectId\";` serialises\n * to `ps.&quotprojectId&quot;` — `&quot` with no semicolon. Fed back to the\n * setter, the trailing `&quot` fuses with the literal `;` that followed it\n * into a single `\"`, and the semicolon is gone. The getter never terminates\n * an entity, so a `&quot;` in its output is always entity-plus-literal-`;`,\n * never a well-formed `&quot;`. That is what makes the repair unconditional\n * rather than a `(?!;)` lookahead — a lookahead would skip exactly the case\n * that loses a character.\n * 2. **Space runs come back literal**, and HTML collapses them on re-parse, so\n * every indented line loses its indentation and every aligned block loses\n * its alignment.\n *\n * Order matters: entities first, then spaces. The space pass emits `&nbsp;`\n * with a terminating semicolon, and running the entity pass afterwards would\n * rewrite those into `&nbsp;;`.\n *\n * Exported as source rather than a function because it runs inside the JXA\n * script, in osascript's JavaScriptCore. `test/repair.test.ts` evaluates this\n * same string, so the tested code is the shipped code.\n */\nexport const REPAIR_BODY_SRC = `\nvar repairBody = function (html) {\n var entities = html\n .replace(/&(amp|quot|apos|lt|gt|nbsp)/g, \"&$1;\")\n .replace(/&#(x?[0-9a-fA-F]+)/g, \"&#$1;\");\n return entities.replace(/>([^<]*)</g, function (_m, text) {\n return \">\" + text\n .replace(/ {2,}/g, function (run) { return new Array(run.length + 1).join(\"&nbsp;\"); })\n .replace(/^ /, \"&nbsp;\") + \"<\";\n });\n};\n`;\n","import { script } from \"./core.js\";\nimport { REPAIR_BODY_SRC } from \"./repair.js\";\n\n/**\n * Mutations may launch Notes.\n *\n * A read that silently launches an app is a read with a side effect, which is\n * why the read scripts refuse. A tool whose whole purpose is to create or change\n * a note has no such problem — the user asked for the change.\n */\nconst MUTATION = { allowLaunch: true } as const;\n\n/**\n * Create a note.\n *\n * Notes derives a note's `name` from the first line of its body rather than from\n * a settable property, so the title is prepended as a heading instead of\n * assigned. Setting `name` directly is silently ignored.\n */\nexport const CREATE_NOTE = script(\n `\n var folder = null;\n if (p.folderId) {\n try { folder = N.folders.byId(String(p.folderId)); } catch (e) { folder = null; }\n if (!folder) return err(\"FOLDER_NOT_FOUND\", \"No folder with id \" + p.folderId);\n } else {\n var accounts = N.accounts();\n if (!accounts.length) return err(\"NO_ACCOUNT\", \"Notes reports no accounts.\");\n folder = accounts[0].defaultFolder;\n }\n\n var html = \"\";\n if (p.title) html += \"<h1>\" + p.title + \"</h1>\";\n if (p.body) html += p.body;\n if (!html) return err(\"EMPTY_NOTE\", \"A note needs a title or a body.\");\n\n var note = N.Note({ body: html });\n folder.notes.push(note);\n return ok({\n id: prop(function () { return String(note.id()); }, null),\n name: prop(function () { return String(note.name()); }, null),\n folder: prop(function () { return String(folder.name()); }, null)\n });\n`,\n MUTATION,\n);\n\n/**\n * Replace a note's body. The title follows the first line, as it does on create.\n *\n * Append re-serialises the *existing* body through Notes' HTML parser, which is\n * lossy in both directions — see `repair.ts`. `repairBody` undoes the getter's\n * damage before the concatenation, so an append leaves untouched content byte\n * identical instead of silently flattening indentation and eating characters.\n */\nexport const UPDATE_NOTE = script(\n REPAIR_BODY_SRC +\n `\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n if (prop(function () { return note.passwordProtected(); }, false)) {\n return err(\"NOTE_LOCKED\", \"That note is password-protected; unlock it in Notes first.\");\n }\n\n var current = prop(function () { return String(note.body()); }, \"\");\n var next = p.mode === \"append\" ? repairBody(current) + (p.body || \"\") : (p.body || \"\");\n note.body = next;\n return ok({\n id: String(p.id),\n name: prop(function () { return String(note.name()); }, null),\n bodyLength: next.length,\n mode: p.mode || \"replace\"\n });\n`,\n MUTATION,\n);\n\n/** Move a note to another folder. */\nexport const MOVE_NOTE = script(\n `\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n var folder = null;\n try { folder = N.folders.byId(String(p.folderId)); } catch (e) { folder = null; }\n if (!folder) return err(\"FOLDER_NOT_FOUND\", \"No folder with id \" + p.folderId);\n\n N.move(note, { to: folder });\n return ok({\n id: String(p.id),\n folder: prop(function () { return String(folder.name()); }, null)\n });\n`,\n MUTATION,\n);\n\n/**\n * Attach a file to a note.\n *\n * The scripting dictionary looks read-only on attachments — every property of\n * the `attachment` class is `access=\"r\"` — but that reads the wrong half of it.\n * `note` declares `<element type=\"attachment\">`, and `make` comes from the\n * Standard Suite, so it appears in no command list of the Notes suite. Apple\n * says so outright in a comment on the hidden `contents` property: the point of\n * hiding it is \"to facilitate creating an attachment like this: make new\n * attachment with data myFile\".\n *\n * THE FORM MATTERS. Measured on macOS 26.6, only one of the three works:\n *\n * N.make({new: \"attachment\", at: note, withData: Path(f)}) works\n * N.make({new: \"attachment\", at: note.attachments, withData: f}) -10014\n * note.attachments.push(N.Attachment({contents: Path(f)})) -10000\n *\n * `at:` takes the note itself, not its attachment collection — the opposite of\n * how `folder.notes.push` creates a note two functions up.\n *\n * Setting an image through the note `body` instead does NOT work and fails\n * deceptively: `<img src=\"file://...\">` creates an attachment row whose bytes\n * are never fetched, and a `data:` URI lands as `public.data` with no preview.\n * See docs/notes.md.\n */\nexport const ADD_ATTACHMENT = script(\n `\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n if (prop(function () { return note.passwordProtected(); }, false)) {\n return err(\"NOTE_LOCKED\", \"That note is password-protected; unlock it in Notes first.\");\n }\n\n var before = {};\n var existing = prop(function () { return note.attachments(); }, []);\n for (var i = 0; i < existing.length; i++) {\n try { before[String(existing[i].id())] = true; } catch (e) {}\n }\n\n try {\n N.make({ new: \"attachment\", at: note, withData: Path(String(p.path)) });\n } catch (e) {\n return err(\"ATTACH_FAILED\", \"Notes refused the file: \" + e);\n }\n\n // Report the attachment that appeared, not a count. Notes can enumerate the\n // same attachment more than once, so a count would be wrong as often as right.\n var added = null;\n var after = prop(function () { return note.attachments(); }, []);\n for (var j = 0; j < after.length; j++) {\n var id = null;\n try { id = String(after[j].id()); } catch (e) { continue; }\n if (!before[id]) {\n added = { id: id, name: prop((function (a) { return function () { return String(a.name()); }; })(after[j]), null) };\n break;\n }\n }\n if (!added) return err(\"ATTACH_FAILED\", \"Notes reported no new attachment on that note.\");\n\n return ok({ noteId: String(p.id), attachment: added });\n`,\n MUTATION,\n);\n\n/**\n * Delete notes.\n *\n * Notes moves them to Recently Deleted rather than destroying them, which is\n * what makes this safe enough to expose at all — but it is still gated behind an\n * explicit confirm at the tool layer.\n */\nexport const DELETE_NOTES = script(\n `\n var deleted = [];\n var missing = [];\n for (var i = 0; i < p.ids.length; i++) {\n var id = String(p.ids[i]);\n var note = null;\n try { note = N.notes.byId(id); } catch (e) { note = null; }\n if (!note) { missing.push(id); continue; }\n try { N.delete(note); deleted.push(id); } catch (e) { missing.push(id); }\n }\n return ok({ deleted: deleted, missing: missing, note: \"Deleted notes go to Recently Deleted.\" });\n`,\n MUTATION,\n);\n","/**\n * NoteRef — the one identifier any tool accepts or returns.\n *\n * Notes' own id is already globally unique and resolvable without a container,\n * unlike Mail's message ids: `x-coredata://<store>/ICNote/p<N>` addresses a note\n * outright, so this carries no folder and needs no lookup ladder.\n *\n * Wire format: n1:<coredata-id>\n *\n * The `n1:` prefix is deliberate. If the id scheme ever changes, a versioned\n * prefix makes that an additive change instead of a silent reinterpretation of\n * every ref already in a conversation.\n *\n * Deliberately NOT shared with Mail's `ref.ts`: that codec's `#(\\d+)$` and\n * `Number.isSafeInteger` both assume an integer id, which this is not.\n */\n\nimport { PreconditionError } from \"./errors.js\";\n\nexport const REF_VERSION = \"n1\";\n\n/** `x-coredata://<uuid>/ICNote/p123` — the primary key is the trailing p-number. */\nconst CORE_DATA_ID = /^x-coredata:\\/\\/[^/]+\\/ICNote\\/p(\\d+)$/;\n\nexport type NoteRef = {\n /** The full Core Data URI, which is what Apple Events resolves. */\n id: string;\n /** `Z_PK` in `ZICCLOUDSYNCINGOBJECT` — measured, see docs/notes.md. */\n primaryKey: number;\n};\n\nexport const encodeRef = (id: string): string => `${REF_VERSION}:${id}`;\n\nexport const decodeRef = (raw: string): NoteRef => {\n const m = /^([a-z0-9]+):(.+)$/.exec(raw);\n if (!m) {\n throw new PreconditionError(\n `Malformed note ref \"${raw}\". Refs come from the search and list tools — ` +\n `construct them from those results rather than by hand.`,\n { expected: `${REF_VERSION}:x-coredata://<store>/ICNote/p<N>` },\n );\n }\n const [, version, id] = m;\n if (version !== REF_VERSION) {\n throw new PreconditionError(\n `Unknown note ref version \"${version}\". This server issues \"${REF_VERSION}:\" refs.`,\n );\n }\n const pk = CORE_DATA_ID.exec(id ?? \"\")?.[1];\n if (!pk) {\n throw new PreconditionError(`Note ref \"${raw}\" does not carry a Core Data note id.`);\n }\n return { id: id as string, primaryKey: Number(pk) };\n};\n\n/**\n * `Z_PK` for an attachment id, which is shaped like a note id but for ICAttachment.\n *\n * The entity segment is not pinned: Apple Events hands back ICAttachment for a\n * file and ICInlineAttachment for an inline one, and both address a row in the\n * same table by the same trailing p-number.\n */\nconst CORE_DATA_ATTACHMENT_ID = /^x-coredata:\\/\\/[^/]+\\/IC\\w*Attachment\\w*\\/p(\\d+)$/;\n\nexport const attachmentPrimaryKey = (id: string): number | null => {\n const pk = CORE_DATA_ATTACHMENT_ID.exec(id)?.[1];\n return pk ? Number(pk) : null;\n};\n\n/** Build a ref from an index row, which only ever knows the primary key. */\nexport const refFromPrimaryKey = (storeUuid: string, primaryKey: number): string =>\n encodeRef(`x-coredata://${storeUuid}/ICNote/p${primaryKey}`);\n","/**\n * Minimal protobuf wire-format reader, enough to pull the body text out of a\n * `ZICNOTEDATA.ZDATA` blob.\n *\n * ## Why it does not hardcode Apple's schema\n *\n * The usual approach compiles Apple's unpublished `.proto` and reads\n * `document.note.note_text` by field number. That breaks the first time Apple\n * renumbers a field, and this project already budgets for the schema drifting\n * on every macOS release.\n *\n * So this walks the wire format structurally, and the path is *measured* rather\n * than assumed — see `NOTE_TEXT_PATH`.\n *\n * Wire format, for reference:\n * tag = varint; field = tag >> 3; wire type = tag & 7\n * 0 varint · 1 fixed64 · 2 length-delimited · 5 fixed32 · 3/4 groups (legacy)\n *\n * Pure and I/O-free: callers gunzip, callers decide what to do with the result.\n */\n\n/** Depth cap. Notes nests a handful deep; anything past this is a misparse. */\nconst MAX_DEPTH = 8;\n/** Below this, a \"string\" is more likely coincidence than text. */\nconst MIN_TEXT_LENGTH = 2;\n/** Above this share of control characters, the bytes are not text. */\nconst MAX_CONTROL_RATIO = 0.05;\n\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\n/** Tab, newline and carriage return are text; the rest of C0 and DEL are not. */\nconst isControl = (code: number): boolean =>\n (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) || code === 0x7f;\n\nexport type Varint = { value: number; next: number };\n\n/** Read a base-128 varint. Returns null rather than throwing on a truncated one. */\nexport const readVarint = (buf: Uint8Array, pos: number): Varint | null => {\n let value = 0;\n let shift = 0;\n let p = pos;\n while (p < buf.length) {\n const byte = buf[p] as number;\n p += 1;\n // Beyond 2^53 the arithmetic stops being exact, and no tag or length we\n // care about is that large, so bail rather than return a wrong number.\n if (shift > 45) return null;\n value += (byte & 0x7f) * 2 ** shift;\n if ((byte & 0x80) === 0) return { value, next: p };\n shift += 7;\n }\n return null;\n};\n\nexport type WireField = {\n field: number;\n wire: number;\n value?: number;\n from?: number;\n to?: number;\n};\n\n/**\n * Split a buffer into wire-format fields. Returns null if the bytes are not a\n * well-formed message ending exactly on the boundary — that \"exactly\" is what\n * makes this usable as a test for whether a blob is a nested message.\n */\nexport const parseFields = (\n buf: Uint8Array,\n start = 0,\n end: number = buf.length,\n): WireField[] | null => {\n const fields: WireField[] = [];\n let p = start;\n while (p < end) {\n const tag = readVarint(buf, p);\n if (!tag) return null;\n const field = Math.floor(tag.value / 8);\n const wire = tag.value % 8;\n if (field === 0) return null;\n p = tag.next;\n if (wire === 0) {\n const v = readVarint(buf, p);\n if (!v) return null;\n fields.push({ field, wire, value: v.value });\n p = v.next;\n } else if (wire === 2) {\n const len = readVarint(buf, p);\n if (!len) return null;\n const from = len.next;\n const to = from + len.value;\n if (to > end) return null;\n fields.push({ field, wire, from, to });\n p = to;\n } else if (wire === 1 || wire === 5) {\n const width = wire === 1 ? 8 : 4;\n if (p + width > end) return null;\n fields.push({ field, wire });\n p += width;\n } else {\n // Groups (3/4) are legacy and Notes does not use them; treating them as a\n // misparse is safer than guessing where the group ends.\n return null;\n }\n }\n return p === end ? fields : null;\n};\n\n/** Valid UTF-8 that is mostly not control characters. */\nconst asText = (bytes: Uint8Array): string | null => {\n let text: string;\n try {\n text = utf8.decode(bytes);\n } catch {\n return null;\n }\n if (text.length < MIN_TEXT_LENGTH) return null;\n let controls = 0;\n for (let i = 0; i < text.length; i += 1) {\n if (isControl(text.charCodeAt(i))) controls += 1;\n }\n return controls / text.length > MAX_CONTROL_RATIO ? null : text;\n};\n\nexport type StringCandidate = { path: string; text: string; length: number };\n\n/**\n * Every string in the blob, with the field path that reached it.\n *\n * A length-delimited field can be both a valid message and valid text, so both\n * readings are followed. That ambiguity is the normal case, not an error.\n */\nexport const collectStrings = (buf: Uint8Array, maxDepth = MAX_DEPTH): StringCandidate[] => {\n const out: StringCandidate[] = [];\n const walk = (start: number, end: number, path: number[], depth: number): void => {\n if (depth > maxDepth) return;\n const fields = parseFields(buf, start, end);\n if (!fields) return;\n for (const f of fields) {\n if (f.wire !== 2 || f.from === undefined || f.to === undefined) continue;\n const here = [...path, f.field];\n const text = asText(buf.subarray(f.from, f.to));\n if (text) out.push({ path: here.join(\".\"), text, length: text.length });\n walk(f.from, f.to, here, depth + 1);\n }\n };\n walk(0, buf.length, [], 0);\n return out;\n};\n\n/**\n * Where the body lives: `document(2) -> note(3) -> note_text(2)`.\n *\n * Measured, not assumed. Running the structural walk over 53 real notes and\n * checking each result against the same note's Apple Events plaintext, every\n * note read from this path matched exactly (53/53), and picking the longest\n * string instead matched only 27/53 — on the other 26 an attribute-run field\n * outran the body by ~60 characters. That failure is silent: it returns\n * plausible, wrong text rather than an error.\n */\nexport const NOTE_TEXT_PATH = \"2.3.2\";\n\nexport type DecodedNote = {\n text: string | null;\n path: string | null;\n /** `pinned` or `longest`. A shift to `longest` is the schema-drift signal. */\n via: \"pinned\" | \"longest\" | null;\n candidateCount: number;\n};\n\n/**\n * The note body.\n *\n * Reads the pinned path first and falls back to the longest string only if that\n * path is absent, reporting which route it took so drift is detectable rather\n * than silently wrong.\n */\nexport const extractNoteText = (\n buf: Uint8Array,\n preferPath: string | null = NOTE_TEXT_PATH,\n): DecodedNote => {\n const candidates = collectStrings(buf);\n const pinned = preferPath ? (candidates.find((c) => c.path === preferPath) ?? null) : null;\n const longest = candidates.toSorted((a, b) => b.length - a.length)[0] ?? null;\n const best = pinned ?? longest;\n return {\n text: best?.text ?? null,\n path: best?.path ?? null,\n via: best === null ? null : pinned ? \"pinned\" : \"longest\",\n candidateCount: candidates.length,\n };\n};\n","import type { DatabaseSync } from \"node:sqlite\";\nimport { gunzipSync } from \"node:zlib\";\n\nimport {\n columnsOf,\n CORE_DATA_EPOCH_OFFSET,\n escapeLike,\n fingerprintSchema,\n openReadOnly,\n parseBound,\n type Logger,\n type ReadOnlyMode,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { SchemaDriftError } from \"./errors.js\";\nimport { extractNoteText } from \"./protobuf.js\";\n\n/**\n * Read-only access to `NoteStore.sqlite`.\n *\n * Everything here is measured against a real store — see `docs/notes.md`. Three\n * facts do most of the work:\n *\n * 1. **`Z_ENT` alone is not \"a note the user sees\".** On the probed library it\n * matched 1191 rows against 921 notes reported by Apple Events, and the two\n * deletion columns explained none of the difference. `ZTITLE1 IS NOT NULL`\n * reconciles them **exactly** — verified by comparing `Z_PK` sets, not counts.\n * 2. **No column holds the body.** `ZTITLE1` is the title (100% coverage) and\n * `ZWIDGETSNIPPET` a ~86-character preview; the text lives in a gzipped\n * protobuf in `ZICNOTEDATA.ZDATA`.\n * 3. **Dates are Core Data seconds**, offset 978307200.\n */\n\n/** Every column this module reads, so a missing one is named rather than thrown at. */\nconst REQUIRED_COLUMNS = [\"Z_PK\", \"Z_ENT\", \"ZTITLE1\", \"ZMODIFICATIONDATE1\", \"ZCREATIONDATE1\"];\n\nexport type NoteRow = {\n primaryKey: number;\n title: string | null;\n snippet: string | null;\n modified: string | null;\n created: string | null;\n folderPk: number | null;\n locked: boolean;\n};\n\nexport type StoreCapabilities = {\n fingerprint: string;\n /**\n * The persistent store UUID, which is the middle segment of every note's\n * Apple Events id (`x-coredata://<uuid>/ICNote/p<N>`). Reading it here lets an\n * index row be turned into a ref without asking Notes anything.\n */\n storeUuid: string | null;\n /** `Z_ENT` for `ICNote`, looked up by name rather than hardcoded to 12. */\n noteEnt: number | null;\n /** Present on this schema, so the queries can use them. */\n has: {\n title: boolean;\n snippet: boolean;\n widgetSnippet: boolean;\n folder: boolean;\n locked: boolean;\n /** `ZMEDIA`, the ICAttachment -> ICMedia hop that leads to the bytes. */\n media: boolean;\n mediaFilename: boolean;\n /** `ZGENERATION1`, the inner directory. Absent on older schemas. */\n mediaGeneration: boolean;\n };\n deletionColumns: string[];\n missing: string[];\n};\n\nconst OPTIONAL = {\n snippet: \"ZSNIPPET\",\n widgetSnippet: \"ZWIDGETSNIPPET\",\n folder: \"ZFOLDER\",\n locked: \"ZISPASSWORDPROTECTED\",\n media: \"ZMEDIA\",\n mediaFilename: \"ZFILENAME\",\n mediaGeneration: \"ZGENERATION1\",\n} as const;\n\nexport const introspect = (db: DatabaseSync): StoreCapabilities => {\n const cols = columnsOf(db, \"ZICCLOUDSYNCINGOBJECT\");\n const missing = REQUIRED_COLUMNS.filter((c) => !cols.includes(c));\n\n let noteEnt: number | null = null;\n try {\n const row = db.prepare(\"SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICNote'\").get() as\n | { Z_ENT: number }\n | undefined;\n noteEnt = row?.Z_ENT ?? null;\n } catch {\n noteEnt = null;\n }\n if (noteEnt === null) missing.push(\"Z_PRIMARYKEY row for ICNote\");\n\n let storeUuid: string | null = null;\n try {\n const row = db.prepare(\"SELECT Z_UUID FROM Z_METADATA LIMIT 1\").get() as\n | { Z_UUID: string }\n | undefined;\n storeUuid = row?.Z_UUID ?? null;\n } catch {\n storeUuid = null;\n }\n\n return {\n fingerprint: fingerprintSchema(db),\n storeUuid,\n noteEnt,\n has: {\n title: cols.includes(\"ZTITLE1\"),\n snippet: cols.includes(OPTIONAL.snippet),\n widgetSnippet: cols.includes(OPTIONAL.widgetSnippet),\n folder: cols.includes(OPTIONAL.folder),\n locked: cols.includes(OPTIONAL.locked),\n media: cols.includes(OPTIONAL.media),\n mediaFilename: cols.includes(OPTIONAL.mediaFilename),\n mediaGeneration: cols.includes(OPTIONAL.mediaGeneration),\n },\n // Kept in the predicate even though the probed library had nothing in the\n // trash: that clause was never exercised, which makes it untested rather\n // than proven redundant, and a library with trashed notes is exactly where\n // its absence would show up as phantom results.\n deletionColumns: cols.filter((c) => /^Z(ISRECOVERINGFROMTRASH|MARKEDFORDELETION)$/.test(c)),\n missing,\n };\n};\n\nexport const assertUsable = (caps: StoreCapabilities): void => {\n if (caps.missing.length) {\n throw new SchemaDriftError(\n `NoteStore.sqlite is not the schema this server knows how to read (fingerprint ` +\n `${caps.fingerprint}). Missing: ${caps.missing.join(\", \")}. The index lane is unavailable; ` +\n `the Apple Events lane still works.`,\n );\n }\n};\n\nconst toIso = (coreDataSeconds: number | null): string | null => {\n if (coreDataSeconds === null || !Number.isFinite(coreDataSeconds)) return null;\n return new Date((coreDataSeconds + CORE_DATA_EPOCH_OFFSET) * 1000).toISOString();\n};\n\nexport type SearchFilters = {\n query?: string | undefined;\n folderPk?: number | undefined;\n modifiedAfter?: string | undefined;\n modifiedBefore?: string | undefined;\n limit: number;\n offset: number;\n};\n\nexport class NoteStore {\n readonly #db: DatabaseSync;\n readonly caps: StoreCapabilities;\n readonly mode: \"ro\" | \"immutable\";\n\n constructor(db: DatabaseSync, caps: StoreCapabilities, mode: \"ro\" | \"immutable\") {\n this.#db = db;\n this.caps = caps;\n this.mode = mode;\n }\n\n /**\n * The predicate for \"a note the user sees\".\n *\n * `Z_ENT` picks note rows; `ZTITLE1 IS NOT NULL` drops the title-less rows\n * that Apple Events does not report. Verified by set equality against the\n * 921 ids Apple Events returned — a matching count would not have been\n * evidence, as the first ZDATA decoder demonstrated.\n */\n #where(): string {\n const deletion = this.caps.deletionColumns\n .map((c) => ` AND (o.\"${c}\" IS NULL OR o.\"${c}\" = 0)`)\n .join(\"\");\n return `o.Z_ENT = ? AND o.ZTITLE1 IS NOT NULL${deletion}`;\n }\n\n #select(): string {\n const snippet = this.caps.has.widgetSnippet\n ? \"o.ZWIDGETSNIPPET\"\n : this.caps.has.snippet\n ? \"o.ZSNIPPET\"\n : \"NULL\";\n const folder = this.caps.has.folder ? \"o.ZFOLDER\" : \"NULL\";\n const locked = this.caps.has.locked ? \"o.ZISPASSWORDPROTECTED\" : \"0\";\n return `SELECT o.Z_PK AS pk, o.ZTITLE1 AS title, ${snippet} AS snippet,\n o.ZMODIFICATIONDATE1 AS modified, o.ZCREATIONDATE1 AS created,\n ${folder} AS folderPk, ${locked} AS locked\n FROM ZICCLOUDSYNCINGOBJECT o`;\n }\n\n #toRow(r: Record<string, unknown>): NoteRow {\n return {\n primaryKey: r.pk as number,\n title: (r.title as string | null) ?? null,\n snippet: (r.snippet as string | null) ?? null,\n modified: toIso((r.modified as number | null) ?? null),\n created: toIso((r.created as number | null) ?? null),\n folderPk: (r.folderPk as number | null) ?? null,\n locked: Boolean(r.locked),\n };\n }\n\n count(): number {\n const row = this.#db\n .prepare(`SELECT COUNT(*) AS c FROM ZICCLOUDSYNCINGOBJECT o WHERE ${this.#where()}`)\n .get(this.caps.noteEnt) as { c: number };\n return row.c;\n }\n\n /**\n * Title and snippet search.\n *\n * Deliberately not full text: no indexed column carries the body. Callers that\n * need body matching combine this with `bodyOf` or fall back to the Apple\n * Events bulk scan.\n */\n search(filters: SearchFilters): NoteRow[] {\n const clauses = [this.#where()];\n const params: unknown[] = [this.caps.noteEnt];\n\n if (filters.query) {\n const like = `%${escapeLike(filters.query)}%`;\n const snippetCol = this.caps.has.widgetSnippet\n ? \"o.ZWIDGETSNIPPET\"\n : this.caps.has.snippet\n ? \"o.ZSNIPPET\"\n : null;\n clauses.push(\n snippetCol\n ? `(o.ZTITLE1 LIKE ? ESCAPE '\\\\' OR ${snippetCol} LIKE ? ESCAPE '\\\\')`\n : `o.ZTITLE1 LIKE ? ESCAPE '\\\\'`,\n );\n params.push(like);\n if (snippetCol) params.push(like);\n }\n if (filters.folderPk !== undefined && this.caps.has.folder) {\n clauses.push(\"o.ZFOLDER = ?\");\n params.push(filters.folderPk);\n }\n // Through the shared grammar, which refuses what it cannot read. `Date.parse`\n // answered NaN, node:sqlite bound NaN as NULL, and `>= NULL` matched nothing\n // — an empty result presented as an answer. No tool reaches these today,\n // which is exactly why it would have gone unnoticed when one did.\n if (filters.modifiedAfter) {\n clauses.push(\"o.ZMODIFICATIONDATE1 >= ?\");\n params.push(\n parseBound(\"modifiedAfter\", filters.modifiedAfter, \"start\").getTime() / 1000 -\n CORE_DATA_EPOCH_OFFSET,\n );\n }\n if (filters.modifiedBefore) {\n // Exclusive, matching the next-day midnight a bare day resolves to.\n clauses.push(\"o.ZMODIFICATIONDATE1 < ?\");\n params.push(\n parseBound(\"modifiedBefore\", filters.modifiedBefore, \"end\").getTime() / 1000 -\n CORE_DATA_EPOCH_OFFSET,\n );\n }\n\n const sql = `${this.#select()} WHERE ${clauses.join(\" AND \")}\n ORDER BY o.ZMODIFICATIONDATE1 DESC LIMIT ? OFFSET ?`;\n params.push(filters.limit, filters.offset);\n return (this.#db.prepare(sql).all(...(params as never[])) as Record<string, unknown>[]).map(\n (r) => this.#toRow(r),\n );\n }\n\n byPrimaryKey(pk: number): NoteRow | null {\n const row = this.#db\n .prepare(`${this.#select()} WHERE ${this.#where()} AND o.Z_PK = ?`)\n .get(this.caps.noteEnt, pk) as Record<string, unknown> | undefined;\n return row ? this.#toRow(row) : null;\n }\n\n /**\n * The note body, decoded from the gzipped protobuf.\n *\n * Returns null for password-protected notes: their `ZDATA` is AES ciphertext,\n * flagged by a non-null `ZCRYPTOTAG`. No permission makes that readable.\n */\n bodyOf(pk: number): { text: string | null; via: string | null; encrypted: boolean } {\n const row = this.#db\n .prepare(\"SELECT ZDATA AS data, ZCRYPTOTAG AS tag FROM ZICNOTEDATA WHERE ZNOTE = ?\")\n .get(pk) as { data: Uint8Array | null; tag: Uint8Array | null } | undefined;\n if (!row?.data) return { text: null, via: null, encrypted: false };\n if (row.tag) return { text: null, via: null, encrypted: true };\n\n const blob = Buffer.from(row.data);\n if (blob[0] !== 0x1f || blob[1] !== 0x8b) return { text: null, via: null, encrypted: false };\n let inflated: Buffer;\n try {\n inflated = gunzipSync(blob);\n } catch {\n return { text: null, via: null, encrypted: false };\n }\n const decoded = extractNoteText(inflated);\n return { text: decoded.text, via: decoded.via, encrypted: false };\n }\n\n /**\n * Where an attachment's bytes live, resolved through ICMedia.\n *\n * ICAttachment carries NO path of its own — not a filename, not a directory.\n * It points at an ICMedia row through `ZMEDIA`, and that row holds the two\n * directory names and the filename:\n *\n * Accounts/<account>/Media/<ZIDENTIFIER>/<ZGENERATION1>/<ZFILENAME>\n *\n * Established against a real store (see `scripts/probe-notes-media.mjs`): the\n * identifier directory contains only the generation directory, and only the\n * inner one holds bytes — 657 of each on the probed library, exactly paired.\n * That is why the id Apple Events returns cannot locate anything by itself,\n * and why matching a directory against it never worked.\n *\n * `ZGENERATION1` is treated as optional rather than assumed: the caller falls\n * back to the identifier directory when it is absent, so a schema without the\n * generation level degrades instead of failing.\n */\n mediaFor(attachmentPk: number): {\n identifier: string;\n generation: string | null;\n filename: string | null;\n } | null {\n if (!this.caps.has.media) return null;\n const generation = this.caps.has.mediaGeneration ? \"m.ZGENERATION1\" : \"NULL\";\n const filename = this.caps.has.mediaFilename ? \"m.ZFILENAME\" : \"NULL\";\n const row = this.#db\n .prepare(\n `SELECT m.ZIDENTIFIER AS identifier, ${generation} AS generation, ${filename} AS filename ` +\n `FROM ZICCLOUDSYNCINGOBJECT a ` +\n `JOIN ZICCLOUDSYNCINGOBJECT m ON m.Z_PK = a.ZMEDIA ` +\n `WHERE a.Z_PK = ?`,\n )\n .get(attachmentPk) as\n | { identifier: string | null; generation: string | null; filename: string | null }\n | undefined;\n if (!row?.identifier) return null;\n return {\n identifier: row.identifier,\n generation: row.generation ?? null,\n filename: row.filename ?? null,\n };\n }\n\n close(): void {\n this.#db.close();\n }\n}\n\nexport const openStore = (path: string, mode: ReadOnlyMode, logger?: Logger): NoteStore => {\n const {\n db,\n mode: opened,\n validated: caps,\n } = openReadOnly<StoreCapabilities>(path, mode, {\n envVar: \"APPLE_NOTES_INDEX_MODE\",\n label: \"Notes' store\",\n hint: \"If this is a permission error, grant Full Disk Access to the app running this server.\",\n validate: (conn) => {\n const found = introspect(conn);\n assertUsable(found);\n return found;\n },\n fatal: (err) => err instanceof SchemaDriftError,\n onFallback: () =>\n logger?.warn?.(\"opened the store with immutable=1; results may omit notes still in the -wal\"),\n });\n return new NoteStore(db, caps, opened);\n};\n","import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, join, resolve, sep } from \"node:path\";\n\nimport {\n createOsascriptRunner,\n withBusyRetry,\n type Logger,\n type OsascriptRunner,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport type { Config } from \"../config.js\";\nimport { NoteLockedError, NoteNotFoundError, NOTES_SURFACE, PreconditionError } from \"./errors.js\";\nimport {\n BULK_NOTES,\n BULK_PLAINTEXT,\n GET_NOTE_BODIES,\n LIST_ACCOUNTS,\n LIST_ATTACHMENTS,\n LIST_FOLDERS,\n} from \"./jxa/read.js\";\nimport { ADD_ATTACHMENT, CREATE_NOTE, DELETE_NOTES, MOVE_NOTE, UPDATE_NOTE } from \"./jxa/write.js\";\nimport { locateStore, type LocateResult } from \"./locate.js\";\nimport { attachmentPrimaryKey, decodeRef, encodeRef, refFromPrimaryKey } from \"./ref.js\";\nimport { NoteStore, openStore, type NoteRow } from \"./store.js\";\n\nexport type LaneStatus = {\n applescript: \"live\" | \"unavailable\";\n index: \"live\" | \"unavailable\" | \"disabled\";\n indexMode: string | null;\n storeFingerprint: string | null;\n reason: string | null;\n};\n\nexport type NoteAccount = {\n id: string | null;\n name: string | null;\n defaultFolder: string | null;\n folderCount: number;\n noteCount: number;\n};\n\nexport type NoteFolder = {\n id: string | null;\n name: string | null;\n accountId: string | null;\n accountName: string | null;\n depth: number;\n shared: boolean;\n noteCount: number | null;\n};\n\nexport type NoteSummary = {\n ref: string;\n title: string | null;\n snippet: string | null;\n folder: string | null;\n account: string | null;\n modified: string | null;\n created: string | null;\n locked: boolean;\n source: \"index\" | \"apple-events\";\n};\n\n/**\n * A page of notes, plus the two facts a bare array had no room for: which lane\n * answered, and whether anything was left behind.\n *\n * `hasMore` is about the PAGE — more notes matched than `limit` allowed. `note`\n * is about the LANE: the Apple Events bulk read is capped independently of\n * `limit`, so a caller asking for 500 could be handed 200 with nothing saying\n * which of the two bounds stopped the list. Only the second one needs prose,\n * because only the second one is fixable by the person reading it.\n */\nexport type NoteListing = {\n notes: NoteSummary[];\n source: \"index\" | \"apple-events\";\n hasMore: boolean;\n note?: string;\n};\n\ntype BodyCache = {\n at: number;\n source: \"index\" | \"apple-events\";\n /** Note id -> body text. */\n texts: Map<string, string>;\n};\n\nexport type AppleNotesClientOptions = {\n config: Config;\n logger?: Logger | undefined;\n /** Injected by tests so nothing spawns a process or touches a real Notes. */\n osascript?: OsascriptRunner | undefined;\n};\n\n/**\n * The Notes client.\n *\n * Two lanes, and which one answers depends on what was granted:\n *\n * - **Apple Events** — accounts, folders, every mutation, and bodies. Always the\n * authority: after a write, the result is what Notes re-read.\n * - **Index** — `NoteStore.sqlite`, for search and metadata at a scale Apple\n * Events cannot reach.\n *\n * The discipline the measurements impose is in `jxa/core.ts`: bulk array fetches\n * only, never `whose`, never a property read per note.\n */\nexport class AppleNotesClient {\n readonly config: Config;\n readonly runner: OsascriptRunner;\n readonly #logger: Logger | undefined;\n #store: NoteStore | null = null;\n #storeTried = false;\n #bodies: BodyCache | null = null;\n\n constructor(opts: AppleNotesClientOptions) {\n this.config = opts.config;\n this.#logger = opts.logger;\n this.runner =\n opts.osascript ??\n createOsascriptRunner({\n osascriptPath: opts.config.osascriptPath,\n timeoutMs: opts.config.osascriptTimeoutMs,\n surface: NOTES_SURFACE,\n logger: opts.logger,\n });\n }\n\n locate(): LocateResult {\n return locateStore({ storePath: this.config.storePath });\n }\n\n /** Open the index once and remember the outcome, including the failure. */\n index(): NoteStore | null {\n if (this.#storeTried) return this.#store;\n this.#storeTried = true;\n if (this.config.indexMode === \"off\") return null;\n try {\n this.#store = openStore(this.locate().storePath, this.config.indexMode, this.#logger);\n } catch (err) {\n this.#logger?.debug?.(\"index lane unavailable\", err);\n this.#store = null;\n }\n return this.#store;\n }\n\n /**\n * Probe both lanes.\n *\n * Apple Events is probed twice with a gap: the first event is what triggers\n * the Automation prompt, and it can return failure while the user is still\n * deciding — reporting \"denied\" then would be wrong.\n */\n async lanes(): Promise<LaneStatus> {\n let applescript: LaneStatus[\"applescript\"] = \"unavailable\";\n for (let attempt = 0; attempt < 2; attempt += 1) {\n try {\n await this.runner.run(LIST_ACCOUNTS);\n applescript = \"live\";\n break;\n } catch (err) {\n this.#logger?.debug?.(\"applescript probe failed\", err);\n if (attempt === 0) await new Promise((r) => setTimeout(r, 400));\n }\n }\n\n const located = this.locate();\n const store = this.index();\n return {\n applescript,\n index: this.config.indexMode === \"off\" ? \"disabled\" : store ? \"live\" : \"unavailable\",\n indexMode: store?.mode ?? null,\n storeFingerprint: store?.caps.fingerprint ?? null,\n reason: store ? null : located.reason,\n };\n }\n\n #allowed(accountName: string | null): boolean {\n const list = this.config.accounts;\n if (!list.length) return true;\n return Boolean(accountName && list.some((a) => a.toLowerCase() === accountName.toLowerCase()));\n }\n\n /**\n * Whether the index can answer this request FAITHFULLY.\n *\n * Two things it cannot do, and both used to be ignored rather than declined.\n *\n * The store has no readable account name on a note row — `#fromIndex` sets\n * `account: null` — so an account allowlist cannot be applied to index rows.\n * Answering from the index anyway ignored the one setting whose whole job is\n * limiting what gets read, and did so ONLY on machines with Full Disk Access,\n * which is exactly where it matters.\n *\n * And `folder` was accepted, resolved to `undefined` by a stub whose comment\n * claimed the opposite, and dropped: `list_notes({folder: \"Projects\"})`\n * returned the newest notes from every folder.\n *\n * Reminders reached the same conclusion first, in `#indexCanAnswer` there.\n */\n #indexCanAnswer(opts: { folder?: string | undefined } = {}): boolean {\n return this.config.accounts.length === 0 && !opts.folder;\n }\n\n async accounts(): Promise<NoteAccount[]> {\n const all = await withBusyRetry(() => this.runner.run<NoteAccount[]>(LIST_ACCOUNTS));\n return all.filter((a) => this.#allowed(a.name));\n }\n\n async folders(): Promise<NoteFolder[]> {\n const all = await withBusyRetry(() => this.runner.run<NoteFolder[]>(LIST_FOLDERS));\n return all.filter((f) => this.#allowed(f.accountName));\n }\n\n /**\n * Note metadata.\n *\n * Prefers the index — it holds the same fields, costs no Apple Event, and\n * keeps working as a library grows past the point where a bulk fetch stops\n * being instant.\n */\n async listNotes(opts: { folder?: string | undefined; limit: number }): Promise<NoteListing> {\n const store = this.#indexCanAnswer(opts) ? this.index() : null;\n if (store?.caps.storeUuid) {\n // One row past the page, so a full page can be told from an exhausted\n // library. SQL is asked for it rather than counted separately: a COUNT\n // over the whole table costs more than the extra row on every call.\n const rows = store.search({ limit: opts.limit + 1, offset: 0 });\n const uuid = store.caps.storeUuid;\n return {\n notes: rows.slice(0, opts.limit).map((r) => this.#fromIndex(r, uuid)),\n source: \"index\",\n hasMore: rows.length > opts.limit,\n };\n }\n return this.#bulkNotes(opts);\n }\n\n #fromIndex(row: NoteRow, storeUuid: string): NoteSummary {\n return {\n ref: refFromPrimaryKey(storeUuid, row.primaryKey),\n title: row.title,\n snippet: row.snippet,\n folder: null,\n account: null,\n modified: row.modified,\n created: row.created,\n locked: row.locked,\n source: \"index\",\n };\n }\n\n async #bulkNotes(opts: { folder?: string | undefined; limit: number }): Promise<NoteListing> {\n const data = await withBusyRetry(() =>\n this.runner.run<{\n count: number;\n notes: {\n id: string;\n name: string | null;\n modified: string | null;\n created: string | null;\n locked: boolean;\n folder: string | null;\n account: string | null;\n }[];\n }>(BULK_NOTES),\n );\n const matched = data.notes\n .filter((n) => this.#allowed(n.account))\n .filter((n) => !opts.folder || n.folder === opts.folder);\n const cap = Math.min(opts.limit, this.config.degradedMaxNotes);\n return {\n notes: matched.slice(0, cap).map((n) => ({\n ref: encodeRef(n.id),\n title: n.name,\n snippet: null,\n folder: n.folder,\n account: n.account,\n modified: n.modified,\n created: n.created,\n locked: n.locked,\n source: \"apple-events\" as const,\n })),\n source: \"apple-events\",\n hasMore: matched.length > cap,\n // Only when the LANE is what stopped the list — the cap sitting below a\n // larger `limit`, with more notes behind it. A caller whose own limit ran\n // out first is not being shortchanged and does not need telling.\n ...(matched.length > cap && this.config.degradedMaxNotes < opts.limit\n ? {\n note:\n `The Apple Events lane is capped at ${this.config.degradedMaxNotes} notes and you ` +\n `asked for ${opts.limit}, so this list stops short of what matched. Grant Full ` +\n `Disk Access to read Notes' own index instead, or raise ` +\n `APPLE_NOTES_DEGRADED_MAX_NOTES.`,\n }\n : {}),\n };\n }\n\n /**\n * Body text for every note, cached.\n *\n * A TTL rather than invalidation, deliberately: checking whether the cache is\n * stale costs *more* over Apple Events (fetching ids and modification dates,\n * 128ms) than simply redoing the scan (97ms). There is no cheap freshness\n * check to be had, so bounded staleness is the honest trade.\n */\n async #allBodies(): Promise<BodyCache> {\n const now = Date.now();\n if (this.#bodies && now - this.#bodies.at < this.config.searchCacheTtlMs) return this.#bodies;\n\n const store = this.#indexCanAnswer() ? this.index() : null;\n const texts = new Map<string, string>();\n if (store?.caps.storeUuid) {\n const uuid = store.caps.storeUuid;\n for (const row of store.search({ limit: 100_000, offset: 0 })) {\n const body = store.bodyOf(row.primaryKey);\n if (body.text) texts.set(refFromPrimaryKey(uuid, row.primaryKey), body.text);\n }\n this.#bodies = { at: now, source: \"index\", texts };\n return this.#bodies;\n }\n\n const bulk = await withBusyRetry(() =>\n this.runner.run<{ ids: string[]; texts: (string | null)[] }>(BULK_PLAINTEXT),\n );\n bulk.ids.forEach((id, i) => {\n const text = bulk.texts[i];\n if (text) texts.set(encodeRef(String(id)), String(text));\n });\n this.#bodies = { at: now, source: \"apple-events\", texts };\n return this.#bodies;\n }\n\n /**\n * Search.\n *\n * `title` uses the index's title and snippet columns, which is a SQL query.\n * `full` needs the body, which no indexed column carries — so it decodes the\n * gzipped protobuf when the index is available and falls back to the Apple\n * Events bulk scan when it is not. Either way the match happens in JS over a\n * cached corpus, because `whose` is 6.9x slower than fetching and filtering.\n */\n async searchNotes(opts: {\n query: string;\n scope: \"title\" | \"full\";\n limit: number;\n offset: number;\n }): Promise<NoteListing & { scope: string }> {\n const store = this.#indexCanAnswer() ? this.index() : null;\n\n if (opts.scope === \"title\" && store?.caps.storeUuid) {\n const rows = store.search({\n query: opts.query,\n limit: opts.limit + 1,\n offset: opts.offset,\n });\n const uuid = store.caps.storeUuid;\n return {\n notes: rows.slice(0, opts.limit).map((r) => this.#fromIndex(r, uuid)),\n source: \"index\",\n hasMore: rows.length > opts.limit,\n scope: \"title+snippet\",\n };\n }\n\n const corpus = await this.#allBodies();\n const needle = opts.query.toLowerCase();\n const hits: string[] = [];\n for (const [ref, text] of corpus.texts) {\n if (text.toLowerCase().includes(needle)) hits.push(ref);\n }\n const page = hits.slice(opts.offset, opts.offset + opts.limit);\n\n const summaries = await Promise.all(page.map((ref) => this.getNote(ref, { body: false })));\n return {\n notes: summaries.map((s) => s.summary),\n source: corpus.source,\n // Exact, not a guess: the corpus behind a full-text search is every note\n // in both lanes, so `hits` is the whole match set and not a page of it.\n hasMore: hits.length > opts.offset + opts.limit,\n scope: \"full-text\",\n };\n }\n\n async getNote(\n ref: string,\n opts: { body?: boolean } = {},\n ): Promise<{ summary: NoteSummary; body: string | null; bodySource: string | null }> {\n const decoded = decodeRef(ref);\n const store = this.#indexCanAnswer() ? this.index() : null;\n\n if (store?.caps.storeUuid) {\n const row = store.byPrimaryKey(decoded.primaryKey);\n if (row) {\n const summary = this.#fromIndex(row, store.caps.storeUuid);\n if (opts.body === false) return { summary, body: null, bodySource: null };\n if (row.locked) throw new NoteLockedError(ref);\n const body = store.bodyOf(decoded.primaryKey);\n if (body.encrypted) throw new NoteLockedError(ref);\n if (body.text !== null) {\n return { summary, body: this.#truncate(body.text), bodySource: `index:${body.via}` };\n }\n }\n }\n\n const [found] = await withBusyRetry(() =>\n this.runner.run<\n {\n id: string;\n found: boolean;\n locked: boolean;\n name: string | null;\n plaintext: string | null;\n modified: string | null;\n created: string | null;\n }[]\n >(GET_NOTE_BODIES, { ids: [decoded.id] }),\n );\n if (!found?.found) throw new NoteNotFoundError(ref);\n if (found.locked) throw new NoteLockedError(ref);\n return {\n summary: {\n ref,\n title: found.name,\n snippet: null,\n folder: null,\n account: null,\n modified: found.modified,\n created: found.created,\n locked: false,\n source: \"apple-events\",\n },\n body: found.plaintext === null ? null : this.#truncate(found.plaintext),\n bodySource: \"apple-events\",\n };\n }\n\n #truncate(text: string): string {\n const max = this.config.bodyMaxBytes;\n if (Buffer.byteLength(text, \"utf8\") <= max) return text;\n return `${text.slice(0, max)}\\n\\n[truncated at ${max} bytes; raise APPLE_NOTES_BODY_MAX_BYTES]`;\n }\n\n async attachments(ref: string): Promise<\n {\n id: string | null;\n name: string | null;\n url: string | null;\n contentIdentifier: string | null;\n }[]\n > {\n const decoded = decodeRef(ref);\n const listed = await withBusyRetry(() =>\n this.runner.run<\n {\n id: string | null;\n name: string | null;\n url: string | null;\n contentIdentifier: string | null;\n }[]\n >(LIST_ATTACHMENTS, { id: decoded.id }),\n );\n // Notes can enumerate the same attachment more than once — measured after a\n // `make`, where one new attachment appeared twice in the element collection\n // and `count of attachments` said 3 for 2 real ones. Passing that through\n // would have a caller save the same file twice, or believe a note holds\n // attachments it does not.\n const seen = new Set<string>();\n return listed.filter((a) => {\n if (!a.id) return true;\n if (seen.has(a.id)) return false;\n seen.add(a.id);\n return true;\n });\n }\n\n /**\n * Save an attachment's bytes.\n *\n * The scripting dictionary exposes `name`, `id`, `URL` and `content\n * identifier` but **no filesystem path**, so the bytes can only come from the\n * media directory — which is Full Disk Access territory. Without it this\n * reports what is missing rather than failing obscurely.\n */\n async saveAttachment(\n ref: string,\n attachmentId: string,\n targetDir?: string,\n opts: { overwrite?: boolean } = {},\n ): Promise<{\n path: string;\n bytes: number;\n }> {\n const located = this.locate();\n if (!located.readable) {\n throw new PreconditionError(\n \"Attachment bytes need Full Disk Access: the scripting dictionary carries no file path \" +\n \"for attachments, so they can only be read from the Notes media directory. \" +\n \"Call apple_notes_diagnostics for the exact pane to open.\",\n );\n }\n const list = await this.attachments(ref);\n const meta = list.find((a) => a.id === attachmentId);\n if (!meta) throw new PreconditionError(`No attachment ${attachmentId} on note ${ref}.`);\n\n // ICAttachment holds no path, so the directory names come from the ICMedia\n // row it points at. That needs the index; without it there is nothing to\n // resolve against and saying so beats walking the tree for a name that is\n // not there.\n const pk = attachmentPrimaryKey(attachmentId);\n const media = pk === null ? null : (this.index()?.mediaFor(pk) ?? null);\n if (!media) {\n throw new PreconditionError(\n `Cannot resolve where ${attachmentId} is stored. Its media record is missing from ` +\n `NoteStore.sqlite, so there is no directory name to look for.`,\n );\n }\n\n const source = this.#findMedia(located.mediaRoot, media);\n if (!source) {\n throw new PreconditionError(\n `Found the attachment's metadata but no file for it under ${located.mediaRoot}.`,\n );\n }\n\n // The configured directory is the confinement boundary, not a default that\n // `targetDir` replaces: an override may only select a subdirectory of it.\n // Resolving the override *against* the root means a relative path lands\n // inside it and an absolute one is caught by the check below.\n const root = resolve(this.config.attachmentDir);\n const dir = targetDir ? resolve(root, targetDir) : root;\n if (dir !== root && !dir.startsWith(root + sep)) {\n throw new PreconditionError(\n `Refusing to write outside ${root}. Set APPLE_NOTES_ATTACHMENT_DIR to change the destination.`,\n );\n }\n // basename() first: the name comes from note content, which is\n // attacker-controlled in exactly the way path traversal needs.\n const name = basename(media.filename ?? meta.name ?? basename(source));\n const target = resolve(join(dir, name));\n if (target !== join(dir, name) || !target.startsWith(dir + sep)) {\n throw new PreconditionError(`Refusing to write outside ${dir}.`);\n }\n if (existsSync(target) && !opts.overwrite) {\n throw new PreconditionError(`${target} already exists; refusing to overwrite it.`);\n }\n const bytes = readFileSync(source);\n mkdirSync(dir, { recursive: true });\n writeFileSync(target, bytes, { mode: 0o600 });\n return { path: target, bytes: bytes.length };\n }\n\n /**\n * The file for an ICMedia row.\n *\n * Layout, verified on a real store:\n *\n * Accounts/<account>/Media/<identifier>/<generation>/<filename>\n *\n * The account directory is not known here, so each one is tried — there are a\n * handful, not thousands. Every segment after the identifier is treated as a\n * hint rather than a certainty: the generation directory is skipped when the\n * row has none, and a directory holding a single file answers even when the\n * filename on disk differs from the one recorded.\n */\n #findMedia(\n root: string,\n media: { identifier: string; generation: string | null; filename: string | null },\n ): string | null {\n const accounts = (() => {\n try {\n return readdirSync(root);\n } catch {\n return [];\n }\n })();\n\n const fileIn = (dir: string): string | null => {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return null;\n }\n const files = entries.filter((e) => {\n try {\n return statSync(join(dir, e)).isFile();\n } catch {\n return false;\n }\n });\n if (media.filename && files.includes(media.filename)) return join(dir, media.filename);\n // A single file is unambiguous whatever it is called; several are not, and\n // guessing among them would hand back the wrong bytes silently.\n return files.length === 1 ? join(dir, files[0] as string) : null;\n };\n\n for (const account of accounts) {\n const base = join(root, account, \"Media\", media.identifier);\n try {\n if (!statSync(base).isDirectory()) continue;\n } catch {\n continue;\n }\n const candidates = media.generation ? [join(base, media.generation), base] : [base];\n for (const dir of candidates) {\n const hit = fileIn(dir);\n if (hit) return hit;\n }\n // The generation directory is named in the row, but fall back to whatever\n // single subdirectory is actually there rather than failing on a rename.\n const subdirs = (() => {\n try {\n return readdirSync(base).filter((e) => statSync(join(base, e)).isDirectory());\n } catch {\n return [];\n }\n })();\n if (subdirs.length === 1) {\n const hit = fileIn(join(base, subdirs[0] as string));\n if (hit) return hit;\n }\n }\n return null;\n }\n\n // ─── writes ────────────────────────────────────────────────────────────────\n\n async createNote(opts: { title?: string; body?: string; folderId?: string }): Promise<unknown> {\n this.#bodies = null;\n return withBusyRetry(() => this.runner.run(CREATE_NOTE, opts));\n }\n\n /**\n * Attach a file to a note.\n *\n * The path is resolved and checked here rather than in JXA: Notes reports a\n * missing or unreadable file as a generic refusal, which would surface as\n * \"Notes refused the file\" for what is really a typo.\n */\n async addAttachment(ref: string, filePath: string): Promise<unknown> {\n const decoded = decodeRef(ref);\n const resolved = resolve(filePath);\n let stats;\n try {\n stats = statSync(resolved);\n } catch {\n throw new PreconditionError(`No file at ${resolved}.`);\n }\n if (!stats.isFile()) {\n throw new PreconditionError(`${resolved} is not a file.`);\n }\n this.#bodies = null;\n return withBusyRetry(() => this.runner.run(ADD_ATTACHMENT, { id: decoded.id, path: resolved }));\n }\n\n async updateNote(ref: string, body: string, mode: \"replace\" | \"append\"): Promise<unknown> {\n this.#bodies = null;\n const decoded = decodeRef(ref);\n return withBusyRetry(() => this.runner.run(UPDATE_NOTE, { id: decoded.id, body, mode }));\n }\n\n async moveNote(ref: string, folderId: string): Promise<unknown> {\n this.#bodies = null;\n const decoded = decodeRef(ref);\n return withBusyRetry(() => this.runner.run(MOVE_NOTE, { id: decoded.id, folderId }));\n }\n\n async deleteNotes(refs: string[]): Promise<unknown> {\n this.#bodies = null;\n const ids = refs.map((r) => decodeRef(r).id);\n return withBusyRetry(() => this.runner.run(DELETE_NOTES, { ids }));\n }\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport {\n BaseConfigSchema,\n parseBool,\n parseConfig,\n parseIntOpt,\n parseList,\n trimmed,\n} from \"@mgcrea/mcp-apple-core\";\nimport { z } from \"zod\";\n\n/**\n * Configuration is environment-only — this server holds no secret at all, its\n * access is the macOS permission the user granted.\n *\n * `allowWrites`, `debug`, `osascriptPath`, `osascriptTimeoutMs` and `maxResults`\n * come from `BaseConfigSchema`.\n */\nconst ConfigSchema = BaseConfigSchema.extend({\n /**\n * Account allowlist (names or ids). Empty means every account.\n *\n * `allowWrites` gates mutation, but the larger blast radius is *reading* a\n * person's whole notebook, so this is the read-side control.\n */\n accounts: z.array(z.string().min(1)).default([]),\n /** Explicit NoteStore.sqlite path. Bypasses discovery — for tests and forensic copies. */\n storePath: z.string().optional(),\n indexMode: z.enum([\"auto\", \"ro\", \"immutable\", \"off\"]).default(\"auto\"),\n /**\n * Cap for the Apple Events listing lane. Generous compared to Mail's because\n * a bulk property fetch here is one round trip at ~0.1ms per note, not 42ms.\n */\n degradedMaxNotes: z.number().int().min(1).max(1_000).default(200),\n bodyMaxBytes: z.number().int().min(1_024).max(10_000_000).default(262_144),\n /**\n * The confinement boundary for save_attachment, not merely its default: the\n * tool's `directory` argument may select a subdirectory of this, never escape\n * it. Enforced lexically with `resolve()` + `basename()`, not `realpath()` —\n * the destination may not exist yet.\n */\n attachmentDir: z.string().default(join(homedir(), \"Downloads\")),\n /**\n * How long the Apple Events plaintext cache stays warm.\n *\n * Measured: fetching ids + modification dates to check freshness costs *more*\n * (128ms) than simply re-scanning every body (97ms), so there is no cheap\n * invalidation to be had. A TTL with bounded staleness is the only sensible\n * trade when the index lane is unavailable.\n */\n searchCacheTtlMs: z.number().int().min(0).max(3_600_000).default(60_000),\n}).strict();\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * `env` is a parameter with a default so tests are hermetic — they pass their\n * own object rather than mutating (and having to restore) process.env.\n */\nexport const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config =>\n parseConfig(ConfigSchema, {\n allowWrites: parseBool(env.APPLE_NOTES_ALLOW_WRITES),\n exposePrompts: parseBool(env.APPLE_NOTES_EXPOSE_PROMPTS),\n lazyTools: parseBool(env.APPLE_NOTES_LAZY_TOOLS),\n debug: parseBool(env.APPLE_NOTES_DEBUG),\n accounts: parseList(env.APPLE_NOTES_ACCOUNTS),\n storePath: trimmed(env.APPLE_NOTES_STORE),\n indexMode: trimmed(env.APPLE_NOTES_INDEX_MODE),\n osascriptPath: trimmed(env.APPLE_NOTES_OSASCRIPT_PATH),\n osascriptTimeoutMs: parseIntOpt(env.APPLE_NOTES_OSASCRIPT_TIMEOUT_MS),\n maxResults: parseIntOpt(env.APPLE_NOTES_MAX_RESULTS),\n degradedMaxNotes: parseIntOpt(env.APPLE_NOTES_DEGRADED_MAX_NOTES),\n bodyMaxBytes: parseIntOpt(env.APPLE_NOTES_BODY_MAX_BYTES),\n attachmentDir: trimmed(env.APPLE_NOTES_ATTACHMENT_DIR),\n searchCacheTtlMs: parseIntOpt(env.APPLE_NOTES_SEARCH_CACHE_TTL_MS),\n });\n","/**\n * The Notes operating manual, served as `cupertino://notes/guide` and embedded\n * ahead of every Notes prompt.\n *\n * Static by design — see the same note in the Mail guide. Nothing here may\n * depend on a probe: account names, folder names and permission verdicts belong\n * to `cupertino://notes/inventory` and `cupertino://notes/diagnostics`, both of\n * which are allowed to fail.\n */\nexport const NOTES_GUIDE = `# Apple Notes — how to drive this server\n\n## Refs are opaque\n\nNotes come back with a \\`ref\\` like \\`n1:x-coredata://…/ICNote/p123\\`. Pass it back\nverbatim; never build or parse one, and never carry it to another surface.\n\n## Which tool, under which constraint\n\n- **Finding anything by content** is \\`apple_notes_search_notes\\`.\n- **\\`apple_notes_list_notes\\`** is a capped listing of a folder in date order —\n for \"what did I write last week\", not for \"where did I write about X\".\n- **\\`apple_notes_get_note\\`** returns the body. Search results are summaries;\n do not quote a note you have only seen the title of.\n- **Folders** are \\`apple_notes_list_folders\\`, and folder names must match what\n Notes actually calls them. Read the inventory rather than guessing.\n\n## Locked notes are not empty notes\n\nA password-protected note is encrypted at rest and comes back with its text\nnull. **No permission unlocks it** — not Full Disk Access, not Automation.\nReport it as locked. Reporting it as empty invents an absence of content that\nmay be the exact content someone is looking for.\n\nWithout Full Disk Access the index lane is gone and search falls back to an\nApple Events scan: correct, but linear in the size of the library. A slow\nsearch on a big library is this, not a hang.\n\nAttachment bytes need Full Disk Access outright — the scripting dictionary\ncarries no file path — so \\`apple_notes_save_attachment\\` fails without it while\nthe rest of the server keeps working.\n\n## Writes\n\nMutating tools exist only when writes are enabled. If you cannot see\n\\`apple_notes_create_note\\`, writes are off; say so rather than describing a note\nyou did not create.\n\n\\`apple_notes_update_note\\` **replaces** a note's body. It does not append. Read\nthe note first and write back the whole thing, or the rest of it is gone.\n`;\n","import {\n promptArg,\n registerWorkflowPrompt,\n requiredPromptArg,\n type PromptContext,\n} from \"@mgcrea/mcp-apple-core\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { NOTES_GUIDE } from \"./guide.js\";\n\nconst CTX: PromptContext = { surface: \"notes\", guide: NOTES_GUIDE };\n\n/**\n * Notes' workflow prompts. Both encode an ordering no single tool description\n * can hold: search results are summaries and must be opened before they are\n * quoted, and a capture must look before it writes or it makes a duplicate.\n */\nexport const registerPrompts = (server: McpServer, allowWrites: boolean): void => {\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_notes_find\",\n title: \"Find notes about something\",\n description:\n \"Search the note library for everything on a topic and summarise what is actually written \" +\n \"there — opening the notes rather than answering from titles.\",\n argsSchema: {\n about: requiredPromptArg('What to look for, e.g. \"the Atlas pricing decision\".'),\n folder: promptArg(\"Restrict to one folder, exactly as Notes spells it. Omit to span all.\"),\n },\n build: ({ about, folder }) => `Find what my notes say about: ${about}${\n folder ? `\\n\\nRestrict this to the ${folder} folder.` : \"\"\n }\n\n1. \\`apple_notes_search_notes\\` for the distinctive words${\n folder ? \", scoped to that folder\" : \"\"\n }. Try more than one phrasing before concluding there is nothing — these are\n personal notes, so the user's own shorthand is often what is written.\n2. **Open the promising ones with \\`apple_notes_get_note\\`.** Search returns\n summaries; a title tells you a note exists, not what it says. Never quote or\n summarise a note you have only seen the title of.\n3. Synthesise across the notes rather than listing them one by one. Say what was\n decided, what is still open, and where the notes contradict each other — that\n last one is usually the useful part.\n\nReport any locked note you hit by name and say it is password-protected. It is\nnot empty, and nothing available here can open it.`,\n });\n\n if (!allowWrites) return;\n\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_notes_capture\",\n title: \"Capture something into Notes\",\n description:\n \"Write something into the note library, checking first whether it belongs in a note that \" +\n \"already exists. Requires writes to be enabled.\",\n argsSchema: {\n what: requiredPromptArg(\"What to capture — the content, or where to get it from.\"),\n folder: promptArg(\"Folder to file it under, exactly as Notes spells it.\"),\n },\n build: ({ what, folder }) => `Capture this into Notes: ${what}\n\n1. **Search first.** \\`apple_notes_search_notes\\` for the subject. A running note\n on this topic probably already exists, and a second one is worse than a\n longer first one.\n2. If one exists, read it in full with \\`apple_notes_get_note\\`, then write back\n the existing content plus the new material with \\`apple_notes_update_note\\`.\n That tool **replaces** the body — send the whole note, not the addition, or\n you will delete everything that was there.\n3. If nothing exists, \\`apple_notes_create_note\\`${folder ? ` in the ${folder} folder` : \"\"}. ${\n folder\n ? \"Confirm that folder exists with `apple_notes_list_folders` first — a name that does not match is not created for you.\"\n : \"Pick a folder from `apple_notes_list_folders` rather than inventing one.\"\n }\n4. Give it a title someone would search for in six months, not a timestamp.\n\nTell the user which note you wrote to and whether it was new or extended.`,\n });\n};\n","import { z } from \"zod\";\n\n/**\n * Notes' tool helpers. The generic half lives in `@mgcrea/mcp-apple-core`;\n * re-exported here so tools import from one place.\n */\nexport {\n compact,\n confirmArg,\n fail,\n limitArg,\n ok,\n okText,\n resolveLimit,\n toFailure,\n wrap,\n wrapResult,\n type ToolResult,\n} from \"@mgcrea/mcp-apple-core\";\n\n// ─── Notes' own args ─────────────────────────────────────────────────────────\n\nexport const noteRefArg = z\n .string()\n .min(1)\n .describe(\n 'An opaque note ref from a search or list result (looks like \"n1:x-coredata://…/ICNote/p123\"). ' +\n \"Do not construct one by hand.\",\n );\n\nexport const noteRefsArg = z\n .array(z.string().min(1))\n .min(1)\n .max(200)\n .describe(\"Note refs from a search or list result.\");\n\nexport const folderArg = z\n .string()\n .optional()\n .describe('Folder name, e.g. \"Notes\" or \"Projects\". Folders nest; give the leaf name.');\n\nexport const folderIdArg = z.string().min(1).describe(\"A folder id from apple_notes_list_folders.\");\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { ok, wrap } from \"./util.js\";\n\n/**\n * Build the report.\n *\n * Split out of the tool registration so the `cupertino://notes/diagnostics`\n * resource can serve the same bytes. Two renderings of one probe: duplicated,\n * the resource and the tool would drift, and the disagreement would surface as\n * \"the diagnostics lied\" — the one thing this file must never do.\n */\nexport const buildDiagnostics = async (\n client: AppleNotesClient,\n ctx: { allowWrites: boolean },\n): Promise<Record<string, unknown>> => {\n const lanes = await client.lanes();\n const located = client.locate();\n const accounts = lanes.applescript === \"live\" ? await client.accounts() : [];\n\n const fullDiskAccess = located.readable\n ? \"granted\"\n : located.exists\n ? \"denied\"\n : \"unknown (no store found)\";\n\n return {\n server: { lanes },\n permissions: {\n automation: lanes.applescript === \"live\" ? \"granted\" : \"denied-or-notes-not-running\",\n fullDiskAccess,\n ...(located.readable\n ? {}\n : {\n howToGrant: [\n \"System Settings > Privacy & Security > Full Disk Access\",\n \"Add the app that launches this server (Terminal, iTerm, VS Code, Claude...), then restart it.\",\n \"Granting it to Notes.app does nothing — the reader needs the permission, not Notes.\",\n ],\n }),\n },\n store: {\n path: located.storePath,\n exists: located.exists,\n readable: located.readable,\n sizeBytes: located.size,\n walPresent: located.walPresent,\n walSizeBytes: located.walSizeBytes,\n fingerprint: lanes.storeFingerprint,\n reason: located.reason,\n },\n accounts,\n settings: {\n allowWrites: ctx.allowWrites,\n // Off means the prompts and the cupertino:// resources are not registered\n // at all. Reported here because this tool still is, so it stays the one\n // place that explains a capability the client cannot see.\n exposePrompts: client.config.exposePrompts,\n accountAllowlist: client.config.accounts,\n indexMode: client.config.indexMode,\n maxResults: client.config.maxResults,\n // The Apple Events listing cap. Reported because list_notes now names it\n // when it is what ended a list, and a number in an error is only useful\n // if there is somewhere to go and see what it is.\n degradedMaxNotes: client.config.degradedMaxNotes,\n searchCacheTtlMs: client.config.searchCacheTtlMs,\n attachmentDir: client.config.attachmentDir,\n },\n caveats: [\n \"Without Full Disk Access the index lane is unavailable: search falls back to an \" +\n \"Apple Events bulk scan, which is fine on a small library and degrades linearly.\",\n `Listings over Apple Events stop at ${client.config.degradedMaxNotes} notes whatever ` +\n \"`limit` says. When that is what ended a list, list_notes says so in a `note` field \" +\n \"rather than returning a short list silently.\",\n \"Password-protected notes are encrypted at rest; no permission makes them readable.\",\n \"Attachment bytes need Full Disk Access — the scripting dictionary carries no file path.\",\n ],\n };\n};\n\n/**\n * One tool that answers \"why is this not working\".\n *\n * Lanes are probed before anything else is read, because the first Apple Event\n * is what triggers the Automation prompt: reading accounts first and probing\n * afterwards makes the two disagree whenever that first event loses the race.\n */\nexport const registerDiagnosticsTools = (\n server: McpServer,\n client: AppleNotesClient,\n ctx: { allowWrites: boolean },\n): void => {\n server.registerTool(\n \"apple_notes_diagnostics\",\n {\n description:\n \"Report which lanes are live, which macOS permissions are granted, and what each \" +\n \"missing one is blocking. Start here when a tool fails, or answers from the Apple \" +\n \"Events lane when you expected the index — it names the exact System Settings pane to \" +\n \"open.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => buildDiagnostics(client, ctx)).then((r) => r ?? ok({})),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { wrap } from \"./util.js\";\n\nexport const registerAccountTools = (server: McpServer, client: AppleNotesClient): void => {\n server.registerTool(\n \"apple_notes_list_accounts\",\n {\n description:\n \"List the Notes accounts, with how many folders and notes each holds. Start here to \" +\n \"discover account names for the other tools.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => client.accounts()),\n );\n\n server.registerTool(\n \"apple_notes_list_folders\",\n {\n description:\n \"List folders across every account. Folders nest, so each entry carries its depth and \" +\n \"the account it belongs to. Use the returned id with the move and create tools.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => client.folders()),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { confirmArg, folderIdArg, noteRefArg, noteRefsArg, wrap } from \"./util.js\";\n\n/**\n * The mutating tools.\n *\n * Registered only when writes are enabled, so with the flag off they are\n * invisible to the model rather than merely refused.\n */\nexport const registerActionTools = (server: McpServer, client: AppleNotesClient): void => {\n server.registerTool(\n \"apple_notes_create_note\",\n {\n description:\n \"Create a note. Notes takes its title from the first line of the body, so `title` is \" +\n \"prepended as a heading rather than set as a property.\",\n inputSchema: {\n title: z.string().optional().describe(\"Becomes the first line, and therefore the title.\"),\n body: z.string().optional().describe(\"Body content. Simple HTML is accepted.\"),\n folderId: z\n .string()\n .optional()\n .describe(\"Target folder id. Defaults to the first account's default folder.\"),\n },\n },\n async ({ title, body, folderId }) => wrap(() => client.createNote({ title, body, folderId })),\n );\n\n server.registerTool(\n \"apple_notes_update_note\",\n {\n description:\n \"Replace or append to a note's body. Editing the first line changes the title, because \" +\n \"that is where Notes takes it from.\",\n inputSchema: {\n ref: noteRefArg,\n body: z.string().min(1).describe(\"The new content. Simple HTML is accepted.\"),\n mode: z\n .enum([\"replace\", \"append\"])\n .optional()\n .describe(\"replace overwrites the body (default). append adds to the end.\"),\n },\n },\n async ({ ref, body, mode }) => wrap(() => client.updateNote(ref, body, mode ?? \"replace\")),\n );\n\n server.registerTool(\n \"apple_notes_add_attachment\",\n {\n description:\n \"Attach a file to a note. This is the only way to put an image in a note — an <img> tag \" +\n \"in the body creates an attachment whose bytes are never loaded.\",\n inputSchema: {\n ref: noteRefArg,\n path: z\n .string()\n .min(1)\n .describe(\"Absolute path to the file to attach. It is read by Notes, not copied here.\"),\n },\n },\n async ({ ref, path }) => wrap(() => client.addAttachment(ref, path)),\n );\n\n server.registerTool(\n \"apple_notes_move_note\",\n {\n description: \"Move a note to another folder.\",\n inputSchema: { ref: noteRefArg, folderId: folderIdArg },\n },\n async ({ ref, folderId }) => wrap(() => client.moveNote(ref, folderId)),\n );\n\n server.registerTool(\n \"apple_notes_delete_notes\",\n {\n description:\n \"Delete notes. They go to Recently Deleted rather than being destroyed, but this still \" +\n \"requires an explicit confirm.\",\n inputSchema: { refs: noteRefsArg, confirm: confirmArg },\n },\n async ({ refs }) => wrap(() => client.deleteNotes(refs)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport {\n folderArg,\n limitArg,\n noteRefArg,\n ok,\n okText,\n resolveLimit,\n wrap,\n wrapResult,\n} from \"./util.js\";\n\nexport const registerNoteTools = (\n server: McpServer,\n client: AppleNotesClient,\n allowWrites: boolean,\n): void => {\n server.registerTool(\n \"apple_notes_list_notes\",\n {\n description:\n \"List notes, newest first. Returns `notes` with a `ref` per note for the read and \" +\n \"action tools, alongside `source` and `hasMore`. Prefers Notes' own index, which stays \" +\n \"fast as a library grows. `hasMore` means more notes matched than `limit` allowed; a \" +\n \"`note` field appears when the Apple Events lane's own cap ended the list early, which \" +\n \"is a different thing and is worth reading.\",\n inputSchema: {\n folder: folderArg,\n limit: limitArg,\n },\n annotations: { readOnlyHint: true },\n },\n async ({ folder, limit }) =>\n wrap(() =>\n client.listNotes({ folder, limit: resolveLimit(limit, client.config.maxResults) }),\n ),\n );\n\n server.registerTool(\n \"apple_notes_search_notes\",\n {\n description:\n 'Search notes. `scope: \"full\"` (the default) matches the body text; `scope: \"title\"` ' +\n \"matches only titles and previews and is the cheaper query. Returns `notes` with a \" +\n \"`ref` per hit, plus `hasMore` — set when more matched than this page carries, so a \" +\n \"full page is never mistaken for the end of the results.\",\n inputSchema: {\n query: z.string().min(1).describe(\"Text to look for. Case-insensitive substring match.\"),\n scope: z\n .enum([\"full\", \"title\"])\n .optional()\n .describe(\n \"full = search body text (default). title = titles and previews only, which is a \" +\n \"single indexed query and cheaper on a large library.\",\n ),\n limit: limitArg,\n offset: z.number().int().min(0).optional().describe(\"Skip this many results, for paging.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ query, scope, limit, offset }) =>\n wrap(() =>\n client.searchNotes({\n query,\n scope: scope ?? \"full\",\n limit: resolveLimit(limit, client.config.maxResults),\n offset: offset ?? 0,\n }),\n ),\n );\n\n server.registerTool(\n \"apple_notes_get_note\",\n {\n description:\n \"Read one note's full text by ref. Password-protected notes report as locked rather \" +\n \"than returning an empty body — their text is encrypted at rest.\",\n inputSchema: {\n ref: noteRefArg,\n format: z\n .enum([\"text\", \"json\"])\n .optional()\n .describe(\"text returns the body as-is (default). json wraps it with the metadata.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ ref, format }) =>\n wrapResult(async () => {\n const result = await client.getNote(ref);\n if ((format ?? \"text\") === \"text\" && result.body !== null) return okText(result.body);\n return ok(result);\n }),\n );\n\n server.registerTool(\n \"apple_notes_list_attachments\",\n {\n description:\n \"List a note's attachments. Metadata only — retrieving the bytes needs Full Disk \" +\n \"Access, because the scripting dictionary carries no file path.\",\n inputSchema: { ref: noteRefArg },\n annotations: { readOnlyHint: true },\n },\n async ({ ref }) => wrap(() => client.attachments(ref)),\n );\n\n if (!allowWrites) return;\n\n server.registerTool(\n \"apple_notes_save_attachment\",\n {\n description:\n \"Save one attachment to disk. Needs Full Disk Access. It can only write into \" +\n \"APPLE_NOTES_ATTACHMENT_DIR (default ~/Downloads) and will not overwrite an existing \" +\n \"file unless you ask it to. Write-gated because it puts a file on the user's disk, \" +\n \"even though it changes nothing in Notes.\",\n inputSchema: {\n ref: noteRefArg,\n attachmentId: z.string().min(1).describe(\"An id from apple_notes_list_attachments.\"),\n directory: z\n .string()\n .optional()\n .describe(\"Override the target directory. Must still resolve inside the configured one.\"),\n overwrite: z.boolean().optional().describe(\"Replace an existing file. Default false.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ ref, attachmentId, directory, overwrite }) =>\n wrap(() =>\n client.saveAttachment(ref, attachmentId, directory, overwrite ? { overwrite } : {}),\n ),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { registerAccountTools } from \"./accounts.js\";\nimport { registerActionTools } from \"./actions.js\";\nimport { registerDiagnosticsTools } from \"./diagnostics.js\";\nimport { registerNoteTools } from \"./notes.js\";\n\nexport type ToolContext = {\n /**\n * Register the mutating tools too. Off by default — with the flag off they are\n * not merely refused, they are invisible and cannot be called at all.\n */\n allowWrites: boolean;\n};\n\n/**\n * Register the Apple Notes tools.\n *\n * The registered set is a pure function of `allowWrites` and nothing else. In\n * particular it does NOT vary with whether Full Disk Access is granted: that is\n * a runtime condition which can change while the process lives, and MCP clients\n * cache the tool list, so a tool that appears and disappears would leave clients\n * calling names the server no longer has. Tools that need the index instead\n * report their source, or explain what is missing.\n */\nexport const registerTools = (\n server: McpServer,\n client: AppleNotesClient,\n ctx: ToolContext,\n): void => {\n registerDiagnosticsTools(server, client, ctx);\n registerAccountTools(server, client);\n registerNoteTools(server, client, ctx.allowWrites);\n\n if (!ctx.allowWrites) return;\n registerActionTools(server, client);\n};\n","import {\n type Logger,\n type OsascriptRunner,\n registerSurfaceResources,\n withLazyTools,\n} from \"@mgcrea/mcp-apple-core\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { AppleNotesClient } from \"./client/notes.js\";\nimport type { Config } from \"./config.js\";\nimport { NOTES_GUIDE } from \"./guide.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { buildDiagnostics } from \"./tools/diagnostics.js\";\nimport { registerTools } from \"./tools/index.js\";\n\nexport const SERVER_NAME = BUILD_INFO.name;\nexport const SERVER_VERSION = BUILD_INFO.version;\n\nexport type CreateServerOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so nothing spawns a process or touches a real Notes. */\n osascript?: OsascriptRunner;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: AppleNotesClient;\n};\n\n/**\n * Build the server. Side-effect free: it opens no connection, spawns no\n * process and reads no file, so a test can construct it freely and every\n * external dependency arrives through an option.\n */\nexport const createServer = (opts: CreateServerOptions): CreatedServer => {\n const { config } = opts;\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });\n\n const client = new AppleNotesClient({\n config,\n ...(opts.logger ? { logger: opts.logger } : {}),\n ...(opts.osascript ? { osascript: opts.osascript } : {}),\n });\n\n withLazyTools(\n server,\n {\n surface: \"notes\",\n displayName: \"Notes\",\n lazy: config.lazyTools,\n allowWrites: config.allowWrites,\n },\n (target, allowWrites) => registerTools(target, client, { allowWrites }),\n );\n /*\n * One flag, both primitives — see `exposePrompts` in core's config. A prompt\n * embeds its surface guide, so registering prompts without the resources\n * would leave every expansion naming a `cupertino://…/guide` that this\n * server does not serve.\n */\n if (config.exposePrompts) {\n registerPrompts(server, config.allowWrites);\n registerSurfaceResources(server, {\n surface: \"notes\",\n displayName: \"Notes\",\n guide: NOTES_GUIDE,\n diagnostics: () => buildDiagnostics(client, { allowWrites: config.allowWrites }),\n inventory: {\n describes: \"accounts and folders\",\n read: async () => ({\n accounts: await client.accounts(),\n folders: await client.folders(),\n }),\n },\n });\n }\n\n return { server, client };\n};\n"],"mappings":";;;;;;;;AAWA,MAAM,MAAM,oBAAoB,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG;CAC3E,MAAM;CACN,SAAS;AACX,CAAC;AAID,MAAa,aAAwB;CACnC,MAAM,IAAI;CACV,SAAS,IAAI;CACb,WAAA;CACA,eAAA;AACF;;;;;;;;;ACdA,MAAa,gBAAgC;CAAE,SAAS;CAAS,WAAW;AAAc;;AAiB1F,IAAa,oBAAb,cAAuC,qBAAqB;CAC1D,OAAyB;CAEzB,YAAY,KAAa;EACvB,MACE,oBAAoB,IAAI,yFAE1B;CACF;AACF;;;;;;;;AASA,IAAa,kBAAb,cAAqC,qBAAqB;CACxD,OAAyB;CAEzB,YAAY,KAAa;EACvB,MACE,SAAS,IAAI,+IAEf;CACF;AACF;;;;;;;;;;;;;;;;AClCA,MAAa,kBAAkB;AAS/B,MAAa,oBAAoB,OAAe,QAAQ,MACtD,KAAK,MAAM,WAAW,oBAAoB,iBAAiB,kBAAkB;AAE/E,MAAa,eACX,OAA0D,CAAC,MAC1C;CACjB,MAAM,YAAY,KAAK,aAAa,iBAAiB,KAAK,IAAI;CAC9D,MAAM,QAAQ,cAAc,SAAS;CAErC,MAAM,SAAS,MAAM,SACjB,MAAM,WACJ,OACA,yPAGF,0BAA0B,UAAU;CAExC,OAAO;EAAE,GAAG;EAAO;EAAW,WAAW,KAAK,QAAQ,SAAS,GAAG,UAAU;EAAG;CAAO;AACxF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACPA,MAAa,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EvB,MAAa,UAAU,MAAc,OAAkC,CAAC,MAAc;EACpF,QAAQ;;;8BAGoB,KAAK,cAAc,SAAS,QAAQ;;;;;MAK5D,KAAK;;;;;;;;;;;ACvHX,MAAa,gBAAgB,OAAO;;;;;;;;;;;;;;CAcnC;;AAGD,MAAa,eAAe,OAAO;;;;;;;;;;;;;;CAclC;;;;;;;;;AAUD,MAAa,aAAa,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDhC;;;;;;;;AASD,MAAa,iBAAiB,OAAO;;CAEpC;;AAGD,MAAa,kBAAkB,OAAO;;;;;;;;;;;;;;;;;;;;CAoBrC;;AAGD,MAAa,mBAAmB,OAAO;;;;;;;;;;;;;;;;;;CAkBtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxHD,MAAa,kBAAkB;;;;;;;;;;;;;;;;;;;;;AClB/B,MAAM,WAAW,EAAE,aAAa,KAAK;;;;;;;;AASrC,MAAa,cAAc,OACzB;;;;;;;;;;;;;;;;;;;;;;;GAwBA,QACF;;;;;;;;;AAUA,MAAa,cAAc,OACzB,kBACE;;;;;;;;;;;;;;;;;GAkBF,QACF;;AAGA,MAAa,YAAY,OACvB;;;;;;;;;;;;;GAcA,QACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,iBAAiB,OAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCA,QACF;;;;;;;;AASA,MAAa,eAAe,OAC1B;;;;;;;;;;;GAYA,QACF;;;;;;;;;;;;;;;;;;;ACpKA,MAAa,cAAc;;AAG3B,MAAM,eAAe;AASrB,MAAa,aAAa,OAAuB,MAAkB;AAEnE,MAAa,aAAa,QAAyB;CACjD,MAAM,IAAI,qBAAqB,KAAK,GAAG;CACvC,IAAI,CAAC,GACH,MAAM,IAAI,kBACR,uBAAuB,IAAI,uGAE3B,EAAE,UAAU,sCAAkD,CAChE;CAEF,MAAM,GAAG,SAAS,MAAM;CACxB,IAAI,YAAA,MACF,MAAM,IAAI,kBACR,6BAA6B,QAAQ,kCACvC;CAEF,MAAM,KAAK,aAAa,KAAK,MAAM,EAAE,CAAC,GAAG;CACzC,IAAI,CAAC,IACH,MAAM,IAAI,kBAAkB,aAAa,IAAI,sCAAsC;CAErF,OAAO;EAAM;EAAc,YAAY,OAAO,EAAE;CAAE;AACpD;;;;;;;;AASA,MAAM,0BAA0B;AAEhC,MAAa,wBAAwB,OAA8B;CACjE,MAAM,KAAK,wBAAwB,KAAK,EAAE,CAAC,GAAG;CAC9C,OAAO,KAAK,OAAO,EAAE,IAAI;AAC3B;;AAGA,MAAa,qBAAqB,WAAmB,eACnD,UAAU,gBAAgB,UAAU,WAAW,YAAY;;;;;;;;;;;;;;;;;;;;;;;;ACjD7D,MAAM,YAAY;;AAElB,MAAM,kBAAkB;;AAExB,MAAM,oBAAoB;AAE1B,MAAM,OAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;;AAGrD,MAAM,aAAa,SAChB,OAAO,MAAQ,SAAS,KAAQ,SAAS,MAAQ,SAAS,MAAS,SAAS;;AAK/E,MAAa,cAAc,KAAiB,QAA+B;CACzE,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,IAAI;CACR,OAAO,IAAI,IAAI,QAAQ;EACrB,MAAM,OAAO,IAAI;EACjB,KAAK;EAGL,IAAI,QAAQ,IAAI,OAAO;EACvB,UAAU,OAAO,OAAQ,KAAK;EAC9B,KAAK,OAAO,SAAU,GAAG,OAAO;GAAE;GAAO,MAAM;EAAE;EACjD,SAAS;CACX;CACA,OAAO;AACT;;;;;;AAeA,MAAa,eACX,KACA,QAAQ,GACR,MAAc,IAAI,WACK;CACvB,MAAM,SAAsB,CAAC;CAC7B,IAAI,IAAI;CACR,OAAO,IAAI,KAAK;EACd,MAAM,MAAM,WAAW,KAAK,CAAC;EAC7B,IAAI,CAAC,KAAK,OAAO;EACjB,MAAM,QAAQ,KAAK,MAAM,IAAI,QAAQ,CAAC;EACtC,MAAM,OAAO,IAAI,QAAQ;EACzB,IAAI,UAAU,GAAG,OAAO;EACxB,IAAI,IAAI;EACR,IAAI,SAAS,GAAG;GACd,MAAM,IAAI,WAAW,KAAK,CAAC;GAC3B,IAAI,CAAC,GAAG,OAAO;GACf,OAAO,KAAK;IAAE;IAAO;IAAM,OAAO,EAAE;GAAM,CAAC;GAC3C,IAAI,EAAE;EACR,OAAO,IAAI,SAAS,GAAG;GACrB,MAAM,MAAM,WAAW,KAAK,CAAC;GAC7B,IAAI,CAAC,KAAK,OAAO;GACjB,MAAM,OAAO,IAAI;GACjB,MAAM,KAAK,OAAO,IAAI;GACtB,IAAI,KAAK,KAAK,OAAO;GACrB,OAAO,KAAK;IAAE;IAAO;IAAM;IAAM;GAAG,CAAC;GACrC,IAAI;EACN,OAAO,IAAI,SAAS,KAAK,SAAS,GAAG;GACnC,MAAM,QAAQ,SAAS,IAAI,IAAI;GAC/B,IAAI,IAAI,QAAQ,KAAK,OAAO;GAC5B,OAAO,KAAK;IAAE;IAAO;GAAK,CAAC;GAC3B,KAAK;EACP,OAGE,OAAO;CAEX;CACA,OAAO,MAAM,MAAM,SAAS;AAC9B;;AAGA,MAAM,UAAU,UAAqC;CACnD,IAAI;CACJ,IAAI;EACF,OAAO,KAAK,OAAO,KAAK;CAC1B,QAAQ;EACN,OAAO;CACT;CACA,IAAI,KAAK,SAAS,iBAAiB,OAAO;CAC1C,IAAI,WAAW;CACf,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GACpC,IAAI,UAAU,KAAK,WAAW,CAAC,CAAC,GAAG,YAAY;CAEjD,OAAO,WAAW,KAAK,SAAS,oBAAoB,OAAO;AAC7D;;;;;;;AAUA,MAAa,kBAAkB,KAAiB,WAAW,cAAiC;CAC1F,MAAM,MAAyB,CAAC;CAChC,MAAM,QAAQ,OAAe,KAAa,MAAgB,UAAwB;EAChF,IAAI,QAAQ,UAAU;EACtB,MAAM,SAAS,YAAY,KAAK,OAAO,GAAG;EAC1C,IAAI,CAAC,QAAQ;EACb,KAAK,MAAM,KAAK,QAAQ;GACtB,IAAI,EAAE,SAAS,KAAK,EAAE,SAAS,KAAA,KAAa,EAAE,OAAO,KAAA,GAAW;GAChE,MAAM,OAAO,CAAC,GAAG,MAAM,EAAE,KAAK;GAC9B,MAAM,OAAO,OAAO,IAAI,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;GAC9C,IAAI,MAAM,IAAI,KAAK;IAAE,MAAM,KAAK,KAAK,GAAG;IAAG;IAAM,QAAQ,KAAK;GAAO,CAAC;GACtE,KAAK,EAAE,MAAM,EAAE,IAAI,MAAM,QAAQ,CAAC;EACpC;CACF;CACA,KAAK,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC;CACzB,OAAO;AACT;;;;;;;;;;;AAYA,MAAa,iBAAiB;;;;;;;;AAiB9B,MAAa,mBACX,KACA,aAA4B,mBACZ;CAChB,MAAM,aAAa,eAAe,GAAG;CACrC,MAAM,SAAS,aAAc,WAAW,MAAM,MAAM,EAAE,SAAS,UAAU,KAAK,OAAQ;CACtF,MAAM,UAAU,WAAW,UAAU,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM;CACzE,MAAM,OAAO,UAAU;CACvB,OAAO;EACL,MAAM,MAAM,QAAQ;EACpB,MAAM,MAAM,QAAQ;EACpB,KAAK,SAAS,OAAO,OAAO,SAAS,WAAW;EAChD,gBAAgB,WAAW;CAC7B;AACF;;;;;;;;;;;;;;;;;;;AC7JA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAW;CAAsB;AAAgB;AAuC5F,MAAM,WAAW;CACf,SAAS;CACT,eAAe;CACf,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,eAAe;CACf,iBAAiB;AACnB;AAEA,MAAa,cAAc,OAAwC;CACjE,MAAM,OAAO,UAAU,IAAI,uBAAuB;CAClD,MAAM,UAAU,iBAAiB,QAAQ,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC;CAEhE,IAAI,UAAyB;CAC7B,IAAI;EAIF,UAHY,GAAG,QAAQ,wDAAwD,CAAC,CAAC,IAGrE,CAAC,EAAE,SAAS;CAC1B,QAAQ;EACN,UAAU;CACZ;CACA,IAAI,YAAY,MAAM,QAAQ,KAAK,6BAA6B;CAEhE,IAAI,YAA2B;CAC/B,IAAI;EAIF,YAHY,GAAG,QAAQ,uCAAuC,CAAC,CAAC,IAGlD,CAAC,EAAE,UAAU;CAC7B,QAAQ;EACN,YAAY;CACd;CAEA,OAAO;EACL,aAAa,kBAAkB,EAAE;EACjC;EACA;EACA,KAAK;GACH,OAAO,KAAK,SAAS,SAAS;GAC9B,SAAS,KAAK,SAAS,SAAS,OAAO;GACvC,eAAe,KAAK,SAAS,SAAS,aAAa;GACnD,QAAQ,KAAK,SAAS,SAAS,MAAM;GACrC,QAAQ,KAAK,SAAS,SAAS,MAAM;GACrC,OAAO,KAAK,SAAS,SAAS,KAAK;GACnC,eAAe,KAAK,SAAS,SAAS,aAAa;GACnD,iBAAiB,KAAK,SAAS,SAAS,eAAe;EACzD;EAKA,iBAAiB,KAAK,QAAQ,MAAM,+CAA+C,KAAK,CAAC,CAAC;EAC1F;CACF;AACF;AAEA,MAAa,gBAAgB,SAAkC;CAC7D,IAAI,KAAK,QAAQ,QACf,MAAM,IAAI,iBACR,iFACK,KAAK,YAAY,cAAc,KAAK,QAAQ,KAAK,IAAI,EAAE,oEAE9D;AAEJ;AAEA,MAAM,SAAS,oBAAkD;CAC/D,IAAI,oBAAoB,QAAQ,CAAC,OAAO,SAAS,eAAe,GAAG,OAAO;CAC1E,wBAAO,IAAI,MAAM,kBAAkB,0BAA0B,GAAI,EAAA,CAAE,YAAY;AACjF;AAWA,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,YAAY,IAAkB,MAAyB,MAA0B;EAC/E,KAAKA,MAAM;EACX,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;;;;;;;;;CAUA,SAAiB;EAIf,OAAO,wCAHU,KAAK,KAAK,gBACxB,KAAK,MAAM,YAAY,EAAE,kBAAkB,EAAE,OAAO,CAAC,CACrD,KAAK,EAC8C;CACxD;CAEA,UAAkB;EAQhB,OAAO,4CAPS,KAAK,KAAK,IAAI,gBAC1B,qBACA,KAAK,KAAK,IAAI,UACZ,eACA,OAGqD;;qBAF5C,KAAK,KAAK,IAAI,SAAS,cAAc,OAI5B,gBAHT,KAAK,KAAK,IAAI,SAAS,2BAA2B,IAGlB;;CAEjD;CAEA,OAAO,GAAqC;EAC1C,OAAO;GACL,YAAY,EAAE;GACd,OAAQ,EAAE,SAA2B;GACrC,SAAU,EAAE,WAA6B;GACzC,UAAU,MAAO,EAAE,YAA8B,IAAI;GACrD,SAAS,MAAO,EAAE,WAA6B,IAAI;GACnD,UAAW,EAAE,YAA8B;GAC3C,QAAQ,QAAQ,EAAE,MAAM;EAC1B;CACF;CAEA,QAAgB;EAId,OAHY,KAAKA,IACd,QAAQ,2DAA2D,KAAKC,OAAO,GAAG,CAAC,CACnF,IAAI,KAAK,KAAK,OACR,CAAC,CAAC;CACb;;;;;;;;CASA,OAAO,SAAmC;EACxC,MAAM,UAAU,CAAC,KAAKA,OAAO,CAAC;EAC9B,MAAM,SAAoB,CAAC,KAAK,KAAK,OAAO;EAE5C,IAAI,QAAQ,OAAO;GACjB,MAAM,OAAO,IAAI,WAAW,QAAQ,KAAK,EAAE;GAC3C,MAAM,aAAa,KAAK,KAAK,IAAI,gBAC7B,qBACA,KAAK,KAAK,IAAI,UACZ,eACA;GACN,QAAQ,KACN,aACI,oCAAoC,WAAW,wBAC/C,8BACN;GACA,OAAO,KAAK,IAAI;GAChB,IAAI,YAAY,OAAO,KAAK,IAAI;EAClC;EACA,IAAI,QAAQ,aAAa,KAAA,KAAa,KAAK,KAAK,IAAI,QAAQ;GAC1D,QAAQ,KAAK,eAAe;GAC5B,OAAO,KAAK,QAAQ,QAAQ;EAC9B;EAKA,IAAI,QAAQ,eAAe;GACzB,QAAQ,KAAK,2BAA2B;GACxC,OAAO,KACL,WAAW,iBAAiB,QAAQ,eAAe,OAAO,CAAC,CAAC,QAAQ,IAAI,MACtE,sBACJ;EACF;EACA,IAAI,QAAQ,gBAAgB;GAE1B,QAAQ,KAAK,0BAA0B;GACvC,OAAO,KACL,WAAW,kBAAkB,QAAQ,gBAAgB,KAAK,CAAC,CAAC,QAAQ,IAAI,MACtE,sBACJ;EACF;EAEA,MAAM,MAAM,GAAG,KAAKC,QAAQ,EAAE,SAAS,QAAQ,KAAK,OAAO,EAAE;;EAE7D,OAAO,KAAK,QAAQ,OAAO,QAAQ,MAAM;EACzC,OAAQ,KAAKF,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAI,MAAkB,CAAC,CAA+B,KACrF,MAAM,KAAKG,OAAO,CAAC,CACtB;CACF;CAEA,aAAa,IAA4B;EACvC,MAAM,MAAM,KAAKH,IACd,QAAQ,GAAG,KAAKE,QAAQ,EAAE,SAAS,KAAKD,OAAO,EAAE,gBAAgB,CAAC,CAClE,IAAI,KAAK,KAAK,SAAS,EAAE;EAC5B,OAAO,MAAM,KAAKE,OAAO,GAAG,IAAI;CAClC;;;;;;;CAQA,OAAO,IAA6E;EAClF,MAAM,MAAM,KAAKH,IACd,QAAQ,0EAA0E,CAAC,CACnF,IAAI,EAAE;EACT,IAAI,CAAC,KAAK,MAAM,OAAO;GAAE,MAAM;GAAM,KAAK;GAAM,WAAW;EAAM;EACjE,IAAI,IAAI,KAAK,OAAO;GAAE,MAAM;GAAM,KAAK;GAAM,WAAW;EAAK;EAE7D,MAAM,OAAO,OAAO,KAAK,IAAI,IAAI;EACjC,IAAI,KAAK,OAAO,MAAQ,KAAK,OAAO,KAAM,OAAO;GAAE,MAAM;GAAM,KAAK;GAAM,WAAW;EAAM;EAC3F,IAAI;EACJ,IAAI;GACF,WAAW,WAAW,IAAI;EAC5B,QAAQ;GACN,OAAO;IAAE,MAAM;IAAM,KAAK;IAAM,WAAW;GAAM;EACnD;EACA,MAAM,UAAU,gBAAgB,QAAQ;EACxC,OAAO;GAAE,MAAM,QAAQ;GAAM,KAAK,QAAQ;GAAK,WAAW;EAAM;CAClE;;;;;;;;;;;;;;;;;;;;CAqBA,SAAS,cAIA;EACP,IAAI,CAAC,KAAK,KAAK,IAAI,OAAO,OAAO;EACjC,MAAM,aAAa,KAAK,KAAK,IAAI,kBAAkB,mBAAmB;EACtE,MAAM,WAAW,KAAK,KAAK,IAAI,gBAAgB,gBAAgB;EAC/D,MAAM,MAAM,KAAKA,IACd,QACC,uCAAuC,WAAW,kBAAkB,SAAS,6GAI/E,CAAC,CACA,IAAI,YAAY;EAGnB,IAAI,CAAC,KAAK,YAAY,OAAO;EAC7B,OAAO;GACL,YAAY,IAAI;GAChB,YAAY,IAAI,cAAc;GAC9B,UAAU,IAAI,YAAY;EAC5B;CACF;CAEA,QAAc;EACZ,KAAKA,IAAI,MAAM;CACjB;AACF;AAEA,MAAa,aAAa,MAAc,MAAoB,WAA+B;CACzF,MAAM,EACJ,IACA,MAAM,QACN,WAAW,SACT,aAAgC,MAAM,MAAM;EAC9C,QAAQ;EACR,OAAO;EACP,MAAM;EACN,WAAW,SAAS;GAClB,MAAM,QAAQ,WAAW,IAAI;GAC7B,aAAa,KAAK;GAClB,OAAO;EACT;EACA,QAAQ,QAAQ,eAAe;EAC/B,kBACE,QAAQ,OAAO,6EAA6E;CAChG,CAAC;CACD,OAAO,IAAI,UAAU,IAAI,MAAM,MAAM;AACvC;;;;;;;;;;;;;;;;AC1QA,IAAa,mBAAb,MAA8B;CAC5B;CACA;CACA;CACA,SAA2B;CAC3B,cAAc;CACd,UAA4B;CAE5B,YAAY,MAA+B;EACzC,KAAK,SAAS,KAAK;EACnB,KAAKI,UAAU,KAAK;EACpB,KAAK,SACH,KAAK,aACL,sBAAsB;GACpB,eAAe,KAAK,OAAO;GAC3B,WAAW,KAAK,OAAO;GACvB,SAAS;GACT,QAAQ,KAAK;EACf,CAAC;CACL;CAEA,SAAuB;EACrB,OAAO,YAAY,EAAE,WAAW,KAAK,OAAO,UAAU,CAAC;CACzD;;CAGA,QAA0B;EACxB,IAAI,KAAKC,aAAa,OAAO,KAAKC;EAClC,KAAKD,cAAc;EACnB,IAAI,KAAK,OAAO,cAAc,OAAO,OAAO;EAC5C,IAAI;GACF,KAAKC,SAAS,UAAU,KAAK,OAAO,CAAC,CAAC,WAAW,KAAK,OAAO,WAAW,KAAKF,OAAO;EACtF,SAAS,KAAK;GACZ,KAAKA,SAAS,QAAQ,0BAA0B,GAAG;GACnD,KAAKE,SAAS;EAChB;EACA,OAAO,KAAKA;CACd;;;;;;;;CASA,MAAM,QAA6B;EACjC,IAAI,cAAyC;EAC7C,KAAK,IAAI,UAAU,GAAG,UAAU,GAAG,WAAW,GAC5C,IAAI;GACF,MAAM,KAAK,OAAO,IAAI,aAAa;GACnC,cAAc;GACd;EACF,SAAS,KAAK;GACZ,KAAKF,SAAS,QAAQ,4BAA4B,GAAG;GACrD,IAAI,YAAY,GAAG,MAAM,IAAI,SAAS,MAAM,WAAW,GAAG,GAAG,CAAC;EAChE;EAGF,MAAM,UAAU,KAAK,OAAO;EAC5B,MAAM,QAAQ,KAAK,MAAM;EACzB,OAAO;GACL;GACA,OAAO,KAAK,OAAO,cAAc,QAAQ,aAAa,QAAQ,SAAS;GACvE,WAAW,OAAO,QAAQ;GAC1B,kBAAkB,OAAO,KAAK,eAAe;GAC7C,QAAQ,QAAQ,OAAO,QAAQ;EACjC;CACF;CAEA,SAAS,aAAqC;EAC5C,MAAM,OAAO,KAAK,OAAO;EACzB,IAAI,CAAC,KAAK,QAAQ,OAAO;EACzB,OAAO,QAAQ,eAAe,KAAK,MAAM,MAAM,EAAE,YAAY,MAAM,YAAY,YAAY,CAAC,CAAC;CAC/F;;;;;;;;;;;;;;;;;;CAmBA,gBAAgB,OAAwC,CAAC,GAAY;EACnE,OAAO,KAAK,OAAO,SAAS,WAAW,KAAK,CAAC,KAAK;CACpD;CAEA,MAAM,WAAmC;EAEvC,QAAO,MADW,oBAAoB,KAAK,OAAO,IAAmB,aAAa,CAAC,EAAA,CACxE,QAAQ,MAAM,KAAKG,SAAS,EAAE,IAAI,CAAC;CAChD;CAEA,MAAM,UAAiC;EAErC,QAAO,MADW,oBAAoB,KAAK,OAAO,IAAkB,YAAY,CAAC,EAAA,CACtE,QAAQ,MAAM,KAAKA,SAAS,EAAE,WAAW,CAAC;CACvD;;;;;;;;CASA,MAAM,UAAU,MAA4E;EAC1F,MAAM,QAAQ,KAAKC,gBAAgB,IAAI,IAAI,KAAK,MAAM,IAAI;EAC1D,IAAI,OAAO,KAAK,WAAW;GAIzB,MAAM,OAAO,MAAM,OAAO;IAAE,OAAO,KAAK,QAAQ;IAAG,QAAQ;GAAE,CAAC;GAC9D,MAAM,OAAO,MAAM,KAAK;GACxB,OAAO;IACL,OAAO,KAAK,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC,KAAK,MAAM,KAAKC,WAAW,GAAG,IAAI,CAAC;IACpE,QAAQ;IACR,SAAS,KAAK,SAAS,KAAK;GAC9B;EACF;EACA,OAAO,KAAKC,WAAW,IAAI;CAC7B;CAEA,WAAW,KAAc,WAAgC;EACvD,OAAO;GACL,KAAK,kBAAkB,WAAW,IAAI,UAAU;GAChD,OAAO,IAAI;GACX,SAAS,IAAI;GACb,QAAQ;GACR,SAAS;GACT,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ;EACV;CACF;CAEA,MAAMA,WAAW,MAA4E;EAe3F,MAAM,WAAU,MAdG,oBACjB,KAAK,OAAO,IAWT,UAAU,CACf,EAAA,CACqB,MAClB,QAAQ,MAAM,KAAKH,SAAS,EAAE,OAAO,CAAC,CAAC,CACvC,QAAQ,MAAM,CAAC,KAAK,UAAU,EAAE,WAAW,KAAK,MAAM;EACzD,MAAM,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,OAAO,gBAAgB;EAC7D,OAAO;GACL,OAAO,QAAQ,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,OAAO;IACvC,KAAK,UAAU,EAAE,EAAE;IACnB,OAAO,EAAE;IACT,SAAS;IACT,QAAQ,EAAE;IACV,SAAS,EAAE;IACX,UAAU,EAAE;IACZ,SAAS,EAAE;IACX,QAAQ,EAAE;IACV,QAAQ;GACV,EAAE;GACF,QAAQ;GACR,SAAS,QAAQ,SAAS;GAI1B,GAAI,QAAQ,SAAS,OAAO,KAAK,OAAO,mBAAmB,KAAK,QAC5D,EACE,MACE,sCAAsC,KAAK,OAAO,iBAAiB,2BACtD,KAAK,MAAM,+IAG5B,IACA,CAAC;EACP;CACF;;;;;;;;;CAUA,MAAMI,aAAiC;EACrC,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,KAAKC,WAAW,MAAM,KAAKA,QAAQ,KAAK,KAAK,OAAO,kBAAkB,OAAO,KAAKA;EAEtF,MAAM,QAAQ,KAAKJ,gBAAgB,IAAI,KAAK,MAAM,IAAI;EACtD,MAAM,wBAAQ,IAAI,IAAoB;EACtC,IAAI,OAAO,KAAK,WAAW;GACzB,MAAM,OAAO,MAAM,KAAK;GACxB,KAAK,MAAM,OAAO,MAAM,OAAO;IAAE,OAAO;IAAS,QAAQ;GAAE,CAAC,GAAG;IAC7D,MAAM,OAAO,MAAM,OAAO,IAAI,UAAU;IACxC,IAAI,KAAK,MAAM,MAAM,IAAI,kBAAkB,MAAM,IAAI,UAAU,GAAG,KAAK,IAAI;GAC7E;GACA,KAAKI,UAAU;IAAE,IAAI;IAAK,QAAQ;IAAS;GAAM;GACjD,OAAO,KAAKA;EACd;EAEA,MAAM,OAAO,MAAM,oBACjB,KAAK,OAAO,IAAiD,cAAc,CAC7E;EACA,KAAK,IAAI,SAAS,IAAI,MAAM;GAC1B,MAAM,OAAO,KAAK,MAAM;GACxB,IAAI,MAAM,MAAM,IAAI,UAAU,OAAO,EAAE,CAAC,GAAG,OAAO,IAAI,CAAC;EACzD,CAAC;EACD,KAAKA,UAAU;GAAE,IAAI;GAAK,QAAQ;GAAgB;EAAM;EACxD,OAAO,KAAKA;CACd;;;;;;;;;;CAWA,MAAM,YAAY,MAK2B;EAC3C,MAAM,QAAQ,KAAKJ,gBAAgB,IAAI,KAAK,MAAM,IAAI;EAEtD,IAAI,KAAK,UAAU,WAAW,OAAO,KAAK,WAAW;GACnD,MAAM,OAAO,MAAM,OAAO;IACxB,OAAO,KAAK;IACZ,OAAO,KAAK,QAAQ;IACpB,QAAQ,KAAK;GACf,CAAC;GACD,MAAM,OAAO,MAAM,KAAK;GACxB,OAAO;IACL,OAAO,KAAK,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC,KAAK,MAAM,KAAKC,WAAW,GAAG,IAAI,CAAC;IACpE,QAAQ;IACR,SAAS,KAAK,SAAS,KAAK;IAC5B,OAAO;GACT;EACF;EAEA,MAAM,SAAS,MAAM,KAAKE,WAAW;EACrC,MAAM,SAAS,KAAK,MAAM,YAAY;EACtC,MAAM,OAAiB,CAAC;EACxB,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,OAC/B,IAAI,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,KAAK,GAAG;EAExD,MAAM,OAAO,KAAK,MAAM,KAAK,QAAQ,KAAK,SAAS,KAAK,KAAK;EAG7D,OAAO;GACL,QAAO,MAFe,QAAQ,IAAI,KAAK,KAAK,QAAQ,KAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,CAAC,CAAC,CAAC,EAAA,CAEtE,KAAK,MAAM,EAAE,OAAO;GACrC,QAAQ,OAAO;GAGf,SAAS,KAAK,SAAS,KAAK,SAAS,KAAK;GAC1C,OAAO;EACT;CACF;CAEA,MAAM,QACJ,KACA,OAA2B,CAAC,GACuD;EACnF,MAAM,UAAU,UAAU,GAAG;EAC7B,MAAM,QAAQ,KAAKH,gBAAgB,IAAI,KAAK,MAAM,IAAI;EAEtD,IAAI,OAAO,KAAK,WAAW;GACzB,MAAM,MAAM,MAAM,aAAa,QAAQ,UAAU;GACjD,IAAI,KAAK;IACP,MAAM,UAAU,KAAKC,WAAW,KAAK,MAAM,KAAK,SAAS;IACzD,IAAI,KAAK,SAAS,OAAO,OAAO;KAAE;KAAS,MAAM;KAAM,YAAY;IAAK;IACxE,IAAI,IAAI,QAAQ,MAAM,IAAI,gBAAgB,GAAG;IAC7C,MAAM,OAAO,MAAM,OAAO,QAAQ,UAAU;IAC5C,IAAI,KAAK,WAAW,MAAM,IAAI,gBAAgB,GAAG;IACjD,IAAI,KAAK,SAAS,MAChB,OAAO;KAAE;KAAS,MAAM,KAAKI,UAAU,KAAK,IAAI;KAAG,YAAY,SAAS,KAAK;IAAM;GAEvF;EACF;EAEA,MAAM,CAAC,SAAS,MAAM,oBACpB,KAAK,OAAO,IAUV,iBAAiB,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAC1C;EACA,IAAI,CAAC,OAAO,OAAO,MAAM,IAAI,kBAAkB,GAAG;EAClD,IAAI,MAAM,QAAQ,MAAM,IAAI,gBAAgB,GAAG;EAC/C,OAAO;GACL,SAAS;IACP;IACA,OAAO,MAAM;IACb,SAAS;IACT,QAAQ;IACR,SAAS;IACT,UAAU,MAAM;IAChB,SAAS,MAAM;IACf,QAAQ;IACR,QAAQ;GACV;GACA,MAAM,MAAM,cAAc,OAAO,OAAO,KAAKA,UAAU,MAAM,SAAS;GACtE,YAAY;EACd;CACF;CAEA,UAAU,MAAsB;EAC9B,MAAM,MAAM,KAAK,OAAO;EACxB,IAAI,OAAO,WAAW,MAAM,MAAM,KAAK,KAAK,OAAO;EACnD,OAAO,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE,oBAAoB,IAAI;CACvD;CAEA,MAAM,YAAY,KAOhB;EACA,MAAM,UAAU,UAAU,GAAG;EAC7B,MAAM,SAAS,MAAM,oBACnB,KAAK,OAAO,IAOV,kBAAkB,EAAE,IAAI,QAAQ,GAAG,CAAC,CACxC;EAMA,MAAM,uBAAO,IAAI,IAAY;EAC7B,OAAO,OAAO,QAAQ,MAAM;GAC1B,IAAI,CAAC,EAAE,IAAI,OAAO;GAClB,IAAI,KAAK,IAAI,EAAE,EAAE,GAAG,OAAO;GAC3B,KAAK,IAAI,EAAE,EAAE;GACb,OAAO;EACT,CAAC;CACH;;;;;;;;;CAUA,MAAM,eACJ,KACA,cACA,WACA,OAAgC,CAAC,GAIhC;EACD,MAAM,UAAU,KAAK,OAAO;EAC5B,IAAI,CAAC,QAAQ,UACX,MAAM,IAAI,kBACR,0NAGF;EAGF,MAAM,QAAO,MADM,KAAK,YAAY,GAAG,EAAA,CACrB,MAAM,MAAM,EAAE,OAAO,YAAY;EACnD,IAAI,CAAC,MAAM,MAAM,IAAI,kBAAkB,iBAAiB,aAAa,WAAW,IAAI,EAAE;EAMtF,MAAM,KAAK,qBAAqB,YAAY;EAC5C,MAAM,QAAQ,OAAO,OAAO,OAAQ,KAAK,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK;EAClE,IAAI,CAAC,OACH,MAAM,IAAI,kBACR,wBAAwB,aAAa,0GAEvC;EAGF,MAAM,SAAS,KAAKC,WAAW,QAAQ,WAAW,KAAK;EACvD,IAAI,CAAC,QACH,MAAM,IAAI,kBACR,4DAA4D,QAAQ,UAAU,EAChF;EAOF,MAAM,OAAO,QAAQ,KAAK,OAAO,aAAa;EAC9C,MAAM,MAAM,YAAY,QAAQ,MAAM,SAAS,IAAI;EACnD,IAAI,QAAQ,QAAQ,CAAC,IAAI,WAAW,OAAO,GAAG,GAC5C,MAAM,IAAI,kBACR,6BAA6B,KAAK,4DACpC;EAIF,MAAM,OAAO,SAAS,MAAM,YAAY,KAAK,QAAQ,SAAS,MAAM,CAAC;EACrE,MAAM,SAAS,QAAQ,KAAK,KAAK,IAAI,CAAC;EACtC,IAAI,WAAW,KAAK,KAAK,IAAI,KAAK,CAAC,OAAO,WAAW,MAAM,GAAG,GAC5D,MAAM,IAAI,kBAAkB,6BAA6B,IAAI,EAAE;EAEjE,IAAI,WAAW,MAAM,KAAK,CAAC,KAAK,WAC9B,MAAM,IAAI,kBAAkB,GAAG,OAAO,2CAA2C;EAEnF,MAAM,QAAQ,aAAa,MAAM;EACjC,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;EAClC,cAAc,QAAQ,OAAO,EAAE,MAAM,IAAM,CAAC;EAC5C,OAAO;GAAE,MAAM;GAAQ,OAAO,MAAM;EAAO;CAC7C;;;;;;;;;;;;;;CAeA,WACE,MACA,OACe;EACf,MAAM,kBAAkB;GACtB,IAAI;IACF,OAAO,YAAY,IAAI;GACzB,QAAQ;IACN,OAAO,CAAC;GACV;EACF,EAAA,CAAG;EAEH,MAAM,UAAU,QAA+B;GAC7C,IAAI;GACJ,IAAI;IACF,UAAU,YAAY,GAAG;GAC3B,QAAQ;IACN,OAAO;GACT;GACA,MAAM,QAAQ,QAAQ,QAAQ,MAAM;IAClC,IAAI;KACF,OAAO,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO;IACvC,QAAQ;KACN,OAAO;IACT;GACF,CAAC;GACD,IAAI,MAAM,YAAY,MAAM,SAAS,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,MAAM,QAAQ;GAGrF,OAAO,MAAM,WAAW,IAAI,KAAK,KAAK,MAAM,EAAY,IAAI;EAC9D;EAEA,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,OAAO,KAAK,MAAM,SAAS,SAAS,MAAM,UAAU;GAC1D,IAAI;IACF,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,YAAY,GAAG;GACrC,QAAQ;IACN;GACF;GACA,MAAM,aAAa,MAAM,aAAa,CAAC,KAAK,MAAM,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI;GAClF,KAAK,MAAM,OAAO,YAAY;IAC5B,MAAM,MAAM,OAAO,GAAG;IACtB,IAAI,KAAK,OAAO;GAClB;GAGA,MAAM,iBAAiB;IACrB,IAAI;KACF,OAAO,YAAY,IAAI,CAAC,CAAC,QAAQ,MAAM,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IAC9E,QAAQ;KACN,OAAO,CAAC;IACV;GACF,EAAA,CAAG;GACH,IAAI,QAAQ,WAAW,GAAG;IACxB,MAAM,MAAM,OAAO,KAAK,MAAM,QAAQ,EAAY,CAAC;IACnD,IAAI,KAAK,OAAO;GAClB;EACF;EACA,OAAO;CACT;CAIA,MAAM,WAAW,MAA8E;EAC7F,KAAKF,UAAU;EACf,OAAO,oBAAoB,KAAK,OAAO,IAAI,aAAa,IAAI,CAAC;CAC/D;;;;;;;;CASA,MAAM,cAAc,KAAa,UAAoC;EACnE,MAAM,UAAU,UAAU,GAAG;EAC7B,MAAM,WAAW,QAAQ,QAAQ;EACjC,IAAI;EACJ,IAAI;GACF,QAAQ,SAAS,QAAQ;EAC3B,QAAQ;GACN,MAAM,IAAI,kBAAkB,cAAc,SAAS,EAAE;EACvD;EACA,IAAI,CAAC,MAAM,OAAO,GAChB,MAAM,IAAI,kBAAkB,GAAG,SAAS,gBAAgB;EAE1D,KAAKA,UAAU;EACf,OAAO,oBAAoB,KAAK,OAAO,IAAI,gBAAgB;GAAE,IAAI,QAAQ;GAAI,MAAM;EAAS,CAAC,CAAC;CAChG;CAEA,MAAM,WAAW,KAAa,MAAc,MAA8C;EACxF,KAAKA,UAAU;EACf,MAAM,UAAU,UAAU,GAAG;EAC7B,OAAO,oBAAoB,KAAK,OAAO,IAAI,aAAa;GAAE,IAAI,QAAQ;GAAI;GAAM;EAAK,CAAC,CAAC;CACzF;CAEA,MAAM,SAAS,KAAa,UAAoC;EAC9D,KAAKA,UAAU;EACf,MAAM,UAAU,UAAU,GAAG;EAC7B,OAAO,oBAAoB,KAAK,OAAO,IAAI,WAAW;GAAE,IAAI,QAAQ;GAAI;EAAS,CAAC,CAAC;CACrF;CAEA,MAAM,YAAY,MAAkC;EAClD,KAAKA,UAAU;EACf,MAAM,MAAM,KAAK,KAAK,MAAM,UAAU,CAAC,CAAC,CAAC,EAAE;EAC3C,OAAO,oBAAoB,KAAK,OAAO,IAAI,cAAc,EAAE,IAAI,CAAC,CAAC;CACnE;AACF;;;;;;;;;;AC/oBA,MAAM,eAAe,iBAAiB,OAAO;;;;;;;CAO3C,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;;CAE/C,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,WAAW,EAAE,KAAK;EAAC;EAAQ;EAAM;EAAa;CAAK,CAAC,CAAC,CAAC,QAAQ,MAAM;;;;;CAKpE,kBAAkB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAK,CAAC,CAAC,QAAQ,GAAG;CAChE,cAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAK,CAAC,CAAC,IAAI,GAAU,CAAC,CAAC,QAAQ,MAAO;;;;;;;CAOzE,eAAe,EAAE,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,GAAG,WAAW,CAAC;;;;;;;;;CAS9D,kBAAkB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAS,CAAC,CAAC,QAAQ,GAAM;AACzE,CAAC,CAAC,CAAC,OAAO;;;;;AAQV,MAAa,cAAc,MAAyB,QAAQ,QAC1D,YAAY,cAAc;CACxB,aAAa,UAAU,IAAI,wBAAwB;CACnD,eAAe,UAAU,IAAI,0BAA0B;CACvD,WAAW,UAAU,IAAI,sBAAsB;CAC/C,OAAO,UAAU,IAAI,iBAAiB;CACtC,UAAU,UAAU,IAAI,oBAAoB;CAC5C,WAAW,QAAQ,IAAI,iBAAiB;CACxC,WAAW,QAAQ,IAAI,sBAAsB;CAC7C,eAAe,QAAQ,IAAI,0BAA0B;CACrD,oBAAoB,YAAY,IAAI,gCAAgC;CACpE,YAAY,YAAY,IAAI,uBAAuB;CACnD,kBAAkB,YAAY,IAAI,8BAA8B;CAChE,cAAc,YAAY,IAAI,0BAA0B;CACxD,eAAe,QAAQ,IAAI,0BAA0B;CACrD,kBAAkB,YAAY,IAAI,+BAA+B;AACnE,CAAC;;;;;;;;;;;;ACpEH,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACC3B,MAAM,MAAqB;CAAE,SAAS;CAAS,OAAO;AAAY;;;;;;AAOlE,MAAa,mBAAmB,QAAmB,gBAA+B;CAChF,uBAAuB,QAAQ,KAAK;EAClC,MAAM;EACN,OAAO;EACP,aACE;EAEF,YAAY;GACV,OAAO,kBAAkB,wDAAsD;GAC/E,QAAQ,UAAU,uEAAuE;EAC3F;EACA,QAAQ,EAAE,OAAO,aAAa,iCAAiC,QAC7D,SAAS,4BAA4B,OAAO,YAAY,GACzD;;2DAGC,SAAS,4BAA4B,GACtC;;;;;;;;;;;CAWH,CAAC;CAED,IAAI,CAAC,aAAa;CAElB,uBAAuB,QAAQ,KAAK;EAClC,MAAM;EACN,OAAO;EACP,aACE;EAEF,YAAY;GACV,MAAM,kBAAkB,yDAAyD;GACjF,QAAQ,UAAU,sDAAsD;EAC1E;EACA,QAAQ,EAAE,MAAM,aAAa,4BAA4B,KAAK;;;;;;;;;mDASf,SAAS,WAAW,OAAO,WAAW,GAAG,IACtF,SACI,0HACA,2EACL;;;;CAIH,CAAC;AACH;;;ACvDA,MAAa,aAAa,EACvB,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,SACC,+HAEF;AAEF,MAAa,cAAc,EACxB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACxB,IAAI,CAAC,CAAC,CACN,IAAI,GAAG,CAAC,CACR,SAAS,yCAAyC;AAErD,MAAa,YAAY,EACtB,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SAAS,gFAA4E;AAExF,MAAa,cAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4CAA4C;;;;;;;;;;;AC5BlG,MAAa,mBAAmB,OAC9B,QACA,QACqC;CACrC,MAAM,QAAQ,MAAM,OAAO,MAAM;CACjC,MAAM,UAAU,OAAO,OAAO;CAC9B,MAAM,WAAW,MAAM,gBAAgB,SAAS,MAAM,OAAO,SAAS,IAAI,CAAC;CAE3E,MAAM,iBAAiB,QAAQ,WAC3B,YACA,QAAQ,SACN,WACA;CAEN,OAAO;EACL,QAAQ,EAAE,MAAM;EAChB,aAAa;GACX,YAAY,MAAM,gBAAgB,SAAS,YAAY;GACvD;GACA,GAAI,QAAQ,WACR,CAAC,IACD,EACE,YAAY;IACV;IACA;IACA;GACF,EACF;EACN;EACA,OAAO;GACL,MAAM,QAAQ;GACd,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,WAAW,QAAQ;GACnB,YAAY,QAAQ;GACpB,cAAc,QAAQ;GACtB,aAAa,MAAM;GACnB,QAAQ,QAAQ;EAClB;EACA;EACA,UAAU;GACR,aAAa,IAAI;GAIjB,eAAe,OAAO,OAAO;GAC7B,kBAAkB,OAAO,OAAO;GAChC,WAAW,OAAO,OAAO;GACzB,YAAY,OAAO,OAAO;GAI1B,kBAAkB,OAAO,OAAO;GAChC,kBAAkB,OAAO,OAAO;GAChC,eAAe,OAAO,OAAO;EAC/B;EACA,SAAS;GACP;GAEA,sCAAsC,OAAO,OAAO,iBAAiB;GAGrE;GACA;EACF;CACF;AACF;;;;;;;;AASA,MAAa,4BACX,QACA,QACA,QACS;CACT,OAAO,aACL,2BACA;EACE,aACE;EAIF,aAAa,CAAC;EACd,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,YAAY,WAAW,iBAAiB,QAAQ,GAAG,CAAC,CAAC,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAC/E;AACF;;;ACrGA,MAAa,wBAAwB,QAAmB,WAAmC;CACzF,OAAO,aACL,6BACA;EACE,aACE;EAEF,aAAa,CAAC;EACd,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,YAAY,WAAW,OAAO,SAAS,CAAC,CAC1C;CAEA,OAAO,aACL,4BACA;EACE,aACE;EAEF,aAAa,CAAC;EACd,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,YAAY,WAAW,OAAO,QAAQ,CAAC,CACzC;AACF;;;;;;;;;ACjBA,MAAa,uBAAuB,QAAmB,WAAmC;CACxF,OAAO,aACL,2BACA;EACE,aACE;EAEF,aAAa;GACX,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,kDAAkD;GACxF,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,wCAAwC;GAC7E,UAAU,EACP,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SAAS,mEAAmE;EACjF;CACF,GACA,OAAO,EAAE,OAAO,MAAM,eAAe,WAAW,OAAO,WAAW;EAAE;EAAO;EAAM;CAAS,CAAC,CAAC,CAC9F;CAEA,OAAO,aACL,2BACA;EACE,aACE;EAEF,aAAa;GACX,KAAK;GACL,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,2CAA2C;GAC5E,MAAM,EACH,KAAK,CAAC,WAAW,QAAQ,CAAC,CAAC,CAC3B,SAAS,CAAC,CACV,SAAS,gEAAgE;EAC9E;CACF,GACA,OAAO,EAAE,KAAK,MAAM,WAAW,WAAW,OAAO,WAAW,KAAK,MAAM,QAAQ,SAAS,CAAC,CAC3F;CAEA,OAAO,aACL,8BACA;EACE,aACE;EAEF,aAAa;GACX,KAAK;GACL,MAAM,EACH,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,SAAS,4EAA4E;EAC1F;CACF,GACA,OAAO,EAAE,KAAK,WAAW,WAAW,OAAO,cAAc,KAAK,IAAI,CAAC,CACrE;CAEA,OAAO,aACL,yBACA;EACE,aAAa;EACb,aAAa;GAAE,KAAK;GAAY,UAAU;EAAY;CACxD,GACA,OAAO,EAAE,KAAK,eAAe,WAAW,OAAO,SAAS,KAAK,QAAQ,CAAC,CACxE;CAEA,OAAO,aACL,4BACA;EACE,aACE;EAEF,aAAa;GAAE,MAAM;GAAa,SAAS;EAAW;CACxD,GACA,OAAO,EAAE,WAAW,WAAW,OAAO,YAAY,IAAI,CAAC,CACzD;AACF;;;ACtEA,MAAa,qBACX,QACA,QACA,gBACS;CACT,OAAO,aACL,0BACA;EACE,aACE;EAKF,aAAa;GACX,QAAQ;GACR,OAAO;EACT;EACA,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,OAAO,EAAE,QAAQ,YACf,WACE,OAAO,UAAU;EAAE;EAAQ,OAAO,aAAa,OAAO,OAAO,OAAO,UAAU;CAAE,CAAC,CACnF,CACJ;CAEA,OAAO,aACL,4BACA;EACE,aACE;EAIF,aAAa;GACX,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,qDAAqD;GACvF,OAAO,EACJ,KAAK,CAAC,QAAQ,OAAO,CAAC,CAAC,CACvB,SAAS,CAAC,CACV,SACC,sIAEF;GACF,OAAO;GACP,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,qCAAqC;EAC3F;EACA,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,OAAO,EAAE,OAAO,OAAO,OAAO,aAC5B,WACE,OAAO,YAAY;EACjB;EACA,OAAO,SAAS;EAChB,OAAO,aAAa,OAAO,OAAO,OAAO,UAAU;EACnD,QAAQ,UAAU;CACpB,CAAC,CACH,CACJ;CAEA,OAAO,aACL,wBACA;EACE,aACE;EAEF,aAAa;GACX,KAAK;GACL,QAAQ,EACL,KAAK,CAAC,QAAQ,MAAM,CAAC,CAAC,CACtB,SAAS,CAAC,CACV,SAAS,yEAAyE;EACvF;EACA,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,OAAO,EAAE,KAAK,aACZ,WAAW,YAAY;EACrB,MAAM,SAAS,MAAM,OAAO,QAAQ,GAAG;EACvC,KAAK,UAAU,YAAY,UAAU,OAAO,SAAS,MAAM,OAAO,OAAO,OAAO,IAAI;EACpF,OAAO,GAAG,MAAM;CAClB,CAAC,CACL;CAEA,OAAO,aACL,gCACA;EACE,aACE;EAEF,aAAa,EAAE,KAAK,WAAW;EAC/B,aAAa,EAAE,cAAc,KAAK;CACpC,GACA,OAAO,EAAE,UAAU,WAAW,OAAO,YAAY,GAAG,CAAC,CACvD;CAEA,IAAI,CAAC,aAAa;CAElB,OAAO,aACL,+BACA;EACE,aACE;EAIF,aAAa;GACX,KAAK;GACL,cAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,0CAA0C;GACnF,WAAW,EACR,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SAAS,8EAA8E;GAC1F,WAAW,EAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,0CAA0C;EACvF;EACA,aAAa;GAAE,cAAc;GAAO,iBAAiB;GAAO,gBAAgB;EAAM;CACpF,GACA,OAAO,EAAE,KAAK,cAAc,WAAW,gBACrC,WACE,OAAO,eAAe,KAAK,cAAc,WAAW,YAAY,EAAE,UAAU,IAAI,CAAC,CAAC,CACpF,CACJ;AACF;;;;;;;;;;;;;AC7GA,MAAa,iBACX,QACA,QACA,QACS;CACT,yBAAyB,QAAQ,QAAQ,GAAG;CAC5C,qBAAqB,QAAQ,MAAM;CACnC,kBAAkB,QAAQ,QAAQ,IAAI,WAAW;CAEjD,IAAI,CAAC,IAAI,aAAa;CACtB,oBAAoB,QAAQ,MAAM;AACpC;;;ACrBA,MAAa,cAAc,WAAW;AACtC,MAAa,iBAAiB,WAAW;;;;;;AAmBzC,MAAa,gBAAgB,SAA6C;CACxE,MAAM,EAAE,WAAW;CACnB,MAAM,SAAS,IAAI,UAAU;EAAE,MAAM;EAAa,SAAS;CAAe,CAAC;CAE3E,MAAM,SAAS,IAAI,iBAAiB;EAClC;EACA,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;EAC7C,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;CACxD,CAAC;CAED,cACE,QACA;EACE,SAAS;EACT,aAAa;EACb,MAAM,OAAO;EACb,aAAa,OAAO;CACtB,IACC,QAAQ,gBAAgB,cAAc,QAAQ,QAAQ,EAAE,YAAY,CAAC,CACxE;CAOA,IAAI,OAAO,eAAe;EACxB,gBAAgB,QAAQ,OAAO,WAAW;EAC1C,yBAAyB,QAAQ;GAC/B,SAAS;GACT,aAAa;GACb,OAAO;GACP,mBAAmB,iBAAiB,QAAQ,EAAE,aAAa,OAAO,YAAY,CAAC;GAC/E,WAAW;IACT,WAAW;IACX,MAAM,aAAa;KACjB,UAAU,MAAM,OAAO,SAAS;KAChC,SAAS,MAAM,OAAO,QAAQ;IAChC;GACF;EACF,CAAC;CACH;CAEA,OAAO;EAAE;EAAQ;CAAO;AAC1B"}
1
+ {"version":3,"file":"server-Dewhz4d9.js","names":["#db","#where","#select","#toRow","#logger","#storeTried","#store","#allowed","#indexCanAnswer","#fromIndex","#bulkNotes","#allBodies","#bodies","#truncate","#findMedia"],"sources":["../src/build-info.ts","../src/client/errors.ts","../src/client/locate.ts","../src/client/jxa/core.ts","../src/client/jxa/read.ts","../src/client/jxa/repair.ts","../src/client/jxa/write.ts","../src/client/ref.ts","../src/client/protobuf.ts","../src/client/store.ts","../src/client/notes.ts","../src/config.ts","../src/guide.ts","../src/prompts.ts","../src/tools/util.ts","../src/tools/diagnostics.ts","../src/tools/accounts.ts","../src/tools/actions.ts","../src/tools/notes.ts","../src/tools/index.ts","../src/server.ts"],"sourcesContent":["// Build-time / runtime identity for the running server. `name`/`version` are\n// read from package.json at startup (always accurate); `gitCommit` /\n// `gitCommitDate` are injected by tsdown's `define` substitution at build time\n// and fall back to \"unknown\" when running from source (e.g. vitest).\n\nimport { readPackageIdentity, type BuildInfo } from \"@mgcrea/mcp-apple-core\";\n\n// oxlint-disable no-underscore-dangle -- bundler-injected build-time constants.\ndeclare const __GIT_COMMIT__: string;\ndeclare const __GIT_COMMIT_DATE__: string;\n\nconst pkg = readPackageIdentity(new URL(\"../package.json\", import.meta.url), {\n name: \"@mgcrea/mcp-apple-notes\",\n version: \"0.0.0\",\n});\n\nexport type { BuildInfo };\n\nexport const BUILD_INFO: BuildInfo = {\n name: pkg.name,\n version: pkg.version,\n gitCommit: typeof __GIT_COMMIT__ === \"string\" ? __GIT_COMMIT__ : \"unknown\",\n gitCommitDate: typeof __GIT_COMMIT_DATE__ === \"string\" ? __GIT_COMMIT_DATE__ : \"unknown\",\n};\n","/**\n * Notes' error surface. The taxonomy lives in `@mgcrea/mcp-apple-core`; what\n * belongs here is the identity those messages are written against, and the\n * errors that are genuinely about notes.\n */\n\nimport { AppleAutomationError, type SurfaceContext } from \"@mgcrea/mcp-apple-core\";\n\n/** Named in every user-facing error and in the env vars they mention. */\nexport const NOTES_SURFACE: SurfaceContext = { appName: \"Notes\", envPrefix: \"APPLE_NOTES\" };\n\nexport {\n AppleAutomationError as AppleNotesError,\n AppBusyError as NotesBusyError,\n AppNotRunningError as NotesNotRunningError,\n IndexUnavailableError,\n OsascriptTimeoutError,\n PlatformError,\n PreconditionError,\n ProtocolError,\n SchemaDriftError,\n TccDeniedError,\n WritesDisabledError,\n} from \"@mgcrea/mcp-apple-core\";\n\n/** A NoteRef no longer resolves — the note was deleted, or the index is stale. */\nexport class NoteNotFoundError extends AppleAutomationError {\n override readonly name = \"NoteNotFoundError\";\n\n constructor(ref: string) {\n super(\n `No note for ref \"${ref}\". It was probably deleted since the search ran. ` +\n `Re-run the search to get a current ref.`,\n );\n }\n}\n\n/**\n * The note is password-protected and Notes will not hand over its text.\n *\n * The file lane cannot help either: locked notes are AES ciphertext at rest,\n * which is why `ZICNOTEDATA` carries `ZCRYPTOINITIALIZATIONVECTOR` and\n * `ZCRYPTOTAG` columns. Saying so is more useful than an empty body.\n */\nexport class NoteLockedError extends AppleAutomationError {\n override readonly name = \"NoteLockedError\";\n\n constructor(ref: string) {\n super(\n `Note \"${ref}\" is password-protected. Unlock it in Notes and retry — its text is ` +\n `encrypted at rest, so no amount of permission makes it readable from here.`,\n );\n }\n}\n","import { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { describeStore, type StoreFacts } from \"@mgcrea/mcp-apple-core\";\n\n/**\n * Find Notes' store.\n *\n * Simpler than Mail's, and measurably so: Mail's root is version-numbered\n * (`~/Library/Mail/V10`) *and* TCC-hidden, so it needs a four-branch strategy\n * ladder that asks Mail to name its own directory. Notes has one constant path,\n * confirmed on macOS 26.6.\n *\n * The path is TCC-protected, so `statSync` succeeds on it while `open` and\n * `access` are denied — existence and readability are different questions, and\n * only readability answers whether Full Disk Access is granted.\n */\n\n/** `~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite`. */\nexport const GROUP_CONTAINER = \"group.com.apple.notes\";\n\nexport type LocateResult = StoreFacts & {\n storePath: string;\n /** Where attachment bytes live, one directory per account. */\n mediaRoot: string;\n reason: string | null;\n};\n\nexport const defaultStorePath = (home: string = homedir()): string =>\n join(home, \"Library\", \"Group Containers\", GROUP_CONTAINER, \"NoteStore.sqlite\");\n\nexport const locateStore = (\n opts: { storePath?: string | undefined; home?: string } = {},\n): LocateResult => {\n const storePath = opts.storePath ?? defaultStorePath(opts.home);\n const facts = describeStore(storePath);\n\n const reason = facts.exists\n ? facts.readable\n ? null\n : \"NoteStore.sqlite exists but cannot be read. Grant Full Disk Access to the app running \" +\n \"this server (System Settings > Privacy & Security > Full Disk Access) and restart it. \" +\n \"Granting it to Notes.app does nothing — the reader needs the permission.\"\n : `No NoteStore.sqlite at ${storePath}. Has Notes ever been set up on this account?`;\n\n return { ...facts, storePath, mediaRoot: join(dirname(storePath), \"Accounts\"), reason };\n};\n","/**\n * JXA script fragments.\n *\n * Every script here is a static constant. None of them may contain a template\n * interpolation — `assertStaticScript` rejects any script containing `${`, and\n * that includes JavaScript template literals written *inside* the JXA source.\n * Use string concatenation in JXA code.\n *\n * Every script follows the same contract:\n * - it reads its parameters from `JSON.parse(argv[0])`\n * - it returns `JSON.stringify({ok: true, data})` on success\n * - it returns `JSON.stringify({ok: false, error: {code, message}})` on an\n * application-level failure, still exiting 0\n * so that a non-zero exit always means infrastructure rather than \"no such note\".\n *\n * ## Two rules the measurements impose\n *\n * 1. **Never read a property per note in a loop.** One note's `plaintext` costs\n * ~116ms, so a 921-note library would take ~107 seconds. Every read here is a\n * bulk array fetch (`N.notes.plaintext()`), which is one Apple Event whatever\n * the size.\n * 2. **Never use `whose` for text search.** `notes.whose({plaintext: ...})` took\n * 671ms against 97ms for pulling everything and filtering in JS — 6.9x slower,\n * because the specifier evaluates per note across the Apple Event bridge while\n * the bulk form is a single round trip.\n */\n\n/**\n * Shared prelude.\n *\n * The liveness gate matters more than it looks. `Application(\"Notes\").notes`\n * *launches* Notes if it isn't running — which steals focus as a side effect of\n * a read. `NSRunningApplication` answers the same question without launching\n * anything and without a second TCC prompt.\n *\n * Deliberately NOT shared with Mail's prelude: 45% of that file is a UI-scripting\n * workaround for Mail's forced blockquote citation, which every script there\n * carries whether it composes mail or lists accounts. Notes needs none of it.\n */\nexport const PRELUDE = `\nObjC.import(\"AppKit\");\n\nfunction isNotesRunning() {\n var apps = $.NSRunningApplication.runningApplicationsWithBundleIdentifier(\"com.apple.Notes\");\n return apps.count > 0;\n}\n\nfunction ok(data) { return JSON.stringify({ ok: true, data: data }); }\nfunction err(code, message) { return JSON.stringify({ ok: false, error: { code: code, message: String(message) } }); }\n\nfunction iso(d) {\n try { return d ? d.toISOString() : null; } catch (e) { return null; }\n}\n\n/** Read one property defensively: Notes throws on properties it cannot supply. */\nfunction prop(fn, fallback) {\n try {\n var v = fn();\n return v === undefined ? fallback : v;\n } catch (e) {\n return fallback;\n }\n}\n\n/**\n * Zip parallel bulk arrays into records.\n *\n * Bulk fetches are the only affordable read shape, and they come back as\n * separate arrays that line up by index. Rebuilding records here keeps that\n * assumption in one place.\n */\nfunction zip(keys, columns, count) {\n var out = [];\n for (var i = 0; i < count; i++) {\n var row = {};\n for (var k = 0; k < keys.length; k++) {\n var col = columns[k];\n row[keys[k]] = col && i < col.length ? col[i] : null;\n }\n out.push(row);\n }\n return out;\n}\n\n/** Walk the folder tree. Folders nest in the dictionary, so this recurses. */\nfunction folderTree(container, accountId, depth, out) {\n if (depth > 8) return out;\n var subs = [];\n try { subs = container.folders(); } catch (e) { return out; }\n for (var i = 0; i < subs.length; i++) {\n var f = subs[i];\n var id = prop(function () { return String(f.id()); }, null);\n out.push({\n id: id,\n name: prop(function () { return String(f.name()); }, null),\n accountId: accountId,\n depth: depth,\n shared: prop(function () { return f.shared(); }, false),\n noteCount: prop(function () { return f.notes().length; }, null)\n });\n folderTree(f, accountId, depth + 1, out);\n }\n return out;\n}\n`;\n\n/**\n * Wrap a script body in the prelude and the liveness gate.\n *\n * `allowLaunch` is opt-in per script, and only tools whose whole purpose is to\n * make Notes do something set it. A read that silently launches Notes is a read\n * with a side effect.\n */\nexport const script = (body: string, opts: { allowLaunch?: boolean } = {}): string => `\n${PRELUDE}\nfunction run(argv) {\n var p = JSON.parse(argv[0] || \"{}\");\n if (!isNotesRunning() && !${opts.allowLaunch ? \"true\" : \"false\"}) {\n return err(\"APP_NOT_RUNNING\", \"Notes is not running.\");\n }\n try {\n var N = Application(\"Notes\");\n ${body}\n } catch (e) {\n var msg = String(e && e.message ? e.message : e);\n if (msg.indexOf(\"-1743\") !== -1) return err(\"NOT_AUTHORIZED\", msg);\n return err(\"SCRIPT_ERROR\", msg);\n }\n}\n`;\n","import { script } from \"./core.js\";\n\n/** Accounts, with the folder count each one holds. */\nexport const LIST_ACCOUNTS = script(`\n var accounts = N.accounts();\n var out = [];\n for (var i = 0; i < accounts.length; i++) {\n var a = accounts[i];\n out.push({\n id: prop(function () { return String(a.id()); }, null),\n name: prop(function () { return String(a.name()); }, null),\n defaultFolder: prop(function () { return String(a.defaultFolder.name()); }, null),\n folderCount: prop(function () { return a.folders().length; }, 0),\n noteCount: prop(function () { return a.notes().length; }, 0)\n });\n }\n return ok(out);\n`);\n\n/** The folder tree. Folders nest, so this is depth-first rather than a flat list. */\nexport const LIST_FOLDERS = script(`\n var accounts = N.accounts();\n var out = [];\n for (var i = 0; i < accounts.length; i++) {\n var a = accounts[i];\n var accountId = prop(function () { return String(a.id()); }, null);\n var accountName = prop(function () { return String(a.name()); }, null);\n var folders = folderTree(a, accountId, 1, []);\n for (var j = 0; j < folders.length; j++) {\n folders[j].accountName = accountName;\n out.push(folders[j]);\n }\n }\n return ok(out);\n`);\n\n/**\n * Every note's metadata, as bulk arrays.\n *\n * One Apple Event per property for the whole library — ~0.1ms per note — versus\n * ~116ms per note for reading properties one note at a time. Folder membership\n * is resolved with one bulk call per *folder*, which is a handful of round trips\n * rather than one per note.\n */\nexport const BULK_NOTES = script(`\n var ids = N.notes.id();\n var count = ids.length;\n var rows = zip(\n [\"id\", \"name\", \"modified\", \"created\", \"locked\", \"shared\"],\n [\n ids,\n prop(function () { return N.notes.name(); }, []),\n prop(function () { return N.notes.modificationDate(); }, []),\n prop(function () { return N.notes.creationDate(); }, []),\n prop(function () { return N.notes.passwordProtected(); }, []),\n prop(function () { return N.notes.shared(); }, [])\n ],\n count\n );\n\n // Note -> folder, one bulk call per folder rather than one per note.\n var folderOf = {};\n var accounts = N.accounts();\n for (var i = 0; i < accounts.length; i++) {\n var a = accounts[i];\n var accountId = prop(function () { return String(a.id()); }, null);\n var accountName = prop(function () { return String(a.name()); }, null);\n var folders = folderTree(a, accountId, 1, []);\n for (var j = 0; j < folders.length; j++) {\n var f = folders[j];\n var noteIds = [];\n try {\n var handle = N.folders.byId(f.id);\n noteIds = handle.notes.id();\n } catch (e) { noteIds = []; }\n for (var k = 0; k < noteIds.length; k++) {\n folderOf[String(noteIds[k])] = { folderId: f.id, folder: f.name, accountId: accountId, account: accountName };\n }\n }\n }\n\n for (var r = 0; r < rows.length; r++) {\n var row = rows[r];\n row.id = String(row.id);\n row.modified = iso(row.modified);\n row.created = iso(row.created);\n var where = folderOf[row.id];\n row.folder = where ? where.folder : null;\n row.folderId = where ? where.folderId : null;\n row.account = where ? where.account : null;\n row.accountId = where ? where.accountId : null;\n }\n return ok({ count: count, notes: rows });\n`);\n\n/**\n * Every note's id and plaintext, in one round trip.\n *\n * This is the search lane when the index is unavailable: pull everything, filter\n * in JS. Measured at 97ms over 921 notes and 250KB — against 671ms for the\n * equivalent `whose` specifier.\n */\nexport const BULK_PLAINTEXT = script(`\n return ok({ ids: N.notes.id(), texts: N.notes.plaintext() });\n`);\n\n/** Bodies for a specific set of notes, by id. Used after the index narrows things down. */\nexport const GET_NOTE_BODIES = script(`\n var out = [];\n for (var i = 0; i < p.ids.length; i++) {\n var id = String(p.ids[i]);\n var note = null;\n try { note = N.notes.byId(id); } catch (e) { note = null; }\n if (!note) { out.push({ id: id, found: false }); continue; }\n var locked = prop(function () { return note.passwordProtected(); }, false);\n out.push({\n id: id,\n found: true,\n locked: locked,\n name: prop(function () { return String(note.name()); }, null),\n plaintext: locked ? null : prop(function () { return String(note.plaintext()); }, null),\n body: locked ? null : prop(function () { return String(note.body()); }, null),\n modified: iso(prop(function () { return note.modificationDate(); }, null)),\n created: iso(prop(function () { return note.creationDate(); }, null))\n });\n }\n return ok(out);\n`);\n\n/** Attachment metadata for one note. There is no filesystem path in the dictionary. */\nexport const LIST_ATTACHMENTS = script(`\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n var atts = prop(function () { return note.attachments(); }, []);\n var out = [];\n for (var i = 0; i < atts.length; i++) {\n var a = atts[i];\n out.push({\n id: prop(function () { return String(a.id()); }, null),\n name: prop(function () { return String(a.name()); }, null),\n contentIdentifier: prop(function () { return String(a.contentIdentifier()); }, null),\n url: prop(function () { return String(a.URL()); }, null),\n created: iso(prop(function () { return a.creationDate(); }, null)),\n modified: iso(prop(function () { return a.modificationDate(); }, null))\n });\n }\n return ok(out);\n`);\n","/**\n * Repair for Notes' lossy `body` round trip.\n *\n * `UPDATE_NOTE`'s append mode reads `note.body()` and assigns the result back,\n * so whatever the getter mangles is written back mangled — onto content the\n * caller never touched. Two independent losses, both reproduced against Notes\n * on macOS 15:\n *\n * 1. **Entities come back unterminated.** The text `ps.\"projectId\";` serialises\n * to `ps.&quotprojectId&quot;` — `&quot` with no semicolon. Fed back to the\n * setter, the trailing `&quot` fuses with the literal `;` that followed it\n * into a single `\"`, and the semicolon is gone. The getter never terminates\n * an entity, so a `&quot;` in its output is always entity-plus-literal-`;`,\n * never a well-formed `&quot;`. That is what makes the repair unconditional\n * rather than a `(?!;)` lookahead — a lookahead would skip exactly the case\n * that loses a character.\n * 2. **Space runs come back literal**, and HTML collapses them on re-parse, so\n * every indented line loses its indentation and every aligned block loses\n * its alignment.\n *\n * Order matters: entities first, then spaces. The space pass emits `&nbsp;`\n * with a terminating semicolon, and running the entity pass afterwards would\n * rewrite those into `&nbsp;;`.\n *\n * Exported as source rather than a function because it runs inside the JXA\n * script, in osascript's JavaScriptCore. `test/repair.test.ts` evaluates this\n * same string, so the tested code is the shipped code.\n */\nexport const REPAIR_BODY_SRC = `\nvar repairBody = function (html) {\n var entities = html\n .replace(/&(amp|quot|apos|lt|gt|nbsp)/g, \"&$1;\")\n .replace(/&#(x?[0-9a-fA-F]+)/g, \"&#$1;\");\n return entities.replace(/>([^<]*)</g, function (_m, text) {\n return \">\" + text\n .replace(/ {2,}/g, function (run) { return new Array(run.length + 1).join(\"&nbsp;\"); })\n .replace(/^ /, \"&nbsp;\") + \"<\";\n });\n};\n`;\n","import { script } from \"./core.js\";\nimport { REPAIR_BODY_SRC } from \"./repair.js\";\n\n/**\n * Mutations may launch Notes.\n *\n * A read that silently launches an app is a read with a side effect, which is\n * why the read scripts refuse. A tool whose whole purpose is to create or change\n * a note has no such problem — the user asked for the change.\n */\nconst MUTATION = { allowLaunch: true } as const;\n\n/**\n * Create a note.\n *\n * Notes derives a note's `name` from the first line of its body rather than from\n * a settable property, so the title is prepended as a heading instead of\n * assigned. Setting `name` directly is silently ignored.\n */\nexport const CREATE_NOTE = script(\n `\n var folder = null;\n if (p.folderId) {\n try { folder = N.folders.byId(String(p.folderId)); } catch (e) { folder = null; }\n if (!folder) return err(\"FOLDER_NOT_FOUND\", \"No folder with id \" + p.folderId);\n } else {\n var accounts = N.accounts();\n if (!accounts.length) return err(\"NO_ACCOUNT\", \"Notes reports no accounts.\");\n folder = accounts[0].defaultFolder;\n }\n\n var html = \"\";\n if (p.title) html += \"<h1>\" + p.title + \"</h1>\";\n if (p.body) html += p.body;\n if (!html) return err(\"EMPTY_NOTE\", \"A note needs a title or a body.\");\n\n var note = N.Note({ body: html });\n folder.notes.push(note);\n return ok({\n id: prop(function () { return String(note.id()); }, null),\n name: prop(function () { return String(note.name()); }, null),\n folder: prop(function () { return String(folder.name()); }, null)\n });\n`,\n MUTATION,\n);\n\n/**\n * Replace a note's body. The title follows the first line, as it does on create.\n *\n * Append re-serialises the *existing* body through Notes' HTML parser, which is\n * lossy in both directions — see `repair.ts`. `repairBody` undoes the getter's\n * damage before the concatenation, so an append leaves untouched content byte\n * identical instead of silently flattening indentation and eating characters.\n */\nexport const UPDATE_NOTE = script(\n REPAIR_BODY_SRC +\n `\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n if (prop(function () { return note.passwordProtected(); }, false)) {\n return err(\"NOTE_LOCKED\", \"That note is password-protected; unlock it in Notes first.\");\n }\n\n var current = prop(function () { return String(note.body()); }, \"\");\n var next = p.mode === \"append\" ? repairBody(current) + (p.body || \"\") : (p.body || \"\");\n note.body = next;\n return ok({\n id: String(p.id),\n name: prop(function () { return String(note.name()); }, null),\n bodyLength: next.length,\n mode: p.mode || \"replace\"\n });\n`,\n MUTATION,\n);\n\n/** Move a note to another folder. */\nexport const MOVE_NOTE = script(\n `\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n var folder = null;\n try { folder = N.folders.byId(String(p.folderId)); } catch (e) { folder = null; }\n if (!folder) return err(\"FOLDER_NOT_FOUND\", \"No folder with id \" + p.folderId);\n\n N.move(note, { to: folder });\n return ok({\n id: String(p.id),\n folder: prop(function () { return String(folder.name()); }, null)\n });\n`,\n MUTATION,\n);\n\n/**\n * Attach a file to a note.\n *\n * The scripting dictionary looks read-only on attachments — every property of\n * the `attachment` class is `access=\"r\"` — but that reads the wrong half of it.\n * `note` declares `<element type=\"attachment\">`, and `make` comes from the\n * Standard Suite, so it appears in no command list of the Notes suite. Apple\n * says so outright in a comment on the hidden `contents` property: the point of\n * hiding it is \"to facilitate creating an attachment like this: make new\n * attachment with data myFile\".\n *\n * THE FORM MATTERS. Measured on macOS 26.6, only one of the three works:\n *\n * N.make({new: \"attachment\", at: note, withData: Path(f)}) works\n * N.make({new: \"attachment\", at: note.attachments, withData: f}) -10014\n * note.attachments.push(N.Attachment({contents: Path(f)})) -10000\n *\n * `at:` takes the note itself, not its attachment collection — the opposite of\n * how `folder.notes.push` creates a note two functions up.\n *\n * Setting an image through the note `body` instead does NOT work and fails\n * deceptively: `<img src=\"file://...\">` creates an attachment row whose bytes\n * are never fetched, and a `data:` URI lands as `public.data` with no preview.\n * See docs/notes.md.\n */\nexport const ADD_ATTACHMENT = script(\n `\n var note = null;\n try { note = N.notes.byId(String(p.id)); } catch (e) { note = null; }\n if (!note) return err(\"NOTE_NOT_FOUND\", \"No note with id \" + p.id);\n if (prop(function () { return note.passwordProtected(); }, false)) {\n return err(\"NOTE_LOCKED\", \"That note is password-protected; unlock it in Notes first.\");\n }\n\n var before = {};\n var existing = prop(function () { return note.attachments(); }, []);\n for (var i = 0; i < existing.length; i++) {\n try { before[String(existing[i].id())] = true; } catch (e) {}\n }\n\n try {\n N.make({ new: \"attachment\", at: note, withData: Path(String(p.path)) });\n } catch (e) {\n return err(\"ATTACH_FAILED\", \"Notes refused the file: \" + e);\n }\n\n // Report the attachment that appeared, not a count. Notes can enumerate the\n // same attachment more than once, so a count would be wrong as often as right.\n var added = null;\n var after = prop(function () { return note.attachments(); }, []);\n for (var j = 0; j < after.length; j++) {\n var id = null;\n try { id = String(after[j].id()); } catch (e) { continue; }\n if (!before[id]) {\n added = { id: id, name: prop((function (a) { return function () { return String(a.name()); }; })(after[j]), null) };\n break;\n }\n }\n if (!added) return err(\"ATTACH_FAILED\", \"Notes reported no new attachment on that note.\");\n\n return ok({ noteId: String(p.id), attachment: added });\n`,\n MUTATION,\n);\n\n/**\n * Delete notes.\n *\n * Notes moves them to Recently Deleted rather than destroying them, which is\n * what makes this safe enough to expose at all — but it is still gated behind an\n * explicit confirm at the tool layer.\n */\nexport const DELETE_NOTES = script(\n `\n var deleted = [];\n var missing = [];\n for (var i = 0; i < p.ids.length; i++) {\n var id = String(p.ids[i]);\n var note = null;\n try { note = N.notes.byId(id); } catch (e) { note = null; }\n if (!note) { missing.push(id); continue; }\n try { N.delete(note); deleted.push(id); } catch (e) { missing.push(id); }\n }\n return ok({ deleted: deleted, missing: missing, note: \"Deleted notes go to Recently Deleted.\" });\n`,\n MUTATION,\n);\n","/**\n * NoteRef — the one identifier any tool accepts or returns.\n *\n * Notes' own id is already globally unique and resolvable without a container,\n * unlike Mail's message ids: `x-coredata://<store>/ICNote/p<N>` addresses a note\n * outright, so this carries no folder and needs no lookup ladder.\n *\n * Wire format: n1:<coredata-id>\n *\n * The `n1:` prefix is deliberate. If the id scheme ever changes, a versioned\n * prefix makes that an additive change instead of a silent reinterpretation of\n * every ref already in a conversation.\n *\n * Deliberately NOT shared with Mail's `ref.ts`: that codec's `#(\\d+)$` and\n * `Number.isSafeInteger` both assume an integer id, which this is not.\n */\n\nimport { PreconditionError } from \"./errors.js\";\n\nexport const REF_VERSION = \"n1\";\n\n/** `x-coredata://<uuid>/ICNote/p123` — the primary key is the trailing p-number. */\nconst CORE_DATA_ID = /^x-coredata:\\/\\/[^/]+\\/ICNote\\/p(\\d+)$/;\n\nexport type NoteRef = {\n /** The full Core Data URI, which is what Apple Events resolves. */\n id: string;\n /** `Z_PK` in `ZICCLOUDSYNCINGOBJECT` — measured, see docs/notes.md. */\n primaryKey: number;\n};\n\nexport const encodeRef = (id: string): string => `${REF_VERSION}:${id}`;\n\nexport const decodeRef = (raw: string): NoteRef => {\n const m = /^([a-z0-9]+):(.+)$/.exec(raw);\n if (!m) {\n throw new PreconditionError(\n `Malformed note ref \"${raw}\". Refs come from the search and list tools — ` +\n `construct them from those results rather than by hand.`,\n { expected: `${REF_VERSION}:x-coredata://<store>/ICNote/p<N>` },\n );\n }\n const [, version, id] = m;\n if (version !== REF_VERSION) {\n throw new PreconditionError(\n `Unknown note ref version \"${version}\". This server issues \"${REF_VERSION}:\" refs.`,\n );\n }\n const pk = CORE_DATA_ID.exec(id ?? \"\")?.[1];\n if (!pk) {\n throw new PreconditionError(`Note ref \"${raw}\" does not carry a Core Data note id.`);\n }\n return { id: id as string, primaryKey: Number(pk) };\n};\n\n/**\n * `Z_PK` for an attachment id, which is shaped like a note id but for ICAttachment.\n *\n * The entity segment is not pinned: Apple Events hands back ICAttachment for a\n * file and ICInlineAttachment for an inline one, and both address a row in the\n * same table by the same trailing p-number.\n */\nconst CORE_DATA_ATTACHMENT_ID = /^x-coredata:\\/\\/[^/]+\\/IC\\w*Attachment\\w*\\/p(\\d+)$/;\n\nexport const attachmentPrimaryKey = (id: string): number | null => {\n const pk = CORE_DATA_ATTACHMENT_ID.exec(id)?.[1];\n return pk ? Number(pk) : null;\n};\n\n/** Build a ref from an index row, which only ever knows the primary key. */\nexport const refFromPrimaryKey = (storeUuid: string, primaryKey: number): string =>\n encodeRef(`x-coredata://${storeUuid}/ICNote/p${primaryKey}`);\n","/**\n * Minimal protobuf wire-format reader, enough to pull the body text out of a\n * `ZICNOTEDATA.ZDATA` blob.\n *\n * ## Why it does not hardcode Apple's schema\n *\n * The usual approach compiles Apple's unpublished `.proto` and reads\n * `document.note.note_text` by field number. That breaks the first time Apple\n * renumbers a field, and this project already budgets for the schema drifting\n * on every macOS release.\n *\n * So this walks the wire format structurally, and the path is *measured* rather\n * than assumed — see `NOTE_TEXT_PATH`.\n *\n * Wire format, for reference:\n * tag = varint; field = tag >> 3; wire type = tag & 7\n * 0 varint · 1 fixed64 · 2 length-delimited · 5 fixed32 · 3/4 groups (legacy)\n *\n * Pure and I/O-free: callers gunzip, callers decide what to do with the result.\n */\n\n/** Depth cap. Notes nests a handful deep; anything past this is a misparse. */\nconst MAX_DEPTH = 8;\n/** Below this, a \"string\" is more likely coincidence than text. */\nconst MIN_TEXT_LENGTH = 2;\n/** Above this share of control characters, the bytes are not text. */\nconst MAX_CONTROL_RATIO = 0.05;\n\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\n/** Tab, newline and carriage return are text; the rest of C0 and DEL are not. */\nconst isControl = (code: number): boolean =>\n (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) || code === 0x7f;\n\nexport type Varint = { value: number; next: number };\n\n/** Read a base-128 varint. Returns null rather than throwing on a truncated one. */\nexport const readVarint = (buf: Uint8Array, pos: number): Varint | null => {\n let value = 0;\n let shift = 0;\n let p = pos;\n while (p < buf.length) {\n const byte = buf[p] as number;\n p += 1;\n // Beyond 2^53 the arithmetic stops being exact, and no tag or length we\n // care about is that large, so bail rather than return a wrong number.\n if (shift > 45) return null;\n value += (byte & 0x7f) * 2 ** shift;\n if ((byte & 0x80) === 0) return { value, next: p };\n shift += 7;\n }\n return null;\n};\n\nexport type WireField = {\n field: number;\n wire: number;\n value?: number;\n from?: number;\n to?: number;\n};\n\n/**\n * Split a buffer into wire-format fields. Returns null if the bytes are not a\n * well-formed message ending exactly on the boundary — that \"exactly\" is what\n * makes this usable as a test for whether a blob is a nested message.\n */\nexport const parseFields = (\n buf: Uint8Array,\n start = 0,\n end: number = buf.length,\n): WireField[] | null => {\n const fields: WireField[] = [];\n let p = start;\n while (p < end) {\n const tag = readVarint(buf, p);\n if (!tag) return null;\n const field = Math.floor(tag.value / 8);\n const wire = tag.value % 8;\n if (field === 0) return null;\n p = tag.next;\n if (wire === 0) {\n const v = readVarint(buf, p);\n if (!v) return null;\n fields.push({ field, wire, value: v.value });\n p = v.next;\n } else if (wire === 2) {\n const len = readVarint(buf, p);\n if (!len) return null;\n const from = len.next;\n const to = from + len.value;\n if (to > end) return null;\n fields.push({ field, wire, from, to });\n p = to;\n } else if (wire === 1 || wire === 5) {\n const width = wire === 1 ? 8 : 4;\n if (p + width > end) return null;\n fields.push({ field, wire });\n p += width;\n } else {\n // Groups (3/4) are legacy and Notes does not use them; treating them as a\n // misparse is safer than guessing where the group ends.\n return null;\n }\n }\n return p === end ? fields : null;\n};\n\n/** Valid UTF-8 that is mostly not control characters. */\nconst asText = (bytes: Uint8Array): string | null => {\n let text: string;\n try {\n text = utf8.decode(bytes);\n } catch {\n return null;\n }\n if (text.length < MIN_TEXT_LENGTH) return null;\n let controls = 0;\n for (let i = 0; i < text.length; i += 1) {\n if (isControl(text.charCodeAt(i))) controls += 1;\n }\n return controls / text.length > MAX_CONTROL_RATIO ? null : text;\n};\n\nexport type StringCandidate = { path: string; text: string; length: number };\n\n/**\n * Every string in the blob, with the field path that reached it.\n *\n * A length-delimited field can be both a valid message and valid text, so both\n * readings are followed. That ambiguity is the normal case, not an error.\n */\nexport const collectStrings = (buf: Uint8Array, maxDepth = MAX_DEPTH): StringCandidate[] => {\n const out: StringCandidate[] = [];\n const walk = (start: number, end: number, path: number[], depth: number): void => {\n if (depth > maxDepth) return;\n const fields = parseFields(buf, start, end);\n if (!fields) return;\n for (const f of fields) {\n if (f.wire !== 2 || f.from === undefined || f.to === undefined) continue;\n const here = [...path, f.field];\n const text = asText(buf.subarray(f.from, f.to));\n if (text) out.push({ path: here.join(\".\"), text, length: text.length });\n walk(f.from, f.to, here, depth + 1);\n }\n };\n walk(0, buf.length, [], 0);\n return out;\n};\n\n/**\n * Where the body lives: `document(2) -> note(3) -> note_text(2)`.\n *\n * Measured, not assumed. Running the structural walk over 53 real notes and\n * checking each result against the same note's Apple Events plaintext, every\n * note read from this path matched exactly (53/53), and picking the longest\n * string instead matched only 27/53 — on the other 26 an attribute-run field\n * outran the body by ~60 characters. That failure is silent: it returns\n * plausible, wrong text rather than an error.\n */\nexport const NOTE_TEXT_PATH = \"2.3.2\";\n\nexport type DecodedNote = {\n text: string | null;\n path: string | null;\n /** `pinned` or `longest`. A shift to `longest` is the schema-drift signal. */\n via: \"pinned\" | \"longest\" | null;\n candidateCount: number;\n};\n\n/**\n * The note body.\n *\n * Reads the pinned path first and falls back to the longest string only if that\n * path is absent, reporting which route it took so drift is detectable rather\n * than silently wrong.\n */\nexport const extractNoteText = (\n buf: Uint8Array,\n preferPath: string | null = NOTE_TEXT_PATH,\n): DecodedNote => {\n const candidates = collectStrings(buf);\n const pinned = preferPath ? (candidates.find((c) => c.path === preferPath) ?? null) : null;\n const longest = candidates.toSorted((a, b) => b.length - a.length)[0] ?? null;\n const best = pinned ?? longest;\n return {\n text: best?.text ?? null,\n path: best?.path ?? null,\n via: best === null ? null : pinned ? \"pinned\" : \"longest\",\n candidateCount: candidates.length,\n };\n};\n","import type { DatabaseSync } from \"node:sqlite\";\nimport { gunzipSync } from \"node:zlib\";\n\nimport {\n columnsOf,\n CORE_DATA_EPOCH_OFFSET,\n escapeLike,\n fingerprintSchema,\n openReadOnly,\n parseBound,\n type Logger,\n type ReadOnlyMode,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { SchemaDriftError } from \"./errors.js\";\nimport { extractNoteText } from \"./protobuf.js\";\n\n/**\n * Read-only access to `NoteStore.sqlite`.\n *\n * Everything here is measured against a real store — see `docs/notes.md`. Three\n * facts do most of the work:\n *\n * 1. **`Z_ENT` alone is not \"a note the user sees\".** On the probed library it\n * matched 1191 rows against 921 notes reported by Apple Events, and the two\n * deletion columns explained none of the difference. `ZTITLE1 IS NOT NULL`\n * reconciles them **exactly** — verified by comparing `Z_PK` sets, not counts.\n * 2. **No column holds the body.** `ZTITLE1` is the title (100% coverage) and\n * `ZWIDGETSNIPPET` a ~86-character preview; the text lives in a gzipped\n * protobuf in `ZICNOTEDATA.ZDATA`.\n * 3. **Dates are Core Data seconds**, offset 978307200.\n */\n\n/** Every column this module reads, so a missing one is named rather than thrown at. */\nconst REQUIRED_COLUMNS = [\"Z_PK\", \"Z_ENT\", \"ZTITLE1\", \"ZMODIFICATIONDATE1\", \"ZCREATIONDATE1\"];\n\nexport type NoteRow = {\n primaryKey: number;\n title: string | null;\n snippet: string | null;\n modified: string | null;\n created: string | null;\n folderPk: number | null;\n locked: boolean;\n};\n\nexport type StoreCapabilities = {\n fingerprint: string;\n /**\n * The persistent store UUID, which is the middle segment of every note's\n * Apple Events id (`x-coredata://<uuid>/ICNote/p<N>`). Reading it here lets an\n * index row be turned into a ref without asking Notes anything.\n */\n storeUuid: string | null;\n /** `Z_ENT` for `ICNote`, looked up by name rather than hardcoded to 12. */\n noteEnt: number | null;\n /** Present on this schema, so the queries can use them. */\n has: {\n title: boolean;\n snippet: boolean;\n widgetSnippet: boolean;\n folder: boolean;\n locked: boolean;\n /** `ZMEDIA`, the ICAttachment -> ICMedia hop that leads to the bytes. */\n media: boolean;\n mediaFilename: boolean;\n /** `ZGENERATION1`, the inner directory. Absent on older schemas. */\n mediaGeneration: boolean;\n };\n deletionColumns: string[];\n missing: string[];\n};\n\nconst OPTIONAL = {\n snippet: \"ZSNIPPET\",\n widgetSnippet: \"ZWIDGETSNIPPET\",\n folder: \"ZFOLDER\",\n locked: \"ZISPASSWORDPROTECTED\",\n media: \"ZMEDIA\",\n mediaFilename: \"ZFILENAME\",\n mediaGeneration: \"ZGENERATION1\",\n} as const;\n\nexport const introspect = (db: DatabaseSync): StoreCapabilities => {\n const cols = columnsOf(db, \"ZICCLOUDSYNCINGOBJECT\");\n const missing = REQUIRED_COLUMNS.filter((c) => !cols.includes(c));\n\n let noteEnt: number | null = null;\n try {\n const row = db.prepare(\"SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICNote'\").get() as\n | { Z_ENT: number }\n | undefined;\n noteEnt = row?.Z_ENT ?? null;\n } catch {\n noteEnt = null;\n }\n if (noteEnt === null) missing.push(\"Z_PRIMARYKEY row for ICNote\");\n\n let storeUuid: string | null = null;\n try {\n const row = db.prepare(\"SELECT Z_UUID FROM Z_METADATA LIMIT 1\").get() as\n | { Z_UUID: string }\n | undefined;\n storeUuid = row?.Z_UUID ?? null;\n } catch {\n storeUuid = null;\n }\n\n return {\n fingerprint: fingerprintSchema(db),\n storeUuid,\n noteEnt,\n has: {\n title: cols.includes(\"ZTITLE1\"),\n snippet: cols.includes(OPTIONAL.snippet),\n widgetSnippet: cols.includes(OPTIONAL.widgetSnippet),\n folder: cols.includes(OPTIONAL.folder),\n locked: cols.includes(OPTIONAL.locked),\n media: cols.includes(OPTIONAL.media),\n mediaFilename: cols.includes(OPTIONAL.mediaFilename),\n mediaGeneration: cols.includes(OPTIONAL.mediaGeneration),\n },\n // Kept in the predicate even though the probed library had nothing in the\n // trash: that clause was never exercised, which makes it untested rather\n // than proven redundant, and a library with trashed notes is exactly where\n // its absence would show up as phantom results.\n deletionColumns: cols.filter((c) => /^Z(ISRECOVERINGFROMTRASH|MARKEDFORDELETION)$/.test(c)),\n missing,\n };\n};\n\nexport const assertUsable = (caps: StoreCapabilities): void => {\n if (caps.missing.length) {\n throw new SchemaDriftError(\n `NoteStore.sqlite is not the schema this server knows how to read (fingerprint ` +\n `${caps.fingerprint}). Missing: ${caps.missing.join(\", \")}. The index lane is unavailable; ` +\n `the Apple Events lane still works.`,\n );\n }\n};\n\nconst toIso = (coreDataSeconds: number | null): string | null => {\n if (coreDataSeconds === null || !Number.isFinite(coreDataSeconds)) return null;\n return new Date((coreDataSeconds + CORE_DATA_EPOCH_OFFSET) * 1000).toISOString();\n};\n\nexport type SearchFilters = {\n query?: string | undefined;\n folderPk?: number | undefined;\n modifiedAfter?: string | undefined;\n modifiedBefore?: string | undefined;\n limit: number;\n offset: number;\n};\n\nexport class NoteStore {\n readonly #db: DatabaseSync;\n readonly caps: StoreCapabilities;\n readonly mode: \"ro\" | \"immutable\";\n\n constructor(db: DatabaseSync, caps: StoreCapabilities, mode: \"ro\" | \"immutable\") {\n this.#db = db;\n this.caps = caps;\n this.mode = mode;\n }\n\n /**\n * The predicate for \"a note the user sees\".\n *\n * `Z_ENT` picks note rows; `ZTITLE1 IS NOT NULL` drops the title-less rows\n * that Apple Events does not report. Verified by set equality against the\n * 921 ids Apple Events returned — a matching count would not have been\n * evidence, as the first ZDATA decoder demonstrated.\n */\n #where(): string {\n const deletion = this.caps.deletionColumns\n .map((c) => ` AND (o.\"${c}\" IS NULL OR o.\"${c}\" = 0)`)\n .join(\"\");\n return `o.Z_ENT = ? AND o.ZTITLE1 IS NOT NULL${deletion}`;\n }\n\n #select(): string {\n const snippet = this.caps.has.widgetSnippet\n ? \"o.ZWIDGETSNIPPET\"\n : this.caps.has.snippet\n ? \"o.ZSNIPPET\"\n : \"NULL\";\n const folder = this.caps.has.folder ? \"o.ZFOLDER\" : \"NULL\";\n const locked = this.caps.has.locked ? \"o.ZISPASSWORDPROTECTED\" : \"0\";\n return `SELECT o.Z_PK AS pk, o.ZTITLE1 AS title, ${snippet} AS snippet,\n o.ZMODIFICATIONDATE1 AS modified, o.ZCREATIONDATE1 AS created,\n ${folder} AS folderPk, ${locked} AS locked\n FROM ZICCLOUDSYNCINGOBJECT o`;\n }\n\n #toRow(r: Record<string, unknown>): NoteRow {\n return {\n primaryKey: r.pk as number,\n title: (r.title as string | null) ?? null,\n snippet: (r.snippet as string | null) ?? null,\n modified: toIso((r.modified as number | null) ?? null),\n created: toIso((r.created as number | null) ?? null),\n folderPk: (r.folderPk as number | null) ?? null,\n locked: Boolean(r.locked),\n };\n }\n\n count(): number {\n const row = this.#db\n .prepare(`SELECT COUNT(*) AS c FROM ZICCLOUDSYNCINGOBJECT o WHERE ${this.#where()}`)\n .get(this.caps.noteEnt) as { c: number };\n return row.c;\n }\n\n /**\n * Title and snippet search.\n *\n * Deliberately not full text: no indexed column carries the body. Callers that\n * need body matching combine this with `bodyOf` or fall back to the Apple\n * Events bulk scan.\n */\n search(filters: SearchFilters): NoteRow[] {\n const clauses = [this.#where()];\n const params: unknown[] = [this.caps.noteEnt];\n\n if (filters.query) {\n const like = `%${escapeLike(filters.query)}%`;\n const snippetCol = this.caps.has.widgetSnippet\n ? \"o.ZWIDGETSNIPPET\"\n : this.caps.has.snippet\n ? \"o.ZSNIPPET\"\n : null;\n clauses.push(\n snippetCol\n ? `(o.ZTITLE1 LIKE ? ESCAPE '\\\\' OR ${snippetCol} LIKE ? ESCAPE '\\\\')`\n : `o.ZTITLE1 LIKE ? ESCAPE '\\\\'`,\n );\n params.push(like);\n if (snippetCol) params.push(like);\n }\n if (filters.folderPk !== undefined && this.caps.has.folder) {\n clauses.push(\"o.ZFOLDER = ?\");\n params.push(filters.folderPk);\n }\n // Through the shared grammar, which refuses what it cannot read. `Date.parse`\n // answered NaN, node:sqlite bound NaN as NULL, and `>= NULL` matched nothing\n // — an empty result presented as an answer. No tool reaches these today,\n // which is exactly why it would have gone unnoticed when one did.\n if (filters.modifiedAfter) {\n clauses.push(\"o.ZMODIFICATIONDATE1 >= ?\");\n params.push(\n parseBound(\"modifiedAfter\", filters.modifiedAfter, \"start\").getTime() / 1000 -\n CORE_DATA_EPOCH_OFFSET,\n );\n }\n if (filters.modifiedBefore) {\n // Exclusive, matching the next-day midnight a bare day resolves to.\n clauses.push(\"o.ZMODIFICATIONDATE1 < ?\");\n params.push(\n parseBound(\"modifiedBefore\", filters.modifiedBefore, \"end\").getTime() / 1000 -\n CORE_DATA_EPOCH_OFFSET,\n );\n }\n\n const sql = `${this.#select()} WHERE ${clauses.join(\" AND \")}\n ORDER BY o.ZMODIFICATIONDATE1 DESC LIMIT ? OFFSET ?`;\n params.push(filters.limit, filters.offset);\n return (this.#db.prepare(sql).all(...(params as never[])) as Record<string, unknown>[]).map(\n (r) => this.#toRow(r),\n );\n }\n\n byPrimaryKey(pk: number): NoteRow | null {\n const row = this.#db\n .prepare(`${this.#select()} WHERE ${this.#where()} AND o.Z_PK = ?`)\n .get(this.caps.noteEnt, pk) as Record<string, unknown> | undefined;\n return row ? this.#toRow(row) : null;\n }\n\n /**\n * The note body, decoded from the gzipped protobuf.\n *\n * Returns null for password-protected notes: their `ZDATA` is AES ciphertext,\n * flagged by a non-null `ZCRYPTOTAG`. No permission makes that readable.\n */\n bodyOf(pk: number): { text: string | null; via: string | null; encrypted: boolean } {\n const row = this.#db\n .prepare(\"SELECT ZDATA AS data, ZCRYPTOTAG AS tag FROM ZICNOTEDATA WHERE ZNOTE = ?\")\n .get(pk) as { data: Uint8Array | null; tag: Uint8Array | null } | undefined;\n if (!row?.data) return { text: null, via: null, encrypted: false };\n if (row.tag) return { text: null, via: null, encrypted: true };\n\n const blob = Buffer.from(row.data);\n if (blob[0] !== 0x1f || blob[1] !== 0x8b) return { text: null, via: null, encrypted: false };\n let inflated: Buffer;\n try {\n inflated = gunzipSync(blob);\n } catch {\n return { text: null, via: null, encrypted: false };\n }\n const decoded = extractNoteText(inflated);\n return { text: decoded.text, via: decoded.via, encrypted: false };\n }\n\n /**\n * Where an attachment's bytes live, resolved through ICMedia.\n *\n * ICAttachment carries NO path of its own — not a filename, not a directory.\n * It points at an ICMedia row through `ZMEDIA`, and that row holds the two\n * directory names and the filename:\n *\n * Accounts/<account>/Media/<ZIDENTIFIER>/<ZGENERATION1>/<ZFILENAME>\n *\n * Established against a real store (see `scripts/probe-notes-media.mjs`): the\n * identifier directory contains only the generation directory, and only the\n * inner one holds bytes — 657 of each on the probed library, exactly paired.\n * That is why the id Apple Events returns cannot locate anything by itself,\n * and why matching a directory against it never worked.\n *\n * `ZGENERATION1` is treated as optional rather than assumed: the caller falls\n * back to the identifier directory when it is absent, so a schema without the\n * generation level degrades instead of failing.\n */\n mediaFor(attachmentPk: number): {\n identifier: string;\n generation: string | null;\n filename: string | null;\n } | null {\n if (!this.caps.has.media) return null;\n const generation = this.caps.has.mediaGeneration ? \"m.ZGENERATION1\" : \"NULL\";\n const filename = this.caps.has.mediaFilename ? \"m.ZFILENAME\" : \"NULL\";\n const row = this.#db\n .prepare(\n `SELECT m.ZIDENTIFIER AS identifier, ${generation} AS generation, ${filename} AS filename ` +\n `FROM ZICCLOUDSYNCINGOBJECT a ` +\n `JOIN ZICCLOUDSYNCINGOBJECT m ON m.Z_PK = a.ZMEDIA ` +\n `WHERE a.Z_PK = ?`,\n )\n .get(attachmentPk) as\n | { identifier: string | null; generation: string | null; filename: string | null }\n | undefined;\n if (!row?.identifier) return null;\n return {\n identifier: row.identifier,\n generation: row.generation ?? null,\n filename: row.filename ?? null,\n };\n }\n\n close(): void {\n this.#db.close();\n }\n}\n\nexport const openStore = (path: string, mode: ReadOnlyMode, logger?: Logger): NoteStore => {\n const {\n db,\n mode: opened,\n validated: caps,\n } = openReadOnly<StoreCapabilities>(path, mode, {\n envVar: \"APPLE_NOTES_INDEX_MODE\",\n label: \"Notes' store\",\n hint: \"If this is a permission error, grant Full Disk Access to the app running this server.\",\n validate: (conn) => {\n const found = introspect(conn);\n assertUsable(found);\n return found;\n },\n fatal: (err) => err instanceof SchemaDriftError,\n onFallback: () =>\n logger?.warn?.(\"opened the store with immutable=1; results may omit notes still in the -wal\"),\n });\n return new NoteStore(db, caps, opened);\n};\n","import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, join, resolve, sep } from \"node:path\";\n\nimport {\n createOsascriptRunner,\n withBusyRetry,\n type Logger,\n type OsascriptRunner,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport type { Config } from \"../config.js\";\nimport { NoteLockedError, NoteNotFoundError, NOTES_SURFACE, PreconditionError } from \"./errors.js\";\nimport {\n BULK_NOTES,\n BULK_PLAINTEXT,\n GET_NOTE_BODIES,\n LIST_ACCOUNTS,\n LIST_ATTACHMENTS,\n LIST_FOLDERS,\n} from \"./jxa/read.js\";\nimport { ADD_ATTACHMENT, CREATE_NOTE, DELETE_NOTES, MOVE_NOTE, UPDATE_NOTE } from \"./jxa/write.js\";\nimport { locateStore, type LocateResult } from \"./locate.js\";\nimport { attachmentPrimaryKey, decodeRef, encodeRef, refFromPrimaryKey } from \"./ref.js\";\nimport { NoteStore, openStore, type NoteRow } from \"./store.js\";\n\nexport type LaneStatus = {\n applescript: \"live\" | \"unavailable\";\n index: \"live\" | \"unavailable\" | \"disabled\";\n indexMode: string | null;\n storeFingerprint: string | null;\n reason: string | null;\n};\n\nexport type NoteAccount = {\n id: string | null;\n name: string | null;\n defaultFolder: string | null;\n folderCount: number;\n noteCount: number;\n};\n\nexport type NoteFolder = {\n id: string | null;\n name: string | null;\n accountId: string | null;\n accountName: string | null;\n depth: number;\n shared: boolean;\n noteCount: number | null;\n};\n\nexport type NoteSummary = {\n ref: string;\n title: string | null;\n snippet: string | null;\n folder: string | null;\n account: string | null;\n modified: string | null;\n created: string | null;\n locked: boolean;\n source: \"index\" | \"apple-events\";\n};\n\n/**\n * A page of notes, plus the two facts a bare array had no room for: which lane\n * answered, and whether anything was left behind.\n *\n * `hasMore` is about the PAGE — more notes matched than `limit` allowed. `note`\n * is about the LANE: the Apple Events bulk read is capped independently of\n * `limit`, so a caller asking for 500 could be handed 200 with nothing saying\n * which of the two bounds stopped the list. Only the second one needs prose,\n * because only the second one is fixable by the person reading it.\n */\nexport type NoteListing = {\n notes: NoteSummary[];\n source: \"index\" | \"apple-events\";\n hasMore: boolean;\n note?: string;\n};\n\ntype BodyCache = {\n at: number;\n source: \"index\" | \"apple-events\";\n /** Note id -> body text. */\n texts: Map<string, string>;\n};\n\nexport type AppleNotesClientOptions = {\n config: Config;\n logger?: Logger | undefined;\n /** Injected by tests so nothing spawns a process or touches a real Notes. */\n osascript?: OsascriptRunner | undefined;\n};\n\n/**\n * The Notes client.\n *\n * Two lanes, and which one answers depends on what was granted:\n *\n * - **Apple Events** — accounts, folders, every mutation, and bodies. Always the\n * authority: after a write, the result is what Notes re-read.\n * - **Index** — `NoteStore.sqlite`, for search and metadata at a scale Apple\n * Events cannot reach.\n *\n * The discipline the measurements impose is in `jxa/core.ts`: bulk array fetches\n * only, never `whose`, never a property read per note.\n */\nexport class AppleNotesClient {\n readonly config: Config;\n readonly runner: OsascriptRunner;\n readonly #logger: Logger | undefined;\n #store: NoteStore | null = null;\n #storeTried = false;\n #bodies: BodyCache | null = null;\n\n constructor(opts: AppleNotesClientOptions) {\n this.config = opts.config;\n this.#logger = opts.logger;\n this.runner =\n opts.osascript ??\n createOsascriptRunner({\n osascriptPath: opts.config.osascriptPath,\n timeoutMs: opts.config.osascriptTimeoutMs,\n surface: NOTES_SURFACE,\n logger: opts.logger,\n });\n }\n\n locate(): LocateResult {\n return locateStore({ storePath: this.config.storePath });\n }\n\n /** Open the index once and remember the outcome, including the failure. */\n index(): NoteStore | null {\n if (this.#storeTried) return this.#store;\n this.#storeTried = true;\n if (this.config.indexMode === \"off\") return null;\n try {\n this.#store = openStore(this.locate().storePath, this.config.indexMode, this.#logger);\n } catch (err) {\n this.#logger?.debug?.(\"index lane unavailable\", err);\n this.#store = null;\n }\n return this.#store;\n }\n\n /**\n * Probe both lanes.\n *\n * Apple Events is probed twice with a gap: the first event is what triggers\n * the Automation prompt, and it can return failure while the user is still\n * deciding — reporting \"denied\" then would be wrong.\n */\n async lanes(): Promise<LaneStatus> {\n let applescript: LaneStatus[\"applescript\"] = \"unavailable\";\n for (let attempt = 0; attempt < 2; attempt += 1) {\n try {\n await this.runner.run(LIST_ACCOUNTS);\n applescript = \"live\";\n break;\n } catch (err) {\n this.#logger?.debug?.(\"applescript probe failed\", err);\n if (attempt === 0) await new Promise((r) => setTimeout(r, 400));\n }\n }\n\n const located = this.locate();\n const store = this.index();\n return {\n applescript,\n index: this.config.indexMode === \"off\" ? \"disabled\" : store ? \"live\" : \"unavailable\",\n indexMode: store?.mode ?? null,\n storeFingerprint: store?.caps.fingerprint ?? null,\n reason: store ? null : located.reason,\n };\n }\n\n #allowed(accountName: string | null): boolean {\n const list = this.config.accounts;\n if (!list.length) return true;\n return Boolean(accountName && list.some((a) => a.toLowerCase() === accountName.toLowerCase()));\n }\n\n /**\n * Whether the index can answer this request FAITHFULLY.\n *\n * Two things it cannot do, and both used to be ignored rather than declined.\n *\n * The store has no readable account name on a note row — `#fromIndex` sets\n * `account: null` — so an account allowlist cannot be applied to index rows.\n * Answering from the index anyway ignored the one setting whose whole job is\n * limiting what gets read, and did so ONLY on machines with Full Disk Access,\n * which is exactly where it matters.\n *\n * And `folder` was accepted, resolved to `undefined` by a stub whose comment\n * claimed the opposite, and dropped: `list_notes({folder: \"Projects\"})`\n * returned the newest notes from every folder.\n *\n * Reminders reached the same conclusion first, in `#indexCanAnswer` there.\n */\n #indexCanAnswer(opts: { folder?: string | undefined } = {}): boolean {\n return this.config.accounts.length === 0 && !opts.folder;\n }\n\n async accounts(): Promise<NoteAccount[]> {\n const all = await withBusyRetry(() => this.runner.run<NoteAccount[]>(LIST_ACCOUNTS));\n return all.filter((a) => this.#allowed(a.name));\n }\n\n async folders(): Promise<NoteFolder[]> {\n const all = await withBusyRetry(() => this.runner.run<NoteFolder[]>(LIST_FOLDERS));\n return all.filter((f) => this.#allowed(f.accountName));\n }\n\n /**\n * Note metadata.\n *\n * Prefers the index — it holds the same fields, costs no Apple Event, and\n * keeps working as a library grows past the point where a bulk fetch stops\n * being instant.\n */\n async listNotes(opts: { folder?: string | undefined; limit: number }): Promise<NoteListing> {\n const store = this.#indexCanAnswer(opts) ? this.index() : null;\n if (store?.caps.storeUuid) {\n // One row past the page, so a full page can be told from an exhausted\n // library. SQL is asked for it rather than counted separately: a COUNT\n // over the whole table costs more than the extra row on every call.\n const rows = store.search({ limit: opts.limit + 1, offset: 0 });\n const uuid = store.caps.storeUuid;\n return {\n notes: rows.slice(0, opts.limit).map((r) => this.#fromIndex(r, uuid)),\n source: \"index\",\n hasMore: rows.length > opts.limit,\n };\n }\n return this.#bulkNotes(opts);\n }\n\n #fromIndex(row: NoteRow, storeUuid: string): NoteSummary {\n return {\n ref: refFromPrimaryKey(storeUuid, row.primaryKey),\n title: row.title,\n snippet: row.snippet,\n folder: null,\n account: null,\n modified: row.modified,\n created: row.created,\n locked: row.locked,\n source: \"index\",\n };\n }\n\n async #bulkNotes(opts: { folder?: string | undefined; limit: number }): Promise<NoteListing> {\n const data = await withBusyRetry(() =>\n this.runner.run<{\n count: number;\n notes: {\n id: string;\n name: string | null;\n modified: string | null;\n created: string | null;\n locked: boolean;\n folder: string | null;\n account: string | null;\n }[];\n }>(BULK_NOTES),\n );\n const matched = data.notes\n .filter((n) => this.#allowed(n.account))\n .filter((n) => !opts.folder || n.folder === opts.folder);\n const cap = Math.min(opts.limit, this.config.degradedMaxNotes);\n return {\n notes: matched.slice(0, cap).map((n) => ({\n ref: encodeRef(n.id),\n title: n.name,\n snippet: null,\n folder: n.folder,\n account: n.account,\n modified: n.modified,\n created: n.created,\n locked: n.locked,\n source: \"apple-events\" as const,\n })),\n source: \"apple-events\",\n hasMore: matched.length > cap,\n // Only when the LANE is what stopped the list — the cap sitting below a\n // larger `limit`, with more notes behind it. A caller whose own limit ran\n // out first is not being shortchanged and does not need telling.\n ...(matched.length > cap && this.config.degradedMaxNotes < opts.limit\n ? {\n note:\n `The Apple Events lane is capped at ${this.config.degradedMaxNotes} notes and you ` +\n `asked for ${opts.limit}, so this list stops short of what matched. Grant Full ` +\n `Disk Access to read Notes' own index instead, or raise ` +\n `APPLE_NOTES_DEGRADED_MAX_NOTES.`,\n }\n : {}),\n };\n }\n\n /**\n * Body text for every note, cached.\n *\n * A TTL rather than invalidation, deliberately: checking whether the cache is\n * stale costs *more* over Apple Events (fetching ids and modification dates,\n * 128ms) than simply redoing the scan (97ms). There is no cheap freshness\n * check to be had, so bounded staleness is the honest trade.\n */\n async #allBodies(): Promise<BodyCache> {\n const now = Date.now();\n if (this.#bodies && now - this.#bodies.at < this.config.searchCacheTtlMs) return this.#bodies;\n\n const store = this.#indexCanAnswer() ? this.index() : null;\n const texts = new Map<string, string>();\n if (store?.caps.storeUuid) {\n const uuid = store.caps.storeUuid;\n for (const row of store.search({ limit: 100_000, offset: 0 })) {\n const body = store.bodyOf(row.primaryKey);\n if (body.text) texts.set(refFromPrimaryKey(uuid, row.primaryKey), body.text);\n }\n this.#bodies = { at: now, source: \"index\", texts };\n return this.#bodies;\n }\n\n const bulk = await withBusyRetry(() =>\n this.runner.run<{ ids: string[]; texts: (string | null)[] }>(BULK_PLAINTEXT),\n );\n bulk.ids.forEach((id, i) => {\n const text = bulk.texts[i];\n if (text) texts.set(encodeRef(String(id)), String(text));\n });\n this.#bodies = { at: now, source: \"apple-events\", texts };\n return this.#bodies;\n }\n\n /**\n * Search.\n *\n * `title` uses the index's title and snippet columns, which is a SQL query.\n * `full` needs the body, which no indexed column carries — so it decodes the\n * gzipped protobuf when the index is available and falls back to the Apple\n * Events bulk scan when it is not. Either way the match happens in JS over a\n * cached corpus, because `whose` is 6.9x slower than fetching and filtering.\n */\n async searchNotes(opts: {\n query: string;\n scope: \"title\" | \"full\";\n limit: number;\n offset: number;\n }): Promise<NoteListing & { scope: string }> {\n const store = this.#indexCanAnswer() ? this.index() : null;\n\n if (opts.scope === \"title\" && store?.caps.storeUuid) {\n const rows = store.search({\n query: opts.query,\n limit: opts.limit + 1,\n offset: opts.offset,\n });\n const uuid = store.caps.storeUuid;\n return {\n notes: rows.slice(0, opts.limit).map((r) => this.#fromIndex(r, uuid)),\n source: \"index\",\n hasMore: rows.length > opts.limit,\n scope: \"title+snippet\",\n };\n }\n\n const corpus = await this.#allBodies();\n const needle = opts.query.toLowerCase();\n const hits: string[] = [];\n for (const [ref, text] of corpus.texts) {\n if (text.toLowerCase().includes(needle)) hits.push(ref);\n }\n const page = hits.slice(opts.offset, opts.offset + opts.limit);\n\n const summaries = await Promise.all(page.map((ref) => this.getNote(ref, { body: false })));\n return {\n notes: summaries.map((s) => s.summary),\n source: corpus.source,\n // Exact, not a guess: the corpus behind a full-text search is every note\n // in both lanes, so `hits` is the whole match set and not a page of it.\n hasMore: hits.length > opts.offset + opts.limit,\n scope: \"full-text\",\n };\n }\n\n async getNote(\n ref: string,\n opts: { body?: boolean } = {},\n ): Promise<{ summary: NoteSummary; body: string | null; bodySource: string | null }> {\n const decoded = decodeRef(ref);\n const store = this.#indexCanAnswer() ? this.index() : null;\n\n if (store?.caps.storeUuid) {\n const row = store.byPrimaryKey(decoded.primaryKey);\n if (row) {\n const summary = this.#fromIndex(row, store.caps.storeUuid);\n if (opts.body === false) return { summary, body: null, bodySource: null };\n if (row.locked) throw new NoteLockedError(ref);\n const body = store.bodyOf(decoded.primaryKey);\n if (body.encrypted) throw new NoteLockedError(ref);\n if (body.text !== null) {\n return { summary, body: this.#truncate(body.text), bodySource: `index:${body.via}` };\n }\n }\n }\n\n const [found] = await withBusyRetry(() =>\n this.runner.run<\n {\n id: string;\n found: boolean;\n locked: boolean;\n name: string | null;\n plaintext: string | null;\n modified: string | null;\n created: string | null;\n }[]\n >(GET_NOTE_BODIES, { ids: [decoded.id] }),\n );\n if (!found?.found) throw new NoteNotFoundError(ref);\n if (found.locked) throw new NoteLockedError(ref);\n return {\n summary: {\n ref,\n title: found.name,\n snippet: null,\n folder: null,\n account: null,\n modified: found.modified,\n created: found.created,\n locked: false,\n source: \"apple-events\",\n },\n body: found.plaintext === null ? null : this.#truncate(found.plaintext),\n bodySource: \"apple-events\",\n };\n }\n\n #truncate(text: string): string {\n const max = this.config.bodyMaxBytes;\n if (Buffer.byteLength(text, \"utf8\") <= max) return text;\n return `${text.slice(0, max)}\\n\\n[truncated at ${max} bytes; raise APPLE_NOTES_BODY_MAX_BYTES]`;\n }\n\n async attachments(ref: string): Promise<\n {\n id: string | null;\n name: string | null;\n url: string | null;\n contentIdentifier: string | null;\n }[]\n > {\n const decoded = decodeRef(ref);\n const listed = await withBusyRetry(() =>\n this.runner.run<\n {\n id: string | null;\n name: string | null;\n url: string | null;\n contentIdentifier: string | null;\n }[]\n >(LIST_ATTACHMENTS, { id: decoded.id }),\n );\n // Notes can enumerate the same attachment more than once — measured after a\n // `make`, where one new attachment appeared twice in the element collection\n // and `count of attachments` said 3 for 2 real ones. Passing that through\n // would have a caller save the same file twice, or believe a note holds\n // attachments it does not.\n const seen = new Set<string>();\n return listed.filter((a) => {\n if (!a.id) return true;\n if (seen.has(a.id)) return false;\n seen.add(a.id);\n return true;\n });\n }\n\n /**\n * Save an attachment's bytes.\n *\n * The scripting dictionary exposes `name`, `id`, `URL` and `content\n * identifier` but **no filesystem path**, so the bytes can only come from the\n * media directory — which is Full Disk Access territory. Without it this\n * reports what is missing rather than failing obscurely.\n */\n async saveAttachment(\n ref: string,\n attachmentId: string,\n targetDir?: string,\n opts: { overwrite?: boolean } = {},\n ): Promise<{\n path: string;\n bytes: number;\n }> {\n const located = this.locate();\n if (!located.readable) {\n throw new PreconditionError(\n \"Attachment bytes need Full Disk Access: the scripting dictionary carries no file path \" +\n \"for attachments, so they can only be read from the Notes media directory. \" +\n \"Call apple_notes_diagnostics for the exact pane to open.\",\n );\n }\n const list = await this.attachments(ref);\n const meta = list.find((a) => a.id === attachmentId);\n if (!meta) throw new PreconditionError(`No attachment ${attachmentId} on note ${ref}.`);\n\n // ICAttachment holds no path, so the directory names come from the ICMedia\n // row it points at. That needs the index; without it there is nothing to\n // resolve against and saying so beats walking the tree for a name that is\n // not there.\n const pk = attachmentPrimaryKey(attachmentId);\n const media = pk === null ? null : (this.index()?.mediaFor(pk) ?? null);\n if (!media) {\n throw new PreconditionError(\n `Cannot resolve where ${attachmentId} is stored. Its media record is missing from ` +\n `NoteStore.sqlite, so there is no directory name to look for.`,\n );\n }\n\n const source = this.#findMedia(located.mediaRoot, media);\n if (!source) {\n throw new PreconditionError(\n `Found the attachment's metadata but no file for it under ${located.mediaRoot}.`,\n );\n }\n\n // The configured directory is the confinement boundary, not a default that\n // `targetDir` replaces: an override may only select a subdirectory of it.\n // Resolving the override *against* the root means a relative path lands\n // inside it and an absolute one is caught by the check below.\n const root = resolve(this.config.attachmentDir);\n const dir = targetDir ? resolve(root, targetDir) : root;\n if (dir !== root && !dir.startsWith(root + sep)) {\n throw new PreconditionError(\n `Refusing to write outside ${root}. Set APPLE_NOTES_ATTACHMENT_DIR to change the destination.`,\n );\n }\n // basename() first: the name comes from note content, which is\n // attacker-controlled in exactly the way path traversal needs.\n const name = basename(media.filename ?? meta.name ?? basename(source));\n const target = resolve(join(dir, name));\n if (target !== join(dir, name) || !target.startsWith(dir + sep)) {\n throw new PreconditionError(`Refusing to write outside ${dir}.`);\n }\n if (existsSync(target) && !opts.overwrite) {\n throw new PreconditionError(`${target} already exists; refusing to overwrite it.`);\n }\n const bytes = readFileSync(source);\n mkdirSync(dir, { recursive: true });\n writeFileSync(target, bytes, { mode: 0o600 });\n return { path: target, bytes: bytes.length };\n }\n\n /**\n * The file for an ICMedia row.\n *\n * Layout, verified on a real store:\n *\n * Accounts/<account>/Media/<identifier>/<generation>/<filename>\n *\n * The account directory is not known here, so each one is tried — there are a\n * handful, not thousands. Every segment after the identifier is treated as a\n * hint rather than a certainty: the generation directory is skipped when the\n * row has none, and a directory holding a single file answers even when the\n * filename on disk differs from the one recorded.\n */\n #findMedia(\n root: string,\n media: { identifier: string; generation: string | null; filename: string | null },\n ): string | null {\n const accounts = (() => {\n try {\n return readdirSync(root);\n } catch {\n return [];\n }\n })();\n\n const fileIn = (dir: string): string | null => {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return null;\n }\n const files = entries.filter((e) => {\n try {\n return statSync(join(dir, e)).isFile();\n } catch {\n return false;\n }\n });\n if (media.filename && files.includes(media.filename)) return join(dir, media.filename);\n // A single file is unambiguous whatever it is called; several are not, and\n // guessing among them would hand back the wrong bytes silently.\n return files.length === 1 ? join(dir, files[0] as string) : null;\n };\n\n for (const account of accounts) {\n const base = join(root, account, \"Media\", media.identifier);\n try {\n if (!statSync(base).isDirectory()) continue;\n } catch {\n continue;\n }\n const candidates = media.generation ? [join(base, media.generation), base] : [base];\n for (const dir of candidates) {\n const hit = fileIn(dir);\n if (hit) return hit;\n }\n // The generation directory is named in the row, but fall back to whatever\n // single subdirectory is actually there rather than failing on a rename.\n const subdirs = (() => {\n try {\n return readdirSync(base).filter((e) => statSync(join(base, e)).isDirectory());\n } catch {\n return [];\n }\n })();\n if (subdirs.length === 1) {\n const hit = fileIn(join(base, subdirs[0] as string));\n if (hit) return hit;\n }\n }\n return null;\n }\n\n // ─── writes ────────────────────────────────────────────────────────────────\n\n async createNote(opts: { title?: string; body?: string; folderId?: string }): Promise<unknown> {\n this.#bodies = null;\n return withBusyRetry(() => this.runner.run(CREATE_NOTE, opts));\n }\n\n /**\n * Attach a file to a note.\n *\n * The path is resolved and checked here rather than in JXA: Notes reports a\n * missing or unreadable file as a generic refusal, which would surface as\n * \"Notes refused the file\" for what is really a typo.\n */\n async addAttachment(ref: string, filePath: string): Promise<unknown> {\n const decoded = decodeRef(ref);\n const resolved = resolve(filePath);\n let stats;\n try {\n stats = statSync(resolved);\n } catch {\n throw new PreconditionError(`No file at ${resolved}.`);\n }\n if (!stats.isFile()) {\n throw new PreconditionError(`${resolved} is not a file.`);\n }\n this.#bodies = null;\n return withBusyRetry(() => this.runner.run(ADD_ATTACHMENT, { id: decoded.id, path: resolved }));\n }\n\n async updateNote(ref: string, body: string, mode: \"replace\" | \"append\"): Promise<unknown> {\n this.#bodies = null;\n const decoded = decodeRef(ref);\n return withBusyRetry(() => this.runner.run(UPDATE_NOTE, { id: decoded.id, body, mode }));\n }\n\n async moveNote(ref: string, folderId: string): Promise<unknown> {\n this.#bodies = null;\n const decoded = decodeRef(ref);\n return withBusyRetry(() => this.runner.run(MOVE_NOTE, { id: decoded.id, folderId }));\n }\n\n async deleteNotes(refs: string[]): Promise<unknown> {\n this.#bodies = null;\n const ids = refs.map((r) => decodeRef(r).id);\n return withBusyRetry(() => this.runner.run(DELETE_NOTES, { ids }));\n }\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport {\n BaseConfigSchema,\n parseBool,\n parseConfig,\n parseIntOpt,\n parseList,\n trimmed,\n} from \"@mgcrea/mcp-apple-core\";\nimport { z } from \"zod\";\n\n/**\n * Configuration is environment-only — this server holds no secret at all, its\n * access is the macOS permission the user granted.\n *\n * `allowWrites`, `debug`, `osascriptPath`, `osascriptTimeoutMs` and `maxResults`\n * come from `BaseConfigSchema`.\n */\nconst ConfigSchema = BaseConfigSchema.extend({\n /**\n * Account allowlist (names or ids). Empty means every account.\n *\n * `allowWrites` gates mutation, but the larger blast radius is *reading* a\n * person's whole notebook, so this is the read-side control.\n */\n accounts: z.array(z.string().min(1)).default([]),\n /** Explicit NoteStore.sqlite path. Bypasses discovery — for tests and forensic copies. */\n storePath: z.string().optional(),\n indexMode: z.enum([\"auto\", \"ro\", \"immutable\", \"off\"]).default(\"auto\"),\n /**\n * Cap for the Apple Events listing lane. Generous compared to Mail's because\n * a bulk property fetch here is one round trip at ~0.1ms per note, not 42ms.\n */\n degradedMaxNotes: z.number().int().min(1).max(1_000).default(200),\n bodyMaxBytes: z.number().int().min(1_024).max(10_000_000).default(262_144),\n /**\n * The confinement boundary for save_attachment, not merely its default: the\n * tool's `directory` argument may select a subdirectory of this, never escape\n * it. Enforced lexically with `resolve()` + `basename()`, not `realpath()` —\n * the destination may not exist yet.\n */\n attachmentDir: z.string().default(join(homedir(), \"Downloads\")),\n /**\n * How long the Apple Events plaintext cache stays warm.\n *\n * Measured: fetching ids + modification dates to check freshness costs *more*\n * (128ms) than simply re-scanning every body (97ms), so there is no cheap\n * invalidation to be had. A TTL with bounded staleness is the only sensible\n * trade when the index lane is unavailable.\n */\n searchCacheTtlMs: z.number().int().min(0).max(3_600_000).default(60_000),\n}).strict();\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * `env` is a parameter with a default so tests are hermetic — they pass their\n * own object rather than mutating (and having to restore) process.env.\n */\nexport const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config =>\n parseConfig(ConfigSchema, {\n allowWrites: parseBool(env.APPLE_NOTES_ALLOW_WRITES),\n exposePrompts: parseBool(env.APPLE_NOTES_EXPOSE_PROMPTS),\n lazyTools: parseBool(env.APPLE_NOTES_LAZY_TOOLS),\n debug: parseBool(env.APPLE_NOTES_DEBUG),\n accounts: parseList(env.APPLE_NOTES_ACCOUNTS),\n storePath: trimmed(env.APPLE_NOTES_STORE),\n indexMode: trimmed(env.APPLE_NOTES_INDEX_MODE),\n osascriptPath: trimmed(env.APPLE_NOTES_OSASCRIPT_PATH),\n osascriptTimeoutMs: parseIntOpt(env.APPLE_NOTES_OSASCRIPT_TIMEOUT_MS),\n maxResults: parseIntOpt(env.APPLE_NOTES_MAX_RESULTS),\n degradedMaxNotes: parseIntOpt(env.APPLE_NOTES_DEGRADED_MAX_NOTES),\n bodyMaxBytes: parseIntOpt(env.APPLE_NOTES_BODY_MAX_BYTES),\n attachmentDir: trimmed(env.APPLE_NOTES_ATTACHMENT_DIR),\n searchCacheTtlMs: parseIntOpt(env.APPLE_NOTES_SEARCH_CACHE_TTL_MS),\n });\n","/**\n * The Notes operating manual, served as `cupertino://notes/guide` and embedded\n * ahead of every Notes prompt.\n *\n * Static by design — see the same note in the Mail guide. Nothing here may\n * depend on a probe: account names, folder names and permission verdicts belong\n * to `cupertino://notes/inventory` and `cupertino://notes/diagnostics`, both of\n * which are allowed to fail.\n */\nexport const NOTES_GUIDE = `# Apple Notes — how to drive this server\n\n## Refs are opaque\n\nNotes come back with a \\`ref\\` like \\`n1:x-coredata://…/ICNote/p123\\`. Pass it back\nverbatim; never build or parse one, and never carry it to another surface.\n\n## Which tool, under which constraint\n\n- **Finding anything by content** is \\`apple_notes_search_notes\\`.\n- **\\`apple_notes_list_notes\\`** is a capped listing of a folder in date order —\n for \"what did I write last week\", not for \"where did I write about X\".\n- **\\`apple_notes_get_note\\`** returns the body. Search results are summaries;\n do not quote a note you have only seen the title of.\n- **Folders** are \\`apple_notes_list_folders\\`, and folder names must match what\n Notes actually calls them. Read the inventory rather than guessing.\n\n## Locked notes are not empty notes\n\nA password-protected note is encrypted at rest and comes back with its text\nnull. **No permission unlocks it** — not Full Disk Access, not Automation.\nReport it as locked. Reporting it as empty invents an absence of content that\nmay be the exact content someone is looking for.\n\nWithout Full Disk Access the index lane is gone and search falls back to an\nApple Events scan: correct, but linear in the size of the library. A slow\nsearch on a big library is this, not a hang.\n\nAttachment bytes need Full Disk Access outright — the scripting dictionary\ncarries no file path — so \\`apple_notes_save_attachment\\` fails without it while\nthe rest of the server keeps working.\n\n## Writes\n\nMutating tools exist only when writes are enabled. If you cannot see\n\\`apple_notes_create_note\\`, writes are off; say so rather than describing a note\nyou did not create.\n\n\\`apple_notes_update_note\\` **replaces** a note's body. It does not append. Read\nthe note first and write back the whole thing, or the rest of it is gone.\n`;\n","import {\n promptArg,\n registerWorkflowPrompt,\n requiredPromptArg,\n type PromptContext,\n} from \"@mgcrea/mcp-apple-core\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { NOTES_GUIDE } from \"./guide.js\";\n\nconst CTX: PromptContext = { surface: \"notes\", guide: NOTES_GUIDE };\n\n/**\n * Notes' workflow prompts. Both encode an ordering no single tool description\n * can hold: search results are summaries and must be opened before they are\n * quoted, and a capture must look before it writes or it makes a duplicate.\n */\nexport const registerPrompts = (server: McpServer, allowWrites: boolean): void => {\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_notes_find\",\n title: \"Find notes about something\",\n description:\n \"Search the note library for everything on a topic and summarise what is actually written \" +\n \"there — opening the notes rather than answering from titles.\",\n argsSchema: {\n about: requiredPromptArg('What to look for, e.g. \"the Atlas pricing decision\".'),\n folder: promptArg(\"Restrict to one folder, exactly as Notes spells it. Omit to span all.\"),\n },\n build: ({ about, folder }) => `Find what my notes say about: ${about}${\n folder ? `\\n\\nRestrict this to the ${folder} folder.` : \"\"\n }\n\n1. \\`apple_notes_search_notes\\` for the distinctive words${\n folder ? \", scoped to that folder\" : \"\"\n }. Try more than one phrasing before concluding there is nothing — these are\n personal notes, so the user's own shorthand is often what is written.\n2. **Open the promising ones with \\`apple_notes_get_note\\`.** Search returns\n summaries; a title tells you a note exists, not what it says. Never quote or\n summarise a note you have only seen the title of.\n3. Synthesise across the notes rather than listing them one by one. Say what was\n decided, what is still open, and where the notes contradict each other — that\n last one is usually the useful part.\n\nReport any locked note you hit by name and say it is password-protected. It is\nnot empty, and nothing available here can open it.`,\n });\n\n if (!allowWrites) return;\n\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_notes_capture\",\n title: \"Capture something into Notes\",\n description:\n \"Write something into the note library, checking first whether it belongs in a note that \" +\n \"already exists. Requires writes to be enabled.\",\n argsSchema: {\n what: requiredPromptArg(\"What to capture — the content, or where to get it from.\"),\n folder: promptArg(\"Folder to file it under, exactly as Notes spells it.\"),\n },\n build: ({ what, folder }) => `Capture this into Notes: ${what}\n\n1. **Search first.** \\`apple_notes_search_notes\\` for the subject. A running note\n on this topic probably already exists, and a second one is worse than a\n longer first one.\n2. If one exists, read it in full with \\`apple_notes_get_note\\`, then write back\n the existing content plus the new material with \\`apple_notes_update_note\\`.\n That tool **replaces** the body — send the whole note, not the addition, or\n you will delete everything that was there.\n3. If nothing exists, \\`apple_notes_create_note\\`${folder ? ` in the ${folder} folder` : \"\"}. ${\n folder\n ? \"Confirm that folder exists with `apple_notes_list_folders` first — a name that does not match is not created for you.\"\n : \"Pick a folder from `apple_notes_list_folders` rather than inventing one.\"\n }\n4. Give it a title someone would search for in six months, not a timestamp.\n\nTell the user which note you wrote to and whether it was new or extended.`,\n });\n};\n","import { z } from \"zod\";\n\n/**\n * Notes' tool helpers. The generic half lives in `@mgcrea/mcp-apple-core`;\n * re-exported here so tools import from one place.\n */\nexport {\n compact,\n confirmArg,\n fail,\n limitArg,\n ok,\n okText,\n resolveLimit,\n toFailure,\n wrap,\n wrapResult,\n type ToolResult,\n} from \"@mgcrea/mcp-apple-core\";\n\n// ─── Notes' own args ─────────────────────────────────────────────────────────\n\nexport const noteRefArg = z\n .string()\n .min(1)\n .describe(\n 'An opaque note ref from a search or list result (looks like \"n1:x-coredata://…/ICNote/p123\"). ' +\n \"Do not construct one by hand.\",\n );\n\nexport const noteRefsArg = z\n .array(z.string().min(1))\n .min(1)\n .max(200)\n .describe(\"Note refs from a search or list result.\");\n\nexport const folderArg = z\n .string()\n .optional()\n .describe('Folder name, e.g. \"Notes\" or \"Projects\". Folders nest; give the leaf name.');\n\nexport const folderIdArg = z.string().min(1).describe(\"A folder id from apple_notes_list_folders.\");\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { ok, wrap } from \"./util.js\";\n\n/**\n * Build the report.\n *\n * Split out of the tool registration so the `cupertino://notes/diagnostics`\n * resource can serve the same bytes. Two renderings of one probe: duplicated,\n * the resource and the tool would drift, and the disagreement would surface as\n * \"the diagnostics lied\" — the one thing this file must never do.\n */\nexport const buildDiagnostics = async (\n client: AppleNotesClient,\n ctx: { allowWrites: boolean },\n): Promise<Record<string, unknown>> => {\n const lanes = await client.lanes();\n const located = client.locate();\n const accounts = lanes.applescript === \"live\" ? await client.accounts() : [];\n\n const fullDiskAccess = located.readable\n ? \"granted\"\n : located.exists\n ? \"denied\"\n : \"unknown (no store found)\";\n\n return {\n server: { lanes },\n permissions: {\n automation: lanes.applescript === \"live\" ? \"granted\" : \"denied-or-notes-not-running\",\n fullDiskAccess,\n ...(located.readable\n ? {}\n : {\n howToGrant: [\n \"System Settings > Privacy & Security > Full Disk Access\",\n \"Add the app that launches this server (Terminal, iTerm, VS Code, Claude...), then restart it.\",\n \"Granting it to Notes.app does nothing — the reader needs the permission, not Notes.\",\n ],\n }),\n },\n store: {\n path: located.storePath,\n exists: located.exists,\n readable: located.readable,\n sizeBytes: located.size,\n walPresent: located.walPresent,\n walSizeBytes: located.walSizeBytes,\n fingerprint: lanes.storeFingerprint,\n reason: located.reason,\n },\n accounts,\n settings: {\n allowWrites: ctx.allowWrites,\n // Off means the prompts and the cupertino:// resources are not registered\n // at all. Reported here because this tool still is, so it stays the one\n // place that explains a capability the client cannot see.\n exposePrompts: client.config.exposePrompts,\n accountAllowlist: client.config.accounts,\n indexMode: client.config.indexMode,\n maxResults: client.config.maxResults,\n // The Apple Events listing cap. Reported because list_notes now names it\n // when it is what ended a list, and a number in an error is only useful\n // if there is somewhere to go and see what it is.\n degradedMaxNotes: client.config.degradedMaxNotes,\n searchCacheTtlMs: client.config.searchCacheTtlMs,\n attachmentDir: client.config.attachmentDir,\n },\n caveats: [\n \"Without Full Disk Access the index lane is unavailable: search falls back to an \" +\n \"Apple Events bulk scan, which is fine on a small library and degrades linearly.\",\n `Listings over Apple Events stop at ${client.config.degradedMaxNotes} notes whatever ` +\n \"`limit` says. When that is what ended a list, list_notes says so in a `note` field \" +\n \"rather than returning a short list silently.\",\n \"Password-protected notes are encrypted at rest; no permission makes them readable.\",\n \"Attachment bytes need Full Disk Access — the scripting dictionary carries no file path.\",\n ],\n };\n};\n\n/**\n * One tool that answers \"why is this not working\".\n *\n * Lanes are probed before anything else is read, because the first Apple Event\n * is what triggers the Automation prompt: reading accounts first and probing\n * afterwards makes the two disagree whenever that first event loses the race.\n */\nexport const registerDiagnosticsTools = (\n server: McpServer,\n client: AppleNotesClient,\n ctx: { allowWrites: boolean },\n): void => {\n server.registerTool(\n \"apple_notes_diagnostics\",\n {\n description:\n \"Report which lanes are live, which macOS permissions are granted, and what each \" +\n \"missing one is blocking. Start here when a tool fails, or answers from the Apple \" +\n \"Events lane when you expected the index — it names the exact System Settings pane to \" +\n \"open.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => buildDiagnostics(client, ctx)).then((r) => r ?? ok({})),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { wrap } from \"./util.js\";\n\nexport const registerAccountTools = (server: McpServer, client: AppleNotesClient): void => {\n server.registerTool(\n \"apple_notes_list_accounts\",\n {\n description:\n \"List the Notes accounts, with how many folders and notes each holds. Start here to \" +\n \"discover account names for the other tools.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => client.accounts()),\n );\n\n server.registerTool(\n \"apple_notes_list_folders\",\n {\n description:\n \"List folders across every account. Folders nest, so each entry carries its depth and \" +\n \"the account it belongs to. Use the returned id with the move and create tools.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => client.folders()),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { confirmArg, folderIdArg, noteRefArg, noteRefsArg, wrap } from \"./util.js\";\n\n/**\n * The mutating tools.\n *\n * Registered only when writes are enabled, so with the flag off they are\n * invisible to the model rather than merely refused.\n */\nexport const registerActionTools = (server: McpServer, client: AppleNotesClient): void => {\n server.registerTool(\n \"apple_notes_create_note\",\n {\n description:\n \"Create a note. Notes takes its title from the first line of the body, so `title` is \" +\n \"prepended as a heading rather than set as a property.\",\n inputSchema: {\n title: z.string().optional().describe(\"Becomes the first line, and therefore the title.\"),\n body: z.string().optional().describe(\"Body content. Simple HTML is accepted.\"),\n folderId: z\n .string()\n .optional()\n .describe(\"Target folder id. Defaults to the first account's default folder.\"),\n },\n },\n async ({ title, body, folderId }) => wrap(() => client.createNote({ title, body, folderId })),\n );\n\n server.registerTool(\n \"apple_notes_update_note\",\n {\n description:\n \"Replace or append to a note's body. Editing the first line changes the title, because \" +\n \"that is where Notes takes it from.\",\n inputSchema: {\n ref: noteRefArg,\n body: z.string().min(1).describe(\"The new content. Simple HTML is accepted.\"),\n mode: z\n .enum([\"replace\", \"append\"])\n .optional()\n .describe(\"replace overwrites the body (default). append adds to the end.\"),\n },\n },\n async ({ ref, body, mode }) => wrap(() => client.updateNote(ref, body, mode ?? \"replace\")),\n );\n\n server.registerTool(\n \"apple_notes_add_attachment\",\n {\n description:\n \"Attach a file to a note. This is the only way to put an image in a note — an <img> tag \" +\n \"in the body creates an attachment whose bytes are never loaded.\",\n inputSchema: {\n ref: noteRefArg,\n path: z\n .string()\n .min(1)\n .describe(\"Absolute path to the file to attach. It is read by Notes, not copied here.\"),\n },\n },\n async ({ ref, path }) => wrap(() => client.addAttachment(ref, path)),\n );\n\n server.registerTool(\n \"apple_notes_move_note\",\n {\n description: \"Move a note to another folder.\",\n inputSchema: { ref: noteRefArg, folderId: folderIdArg },\n },\n async ({ ref, folderId }) => wrap(() => client.moveNote(ref, folderId)),\n );\n\n server.registerTool(\n \"apple_notes_delete_notes\",\n {\n description:\n \"Delete notes. They go to Recently Deleted rather than being destroyed, but this still \" +\n \"requires an explicit confirm.\",\n inputSchema: { refs: noteRefsArg, confirm: confirmArg },\n },\n async ({ refs }) => wrap(() => client.deleteNotes(refs)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport {\n folderArg,\n limitArg,\n noteRefArg,\n ok,\n okText,\n resolveLimit,\n wrap,\n wrapResult,\n} from \"./util.js\";\n\nexport const registerNoteTools = (\n server: McpServer,\n client: AppleNotesClient,\n allowWrites: boolean,\n): void => {\n server.registerTool(\n \"apple_notes_list_notes\",\n {\n description:\n \"List notes, newest first. Returns `notes` with a `ref` per note for the read and \" +\n \"action tools, alongside `source` and `hasMore`. Prefers Notes' own index, which stays \" +\n \"fast as a library grows. `hasMore` means more notes matched than `limit` allowed; a \" +\n \"`note` field appears when the Apple Events lane's own cap ended the list early, which \" +\n \"is a different thing and is worth reading.\",\n inputSchema: {\n folder: folderArg,\n limit: limitArg,\n },\n annotations: { readOnlyHint: true },\n },\n async ({ folder, limit }) =>\n wrap(() =>\n client.listNotes({ folder, limit: resolveLimit(limit, client.config.maxResults) }),\n ),\n );\n\n server.registerTool(\n \"apple_notes_search_notes\",\n {\n description:\n 'Search notes. `scope: \"full\"` (the default) matches the body text; `scope: \"title\"` ' +\n \"matches only titles and previews and is the cheaper query. Returns `notes` with a \" +\n \"`ref` per hit, plus `hasMore` — set when more matched than this page carries, so a \" +\n \"full page is never mistaken for the end of the results.\",\n inputSchema: {\n query: z.string().min(1).describe(\"Text to look for. Case-insensitive substring match.\"),\n scope: z\n .enum([\"full\", \"title\"])\n .optional()\n .describe(\n \"full = search body text (default). title = titles and previews only, which is a \" +\n \"single indexed query and cheaper on a large library.\",\n ),\n limit: limitArg,\n offset: z.number().int().min(0).optional().describe(\"Skip this many results, for paging.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ query, scope, limit, offset }) =>\n wrap(() =>\n client.searchNotes({\n query,\n scope: scope ?? \"full\",\n limit: resolveLimit(limit, client.config.maxResults),\n offset: offset ?? 0,\n }),\n ),\n );\n\n server.registerTool(\n \"apple_notes_get_note\",\n {\n description:\n \"Read one note's full text by ref. Password-protected notes report as locked rather \" +\n \"than returning an empty body — their text is encrypted at rest.\",\n inputSchema: {\n ref: noteRefArg,\n format: z\n .enum([\"text\", \"json\"])\n .optional()\n .describe(\"text returns the body as-is (default). json wraps it with the metadata.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ ref, format }) =>\n wrapResult(async () => {\n const result = await client.getNote(ref);\n if ((format ?? \"text\") === \"text\" && result.body !== null) return okText(result.body);\n return ok(result);\n }),\n );\n\n server.registerTool(\n \"apple_notes_list_attachments\",\n {\n description:\n \"List a note's attachments. Metadata only — retrieving the bytes needs Full Disk \" +\n \"Access, because the scripting dictionary carries no file path.\",\n inputSchema: { ref: noteRefArg },\n annotations: { readOnlyHint: true },\n },\n async ({ ref }) => wrap(() => client.attachments(ref)),\n );\n\n if (!allowWrites) return;\n\n server.registerTool(\n \"apple_notes_save_attachment\",\n {\n description:\n \"Save one attachment to disk. Needs Full Disk Access. It can only write into \" +\n \"APPLE_NOTES_ATTACHMENT_DIR (default ~/Downloads) and will not overwrite an existing \" +\n \"file unless you ask it to. Write-gated because it puts a file on the user's disk, \" +\n \"even though it changes nothing in Notes.\",\n inputSchema: {\n ref: noteRefArg,\n attachmentId: z.string().min(1).describe(\"An id from apple_notes_list_attachments.\"),\n directory: z\n .string()\n .optional()\n .describe(\"Override the target directory. Must still resolve inside the configured one.\"),\n overwrite: z.boolean().optional().describe(\"Replace an existing file. Default false.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ ref, attachmentId, directory, overwrite }) =>\n wrap(() =>\n client.saveAttachment(ref, attachmentId, directory, overwrite ? { overwrite } : {}),\n ),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleNotesClient } from \"../client/notes.js\";\nimport { registerAccountTools } from \"./accounts.js\";\nimport { registerActionTools } from \"./actions.js\";\nimport { registerDiagnosticsTools } from \"./diagnostics.js\";\nimport { registerNoteTools } from \"./notes.js\";\n\nexport type ToolContext = {\n /**\n * Register the mutating tools too. Off by default — with the flag off they are\n * not merely refused, they are invisible and cannot be called at all.\n */\n allowWrites: boolean;\n};\n\n/**\n * Register the Apple Notes tools.\n *\n * The registered set is a pure function of `allowWrites` and nothing else. In\n * particular it does NOT vary with whether Full Disk Access is granted: that is\n * a runtime condition which can change while the process lives, and MCP clients\n * cache the tool list, so a tool that appears and disappears would leave clients\n * calling names the server no longer has. Tools that need the index instead\n * report their source, or explain what is missing.\n */\nexport const registerTools = (\n server: McpServer,\n client: AppleNotesClient,\n ctx: ToolContext,\n): void => {\n registerDiagnosticsTools(server, client, ctx);\n registerAccountTools(server, client);\n registerNoteTools(server, client, ctx.allowWrites);\n\n if (!ctx.allowWrites) return;\n registerActionTools(server, client);\n};\n","import {\n type Logger,\n type OsascriptRunner,\n registerSurfaceResources,\n withLazyTools,\n} from \"@mgcrea/mcp-apple-core\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { AppleNotesClient } from \"./client/notes.js\";\nimport type { Config } from \"./config.js\";\nimport { NOTES_GUIDE } from \"./guide.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { buildDiagnostics } from \"./tools/diagnostics.js\";\nimport { registerTools } from \"./tools/index.js\";\n\nexport const SERVER_NAME = BUILD_INFO.name;\nexport const SERVER_VERSION = BUILD_INFO.version;\n\nexport type CreateServerOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so nothing spawns a process or touches a real Notes. */\n osascript?: OsascriptRunner;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: AppleNotesClient;\n};\n\n/**\n * Build the server. Side-effect free: it opens no connection, spawns no\n * process and reads no file, so a test can construct it freely and every\n * external dependency arrives through an option.\n */\nexport const createServer = (opts: CreateServerOptions): CreatedServer => {\n const { config } = opts;\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });\n\n const client = new AppleNotesClient({\n config,\n ...(opts.logger ? { logger: opts.logger } : {}),\n ...(opts.osascript ? { osascript: opts.osascript } : {}),\n });\n\n withLazyTools(\n server,\n {\n surface: \"notes\",\n displayName: \"Notes\",\n lazy: config.lazyTools,\n allowWrites: config.allowWrites,\n },\n (target, allowWrites) => registerTools(target, client, { allowWrites }),\n );\n /*\n * One flag, both primitives — see `exposePrompts` in core's config. A prompt\n * embeds its surface guide, so registering prompts without the resources\n * would leave every expansion naming a `cupertino://…/guide` that this\n * server does not serve.\n */\n if (config.exposePrompts) {\n registerPrompts(server, config.allowWrites);\n registerSurfaceResources(server, {\n surface: \"notes\",\n displayName: \"Notes\",\n guide: NOTES_GUIDE,\n diagnostics: () => buildDiagnostics(client, { allowWrites: config.allowWrites }),\n inventory: {\n describes: \"accounts and folders\",\n read: async () => ({\n accounts: await client.accounts(),\n folders: await client.folders(),\n }),\n },\n });\n }\n\n return { server, client };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/mcp-apple-notes",
3
- "version": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "description": "Search, read and act on Apple Notes — folders, attachments, writes off by default",
5
5
  "keywords": [
6
6
  "apple",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@modelcontextprotocol/sdk": "^1.30.0",
47
47
  "zod": "^4.4.3",
48
- "@mgcrea/mcp-apple-core": "^1.22.1"
48
+ "@mgcrea/mcp-apple-core": "^1.23.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^26.2.0",