apple-notes-mcp 2.8.30 → 2.8.31
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 +39 -4
- package/build/index.js +239 -67
- 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
|
|
|
@@ -1693,7 +1728,7 @@ All configuration is optional — the server works out of the box. Override beha
|
|
|
1693
1728
|
| `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
1729
|
| `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
1730
|
| `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. |
|
|
1731
|
+
| `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
1732
|
| `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
1733
|
| `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
1734
|
| `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,11 +46738,11 @@ 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
|
|
@@ -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) {
|
|
@@ -46896,9 +46984,9 @@ function registerNativeTagsBridge(server2, manager) {
|
|
|
46896
46984
|
{ id: id2, expectedContentHash, scopeText, tags, title: initial.title },
|
|
46897
46985
|
{
|
|
46898
46986
|
read,
|
|
46899
|
-
candidates: (title,
|
|
46987
|
+
candidates: (title, scope2) => manager.listAccounts().flatMap(
|
|
46900
46988
|
(account) => manager.searchNotes(title, false, account.name).filter(
|
|
46901
|
-
(note) => !note.passwordProtected && manager.getNotePlaintextById(note.id).toLocaleLowerCase().includes(
|
|
46989
|
+
(note) => !note.passwordProtected && manager.getNotePlaintextById(note.id).toLocaleLowerCase().includes(scope2.toLocaleLowerCase())
|
|
46902
46990
|
).map((note) => note.id)
|
|
46903
46991
|
),
|
|
46904
46992
|
run: runNativeTagsShortcut
|
|
@@ -47437,7 +47525,11 @@ function errorResponse(message, cause) {
|
|
|
47437
47525
|
function withErrorHandling(handler, errorPrefix) {
|
|
47438
47526
|
return async (params) => {
|
|
47439
47527
|
try {
|
|
47440
|
-
|
|
47528
|
+
const seconds = params.timeoutSeconds;
|
|
47529
|
+
return runWithCallTimeout(
|
|
47530
|
+
typeof seconds === "number" ? seconds : void 0,
|
|
47531
|
+
() => handler(params)
|
|
47532
|
+
);
|
|
47441
47533
|
} catch (error2) {
|
|
47442
47534
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
47443
47535
|
return errorResponse(`${errorPrefix}: ${message}`, error2);
|
|
@@ -47457,6 +47549,7 @@ var MAX = {
|
|
|
47457
47549
|
TAGS: 100,
|
|
47458
47550
|
BATCH_IDS: 500
|
|
47459
47551
|
};
|
|
47552
|
+
var MAX_CONTENT_FILE_BYTES = 1024 * 1024;
|
|
47460
47553
|
var noteTitleSchema = {
|
|
47461
47554
|
title: external_exports.string().min(1, "Note title is required").max(MAX.TITLE),
|
|
47462
47555
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
@@ -47485,6 +47578,9 @@ function noteIdentifiers(id2) {
|
|
|
47485
47578
|
var expectedContentHashInput = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/, "expectedContentHash must come from get-note-content").describe(
|
|
47486
47579
|
"Revision token returned by get-note-content for this exact ID. The mutation stops if the note changed since that read."
|
|
47487
47580
|
);
|
|
47581
|
+
var timeoutSecondsInput = external_exports.number().int().min(CALL_TIMEOUT_SECONDS.min).max(CALL_TIMEOUT_SECONDS.max).optional().describe(
|
|
47582
|
+
`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.`
|
|
47583
|
+
);
|
|
47488
47584
|
function readExactNoteSnapshot(id2) {
|
|
47489
47585
|
const note = notesManager.getNoteById(id2);
|
|
47490
47586
|
if (!note) return { error: `Note with ID "${id2}" not found` };
|
|
@@ -47498,6 +47594,7 @@ function readExactNoteSnapshot(id2) {
|
|
|
47498
47594
|
const rich = enrichNoteRead(id2, body);
|
|
47499
47595
|
return { note, body, rich, contentHash: richContentHash(body, rich) };
|
|
47500
47596
|
}
|
|
47597
|
+
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
47598
|
function revisionConflictMessage(title) {
|
|
47502
47599
|
return `Note "${title}" changed after it was read. Read it again and review the newer version before retrying.`;
|
|
47503
47600
|
}
|
|
@@ -47521,11 +47618,17 @@ registerTool(
|
|
|
47521
47618
|
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
47619
|
inputSchema: {
|
|
47523
47620
|
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.'
|
|
47621
|
+
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. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with \u21E7\u2318L.'
|
|
47623
|
+
),
|
|
47624
|
+
contentPath: external_exports.string().min(1).max(MAX.SAVE_PATH).optional().describe(
|
|
47625
|
+
`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
47626
|
),
|
|
47527
47627
|
format: external_exports.enum(["plaintext", "html", "markdown"]).optional().default("plaintext").describe(
|
|
47528
|
-
"Content format: 'plaintext' (default), 'html' for rich formatting, or 'markdown'
|
|
47628
|
+
"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
|
+
),
|
|
47630
|
+
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."
|
|
47529
47632
|
),
|
|
47530
47633
|
tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
|
|
47531
47634
|
"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 +47638,8 @@ registerTool(
|
|
|
47535
47638
|
),
|
|
47536
47639
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
47537
47640
|
"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
|
-
)
|
|
47641
|
+
),
|
|
47642
|
+
timeoutSeconds: timeoutSecondsInput
|
|
47539
47643
|
},
|
|
47540
47644
|
outputSchema: {
|
|
47541
47645
|
ok: external_exports.boolean().optional(),
|
|
@@ -47544,10 +47648,66 @@ registerTool(
|
|
|
47544
47648
|
folder: external_exports.string().optional(),
|
|
47545
47649
|
account: external_exports.string().optional(),
|
|
47546
47650
|
contentHash: external_exports.string().optional(),
|
|
47547
|
-
verified: external_exports.boolean().optional()
|
|
47651
|
+
verified: external_exports.boolean().optional(),
|
|
47652
|
+
strippedDuplicateTitle: external_exports.boolean().optional(),
|
|
47653
|
+
taskItemsRendered: external_exports.number().optional()
|
|
47548
47654
|
}
|
|
47549
47655
|
},
|
|
47550
|
-
withErrorHandling((
|
|
47656
|
+
withErrorHandling((params) => {
|
|
47657
|
+
const {
|
|
47658
|
+
title,
|
|
47659
|
+
contentPath,
|
|
47660
|
+
format = "plaintext",
|
|
47661
|
+
markdownRoute = "shortcut",
|
|
47662
|
+
tags = [],
|
|
47663
|
+
folder,
|
|
47664
|
+
account
|
|
47665
|
+
} = params;
|
|
47666
|
+
if (params.content === void 0 === (contentPath === void 0))
|
|
47667
|
+
return errorResponse("Provide exactly one of content or contentPath");
|
|
47668
|
+
let content = params.content ?? readAllowedTextFile(contentPath, MAX_CONTENT_FILE_BYTES);
|
|
47669
|
+
if (format !== "markdown" && markdownRoute !== "shortcut")
|
|
47670
|
+
return errorResponse('markdownRoute applies to format "markdown" only');
|
|
47671
|
+
let strippedDuplicateTitle = false;
|
|
47672
|
+
if (format === "markdown") {
|
|
47673
|
+
const stripped = stripDuplicateTitleHeading(content, title);
|
|
47674
|
+
strippedDuplicateTitle = stripped.stripped;
|
|
47675
|
+
content = stripped.content;
|
|
47676
|
+
if (!content.trim())
|
|
47677
|
+
return errorResponse(
|
|
47678
|
+
"The Markdown holds only the title heading; add body content, or use format 'plaintext' for a title-only note"
|
|
47679
|
+
);
|
|
47680
|
+
}
|
|
47681
|
+
const titleNote = strippedDuplicateTitle ? { strippedDuplicateTitle: true } : {};
|
|
47682
|
+
if (format === "markdown" && markdownRoute === "html") {
|
|
47683
|
+
const taskItemsRendered = countTaskItems(content);
|
|
47684
|
+
const html = appendMarkdownHtml(content, { taskGlyphs: true });
|
|
47685
|
+
const note2 = notesManager.createNote(title, html, tags, folder, account, "html");
|
|
47686
|
+
if (!note2)
|
|
47687
|
+
return errorResponse(
|
|
47688
|
+
`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).`
|
|
47689
|
+
);
|
|
47690
|
+
const createdBody2 = notesManager.getNoteContentById(note2.id);
|
|
47691
|
+
if (!notesManager.getNoteById(note2.id) || !createdBody2)
|
|
47692
|
+
return errorResponse(
|
|
47693
|
+
`A note may have been created, but its exact ID could not be verified. Do not retry automatically. Returned ID: ${note2.id}`
|
|
47694
|
+
);
|
|
47695
|
+
const glyphNote = taskItemsRendered ? ` ${taskItemsRendered} task item(s) were rendered as visible \u2610 / \u2611 text, not native checklist items.` : "";
|
|
47696
|
+
return successResponse(
|
|
47697
|
+
`Note created from Markdown: "${note2.title}" [id: ${note2.id}]${glyphNote}`,
|
|
47698
|
+
{
|
|
47699
|
+
ok: true,
|
|
47700
|
+
id: note2.id,
|
|
47701
|
+
title: note2.title,
|
|
47702
|
+
folder,
|
|
47703
|
+
account,
|
|
47704
|
+
contentHash: richContentHash(createdBody2, enrichNoteRead(note2.id, createdBody2)),
|
|
47705
|
+
verified: true,
|
|
47706
|
+
taskItemsRendered,
|
|
47707
|
+
...titleNote
|
|
47708
|
+
}
|
|
47709
|
+
);
|
|
47710
|
+
}
|
|
47551
47711
|
if (format === "markdown") {
|
|
47552
47712
|
if (account)
|
|
47553
47713
|
return errorResponse(
|
|
@@ -47559,7 +47719,10 @@ registerTool(
|
|
|
47559
47719
|
);
|
|
47560
47720
|
requireValidated("create-note-markdown");
|
|
47561
47721
|
const result = createMarkdownNote(notesManager, { title, content, folder });
|
|
47562
|
-
return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`,
|
|
47722
|
+
return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`, {
|
|
47723
|
+
...result,
|
|
47724
|
+
...titleNote
|
|
47725
|
+
});
|
|
47563
47726
|
}
|
|
47564
47727
|
const note = notesManager.createNote(title, content, tags, folder, account, format);
|
|
47565
47728
|
if (!note) {
|
|
@@ -47705,7 +47868,7 @@ registerTool(
|
|
|
47705
47868
|
}
|
|
47706
47869
|
throw error2;
|
|
47707
47870
|
}
|
|
47708
|
-
const
|
|
47871
|
+
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
47872
|
const notes = [];
|
|
47710
47873
|
if (result.truncated) {
|
|
47711
47874
|
notes.push(
|
|
@@ -47721,7 +47884,7 @@ registerTool(
|
|
|
47721
47884
|
|
|
47722
47885
|
${notes.join("\n")}` : "";
|
|
47723
47886
|
if (result.count === 0) {
|
|
47724
|
-
return successResponse(`No notes matched (${
|
|
47887
|
+
return successResponse(`No notes matched (${scope2}).${footer}`, { ...result });
|
|
47725
47888
|
}
|
|
47726
47889
|
const lines = result.notes.map((n) => {
|
|
47727
47890
|
const where = [n.account, n.folder].filter(Boolean).join(" / ");
|
|
@@ -47730,7 +47893,7 @@ ${notes.join("\n")}` : "";
|
|
|
47730
47893
|
return ` - ${n.title}${where ? ` (${where})` : ""}${n.locked ? " [locked]" : ""} [id: ${n.id}]${snippet}`;
|
|
47731
47894
|
}).join("\n");
|
|
47732
47895
|
return successResponse(
|
|
47733
|
-
`Found ${result.matched} matching notes (${
|
|
47896
|
+
`Found ${result.matched} matching notes (${scope2}):
|
|
47734
47897
|
${lines}${footer}`,
|
|
47735
47898
|
{ ...result }
|
|
47736
47899
|
);
|
|
@@ -48277,7 +48440,8 @@ registerTool(
|
|
|
48277
48440
|
newContent: external_exports.string().min(1, "New content is required").max(MAX.CONTENT).describe(
|
|
48278
48441
|
"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
48442
|
),
|
|
48280
|
-
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting")
|
|
48443
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
48444
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48281
48445
|
},
|
|
48282
48446
|
outputSchema: {
|
|
48283
48447
|
ok: external_exports.boolean().optional(),
|
|
@@ -48488,10 +48652,11 @@ registerTool(
|
|
|
48488
48652
|
content: external_exports.string().min(1, "Content to append is required").max(MAX.CONTENT).describe("Text to append to the note body"),
|
|
48489
48653
|
scopeText: external_exports.string().min(12).max(500).optional().describe("Existing unique phrase required for native append to protected notes"),
|
|
48490
48654
|
position: external_exports.enum(["after", "before"]).optional().default("after").describe(
|
|
48491
|
-
"Where to insert: 'after' appends to the end (default)
|
|
48655
|
+
"Where to insert: 'after' appends to the end (default); 'before' inserts directly below the note's title line, so the title stays first"
|
|
48492
48656
|
),
|
|
48493
48657
|
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'")
|
|
48658
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'"),
|
|
48659
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48495
48660
|
},
|
|
48496
48661
|
outputSchema: {
|
|
48497
48662
|
ok: external_exports.boolean().optional(),
|
|
@@ -48580,7 +48745,8 @@ registerTool(
|
|
|
48580
48745
|
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
48746
|
inputSchema: {
|
|
48582
48747
|
id: noteIdInput,
|
|
48583
|
-
expectedContentHash: expectedContentHashInput
|
|
48748
|
+
expectedContentHash: expectedContentHashInput,
|
|
48749
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48584
48750
|
},
|
|
48585
48751
|
outputSchema: {
|
|
48586
48752
|
ok: external_exports.boolean().optional(),
|
|
@@ -48600,6 +48766,9 @@ registerTool(
|
|
|
48600
48766
|
if (result.status === "conflict") {
|
|
48601
48767
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
48602
48768
|
}
|
|
48769
|
+
if (result.status === "not-deleted") {
|
|
48770
|
+
return errorResponse(NOT_DELETED_MESSAGE);
|
|
48771
|
+
}
|
|
48603
48772
|
if (result.status !== "deleted") {
|
|
48604
48773
|
return errorResponse(
|
|
48605
48774
|
`The delete result for note "${snapshot.note.title}" is uncertain. Inspect exact ID ${id2} before retrying.`
|
|
@@ -48625,7 +48794,8 @@ registerTool(
|
|
|
48625
48794
|
inputSchema: {
|
|
48626
48795
|
id: noteIdInput,
|
|
48627
48796
|
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")
|
|
48797
|
+
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note/folder"),
|
|
48798
|
+
timeoutSeconds: timeoutSecondsInput
|
|
48629
48799
|
},
|
|
48630
48800
|
outputSchema: {
|
|
48631
48801
|
ok: external_exports.boolean().optional(),
|
|
@@ -49188,6 +49358,8 @@ registerTool(
|
|
|
49188
49358
|
if (result.status === "conflict") {
|
|
49189
49359
|
return { id: id2, success: false, error: revisionConflictMessage(snapshot.note.title) };
|
|
49190
49360
|
}
|
|
49361
|
+
if (result.status === "not-deleted")
|
|
49362
|
+
return { id: id2, success: false, error: NOT_DELETED_MESSAGE };
|
|
49191
49363
|
return { id: id2, success: false, error: "Delete result uncertain; inspect this exact ID" };
|
|
49192
49364
|
});
|
|
49193
49365
|
const succeeded = results.filter((r) => r.success).length;
|
package/package.json
CHANGED