@disco_trooper/apple-notes-mcp 1.2.0 → 1.4.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 +136 -24
- package/package.json +13 -9
- 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 +209 -2
- package/src/db/lancedb.ts +373 -7
- package/src/embeddings/cache.test.ts +150 -0
- package/src/embeddings/cache.ts +204 -0
- package/src/embeddings/index.ts +21 -2
- package/src/embeddings/local.ts +61 -10
- package/src/embeddings/openrouter.ts +233 -11
- 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 +376 -10
- package/src/notes/crud.test.ts +148 -3
- package/src/notes/crud.ts +250 -5
- package/src/notes/read.ts +83 -68
- package/src/search/chunk-indexer.test.ts +353 -0
- package/src/search/chunk-indexer.ts +254 -0
- package/src/search/chunk-search.test.ts +327 -0
- package/src/search/chunk-search.ts +298 -0
- package/src/search/indexer.ts +151 -109
- package/src/search/refresh.test.ts +173 -0
- package/src/search/refresh.ts +151 -0
- package/src/setup.ts +46 -67
- 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/runtime.test.ts +70 -0
- package/src/utils/runtime.ts +40 -0
package/src/notes/crud.ts
CHANGED
|
@@ -115,20 +115,42 @@ export async function createNote(
|
|
|
115
115
|
debug(`Note created: "${title}"`);
|
|
116
116
|
}
|
|
117
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
|
+
|
|
118
133
|
/**
|
|
119
134
|
* Update an existing note's content.
|
|
120
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
|
+
*
|
|
121
140
|
* @param title - Note title (supports folder prefix: "Work/My Note")
|
|
122
141
|
* @param content - New content (Markdown)
|
|
142
|
+
* @returns UpdateResult with original and new title
|
|
123
143
|
* @throws Error if READONLY_MODE is enabled
|
|
124
144
|
* @throws Error if note not found or duplicate titles without folder prefix
|
|
125
145
|
*/
|
|
126
|
-
export async function updateNote(title: string, content: string): Promise<
|
|
146
|
+
export async function updateNote(title: string, content: string): Promise<UpdateResult> {
|
|
127
147
|
checkReadOnly();
|
|
128
148
|
|
|
129
149
|
debug(`Updating note: "${title}"`);
|
|
130
150
|
|
|
131
151
|
const note = await resolveNoteOrThrow(title);
|
|
152
|
+
const originalTitle = note.title;
|
|
153
|
+
const folder = note.folder;
|
|
132
154
|
|
|
133
155
|
// Convert Markdown to HTML
|
|
134
156
|
const htmlContent = markdownToHtml(content);
|
|
@@ -137,7 +159,8 @@ export async function updateNote(title: string, content: string): Promise<void>
|
|
|
137
159
|
|
|
138
160
|
debug(`HTML content length: ${htmlContent.length}`);
|
|
139
161
|
|
|
140
|
-
|
|
162
|
+
// Update the note and get its new title (Apple Notes may rename it)
|
|
163
|
+
const result = await runJxa(`
|
|
141
164
|
const app = Application('Notes');
|
|
142
165
|
const noteId = ${escapedNoteId};
|
|
143
166
|
const content = ${escapedContent};
|
|
@@ -152,10 +175,25 @@ export async function updateNote(title: string, content: string): Promise<void>
|
|
|
152
175
|
// Update the body
|
|
153
176
|
note.body = content;
|
|
154
177
|
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
184
|
+
|
|
185
|
+
if (titleChanged) {
|
|
186
|
+
debug(`Note renamed by Apple Notes: "${originalTitle}" -> "${newTitle}"`);
|
|
187
|
+
} else {
|
|
188
|
+
debug(`Note updated: "${title}"`);
|
|
189
|
+
}
|
|
157
190
|
|
|
158
|
-
|
|
191
|
+
return {
|
|
192
|
+
originalTitle,
|
|
193
|
+
newTitle,
|
|
194
|
+
folder,
|
|
195
|
+
titleChanged,
|
|
196
|
+
};
|
|
159
197
|
}
|
|
160
198
|
|
|
161
199
|
/**
|
|
@@ -304,3 +342,210 @@ export async function editTable(
|
|
|
304
342
|
|
|
305
343
|
debug(`Table ${tableIndex} updated in note: "${title}"`);
|
|
306
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Result of a batch operation.
|
|
348
|
+
*/
|
|
349
|
+
export interface BatchResult {
|
|
350
|
+
/** Number of notes successfully processed */
|
|
351
|
+
deleted: number;
|
|
352
|
+
/** Notes that failed to process */
|
|
353
|
+
failed: string[];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Options for batch delete.
|
|
358
|
+
*/
|
|
359
|
+
export interface BatchDeleteOptions {
|
|
360
|
+
/** List of note titles (supports folder/title and id:xxx formats) */
|
|
361
|
+
titles?: string[];
|
|
362
|
+
/** Delete all notes in this folder */
|
|
363
|
+
folder?: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Delete multiple notes at once.
|
|
368
|
+
*
|
|
369
|
+
* @param options - Either titles array OR folder name (not both)
|
|
370
|
+
* @returns BatchResult with deleted count and failed notes
|
|
371
|
+
* @throws Error if READONLY_MODE is enabled
|
|
372
|
+
* @throws Error if both titles and folder provided
|
|
373
|
+
* @throws Error if neither titles nor folder provided
|
|
374
|
+
*/
|
|
375
|
+
export async function batchDelete(options: BatchDeleteOptions): Promise<BatchResult> {
|
|
376
|
+
checkReadOnly();
|
|
377
|
+
|
|
378
|
+
const { titles, folder } = options;
|
|
379
|
+
|
|
380
|
+
if (titles && folder) {
|
|
381
|
+
throw new Error("Specify either titles or folder, not both");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!titles && !folder) {
|
|
385
|
+
throw new Error("Specify either titles or folder");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const result: BatchResult = { deleted: 0, failed: [] };
|
|
389
|
+
|
|
390
|
+
if (folder) {
|
|
391
|
+
// Delete all notes in folder via single JXA call
|
|
392
|
+
debug(`Batch deleting all notes in folder: "${folder}"`);
|
|
393
|
+
|
|
394
|
+
const escapedFolder = JSON.stringify(folder);
|
|
395
|
+
const jxaResult = await runJxa(`
|
|
396
|
+
const app = Application('Notes');
|
|
397
|
+
const folderName = ${escapedFolder};
|
|
398
|
+
|
|
399
|
+
const folders = app.folders.whose({name: folderName})();
|
|
400
|
+
if (folders.length === 0) {
|
|
401
|
+
throw new Error("Folder not found: " + folderName);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const folder = folders[0];
|
|
405
|
+
const notes = folder.notes();
|
|
406
|
+
let deletedCount = 0;
|
|
407
|
+
|
|
408
|
+
// Delete in reverse order to avoid index shifting
|
|
409
|
+
for (let i = notes.length - 1; i >= 0; i--) {
|
|
410
|
+
try {
|
|
411
|
+
notes[i].delete();
|
|
412
|
+
deletedCount++;
|
|
413
|
+
} catch (e) {
|
|
414
|
+
// Continue on individual failures
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return JSON.stringify({ deletedCount });
|
|
419
|
+
`);
|
|
420
|
+
|
|
421
|
+
const { deletedCount } = JSON.parse(jxaResult as string);
|
|
422
|
+
result.deleted = deletedCount;
|
|
423
|
+
} else if (titles) {
|
|
424
|
+
// Delete individual notes
|
|
425
|
+
debug(`Batch deleting ${titles.length} notes by title`);
|
|
426
|
+
|
|
427
|
+
for (const title of titles) {
|
|
428
|
+
try {
|
|
429
|
+
await deleteNote(title);
|
|
430
|
+
result.deleted++;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
result.failed.push(title);
|
|
433
|
+
debug(`Failed to delete "${title}":`, error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
debug(`Batch delete complete: ${result.deleted} deleted, ${result.failed.length} failed`);
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Result of a batch move operation.
|
|
444
|
+
*/
|
|
445
|
+
export interface BatchMoveResult {
|
|
446
|
+
/** Number of notes successfully moved */
|
|
447
|
+
moved: number;
|
|
448
|
+
/** Notes that failed to move */
|
|
449
|
+
failed: string[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Options for batch move.
|
|
454
|
+
*/
|
|
455
|
+
export interface BatchMoveOptions {
|
|
456
|
+
/** List of note titles (supports folder/title and id:xxx formats) */
|
|
457
|
+
titles?: string[];
|
|
458
|
+
/** Move all notes from this folder */
|
|
459
|
+
sourceFolder?: string;
|
|
460
|
+
/** Target folder (required) */
|
|
461
|
+
targetFolder: string;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Move multiple notes to a target folder.
|
|
466
|
+
*
|
|
467
|
+
* @param options - Either titles array OR sourceFolder (not both) + targetFolder
|
|
468
|
+
* @returns BatchMoveResult with moved count and failed notes
|
|
469
|
+
* @throws Error if READONLY_MODE is enabled
|
|
470
|
+
* @throws Error if both titles and sourceFolder provided
|
|
471
|
+
* @throws Error if neither titles nor sourceFolder provided
|
|
472
|
+
* @throws Error if targetFolder is empty
|
|
473
|
+
*/
|
|
474
|
+
export async function batchMove(options: BatchMoveOptions): Promise<BatchMoveResult> {
|
|
475
|
+
checkReadOnly();
|
|
476
|
+
|
|
477
|
+
const { titles, sourceFolder, targetFolder } = options;
|
|
478
|
+
|
|
479
|
+
if (!targetFolder) {
|
|
480
|
+
throw new Error("targetFolder is required");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (titles && sourceFolder) {
|
|
484
|
+
throw new Error("Specify either titles or sourceFolder, not both");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!titles && !sourceFolder) {
|
|
488
|
+
throw new Error("Specify either titles or sourceFolder");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const result: BatchMoveResult = { moved: 0, failed: [] };
|
|
492
|
+
|
|
493
|
+
if (sourceFolder) {
|
|
494
|
+
// Move all notes from source folder via single JXA call
|
|
495
|
+
debug(`Batch moving all notes from "${sourceFolder}" to "${targetFolder}"`);
|
|
496
|
+
|
|
497
|
+
const escapedSource = JSON.stringify(sourceFolder);
|
|
498
|
+
const escapedTarget = JSON.stringify(targetFolder);
|
|
499
|
+
const jxaResult = await runJxa(`
|
|
500
|
+
const app = Application('Notes');
|
|
501
|
+
const sourceName = ${escapedSource};
|
|
502
|
+
const targetName = ${escapedTarget};
|
|
503
|
+
|
|
504
|
+
const sourceFolders = app.folders.whose({name: sourceName})();
|
|
505
|
+
if (sourceFolders.length === 0) {
|
|
506
|
+
throw new Error("Source folder not found: " + sourceName);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const targetFolders = app.folders.whose({name: targetName})();
|
|
510
|
+
if (targetFolders.length === 0) {
|
|
511
|
+
throw new Error("Target folder not found: " + targetName);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const source = sourceFolders[0];
|
|
515
|
+
const target = targetFolders[0];
|
|
516
|
+
const notes = source.notes();
|
|
517
|
+
let movedCount = 0;
|
|
518
|
+
|
|
519
|
+
// Move in reverse order to avoid index shifting
|
|
520
|
+
for (let i = notes.length - 1; i >= 0; i--) {
|
|
521
|
+
try {
|
|
522
|
+
notes[i].move({to: target});
|
|
523
|
+
movedCount++;
|
|
524
|
+
} catch (e) {
|
|
525
|
+
// Continue on individual failures
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return JSON.stringify({ movedCount });
|
|
530
|
+
`);
|
|
531
|
+
|
|
532
|
+
const { movedCount } = JSON.parse(jxaResult as string);
|
|
533
|
+
result.moved = movedCount;
|
|
534
|
+
} else if (titles) {
|
|
535
|
+
// Move individual notes
|
|
536
|
+
debug(`Batch moving ${titles.length} notes to "${targetFolder}"`);
|
|
537
|
+
|
|
538
|
+
for (const title of titles) {
|
|
539
|
+
try {
|
|
540
|
+
await moveNote(title, targetFolder);
|
|
541
|
+
result.moved++;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
result.failed.push(title);
|
|
544
|
+
debug(`Failed to move "${title}":`, error);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
debug(`Batch move complete: ${result.moved} moved, ${result.failed.length} failed`);
|
|
550
|
+
return result;
|
|
551
|
+
}
|
package/src/notes/read.ts
CHANGED
|
@@ -40,9 +40,19 @@ export interface NoteDetails extends NoteInfo {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// -----------------------------------------------------------------------------
|
|
43
|
-
// Internal
|
|
43
|
+
// Internal types and helpers
|
|
44
44
|
// -----------------------------------------------------------------------------
|
|
45
45
|
|
|
46
|
+
/** Raw note data from JXA before markdown conversion */
|
|
47
|
+
interface RawNoteData {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
folder: string;
|
|
51
|
+
created: string;
|
|
52
|
+
modified: string;
|
|
53
|
+
htmlContent: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
/**
|
|
47
57
|
* Execute JXA code safely with error handling
|
|
48
58
|
*/
|
|
@@ -58,6 +68,21 @@ async function executeJxa<T>(code: string): Promise<T> {
|
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Convert raw JXA note data to NoteDetails with markdown content
|
|
73
|
+
*/
|
|
74
|
+
function toNoteDetails(raw: RawNoteData): NoteDetails {
|
|
75
|
+
return {
|
|
76
|
+
id: raw.id,
|
|
77
|
+
title: raw.title,
|
|
78
|
+
folder: raw.folder,
|
|
79
|
+
created: raw.created,
|
|
80
|
+
modified: raw.modified,
|
|
81
|
+
content: htmlToMarkdown(raw.htmlContent),
|
|
82
|
+
htmlContent: raw.htmlContent,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
// -----------------------------------------------------------------------------
|
|
62
87
|
// Public API
|
|
63
88
|
// -----------------------------------------------------------------------------
|
|
@@ -180,14 +205,7 @@ export async function getNoteByTitle(
|
|
|
180
205
|
`;
|
|
181
206
|
|
|
182
207
|
const result = await executeJxa<string>(jxaCode);
|
|
183
|
-
const notes = JSON.parse(result) as
|
|
184
|
-
id: string;
|
|
185
|
-
title: string;
|
|
186
|
-
folder: string;
|
|
187
|
-
created: string;
|
|
188
|
-
modified: string;
|
|
189
|
-
htmlContent: string;
|
|
190
|
-
}>;
|
|
208
|
+
const notes = JSON.parse(result) as RawNoteData[];
|
|
191
209
|
|
|
192
210
|
if (notes.length === 0) {
|
|
193
211
|
debug("Note not found");
|
|
@@ -196,27 +214,11 @@ export async function getNoteByTitle(
|
|
|
196
214
|
|
|
197
215
|
if (notes.length > 1) {
|
|
198
216
|
debug(`Multiple notes found with title: ${targetTitle}`);
|
|
199
|
-
|
|
200
|
-
// but log a warning
|
|
201
|
-
debug(
|
|
202
|
-
"Returning first match. Use folder/title format for disambiguation."
|
|
203
|
-
);
|
|
217
|
+
debug("Returning first match. Use folder/title format for disambiguation.");
|
|
204
218
|
}
|
|
205
219
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
debug(`Found note in folder: ${note.folder}`);
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
id: note.id,
|
|
213
|
-
title: note.title,
|
|
214
|
-
folder: note.folder,
|
|
215
|
-
created: note.created,
|
|
216
|
-
modified: note.modified,
|
|
217
|
-
content,
|
|
218
|
-
htmlContent: note.htmlContent,
|
|
219
|
-
};
|
|
220
|
+
debug(`Found note in folder: ${notes[0].folder}`);
|
|
221
|
+
return toNoteDetails(notes[0]);
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
/**
|
|
@@ -268,33 +270,15 @@ export async function getNoteById(id: string): Promise<NoteDetails | null> {
|
|
|
268
270
|
`;
|
|
269
271
|
|
|
270
272
|
const result = await executeJxa<string>(jxaCode);
|
|
271
|
-
const note = JSON.parse(result) as
|
|
272
|
-
id: string;
|
|
273
|
-
title: string;
|
|
274
|
-
folder: string;
|
|
275
|
-
created: string;
|
|
276
|
-
modified: string;
|
|
277
|
-
htmlContent: string;
|
|
278
|
-
} | null;
|
|
273
|
+
const note = JSON.parse(result) as RawNoteData | null;
|
|
279
274
|
|
|
280
275
|
if (!note) {
|
|
281
276
|
debug("Note not found by ID");
|
|
282
277
|
return null;
|
|
283
278
|
}
|
|
284
279
|
|
|
285
|
-
const content = htmlToMarkdown(note.htmlContent);
|
|
286
|
-
|
|
287
280
|
debug(`Found note: ${note.title} in folder: ${note.folder}`);
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
id: note.id,
|
|
291
|
-
title: note.title,
|
|
292
|
-
folder: note.folder,
|
|
293
|
-
created: note.created,
|
|
294
|
-
modified: note.modified,
|
|
295
|
-
content,
|
|
296
|
-
htmlContent: note.htmlContent,
|
|
297
|
-
};
|
|
281
|
+
return toNoteDetails(note);
|
|
298
282
|
}
|
|
299
283
|
|
|
300
284
|
/**
|
|
@@ -357,34 +341,65 @@ export async function getNoteByFolderAndTitle(
|
|
|
357
341
|
`;
|
|
358
342
|
|
|
359
343
|
const result = await executeJxa<string>(jxaCode);
|
|
360
|
-
const notes = JSON.parse(result) as
|
|
361
|
-
id: string;
|
|
362
|
-
title: string;
|
|
363
|
-
folder: string;
|
|
364
|
-
created: string;
|
|
365
|
-
modified: string;
|
|
366
|
-
htmlContent: string;
|
|
367
|
-
}>;
|
|
344
|
+
const notes = JSON.parse(result) as RawNoteData[];
|
|
368
345
|
|
|
369
346
|
if (notes.length === 0) {
|
|
370
347
|
debug("Note not found");
|
|
371
348
|
return null;
|
|
372
349
|
}
|
|
373
350
|
|
|
374
|
-
|
|
375
|
-
|
|
351
|
+
debug(`Found note in folder: ${notes[0].folder}`);
|
|
352
|
+
return toNoteDetails(notes[0]);
|
|
353
|
+
}
|
|
376
354
|
|
|
377
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Get all notes with full content in a single JXA call.
|
|
357
|
+
* This is much faster than calling getNoteByFolderAndTitle for each note
|
|
358
|
+
* because it avoids the JXA process spawn overhead per note.
|
|
359
|
+
*
|
|
360
|
+
* @returns Array of note details with content
|
|
361
|
+
*/
|
|
362
|
+
export async function getAllNotesWithContent(): Promise<NoteDetails[]> {
|
|
363
|
+
debug("Getting all notes with content (single JXA call)...");
|
|
378
364
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
365
|
+
const jxaCode = `
|
|
366
|
+
const app = Application('Notes');
|
|
367
|
+
app.includeStandardAdditions = true;
|
|
368
|
+
|
|
369
|
+
const allNotes = [];
|
|
370
|
+
const folders = app.folders();
|
|
371
|
+
|
|
372
|
+
for (const folder of folders) {
|
|
373
|
+
const folderName = folder.name();
|
|
374
|
+
const notes = folder.notes();
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < notes.length; i++) {
|
|
377
|
+
try {
|
|
378
|
+
const note = notes[i];
|
|
379
|
+
const props = note.properties();
|
|
380
|
+
allNotes.push({
|
|
381
|
+
id: note.id(),
|
|
382
|
+
title: props.name || '',
|
|
383
|
+
folder: folderName,
|
|
384
|
+
created: props.creationDate ? props.creationDate.toISOString() : '',
|
|
385
|
+
modified: props.modificationDate ? props.modificationDate.toISOString() : '',
|
|
386
|
+
htmlContent: note.body()
|
|
387
|
+
});
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// Skip notes that can't be accessed
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return JSON.stringify(allNotes);
|
|
395
|
+
`;
|
|
396
|
+
|
|
397
|
+
const result = await executeJxa<string>(jxaCode);
|
|
398
|
+
const notes = JSON.parse(result) as RawNoteData[];
|
|
399
|
+
|
|
400
|
+
debug(`Fetched ${notes.length} notes with content`);
|
|
401
|
+
|
|
402
|
+
return notes.map(toNoteDetails);
|
|
388
403
|
}
|
|
389
404
|
|
|
390
405
|
/**
|