@disco_trooper/apple-notes-mcp 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -24
- package/package.json +11 -12
- package/src/config/claude.test.ts +47 -0
- package/src/config/claude.ts +106 -0
- package/src/config/constants.ts +11 -2
- package/src/config/paths.test.ts +40 -0
- package/src/config/paths.ts +86 -0
- package/src/db/arrow-fix.test.ts +101 -0
- package/src/db/lancedb.test.ts +254 -2
- package/src/db/lancedb.ts +385 -38
- package/src/embeddings/cache.test.ts +150 -0
- package/src/embeddings/cache.ts +204 -0
- package/src/embeddings/index.ts +22 -4
- package/src/embeddings/local.ts +57 -17
- package/src/embeddings/openrouter.ts +233 -11
- package/src/errors/index.test.ts +64 -0
- package/src/errors/index.ts +62 -0
- package/src/graph/export.test.ts +81 -0
- package/src/graph/export.ts +163 -0
- package/src/graph/extract.test.ts +90 -0
- package/src/graph/extract.ts +52 -0
- package/src/graph/queries.test.ts +156 -0
- package/src/graph/queries.ts +224 -0
- package/src/index.ts +309 -23
- package/src/notes/conversion.ts +62 -0
- package/src/notes/crud.test.ts +41 -8
- package/src/notes/crud.ts +75 -64
- package/src/notes/read.test.ts +58 -3
- package/src/notes/read.ts +142 -210
- package/src/notes/resolve.ts +174 -0
- package/src/notes/tables.ts +69 -40
- package/src/search/chunk-indexer.test.ts +353 -0
- package/src/search/chunk-indexer.ts +207 -0
- package/src/search/chunk-search.test.ts +327 -0
- package/src/search/chunk-search.ts +298 -0
- package/src/search/index.ts +4 -6
- package/src/search/indexer.ts +164 -109
- package/src/setup.ts +46 -67
- package/src/types/index.ts +4 -0
- package/src/utils/chunker.test.ts +182 -0
- package/src/utils/chunker.ts +170 -0
- package/src/utils/content-filter.test.ts +225 -0
- package/src/utils/content-filter.ts +275 -0
- package/src/utils/debug.ts +0 -2
- package/src/utils/runtime.test.ts +70 -0
- package/src/utils/runtime.ts +40 -0
- package/src/utils/text.test.ts +32 -0
- package/CLAUDE.md +0 -56
- package/src/server.ts +0 -427
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML to Markdown conversion for Apple Notes content.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import TurndownService from "turndown";
|
|
6
|
+
|
|
7
|
+
// Initialize Turndown for HTML to Markdown conversion
|
|
8
|
+
const turndownService = new TurndownService({
|
|
9
|
+
headingStyle: "atx",
|
|
10
|
+
codeBlockStyle: "fenced",
|
|
11
|
+
bulletListMarker: "-",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Add custom rule to handle Apple Notes attachment placeholders
|
|
15
|
+
turndownService.addRule("attachments", {
|
|
16
|
+
filter: (node) => {
|
|
17
|
+
// Apple Notes uses object tags for attachments
|
|
18
|
+
return node.nodeName === "OBJECT" || node.nodeName === "IMG";
|
|
19
|
+
},
|
|
20
|
+
replacement: (_content, node) => {
|
|
21
|
+
// Turndown uses its own Node type, cast to access attributes
|
|
22
|
+
const element = node as unknown as {
|
|
23
|
+
getAttribute: (name: string) => string | null;
|
|
24
|
+
};
|
|
25
|
+
const filename =
|
|
26
|
+
element.getAttribute("data-filename") ||
|
|
27
|
+
element.getAttribute("alt") ||
|
|
28
|
+
element.getAttribute("src")?.split("/").pop() ||
|
|
29
|
+
"unknown";
|
|
30
|
+
return `[Attachment: ${filename}]`;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert HTML content to Markdown, handling Apple Notes specifics.
|
|
36
|
+
*/
|
|
37
|
+
export function htmlToMarkdown(html: string): string {
|
|
38
|
+
if (!html) return "";
|
|
39
|
+
|
|
40
|
+
// Pre-process: handle Apple Notes specific markup
|
|
41
|
+
let processed = html;
|
|
42
|
+
|
|
43
|
+
// Replace attachment objects with placeholder text before Turndown
|
|
44
|
+
processed = processed.replace(
|
|
45
|
+
/<object[^>]*data-filename="([^"]*)"[^>]*>.*?<\/object>/gi,
|
|
46
|
+
"[Attachment: $1]"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Handle inline images
|
|
50
|
+
processed = processed.replace(
|
|
51
|
+
/<img[^>]*(?:alt="([^"]*)")?[^>]*>/gi,
|
|
52
|
+
(_match, alt) => {
|
|
53
|
+
const filename = alt || "image";
|
|
54
|
+
return `[Attachment: ${filename}]`;
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Convert to Markdown
|
|
59
|
+
const markdown = turndownService.turndown(processed);
|
|
60
|
+
|
|
61
|
+
return markdown.trim();
|
|
62
|
+
}
|
package/src/notes/crud.test.ts
CHANGED
|
@@ -111,18 +111,45 @@ describe("updateNote", () => {
|
|
|
111
111
|
success: true,
|
|
112
112
|
note: { id: "123", title: "Test", folder: "Work" },
|
|
113
113
|
});
|
|
114
|
-
|
|
114
|
+
// Mock JXA returning the new title (same as original in this case)
|
|
115
|
+
vi.mocked(runJxa).mockResolvedValueOnce(JSON.stringify({ newTitle: "Test" }));
|
|
116
|
+
|
|
117
|
+
const result = await updateNote("Test", "New Content");
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
originalTitle: "Test",
|
|
120
|
+
newTitle: "Test",
|
|
121
|
+
folder: "Work",
|
|
122
|
+
titleChanged: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
it("should detect when Apple Notes renames the note", async () => {
|
|
127
|
+
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
128
|
+
success: true,
|
|
129
|
+
note: { id: "123", title: "Original Title", folder: "Work" },
|
|
130
|
+
});
|
|
131
|
+
// Mock JXA returning a different title (Apple Notes renamed it)
|
|
132
|
+
vi.mocked(runJxa).mockResolvedValueOnce(JSON.stringify({ newTitle: "New Heading" }));
|
|
133
|
+
|
|
134
|
+
const result = await updateNote("Original Title", "# New Heading\n\nContent");
|
|
135
|
+
expect(result).toEqual({
|
|
136
|
+
originalTitle: "Original Title",
|
|
137
|
+
newTitle: "New Heading",
|
|
138
|
+
folder: "Work",
|
|
139
|
+
titleChanged: true,
|
|
140
|
+
});
|
|
117
141
|
});
|
|
118
142
|
|
|
119
143
|
it("should include suggestions in error when multiple notes found", async () => {
|
|
120
144
|
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
121
145
|
success: false,
|
|
122
146
|
error: "Multiple notes found",
|
|
123
|
-
suggestions: [
|
|
147
|
+
suggestions: [
|
|
148
|
+
{ id: "id-1", folder: "Work", title: "Note", created: "2026-01-09T10:00:00.000Z" },
|
|
149
|
+
{ id: "id-2", folder: "Personal", title: "Note", created: "2026-01-09T11:00:00.000Z" },
|
|
150
|
+
],
|
|
124
151
|
});
|
|
125
|
-
await expect(updateNote("Note", "Content")).rejects.toThrow("
|
|
152
|
+
await expect(updateNote("Note", "Content")).rejects.toThrow("Use ID prefix");
|
|
126
153
|
});
|
|
127
154
|
});
|
|
128
155
|
|
|
@@ -159,9 +186,12 @@ describe("deleteNote", () => {
|
|
|
159
186
|
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
160
187
|
success: false,
|
|
161
188
|
error: "Multiple notes found",
|
|
162
|
-
suggestions: [
|
|
189
|
+
suggestions: [
|
|
190
|
+
{ id: "id-1", folder: "Work", title: "Note", created: "2026-01-09T10:00:00.000Z" },
|
|
191
|
+
{ id: "id-2", folder: "Personal", title: "Note", created: "2026-01-09T11:00:00.000Z" },
|
|
192
|
+
],
|
|
163
193
|
});
|
|
164
|
-
await expect(deleteNote("Note")).rejects.toThrow("
|
|
194
|
+
await expect(deleteNote("Note")).rejects.toThrow("Use ID prefix");
|
|
165
195
|
});
|
|
166
196
|
});
|
|
167
197
|
|
|
@@ -198,9 +228,12 @@ describe("moveNote", () => {
|
|
|
198
228
|
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
199
229
|
success: false,
|
|
200
230
|
error: "Multiple notes found",
|
|
201
|
-
suggestions: [
|
|
231
|
+
suggestions: [
|
|
232
|
+
{ id: "id-1", folder: "Work", title: "Note", created: "2026-01-09T10:00:00.000Z" },
|
|
233
|
+
{ id: "id-2", folder: "Personal", title: "Note", created: "2026-01-09T11:00:00.000Z" },
|
|
234
|
+
],
|
|
202
235
|
});
|
|
203
|
-
await expect(moveNote("Note", "Archive")).rejects.toThrow("
|
|
236
|
+
await expect(moveNote("Note", "Archive")).rejects.toThrow("Use ID prefix");
|
|
204
237
|
});
|
|
205
238
|
});
|
|
206
239
|
|
package/src/notes/crud.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { marked } from "marked";
|
|
|
10
10
|
import { resolveNoteTitle } from "./read.js";
|
|
11
11
|
import { createDebugLogger } from "../utils/debug.js";
|
|
12
12
|
import { findTables, updateTableCell } from "./tables.js";
|
|
13
|
+
import { ReadOnlyModeError, NoteNotFoundError, DuplicateNoteError } from "../errors/index.js";
|
|
13
14
|
|
|
14
15
|
// Debug logging
|
|
15
16
|
const debug = createDebugLogger("CRUD");
|
|
@@ -22,25 +23,40 @@ const debug = createDebugLogger("CRUD");
|
|
|
22
23
|
*/
|
|
23
24
|
export function checkReadOnly(): void {
|
|
24
25
|
if (process.env.READONLY_MODE === "true") {
|
|
25
|
-
throw new
|
|
26
|
+
throw new ReadOnlyModeError();
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* Resolve note title and throw if not found.
|
|
32
|
+
* Consolidates the repeated error handling pattern.
|
|
31
33
|
*
|
|
32
|
-
* @param
|
|
33
|
-
* @returns
|
|
34
|
+
* @param title - Note title (supports folder prefix)
|
|
35
|
+
* @returns The resolved note with id, title, and folder
|
|
36
|
+
* @throws Error if note not found or duplicates exist without folder prefix
|
|
37
|
+
*/
|
|
38
|
+
async function resolveNoteOrThrow(title: string): Promise<{ id: string; title: string; folder: string }> {
|
|
39
|
+
const resolved = await resolveNoteTitle(title);
|
|
40
|
+
|
|
41
|
+
if (!resolved.success || !resolved.note) {
|
|
42
|
+
if (resolved.suggestions && resolved.suggestions.length > 0) {
|
|
43
|
+
throw new DuplicateNoteError(title, resolved.suggestions);
|
|
44
|
+
}
|
|
45
|
+
throw new NoteNotFoundError(title);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return resolved.note;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert Markdown content to HTML for Apple Notes.
|
|
34
53
|
*/
|
|
35
54
|
function markdownToHtml(markdown: string): string {
|
|
36
|
-
|
|
37
|
-
const html = marked.parse(markdown, {
|
|
55
|
+
return marked.parse(markdown, {
|
|
38
56
|
async: false,
|
|
39
57
|
gfm: true,
|
|
40
58
|
breaks: true,
|
|
41
59
|
}) as string;
|
|
42
|
-
|
|
43
|
-
return html;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
/**
|
|
@@ -99,39 +115,52 @@ export async function createNote(
|
|
|
99
115
|
debug(`Note created: "${title}"`);
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Result of an update operation.
|
|
120
|
+
* Apple Notes may rename the note based on content (first h1 heading).
|
|
121
|
+
*/
|
|
122
|
+
export interface UpdateResult {
|
|
123
|
+
/** Original title before update */
|
|
124
|
+
originalTitle: string;
|
|
125
|
+
/** Current title after update (may differ if Apple Notes renamed it) */
|
|
126
|
+
newTitle: string;
|
|
127
|
+
/** Folder containing the note */
|
|
128
|
+
folder: string;
|
|
129
|
+
/** Whether the title changed */
|
|
130
|
+
titleChanged: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
/**
|
|
103
134
|
* Update an existing note's content.
|
|
104
135
|
*
|
|
136
|
+
* Note: Apple Notes may automatically rename the note based on the first
|
|
137
|
+
* heading in the content. The returned UpdateResult contains the actual
|
|
138
|
+
* title after the update.
|
|
139
|
+
*
|
|
105
140
|
* @param title - Note title (supports folder prefix: "Work/My Note")
|
|
106
141
|
* @param content - New content (Markdown)
|
|
142
|
+
* @returns UpdateResult with original and new title
|
|
107
143
|
* @throws Error if READONLY_MODE is enabled
|
|
108
144
|
* @throws Error if note not found or duplicate titles without folder prefix
|
|
109
145
|
*/
|
|
110
|
-
export async function updateNote(title: string, content: string): Promise<
|
|
146
|
+
export async function updateNote(title: string, content: string): Promise<UpdateResult> {
|
|
111
147
|
checkReadOnly();
|
|
112
148
|
|
|
113
149
|
debug(`Updating note: "${title}"`);
|
|
114
150
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
if (!resolved.success || !resolved.note) {
|
|
119
|
-
if (resolved.suggestions && resolved.suggestions.length > 0) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
`${resolved.error} Suggestions: ${resolved.suggestions.join(", ")}`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
throw new Error(resolved.error || `Note not found: "${title}"`);
|
|
125
|
-
}
|
|
151
|
+
const note = await resolveNoteOrThrow(title);
|
|
152
|
+
const originalTitle = note.title;
|
|
153
|
+
const folder = note.folder;
|
|
126
154
|
|
|
127
155
|
// Convert Markdown to HTML
|
|
128
156
|
const htmlContent = markdownToHtml(content);
|
|
129
|
-
const escapedNoteId = JSON.stringify(
|
|
157
|
+
const escapedNoteId = JSON.stringify(note.id);
|
|
130
158
|
const escapedContent = JSON.stringify(htmlContent);
|
|
131
159
|
|
|
132
160
|
debug(`HTML content length: ${htmlContent.length}`);
|
|
133
161
|
|
|
134
|
-
|
|
162
|
+
// Update the note and get its new title (Apple Notes may rename it)
|
|
163
|
+
const result = await runJxa(`
|
|
135
164
|
const app = Application('Notes');
|
|
136
165
|
const noteId = ${escapedNoteId};
|
|
137
166
|
const content = ${escapedContent};
|
|
@@ -146,10 +175,25 @@ export async function updateNote(title: string, content: string): Promise<void>
|
|
|
146
175
|
// Update the body
|
|
147
176
|
note.body = content;
|
|
148
177
|
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
// Return the current title (may have changed)
|
|
179
|
+
return JSON.stringify({ newTitle: note.name() });
|
|
180
|
+
`) as string;
|
|
181
|
+
|
|
182
|
+
const { newTitle } = JSON.parse(result);
|
|
183
|
+
const titleChanged = newTitle !== originalTitle;
|
|
151
184
|
|
|
152
|
-
|
|
185
|
+
if (titleChanged) {
|
|
186
|
+
debug(`Note renamed by Apple Notes: "${originalTitle}" -> "${newTitle}"`);
|
|
187
|
+
} else {
|
|
188
|
+
debug(`Note updated: "${title}"`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
originalTitle,
|
|
193
|
+
newTitle,
|
|
194
|
+
folder,
|
|
195
|
+
titleChanged,
|
|
196
|
+
};
|
|
153
197
|
}
|
|
154
198
|
|
|
155
199
|
/**
|
|
@@ -167,19 +211,8 @@ export async function deleteNote(title: string): Promise<void> {
|
|
|
167
211
|
|
|
168
212
|
debug(`Deleting note: "${title}"`);
|
|
169
213
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
if (!resolved.success || !resolved.note) {
|
|
174
|
-
if (resolved.suggestions && resolved.suggestions.length > 0) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`${resolved.error} Suggestions: ${resolved.suggestions.join(", ")}`
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
throw new Error(resolved.error || `Note not found: "${title}"`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const escapedNoteId = JSON.stringify(resolved.note.id);
|
|
214
|
+
const note = await resolveNoteOrThrow(title);
|
|
215
|
+
const escapedNoteId = JSON.stringify(note.id);
|
|
183
216
|
|
|
184
217
|
await runJxa(`
|
|
185
218
|
const app = Application('Notes');
|
|
@@ -214,19 +247,8 @@ export async function moveNote(title: string, folder: string): Promise<void> {
|
|
|
214
247
|
|
|
215
248
|
debug(`Moving note: "${title}" to folder: "${folder}"`);
|
|
216
249
|
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (!resolved.success || !resolved.note) {
|
|
221
|
-
if (resolved.suggestions && resolved.suggestions.length > 0) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
`${resolved.error} Suggestions: ${resolved.suggestions.join(", ")}`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
throw new Error(resolved.error || `Note not found: "${title}"`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const escapedNoteId = JSON.stringify(resolved.note.id);
|
|
250
|
+
const note = await resolveNoteOrThrow(title);
|
|
251
|
+
const escapedNoteId = JSON.stringify(note.id);
|
|
230
252
|
const escapedFolder = JSON.stringify(folder);
|
|
231
253
|
|
|
232
254
|
await runJxa(`
|
|
@@ -279,19 +301,8 @@ export async function editTable(
|
|
|
279
301
|
|
|
280
302
|
debug(`Editing table ${tableIndex} in note: "${title}"`);
|
|
281
303
|
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
if (!resolved.success || !resolved.note) {
|
|
285
|
-
if (resolved.suggestions && resolved.suggestions.length > 0) {
|
|
286
|
-
throw new Error(
|
|
287
|
-
`${resolved.error} Suggestions: ${resolved.suggestions.join(", ")}`
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
throw new Error(resolved.error || `Note not found: "${title}"`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Get current HTML content
|
|
294
|
-
const escapedNoteId = JSON.stringify(resolved.note.id);
|
|
304
|
+
const note = await resolveNoteOrThrow(title);
|
|
305
|
+
const escapedNoteId = JSON.stringify(note.id);
|
|
295
306
|
const htmlResult = await runJxa(`
|
|
296
307
|
const app = Application('Notes');
|
|
297
308
|
const note = app.notes.byId(${escapedNoteId});
|
package/src/notes/read.test.ts
CHANGED
|
@@ -118,14 +118,69 @@ describe("resolveNoteTitle", () => {
|
|
|
118
118
|
|
|
119
119
|
it("should return suggestions when multiple matches", async () => {
|
|
120
120
|
const mockNotes = [
|
|
121
|
-
{ id: "123", title: "Note", folder: "Work" },
|
|
122
|
-
{ id: "456", title: "Note", folder: "Personal" },
|
|
121
|
+
{ id: "123", title: "Note", folder: "Work", created: "2026-01-09T10:00:00.000Z" },
|
|
122
|
+
{ id: "456", title: "Note", folder: "Personal", created: "2026-01-09T11:00:00.000Z" },
|
|
123
123
|
];
|
|
124
124
|
vi.mocked(runJxa).mockResolvedValueOnce(JSON.stringify(mockNotes));
|
|
125
125
|
|
|
126
126
|
const result = await resolveNoteTitle("Note");
|
|
127
127
|
expect(result.success).toBe(false);
|
|
128
128
|
expect(result.suggestions).toHaveLength(2);
|
|
129
|
-
expect(result.suggestions).
|
|
129
|
+
expect(result.suggestions?.[0].id).toBe("123");
|
|
130
|
+
expect(result.suggestions?.[0].folder).toBe("Work");
|
|
131
|
+
expect(result.suggestions?.[1].id).toBe("456");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should resolve note by ID prefix", async () => {
|
|
135
|
+
const mockNote = {
|
|
136
|
+
id: "x-coredata://123",
|
|
137
|
+
title: "ID Note",
|
|
138
|
+
folder: "Work",
|
|
139
|
+
created: "2026-01-09T10:00:00.000Z",
|
|
140
|
+
modified: "2026-01-09T11:00:00.000Z",
|
|
141
|
+
htmlContent: "<p>Content</p>",
|
|
142
|
+
};
|
|
143
|
+
vi.mocked(runJxa).mockResolvedValueOnce(JSON.stringify(mockNote));
|
|
144
|
+
|
|
145
|
+
const result = await resolveNoteTitle("id:x-coredata://123");
|
|
146
|
+
expect(result.success).toBe(true);
|
|
147
|
+
expect(result.note?.id).toBe("x-coredata://123");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should return error for invalid ID", async () => {
|
|
151
|
+
vi.mocked(runJxa).mockResolvedValueOnce("null");
|
|
152
|
+
|
|
153
|
+
const result = await resolveNoteTitle("id:invalid-id");
|
|
154
|
+
expect(result.success).toBe(false);
|
|
155
|
+
expect(result.error).toContain("not found");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("getNoteByTitle with ID prefix", () => {
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
vi.clearAllMocks();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should route id: prefix to ID lookup", async () => {
|
|
165
|
+
const mockNote = {
|
|
166
|
+
id: "x-coredata://abc",
|
|
167
|
+
title: "My Note",
|
|
168
|
+
folder: "Work",
|
|
169
|
+
created: "2026-01-09T10:00:00.000Z",
|
|
170
|
+
modified: "2026-01-09T11:00:00.000Z",
|
|
171
|
+
htmlContent: "<p>Hello</p>",
|
|
172
|
+
};
|
|
173
|
+
vi.mocked(runJxa).mockResolvedValueOnce(JSON.stringify(mockNote));
|
|
174
|
+
|
|
175
|
+
const note = await getNoteByTitle("id:x-coredata://abc");
|
|
176
|
+
expect(note).not.toBeNull();
|
|
177
|
+
expect(note?.id).toBe("x-coredata://abc");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should return null for non-existent ID", async () => {
|
|
181
|
+
vi.mocked(runJxa).mockResolvedValueOnce("null");
|
|
182
|
+
|
|
183
|
+
const note = await getNoteByTitle("id:nonexistent");
|
|
184
|
+
expect(note).toBeNull();
|
|
130
185
|
});
|
|
131
186
|
});
|