apple-notes-mcp 2.8.20 → 2.8.21
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 +63 -0
- package/build/index.js +292 -110
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -663,6 +663,69 @@ Ordinary notes take the guarded HTML path and are not restricted to that subset.
|
|
|
663
663
|
|
|
664
664
|
---
|
|
665
665
|
|
|
666
|
+
#### `insert-link`
|
|
667
|
+
|
|
668
|
+
Adds one web, mail or Notes link to an exact note as its own paragraph, then
|
|
669
|
+
proves it from the link runs Notes actually stored. Use `mode: "raw"` to show
|
|
670
|
+
the URL itself, or `mode: "hyperlink"` with a `label` to show text that links to
|
|
671
|
+
the URL. For a link to another note by id, use
|
|
672
|
+
[`insert-note-link`](#insert-note-link), which looks up that note's real deep
|
|
673
|
+
link.
|
|
674
|
+
|
|
675
|
+
| Parameter | Type | Required | Description |
|
|
676
|
+
|-----------|------|----------|-------------|
|
|
677
|
+
| `id` | string | Yes | Exact CoreData note ID |
|
|
678
|
+
| `expectedContentHash` | string | Yes | `contentHash` from the exact note version being extended |
|
|
679
|
+
| `url` | string | Yes | Absolute `http(s)` URL with a host, or a `mailto:`, `notes://` or `applenotes:` link. No spaces, `<`, `>` or `"` |
|
|
680
|
+
| `mode` | string | No | `"raw"` (default) shows the URL; `"hyperlink"` shows `label` |
|
|
681
|
+
| `label` | string | Hyperlink only | Visible text for `mode: "hyperlink"`; refused in raw mode |
|
|
682
|
+
| `linked` | boolean | No | Raw mode only. `true` (default) stores a real link on the URL text. `false` writes plain text with no stored link |
|
|
683
|
+
| `position` | string | No | `"end"` (default) or `"after-title"` (first paragraph under the title) |
|
|
684
|
+
| `blankLine` | boolean | No | Leave a blank line between existing text and the link paragraph (default `true`) |
|
|
685
|
+
| `scopeText` | string | Native-object notes only | Unique existing phrase, as for [`append-native`](#append-native) |
|
|
686
|
+
|
|
687
|
+
**Example - Hyperlink under the title:**
|
|
688
|
+
```json
|
|
689
|
+
{
|
|
690
|
+
"id": "x-coredata://ABC123/ICNote/p456",
|
|
691
|
+
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
692
|
+
"url": "https://example.com/report",
|
|
693
|
+
"mode": "hyperlink",
|
|
694
|
+
"label": "Quarterly report",
|
|
695
|
+
"position": "after-title"
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**Returns:** `route` (`"applescript"` for ordinary notes, `"native"` for notes
|
|
700
|
+
with native objects), `linkStored`, `storedUrl` (the destination read back from
|
|
701
|
+
the note, for example `https://example.com/` for a bare origin), and the new
|
|
702
|
+
`contentHash`.
|
|
703
|
+
|
|
704
|
+
**Safety:** The same guards as [`append-to-note`](#append-to-note): a fresh
|
|
705
|
+
`expectedContentHash`, the attachment block, and every existing link must
|
|
706
|
+
survive. A linked insert must add exactly one stored link with the requested
|
|
707
|
+
label and destination; if the text lands but that proof fails, the error says
|
|
708
|
+
the write happened so it is not repeated.
|
|
709
|
+
|
|
710
|
+
**Limits:**
|
|
711
|
+
|
|
712
|
+
- Plain URL text is not linked by Notes when written this way. With
|
|
713
|
+
`linked: false`, the note stores no link and readers of the body see ordinary
|
|
714
|
+
text; Notes.app may still underline the URL on screen through its own data
|
|
715
|
+
detection. The result reports `linkStored: false`.
|
|
716
|
+
- Notes with native objects (a table, a checklist, native tags) take the native
|
|
717
|
+
end-append path, so only `position: "end"` with `blankLine: true` works there.
|
|
718
|
+
- The link always gets its own paragraph. Placing it at the end of one existing
|
|
719
|
+
paragraph, or inside the text, is not available.
|
|
720
|
+
- Rich URL preview cards (the link tile Notes makes when you paste a URL) are
|
|
721
|
+
not produced. No public automation route creates one: the Shortcuts Notes
|
|
722
|
+
actions write text, and AppleScript's `body` has no card markup.
|
|
723
|
+
- To start a new note with a link, use [`create-note`](#create-note) with
|
|
724
|
+
`format: "html"` and an `<a href>` in `content`; the link is stored the same
|
|
725
|
+
way.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
666
729
|
#### `get-note-link`
|
|
667
730
|
|
|
668
731
|
Returns the `notes://showNote?identifier=<uuid>` deep-link URL for a note. The URL opens the note in Notes.app on iOS and macOS and can be stored in Reminders tasks or shared links.
|
package/build/index.js
CHANGED
|
@@ -43796,6 +43796,121 @@ function parseNoteTable(compressed) {
|
|
|
43796
43796
|
return { rows: values, rowIds: rows.ids, columnIds: columns.ids };
|
|
43797
43797
|
}
|
|
43798
43798
|
|
|
43799
|
+
// src/utils/linkInsert.ts
|
|
43800
|
+
var MAX_LINK_URL_LENGTH = 4096;
|
|
43801
|
+
var MAX_LINK_LABEL_LENGTH = 2e3;
|
|
43802
|
+
var LINK_SCHEMES = /^(?:https?:\/\/[^/?#\s]+|mailto:[^\s]|notes:\/\/[^\s]|applenotes:[^\s])/i;
|
|
43803
|
+
function validateLinkUrl(url) {
|
|
43804
|
+
if (!url) throw new Error("Link URL is required");
|
|
43805
|
+
if (url.length > MAX_LINK_URL_LENGTH)
|
|
43806
|
+
throw new Error(`Link URL is longer than ${MAX_LINK_URL_LENGTH} characters`);
|
|
43807
|
+
if (Array.from(url).some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127))
|
|
43808
|
+
throw new Error("Link URL cannot contain spaces, line breaks or control characters");
|
|
43809
|
+
if (/[<>"]/u.test(url)) throw new Error('Link URL cannot contain <, > or "');
|
|
43810
|
+
if (!LINK_SCHEMES.test(url))
|
|
43811
|
+
throw new Error(
|
|
43812
|
+
"Link URL must be an absolute http(s) URL with a host, or a mailto:, notes:// or applenotes: link"
|
|
43813
|
+
);
|
|
43814
|
+
return url;
|
|
43815
|
+
}
|
|
43816
|
+
function validateLinkLabel(label) {
|
|
43817
|
+
if (!label.trim()) throw new Error("Hyperlink label must contain visible text");
|
|
43818
|
+
if (label.length > MAX_LINK_LABEL_LENGTH)
|
|
43819
|
+
throw new Error(`Hyperlink label is longer than ${MAX_LINK_LABEL_LENGTH} characters`);
|
|
43820
|
+
if (Array.from(label).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
43821
|
+
throw new Error("Hyperlink label must be one line without control characters");
|
|
43822
|
+
return label;
|
|
43823
|
+
}
|
|
43824
|
+
var escapeText = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
43825
|
+
var escapeHref = (url) => escapeText(url).replace(/"/g, """);
|
|
43826
|
+
function buildLinkInsertion(request) {
|
|
43827
|
+
const url = validateLinkUrl(request.url);
|
|
43828
|
+
if (request.mode === "hyperlink") {
|
|
43829
|
+
if (request.label === void 0) throw new Error("Hyperlink mode requires a label");
|
|
43830
|
+
if (request.linked === false)
|
|
43831
|
+
throw new Error("linked=false applies to raw mode only; a hyperlink is always linked");
|
|
43832
|
+
const label = validateLinkLabel(request.label);
|
|
43833
|
+
return {
|
|
43834
|
+
html: `<div><a href="${escapeHref(url)}">${escapeText(label)}</a></div>`,
|
|
43835
|
+
text: label,
|
|
43836
|
+
link: { text: label, url }
|
|
43837
|
+
};
|
|
43838
|
+
}
|
|
43839
|
+
if (request.label !== void 0)
|
|
43840
|
+
throw new Error("A label applies to hyperlink mode only; raw mode shows the URL itself");
|
|
43841
|
+
if (request.linked === false)
|
|
43842
|
+
return { html: `<div>${escapeText(url)}</div>`, text: url, link: null };
|
|
43843
|
+
return {
|
|
43844
|
+
html: `<div><a href="${escapeHref(url)}">${escapeText(url)}</a></div>`,
|
|
43845
|
+
text: url,
|
|
43846
|
+
link: { text: url, url }
|
|
43847
|
+
};
|
|
43848
|
+
}
|
|
43849
|
+
function countMatchingLinks(links, expected) {
|
|
43850
|
+
const wanted = linkSignature([expected]);
|
|
43851
|
+
return links.filter((link) => linkSignature([link]) === wanted).length;
|
|
43852
|
+
}
|
|
43853
|
+
function countLinksTo(links, url) {
|
|
43854
|
+
const wanted = linkSignature([{ text: "x", url }]);
|
|
43855
|
+
return links.filter((link) => linkSignature([{ text: "x", url: link.url }]) === wanted).length;
|
|
43856
|
+
}
|
|
43857
|
+
function verifyLinkReadback(before, after, insertion, url) {
|
|
43858
|
+
for (const link of before) {
|
|
43859
|
+
if (countMatchingLinks(after, link) < countMatchingLinks(before, link))
|
|
43860
|
+
throw new Error("An existing link changed during the insert; read the note before retrying");
|
|
43861
|
+
}
|
|
43862
|
+
if (insertion.link) {
|
|
43863
|
+
const added = countMatchingLinks(after, insertion.link) - countMatchingLinks(before, insertion.link);
|
|
43864
|
+
if (added !== 1)
|
|
43865
|
+
throw new Error(
|
|
43866
|
+
"The inserted link was not found in the note's stored links; read the note before retrying"
|
|
43867
|
+
);
|
|
43868
|
+
const stored = after.filter(
|
|
43869
|
+
(link) => linkSignature([link]) === linkSignature([insertion.link])
|
|
43870
|
+
);
|
|
43871
|
+
return { linkStored: true, storedUrl: stored[stored.length - 1].url };
|
|
43872
|
+
}
|
|
43873
|
+
const gained = countLinksTo(after, url) - countLinksTo(before, url);
|
|
43874
|
+
return gained > 0 ? {
|
|
43875
|
+
linkStored: true,
|
|
43876
|
+
storedUrl: after.filter((link) => countLinksTo([link], url) === 1).pop().url
|
|
43877
|
+
} : { linkStored: false };
|
|
43878
|
+
}
|
|
43879
|
+
|
|
43880
|
+
// src/services/linkInsert.ts
|
|
43881
|
+
function insertLink(request, deps) {
|
|
43882
|
+
const insertion = buildLinkInsertion(request);
|
|
43883
|
+
const before = deps.readLinks(request.id);
|
|
43884
|
+
const outcome = deps.append({
|
|
43885
|
+
id: request.id,
|
|
43886
|
+
expectedContentHash: request.expectedContentHash,
|
|
43887
|
+
content: insertion.html,
|
|
43888
|
+
position: request.position === "after-title" ? "before" : "after",
|
|
43889
|
+
separator: request.blankLine ? "\n\n" : "",
|
|
43890
|
+
scopeText: request.scopeText
|
|
43891
|
+
});
|
|
43892
|
+
let readback;
|
|
43893
|
+
try {
|
|
43894
|
+
readback = verifyLinkReadback(before, deps.readLinks(request.id), insertion, request.url);
|
|
43895
|
+
} catch (error2) {
|
|
43896
|
+
throw new Error(
|
|
43897
|
+
`The text was written, but link verification failed: ${error2 instanceof Error ? error2.message : String(error2)}. Do not retry automatically.`
|
|
43898
|
+
);
|
|
43899
|
+
}
|
|
43900
|
+
return {
|
|
43901
|
+
ok: true,
|
|
43902
|
+
id: request.id,
|
|
43903
|
+
mode: request.mode,
|
|
43904
|
+
url: request.url,
|
|
43905
|
+
text: insertion.text,
|
|
43906
|
+
position: request.position,
|
|
43907
|
+
route: outcome.route,
|
|
43908
|
+
...readback,
|
|
43909
|
+
previousContentHash: request.expectedContentHash,
|
|
43910
|
+
contentHash: outcome.contentHash
|
|
43911
|
+
};
|
|
43912
|
+
}
|
|
43913
|
+
|
|
43799
43914
|
// src/tools/directOperations.ts
|
|
43800
43915
|
import { createHash as createHash2 } from "node:crypto";
|
|
43801
43916
|
import {
|
|
@@ -45277,6 +45392,115 @@ registerTool(
|
|
|
45277
45392
|
"Error updating note"
|
|
45278
45393
|
)
|
|
45279
45394
|
);
|
|
45395
|
+
function guardedAppend({
|
|
45396
|
+
id: id2,
|
|
45397
|
+
expectedContentHash,
|
|
45398
|
+
content,
|
|
45399
|
+
position = "after",
|
|
45400
|
+
separator = "\n\n",
|
|
45401
|
+
format = "plaintext",
|
|
45402
|
+
scopeText,
|
|
45403
|
+
htmlSeparator
|
|
45404
|
+
}, onRoute) {
|
|
45405
|
+
const contentToHtml = (text) => {
|
|
45406
|
+
if (format === "html") return text;
|
|
45407
|
+
return text.split("\n").map((line) => {
|
|
45408
|
+
const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45409
|
+
return `<div>${escaped || "<br>"}</div>`;
|
|
45410
|
+
}).join("");
|
|
45411
|
+
};
|
|
45412
|
+
const separatorToHtml = (sep2) => {
|
|
45413
|
+
if (format === "html") return sep2;
|
|
45414
|
+
if (sep2 === "\n\n") return "<div><br></div>";
|
|
45415
|
+
const escaped = sep2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45416
|
+
return `<div>${escaped}</div>`;
|
|
45417
|
+
};
|
|
45418
|
+
const snapshot = readExactNoteSnapshot(id2);
|
|
45419
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
45420
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
45421
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
45422
|
+
}
|
|
45423
|
+
if (!snapshot.rich.writable) {
|
|
45424
|
+
if (separator !== "\n\n")
|
|
45425
|
+
return errorResponse("Native append supports the default blank-line separator only");
|
|
45426
|
+
if (position !== "after") return errorResponse("Native append supports the end of a note only");
|
|
45427
|
+
if (!scopeText)
|
|
45428
|
+
return errorResponse("Provide scopeText: a unique existing phrase for native append");
|
|
45429
|
+
if (!VERIFIED_BACKGROUND.has("append-native"))
|
|
45430
|
+
return errorResponse("Native append has not passed live validation; see get-capabilities");
|
|
45431
|
+
onRoute?.("native");
|
|
45432
|
+
const result2 = appendNative(notesManager, {
|
|
45433
|
+
id: id2,
|
|
45434
|
+
expectedContentHash,
|
|
45435
|
+
scopeText,
|
|
45436
|
+
content,
|
|
45437
|
+
format
|
|
45438
|
+
});
|
|
45439
|
+
return successResponse("Native append verified without replacing existing objects", result2);
|
|
45440
|
+
}
|
|
45441
|
+
onRoute?.("applescript");
|
|
45442
|
+
const attachments = notesManager.listAttachmentsById(id2);
|
|
45443
|
+
if (attachments.length > 0) {
|
|
45444
|
+
return errorResponse(
|
|
45445
|
+
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
|
|
45446
|
+
);
|
|
45447
|
+
}
|
|
45448
|
+
assertLinkedWrite(snapshot.rich, snapshot.rich.content, "html");
|
|
45449
|
+
const firstDivEnd = snapshot.rich.content.indexOf("</div>");
|
|
45450
|
+
const titleDiv = firstDivEnd !== -1 ? snapshot.rich.content.slice(0, firstDivEnd + 6) : "";
|
|
45451
|
+
const bodyHtml = firstDivEnd !== -1 ? snapshot.rich.content.slice(firstDivEnd + 6) : snapshot.rich.content;
|
|
45452
|
+
const newBlock = contentToHtml(content);
|
|
45453
|
+
const sepHtml = htmlSeparator ?? separatorToHtml(separator);
|
|
45454
|
+
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
45455
|
+
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
45456
|
+
id2,
|
|
45457
|
+
snapshot.note.title,
|
|
45458
|
+
snapshot.body,
|
|
45459
|
+
void 0,
|
|
45460
|
+
combinedBody,
|
|
45461
|
+
"html",
|
|
45462
|
+
snapshot.rich.revision
|
|
45463
|
+
);
|
|
45464
|
+
if (result.status === "conflict") {
|
|
45465
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
45466
|
+
}
|
|
45467
|
+
if (result.status === "attachments") {
|
|
45468
|
+
return errorResponse(
|
|
45469
|
+
`Note "${snapshot.note.title}" gained an attachment before saving. No content was appended.`
|
|
45470
|
+
);
|
|
45471
|
+
}
|
|
45472
|
+
if (result.status !== "updated") {
|
|
45473
|
+
return errorResponse(
|
|
45474
|
+
`The append result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
45475
|
+
);
|
|
45476
|
+
}
|
|
45477
|
+
const readback = notesManager.getNoteContentById(id2);
|
|
45478
|
+
const richReadback = enrichNoteRead(id2, readback || "");
|
|
45479
|
+
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
45480
|
+
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
45481
|
+
return errorResponse(
|
|
45482
|
+
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
45483
|
+
);
|
|
45484
|
+
}
|
|
45485
|
+
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
45486
|
+
return errorResponse(
|
|
45487
|
+
`The note accepted an append, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id2} in Notes.app.`
|
|
45488
|
+
);
|
|
45489
|
+
}
|
|
45490
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
45491
|
+
return successResponse(
|
|
45492
|
+
`Note appended; visible text verified: "${snapshot.note.title}"${sharedWarning}`,
|
|
45493
|
+
{
|
|
45494
|
+
ok: true,
|
|
45495
|
+
id: id2,
|
|
45496
|
+
title: snapshot.note.title,
|
|
45497
|
+
shared: snapshot.note.shared ?? false,
|
|
45498
|
+
previousContentHash: expectedContentHash,
|
|
45499
|
+
contentHash,
|
|
45500
|
+
verifiedVisibleText: true
|
|
45501
|
+
}
|
|
45502
|
+
);
|
|
45503
|
+
}
|
|
45280
45504
|
registerTool(
|
|
45281
45505
|
"append-to-note",
|
|
45282
45506
|
{
|
|
@@ -45302,118 +45526,76 @@ registerTool(
|
|
|
45302
45526
|
verifiedVisibleText: external_exports.boolean().optional()
|
|
45303
45527
|
}
|
|
45304
45528
|
},
|
|
45305
|
-
withErrorHandling(
|
|
45306
|
-
|
|
45307
|
-
|
|
45308
|
-
|
|
45309
|
-
|
|
45310
|
-
|
|
45311
|
-
|
|
45312
|
-
|
|
45313
|
-
|
|
45314
|
-
|
|
45315
|
-
|
|
45316
|
-
|
|
45317
|
-
|
|
45318
|
-
|
|
45319
|
-
|
|
45320
|
-
|
|
45321
|
-
|
|
45322
|
-
|
|
45323
|
-
|
|
45324
|
-
|
|
45325
|
-
|
|
45326
|
-
|
|
45327
|
-
|
|
45328
|
-
|
|
45329
|
-
|
|
45330
|
-
|
|
45331
|
-
|
|
45332
|
-
|
|
45333
|
-
|
|
45334
|
-
|
|
45335
|
-
|
|
45336
|
-
|
|
45337
|
-
|
|
45338
|
-
|
|
45339
|
-
|
|
45340
|
-
|
|
45341
|
-
|
|
45342
|
-
|
|
45529
|
+
withErrorHandling((args) => guardedAppend(args), "Error appending to note")
|
|
45530
|
+
);
|
|
45531
|
+
registerTool(
|
|
45532
|
+
"insert-link",
|
|
45533
|
+
{
|
|
45534
|
+
description: "Use when: adding one web, mail or Notes link to an exact note, either as the raw URL (mode 'raw') or as label text that links to the URL (mode 'hyperlink').\nReturns: exact id, the route used, whether Notes stored a link on the inserted text, the stored destination read back from the note, and the new content hash.\nDo not use when: linking to another note by id (insert-note-link fetches its real deep link), or you want a rich URL preview card (not available: no public automation route creates one).\nSafety: same guards as append-to-note (fresh expectedContentHash, attachment block, existing links must survive). Notes with native objects use native end-append and need scopeText; there only position 'end' with a blank line is supported. The link is placed in its own paragraph at the end or directly after the title; placing it inside an existing paragraph is not supported.",
|
|
45535
|
+
inputSchema: {
|
|
45536
|
+
id: noteIdInput,
|
|
45537
|
+
expectedContentHash: expectedContentHashInput,
|
|
45538
|
+
url: external_exports.string().min(1, "url is required").max(MAX_LINK_URL_LENGTH).describe(
|
|
45539
|
+
"Link destination: absolute http(s) URL with a host, or a mailto:, notes:// or applenotes: link. No spaces."
|
|
45540
|
+
),
|
|
45541
|
+
mode: external_exports.enum(["raw", "hyperlink"]).optional().default("raw").describe(
|
|
45542
|
+
"'raw' (default) shows the URL itself; 'hyperlink' shows `label` linking to the URL"
|
|
45543
|
+
),
|
|
45544
|
+
label: external_exports.string().max(MAX_LINK_LABEL_LENGTH).optional().describe("Visible text for mode 'hyperlink' (required there, refused in raw mode)"),
|
|
45545
|
+
linked: external_exports.boolean().optional().describe(
|
|
45546
|
+
"Raw mode only. true (default) stores a real link on the URL text. false writes plain text with no stored link; Notes may still detect it when displaying, and the result reports linkStored."
|
|
45547
|
+
),
|
|
45548
|
+
position: external_exports.enum(["end", "after-title"]).optional().default("end").describe("'end' (default) or 'after-title' (first paragraph below the title)"),
|
|
45549
|
+
blankLine: external_exports.boolean().optional().default(true).describe(
|
|
45550
|
+
"Leave a blank line between existing text and the link paragraph (default true; native-object notes require true)"
|
|
45551
|
+
),
|
|
45552
|
+
scopeText: external_exports.string().min(12).max(500).optional().describe("Existing unique phrase; required only for notes with native objects")
|
|
45553
|
+
},
|
|
45554
|
+
outputSchema: {
|
|
45555
|
+
ok: external_exports.boolean().optional(),
|
|
45556
|
+
id: external_exports.string().optional(),
|
|
45557
|
+
mode: external_exports.string().optional(),
|
|
45558
|
+
url: external_exports.string().optional(),
|
|
45559
|
+
text: external_exports.string().optional(),
|
|
45560
|
+
position: external_exports.string().optional(),
|
|
45561
|
+
route: external_exports.string().optional(),
|
|
45562
|
+
linkStored: external_exports.boolean().optional(),
|
|
45563
|
+
storedUrl: external_exports.string().optional(),
|
|
45564
|
+
previousContentHash: external_exports.string().optional(),
|
|
45565
|
+
contentHash: external_exports.string().optional()
|
|
45566
|
+
}
|
|
45567
|
+
},
|
|
45568
|
+
withErrorHandling((args) => {
|
|
45569
|
+
const result = insertLink(
|
|
45570
|
+
{
|
|
45571
|
+
...args,
|
|
45572
|
+
mode: args.mode ?? "raw",
|
|
45573
|
+
position: args.position ?? "end",
|
|
45574
|
+
blankLine: args.blankLine ?? true
|
|
45575
|
+
},
|
|
45576
|
+
{
|
|
45577
|
+
readLinks: (noteId3) => readRichNote(noteId3).links,
|
|
45578
|
+
append: (request) => {
|
|
45579
|
+
let route = "applescript";
|
|
45580
|
+
const response = guardedAppend(
|
|
45581
|
+
{
|
|
45582
|
+
...request,
|
|
45583
|
+
format: "html",
|
|
45584
|
+
htmlSeparator: request.separator ? "<div><br></div>" : ""
|
|
45585
|
+
},
|
|
45586
|
+
(r) => route = r
|
|
45343
45587
|
);
|
|
45344
|
-
|
|
45345
|
-
|
|
45346
|
-
expectedContentHash,
|
|
45347
|
-
scopeText,
|
|
45348
|
-
content,
|
|
45349
|
-
format
|
|
45350
|
-
});
|
|
45351
|
-
return successResponse("Native append verified without replacing existing objects", result2);
|
|
45352
|
-
}
|
|
45353
|
-
const attachments = notesManager.listAttachmentsById(id2);
|
|
45354
|
-
if (attachments.length > 0) {
|
|
45355
|
-
return errorResponse(
|
|
45356
|
-
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
|
|
45357
|
-
);
|
|
45358
|
-
}
|
|
45359
|
-
assertLinkedWrite(snapshot.rich, snapshot.rich.content, "html");
|
|
45360
|
-
const firstDivEnd = snapshot.rich.content.indexOf("</div>");
|
|
45361
|
-
const titleDiv = firstDivEnd !== -1 ? snapshot.rich.content.slice(0, firstDivEnd + 6) : "";
|
|
45362
|
-
const bodyHtml = firstDivEnd !== -1 ? snapshot.rich.content.slice(firstDivEnd + 6) : snapshot.rich.content;
|
|
45363
|
-
const newBlock = contentToHtml(content);
|
|
45364
|
-
const sepHtml = separatorToHtml(separator);
|
|
45365
|
-
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
45366
|
-
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
45367
|
-
id2,
|
|
45368
|
-
snapshot.note.title,
|
|
45369
|
-
snapshot.body,
|
|
45370
|
-
void 0,
|
|
45371
|
-
combinedBody,
|
|
45372
|
-
"html",
|
|
45373
|
-
snapshot.rich.revision
|
|
45374
|
-
);
|
|
45375
|
-
if (result.status === "conflict") {
|
|
45376
|
-
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
45377
|
-
}
|
|
45378
|
-
if (result.status === "attachments") {
|
|
45379
|
-
return errorResponse(
|
|
45380
|
-
`Note "${snapshot.note.title}" gained an attachment before saving. No content was appended.`
|
|
45381
|
-
);
|
|
45382
|
-
}
|
|
45383
|
-
if (result.status !== "updated") {
|
|
45384
|
-
return errorResponse(
|
|
45385
|
-
`The append result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
45386
|
-
);
|
|
45387
|
-
}
|
|
45388
|
-
const readback = notesManager.getNoteContentById(id2);
|
|
45389
|
-
const richReadback = enrichNoteRead(id2, readback || "");
|
|
45390
|
-
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
45391
|
-
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
45392
|
-
return errorResponse(
|
|
45393
|
-
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
45394
|
-
);
|
|
45395
|
-
}
|
|
45396
|
-
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
45397
|
-
return errorResponse(
|
|
45398
|
-
`The note accepted an append, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id2} in Notes.app.`
|
|
45399
|
-
);
|
|
45400
|
-
}
|
|
45401
|
-
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
45402
|
-
return successResponse(
|
|
45403
|
-
`Note appended; visible text verified: "${snapshot.note.title}"${sharedWarning}`,
|
|
45404
|
-
{
|
|
45405
|
-
ok: true,
|
|
45406
|
-
id: id2,
|
|
45407
|
-
title: snapshot.note.title,
|
|
45408
|
-
shared: snapshot.note.shared ?? false,
|
|
45409
|
-
previousContentHash: expectedContentHash,
|
|
45410
|
-
contentHash,
|
|
45411
|
-
verifiedVisibleText: true
|
|
45588
|
+
if (response.isError) throw new Error(response.content[0].text);
|
|
45589
|
+
return { route, contentHash: String(response.structuredContent?.contentHash ?? "") };
|
|
45412
45590
|
}
|
|
45413
|
-
|
|
45414
|
-
|
|
45415
|
-
|
|
45416
|
-
|
|
45591
|
+
}
|
|
45592
|
+
);
|
|
45593
|
+
const stored = result.linkStored ? `stored link to ${result.storedUrl}` : "no stored link (plain text)";
|
|
45594
|
+
return successResponse(
|
|
45595
|
+
`Link inserted (${result.mode}, ${result.position}, ${result.route}); ${stored}`,
|
|
45596
|
+
{ ...result }
|
|
45597
|
+
);
|
|
45598
|
+
}, "Error inserting link")
|
|
45417
45599
|
);
|
|
45418
45600
|
registerTool(
|
|
45419
45601
|
"delete-note",
|
package/package.json
CHANGED