apple-notes-mcp 2.8.42 → 2.8.44
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 +35 -2
- package/build/index.js +311 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -420,7 +420,7 @@ Searches for notes by title or content.
|
|
|
420
420
|
| Parameter | Type | Required | Description |
|
|
421
421
|
|-----------|------|----------|-------------|
|
|
422
422
|
| `query` | string | Yes | Text to search for |
|
|
423
|
-
| `searchContent` | boolean | No | If `true`, searches note
|
|
423
|
+
| `searchContent` | boolean | No | If `true`, searches note content (title line included); if `false` (default), searches titles only. With Full Disk Access, content search reads the Notes database (well under a second, the 5000 most recently modified notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on a broad term |
|
|
424
424
|
| `account` | string | No | Account to search in (defaults to Notes.app's default account; exact or unique-prefix match) |
|
|
425
425
|
| `folder` | string | No | Limit search to a specific folder (supports nested paths like `"Work/Clients"`) |
|
|
426
426
|
| `modifiedSince` | string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., `"2025-01-01"`) |
|
|
@@ -451,7 +451,7 @@ Searches for notes by title or content.
|
|
|
451
451
|
}
|
|
452
452
|
```
|
|
453
453
|
|
|
454
|
-
**Returns:** List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like `get-note-content`, `update-note`, etc.
|
|
454
|
+
**Returns:** List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like `get-note-content`, `update-note`, etc. A content search also returns `source` (`"database"` or `"applescript"`), and `scanTruncated` when the database path left older notes unsearched.
|
|
455
455
|
|
|
456
456
|
---
|
|
457
457
|
|
|
@@ -763,6 +763,7 @@ Updates an existing note's content and/or title.
|
|
|
763
763
|
| `newContent` | string | Yes | New content for the note body |
|
|
764
764
|
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content replaces the entire note body as raw HTML and `newTitle` is ignored (the first HTML element serves as the title) |
|
|
765
765
|
| `allowLinkChanges` | boolean | No | Set to `true` only when intentionally changing or removing existing links |
|
|
766
|
+
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
|
|
766
767
|
|
|
767
768
|
Title-only updates are rejected because Apple Notes titles are not unique.
|
|
768
769
|
|
|
@@ -807,6 +808,7 @@ Deletes a note (moves to Recently Deleted in Notes.app).
|
|
|
807
808
|
|-----------|------|----------|-------------|
|
|
808
809
|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
|
|
809
810
|
| `expectedContentHash` | string | Yes | `contentHash` from the exact note version being deleted |
|
|
811
|
+
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
|
|
810
812
|
|
|
811
813
|
Title-only deletion is rejected. If the note changed after the supplied hash
|
|
812
814
|
was read, deletion is also rejected.
|
|
@@ -842,6 +844,7 @@ Moves a note to a different folder. The note is relocated in place via Notes.app
|
|
|
842
844
|
|-----------|------|----------|-------------|
|
|
843
845
|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
|
|
844
846
|
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`) |
|
|
847
|
+
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
|
|
845
848
|
|
|
846
849
|
Title-only moves are rejected.
|
|
847
850
|
|
|
@@ -858,6 +861,35 @@ actual destination folder ID matches the requested folder.
|
|
|
858
861
|
|
|
859
862
|
---
|
|
860
863
|
|
|
864
|
+
#### Folder scope guards
|
|
865
|
+
|
|
866
|
+
`update-note`, `append-to-note`, `delete-note`, and `move-note` accept three
|
|
867
|
+
optional folder preconditions. Use exact folder ids from `list-folders`.
|
|
868
|
+
|
|
869
|
+
- `ifFolderId`: the note must currently be in exactly this folder.
|
|
870
|
+
- `ifAncestorFolderId`: the note must be inside this folder or any of its
|
|
871
|
+
subfolders.
|
|
872
|
+
- `forbiddenAncestorFolderIds` (up to 50): the note must not be inside any of
|
|
873
|
+
these folders or their subfolders. For `move-note`, the destination must not
|
|
874
|
+
be either.
|
|
875
|
+
|
|
876
|
+
The checks read Notes.app's live folders inside the same AppleScript as the
|
|
877
|
+
write, immediately before it, so a note moved after you reviewed it is left
|
|
878
|
+
alone and the call fails with `Scope guard failed: …`. The one exception is a
|
|
879
|
+
native append to a protected note (it runs through Shortcuts): there the check
|
|
880
|
+
is a separate read just before the append, so it is not atomic.
|
|
881
|
+
|
|
882
|
+
**Example - retire a note only while it is still in the inbox:**
|
|
883
|
+
```json
|
|
884
|
+
{
|
|
885
|
+
"id": "x-coredata://ABC123/ICNote/p456",
|
|
886
|
+
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
887
|
+
"ifFolderId": "x-coredata://ABC123/ICFolder/p12"
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
---
|
|
892
|
+
|
|
861
893
|
#### `append-to-note`
|
|
862
894
|
|
|
863
895
|
Appends or prepends content to an existing note without replacing it. Always reads and writes as HTML, preserving all existing rich formatting.
|
|
@@ -870,6 +902,7 @@ Appends or prepends content to an existing note without replacing it. Always rea
|
|
|
870
902
|
| `position` | string | No | `"after"` (default) appends to the end; `"before"` inserts directly below the note's title line, so the title stays first |
|
|
871
903
|
| `separator` | string | No | String placed between existing content and new content (default: two newlines → `<div><br></div>` in HTML) |
|
|
872
904
|
| `format` | string | No | Format of the content being appended: `"plaintext"` (default) or `"html"` |
|
|
905
|
+
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
|
|
873
906
|
|
|
874
907
|
Title-only appends are rejected.
|
|
875
908
|
|
package/build/index.js
CHANGED
|
@@ -42053,6 +42053,77 @@ function exportDrawingRaster(drawing, savePath, containerDir = NOTES_CONTAINER_D
|
|
|
42053
42053
|
}
|
|
42054
42054
|
}
|
|
42055
42055
|
|
|
42056
|
+
// src/utils/scopeGuard.ts
|
|
42057
|
+
var SCOPE_FOLDER_ID = /^x-coredata:\/\/[0-9a-f-]+\/ICFolder\/p\d+$/i;
|
|
42058
|
+
var MAX_FORBIDDEN_FOLDERS = 50;
|
|
42059
|
+
var SCOPE_MARKER = "SAFETY_SCOPE";
|
|
42060
|
+
function hasScopeGuard(guard) {
|
|
42061
|
+
return Boolean(
|
|
42062
|
+
guard && (guard.ifFolderId || guard.ifAncestorFolderId || guard.forbiddenAncestorFolderIds && guard.forbiddenAncestorFolderIds.length > 0)
|
|
42063
|
+
);
|
|
42064
|
+
}
|
|
42065
|
+
function validateScopeGuard(guard) {
|
|
42066
|
+
const ids = [
|
|
42067
|
+
guard.ifFolderId,
|
|
42068
|
+
guard.ifAncestorFolderId,
|
|
42069
|
+
...guard.forbiddenAncestorFolderIds ?? []
|
|
42070
|
+
].filter((value) => value !== void 0);
|
|
42071
|
+
for (const id2 of ids) {
|
|
42072
|
+
if (!SCOPE_FOLDER_ID.test(id2)) throw new Error(`Scope guard needs exact folder ids: ${id2}`);
|
|
42073
|
+
}
|
|
42074
|
+
if ((guard.forbiddenAncestorFolderIds?.length ?? 0) > MAX_FORBIDDEN_FOLDERS)
|
|
42075
|
+
throw new Error(`At most ${MAX_FORBIDDEN_FOLDERS} forbidden folder ids are allowed`);
|
|
42076
|
+
}
|
|
42077
|
+
function chainScript(chainVar, startExpr) {
|
|
42078
|
+
return `
|
|
42079
|
+
set ${chainVar} to {}
|
|
42080
|
+
set ${chainVar}Cursor to ${startExpr}
|
|
42081
|
+
repeat while class of ${chainVar}Cursor is folder
|
|
42082
|
+
set end of ${chainVar} to (id of ${chainVar}Cursor)
|
|
42083
|
+
set ${chainVar}Cursor to container of ${chainVar}Cursor
|
|
42084
|
+
end repeat`;
|
|
42085
|
+
}
|
|
42086
|
+
function idList(ids) {
|
|
42087
|
+
return `{${ids.map((id2) => `"${id2}"`).join(", ")}}`;
|
|
42088
|
+
}
|
|
42089
|
+
function buildScopeGuardScript(noteVar, guard, destinationVar) {
|
|
42090
|
+
if (!hasScopeGuard(guard)) return "";
|
|
42091
|
+
validateScopeGuard(guard);
|
|
42092
|
+
const forbidden = guard.forbiddenAncestorFolderIds ?? [];
|
|
42093
|
+
let script = `
|
|
42094
|
+
set scopeFolder to container of ${noteVar}
|
|
42095
|
+
if class of scopeFolder is not folder then return "${SCOPE_MARKER}:the note is not in a folder"`;
|
|
42096
|
+
if (guard.ifFolderId)
|
|
42097
|
+
script += `
|
|
42098
|
+
if (id of scopeFolder) is not "${guard.ifFolderId}" then return "${SCOPE_MARKER}:the note is not in the expected folder"`;
|
|
42099
|
+
if (guard.ifAncestorFolderId || forbidden.length > 0)
|
|
42100
|
+
script += chainScript("scopeChain", "scopeFolder");
|
|
42101
|
+
if (guard.ifAncestorFolderId)
|
|
42102
|
+
script += `
|
|
42103
|
+
if scopeChain does not contain "${guard.ifAncestorFolderId}" then return "${SCOPE_MARKER}:the note is not inside the expected ancestor folder"`;
|
|
42104
|
+
if (forbidden.length > 0) {
|
|
42105
|
+
script += `
|
|
42106
|
+
repeat with forbiddenId in ${idList(forbidden)}
|
|
42107
|
+
if scopeChain contains (contents of forbiddenId) then return "${SCOPE_MARKER}:the note is inside a forbidden folder"
|
|
42108
|
+
end repeat`;
|
|
42109
|
+
if (destinationVar) {
|
|
42110
|
+
script += chainScript("scopeDestChain", destinationVar);
|
|
42111
|
+
script += `
|
|
42112
|
+
repeat with forbiddenId in ${idList(forbidden)}
|
|
42113
|
+
if scopeDestChain contains (contents of forbiddenId) then return "${SCOPE_MARKER}:the destination is inside a forbidden folder"
|
|
42114
|
+
end repeat`;
|
|
42115
|
+
}
|
|
42116
|
+
}
|
|
42117
|
+
return script;
|
|
42118
|
+
}
|
|
42119
|
+
function parseScopeFailure(output) {
|
|
42120
|
+
const trimmed = output.trim();
|
|
42121
|
+
return trimmed.startsWith(`${SCOPE_MARKER}:`) ? trimmed.slice(SCOPE_MARKER.length + 1) : null;
|
|
42122
|
+
}
|
|
42123
|
+
function scopeConflictMessage(reason) {
|
|
42124
|
+
return `Scope guard failed: ${reason}. Nothing was changed; read the note's current folder and review before retrying.`;
|
|
42125
|
+
}
|
|
42126
|
+
|
|
42056
42127
|
// src/services/appleNotesManager.ts
|
|
42057
42128
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
42058
42129
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -42373,6 +42444,16 @@ var AppleNotesManager = class {
|
|
|
42373
42444
|
}
|
|
42374
42445
|
return this.defaultAccountName || void 0;
|
|
42375
42446
|
}
|
|
42447
|
+
/**
|
|
42448
|
+
* The account a search with this `account` argument is scoped to: the
|
|
42449
|
+
* caller's account when named, else Notes.app's default account (cached).
|
|
42450
|
+
* Returns `undefined` when no account was named and the default cannot be
|
|
42451
|
+
* determined. Used by search-notes' database path so it searches the same
|
|
42452
|
+
* account the AppleScript path would.
|
|
42453
|
+
*/
|
|
42454
|
+
searchAccountScope(account) {
|
|
42455
|
+
return this.reportedAccount(this.resolveAccount(account));
|
|
42456
|
+
}
|
|
42376
42457
|
/**
|
|
42377
42458
|
* Checks if a note is password-protected by its ID.
|
|
42378
42459
|
*
|
|
@@ -42816,7 +42897,7 @@ var AppleNotesManager = class {
|
|
|
42816
42897
|
* race that would exist if JavaScript checked the note and then issued a
|
|
42817
42898
|
* separate unconditional `set body` command.
|
|
42818
42899
|
*/
|
|
42819
|
-
updateNoteByIdIfUnchanged(id2, currentTitle, expectedBody, newTitle, newContent, format = "plaintext", expectedRichRevision) {
|
|
42900
|
+
updateNoteByIdIfUnchanged(id2, currentTitle, expectedBody, newTitle, newContent, format = "plaintext", expectedRichRevision, scope2) {
|
|
42820
42901
|
const safeId = sanitizeNoteId(id2);
|
|
42821
42902
|
if (expectedRichRevision) {
|
|
42822
42903
|
const rich = readRichNote(id2);
|
|
@@ -42837,7 +42918,7 @@ var AppleNotesManager = class {
|
|
|
42837
42918
|
const safeExpectedBody = escapeHtmlForAppleScript(expectedBody);
|
|
42838
42919
|
const safeWrittenBody = escapeHtmlForAppleScript(writtenBody);
|
|
42839
42920
|
const script = buildAppLevelScript(`
|
|
42840
|
-
set noteRef to note id "${safeId}"
|
|
42921
|
+
set noteRef to note id "${safeId}"${buildScopeGuardScript("noteRef", scope2)}
|
|
42841
42922
|
if (count of attachments of noteRef) is greater than 0 then return "SAFETY_ATTACHMENTS"
|
|
42842
42923
|
set currentBody to body of noteRef
|
|
42843
42924
|
considering case
|
|
@@ -42851,6 +42932,8 @@ var AppleNotesManager = class {
|
|
|
42851
42932
|
console.error(`Failed guarded update for note ID "${id2}":`, result.error);
|
|
42852
42933
|
return { status: "failed" };
|
|
42853
42934
|
}
|
|
42935
|
+
const updateScopeFailure = parseScopeFailure(result.output);
|
|
42936
|
+
if (updateScopeFailure) return { status: "scope_conflict", reason: updateScopeFailure };
|
|
42854
42937
|
const status = result.output.trim();
|
|
42855
42938
|
if (status === "SAFETY_CONFLICT") return { status: "conflict" };
|
|
42856
42939
|
if (status === "SAFETY_ATTACHMENTS") return { status: "attachments" };
|
|
@@ -42862,12 +42945,12 @@ var AppleNotesManager = class {
|
|
|
42862
42945
|
* the caller reviewed. The comparison and delete are one AppleScript action,
|
|
42863
42946
|
* so a concurrent edit cannot slip between the guard and deletion.
|
|
42864
42947
|
*/
|
|
42865
|
-
deleteNoteByIdIfUnchanged(id2, expectedBody) {
|
|
42948
|
+
deleteNoteByIdIfUnchanged(id2, expectedBody, scope2) {
|
|
42866
42949
|
const safeId = sanitizeNoteId(id2);
|
|
42867
42950
|
validateLength(expectedBody, MAX_CONTENT_LENGTH, "Expected note content");
|
|
42868
42951
|
const safeExpectedBody = escapeHtmlForAppleScript(expectedBody);
|
|
42869
42952
|
const script = buildAppLevelScript(`
|
|
42870
|
-
set noteRef to note id "${safeId}"
|
|
42953
|
+
set noteRef to note id "${safeId}"${buildScopeGuardScript("noteRef", scope2)}
|
|
42871
42954
|
set originalFolder to missing value
|
|
42872
42955
|
try
|
|
42873
42956
|
set originalFolder to container of noteRef
|
|
@@ -42899,12 +42982,31 @@ var AppleNotesManager = class {
|
|
|
42899
42982
|
console.error(`Failed guarded delete for note ID "${id2}":`, result.error);
|
|
42900
42983
|
return { status: "failed" };
|
|
42901
42984
|
}
|
|
42985
|
+
const deleteScopeFailure = parseScopeFailure(result.output);
|
|
42986
|
+
if (deleteScopeFailure) return { status: "scope_conflict", reason: deleteScopeFailure };
|
|
42902
42987
|
const status = result.output.trim();
|
|
42903
42988
|
if (status === "SAFETY_CONFLICT") return { status: "conflict" };
|
|
42904
42989
|
if (status === "SAFETY_IN_RECENTLY_DELETED") return { status: "in-recently-deleted" };
|
|
42905
42990
|
if (status === "SAFETY_NOT_DELETED") return { status: "not-deleted" };
|
|
42906
42991
|
return status === "SAFETY_DELETED" ? { status: "deleted" } : { status: "failed" };
|
|
42907
42992
|
}
|
|
42993
|
+
/**
|
|
42994
|
+
* Read-only scope pre-check for writes that do not run as AppleScript (the
|
|
42995
|
+
* native Shortcuts operations). Runs the same checks the AppleScript writes
|
|
42996
|
+
* embed, but in a separate script, so it is not atomic with the write.
|
|
42997
|
+
*
|
|
42998
|
+
* @returns null when every precondition holds, otherwise the failure reason
|
|
42999
|
+
*/
|
|
43000
|
+
checkNoteScope(id2, scope2) {
|
|
43001
|
+
const safeId = sanitizeNoteId(id2);
|
|
43002
|
+
const result = executeAppleScript(
|
|
43003
|
+
buildAppLevelScript(`
|
|
43004
|
+
set noteRef to note id "${safeId}"${buildScopeGuardScript("noteRef", scope2)}
|
|
43005
|
+
return "SCOPE_OK"`)
|
|
43006
|
+
);
|
|
43007
|
+
if (!result.success) return result.error || "the note's folder could not be read";
|
|
43008
|
+
return parseScopeFailure(result.output);
|
|
43009
|
+
}
|
|
42908
43010
|
/**
|
|
42909
43011
|
* Builds the AppleScript body for a bulk note listing.
|
|
42910
43012
|
*
|
|
@@ -43491,14 +43593,14 @@ var AppleNotesManager = class {
|
|
|
43491
43593
|
* @param account - Account containing the destination folder (defaults to Notes.app's default account)
|
|
43492
43594
|
* @returns true if the move succeeded, false otherwise
|
|
43493
43595
|
*/
|
|
43494
|
-
moveNoteById(id2, destinationFolder, account) {
|
|
43596
|
+
moveNoteById(id2, destinationFolder, account, scope2) {
|
|
43495
43597
|
const targetAccount = this.resolveAccount(account);
|
|
43496
43598
|
const safeId = sanitizeNoteId(id2);
|
|
43497
43599
|
const destFolderRef = `${buildFolderReference(destinationFolder)} of ${AS_ACCOUNT_REF}`;
|
|
43498
43600
|
const moveCommand = `
|
|
43499
43601
|
${buildAccountResolution(targetAccount)}
|
|
43500
43602
|
set destFolder to ${destFolderRef}
|
|
43501
|
-
set noteRef to note id "${safeId}"
|
|
43603
|
+
set noteRef to note id "${safeId}"${buildScopeGuardScript("noteRef", scope2, "destFolder")}
|
|
43502
43604
|
move noteRef to destFolder
|
|
43503
43605
|
set movedNoteRef to note id "${safeId}"
|
|
43504
43606
|
set actualFolder to container of movedNoteRef
|
|
@@ -43515,6 +43617,8 @@ var AppleNotesManager = class {
|
|
|
43515
43617
|
);
|
|
43516
43618
|
return false;
|
|
43517
43619
|
}
|
|
43620
|
+
const scopeFailure = parseScopeFailure(result.output);
|
|
43621
|
+
if (scopeFailure) throw new Error(scopeConflictMessage(scopeFailure));
|
|
43518
43622
|
if (result.output.trim() !== "SAFETY_MOVED") {
|
|
43519
43623
|
console.error(
|
|
43520
43624
|
`Move result for note ID "${id2}" did not verify destination "${destinationFolder}"`
|
|
@@ -44416,12 +44520,12 @@ var AppleNotesManager = class {
|
|
|
44416
44520
|
}
|
|
44417
44521
|
});
|
|
44418
44522
|
if (runnable.length > 0) {
|
|
44419
|
-
const
|
|
44523
|
+
const idList2 = runnable.map((r) => `"${r.safe}"`).join(", ");
|
|
44420
44524
|
const script = buildAppLevelScript(`
|
|
44421
44525
|
${buildAccountResolution(targetAccount)}
|
|
44422
44526
|
set destFolder to ${destFolderRef}
|
|
44423
44527
|
set out to ""
|
|
44424
|
-
repeat with rawId in {${
|
|
44528
|
+
repeat with rawId in {${idList2}}
|
|
44425
44529
|
set theId to (rawId as text)
|
|
44426
44530
|
set noteRef to missing value
|
|
44427
44531
|
try
|
|
@@ -45681,6 +45785,13 @@ function describeSearchScope(searchContent, resultCount) {
|
|
|
45681
45785
|
return "\n\n\u2139\uFE0F Only note titles were searched, so a term that appears in note bodies would not match. Retry with `searchContent: true` to search bodies instead.";
|
|
45682
45786
|
}
|
|
45683
45787
|
|
|
45788
|
+
// src/utils/noteQueryStore.ts
|
|
45789
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
45790
|
+
import * as fs8 from "fs";
|
|
45791
|
+
import * as os8 from "os";
|
|
45792
|
+
import * as path8 from "path";
|
|
45793
|
+
import { gunzipSync as gunzipSync7 } from "zlib";
|
|
45794
|
+
|
|
45684
45795
|
// src/utils/noteQuery.ts
|
|
45685
45796
|
var QUERY_LIMITS = {
|
|
45686
45797
|
/** Maximum number of tokens (terms, operators, parentheses). */
|
|
@@ -46127,11 +46238,6 @@ function positiveTextTerms(node, negated = false) {
|
|
|
46127
46238
|
}
|
|
46128
46239
|
|
|
46129
46240
|
// src/utils/noteQueryStore.ts
|
|
46130
|
-
import { execFileSync as execFileSync13 } from "child_process";
|
|
46131
|
-
import * as fs8 from "fs";
|
|
46132
|
-
import * as os8 from "os";
|
|
46133
|
-
import * as path8 from "path";
|
|
46134
|
-
import { gunzipSync as gunzipSync7 } from "zlib";
|
|
46135
46241
|
var NOTES_DB_PATH9 = path8.join(
|
|
46136
46242
|
os8.homedir(),
|
|
46137
46243
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
@@ -46327,7 +46433,13 @@ function buildSnippet(text2, terms) {
|
|
|
46327
46433
|
}
|
|
46328
46434
|
function queryNotes(expression, options = {}) {
|
|
46329
46435
|
const ast = parseNoteQuery(expression);
|
|
46330
|
-
|
|
46436
|
+
return runNoteQuery(ast, {
|
|
46437
|
+
...options,
|
|
46438
|
+
limit: Math.min(options.limit ?? QUERY_RESULTS.DEFAULT, QUERY_RESULTS.MAX)
|
|
46439
|
+
});
|
|
46440
|
+
}
|
|
46441
|
+
function runNoteQuery(ast, options = {}) {
|
|
46442
|
+
const limit = options.limit ?? QUERY_RESULTS.DEFAULT;
|
|
46331
46443
|
const scanLimit = Math.min(options.scanLimit ?? QUERY_SCAN.DEFAULT, QUERY_SCAN.MAX);
|
|
46332
46444
|
const includeDeleted = options.includeDeleted ?? false;
|
|
46333
46445
|
const withBodies = needsContent(ast);
|
|
@@ -46464,6 +46576,66 @@ function queryNotes(expression, options = {}) {
|
|
|
46464
46576
|
};
|
|
46465
46577
|
}
|
|
46466
46578
|
|
|
46579
|
+
// src/utils/searchContentDb.ts
|
|
46580
|
+
function buildSearchContentQuery(options) {
|
|
46581
|
+
const children = [{ type: "text", field: "any", value: options.query }];
|
|
46582
|
+
const folder = options.folder?.trim();
|
|
46583
|
+
if (folder) children.push({ type: "folder", value: folder });
|
|
46584
|
+
const account = options.account?.trim();
|
|
46585
|
+
if (account) children.push({ type: "account", value: account });
|
|
46586
|
+
if (options.modifiedSince) {
|
|
46587
|
+
const start = new Date(options.modifiedSince).getTime();
|
|
46588
|
+
if (Number.isFinite(start)) {
|
|
46589
|
+
children.push({
|
|
46590
|
+
type: "date",
|
|
46591
|
+
field: "modified",
|
|
46592
|
+
op: ">=",
|
|
46593
|
+
date: options.modifiedSince,
|
|
46594
|
+
start,
|
|
46595
|
+
end: start + 24 * 60 * 60 * 1e3
|
|
46596
|
+
});
|
|
46597
|
+
}
|
|
46598
|
+
}
|
|
46599
|
+
return children.length === 1 ? children[0] : { type: "and", children };
|
|
46600
|
+
}
|
|
46601
|
+
function searchContentViaDatabase(options) {
|
|
46602
|
+
const result = runNoteQuery(buildSearchContentQuery(options), {
|
|
46603
|
+
limit: options.limit,
|
|
46604
|
+
scanLimit: QUERY_SCAN.MAX,
|
|
46605
|
+
dbPath: options.dbPath
|
|
46606
|
+
});
|
|
46607
|
+
const notes = result.notes.map((hit) => ({
|
|
46608
|
+
id: hit.id,
|
|
46609
|
+
title: hit.title,
|
|
46610
|
+
content: "",
|
|
46611
|
+
tags: [],
|
|
46612
|
+
created: hit.created ? new Date(hit.created) : /* @__PURE__ */ new Date(0),
|
|
46613
|
+
modified: hit.modified ? new Date(hit.modified) : /* @__PURE__ */ new Date(0),
|
|
46614
|
+
...hit.folder !== void 0 ? { folder: hit.folder } : {},
|
|
46615
|
+
...hit.account !== void 0 ? { account: hit.account } : {}
|
|
46616
|
+
}));
|
|
46617
|
+
return {
|
|
46618
|
+
notes,
|
|
46619
|
+
scan: {
|
|
46620
|
+
scanned: result.scanned,
|
|
46621
|
+
eligible: result.eligible,
|
|
46622
|
+
scanTruncated: result.scanTruncated,
|
|
46623
|
+
matched: result.matched
|
|
46624
|
+
}
|
|
46625
|
+
};
|
|
46626
|
+
}
|
|
46627
|
+
function describeContentScan(scan) {
|
|
46628
|
+
if (!scan?.scanTruncated) return "";
|
|
46629
|
+
return `
|
|
46630
|
+
|
|
46631
|
+
\u2139\uFE0F Searched the ${scan.scanned} most recently modified of ${scan.eligible} notes; older notes were not searched. A title search (\`searchContent: false\`) covers every note.`;
|
|
46632
|
+
}
|
|
46633
|
+
function contentSearchFailureHint(message, dbUnavailable) {
|
|
46634
|
+
if (!/timed out/i.test(message)) return message;
|
|
46635
|
+
const remedy = dbUnavailable === "no_fda" ? " Grant Full Disk Access to the app that launches this server (run the doctor tool) so search-notes can search note bodies through the Notes database instead, which takes well under a second." : "";
|
|
46636
|
+
return `${message} Body search through AppleScript scans every note body before the result limit applies, so a broad term can exceed the time budget on a large library.${remedy} Otherwise narrow the search with \`folder\` or \`modifiedSince\`, or use a more specific term.`;
|
|
46637
|
+
}
|
|
46638
|
+
|
|
46467
46639
|
// src/tools/doctor.ts
|
|
46468
46640
|
import { spawnSync } from "child_process";
|
|
46469
46641
|
|
|
@@ -50801,6 +50973,36 @@ function noteIdentifiers(id2) {
|
|
|
50801
50973
|
var expectedContentHashInput = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/, "expectedContentHash must come from get-note-content").describe(
|
|
50802
50974
|
"Revision token returned by get-note-content for this exact ID. The mutation stops if the note changed since that read."
|
|
50803
50975
|
);
|
|
50976
|
+
var SCOPE_FOLDER_ID_MESSAGE = "An exact folder id is required: x-coredata://.../ICFolder/p..., Notes UUID, or numeric key (from list-folders)";
|
|
50977
|
+
var scopeFolderIdInput = exactIdInput("ICFolder", SCOPE_FOLDER_ID, SCOPE_FOLDER_ID_MESSAGE, {
|
|
50978
|
+
maxLength: MAX.ID
|
|
50979
|
+
});
|
|
50980
|
+
var scopeGuardInputs = {
|
|
50981
|
+
ifFolderId: scopeFolderIdInput.optional().describe(
|
|
50982
|
+
"Precondition: the note must currently be in exactly this folder (id from list-folders). Re-checked immediately before the write."
|
|
50983
|
+
),
|
|
50984
|
+
ifAncestorFolderId: scopeFolderIdInput.optional().describe(
|
|
50985
|
+
"Precondition: the note must be inside this folder or any of its subfolders. Re-checked immediately before the write."
|
|
50986
|
+
),
|
|
50987
|
+
forbiddenAncestorFolderIds: exactIdArrayInput(
|
|
50988
|
+
"ICFolder",
|
|
50989
|
+
SCOPE_FOLDER_ID,
|
|
50990
|
+
SCOPE_FOLDER_ID_MESSAGE,
|
|
50991
|
+
{
|
|
50992
|
+
maxLength: MAX.ID,
|
|
50993
|
+
maxItems: MAX_FORBIDDEN_FOLDERS
|
|
50994
|
+
}
|
|
50995
|
+
).optional().describe(
|
|
50996
|
+
"Precondition: the note must not be inside any of these folders or their subfolders (for move-note, neither may the destination). Re-checked immediately before the write."
|
|
50997
|
+
)
|
|
50998
|
+
};
|
|
50999
|
+
function scopeFrom(args) {
|
|
51000
|
+
return {
|
|
51001
|
+
ifFolderId: args.ifFolderId,
|
|
51002
|
+
ifAncestorFolderId: args.ifAncestorFolderId,
|
|
51003
|
+
forbiddenAncestorFolderIds: args.forbiddenAncestorFolderIds
|
|
51004
|
+
};
|
|
51005
|
+
}
|
|
50804
51006
|
var timeoutSecondsInput = external_exports.number().int().min(CALL_TIMEOUT_SECONDS.min).max(CALL_TIMEOUT_SECONDS.max).optional().describe(
|
|
50805
51007
|
`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.`
|
|
50806
51008
|
);
|
|
@@ -50981,10 +51183,12 @@ registerTool(
|
|
|
50981
51183
|
registerTool(
|
|
50982
51184
|
"search-notes",
|
|
50983
51185
|
{
|
|
50984
|
-
description: "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id.\nDo not use when: you already have a note id (use get-note-content)
|
|
51186
|
+
description: "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id; `source` says whether a body search read the Notes database or fell back to AppleScript.\nDo not use when: you already have a note id (use get-note-content), want every note (use list-notes), or need boolean or metadata filters (use query-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.\nNote: with Full Disk Access, body search reads the Notes database (fast; the most recent 5000 notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on broad terms.",
|
|
50985
51187
|
inputSchema: {
|
|
50986
51188
|
query: external_exports.string().min(1, "Search query is required").max(MAX.QUERY),
|
|
50987
|
-
searchContent: external_exports.boolean().optional().describe(
|
|
51189
|
+
searchContent: external_exports.boolean().optional().describe(
|
|
51190
|
+
"Search note content (title line included) instead of titles. Uses the Notes database when Full Disk Access is available, else AppleScript"
|
|
51191
|
+
),
|
|
50988
51192
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account to search in"),
|
|
50989
51193
|
folder: external_exports.string().max(MAX.FOLDER).optional().describe("Limit search to a specific folder"),
|
|
50990
51194
|
modifiedSince: external_exports.string().max(64).optional().describe(
|
|
@@ -50996,21 +51200,59 @@ registerTool(
|
|
|
50996
51200
|
},
|
|
50997
51201
|
outputSchema: {
|
|
50998
51202
|
notes: external_exports.array(external_exports.object({}).passthrough()).optional(),
|
|
50999
|
-
count: external_exports.number().optional()
|
|
51203
|
+
count: external_exports.number().optional(),
|
|
51204
|
+
source: external_exports.enum(["database", "applescript"]).optional(),
|
|
51205
|
+
scanTruncated: external_exports.boolean().optional()
|
|
51000
51206
|
}
|
|
51001
51207
|
},
|
|
51002
51208
|
withErrorHandling(({ query: query2, searchContent = false, account, folder, modifiedSince, limit }) => {
|
|
51003
51209
|
const effectiveLimit = resolveSearchLimit(limit);
|
|
51004
51210
|
const limitWasDefault = limit === void 0;
|
|
51211
|
+
let source;
|
|
51212
|
+
let dbScan;
|
|
51213
|
+
let dbUnavailable;
|
|
51214
|
+
const runSearch = () => {
|
|
51215
|
+
if (searchContent) {
|
|
51216
|
+
try {
|
|
51217
|
+
const db = searchContentViaDatabase({
|
|
51218
|
+
query: query2,
|
|
51219
|
+
account: notesManager.searchAccountScope(account),
|
|
51220
|
+
folder,
|
|
51221
|
+
modifiedSince,
|
|
51222
|
+
limit: effectiveLimit
|
|
51223
|
+
});
|
|
51224
|
+
source = "database";
|
|
51225
|
+
dbScan = db.scan;
|
|
51226
|
+
return db.notes;
|
|
51227
|
+
} catch (error2) {
|
|
51228
|
+
if (!(error2 instanceof NoteQueryStoreError)) throw error2;
|
|
51229
|
+
dbUnavailable = error2.kind;
|
|
51230
|
+
}
|
|
51231
|
+
source = "applescript";
|
|
51232
|
+
}
|
|
51233
|
+
try {
|
|
51234
|
+
return notesManager.searchNotes(
|
|
51235
|
+
query2,
|
|
51236
|
+
searchContent,
|
|
51237
|
+
account,
|
|
51238
|
+
folder,
|
|
51239
|
+
modifiedSince,
|
|
51240
|
+
effectiveLimit
|
|
51241
|
+
);
|
|
51242
|
+
} catch (error2) {
|
|
51243
|
+
if (!searchContent || !(error2 instanceof Error)) throw error2;
|
|
51244
|
+
throw new Error(contentSearchFailureHint(error2.message, dbUnavailable));
|
|
51245
|
+
}
|
|
51246
|
+
};
|
|
51005
51247
|
const {
|
|
51006
51248
|
result: notes,
|
|
51007
51249
|
syncBefore,
|
|
51008
51250
|
syncInterference
|
|
51009
|
-
} = withSyncAwarenessSync(
|
|
51010
|
-
|
|
51011
|
-
|
|
51012
|
-
|
|
51013
|
-
const
|
|
51251
|
+
} = withSyncAwarenessSync("search-notes", runSearch);
|
|
51252
|
+
const searchType = searchContent ? source === "database" ? "content via the Notes database" : "content" : "titles";
|
|
51253
|
+
const sourceFields = source ? { source } : {};
|
|
51254
|
+
const scanFields = dbScan ? { scanTruncated: dbScan.scanTruncated } : {};
|
|
51255
|
+
const scanNote = describeContentScan(dbScan);
|
|
51014
51256
|
const folderInfo = folder ? ` in folder "${folder}"` : "";
|
|
51015
51257
|
const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
|
|
51016
51258
|
const { info: limitInfo, truncationNote } = describeSearchLimit(
|
|
@@ -51031,8 +51273,8 @@ ${syncWarnings.join(" ")}` : "";
|
|
|
51031
51273
|
if (notes.length === 0) {
|
|
51032
51274
|
const scopeHint = describeSearchScope(searchContent, notes.length);
|
|
51033
51275
|
return successResponse(
|
|
51034
|
-
`No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${syncNote}`,
|
|
51035
|
-
{ notes: [], count: 0 }
|
|
51276
|
+
`No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${scanNote}${syncNote}`,
|
|
51277
|
+
{ notes: [], count: 0, ...sourceFields, ...scanFields }
|
|
51036
51278
|
);
|
|
51037
51279
|
}
|
|
51038
51280
|
const noteList = notes.map((n) => {
|
|
@@ -51046,8 +51288,13 @@ ${syncWarnings.join(" ")}` : "";
|
|
|
51046
51288
|
}).join("\n");
|
|
51047
51289
|
return successResponse(
|
|
51048
51290
|
`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):
|
|
51049
|
-
${noteList}${truncationNote}${syncNote}`,
|
|
51050
|
-
{
|
|
51291
|
+
${noteList}${truncationNote}${scanNote}${syncNote}`,
|
|
51292
|
+
{
|
|
51293
|
+
notes: withStableIdentifiers(notes, "ICNote"),
|
|
51294
|
+
count: notes.length,
|
|
51295
|
+
...sourceFields,
|
|
51296
|
+
...scanFields
|
|
51297
|
+
}
|
|
51051
51298
|
);
|
|
51052
51299
|
}, "Error searching notes")
|
|
51053
51300
|
);
|
|
@@ -51682,7 +51929,7 @@ ${lines.join("\n")}` : ".") + partial2,
|
|
|
51682
51929
|
registerTool(
|
|
51683
51930
|
"update-note",
|
|
51684
51931
|
{
|
|
51685
|
-
description: "Use when: replacing the body of one exact Apple Note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or the note has attachments.\nSafety: requires the exact note id and expectedContentHash from get-note-content. The server checks rich metadata revision, atomically checks the AppleScript body, blocks native objects/checklists, and verifies actual link destinations after saving. Preserve returned HTML links unless allowLinkChanges is explicitly requested. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
|
|
51932
|
+
description: "Use when: replacing the body of one exact Apple Note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title, the note changed since the read, or the note has attachments.\nSafety: requires the exact note id and expectedContentHash from get-note-content. The server checks rich metadata revision, atomically checks the AppleScript body, blocks native objects/checklists, and verifies actual link destinations after saving. Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds are re-checked inside the same AppleScript as the write. Preserve returned HTML links unless allowLinkChanges is explicitly requested. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
|
|
51686
51933
|
inputSchema: {
|
|
51687
51934
|
id: noteIdInput,
|
|
51688
51935
|
expectedContentHash: expectedContentHashInput,
|
|
@@ -51696,7 +51943,8 @@ registerTool(
|
|
|
51696
51943
|
"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."
|
|
51697
51944
|
),
|
|
51698
51945
|
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
51699
|
-
timeoutSeconds: timeoutSecondsInput
|
|
51946
|
+
timeoutSeconds: timeoutSecondsInput,
|
|
51947
|
+
...scopeGuardInputs
|
|
51700
51948
|
},
|
|
51701
51949
|
outputSchema: {
|
|
51702
51950
|
ok: external_exports.boolean().optional(),
|
|
@@ -51715,7 +51963,8 @@ registerTool(
|
|
|
51715
51963
|
newTitle,
|
|
51716
51964
|
newContent,
|
|
51717
51965
|
format = "plaintext",
|
|
51718
|
-
allowLinkChanges = false
|
|
51966
|
+
allowLinkChanges = false,
|
|
51967
|
+
...scopeArgs
|
|
51719
51968
|
}) => {
|
|
51720
51969
|
const snapshot = readExactNoteSnapshot(id2);
|
|
51721
51970
|
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
@@ -51736,11 +51985,15 @@ registerTool(
|
|
|
51736
51985
|
newTitle,
|
|
51737
51986
|
newContent,
|
|
51738
51987
|
format,
|
|
51739
|
-
snapshot.rich.revision
|
|
51988
|
+
snapshot.rich.revision,
|
|
51989
|
+
scopeFrom(scopeArgs)
|
|
51740
51990
|
);
|
|
51741
51991
|
if (result.status === "conflict") {
|
|
51742
51992
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
51743
51993
|
}
|
|
51994
|
+
if (result.status === "scope_conflict") {
|
|
51995
|
+
return errorResponse(scopeConflictMessage(result.reason));
|
|
51996
|
+
}
|
|
51744
51997
|
if (result.status === "attachments") {
|
|
51745
51998
|
return errorResponse(
|
|
51746
51999
|
`Note "${snapshot.note.title}" gained an attachment before saving. No content was replaced.`
|
|
@@ -51796,7 +52049,8 @@ function guardedAppend({
|
|
|
51796
52049
|
separator = "\n\n",
|
|
51797
52050
|
format = "plaintext",
|
|
51798
52051
|
scopeText,
|
|
51799
|
-
htmlSeparator
|
|
52052
|
+
htmlSeparator,
|
|
52053
|
+
...scopeArgs
|
|
51800
52054
|
}, onRoute) {
|
|
51801
52055
|
const contentToHtml = (text2) => {
|
|
51802
52056
|
if (format === "html") return text2;
|
|
@@ -51824,6 +52078,11 @@ function guardedAppend({
|
|
|
51824
52078
|
return errorResponse("Provide scopeText: a unique existing phrase for native append");
|
|
51825
52079
|
if (!VERIFIED_BACKGROUND.has("append-native"))
|
|
51826
52080
|
return errorResponse("Native append has not passed live validation; see get-capabilities");
|
|
52081
|
+
const nativeScope = scopeFrom(scopeArgs);
|
|
52082
|
+
if (hasScopeGuard(nativeScope)) {
|
|
52083
|
+
const reason = notesManager.checkNoteScope(id2, nativeScope);
|
|
52084
|
+
if (reason) return errorResponse(scopeConflictMessage(reason));
|
|
52085
|
+
}
|
|
51827
52086
|
onRoute?.("native");
|
|
51828
52087
|
const result2 = appendNative(notesManager, {
|
|
51829
52088
|
id: id2,
|
|
@@ -51855,11 +52114,15 @@ function guardedAppend({
|
|
|
51855
52114
|
void 0,
|
|
51856
52115
|
combinedBody,
|
|
51857
52116
|
"html",
|
|
51858
|
-
snapshot.rich.revision
|
|
52117
|
+
snapshot.rich.revision,
|
|
52118
|
+
scopeFrom(scopeArgs)
|
|
51859
52119
|
);
|
|
51860
52120
|
if (result.status === "conflict") {
|
|
51861
52121
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
51862
52122
|
}
|
|
52123
|
+
if (result.status === "scope_conflict") {
|
|
52124
|
+
return errorResponse(scopeConflictMessage(result.reason));
|
|
52125
|
+
}
|
|
51863
52126
|
if (result.status === "attachments") {
|
|
51864
52127
|
return errorResponse(
|
|
51865
52128
|
`Note "${snapshot.note.title}" gained an attachment before saving. No content was appended.`
|
|
@@ -51900,7 +52163,7 @@ function guardedAppend({
|
|
|
51900
52163
|
registerTool(
|
|
51901
52164
|
"append-to-note",
|
|
51902
52165
|
{
|
|
51903
|
-
description: "Use when: adding content to one exact note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title or the note changed since the read.\nSafety: protected native-object notes use native end-append with scopeText; ordinary notes retain guarded HTML editing. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.\nNative-append HTML subset (protected notes only; ordinary notes accept any HTML Notes.app renders): " + NATIVE_APPEND_HTML_SUBSET + " Native append also requires scopeText, the default blank-line separator and position 'after'.",
|
|
52166
|
+
description: "Use when: adding content to one exact note after reading it by id.\nReturns: exact id, new content hash, and visible-text readback verification.\nDo not use when: you only have a title or the note changed since the read.\nSafety: protected native-object notes use native end-append with scopeText; ordinary notes retain guarded HTML editing. Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds are re-checked inside the write AppleScript for ordinary notes, and as a separate read just before a native append. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.\nNative-append HTML subset (protected notes only; ordinary notes accept any HTML Notes.app renders): " + NATIVE_APPEND_HTML_SUBSET + " Native append also requires scopeText, the default blank-line separator and position 'after'.",
|
|
51904
52167
|
inputSchema: {
|
|
51905
52168
|
id: noteIdInput,
|
|
51906
52169
|
expectedContentHash: expectedContentHashInput,
|
|
@@ -51911,7 +52174,8 @@ registerTool(
|
|
|
51911
52174
|
),
|
|
51912
52175
|
separator: external_exports.string().max(20).optional().default("\n\n").describe("String placed between existing content and new content (default: two newlines)"),
|
|
51913
52176
|
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'"),
|
|
51914
|
-
timeoutSeconds: timeoutSecondsInput
|
|
52177
|
+
timeoutSeconds: timeoutSecondsInput,
|
|
52178
|
+
...scopeGuardInputs
|
|
51915
52179
|
},
|
|
51916
52180
|
outputSchema: {
|
|
51917
52181
|
ok: external_exports.boolean().optional(),
|
|
@@ -51997,11 +52261,12 @@ registerTool(
|
|
|
51997
52261
|
registerTool(
|
|
51998
52262
|
"delete-note",
|
|
51999
52263
|
{
|
|
52000
|
-
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. Refuses a note already in Recently Deleted, where a delete would be permanent.",
|
|
52264
|
+
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. Refuses a note already in Recently Deleted, where a delete would be permanent. Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds are re-checked inside that AppleScript too.",
|
|
52001
52265
|
inputSchema: {
|
|
52002
52266
|
id: noteIdInput,
|
|
52003
52267
|
expectedContentHash: expectedContentHashInput,
|
|
52004
|
-
timeoutSeconds: timeoutSecondsInput
|
|
52268
|
+
timeoutSeconds: timeoutSecondsInput,
|
|
52269
|
+
...scopeGuardInputs
|
|
52005
52270
|
},
|
|
52006
52271
|
outputSchema: {
|
|
52007
52272
|
ok: external_exports.boolean().optional(),
|
|
@@ -52011,16 +52276,19 @@ registerTool(
|
|
|
52011
52276
|
previousContentHash: external_exports.string().optional()
|
|
52012
52277
|
}
|
|
52013
52278
|
},
|
|
52014
|
-
withErrorHandling(({ id: id2, expectedContentHash }) => {
|
|
52279
|
+
withErrorHandling(({ id: id2, expectedContentHash, ...scopeArgs }) => {
|
|
52015
52280
|
const snapshot = readExactNoteSnapshot(id2);
|
|
52016
52281
|
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
52017
52282
|
if (snapshot.contentHash !== expectedContentHash) {
|
|
52018
52283
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
52019
52284
|
}
|
|
52020
|
-
const result = notesManager.deleteNoteByIdIfUnchanged(id2, snapshot.body);
|
|
52285
|
+
const result = notesManager.deleteNoteByIdIfUnchanged(id2, snapshot.body, scopeFrom(scopeArgs));
|
|
52021
52286
|
if (result.status === "conflict") {
|
|
52022
52287
|
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
52023
52288
|
}
|
|
52289
|
+
if (result.status === "scope_conflict") {
|
|
52290
|
+
return errorResponse(scopeConflictMessage(result.reason));
|
|
52291
|
+
}
|
|
52024
52292
|
if (result.status === "in-recently-deleted") {
|
|
52025
52293
|
return errorResponse(inRecentlyDeletedMessage(snapshot.note.title));
|
|
52026
52294
|
}
|
|
@@ -52048,12 +52316,13 @@ registerTool(
|
|
|
52048
52316
|
registerTool(
|
|
52049
52317
|
"move-note",
|
|
52050
52318
|
{
|
|
52051
|
-
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.",
|
|
52319
|
+
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.",
|
|
52052
52320
|
inputSchema: {
|
|
52053
52321
|
id: noteIdInput,
|
|
52054
52322
|
folder: external_exports.string().min(1, "Destination folder is required").max(MAX.FOLDER),
|
|
52055
52323
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note/folder"),
|
|
52056
|
-
timeoutSeconds: timeoutSecondsInput
|
|
52324
|
+
timeoutSeconds: timeoutSecondsInput,
|
|
52325
|
+
...scopeGuardInputs
|
|
52057
52326
|
},
|
|
52058
52327
|
outputSchema: {
|
|
52059
52328
|
ok: external_exports.boolean().optional(),
|
|
@@ -52063,12 +52332,12 @@ registerTool(
|
|
|
52063
52332
|
verified: external_exports.boolean().optional()
|
|
52064
52333
|
}
|
|
52065
52334
|
},
|
|
52066
|
-
withErrorHandling(({ id: id2, folder, account }) => {
|
|
52335
|
+
withErrorHandling(({ id: id2, folder, account, ...scopeArgs }) => {
|
|
52067
52336
|
const note = notesManager.getNoteById(id2);
|
|
52068
52337
|
if (!note) {
|
|
52069
52338
|
return errorResponse(`Note with ID "${id2}" not found`);
|
|
52070
52339
|
}
|
|
52071
|
-
const success = notesManager.moveNoteById(id2, folder, account);
|
|
52340
|
+
const success = notesManager.moveNoteById(id2, folder, account, scopeFrom(scopeArgs));
|
|
52072
52341
|
if (!success) {
|
|
52073
52342
|
return errorResponse(
|
|
52074
52343
|
`Failed to move note "${note.title}" to folder "${folder}". Folder may not exist.`
|
package/package.json
CHANGED