@peerasak-u/apple-notes 1.0.2 → 1.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/jxa/notes.js +141 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerasak-u/apple-notes",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "CLI tool for interacting with Apple Notes on macOS",
5
5
  "type": "module",
6
6
  "bin": {
package/src/jxa/notes.js CHANGED
@@ -11,44 +11,55 @@ function run(argv) {
11
11
  const command = argv[0];
12
12
 
13
13
  switch (command) {
14
- case "search":
14
+ case 'search':
15
15
  if (argv.length < 2) {
16
- return "Error: search requires a query string\n" + getUsage();
16
+ return 'Error: search requires a query string\n' + getUsage();
17
17
  }
18
18
  return searchNotes(argv[1]);
19
19
 
20
- case "list":
20
+ case 'list':
21
21
  if (argv.length < 2) {
22
- return "Error: list requires a search term\n" + getUsage();
22
+ return 'Error: list requires a search term\n' + getUsage();
23
23
  }
24
24
  return listNotes(argv[1]);
25
25
 
26
- case "read":
26
+ case 'read':
27
27
  if (argv.length < 2) {
28
- return "Error: read requires a note identifier\n" + getUsage();
28
+ return 'Error: read requires a note identifier\n' + getUsage();
29
29
  }
30
- return readNote(argv[1], argv[2] || "");
30
+ return readNote(argv[1], argv[2] || '');
31
31
 
32
- case "read-index":
32
+ case 'read-index':
33
33
  if (argv.length < 3) {
34
- return "Error: read-index requires search term and index\n" + getUsage();
34
+ return (
35
+ 'Error: read-index requires search term and index\n' + getUsage()
36
+ );
35
37
  }
36
38
  return readNoteByIndex(argv[1], parseInt(argv[2], 10));
37
39
 
38
- case "recent":
40
+ case 'recent':
39
41
  return parseRecentArgs(argv.slice(1));
40
42
 
41
- case "create":
43
+ case 'create':
42
44
  if (argv.length < 3) {
43
- return "Error: create requires title and body\n" + getUsage();
45
+ return 'Error: create requires title and body\n' + getUsage();
44
46
  }
45
- return createNote(argv[1], argv[2], argv[3] || "Notes");
47
+ return createNote(argv[1], argv[2], argv[3] || 'Notes');
46
48
 
47
- case "delete":
49
+ case 'delete':
48
50
  if (argv.length < 2) {
49
- return "Error: delete requires a note title\n" + getUsage();
51
+ return 'Error: delete requires a note title\n' + getUsage();
50
52
  }
51
- return deleteNote(argv[1], argv[2] || "");
53
+ return deleteNote(argv[1], argv[2] || '');
54
+
55
+ case 'move':
56
+ if (argv.length < 3) {
57
+ return (
58
+ 'Error: move requires a note title and destination folder\n' +
59
+ getUsage()
60
+ );
61
+ }
62
+ return moveNote(argv[1], argv[2], argv[3] || '');
52
63
 
53
64
  default:
54
65
  return `Error: Unknown command '${command}'\n` + getUsage();
@@ -63,7 +74,7 @@ function run(argv) {
63
74
  // JXA now passes raw HTML to/from Apple Notes
64
75
 
65
76
  function getNotesApp() {
66
- return Application("Notes");
77
+ return Application('Notes');
67
78
  }
68
79
 
69
80
  function searchNotes(query) {
@@ -96,7 +107,7 @@ function searchNotes(query) {
96
107
  output += `Folder: ${r.folder}\n`;
97
108
  output += `Modified: ${r.modified}\n`;
98
109
  output += `Preview: ${r.preview}\n`;
99
- output += "---\n\n";
110
+ output += '---\n\n';
100
111
  }
101
112
 
102
113
  return output;
@@ -112,7 +123,7 @@ function listNotes(query) {
112
123
  const name = note.name();
113
124
  if (name && name.toLowerCase().includes(query.toLowerCase())) {
114
125
  const folderName = getFolderName(note);
115
- const body = note.body() || "";
126
+ const body = note.body() || '';
116
127
  const preview = getPreview(body, 80);
117
128
  results.push({
118
129
  index: results.length + 1,
@@ -145,7 +156,7 @@ function readNote(identifier, folderName) {
145
156
  const app = getNotesApp();
146
157
  let matchingNotes = [];
147
158
 
148
- if (folderName === "") {
159
+ if (folderName === '') {
149
160
  // Search all notes
150
161
  const allNotes = app.notes();
151
162
  for (let i = 0; i < allNotes.length; i++) {
@@ -192,7 +203,7 @@ function readNote(identifier, folderName) {
192
203
 
193
204
  if (matchingNotes.length === 0) {
194
205
  let errorMsg = `Error: No note found matching '${identifier}'`;
195
- if (folderName !== "") {
206
+ if (folderName !== '') {
196
207
  errorMsg += ` in folder '${folderName}'`;
197
208
  }
198
209
  return errorMsg;
@@ -239,7 +250,7 @@ function readNoteByIndex(query, index) {
239
250
 
240
251
  function parseRecentArgs(args) {
241
252
  let limit = 5;
242
- let folderName = "";
253
+ let folderName = '';
243
254
 
244
255
  if (args.length >= 1) {
245
256
  const arg1 = args[0];
@@ -267,7 +278,7 @@ function getRecentNotes(limit, folderName) {
267
278
  const app = getNotesApp();
268
279
  let notes = [];
269
280
 
270
- if (folderName === "") {
281
+ if (folderName === '') {
271
282
  notes = app.notes();
272
283
  } else {
273
284
  const folder = findFolder(folderName);
@@ -278,8 +289,8 @@ function getRecentNotes(limit, folderName) {
278
289
  }
279
290
 
280
291
  if (notes.length === 0) {
281
- let msg = "No notes found";
282
- if (folderName !== "") {
292
+ let msg = 'No notes found';
293
+ if (folderName !== '') {
283
294
  msg += ` in '${folderName}' folder`;
284
295
  }
285
296
  return msg;
@@ -301,15 +312,15 @@ function getRecentNotes(limit, folderName) {
301
312
  // Get top N
302
313
  const count = Math.min(limit, notesWithDates.length);
303
314
  let output = `Last ${count} note(s)`;
304
- if (folderName !== "") {
315
+ if (folderName !== '') {
305
316
  output += ` from '${folderName}' folder`;
306
317
  }
307
- output += ":\n\n";
318
+ output += ':\n\n';
308
319
 
309
320
  for (let i = 0; i < count; i++) {
310
321
  const item = notesWithDates[i];
311
322
  const note = item.note;
312
- const body = note.body() || "";
323
+ const body = note.body() || '';
313
324
  const preview = getPreview(body, 100);
314
325
  const folder = getFolderName(note) || folderName;
315
326
 
@@ -337,12 +348,12 @@ function createNote(title, body, folderName) {
337
348
  folder.notes.push(newNote);
338
349
 
339
350
  // Get the created note to return info
340
- const createdNote = folder.notes().find((n) => n.name() === uniqueTitle);
351
+ const createdNote = folder.notes().find(n => n.name() === uniqueTitle);
341
352
  if (!createdNote) {
342
353
  return `Note created but could not retrieve details.\nTitle: ${uniqueTitle}\nFolder: ${folderName}`;
343
354
  }
344
355
 
345
- let output = "Note created successfully!\n\n";
356
+ let output = 'Note created successfully!\n\n';
346
357
  output += `Title: ${createdNote.name()}\n`;
347
358
  output += `Folder: ${folderName}\n`;
348
359
  output += `Created: ${createdNote.creationDate().toString()}\n`;
@@ -355,7 +366,7 @@ function deleteNote(title, folderName) {
355
366
  let targetNote = null;
356
367
  let targetFolder = null;
357
368
 
358
- if (folderName === "") {
369
+ if (folderName === '') {
359
370
  // Search all notes for exact match
360
371
  const allNotes = app.notes();
361
372
  for (let i = 0; i < allNotes.length; i++) {
@@ -382,7 +393,7 @@ function deleteNote(title, folderName) {
382
393
 
383
394
  if (!targetNote) {
384
395
  let errorMsg = `Error: No note found with exact title '${title}'`;
385
- if (folderName !== "") {
396
+ if (folderName !== '') {
386
397
  errorMsg += ` in folder '${folderName}'`;
387
398
  }
388
399
  return errorMsg;
@@ -397,6 +408,78 @@ function deleteNote(title, folderName) {
397
408
  return `Note deleted successfully!\n\nTitle: ${deletedTitle}\nFolder: ${deletedFolder}`;
398
409
  }
399
410
 
411
+ function moveNote(title, destFolderName, sourceFolderName) {
412
+ const app = getNotesApp();
413
+ const destFolder = findFolder(destFolderName);
414
+
415
+ if (!destFolder) {
416
+ return `Error: Destination folder '${destFolderName}' not found`;
417
+ }
418
+
419
+ let targetNote = null;
420
+ let sourceFolder = null;
421
+
422
+ if (sourceFolderName && sourceFolderName !== '') {
423
+ sourceFolder = findFolder(sourceFolderName);
424
+ if (!sourceFolder) {
425
+ return `Error: Source folder '${sourceFolderName}' not found`;
426
+ }
427
+ const folderNotes = sourceFolder.notes();
428
+ for (let i = 0; i < folderNotes.length; i++) {
429
+ const note = folderNotes[i];
430
+ if (note.name() === title) {
431
+ targetNote = note;
432
+ break;
433
+ }
434
+ }
435
+ } else {
436
+ // Global search
437
+ // We need to check for ambiguity
438
+ const allNotes = app.notes();
439
+ const matches = [];
440
+ for (let i = 0; i < allNotes.length; i++) {
441
+ const note = allNotes[i];
442
+ if (note.name() === title) {
443
+ matches.push(note);
444
+ }
445
+ }
446
+
447
+ if (matches.length === 0) {
448
+ return `Error: No note found with exact title '${title}'`;
449
+ } else if (matches.length > 1) {
450
+ let msg = `Error: Multiple notes found with title '${title}'. Please specify the source folder:\n\n`;
451
+ for (let i = 0; i < matches.length; i++) {
452
+ const note = matches[i];
453
+ const folder = getFolderName(note);
454
+ msg += `- ${title} (in '${folder}')\n`;
455
+ }
456
+ return msg;
457
+ } else {
458
+ targetNote = matches[0];
459
+ }
460
+ }
461
+
462
+ if (!targetNote) {
463
+ let errorMsg = `Error: No note found with exact title '${title}'`;
464
+ if (sourceFolderName) {
465
+ errorMsg += ` in folder '${sourceFolderName}'`;
466
+ }
467
+ return errorMsg;
468
+ }
469
+
470
+ const oldFolderName = getFolderName(targetNote);
471
+
472
+ // If already in destination, do nothing
473
+ if (oldFolderName === destFolder.name()) {
474
+ return `Note '${title}' is already in '${destFolderName}'`;
475
+ }
476
+
477
+ // Execute move
478
+ app.move(targetNote, { to: destFolder });
479
+
480
+ return `Note moved successfully!\n\nTitle: ${title}\nFrom: ${oldFolderName}\nTo: ${destFolder.name()}`;
481
+ }
482
+
400
483
  // ============================================================================
401
484
  // Helper Functions
402
485
  // ============================================================================
@@ -405,21 +488,31 @@ function findFolder(folderPath) {
405
488
  const app = getNotesApp();
406
489
  const defaultAccount = app.defaultAccount();
407
490
 
408
- if (folderPath.includes("/")) {
491
+ if (folderPath.includes('/')) {
409
492
  // Nested folder path
410
- const parts = folderPath.split("/");
411
- let currentFolder = defaultAccount.folders().find((f) => f.name() === "Notes");
493
+ const parts = folderPath.split('/');
494
+ let currentFolder = null;
495
+
496
+ // Find the first folder at the account level
497
+ const topLevelFolders = defaultAccount.folders();
498
+ for (let i = 0; i < topLevelFolders.length; i++) {
499
+ if (topLevelFolders[i].name() === parts[0]) {
500
+ currentFolder = topLevelFolders[i];
501
+ break;
502
+ }
503
+ }
412
504
 
413
505
  if (!currentFolder) {
414
506
  return null;
415
507
  }
416
508
 
417
- for (const part of parts) {
509
+ // Traverse the rest of the path
510
+ for (let i = 1; i < parts.length; i++) {
418
511
  const subfolders = currentFolder.folders();
419
512
  let found = false;
420
- for (let i = 0; i < subfolders.length; i++) {
421
- if (subfolders[i].name() === part) {
422
- currentFolder = subfolders[i];
513
+ for (let j = 0; j < subfolders.length; j++) {
514
+ if (subfolders[j].name() === parts[i]) {
515
+ currentFolder = subfolders[j];
423
516
  found = true;
424
517
  break;
425
518
  }
@@ -451,33 +544,33 @@ function getFolderName(note) {
451
544
  } catch (e) {
452
545
  // Container might not be accessible
453
546
  }
454
- return "";
547
+ return '';
455
548
  }
456
549
 
457
550
  function getPreview(body, maxLength) {
458
- if (!body) return "";
551
+ if (!body) return '';
459
552
  const length = Math.min(maxLength, body.length);
460
553
  let preview = body.substring(0, length);
461
554
  if (body.length > length) {
462
- preview += "...";
555
+ preview += '...';
463
556
  }
464
557
  return preview;
465
558
  }
466
559
 
467
560
  function formatNoteContent(note) {
468
561
  const title = note.name();
469
- const body = note.body() || "";
562
+ const body = note.body() || '';
470
563
  const folder = getFolderName(note);
471
564
  const created = note.creationDate().toString();
472
565
  const modified = note.modificationDate().toString();
473
566
 
474
- let output = "========================================\n";
567
+ let output = '========================================\n';
475
568
  output += `Title: ${title}\n`;
476
569
  output += `Folder: ${folder}\n`;
477
570
  output += `Created: ${created}\n`;
478
571
  output += `Modified: ${modified}\n`;
479
- output += "========================================\n\n";
480
- output += body + "\n";
572
+ output += '========================================\n\n';
573
+ output += body + '\n';
481
574
 
482
575
  return output;
483
576
  }
@@ -513,6 +606,7 @@ function getUsage() {
513
606
  apple-notes recent [count] [folder] - Get recent notes (default: 5)
514
607
  apple-notes create <title> <body> [folder] - Create note from markdown
515
608
  apple-notes delete <title> [folder] - Delete note by title
609
+ apple-notes move <title> <destination> [source] - Move note to folder
516
610
 
517
611
  Examples:
518
612
  apple-notes list 'meeting'
@@ -521,5 +615,6 @@ Examples:
521
615
  apple-notes recent 10 'Blog'
522
616
  apple-notes create 'Meeting Notes' '# Agenda\\n- Item 1' 'Work'
523
617
  apple-notes delete 'Old Note' 'Archive'
618
+ apple-notes move 'Idea' 'Projects' 'Inbox'
524
619
  `;
525
620
  }