apple-notes-mcp 2.9.22 → 2.9.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/build/index.js +233 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -274,7 +274,7 @@ Creates a new note in Apple Notes.
274
274
  |-----------|------|----------|-------------|
275
275
  | `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
276
276
  | `content` | string | One of `content`/`contentPath` | The body content of the note (do not repeat the title here) |
277
- | `contentPath` | string | One of `content`/`contentPath` | Absolute path of a local UTF-8 file to use as the body instead of `content`. Allowed in the same places [`save-attachment`](#save-attachment) may write (home, temp, `/Volumes`); symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped |
277
+ | `contentPath` | string | One of `content`/`contentPath` | Absolute path of a local UTF-8 file to use as the body instead of `content`. Allowed in the same places [`save-attachment`](#save-attachment) may write (home, temp, `/Volumes`), except hidden paths (any component starting with `.`, such as `~/.ssh` or a project `.env`) and `~/Library` other than iCloud Drive (`~/Library/Mobile Documents`) and cloud storage folders (`~/Library/CloudStorage`), which can hold credentials; set `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1` to allow those. Symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped |
278
278
  | `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Inline `#hashtags` in `content` stay searchable text and are returned as `hashtags`, but they do not become native Notes tags; add real tags afterwards with [`add-native-tags`](#add-native-tags). Refused with `format: "markdown"` |
279
279
  | `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). A smart folder is refused (see [`move-note`](#move-note)). Defaults to account root |
280
280
  | `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
@@ -2765,6 +2765,7 @@ All configuration is optional — the server works out of the box. Override beha
2765
2765
  | `APPLE_NOTES_MCP_BACKGROUND_SHORTCUT` | `Apple Notes MCP - Background Operations v5` | Name or UUID of the installed Background Operations bridge to run. `setup` still installs and checks the default name. |
2766
2766
  | `APPLE_NOTES_MCP_MARKDOWN_SHORTCUT` | `Apple Notes MCP - Create Markdown Note` | Name or UUID of the installed Create Markdown Note bridge to run. `setup` still installs and checks the default name. |
2767
2767
  | `APPLE_NOTES_MCP_PASTEBOARD_NAME` | unset | Testing only: makes [`add-attachment-from-pasteboard`](#add-attachment-from-pasteboard) read a private named pasteboard instead of the general clipboard. |
2768
+ | `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS` | unset | Set to `1` to let `create-note` read a [`contentPath`](#create-note) inside a hidden directory or file (such as `~/.config`) or `~/Library` outside iCloud Drive and `~/Library/CloudStorage`. Off by default because those places hold keys, tokens and app data. |
2768
2769
  | `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
2769
2770
 
2770
2771
  ### Configuration file (when the host strips `env`)
@@ -3008,6 +3009,12 @@ A software consulting, contracting, and development company.
3008
3009
  - Email: rob@superiortech.io
3009
3010
  - GitHub: [@sweetrb](https://github.com/sweetrb)
3010
3011
 
3012
+ ### Contributors
3013
+
3014
+ - **Oliver Ames** ([@oliverames](https://github.com/oliverames)) — the project's most prolific
3015
+ contributor, with dozens of merged pull requests spanning features, fixes, documentation, and
3016
+ sharp, well-reproduced bug reports.
3017
+
3011
3018
  ## License
3012
3019
 
3013
3020
  MIT License - see [LICENSE](https://github.com/sweetrb/apple-notes-mcp/blob/main/LICENSE) for details.
package/build/index.js CHANGED
@@ -41184,11 +41184,36 @@ function assertReadableInRoots(p, roots = allowedSaveRoots(), label = "Content f
41184
41184
  );
41185
41185
  return abs;
41186
41186
  }
41187
- function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots()) {
41187
+ var ALLOW_PRIVATE_CONTENT_ENV = "APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS";
41188
+ function privateContentReason(p, roots) {
41189
+ const containing = roots.map((r) => r.endsWith(sep) ? r.slice(0, -1) : r).filter((r) => isWithinRoots(p, [r])).sort((a, b) => b.length - a.length)[0];
41190
+ const below = containing === void 0 ? p : relative(containing, p);
41191
+ if (below.split(sep).some((part) => part.startsWith("."))) return "a hidden file or directory";
41192
+ const home = resolve(homedir6());
41193
+ const libraries = [
41194
+ join6(home, "Library"),
41195
+ ...canonicalRoots([home]).map((h) => join6(h, "Library"))
41196
+ ];
41197
+ if (!isWithinRoots(p, libraries)) return null;
41198
+ const cloudDocuments = libraries.flatMap((l) => CLOUD_DOCUMENT_DIRS.map((d) => join6(l, d)));
41199
+ return isWithinRoots(p, cloudDocuments) ? null : "~/Library";
41200
+ }
41201
+ var CLOUD_DOCUMENT_DIRS = ["Mobile Documents", "CloudStorage"];
41202
+ function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots(), allowPrivate = process.env[ALLOW_PRIVATE_CONTENT_ENV] === "1") {
41188
41203
  const abs = assertReadableInRoots(p, roots);
41204
+ const assertNotPrivate = (candidate, candidateRoots) => {
41205
+ if (allowPrivate) return;
41206
+ const reason = privateContentReason(candidate, candidateRoots);
41207
+ if (reason)
41208
+ throw new Error(
41209
+ `Refusing to read "${abs}": it is in ${reason}, which can hold credentials or app data. Move the file to a regular folder, or set ${ALLOW_PRIVATE_CONTENT_ENV}=1 for the server to allow it.`
41210
+ );
41211
+ };
41212
+ assertNotPrivate(abs, roots);
41213
+ assertNotPrivate(canonicalize(abs), canonicalRoots(roots));
41189
41214
  let descriptor;
41190
41215
  try {
41191
- descriptor = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW);
41216
+ descriptor = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
41192
41217
  } catch (error2) {
41193
41218
  if (error2.code === "ELOOP")
41194
41219
  throw new Error(`Refusing to read the symbolic link "${abs}".`);
@@ -41197,6 +41222,20 @@ function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots()) {
41197
41222
  try {
41198
41223
  const stat = fstatSync(descriptor);
41199
41224
  if (!stat.isFile()) throw new Error(`Content file is not a regular file: "${abs}"`);
41225
+ let after;
41226
+ try {
41227
+ after = canonicalize(abs);
41228
+ } catch {
41229
+ throw new Error(`Content file changed while it was being opened; try again: "${abs}"`);
41230
+ }
41231
+ if (!isWithinRoots(after, canonicalRoots(roots)))
41232
+ throw new Error(
41233
+ `Refusing to read outside allowed locations (home, temp, /Volumes): "${abs}" resolves to "${after}".`
41234
+ );
41235
+ assertNotPrivate(after, canonicalRoots(roots));
41236
+ const named = statSync(after);
41237
+ if (named.dev !== stat.dev || named.ino !== stat.ino)
41238
+ throw new Error(`Content file changed while it was being opened; try again: "${abs}"`);
41200
41239
  if (stat.size === 0) throw new Error(`Content file is empty: "${abs}"`);
41201
41240
  if (stat.size > maxBytes)
41202
41241
  throw new Error(`Content file is ${stat.size} bytes, over the ${maxBytes}-byte limit.`);
@@ -41725,16 +41764,17 @@ function copyFileExclusive(src, dest) {
41725
41764
  }
41726
41765
  function exportFileName(record2, source, kind) {
41727
41766
  const ext = extname(source);
41767
+ const id2 = safeComponent(record2.identifier) ?? "attachment";
41728
41768
  if (kind === "preview") {
41729
41769
  const stored2 = safeComponent(record2.filename ? basename(record2.filename) : null);
41730
- const stem = stored2 ? basename(stored2, extname(stored2)) : record2.identifier;
41731
- return `${safeComponent(stem) ?? record2.identifier}-preview${ext}`;
41770
+ const stem = stored2 ? basename(stored2, extname(stored2)) : id2;
41771
+ return `${safeComponent(stem) ?? id2}-preview${ext}`;
41732
41772
  }
41733
41773
  const stored = safeComponent(record2.filename ? basename(record2.filename) : null);
41734
41774
  if (stored && !GENERIC_FILE_NAMES.has(stored.toLowerCase())) return stored;
41735
- const own = basename(source);
41736
- if (!GENERIC_FILE_NAMES.has(own.toLowerCase())) return own;
41737
- return `${record2.identifier}${ext}`;
41775
+ const own = safeComponent(basename(source));
41776
+ if (own && !GENERIC_FILE_NAMES.has(own.toLowerCase())) return own;
41777
+ return `${id2}${ext}`;
41738
41778
  }
41739
41779
  function collisionName(name, attempt) {
41740
41780
  if (attempt <= 1) return name;
@@ -41768,6 +41808,8 @@ function exportOneAttachment(record2, dir, source) {
41768
41808
  for (let attempt = 1; attempt <= MAX_COLLISION_SUFFIX; attempt++) {
41769
41809
  const dest = join7(dir, collisionName(name, attempt));
41770
41810
  try {
41811
+ if (dirname2(dest) !== resolve2(dir))
41812
+ throw new Error(`Refusing to write outside the export directory: "${dest}"`);
41771
41813
  assertSafeSavePath(dest);
41772
41814
  copyFileExclusive(source.path, dest);
41773
41815
  return { ...base, exportedTo: dest, exportedKind: source.kind };
@@ -52590,21 +52632,23 @@ function renderNotesWithTemplate(notes, ctx, options) {
52590
52632
  }
52591
52633
 
52592
52634
  // src/utils/templateAssets.ts
52593
- import { createHash as createHash2 } from "node:crypto";
52635
+ import { createHash as createHash2, randomBytes } from "node:crypto";
52594
52636
  import {
52595
52637
  closeSync as closeSync5,
52596
52638
  constants as constants5,
52597
52639
  fstatSync as fstatSync5,
52640
+ linkSync,
52598
52641
  lstatSync as lstatSync4,
52599
52642
  mkdirSync as mkdirSync5,
52600
52643
  openSync as openSync5,
52601
52644
  readSync as readSync4,
52645
+ unlinkSync as unlinkSync3,
52602
52646
  writeSync as writeSync4
52603
52647
  } from "node:fs";
52604
52648
  import { extname as extname4, isAbsolute as isAbsolute3, join as join21, relative as relative4, resolve as resolve4, sep as sep4 } from "node:path";
52605
52649
  var CHUNK = 1024 * 1024;
52606
52650
  function openRegular(path10) {
52607
- const fd = openSync5(path10, constants5.O_RDONLY | constants5.O_NOFOLLOW);
52651
+ const fd = openSync5(path10, constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK);
52608
52652
  if (!fstatSync5(fd).isFile()) {
52609
52653
  closeSync5(fd);
52610
52654
  throw new Error("not a regular file");
@@ -52690,26 +52734,56 @@ var HashedSidecarWriter = class {
52690
52734
  closeSync5(source);
52691
52735
  }
52692
52736
  }
52737
+ /**
52738
+ * Copy to a private temporary name, then hard-link it into place, so the
52739
+ * hashed name only ever holds complete content: an interrupted copy can't
52740
+ * leave a truncated file that later exports would report as name-taken.
52741
+ * Where the volume has no hard links, copy straight to the hashed name and
52742
+ * remove it if the copy fails.
52743
+ */
52693
52744
  copy(source, target) {
52694
- const out = openSync5(
52695
- target,
52696
- constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL | constants5.O_NOFOLLOW,
52697
- 420
52698
- );
52745
+ const temp = join21(this.dir, `.asset-${randomBytes(6).toString("hex")}.tmp`);
52746
+ writeNew(source, temp);
52699
52747
  try {
52700
- const chunk = Buffer.alloc(CHUNK);
52701
- for (let position = 0; ; ) {
52702
- const n = readSync4(source, chunk, 0, chunk.length, position);
52703
- if (n <= 0) break;
52704
- let written = 0;
52705
- while (written < n) written += writeSync4(out, chunk, written, n - written);
52706
- position += n;
52707
- }
52748
+ linkSync(temp, target);
52749
+ return;
52750
+ } catch (error2) {
52751
+ const code = error2.code;
52752
+ if (code !== "ENOTSUP" && code !== "EPERM" && code !== "EXDEV") throw error2;
52708
52753
  } finally {
52709
- closeSync5(out);
52754
+ try {
52755
+ unlinkSync3(temp);
52756
+ } catch {
52757
+ }
52710
52758
  }
52759
+ writeNew(source, target);
52711
52760
  }
52712
52761
  };
52762
+ function writeNew(source, target) {
52763
+ const out = openSync5(
52764
+ target,
52765
+ constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL | constants5.O_NOFOLLOW,
52766
+ 420
52767
+ );
52768
+ try {
52769
+ const chunk = Buffer.alloc(CHUNK);
52770
+ for (let position = 0; ; ) {
52771
+ const n = readSync4(source, chunk, 0, chunk.length, position);
52772
+ if (n <= 0) break;
52773
+ let written = 0;
52774
+ while (written < n) written += writeSync4(out, chunk, written, n - written);
52775
+ position += n;
52776
+ }
52777
+ } catch (error2) {
52778
+ closeSync5(out);
52779
+ try {
52780
+ unlinkSync3(target);
52781
+ } catch {
52782
+ }
52783
+ throw error2;
52784
+ }
52785
+ closeSync5(out);
52786
+ }
52713
52787
  var ReferenceWriter = class {
52714
52788
  constructor(linkBase) {
52715
52789
  this.linkBase = linkBase;
@@ -53276,7 +53350,7 @@ function pasteboardFilename(requested, frozenFilename) {
53276
53350
  function readRegularFile(path10) {
53277
53351
  let descriptor;
53278
53352
  try {
53279
- descriptor = openSync6(path10, constants6.O_RDONLY | constants6.O_NOFOLLOW);
53353
+ descriptor = openSync6(path10, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
53280
53354
  } catch {
53281
53355
  throw new PasteboardError("file_unreadable", MESSAGES.file_unreadable);
53282
53356
  }
@@ -54031,19 +54105,19 @@ function registerFolderDelete(server2, manager, deps = defaultDeps) {
54031
54105
  }
54032
54106
 
54033
54107
  // src/services/templateStore.ts
54034
- import { randomBytes } from "node:crypto";
54108
+ import { randomBytes as randomBytes2 } from "node:crypto";
54035
54109
  import {
54036
54110
  closeSync as closeSync8,
54037
54111
  constants as constants8,
54038
54112
  fstatSync as fstatSync8,
54039
- linkSync,
54113
+ linkSync as linkSync2,
54040
54114
  lstatSync as lstatSync5,
54041
54115
  mkdirSync as mkdirSync7,
54042
54116
  openSync as openSync8,
54043
54117
  readdirSync as readdirSync4,
54044
54118
  readSync as readSync6,
54045
54119
  renameSync,
54046
- unlinkSync as unlinkSync3,
54120
+ unlinkSync as unlinkSync4,
54047
54121
  writeSync as writeSync5
54048
54122
  } from "node:fs";
54049
54123
  import { homedir as homedir19 } from "node:os";
@@ -54106,14 +54180,26 @@ var TemplateStore = class {
54106
54180
  file(name) {
54107
54181
  return join26(this.dir, `${name}.json`);
54108
54182
  }
54109
- /** Read a saved template's text, refusing symlinks and non-regular files. */
54183
+ /**
54184
+ * Read a saved template's text, refusing symlinks and non-regular files.
54185
+ * O_NONBLOCK keeps a FIFO from blocking the event loop (it is then refused as
54186
+ * not a regular file); it does not affect reading a regular file. Only a
54187
+ * symlink (ELOOP) is reported as unsafe-path; other open failures, such as
54188
+ * EACCES, are rethrown as they are.
54189
+ */
54110
54190
  readText(path10) {
54111
54191
  let fd;
54112
54192
  try {
54113
- fd = openSync8(path10, constants8.O_RDONLY | constants8.O_NOFOLLOW);
54193
+ fd = openSync8(path10, constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK);
54114
54194
  } catch (error2) {
54115
- if (error2.code === "ENOENT") return void 0;
54116
- throw new TemplateStoreError("unsafe-path", `Refusing to read ${path10}: not a regular file.`);
54195
+ const errno = error2.code;
54196
+ if (errno === "ENOENT") return void 0;
54197
+ if (errno === "ELOOP")
54198
+ throw new TemplateStoreError(
54199
+ "unsafe-path",
54200
+ `Refusing to read ${path10}: it is a symbolic link.`
54201
+ );
54202
+ throw error2;
54117
54203
  }
54118
54204
  try {
54119
54205
  const stat = fstatSync8(fd);
@@ -54218,7 +54304,7 @@ var TemplateStore = class {
54218
54304
  "template-exists",
54219
54305
  `A template named "${name}" already exists. Pass force: true to replace it.`
54220
54306
  );
54221
- const temp = join26(this.dir, `.${name}.${randomBytes(6).toString("hex")}.tmp`);
54307
+ const temp = join26(this.dir, `.${name}.${randomBytes2(6).toString("hex")}.tmp`);
54222
54308
  const fd = openSync8(
54223
54309
  temp,
54224
54310
  constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW,
@@ -54228,14 +54314,20 @@ var TemplateStore = class {
54228
54314
  const data = Buffer.from(body, "utf8");
54229
54315
  let written = 0;
54230
54316
  while (written < data.length) written += writeSync5(fd, data, written);
54231
- } finally {
54317
+ } catch (error2) {
54232
54318
  closeSync8(fd);
54319
+ try {
54320
+ unlinkSync4(temp);
54321
+ } catch {
54322
+ }
54323
+ throw error2;
54233
54324
  }
54325
+ closeSync8(fd);
54234
54326
  try {
54235
54327
  if (force) renameSync(temp, path10);
54236
54328
  else {
54237
54329
  try {
54238
- linkSync(temp, path10);
54330
+ linkSync2(temp, path10);
54239
54331
  } catch (error2) {
54240
54332
  if (error2.code === "EEXIST")
54241
54333
  throw new TemplateStoreError(
@@ -54247,7 +54339,7 @@ var TemplateStore = class {
54247
54339
  }
54248
54340
  } finally {
54249
54341
  try {
54250
- unlinkSync3(temp);
54342
+ unlinkSync4(temp);
54251
54343
  } catch {
54252
54344
  }
54253
54345
  }
@@ -54270,7 +54362,7 @@ var TemplateStore = class {
54270
54362
  "unsafe-path",
54271
54363
  `Refusing to delete ${path10}: not a regular file.`
54272
54364
  );
54273
- unlinkSync3(path10);
54365
+ unlinkSync4(path10);
54274
54366
  return { path: path10 };
54275
54367
  }
54276
54368
  };
@@ -54656,7 +54748,7 @@ function parseColor(value) {
54656
54748
  }
54657
54749
  function parsePaint(value) {
54658
54750
  const v = value.trim();
54659
- if (v === "none") return { kind: "none" };
54751
+ if (/^none$/i.test(v)) return { kind: "none" };
54660
54752
  if (/^currentcolor$/i.test(v)) return { kind: "current" };
54661
54753
  const url = /^url\(\s*['"]?([^'")]*)['"]?\s*\)/i.exec(v);
54662
54754
  if (url) return { kind: "url", target: url[1].trim() };
@@ -55031,8 +55123,9 @@ function distanceToChord(p, a, b) {
55031
55123
  if (len === 0) return Math.hypot(p[0] - a[0], p[1] - a[1]);
55032
55124
  return Math.abs((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / len;
55033
55125
  }
55034
- function flattenCubic(p0, c1, c2, p3, tolerance, out, depth = 0) {
55126
+ function flattenCubic(p0, c1, c2, p3, tolerance, out, depth = 0, charge) {
55035
55127
  if (depth >= 16 || Math.max(distanceToChord(c1, p0, p3), distanceToChord(c2, p0, p3)) <= tolerance) {
55128
+ charge?.();
55036
55129
  out.push(p3);
55037
55130
  return;
55038
55131
  }
@@ -55043,32 +55136,48 @@ function flattenCubic(p0, c1, c2, p3, tolerance, out, depth = 0) {
55043
55136
  const d = mid(a, b);
55044
55137
  const e = mid(b, c);
55045
55138
  const m = mid(d, e);
55046
- flattenCubic(p0, a, d, m, tolerance, out, depth + 1);
55047
- flattenCubic(m, e, c, p3, tolerance, out, depth + 1);
55139
+ flattenCubic(p0, a, d, m, tolerance, out, depth + 1, charge);
55140
+ flattenCubic(m, e, c, p3, tolerance, out, depth + 1, charge);
55048
55141
  }
55049
- function flattenSubpath(sub4, m, tolerance) {
55142
+ function flattenSubpath(sub4, m, tolerance, charge) {
55143
+ charge?.();
55050
55144
  const points = [apply(m, sub4.start)];
55051
55145
  let pen = sub4.start;
55052
55146
  for (const seg of sub4.segments) {
55053
- if (seg.kind === "L") points.push(apply(m, seg.to));
55054
- else
55147
+ if (seg.kind === "L") {
55148
+ charge?.();
55149
+ points.push(apply(m, seg.to));
55150
+ } else
55055
55151
  flattenCubic(
55056
55152
  apply(m, pen),
55057
55153
  apply(m, seg.c1),
55058
55154
  apply(m, seg.c2),
55059
55155
  apply(m, seg.to),
55060
55156
  tolerance,
55061
- points
55157
+ points,
55158
+ 0,
55159
+ charge
55062
55160
  );
55063
55161
  pen = seg.to;
55064
55162
  }
55065
55163
  if (sub4.closed) {
55066
55164
  const first2 = points[0];
55067
55165
  const last = points[points.length - 1];
55068
- if (last[0] !== first2[0] || last[1] !== first2[1]) points.push(first2);
55166
+ if (last[0] !== first2[0] || last[1] !== first2[1]) {
55167
+ charge?.();
55168
+ points.push(first2);
55169
+ }
55069
55170
  }
55070
55171
  return points;
55071
55172
  }
55173
+ function subpathControlPoints(sub4, m) {
55174
+ const out = [apply(m, sub4.start)];
55175
+ for (const seg of sub4.segments) {
55176
+ if (seg.kind === "C") out.push(apply(m, seg.c1), apply(m, seg.c2));
55177
+ out.push(apply(m, seg.to));
55178
+ }
55179
+ return out;
55180
+ }
55072
55181
  function clipSegment(a, b, r) {
55073
55182
  const dx = b[0] - a[0];
55074
55183
  const dy = b[1] - a[1];
@@ -55248,6 +55357,13 @@ var SvgError = class extends Error {
55248
55357
  code;
55249
55358
  location;
55250
55359
  };
55360
+ function lookupNamespace(scope2, prefix) {
55361
+ for (let s = scope2; s; s = s.parent) {
55362
+ const ns = s.own?.get(prefix);
55363
+ if (ns !== void 0) return ns;
55364
+ }
55365
+ return void 0;
55366
+ }
55251
55367
  var NAME_START = /[A-Za-z_À-￿]/;
55252
55368
  var NAME_CHAR = /[A-Za-z0-9_.\-:·À-￿]/;
55253
55369
  var PREDEFINED = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
@@ -55276,6 +55392,9 @@ function parseXml(source, limits) {
55276
55392
  let pos = 0;
55277
55393
  let line = 1;
55278
55394
  let elementCount = 0;
55395
+ let namespaceDeclarations = 0;
55396
+ const maxAttributes = limits.maxAttributes ?? 1024;
55397
+ const maxNamespaceDeclarations = limits.maxNamespaceDeclarations ?? 1024;
55279
55398
  const stack = [];
55280
55399
  let root = null;
55281
55400
  const fail = (message) => {
@@ -55337,6 +55456,7 @@ function parseXml(source, limits) {
55337
55456
  while (i < source.length && NAME_CHAR.test(source[i])) i++;
55338
55457
  const name = source.slice(pos + 1, i);
55339
55458
  const rawAttributes = [];
55459
+ const attributeNames = /* @__PURE__ */ new Set();
55340
55460
  let selfClosing = false;
55341
55461
  for (; ; ) {
55342
55462
  const ws = i;
@@ -55366,8 +55486,13 @@ function parseXml(source, limits) {
55366
55486
  if (close < 0) fail("Unterminated attribute value");
55367
55487
  const rawValue = source.slice(i + 1, close);
55368
55488
  if (rawValue.includes("<")) fail("'<' in an attribute value");
55369
- if (rawAttributes.some((a) => a.name === attrName))
55370
- fail(`Duplicate attribute ${attrName.slice(0, 40)}`);
55489
+ if (attributeNames.has(attrName)) fail(`Duplicate attribute ${attrName.slice(0, 40)}`);
55490
+ attributeNames.add(attrName);
55491
+ if (attributeNames.size > maxAttributes)
55492
+ throw new SvgError(
55493
+ "svg_complexity_limit",
55494
+ `An element has more than ${maxAttributes} attributes (line ${line})`
55495
+ );
55371
55496
  rawAttributes.push({
55372
55497
  name: attrName,
55373
55498
  value: decodeEntities(rawValue.replace(/[\t\n\r]/g, " "), line)
@@ -55384,19 +55509,30 @@ function parseXml(source, limits) {
55384
55509
  );
55385
55510
  if (stack.length + 1 > limits.maxDepth)
55386
55511
  throw new SvgError("svg_complexity_limit", `Elements nest deeper than ${limits.maxDepth}`);
55387
- const parentScope = stack.length ? stack[stack.length - 1].scope : /* @__PURE__ */ new Map();
55388
- const scope2 = new Map(parentScope);
55512
+ const parentScope = stack.length ? stack[stack.length - 1].scope : null;
55513
+ let own = null;
55389
55514
  for (const a of rawAttributes) {
55390
- if (a.name === "xmlns") scope2.set("", a.value);
55391
- else if (a.name.startsWith("xmlns:")) scope2.set(a.name.slice(6), a.value);
55515
+ const prefix = a.name === "xmlns" ? "" : a.name.startsWith("xmlns:") ? a.name.slice(6) : null;
55516
+ if (prefix === null) continue;
55517
+ if (++namespaceDeclarations > maxNamespaceDeclarations)
55518
+ throw new SvgError(
55519
+ "svg_complexity_limit",
55520
+ `The document declares more than ${maxNamespaceDeclarations} namespaces`
55521
+ );
55522
+ (own ??= /* @__PURE__ */ new Map()).set(prefix, a.value);
55392
55523
  }
55524
+ const scope2 = own ? { own, parent: parentScope } : parentScope ?? { own: null, parent: null };
55393
55525
  const resolve9 = (qualified, isAttribute) => {
55394
55526
  const colon = qualified.indexOf(":");
55395
- if (colon < 0) return { local: qualified, ns: isAttribute ? null : scope2.get("") ?? null };
55527
+ if (colon < 0)
55528
+ return {
55529
+ local: qualified,
55530
+ ns: isAttribute ? null : lookupNamespace(scope2, "") ?? null
55531
+ };
55396
55532
  const prefix = qualified.slice(0, colon);
55397
55533
  const local = qualified.slice(colon + 1);
55398
55534
  if (prefix === "xml") return { local, ns: XML_NS };
55399
- const ns = scope2.get(prefix);
55535
+ const ns = lookupNamespace(scope2, prefix);
55400
55536
  if (ns === void 0) fail(`Undeclared namespace prefix "${prefix.slice(0, 40)}"`);
55401
55537
  return { local, ns };
55402
55538
  };
@@ -55424,6 +55560,8 @@ var SVG_ANALYZER_VERSION = "apple-notes-mcp/svg-analyzer@1";
55424
55560
  var SVG_LIMITS = {
55425
55561
  maxSourceBytes: 1048576,
55426
55562
  maxSourceElements: 16384,
55563
+ maxAttributesPerElement: 1024,
55564
+ maxNamespaceDeclarations: 1024,
55427
55565
  maxExpandedElements: 16384,
55428
55566
  maxReferenceExpansions: 8192,
55429
55567
  maxDepth: 64,
@@ -55595,7 +55733,7 @@ function attr(el, local) {
55595
55733
  return el.attributes.find((a) => a.local === local && a.ns === null)?.value;
55596
55734
  }
55597
55735
  function href(el) {
55598
- return el.attributes.find((a) => a.local === "href" && (a.ns === null || a.ns === XLINK_NS))?.value ?? void 0;
55736
+ return el.attributes.find((a) => a.local === "href" && a.ns === null)?.value ?? el.attributes.find((a) => a.local === "href" && a.ns === XLINK_NS)?.value;
55599
55737
  }
55600
55738
  function parseStyleAttribute(value) {
55601
55739
  const out = [];
@@ -55611,7 +55749,24 @@ function parseStyleAttribute(value) {
55611
55749
  function hasText(el) {
55612
55750
  return el.text.trim().length > 0 || el.children.some(hasText);
55613
55751
  }
55614
- var ACTIVE_SCHEME = /^\s*(?:javascript|vbscript|data)\s*:/i;
55752
+ var ACTIVE_SCHEME = /^(?:javascript|vbscript|data)\s*:/i;
55753
+ function urlAsParsed(value) {
55754
+ return value.replace(/[\t\n\r]/g, "").replace(/^[\x00-\x20]+|[\x00-\x20]+$/g, "");
55755
+ }
55756
+ function unescapeCss(value) {
55757
+ if (!value.includes("\\")) return value;
55758
+ return value.replace(
55759
+ /\\(?:([0-9A-Fa-f]{1,6})(?:\r\n|[ \t\n\r\f])?|(\r\n|[\n\r\f])|([\s\S]))/g,
55760
+ (_m, hex3, newline, other) => {
55761
+ if (hex3 !== void 0) {
55762
+ const code = Number.parseInt(hex3, 16);
55763
+ return code === 0 || code > 1114111 || code >= 55296 && code <= 57343 ? "\uFFFD" : String.fromCodePoint(code);
55764
+ }
55765
+ if (newline !== void 0) return "";
55766
+ return other;
55767
+ }
55768
+ );
55769
+ }
55615
55770
  function assertInert(el, location) {
55616
55771
  if (UNSAFE_ELEMENTS.has(el.local))
55617
55772
  throw new SvgError(
@@ -55628,7 +55783,7 @@ function assertInert(el, location) {
55628
55783
  );
55629
55784
  const value = a.value;
55630
55785
  if (a.local === "href" && (a.ns === null || a.ns === XLINK_NS)) {
55631
- const target = value.trim();
55786
+ const target = urlAsParsed(value);
55632
55787
  if (el.local === "a") {
55633
55788
  if (ACTIVE_SCHEME.test(target))
55634
55789
  throw new SvgError("svg_unsafe", "Link uses an active URL scheme", location);
@@ -55641,10 +55796,11 @@ function assertInert(el, location) {
55641
55796
  );
55642
55797
  }
55643
55798
  }
55644
- for (const m of value.matchAll(/url\(\s*['"]?([^'")]*)/gi))
55645
- if (!m[1].trim().startsWith("#"))
55799
+ const css = unescapeCss(value);
55800
+ for (const m of css.matchAll(/url\(\s*['"]?([^'")]*)/gi))
55801
+ if (!urlAsParsed(m[1]).startsWith("#"))
55646
55802
  throw new SvgError("svg_unsafe", "url() references must be local (#id)", location);
55647
- if (a.local === "style" && /@import|expression\s*\(|javascript\s*:/i.test(value))
55803
+ if (a.local === "style" && /@import|expression\s*\(|javascript\s*:/i.test(urlAsParsed(css)))
55648
55804
  throw new SvgError("svg_unsafe", "Active content in a style attribute", location);
55649
55805
  }
55650
55806
  const counts = /* @__PURE__ */ new Map();
@@ -55726,7 +55882,8 @@ var Analyzer = class {
55726
55882
  if (styleAttr) declarations.push(...parseStyleAttribute(styleAttr));
55727
55883
  const diag = Math.hypot(ctx.viewport[0], ctx.viewport[1]) / Math.SQRT2;
55728
55884
  for (const [name, value] of declarations) {
55729
- if (value === "inherit") continue;
55885
+ const keyword = value.toLowerCase();
55886
+ if (keyword === "inherit") continue;
55730
55887
  const invalid2 = () => this.issue(
55731
55888
  "invalid_value",
55732
55889
  null,
@@ -55765,23 +55922,23 @@ var Analyzer = class {
55765
55922
  break;
55766
55923
  }
55767
55924
  case "fill-rule":
55768
- if (value === "evenodd" || value === "nonzero") style.evenOdd = value === "evenodd";
55925
+ if (keyword === "evenodd" || keyword === "nonzero") style.evenOdd = keyword === "evenodd";
55769
55926
  else invalid2();
55770
55927
  break;
55771
55928
  case "visibility":
55772
- style.visible = value === "visible";
55929
+ style.visible = keyword === "visible";
55773
55930
  break;
55774
55931
  case "display":
55775
- if (value === "none") display = false;
55932
+ display = keyword !== "none";
55776
55933
  break;
55777
55934
  case "stroke-linecap":
55778
- style.linecap = value;
55935
+ style.linecap = keyword;
55779
55936
  break;
55780
55937
  case "stroke-linejoin":
55781
- style.linejoin = value;
55938
+ style.linejoin = keyword;
55782
55939
  break;
55783
55940
  case "stroke-dasharray": {
55784
- if (value === "none") {
55941
+ if (keyword === "none") {
55785
55942
  style.dasharray = null;
55786
55943
  break;
55787
55944
  }
@@ -55802,7 +55959,7 @@ var Analyzer = class {
55802
55959
  case "overflow":
55803
55960
  break;
55804
55961
  case "mix-blend-mode":
55805
- if (value !== "normal")
55962
+ if (keyword !== "normal")
55806
55963
  this.issue(
55807
55964
  "blend_mode_ignored",
55808
55965
  "paint-approximation",
@@ -55811,7 +55968,7 @@ var Analyzer = class {
55811
55968
  );
55812
55969
  break;
55813
55970
  case "paint-order":
55814
- if (value !== "normal" && !/^fill(\s+stroke)?(\s+markers)?$/.test(value))
55971
+ if (keyword !== "normal" && !/^fill(\s+stroke)?(\s+markers)?$/.test(keyword))
55815
55972
  this.issue(
55816
55973
  "paint_order_ignored",
55817
55974
  "paint-approximation",
@@ -55821,7 +55978,7 @@ var Analyzer = class {
55821
55978
  break;
55822
55979
  default:
55823
55980
  if (DROPPED_MODIFIERS.has(name)) {
55824
- if (value !== "none")
55981
+ if (keyword !== "none")
55825
55982
  this.issue(
55826
55983
  "modifier_dropped",
55827
55984
  "drop-content",
@@ -55843,7 +56000,7 @@ var Analyzer = class {
55843
56000
  propertyValue(el, name) {
55844
56001
  const styleAttr = attr(el, "style");
55845
56002
  const fromStyle = styleAttr ? parseStyleAttribute(styleAttr).filter(([n]) => n === name).pop() : void 0;
55846
- return fromStyle ? fromStyle[1] : attr(el, name);
56003
+ return (fromStyle ? fromStyle[1] : attr(el, name))?.trim().toLowerCase();
55847
56004
  }
55848
56005
  run() {
55849
56006
  const root = this.root;
@@ -56243,9 +56400,7 @@ var Analyzer = class {
56243
56400
  if (!geometry || !ctx.style.visible) return;
56244
56401
  const { style } = ctx;
56245
56402
  const polylines = geometry.subpaths.filter((s) => s.segments.length > 0).map((s) => {
56246
- const pts = flattenSubpath(s, ctx.matrix, TOLERANCE);
56247
- this.charge("geometryWork", pts.length);
56248
- if (pts.some(
56403
+ if (subpathControlPoints(s, ctx.matrix).some(
56249
56404
  (p) => !(Math.abs(p[0]) <= SVG_LIMITS.maxCoordinate && Math.abs(p[1]) <= SVG_LIMITS.maxCoordinate)
56250
56405
  ))
56251
56406
  throw new SvgError(
@@ -56253,7 +56408,7 @@ var Analyzer = class {
56253
56408
  "Geometry is not finite or exceeds the coordinate limit",
56254
56409
  ctx.location
56255
56410
  );
56256
- return pts;
56411
+ return flattenSubpath(s, ctx.matrix, TOLERANCE, () => this.charge("geometryWork", 1));
56257
56412
  });
56258
56413
  if (!polylines.length) return;
56259
56414
  const fill = this.paintColor(style.fill, style);
@@ -56409,7 +56564,9 @@ function analyzeSvgBuffer(source) {
56409
56564
  if (text2.charCodeAt(0) === 65279) text2 = text2.slice(1);
56410
56565
  const root = parseXml(text2, {
56411
56566
  maxElements: SVG_LIMITS.maxSourceElements,
56412
- maxDepth: SVG_LIMITS.maxDepth
56567
+ maxDepth: SVG_LIMITS.maxDepth,
56568
+ maxAttributes: SVG_LIMITS.maxAttributesPerElement,
56569
+ maxNamespaceDeclarations: SVG_LIMITS.maxNamespaceDeclarations
56413
56570
  });
56414
56571
  const analyzer = new Analyzer(root);
56415
56572
  const viewport = analyzer.run();
@@ -58980,7 +59137,7 @@ registerTool(
58980
59137
  'Note body; required unless contentPath is given (pass exactly one). In plaintext and HTML, AppleScript cannot create true Apple Notes checklists \u2014 `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items; create a plain `<ul>` or `- ` list and convert it in Notes.app with \u21E7\u2318L. With format "markdown" (markdownRoute "shortcut"), `- [ ]`/`- [x]` lines, `>` block quotes, ``` fenced code, `---` dividers and `inline code` (which Notes renders as a highlight, not monospace) become native styles (see create-note-markdown-blocks in get-capabilities).'
58981
59138
  ),
58982
59139
  contentPath: external_exports.string().min(1).max(MAX.SAVE_PATH).optional().describe(
58983
- `Absolute path of a local UTF-8 file to use as the body instead of content (pass exactly one). The same locations save-attachment may write to are allowed (home, temp, /Volumes); symbolic links and non-regular files are refused. Limit ${MAX_CONTENT_FILE_BYTES} bytes.`
59140
+ `Absolute path of a local UTF-8 file to use as the body instead of content (pass exactly one). The same locations save-attachment may write to are allowed (home, temp, /Volumes), except hidden paths (such as ~/.ssh or ~/.config) and ~/Library (iCloud Drive and ~/Library/CloudStorage are allowed); symbolic links and non-regular files are refused. Limit ${MAX_CONTENT_FILE_BYTES} bytes.`
58984
59141
  ),
58985
59142
  format: external_exports.enum(["plaintext", "html", "markdown"]).optional().default("plaintext").describe(
58986
59143
  "Content format: 'plaintext' (default), 'html' for rich formatting, or 'markdown'. Markdown whose first line is exactly `# <title>` (same case and spacing) has that line and one blank line after it removed, since the title is supplied separately."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.22",
3
+ "version": "2.9.24",
4
4
  "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",