apple-notes-mcp 2.8.31 → 2.8.32
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 +47 -1
- package/build/index.js +141 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1349,6 +1349,50 @@ latest `expectedContentHash`, and an absolute `path`. The server never retries
|
|
|
1349
1349
|
the insertion. It verifies that existing rich content survived and compares the
|
|
1350
1350
|
fetched attachment bytes with the source before reporting success.
|
|
1351
1351
|
|
|
1352
|
+
| Parameter | Type | Required | Description |
|
|
1353
|
+
|-----------|------|----------|-------------|
|
|
1354
|
+
| `id` | string | Yes | Exact CoreData note ID |
|
|
1355
|
+
| `expectedContentHash` | string | Yes | `contentHash` from the note version being extended |
|
|
1356
|
+
| `path` | string | Yes | Absolute path of the local file; symbolic links are refused |
|
|
1357
|
+
| `filename` | string | No | Name the attachment gets in Notes instead of the source file's name. One path component that keeps the source file's extension, with no slash, colon, backslash, control character, leading dot or surrounding spaces. Notes names a file attachment after the file it receives, so the server gives its private temporary copy this name |
|
|
1358
|
+
|
|
1359
|
+
**Returns:** `attachmentId`, `bytes`, the `name` Notes reports, and the new
|
|
1360
|
+
`contentHash`. With `filename`, `filenameVerified` says whether Notes reports
|
|
1361
|
+
that exact name. A mismatch is a warning (`filenameWarning`), not a failure,
|
|
1362
|
+
because the attachment and its bytes are already verified.
|
|
1363
|
+
|
|
1364
|
+
---
|
|
1365
|
+
|
|
1366
|
+
#### `create-note-with-attachment`
|
|
1367
|
+
|
|
1368
|
+
Creates a note and attaches one local file to it in a single call. It checks the
|
|
1369
|
+
file and `filename` before creating anything, creates the note through Notes.app
|
|
1370
|
+
the way [`create-note`](#create-note) does in plaintext, then attaches with the
|
|
1371
|
+
same verification as [`add-attachment`](#add-attachment).
|
|
1372
|
+
|
|
1373
|
+
| Parameter | Type | Required | Description |
|
|
1374
|
+
|-----------|------|----------|-------------|
|
|
1375
|
+
| `title` | string | Yes | One-line note title |
|
|
1376
|
+
| `content` | string | No | Plain-text body placed above the attachment |
|
|
1377
|
+
| `folder` | string | No | Existing folder or nested path; create it first with [`create-folder`](#create-folder) |
|
|
1378
|
+
| `account` | string | No | Account name; defaults to Notes.app's default account |
|
|
1379
|
+
| `path` | string | Yes | Absolute path of the local file (at most 64 MiB) |
|
|
1380
|
+
| `filename` | string | No | Attachment name override, as in `add-attachment` |
|
|
1381
|
+
|
|
1382
|
+
```json
|
|
1383
|
+
{
|
|
1384
|
+
"title": "Signed lease",
|
|
1385
|
+
"folder": "Home",
|
|
1386
|
+
"path": "/Users/me/Downloads/scan-0042.pdf",
|
|
1387
|
+
"filename": "Lease 2026.pdf"
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
**Returns:** the new note's `id` with `noteCreated: true` and the
|
|
1392
|
+
`add-attachment` result. If the attachment step fails after the note exists,
|
|
1393
|
+
the error names the new note's id: attach to it with `add-attachment` instead of
|
|
1394
|
+
calling this tool again, which would create a second note.
|
|
1395
|
+
|
|
1352
1396
|
---
|
|
1353
1397
|
|
|
1354
1398
|
#### `list-attachments`
|
|
@@ -1570,7 +1614,9 @@ the note before retrying, and retry only the items that are not present.
|
|
|
1570
1614
|
#### `create-table`
|
|
1571
1615
|
|
|
1572
1616
|
Appends a native table from rectangular string rows and verifies every decoded
|
|
1573
|
-
cell. It never substitutes a text table.
|
|
1617
|
+
cell. It never substitutes a text table. Omit `rows` for an empty 2 × 2 table,
|
|
1618
|
+
the size Notes itself inserts from Format > Table; its four empty cells are
|
|
1619
|
+
verified the same way.
|
|
1574
1620
|
|
|
1575
1621
|
#### `set-note-pinned`
|
|
1576
1622
|
|
package/build/index.js
CHANGED
|
@@ -46748,7 +46748,7 @@ import {
|
|
|
46748
46748
|
writeFileSync as writeFileSync3
|
|
46749
46749
|
} from "node:fs";
|
|
46750
46750
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
46751
|
-
import { basename, isAbsolute as isAbsolute2, join as join15 } from "node:path";
|
|
46751
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join15 } from "node:path";
|
|
46752
46752
|
var noteId = exactIdInput(
|
|
46753
46753
|
"ICNote",
|
|
46754
46754
|
/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
|
|
@@ -46764,7 +46764,7 @@ function readSnapshot(manager, id2) {
|
|
|
46764
46764
|
if (!body) throw new Error("Note content is unavailable");
|
|
46765
46765
|
const enriched = enrichNoteRead(id2, body);
|
|
46766
46766
|
const rich = readRichNote(id2);
|
|
46767
|
-
if (
|
|
46767
|
+
if (enriched.revision !== rich.revision)
|
|
46768
46768
|
throw new Error("Native metadata changed during read; read the note again");
|
|
46769
46769
|
return { id: id2, title: note.title, body, rich, hash: richContentHash(body, enriched) };
|
|
46770
46770
|
}
|
|
@@ -46853,66 +46853,144 @@ function registerDirectOperations(server2, manager) {
|
|
|
46853
46853
|
...manager.renameFolderById(args.id, args.expectedName, args.expectedParentId, args.newName)
|
|
46854
46854
|
})
|
|
46855
46855
|
);
|
|
46856
|
+
const attachmentInput = {
|
|
46857
|
+
path: external_exports.string().min(1).max(4096).describe("Absolute path of the local file to attach"),
|
|
46858
|
+
filename: external_exports.string().min(1).max(255).optional().describe(
|
|
46859
|
+
"Name the attachment gets in Notes instead of the source file's name. One path component with the source file's extension; no slash, colon, or control characters, and no leading dot."
|
|
46860
|
+
)
|
|
46861
|
+
};
|
|
46856
46862
|
tool(
|
|
46857
46863
|
"add-attachment",
|
|
46858
|
-
"Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, and verifies existing content plus fetched bytes.",
|
|
46859
|
-
{ id: noteId, expectedContentHash: revision,
|
|
46860
|
-
(
|
|
46861
|
-
|
|
46862
|
-
|
|
46863
|
-
|
|
46864
|
-
|
|
46865
|
-
|
|
46866
|
-
|
|
46864
|
+
"Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, attachment name, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, and verifies existing content plus fetched bytes.",
|
|
46865
|
+
{ id: noteId, expectedContentHash: revision, ...attachmentInput },
|
|
46866
|
+
(args) => attachFile(manager, args)
|
|
46867
|
+
);
|
|
46868
|
+
tool(
|
|
46869
|
+
"create-note-with-attachment",
|
|
46870
|
+
"Use when: creating a new note that holds one local file, in one call.\nReturns: the new note id plus the add-attachment result (attachment id, bytes, name, content hash).\nDo not use when: the note already exists (add-attachment).\nSafety: checks the file and filename before creating anything, creates the note through Notes.app like create-note, then attaches with the same byte verification as add-attachment. If the attachment step fails after the note exists, the error names the new note's id; attach to it with add-attachment instead of creating another note.",
|
|
46871
|
+
{
|
|
46872
|
+
title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
|
|
46873
|
+
content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
|
|
46874
|
+
folder: external_exports.string().max(1e3).optional().describe("Existing folder or nested path; create it first with create-folder"),
|
|
46875
|
+
account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
|
|
46876
|
+
...attachmentInput
|
|
46877
|
+
},
|
|
46878
|
+
(args) => {
|
|
46879
|
+
const name = attachmentName(args.path, args.filename);
|
|
46880
|
+
localAttachment(args.path);
|
|
46881
|
+
const note = manager.createNote(
|
|
46882
|
+
args.title,
|
|
46883
|
+
args.content ?? "",
|
|
46884
|
+
[],
|
|
46885
|
+
args.folder,
|
|
46886
|
+
args.account,
|
|
46887
|
+
"plaintext"
|
|
46888
|
+
);
|
|
46889
|
+
if (!note)
|
|
46890
|
+
throw new Error(
|
|
46891
|
+
`Failed to create note "${args.title}". Check that the folder and account exist (list-folders, list-accounts); nothing was attached`
|
|
46892
|
+
);
|
|
46893
|
+
const handOff = `Note ${note.id} was created; attach to it with add-attachment instead of creating another note`;
|
|
46894
|
+
let snapshot;
|
|
46895
|
+
try {
|
|
46896
|
+
snapshot = readSnapshot(manager, note.id);
|
|
46897
|
+
} catch (error2) {
|
|
46898
|
+
throw new Error(`${handOff}. The new note could not be read back: ${String(error2)}`);
|
|
46899
|
+
}
|
|
46867
46900
|
try {
|
|
46868
|
-
writeFileSync3(temporaryFile, bytes, { mode: 384 });
|
|
46869
|
-
if (readSnapshot(manager, id2).hash !== before.hash)
|
|
46870
|
-
throw new Error("Note revision changed");
|
|
46871
|
-
let returnedId;
|
|
46872
|
-
let transportUncertain = false;
|
|
46873
|
-
try {
|
|
46874
|
-
returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
|
|
46875
|
-
} catch {
|
|
46876
|
-
transportUncertain = true;
|
|
46877
|
-
}
|
|
46878
|
-
const after = readSnapshot(manager, id2);
|
|
46879
|
-
assertExistingContentPreserved(before, after);
|
|
46880
|
-
const readInserted = () => [
|
|
46881
|
-
...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()
|
|
46882
|
-
].filter((item) => !beforeAttachments.some((existing) => existing.id === item.id));
|
|
46883
|
-
let inserted = readInserted();
|
|
46884
|
-
for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
|
|
46885
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
46886
|
-
inserted = readInserted();
|
|
46887
|
-
}
|
|
46888
|
-
const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
|
|
46889
|
-
if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
|
|
46890
|
-
throw new Error(
|
|
46891
|
-
"Attachment insertion outcome uncertain; read the exact note before retrying"
|
|
46892
|
-
);
|
|
46893
|
-
const attachmentId = inserted[0].id;
|
|
46894
|
-
const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
|
|
46895
|
-
const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
|
|
46896
|
-
if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
|
|
46897
|
-
throw new Error(
|
|
46898
|
-
"Attachment bytes were not verified; read the exact note before retrying"
|
|
46899
|
-
);
|
|
46900
46901
|
return {
|
|
46901
|
-
|
|
46902
|
-
|
|
46903
|
-
|
|
46904
|
-
|
|
46905
|
-
|
|
46906
|
-
|
|
46907
|
-
|
|
46908
|
-
|
|
46902
|
+
...attachFile(manager, {
|
|
46903
|
+
id: note.id,
|
|
46904
|
+
expectedContentHash: snapshot.hash,
|
|
46905
|
+
path: args.path,
|
|
46906
|
+
filename: name
|
|
46907
|
+
}),
|
|
46908
|
+
title: args.title,
|
|
46909
|
+
folder: args.folder,
|
|
46910
|
+
noteCreated: true
|
|
46909
46911
|
};
|
|
46910
|
-
}
|
|
46911
|
-
|
|
46912
|
+
} catch (error2) {
|
|
46913
|
+
throw new Error(`${handOff}. ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
46912
46914
|
}
|
|
46913
46915
|
}
|
|
46914
46916
|
);
|
|
46915
46917
|
}
|
|
46918
|
+
function attachmentName(path7, filename) {
|
|
46919
|
+
const source = basename(path7);
|
|
46920
|
+
if (filename === void 0) return source;
|
|
46921
|
+
if (Buffer.byteLength(filename, "utf8") > 255)
|
|
46922
|
+
throw new Error("filename must be at most 255 bytes");
|
|
46923
|
+
if (filename !== filename.trim() || filename === "" || filename.startsWith(".") || /[/:\\\p{Cc}]/u.test(filename))
|
|
46924
|
+
throw new Error(
|
|
46925
|
+
"filename must be one path component with no slash, colon, backslash, control character, leading dot, or surrounding spaces"
|
|
46926
|
+
);
|
|
46927
|
+
if (extname(filename).toLowerCase() !== extname(source).toLowerCase())
|
|
46928
|
+
throw new Error(
|
|
46929
|
+
`filename must keep the source file's extension (${extname(source) || "none"})`
|
|
46930
|
+
);
|
|
46931
|
+
return filename;
|
|
46932
|
+
}
|
|
46933
|
+
function attachFile(manager, args) {
|
|
46934
|
+
const { id: id2, expectedContentHash, path: path7 } = args;
|
|
46935
|
+
const name = attachmentName(path7, args.filename);
|
|
46936
|
+
const before = readSnapshot(manager, id2);
|
|
46937
|
+
if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
|
|
46938
|
+
const bytes = localAttachment(path7);
|
|
46939
|
+
const beforeAttachments = manager.listAttachmentsById(id2);
|
|
46940
|
+
const directory = mkdtempSync4(join15(tmpdir4(), "notes-attachment-add-"));
|
|
46941
|
+
const temporaryFile = join15(directory, name);
|
|
46942
|
+
try {
|
|
46943
|
+
writeFileSync3(temporaryFile, bytes, { mode: 384 });
|
|
46944
|
+
if (readSnapshot(manager, id2).hash !== before.hash) throw new Error("Note revision changed");
|
|
46945
|
+
let returnedId;
|
|
46946
|
+
let transportUncertain = false;
|
|
46947
|
+
try {
|
|
46948
|
+
returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
|
|
46949
|
+
} catch {
|
|
46950
|
+
transportUncertain = true;
|
|
46951
|
+
}
|
|
46952
|
+
const after = readSnapshot(manager, id2);
|
|
46953
|
+
assertExistingContentPreserved(before, after);
|
|
46954
|
+
const readInserted = () => [...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()].filter(
|
|
46955
|
+
(item) => !beforeAttachments.some((existing) => existing.id === item.id)
|
|
46956
|
+
);
|
|
46957
|
+
let inserted = readInserted();
|
|
46958
|
+
for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
|
|
46959
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
46960
|
+
inserted = readInserted();
|
|
46961
|
+
}
|
|
46962
|
+
const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
|
|
46963
|
+
if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
|
|
46964
|
+
throw new Error(
|
|
46965
|
+
"Attachment insertion outcome uncertain; read the exact note before retrying"
|
|
46966
|
+
);
|
|
46967
|
+
const attachmentId = inserted[0].id;
|
|
46968
|
+
const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
|
|
46969
|
+
const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
|
|
46970
|
+
if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
|
|
46971
|
+
throw new Error("Attachment bytes were not verified; read the exact note before retrying");
|
|
46972
|
+
const nameVerified = inserted[0].name === name;
|
|
46973
|
+
return {
|
|
46974
|
+
ok: true,
|
|
46975
|
+
id: id2,
|
|
46976
|
+
attachmentId,
|
|
46977
|
+
contentHash: after.hash,
|
|
46978
|
+
bytes: bytes.length,
|
|
46979
|
+
name: inserted[0].name,
|
|
46980
|
+
...args.filename === void 0 ? {} : {
|
|
46981
|
+
filenameVerified: nameVerified,
|
|
46982
|
+
...nameVerified ? {} : {
|
|
46983
|
+
filenameWarning: "The attachment and its bytes were verified, but Notes reports a different name"
|
|
46984
|
+
}
|
|
46985
|
+
},
|
|
46986
|
+
...transportUncertain ? {
|
|
46987
|
+
transportWarning: "Transport was uncertain; exact bytes and prior content were verified"
|
|
46988
|
+
} : {}
|
|
46989
|
+
};
|
|
46990
|
+
} finally {
|
|
46991
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
46992
|
+
}
|
|
46993
|
+
}
|
|
46916
46994
|
|
|
46917
46995
|
// src/tools/nativeTagsBridge.ts
|
|
46918
46996
|
var noteId2 = exactIdInput(
|
|
@@ -47040,6 +47118,10 @@ var common = {
|
|
|
47040
47118
|
"Distinctive existing phrase used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
|
|
47041
47119
|
)
|
|
47042
47120
|
};
|
|
47121
|
+
var EMPTY_TABLE_ROWS = [
|
|
47122
|
+
["", ""],
|
|
47123
|
+
["", ""]
|
|
47124
|
+
];
|
|
47043
47125
|
var htmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
47044
47126
|
var MAX_CHECKLIST_BATCH = 20;
|
|
47045
47127
|
var byPosition = (items = []) => [...items].sort((a, b) => a.start - b.start);
|
|
@@ -47241,13 +47323,16 @@ function registerNativeOperations(server2, manager) {
|
|
|
47241
47323
|
);
|
|
47242
47324
|
tool(
|
|
47243
47325
|
"create-table",
|
|
47244
|
-
"Use when: appending one native Notes table from a rectangular array of strings.\nReturns: the native table identity and decoded cells after readback.\nDo not use when: a text table is acceptable or the rows are not rectangular.\nSafety: never substitutes text; preserves existing rich content and reports failure unless the native table is verified.",
|
|
47326
|
+
"Use when: appending one native Notes table from a rectangular array of strings, or an empty table when rows is omitted.\nReturns: the native table identity and decoded cells after readback.\nDo not use when: a text table is acceptable or the rows are not rectangular.\nSafety: never substitutes text; preserves existing rich content and reports failure unless the native table is verified.",
|
|
47245
47327
|
{
|
|
47246
47328
|
...common,
|
|
47247
|
-
rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3)
|
|
47329
|
+
rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3).optional().describe(
|
|
47330
|
+
"Cell text, row by row. Omit for an empty 2 x 2 table, the size Notes itself inserts."
|
|
47331
|
+
)
|
|
47248
47332
|
},
|
|
47249
|
-
(
|
|
47333
|
+
(input) => {
|
|
47250
47334
|
requireValidated("create-table");
|
|
47335
|
+
const args = { ...input, rows: input.rows ?? EMPTY_TABLE_ROWS };
|
|
47251
47336
|
if (args.rows.some((row) => row.length !== args.rows[0].length))
|
|
47252
47337
|
throw new Error("Table rows must have equal cell counts");
|
|
47253
47338
|
const before = readRichNote(args.id);
|
package/package.json
CHANGED