apple-notes-mcp 2.9.12 → 2.9.14
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 +23 -4
- package/build/index.js +456 -217
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ Creates a new note in Apple Notes.
|
|
|
257
257
|
| `content` | string | One of `content`/`contentPath` | The body content of the note (do not repeat the title here) |
|
|
258
258
|
| `contentPath` | string | One of `content`/`contentPath` | Absolute path of a local UTF-8 file to use as the body instead of `content`. Allowed in the same places [`save-attachment`](#save-attachment) may write (home, temp, `/Volumes`); symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped |
|
|
259
259
|
| `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags). Refused with `format: "markdown"` |
|
|
260
|
-
| `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). Defaults to account root |
|
|
260
|
+
| `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). A smart folder is refused (see [`move-note`](#move-note)). Defaults to account root |
|
|
261
261
|
| `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
|
|
262
262
|
| `format` | string | No | Content format: `"plaintext"` (default), `"html"`, or `"markdown"`. In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities. `"markdown"` produces real Title/Heading/Subheading styles through a Shortcut; see [Markdown notes](#markdown-notes) |
|
|
263
263
|
| `markdownRoute` | string | No | With `format: "markdown"` only: `"shortcut"` (default) or `"html"`. See [Markdown through HTML](#markdown-through-html) |
|
|
@@ -425,6 +425,7 @@ Searches for notes by title or content.
|
|
|
425
425
|
| `folder` | string | No | Limit search to a specific folder (supports nested paths like `"Work/Clients"`) |
|
|
426
426
|
| `modifiedSince` | string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., `"2025-01-01"`) |
|
|
427
427
|
| `limit` | number | No | Maximum number of results to return. **Defaults to 50** — a broad query reads several properties per match via AppleScript (~200ms/note), so an unbounded search over hundreds of matches can exceed Notes' 30s timeout and return an error instead of results. Pass a higher value to see more; the applied limit (and whether it truncated the results) is disclosed in the response. |
|
|
428
|
+
| `includeWordCount` | boolean | No | Add `wordCount` to each result (`null` when the note is locked or its body unreadable). A database content search already has the text. Otherwise the bodies are read in one batched read-only database query, never one AppleScript call per note; that needs Full Disk Access and also adds `matchedIn`. Without it, the response carries `wordCountUnavailable` and the results are unchanged. |
|
|
428
429
|
|
|
429
430
|
**Example - Search titles:**
|
|
430
431
|
```json
|
|
@@ -453,6 +454,17 @@ Searches for notes by title or content.
|
|
|
453
454
|
|
|
454
455
|
**Returns:** List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like `get-note-content`, `update-note`, etc. A content search also returns `source` (`"database"` or `"applescript"`), and `scanTruncated` when the database path left older notes unsearched.
|
|
455
456
|
|
|
457
|
+
When the note text came from the database (a database content search, or any search with `includeWordCount`), each result also carries `matchedIn`: `["title"]`, `["body"]`, or `["title", "body"]`, saying where the query text occurs. The body is the text after the first line. `matchedIn` is absent for locked notes and for AppleScript searches without `includeWordCount`. The text output appends the same details to each line, for example `· matched in title, body · 245 words`.
|
|
458
|
+
|
|
459
|
+
**Example - Content search with word counts:**
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"query": "budget",
|
|
463
|
+
"searchContent": true,
|
|
464
|
+
"includeWordCount": true
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
456
468
|
---
|
|
457
469
|
|
|
458
470
|
#### `query-notes`
|
|
@@ -470,6 +482,7 @@ can match titles and bodies together.
|
|
|
470
482
|
| `limit` | number | No | Maximum notes to return. Defaults to 50, maximum 500. The response reports the total match count. |
|
|
471
483
|
| `scanLimit` | number | No | How many of the most recently modified notes to examine. Defaults to 500, maximum 5000. The response says when older notes were left unscanned. |
|
|
472
484
|
| `includeDeleted` | boolean | No | Also scan notes in Recently Deleted, notes pending deletion, and folderless notes. Defaults to `false`. |
|
|
485
|
+
| `includeWordCount` | boolean | No | Add `wordCount` to each returned note, the same count `words:` filters on (`null` when locked or unreadable). Free when the query already reads bodies; a metadata-only query (for example `pinned`) reads just the returned notes' bodies in one extra read-only query. Defaults to `false`. |
|
|
473
486
|
|
|
474
487
|
**Syntax:**
|
|
475
488
|
|
|
@@ -514,7 +527,11 @@ Their snippets are always empty.
|
|
|
514
527
|
|
|
515
528
|
**Returns:** Matching notes, most recently modified first, each with `id`,
|
|
516
529
|
`title`, `folder`, `account`, `modified`, `created`, and a `snippet` centred on
|
|
517
|
-
the first matched phrase.
|
|
530
|
+
the first matched phrase. Each note also has `matchedIn` when the query has a
|
|
531
|
+
positive text term: `["title"]`, `["body"]`, or both, saying where those terms
|
|
532
|
+
occur (a `title:` term is only looked for in the title, a `body:` term only in
|
|
533
|
+
the body). It is absent for locked or undecodable notes, and an empty list
|
|
534
|
+
means the note matched through a non-text branch such as `pinned OR x`. The ids are the same `x-coredata://…/ICNote/p…` form
|
|
518
535
|
every other tool accepts. `structuredContent` also reports `matched` (total
|
|
519
536
|
matches), `scanned`, `eligible`, `scanTruncated`, `truncated`, and `unreadable`
|
|
520
537
|
(bodies that could not be decoded). A malformed query returns an error naming
|
|
@@ -1032,6 +1049,8 @@ such a note for good, do it in Notes.app.
|
|
|
1032
1049
|
|
|
1033
1050
|
Moves a note to a different folder. The note is relocated in place via Notes.app's native `move`, so its id, creation date, and all embedded attachments (files, images, scans, PDFs, audio) are preserved. The destination folder must already exist — create it first with [`create-folder`](#create-folder).
|
|
1034
1051
|
|
|
1052
|
+
A smart folder is never a destination: it only gathers notes by its rules, and Notes.app would move the note to Recently Deleted or store a created note where no folder shows it. `create-note`, `create-note-with-attachment`, `move-note`, `batch-move-notes`, and `create-folder` refuse one before writing anything, with `code: "unsupported"`, `committed: false`, and `reason: "smart_folder_destination"`. An ordinary folder that shares a smart folder's name is still found. Detection reads the NoteStore database, so it needs Full Disk Access; without it, destinations resolve as before.
|
|
1053
|
+
|
|
1035
1054
|
| Parameter | Type | Required | Description |
|
|
1036
1055
|
|-----------|------|----------|-------------|
|
|
1037
1056
|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
|
|
@@ -1402,7 +1421,7 @@ Creates a new folder, including a whole nested hierarchy in one call.
|
|
|
1402
1421
|
|
|
1403
1422
|
| Parameter | Type | Required | Description |
|
|
1404
1423
|
|-----------|------|----------|-------------|
|
|
1405
|
-
| `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped |
|
|
1424
|
+
| `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped. A segment that names a smart folder is refused before anything is created |
|
|
1406
1425
|
| `account` | string | No | Account to create folder in (defaults to Notes.app's default account; exact or unique-prefix match) |
|
|
1407
1426
|
|
|
1408
1427
|
**Example:**
|
|
@@ -1590,7 +1609,7 @@ Moves multiple notes to a folder.
|
|
|
1590
1609
|
| Parameter | Type | Required | Description |
|
|
1591
1610
|
|-----------|------|----------|-------------|
|
|
1592
1611
|
| `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
|
|
1593
|
-
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder) |
|
|
1612
|
+
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder). A smart folder is refused for the whole call before any note moves |
|
|
1594
1613
|
| `account` | string | No | Account containing the folder |
|
|
1595
1614
|
|
|
1596
1615
|
**Returns:** Summary of successes and failures. Each success is reported only
|
package/build/index.js
CHANGED
|
@@ -39588,6 +39588,77 @@ function getChecklistItems(noteId3) {
|
|
|
39588
39588
|
return { items };
|
|
39589
39589
|
}
|
|
39590
39590
|
|
|
39591
|
+
// src/utils/errorCodes.ts
|
|
39592
|
+
var CodedError = class extends Error {
|
|
39593
|
+
envelope;
|
|
39594
|
+
constructor(message, envelope) {
|
|
39595
|
+
super(message);
|
|
39596
|
+
this.name = "CodedError";
|
|
39597
|
+
this.envelope = envelope;
|
|
39598
|
+
}
|
|
39599
|
+
};
|
|
39600
|
+
var RULES = [
|
|
39601
|
+
{ code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
|
|
39602
|
+
{
|
|
39603
|
+
code: "verification_failed",
|
|
39604
|
+
pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
|
|
39605
|
+
},
|
|
39606
|
+
{
|
|
39607
|
+
code: "revision_conflict",
|
|
39608
|
+
pattern: /changed after it was read|revision changed|(?:pinned|note) state changed|changed during (?:read|preflight)|changed; read (?:it|the note) again|rich text do not match/i
|
|
39609
|
+
},
|
|
39610
|
+
{ code: "full_disk_access_missing", pattern: /Full Disk Access/i },
|
|
39611
|
+
{
|
|
39612
|
+
code: "shortcut_not_installed",
|
|
39613
|
+
pattern: /(?:Install|Import) the supplied|no such shortcut|Run apple-notes-mcp setup/i
|
|
39614
|
+
},
|
|
39615
|
+
{
|
|
39616
|
+
code: "not_found",
|
|
39617
|
+
pattern: /not found|does not exist|\bNo \w+ found\b|does not contain any checklist items|Can[’']t get (?:note|folder|account|attachment)|Cannot find note|Scope is absent/i
|
|
39618
|
+
},
|
|
39619
|
+
{ code: "ambiguous", pattern: /ambiguous|more than one|exactly once|Duplicate note IDs/i },
|
|
39620
|
+
{
|
|
39621
|
+
code: "notes_unavailable",
|
|
39622
|
+
pattern: /Notes\.app is (?:not responding|busy)|Lost connection to Notes|isn't running/i
|
|
39623
|
+
},
|
|
39624
|
+
{
|
|
39625
|
+
code: "unsupported",
|
|
39626
|
+
pattern: /password-protected|Locked notes|unsupported|not supported|is blocked|blocked because|has not passed live|refuses to sign|No (?:supported )?background|only one where|Real Notes link unavailable/i
|
|
39627
|
+
},
|
|
39628
|
+
{
|
|
39629
|
+
code: "validation_error",
|
|
39630
|
+
pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
|
|
39631
|
+
}
|
|
39632
|
+
];
|
|
39633
|
+
var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
|
|
39634
|
+
var WRITE_ACCEPTED = /\baccepted (?:the|an) \w+, but/i;
|
|
39635
|
+
function classifyError(message, cause) {
|
|
39636
|
+
if (cause instanceof CodedError) return { ...cause.envelope };
|
|
39637
|
+
const text2 = cause instanceof Error && cause.message && !message.includes(cause.message) ? `${message}
|
|
39638
|
+
${cause.message}` : message;
|
|
39639
|
+
const timeoutCause = typeof cause === "object" && cause !== null && cause.code === "ETIMEDOUT";
|
|
39640
|
+
let code = "operation_failed";
|
|
39641
|
+
if (timeoutCause) code = "timeout_indeterminate";
|
|
39642
|
+
else if (isPermissionDenied(text2)) code = "permission_denied";
|
|
39643
|
+
else code = RULES.find((rule) => rule.pattern.test(text2))?.code ?? "operation_failed";
|
|
39644
|
+
const envelope = { code };
|
|
39645
|
+
if (code === "timeout_indeterminate" || code === "verification_failed") {
|
|
39646
|
+
envelope.indeterminate = true;
|
|
39647
|
+
if (WRITE_ACCEPTED.test(text2)) envelope.committed = true;
|
|
39648
|
+
} else if (code === "revision_conflict" || NOTHING_WRITTEN.test(text2)) {
|
|
39649
|
+
envelope.committed = false;
|
|
39650
|
+
envelope.indeterminate = false;
|
|
39651
|
+
}
|
|
39652
|
+
return envelope;
|
|
39653
|
+
}
|
|
39654
|
+
function errorResult(message, cause) {
|
|
39655
|
+
return {
|
|
39656
|
+
content: [{ type: "text", text: message }],
|
|
39657
|
+
structuredContent: classifyError(message, cause),
|
|
39658
|
+
isError: true
|
|
39659
|
+
};
|
|
39660
|
+
}
|
|
39661
|
+
|
|
39591
39662
|
// src/utils/inlineImages.ts
|
|
39592
39663
|
var DEFAULT_MAX_INLINE_IMAGE_BYTES = 256 * 1024;
|
|
39593
39664
|
function maxInlineImageBytes(env = process.env) {
|
|
@@ -42358,6 +42429,24 @@ function buildFolderReference(folderPath) {
|
|
|
42358
42429
|
}
|
|
42359
42430
|
var AS_ACCOUNT_REF = "__acctRef";
|
|
42360
42431
|
var LIVE_FOLDER_NOT_FOUND = "Folder not found";
|
|
42432
|
+
var SMART_FOLDER_DESTINATION = "SMART_FOLDER_DESTINATION";
|
|
42433
|
+
function readSmartFolderIds(read = readSmartFolders) {
|
|
42434
|
+
return (read().folders ?? []).map((folder) => folder.id).filter((id2) => FOLDER_ID_PATTERN.test(id2));
|
|
42435
|
+
}
|
|
42436
|
+
function smartFolderDestinationError(folderPath) {
|
|
42437
|
+
return new CodedError(
|
|
42438
|
+
`Refused: "${folderPath}" is a smart folder. Smart folders only gather notes by their rules and cannot hold notes or folders; choose an ordinary folder (list-folders). Nothing was changed`,
|
|
42439
|
+
{
|
|
42440
|
+
code: "unsupported",
|
|
42441
|
+
committed: false,
|
|
42442
|
+
indeterminate: false,
|
|
42443
|
+
reason: "smart_folder_destination"
|
|
42444
|
+
}
|
|
42445
|
+
);
|
|
42446
|
+
}
|
|
42447
|
+
function throwIfSmartFolderDestination(error2, folderPath) {
|
|
42448
|
+
if (error2?.includes(SMART_FOLDER_DESTINATION)) throw smartFolderDestinationError(folderPath);
|
|
42449
|
+
}
|
|
42361
42450
|
function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
42362
42451
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) throw new Error("Invalid AppleScript variable");
|
|
42363
42452
|
buildFolderReference(folderPath);
|
|
@@ -42366,10 +42455,26 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42366
42455
|
const cid = `${varName}_cid`;
|
|
42367
42456
|
const any = `${varName}_any`;
|
|
42368
42457
|
const parent = `${varName}_p`;
|
|
42369
|
-
const
|
|
42370
|
-
const
|
|
42458
|
+
const quotedPath = escapePlainStringForAppleScript(folderPath);
|
|
42459
|
+
const notFound = `error "${LIVE_FOLDER_NOT_FOUND}: ${quotedPath}" number -1728`;
|
|
42460
|
+
const excluded = (opts.excludeFolderIds ?? []).filter((id2) => FOLDER_ID_PATTERN.test(id2));
|
|
42461
|
+
const skipIds = `${varName}_sx`;
|
|
42462
|
+
const skipped = `${varName}_sh`;
|
|
42463
|
+
const candidateTest = excluded.length ? [
|
|
42464
|
+
` if ${skipIds} contains ${cid} then`,
|
|
42465
|
+
` set ${skipped} to true`,
|
|
42466
|
+
` else if exists folder id ${cid} then`
|
|
42467
|
+
] : [` if exists folder id ${cid} then`];
|
|
42468
|
+
const unresolved = excluded.length ? [
|
|
42469
|
+
`if ${varName} is missing value then`,
|
|
42470
|
+
` if ${skipped} then error "${SMART_FOLDER_DESTINATION}: ${quotedPath}" number -1728`,
|
|
42471
|
+
` ${notFound}`,
|
|
42472
|
+
`end if`
|
|
42473
|
+
] : [`if ${varName} is missing value then ${notFound}`];
|
|
42474
|
+
const lines = excluded.length ? [`set ${skipIds} to {${excluded.map((id2) => `"${id2}"`).join(", ")}}`] : [];
|
|
42371
42475
|
parts.forEach((part, i) => {
|
|
42372
42476
|
const name = escapePlainStringForAppleScript(part);
|
|
42477
|
+
if (excluded.length) lines.push(`set ${skipped} to false`);
|
|
42373
42478
|
if (i === 0) {
|
|
42374
42479
|
lines.push(
|
|
42375
42480
|
`set ${varName} to missing value`,
|
|
@@ -42377,7 +42482,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42377
42482
|
`repeat with ${c} in (folders of ${AS_ACCOUNT_REF} whose name is "${name}")`,
|
|
42378
42483
|
` try`,
|
|
42379
42484
|
` set ${cid} to id of ${c}`,
|
|
42380
|
-
|
|
42485
|
+
...candidateTest,
|
|
42381
42486
|
` if class of (container of ${c}) is not folder then`,
|
|
42382
42487
|
` set ${varName} to folder id ${cid}`,
|
|
42383
42488
|
` exit repeat`,
|
|
@@ -42397,7 +42502,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42397
42502
|
`repeat with ${c} in (folders of ${parent} whose name is "${name}")`,
|
|
42398
42503
|
` try`,
|
|
42399
42504
|
` set ${cid} to id of ${c}`,
|
|
42400
|
-
|
|
42505
|
+
...candidateTest,
|
|
42401
42506
|
` set ${varName} to folder id ${cid}`,
|
|
42402
42507
|
` exit repeat`,
|
|
42403
42508
|
` end if`,
|
|
@@ -42405,7 +42510,7 @@ function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
|
42405
42510
|
`end repeat`
|
|
42406
42511
|
);
|
|
42407
42512
|
}
|
|
42408
|
-
lines.push(
|
|
42513
|
+
lines.push(...unresolved);
|
|
42409
42514
|
});
|
|
42410
42515
|
return lines.join("\n");
|
|
42411
42516
|
}
|
|
@@ -42666,7 +42771,9 @@ var AppleNotesManager = class {
|
|
|
42666
42771
|
const safeBody = escapeHtmlForAppleScript(`<h1>${htmlTitle}</h1>${bodyContent}`);
|
|
42667
42772
|
let createCommand;
|
|
42668
42773
|
if (folder) {
|
|
42669
|
-
createCommand = `${buildLiveFolderResolution(folder, "__folder"
|
|
42774
|
+
createCommand = `${buildLiveFolderResolution(folder, "__folder", {
|
|
42775
|
+
excludeFolderIds: this.smartFolderIds()
|
|
42776
|
+
})}
|
|
42670
42777
|
make new note at __folder with properties {body:"${safeBody}"}`;
|
|
42671
42778
|
} else {
|
|
42672
42779
|
createCommand = `
|
|
@@ -42678,6 +42785,7 @@ var AppleNotesManager = class {
|
|
|
42678
42785
|
const result = executeMutationAppleScript(script);
|
|
42679
42786
|
if (!result.success) {
|
|
42680
42787
|
throwIfAccountResolutionFailed(result.error);
|
|
42788
|
+
if (folder) throwIfSmartFolderDestination(result.error, folder);
|
|
42681
42789
|
console.error(`Failed to create note "${title}":`, result.error);
|
|
42682
42790
|
return null;
|
|
42683
42791
|
}
|
|
@@ -43282,6 +43390,35 @@ var AppleNotesManager = class {
|
|
|
43282
43390
|
return (resultList as text)${trashSuffix}
|
|
43283
43391
|
`;
|
|
43284
43392
|
}
|
|
43393
|
+
/**
|
|
43394
|
+
* Smart folder ids that a destination folder must never resolve to. Read
|
|
43395
|
+
* fresh on every write (a smart folder can be added at any time); empty
|
|
43396
|
+
* without Full Disk Access. See {@link readSmartFolderIds}.
|
|
43397
|
+
*/
|
|
43398
|
+
smartFolderIds() {
|
|
43399
|
+
return readSmartFolderIds();
|
|
43400
|
+
}
|
|
43401
|
+
/**
|
|
43402
|
+
* Read-only preflight: throws the smart-folder refusal when `folderPath`
|
|
43403
|
+
* resolves in `account` only to a smart folder. For write paths that create
|
|
43404
|
+
* something before they reach their own destination check (the Markdown
|
|
43405
|
+
* bridge creates first and moves second). Other failures are left to the
|
|
43406
|
+
* write itself, which reports them as before.
|
|
43407
|
+
*
|
|
43408
|
+
* @param folderPath - Destination folder path (list-folders syntax)
|
|
43409
|
+
* @param account - Account to resolve in (defaults to Notes' default)
|
|
43410
|
+
*/
|
|
43411
|
+
assertNotSmartFolderDestination(folderPath, account) {
|
|
43412
|
+
const excludeFolderIds = this.smartFolderIds();
|
|
43413
|
+
if (excludeFolderIds.length === 0) return;
|
|
43414
|
+
const script = buildAccountScopedScript(
|
|
43415
|
+
{ account: this.resolveAccount(account) },
|
|
43416
|
+
`${buildLiveFolderResolution(folderPath, "__folder", { excludeFolderIds })}
|
|
43417
|
+
return id of __folder`
|
|
43418
|
+
);
|
|
43419
|
+
const result = executeAppleScript(script);
|
|
43420
|
+
if (!result.success) throwIfSmartFolderDestination(result.error, folderPath);
|
|
43421
|
+
}
|
|
43285
43422
|
/**
|
|
43286
43423
|
* Lists every smart folder with its decoded query, read-only.
|
|
43287
43424
|
*
|
|
@@ -43823,37 +43960,40 @@ var AppleNotesManager = class {
|
|
|
43823
43960
|
console.error(`Invalid folder name: "${name}"`);
|
|
43824
43961
|
return null;
|
|
43825
43962
|
}
|
|
43963
|
+
const smart = { rootOnly: true, excludeFolderIds: this.smartFolderIds() };
|
|
43826
43964
|
for (let i = 0; i < parts.length; i++) {
|
|
43827
43965
|
const currentPath = parts.slice(0, i + 1).map((p) => escapeFolderName(p)).join("/");
|
|
43828
43966
|
const checkScript = buildAccountScopedScript(
|
|
43829
43967
|
{ account: targetAccount },
|
|
43830
|
-
`${buildLiveFolderResolution(currentPath, "__folder",
|
|
43968
|
+
`${buildLiveFolderResolution(currentPath, "__folder", smart)}
|
|
43831
43969
|
return id of __folder`
|
|
43832
43970
|
);
|
|
43833
43971
|
const checkResult = executeAppleScript(checkScript);
|
|
43834
43972
|
if (checkResult.success) {
|
|
43835
43973
|
continue;
|
|
43836
43974
|
}
|
|
43975
|
+
throwIfSmartFolderDestination(checkResult.error, currentPath);
|
|
43837
43976
|
const segmentName = escapePlainStringForAppleScript(parts[i]);
|
|
43838
43977
|
let createCommand;
|
|
43839
43978
|
if (i === 0) {
|
|
43840
43979
|
createCommand = `make new folder with properties {name:"${segmentName}"}`;
|
|
43841
43980
|
} else {
|
|
43842
43981
|
const parentPath = parts.slice(0, i).map((p) => escapeFolderName(p)).join("/");
|
|
43843
|
-
createCommand = `${buildLiveFolderResolution(parentPath, "__parent",
|
|
43982
|
+
createCommand = `${buildLiveFolderResolution(parentPath, "__parent", smart)}
|
|
43844
43983
|
make new folder at __parent with properties {name:"${segmentName}"}`;
|
|
43845
43984
|
}
|
|
43846
43985
|
const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
|
|
43847
43986
|
const result = executeMutationAppleScript(script);
|
|
43848
43987
|
if (!result.success) {
|
|
43849
43988
|
throwIfAccountResolutionFailed(result.error);
|
|
43989
|
+
throwIfSmartFolderDestination(result.error, name);
|
|
43850
43990
|
console.error(`Failed to create folder "${name}":`, result.error);
|
|
43851
43991
|
return null;
|
|
43852
43992
|
}
|
|
43853
43993
|
}
|
|
43854
43994
|
const idScript = buildAccountScopedScript(
|
|
43855
43995
|
{ account: targetAccount },
|
|
43856
|
-
`${buildLiveFolderResolution(name, "__folder",
|
|
43996
|
+
`${buildLiveFolderResolution(name, "__folder", smart)}
|
|
43857
43997
|
return (id of __folder) & ${AS_FIELD_SEP} & (name of it)`
|
|
43858
43998
|
);
|
|
43859
43999
|
const idResult = executeAppleScript(idScript);
|
|
@@ -43912,7 +44052,9 @@ var AppleNotesManager = class {
|
|
|
43912
44052
|
moveNoteById(id2, destinationFolder, account, scope2) {
|
|
43913
44053
|
const targetAccount = this.resolveAccount(account);
|
|
43914
44054
|
const safeId = sanitizeNoteId(id2);
|
|
43915
|
-
const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder"
|
|
44055
|
+
const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder", {
|
|
44056
|
+
excludeFolderIds: this.smartFolderIds()
|
|
44057
|
+
});
|
|
43916
44058
|
const moveCommand = `
|
|
43917
44059
|
${buildAccountResolution(targetAccount)}
|
|
43918
44060
|
${destFolderSetup}
|
|
@@ -43927,6 +44069,7 @@ var AppleNotesManager = class {
|
|
|
43927
44069
|
const result = executeMutationAppleScript(script);
|
|
43928
44070
|
if (!result.success) {
|
|
43929
44071
|
throwIfAccountResolutionFailed(result.error);
|
|
44072
|
+
throwIfSmartFolderDestination(result.error, destinationFolder);
|
|
43930
44073
|
console.error(
|
|
43931
44074
|
`Cannot move note to "${destinationFolder}" (folder may not exist):`,
|
|
43932
44075
|
result.error
|
|
@@ -44821,7 +44964,9 @@ var AppleNotesManager = class {
|
|
|
44821
44964
|
batchMoveNotes(ids, folder, account) {
|
|
44822
44965
|
if (ids.length === 0) return [];
|
|
44823
44966
|
const targetAccount = this.resolveAccount(account);
|
|
44824
|
-
const destFolderSetup = buildLiveFolderResolution(folder, "destFolder"
|
|
44967
|
+
const destFolderSetup = buildLiveFolderResolution(folder, "destFolder", {
|
|
44968
|
+
excludeFolderIds: this.smartFolderIds()
|
|
44969
|
+
});
|
|
44825
44970
|
const results = new Array(ids.length);
|
|
44826
44971
|
const runnable = [];
|
|
44827
44972
|
ids.forEach((id2, i) => {
|
|
@@ -44877,6 +45022,7 @@ var AppleNotesManager = class {
|
|
|
44877
45022
|
const res = executeMutationAppleScript(script);
|
|
44878
45023
|
if (!res.success) {
|
|
44879
45024
|
throwIfAccountResolutionFailed(res.error);
|
|
45025
|
+
throwIfSmartFolderDestination(res.error, folder);
|
|
44880
45026
|
for (const r of runnable) {
|
|
44881
45027
|
results[r.index] = this.createBatchResult(
|
|
44882
45028
|
ids[r.index],
|
|
@@ -46787,13 +46933,6 @@ function describeSearchScope(searchContent, resultCount) {
|
|
|
46787
46933
|
return "\n\n\u2139\uFE0F Only note titles were searched, so a term that appears in note bodies would not match. Retry with `searchContent: true` to search bodies instead.";
|
|
46788
46934
|
}
|
|
46789
46935
|
|
|
46790
|
-
// src/utils/noteQueryStore.ts
|
|
46791
|
-
import { execFileSync as execFileSync14 } from "child_process";
|
|
46792
|
-
import * as fs8 from "fs";
|
|
46793
|
-
import * as os8 from "os";
|
|
46794
|
-
import * as path8 from "path";
|
|
46795
|
-
import { gunzipSync as gunzipSync8 } from "zlib";
|
|
46796
|
-
|
|
46797
46936
|
// src/utils/noteQuery.ts
|
|
46798
46937
|
var QUERY_LIMITS = {
|
|
46799
46938
|
/** Maximum number of tokens (terms, operators, parentheses). */
|
|
@@ -47225,21 +47364,50 @@ function evaluateNoteQuery(node, note) {
|
|
|
47225
47364
|
}
|
|
47226
47365
|
}
|
|
47227
47366
|
}
|
|
47228
|
-
function
|
|
47367
|
+
function positiveTextPredicates(node, negated = false) {
|
|
47229
47368
|
switch (node.type) {
|
|
47230
47369
|
case "and":
|
|
47231
47370
|
case "or":
|
|
47232
|
-
return node.children.flatMap((child) =>
|
|
47371
|
+
return node.children.flatMap((child) => positiveTextPredicates(child, negated));
|
|
47233
47372
|
case "not":
|
|
47234
|
-
return
|
|
47373
|
+
return positiveTextPredicates(node.child, !negated);
|
|
47235
47374
|
case "text":
|
|
47236
|
-
return negated ? [] : [node.value];
|
|
47375
|
+
return negated ? [] : [{ field: node.field, value: node.value }];
|
|
47237
47376
|
default:
|
|
47238
47377
|
return [];
|
|
47239
47378
|
}
|
|
47240
47379
|
}
|
|
47380
|
+
function positiveTextTerms(node, negated = false) {
|
|
47381
|
+
return positiveTextPredicates(node, negated).map((predicate) => predicate.value);
|
|
47382
|
+
}
|
|
47383
|
+
function matchLocations(predicates, title, text2) {
|
|
47384
|
+
if (predicates.length === 0) return void 0;
|
|
47385
|
+
if (text2 === null && predicates.some((p) => p.field !== "title")) return void 0;
|
|
47386
|
+
const firstBreak = text2 === null ? -1 : text2.indexOf("\n");
|
|
47387
|
+
const titleLower = normalizeForMatch(title);
|
|
47388
|
+
const firstLineLower = text2 === null ? "" : normalizeForMatch(firstBreak === -1 ? text2 : text2.slice(0, firstBreak));
|
|
47389
|
+
const bodyLower = text2 === null || firstBreak === -1 ? "" : normalizeForMatch(text2.slice(firstBreak + 1));
|
|
47390
|
+
let inTitle = false;
|
|
47391
|
+
let inBody = false;
|
|
47392
|
+
for (const predicate of predicates) {
|
|
47393
|
+
const needle = normalizeForMatch(predicate.value);
|
|
47394
|
+
if (predicate.field !== "body" && !inTitle) {
|
|
47395
|
+
inTitle = titleLower.includes(needle) || firstLineLower.includes(needle);
|
|
47396
|
+
}
|
|
47397
|
+
if (predicate.field !== "title" && !inBody) inBody = bodyLower.includes(needle);
|
|
47398
|
+
}
|
|
47399
|
+
const locations = [];
|
|
47400
|
+
if (inTitle) locations.push("title");
|
|
47401
|
+
if (inBody) locations.push("body");
|
|
47402
|
+
return locations;
|
|
47403
|
+
}
|
|
47241
47404
|
|
|
47242
47405
|
// src/utils/noteQueryStore.ts
|
|
47406
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
47407
|
+
import * as fs8 from "fs";
|
|
47408
|
+
import * as os8 from "os";
|
|
47409
|
+
import * as path8 from "path";
|
|
47410
|
+
import { gunzipSync as gunzipSync8 } from "zlib";
|
|
47243
47411
|
var NOTES_DB_PATH10 = path8.join(
|
|
47244
47412
|
os8.homedir(),
|
|
47245
47413
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
@@ -47321,6 +47489,15 @@ function decodeNoteBody(data) {
|
|
|
47321
47489
|
objectIds
|
|
47322
47490
|
};
|
|
47323
47491
|
}
|
|
47492
|
+
function decodeBodyHex(hex3) {
|
|
47493
|
+
try {
|
|
47494
|
+
return decodeNoteBody(
|
|
47495
|
+
new Uint8Array(gunzipSync8(Buffer.from(hex3, "hex"), { maxOutputLength: 32 * 1024 * 1024 }))
|
|
47496
|
+
);
|
|
47497
|
+
} catch {
|
|
47498
|
+
return null;
|
|
47499
|
+
}
|
|
47500
|
+
}
|
|
47324
47501
|
function countWords(text2) {
|
|
47325
47502
|
let count = 0;
|
|
47326
47503
|
for (const word of text2.replace(/\ufffc/gu, " ").split(/\s+/u)) {
|
|
@@ -47391,6 +47568,67 @@ function presentColumns2(dbPath2) {
|
|
|
47391
47568
|
}
|
|
47392
47569
|
return cols;
|
|
47393
47570
|
}
|
|
47571
|
+
function readStore(dbPath2, build) {
|
|
47572
|
+
if (!fs8.existsSync(dbPath2)) throw new NoteQueryStoreError(QUERY_FDA_MESSAGE, "no_fda");
|
|
47573
|
+
try {
|
|
47574
|
+
return runSqlite6(dbPath2, build(presentColumns2(dbPath2)));
|
|
47575
|
+
} catch (error2) {
|
|
47576
|
+
if (error2 instanceof NoteQueryStoreError) throw error2;
|
|
47577
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
47578
|
+
if (message.includes("authorization denied") || message.includes("unable to open database")) {
|
|
47579
|
+
throw new NoteQueryStoreError(QUERY_FDA_MESSAGE, "no_fda");
|
|
47580
|
+
}
|
|
47581
|
+
console.error(`query-notes: database read failed: ${message}`);
|
|
47582
|
+
throw new NoteQueryStoreError("Failed to read the Notes database.", "query_error");
|
|
47583
|
+
}
|
|
47584
|
+
}
|
|
47585
|
+
var NOTE_TEXT_BATCH_MAX = QUERY_RESULTS.MAX;
|
|
47586
|
+
function buildNoteTextsSql(available, pks) {
|
|
47587
|
+
const missing = ["Z_PK", "Z_ENT"].filter((c) => !available.has(c));
|
|
47588
|
+
if (missing.length) {
|
|
47589
|
+
throw new NoteQueryStoreError(
|
|
47590
|
+
`This macOS version's Notes database lacks columns search enrichment needs (${missing.join(", ")}).`,
|
|
47591
|
+
"schema"
|
|
47592
|
+
);
|
|
47593
|
+
}
|
|
47594
|
+
const keys = [...new Set(pks)];
|
|
47595
|
+
if (keys.length > NOTE_TEXT_BATCH_MAX) {
|
|
47596
|
+
throw new NoteQueryStoreError(
|
|
47597
|
+
`At most ${NOTE_TEXT_BATCH_MAX} notes can be read at once.`,
|
|
47598
|
+
"query_error"
|
|
47599
|
+
);
|
|
47600
|
+
}
|
|
47601
|
+
for (const pk of keys) {
|
|
47602
|
+
if (!Number.isSafeInteger(pk) || pk < 1) {
|
|
47603
|
+
throw new NoteQueryStoreError(`Invalid note key ${String(pk)}`, "query_error");
|
|
47604
|
+
}
|
|
47605
|
+
}
|
|
47606
|
+
const locked = available.has("ZISPASSWORDPROTECTED") ? "COALESCE(n.ZISPASSWORDPROTECTED, 0)" : "0";
|
|
47607
|
+
return [
|
|
47608
|
+
"BEGIN;",
|
|
47609
|
+
"SELECT json_object('k', 'meta', 'uuid', (SELECT Z_UUID FROM Z_METADATA LIMIT 1));",
|
|
47610
|
+
`SELECT json_object('k', 'note', 'pk', n.Z_PK, 'locked', ${locked}, 'data', (SELECT hex(d.ZDATA) FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK ORDER BY d.Z_PK DESC LIMIT 1)) FROM ZICCLOUDSYNCINGOBJECT n WHERE n.Z_ENT = ${entity2("ICNote")} AND n.Z_PK IN (${keys.length ? keys.join(", ") : "NULL"});`,
|
|
47611
|
+
"COMMIT;"
|
|
47612
|
+
].join(" ");
|
|
47613
|
+
}
|
|
47614
|
+
function readNoteTexts(pks, options = {}) {
|
|
47615
|
+
const output = readStore(
|
|
47616
|
+
options.dbPath ?? NOTES_DB_PATH10,
|
|
47617
|
+
(available) => buildNoteTextsSql(available, pks)
|
|
47618
|
+
);
|
|
47619
|
+
let uuid2;
|
|
47620
|
+
const texts = /* @__PURE__ */ new Map();
|
|
47621
|
+
for (const line of output.split("\n")) {
|
|
47622
|
+
if (!line.trim()) continue;
|
|
47623
|
+
const row = JSON.parse(line);
|
|
47624
|
+
if (row.k === "meta") uuid2 = typeof row.uuid === "string" ? row.uuid : void 0;
|
|
47625
|
+
else if (row.k === "note" && typeof row.pk === "number") {
|
|
47626
|
+
const body = row.locked || !row.data ? null : decodeBodyHex(row.data);
|
|
47627
|
+
texts.set(row.pk, body ? body.text : null);
|
|
47628
|
+
}
|
|
47629
|
+
}
|
|
47630
|
+
return { uuid: uuid2, texts };
|
|
47631
|
+
}
|
|
47394
47632
|
var escapeSegment = escapeFolderName;
|
|
47395
47633
|
function resolveFolders(rows) {
|
|
47396
47634
|
const byPk = new Map(rows.map((row) => [row.pk, row]));
|
|
@@ -47447,23 +47685,10 @@ function runNoteQuery(ast, options = {}) {
|
|
|
47447
47685
|
const withBodies = needsContent(ast);
|
|
47448
47686
|
const withTags = needsTags(ast);
|
|
47449
47687
|
const dbPath2 = options.dbPath ?? NOTES_DB_PATH10;
|
|
47450
|
-
|
|
47451
|
-
|
|
47452
|
-
|
|
47453
|
-
|
|
47454
|
-
output = runSqlite6(
|
|
47455
|
-
dbPath2,
|
|
47456
|
-
buildScanSql(available, { scanLimit, includeDeleted, withBodies, withTags })
|
|
47457
|
-
);
|
|
47458
|
-
} catch (error2) {
|
|
47459
|
-
if (error2 instanceof NoteQueryStoreError) throw error2;
|
|
47460
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
47461
|
-
if (message.includes("authorization denied") || message.includes("unable to open database")) {
|
|
47462
|
-
throw new NoteQueryStoreError(QUERY_FDA_MESSAGE, "no_fda");
|
|
47463
|
-
}
|
|
47464
|
-
console.error(`query-notes: database read failed: ${message}`);
|
|
47465
|
-
throw new NoteQueryStoreError("Failed to read the Notes database.", "query_error");
|
|
47466
|
-
}
|
|
47688
|
+
const output = readStore(
|
|
47689
|
+
dbPath2,
|
|
47690
|
+
(available) => buildScanSql(available, { scanLimit, includeDeleted, withBodies, withTags })
|
|
47691
|
+
);
|
|
47467
47692
|
let uuid2;
|
|
47468
47693
|
let eligible = 0;
|
|
47469
47694
|
const folderRows = [];
|
|
@@ -47487,6 +47712,8 @@ function runNoteQuery(ast, options = {}) {
|
|
|
47487
47712
|
}
|
|
47488
47713
|
const folders = resolveFolders(folderRows);
|
|
47489
47714
|
const textTerms = positiveTextTerms(ast);
|
|
47715
|
+
const textPredicates = positiveTextPredicates(ast);
|
|
47716
|
+
const pendingWordCounts = [];
|
|
47490
47717
|
const hits = [];
|
|
47491
47718
|
let matched = 0;
|
|
47492
47719
|
let unreadable = 0;
|
|
@@ -47504,15 +47731,7 @@ function runNoteQuery(ast, options = {}) {
|
|
|
47504
47731
|
if (withBodies) unreadable++;
|
|
47505
47732
|
return decoded;
|
|
47506
47733
|
}
|
|
47507
|
-
|
|
47508
|
-
decoded = decodeNoteBody(
|
|
47509
|
-
new Uint8Array(
|
|
47510
|
-
gunzipSync8(Buffer.from(row.data, "hex"), { maxOutputLength: 32 * 1024 * 1024 })
|
|
47511
|
-
)
|
|
47512
|
-
);
|
|
47513
|
-
} catch {
|
|
47514
|
-
decoded = null;
|
|
47515
|
-
}
|
|
47734
|
+
decoded = decodeBodyHex(row.data);
|
|
47516
47735
|
if (!decoded) unreadable++;
|
|
47517
47736
|
return decoded;
|
|
47518
47737
|
};
|
|
@@ -47553,6 +47772,8 @@ function runNoteQuery(ast, options = {}) {
|
|
|
47553
47772
|
matched++;
|
|
47554
47773
|
if (hits.length >= limit) continue;
|
|
47555
47774
|
const body = locked ? null : decode2();
|
|
47775
|
+
const matchedIn = matchLocations(textPredicates, row.title ?? "", body ? body.text : null);
|
|
47776
|
+
if (options.includeWordCount && !withBodies) pendingWordCounts.push(row.pk);
|
|
47556
47777
|
hits.push({
|
|
47557
47778
|
id: `x-coredata://${uuid2}/ICNote/p${row.pk}`,
|
|
47558
47779
|
title: row.title ?? "",
|
|
@@ -47561,7 +47782,16 @@ function runNoteQuery(ast, options = {}) {
|
|
|
47561
47782
|
...note.modified !== void 0 ? { modified: new Date(note.modified).toISOString() } : {},
|
|
47562
47783
|
...note.created !== void 0 ? { created: new Date(note.created).toISOString() } : {},
|
|
47563
47784
|
snippet: locked ? "" : body ? buildSnippet(body.text, textTerms) : (row.snippet ?? "").replace(/\s+/gu, " ").trim(),
|
|
47564
|
-
...locked ? { locked: true } : {}
|
|
47785
|
+
...locked ? { locked: true } : {},
|
|
47786
|
+
...matchedIn ? { matchedIn } : {},
|
|
47787
|
+
...options.includeWordCount && withBodies ? { wordCount: body ? countWords(body.text) : null } : {}
|
|
47788
|
+
});
|
|
47789
|
+
}
|
|
47790
|
+
if (pendingWordCounts.length) {
|
|
47791
|
+
const { texts } = readNoteTexts(pendingWordCounts, { dbPath: dbPath2 });
|
|
47792
|
+
pendingWordCounts.forEach((pk, index) => {
|
|
47793
|
+
const text2 = texts.get(pk);
|
|
47794
|
+
hits[index].wordCount = typeof text2 === "string" ? countWords(text2) : null;
|
|
47565
47795
|
});
|
|
47566
47796
|
}
|
|
47567
47797
|
return {
|
|
@@ -47604,6 +47834,7 @@ function searchContentViaDatabase(options) {
|
|
|
47604
47834
|
const result = runNoteQuery(buildSearchContentQuery(options), {
|
|
47605
47835
|
limit: options.limit,
|
|
47606
47836
|
scanLimit: QUERY_SCAN.MAX,
|
|
47837
|
+
includeWordCount: options.includeWordCount,
|
|
47607
47838
|
dbPath: options.dbPath
|
|
47608
47839
|
});
|
|
47609
47840
|
const notes = result.notes.map((hit) => ({
|
|
@@ -47614,7 +47845,9 @@ function searchContentViaDatabase(options) {
|
|
|
47614
47845
|
created: hit.created ? new Date(hit.created) : /* @__PURE__ */ new Date(0),
|
|
47615
47846
|
modified: hit.modified ? new Date(hit.modified) : /* @__PURE__ */ new Date(0),
|
|
47616
47847
|
...hit.folder !== void 0 ? { folder: hit.folder } : {},
|
|
47617
|
-
...hit.account !== void 0 ? { account: hit.account } : {}
|
|
47848
|
+
...hit.account !== void 0 ? { account: hit.account } : {},
|
|
47849
|
+
...hit.matchedIn ? { matchedIn: hit.matchedIn } : {},
|
|
47850
|
+
...hit.wordCount !== void 0 ? { wordCount: hit.wordCount } : {}
|
|
47618
47851
|
}));
|
|
47619
47852
|
return {
|
|
47620
47853
|
notes,
|
|
@@ -47637,6 +47870,55 @@ function contentSearchFailureHint(message, dbUnavailable) {
|
|
|
47637
47870
|
const remedy = dbUnavailable === "no_fda" ? " Grant Full Disk Access to the Node binary running this server (run the doctor tool for its path) so search-notes can search note bodies through the Notes database instead, which takes well under a second." : "";
|
|
47638
47871
|
return `${message} Body search through AppleScript scans every note body before the result limit applies, so a broad term can exceed the time budget on a large library.${remedy} Otherwise narrow the search with \`folder\` or \`modifiedSince\`, or use a more specific term.`;
|
|
47639
47872
|
}
|
|
47873
|
+
var NOTE_ID = /^x-coredata:\/\/([0-9A-Fa-f-]+)\/ICNote\/p(\d{1,15})$/;
|
|
47874
|
+
function addWordCountsFromDatabase(notes, query2, options = {}) {
|
|
47875
|
+
const keys = /* @__PURE__ */ new Map();
|
|
47876
|
+
for (const note of notes) {
|
|
47877
|
+
const match = NOTE_ID.exec(note.id ?? "");
|
|
47878
|
+
if (match) keys.set(note, { store: match[1].toUpperCase(), pk: Number(match[2]) });
|
|
47879
|
+
}
|
|
47880
|
+
const pks = [...new Set([...keys.values()].map((key) => key.pk))];
|
|
47881
|
+
let store;
|
|
47882
|
+
const texts = /* @__PURE__ */ new Map();
|
|
47883
|
+
try {
|
|
47884
|
+
for (let start = 0; start < pks.length; start += NOTE_TEXT_BATCH_MAX) {
|
|
47885
|
+
const read = readNoteTexts(pks.slice(start, start + NOTE_TEXT_BATCH_MAX), options);
|
|
47886
|
+
store = read.uuid?.toUpperCase();
|
|
47887
|
+
for (const [pk, text2] of read.texts) texts.set(pk, text2);
|
|
47888
|
+
}
|
|
47889
|
+
} catch (error2) {
|
|
47890
|
+
if (error2 instanceof NoteQueryStoreError) return { notes, unavailable: error2.kind };
|
|
47891
|
+
throw error2;
|
|
47892
|
+
}
|
|
47893
|
+
const predicates = [{ field: "any", value: query2 }];
|
|
47894
|
+
return {
|
|
47895
|
+
notes: notes.map((note) => {
|
|
47896
|
+
const key = keys.get(note);
|
|
47897
|
+
const text2 = key && key.store === store ? texts.get(key.pk) : void 0;
|
|
47898
|
+
if (typeof text2 !== "string") return { ...note, wordCount: null };
|
|
47899
|
+
const matchedIn = matchLocations(predicates, note.title, text2);
|
|
47900
|
+
return {
|
|
47901
|
+
...note,
|
|
47902
|
+
...matchedIn?.length ? { matchedIn } : {},
|
|
47903
|
+
wordCount: countWords(text2)
|
|
47904
|
+
};
|
|
47905
|
+
})
|
|
47906
|
+
};
|
|
47907
|
+
}
|
|
47908
|
+
function describeMatchDetails(hit) {
|
|
47909
|
+
const parts = [];
|
|
47910
|
+
if (hit.matchedIn) {
|
|
47911
|
+
parts.push(
|
|
47912
|
+
hit.matchedIn.length ? `matched in ${hit.matchedIn.join(", ")}` : "no text match (metadata)"
|
|
47913
|
+
);
|
|
47914
|
+
}
|
|
47915
|
+
if (hit.wordCount !== void 0) {
|
|
47916
|
+
parts.push(
|
|
47917
|
+
hit.wordCount === null ? "word count unavailable" : `${hit.wordCount} word${hit.wordCount === 1 ? "" : "s"}`
|
|
47918
|
+
);
|
|
47919
|
+
}
|
|
47920
|
+
return parts.map((part) => ` \xB7 ${part}`).join("");
|
|
47921
|
+
}
|
|
47640
47922
|
|
|
47641
47923
|
// src/tools/doctor.ts
|
|
47642
47924
|
import { spawnSync } from "child_process";
|
|
@@ -48358,12 +48640,15 @@ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
|
|
|
48358
48640
|
const segments = (path10) => JSON.stringify(splitFolderPath(path10).map((part) => part.toLocaleLowerCase()));
|
|
48359
48641
|
if (request.folder) {
|
|
48360
48642
|
const wanted = segments(request.folder);
|
|
48361
|
-
|
|
48362
|
-
|
|
48643
|
+
const accounts = manager.listAccounts().filter((account) => account.defaultFolder);
|
|
48644
|
+
if (!accounts.some(
|
|
48645
|
+
(account) => manager.listFolders(account.name).some((folder) => segments(folder.name) === wanted)
|
|
48363
48646
|
))
|
|
48364
48647
|
throw new Error(
|
|
48365
48648
|
`Folder "${request.folder}" does not exist; create it with create-folder first. Nothing was created`
|
|
48366
48649
|
);
|
|
48650
|
+
for (const account of accounts)
|
|
48651
|
+
manager.assertNotSmartFolderDestination(request.folder, account.name);
|
|
48367
48652
|
}
|
|
48368
48653
|
const defaultFolderNotes = () => new Map(
|
|
48369
48654
|
manager.listAccounts().flatMap(
|
|
@@ -49031,77 +49316,6 @@ function withJsonSchema2020_12(transport2) {
|
|
|
49031
49316
|
return transport2;
|
|
49032
49317
|
}
|
|
49033
49318
|
|
|
49034
|
-
// src/utils/errorCodes.ts
|
|
49035
|
-
var CodedError = class extends Error {
|
|
49036
|
-
envelope;
|
|
49037
|
-
constructor(message, envelope) {
|
|
49038
|
-
super(message);
|
|
49039
|
-
this.name = "CodedError";
|
|
49040
|
-
this.envelope = envelope;
|
|
49041
|
-
}
|
|
49042
|
-
};
|
|
49043
|
-
var RULES = [
|
|
49044
|
-
{ code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
|
|
49045
|
-
{
|
|
49046
|
-
code: "verification_failed",
|
|
49047
|
-
pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
|
|
49048
|
-
},
|
|
49049
|
-
{
|
|
49050
|
-
code: "revision_conflict",
|
|
49051
|
-
pattern: /changed after it was read|revision changed|(?:pinned|note) state changed|changed during (?:read|preflight)|changed; read (?:it|the note) again|rich text do not match/i
|
|
49052
|
-
},
|
|
49053
|
-
{ code: "full_disk_access_missing", pattern: /Full Disk Access/i },
|
|
49054
|
-
{
|
|
49055
|
-
code: "shortcut_not_installed",
|
|
49056
|
-
pattern: /(?:Install|Import) the supplied|no such shortcut|Run apple-notes-mcp setup/i
|
|
49057
|
-
},
|
|
49058
|
-
{
|
|
49059
|
-
code: "not_found",
|
|
49060
|
-
pattern: /not found|does not exist|\bNo \w+ found\b|does not contain any checklist items|Can[’']t get (?:note|folder|account|attachment)|Cannot find note|Scope is absent/i
|
|
49061
|
-
},
|
|
49062
|
-
{ code: "ambiguous", pattern: /ambiguous|more than one|exactly once|Duplicate note IDs/i },
|
|
49063
|
-
{
|
|
49064
|
-
code: "notes_unavailable",
|
|
49065
|
-
pattern: /Notes\.app is (?:not responding|busy)|Lost connection to Notes|isn't running/i
|
|
49066
|
-
},
|
|
49067
|
-
{
|
|
49068
|
-
code: "unsupported",
|
|
49069
|
-
pattern: /password-protected|Locked notes|unsupported|not supported|is blocked|blocked because|has not passed live|refuses to sign|No (?:supported )?background|only one where|Real Notes link unavailable/i
|
|
49070
|
-
},
|
|
49071
|
-
{
|
|
49072
|
-
code: "validation_error",
|
|
49073
|
-
pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
|
|
49074
|
-
}
|
|
49075
|
-
];
|
|
49076
|
-
var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
|
|
49077
|
-
var WRITE_ACCEPTED = /\baccepted (?:the|an) \w+, but/i;
|
|
49078
|
-
function classifyError(message, cause) {
|
|
49079
|
-
if (cause instanceof CodedError) return { ...cause.envelope };
|
|
49080
|
-
const text2 = cause instanceof Error && cause.message && !message.includes(cause.message) ? `${message}
|
|
49081
|
-
${cause.message}` : message;
|
|
49082
|
-
const timeoutCause = typeof cause === "object" && cause !== null && cause.code === "ETIMEDOUT";
|
|
49083
|
-
let code = "operation_failed";
|
|
49084
|
-
if (timeoutCause) code = "timeout_indeterminate";
|
|
49085
|
-
else if (isPermissionDenied(text2)) code = "permission_denied";
|
|
49086
|
-
else code = RULES.find((rule) => rule.pattern.test(text2))?.code ?? "operation_failed";
|
|
49087
|
-
const envelope = { code };
|
|
49088
|
-
if (code === "timeout_indeterminate" || code === "verification_failed") {
|
|
49089
|
-
envelope.indeterminate = true;
|
|
49090
|
-
if (WRITE_ACCEPTED.test(text2)) envelope.committed = true;
|
|
49091
|
-
} else if (code === "revision_conflict" || NOTHING_WRITTEN.test(text2)) {
|
|
49092
|
-
envelope.committed = false;
|
|
49093
|
-
envelope.indeterminate = false;
|
|
49094
|
-
}
|
|
49095
|
-
return envelope;
|
|
49096
|
-
}
|
|
49097
|
-
function errorResult(message, cause) {
|
|
49098
|
-
return {
|
|
49099
|
-
content: [{ type: "text", text: message }],
|
|
49100
|
-
structuredContent: classifyError(message, cause),
|
|
49101
|
-
isError: true
|
|
49102
|
-
};
|
|
49103
|
-
}
|
|
49104
|
-
|
|
49105
49319
|
// src/utils/shutdown.ts
|
|
49106
49320
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 2e3;
|
|
49107
49321
|
function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
|
|
@@ -49255,7 +49469,7 @@ function selectParagraph(paragraphs, selector) {
|
|
|
49255
49469
|
);
|
|
49256
49470
|
return matches[occurrence - 1];
|
|
49257
49471
|
}
|
|
49258
|
-
var
|
|
49472
|
+
var NOTE_ID2 = /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p([0-9]{1,15})$/i;
|
|
49259
49473
|
function titleMatchSql(columns) {
|
|
49260
49474
|
return `SELECT json_object('pk', n.Z_PK, 'folder', n.ZFOLDER) FROM ZICCLOUDSYNCINGOBJECT n LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = n.ZFOLDER WHERE n.Z_ENT = ${entity("ICNote")} AND n.ZTITLE1 = CAST(@title AS TEXT) AND ${activeNoteSql(columns, "n", "f")} ORDER BY n.Z_PK;`;
|
|
49261
49475
|
}
|
|
@@ -49266,7 +49480,7 @@ function checkNoteSelector(selector) {
|
|
|
49266
49480
|
if (selector.folder !== void 0 && selector.title === void 0)
|
|
49267
49481
|
throw new ParagraphLinkError("invalid-argument", "folder only narrows a title lookup");
|
|
49268
49482
|
if (selector.id === void 0) return void 0;
|
|
49269
|
-
const pk =
|
|
49483
|
+
const pk = NOTE_ID2.exec(selector.id)?.[1];
|
|
49270
49484
|
if (!pk)
|
|
49271
49485
|
throw new ParagraphLinkError(
|
|
49272
49486
|
"invalid-argument",
|
|
@@ -51577,7 +51791,9 @@ function registerDirectOperations(server2, manager) {
|
|
|
51577
51791
|
{
|
|
51578
51792
|
title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
|
|
51579
51793
|
content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
|
|
51580
|
-
folder: external_exports.string().max(1e3).optional().describe(
|
|
51794
|
+
folder: external_exports.string().max(1e3).optional().describe(
|
|
51795
|
+
"Existing folder or nested path; create it first with create-folder. A smart folder is refused before the note is created"
|
|
51796
|
+
),
|
|
51581
51797
|
account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
|
|
51582
51798
|
...attachmentInput
|
|
51583
51799
|
},
|
|
@@ -56607,7 +56823,7 @@ function registerTool(name, config2, cb) {
|
|
|
56607
56823
|
registerTool(
|
|
56608
56824
|
"create-note",
|
|
56609
56825
|
{
|
|
56610
|
-
description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field). A 'folder' must already exist \u2014 create-folder first (it is idempotent), since this tool does not create it.",
|
|
56826
|
+
description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field). A 'folder' must already exist \u2014 create-folder first (it is idempotent), since this tool does not create it. A smart folder is refused (code unsupported); it cannot hold notes.",
|
|
56611
56827
|
inputSchema: {
|
|
56612
56828
|
title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
|
|
56613
56829
|
content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).optional().describe(
|
|
@@ -56747,7 +56963,7 @@ registerTool(
|
|
|
56747
56963
|
registerTool(
|
|
56748
56964
|
"search-notes",
|
|
56749
56965
|
{
|
|
56750
|
-
description: "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id; `source` says whether a body search read the Notes database or fell back to AppleScript.\nDo not use when: you already have a note id (use get-note-content), want every note (use list-notes), or need boolean or metadata filters (use query-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.\nNote: with Full Disk Access, body search reads the Notes database (fast; the most recent 5000 notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on broad terms.",
|
|
56966
|
+
description: "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id; `source` says whether a body search read the Notes database or fell back to AppleScript. When the note text came from the database, each result has matchedIn (title, body, or both); includeWordCount adds wordCount.\nDo not use when: you already have a note id (use get-note-content), want every note (use list-notes), or need boolean or metadata filters (use query-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.\nNote: with Full Disk Access, body search reads the Notes database (fast; the most recent 5000 notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on broad terms.",
|
|
56751
56967
|
inputSchema: {
|
|
56752
56968
|
query: external_exports.string().min(1, "Search query is required").max(MAX.QUERY),
|
|
56753
56969
|
searchContent: external_exports.boolean().optional().describe(
|
|
@@ -56760,112 +56976,132 @@ registerTool(
|
|
|
56760
56976
|
),
|
|
56761
56977
|
limit: external_exports.number().int().positive().optional().describe(
|
|
56762
56978
|
"Maximum number of results to return. Defaults to 50 \u2014 a broad query reads several properties per match via AppleScript, so an unbounded search can time out. Pass a higher value to see more; the applied limit is disclosed in the response."
|
|
56979
|
+
),
|
|
56980
|
+
includeWordCount: external_exports.boolean().optional().describe(
|
|
56981
|
+
"Add wordCount to each result (null when locked or unreadable). A database body search already has the text; otherwise the bodies are read in one batched read-only database query (needs Full Disk Access), which also adds matchedIn"
|
|
56763
56982
|
)
|
|
56764
56983
|
},
|
|
56765
56984
|
outputSchema: {
|
|
56766
56985
|
notes: external_exports.array(external_exports.object({}).passthrough()).optional(),
|
|
56767
56986
|
count: external_exports.number().optional(),
|
|
56768
56987
|
source: external_exports.enum(["database", "applescript"]).optional(),
|
|
56769
|
-
scanTruncated: external_exports.boolean().optional()
|
|
56988
|
+
scanTruncated: external_exports.boolean().optional(),
|
|
56989
|
+
wordCountUnavailable: external_exports.string().optional()
|
|
56770
56990
|
}
|
|
56771
56991
|
},
|
|
56772
|
-
withErrorHandling(
|
|
56773
|
-
|
|
56774
|
-
|
|
56775
|
-
|
|
56776
|
-
|
|
56777
|
-
|
|
56778
|
-
|
|
56779
|
-
|
|
56992
|
+
withErrorHandling(
|
|
56993
|
+
({ query: query2, searchContent = false, account, folder, modifiedSince, limit, includeWordCount }) => {
|
|
56994
|
+
const effectiveLimit = resolveSearchLimit(limit);
|
|
56995
|
+
const limitWasDefault = limit === void 0;
|
|
56996
|
+
let source;
|
|
56997
|
+
let dbScan;
|
|
56998
|
+
let dbUnavailable;
|
|
56999
|
+
const runSearch = () => {
|
|
57000
|
+
if (searchContent) {
|
|
57001
|
+
try {
|
|
57002
|
+
const db = searchContentViaDatabase({
|
|
57003
|
+
query: query2,
|
|
57004
|
+
account: notesManager.searchAccountScope(account),
|
|
57005
|
+
folder,
|
|
57006
|
+
modifiedSince,
|
|
57007
|
+
limit: effectiveLimit,
|
|
57008
|
+
includeWordCount
|
|
57009
|
+
});
|
|
57010
|
+
source = "database";
|
|
57011
|
+
dbScan = db.scan;
|
|
57012
|
+
return db.notes;
|
|
57013
|
+
} catch (error2) {
|
|
57014
|
+
if (!(error2 instanceof NoteQueryStoreError)) throw error2;
|
|
57015
|
+
dbUnavailable = error2.kind;
|
|
57016
|
+
}
|
|
57017
|
+
source = "applescript";
|
|
57018
|
+
}
|
|
56780
57019
|
try {
|
|
56781
|
-
|
|
56782
|
-
|
|
56783
|
-
|
|
57020
|
+
return notesManager.searchNotes(
|
|
57021
|
+
query2,
|
|
57022
|
+
searchContent,
|
|
57023
|
+
account,
|
|
56784
57024
|
folder,
|
|
56785
57025
|
modifiedSince,
|
|
56786
|
-
|
|
56787
|
-
|
|
56788
|
-
source = "database";
|
|
56789
|
-
dbScan = db.scan;
|
|
56790
|
-
return db.notes;
|
|
57026
|
+
effectiveLimit
|
|
57027
|
+
);
|
|
56791
57028
|
} catch (error2) {
|
|
56792
|
-
if (!(error2 instanceof
|
|
56793
|
-
|
|
57029
|
+
if (!searchContent || !(error2 instanceof Error)) throw error2;
|
|
57030
|
+
throw new Error(contentSearchFailureHint(error2.message, dbUnavailable));
|
|
56794
57031
|
}
|
|
56795
|
-
|
|
57032
|
+
};
|
|
57033
|
+
const {
|
|
57034
|
+
result: found,
|
|
57035
|
+
syncBefore,
|
|
57036
|
+
syncInterference
|
|
57037
|
+
} = withSyncAwarenessSync("search-notes", runSearch);
|
|
57038
|
+
let notes = found;
|
|
57039
|
+
let wordCountUnavailable;
|
|
57040
|
+
if (includeWordCount && source !== "database" && found.length > 0) {
|
|
57041
|
+
const enriched = addWordCountsFromDatabase(found, query2);
|
|
57042
|
+
notes = enriched.notes;
|
|
57043
|
+
wordCountUnavailable = enriched.unavailable;
|
|
57044
|
+
}
|
|
57045
|
+
const wordCountFields = wordCountUnavailable ? { wordCountUnavailable } : {};
|
|
57046
|
+
const wordCountNote = wordCountUnavailable ? `
|
|
57047
|
+
|
|
57048
|
+
\u2139\uFE0F Word counts were not added: ${wordCountUnavailable === "no_fda" ? "they are read from the Notes database, which needs Full Disk Access (run the doctor tool)" : "the Notes database could not be read"}.` : "";
|
|
57049
|
+
const searchType = searchContent ? source === "database" ? "content via the Notes database" : "content" : "titles";
|
|
57050
|
+
const sourceFields = source ? { source } : {};
|
|
57051
|
+
const scanFields = dbScan ? { scanTruncated: dbScan.scanTruncated } : {};
|
|
57052
|
+
const scanNote = describeContentScan(dbScan);
|
|
57053
|
+
const folderInfo = folder ? ` in folder "${folder}"` : "";
|
|
57054
|
+
const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
|
|
57055
|
+
const { info: limitInfo, truncationNote } = describeSearchLimit(
|
|
57056
|
+
effectiveLimit,
|
|
57057
|
+
limitWasDefault,
|
|
57058
|
+
notes.length
|
|
57059
|
+
);
|
|
57060
|
+
const syncWarnings = [];
|
|
57061
|
+
if (syncBefore.syncDetected) {
|
|
57062
|
+
syncWarnings.push(`\u26A0\uFE0F iCloud sync was active during search.`);
|
|
56796
57063
|
}
|
|
56797
|
-
|
|
56798
|
-
|
|
56799
|
-
query2,
|
|
56800
|
-
searchContent,
|
|
56801
|
-
account,
|
|
56802
|
-
folder,
|
|
56803
|
-
modifiedSince,
|
|
56804
|
-
effectiveLimit
|
|
56805
|
-
);
|
|
56806
|
-
} catch (error2) {
|
|
56807
|
-
if (!searchContent || !(error2 instanceof Error)) throw error2;
|
|
56808
|
-
throw new Error(contentSearchFailureHint(error2.message, dbUnavailable));
|
|
57064
|
+
if (syncInterference) {
|
|
57065
|
+
syncWarnings.push(`\u26A0\uFE0F Sync activity detected - results may be incomplete.`);
|
|
56809
57066
|
}
|
|
56810
|
-
|
|
56811
|
-
const {
|
|
56812
|
-
result: notes,
|
|
56813
|
-
syncBefore,
|
|
56814
|
-
syncInterference
|
|
56815
|
-
} = withSyncAwarenessSync("search-notes", runSearch);
|
|
56816
|
-
const searchType = searchContent ? source === "database" ? "content via the Notes database" : "content" : "titles";
|
|
56817
|
-
const sourceFields = source ? { source } : {};
|
|
56818
|
-
const scanFields = dbScan ? { scanTruncated: dbScan.scanTruncated } : {};
|
|
56819
|
-
const scanNote = describeContentScan(dbScan);
|
|
56820
|
-
const folderInfo = folder ? ` in folder "${folder}"` : "";
|
|
56821
|
-
const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
|
|
56822
|
-
const { info: limitInfo, truncationNote } = describeSearchLimit(
|
|
56823
|
-
effectiveLimit,
|
|
56824
|
-
limitWasDefault,
|
|
56825
|
-
notes.length
|
|
56826
|
-
);
|
|
56827
|
-
const syncWarnings = [];
|
|
56828
|
-
if (syncBefore.syncDetected) {
|
|
56829
|
-
syncWarnings.push(`\u26A0\uFE0F iCloud sync was active during search.`);
|
|
56830
|
-
}
|
|
56831
|
-
if (syncInterference) {
|
|
56832
|
-
syncWarnings.push(`\u26A0\uFE0F Sync activity detected - results may be incomplete.`);
|
|
56833
|
-
}
|
|
56834
|
-
const syncNote = syncWarnings.length > 0 ? `
|
|
57067
|
+
const syncNote = syncWarnings.length > 0 ? `
|
|
56835
57068
|
|
|
56836
57069
|
${syncWarnings.join(" ")}` : "";
|
|
56837
|
-
|
|
56838
|
-
|
|
57070
|
+
if (notes.length === 0) {
|
|
57071
|
+
const scopeHint = describeSearchScope(searchContent, notes.length);
|
|
57072
|
+
return successResponse(
|
|
57073
|
+
`No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${scanNote}${syncNote}`,
|
|
57074
|
+
{ notes: [], count: 0, ...sourceFields, ...scanFields }
|
|
57075
|
+
);
|
|
57076
|
+
}
|
|
57077
|
+
const noteList = notes.map((n) => {
|
|
57078
|
+
const idSuffix = `${n.id ? ` [id: ${n.id}]` : ""}${describeMatchDetails(n)}`;
|
|
57079
|
+
if (n.folder === "Recently Deleted") {
|
|
57080
|
+
return ` - ${n.title} [DELETED]${idSuffix}`;
|
|
57081
|
+
} else if (n.folder) {
|
|
57082
|
+
return ` - ${n.title} (${n.folder})${idSuffix}`;
|
|
57083
|
+
}
|
|
57084
|
+
return ` - ${n.title}${idSuffix}`;
|
|
57085
|
+
}).join("\n");
|
|
56839
57086
|
return successResponse(
|
|
56840
|
-
`
|
|
56841
|
-
|
|
57087
|
+
`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):
|
|
57088
|
+
${noteList}${truncationNote}${scanNote}${wordCountNote}${syncNote}`,
|
|
57089
|
+
{
|
|
57090
|
+
notes: withStableIdentifiers(notes, "ICNote"),
|
|
57091
|
+
count: notes.length,
|
|
57092
|
+
...sourceFields,
|
|
57093
|
+
...scanFields,
|
|
57094
|
+
...wordCountFields
|
|
57095
|
+
}
|
|
56842
57096
|
);
|
|
56843
|
-
}
|
|
56844
|
-
|
|
56845
|
-
|
|
56846
|
-
if (n.folder === "Recently Deleted") {
|
|
56847
|
-
return ` - ${n.title} [DELETED]${idSuffix}`;
|
|
56848
|
-
} else if (n.folder) {
|
|
56849
|
-
return ` - ${n.title} (${n.folder})${idSuffix}`;
|
|
56850
|
-
}
|
|
56851
|
-
return ` - ${n.title}${idSuffix}`;
|
|
56852
|
-
}).join("\n");
|
|
56853
|
-
return successResponse(
|
|
56854
|
-
`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):
|
|
56855
|
-
${noteList}${truncationNote}${scanNote}${syncNote}`,
|
|
56856
|
-
{
|
|
56857
|
-
notes: withStableIdentifiers(notes, "ICNote"),
|
|
56858
|
-
count: notes.length,
|
|
56859
|
-
...sourceFields,
|
|
56860
|
-
...scanFields
|
|
56861
|
-
}
|
|
56862
|
-
);
|
|
56863
|
-
}, "Error searching notes")
|
|
57097
|
+
},
|
|
57098
|
+
"Error searching notes"
|
|
57099
|
+
)
|
|
56864
57100
|
);
|
|
56865
57101
|
registerTool(
|
|
56866
57102
|
"query-notes",
|
|
56867
57103
|
{
|
|
56868
|
-
description: 'Use when: finding notes with a boolean expression over text and metadata \u2014 e.g. `folder:Work has:checklist -checklist:done`, `(title:invoice OR tag:finance) modified:>=2026-07-01`, `pinned words:>250`. Reads the Notes database directly, so it is fast and can match title OR body in one call.\nSyntax: bare words and "quoted phrases" match title or body (case-insensitive substring); fields title:, body:, text:, folder:, account:, tag: (values may be quoted, e.g. folder:"Work Projects"); facets has:link|attachment|checklist|drawing|image|video|audio|pdf|table|scan|tag; checklist:open|done; flags pinned, locked, shared (or is:pinned); words:>250 and created:/modified: with =, >, >=, <, <= and YYYY-MM-DD local dates. AND is implicit; OR, NOT, leading -, and parentheses are supported; operators are case-insensitive and a quoted "and" searches the literal word.\nReturns: matching notes (most recently modified first) with id, title, folder, account, modified date, and
|
|
57104
|
+
description: 'Use when: finding notes with a boolean expression over text and metadata \u2014 e.g. `folder:Work has:checklist -checklist:done`, `(title:invoice OR tag:finance) modified:>=2026-07-01`, `pinned words:>250`. Reads the Notes database directly, so it is fast and can match title OR body in one call.\nSyntax: bare words and "quoted phrases" match title or body (case-insensitive substring); fields title:, body:, text:, folder:, account:, tag: (values may be quoted, e.g. folder:"Work Projects"); facets has:link|attachment|checklist|drawing|image|video|audio|pdf|table|scan|tag; checklist:open|done; flags pinned, locked, shared (or is:pinned); words:>250 and created:/modified: with =, >, >=, <, <= and YYYY-MM-DD local dates. AND is implicit; OR, NOT, leading -, and parentheses are supported; operators are case-insensitive and a quoted "and" searches the literal word.\nReturns: matching notes (most recently modified first) with id, title, folder, account, modified date, snippet, and matchedIn (where the positive text terms occur: title, body, or both; absent when the body is unreadable or the query has no text term), plus scan/match counts; includeWordCount adds wordCount. Ids work with get-note-content and every other id-based tool.\nDo not use when: Full Disk Access is unavailable (use search-notes). Scans the most recent scanLimit notes (default 500); raise it for older notes.\nSafety: read-only; never writes the database. Excludes Recently Deleted and folderless notes unless includeDeleted is true. Locked notes match on title and metadata only; body predicates never match them.',
|
|
56869
57105
|
inputSchema: {
|
|
56870
57106
|
query: external_exports.string().min(1, "A query expression is required").max(MAX.QUERY).describe(
|
|
56871
57107
|
'Boolean query expression, e.g. `folder:"Work Projects" has:checklist -checklist:done`'
|
|
@@ -56878,6 +57114,9 @@ registerTool(
|
|
|
56878
57114
|
),
|
|
56879
57115
|
includeDeleted: external_exports.boolean().optional().describe(
|
|
56880
57116
|
"Also scan notes in Recently Deleted, notes pending deletion, and folderless notes (default false)"
|
|
57117
|
+
),
|
|
57118
|
+
includeWordCount: external_exports.boolean().optional().describe(
|
|
57119
|
+
"Add wordCount to each returned note (the count words: filters on; null when locked or unreadable). Free when the query reads bodies; a metadata-only query reads just the returned notes' bodies in one extra query (default false)"
|
|
56881
57120
|
)
|
|
56882
57121
|
},
|
|
56883
57122
|
outputSchema: {
|
|
@@ -56894,10 +57133,10 @@ registerTool(
|
|
|
56894
57133
|
},
|
|
56895
57134
|
annotations: { readOnlyHint: true }
|
|
56896
57135
|
},
|
|
56897
|
-
withErrorHandling(({ query: query2, limit, scanLimit, includeDeleted }) => {
|
|
57136
|
+
withErrorHandling(({ query: query2, limit, scanLimit, includeDeleted, includeWordCount }) => {
|
|
56898
57137
|
let result;
|
|
56899
57138
|
try {
|
|
56900
|
-
result = queryNotes(query2, { limit, scanLimit, includeDeleted });
|
|
57139
|
+
result = queryNotes(query2, { limit, scanLimit, includeDeleted, includeWordCount });
|
|
56901
57140
|
} catch (error2) {
|
|
56902
57141
|
if (error2 instanceof NoteQueryError || error2 instanceof NoteQueryStoreError) {
|
|
56903
57142
|
return errorResponse(
|
|
@@ -56928,7 +57167,7 @@ ${notes.join("\n")}` : "";
|
|
|
56928
57167
|
const where = [n.account, n.folder].filter(Boolean).join(" / ");
|
|
56929
57168
|
const snippet = n.snippet ? `
|
|
56930
57169
|
${n.snippet}` : "";
|
|
56931
|
-
return ` - ${n.title}${where ? ` (${where})` : ""}${n.locked ? " [locked]" : ""} [id: ${n.id}]${snippet}`;
|
|
57170
|
+
return ` - ${n.title}${where ? ` (${where})` : ""}${n.locked ? " [locked]" : ""} [id: ${n.id}]${describeMatchDetails(n)}${snippet}`;
|
|
56932
57171
|
}).join("\n");
|
|
56933
57172
|
return successResponse(
|
|
56934
57173
|
`Found ${result.matched} matching notes (${scope2}):
|
|
@@ -58099,7 +58338,7 @@ registerTool(
|
|
|
58099
58338
|
registerTool(
|
|
58100
58339
|
"move-note",
|
|
58101
58340
|
{
|
|
58102
|
-
description: "Use when: moving one exact note to a different folder by id.\nReturns: confirmation and exact-ID readback.\nDo not use when: you only have a title or want to move many notes (batch-move-notes).\nNote: Notes.app's native move preserves the note id, creation date, body, and attachments. The destination folder must already exist. Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds (which also covers the destination) are re-checked inside the move AppleScript.",
|
|
58341
|
+
description: "Use when: moving one exact note to a different folder by id.\nReturns: confirmation and exact-ID readback.\nDo not use when: you only have a title or want to move many notes (batch-move-notes).\nNote: Notes.app's native move preserves the note id, creation date, body, and attachments. The destination folder must already exist and cannot be a smart folder (refused with code unsupported before the move). Optional ifFolderId, ifAncestorFolderId, and forbiddenAncestorFolderIds (which also covers the destination) are re-checked inside the move AppleScript.",
|
|
58103
58342
|
inputSchema: {
|
|
58104
58343
|
id: noteIdInput,
|
|
58105
58344
|
folder: external_exports.string().min(1, "Destination folder is required").max(MAX.FOLDER),
|
|
@@ -58341,7 +58580,7 @@ ${lines.join("\n")}${fullyDecoded ? "" : '\n\nSome rules were not recognized; se
|
|
|
58341
58580
|
registerTool(
|
|
58342
58581
|
"create-folder",
|
|
58343
58582
|
{
|
|
58344
|
-
description: "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nDo not use when: creating a note (create-note).",
|
|
58583
|
+
description: "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nNote: a path segment that names a smart folder is refused (code unsupported) before anything is created; smart folders cannot hold folders.\nDo not use when: creating a note (create-note).",
|
|
58345
58584
|
inputSchema: {
|
|
58346
58585
|
name: external_exports.string().min(1, "Folder name is required").max(MAX.FOLDER).describe(
|
|
58347
58586
|
'Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'
|
|
@@ -58782,7 +59021,7 @@ registerTool(
|
|
|
58782
59021
|
registerTool(
|
|
58783
59022
|
"batch-move-notes",
|
|
58784
59023
|
{
|
|
58785
|
-
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder).",
|
|
59024
|
+
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder); a smart folder is refused for the whole call (code unsupported) before any note moves.",
|
|
58786
59025
|
inputSchema: {
|
|
58787
59026
|
ids: noteIdArrayInput.describe(
|
|
58788
59027
|
`Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
|
package/package.json
CHANGED