apple-notes-mcp 2.8.31 → 2.8.33
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 +82 -5
- package/build/index.js +322 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -332,13 +332,42 @@ title line, with no seed line.
|
|
|
332
332
|
It also refuses Markdown that Notes would rewrite and the server could not
|
|
333
333
|
verify: `_` emphasis (underscores inside a word, as in `snake_case`, and
|
|
334
334
|
inside a link destination, as in `[docs](https://example.com/_next/static)`,
|
|
335
|
-
are fine), backslash escapes, character references such as `&`,
|
|
336
|
-
|
|
335
|
+
are fine), backslash escapes, character references such as `&`, `===` lines
|
|
336
|
+
and rule lines other than the `---` divider described below, indented headings or list items, `1)` lists, closing `#`s, and
|
|
337
337
|
formatting inside link labels. Content that needs one of these literally, such
|
|
338
338
|
as a `/_next` path or a literal `\*`, has no Markdown form here: use
|
|
339
339
|
`format: "html"` for that note (or `append-native` with `format: "html"` on an
|
|
340
340
|
existing one), which keeps the characters but not the Heading and Subheading
|
|
341
341
|
styles. Markdown punctuation in `title` is escaped, so the title stays literal.
|
|
342
|
+
- Notes' importer also maps these block constructs to native styles. They are
|
|
343
|
+
gated separately, and `get-capabilities` reports them as
|
|
344
|
+
`create-note-markdown-blocks`. Each mapping below was live-verified on
|
|
345
|
+
macOS 27.2 by reading the created note's stored styles.
|
|
346
|
+
|
|
347
|
+
| Markdown | Native result |
|
|
348
|
+
|----------|---------------|
|
|
349
|
+
| `- [ ] item` / `- [x] item` | Checklist item, unchecked / checked |
|
|
350
|
+
| `> text` (consecutive lines form one quote) | Body paragraph with a block quote |
|
|
351
|
+
| A fence of bare ```` ``` ```` lines (no language) | Monospaced paragraphs; the code text is kept literally |
|
|
352
|
+
| `---` on its own line, after a blank line | Divider line |
|
|
353
|
+
| `` `inline code` `` | **Highlighted** text, not monospace |
|
|
354
|
+
|
|
355
|
+
Constructs Notes would not render faithfully, or that this server cannot yet
|
|
356
|
+
verify, are refused before anything is created: `~~~` fences, a language
|
|
357
|
+
after the opening fence, nested (`>>`) or indented quotes, lists or headings
|
|
358
|
+
inside a quote, a quote followed directly by text (Markdown would join that
|
|
359
|
+
text to the quote), `---` directly under text (Markdown would make that text
|
|
360
|
+
a heading), `***`/`___`/`----` rules, `[X]`, `* [ ]` or `+ [ ]` items, checklist
|
|
361
|
+
items directly next to ordinary list items, inline code padded with spaces or
|
|
362
|
+
inside a link label, tables, and `~~strikethrough~~`. The readback checks the
|
|
363
|
+
block-quote text, the Monospaced text, each checklist item's text and done
|
|
364
|
+
state, the divider count, and the highlighted text. These mappings apply to
|
|
365
|
+
the default Shortcut route only; see
|
|
366
|
+
[Markdown through HTML](#markdown-through-html) for `markdownRoute: "html"`.
|
|
367
|
+
- [`append-native`](#append-native)'s `format: "markdown"` refuses all of the
|
|
368
|
+
constructs above: its Shortcuts converter is a different one, which renders
|
|
369
|
+
a quote and a fenced block as plain body text, `- [ ]` as a bullet with
|
|
370
|
+
literal brackets, and inline code as plain text, and drops `---`.
|
|
342
371
|
- The server finds the new note among the notes added to the default folder
|
|
343
372
|
during the run by verifying each one's visible text, heading levels and links
|
|
344
373
|
by exact-ID readback, and moves it only after exactly one verifies. On any
|
|
@@ -366,7 +395,9 @@ On this route, bullet task items (`- [ ] item`, `- [x] item`) become ordinary
|
|
|
366
395
|
list rows that start with a visible `☐` or `☑` character, and the response
|
|
367
396
|
reports how many as `taskItemsRendered`. They are text, not native checkable
|
|
368
397
|
checklist items. Use this route as the glyph fallback when the Shortcut is not
|
|
369
|
-
installed or the note is outside iCloud.
|
|
398
|
+
installed or the note is outside iCloud. Block quotes, fenced code and inline code, which
|
|
399
|
+
the Shortcut route maps natively, are refused on this route, and a `---` line
|
|
400
|
+
stays literal text rather than becoming a divider.
|
|
370
401
|
|
|
371
402
|
```json
|
|
372
403
|
{
|
|
@@ -1349,6 +1380,50 @@ latest `expectedContentHash`, and an absolute `path`. The server never retries
|
|
|
1349
1380
|
the insertion. It verifies that existing rich content survived and compares the
|
|
1350
1381
|
fetched attachment bytes with the source before reporting success.
|
|
1351
1382
|
|
|
1383
|
+
| Parameter | Type | Required | Description |
|
|
1384
|
+
|-----------|------|----------|-------------|
|
|
1385
|
+
| `id` | string | Yes | Exact CoreData note ID |
|
|
1386
|
+
| `expectedContentHash` | string | Yes | `contentHash` from the note version being extended |
|
|
1387
|
+
| `path` | string | Yes | Absolute path of the local file; symbolic links are refused |
|
|
1388
|
+
| `filename` | string | No | Name the attachment gets in Notes instead of the source file's name. One path component that keeps the source file's extension, with no slash, colon, backslash, control character, leading dot or surrounding spaces. Notes names a file attachment after the file it receives, so the server gives its private temporary copy this name |
|
|
1389
|
+
|
|
1390
|
+
**Returns:** `attachmentId`, `bytes`, the `name` Notes reports, and the new
|
|
1391
|
+
`contentHash`. With `filename`, `filenameVerified` says whether Notes reports
|
|
1392
|
+
that exact name. A mismatch is a warning (`filenameWarning`), not a failure,
|
|
1393
|
+
because the attachment and its bytes are already verified.
|
|
1394
|
+
|
|
1395
|
+
---
|
|
1396
|
+
|
|
1397
|
+
#### `create-note-with-attachment`
|
|
1398
|
+
|
|
1399
|
+
Creates a note and attaches one local file to it in a single call. It checks the
|
|
1400
|
+
file and `filename` before creating anything, creates the note through Notes.app
|
|
1401
|
+
the way [`create-note`](#create-note) does in plaintext, then attaches with the
|
|
1402
|
+
same verification as [`add-attachment`](#add-attachment).
|
|
1403
|
+
|
|
1404
|
+
| Parameter | Type | Required | Description |
|
|
1405
|
+
|-----------|------|----------|-------------|
|
|
1406
|
+
| `title` | string | Yes | One-line note title |
|
|
1407
|
+
| `content` | string | No | Plain-text body placed above the attachment |
|
|
1408
|
+
| `folder` | string | No | Existing folder or nested path; create it first with [`create-folder`](#create-folder) |
|
|
1409
|
+
| `account` | string | No | Account name; defaults to Notes.app's default account |
|
|
1410
|
+
| `path` | string | Yes | Absolute path of the local file (at most 64 MiB) |
|
|
1411
|
+
| `filename` | string | No | Attachment name override, as in `add-attachment` |
|
|
1412
|
+
|
|
1413
|
+
```json
|
|
1414
|
+
{
|
|
1415
|
+
"title": "Signed lease",
|
|
1416
|
+
"folder": "Home",
|
|
1417
|
+
"path": "/Users/me/Downloads/scan-0042.pdf",
|
|
1418
|
+
"filename": "Lease 2026.pdf"
|
|
1419
|
+
}
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
**Returns:** the new note's `id` with `noteCreated: true` and the
|
|
1423
|
+
`add-attachment` result. If the attachment step fails after the note exists,
|
|
1424
|
+
the error names the new note's id: attach to it with `add-attachment` instead of
|
|
1425
|
+
calling this tool again, which would create a second note.
|
|
1426
|
+
|
|
1352
1427
|
---
|
|
1353
1428
|
|
|
1354
1429
|
#### `list-attachments`
|
|
@@ -1570,7 +1645,9 @@ the note before retrying, and retry only the items that are not present.
|
|
|
1570
1645
|
#### `create-table`
|
|
1571
1646
|
|
|
1572
1647
|
Appends a native table from rectangular string rows and verifies every decoded
|
|
1573
|
-
cell. It never substitutes a text table.
|
|
1648
|
+
cell. It never substitutes a text table. Omit `rows` for an empty 2 × 2 table,
|
|
1649
|
+
the size Notes itself inserts from Format > Table; its four empty cells are
|
|
1650
|
+
verified the same way.
|
|
1574
1651
|
|
|
1575
1652
|
#### `set-note-pinned`
|
|
1576
1653
|
|
|
@@ -1824,7 +1901,7 @@ Apple Notes stores checklists as a paragraph style (`style_type=103`) inside a g
|
|
|
1824
1901
|
**Workarounds:**
|
|
1825
1902
|
|
|
1826
1903
|
1. **Create the note with bulleted list items, then convert manually in Notes.app.** Select the items and press <kbd>⇧⌘L</kbd> (or **Format → Checklist**). This converts the list in place and the resulting checklist will be readable by `get-checklist-state` and annotated by `get-note-markdown`.
|
|
1827
|
-
2. **Use the Apple Shortcuts app** to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript.
|
|
1904
|
+
2. **Use the Apple Shortcuts app** to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript. This server does that for you in two ways: [`create-checklist-item`](#create-checklist-item) appends one unchecked item to an existing note, and [`create-note`](#create-note) with `format: "markdown"` turns `- [ ]` / `- [x]` lines into checklist items with that done state through Notes' own Markdown importer, (see [Markdown notes](#markdown-notes)).
|
|
1828
1905
|
3. **Read-only checklist support is fully implemented** — once a checklist exists (created manually or by another app), `get-checklist-state` and `get-note-markdown` will read its done/undone state correctly (with Full Disk Access).
|
|
1829
1906
|
|
|
1830
1907
|
If you need to *track* todos programmatically and don't strictly need them rendered as Apple Notes checklist UI, plain markdown-style `- [ ] item` / `- [x] item` lines in a `plaintext` note are a reasonable alternative — they are searchable, human-readable, and can be parsed by downstream tooling.
|
package/build/index.js
CHANGED
|
@@ -39675,7 +39675,11 @@ function parseRichNote(data, nativeTags = []) {
|
|
|
39675
39675
|
const length = varintValue(getField(fields, 1));
|
|
39676
39676
|
if (length === void 0 || length < 0 || position + length > text.length)
|
|
39677
39677
|
throw new Error("Invalid Notes run length");
|
|
39678
|
+
const paragraph = embeddedMessage(getField(fields, 2));
|
|
39678
39679
|
styleRuns.push({
|
|
39680
|
+
paragraphStyle: paragraph ? varintValue(getField(paragraph, 1)) ?? 3 : 3,
|
|
39681
|
+
blockQuote: Boolean(paragraph && varintValue(getField(paragraph, 8))),
|
|
39682
|
+
highlight: Boolean(varintValue(getField(fields, 14))),
|
|
39679
39683
|
start: position,
|
|
39680
39684
|
length,
|
|
39681
39685
|
signature: JSON.stringify(
|
|
@@ -39703,7 +39707,6 @@ function parseRichNote(data, nativeTags = []) {
|
|
|
39703
39707
|
start: position,
|
|
39704
39708
|
length
|
|
39705
39709
|
});
|
|
39706
|
-
const paragraph = embeddedMessage(getField(fields, 2));
|
|
39707
39710
|
hasChecklist ||= Boolean(paragraph && varintValue(getField(paragraph, 1)) === 103);
|
|
39708
39711
|
if (paragraph && varintValue(getField(paragraph, 1)) === 103) {
|
|
39709
39712
|
const checklist = embeddedMessage(getField(paragraph, 5));
|
|
@@ -45056,40 +45059,152 @@ function countTaskItems(markdown) {
|
|
|
45056
45059
|
return item !== null && TASK_ITEM.test(item[1]);
|
|
45057
45060
|
}).length;
|
|
45058
45061
|
}
|
|
45059
|
-
|
|
45060
|
-
|
|
45062
|
+
var FENCE = "```";
|
|
45063
|
+
var CHECKLIST_ITEM = /^- \[( |x)\] (.+)$/;
|
|
45064
|
+
function withoutMarkdownCode(markdown) {
|
|
45065
|
+
let inCode = false;
|
|
45066
|
+
return markdown.replace(/\r\n/g, "\n").split("\n").map((line) => {
|
|
45067
|
+
if (line === FENCE) {
|
|
45068
|
+
inCode = !inCode;
|
|
45069
|
+
return "";
|
|
45070
|
+
}
|
|
45071
|
+
return inCode ? "" : line.replace(/`[^`\n]+`/g, "x");
|
|
45072
|
+
}).join("\n");
|
|
45073
|
+
}
|
|
45074
|
+
function usesMarkdownBlocks(markdown) {
|
|
45075
|
+
return /`/.test(markdown) || /^>/m.test(markdown) || /^---[ \t]*$/m.test(markdown) || /^- \[[ x]\] /m.test(markdown);
|
|
45076
|
+
}
|
|
45077
|
+
function renderMarkdown(markdown, options = {}) {
|
|
45078
|
+
const blocks = options.blocks === true;
|
|
45079
|
+
if (blocks ? /[-]/u.test(markdown) : /[]/u.test(markdown))
|
|
45061
45080
|
throw new Error("Unsupported reserved characters");
|
|
45062
45081
|
if (Array.from(markdown).some((c) => c.charCodeAt(0) < 32 && !["\n", "\r", " "].includes(c)))
|
|
45063
45082
|
throw new Error("Unsupported control characters");
|
|
45064
|
-
|
|
45083
|
+
const checked = blocks ? withoutMarkdownCode(markdown) : markdown;
|
|
45084
|
+
if (blocks ? /[`~|]|^#{4,}\s|^[ \t]+>|^>[ \t]*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(checked) : /[`~|]|^#{4,}\s|^\s*>|^\s*\[.+\]:|!\[|<\/?[a-z]/im.test(checked))
|
|
45065
45085
|
throw new Error(
|
|
45066
|
-
"Markdown append supports paragraphs, headings, flat lists, emphasis and inline links; use semantic HTML for other formatting"
|
|
45086
|
+
blocks ? "Markdown import supports paragraphs, headings, flat lists, checklist items, block quotes, fenced code, dividers, emphasis, inline code and inline links; use semantic HTML for other formatting" : "Markdown append supports paragraphs, headings, flat lists, emphasis and inline links; use semantic HTML for other formatting"
|
|
45067
45087
|
);
|
|
45088
|
+
const expect = {
|
|
45089
|
+
quotes: [],
|
|
45090
|
+
code: [],
|
|
45091
|
+
checklist: [],
|
|
45092
|
+
dividers: 0,
|
|
45093
|
+
highlights: []
|
|
45094
|
+
};
|
|
45068
45095
|
const inline = (text) => {
|
|
45096
|
+
const spans = [];
|
|
45097
|
+
let value = text;
|
|
45098
|
+
if (blocks)
|
|
45099
|
+
value = value.replace(/`([^`\n]+)`/g, (_s, code2) => {
|
|
45100
|
+
if (/^\s|\s$/.test(code2)) throw new Error("Inline code cannot start or end with a space");
|
|
45101
|
+
expect.highlights.push(code2);
|
|
45102
|
+
spans.push(`<code>${escape2(code2)}</code>`);
|
|
45103
|
+
return `\uE002${spans.length - 1}\uE003`;
|
|
45104
|
+
});
|
|
45069
45105
|
const links = [];
|
|
45070
|
-
|
|
45106
|
+
value = value.replace(/\[([^\]\n]+)\]\(([^()\s]+)\)/g, (_s, label, url) => {
|
|
45071
45107
|
if (!/^(https?:\/\/|notes:\/\/|applenotes:|mailto:)/i.test(url))
|
|
45072
45108
|
throw new Error("Unsupported Markdown link URL");
|
|
45109
|
+
if (/[]/u.test(label)) throw new Error("Inline code inside a link is unsupported");
|
|
45073
45110
|
links.push(`<a href="${escape2(url)}">${escape2(label)}</a>`);
|
|
45074
45111
|
return `\uE000${links.length - 1}\uE001`;
|
|
45075
45112
|
});
|
|
45076
45113
|
value = escape2(value).replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/\*([^*\n]+)\*/g, "<i>$1</i>");
|
|
45077
45114
|
if (value.includes("[") || value.includes("]") || value.includes("*"))
|
|
45078
45115
|
throw new Error("Unsupported or unbalanced Markdown inline syntax");
|
|
45079
|
-
return value.replace(
|
|
45116
|
+
return value.replace(/(\d+)/g, (_s, i) => links[Number(i)]).replace(/(\d+)/g, (_s, i) => spans[Number(i)]);
|
|
45080
45117
|
};
|
|
45081
|
-
|
|
45118
|
+
const withoutTags = (html2) => {
|
|
45119
|
+
let text = "", inTag = false;
|
|
45120
|
+
for (const ch of html2) {
|
|
45121
|
+
if (ch === "<") inTag = true;
|
|
45122
|
+
else if (ch === ">" && inTag) inTag = false;
|
|
45123
|
+
else if (!inTag) text += ch;
|
|
45124
|
+
}
|
|
45125
|
+
return text;
|
|
45126
|
+
};
|
|
45127
|
+
const visible = (html2) => withoutTags(html2).replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
45128
|
+
let html = "", list, quote = false, code, previousBlank = true;
|
|
45082
45129
|
const close = () => {
|
|
45083
45130
|
if (list) {
|
|
45084
|
-
html += `</${list}>`;
|
|
45131
|
+
html += list === "checklist" ? "</ul>" : `</${list}>`;
|
|
45085
45132
|
list = void 0;
|
|
45086
45133
|
}
|
|
45134
|
+
if (quote) {
|
|
45135
|
+
html += "</blockquote>";
|
|
45136
|
+
quote = false;
|
|
45137
|
+
}
|
|
45087
45138
|
};
|
|
45088
45139
|
for (const line of markdown.replace(/\r\n/g, "\n").split("\n")) {
|
|
45140
|
+
if (code) {
|
|
45141
|
+
if (line === FENCE) {
|
|
45142
|
+
if (!code.length) throw new Error("Empty fenced code blocks are unsupported");
|
|
45143
|
+
html += `<pre>${escape2(code.join("\n"))}</pre>`;
|
|
45144
|
+
expect.code.push(code.join("\n"));
|
|
45145
|
+
code = void 0;
|
|
45146
|
+
previousBlank = false;
|
|
45147
|
+
} else code.push(line);
|
|
45148
|
+
continue;
|
|
45149
|
+
}
|
|
45150
|
+
const blank = !line.trim();
|
|
45151
|
+
if (blocks) {
|
|
45152
|
+
if (quote && !blank && !line.startsWith(">"))
|
|
45153
|
+
throw new Error("End a block quote with a blank line before other text");
|
|
45154
|
+
if (line.startsWith(FENCE)) {
|
|
45155
|
+
if (line !== FENCE)
|
|
45156
|
+
throw new Error("Fenced code must open and close with a bare ``` line (no language)");
|
|
45157
|
+
close();
|
|
45158
|
+
code = [];
|
|
45159
|
+
continue;
|
|
45160
|
+
}
|
|
45161
|
+
if (line.startsWith(">")) {
|
|
45162
|
+
const text = line.replace(/^> ?/, "");
|
|
45163
|
+
if (!text.trim()) throw new Error("Empty block quote lines are unsupported");
|
|
45164
|
+
if (/^(?:#|[-+*][ \t]|\d+[.)][ \t]|>|```)/.test(text))
|
|
45165
|
+
throw new Error("Block quotes support text and inline formatting only");
|
|
45166
|
+
if (!quote) {
|
|
45167
|
+
close();
|
|
45168
|
+
html += "<blockquote>";
|
|
45169
|
+
quote = true;
|
|
45170
|
+
expect.quotes.push("");
|
|
45171
|
+
}
|
|
45172
|
+
const rendered = inline(text);
|
|
45173
|
+
html += `<div>${rendered}</div>`;
|
|
45174
|
+
expect.quotes[expect.quotes.length - 1] += ` ${visible(rendered)}`;
|
|
45175
|
+
previousBlank = false;
|
|
45176
|
+
continue;
|
|
45177
|
+
}
|
|
45178
|
+
if (/^---[ \t]*$/.test(line)) {
|
|
45179
|
+
if (!previousBlank) throw new Error("Put a blank line before a --- divider");
|
|
45180
|
+
close();
|
|
45181
|
+
html += "<hr>";
|
|
45182
|
+
expect.dividers++;
|
|
45183
|
+
previousBlank = false;
|
|
45184
|
+
continue;
|
|
45185
|
+
}
|
|
45186
|
+
const task = CHECKLIST_ITEM.exec(line);
|
|
45187
|
+
if (task) {
|
|
45188
|
+
if (list && list !== "checklist")
|
|
45189
|
+
throw new Error("Separate checklist items from other list items with a blank line");
|
|
45190
|
+
if (list !== "checklist") {
|
|
45191
|
+
close();
|
|
45192
|
+
list = "checklist";
|
|
45193
|
+
html += '<ul class="checklist">';
|
|
45194
|
+
}
|
|
45195
|
+
const rendered = inline(task[2]);
|
|
45196
|
+
html += `<li>${rendered}</li>`;
|
|
45197
|
+
expect.checklist.push({ text: visible(rendered).trim(), done: task[1] === "x" });
|
|
45198
|
+
previousBlank = false;
|
|
45199
|
+
continue;
|
|
45200
|
+
}
|
|
45201
|
+
}
|
|
45089
45202
|
if (/^\s{2,}\S/.test(line)) throw new Error("Nested lists and indented code are unsupported");
|
|
45090
45203
|
const heading = /^(#{1,3})\s+(.+)$/.exec(line), item = /^(?:([-+*])|\d+\.)\s+(.+)$/.exec(line);
|
|
45091
45204
|
if (item) {
|
|
45092
45205
|
const kind = item[1] ? "ul" : "ol";
|
|
45206
|
+
if (list === "checklist")
|
|
45207
|
+
throw new Error("Separate checklist items from other list items with a blank line");
|
|
45093
45208
|
if (list !== kind) {
|
|
45094
45209
|
close();
|
|
45095
45210
|
list = kind;
|
|
@@ -45097,13 +45212,20 @@ function appendMarkdownHtml(markdown, options = {}) {
|
|
|
45097
45212
|
}
|
|
45098
45213
|
const task = options.taskGlyphs && item[1] ? TASK_ITEM.exec(item[2]) : null;
|
|
45099
45214
|
html += task ? `<li>${task[1] === " " ? TASK_GLYPHS.open : TASK_GLYPHS.done} ${inline(task[2])}</li>` : `<li>${inline(item[2])}</li>`;
|
|
45215
|
+
previousBlank = false;
|
|
45100
45216
|
continue;
|
|
45101
45217
|
}
|
|
45102
45218
|
close();
|
|
45103
|
-
html += heading ? `<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>` :
|
|
45219
|
+
html += heading ? `<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>` : !blank ? `<div>${inline(line)}</div>` : "<div><br></div>";
|
|
45220
|
+
previousBlank = blank;
|
|
45104
45221
|
}
|
|
45222
|
+
if (code) throw new Error("Unclosed fenced code block");
|
|
45105
45223
|
close();
|
|
45106
|
-
|
|
45224
|
+
expect.quotes = expect.quotes.map((q) => q.trim());
|
|
45225
|
+
return { html, expect };
|
|
45226
|
+
}
|
|
45227
|
+
function appendMarkdownHtml(markdown, options = {}) {
|
|
45228
|
+
return renderMarkdown(markdown, { taskGlyphs: options.taskGlyphs }).html;
|
|
45107
45229
|
}
|
|
45108
45230
|
|
|
45109
45231
|
// src/utils/noteRevision.ts
|
|
@@ -45197,7 +45319,7 @@ var UNMODELED_MARKDOWN = [
|
|
|
45197
45319
|
[/^#{1,3}[ \t].*[ \t]#+[ \t]*$/m, "closing # sequences"],
|
|
45198
45320
|
[/\[[^\]\n]*[*_][^\]\n]*\]\(/m, "formatting inside link labels"]
|
|
45199
45321
|
];
|
|
45200
|
-
function validateAppendContent(content, format) {
|
|
45322
|
+
function validateAppendContent(content, format, options = {}) {
|
|
45201
45323
|
if (!content || content.length > 1024 * 1024 || content.includes("\0"))
|
|
45202
45324
|
throw new Error("Invalid append content (limit 1 MiB)");
|
|
45203
45325
|
if (format === "html") {
|
|
@@ -45227,11 +45349,12 @@ function validateAppendContent(content, format) {
|
|
|
45227
45349
|
if (/<!--|<!|<\?|<[^>]*$/u.test(content)) throw new Error("Unsupported HTML markup");
|
|
45228
45350
|
}
|
|
45229
45351
|
if (format === "markdown") {
|
|
45230
|
-
|
|
45352
|
+
const checked = options.blocks ? withoutMarkdownCode(content).replace(/^---[ \t]*$/gm, "") : content;
|
|
45353
|
+
if (/!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(checked))
|
|
45231
45354
|
throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
|
|
45232
|
-
const withoutLinkDestinations =
|
|
45355
|
+
const withoutLinkDestinations = checked.replace(/(\[[^\]\n]+\])\([^()\s]+\)/g, "$1()");
|
|
45233
45356
|
for (const [pattern, name] of UNMODELED_MARKDOWN)
|
|
45234
|
-
if (pattern.test(name === UNDERSCORES_OUTSIDE_A_WORD ? withoutLinkDestinations :
|
|
45357
|
+
if (pattern.test(name === UNDERSCORES_OUTSIDE_A_WORD ? withoutLinkDestinations : checked))
|
|
45235
45358
|
throw new Error(
|
|
45236
45359
|
`Markdown cannot use ${name}; Notes would change that text, so the result could not be verified`
|
|
45237
45360
|
);
|
|
@@ -45455,10 +45578,42 @@ var literalMarkdown = (text) => text.replace(/[!-/:-@[-`{-~]/g, "\\$&");
|
|
|
45455
45578
|
function headingLevels(html) {
|
|
45456
45579
|
return [...html.matchAll(/<h([1-3])\b[^>]*>([\s\S]*?)<\/h\1>/gi)].filter((match) => comparableVisibleText(match[2])).map((match) => Number(match[1]));
|
|
45457
45580
|
}
|
|
45581
|
+
var DIVIDER_UTI = "com.apple.notes.inlinetextattachment.dividerline";
|
|
45582
|
+
function assertMarkdownBlocks(rich, expected) {
|
|
45583
|
+
const tidy = (s) => s.replace(/[\s]+/gu, " ").trim();
|
|
45584
|
+
const runs = rich.styleRuns || [];
|
|
45585
|
+
const styledText = (matches) => {
|
|
45586
|
+
let out = "", previous = false;
|
|
45587
|
+
for (const run of runs) {
|
|
45588
|
+
const hit = matches(run);
|
|
45589
|
+
if (hit) out += (previous ? "" : " ") + rich.text.slice(run.start, run.start + run.length);
|
|
45590
|
+
previous = hit;
|
|
45591
|
+
}
|
|
45592
|
+
return tidy(out);
|
|
45593
|
+
};
|
|
45594
|
+
if (styledText((run) => run.blockQuote === true) !== tidy(expected.quotes.join(" ")))
|
|
45595
|
+
throw new Error("Block quotes not verified");
|
|
45596
|
+
if (styledText((run) => run.paragraphStyle === 4) !== tidy(expected.code.join(" ")))
|
|
45597
|
+
throw new Error("Monospaced code blocks not verified");
|
|
45598
|
+
if (styledText((run) => run.highlight === true) !== tidy(expected.highlights.join(" ")))
|
|
45599
|
+
throw new Error("Inline code highlights not verified");
|
|
45600
|
+
const items = (rich.checklistItems || []).map((item) => ({
|
|
45601
|
+
text: tidy(item.text),
|
|
45602
|
+
done: item.done
|
|
45603
|
+
}));
|
|
45604
|
+
const wanted = expected.checklist.map((item) => ({ text: tidy(item.text), done: item.done }));
|
|
45605
|
+
if (JSON.stringify(items) !== JSON.stringify(wanted))
|
|
45606
|
+
throw new Error("Checklist items or their done state not verified");
|
|
45607
|
+
if ((rich.objects || []).filter((o) => o.type === DIVIDER_UTI).length !== expected.dividers)
|
|
45608
|
+
throw new Error("Dividers not verified");
|
|
45609
|
+
}
|
|
45458
45610
|
function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
|
|
45459
45611
|
if (/[\r\n\0]/u.test(request.title)) throw new Error("A Markdown note title must be one line");
|
|
45460
|
-
validateAppendContent(request.content, "markdown");
|
|
45461
|
-
const bodyHtml =
|
|
45612
|
+
validateAppendContent(request.content, "markdown", { blocks: true });
|
|
45613
|
+
const { html: bodyHtml, expect: blockExpectations } = renderMarkdown(request.content, {
|
|
45614
|
+
blocks: true
|
|
45615
|
+
});
|
|
45616
|
+
const checkBlocks = usesMarkdownBlocks(request.content);
|
|
45462
45617
|
if (request.folder) buildFolderReference(request.folder);
|
|
45463
45618
|
const expectedText = `${request.title} ${comparableVisibleText(bodyHtml)}`.replace(/\s+/gu, " ").trim();
|
|
45464
45619
|
const expectedLevels = [1, ...headingLevels(bodyHtml)].join();
|
|
@@ -45511,6 +45666,7 @@ ${request.content}`
|
|
|
45511
45666
|
if (headingLevels(note2.html).join() !== expectedLevels)
|
|
45512
45667
|
throw new Error("Heading styles not verified");
|
|
45513
45668
|
assertAppendedHtmlLinks(0, note2.rich.links, bodyHtml);
|
|
45669
|
+
if (checkBlocks) assertMarkdownBlocks(note2.rich, blockExpectations);
|
|
45514
45670
|
return [{ id: noteId3, account: account2, note: note2 }];
|
|
45515
45671
|
} catch (error2) {
|
|
45516
45672
|
failures.push(`${noteId3}: ${reason(error2)}`);
|
|
@@ -46748,7 +46904,7 @@ import {
|
|
|
46748
46904
|
writeFileSync as writeFileSync3
|
|
46749
46905
|
} from "node:fs";
|
|
46750
46906
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
46751
|
-
import { basename, isAbsolute as isAbsolute2, join as join15 } from "node:path";
|
|
46907
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join15 } from "node:path";
|
|
46752
46908
|
var noteId = exactIdInput(
|
|
46753
46909
|
"ICNote",
|
|
46754
46910
|
/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
|
|
@@ -46764,7 +46920,7 @@ function readSnapshot(manager, id2) {
|
|
|
46764
46920
|
if (!body) throw new Error("Note content is unavailable");
|
|
46765
46921
|
const enriched = enrichNoteRead(id2, body);
|
|
46766
46922
|
const rich = readRichNote(id2);
|
|
46767
|
-
if (
|
|
46923
|
+
if (enriched.revision !== rich.revision)
|
|
46768
46924
|
throw new Error("Native metadata changed during read; read the note again");
|
|
46769
46925
|
return { id: id2, title: note.title, body, rich, hash: richContentHash(body, enriched) };
|
|
46770
46926
|
}
|
|
@@ -46853,66 +47009,144 @@ function registerDirectOperations(server2, manager) {
|
|
|
46853
47009
|
...manager.renameFolderById(args.id, args.expectedName, args.expectedParentId, args.newName)
|
|
46854
47010
|
})
|
|
46855
47011
|
);
|
|
47012
|
+
const attachmentInput = {
|
|
47013
|
+
path: external_exports.string().min(1).max(4096).describe("Absolute path of the local file to attach"),
|
|
47014
|
+
filename: external_exports.string().min(1).max(255).optional().describe(
|
|
47015
|
+
"Name the attachment gets in Notes instead of the source file's name. One path component with the source file's extension; no slash, colon, or control characters, and no leading dot."
|
|
47016
|
+
)
|
|
47017
|
+
};
|
|
46856
47018
|
tool(
|
|
46857
47019
|
"add-attachment",
|
|
46858
|
-
"Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, and verifies existing content plus fetched bytes.",
|
|
46859
|
-
{ id: noteId, expectedContentHash: revision,
|
|
46860
|
-
(
|
|
46861
|
-
|
|
46862
|
-
|
|
46863
|
-
|
|
46864
|
-
|
|
46865
|
-
|
|
46866
|
-
|
|
47020
|
+
"Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, attachment name, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, and verifies existing content plus fetched bytes.",
|
|
47021
|
+
{ id: noteId, expectedContentHash: revision, ...attachmentInput },
|
|
47022
|
+
(args) => attachFile(manager, args)
|
|
47023
|
+
);
|
|
47024
|
+
tool(
|
|
47025
|
+
"create-note-with-attachment",
|
|
47026
|
+
"Use when: creating a new note that holds one local file, in one call.\nReturns: the new note id plus the add-attachment result (attachment id, bytes, name, content hash).\nDo not use when: the note already exists (add-attachment).\nSafety: checks the file and filename before creating anything, creates the note through Notes.app like create-note, then attaches with the same byte verification as add-attachment. If the attachment step fails after the note exists, the error names the new note's id; attach to it with add-attachment instead of creating another note.",
|
|
47027
|
+
{
|
|
47028
|
+
title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
|
|
47029
|
+
content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
|
|
47030
|
+
folder: external_exports.string().max(1e3).optional().describe("Existing folder or nested path; create it first with create-folder"),
|
|
47031
|
+
account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
|
|
47032
|
+
...attachmentInput
|
|
47033
|
+
},
|
|
47034
|
+
(args) => {
|
|
47035
|
+
const name = attachmentName(args.path, args.filename);
|
|
47036
|
+
localAttachment(args.path);
|
|
47037
|
+
const note = manager.createNote(
|
|
47038
|
+
args.title,
|
|
47039
|
+
args.content ?? "",
|
|
47040
|
+
[],
|
|
47041
|
+
args.folder,
|
|
47042
|
+
args.account,
|
|
47043
|
+
"plaintext"
|
|
47044
|
+
);
|
|
47045
|
+
if (!note)
|
|
47046
|
+
throw new Error(
|
|
47047
|
+
`Failed to create note "${args.title}". Check that the folder and account exist (list-folders, list-accounts); nothing was attached`
|
|
47048
|
+
);
|
|
47049
|
+
const handOff = `Note ${note.id} was created; attach to it with add-attachment instead of creating another note`;
|
|
47050
|
+
let snapshot;
|
|
47051
|
+
try {
|
|
47052
|
+
snapshot = readSnapshot(manager, note.id);
|
|
47053
|
+
} catch (error2) {
|
|
47054
|
+
throw new Error(`${handOff}. The new note could not be read back: ${String(error2)}`);
|
|
47055
|
+
}
|
|
46867
47056
|
try {
|
|
46868
|
-
writeFileSync3(temporaryFile, bytes, { mode: 384 });
|
|
46869
|
-
if (readSnapshot(manager, id2).hash !== before.hash)
|
|
46870
|
-
throw new Error("Note revision changed");
|
|
46871
|
-
let returnedId;
|
|
46872
|
-
let transportUncertain = false;
|
|
46873
|
-
try {
|
|
46874
|
-
returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
|
|
46875
|
-
} catch {
|
|
46876
|
-
transportUncertain = true;
|
|
46877
|
-
}
|
|
46878
|
-
const after = readSnapshot(manager, id2);
|
|
46879
|
-
assertExistingContentPreserved(before, after);
|
|
46880
|
-
const readInserted = () => [
|
|
46881
|
-
...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()
|
|
46882
|
-
].filter((item) => !beforeAttachments.some((existing) => existing.id === item.id));
|
|
46883
|
-
let inserted = readInserted();
|
|
46884
|
-
for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
|
|
46885
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
46886
|
-
inserted = readInserted();
|
|
46887
|
-
}
|
|
46888
|
-
const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
|
|
46889
|
-
if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
|
|
46890
|
-
throw new Error(
|
|
46891
|
-
"Attachment insertion outcome uncertain; read the exact note before retrying"
|
|
46892
|
-
);
|
|
46893
|
-
const attachmentId = inserted[0].id;
|
|
46894
|
-
const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
|
|
46895
|
-
const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
|
|
46896
|
-
if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
|
|
46897
|
-
throw new Error(
|
|
46898
|
-
"Attachment bytes were not verified; read the exact note before retrying"
|
|
46899
|
-
);
|
|
46900
47057
|
return {
|
|
46901
|
-
|
|
46902
|
-
|
|
46903
|
-
|
|
46904
|
-
|
|
46905
|
-
|
|
46906
|
-
|
|
46907
|
-
|
|
46908
|
-
|
|
47058
|
+
...attachFile(manager, {
|
|
47059
|
+
id: note.id,
|
|
47060
|
+
expectedContentHash: snapshot.hash,
|
|
47061
|
+
path: args.path,
|
|
47062
|
+
filename: name
|
|
47063
|
+
}),
|
|
47064
|
+
title: args.title,
|
|
47065
|
+
folder: args.folder,
|
|
47066
|
+
noteCreated: true
|
|
46909
47067
|
};
|
|
46910
|
-
}
|
|
46911
|
-
|
|
47068
|
+
} catch (error2) {
|
|
47069
|
+
throw new Error(`${handOff}. ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
46912
47070
|
}
|
|
46913
47071
|
}
|
|
46914
47072
|
);
|
|
46915
47073
|
}
|
|
47074
|
+
function attachmentName(path7, filename) {
|
|
47075
|
+
const source = basename(path7);
|
|
47076
|
+
if (filename === void 0) return source;
|
|
47077
|
+
if (Buffer.byteLength(filename, "utf8") > 255)
|
|
47078
|
+
throw new Error("filename must be at most 255 bytes");
|
|
47079
|
+
if (filename !== filename.trim() || filename === "" || filename.startsWith(".") || /[/:\\\p{Cc}]/u.test(filename))
|
|
47080
|
+
throw new Error(
|
|
47081
|
+
"filename must be one path component with no slash, colon, backslash, control character, leading dot, or surrounding spaces"
|
|
47082
|
+
);
|
|
47083
|
+
if (extname(filename).toLowerCase() !== extname(source).toLowerCase())
|
|
47084
|
+
throw new Error(
|
|
47085
|
+
`filename must keep the source file's extension (${extname(source) || "none"})`
|
|
47086
|
+
);
|
|
47087
|
+
return filename;
|
|
47088
|
+
}
|
|
47089
|
+
function attachFile(manager, args) {
|
|
47090
|
+
const { id: id2, expectedContentHash, path: path7 } = args;
|
|
47091
|
+
const name = attachmentName(path7, args.filename);
|
|
47092
|
+
const before = readSnapshot(manager, id2);
|
|
47093
|
+
if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
|
|
47094
|
+
const bytes = localAttachment(path7);
|
|
47095
|
+
const beforeAttachments = manager.listAttachmentsById(id2);
|
|
47096
|
+
const directory = mkdtempSync4(join15(tmpdir4(), "notes-attachment-add-"));
|
|
47097
|
+
const temporaryFile = join15(directory, name);
|
|
47098
|
+
try {
|
|
47099
|
+
writeFileSync3(temporaryFile, bytes, { mode: 384 });
|
|
47100
|
+
if (readSnapshot(manager, id2).hash !== before.hash) throw new Error("Note revision changed");
|
|
47101
|
+
let returnedId;
|
|
47102
|
+
let transportUncertain = false;
|
|
47103
|
+
try {
|
|
47104
|
+
returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
|
|
47105
|
+
} catch {
|
|
47106
|
+
transportUncertain = true;
|
|
47107
|
+
}
|
|
47108
|
+
const after = readSnapshot(manager, id2);
|
|
47109
|
+
assertExistingContentPreserved(before, after);
|
|
47110
|
+
const readInserted = () => [...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()].filter(
|
|
47111
|
+
(item) => !beforeAttachments.some((existing) => existing.id === item.id)
|
|
47112
|
+
);
|
|
47113
|
+
let inserted = readInserted();
|
|
47114
|
+
for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
|
|
47115
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
47116
|
+
inserted = readInserted();
|
|
47117
|
+
}
|
|
47118
|
+
const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
|
|
47119
|
+
if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
|
|
47120
|
+
throw new Error(
|
|
47121
|
+
"Attachment insertion outcome uncertain; read the exact note before retrying"
|
|
47122
|
+
);
|
|
47123
|
+
const attachmentId = inserted[0].id;
|
|
47124
|
+
const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
|
|
47125
|
+
const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
|
|
47126
|
+
if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
|
|
47127
|
+
throw new Error("Attachment bytes were not verified; read the exact note before retrying");
|
|
47128
|
+
const nameVerified = inserted[0].name === name;
|
|
47129
|
+
return {
|
|
47130
|
+
ok: true,
|
|
47131
|
+
id: id2,
|
|
47132
|
+
attachmentId,
|
|
47133
|
+
contentHash: after.hash,
|
|
47134
|
+
bytes: bytes.length,
|
|
47135
|
+
name: inserted[0].name,
|
|
47136
|
+
...args.filename === void 0 ? {} : {
|
|
47137
|
+
filenameVerified: nameVerified,
|
|
47138
|
+
...nameVerified ? {} : {
|
|
47139
|
+
filenameWarning: "The attachment and its bytes were verified, but Notes reports a different name"
|
|
47140
|
+
}
|
|
47141
|
+
},
|
|
47142
|
+
...transportUncertain ? {
|
|
47143
|
+
transportWarning: "Transport was uncertain; exact bytes and prior content were verified"
|
|
47144
|
+
} : {}
|
|
47145
|
+
};
|
|
47146
|
+
} finally {
|
|
47147
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
47148
|
+
}
|
|
47149
|
+
}
|
|
46916
47150
|
|
|
46917
47151
|
// src/tools/nativeTagsBridge.ts
|
|
46918
47152
|
var noteId2 = exactIdInput(
|
|
@@ -47006,7 +47240,8 @@ var VERIFIED_BACKGROUND = /* @__PURE__ */ new Set([
|
|
|
47006
47240
|
"set-note-pinned",
|
|
47007
47241
|
"remove-native-tags",
|
|
47008
47242
|
"replace-native-tag",
|
|
47009
|
-
"create-note-markdown"
|
|
47243
|
+
"create-note-markdown",
|
|
47244
|
+
"create-note-markdown-blocks"
|
|
47010
47245
|
]);
|
|
47011
47246
|
var LIVE_VALIDATION_BLOCKERS = {};
|
|
47012
47247
|
var signingRefusal = "Installed Shortcuts refuses to sign this Notes action (unsupported features); no background fallback is enabled";
|
|
@@ -47040,6 +47275,10 @@ var common = {
|
|
|
47040
47275
|
"Distinctive existing phrase used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
|
|
47041
47276
|
)
|
|
47042
47277
|
};
|
|
47278
|
+
var EMPTY_TABLE_ROWS = [
|
|
47279
|
+
["", ""],
|
|
47280
|
+
["", ""]
|
|
47281
|
+
];
|
|
47043
47282
|
var htmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
47044
47283
|
var MAX_CHECKLIST_BATCH = 20;
|
|
47045
47284
|
var byPosition = (items = []) => [...items].sort((a, b) => a.start - b.start);
|
|
@@ -47153,7 +47392,8 @@ function registerNativeOperations(server2, manager) {
|
|
|
47153
47392
|
"remove-native-tags",
|
|
47154
47393
|
"replace-native-tag",
|
|
47155
47394
|
"insert-note-link",
|
|
47156
|
-
"create-note-markdown"
|
|
47395
|
+
"create-note-markdown",
|
|
47396
|
+
"create-note-markdown-blocks"
|
|
47157
47397
|
];
|
|
47158
47398
|
let tagBridgeInstalled = false;
|
|
47159
47399
|
try {
|
|
@@ -47165,7 +47405,7 @@ function registerNativeOperations(server2, manager) {
|
|
|
47165
47405
|
markdownBridgeInstalled = markdownNoteStatus().installed;
|
|
47166
47406
|
} catch {
|
|
47167
47407
|
}
|
|
47168
|
-
const installed = (name) => name
|
|
47408
|
+
const installed = (name) => name.startsWith("create-note-markdown") ? markdownBridgeInstalled : bridge.installed;
|
|
47169
47409
|
return {
|
|
47170
47410
|
bridge,
|
|
47171
47411
|
nativeTagBridgeInstalled: tagBridgeInstalled,
|
|
@@ -47241,13 +47481,16 @@ function registerNativeOperations(server2, manager) {
|
|
|
47241
47481
|
);
|
|
47242
47482
|
tool(
|
|
47243
47483
|
"create-table",
|
|
47244
|
-
"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.",
|
|
47484
|
+
"Use when: appending one native Notes table from a rectangular array of strings, or an empty table when rows is omitted.\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.",
|
|
47245
47485
|
{
|
|
47246
47486
|
...common,
|
|
47247
|
-
rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3)
|
|
47487
|
+
rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3).optional().describe(
|
|
47488
|
+
"Cell text, row by row. Omit for an empty 2 x 2 table, the size Notes itself inserts."
|
|
47489
|
+
)
|
|
47248
47490
|
},
|
|
47249
|
-
(
|
|
47491
|
+
(input) => {
|
|
47250
47492
|
requireValidated("create-table");
|
|
47493
|
+
const args = { ...input, rows: input.rows ?? EMPTY_TABLE_ROWS };
|
|
47251
47494
|
if (args.rows.some((row) => row.length !== args.rows[0].length))
|
|
47252
47495
|
throw new Error("Table rows must have equal cell counts");
|
|
47253
47496
|
const before = readRichNote(args.id);
|
|
@@ -47619,7 +47862,7 @@ registerTool(
|
|
|
47619
47862
|
inputSchema: {
|
|
47620
47863
|
title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
|
|
47621
47864
|
content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).optional().describe(
|
|
47622
|
-
'Note body; required unless contentPath is given (pass exactly one). AppleScript cannot create true Apple Notes checklists \u2014 `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items
|
|
47865
|
+
'Note body; required unless contentPath is given (pass exactly one). In plaintext and HTML, AppleScript cannot create true Apple Notes checklists \u2014 `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items; create a plain `<ul>` or `- ` list and convert it in Notes.app with \u21E7\u2318L. With format "markdown" (markdownRoute "shortcut"), `- [ ]`/`- [x]` lines, `>` block quotes, ``` fenced code, `---` dividers and `inline code` (which Notes renders as a highlight, not monospace) become native styles (see create-note-markdown-blocks in get-capabilities).'
|
|
47623
47866
|
),
|
|
47624
47867
|
contentPath: external_exports.string().min(1).max(MAX.SAVE_PATH).optional().describe(
|
|
47625
47868
|
`Absolute path of a local UTF-8 file to use as the body instead of content (pass exactly one). The same locations save-attachment may write to are allowed (home, temp, /Volumes); symbolic links and non-regular files are refused. Limit ${MAX_CONTENT_FILE_BYTES} bytes.`
|
|
@@ -47628,7 +47871,7 @@ registerTool(
|
|
|
47628
47871
|
"Content format: 'plaintext' (default), 'html' for rich formatting, or 'markdown'. Markdown whose first line is exactly `# <title>` (same case and spacing) has that line and one blank line after it removed, since the title is supplied separately."
|
|
47629
47872
|
),
|
|
47630
47873
|
markdownRoute: external_exports.enum(["shortcut", "html"]).optional().default("shortcut").describe(
|
|
47631
|
-
"How format 'markdown' is imported. 'shortcut' (default) uses the Create Markdown Note Shortcut for real Title/Heading/Subheading styles (iCloud only, no tags; see get-capabilities). 'html' is the fallback when that Shortcut is not installed or the note is outside iCloud: it converts the same bounded Markdown subset to HTML and creates the note through AppleScript in any account, rendering `- [ ]` / `- [x]` task items as ordinary list rows that start with a visible \u2610 / \u2611 character \u2014 not native, checkable checklist items."
|
|
47874
|
+
"How format 'markdown' is imported. 'shortcut' (default) uses the Create Markdown Note Shortcut for real Title/Heading/Subheading styles (iCloud only, no tags; see get-capabilities). 'html' is the fallback when that Shortcut is not installed or the note is outside iCloud: it converts the same bounded Markdown subset to HTML and creates the note through AppleScript in any account, rendering `- [ ]` / `- [x]` task items as ordinary list rows that start with a visible \u2610 / \u2611 character \u2014 not native, checkable checklist items \u2014 refusing block quotes, fenced code and inline code, and keeping a `---` line as literal text."
|
|
47632
47875
|
),
|
|
47633
47876
|
tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
|
|
47634
47877
|
"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. Refused with format 'markdown': add tags afterwards with add-native-tags."
|
|
@@ -47718,6 +47961,7 @@ registerTool(
|
|
|
47718
47961
|
'tags are not supported with format "markdown"; create the note without tags, then add them with add-native-tags using the returned id'
|
|
47719
47962
|
);
|
|
47720
47963
|
requireValidated("create-note-markdown");
|
|
47964
|
+
if (usesMarkdownBlocks(content)) requireValidated("create-note-markdown-blocks");
|
|
47721
47965
|
const result = createMarkdownNote(notesManager, { title, content, folder });
|
|
47722
47966
|
return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`, {
|
|
47723
47967
|
...result,
|
package/package.json
CHANGED