apple-notes-mcp 2.9.15 → 2.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1710,7 +1710,7 @@ single note.
1710
1710
  | `outputPath` | string | No | Absolute file to create. Create-only: an existing file (or symlink) is refused with `[output_exists]` |
1711
1711
  | `assetsDir` | string | No | Absolute directory for attachment copies. Existing files are never replaced; a taken name gets `-2`, `-3`, ... |
1712
1712
  | `wrap` | number | No | Hard-wrap prose at this many columns. Code, tables and headings are never wrapped |
1713
- | `template` | string | No | Render through a template: `standard-markdown` or `obsidian`. Exclusive with `templateFile` |
1713
+ | `template` | string | No | Render through a template: `standard-markdown`, `obsidian`, or a saved template's name. Exclusive with `templateFile` |
1714
1714
  | `templateFile` | string | No | Render through the JSON template in this `.json` file (same path rules; at most 256 KiB) |
1715
1715
 
1716
1716
  Both paths follow the `save-attachment` rules (absolute, under the home
@@ -1785,6 +1785,88 @@ already written is deleted if a later step fails.
1785
1785
 
1786
1786
  ---
1787
1787
 
1788
+ #### `list-markdown-templates`
1789
+
1790
+ Lists the Markdown export templates: the built-ins (`standard-markdown`,
1791
+ `obsidian`) and every saved template in the library, with its display name,
1792
+ description, size and modification date. Unreadable, invalid or unsafe files
1793
+ in the library are skipped and counted in `skipped`. Takes no parameters.
1794
+
1795
+ **Returns:** `builtins`, `templates`, `skipped`, and `dir` (the library
1796
+ directory).
1797
+
1798
+ ---
1799
+
1800
+ #### `show-markdown-template`
1801
+
1802
+ Returns one template in its portable form: a built-in, or a saved template
1803
+ exactly as stored (overrides only). Use it as the starting point for a new
1804
+ template.
1805
+
1806
+ | Parameter | Type | Required | Description |
1807
+ |-----------|------|----------|-------------|
1808
+ | `name` | string | Yes | A built-in or saved template name |
1809
+ | `expanded` | boolean | No | Also return `expanded`, with every rule filled in from its base |
1810
+
1811
+ **Returns:** `name`, `source` (`builtin` or `saved`), `template`, and
1812
+ optionally `expanded`.
1813
+
1814
+ ---
1815
+
1816
+ #### `validate-markdown-template`
1817
+
1818
+ Checks a template without saving it. Pass exactly one of `name`, `template`
1819
+ or `templateFile`.
1820
+
1821
+ | Parameter | Type | Required | Description |
1822
+ |-----------|------|----------|-------------|
1823
+ | `name` | string | One of three | A saved or built-in template |
1824
+ | `template` | object or string | One of three | The template as a JSON object or JSON text |
1825
+ | `templateFile` | string | One of three | Absolute path of a JSON template file (home, a temp directory, or `/Volumes`; at most 256 KiB) |
1826
+
1827
+ **Returns:** `valid`, and `errors` as `{path, message}` pairs such as
1828
+ `$.rules["inline.bold"].after: is required when mode is "wrap"`. An invalid
1829
+ template is a normal result, not a tool error.
1830
+
1831
+ ---
1832
+
1833
+ #### `save-markdown-template`
1834
+
1835
+ Validates a template and stores it in the library so `export-notes-markdown`
1836
+ can use it by `template` name.
1837
+
1838
+ | Parameter | Type | Required | Description |
1839
+ |-----------|------|----------|-------------|
1840
+ | `name` | string | Yes | Lowercase `a-z`, `0-9`, `-` and `_`, 1-64 characters, starting with a letter or digit. Built-in names are reserved |
1841
+ | `template` | object or string | One of `template`/`templateFile` | The template |
1842
+ | `templateFile` | string | One of `template`/`templateFile` | A JSON template file |
1843
+ | `force` | boolean | No | Replace an existing template of this name (default `false`) |
1844
+
1845
+ Saving is create-only: an existing name is refused with `[template-exists]`
1846
+ unless `force` is true. An invalid template is refused with
1847
+ `[invalid-template]` and one JSON path per problem. The file is written to a
1848
+ temporary name and then moved into place, with mode `0600` in a `0700`
1849
+ directory. The library is `~/Library/Application Support/apple-notes-mcp/templates`,
1850
+ or `APPLE_NOTES_MCP_TEMPLATE_DIR`. A symlinked library or template file is
1851
+ refused.
1852
+
1853
+ **Returns:** `name`, `path`, `bytes`, and `replaced`.
1854
+
1855
+ ---
1856
+
1857
+ #### `delete-markdown-template`
1858
+
1859
+ Removes one saved template file from the library. Built-in templates cannot
1860
+ be deleted.
1861
+
1862
+ | Parameter | Type | Required | Description |
1863
+ |-----------|------|----------|-------------|
1864
+ | `name` | string | Yes | The saved template to delete |
1865
+
1866
+ **Returns:** `name`, `path`, and `deleted: true`.
1867
+
1868
+ ---
1869
+
1788
1870
  #### `get-checklist-state`
1789
1871
 
1790
1872
  Reads checklist done/undone state for a note. This bypasses the AppleScript limitation where `body of note` strips checklist state, by reading directly from the NoteStore SQLite database.
@@ -2508,6 +2590,7 @@ All configuration is optional — the server works out of the box. Override beha
2508
2590
  | `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` | `262144` (256 KB) | Per-image cap on the base64 payload kept inline in a [`get-note-content`](#get-note-content) response. Inline images over the cap are replaced with placeholders (with a warning appended) so an image-heavy note cannot exceed the MCP client's message limit and drop the connection; export the real files with [`save-attachment`](#save-attachment) or [`fetch-attachment`](#fetch-attachment). Raise it to keep bigger images inline. |
2509
2591
  | `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
2510
2592
  | `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win, and a write tool's `timeoutSeconds` argument overrides it for that call. |
2593
+ | `APPLE_NOTES_MCP_TEMPLATE_DIR` | `~/Library/Application Support/apple-notes-mcp/templates` | Absolute directory of the saved Markdown template library (`save-markdown-template` and friends). |
2511
2594
  | `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (8 MB) | Largest response `export-notes-json` sends; a page closes early to stay under it. `export-notes-markdown` returns inline Markdown up to half of it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages. |
2512
2595
  | `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. [`get-note-structure`](#get-note-structure) also omits note text larger than this. A [`list-note-paragraphs`](#list-note-paragraphs) or [`list-note-links`](#list-note-links) page also stops early to stay under it. |
2513
2596
  | `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum attempts for a read-only AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection). `2` means one retry; set `1` to fail fast with no retries. Retries share the single `APPLE_NOTES_MCP_TIMEOUT_MS` budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a **timeout** has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry. |
package/build/index.js CHANGED
@@ -417,11 +417,11 @@ var require_codegen = __commonJS({
417
417
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
418
418
  return `${varKind} ${this.name}${rhs};` + _n;
419
419
  }
420
- optimizeNames(names, constants8) {
420
+ optimizeNames(names, constants9) {
421
421
  if (!names[this.name.str])
422
422
  return;
423
423
  if (this.rhs)
424
- this.rhs = optimizeExpr(this.rhs, names, constants8);
424
+ this.rhs = optimizeExpr(this.rhs, names, constants9);
425
425
  return this;
426
426
  }
427
427
  get names() {
@@ -438,10 +438,10 @@ var require_codegen = __commonJS({
438
438
  render({ _n }) {
439
439
  return `${this.lhs} = ${this.rhs};` + _n;
440
440
  }
441
- optimizeNames(names, constants8) {
441
+ optimizeNames(names, constants9) {
442
442
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
443
443
  return;
444
- this.rhs = optimizeExpr(this.rhs, names, constants8);
444
+ this.rhs = optimizeExpr(this.rhs, names, constants9);
445
445
  return this;
446
446
  }
447
447
  get names() {
@@ -502,8 +502,8 @@ var require_codegen = __commonJS({
502
502
  optimizeNodes() {
503
503
  return `${this.code}` ? this : void 0;
504
504
  }
505
- optimizeNames(names, constants8) {
506
- this.code = optimizeExpr(this.code, names, constants8);
505
+ optimizeNames(names, constants9) {
506
+ this.code = optimizeExpr(this.code, names, constants9);
507
507
  return this;
508
508
  }
509
509
  get names() {
@@ -532,12 +532,12 @@ var require_codegen = __commonJS({
532
532
  }
533
533
  return nodes.length > 0 ? this : void 0;
534
534
  }
535
- optimizeNames(names, constants8) {
535
+ optimizeNames(names, constants9) {
536
536
  const { nodes } = this;
537
537
  let i = nodes.length;
538
538
  while (i--) {
539
539
  const n = nodes[i];
540
- if (n.optimizeNames(names, constants8))
540
+ if (n.optimizeNames(names, constants9))
541
541
  continue;
542
542
  subtractNames(names, n.names);
543
543
  nodes.splice(i, 1);
@@ -590,12 +590,12 @@ var require_codegen = __commonJS({
590
590
  return void 0;
591
591
  return this;
592
592
  }
593
- optimizeNames(names, constants8) {
593
+ optimizeNames(names, constants9) {
594
594
  var _a;
595
- this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants8);
596
- if (!(super.optimizeNames(names, constants8) || this.else))
595
+ this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants9);
596
+ if (!(super.optimizeNames(names, constants9) || this.else))
597
597
  return;
598
- this.condition = optimizeExpr(this.condition, names, constants8);
598
+ this.condition = optimizeExpr(this.condition, names, constants9);
599
599
  return this;
600
600
  }
601
601
  get names() {
@@ -618,10 +618,10 @@ var require_codegen = __commonJS({
618
618
  render(opts) {
619
619
  return `for(${this.iteration})` + super.render(opts);
620
620
  }
621
- optimizeNames(names, constants8) {
622
- if (!super.optimizeNames(names, constants8))
621
+ optimizeNames(names, constants9) {
622
+ if (!super.optimizeNames(names, constants9))
623
623
  return;
624
- this.iteration = optimizeExpr(this.iteration, names, constants8);
624
+ this.iteration = optimizeExpr(this.iteration, names, constants9);
625
625
  return this;
626
626
  }
627
627
  get names() {
@@ -657,10 +657,10 @@ var require_codegen = __commonJS({
657
657
  render(opts) {
658
658
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
659
659
  }
660
- optimizeNames(names, constants8) {
661
- if (!super.optimizeNames(names, constants8))
660
+ optimizeNames(names, constants9) {
661
+ if (!super.optimizeNames(names, constants9))
662
662
  return;
663
- this.iterable = optimizeExpr(this.iterable, names, constants8);
663
+ this.iterable = optimizeExpr(this.iterable, names, constants9);
664
664
  return this;
665
665
  }
666
666
  get names() {
@@ -702,11 +702,11 @@ var require_codegen = __commonJS({
702
702
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
703
703
  return this;
704
704
  }
705
- optimizeNames(names, constants8) {
705
+ optimizeNames(names, constants9) {
706
706
  var _a, _b;
707
- super.optimizeNames(names, constants8);
708
- (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants8);
709
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants8);
707
+ super.optimizeNames(names, constants9);
708
+ (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants9);
709
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants9);
710
710
  return this;
711
711
  }
712
712
  get names() {
@@ -1007,7 +1007,7 @@ var require_codegen = __commonJS({
1007
1007
  function addExprNames(names, from) {
1008
1008
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
1009
1009
  }
1010
- function optimizeExpr(expr, names, constants8) {
1010
+ function optimizeExpr(expr, names, constants9) {
1011
1011
  if (expr instanceof code_1.Name)
1012
1012
  return replaceName(expr);
1013
1013
  if (!canOptimize(expr))
@@ -1022,14 +1022,14 @@ var require_codegen = __commonJS({
1022
1022
  return items;
1023
1023
  }, []));
1024
1024
  function replaceName(n) {
1025
- const c = constants8[n.str];
1025
+ const c = constants9[n.str];
1026
1026
  if (c === void 0 || names[n.str] !== 1)
1027
1027
  return n;
1028
1028
  delete names[n.str];
1029
1029
  return c;
1030
1030
  }
1031
1031
  function canOptimize(e) {
1032
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants8[c.str] !== void 0);
1032
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants9[c.str] !== void 0);
1033
1033
  }
1034
1034
  }
1035
1035
  function subtractNames(names, from) {
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
2991
2991
  const schOrFunc = root.refs[ref];
2992
2992
  if (schOrFunc)
2993
2993
  return schOrFunc;
2994
- let _sch = resolve8.call(this, root, ref);
2994
+ let _sch = resolve9.call(this, root, ref);
2995
2995
  if (_sch === void 0) {
2996
2996
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2997
2997
  const { schemaId } = this.opts;
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
3018
3018
  function sameSchemaEnv(s1, s2) {
3019
3019
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3020
3020
  }
3021
- function resolve8(root, ref) {
3021
+ function resolve9(root, ref) {
3022
3022
  let sch;
3023
3023
  while (typeof (sch = this.refs[ref]) == "string")
3024
3024
  ref = sch;
@@ -3843,7 +3843,7 @@ var require_fast_uri = __commonJS({
3843
3843
  }
3844
3844
  return uri;
3845
3845
  }
3846
- function resolve8(baseURI, relativeURI, options) {
3846
+ function resolve9(baseURI, relativeURI, options) {
3847
3847
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3848
3848
  const {
3849
3849
  parsed: baseParsed,
@@ -4205,7 +4205,7 @@ var require_fast_uri = __commonJS({
4205
4205
  var fastUri = {
4206
4206
  SCHEMES,
4207
4207
  normalize,
4208
- resolve: resolve8,
4208
+ resolve: resolve9,
4209
4209
  resolveComponent,
4210
4210
  equal,
4211
4211
  serialize,
@@ -7422,7 +7422,7 @@ var require_DOMException = __commonJS({
7422
7422
  "INVALID_NODE_TYPE_ERR (24): the supplied node is invalid or has an invalid ancestor for this operation",
7423
7423
  "DATA_CLONE_ERR (25): the object can not be cloned."
7424
7424
  ];
7425
- var constants8 = {
7425
+ var constants9 = {
7426
7426
  INDEX_SIZE_ERR,
7427
7427
  DOMSTRING_SIZE_ERR: 2,
7428
7428
  // historical
@@ -7461,8 +7461,8 @@ var require_DOMException = __commonJS({
7461
7461
  this.name = names[code];
7462
7462
  }
7463
7463
  DOMException.prototype.__proto__ = Error.prototype;
7464
- for (c in constants8) {
7465
- v = { value: constants8[c] };
7464
+ for (c in constants9) {
7465
+ v = { value: constants9[c] };
7466
7466
  Object.defineProperty(DOMException, c, v);
7467
7467
  Object.defineProperty(DOMException.prototype, c, v);
7468
7468
  }
@@ -24465,14 +24465,14 @@ var require_turndown_cjs = __commonJS({
24465
24465
  } else if (node.nodeType === 1) {
24466
24466
  replacement = replacementForNode.call(self, node);
24467
24467
  }
24468
- return join29(output, replacement);
24468
+ return join30(output, replacement);
24469
24469
  }, "");
24470
24470
  }
24471
24471
  function postProcess(output) {
24472
24472
  var self = this;
24473
24473
  this.rules.forEach(function(rule) {
24474
24474
  if (typeof rule.append === "function") {
24475
- output = join29(output, rule.append(self.options));
24475
+ output = join30(output, rule.append(self.options));
24476
24476
  }
24477
24477
  });
24478
24478
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24484,7 +24484,7 @@ var require_turndown_cjs = __commonJS({
24484
24484
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24485
24485
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24486
24486
  }
24487
- function join29(output, replacement) {
24487
+ function join30(output, replacement) {
24488
24488
  var s1 = trimTrailingNewlines(output);
24489
24489
  var s2 = trimLeadingNewlines(replacement);
24490
24490
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -36608,7 +36608,7 @@ var Protocol = class {
36608
36608
  return;
36609
36609
  }
36610
36610
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
36611
- await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
36611
+ await new Promise((resolve9) => setTimeout(resolve9, pollInterval));
36612
36612
  options?.signal?.throwIfAborted();
36613
36613
  }
36614
36614
  } catch (error2) {
@@ -36625,7 +36625,7 @@ var Protocol = class {
36625
36625
  */
36626
36626
  request(request, resultSchema, options) {
36627
36627
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
36628
- return new Promise((resolve8, reject) => {
36628
+ return new Promise((resolve9, reject) => {
36629
36629
  const earlyReject = (error2) => {
36630
36630
  reject(error2);
36631
36631
  };
@@ -36703,7 +36703,7 @@ var Protocol = class {
36703
36703
  if (!parseResult.success) {
36704
36704
  reject(parseResult.error);
36705
36705
  } else {
36706
- resolve8(parseResult.data);
36706
+ resolve9(parseResult.data);
36707
36707
  }
36708
36708
  } catch (error2) {
36709
36709
  reject(error2);
@@ -36964,12 +36964,12 @@ var Protocol = class {
36964
36964
  }
36965
36965
  } catch {
36966
36966
  }
36967
- return new Promise((resolve8, reject) => {
36967
+ return new Promise((resolve9, reject) => {
36968
36968
  if (signal.aborted) {
36969
36969
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36970
36970
  return;
36971
36971
  }
36972
- const timeoutId = setTimeout(resolve8, interval);
36972
+ const timeoutId = setTimeout(resolve9, interval);
36973
36973
  signal.addEventListener("abort", () => {
36974
36974
  clearTimeout(timeoutId);
36975
36975
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -38282,7 +38282,7 @@ var McpServer = class {
38282
38282
  let task = createTaskResult.task;
38283
38283
  const pollInterval = task.pollInterval ?? 5e3;
38284
38284
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
38285
- await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
38285
+ await new Promise((resolve9) => setTimeout(resolve9, pollInterval));
38286
38286
  const updatedTask = await extra.taskStore.getTask(taskId);
38287
38287
  if (!updatedTask) {
38288
38288
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -38970,12 +38970,12 @@ var StdioServerTransport = class {
38970
38970
  this.onclose?.();
38971
38971
  }
38972
38972
  send(message) {
38973
- return new Promise((resolve8) => {
38973
+ return new Promise((resolve9) => {
38974
38974
  const json2 = serializeMessage(message);
38975
38975
  if (this._stdout.write(json2)) {
38976
- resolve8();
38976
+ resolve9();
38977
38977
  } else {
38978
- this._stdout.once("drain", resolve8);
38978
+ this._stdout.once("drain", resolve9);
38979
38979
  }
38980
38980
  });
38981
38981
  }
@@ -47633,13 +47633,13 @@ var escapeSegment = escapeFolderName;
47633
47633
  function resolveFolders(rows) {
47634
47634
  const byPk = new Map(rows.map((row) => [row.pk, row]));
47635
47635
  const resolved = /* @__PURE__ */ new Map();
47636
- const resolve8 = (pk, seen) => {
47636
+ const resolve9 = (pk, seen) => {
47637
47637
  const cached2 = resolved.get(pk);
47638
47638
  if (cached2) return cached2;
47639
47639
  const row = byPk.get(pk);
47640
47640
  if (!row || seen.has(pk)) return void 0;
47641
47641
  seen.add(pk);
47642
- const parent = row.parent !== null ? resolve8(row.parent, seen) : void 0;
47642
+ const parent = row.parent !== null ? resolve9(row.parent, seen) : void 0;
47643
47643
  const name = row.name ?? "";
47644
47644
  const path10 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
47645
47645
  const plainPath = parent ? `${parent.plainPath}/${name}` : name;
@@ -47653,7 +47653,7 @@ function resolveFolders(rows) {
47653
47653
  resolved.set(pk, info);
47654
47654
  return info;
47655
47655
  };
47656
- for (const row of rows) resolve8(row.pk, /* @__PURE__ */ new Set());
47656
+ for (const row of rows) resolve9(row.pk, /* @__PURE__ */ new Set());
47657
47657
  return resolved;
47658
47658
  }
47659
47659
  var coreDataMs = (seconds) => seconds === null || !Number.isFinite(seconds) ? void 0 : CORE_DATA_EPOCH_MS2 + seconds * 1e3;
@@ -48860,6 +48860,19 @@ var FEATURES = [
48860
48860
  tools: [],
48861
48861
  minimumMacOSVersion: null,
48862
48862
  requirements: [{ kind: "native_write_helper" }]
48863
+ },
48864
+ {
48865
+ name: "markdownTemplateLibrary",
48866
+ description: "Validate, save, list, show and delete Markdown export templates (local JSON files; exporting with one still needs Full Disk Access)",
48867
+ tools: [
48868
+ "list-markdown-templates",
48869
+ "show-markdown-template",
48870
+ "validate-markdown-template",
48871
+ "save-markdown-template",
48872
+ "delete-markdown-template"
48873
+ ],
48874
+ minimumMacOSVersion: null,
48875
+ requirements: []
48863
48876
  }
48864
48877
  ];
48865
48878
  function requirementLabel(requirement) {
@@ -51653,16 +51666,16 @@ var NOTE_PLACEHOLDERS = [
51653
51666
  "exportStem"
51654
51667
  ];
51655
51668
  var PLACEHOLDER_MODIFIERS = ["raw", "yaml"];
51656
- var wrap2 = (before, after = "", join29) => ({
51669
+ var wrap2 = (before, after = "", join30) => ({
51657
51670
  mode: "wrap",
51658
51671
  before,
51659
51672
  after,
51660
- ...join29 ? { join: join29 } : {}
51673
+ ...join30 ? { join: join30 } : {}
51661
51674
  });
51662
- var pattern = (value, join29) => ({
51675
+ var pattern = (value, join30) => ({
51663
51676
  mode: "pattern",
51664
51677
  value,
51665
- ...join29 ? { join: join29 } : {}
51678
+ ...join30 ? { join: join30 } : {}
51666
51679
  });
51667
51680
  var STANDARD = {
51668
51681
  schemaVersion: 1,
@@ -53686,9 +53699,426 @@ function registerFolderDelete(server2, manager, deps = defaultDeps) {
53686
53699
  );
53687
53700
  }
53688
53701
 
53702
+ // src/services/templateStore.ts
53703
+ import { randomBytes } from "node:crypto";
53704
+ import {
53705
+ closeSync as closeSync7,
53706
+ constants as constants7,
53707
+ fstatSync as fstatSync7,
53708
+ linkSync,
53709
+ lstatSync as lstatSync5,
53710
+ mkdirSync as mkdirSync7,
53711
+ openSync as openSync7,
53712
+ readdirSync as readdirSync4,
53713
+ readSync as readSync5,
53714
+ renameSync,
53715
+ unlinkSync as unlinkSync3,
53716
+ writeSync as writeSync5
53717
+ } from "node:fs";
53718
+ import { homedir as homedir19 } from "node:os";
53719
+ import { isAbsolute as isAbsolute5, join as join25, resolve as resolve5 } from "node:path";
53720
+ var TEMPLATE_SLUG = /^[a-z0-9][a-z0-9_-]{0,63}$/;
53721
+ var TemplateStoreError = class extends Error {
53722
+ constructor(code, message) {
53723
+ super(message);
53724
+ this.code = code;
53725
+ this.name = "TemplateStoreError";
53726
+ }
53727
+ code;
53728
+ };
53729
+ function templateDir(env = process.env) {
53730
+ const override = env.APPLE_NOTES_MCP_TEMPLATE_DIR?.trim();
53731
+ if (override) {
53732
+ if (!isAbsolute5(override))
53733
+ throw new TemplateStoreError(
53734
+ "unsafe-path",
53735
+ "APPLE_NOTES_MCP_TEMPLATE_DIR must be an absolute path."
53736
+ );
53737
+ return resolve5(override);
53738
+ }
53739
+ return join25(homedir19(), "Library/Application Support/apple-notes-mcp/templates");
53740
+ }
53741
+ function checkName(name, action) {
53742
+ if (isBuiltinTemplate(name) && action !== "read")
53743
+ throw new TemplateStoreError(
53744
+ "reserved-name",
53745
+ `"${name}" is a built-in template and cannot be ${action === "save" ? "saved over" : "deleted"}.`
53746
+ );
53747
+ if (!TEMPLATE_SLUG.test(name))
53748
+ throw new TemplateStoreError(
53749
+ "invalid-name",
53750
+ `Template names are 1-64 characters of a-z, 0-9, "-" and "_", starting with a letter or digit; got "${name}".`
53751
+ );
53752
+ }
53753
+ var TemplateStore = class {
53754
+ constructor(dir = templateDir()) {
53755
+ this.dir = dir;
53756
+ }
53757
+ dir;
53758
+ /** The library root, which must be a real directory (not a symlink) when it exists. */
53759
+ root(create) {
53760
+ let stat;
53761
+ try {
53762
+ stat = lstatSync5(this.dir);
53763
+ } catch {
53764
+ if (!create) return void 0;
53765
+ mkdirSync7(this.dir, { recursive: true, mode: 448 });
53766
+ stat = lstatSync5(this.dir);
53767
+ }
53768
+ if (!stat.isDirectory())
53769
+ throw new TemplateStoreError(
53770
+ "unsafe-path",
53771
+ `The template library ${this.dir} is not a directory (symlinks are refused).`
53772
+ );
53773
+ return this.dir;
53774
+ }
53775
+ file(name) {
53776
+ return join25(this.dir, `${name}.json`);
53777
+ }
53778
+ /** Read a saved template's text, refusing symlinks and non-regular files. */
53779
+ readText(path10) {
53780
+ let fd;
53781
+ try {
53782
+ fd = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
53783
+ } catch (error2) {
53784
+ if (error2.code === "ENOENT") return void 0;
53785
+ throw new TemplateStoreError("unsafe-path", `Refusing to read ${path10}: not a regular file.`);
53786
+ }
53787
+ try {
53788
+ const stat = fstatSync7(fd);
53789
+ if (!stat.isFile())
53790
+ throw new TemplateStoreError(
53791
+ "unsafe-path",
53792
+ `Refusing to read ${path10}: not a regular file.`
53793
+ );
53794
+ if (stat.size > MAX_TEMPLATE_BYTES)
53795
+ throw new TemplateValidationError([
53796
+ {
53797
+ path: "$",
53798
+ message: `template is ${stat.size} bytes; the limit is ${MAX_TEMPLATE_BYTES}`
53799
+ }
53800
+ ]);
53801
+ const data = Buffer.alloc(stat.size);
53802
+ let read = 0;
53803
+ while (read < stat.size) {
53804
+ const n = readSync5(fd, data, read, stat.size - read, read);
53805
+ if (n <= 0) break;
53806
+ read += n;
53807
+ }
53808
+ return { text: data.subarray(0, read).toString("utf8"), bytes: read, mtime: stat.mtime };
53809
+ } finally {
53810
+ closeSync7(fd);
53811
+ }
53812
+ }
53813
+ /** Saved templates in name order; `skipped` counts entries that are not usable. */
53814
+ list() {
53815
+ const templates = [];
53816
+ let skipped = 0;
53817
+ if (!this.root(false)) return { templates, skipped, dir: this.dir };
53818
+ for (const entry of readdirSync4(this.dir).sort()) {
53819
+ if (!entry.endsWith(".json") || entry.startsWith(".")) continue;
53820
+ const name = entry.slice(0, -5);
53821
+ if (!TEMPLATE_SLUG.test(name) || isBuiltinTemplate(name)) {
53822
+ skipped++;
53823
+ continue;
53824
+ }
53825
+ try {
53826
+ const read = this.readText(this.file(name));
53827
+ if (!read) continue;
53828
+ const template = parseTemplate(read.text);
53829
+ templates.push({
53830
+ name,
53831
+ ...template.name && template.name !== name ? { displayName: template.name } : {},
53832
+ ...template.description ? { description: template.description } : {},
53833
+ ...template.extends ? { extends: template.extends } : {},
53834
+ bytes: read.bytes,
53835
+ modified: read.mtime.toISOString()
53836
+ });
53837
+ } catch {
53838
+ skipped++;
53839
+ }
53840
+ }
53841
+ return { templates, skipped, dir: this.dir };
53842
+ }
53843
+ /**
53844
+ * A saved template in its portable (as stored) form, or undefined when
53845
+ * there is none. Throws {@link TemplateValidationError} for a corrupt file.
53846
+ */
53847
+ find(name) {
53848
+ checkName(name, "read");
53849
+ if (!this.root(false)) return void 0;
53850
+ const read = this.readText(this.file(name));
53851
+ return read ? parseTemplate(read.text) : void 0;
53852
+ }
53853
+ /** Like {@link find}, but a missing template is an error. */
53854
+ get(name) {
53855
+ const template = this.find(name);
53856
+ if (!template)
53857
+ throw new TemplateStoreError(
53858
+ "template-not-found",
53859
+ `No saved template named "${name}". Built-in templates: ${BUILTIN_TEMPLATE_NAMES.join(", ")}.`
53860
+ );
53861
+ return template;
53862
+ }
53863
+ /**
53864
+ * Validate and save a template under `name`. Create-only unless `force`.
53865
+ * Returns the file path and whether an existing template was replaced.
53866
+ */
53867
+ save(name, text2, { force = false } = {}) {
53868
+ checkName(name, "save");
53869
+ const template = parseTemplate(text2);
53870
+ const body = JSON.stringify(template, null, 2) + "\n";
53871
+ this.root(true);
53872
+ const path10 = this.file(name);
53873
+ let exists = false;
53874
+ try {
53875
+ const stat = lstatSync5(path10);
53876
+ if (!stat.isFile())
53877
+ throw new TemplateStoreError(
53878
+ "unsafe-path",
53879
+ `Refusing to replace ${path10}: it is not a regular file.`
53880
+ );
53881
+ exists = true;
53882
+ } catch (error2) {
53883
+ if (error2 instanceof TemplateStoreError) throw error2;
53884
+ }
53885
+ if (exists && !force)
53886
+ throw new TemplateStoreError(
53887
+ "template-exists",
53888
+ `A template named "${name}" already exists. Pass force: true to replace it.`
53889
+ );
53890
+ const temp = join25(this.dir, `.${name}.${randomBytes(6).toString("hex")}.tmp`);
53891
+ const fd = openSync7(
53892
+ temp,
53893
+ constants7.O_WRONLY | constants7.O_CREAT | constants7.O_EXCL | constants7.O_NOFOLLOW,
53894
+ 384
53895
+ );
53896
+ try {
53897
+ const data = Buffer.from(body, "utf8");
53898
+ let written = 0;
53899
+ while (written < data.length) written += writeSync5(fd, data, written);
53900
+ } finally {
53901
+ closeSync7(fd);
53902
+ }
53903
+ try {
53904
+ if (force) renameSync(temp, path10);
53905
+ else {
53906
+ try {
53907
+ linkSync(temp, path10);
53908
+ } catch (error2) {
53909
+ if (error2.code === "EEXIST")
53910
+ throw new TemplateStoreError(
53911
+ "template-exists",
53912
+ `A template named "${name}" already exists. Pass force: true to replace it.`
53913
+ );
53914
+ throw error2;
53915
+ }
53916
+ }
53917
+ } finally {
53918
+ try {
53919
+ unlinkSync3(temp);
53920
+ } catch {
53921
+ }
53922
+ }
53923
+ return { path: path10, replaced: exists, bytes: Buffer.byteLength(body), template };
53924
+ }
53925
+ /** Delete a saved template. Built-ins cannot be deleted. */
53926
+ delete(name) {
53927
+ checkName(name, "delete");
53928
+ const path10 = this.file(name);
53929
+ let stat;
53930
+ try {
53931
+ if (!this.root(false)) throw new Error("absent");
53932
+ stat = lstatSync5(path10);
53933
+ } catch (error2) {
53934
+ if (error2 instanceof TemplateStoreError) throw error2;
53935
+ throw new TemplateStoreError("template-not-found", `No saved template named "${name}".`);
53936
+ }
53937
+ if (!stat.isFile())
53938
+ throw new TemplateStoreError(
53939
+ "unsafe-path",
53940
+ `Refusing to delete ${path10}: not a regular file.`
53941
+ );
53942
+ unlinkSync3(path10);
53943
+ return { path: path10 };
53944
+ }
53945
+ };
53946
+
53947
+ // src/tools/markdownTemplates.ts
53948
+ var ok = (text2, structured) => ({
53949
+ content: [{ type: "text", text: text2 }],
53950
+ structuredContent: structured
53951
+ });
53952
+ function failure(action, error2) {
53953
+ if (error2 instanceof TemplateValidationError)
53954
+ return errorResult(
53955
+ `Error ${action} [invalid-template]: the template is invalid:
53956
+ ` + error2.errors.map((e) => `${e.path}: ${e.message}`).join("\n"),
53957
+ error2
53958
+ );
53959
+ if (error2 instanceof TemplateStoreError)
53960
+ return errorResult(`Error ${action} [${error2.code}]: ${error2.message}`, error2);
53961
+ const message = error2 instanceof Error ? error2.message : String(error2);
53962
+ return errorResult(`Error ${action}: ${message}`, error2);
53963
+ }
53964
+ var nameInput = external_exports.string().min(1).max(64).describe("Template name: a built-in (standard-markdown, obsidian) or a saved template's name");
53965
+ var templateInput = external_exports.union([external_exports.record(external_exports.unknown()), external_exports.string().max(MAX_TEMPLATE_BYTES)]).optional().describe("The template itself, as a JSON object or JSON text (schemaVersion 1)");
53966
+ var templateFileInput = external_exports.string().min(1).max(4096).optional().describe(
53967
+ "Absolute path of a JSON template file ending in .json (home, a temp dir, or /Volumes; symlinks refused; at most 256 KiB)"
53968
+ );
53969
+ function sourceText(args) {
53970
+ if (args.template === void 0 === (args.templateFile === void 0))
53971
+ throw new Error("Provide exactly one of 'template' or 'templateFile'.");
53972
+ if (args.templateFile !== void 0) return readTemplateFile(args.templateFile);
53973
+ return typeof args.template === "string" ? args.template : JSON.stringify(args.template);
53974
+ }
53975
+ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
53976
+ const loose = external_exports.object({}).passthrough();
53977
+ server2.registerTool(
53978
+ "list-markdown-templates",
53979
+ {
53980
+ description: "Use when: choosing a Markdown export template, or checking which saved templates exist.\nReturns: the built-in templates, the saved templates (name, display name, description, size, modified date), the library directory, and how many unusable files were skipped.\nDo not use when: you need a template's rules (show-markdown-template).\nSafety: read-only; never opens Notes.",
53981
+ inputSchema: {},
53982
+ outputSchema: loose,
53983
+ annotations: { readOnlyHint: true }
53984
+ },
53985
+ (async () => {
53986
+ try {
53987
+ const listing = store().list();
53988
+ const builtins = BUILTIN_TEMPLATE_NAMES.map((name) => ({
53989
+ name,
53990
+ description: builtinTemplate(name).description ?? ""
53991
+ }));
53992
+ return ok(
53993
+ `${builtins.length} built-in and ${listing.templates.length} saved template(s)` + (listing.skipped ? `; skipped ${listing.skipped} unusable file(s)` : "") + ".",
53994
+ { builtins, ...listing }
53995
+ );
53996
+ } catch (error2) {
53997
+ return failure("listing templates", error2);
53998
+ }
53999
+ })
54000
+ );
54001
+ const showInput = {
54002
+ name: nameInput,
54003
+ expanded: external_exports.boolean().optional().describe("Also return the fully expanded template (every rule filled from its base)")
54004
+ };
54005
+ server2.registerTool(
54006
+ "show-markdown-template",
54007
+ {
54008
+ description: "Use when: reading a built-in or saved template, to copy it as the start of a new one or to see what an export will do.\nReturns: the template in its portable form (as saved, overrides only), its source, and with expanded: true every rule filled in.\nDo not use when: listing names (list-markdown-templates).\nSafety: read-only.",
54009
+ inputSchema: showInput,
54010
+ outputSchema: loose,
54011
+ annotations: { readOnlyHint: true }
54012
+ },
54013
+ (async ({ name, expanded }) => {
54014
+ try {
54015
+ const builtin = isBuiltinTemplate(name);
54016
+ const template = builtin ? builtinTemplate(name) : store().get(name);
54017
+ const structured = {
54018
+ name,
54019
+ source: builtin ? "builtin" : "saved",
54020
+ template,
54021
+ ...expanded ? { expanded: resolveTemplate(template, name) } : {}
54022
+ };
54023
+ return ok(JSON.stringify(template, null, 2), structured);
54024
+ } catch (error2) {
54025
+ return failure("showing template", error2);
54026
+ }
54027
+ })
54028
+ );
54029
+ const validateInput = {
54030
+ name: nameInput.optional(),
54031
+ template: templateInput,
54032
+ templateFile: templateFileInput
54033
+ };
54034
+ server2.registerTool(
54035
+ "validate-markdown-template",
54036
+ {
54037
+ description: 'Use when: checking a template before saving it or exporting with it.\nReturns: valid true, or valid false with every problem as {path, message}, where path is a JSON path such as $.rules["inline.bold"].after.\nDo not use when: you want to store it (save-markdown-template validates too).\nSafety: read-only. Pass exactly one of name (a saved or built-in template), template (JSON object or text) or templateFile.',
54038
+ inputSchema: validateInput,
54039
+ outputSchema: loose,
54040
+ annotations: { readOnlyHint: true }
54041
+ },
54042
+ (async (args) => {
54043
+ try {
54044
+ const given = [args.name, args.template, args.templateFile].filter(
54045
+ (value) => value !== void 0
54046
+ ).length;
54047
+ if (given !== 1)
54048
+ throw new Error("Provide exactly one of 'name', 'template' or 'templateFile'.");
54049
+ try {
54050
+ if (args.name !== void 0) {
54051
+ if (!isBuiltinTemplate(args.name)) store().get(args.name);
54052
+ } else parseTemplate(sourceText(args));
54053
+ } catch (error2) {
54054
+ if (!(error2 instanceof TemplateValidationError)) throw error2;
54055
+ return ok(
54056
+ `Invalid template (${error2.errors.length} problem(s)):
54057
+ ` + error2.errors.map((e) => `${e.path}: ${e.message}`).join("\n"),
54058
+ { valid: false, errors: error2.errors }
54059
+ );
54060
+ }
54061
+ return ok("The template is valid.", { valid: true, errors: [] });
54062
+ } catch (error2) {
54063
+ return failure("validating template", error2);
54064
+ }
54065
+ })
54066
+ );
54067
+ const saveInput = {
54068
+ name: external_exports.string().min(1).max(64).describe(
54069
+ 'Name to save under: lowercase a-z, 0-9, "-" and "_", starting with a letter or digit'
54070
+ ),
54071
+ template: templateInput,
54072
+ templateFile: templateFileInput,
54073
+ force: external_exports.boolean().optional().describe("Replace an existing saved template of this name (default false: create-only)")
54074
+ };
54075
+ server2.registerTool(
54076
+ "save-markdown-template",
54077
+ {
54078
+ description: "Use when: storing a template so export-notes-markdown can use it by name.\nReturns: name, path, bytes and whether an existing template was replaced.\nDo not use when: exporting once (pass templateFile to export-notes-markdown instead).\nSafety: writes one file in the template library only (APPLE_NOTES_MCP_TEMPLATE_DIR, default ~/Library/Application Support/apple-notes-mcp/templates, mode 0600). Validates first; an invalid template is refused with every JSON path. Create-only: an existing name is refused with [template-exists] unless force is true. Built-in names are reserved.",
54079
+ inputSchema: saveInput,
54080
+ outputSchema: loose,
54081
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
54082
+ },
54083
+ (async (args) => {
54084
+ try {
54085
+ const saved = store().save(args.name, sourceText(args), { force: args.force });
54086
+ return ok(
54087
+ `${saved.replaced ? "Replaced" : "Saved"} template "${args.name}" (${saved.bytes} bytes) at ${saved.path}.`,
54088
+ { name: args.name, path: saved.path, bytes: saved.bytes, replaced: saved.replaced }
54089
+ );
54090
+ } catch (error2) {
54091
+ return failure("saving template", error2);
54092
+ }
54093
+ })
54094
+ );
54095
+ const deleteInput = { name: external_exports.string().min(1).max(64).describe("Saved template to delete") };
54096
+ server2.registerTool(
54097
+ "delete-markdown-template",
54098
+ {
54099
+ description: "Use when: removing a saved Markdown template the user no longer wants.\nReturns: the name and the path of the removed file.\nDo not use when: the template is built in (standard-markdown, obsidian cannot be deleted).\nSafety: permanently removes one file from the template library; never touches Notes.",
54100
+ inputSchema: deleteInput,
54101
+ outputSchema: loose,
54102
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
54103
+ },
54104
+ (async ({ name }) => {
54105
+ try {
54106
+ const removed = store().delete(name);
54107
+ return ok(`Deleted template "${name}" (${removed.path}).`, {
54108
+ name,
54109
+ path: removed.path,
54110
+ deleted: true
54111
+ });
54112
+ } catch (error2) {
54113
+ return failure("deleting template", error2);
54114
+ }
54115
+ })
54116
+ );
54117
+ }
54118
+
53689
54119
  // src/utils/svgAnalyzer.ts
53690
54120
  import { createHash as createHash5 } from "node:crypto";
53691
- import { closeSync as closeSync7, constants as constants7, fstatSync as fstatSync7, openSync as openSync7, readSync as readSync5 } from "node:fs";
54121
+ import { closeSync as closeSync8, constants as constants8, fstatSync as fstatSync8, openSync as openSync8, readSync as readSync6 } from "node:fs";
53692
54122
 
53693
54123
  // src/utils/svgColor.ts
53694
54124
  var NAMED = {
@@ -54629,7 +55059,7 @@ function parseXml(source, limits) {
54629
55059
  if (a.name === "xmlns") scope2.set("", a.value);
54630
55060
  else if (a.name.startsWith("xmlns:")) scope2.set(a.name.slice(6), a.value);
54631
55061
  }
54632
- const resolve8 = (qualified, isAttribute) => {
55062
+ const resolve9 = (qualified, isAttribute) => {
54633
55063
  const colon = qualified.indexOf(":");
54634
55064
  if (colon < 0) return { local: qualified, ns: isAttribute ? null : scope2.get("") ?? null };
54635
55065
  const prefix = qualified.slice(0, colon);
@@ -54639,12 +55069,12 @@ function parseXml(source, limits) {
54639
55069
  if (ns === void 0) fail(`Undeclared namespace prefix "${prefix.slice(0, 40)}"`);
54640
55070
  return { local, ns };
54641
55071
  };
54642
- const resolved = resolve8(name, false);
55072
+ const resolved = resolve9(name, false);
54643
55073
  const element = {
54644
55074
  name,
54645
55075
  local: resolved.local,
54646
55076
  ns: resolved.ns,
54647
- attributes: rawAttributes.filter((a) => a.name !== "xmlns" && !a.name.startsWith("xmlns:")).map((a) => ({ name: a.name, value: a.value, ...resolve8(a.name, true) })),
55077
+ attributes: rawAttributes.filter((a) => a.name !== "xmlns" && !a.name.startsWith("xmlns:")).map((a) => ({ name: a.name, value: a.value, ...resolve9(a.name, true) })),
54648
55078
  children: [],
54649
55079
  text: "",
54650
55080
  line: tagLine
@@ -55657,14 +56087,14 @@ function analyzeSvgBuffer(source) {
55657
56087
  function readSvgSource(path10) {
55658
56088
  let fd;
55659
56089
  try {
55660
- fd = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
56090
+ fd = openSync8(path10, constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK);
55661
56091
  } catch (error2) {
55662
56092
  if (error2.code === "ELOOP")
55663
56093
  throw new SvgError("svg_file_invalid", "The SVG path is a symbolic link");
55664
56094
  throw new SvgError("svg_file_invalid", "The SVG file does not exist or cannot be read");
55665
56095
  }
55666
56096
  try {
55667
- const info = fstatSync7(fd);
56097
+ const info = fstatSync8(fd);
55668
56098
  if (!info.isFile())
55669
56099
  throw new SvgError("svg_file_invalid", "The SVG path is not a regular file");
55670
56100
  if (info.size > SVG_LIMITS.maxSourceBytes)
@@ -55675,7 +56105,7 @@ function readSvgSource(path10) {
55675
56105
  const buffer = Buffer.alloc(SVG_LIMITS.maxSourceBytes + 1);
55676
56106
  let total = 0;
55677
56107
  for (; ; ) {
55678
- const n = readSync5(fd, buffer, total, buffer.length - total, null);
56108
+ const n = readSync6(fd, buffer, total, buffer.length - total, null);
55679
56109
  if (n === 0) break;
55680
56110
  total += n;
55681
56111
  if (total > SVG_LIMITS.maxSourceBytes)
@@ -55686,7 +56116,7 @@ function readSvgSource(path10) {
55686
56116
  }
55687
56117
  return buffer.subarray(0, total);
55688
56118
  } finally {
55689
- closeSync7(fd);
56119
+ closeSync8(fd);
55690
56120
  }
55691
56121
  }
55692
56122
  function analyzeSvgFile(path10) {
@@ -56257,7 +56687,7 @@ function registerNativeOperations(server2, manager) {
56257
56687
  import { spawnSync as spawnSync2 } from "node:child_process";
56258
56688
  import { existsSync as existsSync13 } from "node:fs";
56259
56689
  import { release as release2 } from "node:os";
56260
- import { dirname as dirname7, resolve as resolve5 } from "node:path";
56690
+ import { dirname as dirname7, resolve as resolve6 } from "node:path";
56261
56691
  import { fileURLToPath } from "node:url";
56262
56692
  var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
56263
56693
  var MARKDOWN_MIN_DARWIN_MAJOR = 25;
@@ -56280,11 +56710,11 @@ function setupShortcuts(checkOnly, dependencies = {}) {
56280
56710
  const result = spawnSync2("/usr/bin/open", [path10], { encoding: "utf8" });
56281
56711
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
56282
56712
  });
56283
- const baseDirectory = dependencies.baseDirectory || resolve5(dirname7(fileURLToPath(import.meta.url)), "../shortcuts");
56713
+ const baseDirectory = dependencies.baseDirectory || resolve6(dirname7(fileURLToPath(import.meta.url)), "../shortcuts");
56284
56714
  const osRelease = (dependencies.osRelease || release2)();
56285
56715
  const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
56286
56716
  const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
56287
- const path10 = resolve5(baseDirectory, file);
56717
+ const path10 = resolve6(baseDirectory, file);
56288
56718
  let installed = false;
56289
56719
  let identifier;
56290
56720
  let error2;
@@ -56348,15 +56778,15 @@ import { createHash as createHash6 } from "node:crypto";
56348
56778
  import {
56349
56779
  chmodSync,
56350
56780
  existsSync as existsSync14,
56351
- mkdirSync as mkdirSync7,
56781
+ mkdirSync as mkdirSync8,
56352
56782
  mkdtempSync as mkdtempSync6,
56353
56783
  readFileSync as readFileSync4,
56354
- renameSync,
56784
+ renameSync as renameSync2,
56355
56785
  rmSync as rmSync6,
56356
56786
  writeFileSync as writeFileSync5
56357
56787
  } from "node:fs";
56358
- import { homedir as homedir19, release as release3 } from "node:os";
56359
- import { dirname as dirname8, join as join25, resolve as resolve6 } from "node:path";
56788
+ import { homedir as homedir20, release as release3 } from "node:os";
56789
+ import { dirname as dirname8, join as join26, resolve as resolve7 } from "node:path";
56360
56790
  import { fileURLToPath as fileURLToPath2 } from "node:url";
56361
56791
  var PUBLIC_HELPER_PROTOCOL = 1;
56362
56792
  var PUBLIC_HELPER_DIR_ENV = "APPLE_NOTES_MCP_PUBLIC_HELPER_DIR";
@@ -56384,7 +56814,7 @@ var publicManifestSchema = external_exports.object({
56384
56814
  function packageRoot(fromDir = dirname8(fileURLToPath2(import.meta.url))) {
56385
56815
  let dir = fromDir;
56386
56816
  for (; ; ) {
56387
- const candidate = join25(dir, "package.json");
56817
+ const candidate = join26(dir, "package.json");
56388
56818
  if (existsSync14(candidate)) {
56389
56819
  try {
56390
56820
  if (JSON.parse(readFileSync4(candidate, "utf8")).name === "apple-notes-mcp")
@@ -56393,7 +56823,7 @@ function packageRoot(fromDir = dirname8(fileURLToPath2(import.meta.url))) {
56393
56823
  }
56394
56824
  }
56395
56825
  const parent = dirname8(dir);
56396
- if (parent === dir) return resolve6(fromDir, "..");
56826
+ if (parent === dir) return resolve7(fromDir, "..");
56397
56827
  dir = parent;
56398
56828
  }
56399
56829
  }
@@ -56401,7 +56831,7 @@ function defaultPublicHelperDeps(overrides = {}) {
56401
56831
  return {
56402
56832
  env: process.env,
56403
56833
  platform: process.platform,
56404
- sourcePath: join25(packageRoot(), PUBLIC_HELPER_SOURCE),
56834
+ sourcePath: join26(packageRoot(), PUBLIC_HELPER_SOURCE),
56405
56835
  exists: existsSync14,
56406
56836
  readFile: (path10) => readFileSync4(path10),
56407
56837
  spawn: spawnSync3,
@@ -56412,14 +56842,14 @@ function defaultPublicHelperDeps(overrides = {}) {
56412
56842
  function publicHelperInstallDir(env = process.env) {
56413
56843
  const override = env[PUBLIC_HELPER_DIR_ENV]?.trim();
56414
56844
  if (override) return override;
56415
- return join25(homedir19(), "Library", "Application Support", "apple-notes-mcp", "public-helper");
56845
+ return join26(homedir20(), "Library", "Application Support", "apple-notes-mcp", "public-helper");
56416
56846
  }
56417
56847
  function sha256Hex(data) {
56418
56848
  return createHash6("sha256").update(data).digest("hex");
56419
56849
  }
56420
56850
  function inspectPublicHelper(deps = defaultPublicHelperDeps()) {
56421
56851
  const installDir = publicHelperInstallDir(deps.env);
56422
- const binaryPath = join25(installDir, PUBLIC_HELPER_BINARY);
56852
+ const binaryPath = join26(installDir, PUBLIC_HELPER_BINARY);
56423
56853
  const base = { installDir, binaryPath, sourcePath: deps.sourcePath, manifest: null };
56424
56854
  const fail = (reason, detail) => ({
56425
56855
  ...base,
@@ -56430,7 +56860,7 @@ function inspectPublicHelper(deps = defaultPublicHelperDeps()) {
56430
56860
  if (deps.platform !== "darwin") return fail("unsupported_platform", "macOS only");
56431
56861
  if (!deps.exists(deps.sourcePath))
56432
56862
  return fail("helper_not_installed", `Packaged helper source is missing: ${deps.sourcePath}`);
56433
- const manifestPath = join25(installDir, PUBLIC_HELPER_MANIFEST);
56863
+ const manifestPath = join26(installDir, PUBLIC_HELPER_MANIFEST);
56434
56864
  if (!deps.exists(binaryPath) || !deps.exists(manifestPath))
56435
56865
  return fail(
56436
56866
  "helper_not_installed",
@@ -56522,9 +56952,9 @@ async function callPublicHelperAsync(action, fields = {}, deps = defaultPublicHe
56522
56952
  });
56523
56953
  const chunks = [];
56524
56954
  let size = 0;
56525
- let failure = null;
56955
+ let failure2 = null;
56526
56956
  const stop = (error2) => {
56527
- failure ??= error2;
56957
+ failure2 ??= error2;
56528
56958
  child2.kill("SIGKILL");
56529
56959
  };
56530
56960
  const timer = setTimeout(
@@ -56546,12 +56976,12 @@ async function callPublicHelperAsync(action, fields = {}, deps = defaultPublicHe
56546
56976
  child2.on("error", (error2) => {
56547
56977
  settle();
56548
56978
  reject(
56549
- failure ?? new PublicHelperError("helper_unreachable", `Could not run the helper: ${error2.message}`)
56979
+ failure2 ?? new PublicHelperError("helper_unreachable", `Could not run the helper: ${error2.message}`)
56550
56980
  );
56551
56981
  });
56552
56982
  child2.on("close", (status, exitSignal) => {
56553
56983
  settle();
56554
- if (failure) return reject(failure);
56984
+ if (failure2) return reject(failure2);
56555
56985
  if (status === null && exitSignal)
56556
56986
  return reject(
56557
56987
  new PublicHelperError("helper_crashed", `The helper stopped on signal ${exitSignal}.`)
@@ -56669,8 +57099,8 @@ function publicHelperCompileArguments(sourcePath, digestPath, plistPath, outputP
56669
57099
  }
56670
57100
  function buildPublicHelper(checkOnly, deps = defaultPublicHelperBuildDeps()) {
56671
57101
  const steps = [];
56672
- const done = (ok) => ({
56673
- ok,
57102
+ const done = (ok2) => ({
57103
+ ok: ok2,
56674
57104
  checkOnly,
56675
57105
  steps,
56676
57106
  installation: inspectPublicHelper(deps)
@@ -56706,12 +57136,12 @@ function buildPublicHelper(checkOnly, deps = defaultPublicHelperBuildDeps()) {
56706
57136
  const compiler = String(version3.stdout || version3.stderr || "").split("\n").find((line) => line.includes("Swift version"))?.trim() || "swiftc";
56707
57137
  steps.push({ step: "find compiler", ok: true, detail: compiler });
56708
57138
  const installDir = publicHelperInstallDir(deps.env);
56709
- mkdirSync7(installDir, { recursive: true, mode: 448 });
56710
- const staging = mkdtempSync6(join25(installDir, ".staging-"));
57139
+ mkdirSync8(installDir, { recursive: true, mode: 448 });
57140
+ const staging = mkdtempSync6(join26(installDir, ".staging-"));
56711
57141
  try {
56712
- const stagedBinary = join25(staging, PUBLIC_HELPER_BINARY);
56713
- const digestPath = join25(staging, "source-digest.swift");
56714
- const plistPath = join25(staging, "Info.plist");
57142
+ const stagedBinary = join26(staging, PUBLIC_HELPER_BINARY);
57143
+ const digestPath = join26(staging, "source-digest.swift");
57144
+ const plistPath = join26(staging, "Info.plist");
56715
57145
  writeFileSync5(digestPath, sourceDigestSwift(sourceSha), { mode: 384 });
56716
57146
  writeFileSync5(plistPath, publicHelperInfoPlist(), { mode: 384 });
56717
57147
  const compile = deps.spawn(
@@ -56774,9 +57204,9 @@ function buildPublicHelper(checkOnly, deps = defaultPublicHelperBuildDeps()) {
56774
57204
  compiler
56775
57205
  };
56776
57206
  chmodSync(stagedBinary, 448);
56777
- renameSync(stagedBinary, join25(installDir, PUBLIC_HELPER_BINARY));
57207
+ renameSync2(stagedBinary, join26(installDir, PUBLIC_HELPER_BINARY));
56778
57208
  writeFileSync5(
56779
- join25(installDir, PUBLIC_HELPER_MANIFEST),
57209
+ join26(installDir, PUBLIC_HELPER_MANIFEST),
56780
57210
  JSON.stringify(manifest, null, 2) + "\n",
56781
57211
  { mode: 384 }
56782
57212
  );
@@ -56999,11 +57429,11 @@ function getNoteDrawings(noteId3, options = {}) {
56999
57429
  if (!install.ready)
57000
57430
  throw new PublicHelperError(install.reason ?? "helper_not_installed", install.detail ?? "");
57001
57431
  const drawings = rows.map((row) => decodeRow(row, format, includePoints, deps));
57002
- const ok = drawings.filter((d) => d.status === "ok").length;
57432
+ const ok2 = drawings.filter((d) => d.status === "ok").length;
57003
57433
  return {
57004
57434
  id: noteId3,
57005
57435
  drawingCount: drawings.length,
57006
- status: ok === drawings.length ? "ok" : ok ? "partial" : "error",
57436
+ status: ok2 === drawings.length ? "ok" : ok2 ? "partial" : "error",
57007
57437
  drawings
57008
57438
  };
57009
57439
  }
@@ -57020,8 +57450,8 @@ function formatNoteDrawings(result) {
57020
57450
  }
57021
57451
 
57022
57452
  // src/utils/noteAudio.ts
57023
- import { readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
57024
- import { join as join26 } from "node:path";
57453
+ import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
57454
+ import { join as join27 } from "node:path";
57025
57455
  var EXTRA_AUDIO_UTIS = [
57026
57456
  "public.mp3",
57027
57457
  "public.aiff-audio",
@@ -57046,7 +57476,7 @@ function accountDirsFor(containerDir, accountIdentifier) {
57046
57476
  if (own) return [own];
57047
57477
  let names;
57048
57478
  try {
57049
- names = readdirSync4(join26(containerDir, "Accounts")).sort();
57479
+ names = readdirSync5(join27(containerDir, "Accounts")).sort();
57050
57480
  } catch {
57051
57481
  return [];
57052
57482
  }
@@ -57058,8 +57488,8 @@ function resolveMediaPath(media, accountIdentifier = null, containerDir = NOTES_
57058
57488
  if (!id2 || !filename) return null;
57059
57489
  const generation = safeComponent(media?.generation);
57060
57490
  for (const dir of accountDirsFor(containerDir, accountIdentifier)) {
57061
- const base = join26(dir, "Media", id2);
57062
- const candidates = generation ? [join26(base, generation, filename), join26(base, filename)] : [join26(base, filename)];
57491
+ const base = join27(dir, "Media", id2);
57492
+ const candidates = generation ? [join27(base, generation, filename), join27(base, filename)] : [join27(base, filename)];
57063
57493
  for (const candidate of candidates) {
57064
57494
  const real = realInside(candidate, dir);
57065
57495
  if (real && isFile2(real)) return real;
@@ -57331,21 +57761,21 @@ import { spawnSync as spawnSync5 } from "node:child_process";
57331
57761
  import {
57332
57762
  chmodSync as chmodSync2,
57333
57763
  existsSync as existsSync16,
57334
- mkdirSync as mkdirSync8,
57764
+ mkdirSync as mkdirSync9,
57335
57765
  mkdtempSync as mkdtempSync7,
57336
- renameSync as renameSync2,
57766
+ renameSync as renameSync3,
57337
57767
  rmSync as rmSync7,
57338
57768
  writeFileSync as writeFileSync6
57339
57769
  } from "node:fs";
57340
57770
  import { release as release4 } from "node:os";
57341
- import { join as join28 } from "node:path";
57771
+ import { join as join29 } from "node:path";
57342
57772
 
57343
57773
  // src/services/privateHelper.ts
57344
57774
  import { spawnSync as spawnSync4 } from "node:child_process";
57345
57775
  import { createHash as createHash7 } from "node:crypto";
57346
57776
  import { existsSync as existsSync15, readFileSync as readFileSync5 } from "node:fs";
57347
- import { homedir as homedir20 } from "node:os";
57348
- import { dirname as dirname9, join as join27, resolve as resolve7 } from "node:path";
57777
+ import { homedir as homedir21 } from "node:os";
57778
+ import { dirname as dirname9, join as join28, resolve as resolve8 } from "node:path";
57349
57779
  import { fileURLToPath as fileURLToPath3 } from "node:url";
57350
57780
  var PRIVATE_HELPER_PROTOCOL = 1;
57351
57781
  var ENABLE_ENV = "APPLE_NOTES_MCP_ENABLE_PRIVATE";
@@ -57373,7 +57803,7 @@ var manifestSchema = external_exports.object({
57373
57803
  function packageRoot2(fromDir = dirname9(fileURLToPath3(import.meta.url))) {
57374
57804
  let dir = fromDir;
57375
57805
  for (; ; ) {
57376
- const candidate = join27(dir, "package.json");
57806
+ const candidate = join28(dir, "package.json");
57377
57807
  if (existsSync15(candidate)) {
57378
57808
  try {
57379
57809
  const pkg = JSON.parse(readFileSync5(candidate, "utf8"));
@@ -57382,7 +57812,7 @@ function packageRoot2(fromDir = dirname9(fileURLToPath3(import.meta.url))) {
57382
57812
  }
57383
57813
  }
57384
57814
  const parent = dirname9(dir);
57385
- if (parent === dir) return resolve7(fromDir, "..");
57815
+ if (parent === dir) return resolve8(fromDir, "..");
57386
57816
  dir = parent;
57387
57817
  }
57388
57818
  }
@@ -57390,7 +57820,7 @@ function defaultDeps2(overrides = {}) {
57390
57820
  return {
57391
57821
  env: process.env,
57392
57822
  platform: process.platform,
57393
- sourcePath: join27(packageRoot2(), HELPER_SOURCE_RELATIVE),
57823
+ sourcePath: join28(packageRoot2(), HELPER_SOURCE_RELATIVE),
57394
57824
  exists: existsSync15,
57395
57825
  readFile: (path10) => readFileSync5(path10),
57396
57826
  spawn: spawnSync4,
@@ -57403,14 +57833,14 @@ function privateHelperEnabled(env = process.env) {
57403
57833
  function helperInstallDir(env = process.env) {
57404
57834
  const override = env[HELPER_DIR_ENV]?.trim();
57405
57835
  if (override) return override;
57406
- return join27(homedir20(), "Library", "Application Support", "apple-notes-mcp", "private-helper");
57836
+ return join28(homedir21(), "Library", "Application Support", "apple-notes-mcp", "private-helper");
57407
57837
  }
57408
57838
  function sha256Hex2(data) {
57409
57839
  return createHash7("sha256").update(data).digest("hex");
57410
57840
  }
57411
57841
  function inspectInstallation(deps = defaultDeps2()) {
57412
57842
  const installDir = helperInstallDir(deps.env);
57413
- const binaryPath = join27(installDir, HELPER_BINARY_NAME);
57843
+ const binaryPath = join28(installDir, HELPER_BINARY_NAME);
57414
57844
  const base = {
57415
57845
  installDir,
57416
57846
  binaryPath,
@@ -57428,7 +57858,7 @@ function inspectInstallation(deps = defaultDeps2()) {
57428
57858
  if (!deps.exists(deps.sourcePath))
57429
57859
  return fail("helper_not_installed", `Packaged helper source is missing: ${deps.sourcePath}`);
57430
57860
  base.expectedSourceSha256 = sha256Hex2(deps.readFile(deps.sourcePath));
57431
- const manifestPath = join27(installDir, MANIFEST_NAME);
57861
+ const manifestPath = join28(installDir, MANIFEST_NAME);
57432
57862
  if (!deps.exists(binaryPath) || !deps.exists(manifestPath))
57433
57863
  return fail(
57434
57864
  "helper_not_installed",
@@ -57699,8 +58129,8 @@ function compileArguments(sourcePath, outputPath, sourceSha) {
57699
58129
  }
57700
58130
  function buildPrivateHelper(checkOnly, deps = defaultBuildDeps()) {
57701
58131
  const steps = [];
57702
- const done = (ok) => ({
57703
- ok,
58132
+ const done = (ok2) => ({
58133
+ ok: ok2,
57704
58134
  checkOnly,
57705
58135
  steps,
57706
58136
  installation: inspectInstallation(deps)
@@ -57742,10 +58172,10 @@ function buildPrivateHelper(checkOnly, deps = defaultBuildDeps()) {
57742
58172
  const compiler = String(clangVersion.stdout || "").split("\n")[0] || "clang";
57743
58173
  steps.push({ step: "find compiler", ok: true, detail: compiler });
57744
58174
  const installDir = helperInstallDir(deps.env);
57745
- mkdirSync8(installDir, { recursive: true, mode: 448 });
57746
- const staging = mkdtempSync7(join28(installDir, ".staging-"));
58175
+ mkdirSync9(installDir, { recursive: true, mode: 448 });
58176
+ const staging = mkdtempSync7(join29(installDir, ".staging-"));
57747
58177
  try {
57748
- const stagedBinary = join28(staging, HELPER_BINARY_NAME);
58178
+ const stagedBinary = join29(staging, HELPER_BINARY_NAME);
57749
58179
  const compile = deps.spawn(
57750
58180
  "/usr/bin/xcrun",
57751
58181
  compileArguments(deps.sourcePath, stagedBinary, sourceSha),
@@ -57822,8 +58252,8 @@ function buildPrivateHelper(checkOnly, deps = defaultBuildDeps()) {
57822
58252
  compiler
57823
58253
  };
57824
58254
  chmodSync2(stagedBinary, 448);
57825
- renameSync2(stagedBinary, join28(installDir, HELPER_BINARY_NAME));
57826
- writeFileSync6(join28(installDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2) + "\n", {
58255
+ renameSync3(stagedBinary, join29(installDir, HELPER_BINARY_NAME));
58256
+ writeFileSync6(join29(installDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2) + "\n", {
57827
58257
  mode: 384
57828
58258
  });
57829
58259
  steps.push({ step: "install", ok: true, detail: installDir });
@@ -57991,6 +58421,7 @@ var server = new McpServer({
57991
58421
  var notesManager = new AppleNotesManager();
57992
58422
  registerDirectOperations(server, notesManager);
57993
58423
  registerFolderDelete(server, notesManager);
58424
+ registerMarkdownTemplates(server);
57994
58425
  registerSvgAnalysis(server);
57995
58426
  registerNativeTagsBridge(server, notesManager);
57996
58427
  registerNativeOperations(server, notesManager);
@@ -60808,7 +61239,7 @@ registerTool(
60808
61239
  assetsDir: exportPathInput("Directory that receives copies of attachment files"),
60809
61240
  wrap: external_exports.number().int().min(0).max(1e3).optional().describe("Hard-wrap prose at this many columns (0 or omitted: no wrapping)"),
60810
61241
  template: external_exports.string().min(1).max(64).optional().describe(
60811
- `Render through this template: built-in ${BUILTIN_TEMPLATE_NAMES.map((n) => `'${n}'`).join(" or ")}. Exclusive with templateFile`
61242
+ `Render through this template: built-in ${BUILTIN_TEMPLATE_NAMES.map((n) => `'${n}'`).join(" or ")}, or a saved template's name (list-markdown-templates). Exclusive with templateFile`
60812
61243
  ),
60813
61244
  templateFile: exportPathInput(
60814
61245
  "JSON template file (.json) to render through (exclusive with template; at most 256 KiB)"
@@ -60837,6 +61268,7 @@ registerTool(
60837
61268
  try {
60838
61269
  receipt = exportNotesMarkdown(request, {
60839
61270
  listNoteRefs: (account, folder, since, limit) => notesManager.listNoteRefs(account, folder, since, limit),
61271
+ findTemplate: (name) => TEMPLATE_SLUG.test(name) ? new TemplateStore().find(name) : void 0,
60840
61272
  // The document travels twice (text and structuredContent).
60841
61273
  maxInlineBytes: Math.floor(exportMaxResponseBytes() / 2) - 64 * 1024
60842
61274
  });
@@ -11,11 +11,20 @@ Templates change only the exported Markdown. They never change a note.
11
11
 
12
12
  ## Choosing a template
13
13
 
14
+ Built-in and saved templates are managed with `list-markdown-templates`,
15
+ `show-markdown-template`, `validate-markdown-template`,
16
+ `save-markdown-template` and `delete-markdown-template`. Saved templates live
17
+ in `~/Library/Application Support/apple-notes-mcp/templates` (or the absolute
18
+ directory in `APPLE_NOTES_MCP_TEMPLATE_DIR`) as `<name>.json`, where the name
19
+ is a lowercase slug of `a-z`, `0-9`, `-` and `_`. Saving is create-only unless
20
+ `force` is true, and the built-in names are reserved. The library skips files
21
+ it cannot read or validate instead of trusting them.
22
+
14
23
  Pass exactly one of these to `export-notes-markdown`:
15
24
 
16
25
  | Parameter | Meaning |
17
26
  | --- | --- |
18
- | `template` | A built-in name: `standard-markdown` or `obsidian` |
27
+ | `template` | A built-in name (`standard-markdown` or `obsidian`) or a saved template's name |
19
28
  | `templateFile` | An absolute path to a JSON template file ending in `.json` (home, a temp directory, or `/Volumes`; at most 256 KiB; symlinks refused) |
20
29
 
21
30
  The template is read and validated before any note is opened. An invalid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.15",
3
+ "version": "2.9.16",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",