apple-notes-mcp 2.7.4 → 2.8.0
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 +43 -65
- package/build/index.js +446 -471
- package/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md +32 -8
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -3660,49 +3660,49 @@ var require_fast_uri = __commonJS({
|
|
|
3660
3660
|
schemelessOptions.skipEscape = true;
|
|
3661
3661
|
return serialize(resolved, schemelessOptions);
|
|
3662
3662
|
}
|
|
3663
|
-
function resolveComponent(base,
|
|
3663
|
+
function resolveComponent(base, relative2, options, skipNormalization) {
|
|
3664
3664
|
const target = {};
|
|
3665
3665
|
if (!skipNormalization) {
|
|
3666
3666
|
base = parse3(serialize(base, options), options);
|
|
3667
|
-
|
|
3667
|
+
relative2 = parse3(serialize(relative2, options), options);
|
|
3668
3668
|
}
|
|
3669
3669
|
options = options || {};
|
|
3670
|
-
if (!options.tolerant &&
|
|
3671
|
-
target.scheme =
|
|
3672
|
-
target.userinfo =
|
|
3673
|
-
target.host =
|
|
3674
|
-
target.port =
|
|
3675
|
-
target.path = removeDotSegments(
|
|
3676
|
-
target.query =
|
|
3670
|
+
if (!options.tolerant && relative2.scheme) {
|
|
3671
|
+
target.scheme = relative2.scheme;
|
|
3672
|
+
target.userinfo = relative2.userinfo;
|
|
3673
|
+
target.host = relative2.host;
|
|
3674
|
+
target.port = relative2.port;
|
|
3675
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
3676
|
+
target.query = relative2.query;
|
|
3677
3677
|
} else {
|
|
3678
|
-
if (
|
|
3679
|
-
target.userinfo =
|
|
3680
|
-
target.host =
|
|
3681
|
-
target.port =
|
|
3682
|
-
target.path = removeDotSegments(
|
|
3683
|
-
target.query =
|
|
3678
|
+
if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
|
|
3679
|
+
target.userinfo = relative2.userinfo;
|
|
3680
|
+
target.host = relative2.host;
|
|
3681
|
+
target.port = relative2.port;
|
|
3682
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
3683
|
+
target.query = relative2.query;
|
|
3684
3684
|
} else {
|
|
3685
|
-
if (!
|
|
3685
|
+
if (!relative2.path) {
|
|
3686
3686
|
target.path = base.path;
|
|
3687
|
-
if (
|
|
3688
|
-
target.query =
|
|
3687
|
+
if (relative2.query !== void 0) {
|
|
3688
|
+
target.query = relative2.query;
|
|
3689
3689
|
} else {
|
|
3690
3690
|
target.query = base.query;
|
|
3691
3691
|
}
|
|
3692
3692
|
} else {
|
|
3693
|
-
if (
|
|
3694
|
-
target.path = removeDotSegments(
|
|
3693
|
+
if (relative2.path[0] === "/") {
|
|
3694
|
+
target.path = removeDotSegments(relative2.path);
|
|
3695
3695
|
} else {
|
|
3696
3696
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3697
|
-
target.path = "/" +
|
|
3697
|
+
target.path = "/" + relative2.path;
|
|
3698
3698
|
} else if (!base.path) {
|
|
3699
|
-
target.path =
|
|
3699
|
+
target.path = relative2.path;
|
|
3700
3700
|
} else {
|
|
3701
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3701
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
|
|
3702
3702
|
}
|
|
3703
3703
|
target.path = removeDotSegments(target.path);
|
|
3704
3704
|
}
|
|
3705
|
-
target.query =
|
|
3705
|
+
target.query = relative2.query;
|
|
3706
3706
|
}
|
|
3707
3707
|
target.userinfo = base.userinfo;
|
|
3708
3708
|
target.host = base.host;
|
|
@@ -3710,7 +3710,7 @@ var require_fast_uri = __commonJS({
|
|
|
3710
3710
|
}
|
|
3711
3711
|
target.scheme = base.scheme;
|
|
3712
3712
|
}
|
|
3713
|
-
target.fragment =
|
|
3713
|
+
target.fragment = relative2.fragment;
|
|
3714
3714
|
return target;
|
|
3715
3715
|
}
|
|
3716
3716
|
function equal(uriA, uriB, options) {
|
|
@@ -11912,9 +11912,9 @@ var require_URL = __commonJS({
|
|
|
11912
11912
|
},
|
|
11913
11913
|
// See: http://tools.ietf.org/html/rfc3986#section-5.2
|
|
11914
11914
|
// and https://url.spec.whatwg.org/#constructors
|
|
11915
|
-
resolve: function(
|
|
11915
|
+
resolve: function(relative2) {
|
|
11916
11916
|
var base = this;
|
|
11917
|
-
var r = new URL2(
|
|
11917
|
+
var r = new URL2(relative2);
|
|
11918
11918
|
var t = new URL2();
|
|
11919
11919
|
if (r.scheme !== void 0) {
|
|
11920
11920
|
t.scheme = r.scheme;
|
|
@@ -24193,14 +24193,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
24193
24193
|
} else if (node.nodeType === 1) {
|
|
24194
24194
|
replacement = replacementForNode.call(self, node);
|
|
24195
24195
|
}
|
|
24196
|
-
return
|
|
24196
|
+
return join7(output, replacement);
|
|
24197
24197
|
}, "");
|
|
24198
24198
|
}
|
|
24199
24199
|
function postProcess(output) {
|
|
24200
24200
|
var self = this;
|
|
24201
24201
|
this.rules.forEach(function(rule) {
|
|
24202
24202
|
if (typeof rule.append === "function") {
|
|
24203
|
-
output =
|
|
24203
|
+
output = join7(output, rule.append(self.options));
|
|
24204
24204
|
}
|
|
24205
24205
|
});
|
|
24206
24206
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -24212,7 +24212,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
24212
24212
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
24213
24213
|
return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
|
|
24214
24214
|
}
|
|
24215
|
-
function
|
|
24215
|
+
function join7(output, replacement) {
|
|
24216
24216
|
var s1 = trimTrailingNewlines(output);
|
|
24217
24217
|
var s2 = trimLeadingNewlines(replacement);
|
|
24218
24218
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -39204,8 +39204,17 @@ function getChecklistItems(noteId) {
|
|
|
39204
39204
|
}
|
|
39205
39205
|
|
|
39206
39206
|
// src/utils/attachmentFs.ts
|
|
39207
|
-
import {
|
|
39208
|
-
|
|
39207
|
+
import {
|
|
39208
|
+
existsSync as existsSync2,
|
|
39209
|
+
lstatSync,
|
|
39210
|
+
mkdirSync,
|
|
39211
|
+
mkdtempSync,
|
|
39212
|
+
readFileSync,
|
|
39213
|
+
realpathSync,
|
|
39214
|
+
rmSync,
|
|
39215
|
+
statSync
|
|
39216
|
+
} from "fs";
|
|
39217
|
+
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
39209
39218
|
import { homedir as homedir2, tmpdir } from "os";
|
|
39210
39219
|
function allowedSaveRoots() {
|
|
39211
39220
|
return [
|
|
@@ -39217,17 +39226,79 @@ function allowedSaveRoots() {
|
|
|
39217
39226
|
"/private/tmp"
|
|
39218
39227
|
];
|
|
39219
39228
|
}
|
|
39220
|
-
function
|
|
39229
|
+
function canonicalize(path4) {
|
|
39230
|
+
return realpathSync.native(path4);
|
|
39231
|
+
}
|
|
39232
|
+
function isWithinRoots(candidate, roots) {
|
|
39233
|
+
return roots.some((root) => {
|
|
39234
|
+
const base = root.endsWith(sep) ? root.slice(0, -1) : root;
|
|
39235
|
+
return candidate === base || candidate.startsWith(base + sep);
|
|
39236
|
+
});
|
|
39237
|
+
}
|
|
39238
|
+
function canonicalRoots(roots) {
|
|
39239
|
+
const canonical = [];
|
|
39240
|
+
for (const root of roots) {
|
|
39241
|
+
const abs = resolve(root);
|
|
39242
|
+
let resolved;
|
|
39243
|
+
try {
|
|
39244
|
+
resolved = canonicalize(abs);
|
|
39245
|
+
} catch {
|
|
39246
|
+
resolved = abs;
|
|
39247
|
+
}
|
|
39248
|
+
if (!canonical.includes(resolved)) canonical.push(resolved);
|
|
39249
|
+
}
|
|
39250
|
+
return canonical;
|
|
39251
|
+
}
|
|
39252
|
+
function entryExists(path4) {
|
|
39253
|
+
try {
|
|
39254
|
+
lstatSync(path4);
|
|
39255
|
+
return true;
|
|
39256
|
+
} catch (e) {
|
|
39257
|
+
const code = e.code;
|
|
39258
|
+
return !(code === "ENOENT" || code === "ENOTDIR");
|
|
39259
|
+
}
|
|
39260
|
+
}
|
|
39261
|
+
function deepestExistingAncestor(abs) {
|
|
39262
|
+
let current = abs;
|
|
39263
|
+
for (; ; ) {
|
|
39264
|
+
if (entryExists(current)) return current;
|
|
39265
|
+
const parent = dirname(current);
|
|
39266
|
+
if (parent === current) return current;
|
|
39267
|
+
current = parent;
|
|
39268
|
+
}
|
|
39269
|
+
}
|
|
39270
|
+
function ensureParentDir(abs, roots = allowedSaveRoots()) {
|
|
39271
|
+
assertSafeSavePath(abs, roots);
|
|
39221
39272
|
mkdirSync(dirname(abs), { recursive: true });
|
|
39222
39273
|
}
|
|
39223
39274
|
function assertSafeSavePath(p, roots = allowedSaveRoots()) {
|
|
39224
39275
|
if (!p || !p.trim()) throw new Error("A destination path is required.");
|
|
39225
39276
|
if (!isAbsolute(p)) throw new Error(`Destination path must be absolute: "${p}"`);
|
|
39226
39277
|
const abs = resolve(p);
|
|
39227
|
-
|
|
39228
|
-
if (!ok) {
|
|
39278
|
+
if (!isWithinRoots(abs, roots)) {
|
|
39229
39279
|
throw new Error(`Refusing to write outside allowed locations (home, temp, /Volumes): "${abs}"`);
|
|
39230
39280
|
}
|
|
39281
|
+
const ancestor = deepestExistingAncestor(abs);
|
|
39282
|
+
if (ancestor === abs && lstatSync(abs).isSymbolicLink()) {
|
|
39283
|
+
throw new Error(`Refusing to write to the symbolic link "${abs}".`);
|
|
39284
|
+
}
|
|
39285
|
+
let canonicalAncestor;
|
|
39286
|
+
try {
|
|
39287
|
+
canonicalAncestor = canonicalize(ancestor);
|
|
39288
|
+
} catch {
|
|
39289
|
+
throw new Error(`Destination path cannot be resolved: "${abs}"`);
|
|
39290
|
+
}
|
|
39291
|
+
const suffix = relative(ancestor, abs);
|
|
39292
|
+
if (suffix.split(sep).includes("..")) {
|
|
39293
|
+
throw new Error(`Refusing to write outside allowed locations (home, temp, /Volumes): "${abs}"`);
|
|
39294
|
+
}
|
|
39295
|
+
const canonicalDest = suffix ? join2(canonicalAncestor, suffix) : canonicalAncestor;
|
|
39296
|
+
const allowed = canonicalRoots(roots);
|
|
39297
|
+
if (!isWithinRoots(canonicalAncestor, allowed) || !isWithinRoots(canonicalDest, allowed)) {
|
|
39298
|
+
throw new Error(
|
|
39299
|
+
`Refusing to write outside allowed locations (home, temp, /Volumes): "${abs}" resolves to "${canonicalDest}" through a symbolic link.`
|
|
39300
|
+
);
|
|
39301
|
+
}
|
|
39231
39302
|
return abs;
|
|
39232
39303
|
}
|
|
39233
39304
|
var DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
@@ -39272,7 +39343,7 @@ function cleanupTempDir(dir) {
|
|
|
39272
39343
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
39273
39344
|
import { existsSync as existsSync3 } from "fs";
|
|
39274
39345
|
import { homedir as homedir3 } from "os";
|
|
39275
|
-
import { join as
|
|
39346
|
+
import { join as join3 } from "path";
|
|
39276
39347
|
var FIELD_SEP = "";
|
|
39277
39348
|
var RECORD_SEP = "";
|
|
39278
39349
|
var AS_FIELD_SEP = "(ASCII character 31)";
|
|
@@ -39324,6 +39395,15 @@ function sanitizeId(id) {
|
|
|
39324
39395
|
}
|
|
39325
39396
|
return escapeForAppleScript(id);
|
|
39326
39397
|
}
|
|
39398
|
+
function sanitizeNoteId(id) {
|
|
39399
|
+
const noteIdPattern = /^x-coredata:\/\/[0-9A-Fa-f-]+\/ICNote\/p\d+$/;
|
|
39400
|
+
if (!noteIdPattern.test(id)) {
|
|
39401
|
+
throw new Error(
|
|
39402
|
+
`Invalid note ID format: "${id.substring(0, 80)}". Expected canonical Apple Note ID (x-coredata://.../ICNote/p...).`
|
|
39403
|
+
);
|
|
39404
|
+
}
|
|
39405
|
+
return escapeForAppleScript(id);
|
|
39406
|
+
}
|
|
39327
39407
|
function sanitizeAccountName(account) {
|
|
39328
39408
|
validateLength(account, MAX_ACCOUNT_LENGTH, "Account name");
|
|
39329
39409
|
return escapePlainStringForAppleScript(account);
|
|
@@ -39464,7 +39544,7 @@ function getNoteLinkFromDB(coreDataId) {
|
|
|
39464
39544
|
const match = coreDataId.match(/\/p(\d+)$/);
|
|
39465
39545
|
if (!match) return null;
|
|
39466
39546
|
const pk = parseInt(match[1], 10);
|
|
39467
|
-
const dbPath =
|
|
39547
|
+
const dbPath = join3(homedir3(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
|
|
39468
39548
|
if (!existsSync3(dbPath)) return null;
|
|
39469
39549
|
try {
|
|
39470
39550
|
const { DatabaseSync } = __require("node:sqlite");
|
|
@@ -39616,10 +39696,19 @@ var AppleNotesManager = class {
|
|
|
39616
39696
|
}
|
|
39617
39697
|
const rawOutput = result.output.trim();
|
|
39618
39698
|
const noteId = extractCoreDataId(rawOutput, "note") || rawOutput;
|
|
39699
|
+
if (!noteId) {
|
|
39700
|
+
console.error(`Created note "${title}" but Notes.app returned no canonical note ID`);
|
|
39701
|
+
return null;
|
|
39702
|
+
}
|
|
39703
|
+
try {
|
|
39704
|
+
sanitizeNoteId(noteId);
|
|
39705
|
+
} catch {
|
|
39706
|
+
console.error(`Created note "${title}" but Notes.app returned an invalid note ID`);
|
|
39707
|
+
return null;
|
|
39708
|
+
}
|
|
39619
39709
|
const now = /* @__PURE__ */ new Date();
|
|
39620
39710
|
return {
|
|
39621
|
-
id: noteId
|
|
39622
|
-
// Use real ID, fallback to unique temp ID
|
|
39711
|
+
id: noteId,
|
|
39623
39712
|
title,
|
|
39624
39713
|
content,
|
|
39625
39714
|
tags,
|
|
@@ -39943,141 +40032,75 @@ var AppleNotesManager = class {
|
|
|
39943
40032
|
};
|
|
39944
40033
|
}
|
|
39945
40034
|
/**
|
|
39946
|
-
*
|
|
39947
|
-
*
|
|
39948
|
-
* Note: This permanently deletes the note. It may be recoverable
|
|
39949
|
-
* from the "Recently Deleted" folder in Notes.app.
|
|
40035
|
+
* Replaces one exact note body only if the body is still the snapshot the
|
|
40036
|
+
* caller reviewed and the note has no attachments.
|
|
39950
40037
|
*
|
|
39951
|
-
*
|
|
39952
|
-
*
|
|
39953
|
-
*
|
|
40038
|
+
* Both guards and the write execute inside one AppleScript. This closes the
|
|
40039
|
+
* race that would exist if JavaScript checked the note and then issued a
|
|
40040
|
+
* separate unconditional `set body` command.
|
|
39954
40041
|
*/
|
|
39955
|
-
|
|
39956
|
-
const
|
|
39957
|
-
const safeTitle = escapePlainStringForAppleScript(title);
|
|
39958
|
-
const deleteCommand = `delete note "${safeTitle}"`;
|
|
39959
|
-
const script = buildAccountScopedScript({ account: targetAccount }, deleteCommand);
|
|
39960
|
-
const result = executeMutationAppleScript(script);
|
|
39961
|
-
if (!result.success) {
|
|
39962
|
-
throwIfAccountResolutionFailed(result.error);
|
|
39963
|
-
console.error(`Failed to delete note "${title}":`, result.error);
|
|
39964
|
-
return false;
|
|
39965
|
-
}
|
|
39966
|
-
return true;
|
|
39967
|
-
}
|
|
39968
|
-
/**
|
|
39969
|
-
* Deletes a note by its CoreData ID.
|
|
39970
|
-
*
|
|
39971
|
-
* This is more reliable than deleteNote() because IDs are unique
|
|
39972
|
-
* across all accounts, while titles can be duplicated.
|
|
39973
|
-
*
|
|
39974
|
-
* @param id - CoreData URL identifier for the note
|
|
39975
|
-
* @returns true if deletion succeeded, false otherwise
|
|
39976
|
-
*/
|
|
39977
|
-
deleteNoteById(id) {
|
|
39978
|
-
const safeId = sanitizeId(id);
|
|
39979
|
-
const deleteCommand = `delete note id "${safeId}"`;
|
|
39980
|
-
const script = buildAppLevelScript(deleteCommand);
|
|
39981
|
-
const result = executeMutationAppleScript(script);
|
|
39982
|
-
if (!result.success) {
|
|
39983
|
-
console.error(`Failed to delete note with ID "${id}":`, result.error);
|
|
39984
|
-
return false;
|
|
39985
|
-
}
|
|
39986
|
-
return true;
|
|
39987
|
-
}
|
|
39988
|
-
/**
|
|
39989
|
-
* Updates an existing note's content and optionally its title.
|
|
39990
|
-
*
|
|
39991
|
-
* Apple Notes derives the title from the first line of the body,
|
|
39992
|
-
* so updating content also allows title changes. If newTitle is
|
|
39993
|
-
* not provided, the original title is preserved.
|
|
39994
|
-
*
|
|
39995
|
-
* When format is 'html', newTitle is ignored — the caller must include
|
|
39996
|
-
* the title in the HTML content.
|
|
39997
|
-
*
|
|
39998
|
-
* Note: Password-protected notes will fail with an AppleScript error.
|
|
39999
|
-
* Callers should check for password protection beforehand using
|
|
40000
|
-
* getNoteDetails() or isNotePasswordProtected().
|
|
40001
|
-
*
|
|
40002
|
-
* @param title - Current title of the note to update
|
|
40003
|
-
* @param newTitle - New title (optional, keeps existing if not provided; ignored in html format)
|
|
40004
|
-
* @param newContent - New content for the note body
|
|
40005
|
-
* @param account - Account containing the note (defaults to Notes.app's default account)
|
|
40006
|
-
* @param format - Content format: "plaintext" wraps in div tags (default), "html" uses content as-is
|
|
40007
|
-
* @returns true if update succeeded, false otherwise
|
|
40008
|
-
*/
|
|
40009
|
-
updateNote(title, newTitle, newContent, account, format = "plaintext") {
|
|
40042
|
+
updateNoteByIdIfUnchanged(id, currentTitle, expectedBody, newTitle, newContent, format = "plaintext") {
|
|
40043
|
+
const safeId = sanitizeNoteId(id);
|
|
40010
40044
|
if (newTitle) validateLength(newTitle, MAX_TITLE_LENGTH, "Note title");
|
|
40011
40045
|
validateLength(newContent, MAX_CONTENT_LENGTH, "Note content");
|
|
40012
|
-
|
|
40013
|
-
|
|
40014
|
-
let fullBody;
|
|
40046
|
+
validateLength(expectedBody, MAX_CONTENT_LENGTH, "Expected note content");
|
|
40047
|
+
let writtenBody;
|
|
40015
40048
|
if (format === "html") {
|
|
40016
|
-
|
|
40049
|
+
writtenBody = newContent;
|
|
40017
40050
|
} else {
|
|
40018
|
-
const effectiveTitle = newTitle ||
|
|
40019
|
-
const
|
|
40020
|
-
|
|
40021
|
-
fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
|
|
40051
|
+
const effectiveTitle = newTitle || currentTitle;
|
|
40052
|
+
const encodePlaintext = (value) => escapeForAppleScript(value).replace(/\\"/g, '"');
|
|
40053
|
+
writtenBody = `<div>${encodePlaintext(effectiveTitle)}</div><div>${encodePlaintext(newContent)}</div>`;
|
|
40022
40054
|
}
|
|
40023
|
-
const
|
|
40024
|
-
const
|
|
40055
|
+
const safeExpectedBody = escapeHtmlForAppleScript(expectedBody);
|
|
40056
|
+
const safeWrittenBody = escapeHtmlForAppleScript(writtenBody);
|
|
40057
|
+
const script = buildAppLevelScript(`
|
|
40058
|
+
set noteRef to note id "${safeId}"
|
|
40059
|
+
if (count of attachments of noteRef) is greater than 0 then return "SAFETY_ATTACHMENTS"
|
|
40060
|
+
set currentBody to body of noteRef
|
|
40061
|
+
considering case
|
|
40062
|
+
if currentBody is not "${safeExpectedBody}" and currentBody is not "${safeExpectedBody}" & linefeed then return "SAFETY_CONFLICT"
|
|
40063
|
+
set body of noteRef to "${safeWrittenBody}"
|
|
40064
|
+
end considering
|
|
40065
|
+
return "SAFETY_UPDATED"
|
|
40066
|
+
`);
|
|
40025
40067
|
const result = executeMutationAppleScript(script);
|
|
40026
40068
|
if (!result.success) {
|
|
40027
|
-
|
|
40028
|
-
|
|
40029
|
-
return false;
|
|
40069
|
+
console.error(`Failed guarded update for note ID "${id}":`, result.error);
|
|
40070
|
+
return { status: "failed" };
|
|
40030
40071
|
}
|
|
40031
|
-
|
|
40072
|
+
const status = result.output.trim();
|
|
40073
|
+
if (status === "SAFETY_CONFLICT") return { status: "conflict" };
|
|
40074
|
+
if (status === "SAFETY_ATTACHMENTS") return { status: "attachments" };
|
|
40075
|
+
if (status !== "SAFETY_UPDATED") return { status: "failed" };
|
|
40076
|
+
return { status: "updated", writtenBody };
|
|
40032
40077
|
}
|
|
40033
40078
|
/**
|
|
40034
|
-
*
|
|
40035
|
-
*
|
|
40036
|
-
*
|
|
40037
|
-
* while titles can be duplicated.
|
|
40038
|
-
*
|
|
40039
|
-
* When format is 'html', newTitle is ignored — the caller must include
|
|
40040
|
-
* the title in the HTML content.
|
|
40041
|
-
*
|
|
40042
|
-
* Note: Password-protected notes will fail with an AppleScript error.
|
|
40043
|
-
* Callers should check for password protection beforehand using
|
|
40044
|
-
* getNoteById() or isNotePasswordProtectedById().
|
|
40045
|
-
*
|
|
40046
|
-
* @param id - CoreData URL identifier for the note
|
|
40047
|
-
* @param newTitle - New title (optional, keeps existing if not provided; ignored in html format)
|
|
40048
|
-
* @param newContent - New content for the note body
|
|
40049
|
-
* @param format - Content format: "plaintext" wraps in div tags (default), "html" uses content as-is
|
|
40050
|
-
* @returns true if update succeeded, false otherwise
|
|
40079
|
+
* Deletes one exact note only when its complete body still matches the body
|
|
40080
|
+
* the caller reviewed. The comparison and delete are one AppleScript action,
|
|
40081
|
+
* so a concurrent edit cannot slip between the guard and deletion.
|
|
40051
40082
|
*/
|
|
40052
|
-
|
|
40053
|
-
|
|
40054
|
-
validateLength(
|
|
40055
|
-
|
|
40056
|
-
|
|
40057
|
-
|
|
40058
|
-
|
|
40059
|
-
|
|
40060
|
-
|
|
40061
|
-
|
|
40062
|
-
|
|
40063
|
-
|
|
40064
|
-
|
|
40065
|
-
}
|
|
40066
|
-
effectiveTitle = note.title;
|
|
40067
|
-
}
|
|
40068
|
-
const safeEffectiveTitle = escapeForAppleScript(effectiveTitle);
|
|
40069
|
-
const safeContent = escapeForAppleScript(newContent);
|
|
40070
|
-
fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
|
|
40071
|
-
}
|
|
40072
|
-
const safeId = sanitizeId(id);
|
|
40073
|
-
const updateCommand = `set body of note id "${safeId}" to "${fullBody}"`;
|
|
40074
|
-
const script = buildAppLevelScript(updateCommand);
|
|
40083
|
+
deleteNoteByIdIfUnchanged(id, expectedBody) {
|
|
40084
|
+
const safeId = sanitizeNoteId(id);
|
|
40085
|
+
validateLength(expectedBody, MAX_CONTENT_LENGTH, "Expected note content");
|
|
40086
|
+
const safeExpectedBody = escapeHtmlForAppleScript(expectedBody);
|
|
40087
|
+
const script = buildAppLevelScript(`
|
|
40088
|
+
set noteRef to note id "${safeId}"
|
|
40089
|
+
set currentBody to body of noteRef
|
|
40090
|
+
considering case
|
|
40091
|
+
if currentBody is not "${safeExpectedBody}" and currentBody is not "${safeExpectedBody}" & linefeed then return "SAFETY_CONFLICT"
|
|
40092
|
+
delete noteRef
|
|
40093
|
+
end considering
|
|
40094
|
+
return "SAFETY_DELETED"
|
|
40095
|
+
`);
|
|
40075
40096
|
const result = executeMutationAppleScript(script);
|
|
40076
40097
|
if (!result.success) {
|
|
40077
|
-
console.error(`Failed
|
|
40078
|
-
return
|
|
40098
|
+
console.error(`Failed guarded delete for note ID "${id}":`, result.error);
|
|
40099
|
+
return { status: "failed" };
|
|
40079
40100
|
}
|
|
40080
|
-
|
|
40101
|
+
const status = result.output.trim();
|
|
40102
|
+
if (status === "SAFETY_CONFLICT") return { status: "conflict" };
|
|
40103
|
+
return status === "SAFETY_DELETED" ? { status: "deleted" } : { status: "failed" };
|
|
40081
40104
|
}
|
|
40082
40105
|
/**
|
|
40083
40106
|
* Builds the AppleScript body for a bulk note listing.
|
|
@@ -40461,32 +40484,6 @@ var AppleNotesManager = class {
|
|
|
40461
40484
|
}
|
|
40462
40485
|
return true;
|
|
40463
40486
|
}
|
|
40464
|
-
/**
|
|
40465
|
-
* Moves a note to a different folder, looked up by title.
|
|
40466
|
-
*
|
|
40467
|
-
* Uses Notes.app's native `move` command (the same one `batchMoveNotes`
|
|
40468
|
-
* uses), which relocates the note in place — preserving its identity, id,
|
|
40469
|
-
* creation date, AND all embedded attachments (files/images/PDFs/scans/audio).
|
|
40470
|
-
* The previous copy-then-delete implementation rebuilt the note from its body
|
|
40471
|
-
* HTML, which silently dropped attachments and reset the note's identity.
|
|
40472
|
-
*
|
|
40473
|
-
* The note is resolved to its id first (titles can be duplicated), then moved
|
|
40474
|
-
* by id so the title-based and id-based paths share the same native move.
|
|
40475
|
-
*
|
|
40476
|
-
* @param title - Title of the note to move
|
|
40477
|
-
* @param destinationFolder - Name of the folder to move to (must already exist)
|
|
40478
|
-
* @param account - Account containing the note (defaults to Notes.app's default account)
|
|
40479
|
-
* @returns true if the move succeeded, false otherwise
|
|
40480
|
-
*/
|
|
40481
|
-
moveNote(title, destinationFolder, account) {
|
|
40482
|
-
const targetAccount = this.resolveAccount(account);
|
|
40483
|
-
const originalNote = this.getNoteDetails(title, targetAccount);
|
|
40484
|
-
if (!originalNote) {
|
|
40485
|
-
console.error(`Cannot move note "${title}": note not found`);
|
|
40486
|
-
return false;
|
|
40487
|
-
}
|
|
40488
|
-
return this.moveNoteById(originalNote.id, destinationFolder, targetAccount);
|
|
40489
|
-
}
|
|
40490
40487
|
/**
|
|
40491
40488
|
* Moves a note to a different folder by its CoreData ID.
|
|
40492
40489
|
*
|
|
@@ -40502,13 +40499,17 @@ var AppleNotesManager = class {
|
|
|
40502
40499
|
*/
|
|
40503
40500
|
moveNoteById(id, destinationFolder, account) {
|
|
40504
40501
|
const targetAccount = this.resolveAccount(account);
|
|
40505
|
-
const safeId =
|
|
40502
|
+
const safeId = sanitizeNoteId(id);
|
|
40506
40503
|
const destFolderRef = `${buildFolderReference(destinationFolder)} of ${AS_ACCOUNT_REF}`;
|
|
40507
40504
|
const moveCommand = `
|
|
40508
40505
|
${buildAccountResolution(targetAccount)}
|
|
40509
40506
|
set destFolder to ${destFolderRef}
|
|
40510
40507
|
set noteRef to note id "${safeId}"
|
|
40511
40508
|
move noteRef to destFolder
|
|
40509
|
+
set movedNoteRef to note id "${safeId}"
|
|
40510
|
+
set actualFolder to container of movedNoteRef
|
|
40511
|
+
if (id of actualFolder) is not (id of destFolder) then return "SAFETY_WRONG_FOLDER"
|
|
40512
|
+
return "SAFETY_MOVED"
|
|
40512
40513
|
`;
|
|
40513
40514
|
const script = buildAppLevelScript(moveCommand);
|
|
40514
40515
|
const result = executeMutationAppleScript(script);
|
|
@@ -40520,6 +40521,12 @@ var AppleNotesManager = class {
|
|
|
40520
40521
|
);
|
|
40521
40522
|
return false;
|
|
40522
40523
|
}
|
|
40524
|
+
if (result.output.trim() !== "SAFETY_MOVED") {
|
|
40525
|
+
console.error(
|
|
40526
|
+
`Move result for note ID "${id}" did not verify destination "${destinationFolder}"`
|
|
40527
|
+
);
|
|
40528
|
+
return false;
|
|
40529
|
+
}
|
|
40523
40530
|
return true;
|
|
40524
40531
|
}
|
|
40525
40532
|
// ===========================================================================
|
|
@@ -41304,95 +41311,10 @@ var AppleNotesManager = class {
|
|
|
41304
41311
|
createBatchResult(id, success, error2) {
|
|
41305
41312
|
return error2 ? { id, success, error: error2 } : { id, success };
|
|
41306
41313
|
}
|
|
41307
|
-
/**
|
|
41308
|
-
* Deletes multiple notes by their IDs.
|
|
41309
|
-
*
|
|
41310
|
-
* Each deletion is attempted independently; failures don't stop other deletions.
|
|
41311
|
-
* Returns results for each note indicating success or failure.
|
|
41312
|
-
*
|
|
41313
|
-
* @param ids - Array of CoreData URL identifiers for notes to delete
|
|
41314
|
-
* @returns Array of results with id, success status, and optional error message
|
|
41315
|
-
*
|
|
41316
|
-
* @example
|
|
41317
|
-
* ```typescript
|
|
41318
|
-
* const results = manager.batchDeleteNotes([
|
|
41319
|
-
* "x-coredata://ABC/ICNote/p1",
|
|
41320
|
-
* "x-coredata://ABC/ICNote/p2"
|
|
41321
|
-
* ]);
|
|
41322
|
-
* results.forEach(r => {
|
|
41323
|
-
* if (r.success) console.log(`Deleted ${r.id}`);
|
|
41324
|
-
* else console.log(`Failed to delete ${r.id}: ${r.error}`);
|
|
41325
|
-
* });
|
|
41326
|
-
* ```
|
|
41327
|
-
*/
|
|
41328
|
-
batchDeleteNotes(ids) {
|
|
41329
|
-
if (ids.length === 0) return [];
|
|
41330
|
-
const results = new Array(ids.length);
|
|
41331
|
-
const runnable = [];
|
|
41332
|
-
ids.forEach((id, i) => {
|
|
41333
|
-
try {
|
|
41334
|
-
runnable.push({ index: i, safe: sanitizeId(id) });
|
|
41335
|
-
} catch (e) {
|
|
41336
|
-
results[i] = this.createBatchResult(
|
|
41337
|
-
id,
|
|
41338
|
-
false,
|
|
41339
|
-
e instanceof Error ? e.message : "Invalid note ID"
|
|
41340
|
-
);
|
|
41341
|
-
}
|
|
41342
|
-
});
|
|
41343
|
-
if (runnable.length > 0) {
|
|
41344
|
-
const idList = runnable.map((r) => `"${r.safe}"`).join(", ");
|
|
41345
|
-
const script = buildAppLevelScript(`
|
|
41346
|
-
set out to ""
|
|
41347
|
-
repeat with rawId in {${idList}}
|
|
41348
|
-
set theId to (rawId as text)
|
|
41349
|
-
set noteRef to missing value
|
|
41350
|
-
try
|
|
41351
|
-
set noteRef to note id theId
|
|
41352
|
-
end try
|
|
41353
|
-
if noteRef is missing value then
|
|
41354
|
-
set out to out & "missing" & ${AS_RECORD_SEP}
|
|
41355
|
-
else
|
|
41356
|
-
set isPw to false
|
|
41357
|
-
try
|
|
41358
|
-
set isPw to (password protected of noteRef)
|
|
41359
|
-
end try
|
|
41360
|
-
if isPw then
|
|
41361
|
-
set out to out & "pw" & ${AS_RECORD_SEP}
|
|
41362
|
-
else
|
|
41363
|
-
try
|
|
41364
|
-
delete noteRef
|
|
41365
|
-
set out to out & "ok" & ${AS_RECORD_SEP}
|
|
41366
|
-
on error
|
|
41367
|
-
set out to out & "fail" & ${AS_RECORD_SEP}
|
|
41368
|
-
end try
|
|
41369
|
-
end if
|
|
41370
|
-
end if
|
|
41371
|
-
end repeat
|
|
41372
|
-
return out
|
|
41373
|
-
`);
|
|
41374
|
-
const res = executeMutationAppleScript(script);
|
|
41375
|
-
if (!res.success) {
|
|
41376
|
-
for (const r of runnable) {
|
|
41377
|
-
results[r.index] = this.createBatchResult(
|
|
41378
|
-
ids[r.index],
|
|
41379
|
-
false,
|
|
41380
|
-
res.error ?? "Batch delete failed"
|
|
41381
|
-
);
|
|
41382
|
-
}
|
|
41383
|
-
} else {
|
|
41384
|
-
const statuses = res.output.split(RECORD_SEP).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
41385
|
-
runnable.forEach((r, k) => {
|
|
41386
|
-
results[r.index] = this.mapBatchStatus(ids[r.index], statuses[k], "delete");
|
|
41387
|
-
});
|
|
41388
|
-
}
|
|
41389
|
-
}
|
|
41390
|
-
return results;
|
|
41391
|
-
}
|
|
41392
41314
|
/**
|
|
41393
41315
|
* Maps a per-item status token emitted by a batch AppleScript loop to a
|
|
41394
41316
|
* BatchResult, preserving the human-readable error messages of the original
|
|
41395
|
-
* per-note implementation. See {@link
|
|
41317
|
+
* per-note implementation. See {@link batchMoveNotes}.
|
|
41396
41318
|
*/
|
|
41397
41319
|
mapBatchStatus(id, status, op) {
|
|
41398
41320
|
switch (status) {
|
|
@@ -41408,6 +41330,8 @@ var AppleNotesManager = class {
|
|
|
41408
41330
|
false,
|
|
41409
41331
|
op === "delete" ? "Deletion failed" : "Move failed"
|
|
41410
41332
|
);
|
|
41333
|
+
case "wrongfolder":
|
|
41334
|
+
return this.createBatchResult(id, false, "Destination folder verification failed");
|
|
41411
41335
|
default:
|
|
41412
41336
|
return this.createBatchResult(id, false, "Unknown error");
|
|
41413
41337
|
}
|
|
@@ -41472,7 +41396,13 @@ var AppleNotesManager = class {
|
|
|
41472
41396
|
else
|
|
41473
41397
|
try
|
|
41474
41398
|
move noteRef to destFolder
|
|
41475
|
-
set
|
|
41399
|
+
set movedNoteRef to note id theId
|
|
41400
|
+
set actualFolder to container of movedNoteRef
|
|
41401
|
+
if (id of actualFolder) is (id of destFolder) then
|
|
41402
|
+
set out to out & "ok" & ${AS_RECORD_SEP}
|
|
41403
|
+
else
|
|
41404
|
+
set out to out & "wrongfolder" & ${AS_RECORD_SEP}
|
|
41405
|
+
end if
|
|
41476
41406
|
on error
|
|
41477
41407
|
set out to out & "fail" & ${AS_RECORD_SEP}
|
|
41478
41408
|
end try
|
|
@@ -42108,12 +42038,12 @@ function formatDoctorReport(r) {
|
|
|
42108
42038
|
|
|
42109
42039
|
// src/services/fileConfig.ts
|
|
42110
42040
|
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
42111
|
-
import { join as
|
|
42041
|
+
import { join as join6 } from "path";
|
|
42112
42042
|
import { homedir as homedir6 } from "os";
|
|
42113
42043
|
function fileConfigPath(env = process.env) {
|
|
42114
42044
|
const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
|
|
42115
42045
|
if (override && override.trim()) return override.trim();
|
|
42116
|
-
return
|
|
42046
|
+
return join6(homedir6(), "Library", "Application Support", "apple-notes-mcp", "config.json");
|
|
42117
42047
|
}
|
|
42118
42048
|
function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
|
|
42119
42049
|
const applied = [];
|
|
@@ -42320,6 +42250,18 @@ function withJsonSchema2020_12(transport2) {
|
|
|
42320
42250
|
return transport2;
|
|
42321
42251
|
}
|
|
42322
42252
|
|
|
42253
|
+
// src/utils/noteRevision.ts
|
|
42254
|
+
import { createHash } from "node:crypto";
|
|
42255
|
+
function hashNoteContent(content) {
|
|
42256
|
+
return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
42257
|
+
}
|
|
42258
|
+
function comparableVisibleText(html) {
|
|
42259
|
+
return html.replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, " ").replace(/ | /gi, " ").replace(/"/gi, '"').replace(/'|'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&").replace(/&#(\d+);/g, (_match, codePoint) => String.fromCodePoint(Number(codePoint))).replace(
|
|
42260
|
+
/&#x([0-9a-f]+);/gi,
|
|
42261
|
+
(_match, codePoint) => String.fromCodePoint(Number.parseInt(codePoint, 16))
|
|
42262
|
+
).replace(/\s+/g, " ").trim();
|
|
42263
|
+
}
|
|
42264
|
+
|
|
42323
42265
|
// src/index.ts
|
|
42324
42266
|
loadFileConfig();
|
|
42325
42267
|
var require2 = createRequire(import.meta.url);
|
|
@@ -42370,6 +42312,28 @@ var noteTitleSchema = {
|
|
|
42370
42312
|
"Account name (defaults to Notes.app's default account; exact or unique-prefix match)"
|
|
42371
42313
|
)
|
|
42372
42314
|
};
|
|
42315
|
+
var noteIdInput = external_exports.string().min(1, "Note ID is required").max(MAX.ID).regex(
|
|
42316
|
+
/^x-coredata:\/\/[0-9A-Fa-f-]+\/ICNote\/p\d+$/,
|
|
42317
|
+
"A canonical Apple Note ID is required (x-coredata://.../ICNote/p...)"
|
|
42318
|
+
).describe("Exact CoreData note ID returned by search-notes, list-notes, or create-note");
|
|
42319
|
+
var expectedContentHashInput = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/, "expectedContentHash must come from get-note-content").describe(
|
|
42320
|
+
"Revision token returned by get-note-content for this exact ID. The mutation stops if the note changed since that read."
|
|
42321
|
+
);
|
|
42322
|
+
function readExactNoteSnapshot(id) {
|
|
42323
|
+
const note = notesManager.getNoteById(id);
|
|
42324
|
+
if (!note) return { error: `Note with ID "${id}" not found` };
|
|
42325
|
+
if (note.passwordProtected) {
|
|
42326
|
+
return {
|
|
42327
|
+
error: `Note "${note.title}" is password-protected and cannot be changed. Unlock it in Notes.app first.`
|
|
42328
|
+
};
|
|
42329
|
+
}
|
|
42330
|
+
const body = notesManager.getNoteContentById(id);
|
|
42331
|
+
if (!body) return { error: `Failed to read content of note "${note.title}"` };
|
|
42332
|
+
return { note, body, contentHash: hashNoteContent(body) };
|
|
42333
|
+
}
|
|
42334
|
+
function revisionConflictMessage(title) {
|
|
42335
|
+
return `Note "${title}" changed after it was read. Read it again and review the newer version before retrying.`;
|
|
42336
|
+
}
|
|
42373
42337
|
var folderNameSchema = {
|
|
42374
42338
|
name: external_exports.string().min(1, "Folder name is required").max(MAX.FOLDER),
|
|
42375
42339
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
@@ -42409,7 +42373,9 @@ registerTool(
|
|
|
42409
42373
|
id: external_exports.string().optional(),
|
|
42410
42374
|
title: external_exports.string().optional(),
|
|
42411
42375
|
folder: external_exports.string().optional(),
|
|
42412
|
-
account: external_exports.string().optional()
|
|
42376
|
+
account: external_exports.string().optional(),
|
|
42377
|
+
contentHash: external_exports.string().optional(),
|
|
42378
|
+
verified: external_exports.boolean().optional()
|
|
42413
42379
|
}
|
|
42414
42380
|
},
|
|
42415
42381
|
withErrorHandling(({ title, content, format = "plaintext", tags = [], folder, account }) => {
|
|
@@ -42420,13 +42386,23 @@ registerTool(
|
|
|
42420
42386
|
`Failed to create note "${title}".${target} Otherwise check that Notes.app is running and this server has Automation access (run the doctor tool).`
|
|
42421
42387
|
);
|
|
42422
42388
|
}
|
|
42389
|
+
const created = notesManager.getNoteById(note.id);
|
|
42390
|
+
const createdBody = notesManager.getNoteContentById(note.id);
|
|
42391
|
+
if (!created || !createdBody) {
|
|
42392
|
+
return errorResponse(
|
|
42393
|
+
`A note may have been created, but its exact ID could not be verified. Do not retry automatically. Returned ID: ${note.id}`
|
|
42394
|
+
);
|
|
42395
|
+
}
|
|
42396
|
+
const contentHash = hashNoteContent(createdBody);
|
|
42423
42397
|
const checklistWarning = detectChecklistAttempt(content) ?? "";
|
|
42424
42398
|
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`, {
|
|
42425
42399
|
ok: true,
|
|
42426
42400
|
id: note.id,
|
|
42427
42401
|
title: note.title,
|
|
42428
42402
|
folder,
|
|
42429
|
-
account
|
|
42403
|
+
account,
|
|
42404
|
+
contentHash,
|
|
42405
|
+
verified: true
|
|
42430
42406
|
});
|
|
42431
42407
|
}, "Error creating note")
|
|
42432
42408
|
);
|
|
@@ -42506,7 +42482,7 @@ ${noteList}${truncationNote}${syncNote}`,
|
|
|
42506
42482
|
registerTool(
|
|
42507
42483
|
"get-note-content",
|
|
42508
42484
|
{
|
|
42509
|
-
description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the note
|
|
42485
|
+
description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, and strippedImages/truncated when the body was capped.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
|
|
42510
42486
|
inputSchema: {
|
|
42511
42487
|
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42512
42488
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
@@ -42515,8 +42491,10 @@ registerTool(
|
|
|
42515
42491
|
)
|
|
42516
42492
|
},
|
|
42517
42493
|
outputSchema: {
|
|
42494
|
+
id: external_exports.string().optional(),
|
|
42518
42495
|
title: external_exports.string().optional(),
|
|
42519
42496
|
content: external_exports.string().optional(),
|
|
42497
|
+
contentHash: external_exports.string().optional(),
|
|
42520
42498
|
hashtags: external_exports.array(external_exports.string()).optional(),
|
|
42521
42499
|
/** Number of oversized inline images replaced with text placeholders. */
|
|
42522
42500
|
strippedImages: external_exports.number().optional(),
|
|
@@ -42544,8 +42522,10 @@ registerTool(
|
|
|
42544
42522
|
const hashtags2 = parseHashtags(content2);
|
|
42545
42523
|
const warning2 = strippedImagesWarning(stripped2);
|
|
42546
42524
|
return successResponse(warning2 ? content2 + warning2 : content2, {
|
|
42525
|
+
id,
|
|
42547
42526
|
title: note2.title,
|
|
42548
42527
|
content: content2,
|
|
42528
|
+
contentHash: hashNoteContent(rawContent2),
|
|
42549
42529
|
hashtags: hashtags2,
|
|
42550
42530
|
strippedImages: stripped2.strippedCount,
|
|
42551
42531
|
truncated: stripped2.strippedCount > 0
|
|
@@ -42572,8 +42552,10 @@ registerTool(
|
|
|
42572
42552
|
const hashtags = parseHashtags(content);
|
|
42573
42553
|
const warning = strippedImagesWarning(stripped);
|
|
42574
42554
|
return successResponse(warning ? content + warning : content, {
|
|
42555
|
+
id: note.id,
|
|
42575
42556
|
title,
|
|
42576
42557
|
content,
|
|
42558
|
+
contentHash: hashNoteContent(rawContent),
|
|
42577
42559
|
hashtags,
|
|
42578
42560
|
strippedImages: stripped.strippedCount,
|
|
42579
42561
|
truncated: stripped.strippedCount > 0
|
|
@@ -42817,110 +42799,122 @@ registerTool(
|
|
|
42817
42799
|
registerTool(
|
|
42818
42800
|
"update-note",
|
|
42819
42801
|
{
|
|
42820
|
-
description: "Use when:
|
|
42802
|
+
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 atomically rejects stale content and attachment-bearing notes, then reads the same id back after saving. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
|
|
42821
42803
|
inputSchema: {
|
|
42822
|
-
id:
|
|
42823
|
-
|
|
42804
|
+
id: noteIdInput,
|
|
42805
|
+
expectedContentHash: expectedContentHashInput,
|
|
42824
42806
|
newTitle: external_exports.string().max(MAX.TITLE).optional().describe(
|
|
42825
42807
|
"New title for plaintext updates. Ignored when format is 'html'; include the visible title as the first line of newContent instead."
|
|
42826
42808
|
),
|
|
42827
42809
|
newContent: external_exports.string().min(1, "New content is required").max(MAX.CONTENT).describe(
|
|
42828
42810
|
"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."
|
|
42829
42811
|
),
|
|
42830
|
-
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting")
|
|
42831
|
-
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
|
|
42812
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting")
|
|
42832
42813
|
},
|
|
42833
42814
|
outputSchema: {
|
|
42834
42815
|
ok: external_exports.boolean().optional(),
|
|
42835
42816
|
id: external_exports.string().optional(),
|
|
42836
42817
|
title: external_exports.string().optional(),
|
|
42837
|
-
shared: external_exports.boolean().optional()
|
|
42818
|
+
shared: external_exports.boolean().optional(),
|
|
42819
|
+
previousContentHash: external_exports.string().optional(),
|
|
42820
|
+
contentHash: external_exports.string().optional(),
|
|
42821
|
+
verifiedVisibleText: external_exports.boolean().optional()
|
|
42838
42822
|
}
|
|
42839
42823
|
},
|
|
42840
|
-
withErrorHandling(({ id,
|
|
42841
|
-
|
|
42842
|
-
|
|
42843
|
-
|
|
42844
|
-
|
|
42845
|
-
|
|
42846
|
-
|
|
42847
|
-
|
|
42848
|
-
|
|
42849
|
-
);
|
|
42850
|
-
|
|
42851
|
-
const success2 = notesManager.updateNoteById(id, newTitle, newContent, format);
|
|
42852
|
-
if (!success2) {
|
|
42853
|
-
return errorResponse(`Failed to update note "${note2.title}"`);
|
|
42854
|
-
}
|
|
42855
|
-
const displayTitle = resolveUpdateResponseTitle(note2.title, newTitle, format, newContent);
|
|
42856
|
-
const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
|
|
42857
|
-
const checklistWarning2 = detectChecklistAttempt(newContent) ?? "";
|
|
42858
|
-
return successResponse(`Note updated: "${displayTitle}"${sharedWarning2}${checklistWarning2}`, {
|
|
42859
|
-
ok: true,
|
|
42860
|
-
id,
|
|
42861
|
-
title: displayTitle,
|
|
42862
|
-
shared: note2.shared ?? false
|
|
42863
|
-
});
|
|
42824
|
+
withErrorHandling(({ id, expectedContentHash, newTitle, newContent, format = "plaintext" }) => {
|
|
42825
|
+
const snapshot = readExactNoteSnapshot(id);
|
|
42826
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
42827
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
42828
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
42829
|
+
}
|
|
42830
|
+
const attachments = notesManager.listAttachmentsById(id);
|
|
42831
|
+
if (attachments.length > 0) {
|
|
42832
|
+
return errorResponse(
|
|
42833
|
+
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Full-body replacement is blocked; edit it in Notes.app.`
|
|
42834
|
+
);
|
|
42864
42835
|
}
|
|
42865
|
-
|
|
42866
|
-
|
|
42836
|
+
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
42837
|
+
id,
|
|
42838
|
+
snapshot.note.title,
|
|
42839
|
+
snapshot.body,
|
|
42840
|
+
newTitle,
|
|
42841
|
+
newContent,
|
|
42842
|
+
format
|
|
42843
|
+
);
|
|
42844
|
+
if (result.status === "conflict") {
|
|
42845
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
42867
42846
|
}
|
|
42868
|
-
|
|
42869
|
-
if (!note) {
|
|
42847
|
+
if (result.status === "attachments") {
|
|
42870
42848
|
return errorResponse(
|
|
42871
|
-
`Note "${title}"
|
|
42849
|
+
`Note "${snapshot.note.title}" gained an attachment before saving. No content was replaced.`
|
|
42872
42850
|
);
|
|
42873
42851
|
}
|
|
42874
|
-
if (
|
|
42852
|
+
if (result.status !== "updated") {
|
|
42875
42853
|
return errorResponse(
|
|
42876
|
-
`
|
|
42854
|
+
`The update result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
42877
42855
|
);
|
|
42878
42856
|
}
|
|
42879
|
-
const
|
|
42880
|
-
|
|
42881
|
-
|
|
42857
|
+
const readback = notesManager.getNoteContentById(id);
|
|
42858
|
+
const contentHash = readback ? hashNoteContent(readback) : "";
|
|
42859
|
+
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
42860
|
+
return errorResponse(
|
|
42861
|
+
`The note accepted an update, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
|
|
42862
|
+
);
|
|
42882
42863
|
}
|
|
42883
|
-
const
|
|
42884
|
-
|
|
42864
|
+
const displayTitle = resolveUpdateResponseTitle(
|
|
42865
|
+
snapshot.note.title,
|
|
42866
|
+
newTitle,
|
|
42867
|
+
format,
|
|
42868
|
+
newContent
|
|
42869
|
+
);
|
|
42870
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
42885
42871
|
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
42886
|
-
return successResponse(
|
|
42887
|
-
|
|
42888
|
-
|
|
42889
|
-
|
|
42890
|
-
|
|
42872
|
+
return successResponse(
|
|
42873
|
+
`Note updated; visible text verified: "${displayTitle}" [id: ${id}]${sharedWarning}${checklistWarning}`,
|
|
42874
|
+
{
|
|
42875
|
+
ok: true,
|
|
42876
|
+
id,
|
|
42877
|
+
title: displayTitle,
|
|
42878
|
+
shared: snapshot.note.shared ?? false,
|
|
42879
|
+
previousContentHash: expectedContentHash,
|
|
42880
|
+
contentHash,
|
|
42881
|
+
verifiedVisibleText: true
|
|
42882
|
+
}
|
|
42883
|
+
);
|
|
42891
42884
|
}, "Error updating note")
|
|
42892
42885
|
);
|
|
42893
42886
|
registerTool(
|
|
42894
42887
|
"append-to-note",
|
|
42895
42888
|
{
|
|
42896
|
-
description: "Use when: adding content to
|
|
42889
|
+
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, the note changed since the read, or it has attachments.\nSafety: append still rewrites the full HTML body, so it uses the same exact-ID, revision, attachment, and readback guards as update-note. Notes.app normalizes HTML, so rich formatting is not claimed as byte-identical.",
|
|
42897
42890
|
inputSchema: {
|
|
42898
|
-
id:
|
|
42899
|
-
|
|
42891
|
+
id: noteIdInput,
|
|
42892
|
+
expectedContentHash: expectedContentHashInput,
|
|
42900
42893
|
content: external_exports.string().min(1, "Content to append is required").max(MAX.CONTENT).describe("Text to append to the note body"),
|
|
42901
42894
|
position: external_exports.enum(["after", "before"]).optional().default("after").describe(
|
|
42902
42895
|
"Where to insert: 'after' appends to the end (default), 'before' prepends to the start"
|
|
42903
42896
|
),
|
|
42904
42897
|
separator: external_exports.string().max(20).optional().default("\n\n").describe("String placed between existing content and new content (default: two newlines)"),
|
|
42905
|
-
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'")
|
|
42906
|
-
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
|
|
42898
|
+
format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'")
|
|
42907
42899
|
},
|
|
42908
42900
|
outputSchema: {
|
|
42909
42901
|
ok: external_exports.boolean().optional(),
|
|
42910
42902
|
id: external_exports.string().optional(),
|
|
42911
42903
|
title: external_exports.string().optional(),
|
|
42912
|
-
shared: external_exports.boolean().optional()
|
|
42904
|
+
shared: external_exports.boolean().optional(),
|
|
42905
|
+
previousContentHash: external_exports.string().optional(),
|
|
42906
|
+
contentHash: external_exports.string().optional(),
|
|
42907
|
+
verifiedVisibleText: external_exports.boolean().optional()
|
|
42913
42908
|
}
|
|
42914
42909
|
},
|
|
42915
42910
|
withErrorHandling(
|
|
42916
42911
|
({
|
|
42917
42912
|
id,
|
|
42918
|
-
|
|
42913
|
+
expectedContentHash,
|
|
42919
42914
|
content,
|
|
42920
42915
|
position = "after",
|
|
42921
42916
|
separator = "\n\n",
|
|
42922
|
-
format = "plaintext"
|
|
42923
|
-
account
|
|
42917
|
+
format = "plaintext"
|
|
42924
42918
|
}) => {
|
|
42925
42919
|
const contentToHtml = (text) => {
|
|
42926
42920
|
if (format === "html") return text;
|
|
@@ -42935,72 +42929,64 @@ registerTool(
|
|
|
42935
42929
|
const escaped = sep2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
42936
42930
|
return `<div>${escaped}</div>`;
|
|
42937
42931
|
};
|
|
42938
|
-
|
|
42939
|
-
|
|
42940
|
-
|
|
42941
|
-
|
|
42942
|
-
}
|
|
42943
|
-
if (note2.passwordProtected) {
|
|
42944
|
-
return errorResponse(
|
|
42945
|
-
`Note "${note2.title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
|
|
42946
|
-
);
|
|
42947
|
-
}
|
|
42948
|
-
const existingHtml2 = notesManager.getNoteContentById(id);
|
|
42949
|
-
if (existingHtml2 === null || existingHtml2 === void 0) {
|
|
42950
|
-
return errorResponse(`Failed to read content of note "${note2.title}"`);
|
|
42951
|
-
}
|
|
42952
|
-
const firstDivEnd2 = existingHtml2.indexOf("</div>");
|
|
42953
|
-
const titleDiv2 = firstDivEnd2 !== -1 ? existingHtml2.slice(0, firstDivEnd2 + 6) : "";
|
|
42954
|
-
const bodyHtml2 = firstDivEnd2 !== -1 ? existingHtml2.slice(firstDivEnd2 + 6) : existingHtml2;
|
|
42955
|
-
const newBlock2 = contentToHtml(content);
|
|
42956
|
-
const sepHtml2 = separatorToHtml(separator);
|
|
42957
|
-
const combinedBody2 = position === "before" ? titleDiv2 + newBlock2 + sepHtml2 + bodyHtml2 : titleDiv2 + bodyHtml2 + sepHtml2 + newBlock2;
|
|
42958
|
-
const success2 = notesManager.updateNoteById(id, void 0, combinedBody2, "html");
|
|
42959
|
-
if (!success2) {
|
|
42960
|
-
return errorResponse(`Failed to append to note "${note2.title}"`);
|
|
42961
|
-
}
|
|
42962
|
-
const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
|
|
42963
|
-
return successResponse(`Note appended: "${note2.title}"${sharedWarning2}`, {
|
|
42964
|
-
ok: true,
|
|
42965
|
-
id,
|
|
42966
|
-
title: note2.title,
|
|
42967
|
-
shared: note2.shared ?? false
|
|
42968
|
-
});
|
|
42932
|
+
const snapshot = readExactNoteSnapshot(id);
|
|
42933
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
42934
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
42935
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
42969
42936
|
}
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
}
|
|
42973
|
-
const note = notesManager.getNoteDetails(title, account);
|
|
42974
|
-
if (!note) {
|
|
42937
|
+
const attachments = notesManager.listAttachmentsById(id);
|
|
42938
|
+
if (attachments.length > 0) {
|
|
42975
42939
|
return errorResponse(
|
|
42976
|
-
`Note "${title}"
|
|
42940
|
+
`Note "${snapshot.note.title}" has ${attachments.length} attachment(s). Append is blocked because it rewrites the full body; edit it in Notes.app.`
|
|
42977
42941
|
);
|
|
42978
42942
|
}
|
|
42979
|
-
|
|
42943
|
+
const firstDivEnd = snapshot.body.indexOf("</div>");
|
|
42944
|
+
const titleDiv = firstDivEnd !== -1 ? snapshot.body.slice(0, firstDivEnd + 6) : "";
|
|
42945
|
+
const bodyHtml = firstDivEnd !== -1 ? snapshot.body.slice(firstDivEnd + 6) : snapshot.body;
|
|
42946
|
+
const newBlock = contentToHtml(content);
|
|
42947
|
+
const sepHtml = separatorToHtml(separator);
|
|
42948
|
+
const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
|
|
42949
|
+
const result = notesManager.updateNoteByIdIfUnchanged(
|
|
42950
|
+
id,
|
|
42951
|
+
snapshot.note.title,
|
|
42952
|
+
snapshot.body,
|
|
42953
|
+
void 0,
|
|
42954
|
+
combinedBody,
|
|
42955
|
+
"html"
|
|
42956
|
+
);
|
|
42957
|
+
if (result.status === "conflict") {
|
|
42958
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
42959
|
+
}
|
|
42960
|
+
if (result.status === "attachments") {
|
|
42980
42961
|
return errorResponse(
|
|
42981
|
-
`Note "${title}"
|
|
42962
|
+
`Note "${snapshot.note.title}" gained an attachment before saving. No content was appended.`
|
|
42982
42963
|
);
|
|
42983
42964
|
}
|
|
42984
|
-
|
|
42985
|
-
|
|
42986
|
-
|
|
42965
|
+
if (result.status !== "updated") {
|
|
42966
|
+
return errorResponse(
|
|
42967
|
+
`The append result for note "${snapshot.note.title}" is uncertain. Read the exact ID before retrying.`
|
|
42968
|
+
);
|
|
42987
42969
|
}
|
|
42988
|
-
const
|
|
42989
|
-
const
|
|
42990
|
-
|
|
42991
|
-
|
|
42992
|
-
|
|
42993
|
-
|
|
42994
|
-
const success = notesManager.updateNote(title, void 0, combinedBody, account, "html");
|
|
42995
|
-
if (!success) {
|
|
42996
|
-
return errorResponse(`Failed to append to note "${title}"`);
|
|
42970
|
+
const readback = notesManager.getNoteContentById(id);
|
|
42971
|
+
const contentHash = readback ? hashNoteContent(readback) : "";
|
|
42972
|
+
if (!readback || comparableVisibleText(readback) !== comparableVisibleText(result.writtenBody)) {
|
|
42973
|
+
return errorResponse(
|
|
42974
|
+
`The note accepted an append, but exact-ID readback visible text did not match. Do not retry automatically; inspect note ID ${id} in Notes.app.`
|
|
42975
|
+
);
|
|
42997
42976
|
}
|
|
42998
|
-
const sharedWarning = note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes
|
|
42999
|
-
return successResponse(
|
|
43000
|
-
|
|
43001
|
-
|
|
43002
|
-
|
|
43003
|
-
|
|
42977
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes are visible to them." : "";
|
|
42978
|
+
return successResponse(
|
|
42979
|
+
`Note appended; visible text verified: "${snapshot.note.title}"${sharedWarning}`,
|
|
42980
|
+
{
|
|
42981
|
+
ok: true,
|
|
42982
|
+
id,
|
|
42983
|
+
title: snapshot.note.title,
|
|
42984
|
+
shared: snapshot.note.shared ?? false,
|
|
42985
|
+
previousContentHash: expectedContentHash,
|
|
42986
|
+
contentHash,
|
|
42987
|
+
verifiedVisibleText: true
|
|
42988
|
+
}
|
|
42989
|
+
);
|
|
43004
42990
|
},
|
|
43005
42991
|
"Error appending to note"
|
|
43006
42992
|
)
|
|
@@ -43008,67 +42994,53 @@ registerTool(
|
|
|
43008
42994
|
registerTool(
|
|
43009
42995
|
"delete-note",
|
|
43010
42996
|
{
|
|
43011
|
-
description: "Use when:
|
|
42997
|
+
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.",
|
|
43012
42998
|
inputSchema: {
|
|
43013
|
-
id:
|
|
43014
|
-
|
|
43015
|
-
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
43016
|
-
"Account name (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)"
|
|
43017
|
-
)
|
|
42999
|
+
id: noteIdInput,
|
|
43000
|
+
expectedContentHash: expectedContentHashInput
|
|
43018
43001
|
},
|
|
43019
43002
|
outputSchema: {
|
|
43020
43003
|
ok: external_exports.boolean().optional(),
|
|
43021
43004
|
id: external_exports.string().optional(),
|
|
43022
43005
|
title: external_exports.string().optional(),
|
|
43023
|
-
wasShared: external_exports.boolean().optional()
|
|
43006
|
+
wasShared: external_exports.boolean().optional(),
|
|
43007
|
+
previousContentHash: external_exports.string().optional()
|
|
43024
43008
|
}
|
|
43025
43009
|
},
|
|
43026
|
-
withErrorHandling(({ id,
|
|
43027
|
-
|
|
43028
|
-
|
|
43029
|
-
|
|
43030
|
-
|
|
43031
|
-
}
|
|
43032
|
-
const success2 = notesManager.deleteNoteById(id);
|
|
43033
|
-
if (!success2) {
|
|
43034
|
-
return errorResponse(`Failed to delete note "${note2.title}"`);
|
|
43035
|
-
}
|
|
43036
|
-
const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note was shared with collaborators. They will no longer have access." : "";
|
|
43037
|
-
return successResponse(`Note deleted: "${note2.title}"${sharedWarning2}`, {
|
|
43038
|
-
ok: true,
|
|
43039
|
-
id,
|
|
43040
|
-
title: note2.title,
|
|
43041
|
-
wasShared: note2.shared ?? false
|
|
43042
|
-
});
|
|
43010
|
+
withErrorHandling(({ id, expectedContentHash }) => {
|
|
43011
|
+
const snapshot = readExactNoteSnapshot(id);
|
|
43012
|
+
if ("error" in snapshot) return errorResponse(snapshot.error);
|
|
43013
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
43014
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43043
43015
|
}
|
|
43044
|
-
|
|
43045
|
-
|
|
43016
|
+
const result = notesManager.deleteNoteByIdIfUnchanged(id, snapshot.body);
|
|
43017
|
+
if (result.status === "conflict") {
|
|
43018
|
+
return errorResponse(revisionConflictMessage(snapshot.note.title));
|
|
43046
43019
|
}
|
|
43047
|
-
|
|
43048
|
-
if (!note) {
|
|
43020
|
+
if (result.status !== "deleted") {
|
|
43049
43021
|
return errorResponse(
|
|
43050
|
-
`
|
|
43022
|
+
`The delete result for note "${snapshot.note.title}" is uncertain. Inspect exact ID ${id} before retrying.`
|
|
43051
43023
|
);
|
|
43052
43024
|
}
|
|
43053
|
-
const
|
|
43054
|
-
|
|
43055
|
-
|
|
43056
|
-
|
|
43057
|
-
|
|
43058
|
-
|
|
43059
|
-
|
|
43060
|
-
|
|
43061
|
-
|
|
43062
|
-
|
|
43025
|
+
const sharedWarning = snapshot.note.shared ? "\n\n\u26A0\uFE0F This note was shared with collaborators. They will no longer have access." : "";
|
|
43026
|
+
return successResponse(
|
|
43027
|
+
`Note moved to Recently Deleted: "${snapshot.note.title}"${sharedWarning}`,
|
|
43028
|
+
{
|
|
43029
|
+
ok: true,
|
|
43030
|
+
id,
|
|
43031
|
+
title: snapshot.note.title,
|
|
43032
|
+
wasShared: snapshot.note.shared ?? false,
|
|
43033
|
+
previousContentHash: expectedContentHash
|
|
43034
|
+
}
|
|
43035
|
+
);
|
|
43063
43036
|
}, "Error deleting note")
|
|
43064
43037
|
);
|
|
43065
43038
|
registerTool(
|
|
43066
43039
|
"move-note",
|
|
43067
43040
|
{
|
|
43068
|
-
description: "Use when: moving one note to a different folder
|
|
43041
|
+
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.",
|
|
43069
43042
|
inputSchema: {
|
|
43070
|
-
id:
|
|
43071
|
-
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
43043
|
+
id: noteIdInput,
|
|
43072
43044
|
folder: external_exports.string().min(1, "Destination folder is required").max(MAX.FOLDER),
|
|
43073
43045
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note/folder")
|
|
43074
43046
|
},
|
|
@@ -43076,47 +43048,33 @@ registerTool(
|
|
|
43076
43048
|
ok: external_exports.boolean().optional(),
|
|
43077
43049
|
id: external_exports.string().optional(),
|
|
43078
43050
|
title: external_exports.string().optional(),
|
|
43079
|
-
folder: external_exports.string().optional()
|
|
43051
|
+
folder: external_exports.string().optional(),
|
|
43052
|
+
verified: external_exports.boolean().optional()
|
|
43080
43053
|
}
|
|
43081
43054
|
},
|
|
43082
|
-
withErrorHandling(({ id,
|
|
43083
|
-
|
|
43084
|
-
const note2 = notesManager.getNoteById(id);
|
|
43085
|
-
if (!note2) {
|
|
43086
|
-
return errorResponse(`Note with ID "${id}" not found`);
|
|
43087
|
-
}
|
|
43088
|
-
const success2 = notesManager.moveNoteById(id, folder, account);
|
|
43089
|
-
if (!success2) {
|
|
43090
|
-
return errorResponse(
|
|
43091
|
-
`Failed to move note "${note2.title}" to folder "${folder}". Folder may not exist.`
|
|
43092
|
-
);
|
|
43093
|
-
}
|
|
43094
|
-
return successResponse(`Note moved: "${note2.title}" -> "${folder}"`, {
|
|
43095
|
-
ok: true,
|
|
43096
|
-
id,
|
|
43097
|
-
title: note2.title,
|
|
43098
|
-
folder
|
|
43099
|
-
});
|
|
43100
|
-
}
|
|
43101
|
-
if (!title) {
|
|
43102
|
-
return errorResponse("Either 'id' or 'title' is required");
|
|
43103
|
-
}
|
|
43104
|
-
const note = notesManager.getNoteDetails(title, account);
|
|
43055
|
+
withErrorHandling(({ id, folder, account }) => {
|
|
43056
|
+
const note = notesManager.getNoteById(id);
|
|
43105
43057
|
if (!note) {
|
|
43058
|
+
return errorResponse(`Note with ID "${id}" not found`);
|
|
43059
|
+
}
|
|
43060
|
+
const success = notesManager.moveNoteById(id, folder, account);
|
|
43061
|
+
if (!success) {
|
|
43106
43062
|
return errorResponse(
|
|
43107
|
-
`
|
|
43063
|
+
`Failed to move note "${note.title}" to folder "${folder}". Folder may not exist.`
|
|
43108
43064
|
);
|
|
43109
43065
|
}
|
|
43110
|
-
const
|
|
43111
|
-
if (!
|
|
43066
|
+
const readback = notesManager.getNoteById(id);
|
|
43067
|
+
if (!readback || readback.id !== id) {
|
|
43112
43068
|
return errorResponse(
|
|
43113
|
-
`
|
|
43069
|
+
`The move may have succeeded, but exact-ID readback failed. Inspect note ID ${id} before retrying.`
|
|
43114
43070
|
);
|
|
43115
43071
|
}
|
|
43116
|
-
return successResponse(`Note moved: "${title}" -> "${folder}"`, {
|
|
43072
|
+
return successResponse(`Note moved and verified: "${readback.title}" -> "${folder}"`, {
|
|
43117
43073
|
ok: true,
|
|
43118
|
-
|
|
43119
|
-
|
|
43074
|
+
id,
|
|
43075
|
+
title: readback.title,
|
|
43076
|
+
folder,
|
|
43077
|
+
verified: true
|
|
43120
43078
|
});
|
|
43121
43079
|
}, "Error moving note")
|
|
43122
43080
|
);
|
|
@@ -43552,9 +43510,14 @@ ${attachmentList}`,
|
|
|
43552
43510
|
registerTool(
|
|
43553
43511
|
"batch-delete-notes",
|
|
43554
43512
|
{
|
|
43555
|
-
description: "Use when:
|
|
43513
|
+
description: "Use when: moving several reviewed notes to Recently Deleted.\nReturns: per-note success or conflict.\nDo not use when: deleting a single note.\nSafety: every entry requires an exact id and the content hash from get-note-content. Any note changed since review is preserved and reported as a conflict.",
|
|
43556
43514
|
inputSchema: {
|
|
43557
|
-
|
|
43515
|
+
notes: external_exports.array(
|
|
43516
|
+
external_exports.object({
|
|
43517
|
+
id: noteIdInput,
|
|
43518
|
+
expectedContentHash: expectedContentHashInput
|
|
43519
|
+
})
|
|
43520
|
+
).max(MAX.BATCH_IDS).describe(`Reviewed note IDs and revision tokens to delete (max ${MAX.BATCH_IDS})`)
|
|
43558
43521
|
},
|
|
43559
43522
|
outputSchema: {
|
|
43560
43523
|
ok: external_exports.boolean().optional(),
|
|
@@ -43563,11 +43526,23 @@ registerTool(
|
|
|
43563
43526
|
results: external_exports.array(external_exports.object({}).passthrough()).optional()
|
|
43564
43527
|
}
|
|
43565
43528
|
},
|
|
43566
|
-
withErrorHandling(({
|
|
43567
|
-
if (
|
|
43568
|
-
return errorResponse("No
|
|
43529
|
+
withErrorHandling(({ notes }) => {
|
|
43530
|
+
if (notes.length === 0) {
|
|
43531
|
+
return errorResponse("No reviewed notes provided");
|
|
43569
43532
|
}
|
|
43570
|
-
const results =
|
|
43533
|
+
const results = notes.map(({ id, expectedContentHash }) => {
|
|
43534
|
+
const snapshot = readExactNoteSnapshot(id);
|
|
43535
|
+
if ("error" in snapshot) return { id, success: false, error: snapshot.error };
|
|
43536
|
+
if (snapshot.contentHash !== expectedContentHash) {
|
|
43537
|
+
return { id, success: false, error: revisionConflictMessage(snapshot.note.title) };
|
|
43538
|
+
}
|
|
43539
|
+
const result = notesManager.deleteNoteByIdIfUnchanged(id, snapshot.body);
|
|
43540
|
+
if (result.status === "deleted") return { id, success: true };
|
|
43541
|
+
if (result.status === "conflict") {
|
|
43542
|
+
return { id, success: false, error: revisionConflictMessage(snapshot.note.title) };
|
|
43543
|
+
}
|
|
43544
|
+
return { id, success: false, error: "Delete result uncertain; inspect this exact ID" };
|
|
43545
|
+
});
|
|
43571
43546
|
const succeeded = results.filter((r) => r.success).length;
|
|
43572
43547
|
const failed = results.filter((r) => !r.success).length;
|
|
43573
43548
|
const lines = [`Batch delete: ${succeeded} succeeded, ${failed} failed`];
|
|
@@ -43588,9 +43563,9 @@ registerTool(
|
|
|
43588
43563
|
registerTool(
|
|
43589
43564
|
"batch-move-notes",
|
|
43590
43565
|
{
|
|
43591
|
-
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts.\nDo not use when: moving a single note (move-note).\
|
|
43566
|
+
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).",
|
|
43592
43567
|
inputSchema: {
|
|
43593
|
-
ids: external_exports.array(
|
|
43568
|
+
ids: external_exports.array(noteIdInput).max(MAX.BATCH_IDS).describe(`Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`),
|
|
43594
43569
|
folder: external_exports.string().max(MAX.FOLDER).describe(
|
|
43595
43570
|
'Destination folder name or nested path (e.g. "Work/Clients"). Must already exist \u2014 create-folder first.'
|
|
43596
43571
|
),
|