apple-notes-mcp 2.8.32 → 2.8.34

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 +66 -7
  2. package/build/index.js +1054 -166
  3. package/package.json +1 -1
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, constants3) {
420
+ optimizeNames(names, constants4) {
421
421
  if (!names[this.name.str])
422
422
  return;
423
423
  if (this.rhs)
424
- this.rhs = optimizeExpr(this.rhs, names, constants3);
424
+ this.rhs = optimizeExpr(this.rhs, names, constants4);
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, constants3) {
441
+ optimizeNames(names, constants4) {
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, constants3);
444
+ this.rhs = optimizeExpr(this.rhs, names, constants4);
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, constants3) {
506
- this.code = optimizeExpr(this.code, names, constants3);
505
+ optimizeNames(names, constants4) {
506
+ this.code = optimizeExpr(this.code, names, constants4);
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, constants3) {
535
+ optimizeNames(names, constants4) {
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, constants3))
540
+ if (n.optimizeNames(names, constants4))
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, constants3) {
593
+ optimizeNames(names, constants4) {
594
594
  var _a;
595
- this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants3);
596
- if (!(super.optimizeNames(names, constants3) || this.else))
595
+ this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants4);
596
+ if (!(super.optimizeNames(names, constants4) || this.else))
597
597
  return;
598
- this.condition = optimizeExpr(this.condition, names, constants3);
598
+ this.condition = optimizeExpr(this.condition, names, constants4);
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, constants3) {
622
- if (!super.optimizeNames(names, constants3))
621
+ optimizeNames(names, constants4) {
622
+ if (!super.optimizeNames(names, constants4))
623
623
  return;
624
- this.iteration = optimizeExpr(this.iteration, names, constants3);
624
+ this.iteration = optimizeExpr(this.iteration, names, constants4);
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, constants3) {
661
- if (!super.optimizeNames(names, constants3))
660
+ optimizeNames(names, constants4) {
661
+ if (!super.optimizeNames(names, constants4))
662
662
  return;
663
- this.iterable = optimizeExpr(this.iterable, names, constants3);
663
+ this.iterable = optimizeExpr(this.iterable, names, constants4);
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, constants3) {
705
+ optimizeNames(names, constants4) {
706
706
  var _a, _b;
707
- super.optimizeNames(names, constants3);
708
- (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants3);
709
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants3);
707
+ super.optimizeNames(names, constants4);
708
+ (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants4);
709
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants4);
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, constants3) {
1010
+ function optimizeExpr(expr, names, constants4) {
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 = constants3[n.str];
1025
+ const c = constants4[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 && constants3[c.str] !== void 0);
1032
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants4[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 = resolve3.call(this, root, ref);
2994
+ let _sch = resolve4.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 resolve3(root, ref) {
3021
+ function resolve4(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 resolve3(baseURI, relativeURI, options) {
3846
+ function resolve4(baseURI, relativeURI, options) {
3847
3847
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3848
3848
  const {
3849
3849
  parsed: baseParsed,
@@ -3876,49 +3876,49 @@ var require_fast_uri = __commonJS({
3876
3876
  schemelessOptions.skipEscape = true;
3877
3877
  return serialize(resolved, schemelessOptions);
3878
3878
  }
3879
- function resolveComponent(base, relative2, options, skipNormalization) {
3879
+ function resolveComponent(base, relative3, options, skipNormalization) {
3880
3880
  const target = {};
3881
3881
  if (!skipNormalization) {
3882
3882
  base = parse3(serialize(base, options), options);
3883
- relative2 = parse3(serialize(relative2, options), options);
3883
+ relative3 = parse3(serialize(relative3, options), options);
3884
3884
  }
3885
3885
  options = options || {};
3886
- if (!options.tolerant && relative2.scheme) {
3887
- target.scheme = relative2.scheme;
3888
- target.userinfo = relative2.userinfo;
3889
- target.host = relative2.host;
3890
- target.port = relative2.port;
3891
- target.path = removeDotSegments(relative2.path || "");
3892
- target.query = relative2.query;
3886
+ if (!options.tolerant && relative3.scheme) {
3887
+ target.scheme = relative3.scheme;
3888
+ target.userinfo = relative3.userinfo;
3889
+ target.host = relative3.host;
3890
+ target.port = relative3.port;
3891
+ target.path = removeDotSegments(relative3.path || "");
3892
+ target.query = relative3.query;
3893
3893
  } else {
3894
- if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
3895
- target.userinfo = relative2.userinfo;
3896
- target.host = relative2.host;
3897
- target.port = relative2.port;
3898
- target.path = removeDotSegments(relative2.path || "");
3899
- target.query = relative2.query;
3894
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
3895
+ target.userinfo = relative3.userinfo;
3896
+ target.host = relative3.host;
3897
+ target.port = relative3.port;
3898
+ target.path = removeDotSegments(relative3.path || "");
3899
+ target.query = relative3.query;
3900
3900
  } else {
3901
- if (!relative2.path) {
3901
+ if (!relative3.path) {
3902
3902
  target.path = base.path;
3903
- if (relative2.query !== void 0) {
3904
- target.query = relative2.query;
3903
+ if (relative3.query !== void 0) {
3904
+ target.query = relative3.query;
3905
3905
  } else {
3906
3906
  target.query = base.query;
3907
3907
  }
3908
3908
  } else {
3909
- if (relative2.path[0] === "/") {
3910
- target.path = removeDotSegments(relative2.path);
3909
+ if (relative3.path[0] === "/") {
3910
+ target.path = removeDotSegments(relative3.path);
3911
3911
  } else {
3912
3912
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3913
- target.path = "/" + relative2.path;
3913
+ target.path = "/" + relative3.path;
3914
3914
  } else if (!base.path) {
3915
- target.path = relative2.path;
3915
+ target.path = relative3.path;
3916
3916
  } else {
3917
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
3917
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
3918
3918
  }
3919
3919
  target.path = removeDotSegments(target.path);
3920
3920
  }
3921
- target.query = relative2.query;
3921
+ target.query = relative3.query;
3922
3922
  }
3923
3923
  target.userinfo = base.userinfo;
3924
3924
  target.host = base.host;
@@ -3926,7 +3926,7 @@ var require_fast_uri = __commonJS({
3926
3926
  }
3927
3927
  target.scheme = base.scheme;
3928
3928
  }
3929
- target.fragment = relative2.fragment;
3929
+ target.fragment = relative3.fragment;
3930
3930
  return target;
3931
3931
  }
3932
3932
  function equal(uriA, uriB, options) {
@@ -4205,7 +4205,7 @@ var require_fast_uri = __commonJS({
4205
4205
  var fastUri = {
4206
4206
  SCHEMES,
4207
4207
  normalize,
4208
- resolve: resolve3,
4208
+ resolve: resolve4,
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 constants3 = {
7425
+ var constants4 = {
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 constants3) {
7465
- v = { value: constants3[c] };
7464
+ for (c in constants4) {
7465
+ v = { value: constants4[c] };
7466
7466
  Object.defineProperty(DOMException, c, v);
7467
7467
  Object.defineProperty(DOMException.prototype, c, v);
7468
7468
  }
@@ -12184,9 +12184,9 @@ var require_URL = __commonJS({
12184
12184
  },
12185
12185
  // See: http://tools.ietf.org/html/rfc3986#section-5.2
12186
12186
  // and https://url.spec.whatwg.org/#constructors
12187
- resolve: function(relative2) {
12187
+ resolve: function(relative3) {
12188
12188
  var base = this;
12189
- var r = new URL2(relative2);
12189
+ var r = new URL2(relative3);
12190
12190
  var t = new URL2();
12191
12191
  if (r.scheme !== void 0) {
12192
12192
  t.scheme = r.scheme;
@@ -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 join16(output, replacement);
24468
+ return join17(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 = join16(output, rule.append(self.options));
24475
+ output = join17(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 join16(output, replacement) {
24487
+ function join17(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((resolve3) => setTimeout(resolve3, pollInterval));
36611
+ await new Promise((resolve4) => setTimeout(resolve4, 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((resolve3, reject) => {
36628
+ return new Promise((resolve4, 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
- resolve3(parseResult.data);
36706
+ resolve4(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((resolve3, reject) => {
36967
+ return new Promise((resolve4, reject) => {
36968
36968
  if (signal.aborted) {
36969
36969
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36970
36970
  return;
36971
36971
  }
36972
- const timeoutId = setTimeout(resolve3, interval);
36972
+ const timeoutId = setTimeout(resolve4, 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((resolve3) => setTimeout(resolve3, pollInterval));
38285
+ await new Promise((resolve4) => setTimeout(resolve4, 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((resolve3) => {
38973
+ return new Promise((resolve4) => {
38974
38974
  const json2 = serializeMessage(message);
38975
38975
  if (this._stdout.write(json2)) {
38976
- resolve3();
38976
+ resolve4();
38977
38977
  } else {
38978
- this._stdout.once("drain", resolve3);
38978
+ this._stdout.once("drain", resolve4);
38979
38979
  }
38980
38980
  });
38981
38981
  }
@@ -39675,7 +39675,11 @@ function parseRichNote(data, nativeTags = []) {
39675
39675
  const length = varintValue(getField(fields, 1));
39676
39676
  if (length === void 0 || length < 0 || position + length > text.length)
39677
39677
  throw new Error("Invalid Notes run length");
39678
+ const paragraph = embeddedMessage(getField(fields, 2));
39678
39679
  styleRuns.push({
39680
+ paragraphStyle: paragraph ? varintValue(getField(paragraph, 1)) ?? 3 : 3,
39681
+ blockQuote: Boolean(paragraph && varintValue(getField(paragraph, 8))),
39682
+ highlight: Boolean(varintValue(getField(fields, 14))),
39679
39683
  start: position,
39680
39684
  length,
39681
39685
  signature: JSON.stringify(
@@ -39703,7 +39707,6 @@ function parseRichNote(data, nativeTags = []) {
39703
39707
  start: position,
39704
39708
  length
39705
39709
  });
39706
- const paragraph = embeddedMessage(getField(fields, 2));
39707
39710
  hasChecklist ||= Boolean(paragraph && varintValue(getField(paragraph, 1)) === 103);
39708
39711
  if (paragraph && varintValue(getField(paragraph, 1)) === 103) {
39709
39712
  const checklist = embeddedMessage(getField(paragraph, 5));
@@ -41033,11 +41036,586 @@ function cleanupTempDir(dir) {
41033
41036
  }
41034
41037
  }
41035
41038
 
41039
+ // src/utils/attachmentAssets.ts
41040
+ import { execFileSync as execFileSync6 } from "node:child_process";
41041
+ import {
41042
+ closeSync as closeSync2,
41043
+ constants as constants2,
41044
+ fstatSync as fstatSync2,
41045
+ lstatSync as lstatSync2,
41046
+ mkdirSync as mkdirSync2,
41047
+ openSync as openSync2,
41048
+ readdirSync,
41049
+ readSync,
41050
+ realpathSync as realpathSync2,
41051
+ unlinkSync,
41052
+ writeSync
41053
+ } from "node:fs";
41054
+ import { homedir as homedir6 } from "node:os";
41055
+ import { basename, dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
41056
+ import { gunzipSync as gunzipSync5 } from "node:zlib";
41057
+ var NOTES_CONTAINER_DIR = join6(
41058
+ homedir6(),
41059
+ "Library/Group Containers/group.com.apple.notes"
41060
+ );
41061
+ var AttachmentStoreError = class extends Error {
41062
+ constructor(message, code) {
41063
+ super(message);
41064
+ this.code = code;
41065
+ this.name = "AttachmentStoreError";
41066
+ }
41067
+ code;
41068
+ };
41069
+ var MAX_PREVIEW_DIR_ENTRIES = 1e5;
41070
+ var MAX_BUNDLE_ENTRIES = 64;
41071
+ var MAX_GENERATION_DIRS = 64;
41072
+ var MAX_COLLISION_SUFFIX = 1e4;
41073
+ var MAX_BODY_BYTES = 32 * 1024 * 1024;
41074
+ var PREVIEW_IMAGE_SUFFIXES = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".heic", ".tiff", ".gif"]);
41075
+ var GENERIC_FILE_NAMES = /* @__PURE__ */ new Set([
41076
+ "fallbackimage.png",
41077
+ "fallbackimage.jpg",
41078
+ "fallbackpdf.pdf",
41079
+ "preview.png",
41080
+ "orientedpreview.png"
41081
+ ]);
41082
+ var IMAGE_UTIS = /* @__PURE__ */ new Set([
41083
+ "public.jpeg",
41084
+ "public.png",
41085
+ "public.heic",
41086
+ "public.heif",
41087
+ "public.tiff",
41088
+ "public.image",
41089
+ "com.compuserve.gif",
41090
+ "org.webmproject.webp",
41091
+ "com.microsoft.bmp",
41092
+ "com.adobe.raw-image"
41093
+ ]);
41094
+ function classifyAttachmentKind(uti) {
41095
+ if (!uti) return "other";
41096
+ const u = uti.toLowerCase();
41097
+ if (u === "com.apple.notes.table") return "table";
41098
+ if (u === "public.url" || u.startsWith("public.url")) return "url";
41099
+ if (u === "com.apple.paper.doc.scan" || u === "com.apple.notes.gallery" || u.includes("scan"))
41100
+ return "scan";
41101
+ if (u === "com.apple.paper" || u.startsWith("com.apple.drawing")) return "drawing";
41102
+ if (u === "com.adobe.pdf" || u.includes("pdf")) return "pdf";
41103
+ if (u.includes("audio")) return "audio";
41104
+ if (u === "public.mpeg-4" || u.includes("movie") || u.includes("video")) return "video";
41105
+ if (IMAGE_UTIS.has(u) || u.includes("image")) return "image";
41106
+ return "other";
41107
+ }
41108
+ function parseNoteId2(noteId3) {
41109
+ const m = /^x-coredata:\/\/([0-9A-Fa-f-]+)\/ICNote\/p(\d+)$/.exec(noteId3);
41110
+ if (!m) {
41111
+ throw new AttachmentStoreError(
41112
+ `Invalid note ID format: "${noteId3}". Expected x-coredata://UUID/ICNote/pNNN`,
41113
+ "invalid_id"
41114
+ );
41115
+ }
41116
+ return { store: m[1], pk: Number(m[2]) };
41117
+ }
41118
+ function attachmentCoreDataId(noteId3, pk) {
41119
+ return noteId3.replace(/ICNote\/p\d+$/, `ICAttachment/p${pk}`);
41120
+ }
41121
+ function buildAttachmentRowsSql(notePk, columns) {
41122
+ if (!Number.isSafeInteger(notePk) || notePk < 0) throw new Error("Invalid note primary key");
41123
+ const col2 = (alias, name) => columns.has(name) ? `${alias}.${name}` : "NULL";
41124
+ const firstOf = (alias, names) => {
41125
+ const present = names.filter((n) => columns.has(n)).map((n) => `${alias}.${n}`);
41126
+ if (present.length === 0) return "NULL";
41127
+ return present.length === 1 ? present[0] : `COALESCE(${present.join(", ")})`;
41128
+ };
41129
+ const accountCols = [...columns].filter((c) => /^ZACCOUNT\d*$/.test(c)).sort();
41130
+ const accountRefs = [
41131
+ ...accountCols.map((c) => `a.${c}`),
41132
+ ...accountCols.map((c) => `n.${c}`)
41133
+ ].join(", ");
41134
+ const account = accountCols.length ? `(SELECT acc.ZIDENTIFIER FROM ZICCLOUDSYNCINGOBJECT acc WHERE acc.Z_ENT = (SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICAccount') AND acc.Z_PK IN (${accountRefs}) LIMIT 1)` : "NULL";
41135
+ const noteLink = columns.has("ZNOTE") ? "a.ZNOTE" : "NULL";
41136
+ const parent = col2("a", "ZPARENTATTACHMENT");
41137
+ const deleted = columns.has("ZMARKEDFORDELETION") ? " AND COALESCE(a.ZMARKEDFORDELETION, 0) = 0" : "";
41138
+ const fields = [
41139
+ `'pk', a.Z_PK`,
41140
+ `'identifier', a.ZIDENTIFIER`,
41141
+ `'uti', ${col2("a", "ZTYPEUTI")}`,
41142
+ `'parentPk', ${parent}`,
41143
+ `'filename', ${col2("a", "ZFILENAME")}`,
41144
+ `'mediaIdentifier', m.ZIDENTIFIER`,
41145
+ `'mediaFilename', ${col2("m", "ZFILENAME")}`,
41146
+ `'mediaGeneration', ${firstOf("m", ["ZGENERATION1", "ZGENERATION"])}`,
41147
+ `'fallbackImageGeneration', ${col2("a", "ZFALLBACKIMAGEGENERATION")}`,
41148
+ `'fallbackPdfGeneration', ${col2("a", "ZFALLBACKPDFGENERATION")}`,
41149
+ `'accountIdentifier', ${account}`
41150
+ ].join(", ");
41151
+ const mediaJoin = columns.has("ZMEDIA") ? "LEFT JOIN ZICCLOUDSYNCINGOBJECT m ON m.Z_PK = a.ZMEDIA" : "LEFT JOIN ZICCLOUDSYNCINGOBJECT m ON 0";
41152
+ const childClause = parent === "NULL" ? "" : ` OR a.ZPARENTATTACHMENT IN (SELECT p.Z_PK FROM ZICCLOUDSYNCINGOBJECT p WHERE p.ZNOTE = ${notePk})`;
41153
+ return [
41154
+ "BEGIN;",
41155
+ `SELECT count(*) FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ${notePk} AND Z_ENT = (SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICNote');`,
41156
+ `SELECT json_group_array(json_object(${fields})) FROM ZICCLOUDSYNCINGOBJECT a ${mediaJoin} LEFT JOIN ZICCLOUDSYNCINGOBJECT n ON n.Z_PK = ${noteLink} WHERE a.Z_ENT = (SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICAttachment') AND (${noteLink} = ${notePk}${childClause})${deleted};`,
41157
+ `SELECT hex(ZDATA) FROM ZICNOTEDATA WHERE ZNOTE = ${notePk};`,
41158
+ "COMMIT;"
41159
+ ].join(" ");
41160
+ }
41161
+ function runSqlite3(dbPath2, sql) {
41162
+ try {
41163
+ return execFileSync6("/usr/bin/sqlite3", ["-readonly", dbPath2, sql], {
41164
+ encoding: "utf8",
41165
+ timeout: 1e4,
41166
+ maxBuffer: 128 * 1024 * 1024,
41167
+ stdio: ["pipe", "pipe", "pipe"]
41168
+ });
41169
+ } catch (error2) {
41170
+ const message = error2 instanceof Error ? error2.message : String(error2);
41171
+ if (/authorization denied|unable to open database/i.test(message)) {
41172
+ throw new AttachmentStoreError(
41173
+ "Full Disk Access is required to read attachment paths. Grant it to the app that launches this server, then relaunch it (run the doctor tool to verify).",
41174
+ "no_fda"
41175
+ );
41176
+ }
41177
+ throw new AttachmentStoreError(`Failed to read attachment rows: ${message}`, "query_error");
41178
+ }
41179
+ }
41180
+ function toStringOrNull(value) {
41181
+ return typeof value === "string" && value.length > 0 ? value : null;
41182
+ }
41183
+ function toIntOrNull(value) {
41184
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
41185
+ }
41186
+ function parseAttachmentRows(json2) {
41187
+ const raw = JSON.parse(json2 || "[]");
41188
+ if (!Array.isArray(raw)) throw new Error("Invalid attachment rows");
41189
+ const rows = [];
41190
+ for (const item of raw) {
41191
+ if (!item || typeof item !== "object") continue;
41192
+ const r = item;
41193
+ const pk = toIntOrNull(r.pk);
41194
+ const identifier = toStringOrNull(r.identifier);
41195
+ if (pk === null || identifier === null) continue;
41196
+ rows.push({
41197
+ pk,
41198
+ identifier,
41199
+ uti: toStringOrNull(r.uti),
41200
+ parentPk: toIntOrNull(r.parentPk),
41201
+ filename: toStringOrNull(r.filename),
41202
+ mediaIdentifier: toStringOrNull(r.mediaIdentifier),
41203
+ mediaFilename: toStringOrNull(r.mediaFilename),
41204
+ mediaGeneration: toStringOrNull(r.mediaGeneration),
41205
+ fallbackImageGeneration: toStringOrNull(r.fallbackImageGeneration),
41206
+ fallbackPdfGeneration: toStringOrNull(r.fallbackPdfGeneration),
41207
+ accountIdentifier: toStringOrNull(r.accountIdentifier)
41208
+ });
41209
+ }
41210
+ return rows.sort((a, b) => a.pk - b.pk);
41211
+ }
41212
+ function attachmentOrderFromNoteData(hex3) {
41213
+ if (!hex3 || !/^[0-9a-f]+$/i.test(hex3)) return null;
41214
+ try {
41215
+ const data = gunzipSync5(Buffer.from(hex3, "hex"), { maxOutputLength: MAX_BODY_BYTES });
41216
+ const wrapper = embeddedMessage(getField(decodeMessage(data), 2));
41217
+ const body = wrapper && embeddedMessage(getField(wrapper, 3));
41218
+ if (!body) return null;
41219
+ const ids = [];
41220
+ for (const run of getFields(body, 5)) {
41221
+ const fields = embeddedMessage(run);
41222
+ const info = fields && embeddedMessage(getField(fields, 12));
41223
+ const id2 = info && stringValue(getField(info, 1));
41224
+ if (id2 && !ids.some((seen) => seen.toLowerCase() === id2.toLowerCase())) ids.push(id2);
41225
+ }
41226
+ return ids;
41227
+ } catch {
41228
+ return null;
41229
+ }
41230
+ }
41231
+ function readNoteAttachmentRows(noteId3, dbPath2 = join6(NOTES_CONTAINER_DIR, "NoteStore.sqlite")) {
41232
+ const { pk } = parseNoteId2(noteId3);
41233
+ const columnList = runSqlite3(
41234
+ dbPath2,
41235
+ "SELECT group_concat(name, ',') FROM pragma_table_info('ZICCLOUDSYNCINGOBJECT');"
41236
+ ).trim();
41237
+ const columns = new Set(columnList.split(",").filter(Boolean));
41238
+ if (!columns.has("ZIDENTIFIER")) {
41239
+ throw new AttachmentStoreError("Unsupported Notes database schema", "query_error");
41240
+ }
41241
+ const lines = runSqlite3(dbPath2, buildAttachmentRowsSql(pk, columns)).split("\n");
41242
+ if (lines[0]?.trim() !== "1") {
41243
+ throw new AttachmentStoreError(
41244
+ `No note found in the database for ID "${noteId3}".`,
41245
+ "not_found"
41246
+ );
41247
+ }
41248
+ let rows;
41249
+ try {
41250
+ rows = parseAttachmentRows(lines[1] ?? "[]");
41251
+ } catch {
41252
+ throw new AttachmentStoreError("Attachment rows could not be parsed", "query_error");
41253
+ }
41254
+ return { rows, bodyOrder: attachmentOrderFromNoteData(lines[2]?.trim()) };
41255
+ }
41256
+ function safeComponent(value) {
41257
+ if (typeof value !== "string" || !value || value.length > 255) return null;
41258
+ if (value === "." || value === ".." || value.includes("/") || value.includes("\0")) return null;
41259
+ return value;
41260
+ }
41261
+ function realInside(path7, rootReal) {
41262
+ try {
41263
+ const real = realpathSync2.native(path7);
41264
+ return real === rootReal || real.startsWith(rootReal + sep2) ? real : null;
41265
+ } catch {
41266
+ return null;
41267
+ }
41268
+ }
41269
+ function isRegularFile(path7) {
41270
+ try {
41271
+ return lstatSync2(path7).isFile();
41272
+ } catch {
41273
+ return false;
41274
+ }
41275
+ }
41276
+ function isDirectory(path7) {
41277
+ try {
41278
+ return lstatSync2(path7).isDirectory();
41279
+ } catch {
41280
+ return false;
41281
+ }
41282
+ }
41283
+ function boundedEntries(dir, limit) {
41284
+ try {
41285
+ const entries = readdirSync(dir);
41286
+ return entries.length > limit ? null : entries.sort();
41287
+ } catch {
41288
+ return null;
41289
+ }
41290
+ }
41291
+ function resolveAccountDir(containerDir, accountIdentifier) {
41292
+ const accountsDir = join6(containerDir, "Accounts");
41293
+ let accountsReal;
41294
+ try {
41295
+ accountsReal = realpathSync2.native(accountsDir);
41296
+ } catch {
41297
+ return null;
41298
+ }
41299
+ const account = safeComponent(accountIdentifier);
41300
+ if (account) {
41301
+ const dir = realInside(join6(accountsReal, account), accountsReal);
41302
+ if (dir && isDirectory(dir)) return dir;
41303
+ }
41304
+ const entries = (boundedEntries(accountsReal, 64) ?? []).filter(
41305
+ (e) => isDirectory(join6(accountsReal, e))
41306
+ );
41307
+ if (entries.length !== 1) return null;
41308
+ return realInside(join6(accountsReal, entries[0]), accountsReal);
41309
+ }
41310
+ function generationRank(name) {
41311
+ const m = /^(\d+)_/.exec(name);
41312
+ return m ? Number(m[1]) : 0;
41313
+ }
41314
+ function generationDirs(base, accountDir) {
41315
+ const entries = boundedEntries(base, MAX_GENERATION_DIRS) ?? [];
41316
+ return entries.map((e) => realInside(join6(base, e), accountDir)).filter((p) => p !== null && isDirectory(p)).sort((a, b) => generationRank(basename(b)) - generationRank(basename(a)));
41317
+ }
41318
+ function fallbackFiles(accountDir, rootName, identifier, generation, names) {
41319
+ const base = join6(accountDir, rootName, identifier);
41320
+ const found = [];
41321
+ const add = (candidate) => {
41322
+ const real = realInside(candidate, accountDir);
41323
+ if (real && isRegularFile(real) && !found.includes(real)) found.push(real);
41324
+ };
41325
+ const gen = safeComponent(generation);
41326
+ if (gen) for (const name of names) add(join6(base, gen, name));
41327
+ if (isDirectory(base)) {
41328
+ for (const dir of generationDirs(base, accountDir))
41329
+ for (const name of names) add(join6(dir, name));
41330
+ for (const name of names) add(join6(base, name));
41331
+ }
41332
+ for (const name of names) add(join6(accountDir, rootName, `${identifier}${extname(name)}`));
41333
+ return found;
41334
+ }
41335
+ function previewPixelArea(name) {
41336
+ const matches = [...name.matchAll(/-(\d{1,6})x(\d{1,6})(?=-|\.|$)/g)];
41337
+ const last = matches.at(-1);
41338
+ return last ? Number(last[1]) * Number(last[2]) : 0;
41339
+ }
41340
+ function previewFileInBundle(bundle, accountDir) {
41341
+ const direct = [];
41342
+ const nested = [];
41343
+ for (const entry of boundedEntries(bundle, MAX_BUNDLE_ENTRIES) ?? []) {
41344
+ const child = realInside(join6(bundle, entry), accountDir);
41345
+ if (!child) continue;
41346
+ if (isRegularFile(child)) direct.push(child);
41347
+ else if (isDirectory(child)) nested.push(child);
41348
+ }
41349
+ nested.sort((a, b) => generationRank(basename(b)) - generationRank(basename(a)));
41350
+ const pick2 = (files) => files.find((f) => basename(f) === "Preview.png") ?? files.find((f) => PREVIEW_IMAGE_SUFFIXES.has(extname(f).toLowerCase())) ?? null;
41351
+ for (const dir of nested) {
41352
+ const files = (boundedEntries(dir, MAX_BUNDLE_ENTRIES) ?? []).map((e) => realInside(join6(dir, e), accountDir)).filter((p) => p !== null && isRegularFile(p));
41353
+ const chosen = pick2(files);
41354
+ if (chosen) return chosen;
41355
+ }
41356
+ return pick2(direct);
41357
+ }
41358
+ function listPreviewEntries(accountDir) {
41359
+ return boundedEntries(join6(accountDir, "Previews"), MAX_PREVIEW_DIR_ENTRIES) ?? [];
41360
+ }
41361
+ function previewPaths(accountDir, identifier, entries) {
41362
+ const id2 = safeComponent(identifier);
41363
+ if (!id2) return [];
41364
+ const prefix = `${id2}-`.toLowerCase();
41365
+ const candidates = entries.filter((name) => name.toLowerCase().startsWith(prefix)).filter((name) => {
41366
+ const ext = extname(name).toLowerCase();
41367
+ return !ext || PREVIEW_IMAGE_SUFFIXES.has(ext) || /^\.\d+$/.test(ext);
41368
+ }).sort((a, b) => previewPixelArea(b) - previewPixelArea(a) || a.localeCompare(b));
41369
+ const files = [];
41370
+ for (const name of candidates) {
41371
+ const real = realInside(join6(accountDir, "Previews", name), accountDir);
41372
+ if (!real) continue;
41373
+ const file = isRegularFile(real) ? real : isDirectory(real) ? previewFileInBundle(real, accountDir) : null;
41374
+ if (file && !files.includes(file)) files.push(file);
41375
+ }
41376
+ return files;
41377
+ }
41378
+ function assetPathsFor(accountDir, row) {
41379
+ const found = [];
41380
+ const add = (candidate) => {
41381
+ const real = realInside(candidate, accountDir);
41382
+ if (real && isRegularFile(real) && !found.includes(real)) found.push(real);
41383
+ };
41384
+ const mediaId = safeComponent(row.mediaIdentifier);
41385
+ const mediaName = safeComponent(row.mediaFilename);
41386
+ const mediaGen = safeComponent(row.mediaGeneration);
41387
+ if (mediaId && mediaName) {
41388
+ if (mediaGen) add(join6(accountDir, "Media", mediaId, mediaGen, mediaName));
41389
+ add(join6(accountDir, "Media", mediaId, mediaName));
41390
+ }
41391
+ const id2 = safeComponent(row.identifier);
41392
+ if (!id2) return found;
41393
+ const ownName = safeComponent(row.filename);
41394
+ if (ownName) add(join6(accountDir, "Media", id2, ownName));
41395
+ for (const file of fallbackFiles(accountDir, "FallbackImages", id2, row.fallbackImageGeneration, [
41396
+ "FallbackImage.png",
41397
+ "FallbackImage.jpg"
41398
+ ]))
41399
+ add(file);
41400
+ for (const file of fallbackFiles(accountDir, "FallbackPDFs", id2, row.fallbackPdfGeneration, [
41401
+ "FallbackPDF.pdf"
41402
+ ]))
41403
+ add(file);
41404
+ return found;
41405
+ }
41406
+ function assembleAttachmentAssets(rows, bodyOrder, containerDir = NOTES_CONTAINER_DIR) {
41407
+ const byPk = new Map(rows.map((r) => [r.pk, r]));
41408
+ const roots = rows.filter((r) => r.parentPk === null || !byPk.has(r.parentPk));
41409
+ const bodyIndex = /* @__PURE__ */ new Map();
41410
+ (bodyOrder ?? []).forEach((id2, index) => bodyIndex.set(id2.toLowerCase(), index));
41411
+ const indexOf = (r) => bodyIndex.get(r.identifier.toLowerCase()) ?? null;
41412
+ const inBody = roots.filter((r) => indexOf(r) !== null);
41413
+ const orderSource = inBody.length > 0 ? "body" : "creation";
41414
+ const orderedRoots = [
41415
+ ...inBody.sort((a, b) => indexOf(a) - indexOf(b)),
41416
+ ...roots.filter((r) => indexOf(r) === null)
41417
+ ];
41418
+ const accountDirs = /* @__PURE__ */ new Map();
41419
+ const previewEntries = /* @__PURE__ */ new Map();
41420
+ const accountDirFor = (account) => {
41421
+ if (!accountDirs.has(account))
41422
+ accountDirs.set(account, resolveAccountDir(containerDir, account));
41423
+ return accountDirs.get(account) ?? null;
41424
+ };
41425
+ const record2 = (row, parent) => {
41426
+ const accountDir = accountDirFor(row.accountIdentifier ?? parent?.accountIdentifier ?? null);
41427
+ let assetPaths = [];
41428
+ let previews = [];
41429
+ if (accountDir) {
41430
+ if (!previewEntries.has(accountDir))
41431
+ previewEntries.set(accountDir, listPreviewEntries(accountDir));
41432
+ assetPaths = assetPathsFor(accountDir, row);
41433
+ previews = previewPaths(accountDir, row.identifier, previewEntries.get(accountDir));
41434
+ }
41435
+ const previewPath = previews[0] ?? null;
41436
+ return {
41437
+ pk: row.pk,
41438
+ identifier: row.identifier,
41439
+ uti: row.uti,
41440
+ kind: classifyAttachmentKind(row.uti),
41441
+ parentIdentifier: parent?.identifier ?? null,
41442
+ filename: row.filename ?? row.mediaFilename,
41443
+ bodyIndex: parent ? null : indexOf(row),
41444
+ assetPaths,
41445
+ previewPath,
41446
+ paths: previewPath ? [...assetPaths, previewPath] : [...assetPaths]
41447
+ };
41448
+ };
41449
+ const attachments = [];
41450
+ for (const root of orderedRoots) {
41451
+ attachments.push(record2(root, null));
41452
+ for (const child of rows.filter((r) => r.parentPk === root.pk && r !== root)) {
41453
+ attachments.push(record2(child, root));
41454
+ }
41455
+ }
41456
+ return { orderSource, attachments };
41457
+ }
41458
+ function selectFirstImage(assets) {
41459
+ const candidates = assets.attachments.filter((a) => {
41460
+ if (a.parentIdentifier !== null) return true;
41461
+ return assets.orderSource === "creation" || a.bodyIndex !== null;
41462
+ });
41463
+ const eligibleChild = (a) => a.parentIdentifier === null || candidates.some((p) => p.parentIdentifier === null && p.identifier === a.parentIdentifier);
41464
+ const ordered = candidates.filter(eligibleChild);
41465
+ const chosen = ordered.find((a) => a.kind === "image") ?? ordered.find((a) => a.kind === "scan" || a.kind === "drawing");
41466
+ if (!chosen) return null;
41467
+ const siblings = chosen.parentIdentifier ? ordered.filter((a) => a.parentIdentifier === chosen.parentIdentifier) : [];
41468
+ return {
41469
+ pk: chosen.pk,
41470
+ identifier: chosen.identifier,
41471
+ uti: chosen.uti,
41472
+ kind: chosen.kind,
41473
+ path: chosen.assetPaths[0] ?? null,
41474
+ previewPath: chosen.previewPath,
41475
+ parentIdentifier: chosen.parentIdentifier,
41476
+ galleryIndex: chosen.parentIdentifier ? siblings.indexOf(chosen) : null,
41477
+ orderSource: assets.orderSource
41478
+ };
41479
+ }
41480
+ function canonicalTail(p) {
41481
+ let current = resolve2(p);
41482
+ const tail = [];
41483
+ for (; ; ) {
41484
+ try {
41485
+ return join6(realpathSync2.native(current), ...tail);
41486
+ } catch {
41487
+ const parent = dirname2(current);
41488
+ if (parent === current) return join6(current, ...tail);
41489
+ tail.unshift(basename(current));
41490
+ current = parent;
41491
+ }
41492
+ }
41493
+ }
41494
+ function isInsideNotesContainer(p, containerDir = NOTES_CONTAINER_DIR) {
41495
+ const target = canonicalTail(p).toLowerCase();
41496
+ const container = canonicalTail(containerDir).toLowerCase();
41497
+ const rel = relative2(container, target);
41498
+ return !(rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel));
41499
+ }
41500
+ function copyFileExclusive(src, dest) {
41501
+ const input = openSync2(src, constants2.O_RDONLY | constants2.O_NOFOLLOW);
41502
+ try {
41503
+ if (!fstatSync2(input).isFile()) throw new Error("Source is not a regular file");
41504
+ const output = openSync2(
41505
+ dest,
41506
+ constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW,
41507
+ 384
41508
+ );
41509
+ try {
41510
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
41511
+ for (; ; ) {
41512
+ const read = readSync(input, buffer, 0, buffer.length, null);
41513
+ if (read === 0) break;
41514
+ let written = 0;
41515
+ while (written < read) written += writeSync(output, buffer, written, read - written);
41516
+ }
41517
+ } catch (error2) {
41518
+ closeSync2(output);
41519
+ unlinkSync(dest);
41520
+ throw error2;
41521
+ }
41522
+ closeSync2(output);
41523
+ } finally {
41524
+ closeSync2(input);
41525
+ }
41526
+ }
41527
+ function exportFileName(record2, source, kind) {
41528
+ const ext = extname(source);
41529
+ if (kind === "preview") {
41530
+ const stored2 = safeComponent(record2.filename ? basename(record2.filename) : null);
41531
+ const stem = stored2 ? basename(stored2, extname(stored2)) : record2.identifier;
41532
+ return `${safeComponent(stem) ?? record2.identifier}-preview${ext}`;
41533
+ }
41534
+ const stored = safeComponent(record2.filename ? basename(record2.filename) : null);
41535
+ if (stored && !GENERIC_FILE_NAMES.has(stored.toLowerCase())) return stored;
41536
+ const own = basename(source);
41537
+ if (!GENERIC_FILE_NAMES.has(own.toLowerCase())) return own;
41538
+ return `${record2.identifier}${ext}`;
41539
+ }
41540
+ function collisionName(name, attempt) {
41541
+ if (attempt <= 1) return name;
41542
+ const ext = extname(name);
41543
+ const stem = ext && ext !== name ? name.slice(0, -ext.length) : name;
41544
+ return `${stem}-${attempt}${ext}`;
41545
+ }
41546
+ function prepareExportDir(exportDir, containerDir = NOTES_CONTAINER_DIR) {
41547
+ const abs = assertSafeSavePath(exportDir);
41548
+ if (isInsideNotesContainer(abs, containerDir)) {
41549
+ throw new Error(`Refusing to write inside the Notes data container: "${abs}"`);
41550
+ }
41551
+ mkdirSync2(abs, { recursive: true });
41552
+ if (isInsideNotesContainer(abs, containerDir)) {
41553
+ throw new Error(`Refusing to write inside the Notes data container: "${abs}"`);
41554
+ }
41555
+ return abs;
41556
+ }
41557
+ function exportOneAttachment(record2, dir, source) {
41558
+ const base = {
41559
+ pk: record2.pk,
41560
+ identifier: record2.identifier,
41561
+ uti: record2.uti,
41562
+ kind: record2.kind,
41563
+ parentIdentifier: record2.parentIdentifier,
41564
+ exportedTo: null,
41565
+ exportedKind: null
41566
+ };
41567
+ if (!source) return base;
41568
+ const name = exportFileName(record2, source.path, source.kind);
41569
+ for (let attempt = 1; attempt <= MAX_COLLISION_SUFFIX; attempt++) {
41570
+ const dest = join6(dir, collisionName(name, attempt));
41571
+ try {
41572
+ assertSafeSavePath(dest);
41573
+ copyFileExclusive(source.path, dest);
41574
+ return { ...base, exportedTo: dest, exportedKind: source.kind };
41575
+ } catch (error2) {
41576
+ if (error2.code === "EEXIST") continue;
41577
+ return { ...base, error: error2 instanceof Error ? error2.message : String(error2) };
41578
+ }
41579
+ }
41580
+ return { ...base, error: "Too many name collisions in the export directory" };
41581
+ }
41582
+ function exportSource(record2) {
41583
+ if (record2.assetPaths[0]) return { path: record2.assetPaths[0], kind: "asset" };
41584
+ if (record2.previewPath) return { path: record2.previewPath, kind: "preview" };
41585
+ return null;
41586
+ }
41587
+ function exportAttachmentAssets(assets, exportDir, options = {}) {
41588
+ const dir = prepareExportDir(exportDir, options.containerDir);
41589
+ if (options.firstImageOnly) {
41590
+ const first2 = selectFirstImage(assets);
41591
+ if (!first2) return { exportDir: dir, results: [], firstImage: null };
41592
+ const record2 = assets.attachments.find((a) => a.pk === first2.pk);
41593
+ return {
41594
+ exportDir: dir,
41595
+ results: [exportOneAttachment(record2, dir, exportSource(record2))],
41596
+ firstImage: first2
41597
+ };
41598
+ }
41599
+ const results = [];
41600
+ for (const record2 of assets.attachments) {
41601
+ if (record2.parentIdentifier !== null) {
41602
+ const parent = assets.attachments.find(
41603
+ (a) => a.parentIdentifier === null && a.identifier === record2.parentIdentifier
41604
+ );
41605
+ if (parent && exportSource(parent)) continue;
41606
+ } else if (!exportSource(record2) && assets.attachments.some((c) => c.parentIdentifier === record2.identifier)) {
41607
+ continue;
41608
+ }
41609
+ results.push(exportOneAttachment(record2, dir, exportSource(record2)));
41610
+ }
41611
+ return { exportDir: dir, results };
41612
+ }
41613
+
41036
41614
  // src/services/appleNotesManager.ts
41037
41615
  var import_turndown = __toESM(require_turndown_cjs(), 1);
41038
41616
  import { existsSync as existsSync4 } from "fs";
41039
- import { homedir as homedir6 } from "os";
41040
- import { join as join6 } from "path";
41617
+ import { homedir as homedir7 } from "os";
41618
+ import { join as join7 } from "path";
41041
41619
  var FIELD_SEP = "";
41042
41620
  var RECORD_SEP = "";
41043
41621
  var AS_FIELD_SEP = "(character id 31)";
@@ -41265,7 +41843,7 @@ function getNoteLinkFromDB(coreDataId2) {
41265
41843
  const match = coreDataId2.match(/\/p(\d+)$/);
41266
41844
  if (!match) return null;
41267
41845
  const pk = parseInt(match[1], 10);
41268
- const dbPath2 = join6(homedir6(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
41846
+ const dbPath2 = join7(homedir7(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
41269
41847
  if (!existsSync4(dbPath2)) return null;
41270
41848
  try {
41271
41849
  const { DatabaseSync } = __require("node:sqlite");
@@ -43187,6 +43765,37 @@ var AppleNotesManager = class {
43187
43765
  cleanupTempDir(dir);
43188
43766
  }
43189
43767
  }
43768
+ /**
43769
+ * Reads one note's attachments with their on-disk asset and preview paths,
43770
+ * in body order, from the NoteStore database and the Notes group container
43771
+ * (both read-only). Requires Full Disk Access.
43772
+ *
43773
+ * @param noteId - canonical CoreData note id
43774
+ * @throws AttachmentStoreError (`no_fda`, `invalid_id`, `not_found`, `query_error`)
43775
+ */
43776
+ getAttachmentAssetsById(noteId3) {
43777
+ const { rows, bodyOrder } = readNoteAttachmentRows(noteId3);
43778
+ return assembleAttachmentAssets(rows, bodyOrder);
43779
+ }
43780
+ /**
43781
+ * The note's lead visual: the first image in body order (even when its asset
43782
+ * has not downloaded), else the first scan or drawing, else null.
43783
+ */
43784
+ getFirstImageById(noteId3) {
43785
+ return selectFirstImage(this.getAttachmentAssetsById(noteId3));
43786
+ }
43787
+ /**
43788
+ * Copies a note's attachment files into a directory. Each attachment exports
43789
+ * its real asset; its rendered preview only when no asset exists. Existing
43790
+ * files are never replaced: name collisions get `-2`, `-3`, ... suffixes.
43791
+ * The directory must satisfy the same allowlist as save-attachment and may
43792
+ * not be inside the Notes group container.
43793
+ */
43794
+ exportAttachmentsById(noteId3, exportDir, firstImageOnly = false) {
43795
+ return exportAttachmentAssets(this.getAttachmentAssetsById(noteId3), exportDir, {
43796
+ firstImageOnly
43797
+ });
43798
+ }
43190
43799
  // ===========================================================================
43191
43800
  // Batch Operations
43192
43801
  // ===========================================================================
@@ -43600,7 +44209,7 @@ var AppleNotesManager = class {
43600
44209
  };
43601
44210
 
43602
44211
  // src/utils/syncDetection.ts
43603
- import { execFileSync as execFileSync6 } from "child_process";
44212
+ import { execFileSync as execFileSync7 } from "child_process";
43604
44213
  import * as fs3 from "fs";
43605
44214
  import * as path3 from "path";
43606
44215
  import * as os3 from "os";
@@ -43645,7 +44254,7 @@ function getSyncStatus(useCache = true) {
43645
44254
  WHERE object.ZCLOUDSTATE = state.Z_PK
43646
44255
  );
43647
44256
  `;
43648
- const result = execFileSync6(
44257
+ const result = execFileSync7(
43649
44258
  "sqlite3",
43650
44259
  ["-readonly", NOTES_DB_PATH4, query.replace(/\n/g, " ")],
43651
44260
  {
@@ -43704,7 +44313,7 @@ function withSyncAwarenessSync(operation, fn) {
43704
44313
  }
43705
44314
 
43706
44315
  // src/utils/noteMetadata.ts
43707
- import { execFileSync as execFileSync7 } from "child_process";
44316
+ import { execFileSync as execFileSync8 } from "child_process";
43708
44317
  import * as fs4 from "fs";
43709
44318
  import * as path4 from "path";
43710
44319
  import * as os4 from "os";
@@ -43724,15 +44333,15 @@ var COLUMN_MAP = [
43724
44333
  { key: "widgetSnippet", column: "ZWIDGETSNIPPET", type: "text" },
43725
44334
  { key: "smartFolderQuery", column: "ZSMARTFOLDERQUERYJSON", type: "text" }
43726
44335
  ];
43727
- function runSqlite3(query) {
43728
- return execFileSync7("sqlite3", ["-readonly", NOTES_DB_PATH5, query], {
44336
+ function runSqlite4(query) {
44337
+ return execFileSync8("sqlite3", ["-readonly", NOTES_DB_PATH5, query], {
43729
44338
  encoding: "utf8",
43730
44339
  timeout: 5e3,
43731
44340
  stdio: ["pipe", "pipe", "pipe"]
43732
44341
  }).trim();
43733
44342
  }
43734
44343
  function presentColumns() {
43735
- const out = runSqlite3("PRAGMA table_info(ZICCLOUDSYNCINGOBJECT);");
44344
+ const out = runSqlite4("PRAGMA table_info(ZICCLOUDSYNCINGOBJECT);");
43736
44345
  const cols = /* @__PURE__ */ new Set();
43737
44346
  for (const line of out.split("\n")) {
43738
44347
  const name = line.split("|")[1];
@@ -43760,7 +44369,7 @@ function getNoteMetadata(noteId3) {
43760
44369
  return { metadata: {} };
43761
44370
  }
43762
44371
  const pairs = selected.map((c) => `'${c.key}', ${c.column}`).join(", ");
43763
- const row = runSqlite3(
44372
+ const row = runSqlite4(
43764
44373
  `SELECT json_object(${pairs}) FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ${pk};`
43765
44374
  );
43766
44375
  if (!row) {
@@ -43793,7 +44402,7 @@ function getNoteMetadata(noteId3) {
43793
44402
  }
43794
44403
 
43795
44404
  // src/utils/noteIdentifiers.ts
43796
- import { execFileSync as execFileSync8 } from "child_process";
44405
+ import { execFileSync as execFileSync9 } from "child_process";
43797
44406
  import * as fs5 from "fs";
43798
44407
  import * as os5 from "os";
43799
44408
  import * as path5 from "path";
@@ -43875,7 +44484,7 @@ function runJsonQuery(sql, dbPath2) {
43875
44484
  if (!fs5.existsSync(dbPath2)) throw new IdentifierResolutionError("no_fda", NO_FDA_MESSAGE);
43876
44485
  let out;
43877
44486
  try {
43878
- out = execFileSync8("sqlite3", ["-readonly", dbPath2, sql], {
44487
+ out = execFileSync9("sqlite3", ["-readonly", dbPath2, sql], {
43879
44488
  encoding: "utf8",
43880
44489
  timeout: 5e3,
43881
44490
  stdio: ["pipe", "pipe", "pipe"]
@@ -44565,11 +45174,11 @@ function positiveTextTerms(node, negated = false) {
44565
45174
  }
44566
45175
 
44567
45176
  // src/utils/noteQueryStore.ts
44568
- import { execFileSync as execFileSync9 } from "child_process";
45177
+ import { execFileSync as execFileSync10 } from "child_process";
44569
45178
  import * as fs6 from "fs";
44570
45179
  import * as os6 from "os";
44571
45180
  import * as path6 from "path";
44572
- import { gunzipSync as gunzipSync5 } from "zlib";
45181
+ import { gunzipSync as gunzipSync6 } from "zlib";
44573
45182
  var NOTES_DB_PATH7 = path6.join(
44574
45183
  os6.homedir(),
44575
45184
  "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
@@ -44705,8 +45314,8 @@ function buildScanSql(available, options) {
44705
45314
  "COMMIT;"
44706
45315
  ].join(" ");
44707
45316
  }
44708
- function runSqlite4(dbPath2, query) {
44709
- return execFileSync9("sqlite3", ["-readonly", dbPath2, query], {
45317
+ function runSqlite5(dbPath2, query) {
45318
+ return execFileSync10("sqlite3", ["-readonly", dbPath2, query], {
44710
45319
  encoding: "utf8",
44711
45320
  timeout: 3e4,
44712
45321
  maxBuffer: 512 * 1024 * 1024,
@@ -44715,7 +45324,7 @@ function runSqlite4(dbPath2, query) {
44715
45324
  }
44716
45325
  function presentColumns2(dbPath2) {
44717
45326
  const cols = /* @__PURE__ */ new Set();
44718
- for (const line of runSqlite4(dbPath2, "PRAGMA table_info(ZICCLOUDSYNCINGOBJECT);").split("\n")) {
45327
+ for (const line of runSqlite5(dbPath2, "PRAGMA table_info(ZICCLOUDSYNCINGOBJECT);").split("\n")) {
44719
45328
  const name = line.split("|")[1];
44720
45329
  if (name) cols.add(name);
44721
45330
  }
@@ -44725,13 +45334,13 @@ var escapeSegment = escapeFolderName;
44725
45334
  function resolveFolders(rows) {
44726
45335
  const byPk = new Map(rows.map((row) => [row.pk, row]));
44727
45336
  const resolved = /* @__PURE__ */ new Map();
44728
- const resolve3 = (pk, seen) => {
45337
+ const resolve4 = (pk, seen) => {
44729
45338
  const cached2 = resolved.get(pk);
44730
45339
  if (cached2) return cached2;
44731
45340
  const row = byPk.get(pk);
44732
45341
  if (!row || seen.has(pk)) return void 0;
44733
45342
  seen.add(pk);
44734
- const parent = row.parent !== null ? resolve3(row.parent, seen) : void 0;
45343
+ const parent = row.parent !== null ? resolve4(row.parent, seen) : void 0;
44735
45344
  const name = row.name ?? "";
44736
45345
  const path7 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
44737
45346
  const plainPath = parent ? `${parent.plainPath}/${name}` : name;
@@ -44745,7 +45354,7 @@ function resolveFolders(rows) {
44745
45354
  resolved.set(pk, info);
44746
45355
  return info;
44747
45356
  };
44748
- for (const row of rows) resolve3(row.pk, /* @__PURE__ */ new Set());
45357
+ for (const row of rows) resolve4(row.pk, /* @__PURE__ */ new Set());
44749
45358
  return resolved;
44750
45359
  }
44751
45360
  var coreDataMs = (seconds) => seconds === null || !Number.isFinite(seconds) ? void 0 : CORE_DATA_EPOCH_MS + seconds * 1e3;
@@ -44775,7 +45384,7 @@ function queryNotes(expression, options = {}) {
44775
45384
  let output;
44776
45385
  try {
44777
45386
  const available = presentColumns2(dbPath2);
44778
- output = runSqlite4(
45387
+ output = runSqlite5(
44779
45388
  dbPath2,
44780
45389
  buildScanSql(available, { scanLimit, includeDeleted, withBodies, withTags })
44781
45390
  );
@@ -44831,7 +45440,7 @@ function queryNotes(expression, options = {}) {
44831
45440
  try {
44832
45441
  decoded = decodeNoteBody(
44833
45442
  new Uint8Array(
44834
- gunzipSync5(Buffer.from(row.data, "hex"), { maxOutputLength: 32 * 1024 * 1024 })
45443
+ gunzipSync6(Buffer.from(row.data, "hex"), { maxOutputLength: 32 * 1024 * 1024 })
44835
45444
  )
44836
45445
  );
44837
45446
  } catch {
@@ -44906,10 +45515,10 @@ function queryNotes(expression, options = {}) {
44906
45515
  import { spawnSync } from "child_process";
44907
45516
 
44908
45517
  // src/services/nativeTags.ts
44909
- import { execFileSync as execFileSync10 } from "node:child_process";
45518
+ import { execFileSync as execFileSync11 } from "node:child_process";
44910
45519
  import { mkdtempSync as mkdtempSync2, writeFileSync, rmSync as rmSync2 } from "node:fs";
44911
45520
  import { tmpdir as tmpdir2 } from "node:os";
44912
- import { join as join11 } from "node:path";
45521
+ import { join as join12 } from "node:path";
44913
45522
 
44914
45523
  // src/services/shortcutConsent.ts
44915
45524
  function shortcutConsentHint(shortcut) {
@@ -44985,7 +45594,7 @@ function addNativeTags(request, deps) {
44985
45594
  };
44986
45595
  }
44987
45596
  function listInstalledShortcuts() {
44988
- return execFileSync10("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
45597
+ return execFileSync11("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
44989
45598
  encoding: "utf8",
44990
45599
  timeout: 15e3,
44991
45600
  maxBuffer: 1024 * 1024,
@@ -45011,11 +45620,11 @@ function runNativeTagsShortcut(input) {
45011
45620
  const status = nativeTagsStatus();
45012
45621
  if (!status.installed)
45013
45622
  throw new Error(`Import the supplied ${status.shortcut}.shortcut in Shortcuts first`);
45014
- const directory = mkdtempSync2(join11(tmpdir2(), "apple-notes-native-tags-"));
45623
+ const directory = mkdtempSync2(join12(tmpdir2(), "apple-notes-native-tags-"));
45015
45624
  try {
45016
- const path7 = join11(directory, "request.json");
45625
+ const path7 = join12(directory, "request.json");
45017
45626
  writeFileSync(path7, JSON.stringify(input), { mode: 384 });
45018
- execFileSync10("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path7], {
45627
+ execFileSync11("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path7], {
45019
45628
  encoding: "utf8",
45020
45629
  timeout: 6e4,
45021
45630
  maxBuffer: 1024 * 1024,
@@ -45034,10 +45643,10 @@ function runNativeTagsShortcut(input) {
45034
45643
  }
45035
45644
 
45036
45645
  // src/services/backgroundNotes.ts
45037
- import { execFileSync as execFileSync11 } from "node:child_process";
45646
+ import { execFileSync as execFileSync12 } from "node:child_process";
45038
45647
  import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync2, rmSync as rmSync3 } from "node:fs";
45039
45648
  import { tmpdir as tmpdir3 } from "node:os";
45040
- import { join as join12 } from "node:path";
45649
+ import { join as join13 } from "node:path";
45041
45650
 
45042
45651
  // src/utils/appendMarkdown.ts
45043
45652
  var escape2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -45056,40 +45665,152 @@ function countTaskItems(markdown) {
45056
45665
  return item !== null && TASK_ITEM.test(item[1]);
45057
45666
  }).length;
45058
45667
  }
45059
- function appendMarkdownHtml(markdown, options = {}) {
45060
- if (markdown.includes("\uE000") || markdown.includes("\uE001"))
45668
+ var FENCE = "```";
45669
+ var CHECKLIST_ITEM = /^- \[( |x)\] (.+)$/;
45670
+ function withoutMarkdownCode(markdown) {
45671
+ let inCode = false;
45672
+ return markdown.replace(/\r\n/g, "\n").split("\n").map((line) => {
45673
+ if (line === FENCE) {
45674
+ inCode = !inCode;
45675
+ return "";
45676
+ }
45677
+ return inCode ? "" : line.replace(/`[^`\n]+`/g, "x");
45678
+ }).join("\n");
45679
+ }
45680
+ function usesMarkdownBlocks(markdown) {
45681
+ return /`/.test(markdown) || /^>/m.test(markdown) || /^---[ \t]*$/m.test(markdown) || /^- \[[ x]\] /m.test(markdown);
45682
+ }
45683
+ function renderMarkdown(markdown, options = {}) {
45684
+ const blocks = options.blocks === true;
45685
+ if (blocks ? /[-]/u.test(markdown) : /[]/u.test(markdown))
45061
45686
  throw new Error("Unsupported reserved characters");
45062
45687
  if (Array.from(markdown).some((c) => c.charCodeAt(0) < 32 && !["\n", "\r", " "].includes(c)))
45063
45688
  throw new Error("Unsupported control characters");
45064
- if (/[`~|]|^#{4,}\s|^\s*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(markdown))
45689
+ const checked = blocks ? withoutMarkdownCode(markdown) : markdown;
45690
+ if (blocks ? /[`~|]|^#{4,}\s|^[ \t]+>|^>[ \t]*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(checked) : /[`~|]|^#{4,}\s|^\s*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(checked))
45065
45691
  throw new Error(
45066
- "Markdown append supports paragraphs, headings, flat lists, emphasis and inline links; use semantic HTML for other formatting"
45692
+ blocks ? "Markdown import supports paragraphs, headings, flat lists, checklist items, block quotes, fenced code, dividers, emphasis, inline code and inline links; use semantic HTML for other formatting" : "Markdown append supports paragraphs, headings, flat lists, emphasis and inline links; use semantic HTML for other formatting"
45067
45693
  );
45694
+ const expect = {
45695
+ quotes: [],
45696
+ code: [],
45697
+ checklist: [],
45698
+ dividers: 0,
45699
+ highlights: []
45700
+ };
45068
45701
  const inline = (text) => {
45702
+ const spans = [];
45703
+ let value = text;
45704
+ if (blocks)
45705
+ value = value.replace(/`([^`\n]+)`/g, (_s, code2) => {
45706
+ if (/^\s|\s$/.test(code2)) throw new Error("Inline code cannot start or end with a space");
45707
+ expect.highlights.push(code2);
45708
+ spans.push(`<code>${escape2(code2)}</code>`);
45709
+ return `\uE002${spans.length - 1}\uE003`;
45710
+ });
45069
45711
  const links = [];
45070
- let value = text.replace(/\[([^\]\n]+)\]\(([^()\s]+)\)/g, (_s, label, url) => {
45712
+ value = value.replace(/\[([^\]\n]+)\]\(([^()\s]+)\)/g, (_s, label, url) => {
45071
45713
  if (!/^(https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url))
45072
45714
  throw new Error("Unsupported Markdown link URL");
45715
+ if (/[]/u.test(label)) throw new Error("Inline code inside a link is unsupported");
45073
45716
  links.push(`<a href="${escape2(url)}">${escape2(label)}</a>`);
45074
45717
  return `\uE000${links.length - 1}\uE001`;
45075
45718
  });
45076
45719
  value = escape2(value).replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/\*([^*\n]+)\*/g, "<i>$1</i>");
45077
45720
  if (value.includes("[") || value.includes("]") || value.includes("*"))
45078
45721
  throw new Error("Unsupported or unbalanced Markdown inline syntax");
45079
- return value.replace(/\uE000(\d+)\uE001/g, (_s, i) => links[Number(i)]);
45722
+ return value.replace(/(\d+)/g, (_s, i) => links[Number(i)]).replace(/(\d+)/g, (_s, i) => spans[Number(i)]);
45080
45723
  };
45081
- let html = "", list;
45724
+ const withoutTags = (html2) => {
45725
+ let text = "", inTag = false;
45726
+ for (const ch of html2) {
45727
+ if (ch === "<") inTag = true;
45728
+ else if (ch === ">" && inTag) inTag = false;
45729
+ else if (!inTag) text += ch;
45730
+ }
45731
+ return text;
45732
+ };
45733
+ const visible = (html2) => withoutTags(html2).replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
45734
+ let html = "", list, quote = false, code, previousBlank = true;
45082
45735
  const close = () => {
45083
45736
  if (list) {
45084
- html += `</${list}>`;
45737
+ html += list === "checklist" ? "</ul>" : `</${list}>`;
45085
45738
  list = void 0;
45086
45739
  }
45740
+ if (quote) {
45741
+ html += "</blockquote>";
45742
+ quote = false;
45743
+ }
45087
45744
  };
45088
45745
  for (const line of markdown.replace(/\r\n/g, "\n").split("\n")) {
45746
+ if (code) {
45747
+ if (line === FENCE) {
45748
+ if (!code.length) throw new Error("Empty fenced code blocks are unsupported");
45749
+ html += `<pre>${escape2(code.join("\n"))}</pre>`;
45750
+ expect.code.push(code.join("\n"));
45751
+ code = void 0;
45752
+ previousBlank = false;
45753
+ } else code.push(line);
45754
+ continue;
45755
+ }
45756
+ const blank = !line.trim();
45757
+ if (blocks) {
45758
+ if (quote && !blank && !line.startsWith(">"))
45759
+ throw new Error("End a block quote with a blank line before other text");
45760
+ if (line.startsWith(FENCE)) {
45761
+ if (line !== FENCE)
45762
+ throw new Error("Fenced code must open and close with a bare ``` line (no language)");
45763
+ close();
45764
+ code = [];
45765
+ continue;
45766
+ }
45767
+ if (line.startsWith(">")) {
45768
+ const text = line.replace(/^> ?/, "");
45769
+ if (!text.trim()) throw new Error("Empty block quote lines are unsupported");
45770
+ if (/^(?:#|[-+*][ \t]|\d+[.)][ \t]|>|```)/.test(text))
45771
+ throw new Error("Block quotes support text and inline formatting only");
45772
+ if (!quote) {
45773
+ close();
45774
+ html += "<blockquote>";
45775
+ quote = true;
45776
+ expect.quotes.push("");
45777
+ }
45778
+ const rendered = inline(text);
45779
+ html += `<div>${rendered}</div>`;
45780
+ expect.quotes[expect.quotes.length - 1] += ` ${visible(rendered)}`;
45781
+ previousBlank = false;
45782
+ continue;
45783
+ }
45784
+ if (/^---[ \t]*$/.test(line)) {
45785
+ if (!previousBlank) throw new Error("Put a blank line before a --- divider");
45786
+ close();
45787
+ html += "<hr>";
45788
+ expect.dividers++;
45789
+ previousBlank = false;
45790
+ continue;
45791
+ }
45792
+ const task = CHECKLIST_ITEM.exec(line);
45793
+ if (task) {
45794
+ if (list && list !== "checklist")
45795
+ throw new Error("Separate checklist items from other list items with a blank line");
45796
+ if (list !== "checklist") {
45797
+ close();
45798
+ list = "checklist";
45799
+ html += '<ul class="checklist">';
45800
+ }
45801
+ const rendered = inline(task[2]);
45802
+ html += `<li>${rendered}</li>`;
45803
+ expect.checklist.push({ text: visible(rendered).trim(), done: task[1] === "x" });
45804
+ previousBlank = false;
45805
+ continue;
45806
+ }
45807
+ }
45089
45808
  if (/^\s{2,}\S/.test(line)) throw new Error("Nested lists and indented code are unsupported");
45090
45809
  const heading = /^(#{1,3})\s+(.+)$/.exec(line), item = /^(?:([-+*])|\d+\.)\s+(.+)$/.exec(line);
45091
45810
  if (item) {
45092
45811
  const kind = item[1] ? "ul" : "ol";
45812
+ if (list === "checklist")
45813
+ throw new Error("Separate checklist items from other list items with a blank line");
45093
45814
  if (list !== kind) {
45094
45815
  close();
45095
45816
  list = kind;
@@ -45097,13 +45818,20 @@ function appendMarkdownHtml(markdown, options = {}) {
45097
45818
  }
45098
45819
  const task = options.taskGlyphs && item[1] ? TASK_ITEM.exec(item[2]) : null;
45099
45820
  html += task ? `<li>${task[1] === " " ? TASK_GLYPHS.open : TASK_GLYPHS.done} ${inline(task[2])}</li>` : `<li>${inline(item[2])}</li>`;
45821
+ previousBlank = false;
45100
45822
  continue;
45101
45823
  }
45102
45824
  close();
45103
- html += heading ? `<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>` : line.trim() ? `<div>${inline(line)}</div>` : "<div><br></div>";
45825
+ html += heading ? `<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>` : !blank ? `<div>${inline(line)}</div>` : "<div><br></div>";
45826
+ previousBlank = blank;
45104
45827
  }
45828
+ if (code) throw new Error("Unclosed fenced code block");
45105
45829
  close();
45106
- return html;
45830
+ expect.quotes = expect.quotes.map((q) => q.trim());
45831
+ return { html, expect };
45832
+ }
45833
+ function appendMarkdownHtml(markdown, options = {}) {
45834
+ return renderMarkdown(markdown, { taskGlyphs: options.taskGlyphs }).html;
45107
45835
  }
45108
45836
 
45109
45837
  // src/utils/noteRevision.ts
@@ -45197,7 +45925,7 @@ var UNMODELED_MARKDOWN = [
45197
45925
  [/^#{1,3}[ \t].*[ \t]#+[ \t]*$/m, "closing # sequences"],
45198
45926
  [/\[[^\]\n]*[*_][^\]\n]*\]\(/m, "formatting inside link labels"]
45199
45927
  ];
45200
- function validateAppendContent(content, format) {
45928
+ function validateAppendContent(content, format, options = {}) {
45201
45929
  if (!content || content.length > 1024 * 1024 || content.includes("\0"))
45202
45930
  throw new Error("Invalid append content (limit 1 MiB)");
45203
45931
  if (format === "html") {
@@ -45227,11 +45955,12 @@ function validateAppendContent(content, format) {
45227
45955
  if (/<!--|<!|<\?|<[^>]*$/u.test(content)) throw new Error("Unsupported HTML markup");
45228
45956
  }
45229
45957
  if (format === "markdown") {
45230
- if (/!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
45958
+ const checked = options.blocks ? withoutMarkdownCode(content).replace(/^---[ \t]*$/gm, "") : content;
45959
+ if (/!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(checked))
45231
45960
  throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
45232
- const withoutLinkDestinations = content.replace(/(\[[^\]\n]+\])\([^()\s]+\)/g, "$1()");
45961
+ const withoutLinkDestinations = checked.replace(/(\[[^\]\n]+\])\([^()\s]+\)/g, "$1()");
45233
45962
  for (const [pattern, name] of UNMODELED_MARKDOWN)
45234
- if (pattern.test(name === UNDERSCORES_OUTSIDE_A_WORD ? withoutLinkDestinations : content))
45963
+ if (pattern.test(name === UNDERSCORES_OUTSIDE_A_WORD ? withoutLinkDestinations : checked))
45235
45964
  throw new Error(
45236
45965
  `Markdown cannot use ${name}; Notes would change that text, so the result could not be verified`
45237
45966
  );
@@ -45242,9 +45971,9 @@ function runBackgroundShortcut(input, status = backgroundStatus()) {
45242
45971
  throw new Error(
45243
45972
  `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
45244
45973
  );
45245
- const directory = mkdtempSync3(join12(tmpdir3(), "apple-notes-background-"));
45974
+ const directory = mkdtempSync3(join13(tmpdir3(), "apple-notes-background-"));
45246
45975
  try {
45247
- const file = join12(directory, "request.json");
45976
+ const file = join13(directory, "request.json");
45248
45977
  writeFileSync2(
45249
45978
  file,
45250
45979
  JSON.stringify({
@@ -45264,7 +45993,7 @@ function runBackgroundShortcut(input, status = backgroundStatus()) {
45264
45993
  { mode: 384 }
45265
45994
  );
45266
45995
  try {
45267
- execFileSync11("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
45996
+ execFileSync12("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
45268
45997
  encoding: "utf8",
45269
45998
  timeout: callTimeoutMs() ?? 6e4,
45270
45999
  maxBuffer: 1024 * 1024,
@@ -45455,10 +46184,42 @@ var literalMarkdown = (text) => text.replace(/[!-/:-@[-`{-~]/g, "\\$&");
45455
46184
  function headingLevels(html) {
45456
46185
  return [...html.matchAll(/<h([1-3])\b[^>]*>([\s\S]*?)<\/h\1>/gi)].filter((match) => comparableVisibleText(match[2])).map((match) => Number(match[1]));
45457
46186
  }
46187
+ var DIVIDER_UTI = "com.apple.notes.inlinetextattachment.dividerline";
46188
+ function assertMarkdownBlocks(rich, expected) {
46189
+ const tidy = (s) => s.replace(/[\s]+/gu, " ").trim();
46190
+ const runs = rich.styleRuns || [];
46191
+ const styledText = (matches) => {
46192
+ let out = "", previous = false;
46193
+ for (const run of runs) {
46194
+ const hit = matches(run);
46195
+ if (hit) out += (previous ? "" : " ") + rich.text.slice(run.start, run.start + run.length);
46196
+ previous = hit;
46197
+ }
46198
+ return tidy(out);
46199
+ };
46200
+ if (styledText((run) => run.blockQuote === true) !== tidy(expected.quotes.join(" ")))
46201
+ throw new Error("Block quotes not verified");
46202
+ if (styledText((run) => run.paragraphStyle === 4) !== tidy(expected.code.join(" ")))
46203
+ throw new Error("Monospaced code blocks not verified");
46204
+ if (styledText((run) => run.highlight === true) !== tidy(expected.highlights.join(" ")))
46205
+ throw new Error("Inline code highlights not verified");
46206
+ const items = (rich.checklistItems || []).map((item) => ({
46207
+ text: tidy(item.text),
46208
+ done: item.done
46209
+ }));
46210
+ const wanted = expected.checklist.map((item) => ({ text: tidy(item.text), done: item.done }));
46211
+ if (JSON.stringify(items) !== JSON.stringify(wanted))
46212
+ throw new Error("Checklist items or their done state not verified");
46213
+ if ((rich.objects || []).filter((o) => o.type === DIVIDER_UTI).length !== expected.dividers)
46214
+ throw new Error("Dividers not verified");
46215
+ }
45458
46216
  function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
45459
46217
  if (/[\r\n\0]/u.test(request.title)) throw new Error("A Markdown note title must be one line");
45460
- validateAppendContent(request.content, "markdown");
45461
- const bodyHtml = appendMarkdownHtml(request.content);
46218
+ validateAppendContent(request.content, "markdown", { blocks: true });
46219
+ const { html: bodyHtml, expect: blockExpectations } = renderMarkdown(request.content, {
46220
+ blocks: true
46221
+ });
46222
+ const checkBlocks = usesMarkdownBlocks(request.content);
45462
46223
  if (request.folder) buildFolderReference(request.folder);
45463
46224
  const expectedText = `${request.title} ${comparableVisibleText(bodyHtml)}`.replace(/\s+/gu, " ").trim();
45464
46225
  const expectedLevels = [1, ...headingLevels(bodyHtml)].join();
@@ -45511,6 +46272,7 @@ ${request.content}`
45511
46272
  if (headingLevels(note2.html).join() !== expectedLevels)
45512
46273
  throw new Error("Heading styles not verified");
45513
46274
  assertAppendedHtmlLinks(0, note2.rich.links, bodyHtml);
46275
+ if (checkBlocks) assertMarkdownBlocks(note2.rich, blockExpectations);
45514
46276
  return [{ id: noteId3, account: account2, note: note2 }];
45515
46277
  } catch (error2) {
45516
46278
  failures.push(`${noteId3}: ${reason(error2)}`);
@@ -45556,7 +46318,7 @@ ${request.content}`
45556
46318
  }
45557
46319
 
45558
46320
  // src/services/capabilityMatrix.ts
45559
- import { execFileSync as execFileSync12 } from "node:child_process";
46321
+ import { execFileSync as execFileSync13 } from "node:child_process";
45560
46322
  import { release } from "node:os";
45561
46323
  var MACOS_SHORTCUTS_CLI = "12.0";
45562
46324
  var MACOS_MARKDOWN_IMPORT = "26.0";
@@ -45763,7 +46525,7 @@ function evaluateFeatures(env, features = FEATURES) {
45763
46525
  function readMacOSVersion() {
45764
46526
  if (process.platform !== "darwin") return null;
45765
46527
  try {
45766
- const out = execFileSync12("/usr/bin/sw_vers", ["-productVersion"], {
46528
+ const out = execFileSync13("/usr/bin/sw_vers", ["-productVersion"], {
45767
46529
  encoding: "utf8",
45768
46530
  timeout: 3e3,
45769
46531
  stdio: ["ignore", "pipe", "ignore"]
@@ -45917,12 +46679,12 @@ function formatDoctorReport(r) {
45917
46679
 
45918
46680
  // src/services/fileConfig.ts
45919
46681
  import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
45920
- import { join as join13 } from "path";
45921
- import { homedir as homedir11 } from "os";
46682
+ import { join as join14 } from "path";
46683
+ import { homedir as homedir12 } from "os";
45922
46684
  function fileConfigPath(env = process.env) {
45923
46685
  const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
45924
46686
  if (override && override.trim()) return override.trim();
45925
- return join13(homedir11(), "Library", "Application Support", "apple-notes-mcp", "config.json");
46687
+ return join14(homedir12(), "Library", "Application Support", "apple-notes-mcp", "config.json");
45926
46688
  }
45927
46689
  function loadFileConfig(env = process.env, path7 = fileConfigPath(env)) {
45928
46690
  const applied = [];
@@ -46225,11 +46987,11 @@ function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
46225
46987
  }
46226
46988
 
46227
46989
  // src/utils/noteBlocks.ts
46228
- import { execFileSync as execFileSync13 } from "node:child_process";
46990
+ import { execFileSync as execFileSync14 } from "node:child_process";
46229
46991
  import { existsSync as existsSync10 } from "node:fs";
46230
- import { homedir as homedir12 } from "node:os";
46231
- import { join as join14 } from "node:path";
46232
- import { gunzipSync as gunzipSync6 } from "node:zlib";
46992
+ import { homedir as homedir13 } from "node:os";
46993
+ import { join as join15 } from "node:path";
46994
+ import { gunzipSync as gunzipSync7 } from "node:zlib";
46233
46995
  var NoteBlocksError = class extends Error {
46234
46996
  code;
46235
46997
  constructor(code, message) {
@@ -46517,7 +47279,7 @@ function summarize(blocks, attachments) {
46517
47279
  function decodeCompressedNoteBlocks(compressed) {
46518
47280
  let data;
46519
47281
  try {
46520
- data = gunzipSync6(compressed, { maxOutputLength: 32 * 1024 * 1024 });
47282
+ data = gunzipSync7(compressed, { maxOutputLength: 32 * 1024 * 1024 });
46521
47283
  } catch (error2) {
46522
47284
  throw new NoteBlocksError(
46523
47285
  "decompress-failed",
@@ -46571,8 +47333,8 @@ function pageNoteBlocks(doc, { offset = 0, limit = 500, maxBytes = blocksMaxResp
46571
47333
  undecodedFields: doc.undecodedFields
46572
47334
  };
46573
47335
  }
46574
- var NOTES_DB_PATH8 = join14(
46575
- homedir12(),
47336
+ var NOTES_DB_PATH8 = join15(
47337
+ homedir13(),
46576
47338
  "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
46577
47339
  );
46578
47340
  function readNoteBlocks(id2, { dbPath: dbPath2 = NOTES_DB_PATH8 } = {}) {
@@ -46587,7 +47349,7 @@ function readNoteBlocks(id2, { dbPath: dbPath2 = NOTES_DB_PATH8 } = {}) {
46587
47349
  const sql = "SELECT json_object('exists', (SELECT count(*) FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = @pk AND Z_ENT = (SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICNote')), 'data', (SELECT hex(ZDATA) FROM ZICNOTEDATA WHERE ZNOTE = @pk), 'encrypted', (SELECT ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL FROM ZICNOTEDATA WHERE ZNOTE = @pk));";
46588
47350
  let output;
46589
47351
  try {
46590
- output = execFileSync13(
47352
+ output = execFileSync14(
46591
47353
  "/usr/bin/sqlite3",
46592
47354
  ["-readonly", "-cmd", ".parameter init", "-cmd", `.parameter set @pk ${pk}`, dbPath2, sql],
46593
47355
  {
@@ -46738,17 +47500,17 @@ function insertLink(request, deps) {
46738
47500
  // src/tools/directOperations.ts
46739
47501
  import { createHash as createHash2 } from "node:crypto";
46740
47502
  import {
46741
- closeSync as closeSync2,
46742
- constants as constants2,
46743
- fstatSync as fstatSync2,
47503
+ closeSync as closeSync3,
47504
+ constants as constants3,
47505
+ fstatSync as fstatSync3,
46744
47506
  mkdtempSync as mkdtempSync4,
46745
- openSync as openSync2,
47507
+ openSync as openSync3,
46746
47508
  readFileSync as readFileSync3,
46747
47509
  rmSync as rmSync4,
46748
47510
  writeFileSync as writeFileSync3
46749
47511
  } from "node:fs";
46750
47512
  import { tmpdir as tmpdir4 } from "node:os";
46751
- import { basename, extname, isAbsolute as isAbsolute2, join as join15 } from "node:path";
47513
+ import { basename as basename2, extname as extname2, isAbsolute as isAbsolute3, join as join16 } from "node:path";
46752
47514
  var noteId = exactIdInput(
46753
47515
  "ICNote",
46754
47516
  /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
@@ -46792,10 +47554,10 @@ function assertExistingContentPreserved(before, after) {
46792
47554
  }
46793
47555
  }
46794
47556
  function localAttachment(path7) {
46795
- if (!isAbsolute2(path7)) throw new Error("An absolute local file path is required");
46796
- const descriptor = openSync2(path7, constants2.O_RDONLY | constants2.O_NOFOLLOW);
47557
+ if (!isAbsolute3(path7)) throw new Error("An absolute local file path is required");
47558
+ const descriptor = openSync3(path7, constants3.O_RDONLY | constants3.O_NOFOLLOW);
46797
47559
  try {
46798
- const stat = fstatSync2(descriptor);
47560
+ const stat = fstatSync3(descriptor);
46799
47561
  if (!stat.isFile() || stat.size === 0 || stat.size > 64 * 1024 * 1024)
46800
47562
  throw new Error("Attachment must be a nonempty regular file of at most 64 MiB");
46801
47563
  const bytes = readFileSync3(descriptor);
@@ -46803,7 +47565,7 @@ function localAttachment(path7) {
46803
47565
  throw new Error("Attachment changed while it was being read; try again");
46804
47566
  return bytes;
46805
47567
  } finally {
46806
- closeSync2(descriptor);
47568
+ closeSync3(descriptor);
46807
47569
  }
46808
47570
  }
46809
47571
  function registerDirectOperations(server2, manager) {
@@ -46916,7 +47678,7 @@ function registerDirectOperations(server2, manager) {
46916
47678
  );
46917
47679
  }
46918
47680
  function attachmentName(path7, filename) {
46919
- const source = basename(path7);
47681
+ const source = basename2(path7);
46920
47682
  if (filename === void 0) return source;
46921
47683
  if (Buffer.byteLength(filename, "utf8") > 255)
46922
47684
  throw new Error("filename must be at most 255 bytes");
@@ -46924,9 +47686,9 @@ function attachmentName(path7, filename) {
46924
47686
  throw new Error(
46925
47687
  "filename must be one path component with no slash, colon, backslash, control character, leading dot, or surrounding spaces"
46926
47688
  );
46927
- if (extname(filename).toLowerCase() !== extname(source).toLowerCase())
47689
+ if (extname2(filename).toLowerCase() !== extname2(source).toLowerCase())
46928
47690
  throw new Error(
46929
- `filename must keep the source file's extension (${extname(source) || "none"})`
47691
+ `filename must keep the source file's extension (${extname2(source) || "none"})`
46930
47692
  );
46931
47693
  return filename;
46932
47694
  }
@@ -46937,8 +47699,8 @@ function attachFile(manager, args) {
46937
47699
  if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
46938
47700
  const bytes = localAttachment(path7);
46939
47701
  const beforeAttachments = manager.listAttachmentsById(id2);
46940
- const directory = mkdtempSync4(join15(tmpdir4(), "notes-attachment-add-"));
46941
- const temporaryFile = join15(directory, name);
47702
+ const directory = mkdtempSync4(join16(tmpdir4(), "notes-attachment-add-"));
47703
+ const temporaryFile = join16(directory, name);
46942
47704
  try {
46943
47705
  writeFileSync3(temporaryFile, bytes, { mode: 384 });
46944
47706
  if (readSnapshot(manager, id2).hash !== before.hash) throw new Error("Note revision changed");
@@ -47084,7 +47846,8 @@ var VERIFIED_BACKGROUND = /* @__PURE__ */ new Set([
47084
47846
  "set-note-pinned",
47085
47847
  "remove-native-tags",
47086
47848
  "replace-native-tag",
47087
- "create-note-markdown"
47849
+ "create-note-markdown",
47850
+ "create-note-markdown-blocks"
47088
47851
  ]);
47089
47852
  var LIVE_VALIDATION_BLOCKERS = {};
47090
47853
  var signingRefusal = "Installed Shortcuts refuses to sign this Notes action (unsupported features); no background fallback is enabled";
@@ -47235,7 +47998,8 @@ function registerNativeOperations(server2, manager) {
47235
47998
  "remove-native-tags",
47236
47999
  "replace-native-tag",
47237
48000
  "insert-note-link",
47238
- "create-note-markdown"
48001
+ "create-note-markdown",
48002
+ "create-note-markdown-blocks"
47239
48003
  ];
47240
48004
  let tagBridgeInstalled = false;
47241
48005
  try {
@@ -47247,7 +48011,7 @@ function registerNativeOperations(server2, manager) {
47247
48011
  markdownBridgeInstalled = markdownNoteStatus().installed;
47248
48012
  } catch {
47249
48013
  }
47250
- const installed = (name) => name === "create-note-markdown" ? markdownBridgeInstalled : bridge.installed;
48014
+ const installed = (name) => name.startsWith("create-note-markdown") ? markdownBridgeInstalled : bridge.installed;
47251
48015
  return {
47252
48016
  bridge,
47253
48017
  nativeTagBridgeInstalled: tagBridgeInstalled,
@@ -47496,7 +48260,7 @@ function registerNativeOperations(server2, manager) {
47496
48260
  import { spawnSync as spawnSync2 } from "node:child_process";
47497
48261
  import { existsSync as existsSync11 } from "node:fs";
47498
48262
  import { release as release2 } from "node:os";
47499
- import { dirname as dirname2, resolve as resolve2 } from "node:path";
48263
+ import { dirname as dirname3, resolve as resolve3 } from "node:path";
47500
48264
  import { fileURLToPath } from "node:url";
47501
48265
  var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
47502
48266
  var MARKDOWN_MIN_DARWIN_MAJOR = 25;
@@ -47519,11 +48283,11 @@ function setupShortcuts(checkOnly, dependencies = {}) {
47519
48283
  const result = spawnSync2("/usr/bin/open", [path7], { encoding: "utf8" });
47520
48284
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
47521
48285
  });
47522
- const baseDirectory = dependencies.baseDirectory || resolve2(dirname2(fileURLToPath(import.meta.url)), "../shortcuts");
48286
+ const baseDirectory = dependencies.baseDirectory || resolve3(dirname3(fileURLToPath(import.meta.url)), "../shortcuts");
47523
48287
  const osRelease = (dependencies.osRelease || release2)();
47524
48288
  const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
47525
48289
  const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
47526
- const path7 = resolve2(baseDirectory, file);
48290
+ const path7 = resolve3(baseDirectory, file);
47527
48291
  let installed = false;
47528
48292
  let identifier;
47529
48293
  let error2;
@@ -47704,7 +48468,7 @@ registerTool(
47704
48468
  inputSchema: {
47705
48469
  title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
47706
48470
  content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).optional().describe(
47707
- 'Note body; required unless contentPath is given (pass exactly one). AppleScript cannot create true Apple Notes checklists \u2014 `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with \u21E7\u2318L.'
48471
+ '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).'
47708
48472
  ),
47709
48473
  contentPath: external_exports.string().min(1).max(MAX.SAVE_PATH).optional().describe(
47710
48474
  `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.`
@@ -47713,7 +48477,7 @@ registerTool(
47713
48477
  "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."
47714
48478
  ),
47715
48479
  markdownRoute: external_exports.enum(["shortcut", "html"]).optional().default("shortcut").describe(
47716
- "How format 'markdown' is imported. 'shortcut' (default) uses the Create Markdown Note Shortcut for real Title/Heading/Subheading styles (iCloud only, no tags; see get-capabilities). 'html' is the fallback when that Shortcut is not installed or the note is outside iCloud: it converts the same bounded Markdown subset to HTML and creates the note through AppleScript in any account, rendering `- [ ]` / `- [x]` task items as ordinary list rows that start with a visible \u2610 / \u2611 character \u2014 not native, checkable checklist items."
48480
+ "How format 'markdown' is imported. 'shortcut' (default) uses the Create Markdown Note Shortcut for real Title/Heading/Subheading styles (iCloud only, no tags; see get-capabilities). 'html' is the fallback when that Shortcut is not installed or the note is outside iCloud: it converts the same bounded Markdown subset to HTML and creates the note through AppleScript in any account, rendering `- [ ]` / `- [x]` task items as ordinary list rows that start with a visible \u2610 / \u2611 character \u2014 not native, checkable checklist items \u2014 refusing block quotes, fenced code and inline code, and keeping a `---` line as literal text."
47717
48481
  ),
47718
48482
  tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
47719
48483
  "Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags in the body for searchable text; this does not create native tag objects. Native tags need the Notes Shortcuts action. Refused with format 'markdown': add tags afterwards with add-native-tags."
@@ -47803,6 +48567,7 @@ registerTool(
47803
48567
  'tags are not supported with format "markdown"; create the note without tags, then add them with add-native-tags using the returned id'
47804
48568
  );
47805
48569
  requireValidated("create-note-markdown");
48570
+ if (usesMarkdownBlocks(content)) requireValidated("create-note-markdown-blocks");
47806
48571
  const result = createMarkdownNote(notesManager, { title, content, folder });
47807
48572
  return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`, {
47808
48573
  ...result,
@@ -48635,10 +49400,10 @@ function guardedAppend({
48635
49400
  return `<div>${escaped || "<br>"}</div>`;
48636
49401
  }).join("");
48637
49402
  };
48638
- const separatorToHtml = (sep2) => {
48639
- if (format === "html") return sep2;
48640
- if (sep2 === "\n\n") return "<div><br></div>";
48641
- const escaped = sep2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
49403
+ const separatorToHtml = (sep3) => {
49404
+ if (format === "html") return sep3;
49405
+ if (sep3 === "\n\n") return "<div><br></div>";
49406
+ const escaped = sep3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
48642
49407
  return `<div>${escaped}</div>`;
48643
49408
  };
48644
49409
  const snapshot = readExactNoteSnapshot(id2);
@@ -49354,21 +50119,57 @@ registerTool(
49354
50119
  return successResponse(lines.join("\n"), { ...stats });
49355
50120
  }, "Error getting notes statistics")
49356
50121
  );
50122
+ function attachmentAssetView(noteId3, record2) {
50123
+ return {
50124
+ attachmentId: attachmentCoreDataId(noteId3, record2.pk),
50125
+ identifier: record2.identifier,
50126
+ uti: record2.uti,
50127
+ kind: record2.kind,
50128
+ parentIdentifier: record2.parentIdentifier,
50129
+ bodyIndex: record2.bodyIndex,
50130
+ assetPaths: record2.assetPaths,
50131
+ previewPath: record2.previewPath,
50132
+ paths: record2.paths
50133
+ };
50134
+ }
50135
+ function firstImageView(noteId3, first2) {
50136
+ if (!first2) return null;
50137
+ const { pk, ...rest } = first2;
50138
+ return { attachmentId: attachmentCoreDataId(noteId3, pk), ...rest };
50139
+ }
49357
50140
  registerTool(
49358
50141
  "list-attachments",
49359
50142
  {
49360
- description: "Use when: listing the attachments of one note, by id (preferred) or title.\nReturns: each attachment's name, content type, and id (use with save-attachment/fetch-attachment).\nDo not use when: you want the attachment bytes (fetch-attachment) or a file on disk (save-attachment).",
50143
+ description: "Use when: listing the attachments of one note, by id (preferred) or title. With includePaths, also where each attachment's files are on disk; with firstImage, only the note's lead visual.\nReturns: each attachment's name, content type, and id (use with save-attachment/fetch-attachment). includePaths adds identifier, uti, kind, bodyIndex, assetPaths (the attachment's own files), previewPath (Notes' largest rendered thumbnail, always an image file), and paths. firstImage returns {firstImage, orderSource}: the first image in body order even when its asset has not downloaded (path null), else the first scan or drawing, else null.\nDo not use when: you want the attachment bytes (fetch-attachment) or files on disk (save-attachment, export-attachments).\nNote: includePaths and firstImage need the note id and Full Disk Access; they read NoteStore and the Notes data folder read-only. Treat the returned paths as local data: copy files out with export-attachments rather than handing raw paths on.",
49361
50144
  inputSchema: {
49362
50145
  id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
49363
50146
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
49364
- account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
50147
+ account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)"),
50148
+ includePaths: external_exports.boolean().optional().describe(
50149
+ "Add on-disk assetPaths, previewPath, and paths to each attachment (requires id and Full Disk Access)"
50150
+ ),
50151
+ firstImage: external_exports.boolean().optional().describe(
50152
+ "Return only the note's lead visual in body order instead of the list (requires id and Full Disk Access)"
50153
+ )
49365
50154
  },
49366
50155
  outputSchema: {
49367
50156
  attachments: external_exports.array(external_exports.object({}).passthrough()).optional(),
49368
- count: external_exports.number().optional()
50157
+ count: external_exports.number().optional(),
50158
+ firstImage: external_exports.object({}).passthrough().nullable().optional(),
50159
+ orderSource: external_exports.enum(["body", "creation"]).optional(),
50160
+ pathsError: external_exports.string().optional()
49369
50161
  }
49370
50162
  },
49371
- withErrorHandling(({ id: id2, title, account }) => {
50163
+ withErrorHandling(({ id: id2, title, account, includePaths = false, firstImage = false }) => {
50164
+ if ((includePaths || firstImage) && !id2) {
50165
+ return errorResponse("includePaths and firstImage require the note 'id'");
50166
+ }
50167
+ if (firstImage && id2) {
50168
+ const assets = notesManager.getAttachmentAssetsById(id2);
50169
+ const first2 = firstImageView(id2, selectFirstImage(assets));
50170
+ const text = first2 ? `Lead visual: ${first2.kind} attachment ${first2.attachmentId} (${first2.path ? "asset on disk" : first2.previewPath ? "preview only" : "not downloaded"}; order from ${assets.orderSource}).` : "This note has no image, scan, or drawing attachment.";
50171
+ return successResponse(text, { firstImage: first2, orderSource: assets.orderSource });
50172
+ }
49372
50173
  if (id2) {
49373
50174
  const note2 = notesManager.getNoteById(id2);
49374
50175
  if (!note2) {
@@ -49382,10 +50183,42 @@ registerTool(
49382
50183
  });
49383
50184
  }
49384
50185
  const attachmentList2 = attachments2.map((a) => ` - ${a.name} (${a.contentType})`).join("\n");
50186
+ if (!includePaths) {
50187
+ return successResponse(
50188
+ `Found ${attachments2.length} attachment(s) in "${note2.title}":
50189
+ ${attachmentList2}`,
50190
+ { attachments: attachments2, count: attachments2.length }
50191
+ );
50192
+ }
50193
+ let assets;
50194
+ try {
50195
+ assets = notesManager.getAttachmentAssetsById(id2);
50196
+ } catch (error2) {
50197
+ const pathsError = error2 instanceof Error ? error2.message : String(error2);
50198
+ return successResponse(
50199
+ `Found ${attachments2.length} attachment(s) in "${note2.title}" (paths unavailable: ${pathsError}):
50200
+ ${attachmentList2}`,
50201
+ { attachments: attachments2, count: attachments2.length, pathsError }
50202
+ );
50203
+ }
50204
+ const byPk = new Map(assets.attachments.map((r) => [r.pk, r]));
50205
+ let onDisk = 0;
50206
+ const enriched = attachments2.map((a) => {
50207
+ const pk = Number(/\/ICAttachment\/p(\d+)$/.exec(a.id)?.[1]);
50208
+ const record2 = byPk.get(pk);
50209
+ if (!record2) return a;
50210
+ if (record2.paths.length > 0) onDisk++;
50211
+ const children = assets.attachments.filter((c) => c.parentIdentifier === record2.identifier).map((c) => attachmentAssetView(id2, c));
50212
+ return {
50213
+ ...a,
50214
+ ...attachmentAssetView(id2, record2),
50215
+ ...children.length ? { children } : {}
50216
+ };
50217
+ });
49385
50218
  return successResponse(
49386
- `Found ${attachments2.length} attachment(s) in "${note2.title}":
50219
+ `Found ${attachments2.length} attachment(s) in "${note2.title}" (${onDisk} with files on disk; order from ${assets.orderSource}):
49387
50220
  ${attachmentList2}`,
49388
- { attachments: attachments2, count: attachments2.length }
50221
+ { attachments: enriched, count: attachments2.length, orderSource: assets.orderSource }
49389
50222
  );
49390
50223
  }
49391
50224
  if (!title) {
@@ -49539,6 +50372,61 @@ registerTool(
49539
50372
  });
49540
50373
  }, "Error saving attachment")
49541
50374
  );
50375
+ registerTool(
50376
+ "export-attachments",
50377
+ {
50378
+ description: `Use when: copying every file attachment of one note (or only its lead visual) into a directory on disk.
50379
+ Returns: per attachment its id, kind, exportedTo, and exportedKind: "asset" for the real file, "preview" when the asset never downloaded and only Notes' rendered thumbnail was available, or null when nothing was on disk.
50380
+ Do not use when: exporting one attachment to an exact path (save-attachment) or reading bytes inline (fetch-attachment).
50381
+ Safety: writes files; exportDir must be absolute and under the home directory, a temp dir, or /Volumes, and not inside the Notes data folder. Existing files are never replaced: name collisions get -2, -3, ... suffixes. Reads NoteStore and the Notes data folder read-only; requires Full Disk Access. Notes.app is not opened.`,
50382
+ inputSchema: {
50383
+ noteId: noteIdInput,
50384
+ exportDir: external_exports.string().min(1, "exportDir is required").max(MAX.SAVE_PATH).describe("Absolute destination directory (created if missing; home, temp, or /Volumes)"),
50385
+ firstImageOnly: external_exports.boolean().optional().describe("Export only the note's lead visual (see list-attachments firstImage)")
50386
+ },
50387
+ outputSchema: {
50388
+ exportDir: external_exports.string().optional(),
50389
+ exported: external_exports.number().optional(),
50390
+ previews: external_exports.number().optional(),
50391
+ skipped: external_exports.number().optional(),
50392
+ failed: external_exports.number().optional(),
50393
+ results: external_exports.array(external_exports.object({}).passthrough()).optional(),
50394
+ firstImage: external_exports.object({}).passthrough().nullable().optional()
50395
+ }
50396
+ },
50397
+ withErrorHandling(({ noteId: noteId3, exportDir, firstImageOnly = false }) => {
50398
+ const r = notesManager.exportAttachmentsById(noteId3, exportDir, firstImageOnly);
50399
+ const results = r.results.map(({ pk, ...rest }) => ({
50400
+ attachmentId: attachmentCoreDataId(noteId3, pk),
50401
+ ...rest
50402
+ }));
50403
+ const exported = results.filter((x) => x.exportedKind !== null).length;
50404
+ const previews = results.filter((x) => x.exportedKind === "preview").length;
50405
+ const failed = results.filter((x) => x.error).length;
50406
+ const skipped = results.length - exported - failed;
50407
+ const structured = {
50408
+ exportDir: r.exportDir,
50409
+ exported,
50410
+ previews,
50411
+ skipped,
50412
+ failed,
50413
+ results
50414
+ };
50415
+ if (firstImageOnly) {
50416
+ structured.firstImage = firstImageView(noteId3, r.firstImage ?? null);
50417
+ if (!r.firstImage) {
50418
+ return successResponse(
50419
+ "This note has no image, scan, or drawing attachment; nothing was exported.",
50420
+ structured
50421
+ );
50422
+ }
50423
+ }
50424
+ return successResponse(
50425
+ `Exported ${exported} file(s) to ${r.exportDir} (${previews} preview-only, ${skipped} with nothing on disk, ${failed} failed).`,
50426
+ structured
50427
+ );
50428
+ }, "Error exporting attachments")
50429
+ );
49542
50430
  registerTool(
49543
50431
  "fetch-attachment",
49544
50432
  {