apple-notes-mcp 2.9.2 → 2.9.4
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 +114 -2
- package/build/index.js +677 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -634,6 +634,118 @@ stable code in brackets, such as `[encrypted]` or `[no-full-disk-access]`.
|
|
|
634
634
|
|
|
635
635
|
---
|
|
636
636
|
|
|
637
|
+
#### `list-note-paragraphs`
|
|
638
|
+
|
|
639
|
+
Lists one note's non-empty paragraphs in body order, read-only from the
|
|
640
|
+
NoteStore database. Each paragraph has `blockIndex` (its index in
|
|
641
|
+
`get-note-blocks`, where empty paragraphs also count), `text`, `style`,
|
|
642
|
+
`styleType`, `paragraphId` (the UUID Notes stores on the paragraph's first
|
|
643
|
+
text run), and `paragraphIdStatus`:
|
|
644
|
+
|
|
645
|
+
- `unique`: no other paragraph in the note carries this ID. The paragraph also
|
|
646
|
+
gets `url`, a direct
|
|
647
|
+
`applenotes://showNote?identifier=<note>¶graphID=<paragraph>` link that
|
|
648
|
+
opens Notes at that paragraph.
|
|
649
|
+
- `shared`: other paragraphs carry the same ID (`sharedWith` counts them).
|
|
650
|
+
Notes copies the ID when a paragraph is split, so this is common for body
|
|
651
|
+
text. No `url` is given, because it could open the wrong paragraph.
|
|
652
|
+
- `missing`: no ID is stored.
|
|
653
|
+
|
|
654
|
+
`mixedParagraphIds: true` marks a paragraph whose text runs carry more than
|
|
655
|
+
one ID; only the first-run ID is used. Titles and headings usually have unique
|
|
656
|
+
IDs.
|
|
657
|
+
|
|
658
|
+
**Requires:** Full Disk Access. Password-protected notes are refused.
|
|
659
|
+
|
|
660
|
+
| Parameter | Type | Required | Description |
|
|
661
|
+
|-----------|------|----------|-------------|
|
|
662
|
+
| `id` | string | One of `id`, `title` | Exact note ID, or the note's Notes UUID |
|
|
663
|
+
| `title` | string | One of `id`, `title` | Exact note title; must match one note. Notes in Recently Deleted are ignored, as in `list-notes` |
|
|
664
|
+
| `folder` | string | No | With `title`: the folder's name or full path as `list-folders` shows it, such as `Work/Clients` |
|
|
665
|
+
| `linkableOnly` | boolean | No | Return only paragraphs with a `url` |
|
|
666
|
+
| `offset` | number | No | First paragraph to return (default 0). Use `page.nextOffset` |
|
|
667
|
+
| `limit` | number | No | Maximum paragraphs per page (default 500, max 5000) |
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
#### `get-paragraph-link`
|
|
672
|
+
|
|
673
|
+
Returns a direct link to one paragraph of a note:
|
|
674
|
+
`applenotes://showNote?identifier=<note>¶graphID=<paragraph>`. It selects
|
|
675
|
+
the note like `list-note-paragraphs` and the paragraph by exactly one of:
|
|
676
|
+
|
|
677
|
+
- `contains`: a snippet of the paragraph. Case, runs of spaces, and Unicode
|
|
678
|
+
width are ignored.
|
|
679
|
+
- `match`: the whole paragraph, compared the same way.
|
|
680
|
+
- `blockIndex`: from `list-note-paragraphs`.
|
|
681
|
+
|
|
682
|
+
When `contains` or `match` hits several paragraphs, pass `occurrence` (1-based)
|
|
683
|
+
or a longer snippet.
|
|
684
|
+
|
|
685
|
+
The link is returned only when the paragraph's ID is `unique` in the note.
|
|
686
|
+
Otherwise the result is an error whose `structuredContent` carries the usual
|
|
687
|
+
`code` plus a `reason`: `paragraph-id-shared`, `paragraph-id-missing`,
|
|
688
|
+
`no-match`, `ambiguous-paragraph`, `occurrence-out-of-range`, `ambiguous-note`
|
|
689
|
+
(several notes have that title; the message lists their folders), `not-found`,
|
|
690
|
+
`encrypted`, or `no-body`. The tool never creates or changes a paragraph ID,
|
|
691
|
+
and an edit in Notes can later replace the ID and break the link.
|
|
692
|
+
|
|
693
|
+
**Requires:** Full Disk Access.
|
|
694
|
+
|
|
695
|
+
| Parameter | Type | Required | Description |
|
|
696
|
+
|-----------|------|----------|-------------|
|
|
697
|
+
| `id`, `title`, `folder` | string | One of `id`, `title` | Note selector, as in `list-note-paragraphs` |
|
|
698
|
+
| `contains` | string | One of `contains`, `match`, `blockIndex` | Snippet of the paragraph |
|
|
699
|
+
| `match` | string | One of `contains`, `match`, `blockIndex` | The whole paragraph |
|
|
700
|
+
| `blockIndex` | number | One of `contains`, `match`, `blockIndex` | The paragraph's `blockIndex` |
|
|
701
|
+
| `occurrence` | number | No | Which match to use when several paragraphs match |
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
#### `get-note-structure`
|
|
706
|
+
|
|
707
|
+
Returns a read-only overview of one note from the NoteStore database, in one
|
|
708
|
+
call:
|
|
709
|
+
|
|
710
|
+
- `text` (decoded body), `textLength`, `wordCount`, `charCount` (Unicode
|
|
711
|
+
characters, attachment placeholders excluded), and `blockSummary` (the
|
|
712
|
+
`get-note-blocks` summary counts).
|
|
713
|
+
- `links`, each with a `kind`: `inline` (a hyperlink on text), `card` (a rich
|
|
714
|
+
link preview attachment, with `attachmentId` and `previewPath`), `note` (a
|
|
715
|
+
native link chip to another note), or `section` (a native link chip to a
|
|
716
|
+
heading or paragraph, with `section`). Notes deep links also carry
|
|
717
|
+
`targetNote` and `paragraphId`. `linkCounts` totals them.
|
|
718
|
+
- `tags` (native tags in body order) and `attachments`, listed the way
|
|
719
|
+
[`list-attachments`](#list-attachments) with `includePaths` lists them: the
|
|
720
|
+
same `kind` (`image`, `scan`, `drawing`, `pdf`, `audio`, `video`, `url`,
|
|
721
|
+
`table`, `other`), the same body order, and the same `previewPath`, plus the
|
|
722
|
+
card `title`/`url`, `fileSize`, and body position. Gallery items and
|
|
723
|
+
recording parts are nested under `children`; `attachmentCount` counts
|
|
724
|
+
top-level attachments only.
|
|
725
|
+
- `deepLink`, `isShared` (the note or any enclosing folder is shared),
|
|
726
|
+
`isLocked`, `isPinned`, `inRecentlyDeleted`, `lastViewed`,
|
|
727
|
+
`checklistTotal`, `checklistDone`, `hasDrawing` (classic sketches and Paper
|
|
728
|
+
drawings), and `firstImage` (the same lead visual `list-attachments`
|
|
729
|
+
returns with `firstImage`, plus its attachment `id`).
|
|
730
|
+
|
|
731
|
+
`lastViewed` is an ISO date, or null with `lastViewedStatus` set to
|
|
732
|
+
`never-viewed`, `not-recorded`, `malformed`, or `unsupported` (the column
|
|
733
|
+
does not exist on this macOS version). A password-protected note returns its
|
|
734
|
+
metadata and attachment rows with `bodyDecoded: false` and the body-derived
|
|
735
|
+
fields null.
|
|
736
|
+
|
|
737
|
+
**Requires:** Full Disk Access.
|
|
738
|
+
|
|
739
|
+
| Parameter | Type | Required | Description |
|
|
740
|
+
|-----------|------|----------|-------------|
|
|
741
|
+
| `id` | string | Yes | Exact note ID: the `x-coredata://` id, or the note's Notes UUID or numeric key (see [Identifier forms](#identifier-forms)) |
|
|
742
|
+
| `includeText` | boolean | No | Include the decoded text (default true). Text larger than `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` is omitted with `textOmitted: true` |
|
|
743
|
+
|
|
744
|
+
Link URLs are returned as stored; check `linkSafe` before emitting one into
|
|
745
|
+
HTML.
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
637
749
|
#### `list-native-tags`
|
|
638
750
|
|
|
639
751
|
Lists actual native Notes tags. This differs from textual hashtag search. It is
|
|
@@ -2214,7 +2326,7 @@ All configuration is optional — the server works out of the box. Override beha
|
|
|
2214
2326
|
| `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
|
|
2215
2327
|
| `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win, and a write tool's `timeoutSeconds` argument overrides it for that call. |
|
|
2216
2328
|
| `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (8 MB) | Largest response `export-notes-json` sends; a page closes early to stay under it. `export-notes-markdown` returns inline Markdown up to half of it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages. |
|
|
2217
|
-
| `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. |
|
|
2329
|
+
| `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. [`get-note-structure`](#get-note-structure) also omits note text larger than this. A [`list-note-paragraphs`](#list-note-paragraphs) page also stops early to stay under it. |
|
|
2218
2330
|
| `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum attempts for a read-only AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection). `2` means one retry; set `1` to fail fast with no retries. Retries share the single `APPLE_NOTES_MCP_TIMEOUT_MS` budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a **timeout** has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry. |
|
|
2219
2331
|
| `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
|
|
2220
2332
|
| `APPLE_NOTES_MCP_ENABLE_PRIVATE` | unset | Set to `1` to allow the opt-in [private helper](#private-helper-opt-in-unsupported-apple-api). Any other value keeps it off. |
|
|
@@ -2247,7 +2359,7 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
|
|
|
2247
2359
|
|
|
2248
2360
|
## Full Disk Access
|
|
2249
2361
|
|
|
2250
|
-
Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
|
|
2362
|
+
Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `list-note-paragraphs`, `get-paragraph-link`, `get-note-structure`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
|
|
2251
2363
|
|
|
2252
2364
|
> 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
|
|
2253
2365
|
|
package/build/index.js
CHANGED
|
@@ -46173,10 +46173,10 @@ function parseSince(input) {
|
|
|
46173
46173
|
return { modified: (ms - CORE_DATA_EPOCH_MS) / 1e3 };
|
|
46174
46174
|
}
|
|
46175
46175
|
function textStats(text2) {
|
|
46176
|
-
const
|
|
46177
|
-
let
|
|
46178
|
-
for (const token of
|
|
46179
|
-
return { wordCount, charCount: [...
|
|
46176
|
+
const visible2 = text2.replace(OBJECT_REPLACEMENT, "");
|
|
46177
|
+
let wordCount2 = 0;
|
|
46178
|
+
for (const token of visible2.split(/\s+/u)) if (/[\p{L}\p{N}]/u.test(token)) wordCount2++;
|
|
46179
|
+
return { wordCount: wordCount2, charCount: [...visible2].length };
|
|
46180
46180
|
}
|
|
46181
46181
|
function previewText(text2) {
|
|
46182
46182
|
const flat = text2.replace(OBJECT_REPLACEMENT, " ").replace(/\s+/gu, " ").trim();
|
|
@@ -46329,12 +46329,12 @@ function folderTree(options = {}) {
|
|
|
46329
46329
|
}
|
|
46330
46330
|
function assembleFolderTree(context, counts, includeDeleted, scope2) {
|
|
46331
46331
|
const paths = folderPaths(context.folders);
|
|
46332
|
-
const
|
|
46332
|
+
const visible2 = context.folders.filter(
|
|
46333
46333
|
(folder) => (includeDeleted || !folder.tombstoned) && (!scope2 || folder.account === scope2.pk)
|
|
46334
46334
|
);
|
|
46335
|
-
const byPk = new Map(
|
|
46335
|
+
const byPk = new Map(visible2.map((folder) => [folder.pk, folder]));
|
|
46336
46336
|
const nodes = /* @__PURE__ */ new Map();
|
|
46337
|
-
for (const folder of
|
|
46337
|
+
for (const folder of visible2) {
|
|
46338
46338
|
const node = {
|
|
46339
46339
|
id: `x-coredata://${context.uuid}/ICFolder/p${folder.pk}`,
|
|
46340
46340
|
identifier: folder.identifier,
|
|
@@ -46358,7 +46358,7 @@ function assembleFolderTree(context, counts, includeDeleted, scope2) {
|
|
|
46358
46358
|
return folder.parent;
|
|
46359
46359
|
};
|
|
46360
46360
|
const roots = /* @__PURE__ */ new Map();
|
|
46361
|
-
for (const folder of
|
|
46361
|
+
for (const folder of visible2) {
|
|
46362
46362
|
const node = nodes.get(folder.pk);
|
|
46363
46363
|
const parent = parentOf(folder);
|
|
46364
46364
|
if (parent !== void 0) nodes.get(parent).children.push(node);
|
|
@@ -47790,7 +47790,7 @@ function renderMarkdown(markdown, options = {}) {
|
|
|
47790
47790
|
}
|
|
47791
47791
|
return text2;
|
|
47792
47792
|
};
|
|
47793
|
-
const
|
|
47793
|
+
const visible2 = (html2) => withoutTags(html2).replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
47794
47794
|
let html = "", list, quote = false, code, previousBlank = true;
|
|
47795
47795
|
const close = () => {
|
|
47796
47796
|
if (list) {
|
|
@@ -47837,7 +47837,7 @@ function renderMarkdown(markdown, options = {}) {
|
|
|
47837
47837
|
}
|
|
47838
47838
|
const rendered = inline(text2);
|
|
47839
47839
|
html += `<div>${rendered}</div>`;
|
|
47840
|
-
expect.quotes[expect.quotes.length - 1] += ` ${
|
|
47840
|
+
expect.quotes[expect.quotes.length - 1] += ` ${visible2(rendered)}`;
|
|
47841
47841
|
previousBlank = false;
|
|
47842
47842
|
continue;
|
|
47843
47843
|
}
|
|
@@ -47860,7 +47860,7 @@ function renderMarkdown(markdown, options = {}) {
|
|
|
47860
47860
|
}
|
|
47861
47861
|
const rendered = inline(task[2]);
|
|
47862
47862
|
html += `<li>${rendered}</li>`;
|
|
47863
|
-
expect.checklist.push({ text:
|
|
47863
|
+
expect.checklist.push({ text: visible2(rendered).trim(), done: task[1] === "x" });
|
|
47864
47864
|
previousBlank = false;
|
|
47865
47865
|
continue;
|
|
47866
47866
|
}
|
|
@@ -49052,6 +49052,536 @@ function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
|
|
|
49052
49052
|
};
|
|
49053
49053
|
}
|
|
49054
49054
|
|
|
49055
|
+
// src/utils/noteParagraphs.ts
|
|
49056
|
+
import { gunzipSync as gunzipSync9 } from "node:zlib";
|
|
49057
|
+
var ENVELOPE_CODE = {
|
|
49058
|
+
"invalid-argument": "validation_error",
|
|
49059
|
+
"not-found": "not_found",
|
|
49060
|
+
encrypted: "unsupported",
|
|
49061
|
+
"no-body": "unsupported",
|
|
49062
|
+
"ambiguous-note": "ambiguous",
|
|
49063
|
+
"no-match": "not_found",
|
|
49064
|
+
"ambiguous-paragraph": "ambiguous",
|
|
49065
|
+
"occurrence-out-of-range": "validation_error",
|
|
49066
|
+
"paragraph-id-missing": "unsupported",
|
|
49067
|
+
"paragraph-id-shared": "unsupported"
|
|
49068
|
+
};
|
|
49069
|
+
var ParagraphLinkError = class extends CodedError {
|
|
49070
|
+
reason;
|
|
49071
|
+
constructor(reason, message) {
|
|
49072
|
+
super(message, { code: ENVELOPE_CODE[reason], reason });
|
|
49073
|
+
this.name = "ParagraphLinkError";
|
|
49074
|
+
this.reason = reason;
|
|
49075
|
+
}
|
|
49076
|
+
};
|
|
49077
|
+
var paragraphUrl = (noteIdentifier, paragraphId) => `applenotes://showNote?identifier=${noteIdentifier.toUpperCase()}¶graphID=${paragraphId.toUpperCase()}`;
|
|
49078
|
+
var normalizeParagraphText = (text2) => text2.normalize("NFKC").replace(/\ufffc/g, "").split(/\s+/u).filter(Boolean).join(" ").toLowerCase();
|
|
49079
|
+
var bytesField = (fields, n) => fields.find((f) => f.fieldNumber === n && f.wireType === 2)?.bytes;
|
|
49080
|
+
var uuidOf = (bytes) => {
|
|
49081
|
+
if (bytes?.length !== 16) return void 0;
|
|
49082
|
+
const h = Buffer.from(bytes).toString("hex").toUpperCase();
|
|
49083
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
49084
|
+
};
|
|
49085
|
+
function runParagraphIds(data) {
|
|
49086
|
+
const document = decodeWireFields(bytesField(decodeWireFields(data), 2));
|
|
49087
|
+
const note = decodeWireFields(bytesField(document, 3));
|
|
49088
|
+
const runs = [];
|
|
49089
|
+
let start = 0;
|
|
49090
|
+
for (const field of note.filter((f) => f.fieldNumber === 5)) {
|
|
49091
|
+
const run = decodeWireFields(field.bytes);
|
|
49092
|
+
const length = Number(run.find((f) => f.fieldNumber === 1).varint);
|
|
49093
|
+
const style = bytesField(run, 2);
|
|
49094
|
+
const paragraphId = style ? uuidOf(bytesField(decodeWireFields(style), 9)) : void 0;
|
|
49095
|
+
runs.push({ start, length, ...paragraphId ? { paragraphId } : {} });
|
|
49096
|
+
start += length;
|
|
49097
|
+
}
|
|
49098
|
+
return runs;
|
|
49099
|
+
}
|
|
49100
|
+
function classifyParagraphIds(doc, runs) {
|
|
49101
|
+
const ranges = doc.blocks.map((block) => [block.start, block.start + block.length + 1]);
|
|
49102
|
+
const blocksOfId = /* @__PURE__ */ new Map();
|
|
49103
|
+
const idsOfBlock = doc.blocks.map(() => /* @__PURE__ */ new Set());
|
|
49104
|
+
let b = 0;
|
|
49105
|
+
for (const run of runs) {
|
|
49106
|
+
const end = run.start + run.length;
|
|
49107
|
+
while (b < ranges.length && ranges[b][1] <= run.start) b++;
|
|
49108
|
+
for (let i = b; i < ranges.length && ranges[i][0] < end; i++) {
|
|
49109
|
+
idsOfBlock[i].add(run.paragraphId);
|
|
49110
|
+
if (run.paragraphId) {
|
|
49111
|
+
const owners = blocksOfId.get(run.paragraphId) ?? /* @__PURE__ */ new Set();
|
|
49112
|
+
owners.add(i);
|
|
49113
|
+
blocksOfId.set(run.paragraphId, owners);
|
|
49114
|
+
}
|
|
49115
|
+
}
|
|
49116
|
+
}
|
|
49117
|
+
return doc.blocks.map((block, i) => {
|
|
49118
|
+
const paragraphId = block.paragraphUuid ?? null;
|
|
49119
|
+
const mixed = idsOfBlock[i].size > 1;
|
|
49120
|
+
if (!paragraphId) return { status: "missing", paragraphId, mixed };
|
|
49121
|
+
const owners = blocksOfId.get(paragraphId).size;
|
|
49122
|
+
return owners === 1 ? { status: "unique", paragraphId, mixed } : { status: "shared", paragraphId, sharedWith: owners - 1, mixed };
|
|
49123
|
+
});
|
|
49124
|
+
}
|
|
49125
|
+
function paragraphsOf(doc, runs, noteIdentifier) {
|
|
49126
|
+
const ids = classifyParagraphIds(doc, runs);
|
|
49127
|
+
const out = [];
|
|
49128
|
+
doc.blocks.forEach((block, i) => {
|
|
49129
|
+
if (!normalizeParagraphText(block.text)) return;
|
|
49130
|
+
const { status, paragraphId, sharedWith, mixed } = ids[i];
|
|
49131
|
+
out.push({
|
|
49132
|
+
blockIndex: block.index,
|
|
49133
|
+
text: block.text,
|
|
49134
|
+
style: block.style,
|
|
49135
|
+
styleType: block.styleType,
|
|
49136
|
+
paragraphId,
|
|
49137
|
+
paragraphIdStatus: status,
|
|
49138
|
+
...sharedWith !== void 0 ? { sharedWith } : {},
|
|
49139
|
+
...mixed ? { mixedParagraphIds: true } : {},
|
|
49140
|
+
...status === "unique" && noteIdentifier ? { url: paragraphUrl(noteIdentifier, paragraphId) } : {}
|
|
49141
|
+
});
|
|
49142
|
+
});
|
|
49143
|
+
return out;
|
|
49144
|
+
}
|
|
49145
|
+
function selectParagraph(paragraphs, selector) {
|
|
49146
|
+
const given = [selector.contains, selector.match, selector.blockIndex].filter(
|
|
49147
|
+
(value) => value !== void 0
|
|
49148
|
+
).length;
|
|
49149
|
+
if (given !== 1)
|
|
49150
|
+
throw new ParagraphLinkError(
|
|
49151
|
+
"invalid-argument",
|
|
49152
|
+
"Choose exactly one paragraph selector: contains, match, or blockIndex"
|
|
49153
|
+
);
|
|
49154
|
+
if (selector.blockIndex !== void 0) {
|
|
49155
|
+
const hit = paragraphs.find((p) => p.blockIndex === selector.blockIndex);
|
|
49156
|
+
if (!hit)
|
|
49157
|
+
throw new ParagraphLinkError(
|
|
49158
|
+
"no-match",
|
|
49159
|
+
`Block ${selector.blockIndex} is not a non-empty paragraph of this note`
|
|
49160
|
+
);
|
|
49161
|
+
return hit;
|
|
49162
|
+
}
|
|
49163
|
+
const wanted = normalizeParagraphText(selector.contains ?? selector.match);
|
|
49164
|
+
if (!wanted)
|
|
49165
|
+
throw new ParagraphLinkError("invalid-argument", "The paragraph selector has no visible text");
|
|
49166
|
+
const matches = paragraphs.filter(
|
|
49167
|
+
(p) => selector.match !== void 0 ? normalizeParagraphText(p.text) === wanted : normalizeParagraphText(p.text).includes(wanted)
|
|
49168
|
+
);
|
|
49169
|
+
if (!matches.length)
|
|
49170
|
+
throw new ParagraphLinkError("no-match", "No paragraph matches the selector");
|
|
49171
|
+
if (selector.occurrence === void 0 && matches.length > 1)
|
|
49172
|
+
throw new ParagraphLinkError(
|
|
49173
|
+
"ambiguous-paragraph",
|
|
49174
|
+
`The selector matches ${matches.length} paragraphs; use a longer snippet or pass occurrence (1-${matches.length})`
|
|
49175
|
+
);
|
|
49176
|
+
const occurrence = selector.occurrence ?? 1;
|
|
49177
|
+
if (occurrence > matches.length)
|
|
49178
|
+
throw new ParagraphLinkError(
|
|
49179
|
+
"occurrence-out-of-range",
|
|
49180
|
+
`Occurrence ${occurrence} requested but only ${matches.length} paragraphs match`
|
|
49181
|
+
);
|
|
49182
|
+
return matches[occurrence - 1];
|
|
49183
|
+
}
|
|
49184
|
+
var NOTE_ID = /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p([0-9]{1,15})$/i;
|
|
49185
|
+
function titleMatchSql(columns) {
|
|
49186
|
+
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;`;
|
|
49187
|
+
}
|
|
49188
|
+
function checkNoteSelector(selector) {
|
|
49189
|
+
const given = [selector.id, selector.title].filter((value) => value !== void 0).length;
|
|
49190
|
+
if (given !== 1)
|
|
49191
|
+
throw new ParagraphLinkError("invalid-argument", "Choose exactly one of id or title");
|
|
49192
|
+
if (selector.folder !== void 0 && selector.title === void 0)
|
|
49193
|
+
throw new ParagraphLinkError("invalid-argument", "folder only narrows a title lookup");
|
|
49194
|
+
if (selector.id === void 0) return void 0;
|
|
49195
|
+
const pk = NOTE_ID.exec(selector.id)?.[1];
|
|
49196
|
+
if (!pk)
|
|
49197
|
+
throw new ParagraphLinkError(
|
|
49198
|
+
"invalid-argument",
|
|
49199
|
+
"Invalid note ID: expected x-coredata://<store>/ICNote/p<number> or a Notes UUID"
|
|
49200
|
+
);
|
|
49201
|
+
return Number(pk);
|
|
49202
|
+
}
|
|
49203
|
+
function resolveNote(dbPath2, columns, selector) {
|
|
49204
|
+
const idPk = checkNoteSelector(selector);
|
|
49205
|
+
if (idPk !== void 0) return { pk: idPk, id: selector.id };
|
|
49206
|
+
requireColumns(columns, ["ZTITLE1", "ZFOLDER"], "title lookup");
|
|
49207
|
+
const context = readStoreContext(dbPath2, columns);
|
|
49208
|
+
const paths = folderPaths(context.folders);
|
|
49209
|
+
const names = new Map(context.folders.map((f) => [f.pk, escapeFolderName(f.name ?? "")]));
|
|
49210
|
+
let matches = parseJsonLines(
|
|
49211
|
+
runReadOnlySql(dbPath2, titleMatchSql(columns), {
|
|
49212
|
+
title: { blob: Buffer.from(selector.title, "utf8") }
|
|
49213
|
+
})
|
|
49214
|
+
);
|
|
49215
|
+
if (selector.folder !== void 0) {
|
|
49216
|
+
const wanted = selector.folder;
|
|
49217
|
+
matches = matches.filter(
|
|
49218
|
+
(n) => paths.get(n.folder) === wanted || names.get(n.folder) === wanted
|
|
49219
|
+
);
|
|
49220
|
+
}
|
|
49221
|
+
if (!matches.length) throw new ParagraphLinkError("not-found", "No note matches the selector");
|
|
49222
|
+
if (matches.length > 1)
|
|
49223
|
+
throw new ParagraphLinkError(
|
|
49224
|
+
"ambiguous-note",
|
|
49225
|
+
`${matches.length} notes match; pass folder (a name or a path as list-folders shows it) or use the note id. Folders: ${matches.map((n) => paths.get(n.folder)).join(", ")}`
|
|
49226
|
+
);
|
|
49227
|
+
return { pk: matches[0].pk, id: noteIdFor(context.uuid, matches[0].pk) };
|
|
49228
|
+
}
|
|
49229
|
+
function noteBodySql(columns) {
|
|
49230
|
+
return `SELECT json_object('isNote', n.Z_ENT = ${entity("ICNote")}, 'identifier', ${col(columns, "n", "ZIDENTIFIER")}, 'data', (SELECT hex(d.ZDATA) FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK), 'encrypted', (SELECT d.ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK), 'locked', ${col(columns, "n", "ZISPASSWORDPROTECTED")}) FROM ZICCLOUDSYNCINGOBJECT n WHERE n.Z_PK = @pk;`;
|
|
49231
|
+
}
|
|
49232
|
+
function readNoteParagraphs(selector, { dbPath: dbPath2 = NOTES_DB_PATH7 } = {}) {
|
|
49233
|
+
checkNoteSelector(selector);
|
|
49234
|
+
const columns = readColumns(dbPath2);
|
|
49235
|
+
const { pk, id: id2 } = resolveNote(dbPath2, columns, selector);
|
|
49236
|
+
const [row] = parseJsonLines(runReadOnlySql(dbPath2, noteBodySql(columns), { pk: { int: pk } }));
|
|
49237
|
+
if (!row?.isNote) throw new ParagraphLinkError("not-found", `No note found for ID "${id2}"`);
|
|
49238
|
+
if (row.encrypted || row.locked)
|
|
49239
|
+
throw new ParagraphLinkError(
|
|
49240
|
+
"encrypted",
|
|
49241
|
+
"This note is password-protected; its body is encrypted"
|
|
49242
|
+
);
|
|
49243
|
+
if (!row.data || !/^[0-9a-f]+$/i.test(row.data))
|
|
49244
|
+
throw new ParagraphLinkError("no-body", "No body data is stored for this note");
|
|
49245
|
+
let doc;
|
|
49246
|
+
let runs;
|
|
49247
|
+
try {
|
|
49248
|
+
const data = gunzipSync9(Buffer.from(row.data, "hex"), { maxOutputLength: 32 * 1024 * 1024 });
|
|
49249
|
+
doc = decodeNoteBlocks(data);
|
|
49250
|
+
runs = runParagraphIds(data);
|
|
49251
|
+
} catch (error2) {
|
|
49252
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
49253
|
+
throw new ParagraphLinkError("no-body", `The note body could not be decoded: ${message}`);
|
|
49254
|
+
}
|
|
49255
|
+
const paragraphs = paragraphsOf(doc, runs, row.identifier);
|
|
49256
|
+
const counts = { unique: 0, shared: 0, missing: 0 };
|
|
49257
|
+
for (const p of paragraphs) counts[p.paragraphIdStatus]++;
|
|
49258
|
+
return { id: id2, identifier: row.identifier, paragraphs, counts };
|
|
49259
|
+
}
|
|
49260
|
+
function pageParagraphs(paragraphs, { offset = 0, limit = 500, maxBytes = 4 * 1024 * 1024, linkableOnly = false } = {}) {
|
|
49261
|
+
const list = linkableOnly ? paragraphs.filter((p) => p.url) : paragraphs;
|
|
49262
|
+
const start = Math.min(Math.max(0, offset), list.length);
|
|
49263
|
+
const out = [];
|
|
49264
|
+
let bytes = 0;
|
|
49265
|
+
for (let i = start; i < list.length && out.length < limit; i++) {
|
|
49266
|
+
const size = Buffer.byteLength(JSON.stringify(list[i]));
|
|
49267
|
+
if (out.length && bytes + size > maxBytes) break;
|
|
49268
|
+
out.push(list[i]);
|
|
49269
|
+
bytes += size;
|
|
49270
|
+
}
|
|
49271
|
+
const next = start + out.length;
|
|
49272
|
+
return {
|
|
49273
|
+
paragraphs: out,
|
|
49274
|
+
page: {
|
|
49275
|
+
offset: start,
|
|
49276
|
+
returned: out.length,
|
|
49277
|
+
total: list.length,
|
|
49278
|
+
hasMore: next < list.length,
|
|
49279
|
+
...next < list.length ? { nextOffset: next } : {}
|
|
49280
|
+
}
|
|
49281
|
+
};
|
|
49282
|
+
}
|
|
49283
|
+
function paragraphLink(note, selector) {
|
|
49284
|
+
const paragraph = selectParagraph(note.paragraphs, selector);
|
|
49285
|
+
if (paragraph.paragraphIdStatus === "shared")
|
|
49286
|
+
throw new ParagraphLinkError(
|
|
49287
|
+
"paragraph-id-shared",
|
|
49288
|
+
`This paragraph's ID is shared with ${paragraph.sharedWith} other paragraph(s) in the note, so a link could open the wrong one`
|
|
49289
|
+
);
|
|
49290
|
+
if (!paragraph.url)
|
|
49291
|
+
throw new ParagraphLinkError(
|
|
49292
|
+
"paragraph-id-missing",
|
|
49293
|
+
paragraph.paragraphId ? "The note has no stored identifier, so no link can be built" : "This paragraph has no stored paragraph ID, so Notes cannot open it directly"
|
|
49294
|
+
);
|
|
49295
|
+
return { url: paragraph.url, paragraph };
|
|
49296
|
+
}
|
|
49297
|
+
|
|
49298
|
+
// src/utils/noteStructure.ts
|
|
49299
|
+
import { dirname as dirname4 } from "node:path";
|
|
49300
|
+
|
|
49301
|
+
// src/utils/noteLinks.ts
|
|
49302
|
+
var NOTE_LINK_UTI = "com.apple.notes.inlinetextattachment.link";
|
|
49303
|
+
var HASHTAG_UTI2 = "com.apple.notes.inlinetextattachment.hashtag";
|
|
49304
|
+
var UUID = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
49305
|
+
function parseNotesShowUrl(url) {
|
|
49306
|
+
const match = /^(?:apple)?notes:\/\/showNote\?(.*)$/i.exec(url);
|
|
49307
|
+
if (!match) return void 0;
|
|
49308
|
+
const query2 = new URLSearchParams(match[1]);
|
|
49309
|
+
const note = query2.get("identifier") ?? "";
|
|
49310
|
+
const paragraph = query2.get("paragraphID") ?? "";
|
|
49311
|
+
const result = {};
|
|
49312
|
+
if (UUID.test(note)) result.targetNote = note.toUpperCase();
|
|
49313
|
+
if (UUID.test(paragraph)) result.paragraphId = paragraph.toUpperCase();
|
|
49314
|
+
return result;
|
|
49315
|
+
}
|
|
49316
|
+
function inlineLinks(doc) {
|
|
49317
|
+
const links = [];
|
|
49318
|
+
let last;
|
|
49319
|
+
for (const block of doc.blocks)
|
|
49320
|
+
for (const run of block.runs) {
|
|
49321
|
+
if (!run.link) {
|
|
49322
|
+
last = void 0;
|
|
49323
|
+
continue;
|
|
49324
|
+
}
|
|
49325
|
+
if (last && last.url === run.link && last.start + last.length === run.start) {
|
|
49326
|
+
last.length += run.length;
|
|
49327
|
+
last.text += run.text;
|
|
49328
|
+
continue;
|
|
49329
|
+
}
|
|
49330
|
+
last = {
|
|
49331
|
+
kind: "inline",
|
|
49332
|
+
url: run.link,
|
|
49333
|
+
linkSafe: run.linkSafe === true,
|
|
49334
|
+
text: run.text,
|
|
49335
|
+
start: run.start,
|
|
49336
|
+
length: run.length,
|
|
49337
|
+
blockIndex: block.index,
|
|
49338
|
+
...parseNotesShowUrl(run.link)
|
|
49339
|
+
};
|
|
49340
|
+
links.push(last);
|
|
49341
|
+
}
|
|
49342
|
+
return links;
|
|
49343
|
+
}
|
|
49344
|
+
function markerPosition(doc, identifier) {
|
|
49345
|
+
if (!doc) return {};
|
|
49346
|
+
const wanted = identifier.toUpperCase();
|
|
49347
|
+
const marker = doc.attachments.find((item) => item.id.toUpperCase() === wanted);
|
|
49348
|
+
return marker ? { start: marker.start, blockIndex: marker.blockIndex, inBody: true } : { inBody: false };
|
|
49349
|
+
}
|
|
49350
|
+
function nativeLink(row, doc) {
|
|
49351
|
+
if (!row.token) return void 0;
|
|
49352
|
+
const target = parseNotesShowUrl(row.token) ?? {};
|
|
49353
|
+
const kind = target.paragraphId ? "section" : "note";
|
|
49354
|
+
return {
|
|
49355
|
+
kind,
|
|
49356
|
+
url: row.token,
|
|
49357
|
+
linkSafe: isSafeLink(row.token),
|
|
49358
|
+
...row.alt ? { text: row.alt } : {},
|
|
49359
|
+
...markerPosition(doc, row.identifier),
|
|
49360
|
+
...target,
|
|
49361
|
+
...kind === "section" && row.alt ? { section: row.alt } : {},
|
|
49362
|
+
attachmentIdentifier: row.identifier
|
|
49363
|
+
};
|
|
49364
|
+
}
|
|
49365
|
+
function cardLink(row, {
|
|
49366
|
+
doc,
|
|
49367
|
+
noteId: noteId3,
|
|
49368
|
+
previewPath
|
|
49369
|
+
} = {}) {
|
|
49370
|
+
if (!row.url) return void 0;
|
|
49371
|
+
return {
|
|
49372
|
+
kind: "card",
|
|
49373
|
+
url: row.url,
|
|
49374
|
+
linkSafe: isSafeLink(row.url),
|
|
49375
|
+
...row.title ? { text: row.title } : {},
|
|
49376
|
+
...markerPosition(doc, row.identifier),
|
|
49377
|
+
...parseNotesShowUrl(row.url),
|
|
49378
|
+
attachmentIdentifier: row.identifier,
|
|
49379
|
+
...noteId3 ? { attachmentId: attachmentCoreDataId(noteId3, row.pk) } : {},
|
|
49380
|
+
...previewPath !== void 0 ? { previewPath } : {}
|
|
49381
|
+
};
|
|
49382
|
+
}
|
|
49383
|
+
|
|
49384
|
+
// src/utils/noteStructure.ts
|
|
49385
|
+
var APPLE_EPOCH_OFFSET = 978307200;
|
|
49386
|
+
var LAST_VIEWED_NEVER = -541228980;
|
|
49387
|
+
function lastViewedOf(raw, columnPresent = true, now = Date.now()) {
|
|
49388
|
+
if (!columnPresent) return { lastViewed: null, lastViewedStatus: "unsupported" };
|
|
49389
|
+
if (raw === null || raw === void 0)
|
|
49390
|
+
return { lastViewed: null, lastViewedStatus: "not-recorded" };
|
|
49391
|
+
const seconds = typeof raw === "number" ? raw : Number.NaN;
|
|
49392
|
+
if (seconds === LAST_VIEWED_NEVER) return { lastViewed: null, lastViewedStatus: "never-viewed" };
|
|
49393
|
+
const ms = (seconds + APPLE_EPOCH_OFFSET) * 1e3;
|
|
49394
|
+
if (!Number.isFinite(ms) || ms < Date.UTC(2007, 0, 1) || ms > now + 864e5)
|
|
49395
|
+
return { lastViewed: null, lastViewedStatus: "malformed" };
|
|
49396
|
+
return { lastViewed: new Date(ms).toISOString(), lastViewedStatus: "viewed" };
|
|
49397
|
+
}
|
|
49398
|
+
var visible = (text2) => text2.replace(/\ufffc/g, "");
|
|
49399
|
+
var wordCount = (text2) => visible(text2).split(/\s+/u).filter((word) => word !== "").length;
|
|
49400
|
+
var charCount = (text2) => Array.from(visible(text2)).length;
|
|
49401
|
+
function noteStructureSql(columns) {
|
|
49402
|
+
const c = (alias, name) => col(columns, alias, name);
|
|
49403
|
+
const sharedExpr = columns.has("ZSERVERSHAREDATA") ? `(n.ZSERVERSHAREDATA IS NOT NULL OR EXISTS (
|
|
49404
|
+
WITH RECURSIVE up(pk, depth) AS (
|
|
49405
|
+
SELECT n.ZFOLDER, 0 UNION ALL
|
|
49406
|
+
SELECT ${c("p", "ZPARENT")}, depth + 1 FROM up JOIN ZICCLOUDSYNCINGOBJECT p ON p.Z_PK = up.pk
|
|
49407
|
+
WHERE depth < 64)
|
|
49408
|
+
SELECT 1 FROM up JOIN ZICCLOUDSYNCINGOBJECT s ON s.Z_PK = up.pk WHERE s.ZSERVERSHAREDATA IS NOT NULL))` : "NULL";
|
|
49409
|
+
const children = columns.has("ZPARENTATTACHMENT") ? ` OR att.ZPARENTATTACHMENT IN (SELECT p.Z_PK FROM ZICCLOUDSYNCINGOBJECT p WHERE ${c("p", "ZNOTE")} = @pk)` : "";
|
|
49410
|
+
return [
|
|
49411
|
+
"BEGIN;",
|
|
49412
|
+
`SELECT json_object('k', 'note',
|
|
49413
|
+
'isNote', n.Z_ENT = ${entity("ICNote")},
|
|
49414
|
+
'identifier', ${c("n", "ZIDENTIFIER")},
|
|
49415
|
+
'title', ${c("n", "ZTITLE1")},
|
|
49416
|
+
'folder', ${c("f", "ZTITLE2")},
|
|
49417
|
+
'inTrash', CASE WHEN f.Z_PK IS NULL THEN 0 ELSE ${trashFolderSql(columns, "f")} END,
|
|
49418
|
+
'account', ${c("a", "ZNAME")},
|
|
49419
|
+
'locked', ${c("n", "ZISPASSWORDPROTECTED")},
|
|
49420
|
+
'pinned', ${c("n", "ZISPINNED")},
|
|
49421
|
+
'shared', ${sharedExpr},
|
|
49422
|
+
'lastViewed', ${c("n", "ZLASTVIEWEDMODIFICATIONDATE")},
|
|
49423
|
+
'data', (SELECT hex(d.ZDATA) FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK),
|
|
49424
|
+
'encrypted', (SELECT d.ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK))
|
|
49425
|
+
FROM ZICCLOUDSYNCINGOBJECT n
|
|
49426
|
+
LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = ${c("n", "ZFOLDER")}
|
|
49427
|
+
LEFT JOIN ZICCLOUDSYNCINGOBJECT a ON a.Z_PK = ${accountRef(columns, "n")} AND a.Z_ENT = ${entity("ICAccount")}
|
|
49428
|
+
WHERE n.Z_PK = @pk;`,
|
|
49429
|
+
`SELECT json_object('k', 'attachment',
|
|
49430
|
+
'pk', att.Z_PK,
|
|
49431
|
+
'title', ${c("att", "ZTITLE")},
|
|
49432
|
+
'url', ${c("att", "ZURLSTRING")},
|
|
49433
|
+
'fileSize', ${c("att", "ZFILESIZE")})
|
|
49434
|
+
FROM ZICCLOUDSYNCINGOBJECT att
|
|
49435
|
+
WHERE att.Z_ENT = ${entity("ICAttachment")} AND ${notTombstonedSql(columns, "att")}
|
|
49436
|
+
AND (${c("att", "ZNOTE")} = @pk${children});`,
|
|
49437
|
+
`SELECT json_object('k', 'inline',
|
|
49438
|
+
'identifier', ${c("i", "ZIDENTIFIER")},
|
|
49439
|
+
'uti', ${c("i", "ZTYPEUTI1")},
|
|
49440
|
+
'alt', ${c("i", "ZALTTEXT")},
|
|
49441
|
+
'token', ${c("i", "ZTOKENCONTENTIDENTIFIER")})
|
|
49442
|
+
FROM ZICCLOUDSYNCINGOBJECT i
|
|
49443
|
+
WHERE i.Z_ENT = ${entity("ICInlineAttachment")} AND ${c("i", "ZNOTE1")} = @pk
|
|
49444
|
+
AND ${notTombstonedSql(columns, "i")} AND ${c("i", "ZTYPEUTI1")} IN ('${HASHTAG_UTI2}', '${NOTE_LINK_UTI}');`,
|
|
49445
|
+
"COMMIT;"
|
|
49446
|
+
].join("\n");
|
|
49447
|
+
}
|
|
49448
|
+
function describeNoteStructure(s) {
|
|
49449
|
+
const parts = [
|
|
49450
|
+
s.bodyDecoded ? `${s.blockSummary.blocks} blocks, ${s.wordCount} words` : `body not decoded (${s.bodyError})`,
|
|
49451
|
+
`${s.links.length} links (inline ${s.linkCounts.inline}, card ${s.linkCounts.card}, note ${s.linkCounts.note}, section ${s.linkCounts.section})`,
|
|
49452
|
+
`${s.attachmentCount} attachments`,
|
|
49453
|
+
`${s.tags.length} tags`
|
|
49454
|
+
];
|
|
49455
|
+
if (s.checklistTotal) parts.push(`checklist ${s.checklistDone}/${s.checklistTotal} done`);
|
|
49456
|
+
if (s.hasDrawing) parts.push("has a drawing");
|
|
49457
|
+
if (s.isLocked) parts.push("locked");
|
|
49458
|
+
if (s.isShared) parts.push("shared");
|
|
49459
|
+
return `Note structure: ${parts.join("; ")}.`;
|
|
49460
|
+
}
|
|
49461
|
+
function readNoteStructure(id2, { dbPath: dbPath2 = NOTES_DB_PATH7, includeText = true, maxTextBytes = 4 * 1024 * 1024 } = {}) {
|
|
49462
|
+
const { pk } = parseNoteId2(id2);
|
|
49463
|
+
const columns = readColumns(dbPath2);
|
|
49464
|
+
const rows = parseJsonLines(
|
|
49465
|
+
runReadOnlySql(dbPath2, noteStructureSql(columns), { pk: { int: pk } })
|
|
49466
|
+
);
|
|
49467
|
+
const note = rows.find((row) => row.k === "note");
|
|
49468
|
+
if (!note?.isNote) throw new NoteStoreError(`No note found for ID "${id2}".`, "invalid_input");
|
|
49469
|
+
const details = new Map(
|
|
49470
|
+
rows.filter((row) => row.k === "attachment").map((row) => [row.pk, row])
|
|
49471
|
+
);
|
|
49472
|
+
const inlineRows = rows.filter((row) => row.k === "inline");
|
|
49473
|
+
let doc;
|
|
49474
|
+
let bodyError;
|
|
49475
|
+
if (note.encrypted || note.locked) bodyError = "encrypted";
|
|
49476
|
+
else if (!note.data || !/^[0-9a-f]+$/i.test(note.data)) bodyError = "no-body";
|
|
49477
|
+
else
|
|
49478
|
+
try {
|
|
49479
|
+
doc = decodeCompressedNoteBlocks(Buffer.from(note.data, "hex"));
|
|
49480
|
+
} catch (error2) {
|
|
49481
|
+
if (!(error2 instanceof NoteBlocksError)) throw error2;
|
|
49482
|
+
bodyError = error2.code;
|
|
49483
|
+
}
|
|
49484
|
+
const { rows: attachmentRows, bodyOrder } = readNoteAttachmentRows(id2, dbPath2);
|
|
49485
|
+
const assets = assembleAttachmentAssets(attachmentRows, bodyOrder, dirname4(dbPath2));
|
|
49486
|
+
const roots = /* @__PURE__ */ new Map();
|
|
49487
|
+
const attachments = [];
|
|
49488
|
+
const pks = /* @__PURE__ */ new Map();
|
|
49489
|
+
for (const record2 of assets.attachments) {
|
|
49490
|
+
const detail = details.get(record2.pk);
|
|
49491
|
+
const parent = record2.parentIdentifier !== null ? roots.get(record2.parentIdentifier) : void 0;
|
|
49492
|
+
const item = {
|
|
49493
|
+
id: attachmentCoreDataId(id2, record2.pk),
|
|
49494
|
+
identifier: record2.identifier,
|
|
49495
|
+
uti: record2.uti,
|
|
49496
|
+
kind: record2.kind,
|
|
49497
|
+
...detail?.title ? { title: detail.title } : {},
|
|
49498
|
+
...record2.filename ? { filename: record2.filename } : {},
|
|
49499
|
+
...detail?.url ? { url: detail.url } : {},
|
|
49500
|
+
...typeof detail?.fileSize === "number" ? { fileSize: detail.fileSize } : {},
|
|
49501
|
+
...markerPosition(doc, record2.identifier),
|
|
49502
|
+
previewPath: record2.previewPath,
|
|
49503
|
+
...parent ? { parentId: parent.id } : {}
|
|
49504
|
+
};
|
|
49505
|
+
pks.set(item, record2.pk);
|
|
49506
|
+
if (parent) (parent.children ||= []).push(item);
|
|
49507
|
+
else {
|
|
49508
|
+
roots.set(record2.identifier, item);
|
|
49509
|
+
attachments.push(item);
|
|
49510
|
+
}
|
|
49511
|
+
}
|
|
49512
|
+
const flat = attachments.flatMap((item) => [item, ...item.children ?? []]);
|
|
49513
|
+
const links = doc ? inlineLinks(doc) : [];
|
|
49514
|
+
for (const item of flat) {
|
|
49515
|
+
if (item.kind !== "url") continue;
|
|
49516
|
+
const link = cardLink(
|
|
49517
|
+
{
|
|
49518
|
+
pk: pks.get(item),
|
|
49519
|
+
identifier: item.identifier,
|
|
49520
|
+
url: item.url ?? null,
|
|
49521
|
+
title: item.title ?? null
|
|
49522
|
+
},
|
|
49523
|
+
{ doc, noteId: id2, previewPath: item.previewPath }
|
|
49524
|
+
);
|
|
49525
|
+
if (link) links.push(link);
|
|
49526
|
+
}
|
|
49527
|
+
const tagOrder = [];
|
|
49528
|
+
for (const row of inlineRows) {
|
|
49529
|
+
if (!row.identifier) continue;
|
|
49530
|
+
if (row.uti === NOTE_LINK_UTI) {
|
|
49531
|
+
const link = nativeLink({ identifier: row.identifier, token: row.token, alt: row.alt }, doc);
|
|
49532
|
+
if (link) links.push(link);
|
|
49533
|
+
} else if (row.alt) {
|
|
49534
|
+
const position = markerPosition(doc, row.identifier);
|
|
49535
|
+
if (position.inBody !== false)
|
|
49536
|
+
tagOrder.push({ tag: row.alt.replace(/^#/, ""), start: position.start ?? 0 });
|
|
49537
|
+
}
|
|
49538
|
+
}
|
|
49539
|
+
links.sort((a, b) => (a.start ?? Number.MAX_SAFE_INTEGER) - (b.start ?? Number.MAX_SAFE_INTEGER));
|
|
49540
|
+
const tags = [...new Set(tagOrder.sort((a, b) => a.start - b.start).map((entry) => entry.tag))];
|
|
49541
|
+
let text2;
|
|
49542
|
+
let textOmitted;
|
|
49543
|
+
if (doc && includeText) {
|
|
49544
|
+
if (Buffer.byteLength(doc.text) <= maxTextBytes) text2 = doc.text;
|
|
49545
|
+
else textOmitted = true;
|
|
49546
|
+
}
|
|
49547
|
+
const linkCounts = { inline: 0, card: 0, note: 0, section: 0 };
|
|
49548
|
+
for (const link of links) linkCounts[link.kind]++;
|
|
49549
|
+
const lastViewed = lastViewedOf(note.lastViewed, columns.has("ZLASTVIEWEDMODIFICATIONDATE"));
|
|
49550
|
+
const first2 = selectFirstImage(assets);
|
|
49551
|
+
return {
|
|
49552
|
+
id: id2,
|
|
49553
|
+
identifier: note.identifier,
|
|
49554
|
+
deepLink: note.identifier ? `notes://showNote?identifier=${note.identifier}` : null,
|
|
49555
|
+
title: note.title,
|
|
49556
|
+
folder: note.folder,
|
|
49557
|
+
account: note.account,
|
|
49558
|
+
inRecentlyDeleted: note.inTrash === 1,
|
|
49559
|
+
isShared: note.shared === null ? null : note.shared === 1,
|
|
49560
|
+
isLocked: note.locked === 1 || note.encrypted === 1,
|
|
49561
|
+
isPinned: note.pinned === null ? null : note.pinned === 1,
|
|
49562
|
+
...lastViewed,
|
|
49563
|
+
bodyDecoded: doc !== void 0,
|
|
49564
|
+
...bodyError ? { bodyError } : {},
|
|
49565
|
+
...text2 !== void 0 ? { text: text2 } : {},
|
|
49566
|
+
...textOmitted ? { textOmitted } : {},
|
|
49567
|
+
textLength: doc ? doc.textLength : null,
|
|
49568
|
+
wordCount: doc ? wordCount(doc.text) : null,
|
|
49569
|
+
charCount: doc ? charCount(doc.text) : null,
|
|
49570
|
+
blockSummary: doc ? doc.summary : null,
|
|
49571
|
+
links,
|
|
49572
|
+
linkCounts,
|
|
49573
|
+
linksComplete: doc !== void 0,
|
|
49574
|
+
tags,
|
|
49575
|
+
attachments,
|
|
49576
|
+
attachmentCount: attachments.length,
|
|
49577
|
+
checklistTotal: doc ? doc.summary.checklist.total : null,
|
|
49578
|
+
checklistDone: doc ? doc.summary.checklist.done : null,
|
|
49579
|
+
hasDrawing: flat.some((item) => item.kind === "drawing"),
|
|
49580
|
+
firstImage: first2 ? { ...first2, id: attachmentCoreDataId(id2, first2.pk) } : null,
|
|
49581
|
+
...doc ? { undecodedFields: doc.undecodedFields } : {}
|
|
49582
|
+
};
|
|
49583
|
+
}
|
|
49584
|
+
|
|
49055
49585
|
// src/utils/linkInsert.ts
|
|
49056
49586
|
var MAX_LINK_URL_LENGTH = 4096;
|
|
49057
49587
|
var MAX_LINK_LABEL_LENGTH = 2e3;
|
|
@@ -49169,7 +49699,7 @@ function insertLink(request, deps) {
|
|
|
49169
49699
|
|
|
49170
49700
|
// src/services/notesExport.ts
|
|
49171
49701
|
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
49172
|
-
import { basename as basename4, dirname as
|
|
49702
|
+
import { basename as basename4, dirname as dirname5, extname as extname4, join as join21 } from "node:path";
|
|
49173
49703
|
|
|
49174
49704
|
// src/utils/exportAssets.ts
|
|
49175
49705
|
import {
|
|
@@ -50492,7 +51022,7 @@ function loadNotes(ids, single, read) {
|
|
|
50492
51022
|
return { notes, skipped };
|
|
50493
51023
|
}
|
|
50494
51024
|
function openOutput(output) {
|
|
50495
|
-
mkdirSync5(
|
|
51025
|
+
mkdirSync5(dirname5(output), { recursive: true });
|
|
50496
51026
|
try {
|
|
50497
51027
|
return openCreateOnly(output);
|
|
50498
51028
|
} catch (error2) {
|
|
@@ -50517,7 +51047,7 @@ function exportNotesMarkdown(request, deps) {
|
|
|
50517
51047
|
const ids = selectNotes(request, deps);
|
|
50518
51048
|
const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
|
|
50519
51049
|
const fd = output ? openOutput(output) : void 0;
|
|
50520
|
-
const writer = assetsDir ? new SidecarWriter(assetsDir, output ?
|
|
51050
|
+
const writer = assetsDir ? new SidecarWriter(assetsDir, output ? dirname5(output) : void 0) : void 0;
|
|
50521
51051
|
const ctx = {
|
|
50522
51052
|
stats: emptyStats(),
|
|
50523
51053
|
...writer ? { writer, locator: deps.locator ?? new AssetLocator() } : {}
|
|
@@ -50547,7 +51077,7 @@ function exportNotesMarkdown(request, deps) {
|
|
|
50547
51077
|
return { ...receipt, markdown };
|
|
50548
51078
|
}
|
|
50549
51079
|
function defaultSidecarDir(output) {
|
|
50550
|
-
return join21(
|
|
51080
|
+
return join21(dirname5(output), `${basename4(output, extname4(output))}.assets`);
|
|
50551
51081
|
}
|
|
50552
51082
|
function exportNotesHtml(request, deps) {
|
|
50553
51083
|
if (!request.outputPath)
|
|
@@ -50568,7 +51098,7 @@ function exportNotesHtml(request, deps) {
|
|
|
50568
51098
|
const ids = selectNotes(request, deps);
|
|
50569
51099
|
const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
|
|
50570
51100
|
const fd = openOutput(output);
|
|
50571
|
-
const writer = assetsDir ? new SidecarWriter(assetsDir,
|
|
51101
|
+
const writer = assetsDir ? new SidecarWriter(assetsDir, dirname5(output)) : new DataUrlWriter();
|
|
50572
51102
|
const ctx = {
|
|
50573
51103
|
stats: emptyStats(),
|
|
50574
51104
|
writer,
|
|
@@ -51674,7 +52204,7 @@ function registerNativeOperations(server2, manager) {
|
|
|
51674
52204
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
51675
52205
|
import { existsSync as existsSync13 } from "node:fs";
|
|
51676
52206
|
import { release as release2 } from "node:os";
|
|
51677
|
-
import { dirname as
|
|
52207
|
+
import { dirname as dirname6, resolve as resolve4 } from "node:path";
|
|
51678
52208
|
import { fileURLToPath } from "node:url";
|
|
51679
52209
|
var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
|
|
51680
52210
|
var MARKDOWN_MIN_DARWIN_MAJOR = 25;
|
|
@@ -51697,7 +52227,7 @@ function setupShortcuts(checkOnly, dependencies = {}) {
|
|
|
51697
52227
|
const result = spawnSync2("/usr/bin/open", [path10], { encoding: "utf8" });
|
|
51698
52228
|
return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
|
|
51699
52229
|
});
|
|
51700
|
-
const baseDirectory = dependencies.baseDirectory || resolve4(
|
|
52230
|
+
const baseDirectory = dependencies.baseDirectory || resolve4(dirname6(fileURLToPath(import.meta.url)), "../shortcuts");
|
|
51701
52231
|
const osRelease = (dependencies.osRelease || release2)();
|
|
51702
52232
|
const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
|
|
51703
52233
|
const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
|
|
@@ -51778,7 +52308,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
|
|
|
51778
52308
|
import { createHash as createHash4 } from "node:crypto";
|
|
51779
52309
|
import { existsSync as existsSync14, readFileSync as readFileSync4 } from "node:fs";
|
|
51780
52310
|
import { homedir as homedir19 } from "node:os";
|
|
51781
|
-
import { dirname as
|
|
52311
|
+
import { dirname as dirname7, join as join24, resolve as resolve5 } from "node:path";
|
|
51782
52312
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
51783
52313
|
var PRIVATE_HELPER_PROTOCOL = 1;
|
|
51784
52314
|
var ENABLE_ENV = "APPLE_NOTES_MCP_ENABLE_PRIVATE";
|
|
@@ -51803,7 +52333,7 @@ var manifestSchema = external_exports.object({
|
|
|
51803
52333
|
osVersion: external_exports.string(),
|
|
51804
52334
|
compiler: external_exports.string()
|
|
51805
52335
|
});
|
|
51806
|
-
function packageRoot(fromDir =
|
|
52336
|
+
function packageRoot(fromDir = dirname7(fileURLToPath2(import.meta.url))) {
|
|
51807
52337
|
let dir = fromDir;
|
|
51808
52338
|
for (; ; ) {
|
|
51809
52339
|
const candidate = join24(dir, "package.json");
|
|
@@ -51814,7 +52344,7 @@ function packageRoot(fromDir = dirname6(fileURLToPath2(import.meta.url))) {
|
|
|
51814
52344
|
} catch {
|
|
51815
52345
|
}
|
|
51816
52346
|
}
|
|
51817
|
-
const parent =
|
|
52347
|
+
const parent = dirname7(dir);
|
|
51818
52348
|
if (parent === dir) return resolve5(fromDir, "..");
|
|
51819
52349
|
dir = parent;
|
|
51820
52350
|
}
|
|
@@ -52023,9 +52553,9 @@ function parseOrThrow(schema, value) {
|
|
|
52023
52553
|
);
|
|
52024
52554
|
return parsed.data;
|
|
52025
52555
|
}
|
|
52026
|
-
var
|
|
52556
|
+
var UUID2 = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
52027
52557
|
function assertNoteIdentifier(identifier) {
|
|
52028
|
-
if (!
|
|
52558
|
+
if (!UUID2.test(identifier))
|
|
52029
52559
|
throw new PrivateHelperError("invalid_request", "identifier must be a Notes UUID");
|
|
52030
52560
|
}
|
|
52031
52561
|
function probePrivateHelper(deps = defaultDeps2()) {
|
|
@@ -53435,6 +53965,130 @@ registerTool(
|
|
|
53435
53965
|
);
|
|
53436
53966
|
}, "Error reading note blocks")
|
|
53437
53967
|
);
|
|
53968
|
+
var paragraphNoteSelector = {
|
|
53969
|
+
id: noteIdInput.optional().describe(`Exact note ID (${NOTE_ID_FORMS}); give id or title`),
|
|
53970
|
+
title: external_exports.string().min(1).max(MAX.TITLE).optional().describe("Exact note title; must match one note unless folder narrows it"),
|
|
53971
|
+
folder: external_exports.string().min(1).max(MAX.FOLDER).optional().describe("With title only: the note's folder name or full path as list-folders shows it")
|
|
53972
|
+
};
|
|
53973
|
+
registerTool(
|
|
53974
|
+
"list-note-paragraphs",
|
|
53975
|
+
{
|
|
53976
|
+
description: "Use when: you need a note's paragraphs with their style and stored paragraph ID, for example to choose one to link to.\nReturns: one page of non-empty paragraphs in body order, each with blockIndex (as in get-note-blocks), text, style, paragraphId, paragraphIdStatus (unique, shared, missing) and, only when unique, a direct applenotes:// url that opens that paragraph; plus counts per status and page info (call again with offset set to page.nextOffset while page.hasMore is true).\nDo not use when: you need inline formatting (get-note-blocks).\nSafety: read-only; reads the NoteStore database directly and requires Full Disk Access. Paragraph IDs repeat often (Notes copies them when a paragraph is split), so shared IDs get no url. Title lookups ignore Recently Deleted. Password-protected notes are refused.",
|
|
53977
|
+
inputSchema: {
|
|
53978
|
+
...paragraphNoteSelector,
|
|
53979
|
+
linkableOnly: external_exports.boolean().optional().describe("Return only paragraphs that have a direct url (default false)"),
|
|
53980
|
+
offset: external_exports.number().int().min(0).optional().describe("Index of the first paragraph to return (default 0); use page.nextOffset"),
|
|
53981
|
+
limit: external_exports.number().int().min(1).max(5e3).optional().describe("Maximum paragraphs to return (default 500, max 5000)")
|
|
53982
|
+
},
|
|
53983
|
+
outputSchema: {
|
|
53984
|
+
id: external_exports.string().optional(),
|
|
53985
|
+
identifier: external_exports.string().nullable().optional(),
|
|
53986
|
+
counts: external_exports.record(external_exports.unknown()).optional(),
|
|
53987
|
+
paragraphs: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
53988
|
+
page: external_exports.record(external_exports.unknown()).optional()
|
|
53989
|
+
},
|
|
53990
|
+
annotations: { readOnlyHint: true }
|
|
53991
|
+
},
|
|
53992
|
+
withErrorHandling(({ id: id2, title, folder, linkableOnly, offset, limit }) => {
|
|
53993
|
+
const note = readNoteParagraphs({ id: id2, title, folder });
|
|
53994
|
+
const page = pageParagraphs(note.paragraphs, {
|
|
53995
|
+
offset,
|
|
53996
|
+
limit,
|
|
53997
|
+
linkableOnly,
|
|
53998
|
+
maxBytes: blocksMaxResponseBytes()
|
|
53999
|
+
});
|
|
54000
|
+
const { unique, shared, missing } = note.counts;
|
|
54001
|
+
return successResponse(
|
|
54002
|
+
`${note.paragraphs.length} paragraphs: ${unique} linkable, ${shared} with a shared ID, ${missing} without an ID; returned ${page.page.returned} from offset ${page.page.offset}` + (page.page.hasMore ? `; more at offset ${page.page.nextOffset}` : "") + ".",
|
|
54003
|
+
{ id: note.id, identifier: note.identifier, counts: note.counts, ...page }
|
|
54004
|
+
);
|
|
54005
|
+
}, "Error listing paragraphs")
|
|
54006
|
+
);
|
|
54007
|
+
registerTool(
|
|
54008
|
+
"get-paragraph-link",
|
|
54009
|
+
{
|
|
54010
|
+
description: "Use when: you need a link that opens Notes at one paragraph (for example a heading) of a note.\nReturns: a direct applenotes://showNote?identifier=<note>¶graphID=<paragraph> url and the selected paragraph, only when that paragraph's stored ID is present and appears in no other paragraph of the note. Otherwise an error whose structuredContent.reason says why: paragraph-id-shared, paragraph-id-missing, no-match, ambiguous-paragraph (pass occurrence or a longer snippet), occurrence-out-of-range, ambiguous-note, encrypted.\nDo not use when: you want a link to the whole note (get-note-link).\nSafety: read-only; never creates or changes a paragraph ID, so a paragraph without a unique ID cannot be linked. Requires Full Disk Access. A later edit in Notes can replace the ID and break the link.",
|
|
54011
|
+
inputSchema: {
|
|
54012
|
+
...paragraphNoteSelector,
|
|
54013
|
+
contains: external_exports.string().min(1).max(MAX.CONTENT).optional().describe(
|
|
54014
|
+
"Snippet of the paragraph (case, spacing and Unicode width are ignored); give one of contains, match, blockIndex"
|
|
54015
|
+
),
|
|
54016
|
+
match: external_exports.string().min(1).max(MAX.CONTENT).optional().describe("The whole paragraph text, compared the same way"),
|
|
54017
|
+
blockIndex: external_exports.number().int().min(0).optional().describe("The paragraph's blockIndex from list-note-paragraphs"),
|
|
54018
|
+
occurrence: external_exports.number().int().min(1).optional().describe("Which match to use (1-based) when contains or match hits several paragraphs")
|
|
54019
|
+
},
|
|
54020
|
+
outputSchema: {
|
|
54021
|
+
url: external_exports.string().optional(),
|
|
54022
|
+
id: external_exports.string().optional(),
|
|
54023
|
+
identifier: external_exports.string().nullable().optional(),
|
|
54024
|
+
paragraph: external_exports.record(external_exports.unknown()).optional()
|
|
54025
|
+
},
|
|
54026
|
+
annotations: { readOnlyHint: true }
|
|
54027
|
+
},
|
|
54028
|
+
withErrorHandling(({ id: id2, title, folder, contains, match, blockIndex, occurrence }) => {
|
|
54029
|
+
const note = readNoteParagraphs({ id: id2, title, folder });
|
|
54030
|
+
const result = paragraphLink(note, { contains, match, blockIndex, occurrence });
|
|
54031
|
+
return successResponse(`Paragraph link: ${result.url}`, {
|
|
54032
|
+
url: result.url,
|
|
54033
|
+
id: note.id,
|
|
54034
|
+
identifier: note.identifier,
|
|
54035
|
+
paragraph: { ...result.paragraph }
|
|
54036
|
+
});
|
|
54037
|
+
}, "No paragraph link")
|
|
54038
|
+
);
|
|
54039
|
+
registerTool(
|
|
54040
|
+
"get-note-structure",
|
|
54041
|
+
{
|
|
54042
|
+
description: "Use when: you want one read-only overview of a note by exact id: decoded text, a block summary, every link with its kind (inline hyperlink, rich link card, native note link, native section link, with target note and paragraph UUIDs when the URL carries them), native tags, attachments with the same kind, body order and preview list-attachments reports (gallery and recording children nested), and metadata: deepLink, isShared, isLocked, isPinned, inRecentlyDeleted, lastViewed, wordCount, charCount, attachmentCount, checklistTotal/checklistDone, hasDrawing, firstImage.\nReturns: the structure object. For a password-protected note, metadata and attachment rows only (bodyDecoded false, body-derived fields null). lastViewed is null with lastViewedStatus never-viewed, not-recorded, malformed or unsupported when Notes holds no real view date.\nDo not use when: you need per-paragraph formatting (get-note-blocks) or the editable HTML body (get-note-content).\nSafety: read-only; reads the NoteStore database and the Notes data folder directly and requires Full Disk Access. Link URLs are returned as stored; check linkSafe before emitting them into HTML.",
|
|
54043
|
+
inputSchema: {
|
|
54044
|
+
id: noteIdInput,
|
|
54045
|
+
includeText: external_exports.boolean().optional().describe(
|
|
54046
|
+
"Include the decoded note text (default true). Text over APPLE_NOTES_MCP_BLOCKS_MAX_BYTES is omitted with textOmitted: true"
|
|
54047
|
+
)
|
|
54048
|
+
},
|
|
54049
|
+
outputSchema: {
|
|
54050
|
+
id: external_exports.string().optional(),
|
|
54051
|
+
identifier: external_exports.string().nullable().optional(),
|
|
54052
|
+
deepLink: external_exports.string().nullable().optional(),
|
|
54053
|
+
title: external_exports.string().nullable().optional(),
|
|
54054
|
+
folder: external_exports.string().nullable().optional(),
|
|
54055
|
+
account: external_exports.string().nullable().optional(),
|
|
54056
|
+
inRecentlyDeleted: external_exports.boolean().optional(),
|
|
54057
|
+
isShared: external_exports.boolean().nullable().optional(),
|
|
54058
|
+
isLocked: external_exports.boolean().optional(),
|
|
54059
|
+
isPinned: external_exports.boolean().nullable().optional(),
|
|
54060
|
+
lastViewed: external_exports.string().nullable().optional(),
|
|
54061
|
+
lastViewedStatus: external_exports.string().optional(),
|
|
54062
|
+
bodyDecoded: external_exports.boolean().optional(),
|
|
54063
|
+
bodyError: external_exports.string().optional(),
|
|
54064
|
+
text: external_exports.string().optional(),
|
|
54065
|
+
textOmitted: external_exports.boolean().optional(),
|
|
54066
|
+
textLength: external_exports.number().nullable().optional(),
|
|
54067
|
+
wordCount: external_exports.number().nullable().optional(),
|
|
54068
|
+
charCount: external_exports.number().nullable().optional(),
|
|
54069
|
+
blockSummary: external_exports.record(external_exports.unknown()).nullable().optional(),
|
|
54070
|
+
links: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
54071
|
+
linkCounts: external_exports.record(external_exports.unknown()).optional(),
|
|
54072
|
+
linksComplete: external_exports.boolean().optional(),
|
|
54073
|
+
tags: external_exports.array(external_exports.string()).optional(),
|
|
54074
|
+
attachments: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
54075
|
+
attachmentCount: external_exports.number().optional(),
|
|
54076
|
+
checklistTotal: external_exports.number().nullable().optional(),
|
|
54077
|
+
checklistDone: external_exports.number().nullable().optional(),
|
|
54078
|
+
hasDrawing: external_exports.boolean().optional(),
|
|
54079
|
+
firstImage: external_exports.record(external_exports.unknown()).nullable().optional(),
|
|
54080
|
+
undecodedFields: external_exports.record(external_exports.unknown()).optional()
|
|
54081
|
+
},
|
|
54082
|
+
annotations: { readOnlyHint: true }
|
|
54083
|
+
},
|
|
54084
|
+
withErrorHandling(({ id: id2, includeText }) => {
|
|
54085
|
+
const structure = readNoteStructure(id2, {
|
|
54086
|
+
includeText: includeText ?? true,
|
|
54087
|
+
maxTextBytes: blocksMaxResponseBytes()
|
|
54088
|
+
});
|
|
54089
|
+
return successResponse(describeNoteStructure(structure), { ...structure });
|
|
54090
|
+
}, "Error reading note structure")
|
|
54091
|
+
);
|
|
53438
54092
|
registerTool(
|
|
53439
54093
|
"list-native-tags",
|
|
53440
54094
|
{
|
package/package.json
CHANGED