@peerasak-u/apple-notes 1.0.1 → 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.
package/dist/index.js CHANGED
@@ -4,8 +4,129 @@
4
4
  import { execSync } from "child_process";
5
5
  import { join, dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
+
8
+ // src/converter.ts
9
+ function htmlToMarkdown(html) {
10
+ if (!html)
11
+ return "";
12
+ let md = html;
13
+ md = md.replace(/<div[^>]*>/gi, `
14
+ `);
15
+ md = md.replace(/<\/div>/gi, "");
16
+ md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, `# $1
17
+ `);
18
+ md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, `## $1
19
+ `);
20
+ md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, `### $1
21
+ `);
22
+ md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, `#### $1
23
+ `);
24
+ md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, `##### $1
25
+ `);
26
+ md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, `###### $1
27
+ `);
28
+ md = md.replace(/<(b|strong)[^>]*>(.*?)<\/(b|strong)>/gi, "**$2**");
29
+ md = md.replace(/<(i|em)[^>]*>(.*?)<\/(i|em)>/gi, "*$2*");
30
+ md = md.replace(/<u[^>]*>(.*?)<\/u>/gi, "_$1_");
31
+ md = md.replace(/<s[^>]*>(.*?)<\/s>/gi, "~~$1~~");
32
+ md = md.replace(/<strike[^>]*>(.*?)<\/strike>/gi, "~~$1~~");
33
+ md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, "[$2]($1)");
34
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, "![$2]($1)");
35
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*\/?>/gi, "![]($1)");
36
+ md = md.replace(/<ul[^>]*>/gi, `
37
+ `);
38
+ md = md.replace(/<\/ul>/gi, `
39
+ `);
40
+ md = md.replace(/<ol[^>]*>/gi, `
41
+ `);
42
+ md = md.replace(/<\/ol>/gi, `
43
+ `);
44
+ md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, `- $1
45
+ `);
46
+ md = md.replace(/<br\s*\/?>/gi, `
47
+ `);
48
+ md = md.replace(/<p[^>]*>/gi, `
49
+ `);
50
+ md = md.replace(/<\/p>/gi, `
51
+ `);
52
+ md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
53
+ md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "```\n$1\n```\n");
54
+ md = md.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, `> $1
55
+ `);
56
+ md = md.replace(/<hr\s*\/?>/gi, `
57
+ ---
58
+ `);
59
+ md = md.replace(/<[^>]+>/g, "");
60
+ md = md.replace(/&nbsp;/g, " ");
61
+ md = md.replace(/&amp;/g, "&");
62
+ md = md.replace(/&lt;/g, "<");
63
+ md = md.replace(/&gt;/g, ">");
64
+ md = md.replace(/&quot;/g, '"');
65
+ md = md.replace(/&#39;/g, "'");
66
+ md = md.replace(/&apos;/g, "'");
67
+ md = md.replace(/\n{3,}/g, `
68
+
69
+ `);
70
+ md = md.trim();
71
+ if (/<[a-z][\s\S]*>/i.test(md)) {
72
+ return `[RAW_HTML]
73
+ ` + html;
74
+ }
75
+ return md;
76
+ }
77
+ function markdownToHtml(markdown) {
78
+ if (!markdown)
79
+ return "";
80
+ let html = markdown;
81
+ html = html.replace(/\\n/g, `
82
+ `);
83
+ html = html.replace(/\\t/g, "\t");
84
+ html = html.replace(/^###### (.+)$/gm, "<h6>$1</h6>");
85
+ html = html.replace(/^##### (.+)$/gm, "<h5>$1</h5>");
86
+ html = html.replace(/^#### (.+)$/gm, "<h4>$1</h4>");
87
+ html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
88
+ html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
89
+ html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
90
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<b><i>$1</i></b>");
91
+ html = html.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
92
+ html = html.replace(/\*(.+?)\*/g, "<i>$1</i>");
93
+ html = html.replace(/_(.+?)_/g, "<i>$1</i>");
94
+ html = html.replace(/~~(.+?)~~/g, "<s>$1</s>");
95
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
96
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
97
+ html = html.replace(/```([\s\S]*?)```/g, "<pre>$1</pre>");
98
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
99
+ html = html.replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>");
100
+ html = html.replace(/^---$/gm, "<hr>");
101
+ html = html.replace(/^\*\*\*$/gm, "<hr>");
102
+ html = html.replace(/^- (.+)$/gm, "<li>$1</li>");
103
+ html = html.replace(/^\* (.+)$/gm, "<li>$1</li>");
104
+ html = html.replace(/^\d+\. (.+)$/gm, "<li>$1</li>");
105
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => {
106
+ return "<ul>" + match + "</ul>";
107
+ });
108
+ const lines = html.split(`
109
+ `);
110
+ const result = [];
111
+ for (let i = 0;i < lines.length; i++) {
112
+ const line = lines[i].trim();
113
+ if (line === "") {
114
+ result.push("<br>");
115
+ } else if (!/^</.test(line)) {
116
+ result.push("<div>" + line + "</div>");
117
+ } else {
118
+ result.push(line);
119
+ }
120
+ }
121
+ html = result.join(`
122
+ `);
123
+ return html;
124
+ }
125
+
126
+ // src/executor.ts
7
127
  var __filename2 = fileURLToPath(import.meta.url);
8
128
  var __dirname2 = dirname(__filename2);
129
+ var HTML_OUTPUT_COMMANDS = new Set(["search", "list", "read", "read-index", "recent"]);
9
130
  function getNotesScriptPath() {
10
131
  return join(__dirname2, "..", "src", "jxa", "notes.js");
11
132
  }
@@ -13,16 +134,29 @@ function escapeShellArg(arg) {
13
134
  return `'${arg.replace(/'/g, "'\\''")}'`;
14
135
  }
15
136
  function executeNotesCommand(args) {
137
+ const command = args[0];
138
+ let processedArgs = [...args];
139
+ if (command === "create" && args.length >= 3) {
140
+ const htmlBody = markdownToHtml(args[2]);
141
+ processedArgs = [args[0], args[1], htmlBody];
142
+ if (args[3]) {
143
+ processedArgs.push(args[3]);
144
+ }
145
+ }
16
146
  const scriptPath = getNotesScriptPath();
17
- const escapedArgs = args.map(escapeShellArg).join(" ");
18
- const command = `osascript -l JavaScript "${scriptPath}" ${escapedArgs}`;
147
+ const escapedArgs = processedArgs.map(escapeShellArg).join(" ");
148
+ const shellCommand = `osascript -l JavaScript "${scriptPath}" ${escapedArgs}`;
19
149
  const options = {
20
150
  encoding: "utf-8",
21
151
  maxBuffer: 10 * 1024 * 1024
22
152
  };
23
153
  try {
24
- const result = execSync(command, options);
25
- return result.trim();
154
+ const result = execSync(shellCommand, options);
155
+ const output = result.trim();
156
+ if (HTML_OUTPUT_COMMANDS.has(command)) {
157
+ return htmlToMarkdown(output);
158
+ }
159
+ return output;
26
160
  } catch (error) {
27
161
  if (error instanceof Error && "stderr" in error) {
28
162
  const stderr = error.stderr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerasak-u/apple-notes",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "CLI tool for interacting with Apple Notes on macOS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,9 +12,8 @@
12
12
  ],
13
13
  "scripts": {
14
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",
15
+ "test": "bun test tests/*.test.js",
16
+ "test:coverage": "bun test --coverage tests/*.test.js",
18
17
  "lint": "eslint tests/*.js",
19
18
  "lint:fix": "eslint --fix tests/*.js",
20
19
  "typecheck": "tsc --noEmit"
package/src/jxa/notes.js CHANGED
@@ -11,204 +11,70 @@ 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();
55
66
  }
56
67
  }
57
68
 
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
69
  // ============================================================================
207
70
  // Notes App Interaction
208
71
  // ============================================================================
209
72
 
73
+ // HTML <-> Markdown conversion is now handled in TypeScript (converter.ts)
74
+ // JXA now passes raw HTML to/from Apple Notes
75
+
210
76
  function getNotesApp() {
211
- return Application("Notes");
77
+ return Application('Notes');
212
78
  }
213
79
 
214
80
  function searchNotes(query) {
@@ -226,7 +92,7 @@ function searchNotes(query) {
226
92
  title: note.name(),
227
93
  folder: folderName,
228
94
  modified: note.modificationDate().toString(),
229
- preview: htmlToMarkdown(preview),
95
+ preview: preview,
230
96
  });
231
97
  }
232
98
  }
@@ -241,7 +107,7 @@ function searchNotes(query) {
241
107
  output += `Folder: ${r.folder}\n`;
242
108
  output += `Modified: ${r.modified}\n`;
243
109
  output += `Preview: ${r.preview}\n`;
244
- output += "---\n\n";
110
+ output += '---\n\n';
245
111
  }
246
112
 
247
113
  return output;
@@ -257,14 +123,14 @@ function listNotes(query) {
257
123
  const name = note.name();
258
124
  if (name && name.toLowerCase().includes(query.toLowerCase())) {
259
125
  const folderName = getFolderName(note);
260
- const body = note.body() || "";
126
+ const body = note.body() || '';
261
127
  const preview = getPreview(body, 80);
262
128
  results.push({
263
129
  index: results.length + 1,
264
130
  title: name,
265
131
  folder: folderName,
266
132
  modified: note.modificationDate().toString(),
267
- preview: htmlToMarkdown(preview),
133
+ preview: preview,
268
134
  });
269
135
  }
270
136
  }
@@ -290,7 +156,7 @@ function readNote(identifier, folderName) {
290
156
  const app = getNotesApp();
291
157
  let matchingNotes = [];
292
158
 
293
- if (folderName === "") {
159
+ if (folderName === '') {
294
160
  // Search all notes
295
161
  const allNotes = app.notes();
296
162
  for (let i = 0; i < allNotes.length; i++) {
@@ -337,7 +203,7 @@ function readNote(identifier, folderName) {
337
203
 
338
204
  if (matchingNotes.length === 0) {
339
205
  let errorMsg = `Error: No note found matching '${identifier}'`;
340
- if (folderName !== "") {
206
+ if (folderName !== '') {
341
207
  errorMsg += ` in folder '${folderName}'`;
342
208
  }
343
209
  return errorMsg;
@@ -384,7 +250,7 @@ function readNoteByIndex(query, index) {
384
250
 
385
251
  function parseRecentArgs(args) {
386
252
  let limit = 5;
387
- let folderName = "";
253
+ let folderName = '';
388
254
 
389
255
  if (args.length >= 1) {
390
256
  const arg1 = args[0];
@@ -412,7 +278,7 @@ function getRecentNotes(limit, folderName) {
412
278
  const app = getNotesApp();
413
279
  let notes = [];
414
280
 
415
- if (folderName === "") {
281
+ if (folderName === '') {
416
282
  notes = app.notes();
417
283
  } else {
418
284
  const folder = findFolder(folderName);
@@ -423,8 +289,8 @@ function getRecentNotes(limit, folderName) {
423
289
  }
424
290
 
425
291
  if (notes.length === 0) {
426
- let msg = "No notes found";
427
- if (folderName !== "") {
292
+ let msg = 'No notes found';
293
+ if (folderName !== '') {
428
294
  msg += ` in '${folderName}' folder`;
429
295
  }
430
296
  return msg;
@@ -446,22 +312,22 @@ function getRecentNotes(limit, folderName) {
446
312
  // Get top N
447
313
  const count = Math.min(limit, notesWithDates.length);
448
314
  let output = `Last ${count} note(s)`;
449
- if (folderName !== "") {
315
+ if (folderName !== '') {
450
316
  output += ` from '${folderName}' folder`;
451
317
  }
452
- output += ":\n\n";
318
+ output += ':\n\n';
453
319
 
454
320
  for (let i = 0; i < count; i++) {
455
321
  const item = notesWithDates[i];
456
322
  const note = item.note;
457
- const body = note.body() || "";
323
+ const body = note.body() || '';
458
324
  const preview = getPreview(body, 100);
459
325
  const folder = getFolderName(note) || folderName;
460
326
 
461
327
  output += `[${i + 1}] ${note.name()}\n`;
462
328
  output += ` Folder: ${folder}\n`;
463
329
  output += ` Modified: ${item.modDate.toString()}\n`;
464
- output += ` Preview: ${htmlToMarkdown(preview)}\n\n`;
330
+ output += ` Preview: ${preview}\n\n`;
465
331
  }
466
332
 
467
333
  return output;
@@ -475,19 +341,19 @@ function createNote(title, body, folderName) {
475
341
  return `Error: Folder '${folderName}' not found`;
476
342
  }
477
343
 
478
- const htmlBody = markdownToHtml(body);
344
+ // body is now pre-converted to HTML in TypeScript
479
345
  const uniqueTitle = generateUniqueTitle(title, folder);
480
346
 
481
- const newNote = app.Note({ name: uniqueTitle, body: htmlBody });
347
+ const newNote = app.Note({ name: uniqueTitle, body: body });
482
348
  folder.notes.push(newNote);
483
349
 
484
350
  // Get the created note to return info
485
- const createdNote = folder.notes().find((n) => n.name() === uniqueTitle);
351
+ const createdNote = folder.notes().find(n => n.name() === uniqueTitle);
486
352
  if (!createdNote) {
487
353
  return `Note created but could not retrieve details.\nTitle: ${uniqueTitle}\nFolder: ${folderName}`;
488
354
  }
489
355
 
490
- let output = "Note created successfully!\n\n";
356
+ let output = 'Note created successfully!\n\n';
491
357
  output += `Title: ${createdNote.name()}\n`;
492
358
  output += `Folder: ${folderName}\n`;
493
359
  output += `Created: ${createdNote.creationDate().toString()}\n`;
@@ -500,7 +366,7 @@ function deleteNote(title, folderName) {
500
366
  let targetNote = null;
501
367
  let targetFolder = null;
502
368
 
503
- if (folderName === "") {
369
+ if (folderName === '') {
504
370
  // Search all notes for exact match
505
371
  const allNotes = app.notes();
506
372
  for (let i = 0; i < allNotes.length; i++) {
@@ -527,7 +393,7 @@ function deleteNote(title, folderName) {
527
393
 
528
394
  if (!targetNote) {
529
395
  let errorMsg = `Error: No note found with exact title '${title}'`;
530
- if (folderName !== "") {
396
+ if (folderName !== '') {
531
397
  errorMsg += ` in folder '${folderName}'`;
532
398
  }
533
399
  return errorMsg;
@@ -542,6 +408,78 @@ function deleteNote(title, folderName) {
542
408
  return `Note deleted successfully!\n\nTitle: ${deletedTitle}\nFolder: ${deletedFolder}`;
543
409
  }
544
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
+
545
483
  // ============================================================================
546
484
  // Helper Functions
547
485
  // ============================================================================
@@ -550,21 +488,31 @@ function findFolder(folderPath) {
550
488
  const app = getNotesApp();
551
489
  const defaultAccount = app.defaultAccount();
552
490
 
553
- if (folderPath.includes("/")) {
491
+ if (folderPath.includes('/')) {
554
492
  // Nested folder path
555
- const parts = folderPath.split("/");
556
- 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
+ }
557
504
 
558
505
  if (!currentFolder) {
559
506
  return null;
560
507
  }
561
508
 
562
- for (const part of parts) {
509
+ // Traverse the rest of the path
510
+ for (let i = 1; i < parts.length; i++) {
563
511
  const subfolders = currentFolder.folders();
564
512
  let found = false;
565
- for (let i = 0; i < subfolders.length; i++) {
566
- if (subfolders[i].name() === part) {
567
- currentFolder = subfolders[i];
513
+ for (let j = 0; j < subfolders.length; j++) {
514
+ if (subfolders[j].name() === parts[i]) {
515
+ currentFolder = subfolders[j];
568
516
  found = true;
569
517
  break;
570
518
  }
@@ -596,35 +544,33 @@ function getFolderName(note) {
596
544
  } catch (e) {
597
545
  // Container might not be accessible
598
546
  }
599
- return "";
547
+ return '';
600
548
  }
601
549
 
602
550
  function getPreview(body, maxLength) {
603
- if (!body) return "";
551
+ if (!body) return '';
604
552
  const length = Math.min(maxLength, body.length);
605
553
  let preview = body.substring(0, length);
606
554
  if (body.length > length) {
607
- preview += "...";
555
+ preview += '...';
608
556
  }
609
557
  return preview;
610
558
  }
611
559
 
612
560
  function formatNoteContent(note) {
613
561
  const title = note.name();
614
- const body = note.body() || "";
562
+ const body = note.body() || '';
615
563
  const folder = getFolderName(note);
616
564
  const created = note.creationDate().toString();
617
565
  const modified = note.modificationDate().toString();
618
566
 
619
- const markdownBody = htmlToMarkdown(body);
620
-
621
- let output = "========================================\n";
567
+ let output = '========================================\n';
622
568
  output += `Title: ${title}\n`;
623
569
  output += `Folder: ${folder}\n`;
624
570
  output += `Created: ${created}\n`;
625
571
  output += `Modified: ${modified}\n`;
626
- output += "========================================\n\n";
627
- output += markdownBody + "\n";
572
+ output += '========================================\n\n';
573
+ output += body + '\n';
628
574
 
629
575
  return output;
630
576
  }
@@ -660,6 +606,7 @@ function getUsage() {
660
606
  apple-notes recent [count] [folder] - Get recent notes (default: 5)
661
607
  apple-notes create <title> <body> [folder] - Create note from markdown
662
608
  apple-notes delete <title> [folder] - Delete note by title
609
+ apple-notes move <title> <destination> [source] - Move note to folder
663
610
 
664
611
  Examples:
665
612
  apple-notes list 'meeting'
@@ -668,5 +615,6 @@ Examples:
668
615
  apple-notes recent 10 'Blog'
669
616
  apple-notes create 'Meeting Notes' '# Agenda\\n- Item 1' 'Work'
670
617
  apple-notes delete 'Old Note' 'Archive'
618
+ apple-notes move 'Idea' 'Projects' 'Inbox'
671
619
  `;
672
620
  }