apple-notes-mcp 2.5.12 → 2.6.1
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 +65 -0
- package/build/index.js +383 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -469,6 +469,71 @@ Moves a note to a different folder. The note is relocated in place via Notes.app
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
+
#### `append-to-note`
|
|
473
|
+
|
|
474
|
+
Appends or prepends content to an existing note without replacing it. Always reads and writes as HTML, preserving all existing rich formatting.
|
|
475
|
+
|
|
476
|
+
| Parameter | Type | Required | Description |
|
|
477
|
+
|-----------|------|----------|-------------|
|
|
478
|
+
| `id` | string | No | Note ID (preferred - more reliable than title) |
|
|
479
|
+
| `title` | string | No | Note title (use `id` instead when available) |
|
|
480
|
+
| `content` | string | Yes | Text to append to the note body |
|
|
481
|
+
| `position` | string | No | `"after"` (default) appends to the end; `"before"` prepends to the start |
|
|
482
|
+
| `separator` | string | No | String placed between existing content and new content (default: two newlines → `<div><br></div>` in HTML) |
|
|
483
|
+
| `format` | string | No | Format of the content being appended: `"plaintext"` (default) or `"html"` |
|
|
484
|
+
| `account` | string | No | Account containing the note (defaults to iCloud, ignored if `id` is provided) |
|
|
485
|
+
|
|
486
|
+
**Note:** Either `id` or `title` must be provided. Using `id` is recommended.
|
|
487
|
+
|
|
488
|
+
**Example - Append plaintext:**
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"id": "x-coredata://ABC123/ICNote/p456",
|
|
492
|
+
"content": "New item added today"
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Example - Prepend HTML:**
|
|
497
|
+
```json
|
|
498
|
+
{
|
|
499
|
+
"id": "x-coredata://ABC123/ICNote/p456",
|
|
500
|
+
"content": "<div><b>Status:</b> done</div>",
|
|
501
|
+
"format": "html",
|
|
502
|
+
"position": "before"
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Returns:** Confirmation with note id and title. Warns when the note is shared with collaborators.
|
|
507
|
+
|
|
508
|
+
**⚠️ Safety:** Reads the existing body first, concatenates, then writes back. Run `list-attachments` first if the note may hold embedded files — a full-body rewrite can drop attachments.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
#### `get-note-link`
|
|
513
|
+
|
|
514
|
+
Returns the `notes://showNote?identifier=<uuid>` deep-link URL for a note. The URL opens the note in Notes.app on iOS and macOS and can be stored in Reminders tasks or shared links.
|
|
515
|
+
|
|
516
|
+
| Parameter | Type | Required | Description |
|
|
517
|
+
|-----------|------|----------|-------------|
|
|
518
|
+
| `id` | string | No | Note ID (preferred - more reliable than title) |
|
|
519
|
+
| `title` | string | No | Note title (use `id` instead when available) |
|
|
520
|
+
| `account` | string | No | Account containing the note (defaults to iCloud, ignored if `id` is provided) |
|
|
521
|
+
|
|
522
|
+
**Note:** Either `id` or `title` must be provided. Using `id` is recommended. Password-protected notes cannot be linked.
|
|
523
|
+
|
|
524
|
+
**Example:**
|
|
525
|
+
```json
|
|
526
|
+
{
|
|
527
|
+
"id": "x-coredata://ABC123/ICNote/p456"
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Returns:** `notes://showNote?identifier=<uuid>` URL string, plus the note id and title.
|
|
532
|
+
|
|
533
|
+
**Note:** Requires Full Disk Access for the app that launches the server so the Notes SQLite database is readable. On macOS 12–15 the tool also falls back to the AppleScript `note link` property. Run the `doctor` tool to verify access.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
472
537
|
#### `list-notes`
|
|
473
538
|
|
|
474
539
|
Lists all notes, optionally filtered by folder, date, and limit.
|
package/build/index.js
CHANGED
|
@@ -6,7 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var
|
|
9
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
11
|
+
}) : x)(function(x) {
|
|
12
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
15
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
10
16
|
try {
|
|
11
17
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
12
18
|
} catch (e) {
|
|
@@ -24161,14 +24167,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
24161
24167
|
} else if (node.nodeType === 1) {
|
|
24162
24168
|
replacement = replacementForNode.call(self, node);
|
|
24163
24169
|
}
|
|
24164
|
-
return
|
|
24170
|
+
return join6(output, replacement);
|
|
24165
24171
|
}, "");
|
|
24166
24172
|
}
|
|
24167
24173
|
function postProcess(output) {
|
|
24168
24174
|
var self = this;
|
|
24169
24175
|
this.rules.forEach(function(rule) {
|
|
24170
24176
|
if (typeof rule.append === "function") {
|
|
24171
|
-
output =
|
|
24177
|
+
output = join6(output, rule.append(self.options));
|
|
24172
24178
|
}
|
|
24173
24179
|
});
|
|
24174
24180
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -24180,7 +24186,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
24180
24186
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
24181
24187
|
return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
|
|
24182
24188
|
}
|
|
24183
|
-
function
|
|
24189
|
+
function join6(output, replacement) {
|
|
24184
24190
|
var s1 = trimTrailingNewlines(output);
|
|
24185
24191
|
var s2 = trimLeadingNewlines(replacement);
|
|
24186
24192
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -38700,12 +38706,14 @@ function isTimeoutError(error2) {
|
|
|
38700
38706
|
}
|
|
38701
38707
|
return false;
|
|
38702
38708
|
}
|
|
38709
|
+
var BULK_LIST_MUTATION_ERROR = "Notes changed during listing";
|
|
38703
38710
|
var RETRYABLE_ERROR_PATTERNS = [
|
|
38704
38711
|
/timed? out/i,
|
|
38705
38712
|
/not responding/i,
|
|
38706
38713
|
/connection.*invalid/i,
|
|
38707
38714
|
/lost connection/i,
|
|
38708
|
-
/busy/i
|
|
38715
|
+
/busy/i,
|
|
38716
|
+
/changed during listing/i
|
|
38709
38717
|
];
|
|
38710
38718
|
function isRetryableError(errorMessage) {
|
|
38711
38719
|
return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
@@ -38764,6 +38772,13 @@ var ERROR_MAPPINGS = [
|
|
|
38764
38772
|
pattern: /password protected|locked note/i,
|
|
38765
38773
|
message: "Note is password-protected. Unlock it in Notes.app first."
|
|
38766
38774
|
},
|
|
38775
|
+
// Mid-listing library mutation detected by a bulk-list count guard (#86).
|
|
38776
|
+
// The message must keep the phrase "changed during listing" so
|
|
38777
|
+
// RETRYABLE_ERROR_PATTERNS still matches it after this mapping.
|
|
38778
|
+
{
|
|
38779
|
+
pattern: /changed during listing/i,
|
|
38780
|
+
message: "Notes changed during listing (an iCloud sync may have landed mid-read). The operation is retried automatically; run it again if this persists."
|
|
38781
|
+
},
|
|
38767
38782
|
// Syntax/script errors (usually programming bugs)
|
|
38768
38783
|
{
|
|
38769
38784
|
pattern: /syntax error|expected/i,
|
|
@@ -39200,6 +39215,8 @@ function cleanupTempDir(dir) {
|
|
|
39200
39215
|
// src/services/appleNotesManager.ts
|
|
39201
39216
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
39202
39217
|
import { existsSync as existsSync3 } from "fs";
|
|
39218
|
+
import { homedir as homedir3 } from "os";
|
|
39219
|
+
import { join as join2 } from "path";
|
|
39203
39220
|
var FIELD_SEP = "";
|
|
39204
39221
|
var RECORD_SEP = "";
|
|
39205
39222
|
var AS_FIELD_SEP = "(ASCII character 31)";
|
|
@@ -39276,6 +39293,12 @@ function buildAppleScriptDateVar(date3, varName = "thresholdDate") {
|
|
|
39276
39293
|
const timeInSeconds = date3.getHours() * 3600 + date3.getMinutes() * 60 + date3.getSeconds();
|
|
39277
39294
|
return [
|
|
39278
39295
|
`set ${varName} to current date`,
|
|
39296
|
+
// Reset day to 1 BEFORE assigning month: AppleScript date components roll
|
|
39297
|
+
// over, so setting month to (say) June while the variable still holds the
|
|
39298
|
+
// 31st inherited from `current date` produces July 1 (June 31 doesn't
|
|
39299
|
+
// exist), and the day assignment below then lands in the wrong month.
|
|
39300
|
+
// Day 1 exists in every month, so year/month can never roll over. (#86)
|
|
39301
|
+
`set day of ${varName} to 1`,
|
|
39279
39302
|
`set year of ${varName} to ${year}`,
|
|
39280
39303
|
`set month of ${varName} to ${month}`,
|
|
39281
39304
|
`set day of ${varName} to ${day}`,
|
|
@@ -39342,6 +39365,27 @@ function buildAppLevelScript(command) {
|
|
|
39342
39365
|
end tell
|
|
39343
39366
|
`;
|
|
39344
39367
|
}
|
|
39368
|
+
function getNoteLinkFromDB(coreDataId) {
|
|
39369
|
+
const match = coreDataId.match(/\/p(\d+)$/);
|
|
39370
|
+
if (!match) return null;
|
|
39371
|
+
const pk = parseInt(match[1], 10);
|
|
39372
|
+
const dbPath = join2(homedir3(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
|
|
39373
|
+
if (!existsSync3(dbPath)) return null;
|
|
39374
|
+
try {
|
|
39375
|
+
const { DatabaseSync } = __require("node:sqlite");
|
|
39376
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
39377
|
+
try {
|
|
39378
|
+
const row = db.prepare("SELECT ZIDENTIFIER FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ?").get(pk);
|
|
39379
|
+
const identifier = row?.ZIDENTIFIER;
|
|
39380
|
+
return identifier ? `notes://showNote?identifier=${identifier}` : null;
|
|
39381
|
+
} finally {
|
|
39382
|
+
db.close();
|
|
39383
|
+
}
|
|
39384
|
+
} catch (err) {
|
|
39385
|
+
console.error("getNoteLinkFromDB: failed to query Notes database:", err);
|
|
39386
|
+
return null;
|
|
39387
|
+
}
|
|
39388
|
+
}
|
|
39345
39389
|
function extractCoreDataId(output, prefix) {
|
|
39346
39390
|
const pattern = new RegExp(`${prefix} id ([^\\s]+)`);
|
|
39347
39391
|
const match = output.match(pattern);
|
|
@@ -39888,6 +39932,101 @@ var AppleNotesManager = class {
|
|
|
39888
39932
|
}
|
|
39889
39933
|
return true;
|
|
39890
39934
|
}
|
|
39935
|
+
/**
|
|
39936
|
+
* Builds the AppleScript body for a bulk note listing.
|
|
39937
|
+
*
|
|
39938
|
+
* Names, ids, and (when date-filtering) modification dates are fetched as
|
|
39939
|
+
* whole-list Apple Events instead of two events per note; per-note round
|
|
39940
|
+
* trips scale linearly and push large libraries past client tool timeouts
|
|
39941
|
+
* (#86). The lists are separate snapshots of a live, syncing collection, so
|
|
39942
|
+
* every script guards that they are the same length before zipping them by
|
|
39943
|
+
* index — a mid-listing mutation would otherwise silently mispair names and
|
|
39944
|
+
* ids (grow) or read past the end of a list (shrink). On mismatch the
|
|
39945
|
+
* script raises BULK_LIST_MUTATION_ERROR, which executeAppleScript treats
|
|
39946
|
+
* as retryable, re-running the whole script on a fresh snapshot. A length
|
|
39947
|
+
* check cannot see an exactly-offsetting delete+create landing in the
|
|
39948
|
+
* milliseconds between two fetches; that residual window is accepted —
|
|
39949
|
+
* closing it would cost an extra whole-list fetch per listing.
|
|
39950
|
+
*
|
|
39951
|
+
* @param folderRef - Optional AppleScript folder reference to scope to
|
|
39952
|
+
* @param dateSetup - AppleScript defining thresholdDate; enables date filtering
|
|
39953
|
+
* @param sliceLimit - Fetch only the first N notes (mutually exclusive with
|
|
39954
|
+
* dateSetup: a date filter must scan every note's date). The script then
|
|
39955
|
+
* returns the total note count as a leading record so the caller can
|
|
39956
|
+
* detect a dedup shortfall and fall back to a full fetch.
|
|
39957
|
+
*/
|
|
39958
|
+
buildBulkListCommand(opts) {
|
|
39959
|
+
const { folderRef, dateSetup, sliceLimit } = opts;
|
|
39960
|
+
const fullSource = folderRef ? `notes of ${folderRef}` : "notes";
|
|
39961
|
+
const countGuard = (listVar) => `if (count of ${listVar}) is not (count of noteNames) then error "${BULK_LIST_MUTATION_ERROR}"`;
|
|
39962
|
+
if (sliceLimit !== void 0) {
|
|
39963
|
+
const slicedSource = folderRef ? `(notes 1 thru fetchCount of ${folderRef})` : `(notes 1 thru fetchCount)`;
|
|
39964
|
+
return `
|
|
39965
|
+
set totalCount to count of ${fullSource}
|
|
39966
|
+
set fetchCount to ${sliceLimit}
|
|
39967
|
+
if fetchCount > totalCount then set fetchCount to totalCount
|
|
39968
|
+
set resultList to {}
|
|
39969
|
+
if fetchCount > 0 then
|
|
39970
|
+
try
|
|
39971
|
+
set noteNames to name of ${slicedSource}
|
|
39972
|
+
set noteIds to id of ${slicedSource}
|
|
39973
|
+
on error errMsg number errNum
|
|
39974
|
+
if errNum is -1719 or errNum is -1728 then
|
|
39975
|
+
error "${BULK_LIST_MUTATION_ERROR}"
|
|
39976
|
+
else
|
|
39977
|
+
error errMsg number errNum
|
|
39978
|
+
end if
|
|
39979
|
+
end try
|
|
39980
|
+
${countGuard("noteIds")}
|
|
39981
|
+
repeat with i from 1 to count of noteNames
|
|
39982
|
+
set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)
|
|
39983
|
+
end repeat
|
|
39984
|
+
end if
|
|
39985
|
+
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
39986
|
+
return (totalCount as text) & ${AS_RECORD_SEP} & (resultList as text)
|
|
39987
|
+
`;
|
|
39988
|
+
}
|
|
39989
|
+
const dateFetch = dateSetup ? `set noteDates to modification date of ${fullSource}
|
|
39990
|
+
` : "";
|
|
39991
|
+
const dateCountGuard = dateSetup ? `${countGuard("noteDates")}
|
|
39992
|
+
` : "";
|
|
39993
|
+
const dateGuardOpen = dateSetup ? `if (item i of noteDates) >= thresholdDate then
|
|
39994
|
+
` : "";
|
|
39995
|
+
const dateGuardClose = dateSetup ? `
|
|
39996
|
+
end if` : "";
|
|
39997
|
+
return `
|
|
39998
|
+
${dateSetup ?? ""}set noteNames to name of ${fullSource}
|
|
39999
|
+
set noteIds to id of ${fullSource}
|
|
40000
|
+
${dateFetch}${countGuard("noteIds")}
|
|
40001
|
+
${dateCountGuard}set resultList to {}
|
|
40002
|
+
repeat with i from 1 to count of noteNames
|
|
40003
|
+
${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)${dateGuardClose}
|
|
40004
|
+
end repeat
|
|
40005
|
+
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
40006
|
+
return resultList as text
|
|
40007
|
+
`;
|
|
40008
|
+
}
|
|
40009
|
+
/**
|
|
40010
|
+
* Parses bulk listing output into deduplicated note titles.
|
|
40011
|
+
*
|
|
40012
|
+
* Duplicate CoreData references are deduped by id; the limit is applied
|
|
40013
|
+
* after dedup so duplicates never count against it.
|
|
40014
|
+
*/
|
|
40015
|
+
parseBulkListOutput(output, safeLimit) {
|
|
40016
|
+
if (!output.trim()) return [];
|
|
40017
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
40018
|
+
const titles = [];
|
|
40019
|
+
for (const item of output.split(RECORD_SEP)) {
|
|
40020
|
+
const [title, id] = item.split(FIELD_SEP);
|
|
40021
|
+
if (!title?.trim()) continue;
|
|
40022
|
+
const noteId = id?.trim() || generateFallbackId();
|
|
40023
|
+
if (seenIds.has(noteId)) continue;
|
|
40024
|
+
seenIds.add(noteId);
|
|
40025
|
+
titles.push(title.trim());
|
|
40026
|
+
if (safeLimit !== void 0 && titles.length >= safeLimit) break;
|
|
40027
|
+
}
|
|
40028
|
+
return titles;
|
|
40029
|
+
}
|
|
39891
40030
|
/**
|
|
39892
40031
|
* Lists all notes in an account, optionally filtered by folder, date, and limit.
|
|
39893
40032
|
*
|
|
@@ -39900,89 +40039,41 @@ var AppleNotesManager = class {
|
|
|
39900
40039
|
listNotes(account, folder, modifiedSince, limit) {
|
|
39901
40040
|
const targetAccount = this.resolveAccount(account);
|
|
39902
40041
|
const safeLimit = limit !== void 0 && limit > 0 ? Math.floor(limit) : void 0;
|
|
39903
|
-
|
|
39904
|
-
|
|
39905
|
-
|
|
39906
|
-
|
|
39907
|
-
if (
|
|
39908
|
-
|
|
39909
|
-
|
|
39910
|
-
|
|
39911
|
-
|
|
39912
|
-
|
|
39913
|
-
|
|
39914
|
-
|
|
39915
|
-
|
|
39916
|
-
const listCommand2 = `
|
|
39917
|
-
${dateSetup}set resultList to {}
|
|
39918
|
-
set seenIds to {}
|
|
39919
|
-
repeat with n in ${notesSource}
|
|
39920
|
-
try
|
|
39921
|
-
set noteName to name of n
|
|
39922
|
-
set noteId to id of n
|
|
39923
|
-
if seenIds does not contain noteId then
|
|
39924
|
-
set end of seenIds to noteId
|
|
39925
|
-
set end of resultList to noteName & ${AS_FIELD_SEP} & noteId${limitCheck}
|
|
39926
|
-
end if
|
|
39927
|
-
end try
|
|
39928
|
-
end repeat
|
|
39929
|
-
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
39930
|
-
return resultList as text
|
|
39931
|
-
`;
|
|
39932
|
-
const script2 = buildAccountScopedScript({ account: targetAccount }, listCommand2);
|
|
40042
|
+
const folderRef = folder ? buildFolderReference(folder) : void 0;
|
|
40043
|
+
let dateSetup;
|
|
40044
|
+
if (modifiedSince) {
|
|
40045
|
+
const date3 = new Date(modifiedSince);
|
|
40046
|
+
if (!isNaN(date3.getTime())) {
|
|
40047
|
+
dateSetup = buildAppleScriptDateVar(date3) + "\n";
|
|
40048
|
+
}
|
|
40049
|
+
}
|
|
40050
|
+
if (safeLimit !== void 0 && !dateSetup) {
|
|
40051
|
+
const script2 = buildAccountScopedScript(
|
|
40052
|
+
{ account: targetAccount },
|
|
40053
|
+
this.buildBulkListCommand({ folderRef, sliceLimit: safeLimit })
|
|
40054
|
+
);
|
|
39933
40055
|
const result2 = executeAppleScript(script2);
|
|
39934
40056
|
if (!result2.success) {
|
|
39935
40057
|
throw new Error(`Failed to list notes: ${result2.error ?? "unknown error"}`);
|
|
39936
40058
|
}
|
|
39937
|
-
|
|
39938
|
-
|
|
40059
|
+
const sepIdx = result2.output.indexOf(RECORD_SEP);
|
|
40060
|
+
const header = sepIdx === -1 ? result2.output : result2.output.slice(0, sepIdx);
|
|
40061
|
+
const totalCount = Number.parseInt(header.trim(), 10);
|
|
40062
|
+
const records = sepIdx === -1 ? "" : result2.output.slice(sepIdx + 1);
|
|
40063
|
+
const titles = this.parseBulkListOutput(records, safeLimit);
|
|
40064
|
+
if (!Number.isNaN(totalCount) && (titles.length >= safeLimit || totalCount <= safeLimit)) {
|
|
40065
|
+
return titles;
|
|
39939
40066
|
}
|
|
39940
|
-
const seenIds2 = /* @__PURE__ */ new Set();
|
|
39941
|
-
const titles2 = [];
|
|
39942
|
-
for (const item of result2.output.split(RECORD_SEP)) {
|
|
39943
|
-
const [title, id] = item.split(FIELD_SEP);
|
|
39944
|
-
if (!title?.trim()) continue;
|
|
39945
|
-
const noteId = id?.trim() || generateFallbackId();
|
|
39946
|
-
if (seenIds2.has(noteId)) continue;
|
|
39947
|
-
seenIds2.add(noteId);
|
|
39948
|
-
titles2.push(title.trim());
|
|
39949
|
-
}
|
|
39950
|
-
return titles2;
|
|
39951
40067
|
}
|
|
39952
|
-
const
|
|
39953
|
-
|
|
39954
|
-
|
|
39955
|
-
|
|
39956
|
-
repeat with n in ${notesRef}
|
|
39957
|
-
try
|
|
39958
|
-
set noteName to name of n
|
|
39959
|
-
set noteId to id of n
|
|
39960
|
-
if seenIds does not contain noteId then
|
|
39961
|
-
set end of seenIds to noteId
|
|
39962
|
-
set end of resultList to noteName & ${AS_FIELD_SEP} & noteId
|
|
39963
|
-
end if
|
|
39964
|
-
end try
|
|
39965
|
-
end repeat
|
|
39966
|
-
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
39967
|
-
return resultList as text
|
|
39968
|
-
`;
|
|
39969
|
-
const script = buildAccountScopedScript({ account: targetAccount }, listCommand);
|
|
40068
|
+
const script = buildAccountScopedScript(
|
|
40069
|
+
{ account: targetAccount },
|
|
40070
|
+
this.buildBulkListCommand({ folderRef, dateSetup })
|
|
40071
|
+
);
|
|
39970
40072
|
const result = executeAppleScript(script);
|
|
39971
40073
|
if (!result.success) {
|
|
39972
40074
|
throw new Error(`Failed to list notes: ${result.error ?? "unknown error"}`);
|
|
39973
40075
|
}
|
|
39974
|
-
|
|
39975
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
39976
|
-
const titles = [];
|
|
39977
|
-
for (const item of result.output.split(RECORD_SEP)) {
|
|
39978
|
-
const [title, id] = item.split(FIELD_SEP);
|
|
39979
|
-
if (!title?.trim()) continue;
|
|
39980
|
-
const noteId = id?.trim() || generateFallbackId();
|
|
39981
|
-
if (seenIds.has(noteId)) continue;
|
|
39982
|
-
seenIds.add(noteId);
|
|
39983
|
-
titles.push(title.trim());
|
|
39984
|
-
}
|
|
39985
|
-
return titles;
|
|
40076
|
+
return this.parseBulkListOutput(result.output, safeLimit);
|
|
39986
40077
|
}
|
|
39987
40078
|
/**
|
|
39988
40079
|
* Lists all shared (collaborative) notes across all accounts.
|
|
@@ -40406,6 +40497,47 @@ var AppleNotesManager = class {
|
|
|
40406
40497
|
}
|
|
40407
40498
|
return true;
|
|
40408
40499
|
}
|
|
40500
|
+
/**
|
|
40501
|
+
* Returns the notes:// deep-link URL for a note by its CoreData ID.
|
|
40502
|
+
*
|
|
40503
|
+
* Primary path: queries the Notes SQLite database for ZIDENTIFIER, which
|
|
40504
|
+
* is the UUID used in the notes://showNote?identifier= URL scheme. This
|
|
40505
|
+
* is more reliable than the AppleScript `note link` property, which is
|
|
40506
|
+
* absent from the Notes SDEF on macOS 26+.
|
|
40507
|
+
*
|
|
40508
|
+
* Fallback: AppleScript `note link` property (macOS 12–15).
|
|
40509
|
+
*
|
|
40510
|
+
* @param id - CoreData URL identifier for the note
|
|
40511
|
+
* @returns notes://showNote?identifier=<uuid> string, or null on failure
|
|
40512
|
+
*/
|
|
40513
|
+
getNoteLinkById(id) {
|
|
40514
|
+
const note = this.getNoteById(id);
|
|
40515
|
+
if (!note) return null;
|
|
40516
|
+
if (note.passwordProtected) return null;
|
|
40517
|
+
const sqliteLink = getNoteLinkFromDB(id);
|
|
40518
|
+
if (sqliteLink) return sqliteLink;
|
|
40519
|
+
const safeId = sanitizeId(id);
|
|
40520
|
+
const result = executeAppleScript(
|
|
40521
|
+
buildAppLevelScript(`return note link of (note id "${safeId}")`)
|
|
40522
|
+
);
|
|
40523
|
+
if (result.success && result.output.trim()) {
|
|
40524
|
+
return result.output.trim();
|
|
40525
|
+
}
|
|
40526
|
+
console.error(`Failed to get note link for ID "${id}":`, result.error);
|
|
40527
|
+
return null;
|
|
40528
|
+
}
|
|
40529
|
+
/**
|
|
40530
|
+
* Returns the notes:// deep-link URL for a note by title.
|
|
40531
|
+
*
|
|
40532
|
+
* @param title - Exact note title
|
|
40533
|
+
* @param account - Account to search in (defaults to iCloud)
|
|
40534
|
+
* @returns notes://showNote?identifier=<uuid> string, or null on failure
|
|
40535
|
+
*/
|
|
40536
|
+
getNoteLink(title, account) {
|
|
40537
|
+
const note = this.getNoteDetails(title, account);
|
|
40538
|
+
if (!note) return null;
|
|
40539
|
+
return this.getNoteLinkById(note.id);
|
|
40540
|
+
}
|
|
40409
40541
|
/**
|
|
40410
40542
|
* Reveals a folder in the Notes.app UI by its id.
|
|
40411
40543
|
*
|
|
@@ -41697,12 +41829,12 @@ function formatDoctorReport(r) {
|
|
|
41697
41829
|
|
|
41698
41830
|
// src/services/fileConfig.ts
|
|
41699
41831
|
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
41700
|
-
import { join as
|
|
41701
|
-
import { homedir as
|
|
41832
|
+
import { join as join5 } from "path";
|
|
41833
|
+
import { homedir as homedir6 } from "os";
|
|
41702
41834
|
function fileConfigPath(env = process.env) {
|
|
41703
41835
|
const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
|
|
41704
41836
|
if (override && override.trim()) return override.trim();
|
|
41705
|
-
return
|
|
41837
|
+
return join5(homedir6(), "Library", "Application Support", "apple-notes-mcp", "config.json");
|
|
41706
41838
|
}
|
|
41707
41839
|
function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
|
|
41708
41840
|
const applied = [];
|
|
@@ -42153,6 +42285,61 @@ server.registerTool(
|
|
|
42153
42285
|
return successResponse(`Shown note with ID "${id}" in Notes.app`, { id, separately });
|
|
42154
42286
|
}, "Error showing note")
|
|
42155
42287
|
);
|
|
42288
|
+
server.registerTool(
|
|
42289
|
+
"get-note-link",
|
|
42290
|
+
{
|
|
42291
|
+
description: "Use when: you need the notes:// deep-link URL for a note so it can be stored in a Reminders task, shared, or opened directly.\nReturns: a notes://showNote?identifier=<uuid> URL that opens the note in Notes.app on iOS and macOS.\nDo not use when: you only need the note's CoreData id (get-note-by-id) or want to reveal the note on screen (show-note).\nNote: requires macOS 12+; returns an error on older systems.",
|
|
42292
|
+
inputSchema: {
|
|
42293
|
+
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42294
|
+
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
42295
|
+
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
|
|
42296
|
+
},
|
|
42297
|
+
outputSchema: {
|
|
42298
|
+
id: external_exports.string().optional(),
|
|
42299
|
+
title: external_exports.string().optional(),
|
|
42300
|
+
url: external_exports.string().optional()
|
|
42301
|
+
}
|
|
42302
|
+
},
|
|
42303
|
+
withErrorHandling(({ id, title, account }) => {
|
|
42304
|
+
if (id) {
|
|
42305
|
+
const note2 = notesManager.getNoteById(id);
|
|
42306
|
+
if (!note2) {
|
|
42307
|
+
return errorResponse(`Note with ID "${id}" not found`);
|
|
42308
|
+
}
|
|
42309
|
+
if (note2.passwordProtected) {
|
|
42310
|
+
return errorResponse(
|
|
42311
|
+
`Note "${note2.title}" is password-protected. Unlock it in Notes.app first.`
|
|
42312
|
+
);
|
|
42313
|
+
}
|
|
42314
|
+
const url2 = notesManager.getNoteLinkById(id);
|
|
42315
|
+
if (!url2) {
|
|
42316
|
+
return errorResponse(
|
|
42317
|
+
`Failed to get note link for "${note2.title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the app that launches the server, fully quit and relaunch, then run the doctor tool. See: ${FULL_DISK_ACCESS_GUIDE_URL}. (On macOS 12\u201315 this also falls back to the AppleScript note link property.)`
|
|
42318
|
+
);
|
|
42319
|
+
}
|
|
42320
|
+
return successResponse(`Note link: ${url2}`, { id, title: note2.title, url: url2 });
|
|
42321
|
+
}
|
|
42322
|
+
if (!title) {
|
|
42323
|
+
return errorResponse("Either 'id' or 'title' is required");
|
|
42324
|
+
}
|
|
42325
|
+
const note = notesManager.getNoteDetails(title, account);
|
|
42326
|
+
if (!note) {
|
|
42327
|
+
return errorResponse(
|
|
42328
|
+
`Note "${title}" not found. Use search-notes to find notes, then use the note's ID for reliable operations.`
|
|
42329
|
+
);
|
|
42330
|
+
}
|
|
42331
|
+
if (note.passwordProtected) {
|
|
42332
|
+
return errorResponse(`Note "${title}" is password-protected. Unlock it in Notes.app first.`);
|
|
42333
|
+
}
|
|
42334
|
+
const url = notesManager.getNoteLink(title, account);
|
|
42335
|
+
if (!url) {
|
|
42336
|
+
return errorResponse(
|
|
42337
|
+
`Failed to get note link for "${title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the app that launches the server, fully quit and relaunch, then run the doctor tool. See: ${FULL_DISK_ACCESS_GUIDE_URL}. (On macOS 12\u201315 this also falls back to the AppleScript note link property.)`
|
|
42338
|
+
);
|
|
42339
|
+
}
|
|
42340
|
+
return successResponse(`Note link: ${url}`, { title, url });
|
|
42341
|
+
}, "Error getting note link")
|
|
42342
|
+
);
|
|
42156
42343
|
server.registerTool(
|
|
42157
42344
|
"show-folder",
|
|
42158
42345
|
{
|
|
@@ -42269,6 +42456,121 @@ server.registerTool(
|
|
|
42269
42456
|
});
|
|
42270
42457
|
}, "Error updating note")
|
|
42271
42458
|
);
|
|
42459
|
+
server.registerTool(
|
|
42460
|
+
"append-to-note",
|
|
42461
|
+
{
|
|
42462
|
+
description: "Use when: adding content to an existing note without replacing it, by id (preferred) or title.\nReturns: confirmation with the note id and title.\nDo not use when: creating a new note (create-note) or replacing the entire body (update-note).\nSafety: reads the existing body first, concatenates, then writes back. Run list-attachments first if the note may hold embedded files \u2014 a full-body rewrite can drop attachments.",
|
|
42463
|
+
inputSchema: {
|
|
42464
|
+
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42465
|
+
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
42466
|
+
content: external_exports.string().min(1, "Content to append is required").max(MAX.CONTENT).describe("Text to append to the note body"),
|
|
42467
|
+
position: external_exports.enum(["after", "before"]).optional().default("after").describe(
|
|
42468
|
+
"Where to insert: 'after' appends to the end (default), 'before' prepends to the start"
|
|
42469
|
+
),
|
|
42470
|
+
separator: external_exports.string().max(20).optional().default("\n\n").describe("String placed between existing content and new content (default: two newlines)"),
|
|
42471
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'"),
|
|
42472
|
+
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
|
|
42473
|
+
},
|
|
42474
|
+
outputSchema: {
|
|
42475
|
+
ok: external_exports.boolean().optional(),
|
|
42476
|
+
id: external_exports.string().optional(),
|
|
42477
|
+
title: external_exports.string().optional(),
|
|
42478
|
+
shared: external_exports.boolean().optional()
|
|
42479
|
+
}
|
|
42480
|
+
},
|
|
42481
|
+
withErrorHandling(
|
|
42482
|
+
({
|
|
42483
|
+
id,
|
|
42484
|
+
title,
|
|
42485
|
+
content,
|
|
42486
|
+
position = "after",
|
|
42487
|
+
separator = "\n\n",
|
|
42488
|
+
format = "plaintext",
|
|
42489
|
+
account
|
|
42490
|
+
}) => {
|
|
42491
|
+
const contentToHtml = (text) => {
|
|
42492
|
+
if (format === "html") return text;
|
|
42493
|
+
return text.split("\n").map((line) => {
|
|
42494
|
+
const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
42495
|
+
return `<div>${escaped || "<br>"}</div>`;
|
|
42496
|
+
}).join("");
|
|
42497
|
+
};
|
|
42498
|
+
const separatorToHtml = (sep2) => {
|
|
42499
|
+
if (format === "html") return sep2;
|
|
42500
|
+
if (sep2 === "\n\n") return "<div><br></div>";
|
|
42501
|
+
const escaped = sep2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
42502
|
+
return `<div>${escaped}</div>`;
|
|
42503
|
+
};
|
|
42504
|
+
if (id) {
|
|
42505
|
+
const note2 = notesManager.getNoteById(id);
|
|
42506
|
+
if (!note2) {
|
|
42507
|
+
return errorResponse(`Note with ID "${id}" not found`);
|
|
42508
|
+
}
|
|
42509
|
+
if (note2.passwordProtected) {
|
|
42510
|
+
return errorResponse(
|
|
42511
|
+
`Note "${note2.title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
|
|
42512
|
+
);
|
|
42513
|
+
}
|
|
42514
|
+
const existingHtml2 = notesManager.getNoteContentById(id);
|
|
42515
|
+
if (existingHtml2 === null || existingHtml2 === void 0) {
|
|
42516
|
+
return errorResponse(`Failed to read content of note "${note2.title}"`);
|
|
42517
|
+
}
|
|
42518
|
+
const firstDivEnd2 = existingHtml2.indexOf("</div>");
|
|
42519
|
+
const titleDiv2 = firstDivEnd2 !== -1 ? existingHtml2.slice(0, firstDivEnd2 + 6) : "";
|
|
42520
|
+
const bodyHtml2 = firstDivEnd2 !== -1 ? existingHtml2.slice(firstDivEnd2 + 6) : existingHtml2;
|
|
42521
|
+
const newBlock2 = contentToHtml(content);
|
|
42522
|
+
const sepHtml2 = separatorToHtml(separator);
|
|
42523
|
+
const combinedBody2 = position === "before" ? titleDiv2 + newBlock2 + sepHtml2 + bodyHtml2 : titleDiv2 + bodyHtml2 + sepHtml2 + newBlock2;
|
|
42524
|
+
const success2 = notesManager.updateNoteById(id, void 0, combinedBody2, "html");
|
|
42525
|
+
if (!success2) {
|
|
42526
|
+
return errorResponse(`Failed to append to note "${note2.title}"`);
|
|
42527
|
+
}
|
|
42528
|
+
const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
|
|
42529
|
+
return successResponse(`Note appended: "${note2.title}"${sharedWarning2}`, {
|
|
42530
|
+
ok: true,
|
|
42531
|
+
id,
|
|
42532
|
+
title: note2.title,
|
|
42533
|
+
shared: note2.shared ?? false
|
|
42534
|
+
});
|
|
42535
|
+
}
|
|
42536
|
+
if (!title) {
|
|
42537
|
+
return errorResponse("Either 'id' or 'title' is required");
|
|
42538
|
+
}
|
|
42539
|
+
const note = notesManager.getNoteDetails(title, account);
|
|
42540
|
+
if (!note) {
|
|
42541
|
+
return errorResponse(
|
|
42542
|
+
`Note "${title}" not found. Use search-notes to find notes, then use the note's ID for reliable operations.`
|
|
42543
|
+
);
|
|
42544
|
+
}
|
|
42545
|
+
if (note.passwordProtected) {
|
|
42546
|
+
return errorResponse(
|
|
42547
|
+
`Note "${title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
|
|
42548
|
+
);
|
|
42549
|
+
}
|
|
42550
|
+
const existingHtml = notesManager.getNoteContent(title, account);
|
|
42551
|
+
if (existingHtml === null || existingHtml === void 0) {
|
|
42552
|
+
return errorResponse(`Failed to read content of note "${title}"`);
|
|
42553
|
+
}
|
|
42554
|
+
const firstDivEnd = existingHtml.indexOf("</div>");
|
|
42555
|
+
const titleDiv = firstDivEnd !== -1 ? existingHtml.slice(0, firstDivEnd + 6) : "";
|
|
42556
|
+
const bodyHtml = firstDivEnd !== -1 ? existingHtml.slice(firstDivEnd + 6) : existingHtml;
|
|
42557
|
+
const newBlock = contentToHtml(content);
|
|
42558
|
+
const sepHtml = separatorToHtml(separator);
|
|
42559
|
+
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
42560
|
+
const success = notesManager.updateNote(title, void 0, combinedBody, account, "html");
|
|
42561
|
+
if (!success) {
|
|
42562
|
+
return errorResponse(`Failed to append to note "${title}"`);
|
|
42563
|
+
}
|
|
42564
|
+
const sharedWarning = note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
|
|
42565
|
+
return successResponse(`Note appended: "${title}"${sharedWarning}`, {
|
|
42566
|
+
ok: true,
|
|
42567
|
+
title,
|
|
42568
|
+
shared: note.shared ?? false
|
|
42569
|
+
});
|
|
42570
|
+
},
|
|
42571
|
+
"Error appending to note"
|
|
42572
|
+
)
|
|
42573
|
+
);
|
|
42272
42574
|
server.registerTool(
|
|
42273
42575
|
"delete-note",
|
|
42274
42576
|
{
|
package/package.json
CHANGED