apple-notes-mcp 2.8.44 → 2.8.45

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 +54 -5
  2. package/build/index.js +584 -116
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1178,11 +1178,12 @@ Creates a new folder, including a whole nested hierarchy in one call.
1178
1178
 
1179
1179
  #### `get-folder-by-id`
1180
1180
 
1181
- Reads one exact folder's current name and parent ID. Use these values with
1182
- `rename-folder`; this avoids relying on ambiguous folder names or paths. The
1183
- `id` may also be the folder's Notes UUID or numeric key, and the result adds
1184
- `identifier`, `parentIdentifier`, and `accountIdentifier` when Full Disk Access
1185
- is granted.
1181
+ Reads one exact folder's current name, parent ID, and account ID (`accountId`,
1182
+ plus `isRoot`, true when the folder sits at the account root). Use these values
1183
+ with `rename-folder` or `delete-folder-by-id`; this avoids relying on ambiguous
1184
+ folder names or paths. The `id` may also be the folder's Notes UUID or numeric
1185
+ key, and the result adds `identifier`, `parentIdentifier`, and
1186
+ `accountIdentifier` when Full Disk Access is granted.
1186
1187
 
1187
1188
  ---
1188
1189
 
@@ -1194,6 +1195,54 @@ and descendants. It refuses stale metadata and a conflicting sibling name.
1194
1195
 
1195
1196
  ---
1196
1197
 
1198
+ #### `delete-folder-by-id`
1199
+
1200
+ Deletes one exact, empty, ordinary folder through a plan-then-apply handshake.
1201
+ Read the folder first with `get-folder-by-id`, call once with `dryRun: true`,
1202
+ then repeat the same guards with `dryRun: false` and the returned `revision`.
1203
+
1204
+ | Parameter | Type | Required | Description |
1205
+ |-----------|------|----------|-------------|
1206
+ | `id` | string | Yes | Exact folder id (`x-coredata://…/ICFolder/pN`), or the folder's Notes UUID or numeric key |
1207
+ | `expectedName` | string | Yes | Current folder name (not a path), matched case-sensitively |
1208
+ | `expectedAccountId` | string | Yes | Owning account id (`x-coredata://…/ICAccount/pN`) |
1209
+ | `expectedParentId` | string | One of these two | Current parent folder id (same forms as `id`) |
1210
+ | `expectedRoot` | `true` | One of these two | The folder sits at the account root |
1211
+ | `dryRun` | boolean | Yes | `true` plans; `false` applies |
1212
+ | `expectedRevision` | string | On apply | The `revision` from the dry run |
1213
+
1214
+ **Example (plan, then apply with the returned revision):**
1215
+ ```json
1216
+ {
1217
+ "id": "x-coredata://ABC/ICFolder/p42",
1218
+ "expectedName": "Old Projects",
1219
+ "expectedAccountId": "x-coredata://ABC/ICAccount/p3",
1220
+ "expectedRoot": true,
1221
+ "dryRun": true
1222
+ }
1223
+ ```
1224
+
1225
+ **Returns:** `status: "planned"` with `wouldDelete`, `identifier`, `name`,
1226
+ `accountId`, `parentId`, `folderType: 0`, zero `childFolderCount` and
1227
+ `noteCount`, and `revision`; on apply, `status: "deleted"`, `committed: true`,
1228
+ `verified: true` (Notes.app no longer resolves the id), and `storeTombstoned`.
1229
+
1230
+ **Safety:** it always refuses Recently Deleted, smart folders, the account's
1231
+ default and other system folders, shared folders (or folders inside a shared
1232
+ folder), and any folder that still holds notes or subfolders. There is no
1233
+ override. It is **not atomic**: the guard is a pre-check followed by an
1234
+ AppleScript delete. The name, parent, account, sharing, and emptiness checks
1235
+ repeat inside the delete script, but the folder type and stable identifier come
1236
+ from the local Notes database just before it. It needs Full Disk Access and
1237
+ fails closed without it.
1238
+
1239
+ **Errors** carry the standard `code`: `revision_conflict` (a `Conflict:` message;
1240
+ read and plan again), `unsupported` (a `Refused:` message), `verification_failed`
1241
+ with `indeterminate: true` (read the folder before any retry), and
1242
+ `full_disk_access_missing`.
1243
+
1244
+ ---
1245
+
1197
1246
  #### `delete-folder`
1198
1247
 
1199
1248
  Deletes a folder.
package/build/index.js CHANGED
@@ -3268,8 +3268,8 @@ var require_utils = __commonJS({
3268
3268
  }
3269
3269
  return ind;
3270
3270
  }
3271
- function removeDotSegments(path9) {
3272
- let input = path9;
3271
+ function removeDotSegments(path10) {
3272
+ let input = path10;
3273
3273
  const output = [];
3274
3274
  let nextSlash = -1;
3275
3275
  let len = 0;
@@ -3674,8 +3674,8 @@ var require_schemes = __commonJS({
3674
3674
  }
3675
3675
  if (wsComponent.resourceName) {
3676
3676
  const queryIndex = wsComponent.resourceName.indexOf("?");
3677
- const path9 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3678
- wsComponent.path = path9 && path9 !== "/" ? path9 : void 0;
3677
+ const path10 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3678
+ wsComponent.path = path10 && path10 !== "/" ? path10 : void 0;
3679
3679
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3680
3680
  wsComponent.resourceName = void 0;
3681
3681
  }
@@ -5049,8 +5049,8 @@ var require_multipleOf = __commonJS({
5049
5049
  const { gen, data, schemaCode, it } = cxt;
5050
5050
  const prec = it.opts.multipleOfPrecision;
5051
5051
  const res = gen.let("res");
5052
- const invalid = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`;
5053
- cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`);
5052
+ const invalid2 = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`;
5053
+ cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid2}))`);
5054
5054
  }
5055
5055
  };
5056
5056
  exports.default = def;
@@ -12238,32 +12238,32 @@ var require_URL = __commonJS({
12238
12238
  else
12239
12239
  return basepath.substring(0, lastslash + 1) + refpath;
12240
12240
  }
12241
- function remove_dot_segments(path9) {
12242
- if (!path9) return path9;
12241
+ function remove_dot_segments(path10) {
12242
+ if (!path10) return path10;
12243
12243
  var output = "";
12244
- while (path9.length > 0) {
12245
- if (path9 === "." || path9 === "..") {
12246
- path9 = "";
12244
+ while (path10.length > 0) {
12245
+ if (path10 === "." || path10 === "..") {
12246
+ path10 = "";
12247
12247
  break;
12248
12248
  }
12249
- var twochars = path9.substring(0, 2);
12250
- var threechars = path9.substring(0, 3);
12251
- var fourchars = path9.substring(0, 4);
12249
+ var twochars = path10.substring(0, 2);
12250
+ var threechars = path10.substring(0, 3);
12251
+ var fourchars = path10.substring(0, 4);
12252
12252
  if (threechars === "../") {
12253
- path9 = path9.substring(3);
12253
+ path10 = path10.substring(3);
12254
12254
  } else if (twochars === "./") {
12255
- path9 = path9.substring(2);
12255
+ path10 = path10.substring(2);
12256
12256
  } else if (threechars === "/./") {
12257
- path9 = "/" + path9.substring(3);
12258
- } else if (twochars === "/." && path9.length === 2) {
12259
- path9 = "/";
12260
- } else if (fourchars === "/../" || threechars === "/.." && path9.length === 3) {
12261
- path9 = "/" + path9.substring(4);
12257
+ path10 = "/" + path10.substring(3);
12258
+ } else if (twochars === "/." && path10.length === 2) {
12259
+ path10 = "/";
12260
+ } else if (fourchars === "/../" || threechars === "/.." && path10.length === 3) {
12261
+ path10 = "/" + path10.substring(4);
12262
12262
  output = output.replace(/\/?[^\/]*$/, "");
12263
12263
  } else {
12264
- var segment = path9.match(/(\/?([^\/]*))/)[0];
12264
+ var segment = path10.match(/(\/?([^\/]*))/)[0];
12265
12265
  output += segment;
12266
- path9 = path9.substring(segment.length);
12266
+ path10 = path10.substring(segment.length);
12267
12267
  }
12268
12268
  }
12269
12269
  return output;
@@ -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 join23(output, replacement);
24468
+ return join24(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 = join23(output, rule.append(self.options));
24475
+ output = join24(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 join23(output, replacement) {
24487
+ function join24(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);
@@ -24979,8 +24979,8 @@ function getErrorMap() {
24979
24979
 
24980
24980
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
24981
24981
  var makeIssue = (params) => {
24982
- const { data, path: path9, errorMaps, issueData } = params;
24983
- const fullPath = [...path9, ...issueData.path || []];
24982
+ const { data, path: path10, errorMaps, issueData } = params;
24983
+ const fullPath = [...path10, ...issueData.path || []];
24984
24984
  const fullIssue = {
24985
24985
  ...issueData,
24986
24986
  path: fullPath
@@ -25096,11 +25096,11 @@ var errorUtil;
25096
25096
 
25097
25097
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
25098
25098
  var ParseInputLazyPath = class {
25099
- constructor(parent, value, path9, key) {
25099
+ constructor(parent, value, path10, key) {
25100
25100
  this._cachedPath = [];
25101
25101
  this.parent = parent;
25102
25102
  this.data = value;
25103
- this._path = path9;
25103
+ this._path = path10;
25104
25104
  this._key = key;
25105
25105
  }
25106
25106
  get path() {
@@ -28737,10 +28737,10 @@ function assignProp(target, prop, value) {
28737
28737
  configurable: true
28738
28738
  });
28739
28739
  }
28740
- function getElementAtPath(obj, path9) {
28741
- if (!path9)
28740
+ function getElementAtPath(obj, path10) {
28741
+ if (!path10)
28742
28742
  return obj;
28743
- return path9.reduce((acc, key) => acc?.[key], obj);
28743
+ return path10.reduce((acc, key) => acc?.[key], obj);
28744
28744
  }
28745
28745
  function promiseAllObject(promisesObj) {
28746
28746
  const keys = Object.keys(promisesObj);
@@ -29060,11 +29060,11 @@ function aborted(x, startIndex = 0) {
29060
29060
  }
29061
29061
  return false;
29062
29062
  }
29063
- function prefixIssues(path9, issues) {
29063
+ function prefixIssues(path10, issues) {
29064
29064
  return issues.map((iss) => {
29065
29065
  var _a;
29066
29066
  (_a = iss).path ?? (_a.path = []);
29067
- iss.path.unshift(path9);
29067
+ iss.path.unshift(path10);
29068
29068
  return iss;
29069
29069
  });
29070
29070
  }
@@ -32475,11 +32475,11 @@ function normalizeObjectSchema(schema) {
32475
32475
  }
32476
32476
  return void 0;
32477
32477
  }
32478
- function getDotPath(path9) {
32479
- if (path9.length === 0) {
32478
+ function getDotPath(path10) {
32479
+ if (path10.length === 0) {
32480
32480
  return "object root";
32481
32481
  }
32482
- return path9.reduce((acc, seg, index) => {
32482
+ return path10.reduce((acc, seg, index) => {
32483
32483
  if (index === 0) {
32484
32484
  return String(seg);
32485
32485
  }
@@ -40964,8 +40964,8 @@ function allowedSaveRoots() {
40964
40964
  function deniedSaveRoots() {
40965
40965
  return [join6(homedir6(), "Library/Group Containers/group.com.apple.notes")];
40966
40966
  }
40967
- function canonicalize(path9) {
40968
- return realpathSync.native(path9);
40967
+ function canonicalize(path10) {
40968
+ return realpathSync.native(path10);
40969
40969
  }
40970
40970
  function isWithinRoots(candidate, roots) {
40971
40971
  return roots.some((root) => {
@@ -40987,9 +40987,9 @@ function canonicalRoots(roots) {
40987
40987
  }
40988
40988
  return canonical;
40989
40989
  }
40990
- function entryExists(path9) {
40990
+ function entryExists(path10) {
40991
40991
  try {
40992
- lstatSync(path9);
40992
+ lstatSync(path10);
40993
40993
  return true;
40994
40994
  } catch (e) {
40995
40995
  const code = e.code;
@@ -41363,24 +41363,24 @@ function safeComponent(value) {
41363
41363
  if (value === "." || value === ".." || value.includes("/") || value.includes("\0")) return null;
41364
41364
  return value;
41365
41365
  }
41366
- function realInside(path9, rootReal) {
41366
+ function realInside(path10, rootReal) {
41367
41367
  try {
41368
- const real = realpathSync2.native(path9);
41368
+ const real = realpathSync2.native(path10);
41369
41369
  return real === rootReal || real.startsWith(rootReal + sep2) ? real : null;
41370
41370
  } catch {
41371
41371
  return null;
41372
41372
  }
41373
41373
  }
41374
- function isRegularFile(path9) {
41374
+ function isRegularFile(path10) {
41375
41375
  try {
41376
- return lstatSync2(path9).isFile();
41376
+ return lstatSync2(path10).isFile();
41377
41377
  } catch {
41378
41378
  return false;
41379
41379
  }
41380
41380
  }
41381
- function isDirectory(path9) {
41381
+ function isDirectory(path10) {
41382
41382
  try {
41383
- return lstatSync2(path9).isDirectory();
41383
+ return lstatSync2(path10).isDirectory();
41384
41384
  } catch {
41385
41385
  return false;
41386
41386
  }
@@ -41760,10 +41760,10 @@ function readImageInfoFd(fd) {
41760
41760
  const read = readSync2(fd, buf, 0, buf.length, 0);
41761
41761
  return parseImageHeader(buf.subarray(0, read));
41762
41762
  }
41763
- function readImageInfo(path9) {
41763
+ function readImageInfo(path10) {
41764
41764
  let fd;
41765
41765
  try {
41766
- fd = openSync3(path9, constants3.O_RDONLY | constants3.O_NOFOLLOW);
41766
+ fd = openSync3(path10, constants3.O_RDONLY | constants3.O_NOFOLLOW);
41767
41767
  } catch {
41768
41768
  return null;
41769
41769
  }
@@ -41863,9 +41863,9 @@ function readDrawingRows(noteId3, dbPath2 = join8(NOTES_CONTAINER_DIR, "NoteStor
41863
41863
  throw new AttachmentStoreError("Drawing rows could not be parsed", "query_error");
41864
41864
  }
41865
41865
  }
41866
- function kindOf(path9) {
41866
+ function kindOf(path10) {
41867
41867
  try {
41868
- const st = lstatSync3(path9);
41868
+ const st = lstatSync3(path10);
41869
41869
  return st.isFile() ? "file" : st.isDirectory() ? "dir" : null;
41870
41870
  } catch {
41871
41871
  return null;
@@ -41931,13 +41931,13 @@ function describeDrawings(rows, containerDir = NOTES_CONTAINER_DIR) {
41931
41931
  const previewPath = accountDir ? findLargestPreview(accountDir, row.identifier) : null;
41932
41932
  const bundle = accountDir && id2 && kind === "paper" ? realInside(join8(accountDir, "Paper", "Bundles", `${id2}.bundle`), accountDir) : null;
41933
41933
  let raster = null;
41934
- for (const [path9, source] of [
41934
+ for (const [path10, source] of [
41935
41935
  [fallbackImagePath, "fallback"],
41936
41936
  [previewPath, "preview"]
41937
41937
  ]) {
41938
- const info = path9 ? readImageInfo(path9) : null;
41939
- if (path9 && info) {
41940
- raster = { path: path9, source, ...info };
41938
+ const info = path10 ? readImageInfo(path10) : null;
41939
+ if (path10 && info) {
41940
+ raster = { path: path10, source, ...info };
41941
41941
  break;
41942
41942
  }
41943
41943
  }
@@ -41954,10 +41954,10 @@ function describeDrawings(rows, containerDir = NOTES_CONTAINER_DIR) {
41954
41954
  };
41955
41955
  });
41956
41956
  }
41957
- function verifyWrittenImage(fd, path9, expected) {
41957
+ function verifyWrittenImage(fd, path10, expected) {
41958
41958
  const check2 = readImageInfoFd(fd);
41959
41959
  if (!check2 || check2.format !== expected.format || check2.width !== expected.width || check2.height !== expected.height) {
41960
- unlinkSync2(path9);
41960
+ unlinkSync2(path10);
41961
41961
  throw new Error("The exported image failed validation and was removed.");
41962
41962
  }
41963
41963
  return check2;
@@ -42373,6 +42373,34 @@ function buildAppLevelScript(command) {
42373
42373
  end tell
42374
42374
  `;
42375
42375
  }
42376
+ function folderDeleteFactsScript(id2) {
42377
+ if (!FOLDER_ID_PATTERN.test(id2)) throw new Error("An exact folder ID is required");
42378
+ return `
42379
+ set f to folder id "${id2}"
42380
+ set folderName to name of f
42381
+ set parentRef to container of f
42382
+ set parentId to ""
42383
+ if class of parentRef is folder then set parentId to id of parentRef
42384
+ set sharedAny to (shared of f)
42385
+ set acct to parentRef
42386
+ repeat while class of acct is folder
42387
+ if shared of acct then set sharedAny to true
42388
+ set acct to container of acct
42389
+ end repeat
42390
+ set acctId to id of acct
42391
+ set defaultId to ""
42392
+ try
42393
+ set defaultId to id of default folder of acct
42394
+ end try
42395
+ -- "folders of f" keeps listing a child deleted earlier in this Notes
42396
+ -- session, while "exists folder id" reports it gone, so count only
42397
+ -- children that still exist.
42398
+ set childCount to 0
42399
+ repeat with childRef in folders of f
42400
+ if exists folder id (id of childRef) then set childCount to childCount + 1
42401
+ end repeat
42402
+ set noteCount to count of notes of f`;
42403
+ }
42376
42404
  function getNoteLinkFromDB(coreDataId2) {
42377
42405
  const match = coreDataId2.match(/\/p(\d+)$/);
42378
42406
  if (!match) return null;
@@ -43402,18 +43430,134 @@ var AppleNotesManager = class {
43402
43430
  );
43403
43431
  return { id: id2, name: newName, parentId: expectedParentId };
43404
43432
  }
43405
- /** Read the exact name and parent identity used by guarded folder rename. */
43433
+ /** Read the exact name, parent, and account identity used by guarded folder writes. */
43406
43434
  getFolderById(id2) {
43407
43435
  if (!/^x-coredata:\/\/[0-9a-f-]+\/ICFolder\/p\d+$/i.test(id2))
43408
43436
  throw new Error("An exact folder ID is required");
43409
43437
  const result = executeAppleScript(`tell application "Notes"
43410
43438
  set f to folder id "${id2}"
43411
- return (name of f) & ${AS_FIELD_SEP} & (id of container of f)
43439
+ set acct to container of f
43440
+ repeat while class of acct is folder
43441
+ set acct to container of acct
43442
+ end repeat
43443
+ return (name of f) & ${AS_FIELD_SEP} & (id of container of f) & ${AS_FIELD_SEP} & (id of acct)
43412
43444
  end tell`);
43413
43445
  if (!result.success) throw new Error(result.error || "Folder not found");
43414
- const [name, parentId] = result.output.replace(/\n$/, "").split(FIELD_SEP);
43446
+ const [name, parentId, accountId] = result.output.replace(/\n$/, "").split(FIELD_SEP);
43415
43447
  if (!name || !parentId) throw new Error("Incomplete folder metadata");
43416
- return { id: id2, name, parentId };
43448
+ return accountId ? { id: id2, name, parentId, accountId, isRoot: parentId === accountId } : { id: id2, name, parentId };
43449
+ }
43450
+ /**
43451
+ * Reads, in one AppleScript, everything Notes.app itself knows about a folder
43452
+ * that guarded deletion checks: name, parent (empty at an account root),
43453
+ * account, the account's default folder, shared state of the folder or any
43454
+ * ancestor, and the counts of direct child folders and notes.
43455
+ */
43456
+ readFolderForDelete(id2) {
43457
+ if (!FOLDER_ID_PATTERN.test(id2)) throw new Error("An exact folder ID is required");
43458
+ const result = executeAppleScript(
43459
+ buildAppLevelScript(`${folderDeleteFactsScript(id2)}
43460
+ return folderName & ${AS_FIELD_SEP} & parentId & ${AS_FIELD_SEP} & acctId & ${AS_FIELD_SEP} & defaultId & ${AS_FIELD_SEP} & (sharedAny as text) & ${AS_FIELD_SEP} & (childCount as text) & ${AS_FIELD_SEP} & (noteCount as text)`)
43461
+ );
43462
+ if (!result.success) throw new Error(result.error || "Folder not found");
43463
+ const parts = result.output.replace(/\n$/, "").split(FIELD_SEP);
43464
+ if (parts.length !== 7 || !parts[0] || !parts[2]) throw new Error("Incomplete folder metadata");
43465
+ const count = (value) => {
43466
+ const parsed = Number(value.trim());
43467
+ if (!Number.isInteger(parsed) || parsed < 0) throw new Error("Incomplete folder metadata");
43468
+ return parsed;
43469
+ };
43470
+ return {
43471
+ id: id2,
43472
+ name: parts[0],
43473
+ parentId: parts[1] || null,
43474
+ accountId: parts[2],
43475
+ defaultFolderId: parts[3] || null,
43476
+ shared: parts[4].trim() === "true",
43477
+ childFolderCount: count(parts[5]),
43478
+ noteCount: count(parts[6])
43479
+ };
43480
+ }
43481
+ /**
43482
+ * Deletes one empty folder by exact id, repeating every Notes.app-visible
43483
+ * guard inside the same AppleScript as the `delete` command: exact name
43484
+ * (case-sensitive), parent or account root, account, not the default folder,
43485
+ * not shared (itself or an ancestor), no child folders, no notes.
43486
+ *
43487
+ * Store-only facts (folder type, stable identifier) are checked by the caller
43488
+ * before this runs, so the whole guard is a pre-check followed by an
43489
+ * AppleScript delete, not one atomic transaction.
43490
+ */
43491
+ deleteEmptyFolderIfUnchanged(id2, expected) {
43492
+ if (!FOLDER_ID_PATTERN.test(id2)) throw new Error("An exact folder ID is required");
43493
+ const literal2 = (value) => `"${escapePlainStringForAppleScript(value)}"`;
43494
+ const script = buildAppLevelScript(`${folderDeleteFactsScript(id2)}
43495
+ considering case
43496
+ if folderName is not ${literal2(expected.name)} then return "SAFETY_CONFLICT:name"
43497
+ end considering
43498
+ if parentId is not ${literal2(expected.parentId ?? "")} then return "SAFETY_CONFLICT:parent"
43499
+ if acctId is not ${literal2(expected.accountId)} then return "SAFETY_CONFLICT:account"
43500
+ if defaultId is ${literal2(id2)} then return "SAFETY_REFUSED:default folder"
43501
+ if sharedAny then return "SAFETY_REFUSED:shared folder"
43502
+ if childCount is not 0 then return "SAFETY_REFUSED:folder has child folders"
43503
+ if noteCount is not 0 then return "SAFETY_REFUSED:folder has notes"
43504
+ delete f
43505
+ return "SAFETY_DELETED"`);
43506
+ const result = executeMutationAppleScript(script);
43507
+ if (!result.success) {
43508
+ console.error(`Failed guarded folder delete for "${id2}":`, result.error);
43509
+ return { status: "failed", reason: result.error || "AppleScript failed" };
43510
+ }
43511
+ const output = result.output.trim();
43512
+ const colon = output.indexOf(":");
43513
+ const status = colon === -1 ? output : output.slice(0, colon);
43514
+ const reason = colon === -1 ? "" : output.slice(colon + 1);
43515
+ if (status === "SAFETY_DELETED") return { status: "deleted" };
43516
+ if (status === "SAFETY_CONFLICT") return { status: "conflict", reason };
43517
+ if (status === "SAFETY_REFUSED") return { status: "refused", reason };
43518
+ return { status: "failed", reason: "Unexpected AppleScript result" };
43519
+ }
43520
+ /**
43521
+ * Counts how many of the given notes Notes.app currently places outside the
43522
+ * folder: in another folder, or in Recently Deleted (a note trashed this
43523
+ * session reports a non-folder container). A note Notes.app cannot resolve
43524
+ * is not counted, so the caller keeps treating it as present.
43525
+ *
43526
+ * The local store can keep a just-trashed note in its old folder for
43527
+ * minutes; this lets guarded folder deletion discount such notes using
43528
+ * Notes.app's live view instead of refusing until the store catches up.
43529
+ */
43530
+ countNotesOutsideFolder(folderId2, noteIds) {
43531
+ if (!FOLDER_ID_PATTERN.test(folderId2)) throw new Error("An exact folder ID is required");
43532
+ if (noteIds.length === 0) return 0;
43533
+ const ids = noteIds.map((noteId3) => `"${sanitizeNoteId(noteId3)}"`).join(", ");
43534
+ const result = executeAppleScript(
43535
+ buildAppLevelScript(`
43536
+ set movedCount to 0
43537
+ repeat with noteIdRef in {${ids}}
43538
+ try
43539
+ set c to container of note id (contents of noteIdRef)
43540
+ if class of c is not folder then
43541
+ set movedCount to movedCount + 1
43542
+ else if (id of c) is not "${folderId2}" then
43543
+ set movedCount to movedCount + 1
43544
+ end if
43545
+ end try
43546
+ end repeat
43547
+ return movedCount as text`)
43548
+ );
43549
+ if (!result.success) return 0;
43550
+ const moved = Number(result.output.trim());
43551
+ return Number.isInteger(moved) && moved >= 0 && moved <= noteIds.length ? moved : 0;
43552
+ }
43553
+ /** Whether Notes.app still resolves an exact folder id. */
43554
+ folderExistsById(id2) {
43555
+ if (!FOLDER_ID_PATTERN.test(id2)) throw new Error("An exact folder ID is required");
43556
+ const result = executeAppleScript(
43557
+ buildAppLevelScript(`return (exists folder id "${id2}") as text`)
43558
+ );
43559
+ if (!result.success) throw new Error(result.error || "Could not check folder existence");
43560
+ return result.output.trim() === "true";
43417
43561
  }
43418
43562
  /** Insert one file into an unchanged exact note and return Notes' attachment ID. */
43419
43563
  addAttachmentById(id2, expectedBody, filePath) {
@@ -46401,12 +46545,12 @@ function resolveFolders(rows) {
46401
46545
  seen.add(pk);
46402
46546
  const parent = row.parent !== null ? resolve5(row.parent, seen) : void 0;
46403
46547
  const name = row.name ?? "";
46404
- const path9 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
46548
+ const path10 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
46405
46549
  const plainPath = parent ? `${parent.plainPath}/${name}` : name;
46406
46550
  const info = {
46407
- path: path9,
46551
+ path: path10,
46408
46552
  plainPath,
46409
- keys: [...new Set([name, plainPath, path9].map(normalizeForMatch))],
46553
+ keys: [...new Set([name, plainPath, path10].map(normalizeForMatch))],
46410
46554
  accountPk: row.owner ?? parent?.accountPk ?? null,
46411
46555
  shared: Boolean(row.shared) || Boolean(parent?.shared)
46412
46556
  };
@@ -46747,9 +46891,9 @@ function runNativeTagsShortcut(input) {
46747
46891
  throw new Error(`Import the supplied ${status.shortcut}.shortcut in Shortcuts first`);
46748
46892
  const directory = mkdtempSync2(join15(tmpdir2(), "apple-notes-native-tags-"));
46749
46893
  try {
46750
- const path9 = join15(directory, "request.json");
46751
- writeFileSync(path9, JSON.stringify(input), { mode: 384 });
46752
- execFileSync14("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path9], {
46894
+ const path10 = join15(directory, "request.json");
46895
+ writeFileSync(path10, JSON.stringify(input), { mode: 384 });
46896
+ execFileSync14("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path10], {
46753
46897
  encoding: "utf8",
46754
46898
  timeout: 6e4,
46755
46899
  maxBuffer: 1024 * 1024,
@@ -47353,7 +47497,7 @@ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
47353
47497
  throw new Error(
47354
47498
  `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
47355
47499
  );
47356
- const segments = (path9) => JSON.stringify(splitFolderPath(path9).map((part) => part.toLocaleLowerCase()));
47500
+ const segments = (path10) => JSON.stringify(splitFolderPath(path10).map((part) => part.toLocaleLowerCase()));
47357
47501
  if (request.folder) {
47358
47502
  const wanted = segments(request.folder);
47359
47503
  if (!manager.listAccounts().some(
@@ -47683,9 +47827,9 @@ function getCapabilityMatrix(probe = probeCapabilityEnvironment) {
47683
47827
  return evaluateFeatures(probe());
47684
47828
  }
47685
47829
  function formatCapabilityMatrix(matrix) {
47686
- const os9 = matrix.runtimeOS;
47830
+ const os10 = matrix.runtimeOS;
47687
47831
  const lines = [
47688
- `Feature matrix (macOS ${os9.macOSVersion ?? "unknown"}, Darwin ${os9.darwinRelease}):`
47832
+ `Feature matrix (macOS ${os10.macOSVersion ?? "unknown"}, Darwin ${os10.darwinRelease}):`
47689
47833
  ];
47690
47834
  for (const [name, status] of Object.entries(matrix.features)) {
47691
47835
  lines.push(
@@ -47811,11 +47955,11 @@ function fileConfigPath(env = process.env) {
47811
47955
  if (override && override.trim()) return override.trim();
47812
47956
  return join17(homedir14(), "Library", "Application Support", "apple-notes-mcp", "config.json");
47813
47957
  }
47814
- function loadFileConfig(env = process.env, path9 = fileConfigPath(env)) {
47958
+ function loadFileConfig(env = process.env, path10 = fileConfigPath(env)) {
47815
47959
  const applied = [];
47816
47960
  try {
47817
- if (!existsSync11(path9)) return applied;
47818
- const parsed = JSON.parse(readFileSync2(path9, "utf8"));
47961
+ if (!existsSync11(path10)) return applied;
47962
+ const parsed = JSON.parse(readFileSync2(path10, "utf8"));
47819
47963
  if (!parsed || typeof parsed !== "object") return applied;
47820
47964
  for (const [k, v] of Object.entries(parsed)) {
47821
47965
  if (typeof v !== "string") continue;
@@ -47825,7 +47969,7 @@ function loadFileConfig(env = process.env, path9 = fileConfigPath(env)) {
47825
47969
  }
47826
47970
  }
47827
47971
  } catch (e) {
47828
- console.error(`Failed to load apple-notes-mcp config file ${path9}: ${String(e)}`);
47972
+ console.error(`Failed to load apple-notes-mcp config file ${path10}: ${String(e)}`);
47829
47973
  }
47830
47974
  return applied;
47831
47975
  }
@@ -48668,10 +48812,10 @@ function listDirectory(dir, limit) {
48668
48812
  return [];
48669
48813
  }
48670
48814
  }
48671
- function isFile(path9) {
48815
+ function isFile(path10) {
48672
48816
  let fd;
48673
48817
  try {
48674
- fd = openSync4(path9, constants4.O_RDONLY | constants4.O_NOFOLLOW);
48818
+ fd = openSync4(path10, constants4.O_RDONLY | constants4.O_NOFOLLOW);
48675
48819
  return fstatSync4(fd).isFile();
48676
48820
  } catch {
48677
48821
  return false;
@@ -48679,9 +48823,9 @@ function isFile(path9) {
48679
48823
  if (fd !== void 0) closeSync4(fd);
48680
48824
  }
48681
48825
  }
48682
- function isDirectory2(path9) {
48826
+ function isDirectory2(path10) {
48683
48827
  try {
48684
- return statSync3(path9).isDirectory();
48828
+ return statSync3(path10).isDirectory();
48685
48829
  } catch {
48686
48830
  return false;
48687
48831
  }
@@ -48765,8 +48909,8 @@ var AssetLocator = class {
48765
48909
  join19(account, "Media", mediaId, filename)
48766
48910
  ];
48767
48911
  for (const candidate of candidates) {
48768
- const path9 = confine(candidate, account);
48769
- if (path9 && isFile(path9)) return { path: path9, name: filename, role: "original" };
48912
+ const path10 = confine(candidate, account);
48913
+ if (path10 && isFile(path10)) return { path: path10, name: filename, role: "original" };
48770
48914
  }
48771
48915
  return void 0;
48772
48916
  }
@@ -48781,8 +48925,8 @@ var AssetLocator = class {
48781
48925
  ];
48782
48926
  for (const g of generations)
48783
48927
  for (const name of names) {
48784
- const path9 = confine(g ? join19(base, g, name) : join19(base, name), account);
48785
- if (path9 && isFile(path9)) return path9;
48928
+ const path10 = confine(g ? join19(base, g, name) : join19(base, name), account);
48929
+ if (path10 && isFile(path10)) return path10;
48786
48930
  }
48787
48931
  return void 0;
48788
48932
  }
@@ -48815,16 +48959,16 @@ var AssetLocator = class {
48815
48959
  bundleImage(bundle, account) {
48816
48960
  const found = [];
48817
48961
  for (const child of listDirectory(bundle, MAX_BUNDLE_ENTRIES2)) {
48818
- const path9 = confine(join19(bundle, child), account);
48819
- if (!path9) continue;
48820
- if (isFile(path9)) found.push(path9);
48962
+ const path10 = confine(join19(bundle, child), account);
48963
+ if (!path10) continue;
48964
+ if (isFile(path10)) found.push(path10);
48821
48965
  else
48822
- for (const grandchild of listDirectory(path9, MAX_BUNDLE_ENTRIES2)) {
48823
- const inner = confine(join19(path9, grandchild), account);
48966
+ for (const grandchild of listDirectory(path10, MAX_BUNDLE_ENTRIES2)) {
48967
+ const inner = confine(join19(path10, grandchild), account);
48824
48968
  if (inner && isFile(inner)) found.push(inner);
48825
48969
  }
48826
48970
  }
48827
- return found.find((path9) => basename3(path9) === "Preview.png") ?? found.find((path9) => IMAGE_EXTENSIONS.has(extname3(path9).toLowerCase()));
48971
+ return found.find((path10) => basename3(path10) === "Preview.png") ?? found.find((path10) => IMAGE_EXTENSIONS.has(extname3(path10).toLowerCase()));
48828
48972
  }
48829
48973
  };
48830
48974
  function sniffMime(head, name) {
@@ -48872,8 +49016,8 @@ function safeAssetName(name, mime) {
48872
49016
  if (clean.length > 120) clean = clean.slice(0, 120 - ext.length) + ext;
48873
49017
  return clean;
48874
49018
  }
48875
- function encodePathUrl(path9) {
48876
- return path9.split("/").map(encodeURIComponent).join("/");
49019
+ function encodePathUrl(path10) {
49020
+ return path10.split("/").map(encodeURIComponent).join("/");
48877
49021
  }
48878
49022
  function canonicalForm(abs) {
48879
49023
  let current = abs;
@@ -48892,7 +49036,7 @@ function canonicalForm(abs) {
48892
49036
  }
48893
49037
  function assertExportPath(p, container = NOTES_CONTAINER) {
48894
49038
  const abs = assertSafeSavePath(p);
48895
- const within = (path9, root) => path9 === root || path9.startsWith(root + sep3);
49039
+ const within = (path10, root) => path10 === root || path10.startsWith(root + sep3);
48896
49040
  const roots = [resolve3(container), canonicalForm(resolve3(container))];
48897
49041
  const forms = [abs, canonicalForm(abs)];
48898
49042
  if (forms.some((form) => roots.some((root) => within(form, root))))
@@ -48902,16 +49046,16 @@ function assertExportPath(p, container = NOTES_CONTAINER) {
48902
49046
  var CREATE_FLAGS = constants4.O_WRONLY | constants4.O_CREAT | constants4.O_EXCL | constants4.O_NOFOLLOW;
48903
49047
  var OutputExistsError = class extends Error {
48904
49048
  code = "output_exists";
48905
- constructor(path9) {
48906
- super(`Output file already exists: ${path9}. Choose a new path or remove the old file first.`);
49049
+ constructor(path10) {
49050
+ super(`Output file already exists: ${path10}. Choose a new path or remove the old file first.`);
48907
49051
  this.name = "OutputExistsError";
48908
49052
  }
48909
49053
  };
48910
- function openCreateOnly(path9) {
49054
+ function openCreateOnly(path10) {
48911
49055
  try {
48912
- return openSync4(path9, CREATE_FLAGS, 420);
49056
+ return openSync4(path10, CREATE_FLAGS, 420);
48913
49057
  } catch (error2) {
48914
- if (error2.code === "EEXIST") throw new OutputExistsError(path9);
49058
+ if (error2.code === "EEXIST") throw new OutputExistsError(path10);
48915
49059
  throw error2;
48916
49060
  }
48917
49061
  }
@@ -48925,8 +49069,8 @@ function writeAllAndClose(fd, content) {
48925
49069
  closeSync4(fd);
48926
49070
  }
48927
49071
  }
48928
- function openSource(path9) {
48929
- const fd = openSync4(path9, constants4.O_RDONLY | constants4.O_NOFOLLOW);
49072
+ function openSource(path10) {
49073
+ const fd = openSync4(path10, constants4.O_RDONLY | constants4.O_NOFOLLOW);
48930
49074
  const stat = fstatSync4(fd);
48931
49075
  if (!stat.isFile()) {
48932
49076
  closeSync4(fd);
@@ -49908,9 +50052,9 @@ var NotesExportError = class extends Error {
49908
50052
  }
49909
50053
  code;
49910
50054
  };
49911
- function validPath(path9, what) {
50055
+ function validPath(path10, what) {
49912
50056
  try {
49913
- return assertExportPath(path9);
50057
+ return assertExportPath(path10);
49914
50058
  } catch (error2) {
49915
50059
  throw new NotesExportError(
49916
50060
  "invalid-path",
@@ -50099,9 +50243,9 @@ function assertExistingContentPreserved(before, after) {
50099
50243
  throw new Error("Existing native object content or presentation changed");
50100
50244
  }
50101
50245
  }
50102
- function localAttachment(path9) {
50103
- if (!isAbsolute3(path9)) throw new Error("An absolute local file path is required");
50104
- const descriptor = openSync5(path9, constants5.O_RDONLY | constants5.O_NOFOLLOW);
50246
+ function localAttachment(path10) {
50247
+ if (!isAbsolute3(path10)) throw new Error("An absolute local file path is required");
50248
+ const descriptor = openSync5(path10, constants5.O_RDONLY | constants5.O_NOFOLLOW);
50105
50249
  try {
50106
50250
  const stat = fstatSync5(descriptor);
50107
50251
  if (!stat.isFile() || stat.size === 0 || stat.size > 64 * 1024 * 1024)
@@ -50139,7 +50283,7 @@ function registerDirectOperations(server2, manager) {
50139
50283
  }
50140
50284
  tool(
50141
50285
  "get-folder-by-id",
50142
- "Use when: reading the exact folder name and parent before a guarded rename.\nReturns: folder id, current name, and parent id.\nDo not use when: listing folders by path (list-folders).\nSafety: read-only.",
50286
+ "Use when: reading the exact folder name, parent, and account before a guarded rename or delete-folder-by-id.\nReturns: folder id, current name, parent id, accountId, and isRoot (true when the folder sits at the account root; pass expectedRoot: true to delete-folder-by-id then, otherwise parentId as expectedParentId).\nDo not use when: listing folders by path (list-folders).\nSafety: read-only.",
50143
50287
  { id: folderId },
50144
50288
  ({ id: id2 }) => {
50145
50289
  const folder = manager.getFolderById(id2);
@@ -50223,8 +50367,8 @@ function registerDirectOperations(server2, manager) {
50223
50367
  }
50224
50368
  );
50225
50369
  }
50226
- function attachmentName(path9, filename) {
50227
- const source = basename5(path9);
50370
+ function attachmentName(path10, filename) {
50371
+ const source = basename5(path10);
50228
50372
  if (filename === void 0) return source;
50229
50373
  if (Buffer.byteLength(filename, "utf8") > 255)
50230
50374
  throw new Error("filename must be at most 255 bytes");
@@ -50239,11 +50383,11 @@ function attachmentName(path9, filename) {
50239
50383
  return filename;
50240
50384
  }
50241
50385
  function attachFile(manager, args) {
50242
- const { id: id2, expectedContentHash, path: path9 } = args;
50243
- const name = attachmentName(path9, args.filename);
50386
+ const { id: id2, expectedContentHash, path: path10 } = args;
50387
+ const name = attachmentName(path10, args.filename);
50244
50388
  const before = readSnapshot(manager, id2);
50245
50389
  if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
50246
- const bytes = localAttachment(path9);
50390
+ const bytes = localAttachment(path10);
50247
50391
  const beforeAttachments = manager.listAttachmentsById(id2);
50248
50392
  const directory = mkdtempSync4(join22(tmpdir4(), "notes-attachment-add-"));
50249
50393
  const temporaryFile = join22(directory, name);
@@ -50300,6 +50444,329 @@ function attachFile(manager, args) {
50300
50444
  }
50301
50445
  }
50302
50446
 
50447
+ // src/tools/folderDelete.ts
50448
+ import { createHash as createHash3 } from "node:crypto";
50449
+
50450
+ // src/utils/folderStore.ts
50451
+ import { execFileSync as execFileSync19 } from "child_process";
50452
+ import * as os9 from "os";
50453
+ import * as path9 from "path";
50454
+ var NOTE_STORE_PATH = path9.join(
50455
+ os9.homedir(),
50456
+ "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
50457
+ );
50458
+ var NOTE_KEY_LIMIT = 50;
50459
+ var FolderStoreError = class extends Error {
50460
+ constructor(message, reason) {
50461
+ super(message);
50462
+ this.reason = reason;
50463
+ this.name = "FolderStoreError";
50464
+ }
50465
+ reason;
50466
+ };
50467
+ var REQUIRED_FOLDER_COLUMNS = [
50468
+ "ZFOLDERTYPE",
50469
+ "ZIDENTIFIER",
50470
+ "ZMARKEDFORDELETION",
50471
+ "ZPARENT",
50472
+ "ZOWNER",
50473
+ "ZFOLDER",
50474
+ "ZSERVERSHAREDATA"
50475
+ ];
50476
+ function buildFolderFactsSql(hasSmartQueryColumn) {
50477
+ const smart = hasSmartQueryColumn ? "coalesce(f.ZSMARTFOLDERQUERYJSON, '') <> ''" : "0";
50478
+ return `WITH RECURSIVE anc(pk, depth) AS (
50479
+ SELECT ZPARENT, 1 FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = @pk
50480
+ UNION ALL
50481
+ SELECT o.ZPARENT, anc.depth + 1 FROM ZICCLOUDSYNCINGOBJECT o JOIN anc ON o.Z_PK = anc.pk
50482
+ WHERE anc.pk IS NOT NULL AND anc.depth < 64
50483
+ )
50484
+ SELECT json_object(
50485
+ 'pk', f.Z_PK,
50486
+ 'entity', e.Z_NAME,
50487
+ 'identifier', f.ZIDENTIFIER,
50488
+ 'folderType', f.ZFOLDERTYPE,
50489
+ 'markedForDeletion', coalesce(f.ZMARKEDFORDELETION, 0),
50490
+ 'parentPk', f.ZPARENT,
50491
+ 'accountPk', f.ZOWNER,
50492
+ 'hasSmartQuery', ${smart},
50493
+ 'sharedRecord', f.ZSERVERSHAREDATA IS NOT NULL,
50494
+ 'sharedAncestor', (SELECT count(*) FROM ZICCLOUDSYNCINGOBJECT a JOIN anc ON a.Z_PK = anc.pk
50495
+ WHERE a.ZSERVERSHAREDATA IS NOT NULL) > 0,
50496
+ 'childFolderCount', (SELECT count(*) FROM ZICCLOUDSYNCINGOBJECT c
50497
+ WHERE c.Z_ENT = f.Z_ENT AND c.ZPARENT = f.Z_PK
50498
+ AND coalesce(c.ZMARKEDFORDELETION, 0) = 0),
50499
+ 'noteCount', (SELECT count(*) FROM ZICCLOUDSYNCINGOBJECT n
50500
+ WHERE n.Z_ENT = (SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICNote')
50501
+ AND n.ZFOLDER = f.Z_PK AND coalesce(n.ZMARKEDFORDELETION, 0) = 0),
50502
+ 'noteKeys', json((SELECT json_group_array(k.Z_PK) FROM (
50503
+ SELECT n.Z_PK FROM ZICCLOUDSYNCINGOBJECT n
50504
+ WHERE n.Z_ENT = (SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'ICNote')
50505
+ AND n.ZFOLDER = f.Z_PK AND coalesce(n.ZMARKEDFORDELETION, 0) = 0
50506
+ ORDER BY n.Z_PK LIMIT ${NOTE_KEY_LIMIT}) k))
50507
+ )
50508
+ FROM ZICCLOUDSYNCINGOBJECT f JOIN Z_PRIMARYKEY e ON e.Z_ENT = f.Z_ENT
50509
+ WHERE f.Z_PK = @pk;`;
50510
+ }
50511
+ function runSqlite7(dbPath2, sql, pk) {
50512
+ const args = ["-readonly"];
50513
+ if (pk !== void 0) args.push("-cmd", `.parameter set @pk ${pk}`);
50514
+ args.push(dbPath2, sql);
50515
+ return execFileSync19("sqlite3", args, {
50516
+ encoding: "utf8",
50517
+ timeout: 5e3,
50518
+ stdio: ["pipe", "pipe", "pipe"]
50519
+ }).trim();
50520
+ }
50521
+ function classify(error2) {
50522
+ const message = error2 instanceof Error ? error2.message : String(error2);
50523
+ if (/authorization denied|unable to open database|not authorized/i.test(message)) {
50524
+ return new FolderStoreError(
50525
+ "Full Disk Access is required to verify folder type and contents before deleting. Grant it to the app that launches this server, then relaunch it.",
50526
+ "no_fda"
50527
+ );
50528
+ }
50529
+ return new FolderStoreError(`Failed to read folder facts: ${message}`, "query_error");
50530
+ }
50531
+ function numberOrNull(value) {
50532
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
50533
+ }
50534
+ function readFolderStoreFacts(pk, dbPath2 = NOTE_STORE_PATH) {
50535
+ if (!Number.isSafeInteger(pk) || pk <= 0) {
50536
+ throw new FolderStoreError("Folder key must be a positive integer", "query_error");
50537
+ }
50538
+ let columns;
50539
+ let row;
50540
+ try {
50541
+ columns = new Set(
50542
+ runSqlite7(dbPath2, "PRAGMA table_info(ZICCLOUDSYNCINGOBJECT);").split("\n").map((line) => line.split("|")[1]).filter(Boolean)
50543
+ );
50544
+ const missing = REQUIRED_FOLDER_COLUMNS.filter((column) => !columns.has(column));
50545
+ if (missing.length > 0) {
50546
+ throw new FolderStoreError(
50547
+ `This macOS Notes schema lacks ${missing.join(", ")}; folder safety cannot be verified`,
50548
+ "schema"
50549
+ );
50550
+ }
50551
+ row = runSqlite7(dbPath2, buildFolderFactsSql(columns.has("ZSMARTFOLDERQUERYJSON")), pk);
50552
+ } catch (error2) {
50553
+ if (error2 instanceof FolderStoreError) throw error2;
50554
+ throw classify(error2);
50555
+ }
50556
+ if (!row) return null;
50557
+ const raw = JSON.parse(row);
50558
+ if (raw.entity !== "ICFolder") {
50559
+ throw new FolderStoreError("That identifier does not name a folder", "not_folder");
50560
+ }
50561
+ return {
50562
+ pk,
50563
+ identifier: typeof raw.identifier === "string" ? raw.identifier : null,
50564
+ folderType: numberOrNull(raw.folderType),
50565
+ markedForDeletion: raw.markedForDeletion === 1,
50566
+ parentPk: numberOrNull(raw.parentPk),
50567
+ accountPk: numberOrNull(raw.accountPk),
50568
+ hasSmartQuery: raw.hasSmartQuery === 1,
50569
+ sharedRecord: raw.sharedRecord === 1,
50570
+ sharedAncestor: raw.sharedAncestor === 1,
50571
+ childFolderCount: numberOrNull(raw.childFolderCount) ?? 0,
50572
+ noteCount: numberOrNull(raw.noteCount) ?? 0,
50573
+ noteKeys: Array.isArray(raw.noteKeys) ? raw.noteKeys.filter((key) => typeof key === "number") : []
50574
+ };
50575
+ }
50576
+
50577
+ // src/tools/folderDelete.ts
50578
+ var folderIdSchema = external_exports.string().max(2e3).transform(looseIdTransform("ICFolder"));
50579
+ var accountIdSchema = external_exports.string().max(2e3).regex(/^x-coredata:\/\/[0-9a-f-]+\/ICAccount\/p\d+$/i, "An exact account ID is required");
50580
+ var revisionSchema = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
50581
+ var defaultDeps = {
50582
+ readStore: (pk) => readFolderStoreFacts(pk),
50583
+ sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
50584
+ };
50585
+ function conflict(message) {
50586
+ return new CodedError(`Conflict: ${message}`, { code: "revision_conflict", committed: false });
50587
+ }
50588
+ function refused(message) {
50589
+ return new CodedError(`Refused: ${message}`, { code: "unsupported", committed: false });
50590
+ }
50591
+ function uncertain(message) {
50592
+ return new CodedError(message, { code: "verification_failed", indeterminate: true });
50593
+ }
50594
+ function invalid(message) {
50595
+ return new CodedError(message, { code: "validation_error", committed: false });
50596
+ }
50597
+ function coreDataPk(id2) {
50598
+ const match = /\/p(\d+)$/.exec(id2);
50599
+ if (!match) throw new Error(`Not an x-coredata id: ${id2}`);
50600
+ return Number(match[1]);
50601
+ }
50602
+ function folderDeleteRefusal(app, store) {
50603
+ if (store.folderType === 1 || store.identifier?.startsWith("TrashFolder-"))
50604
+ return "Refusing to delete the Recently Deleted folder";
50605
+ if (store.folderType === 2 || store.hasSmartQuery) return "Refusing to delete a smart folder";
50606
+ if (store.folderType !== 0)
50607
+ return `Refusing to delete a folder with unsupported folder type ${String(store.folderType)}`;
50608
+ if (!store.identifier)
50609
+ return "Refusing to delete a folder without a stable identifier (it cannot be verified)";
50610
+ if (store.identifier.startsWith("DefaultFolder-") || app.defaultFolderId === app.id)
50611
+ return "Refusing to delete a system or default folder";
50612
+ if (app.shared || store.sharedRecord || store.sharedAncestor)
50613
+ return "Refusing to delete a shared or collaborated folder";
50614
+ return null;
50615
+ }
50616
+ function folderDeleteRevision(app, store) {
50617
+ const state = [
50618
+ app.id,
50619
+ store.identifier,
50620
+ app.name,
50621
+ app.accountId,
50622
+ app.parentId,
50623
+ app.defaultFolderId === app.id,
50624
+ app.shared,
50625
+ app.childFolderCount,
50626
+ app.noteCount,
50627
+ store.folderType,
50628
+ store.markedForDeletion,
50629
+ store.hasSmartQuery,
50630
+ store.sharedRecord,
50631
+ store.sharedAncestor,
50632
+ store.childFolderCount,
50633
+ store.noteCount
50634
+ ];
50635
+ return `sha256:${createHash3("sha256").update(JSON.stringify(state)).digest("hex")}`;
50636
+ }
50637
+ function checkFolder(manager, args, deps) {
50638
+ const app = manager.readFolderForDelete(args.id);
50639
+ const store = deps.readStore(coreDataPk(args.id));
50640
+ if (!store)
50641
+ throw conflict(
50642
+ "the folder is not in the local Notes store yet; wait for Notes to save it and plan again"
50643
+ );
50644
+ if (store.markedForDeletion) throw conflict("the folder is already deleted");
50645
+ const expectedParentPk = app.parentId ? coreDataPk(app.parentId) : null;
50646
+ if (store.accountPk !== coreDataPk(app.accountId) || store.parentPk !== expectedParentPk)
50647
+ throw conflict(
50648
+ "Notes.app and the local store disagree about this folder's location; wait a moment and plan again"
50649
+ );
50650
+ if (app.name !== args.expectedName) throw conflict("the folder name changed");
50651
+ if (app.accountId !== args.expectedAccountId)
50652
+ throw conflict("the folder is in a different account");
50653
+ if (args.expectedRoot ? app.parentId !== null : app.parentId !== args.expectedParentId)
50654
+ throw conflict("the folder's parent changed");
50655
+ const refusal = folderDeleteRefusal(app, store);
50656
+ if (refusal) throw refused(refusal);
50657
+ let storeNotes = store.noteCount;
50658
+ if (storeNotes > app.noteCount && store.noteKeys.length === storeNotes) {
50659
+ const notePrefix = args.id.replace(/\/ICFolder\/p\d+$/, "/ICNote/p");
50660
+ storeNotes -= manager.countNotesOutsideFolder(
50661
+ args.id,
50662
+ store.noteKeys.map((key) => `${notePrefix}${key}`)
50663
+ );
50664
+ }
50665
+ const children = Math.max(app.childFolderCount, store.childFolderCount);
50666
+ const notes = Math.max(app.noteCount, storeNotes);
50667
+ if (children > 0 || notes > 0)
50668
+ throw refused(
50669
+ `the folder is not empty (${children} child folder(s), ${notes} note(s)); move or delete them first`
50670
+ );
50671
+ return {
50672
+ app,
50673
+ store,
50674
+ identifier: store.identifier,
50675
+ revision: folderDeleteRevision(app, { ...store, noteCount: storeNotes })
50676
+ };
50677
+ }
50678
+ function runFolderDelete(manager, args, deps = defaultDeps) {
50679
+ if (Boolean(args.expectedRoot) === (args.expectedParentId !== void 0))
50680
+ throw invalid("Pass exactly one of expectedParentId or expectedRoot: true");
50681
+ if (args.dryRun && args.expectedRevision)
50682
+ throw invalid("expectedRevision belongs to the apply call (dryRun: false)");
50683
+ if (!args.dryRun && !args.expectedRevision)
50684
+ throw invalid("Apply requires expectedRevision from a matching dry run");
50685
+ const checked = checkFolder(manager, args, deps);
50686
+ const base = {
50687
+ ok: true,
50688
+ id: args.id,
50689
+ identifier: checked.identifier,
50690
+ name: checked.app.name,
50691
+ accountId: checked.app.accountId,
50692
+ parentId: checked.app.parentId,
50693
+ folderType: 0,
50694
+ childFolderCount: 0,
50695
+ noteCount: 0,
50696
+ revision: checked.revision
50697
+ };
50698
+ if (args.dryRun)
50699
+ return { ...base, status: "planned", dryRun: true, committed: false, wouldDelete: true };
50700
+ if (checked.revision !== args.expectedRevision)
50701
+ throw conflict("the folder changed since the dry run; plan again");
50702
+ const outcome = manager.deleteEmptyFolderIfUnchanged(args.id, {
50703
+ name: checked.app.name,
50704
+ parentId: checked.app.parentId,
50705
+ accountId: checked.app.accountId
50706
+ });
50707
+ if (outcome.status === "conflict")
50708
+ throw conflict(`the folder ${outcome.reason} changed before deletion; plan again`);
50709
+ if (outcome.status === "refused") throw refused(outcome.reason);
50710
+ if (outcome.status === "failed")
50711
+ throw uncertain(
50712
+ `The delete outcome is uncertain (${outcome.reason}); read folder ${args.id} before retrying`
50713
+ );
50714
+ if (manager.folderExistsById(args.id))
50715
+ throw uncertain(
50716
+ `The delete outcome is uncertain: Notes.app still resolves folder ${args.id}; read it before retrying`
50717
+ );
50718
+ let storeTombstoned = false;
50719
+ for (let attempt = 0; attempt < 5 && !storeTombstoned; attempt++) {
50720
+ if (attempt > 0) deps.sleep(200);
50721
+ const after = deps.readStore(coreDataPk(args.id));
50722
+ storeTombstoned = !after || after.markedForDeletion;
50723
+ }
50724
+ return {
50725
+ ...base,
50726
+ status: "deleted",
50727
+ dryRun: false,
50728
+ committed: true,
50729
+ wouldDelete: true,
50730
+ verified: true,
50731
+ storeTombstoned
50732
+ };
50733
+ }
50734
+ function registerFolderDelete(server2, manager, deps = defaultDeps) {
50735
+ const inputSchema = {
50736
+ id: folderIdSchema.describe(
50737
+ "Exact folder id (x-coredata://\u2026/ICFolder/pN) from list-folders, or the folder's Notes UUID or numeric key"
50738
+ ),
50739
+ expectedName: external_exports.string().min(1).max(1e3).describe("Current folder name (not a path), matched case-sensitively"),
50740
+ expectedAccountId: accountIdSchema.describe(
50741
+ "Owning account id, from get-folder-by-id or list-accounts"
50742
+ ),
50743
+ expectedParentId: folderIdSchema.optional().describe("Current parent folder id; omit and pass expectedRoot for a top-level folder"),
50744
+ expectedRoot: external_exports.literal(true).optional().describe("Pass true when the folder sits at the account root (no parent folder)"),
50745
+ dryRun: external_exports.boolean().describe("true plans and returns a revision; false applies with expectedRevision"),
50746
+ expectedRevision: revisionSchema.optional().describe("The revision returned by the dry run; required when dryRun is false")
50747
+ };
50748
+ server2.registerTool(
50749
+ "delete-folder-by-id",
50750
+ {
50751
+ description: "Use when: deleting one exact, empty, ordinary folder by id, with a plan-then-apply handshake.\nReturns: a plan (status planned, wouldDelete, identity fields, zero counts, revision) or, on apply, status deleted with verified readback.\nDo not use when: the folder holds notes or subfolders, or you only have a name or path (delete-folder).\nSafety: requires the exact id plus expectedName, expectedAccountId, and expectedParentId or expectedRoot. Call with dryRun true, then repeat the same guards with dryRun false and expectedRevision. Always refuses Recently Deleted, smart folders, the account's default and other system folders, shared folders, and non-empty folders; there is no override. Not atomic: the guard is a pre-check followed by an AppleScript delete. The Notes.app-visible checks repeat inside the delete script, but folder type is read from the local store just before it. Needs Full Disk Access and fails closed without it.",
50752
+ inputSchema,
50753
+ outputSchema: external_exports.object({ ok: external_exports.boolean().optional() }).passthrough(),
50754
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
50755
+ },
50756
+ (async (args) => {
50757
+ try {
50758
+ const result = runFolderDelete(manager, args, deps);
50759
+ return {
50760
+ content: [{ type: "text", text: JSON.stringify(result) }],
50761
+ structuredContent: result
50762
+ };
50763
+ } catch (error2) {
50764
+ return errorResult(error2 instanceof Error ? error2.message : String(error2), error2);
50765
+ }
50766
+ })
50767
+ );
50768
+ }
50769
+
50303
50770
  // src/tools/nativeTagsBridge.ts
50304
50771
  var noteId2 = exactIdInput(
50305
50772
  "ICNote",
@@ -50825,15 +51292,15 @@ var shortcutFiles = [
50825
51292
  function setupShortcuts(checkOnly, dependencies = {}) {
50826
51293
  const status = dependencies.status || nativeTagsStatus;
50827
51294
  const exists = dependencies.exists || existsSync13;
50828
- const open = dependencies.open || ((path9) => {
50829
- const result = spawnSync2("/usr/bin/open", [path9], { encoding: "utf8" });
51295
+ const open = dependencies.open || ((path10) => {
51296
+ const result = spawnSync2("/usr/bin/open", [path10], { encoding: "utf8" });
50830
51297
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
50831
51298
  });
50832
51299
  const baseDirectory = dependencies.baseDirectory || resolve4(dirname5(fileURLToPath(import.meta.url)), "../shortcuts");
50833
51300
  const osRelease = (dependencies.osRelease || release2)();
50834
51301
  const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
50835
51302
  const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
50836
- const path9 = resolve4(baseDirectory, file);
51303
+ const path10 = resolve4(baseDirectory, file);
50837
51304
  let installed = false;
50838
51305
  let identifier;
50839
51306
  let error2;
@@ -50849,9 +51316,9 @@ function setupShortcuts(checkOnly, dependencies = {}) {
50849
51316
  if (!installed && !checkOnly) {
50850
51317
  if (optional2 && !(darwinMajor >= MARKDOWN_MIN_DARWIN_MAJOR))
50851
51318
  skipped = `requires macOS 26 or later (this Mac reports Darwin ${osRelease})`;
50852
- else if (!exists(path9)) error2 = `Packaged Shortcut is missing: ${path9}`;
51319
+ else if (!exists(path10)) error2 = `Packaged Shortcut is missing: ${path10}`;
50853
51320
  else {
50854
- const result = open(path9);
51321
+ const result = open(path10);
50855
51322
  opened = result.ok;
50856
51323
  if (!result.ok) error2 = result.error || `Could not open ${file}`;
50857
51324
  }
@@ -50860,7 +51327,7 @@ function setupShortcuts(checkOnly, dependencies = {}) {
50860
51327
  name,
50861
51328
  installed,
50862
51329
  identifier,
50863
- file: path9,
51330
+ file: path10,
50864
51331
  opened,
50865
51332
  ...optional2 ? { optional: optional2 } : {},
50866
51333
  ...skipped ? { skipped } : {},
@@ -50907,6 +51374,7 @@ var server = new McpServer({
50907
51374
  });
50908
51375
  var notesManager = new AppleNotesManager();
50909
51376
  registerDirectOperations(server, notesManager);
51377
+ registerFolderDelete(server, notesManager);
50910
51378
  registerNativeTagsBridge(server, notesManager);
50911
51379
  registerNativeOperations(server, notesManager);
50912
51380
  function successResponse(message, structured) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.44",
3
+ "version": "2.8.45",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",