apple-notes-mcp 2.8.4 → 2.8.6
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 +33 -8
- package/build/index.js +699 -146
- 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
|
|
@@ -302,6 +303,26 @@ attachment-bearing notes; edit them in Notes.app.
|
|
|
302
303
|
|
|
303
304
|
---
|
|
304
305
|
|
|
306
|
+
#### `get-native-objects`
|
|
307
|
+
|
|
308
|
+
Reads native object identities and ranges, checklist item IDs and state, actual
|
|
309
|
+
native tags, and native table data from one exact note ID. Table output includes
|
|
310
|
+
stable row and column identifiers. `tableCellsComplete` is false when Notes
|
|
311
|
+
metadata cannot be decoded completely. This tool is read-only and requires Full
|
|
312
|
+
Disk Access.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
#### `list-native-tags`
|
|
317
|
+
|
|
318
|
+
Lists actual native Notes tags used within one explicit `account` and `folder`,
|
|
319
|
+
mapping each tag to its matching note IDs. This differs from textual hashtag
|
|
320
|
+
search. The response reports `complete: false` and per-note errors when some
|
|
321
|
+
native metadata is unavailable. This tool is read-only and requires Full Disk
|
|
322
|
+
Access.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
305
326
|
#### `get-note-plaintext`
|
|
306
327
|
|
|
307
328
|
Retrieves a note's body as plain text, with no HTML markup.
|
|
@@ -385,6 +406,7 @@ Updates an existing note's content and/or title.
|
|
|
385
406
|
| `newTitle` | string | No | New title (if changing the title; ignored when `format` is `"html"`) |
|
|
386
407
|
| `newContent` | string | Yes | New content for the note body |
|
|
387
408
|
| `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) |
|
|
409
|
+
| `allowLinkChanges` | boolean | No | Set to `true` only when intentionally changing or removing existing links |
|
|
388
410
|
|
|
389
411
|
Title-only updates are rejected because Apple Notes titles are not unique.
|
|
390
412
|
|
|
@@ -414,8 +436,10 @@ byte-identical rich formatting.
|
|
|
414
436
|
|
|
415
437
|
**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
438
|
|
|
417
|
-
**
|
|
418
|
-
|
|
439
|
+
**Rich-content safety:** `update-note` refuses to replace a note when its rich
|
|
440
|
+
metadata is unavailable or it contains attachments, native tags, inline
|
|
441
|
+
objects, or checklists that AppleScript cannot preserve. Existing link
|
|
442
|
+
destinations must remain present unless `allowLinkChanges` is explicitly set.
|
|
419
443
|
|
|
420
444
|
---
|
|
421
445
|
|
|
@@ -509,8 +533,9 @@ Title-only appends are rejected.
|
|
|
509
533
|
visible text after saving, not byte-identical rich formatting. Warns when the
|
|
510
534
|
note is shared with collaborators.
|
|
511
535
|
|
|
512
|
-
**Safety:** The append is rejected if the note changed since it was read
|
|
513
|
-
the note contains
|
|
536
|
+
**Safety:** The append is rejected if the note changed since it was read, rich
|
|
537
|
+
metadata is unavailable, or the note contains attachments or other native
|
|
538
|
+
objects. Existing link destinations are verified after saving.
|
|
514
539
|
|
|
515
540
|
---
|
|
516
541
|
|
package/build/index.js
CHANGED
|
@@ -3358,9 +3358,9 @@ var require_utils = __commonJS({
|
|
|
3358
3358
|
let output = "";
|
|
3359
3359
|
for (let i = 0; i < input.length; i++) {
|
|
3360
3360
|
if (input[i] === "%" && i + 2 < input.length) {
|
|
3361
|
-
const
|
|
3362
|
-
if (isHexPair(
|
|
3363
|
-
const normalizedHex =
|
|
3361
|
+
const hex2 = input.slice(i + 1, i + 3);
|
|
3362
|
+
if (isHexPair(hex2)) {
|
|
3363
|
+
const normalizedHex = hex2.toUpperCase();
|
|
3364
3364
|
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3365
3365
|
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3366
3366
|
output += decoded;
|
|
@@ -3380,9 +3380,9 @@ var require_utils = __commonJS({
|
|
|
3380
3380
|
for (let i = 0; i < input.length; i++) {
|
|
3381
3381
|
const ch = input[i];
|
|
3382
3382
|
if (ch === "%" && i + 2 < input.length) {
|
|
3383
|
-
const
|
|
3384
|
-
if (isHexPair(
|
|
3385
|
-
const normalizedHex =
|
|
3383
|
+
const hex2 = input.slice(i + 1, i + 3);
|
|
3384
|
+
if (isHexPair(hex2)) {
|
|
3385
|
+
const normalizedHex = hex2.toUpperCase();
|
|
3386
3386
|
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3387
3387
|
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3388
3388
|
output += decoded;
|
|
@@ -3422,9 +3422,9 @@ var require_utils = __commonJS({
|
|
|
3422
3422
|
for (let i = 0; i < input.length; i++) {
|
|
3423
3423
|
const ch = input[i];
|
|
3424
3424
|
if (ch === "%" && i + 2 < input.length) {
|
|
3425
|
-
const
|
|
3426
|
-
if (isHexPair(
|
|
3427
|
-
output += "%" +
|
|
3425
|
+
const hex2 = input.slice(i + 1, i + 3);
|
|
3426
|
+
if (isHexPair(hex2)) {
|
|
3427
|
+
output += "%" + hex2.toUpperCase();
|
|
3428
3428
|
i += 2;
|
|
3429
3429
|
continue;
|
|
3430
3430
|
}
|
|
@@ -3460,9 +3460,9 @@ var require_utils = __commonJS({
|
|
|
3460
3460
|
for (let i = 0; i < input.length; i++) {
|
|
3461
3461
|
const ch = input[i];
|
|
3462
3462
|
if (ch === "%" && i + 2 < input.length) {
|
|
3463
|
-
const
|
|
3464
|
-
if (isHexPair(
|
|
3465
|
-
output += "%" +
|
|
3463
|
+
const hex2 = input.slice(i + 1, i + 3);
|
|
3464
|
+
if (isHexPair(hex2)) {
|
|
3465
|
+
output += "%" + hex2.toUpperCase();
|
|
3466
3466
|
i += 2;
|
|
3467
3467
|
continue;
|
|
3468
3468
|
}
|
|
@@ -3507,9 +3507,9 @@ var require_utils = __commonJS({
|
|
|
3507
3507
|
for (let i = 0; i < input.length; i++) {
|
|
3508
3508
|
const ch = input[i];
|
|
3509
3509
|
if (ch === "%" && i + 2 < input.length) {
|
|
3510
|
-
const
|
|
3511
|
-
if (isHexPair(
|
|
3512
|
-
const normalizedHex =
|
|
3510
|
+
const hex2 = input.slice(i + 1, i + 3);
|
|
3511
|
+
if (isHexPair(hex2)) {
|
|
3512
|
+
const normalizedHex = hex2.toUpperCase();
|
|
3513
3513
|
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3514
3514
|
if (isUnreserved(decoded)) {
|
|
3515
3515
|
output += decoded;
|
|
@@ -3547,9 +3547,9 @@ var require_utils = __commonJS({
|
|
|
3547
3547
|
let output = "";
|
|
3548
3548
|
for (let i = 0; i < input.length; i++) {
|
|
3549
3549
|
if (input[i] === "%" && i + 2 < input.length) {
|
|
3550
|
-
const
|
|
3551
|
-
if (isHexPair(
|
|
3552
|
-
output += "%" +
|
|
3550
|
+
const hex2 = input.slice(i + 1, i + 3);
|
|
3551
|
+
if (isHexPair(hex2)) {
|
|
3552
|
+
output += "%" + hex2.toUpperCase();
|
|
3553
3553
|
i += 2;
|
|
3554
3554
|
continue;
|
|
3555
3555
|
}
|
|
@@ -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,
|
|
@@ -9329,8 +9329,8 @@ var require_select = __commonJS({
|
|
|
9329
9329
|
regex = regex.replace(name, val.source || val);
|
|
9330
9330
|
return new RegExp(regex);
|
|
9331
9331
|
};
|
|
9332
|
-
var truncateUrl = function(url,
|
|
9333
|
-
return url.replace(/^(?:\w+:\/\/|\/+)/, "").replace(/(?:\/+|\/*#.*?)$/, "").split("/",
|
|
9332
|
+
var truncateUrl = function(url, num2) {
|
|
9333
|
+
return url.replace(/^(?:\w+:\/\/|\/+)/, "").replace(/(?:\/+|\/*#.*?)$/, "").split("/", num2).join("/");
|
|
9334
9334
|
};
|
|
9335
9335
|
var parseNth = function(param_, test) {
|
|
9336
9336
|
var param = param_.replace(/\s+/g, ""), cap;
|
|
@@ -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);
|
|
@@ -24777,8 +24777,8 @@ var ZodError = class _ZodError extends Error {
|
|
|
24777
24777
|
constructor(issues) {
|
|
24778
24778
|
super();
|
|
24779
24779
|
this.issues = [];
|
|
24780
|
-
this.addIssue = (
|
|
24781
|
-
this.issues = [...this.issues,
|
|
24780
|
+
this.addIssue = (sub2) => {
|
|
24781
|
+
this.issues = [...this.issues, sub2];
|
|
24782
24782
|
};
|
|
24783
24783
|
this.addIssues = (subs = []) => {
|
|
24784
24784
|
this.issues = [...this.issues, ...subs];
|
|
@@ -24845,13 +24845,13 @@ var ZodError = class _ZodError extends Error {
|
|
|
24845
24845
|
flatten(mapper = (issue2) => issue2.message) {
|
|
24846
24846
|
const fieldErrors = {};
|
|
24847
24847
|
const formErrors = [];
|
|
24848
|
-
for (const
|
|
24849
|
-
if (
|
|
24850
|
-
const firstEl =
|
|
24848
|
+
for (const sub2 of this.issues) {
|
|
24849
|
+
if (sub2.path.length > 0) {
|
|
24850
|
+
const firstEl = sub2.path[0];
|
|
24851
24851
|
fieldErrors[firstEl] = fieldErrors[firstEl] || [];
|
|
24852
|
-
fieldErrors[firstEl].push(mapper(
|
|
24852
|
+
fieldErrors[firstEl].push(mapper(sub2));
|
|
24853
24853
|
} else {
|
|
24854
|
-
formErrors.push(mapper(
|
|
24854
|
+
formErrors.push(mapper(sub2));
|
|
24855
24855
|
}
|
|
24856
24856
|
}
|
|
24857
24857
|
return { formErrors, fieldErrors };
|
|
@@ -29150,12 +29150,12 @@ var $ZodRealError = $constructor("$ZodError", initializer, { Parent: Error });
|
|
|
29150
29150
|
function flattenError(error2, mapper = (issue2) => issue2.message) {
|
|
29151
29151
|
const fieldErrors = {};
|
|
29152
29152
|
const formErrors = [];
|
|
29153
|
-
for (const
|
|
29154
|
-
if (
|
|
29155
|
-
fieldErrors[
|
|
29156
|
-
fieldErrors[
|
|
29153
|
+
for (const sub2 of error2.issues) {
|
|
29154
|
+
if (sub2.path.length > 0) {
|
|
29155
|
+
fieldErrors[sub2.path[0]] = fieldErrors[sub2.path[0]] || [];
|
|
29156
|
+
fieldErrors[sub2.path[0]].push(mapper(sub2));
|
|
29157
29157
|
} else {
|
|
29158
|
-
formErrors.push(mapper(
|
|
29158
|
+
formErrors.push(mapper(sub2));
|
|
29159
29159
|
}
|
|
29160
29160
|
}
|
|
29161
29161
|
return { formErrors, fieldErrors };
|
|
@@ -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);
|
|
@@ -39364,9 +39364,9 @@ function queryNoteData(noteId) {
|
|
|
39364
39364
|
timeout: 5e3,
|
|
39365
39365
|
stdio: ["pipe", "pipe", "pipe"]
|
|
39366
39366
|
});
|
|
39367
|
-
const
|
|
39368
|
-
if (!
|
|
39369
|
-
return { hex };
|
|
39367
|
+
const hex2 = result.trim();
|
|
39368
|
+
if (!hex2) return { hex: null };
|
|
39369
|
+
return { hex: hex2 };
|
|
39370
39370
|
} catch (error2) {
|
|
39371
39371
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
39372
39372
|
console.error(`Failed to query NoteStore database: ${message}`);
|
|
@@ -39376,10 +39376,10 @@ function queryNoteData(noteId) {
|
|
|
39376
39376
|
return { hex: null };
|
|
39377
39377
|
}
|
|
39378
39378
|
}
|
|
39379
|
-
function hexToBytes(
|
|
39380
|
-
const bytes = new Uint8Array(
|
|
39381
|
-
for (let i = 0; i <
|
|
39382
|
-
bytes[i / 2] = parseInt(
|
|
39379
|
+
function hexToBytes(hex2) {
|
|
39380
|
+
const bytes = new Uint8Array(hex2.length / 2);
|
|
39381
|
+
for (let i = 0; i < hex2.length; i += 2) {
|
|
39382
|
+
bytes[i / 2] = parseInt(hex2.substring(i, i + 2), 16);
|
|
39383
39383
|
}
|
|
39384
39384
|
return bytes;
|
|
39385
39385
|
}
|
|
@@ -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"]
|
|
@@ -42199,7 +42511,7 @@ function decodeHtmlEntities(text) {
|
|
|
42199
42511
|
}
|
|
42200
42512
|
return String.fromCodePoint(codePoint);
|
|
42201
42513
|
};
|
|
42202
|
-
return text.replace(/&#x([0-9a-f]+);?/gi, (match,
|
|
42514
|
+
return text.replace(/&#x([0-9a-f]+);?/gi, (match, hex2) => decodeCodePoint(match, hex2, 16)).replace(/&#([0-9]+);?/g, (match, decimal) => decodeCodePoint(match, decimal, 10)).replace(/ (?:;|(?![0-9a-z]))/gi, " ").replace(/"(?:;|(?![0-9a-z]))/gi, '"').replace(/&apos(?:;|(?![0-9a-z]))/gi, "'").replace(/<(?:;|(?![0-9a-z]))/gi, "<").replace(/>(?:;|(?![0-9a-z]))/gi, ">").replace(/&(?:;|(?![0-9a-z]))/gi, "&");
|
|
42203
42515
|
}
|
|
42204
42516
|
function firstVisibleHtmlLine(html) {
|
|
42205
42517
|
let text = html;
|
|
@@ -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,10 +42844,6 @@ function withJsonSchema2020_12(transport2) {
|
|
|
42532
42844
|
}
|
|
42533
42845
|
|
|
42534
42846
|
// src/utils/noteRevision.ts
|
|
42535
|
-
import { createHash } from "node:crypto";
|
|
42536
|
-
function hashNoteContent(content) {
|
|
42537
|
-
return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
42538
|
-
}
|
|
42539
42847
|
var INLINE_TAG = /^<\/?(?:b|i|u|s|strike|em|strong|span|a|font|sub|sup|code|tt|small|big|mark)\b/i;
|
|
42540
42848
|
function comparableVisibleText(html) {
|
|
42541
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(
|
|
@@ -42544,6 +42852,103 @@ function comparableVisibleText(html) {
|
|
|
42544
42852
|
).replace(/\s+/g, " ").trim();
|
|
42545
42853
|
}
|
|
42546
42854
|
|
|
42855
|
+
// src/utils/noteTables.ts
|
|
42856
|
+
import { gunzipSync as gunzipSync3 } from "node:zlib";
|
|
42857
|
+
var sub = (f, n) => {
|
|
42858
|
+
const value = embeddedMessage(getField(f, n));
|
|
42859
|
+
if (!value) throw new Error(`Missing table field ${n}`);
|
|
42860
|
+
return value;
|
|
42861
|
+
};
|
|
42862
|
+
var num = (f, n) => {
|
|
42863
|
+
const v = varintValue(getField(f, n));
|
|
42864
|
+
if (v === void 0) throw new Error(`Missing table index ${n}`);
|
|
42865
|
+
return v;
|
|
42866
|
+
};
|
|
42867
|
+
var many = (f, n) => getFields(f, n).map((v) => {
|
|
42868
|
+
const m = embeddedMessage(v);
|
|
42869
|
+
if (!m) throw new Error("Invalid table entry");
|
|
42870
|
+
return m;
|
|
42871
|
+
});
|
|
42872
|
+
var hex = (f) => {
|
|
42873
|
+
if (!(f?.value instanceof Uint8Array)) throw new Error("Missing table UUID");
|
|
42874
|
+
return Buffer.from(f.value).toString("hex");
|
|
42875
|
+
};
|
|
42876
|
+
function parseNoteTable(compressed) {
|
|
42877
|
+
const root = decodeMessage(gunzipSync3(compressed, { maxOutputLength: 16 * 1024 * 1024 }));
|
|
42878
|
+
const data = sub(sub(root, 2), 3), entries = many(data, 3);
|
|
42879
|
+
if (entries.length > 1e5) throw new Error("Table too large");
|
|
42880
|
+
const keys = getFields(data, 4).map(stringValue), types = getFields(data, 5).map(stringValue), uuids = getFields(data, 6).map(hex);
|
|
42881
|
+
const entry = (index) => {
|
|
42882
|
+
if (!entries[index]) throw new Error("Invalid table reference");
|
|
42883
|
+
return entries[index];
|
|
42884
|
+
};
|
|
42885
|
+
const uuidIndex = (index) => num(sub(many(sub(entry(index), 13), 3)[0], 2), 2);
|
|
42886
|
+
const roots = entries.filter((e) => {
|
|
42887
|
+
const map = embeddedMessage(getField(e, 13));
|
|
42888
|
+
return map && types[num(map, 1)] === "com.apple.notes.ICTable";
|
|
42889
|
+
});
|
|
42890
|
+
if (roots.length !== 1) throw new Error("Ambiguous native table root");
|
|
42891
|
+
const refs = new Map(
|
|
42892
|
+
many(sub(roots[0], 13), 3).filter((m) => ["crRows", "crColumns", "cellColumns"].includes(keys[num(m, 1)] || "")).map((m) => [keys[num(m, 1)], num(sub(m, 2), 6)])
|
|
42893
|
+
);
|
|
42894
|
+
const ordered = (key) => {
|
|
42895
|
+
const ref = refs.get(key);
|
|
42896
|
+
if (ref === void 0) throw new Error("Missing table dimension");
|
|
42897
|
+
const ordering = sub(sub(entry(ref), 16), 1), array2 = sub(ordering, 1);
|
|
42898
|
+
const ids = many(array2, 2).map((a) => hex(getField(a, 2)));
|
|
42899
|
+
const map = /* @__PURE__ */ new Map();
|
|
42900
|
+
ids.forEach((id, i) => {
|
|
42901
|
+
const index = uuids.indexOf(id);
|
|
42902
|
+
if (index < 0) throw new Error("Missing dimension UUID");
|
|
42903
|
+
map.set(index, i);
|
|
42904
|
+
});
|
|
42905
|
+
const aliases = many(sub(ordering, 2), 1).map((pair) => [
|
|
42906
|
+
uuidIndex(num(sub(pair, 1), 6)),
|
|
42907
|
+
uuidIndex(num(sub(pair, 2), 6))
|
|
42908
|
+
]);
|
|
42909
|
+
for (let pass = 0; pass < aliases.length + 1; pass++) {
|
|
42910
|
+
let changed = false;
|
|
42911
|
+
for (const [key2, value] of aliases)
|
|
42912
|
+
if (map.has(key2) && !map.has(value)) {
|
|
42913
|
+
map.set(value, map.get(key2));
|
|
42914
|
+
changed = true;
|
|
42915
|
+
}
|
|
42916
|
+
if (!changed) break;
|
|
42917
|
+
}
|
|
42918
|
+
return { ids, map };
|
|
42919
|
+
};
|
|
42920
|
+
const rows = ordered("crRows"), columns = ordered("crColumns");
|
|
42921
|
+
if (!rows.ids.length || !columns.ids.length || rows.ids.length * columns.ids.length > 1e5)
|
|
42922
|
+
throw new Error("Unsupported table size");
|
|
42923
|
+
const values = rows.ids.map(() => columns.ids.map(() => ""));
|
|
42924
|
+
const cellRef = refs.get("cellColumns");
|
|
42925
|
+
if (cellRef === void 0) throw new Error("Missing table cells");
|
|
42926
|
+
for (const column of many(sub(entry(cellRef), 6), 1)) {
|
|
42927
|
+
const ci = columns.map.get(uuidIndex(num(sub(column, 1), 6)));
|
|
42928
|
+
const cells = entry(num(sub(column, 2), 6));
|
|
42929
|
+
for (const row of many(sub(cells, 6), 1)) {
|
|
42930
|
+
const ri = rows.map.get(uuidIndex(num(sub(row, 1), 6)));
|
|
42931
|
+
if (ri === void 0 || ci === void 0) continue;
|
|
42932
|
+
const note = sub(entry(num(sub(row, 2), 6)), 10);
|
|
42933
|
+
const text = stringValue(getField(note, 2));
|
|
42934
|
+
if (text === void 0 || text.includes("\uFFFC"))
|
|
42935
|
+
throw new Error("Embedded or unsupported table cell");
|
|
42936
|
+
values[ri][ci] = text.replace(/\n$/u, "");
|
|
42937
|
+
}
|
|
42938
|
+
}
|
|
42939
|
+
const rtl = entries.some((e) => {
|
|
42940
|
+
const map = embeddedMessage(getField(e, 13));
|
|
42941
|
+
return map && many(map, 3).some(
|
|
42942
|
+
(m) => stringValue(getField(sub(m, 2), 4)) === "CRTableColumnDirectionRightToLeft"
|
|
42943
|
+
);
|
|
42944
|
+
});
|
|
42945
|
+
if (rtl) {
|
|
42946
|
+
for (const row of values) row.reverse();
|
|
42947
|
+
columns.ids.reverse();
|
|
42948
|
+
}
|
|
42949
|
+
return { rows: values, rowIds: rows.ids, columnIds: columns.ids };
|
|
42950
|
+
}
|
|
42951
|
+
|
|
42547
42952
|
// src/index.ts
|
|
42548
42953
|
loadFileConfig();
|
|
42549
42954
|
var require2 = createRequire(import.meta.url);
|
|
@@ -42611,7 +43016,8 @@ function readExactNoteSnapshot(id) {
|
|
|
42611
43016
|
}
|
|
42612
43017
|
const body = notesManager.getNoteContentById(id);
|
|
42613
43018
|
if (!body) return { error: `Failed to read content of note "${note.title}"` };
|
|
42614
|
-
|
|
43019
|
+
const rich = enrichNoteRead(id, body);
|
|
43020
|
+
return { note, body, rich, contentHash: richContentHash(body, rich) };
|
|
42615
43021
|
}
|
|
42616
43022
|
function revisionConflictMessage(title) {
|
|
42617
43023
|
return `Note "${title}" changed after it was read. Read it again and review the newer version before retrying.`;
|
|
@@ -42641,7 +43047,7 @@ registerTool(
|
|
|
42641
43047
|
),
|
|
42642
43048
|
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
42643
43049
|
tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
|
|
42644
|
-
"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
|
|
43050
|
+
"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."
|
|
42645
43051
|
),
|
|
42646
43052
|
folder: external_exports.string().max(MAX.FOLDER).optional().describe(
|
|
42647
43053
|
"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."
|
|
@@ -42675,7 +43081,7 @@ registerTool(
|
|
|
42675
43081
|
`A note may have been created, but its exact ID could not be verified. Do not retry automatically. Returned ID: ${note.id}`
|
|
42676
43082
|
);
|
|
42677
43083
|
}
|
|
42678
|
-
const contentHash =
|
|
43084
|
+
const contentHash = richContentHash(createdBody, enrichNoteRead(note.id, createdBody));
|
|
42679
43085
|
const checklistWarning = detectChecklistAttempt(content) ?? "";
|
|
42680
43086
|
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`, {
|
|
42681
43087
|
ok: true,
|
|
@@ -42764,7 +43170,7 @@ ${noteList}${truncationNote}${syncNote}`,
|
|
|
42764
43170
|
registerTool(
|
|
42765
43171
|
"get-note-content",
|
|
42766
43172
|
{
|
|
42767
|
-
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.",
|
|
43173
|
+
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.",
|
|
42768
43174
|
inputSchema: {
|
|
42769
43175
|
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42770
43176
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
@@ -42778,6 +43184,13 @@ registerTool(
|
|
|
42778
43184
|
content: external_exports.string().optional(),
|
|
42779
43185
|
contentHash: external_exports.string().optional(),
|
|
42780
43186
|
hashtags: external_exports.array(external_exports.string()).optional(),
|
|
43187
|
+
nativeTags: external_exports.array(external_exports.string()).optional(),
|
|
43188
|
+
links: external_exports.array(
|
|
43189
|
+
external_exports.object({ start: external_exports.number(), length: external_exports.number(), text: external_exports.string(), url: external_exports.string() })
|
|
43190
|
+
).optional(),
|
|
43191
|
+
richContentComplete: external_exports.boolean().optional(),
|
|
43192
|
+
writable: external_exports.boolean().optional(),
|
|
43193
|
+
warning: external_exports.string().optional(),
|
|
42781
43194
|
/** Number of oversized inline images replaced with text placeholders. */
|
|
42782
43195
|
strippedImages: external_exports.number().optional(),
|
|
42783
43196
|
/** True when content is lossy — see strippedImages. Never write a truncated body back. */
|
|
@@ -42799,15 +43212,21 @@ registerTool(
|
|
|
42799
43212
|
if (!rawContent2) {
|
|
42800
43213
|
return errorResponse(`Failed to read content of note "${note2.title}"`);
|
|
42801
43214
|
}
|
|
42802
|
-
const
|
|
43215
|
+
const rich2 = enrichNoteRead(id, rawContent2);
|
|
43216
|
+
const stripped2 = stripLargeInlineImages(rich2.content);
|
|
42803
43217
|
const content2 = stripped2.html;
|
|
42804
43218
|
const hashtags2 = parseHashtags(content2);
|
|
42805
|
-
const warning2 = strippedImagesWarning(stripped2);
|
|
43219
|
+
const warning2 = [strippedImagesWarning(stripped2), rich2.warning].filter(Boolean).join("\n\n");
|
|
42806
43220
|
return successResponse(warning2 ? content2 + warning2 : content2, {
|
|
42807
43221
|
id,
|
|
42808
43222
|
title: note2.title,
|
|
42809
43223
|
content: content2,
|
|
42810
|
-
contentHash:
|
|
43224
|
+
contentHash: richContentHash(rawContent2, rich2),
|
|
43225
|
+
links: rich2.links,
|
|
43226
|
+
nativeTags: rich2.nativeTags,
|
|
43227
|
+
richContentComplete: rich2.complete,
|
|
43228
|
+
writable: rich2.writable && stripped2.strippedCount === 0,
|
|
43229
|
+
warning: rich2.warning,
|
|
42811
43230
|
hashtags: hashtags2,
|
|
42812
43231
|
strippedImages: stripped2.strippedCount,
|
|
42813
43232
|
truncated: stripped2.strippedCount > 0
|
|
@@ -42829,15 +43248,21 @@ registerTool(
|
|
|
42829
43248
|
if (!rawContent) {
|
|
42830
43249
|
return errorResponse(`Failed to read content of note "${title}"`);
|
|
42831
43250
|
}
|
|
42832
|
-
const
|
|
43251
|
+
const rich = enrichNoteRead(note.id, rawContent);
|
|
43252
|
+
const stripped = stripLargeInlineImages(rich.content);
|
|
42833
43253
|
const content = stripped.html;
|
|
42834
43254
|
const hashtags = parseHashtags(content);
|
|
42835
|
-
const warning = strippedImagesWarning(stripped);
|
|
43255
|
+
const warning = [strippedImagesWarning(stripped), rich.warning].filter(Boolean).join("\n\n");
|
|
42836
43256
|
return successResponse(warning ? content + warning : content, {
|
|
42837
43257
|
id: note.id,
|
|
42838
43258
|
title,
|
|
42839
43259
|
content,
|
|
42840
|
-
contentHash:
|
|
43260
|
+
contentHash: richContentHash(rawContent, rich),
|
|
43261
|
+
links: rich.links,
|
|
43262
|
+
nativeTags: rich.nativeTags,
|
|
43263
|
+
richContentComplete: rich.complete,
|
|
43264
|
+
writable: rich.writable && stripped.strippedCount === 0,
|
|
43265
|
+
warning: rich.warning,
|
|
42841
43266
|
hashtags,
|
|
42842
43267
|
strippedImages: stripped.strippedCount,
|
|
42843
43268
|
truncated: stripped.strippedCount > 0
|
|
@@ -43078,13 +43503,110 @@ registerTool(
|
|
|
43078
43503
|
return successResponse(`Shown account with ID "${id}" in Notes.app`, { id, separately });
|
|
43079
43504
|
}, "Error showing account")
|
|
43080
43505
|
);
|
|
43506
|
+
registerTool(
|
|
43507
|
+
"get-native-objects",
|
|
43508
|
+
{
|
|
43509
|
+
description: "Use when: inspecting native objects, checklist identities, or tables in one exact note.\nReturns: native object IDs and ranges, checklist IDs and state, actual native tags, decoded tables, and the current rich content hash.\nDo not use when: you only need the note body (get-note-content) or AppleScript attachment metadata (list-attachments).\nSafety: read-only; requires Full Disk Access and reports incomplete table metadata instead of guessing.",
|
|
43510
|
+
inputSchema: { id: noteIdInput },
|
|
43511
|
+
outputSchema: {
|
|
43512
|
+
id: external_exports.string().optional(),
|
|
43513
|
+
contentHash: external_exports.string().optional(),
|
|
43514
|
+
objects: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
43515
|
+
checklistItems: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
43516
|
+
nativeTags: external_exports.array(external_exports.string()).optional(),
|
|
43517
|
+
tables: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
43518
|
+
tableCellsComplete: external_exports.boolean().optional()
|
|
43519
|
+
},
|
|
43520
|
+
annotations: { readOnlyHint: true }
|
|
43521
|
+
},
|
|
43522
|
+
withErrorHandling(({ id }) => {
|
|
43523
|
+
const note = notesManager.getNoteById(id);
|
|
43524
|
+
if (!note) return errorResponse(`Note with ID "${id}" not found`);
|
|
43525
|
+
const body = notesManager.getNoteContentById(id);
|
|
43526
|
+
if (!body) return errorResponse(`Failed to read content of note "${note.title}"`);
|
|
43527
|
+
const rich = readRichNote(id);
|
|
43528
|
+
const tables = (rich.objectData || []).filter((object3) => object3.type?.includes("table")).map((object3) => {
|
|
43529
|
+
try {
|
|
43530
|
+
return {
|
|
43531
|
+
id: object3.id,
|
|
43532
|
+
attachmentId: id.replace(/ICNote\/p\d+$/, `ICAttachment/p${object3.pk}`),
|
|
43533
|
+
complete: true,
|
|
43534
|
+
...parseNoteTable(Buffer.from(object3.mergeable, "hex"))
|
|
43535
|
+
};
|
|
43536
|
+
} catch (error2) {
|
|
43537
|
+
return { id: object3.id, complete: false, reason: String(error2) };
|
|
43538
|
+
}
|
|
43539
|
+
});
|
|
43540
|
+
for (const object3 of rich.objects || []) {
|
|
43541
|
+
if (object3.type.includes("table") && !tables.some((table) => table.id === object3.id)) {
|
|
43542
|
+
tables.push({
|
|
43543
|
+
id: object3.id,
|
|
43544
|
+
complete: false,
|
|
43545
|
+
reason: "Native table metadata is unavailable"
|
|
43546
|
+
});
|
|
43547
|
+
}
|
|
43548
|
+
}
|
|
43549
|
+
const richRead = {
|
|
43550
|
+
content: body,
|
|
43551
|
+
links: rich.links,
|
|
43552
|
+
nativeTags: rich.nativeTags,
|
|
43553
|
+
complete: true,
|
|
43554
|
+
writable: !rich.hasNativeObjects && !rich.hasChecklist,
|
|
43555
|
+
revision: rich.revision
|
|
43556
|
+
};
|
|
43557
|
+
return successResponse("Native objects read from the exact note", {
|
|
43558
|
+
id,
|
|
43559
|
+
contentHash: richContentHash(body, richRead),
|
|
43560
|
+
objects: rich.objects,
|
|
43561
|
+
checklistItems: rich.checklistItems,
|
|
43562
|
+
nativeTags: rich.nativeTags,
|
|
43563
|
+
tables,
|
|
43564
|
+
tableCellsComplete: tables.every((table) => table.complete)
|
|
43565
|
+
});
|
|
43566
|
+
}, "Error reading native objects")
|
|
43567
|
+
);
|
|
43568
|
+
registerTool(
|
|
43569
|
+
"list-native-tags",
|
|
43570
|
+
{
|
|
43571
|
+
description: "Use when: listing actual native Notes tags used in one explicit account and folder.\nReturns: each native tag mapped to exact matching note IDs, plus completeness and per-note errors.\nDo not use when: searching textual #hashtags in note bodies (search-notes).\nSafety: read-only; requires Full Disk Access and discloses partial reads.",
|
|
43572
|
+
inputSchema: {
|
|
43573
|
+
account: external_exports.string().min(1).max(MAX.ACCOUNT),
|
|
43574
|
+
folder: external_exports.string().min(1).max(MAX.FOLDER)
|
|
43575
|
+
},
|
|
43576
|
+
outputSchema: {
|
|
43577
|
+
tags: external_exports.record(external_exports.array(external_exports.string())).optional(),
|
|
43578
|
+
complete: external_exports.boolean().optional(),
|
|
43579
|
+
errors: external_exports.record(external_exports.string()).optional()
|
|
43580
|
+
},
|
|
43581
|
+
annotations: { readOnlyHint: true }
|
|
43582
|
+
},
|
|
43583
|
+
withErrorHandling(({ account, folder }) => {
|
|
43584
|
+
const tags = {};
|
|
43585
|
+
const errors = {};
|
|
43586
|
+
for (const note of notesManager.listNoteRefs(account, folder)) {
|
|
43587
|
+
try {
|
|
43588
|
+
for (const tag of readRichNote(note.id).nativeTags) (tags[tag] ||= []).push(note.id);
|
|
43589
|
+
} catch {
|
|
43590
|
+
errors[note.id] = "Native metadata unavailable";
|
|
43591
|
+
}
|
|
43592
|
+
}
|
|
43593
|
+
return successResponse("Native tags read from the requested folder", {
|
|
43594
|
+
tags,
|
|
43595
|
+
complete: Object.keys(errors).length === 0,
|
|
43596
|
+
errors
|
|
43597
|
+
});
|
|
43598
|
+
}, "Error listing native tags")
|
|
43599
|
+
);
|
|
43081
43600
|
registerTool(
|
|
43082
43601
|
"update-note",
|
|
43083
43602
|
{
|
|
43084
|
-
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
|
|
43603
|
+
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.",
|
|
43085
43604
|
inputSchema: {
|
|
43086
43605
|
id: noteIdInput,
|
|
43087
43606
|
expectedContentHash: expectedContentHashInput,
|
|
43607
|
+
allowLinkChanges: external_exports.boolean().optional().default(false).describe(
|
|
43608
|
+
"Set true only when the user explicitly intends to remove, relabel or change existing links. Defaults to preserving all links."
|
|
43609
|
+
),
|
|
43088
43610
|
newTitle: external_exports.string().max(MAX.TITLE).optional().describe(
|
|
43089
43611
|
"New title for plaintext updates. Ignored when format is 'html'; include the visible title as the first line of newContent instead."
|
|
43090
43612
|
),
|
|
@@ -43103,72 +43625,90 @@ registerTool(
|
|
|
43103
43625
|
verifiedVisibleText: external_exports.boolean().optional()
|
|
43104
43626
|
}
|
|
43105
43627
|
},
|
|
43106
|
-
withErrorHandling(
|
|
43107
|
-
|
|
43108
|
-
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
43109
|
-
if (snapshot.contentHash !== expectedContentHash) {
|
|
43110
|
-
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43111
|
-
}
|
|
43112
|
-
const attachments = notesManager.listAttachmentsById(id);
|
|
43113
|
-
if (attachments.length > 0) {
|
|
43114
|
-
return errorResponse(
|
|
43115
|
-
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
|
|
43116
|
-
);
|
|
43117
|
-
}
|
|
43118
|
-
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
43628
|
+
withErrorHandling(
|
|
43629
|
+
({
|
|
43119
43630
|
id,
|
|
43120
|
-
|
|
43121
|
-
snapshot.body,
|
|
43631
|
+
expectedContentHash,
|
|
43122
43632
|
newTitle,
|
|
43123
43633
|
newContent,
|
|
43124
|
-
format
|
|
43125
|
-
|
|
43126
|
-
|
|
43127
|
-
|
|
43128
|
-
|
|
43129
|
-
|
|
43130
|
-
|
|
43131
|
-
|
|
43634
|
+
format = "plaintext",
|
|
43635
|
+
allowLinkChanges = false
|
|
43636
|
+
}) => {
|
|
43637
|
+
const snapshot = readExactNoteSnapshot(id);
|
|
43638
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
43639
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
43640
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43641
|
+
}
|
|
43642
|
+
assertLinkedWrite(snapshot.rich, newContent, format, allowLinkChanges);
|
|
43643
|
+
const attachments = notesManager.listAttachmentsById(id);
|
|
43644
|
+
if (attachments.length > 0) {
|
|
43645
|
+
return errorResponse(
|
|
43646
|
+
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
|
|
43647
|
+
);
|
|
43648
|
+
}
|
|
43649
|
+
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
43650
|
+
id,
|
|
43651
|
+
snapshot.note.title,
|
|
43652
|
+
snapshot.body,
|
|
43653
|
+
newTitle,
|
|
43654
|
+
newContent,
|
|
43655
|
+
format,
|
|
43656
|
+
snapshot.rich.revision
|
|
43132
43657
|
);
|
|
43133
|
-
|
|
43134
|
-
|
|
43135
|
-
|
|
43136
|
-
|
|
43658
|
+
if (result.status === "conflict") {
|
|
43659
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43660
|
+
}
|
|
43661
|
+
if (result.status === "attachments") {
|
|
43662
|
+
return errorResponse(
|
|
43663
|
+
`Note "${snapshot.note.title}" gained an attachment before saving. No content was replaced.`
|
|
43664
|
+
);
|
|
43665
|
+
}
|
|
43666
|
+
if (result.status !== "updated") {
|
|
43667
|
+
return errorResponse(
|
|
43668
|
+
`The update result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
43669
|
+
);
|
|
43670
|
+
}
|
|
43671
|
+
const readback = notesManager.getNoteContentById(id);
|
|
43672
|
+
const richReadback = enrichNoteRead(id, readback || "");
|
|
43673
|
+
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
43674
|
+
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
43675
|
+
return errorResponse(
|
|
43676
|
+
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
43677
|
+
);
|
|
43678
|
+
}
|
|
43679
|
+
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
43680
|
+
return errorResponse(
|
|
43681
|
+
`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.`
|
|
43682
|
+
);
|
|
43683
|
+
}
|
|
43684
|
+
const displayTitle = resolveUpdateResponseTitle(
|
|
43685
|
+
snapshot.note.title,
|
|
43686
|
+
newTitle,
|
|
43687
|
+
format,
|
|
43688
|
+
newContent
|
|
43137
43689
|
);
|
|
43138
|
-
|
|
43139
|
-
|
|
43140
|
-
|
|
43141
|
-
|
|
43142
|
-
|
|
43143
|
-
|
|
43690
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
43691
|
+
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
43692
|
+
return successResponse(
|
|
43693
|
+
`Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
|
|
43694
|
+
{
|
|
43695
|
+
ok: true,
|
|
43696
|
+
id,
|
|
43697
|
+
title: displayTitle,
|
|
43698
|
+
shared: snapshot.note.shared ?? false,
|
|
43699
|
+
previousContentHash: expectedContentHash,
|
|
43700
|
+
contentHash,
|
|
43701
|
+
verifiedVisibleText: true
|
|
43702
|
+
}
|
|
43144
43703
|
);
|
|
43145
|
-
}
|
|
43146
|
-
|
|
43147
|
-
|
|
43148
|
-
newTitle,
|
|
43149
|
-
format,
|
|
43150
|
-
newContent
|
|
43151
|
-
);
|
|
43152
|
-
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
43153
|
-
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
43154
|
-
return successResponse(
|
|
43155
|
-
`Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
|
|
43156
|
-
{
|
|
43157
|
-
ok: true,
|
|
43158
|
-
id,
|
|
43159
|
-
title: displayTitle,
|
|
43160
|
-
shared: snapshot.note.shared ?? false,
|
|
43161
|
-
previousContentHash: expectedContentHash,
|
|
43162
|
-
contentHash,
|
|
43163
|
-
verifiedVisibleText: true
|
|
43164
|
-
}
|
|
43165
|
-
);
|
|
43166
|
-
}, "Error updating note")
|
|
43704
|
+
},
|
|
43705
|
+
"Error updating note"
|
|
43706
|
+
)
|
|
43167
43707
|
);
|
|
43168
43708
|
registerTool(
|
|
43169
43709
|
"append-to-note",
|
|
43170
43710
|
{
|
|
43171
|
-
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
|
|
43711
|
+
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.",
|
|
43172
43712
|
inputSchema: {
|
|
43173
43713
|
id: noteIdInput,
|
|
43174
43714
|
expectedContentHash: expectedContentHashInput,
|
|
@@ -43216,15 +43756,21 @@ registerTool(
|
|
|
43216
43756
|
if (snapshot.contentHash !== expectedContentHash) {
|
|
43217
43757
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43218
43758
|
}
|
|
43759
|
+
if (!snapshot.rich.writable) {
|
|
43760
|
+
return errorResponse(
|
|
43761
|
+
snapshot.rich.warning || `Note "${snapshot.note.title}" contains native objects that cannot be preserved by a full-body append.`
|
|
43762
|
+
);
|
|
43763
|
+
}
|
|
43219
43764
|
const attachments = notesManager.listAttachmentsById(id);
|
|
43220
43765
|
if (attachments.length > 0) {
|
|
43221
43766
|
return errorResponse(
|
|
43222
43767
|
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
|
|
43223
43768
|
);
|
|
43224
43769
|
}
|
|
43225
|
-
|
|
43226
|
-
const
|
|
43227
|
-
const
|
|
43770
|
+
assertLinkedWrite(snapshot.rich, snapshot.rich.content, "html");
|
|
43771
|
+
const firstDivEnd = snapshot.rich.content.indexOf("</div>");
|
|
43772
|
+
const titleDiv = firstDivEnd !== -1 ? snapshot.rich.content.slice(0, firstDivEnd + 6) : "";
|
|
43773
|
+
const bodyHtml = firstDivEnd !== -1 ? snapshot.rich.content.slice(firstDivEnd + 6) : snapshot.rich.content;
|
|
43228
43774
|
const newBlock = contentToHtml(content);
|
|
43229
43775
|
const sepHtml = separatorToHtml(separator);
|
|
43230
43776
|
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
@@ -43234,7 +43780,8 @@ registerTool(
|
|
|
43234
43780
|
snapshot.body,
|
|
43235
43781
|
void 0,
|
|
43236
43782
|
combinedBody,
|
|
43237
|
-
"html"
|
|
43783
|
+
"html",
|
|
43784
|
+
snapshot.rich.revision
|
|
43238
43785
|
);
|
|
43239
43786
|
if (result.status === "conflict") {
|
|
43240
43787
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
@@ -43250,7 +43797,13 @@ registerTool(
|
|
|
43250
43797
|
);
|
|
43251
43798
|
}
|
|
43252
43799
|
const readback = notesManager.getNoteContentById(id);
|
|
43253
|
-
const
|
|
43800
|
+
const richReadback = enrichNoteRead(id, readback || "");
|
|
43801
|
+
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
43802
|
+
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
43803
|
+
return errorResponse(
|
|
43804
|
+
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
43805
|
+
);
|
|
43806
|
+
}
|
|
43254
43807
|
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
43255
43808
|
return errorResponse(
|
|
43256
43809
|
`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