apple-notes-mcp 2.8.3 → 2.8.5
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 +13 -8
- package/build/index.js +470 -107
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,10 +286,11 @@ Retrieves the full content of a specific note.
|
|
|
286
286
|
|
|
287
287
|
**Returns:** The HTML content of the note, its exact `id`, and a
|
|
288
288
|
`contentHash`. Pass that hash back as `expectedContentHash` for a later update,
|
|
289
|
-
append, or delete; the write is rejected if the note
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
append, or delete; the write is rejected if the note's body or rich metadata
|
|
290
|
+
changed after this read. With Full Disk Access, embedded URLs omitted by
|
|
291
|
+
AppleScript are restored and returned in `links`. The response also reports
|
|
292
|
+
actual `nativeTags`, `richContentComplete`, and `writable`. Textual `hashtags`
|
|
293
|
+
remain a separate field and are not proof that Notes registered native tags.
|
|
293
294
|
|
|
294
295
|
**⚠️ The returned body can be lossy — do not write it back verbatim.** Inline
|
|
295
296
|
base64 images larger than `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` (default
|
|
@@ -385,6 +386,7 @@ Updates an existing note's content and/or title.
|
|
|
385
386
|
| `newTitle` | string | No | New title (if changing the title; ignored when `format` is `"html"`) |
|
|
386
387
|
| `newContent` | string | Yes | New content for the note body |
|
|
387
388
|
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content replaces the entire note body as raw HTML and `newTitle` is ignored (the first HTML element serves as the title) |
|
|
389
|
+
| `allowLinkChanges` | boolean | No | Set to `true` only when intentionally changing or removing existing links |
|
|
388
390
|
|
|
389
391
|
Title-only updates are rejected because Apple Notes titles are not unique.
|
|
390
392
|
|
|
@@ -414,8 +416,10 @@ byte-identical rich formatting.
|
|
|
414
416
|
|
|
415
417
|
**Note:** `newContent` **replaces the entire note body** — it is not appended. To add to a note, prefer [`append-to-note`](#append-to-note), which does the read-and-concatenate for you and always round-trips the body as HTML. If you do read-modify-write by hand, note that `get-note-content` replaces oversized inline images with text placeholders (see [`get-note-content`](#get-note-content)) — writing that body back bakes the placeholders in.
|
|
416
418
|
|
|
417
|
-
**
|
|
418
|
-
|
|
419
|
+
**Rich-content safety:** `update-note` refuses to replace a note when its rich
|
|
420
|
+
metadata is unavailable or it contains attachments, native tags, inline
|
|
421
|
+
objects, or checklists that AppleScript cannot preserve. Existing link
|
|
422
|
+
destinations must remain present unless `allowLinkChanges` is explicitly set.
|
|
419
423
|
|
|
420
424
|
---
|
|
421
425
|
|
|
@@ -509,8 +513,9 @@ Title-only appends are rejected.
|
|
|
509
513
|
visible text after saving, not byte-identical rich formatting. Warns when the
|
|
510
514
|
note is shared with collaborators.
|
|
511
515
|
|
|
512
|
-
**Safety:** The append is rejected if the note changed since it was read
|
|
513
|
-
the note contains
|
|
516
|
+
**Safety:** The append is rejected if the note changed since it was read, rich
|
|
517
|
+
metadata is unavailable, or the note contains attachments or other native
|
|
518
|
+
objects. Existing link destinations are verified after saving.
|
|
514
519
|
|
|
515
520
|
---
|
|
516
521
|
|
package/build/index.js
CHANGED
|
@@ -4199,8 +4199,8 @@ var require_fast_uri = __commonJS({
|
|
|
4199
4199
|
} catch {
|
|
4200
4200
|
return void 0;
|
|
4201
4201
|
}
|
|
4202
|
-
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
|
|
4203
|
-
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 :
|
|
4202
|
+
const { normalized: normalized2, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
|
|
4203
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized2;
|
|
4204
4204
|
}
|
|
4205
4205
|
var fastUri = {
|
|
4206
4206
|
SCHEMES,
|
|
@@ -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
|
|
24468
|
+
return join8(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 =
|
|
24475
|
+
output = join8(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
|
|
24487
|
+
function join8(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);
|
|
@@ -30297,7 +30297,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
30297
30297
|
});
|
|
30298
30298
|
const generateFastpass = (shape) => {
|
|
30299
30299
|
const doc = new Doc(["shape", "payload", "ctx"]);
|
|
30300
|
-
const
|
|
30300
|
+
const normalized2 = _normalized.value;
|
|
30301
30301
|
const parseStr = (key) => {
|
|
30302
30302
|
const k = esc(key);
|
|
30303
30303
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
@@ -30305,12 +30305,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
30305
30305
|
doc.write(`const input = payload.value;`);
|
|
30306
30306
|
const ids = /* @__PURE__ */ Object.create(null);
|
|
30307
30307
|
let counter = 0;
|
|
30308
|
-
for (const key of
|
|
30308
|
+
for (const key of normalized2.keys) {
|
|
30309
30309
|
ids[key] = `key_${counter++}`;
|
|
30310
30310
|
}
|
|
30311
30311
|
doc.write(`const newResult = {}`);
|
|
30312
|
-
for (const key of
|
|
30313
|
-
if (
|
|
30312
|
+
for (const key of normalized2.keys) {
|
|
30313
|
+
if (normalized2.optionalKeys.has(key)) {
|
|
30314
30314
|
const id = ids[key];
|
|
30315
30315
|
doc.write(`const ${id} = ${parseStr(key)};`);
|
|
30316
30316
|
const k = esc(key);
|
|
@@ -39484,6 +39484,311 @@ function getChecklistItems(noteId) {
|
|
|
39484
39484
|
return { items };
|
|
39485
39485
|
}
|
|
39486
39486
|
|
|
39487
|
+
// src/utils/noteRichText.ts
|
|
39488
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
39489
|
+
import { createHash } from "node:crypto";
|
|
39490
|
+
import { homedir as homedir2 } from "node:os";
|
|
39491
|
+
import { join as join2 } from "node:path";
|
|
39492
|
+
import { gunzipSync as gunzipSync2 } from "node:zlib";
|
|
39493
|
+
var dbPath = join2(homedir2(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
|
|
39494
|
+
var safeUrl = (url) => /^(?:https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url) && !Array.from(url).some((char) => char.charCodeAt(0) < 32);
|
|
39495
|
+
var escapeAttribute = (text) => text.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
39496
|
+
var normalized = (text) => text.replace(/[\s\ufffc]/gu, "");
|
|
39497
|
+
function styleValue(field) {
|
|
39498
|
+
if (!(field.value instanceof Uint8Array)) return field.value;
|
|
39499
|
+
if (field.fieldNumber === 2) {
|
|
39500
|
+
const data = field.value, kept = [];
|
|
39501
|
+
let offset = 0;
|
|
39502
|
+
try {
|
|
39503
|
+
while (offset < data.length) {
|
|
39504
|
+
const start = offset;
|
|
39505
|
+
let tag, length = -1;
|
|
39506
|
+
[tag, offset] = decodeVarint(data, offset);
|
|
39507
|
+
const wire = tag & 7;
|
|
39508
|
+
if (wire === 0) [, offset] = decodeVarint(data, offset);
|
|
39509
|
+
else if (wire === 1) offset += 8;
|
|
39510
|
+
else if (wire === 5) offset += 4;
|
|
39511
|
+
else if (wire === 2) {
|
|
39512
|
+
[length, offset] = decodeVarint(data, offset);
|
|
39513
|
+
offset += length;
|
|
39514
|
+
} else return Buffer.from(data).toString("hex");
|
|
39515
|
+
if (offset > data.length) return Buffer.from(data).toString("hex");
|
|
39516
|
+
if (!(tag >>> 3 === 9 && wire === 2 && length === 16)) kept.push(data.slice(start, offset));
|
|
39517
|
+
}
|
|
39518
|
+
return Buffer.concat(kept).toString("hex");
|
|
39519
|
+
} catch {
|
|
39520
|
+
return Buffer.from(data).toString("hex");
|
|
39521
|
+
}
|
|
39522
|
+
}
|
|
39523
|
+
return Buffer.from(field.value).toString("hex");
|
|
39524
|
+
}
|
|
39525
|
+
function parseRichNote(data, nativeTags = []) {
|
|
39526
|
+
const doc = decodeMessage(data);
|
|
39527
|
+
const wrapper = embeddedMessage(getField(doc, 2));
|
|
39528
|
+
const body = wrapper && embeddedMessage(getField(wrapper, 3));
|
|
39529
|
+
const text = body && stringValue(getField(body, 2));
|
|
39530
|
+
if (!body || text === void 0) throw new Error("Unsupported Notes document structure");
|
|
39531
|
+
const links = [];
|
|
39532
|
+
const nativeObjectIds = [];
|
|
39533
|
+
const objects = [];
|
|
39534
|
+
const checklistItems = [];
|
|
39535
|
+
const styleRuns = [];
|
|
39536
|
+
let position = 0;
|
|
39537
|
+
let hasNativeObjects = false;
|
|
39538
|
+
let hasChecklist = false;
|
|
39539
|
+
for (const run of getFields(body, 5)) {
|
|
39540
|
+
const fields = embeddedMessage(run);
|
|
39541
|
+
if (!fields) throw new Error("Invalid Notes attribute run");
|
|
39542
|
+
const length = varintValue(getField(fields, 1));
|
|
39543
|
+
if (length === void 0 || length < 0 || position + length > text.length)
|
|
39544
|
+
throw new Error("Invalid Notes run length");
|
|
39545
|
+
styleRuns.push({
|
|
39546
|
+
start: position,
|
|
39547
|
+
length,
|
|
39548
|
+
signature: JSON.stringify(
|
|
39549
|
+
fields.filter((f) => f.fieldNumber >= 2 && f.fieldNumber <= 12 || f.fieldNumber === 14).map((f) => [f.fieldNumber, styleValue(f)])
|
|
39550
|
+
)
|
|
39551
|
+
});
|
|
39552
|
+
const url = stringValue(getField(fields, 9));
|
|
39553
|
+
if (url) {
|
|
39554
|
+
if (!safeUrl(url)) throw new Error("Unsupported link scheme in note");
|
|
39555
|
+
const previous = links.at(-1);
|
|
39556
|
+
if (previous?.url === url && previous.start + previous.length === position) {
|
|
39557
|
+
previous.length += length;
|
|
39558
|
+
previous.text += text.slice(position, position + length);
|
|
39559
|
+
} else
|
|
39560
|
+
links.push({ start: position, length, text: text.slice(position, position + length), url });
|
|
39561
|
+
}
|
|
39562
|
+
hasNativeObjects ||= Boolean(getField(fields, 12));
|
|
39563
|
+
const attachment = embeddedMessage(getField(fields, 12));
|
|
39564
|
+
const attachmentId = attachment && stringValue(getField(attachment, 1));
|
|
39565
|
+
if (attachmentId) nativeObjectIds.push(attachmentId);
|
|
39566
|
+
if (attachmentId)
|
|
39567
|
+
objects.push({
|
|
39568
|
+
id: attachmentId,
|
|
39569
|
+
type: stringValue(getField(attachment, 2)) || "unknown",
|
|
39570
|
+
start: position,
|
|
39571
|
+
length
|
|
39572
|
+
});
|
|
39573
|
+
const paragraph = embeddedMessage(getField(fields, 2));
|
|
39574
|
+
hasChecklist ||= Boolean(paragraph && varintValue(getField(paragraph, 1)) === 103);
|
|
39575
|
+
if (paragraph && varintValue(getField(paragraph, 1)) === 103) {
|
|
39576
|
+
const checklist = embeddedMessage(getField(paragraph, 5));
|
|
39577
|
+
const rawId = checklist && getField(checklist, 1)?.value;
|
|
39578
|
+
const itemId = rawId instanceof Uint8Array ? Buffer.from(rawId).toString("hex") : "";
|
|
39579
|
+
const start = text.lastIndexOf("\n", position - 1) + 1;
|
|
39580
|
+
if (itemId && !checklistItems.some((item) => item.id === itemId))
|
|
39581
|
+
checklistItems.push({
|
|
39582
|
+
id: itemId,
|
|
39583
|
+
start,
|
|
39584
|
+
text: text.slice(
|
|
39585
|
+
start,
|
|
39586
|
+
text.indexOf("\n", start) === -1 ? text.length : text.indexOf("\n", start)
|
|
39587
|
+
),
|
|
39588
|
+
done: checklist ? varintValue(getField(checklist, 2)) === 1 : false
|
|
39589
|
+
});
|
|
39590
|
+
}
|
|
39591
|
+
position += length;
|
|
39592
|
+
}
|
|
39593
|
+
if (position !== text.length) throw new Error("Incomplete Notes attribute runs");
|
|
39594
|
+
return {
|
|
39595
|
+
text,
|
|
39596
|
+
links,
|
|
39597
|
+
nativeTags: hasNativeObjects ? nativeTags : [],
|
|
39598
|
+
nativeObjectIds,
|
|
39599
|
+
hasNativeObjects,
|
|
39600
|
+
hasChecklist,
|
|
39601
|
+
revision: createHash("sha256").update(data).digest("hex"),
|
|
39602
|
+
objects,
|
|
39603
|
+
checklistItems,
|
|
39604
|
+
styleRuns
|
|
39605
|
+
};
|
|
39606
|
+
}
|
|
39607
|
+
function readRichNote(id) {
|
|
39608
|
+
const pk = /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p([0-9]+)$/i.exec(id)?.[1];
|
|
39609
|
+
if (!pk) throw new Error("Invalid exact note ID");
|
|
39610
|
+
const sql = `BEGIN; SELECT hex(ZDATA) FROM ZICNOTEDATA WHERE ZNOTE=${pk}; SELECT json_group_object(ZIDENTIFIER,ZALTTEXT) FROM ZICCLOUDSYNCINGOBJECT WHERE ZNOTE1=${pk} AND ZTYPEUTI1='com.apple.notes.inlinetextattachment.hashtag'; SELECT json_group_array(json_object('id',ZIDENTIFIER,'pk',Z_PK,'type',COALESCE(ZTYPEUTI1,ZTYPEUTI),'mergeable',hex(COALESCE(ZMERGEABLEDATA1,ZMERGEABLEDATA)),'view',ZATTACHMENTVIEWTYPE)) FROM ZICCLOUDSYNCINGOBJECT WHERE ZNOTE1=${pk} OR ZNOTE=${pk}; COMMIT;`;
|
|
39611
|
+
const rows = execFileSync3("/usr/bin/sqlite3", ["-readonly", dbPath, sql], {
|
|
39612
|
+
encoding: "utf8",
|
|
39613
|
+
timeout: 5e3,
|
|
39614
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
39615
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39616
|
+
}).trim().split("\n");
|
|
39617
|
+
if (!rows[0] || !/^[0-9a-f]+$/i.test(rows[0])) throw new Error("No Notes document data");
|
|
39618
|
+
const tags = JSON.parse(rows[1] || "{}");
|
|
39619
|
+
if (!tags || Array.isArray(tags) || typeof tags !== "object" || Object.values(tags).some((tag) => typeof tag !== "string"))
|
|
39620
|
+
throw new Error("Invalid native tags");
|
|
39621
|
+
const rich = parseRichNote(
|
|
39622
|
+
gunzipSync2(Buffer.from(rows[0], "hex"), { maxOutputLength: 32 * 1024 * 1024 })
|
|
39623
|
+
);
|
|
39624
|
+
const tagMap = tags;
|
|
39625
|
+
const objectData = JSON.parse(rows[2] || "[]");
|
|
39626
|
+
if (!Array.isArray(objectData) || objectData.some(
|
|
39627
|
+
(row) => !row || typeof row.id !== "string" || !Number.isInteger(row.pk) || typeof row.mergeable !== "string" || !/^[0-9a-f]*$/i.test(row.mergeable)
|
|
39628
|
+
))
|
|
39629
|
+
throw new Error("Invalid native object metadata");
|
|
39630
|
+
rich.objectData = objectData.filter((row) => rich.nativeObjectIds.includes(row.id)).sort((a, b) => a.id.localeCompare(b.id));
|
|
39631
|
+
rich.revision = createHash("sha256").update(rich.revision).update(JSON.stringify(rich.objectData)).digest("hex");
|
|
39632
|
+
rich.nativeTagObjectIds = {};
|
|
39633
|
+
for (const id2 of rich.nativeObjectIds)
|
|
39634
|
+
if (tagMap[id2]) {
|
|
39635
|
+
const tag = tagMap[id2].replace(/^#/, "");
|
|
39636
|
+
(rich.nativeTagObjectIds[tag] ||= []).push(id2);
|
|
39637
|
+
}
|
|
39638
|
+
rich.nativeTags = [
|
|
39639
|
+
...new Set(
|
|
39640
|
+
rich.nativeObjectIds.flatMap((id2) => tagMap[id2] ? [tagMap[id2].replace(/^#/, "")] : [])
|
|
39641
|
+
)
|
|
39642
|
+
];
|
|
39643
|
+
return rich;
|
|
39644
|
+
}
|
|
39645
|
+
function decodeEntity(value) {
|
|
39646
|
+
const named = {
|
|
39647
|
+
amp: "&",
|
|
39648
|
+
lt: "<",
|
|
39649
|
+
gt: ">",
|
|
39650
|
+
quot: '"',
|
|
39651
|
+
apos: "'",
|
|
39652
|
+
nbsp: " "
|
|
39653
|
+
};
|
|
39654
|
+
if (!value.startsWith("&") || value === "&") return value;
|
|
39655
|
+
const name = value.slice(1).replace(/;$/, "");
|
|
39656
|
+
if (name.startsWith("#")) {
|
|
39657
|
+
const cp = name[1]?.toLowerCase() === "x" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10);
|
|
39658
|
+
if (!Number.isInteger(cp) || cp < 0 || cp > 1114111) throw new Error("Invalid HTML entity");
|
|
39659
|
+
return String.fromCodePoint(cp);
|
|
39660
|
+
}
|
|
39661
|
+
if (!(name in named)) throw new Error("Unsupported HTML entity");
|
|
39662
|
+
return named[name];
|
|
39663
|
+
}
|
|
39664
|
+
function visibleCharacters(html) {
|
|
39665
|
+
const chars = [];
|
|
39666
|
+
let token = 0;
|
|
39667
|
+
for (const part of html.matchAll(/<[^>]*>|[^<]+/g)) {
|
|
39668
|
+
token++;
|
|
39669
|
+
if (part[0].startsWith("<")) continue;
|
|
39670
|
+
for (const item of part[0].matchAll(
|
|
39671
|
+
/&(?:#[0-9]+;?|#x[0-9a-f]+;?|(?:amp|lt|gt|quot|apos|nbsp)(?:;|(?![a-z0-9=])))|[\s\S]/gi
|
|
39672
|
+
)) {
|
|
39673
|
+
const value = decodeEntity(item[0]);
|
|
39674
|
+
for (let i = 0; i < value.length; i++) {
|
|
39675
|
+
if (/\s/u.test(value[i])) continue;
|
|
39676
|
+
chars.push({
|
|
39677
|
+
value: value[i],
|
|
39678
|
+
start: part.index + item.index,
|
|
39679
|
+
end: part.index + item.index + item[0].length,
|
|
39680
|
+
token
|
|
39681
|
+
});
|
|
39682
|
+
}
|
|
39683
|
+
}
|
|
39684
|
+
}
|
|
39685
|
+
return chars;
|
|
39686
|
+
}
|
|
39687
|
+
function restoreNoteLinks(html, rich) {
|
|
39688
|
+
const base = html.replace(/<\/?a\b[^>]*>/gi, "");
|
|
39689
|
+
const chars = visibleCharacters(base);
|
|
39690
|
+
if (chars.map((c) => c.value).join("") !== normalized(rich.text))
|
|
39691
|
+
throw new Error("Notes HTML and rich text do not match; retry after sync");
|
|
39692
|
+
const positions = [];
|
|
39693
|
+
for (let i = 0; i < rich.text.length; i++)
|
|
39694
|
+
if (!/[\s\ufffc]/u.test(rich.text[i])) positions.push(i);
|
|
39695
|
+
const inserts = [];
|
|
39696
|
+
for (const link of rich.links) {
|
|
39697
|
+
if (!safeUrl(link.url)) throw new Error("Unsupported link scheme in note");
|
|
39698
|
+
let span;
|
|
39699
|
+
for (let i = 0; i < chars.length; i++) {
|
|
39700
|
+
if (positions[i] < link.start || positions[i] >= link.start + link.length) continue;
|
|
39701
|
+
const c = chars[i];
|
|
39702
|
+
if (span?.token === c.token) span.end = c.end;
|
|
39703
|
+
else {
|
|
39704
|
+
if (span) inserts.push(span);
|
|
39705
|
+
span = { start: c.start, end: c.end, token: c.token, url: link.url };
|
|
39706
|
+
}
|
|
39707
|
+
}
|
|
39708
|
+
if (span) inserts.push(span);
|
|
39709
|
+
}
|
|
39710
|
+
let result = base;
|
|
39711
|
+
for (const span of inserts.sort((a, b) => b.start - a.start))
|
|
39712
|
+
result = result.slice(0, span.start) + `<a href="${escapeAttribute(span.url)}">` + result.slice(span.start, span.end) + "</a>" + result.slice(span.end);
|
|
39713
|
+
return result;
|
|
39714
|
+
}
|
|
39715
|
+
function enrichNoteRead(id, rawBody) {
|
|
39716
|
+
let metadata;
|
|
39717
|
+
try {
|
|
39718
|
+
const rich = readRichNote(id);
|
|
39719
|
+
metadata = rich;
|
|
39720
|
+
const content = restoreNoteLinks(rawBody, rich);
|
|
39721
|
+
const writable = !rich.hasNativeObjects && !rich.hasChecklist;
|
|
39722
|
+
return {
|
|
39723
|
+
content,
|
|
39724
|
+
links: rich.links,
|
|
39725
|
+
nativeTags: rich.nativeTags,
|
|
39726
|
+
complete: writable,
|
|
39727
|
+
writable,
|
|
39728
|
+
revision: rich.revision,
|
|
39729
|
+
...!writable ? {
|
|
39730
|
+
warning: "Native tags, inline objects or checklists are present. Their state is not writable through AppleScript; full-body edits are blocked to preserve them."
|
|
39731
|
+
} : {}
|
|
39732
|
+
};
|
|
39733
|
+
} catch {
|
|
39734
|
+
return {
|
|
39735
|
+
content: rawBody,
|
|
39736
|
+
links: metadata?.links ?? [],
|
|
39737
|
+
nativeTags: metadata?.nativeTags ?? [],
|
|
39738
|
+
complete: false,
|
|
39739
|
+
writable: false,
|
|
39740
|
+
revision: metadata?.revision ?? "unavailable",
|
|
39741
|
+
warning: "Rich Notes metadata could not be read or matched. Links/native tags may be missing from this view. Full-body edits are blocked; check Full Disk Access and retry after sync."
|
|
39742
|
+
};
|
|
39743
|
+
}
|
|
39744
|
+
}
|
|
39745
|
+
function richContentHash(rawBody, rich) {
|
|
39746
|
+
return `sha256:${createHash("sha256").update(rawBody).update("\0").update(rich.revision).digest("hex")}`;
|
|
39747
|
+
}
|
|
39748
|
+
function htmlLinks(html) {
|
|
39749
|
+
const links = [];
|
|
39750
|
+
for (const match of html.matchAll(
|
|
39751
|
+
/<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi
|
|
39752
|
+
)) {
|
|
39753
|
+
const url = (match[1] ?? match[2]).replace(/&(?:#[0-9]+|#x[0-9a-f]+|[a-z]+);/gi, decodeEntity);
|
|
39754
|
+
if (!safeUrl(url)) throw new Error("Unsupported link scheme");
|
|
39755
|
+
links.push({
|
|
39756
|
+
text: visibleCharacters(match[3]).map((c) => c.value).join(""),
|
|
39757
|
+
url
|
|
39758
|
+
});
|
|
39759
|
+
}
|
|
39760
|
+
return links;
|
|
39761
|
+
}
|
|
39762
|
+
function linkSignature(links) {
|
|
39763
|
+
return JSON.stringify(
|
|
39764
|
+
links.flatMap(
|
|
39765
|
+
(link) => normalized(link.text).split("").map((char) => [char, link.url])
|
|
39766
|
+
)
|
|
39767
|
+
);
|
|
39768
|
+
}
|
|
39769
|
+
function assertLinkedWrite(rich, content, format, allowLinkChanges = false) {
|
|
39770
|
+
if (!rich.writable) throw new Error(rich.warning || "Rich note cannot be safely rewritten");
|
|
39771
|
+
if (rich.links.length && format !== "html" && !allowLinkChanges)
|
|
39772
|
+
throw new Error(
|
|
39773
|
+
"This note has links. Use format=html and preserve the linked HTML returned by get-note-content."
|
|
39774
|
+
);
|
|
39775
|
+
const next = format === "html" ? htmlLinks(content) : [];
|
|
39776
|
+
if (allowLinkChanges) return;
|
|
39777
|
+
const needed = /* @__PURE__ */ new Map();
|
|
39778
|
+
for (const link of rich.links) {
|
|
39779
|
+
const key = linkSignature([link]);
|
|
39780
|
+
needed.set(key, (needed.get(key) || 0) + 1);
|
|
39781
|
+
}
|
|
39782
|
+
const incoming = linkSignature(next);
|
|
39783
|
+
for (const [key, count] of needed) {
|
|
39784
|
+
const sequence = key.slice(1, -1);
|
|
39785
|
+
if (incoming.split(sequence).length - 1 < count)
|
|
39786
|
+
throw new Error(
|
|
39787
|
+
"Update would remove or change an existing link. Preserve its label and URL from get-note-content."
|
|
39788
|
+
);
|
|
39789
|
+
}
|
|
39790
|
+
}
|
|
39791
|
+
|
|
39487
39792
|
// src/utils/attachmentFs.ts
|
|
39488
39793
|
import {
|
|
39489
39794
|
existsSync as existsSync2,
|
|
@@ -39495,11 +39800,11 @@ import {
|
|
|
39495
39800
|
rmSync,
|
|
39496
39801
|
statSync
|
|
39497
39802
|
} from "fs";
|
|
39498
|
-
import { dirname, isAbsolute, join as
|
|
39499
|
-
import { homedir as
|
|
39803
|
+
import { dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
|
|
39804
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
39500
39805
|
function allowedSaveRoots() {
|
|
39501
39806
|
return [
|
|
39502
|
-
resolve(
|
|
39807
|
+
resolve(homedir3()),
|
|
39503
39808
|
resolve(tmpdir()),
|
|
39504
39809
|
"/Volumes",
|
|
39505
39810
|
"/private/var/folders",
|
|
@@ -39573,7 +39878,7 @@ function assertSafeSavePath(p, roots = allowedSaveRoots()) {
|
|
|
39573
39878
|
if (suffix.split(sep).includes("..")) {
|
|
39574
39879
|
throw new Error(`Refusing to write outside allowed locations (home, temp, /Volumes): "${abs}"`);
|
|
39575
39880
|
}
|
|
39576
|
-
const canonicalDest = suffix ?
|
|
39881
|
+
const canonicalDest = suffix ? join3(canonicalAncestor, suffix) : canonicalAncestor;
|
|
39577
39882
|
const allowed = canonicalRoots(roots);
|
|
39578
39883
|
if (!isWithinRoots(canonicalAncestor, allowed) || !isWithinRoots(canonicalDest, allowed)) {
|
|
39579
39884
|
throw new Error(
|
|
@@ -39623,8 +39928,8 @@ function cleanupTempDir(dir) {
|
|
|
39623
39928
|
// src/services/appleNotesManager.ts
|
|
39624
39929
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
39625
39930
|
import { existsSync as existsSync3 } from "fs";
|
|
39626
|
-
import { homedir as
|
|
39627
|
-
import { join as
|
|
39931
|
+
import { homedir as homedir4 } from "os";
|
|
39932
|
+
import { join as join4 } from "path";
|
|
39628
39933
|
var FIELD_SEP = "";
|
|
39629
39934
|
var RECORD_SEP = "";
|
|
39630
39935
|
var AS_FIELD_SEP = "(ASCII character 31)";
|
|
@@ -39702,8 +40007,8 @@ function parseAppleScriptDate(appleScriptDate) {
|
|
|
39702
40007
|
return isNaN(dt.getTime()) ? /* @__PURE__ */ new Date() : dt;
|
|
39703
40008
|
}
|
|
39704
40009
|
const withoutPrefix = s.replace(/^date\s+/, "");
|
|
39705
|
-
const
|
|
39706
|
-
const parsed = new Date(
|
|
40010
|
+
const normalized2 = withoutPrefix.replace(" at ", " ");
|
|
40011
|
+
const parsed = new Date(normalized2);
|
|
39707
40012
|
return isNaN(parsed.getTime()) ? /* @__PURE__ */ new Date() : parsed;
|
|
39708
40013
|
}
|
|
39709
40014
|
function buildAppleScriptDateVar(date3, varName = "thresholdDate") {
|
|
@@ -39825,11 +40130,11 @@ function getNoteLinkFromDB(coreDataId) {
|
|
|
39825
40130
|
const match = coreDataId.match(/\/p(\d+)$/);
|
|
39826
40131
|
if (!match) return null;
|
|
39827
40132
|
const pk = parseInt(match[1], 10);
|
|
39828
|
-
const
|
|
39829
|
-
if (!existsSync3(
|
|
40133
|
+
const dbPath2 = join4(homedir4(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
|
|
40134
|
+
if (!existsSync3(dbPath2)) return null;
|
|
39830
40135
|
try {
|
|
39831
40136
|
const { DatabaseSync } = __require("node:sqlite");
|
|
39832
|
-
const db = new DatabaseSync(
|
|
40137
|
+
const db = new DatabaseSync(dbPath2, { readOnly: true });
|
|
39833
40138
|
try {
|
|
39834
40139
|
const row = db.prepare("SELECT ZIDENTIFIER FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ?").get(pk);
|
|
39835
40140
|
const identifier = row?.ZIDENTIFIER;
|
|
@@ -40320,8 +40625,13 @@ var AppleNotesManager = class {
|
|
|
40320
40625
|
* race that would exist if JavaScript checked the note and then issued a
|
|
40321
40626
|
* separate unconditional `set body` command.
|
|
40322
40627
|
*/
|
|
40323
|
-
updateNoteByIdIfUnchanged(id, currentTitle, expectedBody, newTitle, newContent, format = "plaintext") {
|
|
40628
|
+
updateNoteByIdIfUnchanged(id, currentTitle, expectedBody, newTitle, newContent, format = "plaintext", expectedRichRevision) {
|
|
40324
40629
|
const safeId = sanitizeNoteId(id);
|
|
40630
|
+
if (expectedRichRevision) {
|
|
40631
|
+
const rich = readRichNote(id);
|
|
40632
|
+
if (rich.revision !== expectedRichRevision) return { status: "conflict" };
|
|
40633
|
+
if (rich.hasNativeObjects || rich.hasChecklist) return { status: "attachments" };
|
|
40634
|
+
}
|
|
40325
40635
|
if (newTitle) validateLength(newTitle, MAX_TITLE_LENGTH, "Note title");
|
|
40326
40636
|
validateLength(newContent, MAX_CONTENT_LENGTH, "Note content");
|
|
40327
40637
|
validateLength(expectedBody, MAX_CONTENT_LENGTH, "Expected note content");
|
|
@@ -41887,8 +42197,9 @@ var AppleNotesManager = class {
|
|
|
41887
42197
|
getNoteMarkdown(title, account) {
|
|
41888
42198
|
const html = this.getNoteContent(title, account);
|
|
41889
42199
|
if (!html) return "";
|
|
41890
|
-
let markdown = this.htmlToMarkdown(html);
|
|
41891
42200
|
const note = this.getNoteDetails(title, account);
|
|
42201
|
+
const rich = note?.id ? enrichNoteRead(note.id, html) : void 0;
|
|
42202
|
+
let markdown = this.htmlToMarkdown(rich?.content || html);
|
|
41892
42203
|
if (note?.id) {
|
|
41893
42204
|
const result = getChecklistItems(note.id);
|
|
41894
42205
|
if (result.items) {
|
|
@@ -41913,7 +42224,8 @@ var AppleNotesManager = class {
|
|
|
41913
42224
|
getNoteMarkdownById(id) {
|
|
41914
42225
|
const html = this.getNoteContentById(id);
|
|
41915
42226
|
if (!html) return "";
|
|
41916
|
-
|
|
42227
|
+
const rich = enrichNoteRead(id, html);
|
|
42228
|
+
let markdown = this.htmlToMarkdown(rich.content);
|
|
41917
42229
|
const result = getChecklistItems(id);
|
|
41918
42230
|
if (result.items) {
|
|
41919
42231
|
markdown = this.enrichMarkdownWithChecklists(markdown, result.items);
|
|
@@ -41923,7 +42235,7 @@ var AppleNotesManager = class {
|
|
|
41923
42235
|
};
|
|
41924
42236
|
|
|
41925
42237
|
// src/utils/syncDetection.ts
|
|
41926
|
-
import { execFileSync as
|
|
42238
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
41927
42239
|
import * as fs2 from "fs";
|
|
41928
42240
|
import * as path2 from "path";
|
|
41929
42241
|
import * as os2 from "os";
|
|
@@ -41968,7 +42280,7 @@ function getSyncStatus(useCache = true) {
|
|
|
41968
42280
|
WHERE object.ZCLOUDSTATE = state.Z_PK
|
|
41969
42281
|
);
|
|
41970
42282
|
`;
|
|
41971
|
-
const result =
|
|
42283
|
+
const result = execFileSync4(
|
|
41972
42284
|
"sqlite3",
|
|
41973
42285
|
["-readonly", NOTES_DB_PATH2, query.replace(/\n/g, " ")],
|
|
41974
42286
|
{
|
|
@@ -42027,7 +42339,7 @@ function withSyncAwarenessSync(operation, fn) {
|
|
|
42027
42339
|
}
|
|
42028
42340
|
|
|
42029
42341
|
// src/utils/noteMetadata.ts
|
|
42030
|
-
import { execFileSync as
|
|
42342
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
42031
42343
|
import * as fs3 from "fs";
|
|
42032
42344
|
import * as path3 from "path";
|
|
42033
42345
|
import * as os3 from "os";
|
|
@@ -42048,7 +42360,7 @@ var COLUMN_MAP = [
|
|
|
42048
42360
|
{ key: "smartFolderQuery", column: "ZSMARTFOLDERQUERYJSON", type: "text" }
|
|
42049
42361
|
];
|
|
42050
42362
|
function runSqlite(query) {
|
|
42051
|
-
return
|
|
42363
|
+
return execFileSync5("sqlite3", ["-readonly", NOTES_DB_PATH3, query], {
|
|
42052
42364
|
encoding: "utf8",
|
|
42053
42365
|
timeout: 5e3,
|
|
42054
42366
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -42319,12 +42631,12 @@ function formatDoctorReport(r) {
|
|
|
42319
42631
|
|
|
42320
42632
|
// src/services/fileConfig.ts
|
|
42321
42633
|
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
42322
|
-
import { join as
|
|
42323
|
-
import { homedir as
|
|
42634
|
+
import { join as join7 } from "path";
|
|
42635
|
+
import { homedir as homedir7 } from "os";
|
|
42324
42636
|
function fileConfigPath(env = process.env) {
|
|
42325
42637
|
const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
|
|
42326
42638
|
if (override && override.trim()) return override.trim();
|
|
42327
|
-
return
|
|
42639
|
+
return join7(homedir7(), "Library", "Application Support", "apple-notes-mcp", "config.json");
|
|
42328
42640
|
}
|
|
42329
42641
|
function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
|
|
42330
42642
|
const applied = [];
|
|
@@ -42532,12 +42844,9 @@ function withJsonSchema2020_12(transport2) {
|
|
|
42532
42844
|
}
|
|
42533
42845
|
|
|
42534
42846
|
// src/utils/noteRevision.ts
|
|
42535
|
-
|
|
42536
|
-
function hashNoteContent(content) {
|
|
42537
|
-
return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
42538
|
-
}
|
|
42847
|
+
var INLINE_TAG = /^<\/?(?:b|i|u|s|strike|em|strong|span|a|font|sub|sup|code|tt|small|big|mark)\b/i;
|
|
42539
42848
|
function comparableVisibleText(html) {
|
|
42540
|
-
return html.replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, " ").replace(/ | /gi, " ").replace(/"/gi, '"').replace(/'|'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&").replace(/&#(\d+);/g, (_match, codePoint) => String.fromCodePoint(Number(codePoint))).replace(
|
|
42849
|
+
return html.replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, (tag) => INLINE_TAG.test(tag) ? "" : " ").replace(/ | /gi, " ").replace(/"/gi, '"').replace(/'|'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&").replace(/&#(\d+);/g, (_match, codePoint) => String.fromCodePoint(Number(codePoint))).replace(
|
|
42541
42850
|
/&#x([0-9a-f]+);/gi,
|
|
42542
42851
|
(_match, codePoint) => String.fromCodePoint(Number.parseInt(codePoint, 16))
|
|
42543
42852
|
).replace(/\s+/g, " ").trim();
|
|
@@ -42610,7 +42919,8 @@ function readExactNoteSnapshot(id) {
|
|
|
42610
42919
|
}
|
|
42611
42920
|
const body = notesManager.getNoteContentById(id);
|
|
42612
42921
|
if (!body) return { error: `Failed to read content of note "${note.title}"` };
|
|
42613
|
-
|
|
42922
|
+
const rich = enrichNoteRead(id, body);
|
|
42923
|
+
return { note, body, rich, contentHash: richContentHash(body, rich) };
|
|
42614
42924
|
}
|
|
42615
42925
|
function revisionConflictMessage(title) {
|
|
42616
42926
|
return `Note "${title}" changed after it was read. Read it again and review the newer version before retrying.`;
|
|
@@ -42640,7 +42950,7 @@ registerTool(
|
|
|
42640
42950
|
),
|
|
42641
42951
|
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
42642
42952
|
tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
|
|
42643
|
-
"Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags
|
|
42953
|
+
"Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags in the body for searchable text; this does not create native tag objects. Native tags need the Notes Shortcuts action."
|
|
42644
42954
|
),
|
|
42645
42955
|
folder: external_exports.string().max(MAX.FOLDER).optional().describe(
|
|
42646
42956
|
"Folder to create the note in (supports nested paths like 'Work/Clients'). The folder must already exist \u2014 this tool does not create it; call create-folder first, which is idempotent and creates intermediate segments."
|
|
@@ -42674,7 +42984,7 @@ registerTool(
|
|
|
42674
42984
|
`A note may have been created, but its exact ID could not be verified. Do not retry automatically. Returned ID: ${note.id}`
|
|
42675
42985
|
);
|
|
42676
42986
|
}
|
|
42677
|
-
const contentHash =
|
|
42987
|
+
const contentHash = richContentHash(createdBody, enrichNoteRead(note.id, createdBody));
|
|
42678
42988
|
const checklistWarning = detectChecklistAttempt(content) ?? "";
|
|
42679
42989
|
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`, {
|
|
42680
42990
|
ok: true,
|
|
@@ -42763,7 +43073,7 @@ ${noteList}${truncationNote}${syncNote}`,
|
|
|
42763
43073
|
registerTool(
|
|
42764
43074
|
"get-note-content",
|
|
42765
43075
|
{
|
|
42766
|
-
description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, and strippedImages/truncated when the body was capped.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
|
|
43076
|
+
description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
|
|
42767
43077
|
inputSchema: {
|
|
42768
43078
|
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42769
43079
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
@@ -42777,6 +43087,13 @@ registerTool(
|
|
|
42777
43087
|
content: external_exports.string().optional(),
|
|
42778
43088
|
contentHash: external_exports.string().optional(),
|
|
42779
43089
|
hashtags: external_exports.array(external_exports.string()).optional(),
|
|
43090
|
+
nativeTags: external_exports.array(external_exports.string()).optional(),
|
|
43091
|
+
links: external_exports.array(
|
|
43092
|
+
external_exports.object({ start: external_exports.number(), length: external_exports.number(), text: external_exports.string(), url: external_exports.string() })
|
|
43093
|
+
).optional(),
|
|
43094
|
+
richContentComplete: external_exports.boolean().optional(),
|
|
43095
|
+
writable: external_exports.boolean().optional(),
|
|
43096
|
+
warning: external_exports.string().optional(),
|
|
42780
43097
|
/** Number of oversized inline images replaced with text placeholders. */
|
|
42781
43098
|
strippedImages: external_exports.number().optional(),
|
|
42782
43099
|
/** True when content is lossy — see strippedImages. Never write a truncated body back. */
|
|
@@ -42798,15 +43115,21 @@ registerTool(
|
|
|
42798
43115
|
if (!rawContent2) {
|
|
42799
43116
|
return errorResponse(`Failed to read content of note "${note2.title}"`);
|
|
42800
43117
|
}
|
|
42801
|
-
const
|
|
43118
|
+
const rich2 = enrichNoteRead(id, rawContent2);
|
|
43119
|
+
const stripped2 = stripLargeInlineImages(rich2.content);
|
|
42802
43120
|
const content2 = stripped2.html;
|
|
42803
43121
|
const hashtags2 = parseHashtags(content2);
|
|
42804
|
-
const warning2 = strippedImagesWarning(stripped2);
|
|
43122
|
+
const warning2 = [strippedImagesWarning(stripped2), rich2.warning].filter(Boolean).join("\n\n");
|
|
42805
43123
|
return successResponse(warning2 ? content2 + warning2 : content2, {
|
|
42806
43124
|
id,
|
|
42807
43125
|
title: note2.title,
|
|
42808
43126
|
content: content2,
|
|
42809
|
-
contentHash:
|
|
43127
|
+
contentHash: richContentHash(rawContent2, rich2),
|
|
43128
|
+
links: rich2.links,
|
|
43129
|
+
nativeTags: rich2.nativeTags,
|
|
43130
|
+
richContentComplete: rich2.complete,
|
|
43131
|
+
writable: rich2.writable && stripped2.strippedCount === 0,
|
|
43132
|
+
warning: rich2.warning,
|
|
42810
43133
|
hashtags: hashtags2,
|
|
42811
43134
|
strippedImages: stripped2.strippedCount,
|
|
42812
43135
|
truncated: stripped2.strippedCount > 0
|
|
@@ -42828,15 +43151,21 @@ registerTool(
|
|
|
42828
43151
|
if (!rawContent) {
|
|
42829
43152
|
return errorResponse(`Failed to read content of note "${title}"`);
|
|
42830
43153
|
}
|
|
42831
|
-
const
|
|
43154
|
+
const rich = enrichNoteRead(note.id, rawContent);
|
|
43155
|
+
const stripped = stripLargeInlineImages(rich.content);
|
|
42832
43156
|
const content = stripped.html;
|
|
42833
43157
|
const hashtags = parseHashtags(content);
|
|
42834
|
-
const warning = strippedImagesWarning(stripped);
|
|
43158
|
+
const warning = [strippedImagesWarning(stripped), rich.warning].filter(Boolean).join("\n\n");
|
|
42835
43159
|
return successResponse(warning ? content + warning : content, {
|
|
42836
43160
|
id: note.id,
|
|
42837
43161
|
title,
|
|
42838
43162
|
content,
|
|
42839
|
-
contentHash:
|
|
43163
|
+
contentHash: richContentHash(rawContent, rich),
|
|
43164
|
+
links: rich.links,
|
|
43165
|
+
nativeTags: rich.nativeTags,
|
|
43166
|
+
richContentComplete: rich.complete,
|
|
43167
|
+
writable: rich.writable && stripped.strippedCount === 0,
|
|
43168
|
+
warning: rich.warning,
|
|
42840
43169
|
hashtags,
|
|
42841
43170
|
strippedImages: stripped.strippedCount,
|
|
42842
43171
|
truncated: stripped.strippedCount > 0
|
|
@@ -43080,10 +43409,13 @@ registerTool(
|
|
|
43080
43409
|
registerTool(
|
|
43081
43410
|
"update-note",
|
|
43082
43411
|
{
|
|
43083
|
-
description: "Use when: replacing the body of one exact Apple Note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or the note has attachments.\nSafety: requires the exact note id and expectedContentHash from get-note-content. The server atomically
|
|
43412
|
+
description: "Use when: replacing the body of one exact Apple Note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or the note has attachments.\nSafety: requires the exact note id and expectedContentHash from get-note-content. The server checks rich metadata revision, atomically checks the AppleScript body, blocks native objects/checklists, and verifies actual link destinations after saving. Preserve returned HTML links unless allowLinkChanges is explicitly requested. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
|
|
43084
43413
|
inputSchema: {
|
|
43085
43414
|
id: noteIdInput,
|
|
43086
43415
|
expectedContentHash: expectedContentHashInput,
|
|
43416
|
+
allowLinkChanges: external_exports.boolean().optional().default(false).describe(
|
|
43417
|
+
"Set true only when the user explicitly intends to remove, relabel or change existing links. Defaults to preserving all links."
|
|
43418
|
+
),
|
|
43087
43419
|
newTitle: external_exports.string().max(MAX.TITLE).optional().describe(
|
|
43088
43420
|
"New title for plaintext updates. Ignored when format is 'html'; include the visible title as the first line of newContent instead."
|
|
43089
43421
|
),
|
|
@@ -43102,72 +43434,90 @@ registerTool(
|
|
|
43102
43434
|
verifiedVisibleText: external_exports.boolean().optional()
|
|
43103
43435
|
}
|
|
43104
43436
|
},
|
|
43105
|
-
withErrorHandling(
|
|
43106
|
-
|
|
43107
|
-
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
43108
|
-
if (snapshot.contentHash !== expectedContentHash) {
|
|
43109
|
-
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43110
|
-
}
|
|
43111
|
-
const attachments = notesManager.listAttachmentsById(id);
|
|
43112
|
-
if (attachments.length > 0) {
|
|
43113
|
-
return errorResponse(
|
|
43114
|
-
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
|
|
43115
|
-
);
|
|
43116
|
-
}
|
|
43117
|
-
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
43437
|
+
withErrorHandling(
|
|
43438
|
+
({
|
|
43118
43439
|
id,
|
|
43119
|
-
|
|
43120
|
-
snapshot.body,
|
|
43440
|
+
expectedContentHash,
|
|
43121
43441
|
newTitle,
|
|
43122
43442
|
newContent,
|
|
43123
|
-
format
|
|
43124
|
-
|
|
43125
|
-
|
|
43126
|
-
|
|
43127
|
-
|
|
43128
|
-
|
|
43129
|
-
|
|
43130
|
-
|
|
43443
|
+
format = "plaintext",
|
|
43444
|
+
allowLinkChanges = false
|
|
43445
|
+
}) => {
|
|
43446
|
+
const snapshot = readExactNoteSnapshot(id);
|
|
43447
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
43448
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
43449
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43450
|
+
}
|
|
43451
|
+
assertLinkedWrite(snapshot.rich, newContent, format, allowLinkChanges);
|
|
43452
|
+
const attachments = notesManager.listAttachmentsById(id);
|
|
43453
|
+
if (attachments.length > 0) {
|
|
43454
|
+
return errorResponse(
|
|
43455
|
+
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
|
|
43456
|
+
);
|
|
43457
|
+
}
|
|
43458
|
+
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
43459
|
+
id,
|
|
43460
|
+
snapshot.note.title,
|
|
43461
|
+
snapshot.body,
|
|
43462
|
+
newTitle,
|
|
43463
|
+
newContent,
|
|
43464
|
+
format,
|
|
43465
|
+
snapshot.rich.revision
|
|
43131
43466
|
);
|
|
43132
|
-
|
|
43133
|
-
|
|
43134
|
-
|
|
43135
|
-
|
|
43467
|
+
if (result.status === "conflict") {
|
|
43468
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43469
|
+
}
|
|
43470
|
+
if (result.status === "attachments") {
|
|
43471
|
+
return errorResponse(
|
|
43472
|
+
`Note "${snapshot.note.title}" gained an attachment before saving. No content was replaced.`
|
|
43473
|
+
);
|
|
43474
|
+
}
|
|
43475
|
+
if (result.status !== "updated") {
|
|
43476
|
+
return errorResponse(
|
|
43477
|
+
`The update result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
43478
|
+
);
|
|
43479
|
+
}
|
|
43480
|
+
const readback = notesManager.getNoteContentById(id);
|
|
43481
|
+
const richReadback = enrichNoteRead(id, readback || "");
|
|
43482
|
+
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
43483
|
+
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
43484
|
+
return errorResponse(
|
|
43485
|
+
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
43486
|
+
);
|
|
43487
|
+
}
|
|
43488
|
+
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
43489
|
+
return errorResponse(
|
|
43490
|
+
`The note accepted an update, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
|
|
43491
|
+
);
|
|
43492
|
+
}
|
|
43493
|
+
const displayTitle = resolveUpdateResponseTitle(
|
|
43494
|
+
snapshot.note.title,
|
|
43495
|
+
newTitle,
|
|
43496
|
+
format,
|
|
43497
|
+
newContent
|
|
43136
43498
|
);
|
|
43137
|
-
|
|
43138
|
-
|
|
43139
|
-
|
|
43140
|
-
|
|
43141
|
-
|
|
43142
|
-
|
|
43499
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
43500
|
+
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
43501
|
+
return successResponse(
|
|
43502
|
+
`Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
|
|
43503
|
+
{
|
|
43504
|
+
ok: true,
|
|
43505
|
+
id,
|
|
43506
|
+
title: displayTitle,
|
|
43507
|
+
shared: snapshot.note.shared ?? false,
|
|
43508
|
+
previousContentHash: expectedContentHash,
|
|
43509
|
+
contentHash,
|
|
43510
|
+
verifiedVisibleText: true
|
|
43511
|
+
}
|
|
43143
43512
|
);
|
|
43144
|
-
}
|
|
43145
|
-
|
|
43146
|
-
|
|
43147
|
-
newTitle,
|
|
43148
|
-
format,
|
|
43149
|
-
newContent
|
|
43150
|
-
);
|
|
43151
|
-
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
43152
|
-
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
43153
|
-
return successResponse(
|
|
43154
|
-
`Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
|
|
43155
|
-
{
|
|
43156
|
-
ok: true,
|
|
43157
|
-
id,
|
|
43158
|
-
title: displayTitle,
|
|
43159
|
-
shared: snapshot.note.shared ?? false,
|
|
43160
|
-
previousContentHash: expectedContentHash,
|
|
43161
|
-
contentHash,
|
|
43162
|
-
verifiedVisibleText: true
|
|
43163
|
-
}
|
|
43164
|
-
);
|
|
43165
|
-
}, "Error updating note")
|
|
43513
|
+
},
|
|
43514
|
+
"Error updating note"
|
|
43515
|
+
)
|
|
43166
43516
|
);
|
|
43167
43517
|
registerTool(
|
|
43168
43518
|
"append-to-note",
|
|
43169
43519
|
{
|
|
43170
|
-
description: "Use when: adding content to one exact note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or it has attachments.\nSafety: append
|
|
43520
|
+
description: "Use when: adding content to one exact note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or it has attachments or native objects.\nSafety: append rewrites the HTML body, so it uses exact-ID, rich revision, native-object, attachment, link, and readback guards. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
|
|
43171
43521
|
inputSchema: {
|
|
43172
43522
|
id: noteIdInput,
|
|
43173
43523
|
expectedContentHash: expectedContentHashInput,
|
|
@@ -43215,15 +43565,21 @@ registerTool(
|
|
|
43215
43565
|
if (snapshot.contentHash !== expectedContentHash) {
|
|
43216
43566
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43217
43567
|
}
|
|
43568
|
+
if (!snapshot.rich.writable) {
|
|
43569
|
+
return errorResponse(
|
|
43570
|
+
snapshot.rich.warning || `Note "${snapshot.note.title}" contains native objects that cannot be preserved by a full-body append.`
|
|
43571
|
+
);
|
|
43572
|
+
}
|
|
43218
43573
|
const attachments = notesManager.listAttachmentsById(id);
|
|
43219
43574
|
if (attachments.length > 0) {
|
|
43220
43575
|
return errorResponse(
|
|
43221
43576
|
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
|
|
43222
43577
|
);
|
|
43223
43578
|
}
|
|
43224
|
-
|
|
43225
|
-
const
|
|
43226
|
-
const
|
|
43579
|
+
assertLinkedWrite(snapshot.rich, snapshot.rich.content, "html");
|
|
43580
|
+
const firstDivEnd = snapshot.rich.content.indexOf("</div>");
|
|
43581
|
+
const titleDiv = firstDivEnd !== -1 ? snapshot.rich.content.slice(0, firstDivEnd + 6) : "";
|
|
43582
|
+
const bodyHtml = firstDivEnd !== -1 ? snapshot.rich.content.slice(firstDivEnd + 6) : snapshot.rich.content;
|
|
43227
43583
|
const newBlock = contentToHtml(content);
|
|
43228
43584
|
const sepHtml = separatorToHtml(separator);
|
|
43229
43585
|
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
@@ -43233,7 +43589,8 @@ registerTool(
|
|
|
43233
43589
|
snapshot.body,
|
|
43234
43590
|
void 0,
|
|
43235
43591
|
combinedBody,
|
|
43236
|
-
"html"
|
|
43592
|
+
"html",
|
|
43593
|
+
snapshot.rich.revision
|
|
43237
43594
|
);
|
|
43238
43595
|
if (result.status === "conflict") {
|
|
43239
43596
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
@@ -43249,7 +43606,13 @@ registerTool(
|
|
|
43249
43606
|
);
|
|
43250
43607
|
}
|
|
43251
43608
|
const readback = notesManager.getNoteContentById(id);
|
|
43252
|
-
const
|
|
43609
|
+
const richReadback = enrichNoteRead(id, readback || "");
|
|
43610
|
+
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
43611
|
+
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
43612
|
+
return errorResponse(
|
|
43613
|
+
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
43614
|
+
);
|
|
43615
|
+
}
|
|
43253
43616
|
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
43254
43617
|
return errorResponse(
|
|
43255
43618
|
`The note accepted an append, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
|
package/package.json
CHANGED