apple-notes-mcp 2.15.2 → 2.15.4
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 +29 -0
- package/build/index.js +375 -34
- package/docs/FULL-DISK-ACCESS.md +1 -1
- package/native/private-helper/apple-notes-private-helper.m +132 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2899,6 +2899,35 @@ title, modification date, folder identifier, lock/trash/shared/editable
|
|
|
2899
2899
|
flags, iCloud version counters, and a `revision` change token (compare two
|
|
2900
2900
|
reads to detect a change). Opens the store with Core Data's read-only option.
|
|
2901
2901
|
|
|
2902
|
+
#### `native-checklist-state`
|
|
2903
|
+
|
|
2904
|
+
Reads one note's native checklist by Notes UUID (`identifier`) or x-coredata
|
|
2905
|
+
`id`; pass exactly one. The `id` is resolved through an exact read-only database
|
|
2906
|
+
lookup after checking the opt-in and installed hashes. Requires the same opt-in, current helper, and Full Disk
|
|
2907
|
+
Access as `native-note-state`. Check `native-helper-status`'s `readChecklist`
|
|
2908
|
+
feature first: its probe checks the checklist getters separately. Locked notes
|
|
2909
|
+
or unavailable native bodies are refused. Ordinary `☐` glyphs and Markdown
|
|
2910
|
+
task text do not count as native checklist items.
|
|
2911
|
+
|
|
2912
|
+
Returns `identifier`, `revision`, `items`, `total`, `checked`, and
|
|
2913
|
+
`syncHostRunning`. Each item has its exact native `todoIdentifier` (32 lowercase
|
|
2914
|
+
hex digits), `uuid`, `index`, `done`, and `text`. Items follow the first occurrence
|
|
2915
|
+
of each native todo identity, which can cover multiple style runs. `text` is the
|
|
2916
|
+
line containing its first visible styled character, or the first styled line
|
|
2917
|
+
when it has only line terminators. `lineStart`, `lineLengthUTF16`, `styledStart`,
|
|
2918
|
+
and `styledLengthUTF16` are UTF-16 offsets and lengths; line text excludes the
|
|
2919
|
+
terminator. The styled range is an envelope, including intervening characters
|
|
2920
|
+
when the identity occurs in separated runs.
|
|
2921
|
+
|
|
2922
|
+
Read the ambiguity flags before treating an entry as a single checklist row:
|
|
2923
|
+
`contiguous: false` means separated runs share the identity; `consistent: false`
|
|
2924
|
+
means their done bits disagree; `spansLines: true` means its styled envelope
|
|
2925
|
+
contains multiple lines after leading/trailing terminators are removed. `done`
|
|
2926
|
+
comes from the first visible styled character, falling back to the first run.
|
|
2927
|
+
Totals count native identities, including ambiguous ones. The `revision` is an
|
|
2928
|
+
informational change token. This tool opens the store read-only and cannot
|
|
2929
|
+
toggle, create, or repair checklist items.
|
|
2930
|
+
|
|
2902
2931
|
## Usage Patterns
|
|
2903
2932
|
|
|
2904
2933
|
### Basic Workflow
|
package/build/index.js
CHANGED
|
@@ -40498,6 +40498,87 @@ function styleValue(field) {
|
|
|
40498
40498
|
}
|
|
40499
40499
|
return Buffer.from(field.value).toString("hex");
|
|
40500
40500
|
}
|
|
40501
|
+
function storedStyleSignature(data) {
|
|
40502
|
+
return JSON.stringify(
|
|
40503
|
+
decodeWireFields(data).filter((field) => field.fieldNumber !== 1).map((field) => [
|
|
40504
|
+
field.fieldNumber,
|
|
40505
|
+
field.wireType,
|
|
40506
|
+
field.bytes ? styleValue({
|
|
40507
|
+
fieldNumber: field.fieldNumber,
|
|
40508
|
+
wireType: field.wireType,
|
|
40509
|
+
value: field.bytes
|
|
40510
|
+
}) : field.varint.toString()
|
|
40511
|
+
])
|
|
40512
|
+
);
|
|
40513
|
+
}
|
|
40514
|
+
function nativeRunSemantics(data) {
|
|
40515
|
+
const result = {
|
|
40516
|
+
complete: true,
|
|
40517
|
+
unknown: false,
|
|
40518
|
+
structuredParagraph: false,
|
|
40519
|
+
links: false,
|
|
40520
|
+
objects: []
|
|
40521
|
+
};
|
|
40522
|
+
const check2 = (fields, schema) => {
|
|
40523
|
+
const seen = /* @__PURE__ */ new Set();
|
|
40524
|
+
for (const field of fields) {
|
|
40525
|
+
if (!Object.hasOwn(schema, field.fieldNumber)) result.unknown = true;
|
|
40526
|
+
else if (seen.has(field.fieldNumber) || field.wireType !== schema[field.fieldNumber])
|
|
40527
|
+
result.complete = false;
|
|
40528
|
+
seen.add(field.fieldNumber);
|
|
40529
|
+
}
|
|
40530
|
+
};
|
|
40531
|
+
const nested = (field) => {
|
|
40532
|
+
if (field.wireType !== 2 || !field.bytes) {
|
|
40533
|
+
result.complete = false;
|
|
40534
|
+
return [];
|
|
40535
|
+
}
|
|
40536
|
+
return decodeWireFields(field.bytes);
|
|
40537
|
+
};
|
|
40538
|
+
try {
|
|
40539
|
+
const fields = decodeWireFields(data);
|
|
40540
|
+
check2(fields, { 1: 0, 2: 2, 3: 2, 5: 0, 6: 0, 7: 0, 8: 0, 9: 2, 10: 2, 12: 2, 14: 0 });
|
|
40541
|
+
if (fields.filter((f) => f.fieldNumber === 1).length !== 1) result.complete = false;
|
|
40542
|
+
result.links = fields.some((f) => f.fieldNumber === 9);
|
|
40543
|
+
for (const field of fields) {
|
|
40544
|
+
if (field.fieldNumber === 2) {
|
|
40545
|
+
const paragraph = nested(field);
|
|
40546
|
+
check2(paragraph, { 1: 0, 2: 0, 4: 0, 5: 2, 8: 0, 9: 2 });
|
|
40547
|
+
for (const style of paragraph.filter((f) => f.fieldNumber === 1)) {
|
|
40548
|
+
const type = style.varint === void 0 ? void 0 : Number(BigInt.asIntN(64, style.varint));
|
|
40549
|
+
if (type !== void 0 && type >= 100 && type <= 103) result.structuredParagraph = true;
|
|
40550
|
+
else if (type === void 0 || ![-1, 0, 1, 2, 3, 4].includes(type)) result.unknown = true;
|
|
40551
|
+
}
|
|
40552
|
+
for (const uuid2 of paragraph.filter((f) => f.fieldNumber === 9))
|
|
40553
|
+
if (uuid2.bytes?.length !== 16) result.complete = false;
|
|
40554
|
+
for (const todo of paragraph.filter((f) => f.fieldNumber === 5)) {
|
|
40555
|
+
result.structuredParagraph = true;
|
|
40556
|
+
const item = nested(todo);
|
|
40557
|
+
check2(item, { 1: 2, 2: 0 });
|
|
40558
|
+
const id2 = item.filter((f) => f.fieldNumber === 1);
|
|
40559
|
+
if (id2.length !== 1 || id2[0].bytes?.length !== 16) result.complete = false;
|
|
40560
|
+
}
|
|
40561
|
+
} else if (field.fieldNumber === 3) check2(nested(field), { 1: 2, 2: 5, 3: 0 });
|
|
40562
|
+
else if (field.fieldNumber === 10) check2(nested(field), { 1: 5, 2: 5, 3: 5, 4: 5 });
|
|
40563
|
+
else if (field.fieldNumber === 12) {
|
|
40564
|
+
const attachment = nested(field);
|
|
40565
|
+
check2(attachment, { 1: 2, 2: 2 });
|
|
40566
|
+
const ids = attachment.filter((f) => f.fieldNumber === 1), types = attachment.filter((f) => f.fieldNumber === 2);
|
|
40567
|
+
if (ids.length !== 1 || types.length !== 1 || !ids[0].bytes || !types[0].bytes) {
|
|
40568
|
+
result.complete = false;
|
|
40569
|
+
continue;
|
|
40570
|
+
}
|
|
40571
|
+
const utf82 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
40572
|
+
const id2 = utf82.decode(ids[0].bytes), type = utf82.decode(types[0].bytes);
|
|
40573
|
+
if (!id2 || !type) result.complete = false;
|
|
40574
|
+
result.objects.push({ id: id2, type });
|
|
40575
|
+
}
|
|
40576
|
+
}
|
|
40577
|
+
} catch {
|
|
40578
|
+
result.complete = false;
|
|
40579
|
+
}
|
|
40580
|
+
return result;
|
|
40581
|
+
}
|
|
40501
40582
|
function parseRichNote(data, nativeTags = [], options = {}) {
|
|
40502
40583
|
const doc = decodeMessage(data);
|
|
40503
40584
|
const wrapper = embeddedMessage(getField(doc, 2));
|
|
@@ -40526,9 +40607,8 @@ function parseRichNote(data, nativeTags = [], options = {}) {
|
|
|
40526
40607
|
highlight: Boolean(varintValue(getField(fields, 14))),
|
|
40527
40608
|
start: position,
|
|
40528
40609
|
length,
|
|
40529
|
-
signature:
|
|
40530
|
-
|
|
40531
|
-
)
|
|
40610
|
+
signature: storedStyleSignature(run.value),
|
|
40611
|
+
nativeSemantics: nativeRunSemantics(run.value)
|
|
40532
40612
|
});
|
|
40533
40613
|
if (/[^\s\ufffc]/u.test(text2.slice(position, position + length))) {
|
|
40534
40614
|
const baseline = varintValue(getField(fields, 8)) ?? 0;
|
|
@@ -40596,7 +40676,7 @@ function parseRichNote(data, nativeTags = [], options = {}) {
|
|
|
40596
40676
|
function readRichNote(id2, options = {}) {
|
|
40597
40677
|
const pk = /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p([0-9]+)$/i.exec(id2)?.[1];
|
|
40598
40678
|
if (!pk) throw new Error("Invalid exact note ID");
|
|
40599
|
-
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;`;
|
|
40679
|
+
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,'altText',ZALTTEXT)) FROM ZICCLOUDSYNCINGOBJECT WHERE ZNOTE1=${pk} OR ZNOTE=${pk}; COMMIT;`;
|
|
40600
40680
|
const rows = execFileSync4("/usr/bin/sqlite3", ["-readonly", dbPath, sql], {
|
|
40601
40681
|
encoding: "utf8",
|
|
40602
40682
|
timeout: 5e3,
|
|
@@ -40615,14 +40695,21 @@ function readRichNote(id2, options = {}) {
|
|
|
40615
40695
|
const tagMap = tags;
|
|
40616
40696
|
const objectData = JSON.parse(rows[2] || "[]");
|
|
40617
40697
|
if (!Array.isArray(objectData) || objectData.some(
|
|
40618
|
-
(row) => !row || typeof row.id !== "string" || !Number.isInteger(row.pk) || typeof row.mergeable !== "string" || !/^[0-9a-f]*$/i.test(row.mergeable)
|
|
40698
|
+
(row) => !row || typeof row.id !== "string" || !Number.isInteger(row.pk) || typeof row.mergeable !== "string" || !/^[0-9a-f]*$/i.test(row.mergeable) || row.altText !== void 0 && row.altText !== null && typeof row.altText !== "string"
|
|
40619
40699
|
))
|
|
40620
40700
|
throw new Error("Invalid native object metadata");
|
|
40621
40701
|
rich.objectData = uniqueById(
|
|
40622
40702
|
objectData.filter((row) => rich.nativeObjectIds.includes(row.id)).sort((a, b) => a.id.localeCompare(b.id))
|
|
40623
40703
|
);
|
|
40624
|
-
|
|
40625
|
-
rich.
|
|
40704
|
+
const selectedRows = new Map(rich.objectData.map((row) => [row.id, row]));
|
|
40705
|
+
rich.nativeObjectDataComplete = objectData.every((candidate) => {
|
|
40706
|
+
const row = selectedRows.get(candidate.id);
|
|
40707
|
+
return !row || ["pk", "type", "mergeable", "view", "altText"].every(
|
|
40708
|
+
(key) => candidate[key] === row[key]
|
|
40709
|
+
);
|
|
40710
|
+
});
|
|
40711
|
+
rich.revision = createHash("sha256").update(rich.revision).update(JSON.stringify(rich.objectData)).update(JSON.stringify(rich.nativeObjectDataComplete)).digest("hex");
|
|
40712
|
+
rich.nativeTagObjectIds = /* @__PURE__ */ Object.create(null);
|
|
40626
40713
|
for (const id3 of rich.nativeObjectIds)
|
|
40627
40714
|
if (tagMap[id3]) {
|
|
40628
40715
|
const tag = tagMap[id3].replace(/^#/, "");
|
|
@@ -48649,6 +48736,176 @@ function normalizeNativeTags(tags) {
|
|
|
48649
48736
|
)
|
|
48650
48737
|
];
|
|
48651
48738
|
}
|
|
48739
|
+
var TAG_OBJECT_TYPE = "com.apple.notes.inlinetextattachment.hashtag";
|
|
48740
|
+
function requirePreservationMetadata(rich) {
|
|
48741
|
+
const unavailable = () => {
|
|
48742
|
+
throw new Error("Native object, checklist or formatting preservation metadata is unavailable");
|
|
48743
|
+
};
|
|
48744
|
+
const { objects, objectData, checklistItems, styleRuns, nativeTagObjectIds } = rich;
|
|
48745
|
+
if (!objects || !objectData || !checklistItems || !styleRuns || !nativeTagObjectIds || rich.nativeObjectDataComplete !== true)
|
|
48746
|
+
return unavailable();
|
|
48747
|
+
const ids = new Set(rich.nativeObjectIds);
|
|
48748
|
+
if (ids.size !== rich.nativeObjectIds.length || objects.length !== ids.size || objectData.length !== ids.size || new Set(objects.map((o) => o.id)).size !== ids.size || new Set(objectData.map((o) => o.id)).size !== ids.size || rich.hasNativeObjects !== Boolean(ids.size) || rich.hasChecklist !== Boolean(checklistItems.length))
|
|
48749
|
+
return unavailable();
|
|
48750
|
+
let objectEnd = 0;
|
|
48751
|
+
for (const object3 of objects) {
|
|
48752
|
+
const data = objectData.find((row) => row.id === object3.id);
|
|
48753
|
+
if (!ids.has(object3.id) || !Number.isInteger(object3.start) || !Number.isInteger(object3.length) || object3.start < objectEnd || object3.length < 1 || object3.start + object3.length > rich.text.length || !data || data.type !== object3.type || !Number.isInteger(data.pk) || !/^[0-9a-f]*$/iu.test(data.mergeable) || data.view !== null && !Number.isInteger(data.view) || !Object.hasOwn(data, "altText") || data.altText !== null && typeof data.altText !== "string")
|
|
48754
|
+
return unavailable();
|
|
48755
|
+
objectEnd = object3.start + object3.length;
|
|
48756
|
+
}
|
|
48757
|
+
const mappedIds = /* @__PURE__ */ new Set();
|
|
48758
|
+
if (new Set(rich.nativeTags).size !== rich.nativeTags.length || Object.keys(nativeTagObjectIds).length !== rich.nativeTags.length)
|
|
48759
|
+
return unavailable();
|
|
48760
|
+
for (const [tag, tagIds] of Object.entries(nativeTagObjectIds)) {
|
|
48761
|
+
if (!rich.nativeTags.includes(tag) || !tagIds.length) return unavailable();
|
|
48762
|
+
for (const id2 of tagIds) {
|
|
48763
|
+
if (mappedIds.has(id2) || !objects.some((o) => o.id === id2 && o.type === TAG_OBJECT_TYPE) || objectData.find((o) => o.id === id2)?.altText?.replace(/^#/, "") !== tag)
|
|
48764
|
+
return unavailable();
|
|
48765
|
+
mappedIds.add(id2);
|
|
48766
|
+
}
|
|
48767
|
+
}
|
|
48768
|
+
if (objects.some((o) => o.type === TAG_OBJECT_TYPE && !mappedIds.has(o.id))) return unavailable();
|
|
48769
|
+
if (new Set(checklistItems.map((item) => item.id)).size !== checklistItems.length)
|
|
48770
|
+
return unavailable();
|
|
48771
|
+
for (const item of checklistItems) {
|
|
48772
|
+
const end = item.start + item.text.length;
|
|
48773
|
+
if (!item.id || !Number.isInteger(item.start) || item.start < 0 || end > rich.text.length || rich.text.slice(item.start, end) !== item.text || end < rich.text.length && rich.text[end] !== "\n")
|
|
48774
|
+
return unavailable();
|
|
48775
|
+
}
|
|
48776
|
+
let position = 0;
|
|
48777
|
+
for (const run of styleRuns) {
|
|
48778
|
+
if (run.start !== position || !Number.isInteger(run.length) || run.length < 0 || typeof run.signature !== "string" || run.nativeSemantics?.complete !== true || run.length === 0 && (run.nativeSemantics.unknown || run.nativeSemantics.structuredParagraph || run.nativeSemantics.links || run.nativeSemantics.objects.length > 0))
|
|
48779
|
+
return unavailable();
|
|
48780
|
+
position += run.length;
|
|
48781
|
+
}
|
|
48782
|
+
if (position !== rich.text.length) return unavailable();
|
|
48783
|
+
for (const link of rich.links)
|
|
48784
|
+
if (!Number.isInteger(link.start) || !Number.isInteger(link.length) || link.start < 0 || link.length < 1 || link.start + link.length > rich.text.length || rich.text.slice(link.start, link.start + link.length) !== link.text)
|
|
48785
|
+
return unavailable();
|
|
48786
|
+
}
|
|
48787
|
+
function plainNativeRange(rich, start, length) {
|
|
48788
|
+
return rich.styleRuns.every((run) => {
|
|
48789
|
+
if (run.start >= start + length || run.start + run.length <= start) return true;
|
|
48790
|
+
const metadata = run.nativeSemantics;
|
|
48791
|
+
return metadata.complete && !metadata.unknown && !metadata.structuredParagraph && !metadata.links && !metadata.objects.length;
|
|
48792
|
+
});
|
|
48793
|
+
}
|
|
48794
|
+
function preservedTextSpans(before, after, missing) {
|
|
48795
|
+
const oldIds = new Set(before.nativeObjectIds);
|
|
48796
|
+
const added = after.objects.filter((o) => !oldIds.has(o.id));
|
|
48797
|
+
const tagsById = new Map(
|
|
48798
|
+
Object.entries(after.nativeTagObjectIds).flatMap(
|
|
48799
|
+
([tag, ids]) => ids.map((id2) => [id2, tag])
|
|
48800
|
+
)
|
|
48801
|
+
);
|
|
48802
|
+
if (!added.length || added.some((o) => o.type !== TAG_OBJECT_TYPE || !missing.includes(tagsById.get(o.id))) || missing.some((tag) => !added.some((o) => tagsById.get(o.id) === tag)))
|
|
48803
|
+
return void 0;
|
|
48804
|
+
const spans = [];
|
|
48805
|
+
let oldPosition = 0, newPosition = 0, appended = false;
|
|
48806
|
+
for (const object3 of added) {
|
|
48807
|
+
const prefix = after.text.slice(newPosition, object3.start);
|
|
48808
|
+
const remaining2 = before.text.slice(oldPosition);
|
|
48809
|
+
const length = Math.min(prefix.length, remaining2.length);
|
|
48810
|
+
if (prefix.slice(0, length) !== remaining2.slice(0, length) || prefix.length > length && !/^[ \n]*$/u.test(prefix.slice(length)))
|
|
48811
|
+
return void 0;
|
|
48812
|
+
if (length) spans.push({ before: oldPosition, after: newPosition, length });
|
|
48813
|
+
oldPosition += length;
|
|
48814
|
+
const literal2 = `#${tagsById.get(object3.id)}`;
|
|
48815
|
+
const rendered = after.text.slice(object3.start, object3.start + object3.length);
|
|
48816
|
+
if (rendered !== "\uFFFC" && rendered !== literal2) return void 0;
|
|
48817
|
+
const previous = [...before.text.slice(Math.max(0, oldPosition - 2), oldPosition)].at(-1) || "";
|
|
48818
|
+
const next = String.fromCodePoint(before.text.codePointAt(oldPosition + literal2.length) ?? 0);
|
|
48819
|
+
if (oldPosition === before.text.length) appended = true;
|
|
48820
|
+
else if (before.text.startsWith(literal2, oldPosition) && !/[\p{L}\p{M}\p{N}_#-]/u.test(previous) && !/[\p{L}\p{M}\p{N}_-]/u.test(next) && plainNativeRange(before, oldPosition, literal2.length))
|
|
48821
|
+
oldPosition += literal2.length;
|
|
48822
|
+
else return void 0;
|
|
48823
|
+
newPosition = object3.start + object3.length;
|
|
48824
|
+
}
|
|
48825
|
+
const remaining = before.text.slice(oldPosition);
|
|
48826
|
+
const suffix = after.text.slice(newPosition);
|
|
48827
|
+
if (suffix !== remaining && !(appended && !remaining && /^[ \n]*$/u.test(suffix)))
|
|
48828
|
+
return void 0;
|
|
48829
|
+
if (remaining.length)
|
|
48830
|
+
spans.push({ before: oldPosition, after: newPosition, length: remaining.length });
|
|
48831
|
+
return spans;
|
|
48832
|
+
}
|
|
48833
|
+
function preservesAddedSemantics(before, after, spans) {
|
|
48834
|
+
const oldIds = new Set(before.nativeObjectIds);
|
|
48835
|
+
const added = after.objects.filter((o) => !oldIds.has(o.id));
|
|
48836
|
+
const changed = [];
|
|
48837
|
+
let end = 0;
|
|
48838
|
+
for (const span of spans) {
|
|
48839
|
+
if (end < span.after) changed.push({ start: end, end: span.after });
|
|
48840
|
+
end = span.after + span.length;
|
|
48841
|
+
}
|
|
48842
|
+
if (end < after.text.length) changed.push({ start: end, end: after.text.length });
|
|
48843
|
+
for (const run of after.styleRuns) {
|
|
48844
|
+
if (!changed.some((range) => run.start < range.end && run.start + run.length > range.start))
|
|
48845
|
+
continue;
|
|
48846
|
+
const metadata = run.nativeSemantics;
|
|
48847
|
+
if (!metadata.complete || metadata.unknown || metadata.structuredParagraph || metadata.links)
|
|
48848
|
+
return false;
|
|
48849
|
+
const object3 = added.find((o) => run.start === o.start && run.length === o.length);
|
|
48850
|
+
if (object3) {
|
|
48851
|
+
if (metadata.objects.length !== 1 || metadata.objects[0].id !== object3.id || metadata.objects[0].type !== object3.type)
|
|
48852
|
+
return false;
|
|
48853
|
+
} else if (metadata.objects.length || added.some((o) => run.start < o.start + o.length && run.start + run.length > o.start))
|
|
48854
|
+
return false;
|
|
48855
|
+
}
|
|
48856
|
+
return true;
|
|
48857
|
+
}
|
|
48858
|
+
function preservesNativeContent(before, after, spans) {
|
|
48859
|
+
const rangeStart = (start, length) => {
|
|
48860
|
+
const span = spans.find((s) => start >= s.before && start + length <= s.before + s.length);
|
|
48861
|
+
return span && span.after + start - span.before;
|
|
48862
|
+
};
|
|
48863
|
+
for (const object3 of before.objects) {
|
|
48864
|
+
const actual = after.objects.find((o) => o.id === object3.id);
|
|
48865
|
+
if (!actual || actual.type !== object3.type || actual.length !== object3.length || actual.start !== rangeStart(object3.start, object3.length))
|
|
48866
|
+
return false;
|
|
48867
|
+
}
|
|
48868
|
+
for (const object3 of before.objectData) {
|
|
48869
|
+
const actual = after.objectData.find((o) => o.id === object3.id);
|
|
48870
|
+
if (!actual || actual.pk !== object3.pk || actual.type !== object3.type || actual.mergeable !== object3.mergeable || actual.view !== object3.view || actual.altText !== object3.altText)
|
|
48871
|
+
return false;
|
|
48872
|
+
}
|
|
48873
|
+
for (const tag of before.nativeTags)
|
|
48874
|
+
if (JSON.stringify(before.nativeTagObjectIds[tag]) !== JSON.stringify(after.nativeTagObjectIds[tag]))
|
|
48875
|
+
return false;
|
|
48876
|
+
if (before.checklistItems.length !== after.checklistItems.length) return false;
|
|
48877
|
+
for (const [index, item] of before.checklistItems.entries()) {
|
|
48878
|
+
const actual = after.checklistItems[index];
|
|
48879
|
+
if (actual.id !== item.id || actual.text !== item.text || actual.done !== item.done || actual.start !== rangeStart(item.start, item.text.length))
|
|
48880
|
+
return false;
|
|
48881
|
+
}
|
|
48882
|
+
if (before.links.length !== after.links.length) return false;
|
|
48883
|
+
for (const [index, link] of before.links.entries()) {
|
|
48884
|
+
const actual = after.links[index];
|
|
48885
|
+
if (actual.text !== link.text || actual.url !== link.url || actual.length !== link.length || actual.start !== rangeStart(link.start, link.length))
|
|
48886
|
+
return false;
|
|
48887
|
+
}
|
|
48888
|
+
let oldIndex = 0, newIndex = 0;
|
|
48889
|
+
for (const span of spans) {
|
|
48890
|
+
let offset = 0;
|
|
48891
|
+
while (offset < span.length) {
|
|
48892
|
+
const oldPosition = span.before + offset, newPosition = span.after + offset;
|
|
48893
|
+
while (before.styleRuns[oldIndex].start + before.styleRuns[oldIndex].length <= oldPosition)
|
|
48894
|
+
oldIndex++;
|
|
48895
|
+
while (after.styleRuns[newIndex].start + after.styleRuns[newIndex].length <= newPosition)
|
|
48896
|
+
newIndex++;
|
|
48897
|
+
const oldRun = before.styleRuns[oldIndex], newRun = after.styleRuns[newIndex];
|
|
48898
|
+
if (oldRun.signature !== newRun.signature || oldRun.paragraphStyle !== newRun.paragraphStyle || oldRun.blockQuote !== newRun.blockQuote || oldRun.highlight !== newRun.highlight)
|
|
48899
|
+
return false;
|
|
48900
|
+
offset += Math.min(
|
|
48901
|
+
span.length - offset,
|
|
48902
|
+
oldRun.start + oldRun.length - oldPosition,
|
|
48903
|
+
newRun.start + newRun.length - newPosition
|
|
48904
|
+
);
|
|
48905
|
+
}
|
|
48906
|
+
}
|
|
48907
|
+
return true;
|
|
48908
|
+
}
|
|
48652
48909
|
function addNativeTags(request, deps) {
|
|
48653
48910
|
if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(request.id))
|
|
48654
48911
|
throw new Error("An exact CoreData note ID is required");
|
|
@@ -48663,32 +48920,37 @@ function addNativeTags(request, deps) {
|
|
|
48663
48920
|
const missing = tags.filter((tag) => !before.rich.nativeTags.includes(tag));
|
|
48664
48921
|
if (!missing.length)
|
|
48665
48922
|
return { nativeTags: before.rich.nativeTags, added: [], contentHash: before.contentHash };
|
|
48923
|
+
requirePreservationMetadata(before.rich);
|
|
48666
48924
|
const candidates = deps.candidates(request.title, request.scopeText);
|
|
48667
48925
|
if (candidates.length !== 1 || candidates[0] !== request.id)
|
|
48668
48926
|
throw new Error(
|
|
48669
48927
|
"Shortcuts selection is ambiguous or points to a different note; nothing was changed"
|
|
48670
48928
|
);
|
|
48671
|
-
|
|
48929
|
+
const current = deps.read(request.id);
|
|
48930
|
+
if (current.contentHash !== request.expectedContentHash)
|
|
48672
48931
|
throw new Error("Note revision changed during preflight; nothing was changed");
|
|
48932
|
+
requirePreservationMetadata(current.rich);
|
|
48673
48933
|
let transportWarning;
|
|
48674
48934
|
try {
|
|
48675
48935
|
deps.run({ title: request.title, scopeText: request.scopeText, tags: missing });
|
|
48676
48936
|
} catch (error2) {
|
|
48677
48937
|
transportWarning = error2 instanceof Error ? error2.message : "Shortcuts completion was uncertain";
|
|
48678
48938
|
}
|
|
48679
|
-
const
|
|
48939
|
+
const verificationFailure = (detail) => new CodedError(
|
|
48940
|
+
(transportWarning ? "Shortcuts did not complete cleanly, and exact-ID tags/text/links/native-content readback was not verified." : "Shortcuts ran, but exact-ID tags/text/links/native-content readback was not verified.") + " Read the note before any retry." + (detail ? ` ${detail}` : "") + (transportWarning ? ` ${transportWarning}` : ""),
|
|
48941
|
+
{ code: "verification_failed", indeterminate: true }
|
|
48942
|
+
);
|
|
48943
|
+
let after;
|
|
48944
|
+
try {
|
|
48945
|
+
after = deps.read(request.id);
|
|
48946
|
+
requirePreservationMetadata(after.rich);
|
|
48947
|
+
} catch (error2) {
|
|
48948
|
+
throw verificationFailure(error2 instanceof Error ? error2.message : String(error2));
|
|
48949
|
+
}
|
|
48680
48950
|
const allTags = [.../* @__PURE__ */ new Set([...before.rich.nativeTags, ...tags])];
|
|
48681
|
-
const
|
|
48682
|
-
|
|
48683
|
-
|
|
48684
|
-
text2 = text2.replace(new RegExp(`#${escaped}(?![\\p{L}\\p{N}_-])`, "gu"), "");
|
|
48685
|
-
}
|
|
48686
|
-
return text2.replace(/[\s\ufffc]/gu, "");
|
|
48687
|
-
};
|
|
48688
|
-
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((id2) => !after.rich.nativeObjectIds.includes(id2)) || before.rich.hasChecklist !== after.rich.hasChecklist) {
|
|
48689
|
-
throw new Error(
|
|
48690
|
-
transportWarning ? `Shortcuts did not complete cleanly, and exact-ID tags/text/links readback was not verified. Read the note before any retry. ${transportWarning}` : "Shortcuts ran, but exact-ID tags/text/links readback was not verified. Read the note before any retry"
|
|
48691
|
-
);
|
|
48951
|
+
const spans = preservedTextSpans(before.rich, after.rich, missing);
|
|
48952
|
+
if (after.title !== before.title || after.rich.nativeTags.length !== allTags.length || allTags.some((tag) => !after.rich.nativeTags.includes(tag)) || before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)) || before.rich.hasChecklist !== after.rich.hasChecklist || !spans || !preservesAddedSemantics(before.rich, after.rich, spans) || !preservesNativeContent(before.rich, after.rich, spans)) {
|
|
48953
|
+
throw verificationFailure();
|
|
48692
48954
|
}
|
|
48693
48955
|
return {
|
|
48694
48956
|
nativeTags: after.rich.nativeTags,
|
|
@@ -58520,7 +58782,7 @@ function registerSvgAnalysis(server2) {
|
|
|
58520
58782
|
// src/tools/nativeTagsBridge.ts
|
|
58521
58783
|
var noteId2 = exactIdInput(
|
|
58522
58784
|
"ICNote",
|
|
58523
|
-
/^x-coredata:\/\/[0-9a-
|
|
58785
|
+
/^x-coredata:\/\/[0-9a-fA-F-]+\/ICNote\/p\d+$/i,
|
|
58524
58786
|
NOTE_ID_MESSAGE
|
|
58525
58787
|
);
|
|
58526
58788
|
var revision2 = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
@@ -58554,8 +58816,9 @@ function registerNativeTagsBridge(server2, manager) {
|
|
|
58554
58816
|
if (!body) throw new Error("Note content is unavailable");
|
|
58555
58817
|
const enriched = enrichNoteRead(id2, body);
|
|
58556
58818
|
const rich = readRichNote(id2);
|
|
58557
|
-
if (
|
|
58819
|
+
if (enriched.revision !== rich.revision)
|
|
58558
58820
|
throw new Error("Native metadata changed during read; read the note again");
|
|
58821
|
+
restoreNoteLinks(body, rich);
|
|
58559
58822
|
return {
|
|
58560
58823
|
contentHash: richContentHash(body, enriched),
|
|
58561
58824
|
title: note.title,
|
|
@@ -60365,7 +60628,8 @@ var TIMEOUT_ENV = "APPLE_NOTES_MCP_PRIVATE_HELPER_TIMEOUT_MS";
|
|
|
60365
60628
|
var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
|
|
60366
60629
|
"hello",
|
|
60367
60630
|
"probe",
|
|
60368
|
-
"read_note_state"
|
|
60631
|
+
"read_note_state",
|
|
60632
|
+
"read_checklist"
|
|
60369
60633
|
]);
|
|
60370
60634
|
var HELPER_BINARY_NAME = "apple-notes-private-helper";
|
|
60371
60635
|
var HELPER_SOURCE_RELATIVE = "native/private-helper/apple-notes-private-helper.m";
|
|
@@ -60481,6 +60745,16 @@ var PrivateHelperError = class extends Error {
|
|
|
60481
60745
|
code;
|
|
60482
60746
|
details;
|
|
60483
60747
|
};
|
|
60748
|
+
function assertPrivateHelperReady(deps = defaultDeps2()) {
|
|
60749
|
+
if (!privateHelperEnabled(deps.env))
|
|
60750
|
+
throw new PrivateHelperError(
|
|
60751
|
+
"disabled",
|
|
60752
|
+
`The private helper is off. Set ${ENABLE_ENV}=1 to opt in.`
|
|
60753
|
+
);
|
|
60754
|
+
const install = inspectInstallation(deps);
|
|
60755
|
+
if (!install.ready)
|
|
60756
|
+
throw new PrivateHelperError(install.reason || "helper_not_installed", install.detail || "");
|
|
60757
|
+
}
|
|
60484
60758
|
var errorSchema2 = external_exports.object({
|
|
60485
60759
|
status: external_exports.literal("error"),
|
|
60486
60760
|
code: external_exports.string(),
|
|
@@ -60511,7 +60785,7 @@ var probeSchema = external_exports.object({
|
|
|
60511
60785
|
noteRows: external_exports.number().int().nullable()
|
|
60512
60786
|
}).passthrough(),
|
|
60513
60787
|
syncHostRunning: external_exports.boolean(),
|
|
60514
|
-
features: external_exports.object({ readNoteState: featureSchema }).passthrough()
|
|
60788
|
+
features: external_exports.object({ readNoteState: featureSchema, readChecklist: featureSchema }).passthrough()
|
|
60515
60789
|
}).passthrough();
|
|
60516
60790
|
var cloudSyncSchema = external_exports.object({
|
|
60517
60791
|
available: external_exports.boolean(),
|
|
@@ -60535,12 +60809,53 @@ var noteStateSchema = external_exports.object({
|
|
|
60535
60809
|
cloudSync: cloudSyncSchema,
|
|
60536
60810
|
syncHostRunning: external_exports.boolean()
|
|
60537
60811
|
}).passthrough();
|
|
60812
|
+
var checklistItemSchema = external_exports.object({
|
|
60813
|
+
todoIdentifier: external_exports.string().regex(/^[a-f0-9]{32}$/),
|
|
60814
|
+
uuid: external_exports.string().uuid(),
|
|
60815
|
+
index: external_exports.number().int().nonnegative(),
|
|
60816
|
+
done: external_exports.boolean(),
|
|
60817
|
+
text: external_exports.string(),
|
|
60818
|
+
lineStart: external_exports.number().int().nonnegative(),
|
|
60819
|
+
lineLengthUTF16: external_exports.number().int().nonnegative(),
|
|
60820
|
+
styledStart: external_exports.number().int().nonnegative(),
|
|
60821
|
+
styledLengthUTF16: external_exports.number().int().positive(),
|
|
60822
|
+
contiguous: external_exports.boolean(),
|
|
60823
|
+
consistent: external_exports.boolean(),
|
|
60824
|
+
spansLines: external_exports.boolean()
|
|
60825
|
+
});
|
|
60826
|
+
var checklistStateSchema = external_exports.object({
|
|
60827
|
+
status: external_exports.literal("ok"),
|
|
60828
|
+
identifier: external_exports.string().uuid(),
|
|
60829
|
+
revision: external_exports.string().regex(/^r1:[a-f0-9]{64}$/),
|
|
60830
|
+
items: external_exports.array(checklistItemSchema),
|
|
60831
|
+
total: external_exports.number().int().nonnegative(),
|
|
60832
|
+
checked: external_exports.number().int().nonnegative(),
|
|
60833
|
+
syncHostRunning: external_exports.boolean()
|
|
60834
|
+
}).passthrough().superRefine((state, ctx) => {
|
|
60835
|
+
const identities = /* @__PURE__ */ new Set();
|
|
60836
|
+
if (state.total !== state.items.length || state.checked !== state.items.filter((item) => item.done).length)
|
|
60837
|
+
ctx.addIssue({ code: "custom", message: "Checklist totals disagree with items" });
|
|
60838
|
+
state.items.forEach((item, index) => {
|
|
60839
|
+
if (item.index !== index || identities.has(item.todoIdentifier) || item.uuid.replaceAll("-", "").toLowerCase() !== item.todoIdentifier || item.text.length !== item.lineLengthUTF16)
|
|
60840
|
+
ctx.addIssue({
|
|
60841
|
+
code: "custom",
|
|
60842
|
+
path: ["items", index],
|
|
60843
|
+
message: "Invalid checklist identity or line"
|
|
60844
|
+
});
|
|
60845
|
+
identities.add(item.todoIdentifier);
|
|
60846
|
+
});
|
|
60847
|
+
});
|
|
60538
60848
|
function callPrivateHelper(action, fields = {}, deps = defaultDeps2(), options = {}) {
|
|
60539
60849
|
if (!READ_ONLY_ACTIONS.has(action))
|
|
60540
60850
|
throw new PrivateHelperError(
|
|
60541
60851
|
"unknown_action",
|
|
60542
60852
|
`The private helper is read-only; "${action}" is not a supported action.`
|
|
60543
60853
|
);
|
|
60854
|
+
if (Object.hasOwn(fields, "action") || Object.hasOwn(fields, "protocol"))
|
|
60855
|
+
throw new PrivateHelperError(
|
|
60856
|
+
"invalid_request",
|
|
60857
|
+
"Request fields cannot override action or protocol"
|
|
60858
|
+
);
|
|
60544
60859
|
if (!options.allowDisabled && !privateHelperEnabled(deps.env))
|
|
60545
60860
|
throw new PrivateHelperError(
|
|
60546
60861
|
"disabled",
|
|
@@ -60627,6 +60942,19 @@ function readNoteState(identifier, deps = defaultDeps2()) {
|
|
|
60627
60942
|
assertNoteIdentifier(identifier);
|
|
60628
60943
|
return parseOrThrow(noteStateSchema, callPrivateHelper("read_note_state", { identifier }, deps));
|
|
60629
60944
|
}
|
|
60945
|
+
function readChecklistState(identifier, deps = defaultDeps2()) {
|
|
60946
|
+
assertNoteIdentifier(identifier);
|
|
60947
|
+
const state = parseOrThrow(
|
|
60948
|
+
checklistStateSchema,
|
|
60949
|
+
callPrivateHelper("read_checklist", { identifier }, deps)
|
|
60950
|
+
);
|
|
60951
|
+
if (state.identifier.toLowerCase() !== identifier.toLowerCase())
|
|
60952
|
+
throw new PrivateHelperError(
|
|
60953
|
+
"invalid_response",
|
|
60954
|
+
"The helper returned a different note identifier"
|
|
60955
|
+
);
|
|
60956
|
+
return state;
|
|
60957
|
+
}
|
|
60630
60958
|
function featureFromProbe(feature) {
|
|
60631
60959
|
if (!feature) return { available: false, reason: "private_api_unavailable", detail: null };
|
|
60632
60960
|
if (feature.available) return { available: true, reason: null, detail: null };
|
|
@@ -60645,7 +60973,7 @@ function privateHelperCapabilities(deps = defaultDeps2()) {
|
|
|
60645
60973
|
reason,
|
|
60646
60974
|
detail
|
|
60647
60975
|
});
|
|
60648
|
-
const both = (status) => ({ readNoteState: status });
|
|
60976
|
+
const both = (status) => ({ readNoteState: status, readChecklist: status });
|
|
60649
60977
|
if (installation.reason === "unsupported_platform")
|
|
60650
60978
|
return {
|
|
60651
60979
|
enabled,
|
|
@@ -60688,7 +61016,10 @@ function privateHelperCapabilities(deps = defaultDeps2()) {
|
|
|
60688
61016
|
readOnly: true,
|
|
60689
61017
|
installation,
|
|
60690
61018
|
probe,
|
|
60691
|
-
features: {
|
|
61019
|
+
features: {
|
|
61020
|
+
readNoteState: featureFromProbe(probe.features.readNoteState),
|
|
61021
|
+
readChecklist: featureFromProbe(probe.features.readChecklist)
|
|
61022
|
+
}
|
|
60692
61023
|
};
|
|
60693
61024
|
}
|
|
60694
61025
|
|
|
@@ -61870,19 +62201,19 @@ function relayBroker(socket, leftover, deps) {
|
|
|
61870
62201
|
// src/tools/privateHelperTools.ts
|
|
61871
62202
|
var coreDataId2 = external_exports.string().regex(/^x-coredata:\/\/[0-9A-F-]+\/ICNote\/p\d+$/i);
|
|
61872
62203
|
var notesUuid = external_exports.string().regex(UUID_PATTERN);
|
|
61873
|
-
function resolveIdentifier(
|
|
62204
|
+
function resolveIdentifier(deps, args) {
|
|
61874
62205
|
if (args.identifier && args.id)
|
|
61875
62206
|
throw new PrivateHelperError("invalid_request", "Pass identifier or id, not both");
|
|
61876
62207
|
if (args.identifier) return args.identifier;
|
|
61877
62208
|
if (!args.id) throw new PrivateHelperError("invalid_request", "identifier or id is required");
|
|
61878
|
-
|
|
61879
|
-
const
|
|
61880
|
-
if (!
|
|
62209
|
+
assertPrivateHelperReady(deps);
|
|
62210
|
+
const identifier = lookupStableIdentifiers([args.id], "ICNote").get(args.id)?.identifier;
|
|
62211
|
+
if (!identifier || !UUID_PATTERN.test(identifier))
|
|
61881
62212
|
throw new PrivateHelperError(
|
|
61882
62213
|
"not_found",
|
|
61883
62214
|
"Could not resolve that id to a Notes UUID (needs Full Disk Access); pass identifier instead"
|
|
61884
62215
|
);
|
|
61885
|
-
return
|
|
62216
|
+
return identifier;
|
|
61886
62217
|
}
|
|
61887
62218
|
function envelopeCode(helperCode, message) {
|
|
61888
62219
|
switch (helperCode) {
|
|
@@ -61928,7 +62259,7 @@ function helperErrorResult(error2) {
|
|
|
61928
62259
|
})
|
|
61929
62260
|
);
|
|
61930
62261
|
}
|
|
61931
|
-
function registerPrivateHelperTools(server2,
|
|
62262
|
+
function registerPrivateHelperTools(server2, _manager, depsFactory = () => defaultDeps2()) {
|
|
61932
62263
|
function tool(name, description, inputSchema, annotations, handler) {
|
|
61933
62264
|
server2.registerTool(
|
|
61934
62265
|
name,
|
|
@@ -61953,7 +62284,7 @@ function registerPrivateHelperTools(server2, manager, depsFactory = () => defaul
|
|
|
61953
62284
|
}
|
|
61954
62285
|
tool(
|
|
61955
62286
|
"native-helper-status",
|
|
61956
|
-
"Use when: checking whether the opt-in, read-only native private helper is enabled, built, current, and working on this macOS before calling native-note-state.\nReturns: enabled flag, installation state (path, manifest, stale/modified checks), the live probe (macOS and Notes versions, framework, store access), and per-feature availability with a machine reason.\nDo not use when: checking the Shortcuts bridges (native-tags-status, get-capabilities).\nSafety: read-only. The helper is read-only by design (write support was deliberately deferred by the maintainer); the probe opens the Notes store read-only and only when the helper is enabled and installed.",
|
|
62287
|
+
"Use when: checking whether the opt-in, read-only native private helper is enabled, built, current, and working on this macOS before calling native-note-state or native-checklist-state.\nReturns: enabled flag, installation state (path, manifest, stale/modified checks), the live probe (macOS and Notes versions, framework, store access), and per-feature availability with a machine reason.\nDo not use when: checking the Shortcuts bridges (native-tags-status, get-capabilities).\nSafety: read-only. The helper is read-only by design (write support was deliberately deferred by the maintainer); the probe opens the Notes store read-only and only when the helper is enabled and installed.",
|
|
61957
62288
|
{},
|
|
61958
62289
|
{ readOnlyHint: true, openWorldHint: false },
|
|
61959
62290
|
(_args, deps) => {
|
|
@@ -61972,7 +62303,17 @@ function registerPrivateHelperTools(server2, manager, depsFactory = () => defaul
|
|
|
61972
62303
|
id: coreDataId2.optional().describe("x-coredata note id; resolved to a UUID via the database")
|
|
61973
62304
|
},
|
|
61974
62305
|
{ readOnlyHint: true, openWorldHint: false },
|
|
61975
|
-
(args, deps) => ({ ...readNoteState(resolveIdentifier(
|
|
62306
|
+
(args, deps) => ({ ...readNoteState(resolveIdentifier(deps, args), deps) })
|
|
62307
|
+
);
|
|
62308
|
+
tool(
|
|
62309
|
+
"native-checklist-state",
|
|
62310
|
+
"Use when: reading a note's exact native checklist todo identifiers and done state, including ambiguous style runs.\nReturns: identifier, revision change token, items in first native-style occurrence order, total, checked, and syncHostRunning. Each item has todoIdentifier (32 lowercase hex digits), uuid, index, text and done; line and styled offsets use UTF-16. contiguous=false means the styled envelope includes gaps, consistent=false means runs disagree on done, and spansLines=true means the identity spans multiple lines.\nDo not use when: reading ordinary checkbox-like text (get-note-content) or changing checklist items. Flags describe native identity ambiguity; text is the first visible styled character's line.\nSafety: read-only; requires APPLE_NOTES_MCP_ENABLE_PRIVATE=1, Full Disk Access, and a current built helper. Locked or unavailable native bodies are refused. The helper has no write action.",
|
|
62311
|
+
{
|
|
62312
|
+
identifier: notesUuid.optional().describe("Notes UUID (the notes://showNote identifier)"),
|
|
62313
|
+
id: coreDataId2.optional().describe("x-coredata note id; resolved to a UUID via the database")
|
|
62314
|
+
},
|
|
62315
|
+
{ readOnlyHint: true, openWorldHint: false },
|
|
62316
|
+
(args, deps) => ({ ...readChecklistState(resolveIdentifier(deps, args), deps) })
|
|
61976
62317
|
);
|
|
61977
62318
|
}
|
|
61978
62319
|
|
package/docs/FULL-DISK-ACCESS.md
CHANGED
|
@@ -23,7 +23,7 @@ These groups use protected Notes data:
|
|
|
23
23
|
| Portable exports | `export-notes-markdown`, `export-notes-html`, and attachment/media exports that read the Notes store |
|
|
24
24
|
| Attachments and drawings | `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, `get-note-drawings`, and `transcribe-note-audio` |
|
|
25
25
|
| Guarded operations and identifiers | `delete-folder-by-id`; database-backed copy/active-note deletion guards; UUID or numeric note/folder selectors; native preservation/readback and Smart Folder destination detection |
|
|
26
|
-
| Optional private helper | `native-note-state`; the helper is read-only and requires its separate opt-in and build |
|
|
26
|
+
| Optional private helper | `native-note-state`, `native-checklist-state`; the helper is read-only and requires its separate opt-in and build |
|
|
27
27
|
|
|
28
28
|
`get-note-link` needs FDA on macOS 26 and later. On macOS 12–15 it can fall back to Notes' AppleScript link property. `get-note-markdown` can still return Markdown without FDA, but native checklist done/undone annotations are unavailable.
|
|
29
29
|
|
|
@@ -134,6 +134,14 @@ static const APIRequirement kReadAPI[] = {
|
|
|
134
134
|
{"ICTTMergeableString", "attributedString", NO},
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
// Checklist inspection needs only getters; no editing or todo constructors.
|
|
138
|
+
static const APIRequirement kChecklistReadAPI[] = {
|
|
139
|
+
{"ICTTParagraphStyle", "style", NO},
|
|
140
|
+
{"ICTTParagraphStyle", "todo", NO},
|
|
141
|
+
{"ICTTTodo", "uuid", NO},
|
|
142
|
+
{"ICTTTodo", "done", NO},
|
|
143
|
+
};
|
|
144
|
+
|
|
137
145
|
// Core Data properties are @dynamic: their accessors do not exist until Core
|
|
138
146
|
// Data generates them, so they are checked against the managed object model
|
|
139
147
|
// (entity name, then property names) rather than with respondsToSelector:.
|
|
@@ -207,7 +215,7 @@ static NSArray<NSString *> *MissingModelProperties(void) {
|
|
|
207
215
|
return missing;
|
|
208
216
|
}
|
|
209
217
|
|
|
210
|
-
typedef NS_ENUM(NSInteger, Feature) { FeatureModel, FeatureRead };
|
|
218
|
+
typedef NS_ENUM(NSInteger, Feature) { FeatureModel, FeatureRead, FeatureChecklistRead };
|
|
211
219
|
|
|
212
220
|
static NSArray<NSString *> *MissingForFeature(Feature feature) {
|
|
213
221
|
LoadFramework();
|
|
@@ -219,6 +227,8 @@ static NSArray<NSString *> *MissingForFeature(Feature feature) {
|
|
|
219
227
|
[missing addObjectsFromArray:MissingModelProperties()];
|
|
220
228
|
[missing addObjectsFromArray:MissingAPI(kReadAPI, COUNT(kReadAPI))];
|
|
221
229
|
}
|
|
230
|
+
if (feature == FeatureChecklistRead)
|
|
231
|
+
[missing addObjectsFromArray:MissingAPI(kChecklistReadAPI, COUNT(kChecklistReadAPI))];
|
|
222
232
|
return missing;
|
|
223
233
|
}
|
|
224
234
|
|
|
@@ -401,6 +411,97 @@ static NSString *BodyText(id mergeableString) {
|
|
|
401
411
|
return [attributed isKindOfClass:[NSAttributedString class]] ? [attributed string] : nil;
|
|
402
412
|
}
|
|
403
413
|
|
|
414
|
+
// TTStyle runs can include the previous line's terminator. Group by native
|
|
415
|
+
// todo identity, not by a visible line or a checkbox-like text prefix.
|
|
416
|
+
// This operates only on an attributed-string snapshot, never the store.
|
|
417
|
+
static NSArray<NSDictionary *> *ChecklistItems(NSAttributedString *body) {
|
|
418
|
+
NSString *text = body.string;
|
|
419
|
+
NSCharacterSet *newlines = NSCharacterSet.newlineCharacterSet;
|
|
420
|
+
NSMutableArray<NSString *> *order = [NSMutableArray array];
|
|
421
|
+
NSMutableDictionary<NSString *, NSMutableDictionary *> *groups = [NSMutableDictionary dictionary];
|
|
422
|
+
[body enumerateAttribute:@"TTStyle"
|
|
423
|
+
inRange:NSMakeRange(0, body.length)
|
|
424
|
+
options:0
|
|
425
|
+
usingBlock:^(id style, NSRange run, BOOL *stop) {
|
|
426
|
+
(void)stop;
|
|
427
|
+
if (!style) return;
|
|
428
|
+
if (![style respondsToSelector:sel_registerName("style")])
|
|
429
|
+
Fail(@"private_api_unavailable", @"A native paragraph style has no style getter", nil);
|
|
430
|
+
unsigned int kind =
|
|
431
|
+
((unsigned int (*)(id, SEL))objc_msgSend)(style, sel_registerName("style"));
|
|
432
|
+
if (kind != 103) return; // Notes' native checklist paragraph style.
|
|
433
|
+
if (![style respondsToSelector:sel_registerName("todo")])
|
|
434
|
+
Fail(@"private_api_unavailable", @"A native checklist style has no todo getter", nil);
|
|
435
|
+
id todo = Send(style, "todo");
|
|
436
|
+
if (!todo || ![todo respondsToSelector:sel_registerName("uuid")] ||
|
|
437
|
+
![todo respondsToSelector:sel_registerName("done")])
|
|
438
|
+
Fail(@"private_api_unavailable", @"A native checklist todo cannot be read", nil);
|
|
439
|
+
id uuid = Send(todo, "uuid");
|
|
440
|
+
if (![uuid isKindOfClass:[NSUUID class]])
|
|
441
|
+
Fail(@"unsupported_note", @"A native checklist todo has no valid UUID", nil);
|
|
442
|
+
NSString *hex = [[[uuid UUIDString] stringByReplacingOccurrencesOfString:@"-"
|
|
443
|
+
withString:@""] lowercaseString];
|
|
444
|
+
NSMutableDictionary *group = groups[hex];
|
|
445
|
+
if (!group) {
|
|
446
|
+
group = [@{@"uuid" : [uuid UUIDString],
|
|
447
|
+
@"ranges" : [NSMutableArray array],
|
|
448
|
+
@"states" : [NSMutableSet set]} mutableCopy];
|
|
449
|
+
groups[hex] = group;
|
|
450
|
+
[order addObject:hex];
|
|
451
|
+
}
|
|
452
|
+
NSNumber *done = @(SendBool(todo, "done"));
|
|
453
|
+
[group[@"ranges"] addObject:[NSValue valueWithRange:run]];
|
|
454
|
+
[group[@"states"] addObject:done];
|
|
455
|
+
if (!group[@"fallbackDone"]) group[@"fallbackDone"] = done;
|
|
456
|
+
// Prefer the first styled visible character over a leading
|
|
457
|
+
// newline, whose done bit can differ in malformed content.
|
|
458
|
+
for (NSUInteger i = run.location; i < NSMaxRange(run); i++) {
|
|
459
|
+
if (![newlines characterIsMember:[text characterAtIndex:i]]) {
|
|
460
|
+
if (!group[@"anchor"]) {
|
|
461
|
+
group[@"anchor"] = @(i);
|
|
462
|
+
group[@"done"] = done;
|
|
463
|
+
}
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}];
|
|
468
|
+
NSMutableArray *items = [NSMutableArray array];
|
|
469
|
+
for (NSString *hex in order) {
|
|
470
|
+
NSDictionary *group = groups[hex];
|
|
471
|
+
NSArray<NSValue *> *ranges = group[@"ranges"];
|
|
472
|
+
NSRange first = ranges.firstObject.rangeValue, last = ranges.lastObject.rangeValue;
|
|
473
|
+
NSUInteger start = first.location, end = NSMaxRange(last), covered = 0;
|
|
474
|
+
for (NSValue *range in ranges) covered += range.rangeValue.length;
|
|
475
|
+
NSUInteger anchor = group[@"anchor"] ? [group[@"anchor"] unsignedIntegerValue] : start;
|
|
476
|
+
NSUInteger lineStart = 0, lineEnd = 0;
|
|
477
|
+
[text getLineStart:&lineStart end:NULL contentsEnd:&lineEnd forRange:NSMakeRange(anchor, 0)];
|
|
478
|
+
NSUInteger visibleStart = start, visibleEnd = end;
|
|
479
|
+
while (visibleStart < visibleEnd && [newlines characterIsMember:[text characterAtIndex:visibleStart]])
|
|
480
|
+
visibleStart++;
|
|
481
|
+
while (visibleEnd > visibleStart && [newlines characterIsMember:[text characterAtIndex:visibleEnd - 1]])
|
|
482
|
+
visibleEnd--;
|
|
483
|
+
BOOL spansLines = [text rangeOfCharacterFromSet:newlines
|
|
484
|
+
options:0
|
|
485
|
+
range:NSMakeRange(visibleStart, visibleEnd - visibleStart)].location != NSNotFound;
|
|
486
|
+
[items addObject:@{
|
|
487
|
+
@"todoIdentifier" : hex,
|
|
488
|
+
@"uuid" : group[@"uuid"],
|
|
489
|
+
@"index" : @(items.count),
|
|
490
|
+
@"done" : group[@"done"] ?: group[@"fallbackDone"],
|
|
491
|
+
@"text" : [text substringWithRange:NSMakeRange(lineStart, lineEnd - lineStart)],
|
|
492
|
+
@"lineStart" : @(lineStart),
|
|
493
|
+
@"lineLengthUTF16" : @(lineEnd - lineStart),
|
|
494
|
+
// The envelope includes gaps when contiguous is false.
|
|
495
|
+
@"styledStart" : @(start),
|
|
496
|
+
@"styledLengthUTF16" : @(end - start),
|
|
497
|
+
@"contiguous" : @((BOOL)(covered == end - start)),
|
|
498
|
+
@"consistent" : @((BOOL)([group[@"states"] count] == 1)),
|
|
499
|
+
@"spansLines" : @(spansLines),
|
|
500
|
+
}];
|
|
501
|
+
}
|
|
502
|
+
return items;
|
|
503
|
+
}
|
|
504
|
+
|
|
404
505
|
static BOOL NotesAppRunning(void) {
|
|
405
506
|
return [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.Notes"].count > 0;
|
|
406
507
|
}
|
|
@@ -472,6 +573,7 @@ static NSString *RequireIdentifier(NSDictionary *request) {
|
|
|
472
573
|
static NSDictionary *HandleHello(NSDictionary *request);
|
|
473
574
|
static NSDictionary *HandleProbe(NSDictionary *request);
|
|
474
575
|
static NSDictionary *HandleReadNoteState(NSDictionary *request);
|
|
576
|
+
static NSDictionary *HandleReadChecklist(NSDictionary *request);
|
|
475
577
|
|
|
476
578
|
typedef struct {
|
|
477
579
|
const char *name;
|
|
@@ -485,6 +587,7 @@ static const ActionSpec kActions[] = {
|
|
|
485
587
|
{"hello", "", HandleHello},
|
|
486
588
|
{"probe", "", HandleProbe},
|
|
487
589
|
{"read_note_state", "identifier", HandleReadNoteState},
|
|
590
|
+
{"read_checklist", "identifier", HandleReadChecklist},
|
|
488
591
|
};
|
|
489
592
|
|
|
490
593
|
static NSArray<NSString *> *ActionNames(void) {
|
|
@@ -562,6 +665,7 @@ static NSDictionary *HandleProbe(NSDictionary *request) {
|
|
|
562
665
|
@"syncHostRunning" : @(NotesAppRunning()),
|
|
563
666
|
@"features" : @{
|
|
564
667
|
@"readNoteState" : FeatureReport(FeatureRead, contextOK, contextReason),
|
|
668
|
+
@"readChecklist" : FeatureReport(FeatureChecklistRead, contextOK, contextReason),
|
|
565
669
|
},
|
|
566
670
|
};
|
|
567
671
|
}
|
|
@@ -577,6 +681,33 @@ static NSDictionary *HandleReadNoteState(NSDictionary *request) {
|
|
|
577
681
|
return result;
|
|
578
682
|
}
|
|
579
683
|
|
|
684
|
+
static NSDictionary *HandleReadChecklist(NSDictionary *request) {
|
|
685
|
+
NSString *identifier = RequireIdentifier(request);
|
|
686
|
+
RequireFeature(FeatureChecklistRead);
|
|
687
|
+
NSManagedObjectContext *context = OpenReadOnlyContext(ResolveStore());
|
|
688
|
+
NSManagedObject *note = FetchNote(context, identifier);
|
|
689
|
+
if (SendBool(note, "isPasswordProtected"))
|
|
690
|
+
Fail(@"unsupported_note", @"Locked notes are not supported", nil);
|
|
691
|
+
id mergeable = Send(note, "mergeableString");
|
|
692
|
+
if (!mergeable || ![mergeable respondsToSelector:sel_registerName("attributedString")])
|
|
693
|
+
Fail(@"unsupported_note", @"The note's native body is unavailable", nil);
|
|
694
|
+
id body = Send(mergeable, "attributedString");
|
|
695
|
+
if (![body isKindOfClass:[NSAttributedString class]])
|
|
696
|
+
Fail(@"unsupported_note", @"The note's native body is unavailable", nil);
|
|
697
|
+
NSArray<NSDictionary *> *items = ChecklistItems(body);
|
|
698
|
+
NSUInteger checked = 0;
|
|
699
|
+
for (NSDictionary *item in items) if ([item[@"done"] boolValue]) checked++;
|
|
700
|
+
return @{
|
|
701
|
+
@"status" : @"ok",
|
|
702
|
+
@"identifier" : identifier,
|
|
703
|
+
@"revision" : RevisionToken(note),
|
|
704
|
+
@"items" : items,
|
|
705
|
+
@"total" : @(items.count),
|
|
706
|
+
@"checked" : @(checked),
|
|
707
|
+
@"syncHostRunning" : @(NotesAppRunning()),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
580
711
|
#pragma mark - Main
|
|
581
712
|
|
|
582
713
|
static NSData *ReadStdin(void) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.4",
|
|
4
4
|
"description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|