apple-notes-mcp 2.8.7 → 2.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -323,6 +323,24 @@ Access.
323
323
 
324
324
  ---
325
325
 
326
+ #### `native-tags-status`
327
+
328
+ Checks whether exactly one configured Native Tags Shortcut is installed. An
329
+ installed workflow may still need macOS permission on its first execution.
330
+
331
+ ---
332
+
333
+ #### `add-native-tags`
334
+
335
+ Adds actual native Notes tag objects to one exact note using `id`, a fresh
336
+ `expectedContentHash`, a distinctive existing `scopeText`, and `tags`. The
337
+ operation verifies the note's original text, links, and native objects after the
338
+ Shortcut runs. It refuses ambiguous title-and-scope matches and never retries an
339
+ uncertain write. Install the signed workflow as described in
340
+ [`shortcuts/README.md`](shortcuts/README.md).
341
+
342
+ ---
343
+
326
344
  #### `get-note-plaintext`
327
345
 
328
346
  Retrieves a note's body as plain text, with no HTML markup.
package/build/index.js CHANGED
@@ -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 join9(output, replacement);
24468
+ return join10(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 = join9(output, rule.append(self.options));
24475
+ output = join10(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 join9(output, replacement) {
24487
+ function join10(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);
@@ -39350,10 +39350,10 @@ function hasFullDiskAccess() {
39350
39350
  return false;
39351
39351
  }
39352
39352
  }
39353
- function queryNoteData(noteId2) {
39354
- const pkMatch = noteId2.match(/\/p(\d+)$/);
39353
+ function queryNoteData(noteId3) {
39354
+ const pkMatch = noteId3.match(/\/p(\d+)$/);
39355
39355
  if (!pkMatch) {
39356
- console.error(`Invalid note ID format: ${noteId2}`);
39356
+ console.error(`Invalid note ID format: ${noteId3}`);
39357
39357
  return { hex: null, error: "invalid_id" };
39358
39358
  }
39359
39359
  const pk = pkMatch[1];
@@ -39438,13 +39438,13 @@ function parseChecklistFromProtobuf(data) {
39438
39438
  return null;
39439
39439
  }
39440
39440
  }
39441
- function getChecklistItems(noteId2) {
39442
- const { hex: hexData, error: queryError } = queryNoteData(noteId2);
39441
+ function getChecklistItems(noteId3) {
39442
+ const { hex: hexData, error: queryError } = queryNoteData(noteId3);
39443
39443
  if (queryError === "invalid_id") {
39444
39444
  return {
39445
39445
  items: null,
39446
39446
  error: "invalid_id",
39447
- message: `Invalid note ID format: "${noteId2}". Expected format: x-coredata://UUID/ICNote/pNNN`
39447
+ message: `Invalid note ID format: "${noteId3}". Expected format: x-coredata://UUID/ICNote/pNNN`
39448
39448
  };
39449
39449
  }
39450
39450
  if (queryError === "no_fda") {
@@ -40281,20 +40281,20 @@ var AppleNotesManager = class {
40281
40281
  return null;
40282
40282
  }
40283
40283
  const rawOutput = result.output.trim();
40284
- const noteId2 = extractCoreDataId(rawOutput, "note") || rawOutput;
40285
- if (!noteId2) {
40284
+ const noteId3 = extractCoreDataId(rawOutput, "note") || rawOutput;
40285
+ if (!noteId3) {
40286
40286
  console.error(`Created note "${title}" but Notes.app returned no canonical note ID`);
40287
40287
  return null;
40288
40288
  }
40289
40289
  try {
40290
- sanitizeNoteId(noteId2);
40290
+ sanitizeNoteId(noteId3);
40291
40291
  } catch {
40292
40292
  console.error(`Created note "${title}" but Notes.app returned an invalid note ID`);
40293
40293
  return null;
40294
40294
  }
40295
40295
  const now = /* @__PURE__ */ new Date();
40296
40296
  return {
40297
- id: noteId2,
40297
+ id: noteId3,
40298
40298
  title,
40299
40299
  content,
40300
40300
  tags,
@@ -40408,11 +40408,11 @@ var AppleNotesManager = class {
40408
40408
  for (const item of items) {
40409
40409
  const [title, id, folder2, created, modified] = item.split(FIELD_SEP);
40410
40410
  if (!title?.trim()) continue;
40411
- const noteId2 = id?.trim() || generateFallbackId();
40412
- if (seenIds.has(noteId2)) continue;
40413
- seenIds.add(noteId2);
40411
+ const noteId3 = id?.trim() || generateFallbackId();
40412
+ if (seenIds.has(noteId3)) continue;
40413
+ seenIds.add(noteId3);
40414
40414
  notes.push({
40415
- id: noteId2,
40415
+ id: noteId3,
40416
40416
  title: title.trim(),
40417
40417
  content: "",
40418
40418
  // Not fetched in search
@@ -40780,10 +40780,10 @@ var AppleNotesManager = class {
40780
40780
  for (const item of output.split(RECORD_SEP)) {
40781
40781
  const [title, id] = item.split(FIELD_SEP);
40782
40782
  if (!title?.trim()) continue;
40783
- const noteId2 = id?.trim() || generateFallbackId();
40784
- if (seenIds.has(noteId2)) continue;
40785
- seenIds.add(noteId2);
40786
- refs.push({ title: title.trim(), id: noteId2 });
40783
+ const noteId3 = id?.trim() || generateFallbackId();
40784
+ if (seenIds.has(noteId3)) continue;
40785
+ seenIds.add(noteId3);
40786
+ refs.push({ title: title.trim(), id: noteId3 });
40787
40787
  if (safeLimit !== void 0 && refs.length >= safeLimit) break;
40788
40788
  }
40789
40789
  return refs;
@@ -41435,8 +41435,8 @@ var AppleNotesManager = class {
41435
41435
  * @param separately - Open in a separate window when supported by Notes.app
41436
41436
  * @returns true if Notes.app revealed the attachment, false otherwise
41437
41437
  */
41438
- showAttachmentById(noteId2, attachmentId, separately = false) {
41439
- const safeNoteId = sanitizeId(noteId2);
41438
+ showAttachmentById(noteId3, attachmentId, separately = false) {
41439
+ const safeNoteId = sanitizeId(noteId3);
41440
41440
  const safeAttId = escapePlainStringForAppleScript(attachmentId);
41441
41441
  const separatelyClause = separately ? " separately true" : "";
41442
41442
  const script = `
@@ -41456,13 +41456,13 @@ var AppleNotesManager = class {
41456
41456
  const result = executeMutationAppleScript(script);
41457
41457
  if (!result.success) {
41458
41458
  console.error(
41459
- `Failed to show attachment "${attachmentId}" on note "${noteId2}":`,
41459
+ `Failed to show attachment "${attachmentId}" on note "${noteId3}":`,
41460
41460
  result.error
41461
41461
  );
41462
41462
  return false;
41463
41463
  }
41464
41464
  if ((result.output ?? "").trim().startsWith("ERR")) {
41465
- console.error(`Attachment "${attachmentId}" not found on note "${noteId2}"`);
41465
+ console.error(`Attachment "${attachmentId}" not found on note "${noteId3}"`);
41466
41466
  return false;
41467
41467
  }
41468
41468
  return true;
@@ -41862,7 +41862,7 @@ var AppleNotesManager = class {
41862
41862
  * @param savePath - absolute destination file path (within home / temp / /Volumes)
41863
41863
  * @returns { success, savedPath?, name?, contentType?, error? }
41864
41864
  */
41865
- saveAttachmentById(noteId2, attachmentId, savePath) {
41865
+ saveAttachmentById(noteId3, attachmentId, savePath) {
41866
41866
  let abs;
41867
41867
  try {
41868
41868
  abs = assertSafeSavePath(savePath);
@@ -41870,7 +41870,7 @@ var AppleNotesManager = class {
41870
41870
  } catch (e) {
41871
41871
  return { success: false, error: e instanceof Error ? e.message : String(e) };
41872
41872
  }
41873
- const safeNoteId = sanitizeId(noteId2);
41873
+ const safeNoteId = sanitizeId(noteId3);
41874
41874
  const safeAttId = escapePlainStringForAppleScript(attachmentId);
41875
41875
  const safePath = escapePlainStringForAppleScript(abs);
41876
41876
  const script = `
@@ -41934,11 +41934,11 @@ var AppleNotesManager = class {
41934
41934
  * @param attachmentId - id of the attachment
41935
41935
  * @returns { success, name?, contentType?, base64?, bytes?, error? }
41936
41936
  */
41937
- getAttachmentBase64ById(noteId2, attachmentId) {
41937
+ getAttachmentBase64ById(noteId3, attachmentId) {
41938
41938
  const dir = makeTempDir();
41939
41939
  try {
41940
41940
  const dest = `${dir}/attachment.bin`;
41941
- const saved = this.saveAttachmentById(noteId2, attachmentId, dest);
41941
+ const saved = this.saveAttachmentById(noteId3, attachmentId, dest);
41942
41942
  if (!saved.success || !saved.savedPath) {
41943
41943
  return { success: false, error: saved.error };
41944
41944
  }
@@ -42438,13 +42438,13 @@ function presentColumns() {
42438
42438
  }
42439
42439
  return cols;
42440
42440
  }
42441
- function getNoteMetadata(noteId2) {
42442
- const pkMatch = noteId2.match(/\/p(\d+)$/);
42441
+ function getNoteMetadata(noteId3) {
42442
+ const pkMatch = noteId3.match(/\/p(\d+)$/);
42443
42443
  if (!pkMatch) {
42444
42444
  return {
42445
42445
  metadata: null,
42446
42446
  error: "invalid_id",
42447
- message: `Invalid note ID format: "${noteId2}". Expected format: x-coredata://UUID/ICNote/pNNN`
42447
+ message: `Invalid note ID format: "${noteId3}". Expected format: x-coredata://UUID/ICNote/pNNN`
42448
42448
  };
42449
42449
  }
42450
42450
  const pk = pkMatch[1];
@@ -42465,7 +42465,7 @@ function getNoteMetadata(noteId2) {
42465
42465
  return {
42466
42466
  metadata: null,
42467
42467
  error: "not_found",
42468
- message: `No note found in the database for ID "${noteId2}".`
42468
+ message: `No note found in the database for ID "${noteId3}".`
42469
42469
  };
42470
42470
  }
42471
42471
  const raw = JSON.parse(row);
@@ -43191,6 +43191,201 @@ function registerDirectOperations(server2, manager) {
43191
43191
  );
43192
43192
  }
43193
43193
 
43194
+ // src/services/nativeTags.ts
43195
+ import { execFileSync as execFileSync6 } from "node:child_process";
43196
+ import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync2, rmSync as rmSync3 } from "node:fs";
43197
+ import { tmpdir as tmpdir3 } from "node:os";
43198
+ import { join as join9 } from "node:path";
43199
+ var NATIVE_TAGS_SHORTCUT = "Apple Notes MCP - Native Tags";
43200
+ function normalizeNativeTags(tags) {
43201
+ if (!tags.length || tags.length > 100) throw new Error("Provide between 1 and 100 tags");
43202
+ return [
43203
+ ...new Set(
43204
+ tags.map((value) => {
43205
+ const tag = value.normalize("NFC").replace(/^#/, "");
43206
+ if (tag.length > 100 || !new RegExp("^(?=.*\\p{L})[\\p{L}\\p{N}_-]+$", "u").test(tag))
43207
+ throw new Error(
43208
+ "Tags must contain a letter and only letters, digits, hyphens or underscores"
43209
+ );
43210
+ return tag;
43211
+ })
43212
+ )
43213
+ ];
43214
+ }
43215
+ function addNativeTags(request, deps) {
43216
+ if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(request.id))
43217
+ throw new Error("An exact CoreData note ID is required");
43218
+ if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
43219
+ throw new Error("scopeText must be a stable, single-line project marker of 12\u2013500 characters");
43220
+ const tags = normalizeNativeTags(request.tags);
43221
+ const before = deps.read(request.id);
43222
+ if (before.contentHash !== request.expectedContentHash)
43223
+ throw new Error("Note revision changed; read it again");
43224
+ if (before.title !== request.title || !before.plaintext.includes(request.scopeText))
43225
+ throw new Error("The exact note does not match the title and project marker");
43226
+ const missing = tags.filter((tag) => !before.rich.nativeTags.includes(tag));
43227
+ if (!missing.length)
43228
+ return { nativeTags: before.rich.nativeTags, added: [], contentHash: before.contentHash };
43229
+ const candidates = deps.candidates(request.title, request.scopeText);
43230
+ if (candidates.length !== 1 || candidates[0] !== request.id)
43231
+ throw new Error(
43232
+ "Shortcuts selection is ambiguous or points to a different note; nothing was changed"
43233
+ );
43234
+ if (deps.read(request.id).contentHash !== request.expectedContentHash)
43235
+ throw new Error("Note revision changed during preflight; nothing was changed");
43236
+ let transportWarning;
43237
+ try {
43238
+ deps.run({ title: request.title, scopeText: request.scopeText, tags: missing });
43239
+ } catch (error2) {
43240
+ transportWarning = error2 instanceof Error ? error2.message : "Shortcuts completion was uncertain";
43241
+ }
43242
+ const after = deps.read(request.id);
43243
+ const allTags = [.../* @__PURE__ */ new Set([...before.rich.nativeTags, ...tags])];
43244
+ const textWithoutTags = (text) => {
43245
+ for (const tag of [...allTags].sort((a, b) => b.length - a.length)) {
43246
+ const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43247
+ text = text.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "");
43248
+ }
43249
+ return text.replace(/[\s\ufffc]/gu, "");
43250
+ };
43251
+ if (after.title !== before.title || allTags.some((tag) => !after.rich.nativeTags.includes(tag)) || textWithoutTags(before.rich.text) !== textWithoutTags(after.rich.text) || linkSignature(before.rich.links) !== linkSignature(after.rich.links) || before.rich.nativeObjectIds.some((id) => !after.rich.nativeObjectIds.includes(id)) || before.rich.hasChecklist !== after.rich.hasChecklist) {
43252
+ throw new Error(
43253
+ "Shortcuts ran, but exact-ID tags/text/links readback was not verified. Read the note before any retry"
43254
+ );
43255
+ }
43256
+ return {
43257
+ nativeTags: after.rich.nativeTags,
43258
+ added: missing,
43259
+ contentHash: after.contentHash,
43260
+ ...transportWarning ? {
43261
+ transportWarning: "Shortcuts completion was uncertain, but all requested native tags and preserved content were verified by exact ID."
43262
+ } : {}
43263
+ };
43264
+ }
43265
+ function nativeTagsStatus(shortcut = process.env.APPLE_NOTES_MCP_TAGS_SHORTCUT || NATIVE_TAGS_SHORTCUT) {
43266
+ const lines = execFileSync6("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
43267
+ encoding: "utf8",
43268
+ timeout: 15e3,
43269
+ maxBuffer: 1024 * 1024,
43270
+ stdio: ["ignore", "pipe", "pipe"]
43271
+ }).split(/\r?\n/u);
43272
+ const matches = lines.flatMap((line) => {
43273
+ const match = /^(.*) \(([0-9A-Fa-f-]{36})\)$/.exec(line);
43274
+ return match && (match[1] === shortcut || match[2].toLowerCase() === shortcut.toLowerCase()) ? [{ name: match[1], identifier: match[2] }] : [];
43275
+ });
43276
+ return {
43277
+ shortcut,
43278
+ installed: matches.length === 1,
43279
+ identifier: matches.length === 1 ? matches[0].identifier : void 0
43280
+ };
43281
+ }
43282
+ function runNativeTagsShortcut(input) {
43283
+ const status = nativeTagsStatus();
43284
+ if (!status.installed)
43285
+ throw new Error(`Import the supplied ${status.shortcut}.shortcut in Shortcuts first`);
43286
+ const directory = mkdtempSync3(join9(tmpdir3(), "apple-notes-native-tags-"));
43287
+ try {
43288
+ const path4 = join9(directory, "request.json");
43289
+ writeFileSync2(path4, JSON.stringify(input), { mode: 384 });
43290
+ execFileSync6("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path4], {
43291
+ encoding: "utf8",
43292
+ timeout: 6e4,
43293
+ maxBuffer: 1024 * 1024,
43294
+ stdio: ["ignore", "pipe", "pipe"]
43295
+ });
43296
+ } catch {
43297
+ throw new Error(
43298
+ "Native tag operation did not complete cleanly (possibly waiting for macOS permission). Do not retry automatically; read the exact note and check Shortcuts"
43299
+ );
43300
+ } finally {
43301
+ rmSync3(directory, { recursive: true, force: true });
43302
+ }
43303
+ }
43304
+
43305
+ // src/tools/nativeTagsBridge.ts
43306
+ var noteId2 = external_exports.string().regex(/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i);
43307
+ var revision2 = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
43308
+ function registerNativeTagsBridge(server2, manager) {
43309
+ function tool(name, description, inputSchema, handler) {
43310
+ server2.registerTool(
43311
+ name,
43312
+ {
43313
+ description,
43314
+ inputSchema,
43315
+ outputSchema: external_exports.object({ ok: external_exports.boolean().optional() }).passthrough()
43316
+ },
43317
+ (async (args) => {
43318
+ try {
43319
+ const result = handler(args);
43320
+ return {
43321
+ content: [{ type: "text", text: JSON.stringify(result) }],
43322
+ structuredContent: result
43323
+ };
43324
+ } catch (error2) {
43325
+ return {
43326
+ content: [
43327
+ {
43328
+ type: "text",
43329
+ text: error2 instanceof Error ? error2.message : String(error2)
43330
+ }
43331
+ ],
43332
+ isError: true
43333
+ };
43334
+ }
43335
+ })
43336
+ );
43337
+ }
43338
+ const read = (id) => {
43339
+ const note = manager.getNoteById(id);
43340
+ if (!note) throw new Error("Note not found");
43341
+ if (note.passwordProtected) throw new Error("Locked notes cannot receive native tags");
43342
+ const body = manager.getNoteContentById(id);
43343
+ if (!body) throw new Error("Note content is unavailable");
43344
+ const enriched = enrichNoteRead(id, body);
43345
+ const rich = readRichNote(id);
43346
+ if (!enriched.complete || enriched.revision !== rich.revision)
43347
+ throw new Error("Native metadata changed during read; read the note again");
43348
+ return {
43349
+ contentHash: richContentHash(body, enriched),
43350
+ title: note.title,
43351
+ plaintext: manager.getNotePlaintextById(id),
43352
+ rich
43353
+ };
43354
+ };
43355
+ tool(
43356
+ "native-tags-status",
43357
+ "Use when: checking whether the Native Tags Shortcut is installed before a tag write.\nReturns: the configured Shortcut name, unique installed identifier, and installed state.\nDo not use when: listing tags on notes (list-native-tags).\nSafety: read-only; installation does not by itself prove Notes permission or a successful live mutation.",
43358
+ {},
43359
+ () => nativeTagsStatus()
43360
+ );
43361
+ tool(
43362
+ "add-native-tags",
43363
+ "Use when: adding real clickable Apple Notes tags to one exact, freshly read note.\nReturns: verified native tags, additions, and the post-write content hash.\nDo not use when: textual #hashtags are sufficient or the note cannot be selected uniquely by its existing title and scope text.\nSafety: requires exact ID, fresh revision, and a distinctive existing scope phrase; verifies original text, links, and native objects after the Shortcut runs and never retries automatically.",
43364
+ {
43365
+ id: noteId2,
43366
+ expectedContentHash: revision2,
43367
+ scopeText: external_exports.string().min(12).max(500),
43368
+ tags: external_exports.array(external_exports.string().min(1).max(101)).min(1).max(100)
43369
+ },
43370
+ ({ id, expectedContentHash, scopeText, tags }) => {
43371
+ const initial = read(id);
43372
+ const result = addNativeTags(
43373
+ { id, expectedContentHash, scopeText, tags, title: initial.title },
43374
+ {
43375
+ read,
43376
+ candidates: (title, scope) => manager.listAccounts().flatMap(
43377
+ (account) => manager.searchNotes(title, false, account.name).filter(
43378
+ (note) => !note.passwordProtected && manager.getNotePlaintextById(note.id).toLocaleLowerCase().includes(scope.toLocaleLowerCase())
43379
+ ).map((note) => note.id)
43380
+ ),
43381
+ run: runNativeTagsShortcut
43382
+ }
43383
+ );
43384
+ return { ok: true, id, ...result };
43385
+ }
43386
+ );
43387
+ }
43388
+
43194
43389
  // src/index.ts
43195
43390
  loadFileConfig();
43196
43391
  var require2 = createRequire(import.meta.url);
@@ -43202,6 +43397,7 @@ var server = new McpServer({
43202
43397
  });
43203
43398
  var notesManager = new AppleNotesManager();
43204
43399
  registerDirectOperations(server, notesManager);
43400
+ registerNativeTagsBridge(server, notesManager);
43205
43401
  function successResponse(message, structured) {
43206
43402
  const res = { content: [{ type: "text", text: message }] };
43207
43403
  if (structured) res.structuredContent = structured;
@@ -44697,8 +44893,8 @@ registerTool(
44697
44893
  contentType: external_exports.string().optional()
44698
44894
  }
44699
44895
  },
44700
- withErrorHandling(({ noteId: noteId2, attachmentId, savePath }) => {
44701
- const r = notesManager.saveAttachmentById(noteId2, attachmentId, savePath);
44896
+ withErrorHandling(({ noteId: noteId3, attachmentId, savePath }) => {
44897
+ const r = notesManager.saveAttachmentById(noteId3, attachmentId, savePath);
44702
44898
  if (!r.success) {
44703
44899
  return errorResponse(`Failed to save attachment: ${r.error ?? "unknown error"}`);
44704
44900
  }
@@ -44724,8 +44920,8 @@ registerTool(
44724
44920
  base64: external_exports.string().optional()
44725
44921
  }
44726
44922
  },
44727
- withErrorHandling(({ noteId: noteId2, attachmentId }) => {
44728
- const r = notesManager.getAttachmentBase64ById(noteId2, attachmentId);
44923
+ withErrorHandling(({ noteId: noteId3, attachmentId }) => {
44924
+ const r = notesManager.getAttachmentBase64ById(noteId3, attachmentId);
44729
44925
  if (!r.success || !r.base64) {
44730
44926
  return errorResponse(`Failed to fetch attachment: ${r.error ?? "unknown error"}`);
44731
44927
  }
@@ -44750,13 +44946,13 @@ registerTool(
44750
44946
  separately: external_exports.boolean().optional()
44751
44947
  }
44752
44948
  },
44753
- withErrorHandling(({ noteId: noteId2, attachmentId, separately = false }) => {
44754
- const success = notesManager.showAttachmentById(noteId2, attachmentId, separately);
44949
+ withErrorHandling(({ noteId: noteId3, attachmentId, separately = false }) => {
44950
+ const success = notesManager.showAttachmentById(noteId3, attachmentId, separately);
44755
44951
  if (!success) {
44756
- return errorResponse(`Failed to show attachment "${attachmentId}" on note "${noteId2}"`);
44952
+ return errorResponse(`Failed to show attachment "${attachmentId}" on note "${noteId3}"`);
44757
44953
  }
44758
44954
  return successResponse(`Shown attachment "${attachmentId}" in Notes.app`, {
44759
- noteId: noteId2,
44955
+ noteId: noteId3,
44760
44956
  attachmentId,
44761
44957
  separately
44762
44958
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.7",
3
+ "version": "2.8.8",
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",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "build",
13
13
  "docs",
14
+ "shortcuts",
14
15
  "README.md",
15
16
  "LICENSE"
16
17
  ],
@@ -0,0 +1,563 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>WFWorkflowName</key>
6
+ <string>Apple Notes MCP - Native Tags</string>
7
+ <key>WFWorkflowActions</key>
8
+ <array>
9
+ <dict>
10
+ <key>WFWorkflowActionIdentifier</key>
11
+ <string>is.workflow.actions.comment</string>
12
+ <key>WFWorkflowActionParameters</key>
13
+ <dict>
14
+ <key>UUID</key>
15
+ <string>0098B800-B098-5A38-8E5B-4D2491ACD95D</string>
16
+ <key>WFCommentActionText</key>
17
+ <string>Adds native tags to one note selected by title and existing project scope text. Refuses zero/multiple results. Input: JSON {title, scopeText, tags}. No scripts, network, deletion or body replacement. The MCP checks exact ID/revision before and verifies native tags/text/links afterward.</string>
18
+ </dict>
19
+ </dict>
20
+ <dict>
21
+ <key>WFWorkflowActionIdentifier</key>
22
+ <string>is.workflow.actions.detect.dictionary</string>
23
+ <key>WFWorkflowActionParameters</key>
24
+ <dict>
25
+ <key>UUID</key>
26
+ <string>FC6F66A2-7C9E-5A6C-A530-61B12F8030DC</string>
27
+ <key>WFInput</key>
28
+ <dict>
29
+ <key>Value</key>
30
+ <dict>
31
+ <key>Type</key>
32
+ <string>ExtensionInput</string>
33
+ </dict>
34
+ <key>WFSerializationType</key>
35
+ <string>WFTextTokenAttachment</string>
36
+ </dict>
37
+ </dict>
38
+ </dict>
39
+ <dict>
40
+ <key>WFWorkflowActionIdentifier</key>
41
+ <string>is.workflow.actions.getvalueforkey</string>
42
+ <key>WFWorkflowActionParameters</key>
43
+ <dict>
44
+ <key>UUID</key>
45
+ <string>56F624A3-B9C4-59B9-9BD5-B0A3D539EB60</string>
46
+ <key>WFInput</key>
47
+ <dict>
48
+ <key>Value</key>
49
+ <dict>
50
+ <key>Type</key>
51
+ <string>ActionOutput</string>
52
+ <key>OutputUUID</key>
53
+ <string>FC6F66A2-7C9E-5A6C-A530-61B12F8030DC</string>
54
+ <key>OutputName</key>
55
+ <string>request</string>
56
+ </dict>
57
+ <key>WFSerializationType</key>
58
+ <string>WFTextTokenAttachment</string>
59
+ </dict>
60
+ <key>WFDictionaryKey</key>
61
+ <string>title</string>
62
+ <key>WFGetDictionaryValueType</key>
63
+ <string>Value</string>
64
+ </dict>
65
+ </dict>
66
+ <dict>
67
+ <key>WFWorkflowActionIdentifier</key>
68
+ <string>is.workflow.actions.getvalueforkey</string>
69
+ <key>WFWorkflowActionParameters</key>
70
+ <dict>
71
+ <key>UUID</key>
72
+ <string>ECA164BD-137A-567C-87AF-C5F3F32F84BF</string>
73
+ <key>WFInput</key>
74
+ <dict>
75
+ <key>Value</key>
76
+ <dict>
77
+ <key>Type</key>
78
+ <string>ActionOutput</string>
79
+ <key>OutputUUID</key>
80
+ <string>FC6F66A2-7C9E-5A6C-A530-61B12F8030DC</string>
81
+ <key>OutputName</key>
82
+ <string>request</string>
83
+ </dict>
84
+ <key>WFSerializationType</key>
85
+ <string>WFTextTokenAttachment</string>
86
+ </dict>
87
+ <key>WFDictionaryKey</key>
88
+ <string>scopeText</string>
89
+ <key>WFGetDictionaryValueType</key>
90
+ <string>Value</string>
91
+ </dict>
92
+ </dict>
93
+ <dict>
94
+ <key>WFWorkflowActionIdentifier</key>
95
+ <string>is.workflow.actions.getvalueforkey</string>
96
+ <key>WFWorkflowActionParameters</key>
97
+ <dict>
98
+ <key>UUID</key>
99
+ <string>40662BA1-53A6-58B0-A54E-A9174F25AB48</string>
100
+ <key>WFInput</key>
101
+ <dict>
102
+ <key>Value</key>
103
+ <dict>
104
+ <key>Type</key>
105
+ <string>ActionOutput</string>
106
+ <key>OutputUUID</key>
107
+ <string>FC6F66A2-7C9E-5A6C-A530-61B12F8030DC</string>
108
+ <key>OutputName</key>
109
+ <string>request</string>
110
+ </dict>
111
+ <key>WFSerializationType</key>
112
+ <string>WFTextTokenAttachment</string>
113
+ </dict>
114
+ <key>WFDictionaryKey</key>
115
+ <string>tags</string>
116
+ <key>WFGetDictionaryValueType</key>
117
+ <string>Value</string>
118
+ </dict>
119
+ </dict>
120
+ <dict>
121
+ <key>WFWorkflowActionIdentifier</key>
122
+ <string>is.workflow.actions.filter.notes</string>
123
+ <key>WFWorkflowActionParameters</key>
124
+ <dict>
125
+ <key>UUID</key>
126
+ <string>DFF87C25-7AF8-55BC-BA5F-4F08CC05A668</string>
127
+ <key>WFContentItemLimitEnabled</key>
128
+ <false/>
129
+ <key>WFContentItemFilter</key>
130
+ <dict>
131
+ <key>WFSerializationType</key>
132
+ <string>WFContentPredicateTableTemplate</string>
133
+ <key>Value</key>
134
+ <dict>
135
+ <key>WFActionParameterFilterPrefix</key>
136
+ <integer>1</integer>
137
+ <key>WFContentPredicateBoundedDate</key>
138
+ <false/>
139
+ <key>WFActionParameterFilterTemplates</key>
140
+ <array>
141
+ <dict>
142
+ <key>Property</key>
143
+ <string>Name</string>
144
+ <key>Operator</key>
145
+ <integer>99</integer>
146
+ <key>Removable</key>
147
+ <true/>
148
+ <key>Values</key>
149
+ <dict>
150
+ <key>String</key>
151
+ <dict>
152
+ <key>WFSerializationType</key>
153
+ <string>WFTextTokenString</string>
154
+ <key>Value</key>
155
+ <dict>
156
+ <key>string</key>
157
+ <string></string>
158
+ <key>attachmentsByRange</key>
159
+ <dict>
160
+ <key>{0, 1}</key>
161
+ <dict>
162
+ <key>Type</key>
163
+ <string>ActionOutput</string>
164
+ <key>OutputUUID</key>
165
+ <string>56F624A3-B9C4-59B9-9BD5-B0A3D539EB60</string>
166
+ <key>OutputName</key>
167
+ <string>title</string>
168
+ </dict>
169
+ </dict>
170
+ </dict>
171
+ </dict>
172
+ </dict>
173
+ </dict>
174
+ <dict>
175
+ <key>Property</key>
176
+ <string>Body</string>
177
+ <key>Operator</key>
178
+ <integer>99</integer>
179
+ <key>Removable</key>
180
+ <true/>
181
+ <key>Values</key>
182
+ <dict>
183
+ <key>String</key>
184
+ <dict>
185
+ <key>WFSerializationType</key>
186
+ <string>WFTextTokenString</string>
187
+ <key>Value</key>
188
+ <dict>
189
+ <key>string</key>
190
+ <string></string>
191
+ <key>attachmentsByRange</key>
192
+ <dict>
193
+ <key>{0, 1}</key>
194
+ <dict>
195
+ <key>Type</key>
196
+ <string>ActionOutput</string>
197
+ <key>OutputUUID</key>
198
+ <string>ECA164BD-137A-567C-87AF-C5F3F32F84BF</string>
199
+ <key>OutputName</key>
200
+ <string>scopeText</string>
201
+ </dict>
202
+ </dict>
203
+ </dict>
204
+ </dict>
205
+ </dict>
206
+ </dict>
207
+ </array>
208
+ </dict>
209
+ </dict>
210
+ </dict>
211
+ </dict>
212
+ <dict>
213
+ <key>WFWorkflowActionIdentifier</key>
214
+ <string>is.workflow.actions.count</string>
215
+ <key>WFWorkflowActionParameters</key>
216
+ <dict>
217
+ <key>UUID</key>
218
+ <string>48769A7C-915C-5339-9CD4-09D8E576EA11</string>
219
+ <key>Input</key>
220
+ <dict>
221
+ <key>Value</key>
222
+ <dict>
223
+ <key>Type</key>
224
+ <string>ActionOutput</string>
225
+ <key>OutputUUID</key>
226
+ <string>DFF87C25-7AF8-55BC-BA5F-4F08CC05A668</string>
227
+ <key>OutputName</key>
228
+ <string>target</string>
229
+ </dict>
230
+ <key>WFSerializationType</key>
231
+ <string>WFTextTokenAttachment</string>
232
+ </dict>
233
+ <key>WFCountType</key>
234
+ <string>Items</string>
235
+ </dict>
236
+ </dict>
237
+ <dict>
238
+ <key>WFWorkflowActionIdentifier</key>
239
+ <string>is.workflow.actions.gettext</string>
240
+ <key>WFWorkflowActionParameters</key>
241
+ <dict>
242
+ <key>UUID</key>
243
+ <string>506724BB-49FB-5E33-A2DB-C392798691A7</string>
244
+ <key>WFTextActionText</key>
245
+ <dict>
246
+ <key>WFSerializationType</key>
247
+ <string>WFTextTokenString</string>
248
+ <key>Value</key>
249
+ <dict>
250
+ <key>string</key>
251
+ <string></string>
252
+ <key>attachmentsByRange</key>
253
+ <dict>
254
+ <key>{0, 1}</key>
255
+ <dict>
256
+ <key>Type</key>
257
+ <string>ActionOutput</string>
258
+ <key>OutputUUID</key>
259
+ <string>48769A7C-915C-5339-9CD4-09D8E576EA11</string>
260
+ <key>OutputName</key>
261
+ <string>count</string>
262
+ </dict>
263
+ </dict>
264
+ </dict>
265
+ </dict>
266
+ </dict>
267
+ </dict>
268
+ <dict>
269
+ <key>WFWorkflowActionIdentifier</key>
270
+ <string>is.workflow.actions.conditional</string>
271
+ <key>WFWorkflowActionParameters</key>
272
+ <dict>
273
+ <key>UUID</key>
274
+ <string>2DAAA123-EDA6-573F-B64C-0BE13A7920F1</string>
275
+ <key>GroupingIdentifier</key>
276
+ <string>9DEBBB46-882B-5977-87B4-3E4E05678705</string>
277
+ <key>WFControlFlowMode</key>
278
+ <integer>0</integer>
279
+ <key>WFInput</key>
280
+ <dict>
281
+ <key>Type</key>
282
+ <string>Variable</string>
283
+ <key>Variable</key>
284
+ <dict>
285
+ <key>Value</key>
286
+ <dict>
287
+ <key>Type</key>
288
+ <string>ActionOutput</string>
289
+ <key>OutputUUID</key>
290
+ <string>506724BB-49FB-5E33-A2DB-C392798691A7</string>
291
+ <key>OutputName</key>
292
+ <string>count-text</string>
293
+ </dict>
294
+ <key>WFSerializationType</key>
295
+ <string>WFTextTokenAttachment</string>
296
+ </dict>
297
+ </dict>
298
+ <key>WFCondition</key>
299
+ <integer>4</integer>
300
+ <key>WFConditionalActionString</key>
301
+ <string>1</string>
302
+ </dict>
303
+ </dict>
304
+ <dict>
305
+ <key>WFWorkflowActionIdentifier</key>
306
+ <string>is.workflow.actions.repeat.each</string>
307
+ <key>WFWorkflowActionParameters</key>
308
+ <dict>
309
+ <key>UUID</key>
310
+ <string>603F0BDF-C041-5AA4-BE51-B62DD785A079</string>
311
+ <key>GroupingIdentifier</key>
312
+ <string>3D602350-7BF4-5B87-A104-B5DB7C320E26</string>
313
+ <key>WFControlFlowMode</key>
314
+ <integer>0</integer>
315
+ <key>WFInput</key>
316
+ <dict>
317
+ <key>Value</key>
318
+ <dict>
319
+ <key>Type</key>
320
+ <string>ActionOutput</string>
321
+ <key>OutputUUID</key>
322
+ <string>40662BA1-53A6-58B0-A54E-A9174F25AB48</string>
323
+ <key>OutputName</key>
324
+ <string>tags</string>
325
+ </dict>
326
+ <key>WFSerializationType</key>
327
+ <string>WFTextTokenAttachment</string>
328
+ </dict>
329
+ </dict>
330
+ </dict>
331
+ <dict>
332
+ <key>WFWorkflowActionIdentifier</key>
333
+ <string>com.apple.Notes.CreateTagLinkAction</string>
334
+ <key>WFWorkflowActionParameters</key>
335
+ <dict>
336
+ <key>UUID</key>
337
+ <string>5AE3DC80-27B3-56AC-9668-94AC6AF3F57F</string>
338
+ <key>AppIntentDescriptor</key>
339
+ <dict>
340
+ <key>AppIntentIdentifier</key>
341
+ <string>CreateTagLinkAction</string>
342
+ <key>BundleIdentifier</key>
343
+ <string>com.apple.Notes</string>
344
+ <key>TeamIdentifier</key>
345
+ <string>0000000000</string>
346
+ <key>Name</key>
347
+ <string>Notes</string>
348
+ </dict>
349
+ <key>ShowWhenRun</key>
350
+ <false/>
351
+ <key>name</key>
352
+ <dict>
353
+ <key>WFSerializationType</key>
354
+ <string>WFTextTokenString</string>
355
+ <key>Value</key>
356
+ <dict>
357
+ <key>string</key>
358
+ <string></string>
359
+ <key>attachmentsByRange</key>
360
+ <dict>
361
+ <key>{0, 1}</key>
362
+ <dict>
363
+ <key>Type</key>
364
+ <string>Variable</string>
365
+ <key>VariableName</key>
366
+ <string>Repeat Item</string>
367
+ </dict>
368
+ </dict>
369
+ </dict>
370
+ </dict>
371
+ </dict>
372
+ </dict>
373
+ <dict>
374
+ <key>WFWorkflowActionIdentifier</key>
375
+ <string>com.apple.Notes.AddTagsToNotesLinkAction</string>
376
+ <key>WFWorkflowActionParameters</key>
377
+ <dict>
378
+ <key>UUID</key>
379
+ <string>8C0C4D73-5555-5FC6-BD29-D5B9EE299698</string>
380
+ <key>AppIntentDescriptor</key>
381
+ <dict>
382
+ <key>AppIntentIdentifier</key>
383
+ <string>AddTagsToNotesLinkAction</string>
384
+ <key>BundleIdentifier</key>
385
+ <string>com.apple.Notes</string>
386
+ <key>TeamIdentifier</key>
387
+ <string>0000000000</string>
388
+ <key>Name</key>
389
+ <string>Notes</string>
390
+ </dict>
391
+ <key>ShowWhenRun</key>
392
+ <false/>
393
+ <key>notes</key>
394
+ <dict>
395
+ <key>Value</key>
396
+ <dict>
397
+ <key>Type</key>
398
+ <string>ActionOutput</string>
399
+ <key>OutputUUID</key>
400
+ <string>DFF87C25-7AF8-55BC-BA5F-4F08CC05A668</string>
401
+ <key>OutputName</key>
402
+ <string>target</string>
403
+ </dict>
404
+ <key>WFSerializationType</key>
405
+ <string>WFTextTokenAttachment</string>
406
+ </dict>
407
+ <key>tags</key>
408
+ <dict>
409
+ <key>Value</key>
410
+ <dict>
411
+ <key>Type</key>
412
+ <string>ActionOutput</string>
413
+ <key>OutputUUID</key>
414
+ <string>5AE3DC80-27B3-56AC-9668-94AC6AF3F57F</string>
415
+ <key>OutputName</key>
416
+ <string>native-tag</string>
417
+ </dict>
418
+ <key>WFSerializationType</key>
419
+ <string>WFTextTokenAttachment</string>
420
+ </dict>
421
+ </dict>
422
+ </dict>
423
+ <dict>
424
+ <key>WFWorkflowActionIdentifier</key>
425
+ <string>is.workflow.actions.repeat.each</string>
426
+ <key>WFWorkflowActionParameters</key>
427
+ <dict>
428
+ <key>UUID</key>
429
+ <string>B9B919AF-4E05-5B76-B70B-E8EE6B502A92</string>
430
+ <key>GroupingIdentifier</key>
431
+ <string>3D602350-7BF4-5B87-A104-B5DB7C320E26</string>
432
+ <key>WFControlFlowMode</key>
433
+ <integer>2</integer>
434
+ </dict>
435
+ </dict>
436
+ <dict>
437
+ <key>WFWorkflowActionIdentifier</key>
438
+ <string>is.workflow.actions.gettext</string>
439
+ <key>WFWorkflowActionParameters</key>
440
+ <dict>
441
+ <key>UUID</key>
442
+ <string>95DA2053-00CE-52E9-95AC-CA17A0DC7031</string>
443
+ <key>WFTextActionText</key>
444
+ <string>APPLE_NOTES_TAGS_OK</string>
445
+ </dict>
446
+ </dict>
447
+ <dict>
448
+ <key>WFWorkflowActionIdentifier</key>
449
+ <string>is.workflow.actions.output</string>
450
+ <key>WFWorkflowActionParameters</key>
451
+ <dict>
452
+ <key>UUID</key>
453
+ <string>62F50E9C-D6C6-557C-A037-0E9B4D9661BC</string>
454
+ <key>WFOutput</key>
455
+ <dict>
456
+ <key>Value</key>
457
+ <dict>
458
+ <key>Type</key>
459
+ <string>ActionOutput</string>
460
+ <key>OutputUUID</key>
461
+ <string>95DA2053-00CE-52E9-95AC-CA17A0DC7031</string>
462
+ <key>OutputName</key>
463
+ <string>success</string>
464
+ </dict>
465
+ <key>WFSerializationType</key>
466
+ <string>WFTextTokenAttachment</string>
467
+ </dict>
468
+ </dict>
469
+ </dict>
470
+ <dict>
471
+ <key>WFWorkflowActionIdentifier</key>
472
+ <string>is.workflow.actions.conditional</string>
473
+ <key>WFWorkflowActionParameters</key>
474
+ <dict>
475
+ <key>UUID</key>
476
+ <string>F0968167-BF41-5F06-9942-A281BA37A28B</string>
477
+ <key>GroupingIdentifier</key>
478
+ <string>9DEBBB46-882B-5977-87B4-3E4E05678705</string>
479
+ <key>WFControlFlowMode</key>
480
+ <integer>1</integer>
481
+ </dict>
482
+ </dict>
483
+ <dict>
484
+ <key>WFWorkflowActionIdentifier</key>
485
+ <string>is.workflow.actions.gettext</string>
486
+ <key>WFWorkflowActionParameters</key>
487
+ <dict>
488
+ <key>UUID</key>
489
+ <string>38E0C0FA-E82D-55C4-BB5F-59BE5D2CDB96</string>
490
+ <key>WFTextActionText</key>
491
+ <string>APPLE_NOTES_TAGS_AMBIGUOUS_OR_MISSING</string>
492
+ </dict>
493
+ </dict>
494
+ <dict>
495
+ <key>WFWorkflowActionIdentifier</key>
496
+ <string>is.workflow.actions.output</string>
497
+ <key>WFWorkflowActionParameters</key>
498
+ <dict>
499
+ <key>UUID</key>
500
+ <string>370488B5-ABAD-5A7F-9FDF-05328430A849</string>
501
+ <key>WFOutput</key>
502
+ <dict>
503
+ <key>Value</key>
504
+ <dict>
505
+ <key>Type</key>
506
+ <string>ActionOutput</string>
507
+ <key>OutputUUID</key>
508
+ <string>38E0C0FA-E82D-55C4-BB5F-59BE5D2CDB96</string>
509
+ <key>OutputName</key>
510
+ <string>refusal</string>
511
+ </dict>
512
+ <key>WFSerializationType</key>
513
+ <string>WFTextTokenAttachment</string>
514
+ </dict>
515
+ </dict>
516
+ </dict>
517
+ <dict>
518
+ <key>WFWorkflowActionIdentifier</key>
519
+ <string>is.workflow.actions.conditional</string>
520
+ <key>WFWorkflowActionParameters</key>
521
+ <dict>
522
+ <key>UUID</key>
523
+ <string>882D0A92-5ACB-5731-B3A1-B132714EB158</string>
524
+ <key>GroupingIdentifier</key>
525
+ <string>9DEBBB46-882B-5977-87B4-3E4E05678705</string>
526
+ <key>WFControlFlowMode</key>
527
+ <integer>2</integer>
528
+ </dict>
529
+ </dict>
530
+ </array>
531
+ <key>WFWorkflowClientVersion</key>
532
+ <string>2600</string>
533
+ <key>WFWorkflowMinimumClientVersion</key>
534
+ <integer>900</integer>
535
+ <key>WFWorkflowMinimumClientVersionString</key>
536
+ <string>900</string>
537
+ <key>WFWorkflowTypes</key>
538
+ <array/>
539
+ <key>WFWorkflowHasShortcutInputVariables</key>
540
+ <true/>
541
+ <key>WFWorkflowHasOutputAction</key>
542
+ <true/>
543
+ <key>WFWorkflowInputContentItemClasses</key>
544
+ <array>
545
+ <string>WFStringContentItem</string>
546
+ <string>WFDictionaryContentItem</string>
547
+ <string>WFGenericFileContentItem</string>
548
+ </array>
549
+ <key>WFWorkflowOutputContentItemClasses</key>
550
+ <array>
551
+ <string>WFStringContentItem</string>
552
+ </array>
553
+ <key>WFWorkflowIcon</key>
554
+ <dict>
555
+ <key>WFWorkflowIconStartColor</key>
556
+ <integer>4251333119</integer>
557
+ <key>WFWorkflowIconGlyphNumber</key>
558
+ <integer>59507</integer>
559
+ </dict>
560
+ <key>WFWorkflowImportQuestions</key>
561
+ <array/>
562
+ </dict>
563
+ </plist>
@@ -0,0 +1,21 @@
1
+ # Native Tags bridge
2
+
3
+ `Apple Notes MCP - Native Tags.shortcut` adds actual native Apple Notes tags.
4
+ It contains no note contents or credentials. The unsigned source is committed
5
+ beside it and can be regenerated with:
6
+
7
+ ```sh
8
+ python3 scripts/build-native-tags-shortcut.py
9
+ shortcuts sign --mode anyone \
10
+ --input "shortcuts/Apple Notes MCP - Native Tags.unsigned.shortcut" \
11
+ --output "shortcuts/Apple Notes MCP - Native Tags.shortcut"
12
+ ```
13
+
14
+ Open the signed file and confirm **Add Shortcut** once. macOS may also request
15
+ Notes access on first execution. The `shortcuts` CLI can run an installed
16
+ workflow but cannot silently import one.
17
+
18
+ The bridge receives a private temporary JSON file with a title, a distinctive
19
+ existing scope phrase, and normalized tags. Both the server and workflow refuse
20
+ ambiguous note selection. Server readback, rather than CLI output, establishes
21
+ success and verifies that existing content survived.