apple-notes-mcp 2.9.12 → 2.9.13
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 +5 -3
- package/build/index.js +169 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ Creates a new note in Apple Notes.
|
|
|
257
257
|
| `content` | string | One of `content`/`contentPath` | The body content of the note (do not repeat the title here) |
|
|
258
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 |
|
|
259
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"` |
|
|
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
|
+
| `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). A smart folder is refused (see [`move-note`](#move-note)). Defaults to account root |
|
|
261
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) |
|
|
262
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
263
|
| `markdownRoute` | string | No | With `format: "markdown"` only: `"shortcut"` (default) or `"html"`. See [Markdown through HTML](#markdown-through-html) |
|
|
@@ -1032,6 +1032,8 @@ such a note for good, do it in Notes.app.
|
|
|
1032
1032
|
|
|
1033
1033
|
Moves a note to a different folder. The note is relocated in place via Notes.app's native `move`, so its id, creation date, and all embedded attachments (files, images, scans, PDFs, audio) are preserved. The destination folder must already exist — create it first with [`create-folder`](#create-folder).
|
|
1034
1034
|
|
|
1035
|
+
A smart folder is never a destination: it only gathers notes by its rules, and Notes.app would move the note to Recently Deleted or store a created note where no folder shows it. `create-note`, `create-note-with-attachment`, `move-note`, `batch-move-notes`, and `create-folder` refuse one before writing anything, with `code: "unsupported"`, `committed: false`, and `reason: "smart_folder_destination"`. An ordinary folder that shares a smart folder's name is still found. Detection reads the NoteStore database, so it needs Full Disk Access; without it, destinations resolve as before.
|
|
1036
|
+
|
|
1035
1037
|
| Parameter | Type | Required | Description |
|
|
1036
1038
|
|-----------|------|----------|-------------|
|
|
1037
1039
|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
|
|
@@ -1402,7 +1404,7 @@ Creates a new folder, including a whole nested hierarchy in one call.
|
|
|
1402
1404
|
|
|
1403
1405
|
| Parameter | Type | Required | Description |
|
|
1404
1406
|
|-----------|------|----------|-------------|
|
|
1405
|
-
| `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped |
|
|
1407
|
+
| `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped. A segment that names a smart folder is refused before anything is created |
|
|
1406
1408
|
| `account` | string | No | Account to create folder in (defaults to Notes.app's default account; exact or unique-prefix match) |
|
|
1407
1409
|
|
|
1408
1410
|
**Example:**
|
|
@@ -1590,7 +1592,7 @@ Moves multiple notes to a folder.
|
|
|
1590
1592
|
| Parameter | Type | Required | Description |
|
|
1591
1593
|
|-----------|------|----------|-------------|
|
|
1592
1594
|
| `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
|
|
1593
|
-
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder) |
|
|
1595
|
+
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder). A smart folder is refused for the whole call before any note moves |
|
|
1594
1596
|
| `account` | string | No | Account containing the folder |
|
|
1595
1597
|
|
|
1596
1598
|
**Returns:** Summary of successes and failures. Each success is reported only
|
package/build/index.js
CHANGED
|
@@ -39588,6 +39588,77 @@ function getChecklistItems(noteId3) {
|
|
|
39588
39588
|
return { items };
|
|
39589
39589
|
}
|
|
39590
39590
|
|
|
39591
|
+
// src/utils/errorCodes.ts
|
|
39592
|
+
var CodedError = class extends Error {
|
|
39593
|
+
envelope;
|
|
39594
|
+
constructor(message, envelope) {
|
|
39595
|
+
super(message);
|
|
39596
|
+
this.name = "CodedError";
|
|
39597
|
+
this.envelope = envelope;
|
|
39598
|
+
}
|
|
39599
|
+
};
|
|
39600
|
+
var RULES = [
|
|
39601
|
+
{ code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
|
|
39602
|
+
{
|
|
39603
|
+
code: "verification_failed",
|
|
39604
|
+
pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
|
|
39605
|
+
},
|
|
39606
|
+
{
|
|
39607
|
+
code: "revision_conflict",
|
|
39608
|
+
pattern: /changed after it was read|revision changed|(?:pinned|note) state changed|changed during (?:read|preflight)|changed; read (?:it|the note) again|rich text do not match/i
|
|
39609
|
+
},
|
|
39610
|
+
{ code: "full_disk_access_missing", pattern: /Full Disk Access/i },
|
|
39611
|
+
{
|
|
39612
|
+
code: "shortcut_not_installed",
|
|
39613
|
+
pattern: /(?:Install|Import) the supplied|no such shortcut|Run apple-notes-mcp setup/i
|
|
39614
|
+
},
|
|
39615
|
+
{
|
|
39616
|
+
code: "not_found",
|
|
39617
|
+
pattern: /not found|does not exist|\bNo \w+ found\b|does not contain any checklist items|Can[’']t get (?:note|folder|account|attachment)|Cannot find note|Scope is absent/i
|
|
39618
|
+
},
|
|
39619
|
+
{ code: "ambiguous", pattern: /ambiguous|more than one|exactly once|Duplicate note IDs/i },
|
|
39620
|
+
{
|
|
39621
|
+
code: "notes_unavailable",
|
|
39622
|
+
pattern: /Notes\.app is (?:not responding|busy)|Lost connection to Notes|isn't running/i
|
|
39623
|
+
},
|
|
39624
|
+
{
|
|
39625
|
+
code: "unsupported",
|
|
39626
|
+
pattern: /password-protected|Locked notes|unsupported|not supported|is blocked|blocked because|has not passed live|refuses to sign|No (?:supported )?background|only one where|Real Notes link unavailable/i
|
|
39627
|
+
},
|
|
39628
|
+
{
|
|
39629
|
+
code: "validation_error",
|
|
39630
|
+
pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
|
|
39631
|
+
}
|
|
39632
|
+
];
|
|
39633
|
+
var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
|
|
39634
|
+
var WRITE_ACCEPTED = /\baccepted (?:the|an) \w+, but/i;
|
|
39635
|
+
function classifyError(message, cause) {
|
|
39636
|
+
if (cause instanceof CodedError) return { ...cause.envelope };
|
|
39637
|
+
const text2 = cause instanceof Error && cause.message && !message.includes(cause.message) ? `${message}
|
|
39638
|
+
${cause.message}` : message;
|
|
39639
|
+
const timeoutCause = typeof cause === "object" && cause !== null && cause.code === "ETIMEDOUT";
|
|
39640
|
+
let code = "operation_failed";
|
|
39641
|
+
if (timeoutCause) code = "timeout_indeterminate";
|
|
39642
|
+
else if (isPermissionDenied(text2)) code = "permission_denied";
|
|
39643
|
+
else code = RULES.find((rule) => rule.pattern.test(text2))?.code ?? "operation_failed";
|
|
39644
|
+
const envelope = { code };
|
|
39645
|
+
if (code === "timeout_indeterminate" || code === "verification_failed") {
|
|
39646
|
+
envelope.indeterminate = true;
|
|
39647
|
+
if (WRITE_ACCEPTED.test(text2)) envelope.committed = true;
|
|
39648
|
+
} else if (code === "revision_conflict" || NOTHING_WRITTEN.test(text2)) {
|
|
39649
|
+
envelope.committed = false;
|
|
39650
|
+
envelope.indeterminate = false;
|
|
39651
|
+
}
|
|
39652
|
+
return envelope;
|
|
39653
|
+
}
|
|
39654
|
+
function errorResult(message, cause) {
|
|
39655
|
+
return {
|
|
39656
|
+
content: [{ type: "text", text: message }],
|
|
39657
|
+
structuredContent: classifyError(message, cause),
|
|
39658
|
+
isError: true
|
|
39659
|
+
};
|
|
39660
|
+
}
|
|
39661
|
+
|
|
39591
39662
|
// src/utils/inlineImages.ts
|
|
39592
39663
|
var DEFAULT_MAX_INLINE_IMAGE_BYTES = 256 * 1024;
|
|
39593
39664
|
function maxInlineImageBytes(env = process.env) {
|
|
@@ -42358,6 +42429,24 @@ function buildFolderReference(folderPath) {
|
|
|
42358
42429
|
}
|
|
42359
42430
|
var AS_ACCOUNT_REF = "__acctRef";
|
|
42360
42431
|
var LIVE_FOLDER_NOT_FOUND = "Folder not found";
|
|
42432
|
+
var SMART_FOLDER_DESTINATION = "SMART_FOLDER_DESTINATION";
|
|
42433
|
+
function readSmartFolderIds(read = readSmartFolders) {
|
|
42434
|
+
return (read().folders ?? []).map((folder) => folder.id).filter((id2) => FOLDER_ID_PATTERN.test(id2));
|
|
42435
|
+
}
|
|
42436
|
+
function smartFolderDestinationError(folderPath) {
|
|
42437
|
+
return new CodedError(
|
|
42438
|
+
`Refused: "${folderPath}" is a smart folder. Smart folders only gather notes by their rules and cannot hold notes or folders; choose an ordinary folder (list-folders). Nothing was changed`,
|
|
42439
|
+
{
|
|
42440
|
+
code: "unsupported",
|
|
42441
|
+
committed: false,
|
|
42442
|
+
indeterminate: false,
|
|
42443
|
+
reason: "smart_folder_destination"
|
|
42444
|
+
}
|
|
42445
|
+
);
|
|
42446
|
+
}
|
|
42447
|
+
function throwIfSmartFolderDestination(error2, folderPath) {
|
|
42448
|
+
if (error2?.includes(SMART_FOLDER_DESTINATION)) throw smartFolderDestinationError(folderPath);
|
|
42449
|
+
}
|
|
42361
42450
|
function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
42362
42451
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) throw new Error("Invalid AppleScript variable");
|
|
42363
42452
|
buildFolderReference(folderPath);
|
|
@@ -42366,10 +42455,26 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42366
42455
|
const cid = `${varName}_cid`;
|
|
42367
42456
|
const any = `${varName}_any`;
|
|
42368
42457
|
const parent = `${varName}_p`;
|
|
42369
|
-
const
|
|
42370
|
-
const
|
|
42458
|
+
const quotedPath = escapePlainStringForAppleScript(folderPath);
|
|
42459
|
+
const notFound = `error "${LIVE_FOLDER_NOT_FOUND}: ${quotedPath}" number -1728`;
|
|
42460
|
+
const excluded = (opts.excludeFolderIds ?? []).filter((id2) => FOLDER_ID_PATTERN.test(id2));
|
|
42461
|
+
const skipIds = `${varName}_sx`;
|
|
42462
|
+
const skipped = `${varName}_sh`;
|
|
42463
|
+
const candidateTest = excluded.length ? [
|
|
42464
|
+
` if ${skipIds} contains ${cid} then`,
|
|
42465
|
+
` set ${skipped} to true`,
|
|
42466
|
+
` else if exists folder id ${cid} then`
|
|
42467
|
+
] : [` if exists folder id ${cid} then`];
|
|
42468
|
+
const unresolved = excluded.length ? [
|
|
42469
|
+
`if ${varName} is missing value then`,
|
|
42470
|
+
` if ${skipped} then error "${SMART_FOLDER_DESTINATION}: ${quotedPath}" number -1728`,
|
|
42471
|
+
` ${notFound}`,
|
|
42472
|
+
`end if`
|
|
42473
|
+
] : [`if ${varName} is missing value then ${notFound}`];
|
|
42474
|
+
const lines = excluded.length ? [`set ${skipIds} to {${excluded.map((id2) => `"${id2}"`).join(", ")}}`] : [];
|
|
42371
42475
|
parts.forEach((part, i) => {
|
|
42372
42476
|
const name = escapePlainStringForAppleScript(part);
|
|
42477
|
+
if (excluded.length) lines.push(`set ${skipped} to false`);
|
|
42373
42478
|
if (i === 0) {
|
|
42374
42479
|
lines.push(
|
|
42375
42480
|
`set ${varName} to missing value`,
|
|
@@ -42377,7 +42482,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42377
42482
|
`repeat with ${c} in (folders of ${AS_ACCOUNT_REF} whose name is "${name}")`,
|
|
42378
42483
|
` try`,
|
|
42379
42484
|
` set ${cid} to id of ${c}`,
|
|
42380
|
-
|
|
42485
|
+
...candidateTest,
|
|
42381
42486
|
` if class of (container of ${c}) is not folder then`,
|
|
42382
42487
|
` set ${varName} to folder id ${cid}`,
|
|
42383
42488
|
` exit repeat`,
|
|
@@ -42397,7 +42502,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42397
42502
|
`repeat with ${c} in (folders of ${parent} whose name is "${name}")`,
|
|
42398
42503
|
` try`,
|
|
42399
42504
|
` set ${cid} to id of ${c}`,
|
|
42400
|
-
|
|
42505
|
+
...candidateTest,
|
|
42401
42506
|
` set ${varName} to folder id ${cid}`,
|
|
42402
42507
|
` exit repeat`,
|
|
42403
42508
|
` end if`,
|
|
@@ -42405,7 +42510,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42405
42510
|
`end repeat`
|
|
42406
42511
|
);
|
|
42407
42512
|
}
|
|
42408
|
-
lines.push(
|
|
42513
|
+
lines.push(...unresolved);
|
|
42409
42514
|
});
|
|
42410
42515
|
return lines.join("\n");
|
|
42411
42516
|
}
|
|
@@ -42666,7 +42771,9 @@ var AppleNotesManager = class {
|
|
|
42666
42771
|
const safeBody = escapeHtmlForAppleScript(`<h1>${htmlTitle}</h1>${bodyContent}`);
|
|
42667
42772
|
let createCommand;
|
|
42668
42773
|
if (folder) {
|
|
42669
|
-
createCommand = `${buildLiveFolderResolution(folder, "__folder"
|
|
42774
|
+
createCommand = `${buildLiveFolderResolution(folder, "__folder", {
|
|
42775
|
+
excludeFolderIds: this.smartFolderIds()
|
|
42776
|
+
})}
|
|
42670
42777
|
make new note at __folder with properties {body:"${safeBody}"}`;
|
|
42671
42778
|
} else {
|
|
42672
42779
|
createCommand = `
|
|
@@ -42678,6 +42785,7 @@ var AppleNotesManager = class {
|
|
|
42678
42785
|
const result = executeMutationAppleScript(script);
|
|
42679
42786
|
if (!result.success) {
|
|
42680
42787
|
throwIfAccountResolutionFailed(result.error);
|
|
42788
|
+
if (folder) throwIfSmartFolderDestination(result.error, folder);
|
|
42681
42789
|
console.error(`Failed to create note "${title}":`, result.error);
|
|
42682
42790
|
return null;
|
|
42683
42791
|
}
|
|
@@ -43282,6 +43390,35 @@ var AppleNotesManager = class {
|
|
|
43282
43390
|
return (resultList as text)${trashSuffix}
|
|
43283
43391
|
`;
|
|
43284
43392
|
}
|
|
43393
|
+
/**
|
|
43394
|
+
* Smart folder ids that a destination folder must never resolve to. Read
|
|
43395
|
+
* fresh on every write (a smart folder can be added at any time); empty
|
|
43396
|
+
* without Full Disk Access. See {@link readSmartFolderIds}.
|
|
43397
|
+
*/
|
|
43398
|
+
smartFolderIds() {
|
|
43399
|
+
return readSmartFolderIds();
|
|
43400
|
+
}
|
|
43401
|
+
/**
|
|
43402
|
+
* Read-only preflight: throws the smart-folder refusal when `folderPath`
|
|
43403
|
+
* resolves in `account` only to a smart folder. For write paths that create
|
|
43404
|
+
* something before they reach their own destination check (the Markdown
|
|
43405
|
+
* bridge creates first and moves second). Other failures are left to the
|
|
43406
|
+
* write itself, which reports them as before.
|
|
43407
|
+
*
|
|
43408
|
+
* @param folderPath - Destination folder path (list-folders syntax)
|
|
43409
|
+
* @param account - Account to resolve in (defaults to Notes' default)
|
|
43410
|
+
*/
|
|
43411
|
+
assertNotSmartFolderDestination(folderPath, account) {
|
|
43412
|
+
const excludeFolderIds = this.smartFolderIds();
|
|
43413
|
+
if (excludeFolderIds.length === 0) return;
|
|
43414
|
+
const script = buildAccountScopedScript(
|
|
43415
|
+
{ account: this.resolveAccount(account) },
|
|
43416
|
+
`${buildLiveFolderResolution(folderPath, "__folder", { excludeFolderIds })}
|
|
43417
|
+
return id of __folder`
|
|
43418
|
+
);
|
|
43419
|
+
const result = executeAppleScript(script);
|
|
43420
|
+
if (!result.success) throwIfSmartFolderDestination(result.error, folderPath);
|
|
43421
|
+
}
|
|
43285
43422
|
/**
|
|
43286
43423
|
* Lists every smart folder with its decoded query, read-only.
|
|
43287
43424
|
*
|
|
@@ -43823,37 +43960,40 @@ var AppleNotesManager = class {
|
|
|
43823
43960
|
console.error(`Invalid folder name: "${name}"`);
|
|
43824
43961
|
return null;
|
|
43825
43962
|
}
|
|
43963
|
+
const smart = { rootOnly: true, excludeFolderIds: this.smartFolderIds() };
|
|
43826
43964
|
for (let i = 0; i < parts.length; i++) {
|
|
43827
43965
|
const currentPath = parts.slice(0, i + 1).map((p) => escapeFolderName(p)).join("/");
|
|
43828
43966
|
const checkScript = buildAccountScopedScript(
|
|
43829
43967
|
{ account: targetAccount },
|
|
43830
|
-
`${buildLiveFolderResolution(currentPath, "__folder",
|
|
43968
|
+
`${buildLiveFolderResolution(currentPath, "__folder", smart)}
|
|
43831
43969
|
return id of __folder`
|
|
43832
43970
|
);
|
|
43833
43971
|
const checkResult = executeAppleScript(checkScript);
|
|
43834
43972
|
if (checkResult.success) {
|
|
43835
43973
|
continue;
|
|
43836
43974
|
}
|
|
43975
|
+
throwIfSmartFolderDestination(checkResult.error, currentPath);
|
|
43837
43976
|
const segmentName = escapePlainStringForAppleScript(parts[i]);
|
|
43838
43977
|
let createCommand;
|
|
43839
43978
|
if (i === 0) {
|
|
43840
43979
|
createCommand = `make new folder with properties {name:"${segmentName}"}`;
|
|
43841
43980
|
} else {
|
|
43842
43981
|
const parentPath = parts.slice(0, i).map((p) => escapeFolderName(p)).join("/");
|
|
43843
|
-
createCommand = `${buildLiveFolderResolution(parentPath, "__parent",
|
|
43982
|
+
createCommand = `${buildLiveFolderResolution(parentPath, "__parent", smart)}
|
|
43844
43983
|
make new folder at __parent with properties {name:"${segmentName}"}`;
|
|
43845
43984
|
}
|
|
43846
43985
|
const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
|
|
43847
43986
|
const result = executeMutationAppleScript(script);
|
|
43848
43987
|
if (!result.success) {
|
|
43849
43988
|
throwIfAccountResolutionFailed(result.error);
|
|
43989
|
+
throwIfSmartFolderDestination(result.error, name);
|
|
43850
43990
|
console.error(`Failed to create folder "${name}":`, result.error);
|
|
43851
43991
|
return null;
|
|
43852
43992
|
}
|
|
43853
43993
|
}
|
|
43854
43994
|
const idScript = buildAccountScopedScript(
|
|
43855
43995
|
{ account: targetAccount },
|
|
43856
|
-
`${buildLiveFolderResolution(name, "__folder",
|
|
43996
|
+
`${buildLiveFolderResolution(name, "__folder", smart)}
|
|
43857
43997
|
return (id of __folder) & ${AS_FIELD_SEP} & (name of it)`
|
|
43858
43998
|
);
|
|
43859
43999
|
const idResult = executeAppleScript(idScript);
|
|
@@ -43912,7 +44052,9 @@ var AppleNotesManager = class {
|
|
|
43912
44052
|
moveNoteById(id2, destinationFolder, account, scope2) {
|
|
43913
44053
|
const targetAccount = this.resolveAccount(account);
|
|
43914
44054
|
const safeId = sanitizeNoteId(id2);
|
|
43915
|
-
const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder"
|
|
44055
|
+
const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder", {
|
|
44056
|
+
excludeFolderIds: this.smartFolderIds()
|
|
44057
|
+
});
|
|
43916
44058
|
const moveCommand = `
|
|
43917
44059
|
${buildAccountResolution(targetAccount)}
|
|
43918
44060
|
${destFolderSetup}
|
|
@@ -43927,6 +44069,7 @@ var AppleNotesManager = class {
|
|
|
43927
44069
|
const result = executeMutationAppleScript(script);
|
|
43928
44070
|
if (!result.success) {
|
|
43929
44071
|
throwIfAccountResolutionFailed(result.error);
|
|
44072
|
+
throwIfSmartFolderDestination(result.error, destinationFolder);
|
|
43930
44073
|
console.error(
|
|
43931
44074
|
`Cannot move note to "${destinationFolder}" (folder may not exist):`,
|
|
43932
44075
|
result.error
|
|
@@ -44821,7 +44964,9 @@ var AppleNotesManager = class {
|
|
|
44821
44964
|
batchMoveNotes(ids, folder, account) {
|
|
44822
44965
|
if (ids.length === 0) return [];
|
|
44823
44966
|
const targetAccount = this.resolveAccount(account);
|
|
44824
|
-
const destFolderSetup = buildLiveFolderResolution(folder, "destFolder"
|
|
44967
|
+
const destFolderSetup = buildLiveFolderResolution(folder, "destFolder", {
|
|
44968
|
+
excludeFolderIds: this.smartFolderIds()
|
|
44969
|
+
});
|
|
44825
44970
|
const results = new Array(ids.length);
|
|
44826
44971
|
const runnable = [];
|
|
44827
44972
|
ids.forEach((id2, i) => {
|
|
@@ -44877,6 +45022,7 @@ var AppleNotesManager = class {
|
|
|
44877
45022
|
const res = executeMutationAppleScript(script);
|
|
44878
45023
|
if (!res.success) {
|
|
44879
45024
|
throwIfAccountResolutionFailed(res.error);
|
|
45025
|
+
throwIfSmartFolderDestination(res.error, folder);
|
|
44880
45026
|
for (const r of runnable) {
|
|
44881
45027
|
results[r.index] = this.createBatchResult(
|
|
44882
45028
|
ids[r.index],
|
|
@@ -48358,12 +48504,15 @@ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
|
|
|
48358
48504
|
const segments = (path10) => JSON.stringify(splitFolderPath(path10).map((part) => part.toLocaleLowerCase()));
|
|
48359
48505
|
if (request.folder) {
|
|
48360
48506
|
const wanted = segments(request.folder);
|
|
48361
|
-
|
|
48362
|
-
|
|
48507
|
+
const accounts = manager.listAccounts().filter((account) => account.defaultFolder);
|
|
48508
|
+
if (!accounts.some(
|
|
48509
|
+
(account) => manager.listFolders(account.name).some((folder) => segments(folder.name) === wanted)
|
|
48363
48510
|
))
|
|
48364
48511
|
throw new Error(
|
|
48365
48512
|
`Folder "${request.folder}" does not exist; create it with create-folder first. Nothing was created`
|
|
48366
48513
|
);
|
|
48514
|
+
for (const account of accounts)
|
|
48515
|
+
manager.assertNotSmartFolderDestination(request.folder, account.name);
|
|
48367
48516
|
}
|
|
48368
48517
|
const defaultFolderNotes = () => new Map(
|
|
48369
48518
|
manager.listAccounts().flatMap(
|
|
@@ -49031,77 +49180,6 @@ function withJsonSchema2020_12(transport2) {
|
|
|
49031
49180
|
return transport2;
|
|
49032
49181
|
}
|
|
49033
49182
|
|
|
49034
|
-
// src/utils/errorCodes.ts
|
|
49035
|
-
var CodedError = class extends Error {
|
|
49036
|
-
envelope;
|
|
49037
|
-
constructor(message, envelope) {
|
|
49038
|
-
super(message);
|
|
49039
|
-
this.name = "CodedError";
|
|
49040
|
-
this.envelope = envelope;
|
|
49041
|
-
}
|
|
49042
|
-
};
|
|
49043
|
-
var RULES = [
|
|
49044
|
-
{ code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
|
|
49045
|
-
{
|
|
49046
|
-
code: "verification_failed",
|
|
49047
|
-
pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
|
|
49048
|
-
},
|
|
49049
|
-
{
|
|
49050
|
-
code: "revision_conflict",
|
|
49051
|
-
pattern: /changed after it was read|revision changed|(?:pinned|note) state changed|changed during (?:read|preflight)|changed; read (?:it|the note) again|rich text do not match/i
|
|
49052
|
-
},
|
|
49053
|
-
{ code: "full_disk_access_missing", pattern: /Full Disk Access/i },
|
|
49054
|
-
{
|
|
49055
|
-
code: "shortcut_not_installed",
|
|
49056
|
-
pattern: /(?:Install|Import) the supplied|no such shortcut|Run apple-notes-mcp setup/i
|
|
49057
|
-
},
|
|
49058
|
-
{
|
|
49059
|
-
code: "not_found",
|
|
49060
|
-
pattern: /not found|does not exist|\bNo \w+ found\b|does not contain any checklist items|Can[’']t get (?:note|folder|account|attachment)|Cannot find note|Scope is absent/i
|
|
49061
|
-
},
|
|
49062
|
-
{ code: "ambiguous", pattern: /ambiguous|more than one|exactly once|Duplicate note IDs/i },
|
|
49063
|
-
{
|
|
49064
|
-
code: "notes_unavailable",
|
|
49065
|
-
pattern: /Notes\.app is (?:not responding|busy)|Lost connection to Notes|isn't running/i
|
|
49066
|
-
},
|
|
49067
|
-
{
|
|
49068
|
-
code: "unsupported",
|
|
49069
|
-
pattern: /password-protected|Locked notes|unsupported|not supported|is blocked|blocked because|has not passed live|refuses to sign|No (?:supported )?background|only one where|Real Notes link unavailable/i
|
|
49070
|
-
},
|
|
49071
|
-
{
|
|
49072
|
-
code: "validation_error",
|
|
49073
|
-
pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
|
|
49074
|
-
}
|
|
49075
|
-
];
|
|
49076
|
-
var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
|
|
49077
|
-
var WRITE_ACCEPTED = /\baccepted (?:the|an) \w+, but/i;
|
|
49078
|
-
function classifyError(message, cause) {
|
|
49079
|
-
if (cause instanceof CodedError) return { ...cause.envelope };
|
|
49080
|
-
const text2 = cause instanceof Error && cause.message && !message.includes(cause.message) ? `${message}
|
|
49081
|
-
${cause.message}` : message;
|
|
49082
|
-
const timeoutCause = typeof cause === "object" && cause !== null && cause.code === "ETIMEDOUT";
|
|
49083
|
-
let code = "operation_failed";
|
|
49084
|
-
if (timeoutCause) code = "timeout_indeterminate";
|
|
49085
|
-
else if (isPermissionDenied(text2)) code = "permission_denied";
|
|
49086
|
-
else code = RULES.find((rule) => rule.pattern.test(text2))?.code ?? "operation_failed";
|
|
49087
|
-
const envelope = { code };
|
|
49088
|
-
if (code === "timeout_indeterminate" || code === "verification_failed") {
|
|
49089
|
-
envelope.indeterminate = true;
|
|
49090
|
-
if (WRITE_ACCEPTED.test(text2)) envelope.committed = true;
|
|
49091
|
-
} else if (code === "revision_conflict" || NOTHING_WRITTEN.test(text2)) {
|
|
49092
|
-
envelope.committed = false;
|
|
49093
|
-
envelope.indeterminate = false;
|
|
49094
|
-
}
|
|
49095
|
-
return envelope;
|
|
49096
|
-
}
|
|
49097
|
-
function errorResult(message, cause) {
|
|
49098
|
-
return {
|
|
49099
|
-
content: [{ type: "text", text: message }],
|
|
49100
|
-
structuredContent: classifyError(message, cause),
|
|
49101
|
-
isError: true
|
|
49102
|
-
};
|
|
49103
|
-
}
|
|
49104
|
-
|
|
49105
49183
|
// src/utils/shutdown.ts
|
|
49106
49184
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 2e3;
|
|
49107
49185
|
function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
|
|
@@ -51577,7 +51655,9 @@ function registerDirectOperations(server2, manager) {
|
|
|
51577
51655
|
{
|
|
51578
51656
|
title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
|
|
51579
51657
|
content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
|
|
51580
|
-
folder: external_exports.string().max(1e3).optional().describe(
|
|
51658
|
+
folder: external_exports.string().max(1e3).optional().describe(
|
|
51659
|
+
"Existing folder or nested path; create it first with create-folder. A smart folder is refused before the note is created"
|
|
51660
|
+
),
|
|
51581
51661
|
account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
|
|
51582
51662
|
...attachmentInput
|
|
51583
51663
|
},
|
|
@@ -56607,7 +56687,7 @@ function registerTool(name, config2, cb) {
|
|
|
56607
56687
|
registerTool(
|
|
56608
56688
|
"create-note",
|
|
56609
56689
|
{
|
|
56610
|
-
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.",
|
|
56690
|
+
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. A smart folder is refused (code unsupported); it cannot hold notes.",
|
|
56611
56691
|
inputSchema: {
|
|
56612
56692
|
title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
|
|
56613
56693
|
content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).optional().describe(
|
|
@@ -58099,7 +58179,7 @@ registerTool(
|
|
|
58099
58179
|
registerTool(
|
|
58100
58180
|
"move-note",
|
|
58101
58181
|
{
|
|
58102
|
-
description: "Use when: moving one exact note to a different folder by id.\nReturns: confirmation and exact-ID readback.\nDo not use when: you only have a title or want to move many notes (batch-move-notes).\nNote: Notes.app's native move preserves the note id, creation date, body, and attachments. The destination folder must already exist. Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds (which also covers the destination) are re-checked inside the move AppleScript.",
|
|
58182
|
+
description: "Use when: moving one exact note to a different folder by id.\nReturns: confirmation and exact-ID readback.\nDo not use when: you only have a title or want to move many notes (batch-move-notes).\nNote: Notes.app's native move preserves the note id, creation date, body, and attachments. The destination folder must already exist and cannot be a smart folder (refused with code unsupported before the move). Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds (which also covers the destination) are re-checked inside the move AppleScript.",
|
|
58103
58183
|
inputSchema: {
|
|
58104
58184
|
id: noteIdInput,
|
|
58105
58185
|
folder: external_exports.string().min(1, "Destination folder is required").max(MAX.FOLDER),
|
|
@@ -58341,7 +58421,7 @@ ${lines.join("\n")}${fullyDecoded ? "" : '\n\nSome rules were not recognized; se
|
|
|
58341
58421
|
registerTool(
|
|
58342
58422
|
"create-folder",
|
|
58343
58423
|
{
|
|
58344
|
-
description: "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nDo not use when: creating a note (create-note).",
|
|
58424
|
+
description: "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nNote: a path segment that names a smart folder is refused (code unsupported) before anything is created; smart folders cannot hold folders.\nDo not use when: creating a note (create-note).",
|
|
58345
58425
|
inputSchema: {
|
|
58346
58426
|
name: external_exports.string().min(1, "Folder name is required").max(MAX.FOLDER).describe(
|
|
58347
58427
|
'Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'
|
|
@@ -58782,7 +58862,7 @@ registerTool(
|
|
|
58782
58862
|
registerTool(
|
|
58783
58863
|
"batch-move-notes",
|
|
58784
58864
|
{
|
|
58785
|
-
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder).",
|
|
58865
|
+
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder); a smart folder is refused for the whole call (code unsupported) before any note moves.",
|
|
58786
58866
|
inputSchema: {
|
|
58787
58867
|
ids: noteIdArrayInput.describe(
|
|
58788
58868
|
`Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
|
package/package.json
CHANGED