@noteplanco/noteplan-mcp 1.1.23 → 1.1.24

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 (164) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +6 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/noteplan/attachments-paths.d.ts +13 -0
  5. package/dist/noteplan/attachments-paths.d.ts.map +1 -0
  6. package/dist/noteplan/attachments-paths.js +27 -0
  7. package/dist/noteplan/attachments-paths.js.map +1 -0
  8. package/dist/noteplan/embeddings.js +1 -1
  9. package/dist/noteplan/embeddings.js.map +1 -1
  10. package/dist/noteplan/file-reader.d.ts +37 -46
  11. package/dist/noteplan/file-reader.d.ts.map +1 -1
  12. package/dist/noteplan/file-reader.js +200 -202
  13. package/dist/noteplan/file-reader.js.map +1 -1
  14. package/dist/noteplan/file-reader.test.d.ts +2 -0
  15. package/dist/noteplan/file-reader.test.d.ts.map +1 -0
  16. package/dist/noteplan/file-reader.test.js +67 -0
  17. package/dist/noteplan/file-reader.test.js.map +1 -0
  18. package/dist/noteplan/file-writer.d.ts +35 -31
  19. package/dist/noteplan/file-writer.d.ts.map +1 -1
  20. package/dist/noteplan/file-writer.js +280 -164
  21. package/dist/noteplan/file-writer.js.map +1 -1
  22. package/dist/noteplan/file-writer.test.js +704 -191
  23. package/dist/noteplan/file-writer.test.js.map +1 -1
  24. package/dist/noteplan/filter-store.d.ts +5 -5
  25. package/dist/noteplan/filter-store.d.ts.map +1 -1
  26. package/dist/noteplan/filter-store.js +94 -79
  27. package/dist/noteplan/filter-store.js.map +1 -1
  28. package/dist/noteplan/ripgrep-search.d.ts +25 -2
  29. package/dist/noteplan/ripgrep-search.d.ts.map +1 -1
  30. package/dist/noteplan/ripgrep-search.js +75 -2
  31. package/dist/noteplan/ripgrep-search.js.map +1 -1
  32. package/dist/noteplan/space-row-utils.d.ts +20 -0
  33. package/dist/noteplan/space-row-utils.d.ts.map +1 -0
  34. package/dist/noteplan/space-row-utils.js +78 -0
  35. package/dist/noteplan/space-row-utils.js.map +1 -0
  36. package/dist/noteplan/space-row-utils.test.d.ts +2 -0
  37. package/dist/noteplan/space-row-utils.test.d.ts.map +1 -0
  38. package/dist/noteplan/space-row-utils.test.js +123 -0
  39. package/dist/noteplan/space-row-utils.test.js.map +1 -0
  40. package/dist/noteplan/sqlite-reader.d.ts +12 -27
  41. package/dist/noteplan/sqlite-reader.d.ts.map +1 -1
  42. package/dist/noteplan/sqlite-reader.js +315 -221
  43. package/dist/noteplan/sqlite-reader.js.map +1 -1
  44. package/dist/noteplan/sqlite-writer.d.ts +1 -1
  45. package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
  46. package/dist/noteplan/sqlite-writer.js +2 -2
  47. package/dist/noteplan/sqlite-writer.js.map +1 -1
  48. package/dist/noteplan/unified-store.d.ts +41 -30
  49. package/dist/noteplan/unified-store.d.ts.map +1 -1
  50. package/dist/noteplan/unified-store.js +257 -159
  51. package/dist/noteplan/unified-store.js.map +1 -1
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +142 -61
  54. package/dist/server.js.map +1 -1
  55. package/dist/tools/attachments.d.ts +9 -9
  56. package/dist/tools/attachments.d.ts.map +1 -1
  57. package/dist/tools/attachments.js +74 -83
  58. package/dist/tools/attachments.js.map +1 -1
  59. package/dist/tools/attachments.test.js +170 -129
  60. package/dist/tools/attachments.test.js.map +1 -1
  61. package/dist/tools/calendar.d.ts +16 -13
  62. package/dist/tools/calendar.d.ts.map +1 -1
  63. package/dist/tools/calendar.js +17 -16
  64. package/dist/tools/calendar.js.map +1 -1
  65. package/dist/tools/embeddings.d.ts +6 -6
  66. package/dist/tools/embeddings.d.ts.map +1 -1
  67. package/dist/tools/embeddings.js +6 -6
  68. package/dist/tools/embeddings.js.map +1 -1
  69. package/dist/tools/events.d.ts +7 -3
  70. package/dist/tools/events.d.ts.map +1 -1
  71. package/dist/tools/events.js +51 -16
  72. package/dist/tools/events.js.map +1 -1
  73. package/dist/tools/filters.d.ts +28 -33
  74. package/dist/tools/filters.d.ts.map +1 -1
  75. package/dist/tools/filters.js +42 -105
  76. package/dist/tools/filters.js.map +1 -1
  77. package/dist/tools/notes.d.ts +80 -218
  78. package/dist/tools/notes.d.ts.map +1 -1
  79. package/dist/tools/notes.js +180 -177
  80. package/dist/tools/notes.js.map +1 -1
  81. package/dist/tools/notes.test.js +242 -21
  82. package/dist/tools/notes.test.js.map +1 -1
  83. package/dist/tools/search.d.ts +4 -3
  84. package/dist/tools/search.d.ts.map +1 -1
  85. package/dist/tools/search.js +9 -5
  86. package/dist/tools/search.js.map +1 -1
  87. package/dist/tools/search.test.d.ts +2 -0
  88. package/dist/tools/search.test.d.ts.map +1 -0
  89. package/dist/tools/search.test.js +37 -0
  90. package/dist/tools/search.test.js.map +1 -0
  91. package/dist/tools/spaces.d.ts +20 -20
  92. package/dist/tools/spaces.d.ts.map +1 -1
  93. package/dist/tools/spaces.js +28 -28
  94. package/dist/tools/spaces.js.map +1 -1
  95. package/dist/tools/tasks.d.ts +22 -22
  96. package/dist/tools/tasks.d.ts.map +1 -1
  97. package/dist/tools/tasks.js +22 -22
  98. package/dist/tools/tasks.js.map +1 -1
  99. package/dist/tools/templates.d.ts +7 -7
  100. package/dist/tools/templates.d.ts.map +1 -1
  101. package/dist/tools/templates.js +4 -4
  102. package/dist/tools/templates.js.map +1 -1
  103. package/dist/tools/themes.js +1 -1
  104. package/dist/tools/themes.js.map +1 -1
  105. package/dist/transport/bridge-availability.d.ts +5 -0
  106. package/dist/transport/bridge-availability.d.ts.map +1 -0
  107. package/dist/transport/bridge-availability.js +92 -0
  108. package/dist/transport/bridge-availability.js.map +1 -0
  109. package/dist/transport/bridge-cascade.d.ts +18 -0
  110. package/dist/transport/bridge-cascade.d.ts.map +1 -0
  111. package/dist/transport/bridge-cascade.js +78 -0
  112. package/dist/transport/bridge-cascade.js.map +1 -0
  113. package/dist/transport/bridge-cascade.test.d.ts +2 -0
  114. package/dist/transport/bridge-cascade.test.d.ts.map +1 -0
  115. package/dist/transport/bridge-cascade.test.js +160 -0
  116. package/dist/transport/bridge-cascade.test.js.map +1 -0
  117. package/dist/transport/bridge-client.d.ts +197 -0
  118. package/dist/transport/bridge-client.d.ts.map +1 -0
  119. package/dist/transport/bridge-client.js +288 -0
  120. package/dist/transport/bridge-client.js.map +1 -0
  121. package/dist/transport/bridge-client.test.d.ts +2 -0
  122. package/dist/transport/bridge-client.test.d.ts.map +1 -0
  123. package/dist/transport/bridge-client.test.js +384 -0
  124. package/dist/transport/bridge-client.test.js.map +1 -0
  125. package/dist/transport/bridge-context.d.ts +10 -0
  126. package/dist/transport/bridge-context.d.ts.map +1 -0
  127. package/dist/transport/bridge-context.js +18 -0
  128. package/dist/transport/bridge-context.js.map +1 -0
  129. package/dist/transport/bridge-fs.d.ts +25 -0
  130. package/dist/transport/bridge-fs.d.ts.map +1 -0
  131. package/dist/transport/bridge-fs.js +129 -0
  132. package/dist/transport/bridge-fs.js.map +1 -0
  133. package/dist/utils/date-utils.d.ts +24 -0
  134. package/dist/utils/date-utils.d.ts.map +1 -1
  135. package/dist/utils/date-utils.js +55 -0
  136. package/dist/utils/date-utils.js.map +1 -1
  137. package/dist/utils/date-utils.test.d.ts +2 -0
  138. package/dist/utils/date-utils.test.d.ts.map +1 -0
  139. package/dist/utils/date-utils.test.js +109 -0
  140. package/dist/utils/date-utils.test.js.map +1 -0
  141. package/dist/utils/folder-access.d.ts +23 -0
  142. package/dist/utils/folder-access.d.ts.map +1 -0
  143. package/dist/utils/folder-access.js +131 -0
  144. package/dist/utils/folder-access.js.map +1 -0
  145. package/dist/utils/folder-access.test.d.ts +2 -0
  146. package/dist/utils/folder-access.test.d.ts.map +1 -0
  147. package/dist/utils/folder-access.test.js +182 -0
  148. package/dist/utils/folder-access.test.js.map +1 -0
  149. package/dist/utils/folder-matcher.d.ts.map +1 -1
  150. package/dist/utils/folder-matcher.js +16 -0
  151. package/dist/utils/folder-matcher.js.map +1 -1
  152. package/dist/utils/folder-matcher.test.js +42 -0
  153. package/dist/utils/folder-matcher.test.js.map +1 -1
  154. package/dist/utils/server-config.d.ts +10 -2
  155. package/dist/utils/server-config.d.ts.map +1 -1
  156. package/dist/utils/server-config.js +16 -2
  157. package/dist/utils/server-config.js.map +1 -1
  158. package/dist/utils/version.d.ts +2 -0
  159. package/dist/utils/version.d.ts.map +1 -1
  160. package/dist/utils/version.js +5 -1
  161. package/dist/utils/version.js.map +1 -1
  162. package/package.json +4 -3
  163. package/scripts/calendar-helper +0 -0
  164. package/scripts/reminders-helper +0 -0
@@ -1,26 +1,10 @@
1
- // File system writer for local NotePlan notes
2
1
  import * as fs from 'fs';
3
2
  import * as path from 'path';
4
- import { getNotePlanPath, getNotesPath, getFileExtension, buildCalendarNotePath, getCalendarNote, isValidNoteExtension, } from './file-reader.js';
5
- /**
6
- * Rename/move a file, with fallback for sandboxed/mounted filesystems.
7
- * EPERM/EXDEV: copy + delete instead of atomic rename.
8
- */
9
- function moveFile(source, destination) {
10
- try {
11
- fs.renameSync(source, destination);
12
- }
13
- catch (error) {
14
- const code = error.code;
15
- if (code === 'EPERM' || code === 'EXDEV') {
16
- fs.copyFileSync(source, destination);
17
- fs.unlinkSync(source);
18
- }
19
- else {
20
- throw error;
21
- }
22
- }
23
- }
3
+ import { getNotePlanPath, getNotesPath, getFileExtension, buildCalendarNotePathAsync, getCalendarNote, isValidNoteExtension, } from './file-reader.js';
4
+ import { readFileUtf8, statPath, pathExists, writeFileUtf8, makeDirectory, moveFile, } from '../transport/bridge-fs.js';
5
+ import { BridgeHttpError } from '../transport/bridge-client.js';
6
+ import { ATTACHMENT_SUFFIX, getAttachmentsAbsolutePath, getNoteBaseName, } from './attachments-paths.js';
7
+ import { assertFolderAllowed } from '../utils/folder-access.js';
24
8
  function ensurePathInsideRoot(candidatePath, rootPath, label) {
25
9
  const resolvedCandidate = path.resolve(candidatePath);
26
10
  const resolvedRoot = path.resolve(rootPath);
@@ -193,106 +177,148 @@ function resolveRenamedFilename(currentFullPath, newFilename, keepExtension) {
193
177
  return `${requestedBase}${requestedExt}`;
194
178
  }
195
179
  /**
196
- * Write content to a note file atomically
197
- * NotePlan's FolderMonitor will detect the change within ~300ms.
198
- *
199
- * Important: use in-place writes for existing files so filesystem birthtime
200
- * (used as createdAt in MCP responses) does not reset on every edit.
180
+ * Use in-place writes for existing files so filesystem birthtime (exposed as
181
+ * createdAt in MCP responses) does not reset on every edit.
201
182
  */
202
- export function writeNoteFile(filePath, content) {
183
+ export async function writeNoteFile(filePath, content) {
203
184
  const fullPath = path.isAbsolute(filePath) ? filePath : path.join(getNotePlanPath(), filePath);
204
185
  ensurePathInsideRoot(fullPath, getNotePlanPath(), 'File path');
205
- // Ensure directory exists
206
- const dir = path.dirname(fullPath);
207
- if (!fs.existsSync(dir)) {
208
- fs.mkdirSync(dir, { recursive: true });
209
- }
210
- // Normalize line endings to Unix style
211
186
  const normalizedContent = content.replace(/\r\n/g, '\n');
212
- // Preserve createdAt for existing notes by avoiding temp-file rename.
213
- // Renaming a temp file replaces the inode and resets birthtime.
214
- if (fs.existsSync(fullPath)) {
215
- fs.writeFileSync(fullPath, normalizedContent, { encoding: 'utf-8' });
187
+ if (await pathExists(fullPath)) {
188
+ await writeFileUtf8(fullPath, normalizedContent);
216
189
  return;
217
190
  }
218
- // For new files, create exclusively to avoid accidental overwrite races.
219
191
  try {
220
- fs.writeFileSync(fullPath, normalizedContent, { encoding: 'utf-8', flag: 'wx' });
221
- }
222
- catch (error) {
223
- const maybeErrno = error;
224
- if (maybeErrno.code === 'EEXIST' || maybeErrno.code === 'EPERM') {
225
- // EEXIST: file appeared between existsSync and writeFileSync — safe to overwrite.
226
- // EPERM: sandboxed/mounted filesystems (e.g. Claude Co-Work, iCloud FUSE) may not
227
- // support O_EXCL (wx flag). Fall back to plain write.
228
- fs.writeFileSync(fullPath, normalizedContent, { encoding: 'utf-8' });
192
+ await writeFileUtf8(fullPath, normalizedContent, { exclusive: true });
193
+ }
194
+ catch (err) {
195
+ const code = err.code;
196
+ const conflict = code === 'EEXIST' ||
197
+ code === 'EPERM' ||
198
+ (err instanceof BridgeHttpError && err.status === 409);
199
+ if (conflict) {
200
+ await writeFileUtf8(fullPath, normalizedContent);
229
201
  return;
230
202
  }
231
- throw error;
203
+ throw err;
232
204
  }
233
205
  }
234
206
  /**
235
- * Create a new project note
207
+ * Resolve a caller-supplied filename to a safe basename + extension. Path
208
+ * separators and `..` are rejected up-front because they bypass the
209
+ * folder argument; everything else is run through sanitizeFilename and
210
+ * given the configured default extension if none was provided.
236
211
  */
237
- export function createProjectNote(title, content = '', folder) {
238
- // Sanitize title for filename
239
- const safeTitle = sanitizeFilename(title);
240
- // Strip Notes/ prefix if present to avoid double-nesting (Notes/Notes/...)
241
- const cleanFolder = folder?.replace(/^Notes\//, '');
242
- const folderPath = cleanFolder ? path.join('Notes', cleanFolder) : 'Notes';
243
- const ext = getFileExtension(); // Use detected extension
244
- const filePath = path.join(folderPath, `${safeTitle}${ext}`);
245
- // Check if file already exists (with any extension)
212
+ function resolveExplicitFilename(filename, defaultExt) {
213
+ const trimmed = filename.trim();
214
+ if (trimmed.length === 0) {
215
+ throw new Error('filename is empty');
216
+ }
217
+ if (/[/\\]/.test(trimmed) || trimmed.split(/[/\\]/).some((segment) => segment === '..')) {
218
+ throw new Error('filename must be a basename only (no path separators). Use the "folder" parameter to choose a folder.');
219
+ }
220
+ const ext = path.extname(trimmed).toLowerCase();
221
+ const hasKnownExt = ext === '.md' || ext === '.txt';
222
+ const base = hasKnownExt ? trimmed.slice(0, -ext.length) : trimmed;
223
+ const finalExt = hasKnownExt ? ext : defaultExt;
224
+ const safeBase = sanitizeFilename(base);
225
+ if (safeBase.length === 0) {
226
+ throw new Error('filename is invalid after sanitization');
227
+ }
228
+ return `${safeBase}${finalExt}`;
229
+ }
230
+ /**
231
+ * Reject creation if a note already exists at `filePath` under either
232
+ * `.md` or `.txt`. NotePlan treats both as equivalent for a given base
233
+ * name, so creating `Foo.md` while `Foo.txt` is on disk would produce
234
+ * a duplicate that the app silently merges. Shared by both project- and
235
+ * calendar-note creation.
236
+ */
237
+ async function assertNoteDoesNotExistAtPath(filePath) {
246
238
  const fullPath = path.join(getNotePlanPath(), filePath);
239
+ const ext = path.extname(filePath);
247
240
  const altExt = ext === '.txt' ? '.md' : '.txt';
248
- const altPath = path.join(getNotePlanPath(), folderPath, `${safeTitle}${altExt}`);
249
- if (fs.existsSync(fullPath)) {
241
+ const altRelative = `${filePath.slice(0, -ext.length)}${altExt}`;
242
+ const altFullPath = `${fullPath.slice(0, -ext.length)}${altExt}`;
243
+ if (await pathExists(fullPath)) {
250
244
  throw new Error(`Note already exists: ${filePath}`);
251
245
  }
252
- if (fs.existsSync(altPath)) {
253
- throw new Error(`Note already exists: ${path.join(folderPath, `${safeTitle}${altExt}`)}`);
246
+ if (await pathExists(altFullPath)) {
247
+ throw new Error(`Note already exists: ${altRelative}`);
254
248
  }
255
- // Create content with title as heading if content is empty
249
+ }
250
+ /**
251
+ * Create a new project note. When `filename` is provided it overrides
252
+ * the title-derived basename — callers can keep the on-disk name (e.g.
253
+ * `_context.md`) decoupled from the human-readable title.
254
+ */
255
+ export async function createProjectNote(title, content = '', folder, filename) {
256
+ // Strip an optional `Notes/` prefix OR a bare `Notes` (the root-only
257
+ // case). Without the `(\/|$)` branch, `folder = "Notes"` slipped past
258
+ // and produced `Notes/Notes/<title>` on disk.
259
+ const cleanFolder = folder?.replace(/^Notes(\/|$)/, '');
260
+ const folderPath = cleanFolder ? path.join('Notes', cleanFolder) : 'Notes';
261
+ const defaultExt = getFileExtension();
262
+ const fileBasename = filename
263
+ ? resolveExplicitFilename(filename, defaultExt)
264
+ : `${sanitizeFilename(title)}${defaultExt}`;
265
+ const filePath = path.join(folderPath, fileBasename);
266
+ assertFolderAllowed(filePath, 'create note in');
267
+ await assertNoteDoesNotExistAtPath(filePath);
256
268
  const noteContent = content || `# ${title}\n\n`;
257
- writeNoteFile(filePath, noteContent);
269
+ await writeNoteFile(filePath, noteContent);
258
270
  return filePath;
259
271
  }
260
272
  /**
261
273
  * Create or update a calendar note
262
274
  */
263
- export function createCalendarNote(dateStr, content) {
264
- // Use the detected configuration for file path
265
- const filePath = buildCalendarNotePath(dateStr);
266
- writeNoteFile(filePath, content);
275
+ export async function createCalendarNote(dateStr, content) {
276
+ const filePath = await buildCalendarNotePathAsync(dateStr);
277
+ await writeNoteFile(filePath, content);
278
+ return filePath;
279
+ }
280
+ /**
281
+ * Create a calendar note only if one doesn't already exist for that date,
282
+ * checking both extensions. Mirrors the collision semantics of
283
+ * `createProjectNote` so the `noteplan_manage_note(action: create)` flow
284
+ * can reject duplicates loudly instead of clobbering a periodic note the
285
+ * user has been working on.
286
+ */
287
+ export async function createCalendarNoteIfNew(dateStr, content) {
288
+ const filePath = await buildCalendarNotePathAsync(dateStr);
289
+ assertFolderAllowed(filePath, 'create calendar note in');
290
+ await assertNoteDoesNotExistAtPath(filePath);
291
+ await writeNoteFile(filePath, content);
267
292
  return filePath;
268
293
  }
269
294
  /**
270
295
  * Append content to a note
271
296
  */
272
- export function appendToNote(filePath, content) {
297
+ export async function appendToNote(filePath, content) {
273
298
  const fullPath = path.isAbsolute(filePath) ? filePath : path.join(getNotePlanPath(), filePath);
274
299
  ensurePathInsideRoot(fullPath, getNotePlanPath(), 'File path');
275
- if (!fs.existsSync(fullPath)) {
300
+ assertFolderAllowed(filePath, 'append to');
301
+ const existingContent = await readFileUtf8(fullPath);
302
+ if (existingContent === null) {
276
303
  throw new Error(`Note not found: ${filePath}`);
277
304
  }
278
- const existingContent = fs.readFileSync(fullPath, 'utf-8');
279
305
  const newContent = existingContent.endsWith('\n')
280
306
  ? existingContent + content
281
307
  : existingContent + '\n' + content;
282
- writeNoteFile(filePath, newContent);
308
+ await writeNoteFile(filePath, newContent);
283
309
  }
284
310
  /**
285
311
  * Prepend content to a note (after frontmatter if present)
286
312
  */
287
- export function prependToNote(filePath, content) {
313
+ export async function prependToNote(filePath, content) {
288
314
  const fullPath = path.isAbsolute(filePath) ? filePath : path.join(getNotePlanPath(), filePath);
289
315
  ensurePathInsideRoot(fullPath, getNotePlanPath(), 'File path');
290
- if (!fs.existsSync(fullPath)) {
316
+ assertFolderAllowed(filePath, 'prepend to');
317
+ const existingContent = await readFileUtf8(fullPath);
318
+ if (existingContent === null) {
291
319
  throw new Error(`Note not found: ${filePath}`);
292
320
  }
293
- const existingContent = fs.readFileSync(fullPath, 'utf-8');
294
321
  const lines = existingContent.split('\n');
295
- // Find end of frontmatter if present
296
322
  let insertIndex = 0;
297
323
  if (lines[0]?.trim() === '---') {
298
324
  for (let i = 1; i < lines.length; i++) {
@@ -302,63 +328,107 @@ export function prependToNote(filePath, content) {
302
328
  }
303
329
  }
304
330
  }
305
- // Insert content
306
331
  lines.splice(insertIndex, 0, content);
307
- writeNoteFile(filePath, lines.join('\n'));
332
+ await writeNoteFile(filePath, lines.join('\n'));
308
333
  }
309
334
  /**
310
335
  * Update a note's content
311
336
  */
312
- export function updateNote(filePath, content) {
337
+ export async function updateNote(filePath, content) {
313
338
  const fullPath = toLocalNoteAbsolutePath(filePath);
314
- if (!fs.existsSync(fullPath)) {
339
+ assertFolderAllowed(filePath, 'update');
340
+ if (!(await pathExists(fullPath))) {
315
341
  throw new Error(`Note not found: ${filePath}`);
316
342
  }
317
- writeNoteFile(filePath, content);
343
+ await writeNoteFile(filePath, content);
344
+ }
345
+ /**
346
+ * Move the sibling `_attachments` folder alongside a note file operation.
347
+ * Idempotent: the bridge-Swift path already moves attachments via
348
+ * `DataStore.moveNote` when NotePlan is running, so the pathExists check
349
+ * makes both bridge and fs-fallback paths safe to call unconditionally.
350
+ */
351
+ async function relocateAttachmentsFolderIfPresent(oldNoteAbsolutePath, newNoteAbsolutePath) {
352
+ const oldFolder = getAttachmentsAbsolutePath(oldNoteAbsolutePath);
353
+ const newFolder = getAttachmentsAbsolutePath(newNoteAbsolutePath);
354
+ if (oldFolder === newFolder)
355
+ return;
356
+ if (!(await pathExists(oldFolder)))
357
+ return;
358
+ await moveFile(oldFolder, newFolder);
318
359
  }
319
360
  /**
320
- * Delete a note (move to trash)
361
+ * After a rename that changes the basename, rewrite `[label](old_attachments/file)`
362
+ * style references in the renamed note's content so they point at the
363
+ * new folder name. Best-effort literal-string replace — basenames with
364
+ * URL-encodable characters (spaces, parentheses) may need manual fixup.
321
365
  */
322
- export function deleteNote(filePath) {
366
+ async function rewriteAttachmentLinksAfterRename(noteAbsolutePath, oldBaseName, newBaseName) {
367
+ if (oldBaseName === newBaseName)
368
+ return;
369
+ const content = await readFileUtf8(noteAbsolutePath);
370
+ if (content === null)
371
+ return;
372
+ const oldRef = `${oldBaseName}${ATTACHMENT_SUFFIX}/`;
373
+ if (!content.includes(oldRef))
374
+ return;
375
+ const newRef = `${newBaseName}${ATTACHMENT_SUFFIX}/`;
376
+ await writeFileUtf8(noteAbsolutePath, content.split(oldRef).join(newRef));
377
+ }
378
+ export async function deleteNote(filePath) {
323
379
  const fullPath = toLocalNoteAbsolutePath(filePath);
324
- if (!fs.existsSync(fullPath)) {
380
+ assertFolderAllowed(filePath, 'delete');
381
+ if (!(await pathExists(fullPath))) {
325
382
  throw new Error(`Note not found: ${filePath}`);
326
383
  }
327
- // Reject non-text files (e.g. .key, .pdf) — only .md and .txt are valid notes
328
384
  if (!isValidNoteExtension(path.basename(fullPath))) {
329
385
  throw new Error(`Cannot delete: "${path.basename(fullPath)}" is not a note file. Only .md and .txt files are supported.`);
330
386
  }
331
- // Move to Notes/@Trash (matching NotePlan's own moveToTrash behavior)
332
387
  const trashPath = path.join(getNotesPath(), '@Trash');
333
- if (!fs.existsSync(trashPath)) {
334
- fs.mkdirSync(trashPath, { recursive: true });
388
+ if (!(await pathExists(trashPath))) {
389
+ await makeDirectory(trashPath);
335
390
  }
336
391
  const filename = path.basename(fullPath);
337
392
  const trashFilePath = path.join(trashPath, filename);
338
- // Handle duplicate filenames in trash
339
393
  let finalTrashPath = trashFilePath;
340
394
  let counter = 1;
341
- while (fs.existsSync(finalTrashPath)) {
395
+ while (await pathExists(finalTrashPath)) {
342
396
  const ext = path.extname(filename);
343
397
  const base = path.basename(filename, ext);
344
398
  finalTrashPath = path.join(trashPath, `${base}-${counter}${ext}`);
345
399
  counter++;
346
400
  }
347
- moveFile(fullPath, finalTrashPath);
401
+ await moveFile(fullPath, finalTrashPath);
402
+ // Drag the sibling _attachments/ folder along so embedded images/files
403
+ // are recoverable from @Trash and the note's relative links keep
404
+ // resolving if the user restores it. Match the trashed note's
405
+ // (possibly suffix-disambiguated) basename.
406
+ const sourceAttachments = getAttachmentsAbsolutePath(fullPath);
407
+ const sourceBaseName = getNoteBaseName(fullPath);
408
+ const trashedBaseName = getNoteBaseName(finalTrashPath);
409
+ if (await pathExists(sourceAttachments)) {
410
+ const trashedAttachmentsPath = path.join(path.dirname(finalTrashPath), `${trashedBaseName}${ATTACHMENT_SUFFIX}`);
411
+ await moveFile(sourceAttachments, trashedAttachmentsPath);
412
+ }
413
+ // If the trash collision suffix changed the basename, the in-content
414
+ // attachment links still point at the original folder name — rewrite
415
+ // them so a later restore (which preserves the trashed basename)
416
+ // produces a functional note.
417
+ await rewriteAttachmentLinksAfterRename(finalTrashPath, sourceBaseName, trashedBaseName);
348
418
  return path.relative(getNotePlanPath(), finalTrashPath);
349
419
  }
350
420
  /**
351
421
  * Preview local note move target without mutating the filesystem
352
422
  */
353
- export function previewMoveLocalNote(filePath, destinationFolder) {
423
+ export async function previewMoveLocalNote(filePath, destinationFolder) {
354
424
  const fullPath = toLocalNoteAbsolutePath(filePath);
355
- if (!fs.existsSync(fullPath)) {
425
+ const sourceStat = await statPath(fullPath);
426
+ if (!sourceStat.exists) {
356
427
  throw new Error(`Note not found: ${filePath}`);
357
428
  }
358
- if (fs.statSync(fullPath).isDirectory()) {
429
+ if (sourceStat.isDir) {
359
430
  throw new Error(`Not a note file: ${filePath}`);
360
431
  }
361
- // Reject non-text files (e.g. .key, .pdf) — only .md and .txt are valid notes
362
432
  if (!isValidNoteExtension(path.basename(fullPath))) {
363
433
  throw new Error(`Cannot move: "${path.basename(fullPath)}" is not a note file. Only .md and .txt files are supported.`);
364
434
  }
@@ -371,38 +441,48 @@ export function previewMoveLocalNote(filePath, destinationFolder) {
371
441
  if (path.resolve(nextAbsolutePath) === path.resolve(fullPath)) {
372
442
  throw new Error('Note is already in the destination folder');
373
443
  }
374
- if (fs.existsSync(nextAbsolutePath)) {
444
+ if (await pathExists(nextAbsolutePath)) {
375
445
  throw new Error(`A note already exists at destination: ${path.relative(getNotePlanPath(), nextAbsolutePath)}`);
376
446
  }
447
+ const fromFilename = path.relative(getNotePlanPath(), fullPath);
448
+ const toFilename = path.relative(getNotePlanPath(), nextAbsolutePath);
449
+ // Both source and destination must be accessible — otherwise an agent
450
+ // could exfiltrate from a denied folder by moving notes out of it.
451
+ // Asserting at preview time means dry-run requests also fail loudly
452
+ // instead of leaking path-existence info.
453
+ assertFolderAllowed(fromFilename, 'move from');
454
+ assertFolderAllowed(toFilename, 'move to');
377
455
  return {
378
- fromFilename: path.relative(getNotePlanPath(), fullPath),
379
- toFilename: path.relative(getNotePlanPath(), nextAbsolutePath),
456
+ fromFilename,
457
+ toFilename,
380
458
  destinationFolder: normalizedDestinationFolder,
381
459
  };
382
460
  }
383
461
  /**
384
462
  * Move a local note to another folder
385
463
  */
386
- export function moveLocalNote(filePath, destinationFolder) {
387
- const preview = previewMoveLocalNote(filePath, destinationFolder);
464
+ export async function moveLocalNote(filePath, destinationFolder) {
465
+ const preview = await previewMoveLocalNote(filePath, destinationFolder);
388
466
  const sourcePath = path.join(getNotePlanPath(), preview.fromFilename);
389
467
  const targetPath = path.join(getNotePlanPath(), preview.toFilename);
390
468
  const targetDir = path.dirname(targetPath);
391
- if (!fs.existsSync(targetDir)) {
392
- fs.mkdirSync(targetDir, { recursive: true });
469
+ if (!(await pathExists(targetDir))) {
470
+ await makeDirectory(targetDir);
393
471
  }
394
- moveFile(sourcePath, targetPath);
472
+ await moveFile(sourcePath, targetPath);
473
+ await relocateAttachmentsFolderIfPresent(sourcePath, targetPath);
395
474
  return preview.toFilename;
396
475
  }
397
476
  /**
398
477
  * Preview restoring a local note from @Trash into Notes
399
478
  */
400
- export function previewRestoreLocalNoteFromTrash(filePath, destinationFolder) {
479
+ export async function previewRestoreLocalNoteFromTrash(filePath, destinationFolder) {
401
480
  const fullPath = toLocalNoteAbsolutePath(filePath);
402
- if (!fs.existsSync(fullPath)) {
481
+ const sourceStat = await statPath(fullPath);
482
+ if (!sourceStat.exists) {
403
483
  throw new Error(`Note not found: ${filePath}`);
404
484
  }
405
- if (fs.statSync(fullPath).isDirectory()) {
485
+ if (sourceStat.isDir) {
406
486
  throw new Error(`Not a note file: ${filePath}`);
407
487
  }
408
488
  const trashRoot = path.join(getNotesPath(), '@Trash');
@@ -411,41 +491,47 @@ export function previewRestoreLocalNoteFromTrash(filePath, destinationFolder) {
411
491
  const destinationDir = path.join(getNotePlanPath(), normalizedDestinationFolder);
412
492
  ensurePathInsideRoot(destinationDir, getNotesPath(), 'Destination folder');
413
493
  const nextAbsolutePath = path.join(destinationDir, path.basename(fullPath));
414
- if (fs.existsSync(nextAbsolutePath)) {
494
+ if (await pathExists(nextAbsolutePath)) {
415
495
  throw new Error(`A note already exists at destination: ${path.relative(getNotePlanPath(), nextAbsolutePath)}`);
416
496
  }
497
+ const toFilename = path.relative(getNotePlanPath(), nextAbsolutePath);
498
+ // The trash itself isn't filtered (users restore there even if it
499
+ // sits outside their allowlist), but the destination must be accessible.
500
+ // Asserting at preview time also blocks dry-run probes.
501
+ assertFolderAllowed(toFilename, 'restore to');
417
502
  return {
418
503
  fromFilename: path.relative(getNotePlanPath(), fullPath),
419
- toFilename: path.relative(getNotePlanPath(), nextAbsolutePath),
504
+ toFilename,
420
505
  destinationFolder: normalizedDestinationFolder,
421
506
  };
422
507
  }
423
508
  /**
424
509
  * Restore a local note from @Trash into Notes
425
510
  */
426
- export function restoreLocalNoteFromTrash(filePath, destinationFolder) {
427
- const preview = previewRestoreLocalNoteFromTrash(filePath, destinationFolder);
511
+ export async function restoreLocalNoteFromTrash(filePath, destinationFolder) {
512
+ const preview = await previewRestoreLocalNoteFromTrash(filePath, destinationFolder);
428
513
  const sourcePath = path.join(getNotePlanPath(), preview.fromFilename);
429
514
  const targetPath = path.join(getNotePlanPath(), preview.toFilename);
430
515
  const targetDir = path.dirname(targetPath);
431
- if (!fs.existsSync(targetDir)) {
432
- fs.mkdirSync(targetDir, { recursive: true });
516
+ if (!(await pathExists(targetDir))) {
517
+ await makeDirectory(targetDir);
433
518
  }
434
- moveFile(sourcePath, targetPath);
519
+ await moveFile(sourcePath, targetPath);
520
+ await relocateAttachmentsFolderIfPresent(sourcePath, targetPath);
435
521
  return preview.toFilename;
436
522
  }
437
523
  /**
438
524
  * Preview local note rename target without mutating the filesystem
439
525
  */
440
- export function previewRenameLocalNoteFile(filePath, newFilename, keepExtension = true) {
526
+ export async function previewRenameLocalNoteFile(filePath, newFilename, keepExtension = true) {
441
527
  const fullPath = toLocalNoteAbsolutePath(filePath);
442
- if (!fs.existsSync(fullPath)) {
528
+ const sourceStat = await statPath(fullPath);
529
+ if (!sourceStat.exists) {
443
530
  throw new Error(`Note not found: ${filePath}`);
444
531
  }
445
- if (fs.statSync(fullPath).isDirectory()) {
532
+ if (sourceStat.isDir) {
446
533
  throw new Error(`Not a note file: ${filePath}`);
447
534
  }
448
- // Reject non-text files (e.g. .key, .pdf) — only .md and .txt are valid notes
449
535
  if (!isValidNoteExtension(path.basename(fullPath))) {
450
536
  throw new Error(`Cannot rename: "${path.basename(fullPath)}" is not a note file. Only .md and .txt files are supported.`);
451
537
  }
@@ -456,35 +542,45 @@ export function previewRenameLocalNoteFile(filePath, newFilename, keepExtension
456
542
  if (path.resolve(nextAbsolutePath) === path.resolve(fullPath)) {
457
543
  throw new Error('New filename matches current filename');
458
544
  }
459
- if (fs.existsSync(nextAbsolutePath)) {
545
+ if (await pathExists(nextAbsolutePath)) {
460
546
  throw new Error(`A note already exists with filename: ${path.relative(getNotePlanPath(), nextAbsolutePath)}`);
461
547
  }
548
+ const fromFilename = path.relative(getNotePlanPath(), fullPath);
549
+ // Rename stays in the same folder, so checking the source folder
550
+ // alone covers both. Asserting at preview time also gates dry-run.
551
+ assertFolderAllowed(fromFilename, 'rename');
462
552
  return {
463
- fromFilename: path.relative(getNotePlanPath(), fullPath),
553
+ fromFilename,
464
554
  toFilename: path.relative(getNotePlanPath(), nextAbsolutePath),
465
555
  };
466
556
  }
467
557
  /**
468
558
  * Rename a local note file inside the same folder
469
559
  */
470
- export function renameLocalNoteFile(filePath, newFilename, keepExtension = true) {
471
- const preview = previewRenameLocalNoteFile(filePath, newFilename, keepExtension);
560
+ export async function renameLocalNoteFile(filePath, newFilename, keepExtension = true) {
561
+ const preview = await previewRenameLocalNoteFile(filePath, newFilename, keepExtension);
472
562
  const sourcePath = path.join(getNotePlanPath(), preview.fromFilename);
473
563
  const targetPath = path.join(getNotePlanPath(), preview.toFilename);
474
- moveFile(sourcePath, targetPath);
564
+ await moveFile(sourcePath, targetPath);
565
+ await relocateAttachmentsFolderIfPresent(sourcePath, targetPath);
566
+ await rewriteAttachmentLinksAfterRename(targetPath, getNoteBaseName(sourcePath), getNoteBaseName(targetPath));
475
567
  return preview.toFilename;
476
568
  }
477
569
  /**
478
570
  * Preview creating a local folder under Notes
479
571
  */
480
- export function previewCreateFolder(folderPath) {
572
+ export async function previewCreateFolder(folderPath) {
481
573
  const normalized = normalizeLocalFolderPath(folderPath, 'Folder path');
482
574
  if (!normalized) {
483
575
  throw new Error('Folder path is required');
484
576
  }
485
577
  const fullPath = path.join(getNotesPath(), normalized);
486
578
  ensurePathInsideRoot(fullPath, getNotesPath(), 'Folder path');
487
- if (fs.existsSync(fullPath)) {
579
+ // Block creating a folder inside a denied subtree — that subtree is
580
+ // off-limits for any new content. `Notes/<normalized>` is the
581
+ // relative path the access rules match against.
582
+ assertFolderAllowed(`Notes/${normalized}`, 'create folder at');
583
+ if (await pathExists(fullPath)) {
488
584
  throw new Error(`Folder already exists: ${normalized}`);
489
585
  }
490
586
  return normalized;
@@ -492,28 +588,31 @@ export function previewCreateFolder(folderPath) {
492
588
  /**
493
589
  * Create a folder in the Notes directory
494
590
  */
495
- export function createFolder(folderPath) {
496
- const normalized = previewCreateFolder(folderPath);
591
+ export async function createFolder(folderPath) {
592
+ const normalized = await previewCreateFolder(folderPath);
497
593
  const fullPath = path.join(getNotesPath(), normalized);
498
- if (!fs.existsSync(fullPath)) {
499
- fs.mkdirSync(fullPath, { recursive: true });
594
+ if (!(await pathExists(fullPath))) {
595
+ await makeDirectory(fullPath);
500
596
  }
501
597
  return normalized;
502
598
  }
503
599
  /**
504
600
  * Preview deleting a local folder (validates and returns normalized path)
505
601
  */
506
- export function previewDeleteLocalFolder(folderPath) {
602
+ export async function previewDeleteLocalFolder(folderPath) {
507
603
  const normalized = normalizeLocalFolderPath(folderPath, 'Source folder');
508
604
  if (!normalized) {
509
605
  throw new Error('Folder path is required');
510
606
  }
511
607
  const fullPath = path.join(getNotesPath(), normalized);
512
608
  ensurePathInsideRoot(fullPath, getNotesPath(), 'Source folder');
513
- if (!fs.existsSync(fullPath)) {
609
+ // Deleting a denied folder would bypass the rule for everything inside it.
610
+ assertFolderAllowed(`Notes/${normalized}`, 'delete folder');
611
+ const stat = await statPath(fullPath);
612
+ if (!stat.exists) {
514
613
  throw new Error(`Folder not found: ${normalized}`);
515
614
  }
516
- if (!fs.statSync(fullPath).isDirectory()) {
615
+ if (!stat.isDir) {
517
616
  throw new Error(`Not a folder: ${normalized}`);
518
617
  }
519
618
  const folderName = path.basename(fullPath);
@@ -525,38 +624,38 @@ export function previewDeleteLocalFolder(folderPath) {
525
624
  /**
526
625
  * Delete a local folder (move to @Trash)
527
626
  */
528
- export function deleteLocalFolder(folderPath) {
529
- const normalized = previewDeleteLocalFolder(folderPath);
627
+ export async function deleteLocalFolder(folderPath) {
628
+ const normalized = await previewDeleteLocalFolder(folderPath);
530
629
  const fullPath = path.join(getNotesPath(), normalized);
531
630
  const folderName = path.basename(fullPath);
532
- // Move to Notes/@Trash (matching NotePlan's own moveToTrash behavior)
533
631
  const trashPath = path.join(getNotesPath(), '@Trash');
534
- if (!fs.existsSync(trashPath)) {
535
- fs.mkdirSync(trashPath, { recursive: true });
632
+ if (!(await pathExists(trashPath))) {
633
+ await makeDirectory(trashPath);
536
634
  }
537
635
  let targetPath = path.join(trashPath, folderName);
538
636
  let counter = 1;
539
- while (fs.existsSync(targetPath)) {
637
+ while (await pathExists(targetPath)) {
540
638
  targetPath = path.join(trashPath, `${folderName}-${counter}`);
541
639
  counter++;
542
640
  }
543
- moveFile(fullPath, targetPath);
641
+ await moveFile(fullPath, targetPath);
544
642
  return path.relative(getNotePlanPath(), targetPath);
545
643
  }
546
644
  /**
547
645
  * Preview moving a local folder under Notes
548
646
  */
549
- export function previewMoveLocalFolder(sourceFolder, destinationFolder) {
647
+ export async function previewMoveLocalFolder(sourceFolder, destinationFolder) {
550
648
  const normalizedSource = normalizeLocalFolderPath(sourceFolder, 'Source folder');
551
649
  if (!normalizedSource) {
552
650
  throw new Error('Source folder is required');
553
651
  }
554
652
  const sourcePath = path.join(getNotesPath(), normalizedSource);
555
653
  ensurePathInsideRoot(sourcePath, getNotesPath(), 'Source folder');
556
- if (!fs.existsSync(sourcePath)) {
654
+ const sourceStat = await statPath(sourcePath);
655
+ if (!sourceStat.exists) {
557
656
  throw new Error(`Source folder not found: ${normalizedSource}`);
558
657
  }
559
- if (!fs.statSync(sourcePath).isDirectory()) {
658
+ if (!sourceStat.isDir) {
560
659
  throw new Error(`Source is not a folder: ${normalizedSource}`);
561
660
  }
562
661
  const normalizedDestination = normalizeLocalFolderPath(destinationFolder, 'Destination folder');
@@ -564,10 +663,11 @@ export function previewMoveLocalFolder(sourceFolder, destinationFolder) {
564
663
  ? path.join(getNotesPath(), normalizedDestination)
565
664
  : getNotesPath();
566
665
  ensurePathInsideRoot(destinationPath, getNotesPath(), 'Destination folder');
567
- if (!fs.existsSync(destinationPath)) {
666
+ const destStat = await statPath(destinationPath);
667
+ if (!destStat.exists) {
568
668
  throw new Error(`Destination folder not found: ${normalizedDestination || 'Notes'}`);
569
669
  }
570
- if (!fs.statSync(destinationPath).isDirectory()) {
670
+ if (!destStat.isDir) {
571
671
  throw new Error('Destination is not a folder');
572
672
  }
573
673
  if (path.resolve(destinationPath) === path.resolve(sourcePath) ||
@@ -578,39 +678,47 @@ export function previewMoveLocalFolder(sourceFolder, destinationFolder) {
578
678
  if (path.resolve(targetPath) === path.resolve(sourcePath)) {
579
679
  throw new Error('Folder is already in the destination');
580
680
  }
581
- if (fs.existsSync(targetPath)) {
681
+ if (await pathExists(targetPath)) {
582
682
  throw new Error(`A folder already exists at destination: ${path.relative(getNotesPath(), targetPath)}`);
583
683
  }
684
+ const fromFolder = normalizedSource;
685
+ const toFolder = path.relative(getNotesPath(), targetPath).replace(/\\/g, '/');
686
+ // Both endpoints must be accessible — moving a denied folder out
687
+ // would expose its contents, moving any folder INTO a denied subtree
688
+ // would smuggle content past the rule.
689
+ assertFolderAllowed(`Notes/${fromFolder}`, 'move folder from');
690
+ assertFolderAllowed(`Notes/${toFolder}`, 'move folder to');
584
691
  return {
585
- fromFolder: normalizedSource,
586
- toFolder: path.relative(getNotesPath(), targetPath).replace(/\\/g, '/'),
692
+ fromFolder,
693
+ toFolder,
587
694
  destinationFolder: normalizedDestination || 'Notes',
588
695
  };
589
696
  }
590
697
  /**
591
698
  * Move a local folder under Notes
592
699
  */
593
- export function moveLocalFolder(sourceFolder, destinationFolder) {
594
- const preview = previewMoveLocalFolder(sourceFolder, destinationFolder);
700
+ export async function moveLocalFolder(sourceFolder, destinationFolder) {
701
+ const preview = await previewMoveLocalFolder(sourceFolder, destinationFolder);
595
702
  const sourcePath = path.join(getNotesPath(), preview.fromFolder);
596
703
  const targetPath = path.join(getNotesPath(), preview.toFolder);
597
- moveFile(sourcePath, targetPath);
704
+ await moveFile(sourcePath, targetPath);
598
705
  return preview;
599
706
  }
600
707
  /**
601
708
  * Preview renaming a local folder in place
602
709
  */
603
- export function previewRenameLocalFolder(sourceFolder, newName) {
710
+ export async function previewRenameLocalFolder(sourceFolder, newName) {
604
711
  const normalizedSource = normalizeLocalFolderPath(sourceFolder, 'Source folder');
605
712
  if (!normalizedSource) {
606
713
  throw new Error('Source folder is required');
607
714
  }
608
715
  const sourcePath = path.join(getNotesPath(), normalizedSource);
609
716
  ensurePathInsideRoot(sourcePath, getNotesPath(), 'Source folder');
610
- if (!fs.existsSync(sourcePath)) {
717
+ const sourceStat = await statPath(sourcePath);
718
+ if (!sourceStat.exists) {
611
719
  throw new Error(`Source folder not found: ${normalizedSource}`);
612
720
  }
613
- if (!fs.statSync(sourcePath).isDirectory()) {
721
+ if (!sourceStat.isDir) {
614
722
  throw new Error(`Source is not a folder: ${normalizedSource}`);
615
723
  }
616
724
  let normalizedInput = newName.trim().replace(/\\/g, '/');
@@ -651,22 +759,26 @@ export function previewRenameLocalFolder(sourceFolder, newName) {
651
759
  if (path.resolve(targetPath) === path.resolve(sourcePath)) {
652
760
  throw new Error('New folder name matches current name');
653
761
  }
654
- if (fs.existsSync(targetPath)) {
762
+ if (await pathExists(targetPath)) {
655
763
  throw new Error(`A folder with this name already exists: ${path.relative(getNotesPath(), targetPath)}`);
656
764
  }
657
- return {
658
- fromFolder: normalizedSource,
659
- toFolder: path.relative(getNotesPath(), targetPath).replace(/\\/g, '/'),
660
- };
765
+ const fromFolder = normalizedSource;
766
+ const toFolder = path.relative(getNotesPath(), targetPath).replace(/\\/g, '/');
767
+ // Renaming the source folder bypasses any deny rule that pinned it;
768
+ // asserting source covers the case. We also gate destination because
769
+ // renaming TO a denied name is incoherent.
770
+ assertFolderAllowed(`Notes/${fromFolder}`, 'rename folder');
771
+ assertFolderAllowed(`Notes/${toFolder}`, 'rename folder to');
772
+ return { fromFolder, toFolder };
661
773
  }
662
774
  /**
663
775
  * Rename a local folder in place
664
776
  */
665
- export function renameLocalFolder(sourceFolder, newName) {
666
- const preview = previewRenameLocalFolder(sourceFolder, newName);
777
+ export async function renameLocalFolder(sourceFolder, newName) {
778
+ const preview = await previewRenameLocalFolder(sourceFolder, newName);
667
779
  const sourcePath = path.join(getNotesPath(), preview.fromFolder);
668
780
  const targetPath = path.join(getNotesPath(), preview.toFolder);
669
- moveFile(sourcePath, targetPath);
781
+ await moveFile(sourcePath, targetPath);
670
782
  return preview;
671
783
  }
672
784
  /**
@@ -682,15 +794,19 @@ function sanitizeFilename(name) {
682
794
  * Ensure a calendar note exists, create if not
683
795
  * Returns the path to the existing or newly created note
684
796
  */
685
- export function ensureCalendarNote(dateStr) {
797
+ export async function ensureCalendarNote(dateStr) {
686
798
  // First try to find an existing note (checks multiple paths/extensions)
687
- const existingNote = getCalendarNote(dateStr);
799
+ const existingNote = await getCalendarNote(dateStr);
688
800
  if (existingNote) {
689
801
  return existingNote.filename;
690
802
  }
691
803
  // Create a new one using detected configuration
692
- const filePath = buildCalendarNotePath(dateStr);
693
- writeNoteFile(filePath, '');
804
+ const filePath = await buildCalendarNotePathAsync(dateStr);
805
+ // Otherwise the addToToday / append-to-date flows could materialize a
806
+ // file inside a denied folder before the asserting writer downstream
807
+ // gets a chance to reject the write.
808
+ assertFolderAllowed(filePath, 'create calendar note in');
809
+ await writeNoteFile(filePath, '');
694
810
  return filePath;
695
811
  }
696
812
  //# sourceMappingURL=file-writer.js.map