apple-notes-mcp 2.8.30 → 2.8.32
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 +86 -5
- package/build/index.js +380 -123
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -254,11 +254,14 @@ Creates a new note in Apple Notes.
|
|
|
254
254
|
| Parameter | Type | Required | Description |
|
|
255
255
|
|-----------|------|----------|-------------|
|
|
256
256
|
| `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
|
|
257
|
-
| `content` | string |
|
|
257
|
+
| `content` | string | One of `content`/`contentPath` | The body content of the note (do not repeat the title here) |
|
|
258
|
+
| `contentPath` | string | One of `content`/`contentPath` | Absolute path of a local UTF-8 file to use as the body instead of `content`. Allowed in the same places [`save-attachment`](#save-attachment) may write (home, temp, `/Volumes`); symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped |
|
|
258
259
|
| `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags). Refused with `format: "markdown"` |
|
|
259
260
|
| `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). Defaults to account root |
|
|
260
261
|
| `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
|
|
261
262
|
| `format` | string | No | Content format: `"plaintext"` (default), `"html"`, or `"markdown"`. In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities. `"markdown"` produces real Title/Heading/Subheading styles through a Shortcut; see [Markdown notes](#markdown-notes) |
|
|
263
|
+
| `markdownRoute` | string | No | With `format: "markdown"` only: `"shortcut"` (default) or `"html"`. See [Markdown through HTML](#markdown-through-html) |
|
|
264
|
+
| `timeoutSeconds` | number | No | Whole seconds, 1–120, for each Notes.app automation step this call runs; overrides `APPLE_NOTES_MCP_TIMEOUT_MS` for this call only. A timed-out write is uncertain, not failed: read the note by id before any retry. Also accepted by `update-note`, `append-to-note`, `delete-note` and `move-note` |
|
|
262
265
|
|
|
263
266
|
**Example (tagged with inline hashtags):**
|
|
264
267
|
```json
|
|
@@ -345,6 +348,35 @@ title line, with no seed line.
|
|
|
345
348
|
as `create-note-markdown`. The Create Markdown Note Shortcut is optional and
|
|
346
349
|
needed only for this format (macOS 26+); install and approve it as described in
|
|
347
350
|
[`shortcuts/README.md`](shortcuts/README.md).
|
|
351
|
+
- A first line that is exactly `# <title>` (same case and spacing as `title`) is
|
|
352
|
+
removed together with one blank line after it, because the title is supplied
|
|
353
|
+
separately; the response then carries `strippedDuplicateTitle: true`. A
|
|
354
|
+
different first heading stays in the body. This applies to both Markdown
|
|
355
|
+
routes, and Markdown that holds only that heading is refused.
|
|
356
|
+
|
|
357
|
+
##### Markdown through HTML
|
|
358
|
+
|
|
359
|
+
`markdownRoute: "html"` imports the same bounded Markdown subset without the
|
|
360
|
+
Shortcut: the server converts it to HTML and creates the note through AppleScript,
|
|
361
|
+
like `format: "html"`. It works in any account, accepts `tags`, and needs no
|
|
362
|
+
Shortcut, but headings get the plain bold styling described in the known
|
|
363
|
+
limitation above rather than real Heading and Subheading styles.
|
|
364
|
+
|
|
365
|
+
On this route, bullet task items (`- [ ] item`, `- [x] item`) become ordinary
|
|
366
|
+
list rows that start with a visible `☐` or `☑` character, and the response
|
|
367
|
+
reports how many as `taskItemsRendered`. They are text, not native checkable
|
|
368
|
+
checklist items. Use this route as the glyph fallback when the Shortcut is not
|
|
369
|
+
installed or the note is outside iCloud.
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"title": "Weekly Review",
|
|
374
|
+
"contentPath": "/Users/me/Documents/weekly-review.md",
|
|
375
|
+
"format": "markdown",
|
|
376
|
+
"markdownRoute": "html",
|
|
377
|
+
"folder": "Work"
|
|
378
|
+
}
|
|
379
|
+
```
|
|
348
380
|
|
|
349
381
|
**Returns:** Confirmation message with note title and ID. Save the ID for subsequent operations like `update-note`, `delete-note`, etc.
|
|
350
382
|
|
|
@@ -737,7 +769,10 @@ was read, deletion is also rejected.
|
|
|
737
769
|
}
|
|
738
770
|
```
|
|
739
771
|
|
|
740
|
-
**Returns:** Confirmation message, or error if note not found.
|
|
772
|
+
**Returns:** Confirmation message, or error if note not found. The delete and a
|
|
773
|
+
check of the note's original folder run in one AppleScript: if Notes.app accepts
|
|
774
|
+
the delete but the note is still listed in that folder, the call reports that
|
|
775
|
+
nothing was deleted instead of claiming success.
|
|
741
776
|
|
|
742
777
|
**⚠️ Safety:** Irreversible from the agent's side — requires explicit user confirmation before calling. Prefer `search-notes` / `list-notes` first to confirm the exact id(s) being deleted.
|
|
743
778
|
|
|
@@ -776,7 +811,7 @@ Appends or prepends content to an existing note without replacing it. Always rea
|
|
|
776
811
|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
|
|
777
812
|
| `expectedContentHash` | string | Yes | `contentHash` from the exact note version being extended |
|
|
778
813
|
| `content` | string | Yes | Text to append to the note body |
|
|
779
|
-
| `position` | string | No | `"after"` (default) appends to the end; `"before"`
|
|
814
|
+
| `position` | string | No | `"after"` (default) appends to the end; `"before"` inserts directly below the note's title line, so the title stays first |
|
|
780
815
|
| `separator` | string | No | String placed between existing content and new content (default: two newlines → `<div><br></div>` in HTML) |
|
|
781
816
|
| `format` | string | No | Format of the content being appended: `"plaintext"` (default) or `"html"` |
|
|
782
817
|
|
|
@@ -1314,6 +1349,50 @@ latest `expectedContentHash`, and an absolute `path`. The server never retries
|
|
|
1314
1349
|
the insertion. It verifies that existing rich content survived and compares the
|
|
1315
1350
|
fetched attachment bytes with the source before reporting success.
|
|
1316
1351
|
|
|
1352
|
+
| Parameter | Type | Required | Description |
|
|
1353
|
+
|-----------|------|----------|-------------|
|
|
1354
|
+
| `id` | string | Yes | Exact CoreData note ID |
|
|
1355
|
+
| `expectedContentHash` | string | Yes | `contentHash` from the note version being extended |
|
|
1356
|
+
| `path` | string | Yes | Absolute path of the local file; symbolic links are refused |
|
|
1357
|
+
| `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 |
|
|
1358
|
+
|
|
1359
|
+
**Returns:** `attachmentId`, `bytes`, the `name` Notes reports, and the new
|
|
1360
|
+
`contentHash`. With `filename`, `filenameVerified` says whether Notes reports
|
|
1361
|
+
that exact name. A mismatch is a warning (`filenameWarning`), not a failure,
|
|
1362
|
+
because the attachment and its bytes are already verified.
|
|
1363
|
+
|
|
1364
|
+
---
|
|
1365
|
+
|
|
1366
|
+
#### `create-note-with-attachment`
|
|
1367
|
+
|
|
1368
|
+
Creates a note and attaches one local file to it in a single call. It checks the
|
|
1369
|
+
file and `filename` before creating anything, creates the note through Notes.app
|
|
1370
|
+
the way [`create-note`](#create-note) does in plaintext, then attaches with the
|
|
1371
|
+
same verification as [`add-attachment`](#add-attachment).
|
|
1372
|
+
|
|
1373
|
+
| Parameter | Type | Required | Description |
|
|
1374
|
+
|-----------|------|----------|-------------|
|
|
1375
|
+
| `title` | string | Yes | One-line note title |
|
|
1376
|
+
| `content` | string | No | Plain-text body placed above the attachment |
|
|
1377
|
+
| `folder` | string | No | Existing folder or nested path; create it first with [`create-folder`](#create-folder) |
|
|
1378
|
+
| `account` | string | No | Account name; defaults to Notes.app's default account |
|
|
1379
|
+
| `path` | string | Yes | Absolute path of the local file (at most 64 MiB) |
|
|
1380
|
+
| `filename` | string | No | Attachment name override, as in `add-attachment` |
|
|
1381
|
+
|
|
1382
|
+
```json
|
|
1383
|
+
{
|
|
1384
|
+
"title": "Signed lease",
|
|
1385
|
+
"folder": "Home",
|
|
1386
|
+
"path": "/Users/me/Downloads/scan-0042.pdf",
|
|
1387
|
+
"filename": "Lease 2026.pdf"
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
**Returns:** the new note's `id` with `noteCreated: true` and the
|
|
1392
|
+
`add-attachment` result. If the attachment step fails after the note exists,
|
|
1393
|
+
the error names the new note's id: attach to it with `add-attachment` instead of
|
|
1394
|
+
calling this tool again, which would create a second note.
|
|
1395
|
+
|
|
1317
1396
|
---
|
|
1318
1397
|
|
|
1319
1398
|
#### `list-attachments`
|
|
@@ -1535,7 +1614,9 @@ the note before retrying, and retry only the items that are not present.
|
|
|
1535
1614
|
#### `create-table`
|
|
1536
1615
|
|
|
1537
1616
|
Appends a native table from rectangular string rows and verifies every decoded
|
|
1538
|
-
cell. It never substitutes a text table.
|
|
1617
|
+
cell. It never substitutes a text table. Omit `rows` for an empty 2 × 2 table,
|
|
1618
|
+
the size Notes itself inserts from Format > Table; its four empty cells are
|
|
1619
|
+
verified the same way.
|
|
1539
1620
|
|
|
1540
1621
|
#### `set-note-pinned`
|
|
1541
1622
|
|
|
@@ -1693,7 +1774,7 @@ All configuration is optional — the server works out of the box. Override beha
|
|
|
1693
1774
|
| `APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES` | `26214400` (25 MB) | Max size of an attachment that [`fetch-attachment`](#fetch-attachment) will base64-encode inline. Larger attachments are rejected with an error pointing at [`save-attachment`](#save-attachment) (which streams to disk and has no such limit). Raise it to fetch bigger attachments inline; lower it to cap memory. |
|
|
1694
1775
|
| `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` | `262144` (256 KB) | Per-image cap on the base64 payload kept inline in a [`get-note-content`](#get-note-content) response. Inline images over the cap are replaced with placeholders (with a warning appended) so an image-heavy note cannot exceed the MCP client's message limit and drop the connection; export the real files with [`save-attachment`](#save-attachment) or [`fetch-attachment`](#fetch-attachment). Raise it to keep bigger images inline. |
|
|
1695
1776
|
| `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
|
|
1696
|
-
| `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win. |
|
|
1777
|
+
| `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win, and a write tool's `timeoutSeconds` argument overrides it for that call. |
|
|
1697
1778
|
| `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (8 MB) | Largest response `export-notes-json` sends; a page closes early to stay under it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages. |
|
|
1698
1779
|
| `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. |
|
|
1699
1780
|
| `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum attempts for a read-only AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection). `2` means one retry; set `1` to fail fast with no retries. Retries share the single `APPLE_NOTES_MCP_TIMEOUT_MS` budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a **timeout** has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry. |
|
package/build/index.js
CHANGED
|
@@ -417,11 +417,11 @@ var require_codegen = __commonJS({
|
|
|
417
417
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
418
418
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
419
419
|
}
|
|
420
|
-
optimizeNames(names,
|
|
420
|
+
optimizeNames(names, constants3) {
|
|
421
421
|
if (!names[this.name.str])
|
|
422
422
|
return;
|
|
423
423
|
if (this.rhs)
|
|
424
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
424
|
+
this.rhs = optimizeExpr(this.rhs, names, constants3);
|
|
425
425
|
return this;
|
|
426
426
|
}
|
|
427
427
|
get names() {
|
|
@@ -438,10 +438,10 @@ var require_codegen = __commonJS({
|
|
|
438
438
|
render({ _n }) {
|
|
439
439
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
440
440
|
}
|
|
441
|
-
optimizeNames(names,
|
|
441
|
+
optimizeNames(names, constants3) {
|
|
442
442
|
if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
|
|
443
443
|
return;
|
|
444
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
444
|
+
this.rhs = optimizeExpr(this.rhs, names, constants3);
|
|
445
445
|
return this;
|
|
446
446
|
}
|
|
447
447
|
get names() {
|
|
@@ -502,8 +502,8 @@ var require_codegen = __commonJS({
|
|
|
502
502
|
optimizeNodes() {
|
|
503
503
|
return `${this.code}` ? this : void 0;
|
|
504
504
|
}
|
|
505
|
-
optimizeNames(names,
|
|
506
|
-
this.code = optimizeExpr(this.code, names,
|
|
505
|
+
optimizeNames(names, constants3) {
|
|
506
|
+
this.code = optimizeExpr(this.code, names, constants3);
|
|
507
507
|
return this;
|
|
508
508
|
}
|
|
509
509
|
get names() {
|
|
@@ -532,12 +532,12 @@ var require_codegen = __commonJS({
|
|
|
532
532
|
}
|
|
533
533
|
return nodes.length > 0 ? this : void 0;
|
|
534
534
|
}
|
|
535
|
-
optimizeNames(names,
|
|
535
|
+
optimizeNames(names, constants3) {
|
|
536
536
|
const { nodes } = this;
|
|
537
537
|
let i = nodes.length;
|
|
538
538
|
while (i--) {
|
|
539
539
|
const n = nodes[i];
|
|
540
|
-
if (n.optimizeNames(names,
|
|
540
|
+
if (n.optimizeNames(names, constants3))
|
|
541
541
|
continue;
|
|
542
542
|
subtractNames(names, n.names);
|
|
543
543
|
nodes.splice(i, 1);
|
|
@@ -590,12 +590,12 @@ var require_codegen = __commonJS({
|
|
|
590
590
|
return void 0;
|
|
591
591
|
return this;
|
|
592
592
|
}
|
|
593
|
-
optimizeNames(names,
|
|
593
|
+
optimizeNames(names, constants3) {
|
|
594
594
|
var _a;
|
|
595
|
-
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
596
|
-
if (!(super.optimizeNames(names,
|
|
595
|
+
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants3);
|
|
596
|
+
if (!(super.optimizeNames(names, constants3) || this.else))
|
|
597
597
|
return;
|
|
598
|
-
this.condition = optimizeExpr(this.condition, names,
|
|
598
|
+
this.condition = optimizeExpr(this.condition, names, constants3);
|
|
599
599
|
return this;
|
|
600
600
|
}
|
|
601
601
|
get names() {
|
|
@@ -618,10 +618,10 @@ var require_codegen = __commonJS({
|
|
|
618
618
|
render(opts) {
|
|
619
619
|
return `for(${this.iteration})` + super.render(opts);
|
|
620
620
|
}
|
|
621
|
-
optimizeNames(names,
|
|
622
|
-
if (!super.optimizeNames(names,
|
|
621
|
+
optimizeNames(names, constants3) {
|
|
622
|
+
if (!super.optimizeNames(names, constants3))
|
|
623
623
|
return;
|
|
624
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
624
|
+
this.iteration = optimizeExpr(this.iteration, names, constants3);
|
|
625
625
|
return this;
|
|
626
626
|
}
|
|
627
627
|
get names() {
|
|
@@ -657,10 +657,10 @@ var require_codegen = __commonJS({
|
|
|
657
657
|
render(opts) {
|
|
658
658
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
659
659
|
}
|
|
660
|
-
optimizeNames(names,
|
|
661
|
-
if (!super.optimizeNames(names,
|
|
660
|
+
optimizeNames(names, constants3) {
|
|
661
|
+
if (!super.optimizeNames(names, constants3))
|
|
662
662
|
return;
|
|
663
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
663
|
+
this.iterable = optimizeExpr(this.iterable, names, constants3);
|
|
664
664
|
return this;
|
|
665
665
|
}
|
|
666
666
|
get names() {
|
|
@@ -702,11 +702,11 @@ var require_codegen = __commonJS({
|
|
|
702
702
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
703
703
|
return this;
|
|
704
704
|
}
|
|
705
|
-
optimizeNames(names,
|
|
705
|
+
optimizeNames(names, constants3) {
|
|
706
706
|
var _a, _b;
|
|
707
|
-
super.optimizeNames(names,
|
|
708
|
-
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
709
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names,
|
|
707
|
+
super.optimizeNames(names, constants3);
|
|
708
|
+
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants3);
|
|
709
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants3);
|
|
710
710
|
return this;
|
|
711
711
|
}
|
|
712
712
|
get names() {
|
|
@@ -1007,7 +1007,7 @@ var require_codegen = __commonJS({
|
|
|
1007
1007
|
function addExprNames(names, from) {
|
|
1008
1008
|
return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
|
|
1009
1009
|
}
|
|
1010
|
-
function optimizeExpr(expr, names,
|
|
1010
|
+
function optimizeExpr(expr, names, constants3) {
|
|
1011
1011
|
if (expr instanceof code_1.Name)
|
|
1012
1012
|
return replaceName(expr);
|
|
1013
1013
|
if (!canOptimize(expr))
|
|
@@ -1022,14 +1022,14 @@ var require_codegen = __commonJS({
|
|
|
1022
1022
|
return items;
|
|
1023
1023
|
}, []));
|
|
1024
1024
|
function replaceName(n) {
|
|
1025
|
-
const c =
|
|
1025
|
+
const c = constants3[n.str];
|
|
1026
1026
|
if (c === void 0 || names[n.str] !== 1)
|
|
1027
1027
|
return n;
|
|
1028
1028
|
delete names[n.str];
|
|
1029
1029
|
return c;
|
|
1030
1030
|
}
|
|
1031
1031
|
function canOptimize(e) {
|
|
1032
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 &&
|
|
1032
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants3[c.str] !== void 0);
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
1035
|
function subtractNames(names, from) {
|
|
@@ -7422,7 +7422,7 @@ var require_DOMException = __commonJS({
|
|
|
7422
7422
|
"INVALID_NODE_TYPE_ERR (24): the supplied node is invalid or has an invalid ancestor for this operation",
|
|
7423
7423
|
"DATA_CLONE_ERR (25): the object can not be cloned."
|
|
7424
7424
|
];
|
|
7425
|
-
var
|
|
7425
|
+
var constants3 = {
|
|
7426
7426
|
INDEX_SIZE_ERR,
|
|
7427
7427
|
DOMSTRING_SIZE_ERR: 2,
|
|
7428
7428
|
// historical
|
|
@@ -7461,8 +7461,8 @@ var require_DOMException = __commonJS({
|
|
|
7461
7461
|
this.name = names[code];
|
|
7462
7462
|
}
|
|
7463
7463
|
DOMException.prototype.__proto__ = Error.prototype;
|
|
7464
|
-
for (c in
|
|
7465
|
-
v = { value:
|
|
7464
|
+
for (c in constants3) {
|
|
7465
|
+
v = { value: constants3[c] };
|
|
7466
7466
|
Object.defineProperty(DOMException, c, v);
|
|
7467
7467
|
Object.defineProperty(DOMException.prototype, c, v);
|
|
7468
7468
|
}
|
|
@@ -9872,9 +9872,9 @@ var require_select = __commonJS({
|
|
|
9872
9872
|
var makeSubject = function() {
|
|
9873
9873
|
var target;
|
|
9874
9874
|
function subject(el) {
|
|
9875
|
-
var node = el.ownerDocument,
|
|
9875
|
+
var node = el.ownerDocument, scope2 = node.getElementsByTagName(subject.lname), i = scope2.length;
|
|
9876
9876
|
while (i--) {
|
|
9877
|
-
if (subject.test(
|
|
9877
|
+
if (subject.test(scope2[i]) && target === el) {
|
|
9878
9878
|
target = null;
|
|
9879
9879
|
return true;
|
|
9880
9880
|
}
|
|
@@ -9902,16 +9902,16 @@ var require_select = __commonJS({
|
|
|
9902
9902
|
};
|
|
9903
9903
|
};
|
|
9904
9904
|
var find = function(sel, node) {
|
|
9905
|
-
var results = [], test = compile(sel),
|
|
9906
|
-
while (el =
|
|
9905
|
+
var results = [], test = compile(sel), scope2 = node.getElementsByTagName(test.qname), i = 0, el;
|
|
9906
|
+
while (el = scope2[i++]) {
|
|
9907
9907
|
if (test(el)) results.push(el);
|
|
9908
9908
|
}
|
|
9909
9909
|
if (test.sel) {
|
|
9910
9910
|
while (test.sel) {
|
|
9911
9911
|
test = compile(test.sel);
|
|
9912
|
-
|
|
9912
|
+
scope2 = node.getElementsByTagName(test.qname);
|
|
9913
9913
|
i = 0;
|
|
9914
|
-
while (el =
|
|
9914
|
+
while (el = scope2[i++]) {
|
|
9915
9915
|
if (test(el) && indexOf.call(results, el) === -1) {
|
|
9916
9916
|
results.push(el);
|
|
9917
9917
|
}
|
|
@@ -38990,6 +38990,22 @@ var NODE_RUNTIME_TCC_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blo
|
|
|
38990
38990
|
var AUTOMATION_PERMISSION_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp#permission-denied";
|
|
38991
38991
|
var AUTOMATION_REMEDIATION = `Grant automation access in System Settings > Privacy & Security > Automation to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it \u2014 run the doctor tool to verify. See: ${AUTOMATION_PERMISSION_GUIDE_URL}`;
|
|
38992
38992
|
|
|
38993
|
+
// src/utils/callTimeout.ts
|
|
38994
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
38995
|
+
var CALL_TIMEOUT_SECONDS = { min: 1, max: 120 };
|
|
38996
|
+
var scope = new AsyncLocalStorage();
|
|
38997
|
+
function runWithCallTimeout(seconds, fn) {
|
|
38998
|
+
if (seconds === void 0) return fn();
|
|
38999
|
+
if (!Number.isInteger(seconds) || seconds < CALL_TIMEOUT_SECONDS.min || seconds > CALL_TIMEOUT_SECONDS.max)
|
|
39000
|
+
throw new Error(
|
|
39001
|
+
`timeoutSeconds must be a whole number from ${CALL_TIMEOUT_SECONDS.min} to ${CALL_TIMEOUT_SECONDS.max}`
|
|
39002
|
+
);
|
|
39003
|
+
return scope.run(seconds * 1e3, fn);
|
|
39004
|
+
}
|
|
39005
|
+
function callTimeoutMs() {
|
|
39006
|
+
return scope.getStore();
|
|
39007
|
+
}
|
|
39008
|
+
|
|
38993
39009
|
// src/utils/applescript.ts
|
|
38994
39010
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
38995
39011
|
var DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
@@ -39146,7 +39162,7 @@ function parseErrorMessage(errorOutput) {
|
|
|
39146
39162
|
return coreError.trim() || "Unknown AppleScript error";
|
|
39147
39163
|
}
|
|
39148
39164
|
function executeAppleScript(script, options = {}) {
|
|
39149
|
-
const timeoutMs = options.timeoutMs ?? envPositiveNumber("APPLE_NOTES_MCP_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS;
|
|
39165
|
+
const timeoutMs = options.timeoutMs ?? callTimeoutMs() ?? envPositiveNumber("APPLE_NOTES_MCP_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS;
|
|
39150
39166
|
const maxRetries = options.maxRetries ?? envPositiveNumber("APPLE_NOTES_MCP_MAX_RETRIES") ?? DEFAULT_MAX_RETRIES;
|
|
39151
39167
|
const retryDelayMs = options.retryDelayMs ?? envPositiveNumber("APPLE_NOTES_MCP_RETRY_DELAY_MS") ?? DEFAULT_RETRY_DELAY_MS;
|
|
39152
39168
|
if (!script || !script.trim()) {
|
|
@@ -40837,10 +40853,14 @@ function readSmartFolders(dbPath2 = NOTES_DB_PATH3) {
|
|
|
40837
40853
|
|
|
40838
40854
|
// src/utils/attachmentFs.ts
|
|
40839
40855
|
import {
|
|
40856
|
+
closeSync,
|
|
40857
|
+
constants,
|
|
40840
40858
|
existsSync as existsSync3,
|
|
40859
|
+
fstatSync,
|
|
40841
40860
|
lstatSync,
|
|
40842
40861
|
mkdirSync,
|
|
40843
40862
|
mkdtempSync,
|
|
40863
|
+
openSync,
|
|
40844
40864
|
readFileSync,
|
|
40845
40865
|
realpathSync,
|
|
40846
40866
|
rmSync,
|
|
@@ -40964,6 +40984,48 @@ function fileSize(p) {
|
|
|
40964
40984
|
function makeTempDir() {
|
|
40965
40985
|
return mkdtempSync(resolve(tmpdir(), "apple-notes-att-"));
|
|
40966
40986
|
}
|
|
40987
|
+
function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots()) {
|
|
40988
|
+
if (!p || !p.trim()) throw new Error("A content file path is required.");
|
|
40989
|
+
if (!isAbsolute(p)) throw new Error(`Content file path must be absolute: "${p}"`);
|
|
40990
|
+
const abs = resolve(p);
|
|
40991
|
+
if (!isWithinRoots(abs, roots))
|
|
40992
|
+
throw new Error(`Refusing to read outside allowed locations (home, temp, /Volumes): "${abs}"`);
|
|
40993
|
+
let canonical;
|
|
40994
|
+
try {
|
|
40995
|
+
canonical = canonicalize(abs);
|
|
40996
|
+
} catch {
|
|
40997
|
+
throw new Error(`Content file does not exist or cannot be resolved: "${abs}"`);
|
|
40998
|
+
}
|
|
40999
|
+
if (!isWithinRoots(canonical, canonicalRoots(roots)))
|
|
41000
|
+
throw new Error(
|
|
41001
|
+
`Refusing to read outside allowed locations (home, temp, /Volumes): "${abs}" resolves to "${canonical}".`
|
|
41002
|
+
);
|
|
41003
|
+
let descriptor;
|
|
41004
|
+
try {
|
|
41005
|
+
descriptor = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
41006
|
+
} catch (error2) {
|
|
41007
|
+
if (error2.code === "ELOOP")
|
|
41008
|
+
throw new Error(`Refusing to read the symbolic link "${abs}".`);
|
|
41009
|
+
throw error2;
|
|
41010
|
+
}
|
|
41011
|
+
try {
|
|
41012
|
+
const stat = fstatSync(descriptor);
|
|
41013
|
+
if (!stat.isFile()) throw new Error(`Content file is not a regular file: "${abs}"`);
|
|
41014
|
+
if (stat.size === 0) throw new Error(`Content file is empty: "${abs}"`);
|
|
41015
|
+
if (stat.size > maxBytes)
|
|
41016
|
+
throw new Error(`Content file is ${stat.size} bytes, over the ${maxBytes}-byte limit.`);
|
|
41017
|
+
const bytes = readFileSync(descriptor);
|
|
41018
|
+
if (bytes.length !== stat.size)
|
|
41019
|
+
throw new Error("Content file changed while it was being read; try again");
|
|
41020
|
+
try {
|
|
41021
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "");
|
|
41022
|
+
} catch {
|
|
41023
|
+
throw new Error(`Content file is not valid UTF-8 text: "${abs}"`);
|
|
41024
|
+
}
|
|
41025
|
+
} finally {
|
|
41026
|
+
closeSync(descriptor);
|
|
41027
|
+
}
|
|
41028
|
+
}
|
|
40967
41029
|
function cleanupTempDir(dir) {
|
|
40968
41030
|
try {
|
|
40969
41031
|
if (existsSync3(dir)) rmSync(dir, { recursive: true, force: true });
|
|
@@ -41182,10 +41244,10 @@ function buildAccountResolution(account) {
|
|
|
41182
41244
|
if ${AS_ACCOUNT_REF} is missing value then error "${ACCOUNT_RESOLUTION_ERROR} Account \\"${safeAccount}\\" not found"
|
|
41183
41245
|
`;
|
|
41184
41246
|
}
|
|
41185
|
-
function buildAccountScopedScript(
|
|
41247
|
+
function buildAccountScopedScript(scope2, command) {
|
|
41186
41248
|
return `
|
|
41187
41249
|
tell application "Notes"
|
|
41188
|
-
${buildAccountResolution(
|
|
41250
|
+
${buildAccountResolution(scope2.account)}
|
|
41189
41251
|
tell ${AS_ACCOUNT_REF}
|
|
41190
41252
|
${command}
|
|
41191
41253
|
end tell
|
|
@@ -41765,11 +41827,20 @@ var AppleNotesManager = class {
|
|
|
41765
41827
|
const safeExpectedBody = escapeHtmlForAppleScript(expectedBody);
|
|
41766
41828
|
const script = buildAppLevelScript(`
|
|
41767
41829
|
set noteRef to note id "${safeId}"
|
|
41830
|
+
set originalFolder to missing value
|
|
41831
|
+
try
|
|
41832
|
+
set originalFolder to container of noteRef
|
|
41833
|
+
end try
|
|
41768
41834
|
set currentBody to body of noteRef
|
|
41769
41835
|
considering case
|
|
41770
41836
|
if currentBody is not "${safeExpectedBody}" and currentBody is not "${safeExpectedBody}" & linefeed then return "SAFETY_CONFLICT"
|
|
41771
41837
|
delete noteRef
|
|
41772
41838
|
end considering
|
|
41839
|
+
if originalFolder is not missing value then
|
|
41840
|
+
try
|
|
41841
|
+
if (id of notes of originalFolder) contains "${safeId}" then return "SAFETY_NOT_DELETED"
|
|
41842
|
+
end try
|
|
41843
|
+
end if
|
|
41773
41844
|
return "SAFETY_DELETED"
|
|
41774
41845
|
`);
|
|
41775
41846
|
const result = executeMutationAppleScript(script);
|
|
@@ -41779,6 +41850,7 @@ var AppleNotesManager = class {
|
|
|
41779
41850
|
}
|
|
41780
41851
|
const status = result.output.trim();
|
|
41781
41852
|
if (status === "SAFETY_CONFLICT") return { status: "conflict" };
|
|
41853
|
+
if (status === "SAFETY_NOT_DELETED") return { status: "not-deleted" };
|
|
41782
41854
|
return status === "SAFETY_DELETED" ? { status: "deleted" } : { status: "failed" };
|
|
41783
41855
|
}
|
|
41784
41856
|
/**
|
|
@@ -44969,7 +45041,22 @@ import { join as join12 } from "node:path";
|
|
|
44969
45041
|
|
|
44970
45042
|
// src/utils/appendMarkdown.ts
|
|
44971
45043
|
var escape2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
44972
|
-
|
|
45044
|
+
var TASK_GLYPHS = { open: "\u2610", done: "\u2611" };
|
|
45045
|
+
var TASK_ITEM = /^\[([ xX])\][ \t]+(\S.*)$/;
|
|
45046
|
+
function stripDuplicateTitleHeading(markdown, title) {
|
|
45047
|
+
const normalized2 = markdown.replace(/^\uFEFF/, "");
|
|
45048
|
+
const lines = normalized2.split(/\r?\n/);
|
|
45049
|
+
if (lines[0] !== `# ${title}`) return { content: markdown, stripped: false };
|
|
45050
|
+
const rest = lines.slice(lines.length > 1 && lines[1] === "" ? 2 : 1);
|
|
45051
|
+
return { content: rest.join("\n"), stripped: true };
|
|
45052
|
+
}
|
|
45053
|
+
function countTaskItems(markdown) {
|
|
45054
|
+
return markdown.replace(/\r\n/g, "\n").split("\n").filter((line) => {
|
|
45055
|
+
const item = /^[-+*]\s+(.+)$/.exec(line);
|
|
45056
|
+
return item !== null && TASK_ITEM.test(item[1]);
|
|
45057
|
+
}).length;
|
|
45058
|
+
}
|
|
45059
|
+
function appendMarkdownHtml(markdown, options = {}) {
|
|
44973
45060
|
if (markdown.includes("\uE000") || markdown.includes("\uE001"))
|
|
44974
45061
|
throw new Error("Unsupported reserved characters");
|
|
44975
45062
|
if (Array.from(markdown).some((c) => c.charCodeAt(0) < 32 && !["\n", "\r", " "].includes(c)))
|
|
@@ -45008,7 +45095,8 @@ function appendMarkdownHtml(markdown) {
|
|
|
45008
45095
|
list = kind;
|
|
45009
45096
|
html += `<${kind}>`;
|
|
45010
45097
|
}
|
|
45011
|
-
|
|
45098
|
+
const task = options.taskGlyphs && item[1] ? TASK_ITEM.exec(item[2]) : null;
|
|
45099
|
+
html += task ? `<li>${task[1] === " " ? TASK_GLYPHS.open : TASK_GLYPHS.done} ${inline(task[2])}</li>` : `<li>${inline(item[2])}</li>`;
|
|
45012
45100
|
continue;
|
|
45013
45101
|
}
|
|
45014
45102
|
close();
|
|
@@ -45178,7 +45266,7 @@ function runBackgroundShortcut(input, status = backgroundStatus()) {
|
|
|
45178
45266
|
try {
|
|
45179
45267
|
execFileSync11("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
|
|
45180
45268
|
encoding: "utf8",
|
|
45181
|
-
timeout: 6e4,
|
|
45269
|
+
timeout: callTimeoutMs() ?? 6e4,
|
|
45182
45270
|
maxBuffer: 1024 * 1024,
|
|
45183
45271
|
stdio: ["ignore", "pipe", "pipe"]
|
|
45184
45272
|
});
|
|
@@ -45193,9 +45281,9 @@ function backgroundDependencies(manager) {
|
|
|
45193
45281
|
return {
|
|
45194
45282
|
read: (id2) => readBackgroundSnapshot(manager, id2),
|
|
45195
45283
|
run: runBackgroundShortcut,
|
|
45196
|
-
candidates: (title,
|
|
45284
|
+
candidates: (title, scope2) => manager.listAccounts().flatMap(
|
|
45197
45285
|
(account) => manager.searchNotes(title, false, account.name).filter(
|
|
45198
|
-
(note) => note.title === title && !note.passwordProtected && manager.getNotePlaintextById(note.id).includes(
|
|
45286
|
+
(note) => note.title === title && !note.passwordProtected && manager.getNotePlaintextById(note.id).includes(scope2)
|
|
45199
45287
|
).map((note) => note.id)
|
|
45200
45288
|
)
|
|
45201
45289
|
};
|
|
@@ -46650,17 +46738,17 @@ function insertLink(request, deps) {
|
|
|
46650
46738
|
// src/tools/directOperations.ts
|
|
46651
46739
|
import { createHash as createHash2 } from "node:crypto";
|
|
46652
46740
|
import {
|
|
46653
|
-
closeSync,
|
|
46654
|
-
constants,
|
|
46655
|
-
fstatSync,
|
|
46741
|
+
closeSync as closeSync2,
|
|
46742
|
+
constants as constants2,
|
|
46743
|
+
fstatSync as fstatSync2,
|
|
46656
46744
|
mkdtempSync as mkdtempSync4,
|
|
46657
|
-
openSync,
|
|
46745
|
+
openSync as openSync2,
|
|
46658
46746
|
readFileSync as readFileSync3,
|
|
46659
46747
|
rmSync as rmSync4,
|
|
46660
46748
|
writeFileSync as writeFileSync3
|
|
46661
46749
|
} from "node:fs";
|
|
46662
46750
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
46663
|
-
import { basename, isAbsolute as isAbsolute2, join as join15 } from "node:path";
|
|
46751
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join15 } from "node:path";
|
|
46664
46752
|
var noteId = exactIdInput(
|
|
46665
46753
|
"ICNote",
|
|
46666
46754
|
/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
|
|
@@ -46676,7 +46764,7 @@ function readSnapshot(manager, id2) {
|
|
|
46676
46764
|
if (!body) throw new Error("Note content is unavailable");
|
|
46677
46765
|
const enriched = enrichNoteRead(id2, body);
|
|
46678
46766
|
const rich = readRichNote(id2);
|
|
46679
|
-
if (
|
|
46767
|
+
if (enriched.revision !== rich.revision)
|
|
46680
46768
|
throw new Error("Native metadata changed during read; read the note again");
|
|
46681
46769
|
return { id: id2, title: note.title, body, rich, hash: richContentHash(body, enriched) };
|
|
46682
46770
|
}
|
|
@@ -46705,9 +46793,9 @@ function assertExistingContentPreserved(before, after) {
|
|
|
46705
46793
|
}
|
|
46706
46794
|
function localAttachment(path7) {
|
|
46707
46795
|
if (!isAbsolute2(path7)) throw new Error("An absolute local file path is required");
|
|
46708
|
-
const descriptor =
|
|
46796
|
+
const descriptor = openSync2(path7, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
46709
46797
|
try {
|
|
46710
|
-
const stat =
|
|
46798
|
+
const stat = fstatSync2(descriptor);
|
|
46711
46799
|
if (!stat.isFile() || stat.size === 0 || stat.size > 64 * 1024 * 1024)
|
|
46712
46800
|
throw new Error("Attachment must be a nonempty regular file of at most 64 MiB");
|
|
46713
46801
|
const bytes = readFileSync3(descriptor);
|
|
@@ -46715,7 +46803,7 @@ function localAttachment(path7) {
|
|
|
46715
46803
|
throw new Error("Attachment changed while it was being read; try again");
|
|
46716
46804
|
return bytes;
|
|
46717
46805
|
} finally {
|
|
46718
|
-
|
|
46806
|
+
closeSync2(descriptor);
|
|
46719
46807
|
}
|
|
46720
46808
|
}
|
|
46721
46809
|
function registerDirectOperations(server2, manager) {
|
|
@@ -46765,66 +46853,144 @@ function registerDirectOperations(server2, manager) {
|
|
|
46765
46853
|
...manager.renameFolderById(args.id, args.expectedName, args.expectedParentId, args.newName)
|
|
46766
46854
|
})
|
|
46767
46855
|
);
|
|
46856
|
+
const attachmentInput = {
|
|
46857
|
+
path: external_exports.string().min(1).max(4096).describe("Absolute path of the local file to attach"),
|
|
46858
|
+
filename: external_exports.string().min(1).max(255).optional().describe(
|
|
46859
|
+
"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."
|
|
46860
|
+
)
|
|
46861
|
+
};
|
|
46768
46862
|
tool(
|
|
46769
46863
|
"add-attachment",
|
|
46770
|
-
"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.",
|
|
46771
|
-
{ id: noteId, expectedContentHash: revision,
|
|
46772
|
-
(
|
|
46773
|
-
|
|
46774
|
-
|
|
46775
|
-
|
|
46776
|
-
|
|
46777
|
-
|
|
46778
|
-
|
|
46864
|
+
"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.",
|
|
46865
|
+
{ id: noteId, expectedContentHash: revision, ...attachmentInput },
|
|
46866
|
+
(args) => attachFile(manager, args)
|
|
46867
|
+
);
|
|
46868
|
+
tool(
|
|
46869
|
+
"create-note-with-attachment",
|
|
46870
|
+
"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.",
|
|
46871
|
+
{
|
|
46872
|
+
title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
|
|
46873
|
+
content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
|
|
46874
|
+
folder: external_exports.string().max(1e3).optional().describe("Existing folder or nested path; create it first with create-folder"),
|
|
46875
|
+
account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
|
|
46876
|
+
...attachmentInput
|
|
46877
|
+
},
|
|
46878
|
+
(args) => {
|
|
46879
|
+
const name = attachmentName(args.path, args.filename);
|
|
46880
|
+
localAttachment(args.path);
|
|
46881
|
+
const note = manager.createNote(
|
|
46882
|
+
args.title,
|
|
46883
|
+
args.content ?? "",
|
|
46884
|
+
[],
|
|
46885
|
+
args.folder,
|
|
46886
|
+
args.account,
|
|
46887
|
+
"plaintext"
|
|
46888
|
+
);
|
|
46889
|
+
if (!note)
|
|
46890
|
+
throw new Error(
|
|
46891
|
+
`Failed to create note "${args.title}". Check that the folder and account exist (list-folders, list-accounts); nothing was attached`
|
|
46892
|
+
);
|
|
46893
|
+
const handOff = `Note ${note.id} was created; attach to it with add-attachment instead of creating another note`;
|
|
46894
|
+
let snapshot;
|
|
46895
|
+
try {
|
|
46896
|
+
snapshot = readSnapshot(manager, note.id);
|
|
46897
|
+
} catch (error2) {
|
|
46898
|
+
throw new Error(`${handOff}. The new note could not be read back: ${String(error2)}`);
|
|
46899
|
+
}
|
|
46779
46900
|
try {
|
|
46780
|
-
writeFileSync3(temporaryFile, bytes, { mode: 384 });
|
|
46781
|
-
if (readSnapshot(manager, id2).hash !== before.hash)
|
|
46782
|
-
throw new Error("Note revision changed");
|
|
46783
|
-
let returnedId;
|
|
46784
|
-
let transportUncertain = false;
|
|
46785
|
-
try {
|
|
46786
|
-
returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
|
|
46787
|
-
} catch {
|
|
46788
|
-
transportUncertain = true;
|
|
46789
|
-
}
|
|
46790
|
-
const after = readSnapshot(manager, id2);
|
|
46791
|
-
assertExistingContentPreserved(before, after);
|
|
46792
|
-
const readInserted = () => [
|
|
46793
|
-
...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()
|
|
46794
|
-
].filter((item) => !beforeAttachments.some((existing) => existing.id === item.id));
|
|
46795
|
-
let inserted = readInserted();
|
|
46796
|
-
for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
|
|
46797
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
46798
|
-
inserted = readInserted();
|
|
46799
|
-
}
|
|
46800
|
-
const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
|
|
46801
|
-
if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
|
|
46802
|
-
throw new Error(
|
|
46803
|
-
"Attachment insertion outcome uncertain; read the exact note before retrying"
|
|
46804
|
-
);
|
|
46805
|
-
const attachmentId = inserted[0].id;
|
|
46806
|
-
const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
|
|
46807
|
-
const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
|
|
46808
|
-
if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
|
|
46809
|
-
throw new Error(
|
|
46810
|
-
"Attachment bytes were not verified; read the exact note before retrying"
|
|
46811
|
-
);
|
|
46812
46901
|
return {
|
|
46813
|
-
|
|
46814
|
-
|
|
46815
|
-
|
|
46816
|
-
|
|
46817
|
-
|
|
46818
|
-
|
|
46819
|
-
|
|
46820
|
-
|
|
46902
|
+
...attachFile(manager, {
|
|
46903
|
+
id: note.id,
|
|
46904
|
+
expectedContentHash: snapshot.hash,
|
|
46905
|
+
path: args.path,
|
|
46906
|
+
filename: name
|
|
46907
|
+
}),
|
|
46908
|
+
title: args.title,
|
|
46909
|
+
folder: args.folder,
|
|
46910
|
+
noteCreated: true
|
|
46821
46911
|
};
|
|
46822
|
-
}
|
|
46823
|
-
|
|
46912
|
+
} catch (error2) {
|
|
46913
|
+
throw new Error(`${handOff}. ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
46824
46914
|
}
|
|
46825
46915
|
}
|
|
46826
46916
|
);
|
|
46827
46917
|
}
|
|
46918
|
+
function attachmentName(path7, filename) {
|
|
46919
|
+
const source = basename(path7);
|
|
46920
|
+
if (filename === void 0) return source;
|
|
46921
|
+
if (Buffer.byteLength(filename, "utf8") > 255)
|
|
46922
|
+
throw new Error("filename must be at most 255 bytes");
|
|
46923
|
+
if (filename !== filename.trim() || filename === "" || filename.startsWith(".") || /[/:\\\p{Cc}]/u.test(filename))
|
|
46924
|
+
throw new Error(
|
|
46925
|
+
"filename must be one path component with no slash, colon, backslash, control character, leading dot, or surrounding spaces"
|
|
46926
|
+
);
|
|
46927
|
+
if (extname(filename).toLowerCase() !== extname(source).toLowerCase())
|
|
46928
|
+
throw new Error(
|
|
46929
|
+
`filename must keep the source file's extension (${extname(source) || "none"})`
|
|
46930
|
+
);
|
|
46931
|
+
return filename;
|
|
46932
|
+
}
|
|
46933
|
+
function attachFile(manager, args) {
|
|
46934
|
+
const { id: id2, expectedContentHash, path: path7 } = args;
|
|
46935
|
+
const name = attachmentName(path7, args.filename);
|
|
46936
|
+
const before = readSnapshot(manager, id2);
|
|
46937
|
+
if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
|
|
46938
|
+
const bytes = localAttachment(path7);
|
|
46939
|
+
const beforeAttachments = manager.listAttachmentsById(id2);
|
|
46940
|
+
const directory = mkdtempSync4(join15(tmpdir4(), "notes-attachment-add-"));
|
|
46941
|
+
const temporaryFile = join15(directory, name);
|
|
46942
|
+
try {
|
|
46943
|
+
writeFileSync3(temporaryFile, bytes, { mode: 384 });
|
|
46944
|
+
if (readSnapshot(manager, id2).hash !== before.hash) throw new Error("Note revision changed");
|
|
46945
|
+
let returnedId;
|
|
46946
|
+
let transportUncertain = false;
|
|
46947
|
+
try {
|
|
46948
|
+
returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
|
|
46949
|
+
} catch {
|
|
46950
|
+
transportUncertain = true;
|
|
46951
|
+
}
|
|
46952
|
+
const after = readSnapshot(manager, id2);
|
|
46953
|
+
assertExistingContentPreserved(before, after);
|
|
46954
|
+
const readInserted = () => [...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()].filter(
|
|
46955
|
+
(item) => !beforeAttachments.some((existing) => existing.id === item.id)
|
|
46956
|
+
);
|
|
46957
|
+
let inserted = readInserted();
|
|
46958
|
+
for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
|
|
46959
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
46960
|
+
inserted = readInserted();
|
|
46961
|
+
}
|
|
46962
|
+
const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
|
|
46963
|
+
if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
|
|
46964
|
+
throw new Error(
|
|
46965
|
+
"Attachment insertion outcome uncertain; read the exact note before retrying"
|
|
46966
|
+
);
|
|
46967
|
+
const attachmentId = inserted[0].id;
|
|
46968
|
+
const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
|
|
46969
|
+
const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
|
|
46970
|
+
if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
|
|
46971
|
+
throw new Error("Attachment bytes were not verified; read the exact note before retrying");
|
|
46972
|
+
const nameVerified = inserted[0].name === name;
|
|
46973
|
+
return {
|
|
46974
|
+
ok: true,
|
|
46975
|
+
id: id2,
|
|
46976
|
+
attachmentId,
|
|
46977
|
+
contentHash: after.hash,
|
|
46978
|
+
bytes: bytes.length,
|
|
46979
|
+
name: inserted[0].name,
|
|
46980
|
+
...args.filename === void 0 ? {} : {
|
|
46981
|
+
filenameVerified: nameVerified,
|
|
46982
|
+
...nameVerified ? {} : {
|
|
46983
|
+
filenameWarning: "The attachment and its bytes were verified, but Notes reports a different name"
|
|
46984
|
+
}
|
|
46985
|
+
},
|
|
46986
|
+
...transportUncertain ? {
|
|
46987
|
+
transportWarning: "Transport was uncertain; exact bytes and prior content were verified"
|
|
46988
|
+
} : {}
|
|
46989
|
+
};
|
|
46990
|
+
} finally {
|
|
46991
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
46992
|
+
}
|
|
46993
|
+
}
|
|
46828
46994
|
|
|
46829
46995
|
// src/tools/nativeTagsBridge.ts
|
|
46830
46996
|
var noteId2 = exactIdInput(
|
|
@@ -46896,9 +47062,9 @@ function registerNativeTagsBridge(server2, manager) {
|
|
|
46896
47062
|
{ id: id2, expectedContentHash, scopeText, tags, title: initial.title },
|
|
46897
47063
|
{
|
|
46898
47064
|
read,
|
|
46899
|
-
candidates: (title,
|
|
47065
|
+
candidates: (title, scope2) => manager.listAccounts().flatMap(
|
|
46900
47066
|
(account) => manager.searchNotes(title, false, account.name).filter(
|
|
46901
|
-
(note) => !note.passwordProtected && manager.getNotePlaintextById(note.id).toLocaleLowerCase().includes(
|
|
47067
|
+
(note) => !note.passwordProtected && manager.getNotePlaintextById(note.id).toLocaleLowerCase().includes(scope2.toLocaleLowerCase())
|
|
46902
47068
|
).map((note) => note.id)
|
|
46903
47069
|
),
|
|
46904
47070
|
run: runNativeTagsShortcut
|
|
@@ -46952,6 +47118,10 @@ var common = {
|
|
|
46952
47118
|
"Distinctive existing phrase used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
|
|
46953
47119
|
)
|
|
46954
47120
|
};
|
|
47121
|
+
var EMPTY_TABLE_ROWS = [
|
|
47122
|
+
["", ""],
|
|
47123
|
+
["", ""]
|
|
47124
|
+
];
|
|
46955
47125
|
var htmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
46956
47126
|
var MAX_CHECKLIST_BATCH = 20;
|
|
46957
47127
|
var byPosition = (items = []) => [...items].sort((a, b) => a.start - b.start);
|
|
@@ -47153,13 +47323,16 @@ function registerNativeOperations(server2, manager) {
|
|
|
47153
47323
|
);
|
|
47154
47324
|
tool(
|
|
47155
47325
|
"create-table",
|
|
47156
|
-
"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.",
|
|
47326
|
+
"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.",
|
|
47157
47327
|
{
|
|
47158
47328
|
...common,
|
|
47159
|
-
rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3)
|
|
47329
|
+
rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3).optional().describe(
|
|
47330
|
+
"Cell text, row by row. Omit for an empty 2 x 2 table, the size Notes itself inserts."
|
|
47331
|
+
)
|
|
47160
47332
|
},
|
|
47161
|
-
(
|
|
47333
|
+
(input) => {
|
|
47162
47334
|
requireValidated("create-table");
|
|
47335
|
+
const args = { ...input, rows: input.rows ?? EMPTY_TABLE_ROWS };
|
|
47163
47336
|
if (args.rows.some((row) => row.length !== args.rows[0].length))
|
|
47164
47337
|
throw new Error("Table rows must have equal cell counts");
|
|
47165
47338
|
const before = readRichNote(args.id);
|
|
@@ -47437,7 +47610,11 @@ function errorResponse(message, cause) {
|
|
|
47437
47610
|
function withErrorHandling(handler, errorPrefix) {
|
|
47438
47611
|
return async (params) => {
|
|
47439
47612
|
try {
|
|
47440
|
-
|
|
47613
|
+
const seconds = params.timeoutSeconds;
|
|
47614
|
+
return runWithCallTimeout(
|
|
47615
|
+
typeof seconds === "number" ? seconds : void 0,
|
|
47616
|
+
() => handler(params)
|
|
47617
|
+
);
|
|
47441
47618
|
} catch (error2) {
|
|
47442
47619
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
47443
47620
|
return errorResponse(`${errorPrefix}: ${message}`, error2);
|
|
@@ -47457,6 +47634,7 @@ var MAX = {
|
|
|
47457
47634
|
TAGS: 100,
|
|
47458
47635
|
BATCH_IDS: 500
|
|
47459
47636
|
};
|
|
47637
|
+
var MAX_CONTENT_FILE_BYTES = 1024 * 1024;
|
|
47460
47638
|
var noteTitleSchema = {
|
|
47461
47639
|
title: external_exports.string().min(1, "Note title is required").max(MAX.TITLE),
|
|
47462
47640
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
@@ -47485,6 +47663,9 @@ function noteIdentifiers(id2) {
|
|
|
47485
47663
|
var expectedContentHashInput = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/, "expectedContentHash must come from get-note-content").describe(
|
|
47486
47664
|
"Revision token returned by get-note-content for this exact ID. The mutation stops if the note changed since that read."
|
|
47487
47665
|
);
|
|
47666
|
+
var timeoutSecondsInput = external_exports.number().int().min(CALL_TIMEOUT_SECONDS.min).max(CALL_TIMEOUT_SECONDS.max).optional().describe(
|
|
47667
|
+
`Per-call timeout in seconds (${CALL_TIMEOUT_SECONDS.min}-${CALL_TIMEOUT_SECONDS.max}) for each Notes.app automation step this call runs, overriding APPLE_NOTES_MCP_TIMEOUT_MS. A timed-out write is uncertain, not failed: read the note by id before any retry.`
|
|
47668
|
+
);
|
|
47488
47669
|
function readExactNoteSnapshot(id2) {
|
|
47489
47670
|
const note = notesManager.getNoteById(id2);
|
|
47490
47671
|
if (!note) return { error: `Note with ID "${id2}" not found` };
|
|
@@ -47498,6 +47679,7 @@ function readExactNoteSnapshot(id2) {
|
|
|
47498
47679
|
const rich = enrichNoteRead(id2, body);
|
|
47499
47680
|
return { note, body, rich, contentHash: richContentHash(body, rich) };
|
|
47500
47681
|
}
|
|
47682
|
+
var NOT_DELETED_MESSAGE = "Notes.app accepted the delete, but the note is still in its original folder, so it was not moved to Recently Deleted. Nothing was deleted; read the note again before retrying.";
|
|
47501
47683
|
function revisionConflictMessage(title) {
|
|
47502
47684
|
return `Note "${title}" changed after it was read. Read it again and review the newer version before retrying.`;
|
|
47503
47685
|
}
|
|
@@ -47521,11 +47703,17 @@ registerTool(
|
|
|
47521
47703
|
description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field). A 'folder' must already exist \u2014 create-folder first (it is idempotent), since this tool does not create it.",
|
|
47522
47704
|
inputSchema: {
|
|
47523
47705
|
title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
|
|
47524
|
-
content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).describe(
|
|
47525
|
-
'Note body. AppleScript cannot create true Apple Notes checklists \u2014 `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with \u21E7\u2318L.'
|
|
47706
|
+
content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).optional().describe(
|
|
47707
|
+
'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. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with \u21E7\u2318L.'
|
|
47708
|
+
),
|
|
47709
|
+
contentPath: external_exports.string().min(1).max(MAX.SAVE_PATH).optional().describe(
|
|
47710
|
+
`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.`
|
|
47526
47711
|
),
|
|
47527
47712
|
format: external_exports.enum(["plaintext", "html", "markdown"]).optional().default("plaintext").describe(
|
|
47528
|
-
"Content format: 'plaintext' (default), 'html' for rich formatting, or 'markdown'
|
|
47713
|
+
"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."
|
|
47714
|
+
),
|
|
47715
|
+
markdownRoute: external_exports.enum(["shortcut", "html"]).optional().default("shortcut").describe(
|
|
47716
|
+
"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."
|
|
47529
47717
|
),
|
|
47530
47718
|
tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
|
|
47531
47719
|
"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."
|
|
@@ -47535,7 +47723,8 @@ registerTool(
|
|
|
47535
47723
|
),
|
|
47536
47724
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
47537
47725
|
"Account name (defaults to the account Notes.app itself reports as default). Matched exactly, or by a unique prefix; an ambiguous prefix is refused. Must be an account Notes.app already has configured \u2014 see list-accounts."
|
|
47538
|
-
)
|
|
47726
|
+
),
|
|
47727
|
+
timeoutSeconds: timeoutSecondsInput
|
|
47539
47728
|
},
|
|
47540
47729
|
outputSchema: {
|
|
47541
47730
|
ok: external_exports.boolean().optional(),
|
|
@@ -47544,10 +47733,66 @@ registerTool(
|
|
|
47544
47733
|
folder: external_exports.string().optional(),
|
|
47545
47734
|
account: external_exports.string().optional(),
|
|
47546
47735
|
contentHash: external_exports.string().optional(),
|
|
47547
|
-
verified: external_exports.boolean().optional()
|
|
47736
|
+
verified: external_exports.boolean().optional(),
|
|
47737
|
+
strippedDuplicateTitle: external_exports.boolean().optional(),
|
|
47738
|
+
taskItemsRendered: external_exports.number().optional()
|
|
47548
47739
|
}
|
|
47549
47740
|
},
|
|
47550
|
-
withErrorHandling((
|
|
47741
|
+
withErrorHandling((params) => {
|
|
47742
|
+
const {
|
|
47743
|
+
title,
|
|
47744
|
+
contentPath,
|
|
47745
|
+
format = "plaintext",
|
|
47746
|
+
markdownRoute = "shortcut",
|
|
47747
|
+
tags = [],
|
|
47748
|
+
folder,
|
|
47749
|
+
account
|
|
47750
|
+
} = params;
|
|
47751
|
+
if (params.content === void 0 === (contentPath === void 0))
|
|
47752
|
+
return errorResponse("Provide exactly one of content or contentPath");
|
|
47753
|
+
let content = params.content ?? readAllowedTextFile(contentPath, MAX_CONTENT_FILE_BYTES);
|
|
47754
|
+
if (format !== "markdown" && markdownRoute !== "shortcut")
|
|
47755
|
+
return errorResponse('markdownRoute applies to format "markdown" only');
|
|
47756
|
+
let strippedDuplicateTitle = false;
|
|
47757
|
+
if (format === "markdown") {
|
|
47758
|
+
const stripped = stripDuplicateTitleHeading(content, title);
|
|
47759
|
+
strippedDuplicateTitle = stripped.stripped;
|
|
47760
|
+
content = stripped.content;
|
|
47761
|
+
if (!content.trim())
|
|
47762
|
+
return errorResponse(
|
|
47763
|
+
"The Markdown holds only the title heading; add body content, or use format 'plaintext' for a title-only note"
|
|
47764
|
+
);
|
|
47765
|
+
}
|
|
47766
|
+
const titleNote = strippedDuplicateTitle ? { strippedDuplicateTitle: true } : {};
|
|
47767
|
+
if (format === "markdown" && markdownRoute === "html") {
|
|
47768
|
+
const taskItemsRendered = countTaskItems(content);
|
|
47769
|
+
const html = appendMarkdownHtml(content, { taskGlyphs: true });
|
|
47770
|
+
const note2 = notesManager.createNote(title, html, tags, folder, account, "html");
|
|
47771
|
+
if (!note2)
|
|
47772
|
+
return errorResponse(
|
|
47773
|
+
`Failed to create note "${title}". Check that the folder and account exist (list-folders, list-accounts) and that this server has Automation access (run the doctor tool).`
|
|
47774
|
+
);
|
|
47775
|
+
const createdBody2 = notesManager.getNoteContentById(note2.id);
|
|
47776
|
+
if (!notesManager.getNoteById(note2.id) || !createdBody2)
|
|
47777
|
+
return errorResponse(
|
|
47778
|
+
`A note may have been created, but its exact ID could not be verified. Do not retry automatically. Returned ID: ${note2.id}`
|
|
47779
|
+
);
|
|
47780
|
+
const glyphNote = taskItemsRendered ? ` ${taskItemsRendered} task item(s) were rendered as visible \u2610 / \u2611 text, not native checklist items.` : "";
|
|
47781
|
+
return successResponse(
|
|
47782
|
+
`Note created from Markdown: "${note2.title}" [id: ${note2.id}]${glyphNote}`,
|
|
47783
|
+
{
|
|
47784
|
+
ok: true,
|
|
47785
|
+
id: note2.id,
|
|
47786
|
+
title: note2.title,
|
|
47787
|
+
folder,
|
|
47788
|
+
account,
|
|
47789
|
+
contentHash: richContentHash(createdBody2, enrichNoteRead(note2.id, createdBody2)),
|
|
47790
|
+
verified: true,
|
|
47791
|
+
taskItemsRendered,
|
|
47792
|
+
...titleNote
|
|
47793
|
+
}
|
|
47794
|
+
);
|
|
47795
|
+
}
|
|
47551
47796
|
if (format === "markdown") {
|
|
47552
47797
|
if (account)
|
|
47553
47798
|
return errorResponse(
|
|
@@ -47559,7 +47804,10 @@ registerTool(
|
|
|
47559
47804
|
);
|
|
47560
47805
|
requireValidated("create-note-markdown");
|
|
47561
47806
|
const result = createMarkdownNote(notesManager, { title, content, folder });
|
|
47562
|
-
return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`,
|
|
47807
|
+
return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`, {
|
|
47808
|
+
...result,
|
|
47809
|
+
...titleNote
|
|
47810
|
+
});
|
|
47563
47811
|
}
|
|
47564
47812
|
const note = notesManager.createNote(title, content, tags, folder, account, format);
|
|
47565
47813
|
if (!note) {
|
|
@@ -47705,7 +47953,7 @@ registerTool(
|
|
|
47705
47953
|
}
|
|
47706
47954
|
throw error2;
|
|
47707
47955
|
}
|
|
47708
|
-
const
|
|
47956
|
+
const scope2 = `scanned ${result.scanned} of ${result.eligible} notes` + (result.scanTruncated ? `, the most recent ${result.scanLimit}; pass a higher scanLimit to include older notes` : "");
|
|
47709
47957
|
const notes = [];
|
|
47710
47958
|
if (result.truncated) {
|
|
47711
47959
|
notes.push(
|
|
@@ -47721,7 +47969,7 @@ registerTool(
|
|
|
47721
47969
|
|
|
47722
47970
|
${notes.join("\n")}` : "";
|
|
47723
47971
|
if (result.count === 0) {
|
|
47724
|
-
return successResponse(`No notes matched (${
|
|
47972
|
+
return successResponse(`No notes matched (${scope2}).${footer}`, { ...result });
|
|
47725
47973
|
}
|
|
47726
47974
|
const lines = result.notes.map((n) => {
|
|
47727
47975
|
const where = [n.account, n.folder].filter(Boolean).join(" / ");
|
|
@@ -47730,7 +47978,7 @@ ${notes.join("\n")}` : "";
|
|
|
47730
47978
|
return ` - ${n.title}${where ? ` (${where})` : ""}${n.locked ? " [locked]" : ""} [id: ${n.id}]${snippet}`;
|
|
47731
47979
|
}).join("\n");
|
|
47732
47980
|
return successResponse(
|
|
47733
|
-
`Found ${result.matched} matching notes (${
|
|
47981
|
+
`Found ${result.matched} matching notes (${scope2}):
|
|
47734
47982
|
${lines}${footer}`,
|
|
47735
47983
|
{ ...result }
|
|
47736
47984
|
);
|
|
@@ -48277,7 +48525,8 @@ registerTool(
|
|
|
48277
48525
|
newContent: external_exports.string().min(1, "New content is required").max(MAX.CONTENT).describe(
|
|
48278
48526
|
"New note body. AppleScript cannot produce true Apple Notes checklists; checkbox inputs and `- [ ]` markdown do not render as checkable items. Use a plain list and convert in Notes.app with \u21E7\u2318L."
|
|
48279
48527
|
),
|
|
48280
|
-
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting")
|
|
48528
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
48529
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48281
48530
|
},
|
|
48282
48531
|
outputSchema: {
|
|
48283
48532
|
ok: external_exports.boolean().optional(),
|
|
@@ -48488,10 +48737,11 @@ registerTool(
|
|
|
48488
48737
|
content: external_exports.string().min(1, "Content to append is required").max(MAX.CONTENT).describe("Text to append to the note body"),
|
|
48489
48738
|
scopeText: external_exports.string().min(12).max(500).optional().describe("Existing unique phrase required for native append to protected notes"),
|
|
48490
48739
|
position: external_exports.enum(["after", "before"]).optional().default("after").describe(
|
|
48491
|
-
"Where to insert: 'after' appends to the end (default)
|
|
48740
|
+
"Where to insert: 'after' appends to the end (default); 'before' inserts directly below the note's title line, so the title stays first"
|
|
48492
48741
|
),
|
|
48493
48742
|
separator: external_exports.string().max(20).optional().default("\n\n").describe("String placed between existing content and new content (default: two newlines)"),
|
|
48494
|
-
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'")
|
|
48743
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'"),
|
|
48744
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48495
48745
|
},
|
|
48496
48746
|
outputSchema: {
|
|
48497
48747
|
ok: external_exports.boolean().optional(),
|
|
@@ -48580,7 +48830,8 @@ registerTool(
|
|
|
48580
48830
|
description: "Use when: moving one exact note to Recently Deleted after reading and reviewing it.\nReturns: confirmation with the exact id.\nDo not use when: you only have a title or the note changed since review.\nSafety: requires id and expectedContentHash from get-note-content. The body comparison and delete happen in one AppleScript, so a newer edit is preserved.",
|
|
48581
48831
|
inputSchema: {
|
|
48582
48832
|
id: noteIdInput,
|
|
48583
|
-
expectedContentHash: expectedContentHashInput
|
|
48833
|
+
expectedContentHash: expectedContentHashInput,
|
|
48834
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48584
48835
|
},
|
|
48585
48836
|
outputSchema: {
|
|
48586
48837
|
ok: external_exports.boolean().optional(),
|
|
@@ -48600,6 +48851,9 @@ registerTool(
|
|
|
48600
48851
|
if (result.status === "conflict") {
|
|
48601
48852
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
48602
48853
|
}
|
|
48854
|
+
if (result.status === "not-deleted") {
|
|
48855
|
+
return errorResponse(NOT_DELETED_MESSAGE);
|
|
48856
|
+
}
|
|
48603
48857
|
if (result.status !== "deleted") {
|
|
48604
48858
|
return errorResponse(
|
|
48605
48859
|
`The delete result for note "${snapshot.note.title}" is uncertain. Inspect exact ID ${id2} before retrying.`
|
|
@@ -48625,7 +48879,8 @@ registerTool(
|
|
|
48625
48879
|
inputSchema: {
|
|
48626
48880
|
id: noteIdInput,
|
|
48627
48881
|
folder: external_exports.string().min(1, "Destination folder is required").max(MAX.FOLDER),
|
|
48628
|
-
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note/folder")
|
|
48882
|
+
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note/folder"),
|
|
48883
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48629
48884
|
},
|
|
48630
48885
|
outputSchema: {
|
|
48631
48886
|
ok: external_exports.boolean().optional(),
|
|
@@ -49188,6 +49443,8 @@ registerTool(
|
|
|
49188
49443
|
if (result.status === "conflict") {
|
|
49189
49444
|
return { id: id2, success: false, error: revisionConflictMessage(snapshot.note.title) };
|
|
49190
49445
|
}
|
|
49446
|
+
if (result.status === "not-deleted")
|
|
49447
|
+
return { id: id2, success: false, error: NOT_DELETED_MESSAGE };
|
|
49191
49448
|
return { id: id2, success: false, error: "Delete result uncertain; inspect this exact ID" };
|
|
49192
49449
|
});
|
|
49193
49450
|
const succeeded = results.filter((r) => r.success).length;
|
package/package.json
CHANGED