@peerasak-u/apple-notes 1.0.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/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/executor.ts
4
+ import { execSync } from "child_process";
5
+ import { join, dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ var __filename2 = fileURLToPath(import.meta.url);
8
+ var __dirname2 = dirname(__filename2);
9
+ function getNotesScriptPath() {
10
+ return join(__dirname2, "jxa", "notes.js");
11
+ }
12
+ function escapeShellArg(arg) {
13
+ return `'${arg.replace(/'/g, "'\\''")}'`;
14
+ }
15
+ function executeNotesCommand(args) {
16
+ const scriptPath = getNotesScriptPath();
17
+ const escapedArgs = args.map(escapeShellArg).join(" ");
18
+ const command = `osascript -l JavaScript "${scriptPath}" ${escapedArgs}`;
19
+ const options = {
20
+ encoding: "utf-8",
21
+ maxBuffer: 10 * 1024 * 1024
22
+ };
23
+ try {
24
+ const result = execSync(command, options);
25
+ return result.trim();
26
+ } catch (error) {
27
+ if (error instanceof Error && "stderr" in error) {
28
+ const stderr = error.stderr;
29
+ if (stderr) {
30
+ throw new Error(stderr.toString().trim());
31
+ }
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ // src/index.ts
38
+ var USAGE = `
39
+ Apple Notes CLI - Interact with Apple Notes on macOS
40
+
41
+ Usage:
42
+ apple-notes <command> [arguments]
43
+
44
+ Commands:
45
+ search <query> Search notes by content
46
+ list <query> List notes by title (returns indexed results)
47
+ read <title> [folder] Read a note by title
48
+ read-index <query> <index> Read note by index from list results (1-based)
49
+ recent [count] [folder] Get recently modified notes (default: 5)
50
+ create <title> <body> [folder] Create a new note (body in Markdown)
51
+ delete <title> [folder] Delete a note by exact title
52
+
53
+ Examples:
54
+ apple-notes search "meeting notes"
55
+ apple-notes list "project"
56
+ apple-notes read "My Note"
57
+ apple-notes read "My Note" "Work/Projects"
58
+ apple-notes read-index "budget" 2
59
+ apple-notes recent 10
60
+ apple-notes recent 5 "Work"
61
+ apple-notes create "New Note" "# Hello\\n- Item 1" "Notes"
62
+ apple-notes delete "Old Note"
63
+
64
+ Folder paths use "/" separator for nested folders (e.g., "Work/Projects/2024").
65
+ `;
66
+ function main() {
67
+ const args = process.argv.slice(2);
68
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
69
+ console.log(USAGE.trim());
70
+ process.exit(0);
71
+ }
72
+ if (args[0] === "--version" || args[0] === "-v") {
73
+ console.log("1.0.0");
74
+ process.exit(0);
75
+ }
76
+ try {
77
+ const result = executeNotesCommand(args);
78
+ if (result) {
79
+ console.log(result);
80
+ }
81
+ } catch (error) {
82
+ if (error instanceof Error) {
83
+ console.error(`Error: ${error.message}`);
84
+ } else {
85
+ console.error("An unknown error occurred");
86
+ }
87
+ process.exit(1);
88
+ }
89
+ }
90
+ main();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@peerasak-u/apple-notes",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for interacting with Apple Notes on macOS",
5
+ "type": "module",
6
+ "bin": {
7
+ "apple-notes": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "src/jxa"
12
+ ],
13
+ "scripts": {
14
+ "build": "bun build src/index.ts --outdir dist --target node",
15
+ "sync-utils": "bun scripts/sync-test-utils.js",
16
+ "test": "bun run sync-utils && bun test tests/*.test.js",
17
+ "test:coverage": "bun run sync-utils && bun test --coverage tests/*.test.js",
18
+ "lint": "eslint tests/*.js",
19
+ "lint:fix": "eslint --fix tests/*.js",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "apple-notes",
24
+ "jxa",
25
+ "macos",
26
+ "notes",
27
+ "cli"
28
+ ],
29
+ "author": "peerasak-u",
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "@types/node": "^20.0.0",
37
+ "eslint": "^8.55.0",
38
+ "typescript": "^5.3.0"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/peerasak-u/apple-notes-skill.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/peerasak-u/apple-notes-skill/issues"
46
+ },
47
+ "homepage": "https://github.com/peerasak-u/apple-notes-skill#readme"
48
+ }
@@ -0,0 +1,672 @@
1
+ #!/usr/bin/env osascript -l JavaScript
2
+
3
+ // Apple Notes JXA Script
4
+ // Provides: search, list, read, read-index, recent, create, delete
5
+
6
+ function run(argv) {
7
+ if (argv.length === 0) {
8
+ return getUsage();
9
+ }
10
+
11
+ const command = argv[0];
12
+
13
+ switch (command) {
14
+ case "search":
15
+ if (argv.length < 2) {
16
+ return "Error: search requires a query string\n" + getUsage();
17
+ }
18
+ return searchNotes(argv[1]);
19
+
20
+ case "list":
21
+ if (argv.length < 2) {
22
+ return "Error: list requires a search term\n" + getUsage();
23
+ }
24
+ return listNotes(argv[1]);
25
+
26
+ case "read":
27
+ if (argv.length < 2) {
28
+ return "Error: read requires a note identifier\n" + getUsage();
29
+ }
30
+ return readNote(argv[1], argv[2] || "");
31
+
32
+ case "read-index":
33
+ if (argv.length < 3) {
34
+ return "Error: read-index requires search term and index\n" + getUsage();
35
+ }
36
+ return readNoteByIndex(argv[1], parseInt(argv[2], 10));
37
+
38
+ case "recent":
39
+ return parseRecentArgs(argv.slice(1));
40
+
41
+ case "create":
42
+ if (argv.length < 3) {
43
+ return "Error: create requires title and body\n" + getUsage();
44
+ }
45
+ return createNote(argv[1], argv[2], argv[3] || "Notes");
46
+
47
+ case "delete":
48
+ if (argv.length < 2) {
49
+ return "Error: delete requires a note title\n" + getUsage();
50
+ }
51
+ return deleteNote(argv[1], argv[2] || "");
52
+
53
+ default:
54
+ return `Error: Unknown command '${command}'\n` + getUsage();
55
+ }
56
+ }
57
+
58
+ // ============================================================================
59
+ // HTML <-> Markdown Conversion
60
+ // ============================================================================
61
+
62
+ function htmlToMarkdown(html) {
63
+ if (!html) return "";
64
+
65
+ let md = html;
66
+
67
+ // Remove Apple Notes specific wrapper divs but keep content
68
+ md = md.replace(/<div[^>]*>/gi, "\n");
69
+ md = md.replace(/<\/div>/gi, "");
70
+
71
+ // Headings
72
+ md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, "# $1\n");
73
+ md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, "## $1\n");
74
+ md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, "### $1\n");
75
+ md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, "#### $1\n");
76
+ md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, "##### $1\n");
77
+ md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, "###### $1\n");
78
+
79
+ // Bold and italic
80
+ md = md.replace(/<(b|strong)[^>]*>(.*?)<\/(b|strong)>/gi, "**$2**");
81
+ md = md.replace(/<(i|em)[^>]*>(.*?)<\/(i|em)>/gi, "*$2*");
82
+ md = md.replace(/<u[^>]*>(.*?)<\/u>/gi, "_$1_");
83
+ md = md.replace(/<s[^>]*>(.*?)<\/s>/gi, "~~$1~~");
84
+ md = md.replace(/<strike[^>]*>(.*?)<\/strike>/gi, "~~$1~~");
85
+
86
+ // Links
87
+ md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, "[$2]($1)");
88
+
89
+ // Images
90
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, "![$2]($1)");
91
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*\/?>/gi, "![]($1)");
92
+
93
+ // Lists - handle nested lists
94
+ md = md.replace(/<ul[^>]*>/gi, "\n");
95
+ md = md.replace(/<\/ul>/gi, "\n");
96
+ md = md.replace(/<ol[^>]*>/gi, "\n");
97
+ md = md.replace(/<\/ol>/gi, "\n");
98
+ md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, "- $1\n");
99
+
100
+ // Line breaks and paragraphs
101
+ md = md.replace(/<br\s*\/?>/gi, "\n");
102
+ md = md.replace(/<p[^>]*>/gi, "\n");
103
+ md = md.replace(/<\/p>/gi, "\n");
104
+
105
+ // Code
106
+ md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
107
+ md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "```\n$1\n```\n");
108
+
109
+ // Blockquote
110
+ md = md.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, "> $1\n");
111
+
112
+ // Horizontal rule
113
+ md = md.replace(/<hr\s*\/?>/gi, "\n---\n");
114
+
115
+ // Remove remaining HTML tags
116
+ md = md.replace(/<[^>]+>/g, "");
117
+
118
+ // Decode HTML entities
119
+ md = md.replace(/&nbsp;/g, " ");
120
+ md = md.replace(/&amp;/g, "&");
121
+ md = md.replace(/&lt;/g, "<");
122
+ md = md.replace(/&gt;/g, ">");
123
+ md = md.replace(/&quot;/g, '"');
124
+ md = md.replace(/&#39;/g, "'");
125
+ md = md.replace(/&apos;/g, "'");
126
+
127
+ // Clean up multiple newlines
128
+ md = md.replace(/\n{3,}/g, "\n\n");
129
+ md = md.trim();
130
+
131
+ // Check if conversion was successful (no remaining HTML-like content)
132
+ if (/<[a-z][\s\S]*>/i.test(md)) {
133
+ return "[RAW_HTML]\n" + html;
134
+ }
135
+
136
+ return md;
137
+ }
138
+
139
+ function markdownToHtml(markdown) {
140
+ if (!markdown) return "";
141
+
142
+ let html = markdown;
143
+
144
+ // Unescape common escape sequences from command line
145
+ html = html.replace(/\\n/g, "\n");
146
+ html = html.replace(/\\t/g, "\t");
147
+
148
+ // Headings (must be at start of line)
149
+ html = html.replace(/^###### (.+)$/gm, "<h6>$1</h6>");
150
+ html = html.replace(/^##### (.+)$/gm, "<h5>$1</h5>");
151
+ html = html.replace(/^#### (.+)$/gm, "<h4>$1</h4>");
152
+ html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
153
+ html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
154
+ html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
155
+
156
+ // Bold and italic (order matters)
157
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<b><i>$1</i></b>");
158
+ html = html.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
159
+ html = html.replace(/\*(.+?)\*/g, "<i>$1</i>");
160
+ html = html.replace(/_(.+?)_/g, "<i>$1</i>");
161
+ html = html.replace(/~~(.+?)~~/g, "<s>$1</s>");
162
+
163
+ // Links and images
164
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
165
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
166
+
167
+ // Code
168
+ html = html.replace(/```([\s\S]*?)```/g, "<pre>$1</pre>");
169
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
170
+
171
+ // Blockquote
172
+ html = html.replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>");
173
+
174
+ // Horizontal rule
175
+ html = html.replace(/^---$/gm, "<hr>");
176
+ html = html.replace(/^\*\*\*$/gm, "<hr>");
177
+
178
+ // Lists (simple handling)
179
+ html = html.replace(/^- (.+)$/gm, "<li>$1</li>");
180
+ html = html.replace(/^\* (.+)$/gm, "<li>$1</li>");
181
+ html = html.replace(/^\d+\. (.+)$/gm, "<li>$1</li>");
182
+
183
+ // Wrap consecutive <li> in <ul>
184
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, function (match) {
185
+ return "<ul>" + match + "</ul>";
186
+ });
187
+
188
+ // Paragraphs - wrap lines that aren't already HTML
189
+ const lines = html.split("\n");
190
+ const result = [];
191
+ for (let i = 0; i < lines.length; i++) {
192
+ const line = lines[i].trim();
193
+ if (line === "") {
194
+ result.push("<br>");
195
+ } else if (!/^</.test(line)) {
196
+ result.push("<div>" + line + "</div>");
197
+ } else {
198
+ result.push(line);
199
+ }
200
+ }
201
+ html = result.join("\n");
202
+
203
+ return html;
204
+ }
205
+
206
+ // ============================================================================
207
+ // Notes App Interaction
208
+ // ============================================================================
209
+
210
+ function getNotesApp() {
211
+ return Application("Notes");
212
+ }
213
+
214
+ function searchNotes(query) {
215
+ const app = getNotesApp();
216
+ const allNotes = app.notes();
217
+ const results = [];
218
+
219
+ for (let i = 0; i < allNotes.length; i++) {
220
+ const note = allNotes[i];
221
+ const body = note.body();
222
+ if (body && body.toLowerCase().includes(query.toLowerCase())) {
223
+ const folderName = getFolderName(note);
224
+ const preview = getPreview(body, 150);
225
+ results.push({
226
+ title: note.name(),
227
+ folder: folderName,
228
+ modified: note.modificationDate().toString(),
229
+ preview: htmlToMarkdown(preview),
230
+ });
231
+ }
232
+ }
233
+
234
+ if (results.length === 0) {
235
+ return `No notes found matching: ${query}`;
236
+ }
237
+
238
+ let output = `Found ${results.length} note(s):\n\n`;
239
+ for (const r of results) {
240
+ output += `Title: ${r.title}\n`;
241
+ output += `Folder: ${r.folder}\n`;
242
+ output += `Modified: ${r.modified}\n`;
243
+ output += `Preview: ${r.preview}\n`;
244
+ output += "---\n\n";
245
+ }
246
+
247
+ return output;
248
+ }
249
+
250
+ function listNotes(query) {
251
+ const app = getNotesApp();
252
+ const allNotes = app.notes();
253
+ const results = [];
254
+
255
+ for (let i = 0; i < allNotes.length; i++) {
256
+ const note = allNotes[i];
257
+ const name = note.name();
258
+ if (name && name.toLowerCase().includes(query.toLowerCase())) {
259
+ const folderName = getFolderName(note);
260
+ const body = note.body() || "";
261
+ const preview = getPreview(body, 80);
262
+ results.push({
263
+ index: results.length + 1,
264
+ title: name,
265
+ folder: folderName,
266
+ modified: note.modificationDate().toString(),
267
+ preview: htmlToMarkdown(preview),
268
+ });
269
+ }
270
+ }
271
+
272
+ if (results.length === 0) {
273
+ return `No notes found with title containing: ${query}`;
274
+ }
275
+
276
+ let output = `Found ${results.length} note(s) matching '${query}':\n\n`;
277
+ for (const r of results) {
278
+ output += `[${r.index}] ${r.title}\n`;
279
+ output += ` Folder: ${r.folder}\n`;
280
+ output += ` Modified: ${r.modified}\n`;
281
+ output += ` Preview: ${r.preview}\n\n`;
282
+ }
283
+
284
+ output += `\nUse 'read-index ${query} <index>' to read full note`;
285
+
286
+ return output;
287
+ }
288
+
289
+ function readNote(identifier, folderName) {
290
+ const app = getNotesApp();
291
+ let matchingNotes = [];
292
+
293
+ if (folderName === "") {
294
+ // Search all notes
295
+ const allNotes = app.notes();
296
+ for (let i = 0; i < allNotes.length; i++) {
297
+ const note = allNotes[i];
298
+ const name = note.name();
299
+ if (name === identifier) {
300
+ matchingNotes.push(note);
301
+ }
302
+ }
303
+ // Try partial match if exact match fails
304
+ if (matchingNotes.length === 0) {
305
+ for (let i = 0; i < allNotes.length; i++) {
306
+ const note = allNotes[i];
307
+ const name = note.name();
308
+ if (name && name.toLowerCase().includes(identifier.toLowerCase())) {
309
+ matchingNotes.push(note);
310
+ }
311
+ }
312
+ }
313
+ } else {
314
+ // Search in specific folder
315
+ const folder = findFolder(folderName);
316
+ if (!folder) {
317
+ return `Error: Folder '${folderName}' not found`;
318
+ }
319
+ const folderNotes = folder.notes();
320
+ for (let i = 0; i < folderNotes.length; i++) {
321
+ const note = folderNotes[i];
322
+ const name = note.name();
323
+ if (name === identifier) {
324
+ matchingNotes.push(note);
325
+ }
326
+ }
327
+ if (matchingNotes.length === 0) {
328
+ for (let i = 0; i < folderNotes.length; i++) {
329
+ const note = folderNotes[i];
330
+ const name = note.name();
331
+ if (name && name.toLowerCase().includes(identifier.toLowerCase())) {
332
+ matchingNotes.push(note);
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ if (matchingNotes.length === 0) {
339
+ let errorMsg = `Error: No note found matching '${identifier}'`;
340
+ if (folderName !== "") {
341
+ errorMsg += ` in folder '${folderName}'`;
342
+ }
343
+ return errorMsg;
344
+ }
345
+
346
+ if (matchingNotes.length > 1) {
347
+ let output = `Found ${matchingNotes.length} notes. Please use one of these options:\n\n`;
348
+ for (let i = 0; i < matchingNotes.length; i++) {
349
+ const note = matchingNotes[i];
350
+ const folder = getFolderName(note);
351
+ output += `[${i + 1}] ${note.name()} (${folder})\n`;
352
+ }
353
+ output += `\nUse: read-index '${identifier}' <index>`;
354
+ output += `\nOr: read '${identifier}' '<folder-name>'`;
355
+ return output;
356
+ }
357
+
358
+ return formatNoteContent(matchingNotes[0]);
359
+ }
360
+
361
+ function readNoteByIndex(query, index) {
362
+ const app = getNotesApp();
363
+ const allNotes = app.notes();
364
+ const matchingNotes = [];
365
+
366
+ for (let i = 0; i < allNotes.length; i++) {
367
+ const note = allNotes[i];
368
+ const name = note.name();
369
+ if (name && name.toLowerCase().includes(query.toLowerCase())) {
370
+ matchingNotes.push(note);
371
+ }
372
+ }
373
+
374
+ if (matchingNotes.length === 0) {
375
+ return `Error: No notes found with title containing '${query}'`;
376
+ }
377
+
378
+ if (index < 1 || index > matchingNotes.length) {
379
+ return `Error: Index ${index} out of range. Found ${matchingNotes.length} notes.`;
380
+ }
381
+
382
+ return formatNoteContent(matchingNotes[index - 1]);
383
+ }
384
+
385
+ function parseRecentArgs(args) {
386
+ let limit = 5;
387
+ let folderName = "";
388
+
389
+ if (args.length >= 1) {
390
+ const arg1 = args[0];
391
+ const num1 = parseInt(arg1, 10);
392
+ if (!isNaN(num1)) {
393
+ limit = num1;
394
+ if (args.length >= 2) {
395
+ folderName = args[1];
396
+ }
397
+ } else {
398
+ folderName = arg1;
399
+ if (args.length >= 2) {
400
+ const num2 = parseInt(args[1], 10);
401
+ if (!isNaN(num2)) {
402
+ limit = num2;
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ return getRecentNotes(limit, folderName);
409
+ }
410
+
411
+ function getRecentNotes(limit, folderName) {
412
+ const app = getNotesApp();
413
+ let notes = [];
414
+
415
+ if (folderName === "") {
416
+ notes = app.notes();
417
+ } else {
418
+ const folder = findFolder(folderName);
419
+ if (!folder) {
420
+ return `Error: Folder '${folderName}' not found`;
421
+ }
422
+ notes = folder.notes();
423
+ }
424
+
425
+ if (notes.length === 0) {
426
+ let msg = "No notes found";
427
+ if (folderName !== "") {
428
+ msg += ` in '${folderName}' folder`;
429
+ }
430
+ return msg;
431
+ }
432
+
433
+ // Build array with dates for sorting
434
+ const notesWithDates = [];
435
+ for (let i = 0; i < notes.length; i++) {
436
+ const note = notes[i];
437
+ notesWithDates.push({
438
+ note: note,
439
+ modDate: note.modificationDate(),
440
+ });
441
+ }
442
+
443
+ // Sort by modification date descending
444
+ notesWithDates.sort((a, b) => b.modDate - a.modDate);
445
+
446
+ // Get top N
447
+ const count = Math.min(limit, notesWithDates.length);
448
+ let output = `Last ${count} note(s)`;
449
+ if (folderName !== "") {
450
+ output += ` from '${folderName}' folder`;
451
+ }
452
+ output += ":\n\n";
453
+
454
+ for (let i = 0; i < count; i++) {
455
+ const item = notesWithDates[i];
456
+ const note = item.note;
457
+ const body = note.body() || "";
458
+ const preview = getPreview(body, 100);
459
+ const folder = getFolderName(note) || folderName;
460
+
461
+ output += `[${i + 1}] ${note.name()}\n`;
462
+ output += ` Folder: ${folder}\n`;
463
+ output += ` Modified: ${item.modDate.toString()}\n`;
464
+ output += ` Preview: ${htmlToMarkdown(preview)}\n\n`;
465
+ }
466
+
467
+ return output;
468
+ }
469
+
470
+ function createNote(title, body, folderName) {
471
+ const app = getNotesApp();
472
+ const folder = findFolder(folderName);
473
+
474
+ if (!folder) {
475
+ return `Error: Folder '${folderName}' not found`;
476
+ }
477
+
478
+ const htmlBody = markdownToHtml(body);
479
+ const uniqueTitle = generateUniqueTitle(title, folder);
480
+
481
+ const newNote = app.Note({ name: uniqueTitle, body: htmlBody });
482
+ folder.notes.push(newNote);
483
+
484
+ // Get the created note to return info
485
+ const createdNote = folder.notes().find((n) => n.name() === uniqueTitle);
486
+ if (!createdNote) {
487
+ return `Note created but could not retrieve details.\nTitle: ${uniqueTitle}\nFolder: ${folderName}`;
488
+ }
489
+
490
+ let output = "Note created successfully!\n\n";
491
+ output += `Title: ${createdNote.name()}\n`;
492
+ output += `Folder: ${folderName}\n`;
493
+ output += `Created: ${createdNote.creationDate().toString()}\n`;
494
+
495
+ return output;
496
+ }
497
+
498
+ function deleteNote(title, folderName) {
499
+ const app = getNotesApp();
500
+ let targetNote = null;
501
+ let targetFolder = null;
502
+
503
+ if (folderName === "") {
504
+ // Search all notes for exact match
505
+ const allNotes = app.notes();
506
+ for (let i = 0; i < allNotes.length; i++) {
507
+ const note = allNotes[i];
508
+ if (note.name() === title) {
509
+ targetNote = note;
510
+ break;
511
+ }
512
+ }
513
+ } else {
514
+ targetFolder = findFolder(folderName);
515
+ if (!targetFolder) {
516
+ return `Error: Folder '${folderName}' not found`;
517
+ }
518
+ const folderNotes = targetFolder.notes();
519
+ for (let i = 0; i < folderNotes.length; i++) {
520
+ const note = folderNotes[i];
521
+ if (note.name() === title) {
522
+ targetNote = note;
523
+ break;
524
+ }
525
+ }
526
+ }
527
+
528
+ if (!targetNote) {
529
+ let errorMsg = `Error: No note found with exact title '${title}'`;
530
+ if (folderName !== "") {
531
+ errorMsg += ` in folder '${folderName}'`;
532
+ }
533
+ return errorMsg;
534
+ }
535
+
536
+ const deletedTitle = targetNote.name();
537
+ const deletedFolder = getFolderName(targetNote);
538
+
539
+ // Delete the note
540
+ app.delete(targetNote);
541
+
542
+ return `Note deleted successfully!\n\nTitle: ${deletedTitle}\nFolder: ${deletedFolder}`;
543
+ }
544
+
545
+ // ============================================================================
546
+ // Helper Functions
547
+ // ============================================================================
548
+
549
+ function findFolder(folderPath) {
550
+ const app = getNotesApp();
551
+ const defaultAccount = app.defaultAccount();
552
+
553
+ if (folderPath.includes("/")) {
554
+ // Nested folder path
555
+ const parts = folderPath.split("/");
556
+ let currentFolder = defaultAccount.folders().find((f) => f.name() === "Notes");
557
+
558
+ if (!currentFolder) {
559
+ return null;
560
+ }
561
+
562
+ for (const part of parts) {
563
+ const subfolders = currentFolder.folders();
564
+ let found = false;
565
+ for (let i = 0; i < subfolders.length; i++) {
566
+ if (subfolders[i].name() === part) {
567
+ currentFolder = subfolders[i];
568
+ found = true;
569
+ break;
570
+ }
571
+ }
572
+ if (!found) {
573
+ return null;
574
+ }
575
+ }
576
+
577
+ return currentFolder;
578
+ } else {
579
+ // Top-level folder
580
+ const folders = defaultAccount.folders();
581
+ for (let i = 0; i < folders.length; i++) {
582
+ if (folders[i].name() === folderPath) {
583
+ return folders[i];
584
+ }
585
+ }
586
+ return null;
587
+ }
588
+ }
589
+
590
+ function getFolderName(note) {
591
+ try {
592
+ const container = note.container();
593
+ if (container) {
594
+ return container.name();
595
+ }
596
+ } catch (e) {
597
+ // Container might not be accessible
598
+ }
599
+ return "";
600
+ }
601
+
602
+ function getPreview(body, maxLength) {
603
+ if (!body) return "";
604
+ const length = Math.min(maxLength, body.length);
605
+ let preview = body.substring(0, length);
606
+ if (body.length > length) {
607
+ preview += "...";
608
+ }
609
+ return preview;
610
+ }
611
+
612
+ function formatNoteContent(note) {
613
+ const title = note.name();
614
+ const body = note.body() || "";
615
+ const folder = getFolderName(note);
616
+ const created = note.creationDate().toString();
617
+ const modified = note.modificationDate().toString();
618
+
619
+ const markdownBody = htmlToMarkdown(body);
620
+
621
+ let output = "========================================\n";
622
+ output += `Title: ${title}\n`;
623
+ output += `Folder: ${folder}\n`;
624
+ output += `Created: ${created}\n`;
625
+ output += `Modified: ${modified}\n`;
626
+ output += "========================================\n\n";
627
+ output += markdownBody + "\n";
628
+
629
+ return output;
630
+ }
631
+
632
+ function generateUniqueTitle(baseTitle, folder) {
633
+ const notes = folder.notes();
634
+ const existingTitles = new Set();
635
+
636
+ for (let i = 0; i < notes.length; i++) {
637
+ existingTitles.add(notes[i].name());
638
+ }
639
+
640
+ if (!existingTitles.has(baseTitle)) {
641
+ return baseTitle;
642
+ }
643
+
644
+ let suffix = 2;
645
+ while (true) {
646
+ const candidate = `${baseTitle} (${suffix})`;
647
+ if (!existingTitles.has(candidate)) {
648
+ return candidate;
649
+ }
650
+ suffix++;
651
+ }
652
+ }
653
+
654
+ function getUsage() {
655
+ return `Usage:
656
+ apple-notes search <query> - Search notes by content
657
+ apple-notes list <query> - List notes by title
658
+ apple-notes read <title> [folder] - Read note by title
659
+ apple-notes read-index <query> <index> - Read by search index
660
+ apple-notes recent [count] [folder] - Get recent notes (default: 5)
661
+ apple-notes create <title> <body> [folder] - Create note from markdown
662
+ apple-notes delete <title> [folder] - Delete note by title
663
+
664
+ Examples:
665
+ apple-notes list 'meeting'
666
+ apple-notes read-index 'meeting' 2
667
+ apple-notes read 'Todo' 'Work'
668
+ apple-notes recent 10 'Blog'
669
+ apple-notes create 'Meeting Notes' '# Agenda\\n- Item 1' 'Work'
670
+ apple-notes delete 'Old Note' 'Archive'
671
+ `;
672
+ }