apple-notes-mcp 2.8.20 → 2.8.22
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 +95 -0
- package/build/index.js +372 -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.
|
|
@@ -1169,6 +1232,38 @@ in the foreground.
|
|
|
1169
1232
|
Appends one real unchecked Notes checklist item and verifies its native identity
|
|
1170
1233
|
and text.
|
|
1171
1234
|
|
|
1235
|
+
#### `create-checklist-items`
|
|
1236
|
+
|
|
1237
|
+
Appends several real unchecked checklist items, 1 to 20, in the order given.
|
|
1238
|
+
It takes the same `id`, `expectedContentHash` and `scopeText` as
|
|
1239
|
+
`create-checklist-item`, plus `items`, an array of one-line texts. Each item is
|
|
1240
|
+
one run of the same verified bridge, so the call takes a few seconds per item
|
|
1241
|
+
(a full batch of 20 can run about a minute; if your client times out first,
|
|
1242
|
+
read the note before retrying rather than resending the whole batch) and is
|
|
1243
|
+
gated with `create-checklist-item` in `get-capabilities`.
|
|
1244
|
+
|
|
1245
|
+
After every run the server checks that exactly one new unchecked item with that
|
|
1246
|
+
text and a new native identity appeared, that every item appended earlier in the
|
|
1247
|
+
call kept its identity and text, and that nothing else in the note changed. The
|
|
1248
|
+
verified revision feeds the next run. A final check confirms the new items are
|
|
1249
|
+
the note's last checklist items in the requested order.
|
|
1250
|
+
|
|
1251
|
+
```json
|
|
1252
|
+
{
|
|
1253
|
+
"id": "x-coredata://ABC123/ICNote/p456",
|
|
1254
|
+
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
1255
|
+
"scopeText": "Packing list for the trip",
|
|
1256
|
+
"items": ["Passport", "Charger", "Rain jacket"]
|
|
1257
|
+
}
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
**Returns:** `items`, each with its `index`, native `id` and `text`, plus
|
|
1261
|
+
`orderVerified` and the new `contentHash`. The first uncertain result stops the
|
|
1262
|
+
call without a retry and returns `ok: false` with `landed` (the verified items),
|
|
1263
|
+
`stoppedAt` (the item's index, text, `outcome` of `"not-written"` or
|
|
1264
|
+
`"uncertain"`, and the error), and `notAttempted`. After an uncertain stop, read
|
|
1265
|
+
the note before retrying, and retry only the items that are not present.
|
|
1266
|
+
|
|
1172
1267
|
#### `create-table`
|
|
1173
1268
|
|
|
1174
1269
|
Appends a native table from rectangular string rows and verifies every decoded
|
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 {
|
|
@@ -44106,6 +44221,72 @@ var common = {
|
|
|
44106
44221
|
)
|
|
44107
44222
|
};
|
|
44108
44223
|
var htmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
44224
|
+
var MAX_CHECKLIST_BATCH = 20;
|
|
44225
|
+
var byPosition = (items = []) => [...items].sort((a, b) => a.start - b.start);
|
|
44226
|
+
function appendChecklistItems(args, deps, readRich) {
|
|
44227
|
+
const existing = new Set(byPosition(readRich(args.id).checklistItems).map((item) => item.id));
|
|
44228
|
+
const landed = [];
|
|
44229
|
+
let contentHash = args.expectedContentHash;
|
|
44230
|
+
for (const [index, text] of args.items.entries()) {
|
|
44231
|
+
let wrote = false;
|
|
44232
|
+
try {
|
|
44233
|
+
const result = mutateBackground(
|
|
44234
|
+
{ ...args, expectedContentHash: contentHash },
|
|
44235
|
+
"create-checklist-item",
|
|
44236
|
+
{ text },
|
|
44237
|
+
(before, after) => {
|
|
44238
|
+
wrote = true;
|
|
44239
|
+
assertPreserved(before, after, { append: true });
|
|
44240
|
+
const added = after.checklist.slice(before.checklist.length);
|
|
44241
|
+
if (added.length !== 1 || added[0].text !== text || added[0].done)
|
|
44242
|
+
throw new Error("Native checklist item not verified");
|
|
44243
|
+
},
|
|
44244
|
+
deps
|
|
44245
|
+
);
|
|
44246
|
+
const items = byPosition(readRich(args.id).checklistItems);
|
|
44247
|
+
const fresh = items.filter(
|
|
44248
|
+
(item) => !existing.has(item.id) && !landed.some((done) => done.id === item.id)
|
|
44249
|
+
);
|
|
44250
|
+
if (fresh.length !== 1 || fresh[0].text !== text || fresh[0].done)
|
|
44251
|
+
throw new Error("Native checklist identity not verified");
|
|
44252
|
+
for (const done of landed) {
|
|
44253
|
+
const current = items.find((item) => item.id === done.id);
|
|
44254
|
+
if (!current || current.text !== done.text || current.done)
|
|
44255
|
+
throw new Error("An earlier appended item changed identity or text");
|
|
44256
|
+
}
|
|
44257
|
+
landed.push({ index, id: fresh[0].id, text });
|
|
44258
|
+
contentHash = result.contentHash;
|
|
44259
|
+
} catch (error2) {
|
|
44260
|
+
return {
|
|
44261
|
+
ok: false,
|
|
44262
|
+
id: args.id,
|
|
44263
|
+
landed,
|
|
44264
|
+
stoppedAt: {
|
|
44265
|
+
index,
|
|
44266
|
+
text,
|
|
44267
|
+
outcome: wrote ? "uncertain" : "not-written",
|
|
44268
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
44269
|
+
},
|
|
44270
|
+
notAttempted: args.items.slice(index + 1),
|
|
44271
|
+
contentHash,
|
|
44272
|
+
message: `Stopped at item ${index + 1} of ${args.items.length}; ${landed.length} item(s) landed and were verified. Read the exact note before retrying, and retry only items that are not present.`
|
|
44273
|
+
};
|
|
44274
|
+
}
|
|
44275
|
+
}
|
|
44276
|
+
const finalItems = byPosition(readRich(args.id).checklistItems);
|
|
44277
|
+
const tail = finalItems.slice(finalItems.length - landed.length);
|
|
44278
|
+
const orderVerified = tail.length === landed.length && tail.every((item, i) => item.id === landed[i].id && item.text === landed[i].text);
|
|
44279
|
+
return {
|
|
44280
|
+
ok: orderVerified,
|
|
44281
|
+
id: args.id,
|
|
44282
|
+
contentHash,
|
|
44283
|
+
items: landed,
|
|
44284
|
+
orderVerified,
|
|
44285
|
+
...orderVerified ? {} : {
|
|
44286
|
+
message: "Every item landed and was verified, but they are not the note's last checklist items in the requested order. Read the note before editing it further."
|
|
44287
|
+
}
|
|
44288
|
+
};
|
|
44289
|
+
}
|
|
44109
44290
|
function registerNativeOperations(server2, manager) {
|
|
44110
44291
|
function tool(name, description, input, handler, readOnly = false) {
|
|
44111
44292
|
server2.registerTool(
|
|
@@ -44231,6 +44412,20 @@ function registerNativeOperations(server2, manager) {
|
|
|
44231
44412
|
return { ...result, items: readRichNote(args.id).checklistItems };
|
|
44232
44413
|
}
|
|
44233
44414
|
);
|
|
44415
|
+
tool(
|
|
44416
|
+
"create-checklist-items",
|
|
44417
|
+
"Use when: appending several real unchecked Notes checklist items to one note, in order.\nReturns: each landed item's native identity and text, the final order check, and the new revision; on a stop, which items landed, the item whose outcome is uncertain, and the items not attempted.\nDo not use when: one item is enough (create-checklist-item) or plain text is sufficient.\nSafety: runs the verified single-item bridge once per item, chaining each verified revision into the next; checks after every item that exactly one new unchecked item with that text and a new identity appeared and that earlier items kept theirs, and stops at the first uncertain result without retrying. Each item takes a few seconds, so a full batch can run about a minute; if the call times out on the client side, read the note before retrying and send only items that are not present.",
|
|
44418
|
+
{
|
|
44419
|
+
...common,
|
|
44420
|
+
items: external_exports.array(
|
|
44421
|
+
external_exports.string().min(1).max(1e4).refine((s) => !/[\r\n\0]/u.test(s), "One line per checklist item")
|
|
44422
|
+
).min(1).max(MAX_CHECKLIST_BATCH).describe(`Item texts in the order they should appear (1-${MAX_CHECKLIST_BATCH})`)
|
|
44423
|
+
},
|
|
44424
|
+
(args) => {
|
|
44425
|
+
requireValidated("create-checklist-item");
|
|
44426
|
+
return appendChecklistItems(args, backgroundDependencies(manager), readRichNote);
|
|
44427
|
+
}
|
|
44428
|
+
);
|
|
44234
44429
|
tool(
|
|
44235
44430
|
"create-table",
|
|
44236
44431
|
"Use when: appending one native Notes table from a rectangular array of strings.\nReturns: the native table identity and decoded cells after readback.\nDo not use when: a text table is acceptable or the rows are not rectangular.\nSafety: never substitutes text; preserves existing rich content and reports failure unless the native table is verified.",
|
|
@@ -45277,6 +45472,115 @@ registerTool(
|
|
|
45277
45472
|
"Error updating note"
|
|
45278
45473
|
)
|
|
45279
45474
|
);
|
|
45475
|
+
function guardedAppend({
|
|
45476
|
+
id: id2,
|
|
45477
|
+
expectedContentHash,
|
|
45478
|
+
content,
|
|
45479
|
+
position = "after",
|
|
45480
|
+
separator = "\n\n",
|
|
45481
|
+
format = "plaintext",
|
|
45482
|
+
scopeText,
|
|
45483
|
+
htmlSeparator
|
|
45484
|
+
}, onRoute) {
|
|
45485
|
+
const contentToHtml = (text) => {
|
|
45486
|
+
if (format === "html") return text;
|
|
45487
|
+
return text.split("\n").map((line) => {
|
|
45488
|
+
const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45489
|
+
return `<div>${escaped || "<br>"}</div>`;
|
|
45490
|
+
}).join("");
|
|
45491
|
+
};
|
|
45492
|
+
const separatorToHtml = (sep2) => {
|
|
45493
|
+
if (format === "html") return sep2;
|
|
45494
|
+
if (sep2 === "\n\n") return "<div><br></div>";
|
|
45495
|
+
const escaped = sep2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45496
|
+
return `<div>${escaped}</div>`;
|
|
45497
|
+
};
|
|
45498
|
+
const snapshot = readExactNoteSnapshot(id2);
|
|
45499
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
45500
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
45501
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
45502
|
+
}
|
|
45503
|
+
if (!snapshot.rich.writable) {
|
|
45504
|
+
if (separator !== "\n\n")
|
|
45505
|
+
return errorResponse("Native append supports the default blank-line separator only");
|
|
45506
|
+
if (position !== "after") return errorResponse("Native append supports the end of a note only");
|
|
45507
|
+
if (!scopeText)
|
|
45508
|
+
return errorResponse("Provide scopeText: a unique existing phrase for native append");
|
|
45509
|
+
if (!VERIFIED_BACKGROUND.has("append-native"))
|
|
45510
|
+
return errorResponse("Native append has not passed live validation; see get-capabilities");
|
|
45511
|
+
onRoute?.("native");
|
|
45512
|
+
const result2 = appendNative(notesManager, {
|
|
45513
|
+
id: id2,
|
|
45514
|
+
expectedContentHash,
|
|
45515
|
+
scopeText,
|
|
45516
|
+
content,
|
|
45517
|
+
format
|
|
45518
|
+
});
|
|
45519
|
+
return successResponse("Native append verified without replacing existing objects", result2);
|
|
45520
|
+
}
|
|
45521
|
+
onRoute?.("applescript");
|
|
45522
|
+
const attachments = notesManager.listAttachmentsById(id2);
|
|
45523
|
+
if (attachments.length > 0) {
|
|
45524
|
+
return errorResponse(
|
|
45525
|
+
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
|
|
45526
|
+
);
|
|
45527
|
+
}
|
|
45528
|
+
assertLinkedWrite(snapshot.rich, snapshot.rich.content, "html");
|
|
45529
|
+
const firstDivEnd = snapshot.rich.content.indexOf("</div>");
|
|
45530
|
+
const titleDiv = firstDivEnd !== -1 ? snapshot.rich.content.slice(0, firstDivEnd + 6) : "";
|
|
45531
|
+
const bodyHtml = firstDivEnd !== -1 ? snapshot.rich.content.slice(firstDivEnd + 6) : snapshot.rich.content;
|
|
45532
|
+
const newBlock = contentToHtml(content);
|
|
45533
|
+
const sepHtml = htmlSeparator ?? separatorToHtml(separator);
|
|
45534
|
+
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
45535
|
+
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
45536
|
+
id2,
|
|
45537
|
+
snapshot.note.title,
|
|
45538
|
+
snapshot.body,
|
|
45539
|
+
void 0,
|
|
45540
|
+
combinedBody,
|
|
45541
|
+
"html",
|
|
45542
|
+
snapshot.rich.revision
|
|
45543
|
+
);
|
|
45544
|
+
if (result.status === "conflict") {
|
|
45545
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
45546
|
+
}
|
|
45547
|
+
if (result.status === "attachments") {
|
|
45548
|
+
return errorResponse(
|
|
45549
|
+
`Note "${snapshot.note.title}" gained an attachment before saving. No content was appended.`
|
|
45550
|
+
);
|
|
45551
|
+
}
|
|
45552
|
+
if (result.status !== "updated") {
|
|
45553
|
+
return errorResponse(
|
|
45554
|
+
`The append result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
45555
|
+
);
|
|
45556
|
+
}
|
|
45557
|
+
const readback = notesManager.getNoteContentById(id2);
|
|
45558
|
+
const richReadback = enrichNoteRead(id2, readback || "");
|
|
45559
|
+
const contentHash = readback ? richContentHash(readback, richReadback) : "";
|
|
45560
|
+
if (!richReadback.complete || linkSignature(richReadback.links) !== linkSignature(htmlLinks(result.writtenBody))) {
|
|
45561
|
+
return errorResponse(
|
|
45562
|
+
"The note accepted the write, but rich-link readback is not verified. Read the exact ID before retrying; do not repeat the write automatically."
|
|
45563
|
+
);
|
|
45564
|
+
}
|
|
45565
|
+
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
45566
|
+
return errorResponse(
|
|
45567
|
+
`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.`
|
|
45568
|
+
);
|
|
45569
|
+
}
|
|
45570
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
45571
|
+
return successResponse(
|
|
45572
|
+
`Note appended; visible text verified: "${snapshot.note.title}"${sharedWarning}`,
|
|
45573
|
+
{
|
|
45574
|
+
ok: true,
|
|
45575
|
+
id: id2,
|
|
45576
|
+
title: snapshot.note.title,
|
|
45577
|
+
shared: snapshot.note.shared ?? false,
|
|
45578
|
+
previousContentHash: expectedContentHash,
|
|
45579
|
+
contentHash,
|
|
45580
|
+
verifiedVisibleText: true
|
|
45581
|
+
}
|
|
45582
|
+
);
|
|
45583
|
+
}
|
|
45280
45584
|
registerTool(
|
|
45281
45585
|
"append-to-note",
|
|
45282
45586
|
{
|
|
@@ -45302,118 +45606,76 @@ registerTool(
|
|
|
45302
45606
|
verifiedVisibleText: external_exports.boolean().optional()
|
|
45303
45607
|
}
|
|
45304
45608
|
},
|
|
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
|
-
|
|
45609
|
+
withErrorHandling((args) => guardedAppend(args), "Error appending to note")
|
|
45610
|
+
);
|
|
45611
|
+
registerTool(
|
|
45612
|
+
"insert-link",
|
|
45613
|
+
{
|
|
45614
|
+
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.",
|
|
45615
|
+
inputSchema: {
|
|
45616
|
+
id: noteIdInput,
|
|
45617
|
+
expectedContentHash: expectedContentHashInput,
|
|
45618
|
+
url: external_exports.string().min(1, "url is required").max(MAX_LINK_URL_LENGTH).describe(
|
|
45619
|
+
"Link destination: absolute http(s) URL with a host, or a mailto:, notes:// or applenotes: link. No spaces."
|
|
45620
|
+
),
|
|
45621
|
+
mode: external_exports.enum(["raw", "hyperlink"]).optional().default("raw").describe(
|
|
45622
|
+
"'raw' (default) shows the URL itself; 'hyperlink' shows `label` linking to the URL"
|
|
45623
|
+
),
|
|
45624
|
+
label: external_exports.string().max(MAX_LINK_LABEL_LENGTH).optional().describe("Visible text for mode 'hyperlink' (required there, refused in raw mode)"),
|
|
45625
|
+
linked: external_exports.boolean().optional().describe(
|
|
45626
|
+
"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."
|
|
45627
|
+
),
|
|
45628
|
+
position: external_exports.enum(["end", "after-title"]).optional().default("end").describe("'end' (default) or 'after-title' (first paragraph below the title)"),
|
|
45629
|
+
blankLine: external_exports.boolean().optional().default(true).describe(
|
|
45630
|
+
"Leave a blank line between existing text and the link paragraph (default true; native-object notes require true)"
|
|
45631
|
+
),
|
|
45632
|
+
scopeText: external_exports.string().min(12).max(500).optional().describe("Existing unique phrase; required only for notes with native objects")
|
|
45633
|
+
},
|
|
45634
|
+
outputSchema: {
|
|
45635
|
+
ok: external_exports.boolean().optional(),
|
|
45636
|
+
id: external_exports.string().optional(),
|
|
45637
|
+
mode: external_exports.string().optional(),
|
|
45638
|
+
url: external_exports.string().optional(),
|
|
45639
|
+
text: external_exports.string().optional(),
|
|
45640
|
+
position: external_exports.string().optional(),
|
|
45641
|
+
route: external_exports.string().optional(),
|
|
45642
|
+
linkStored: external_exports.boolean().optional(),
|
|
45643
|
+
storedUrl: external_exports.string().optional(),
|
|
45644
|
+
previousContentHash: external_exports.string().optional(),
|
|
45645
|
+
contentHash: external_exports.string().optional()
|
|
45646
|
+
}
|
|
45647
|
+
},
|
|
45648
|
+
withErrorHandling((args) => {
|
|
45649
|
+
const result = insertLink(
|
|
45650
|
+
{
|
|
45651
|
+
...args,
|
|
45652
|
+
mode: args.mode ?? "raw",
|
|
45653
|
+
position: args.position ?? "end",
|
|
45654
|
+
blankLine: args.blankLine ?? true
|
|
45655
|
+
},
|
|
45656
|
+
{
|
|
45657
|
+
readLinks: (noteId3) => readRichNote(noteId3).links,
|
|
45658
|
+
append: (request) => {
|
|
45659
|
+
let route = "applescript";
|
|
45660
|
+
const response = guardedAppend(
|
|
45661
|
+
{
|
|
45662
|
+
...request,
|
|
45663
|
+
format: "html",
|
|
45664
|
+
htmlSeparator: request.separator ? "<div><br></div>" : ""
|
|
45665
|
+
},
|
|
45666
|
+
(r) => route = r
|
|
45343
45667
|
);
|
|
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
|
|
45668
|
+
if (response.isError) throw new Error(response.content[0].text);
|
|
45669
|
+
return { route, contentHash: String(response.structuredContent?.contentHash ?? "") };
|
|
45412
45670
|
}
|
|
45413
|
-
|
|
45414
|
-
|
|
45415
|
-
|
|
45416
|
-
|
|
45671
|
+
}
|
|
45672
|
+
);
|
|
45673
|
+
const stored = result.linkStored ? `stored link to ${result.storedUrl}` : "no stored link (plain text)";
|
|
45674
|
+
return successResponse(
|
|
45675
|
+
`Link inserted (${result.mode}, ${result.position}, ${result.route}); ${stored}`,
|
|
45676
|
+
{ ...result }
|
|
45677
|
+
);
|
|
45678
|
+
}, "Error inserting link")
|
|
45417
45679
|
);
|
|
45418
45680
|
registerTool(
|
|
45419
45681
|
"delete-note",
|
package/package.json
CHANGED