@noteplanco/noteplan-mcp 1.1.23 → 1.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/noteplan/attachments-paths.d.ts +13 -0
- package/dist/noteplan/attachments-paths.d.ts.map +1 -0
- package/dist/noteplan/attachments-paths.js +27 -0
- package/dist/noteplan/attachments-paths.js.map +1 -0
- package/dist/noteplan/embeddings.js +1 -1
- package/dist/noteplan/embeddings.js.map +1 -1
- package/dist/noteplan/file-reader.d.ts +37 -46
- package/dist/noteplan/file-reader.d.ts.map +1 -1
- package/dist/noteplan/file-reader.js +200 -202
- package/dist/noteplan/file-reader.js.map +1 -1
- package/dist/noteplan/file-reader.test.d.ts +2 -0
- package/dist/noteplan/file-reader.test.d.ts.map +1 -0
- package/dist/noteplan/file-reader.test.js +67 -0
- package/dist/noteplan/file-reader.test.js.map +1 -0
- package/dist/noteplan/file-writer.d.ts +35 -31
- package/dist/noteplan/file-writer.d.ts.map +1 -1
- package/dist/noteplan/file-writer.js +280 -164
- package/dist/noteplan/file-writer.js.map +1 -1
- package/dist/noteplan/file-writer.test.js +704 -191
- package/dist/noteplan/file-writer.test.js.map +1 -1
- package/dist/noteplan/filter-store.d.ts +5 -5
- package/dist/noteplan/filter-store.d.ts.map +1 -1
- package/dist/noteplan/filter-store.js +94 -79
- package/dist/noteplan/filter-store.js.map +1 -1
- package/dist/noteplan/ripgrep-search.d.ts +25 -2
- package/dist/noteplan/ripgrep-search.d.ts.map +1 -1
- package/dist/noteplan/ripgrep-search.js +75 -2
- package/dist/noteplan/ripgrep-search.js.map +1 -1
- package/dist/noteplan/space-row-utils.d.ts +20 -0
- package/dist/noteplan/space-row-utils.d.ts.map +1 -0
- package/dist/noteplan/space-row-utils.js +78 -0
- package/dist/noteplan/space-row-utils.js.map +1 -0
- package/dist/noteplan/space-row-utils.test.d.ts +2 -0
- package/dist/noteplan/space-row-utils.test.d.ts.map +1 -0
- package/dist/noteplan/space-row-utils.test.js +123 -0
- package/dist/noteplan/space-row-utils.test.js.map +1 -0
- package/dist/noteplan/sqlite-reader.d.ts +12 -27
- package/dist/noteplan/sqlite-reader.d.ts.map +1 -1
- package/dist/noteplan/sqlite-reader.js +315 -221
- package/dist/noteplan/sqlite-reader.js.map +1 -1
- package/dist/noteplan/sqlite-writer.d.ts +1 -1
- package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
- package/dist/noteplan/sqlite-writer.js +2 -2
- package/dist/noteplan/sqlite-writer.js.map +1 -1
- package/dist/noteplan/unified-store.d.ts +41 -30
- package/dist/noteplan/unified-store.d.ts.map +1 -1
- package/dist/noteplan/unified-store.js +257 -159
- package/dist/noteplan/unified-store.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +142 -61
- package/dist/server.js.map +1 -1
- package/dist/tools/attachments.d.ts +9 -9
- package/dist/tools/attachments.d.ts.map +1 -1
- package/dist/tools/attachments.js +74 -83
- package/dist/tools/attachments.js.map +1 -1
- package/dist/tools/attachments.test.js +170 -129
- package/dist/tools/attachments.test.js.map +1 -1
- package/dist/tools/calendar.d.ts +16 -13
- package/dist/tools/calendar.d.ts.map +1 -1
- package/dist/tools/calendar.js +17 -16
- package/dist/tools/calendar.js.map +1 -1
- package/dist/tools/embeddings.d.ts +6 -6
- package/dist/tools/embeddings.d.ts.map +1 -1
- package/dist/tools/embeddings.js +6 -6
- package/dist/tools/embeddings.js.map +1 -1
- package/dist/tools/events.d.ts +7 -3
- package/dist/tools/events.d.ts.map +1 -1
- package/dist/tools/events.js +51 -16
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/filters.d.ts +28 -33
- package/dist/tools/filters.d.ts.map +1 -1
- package/dist/tools/filters.js +42 -105
- package/dist/tools/filters.js.map +1 -1
- package/dist/tools/notes.d.ts +80 -218
- package/dist/tools/notes.d.ts.map +1 -1
- package/dist/tools/notes.js +180 -177
- package/dist/tools/notes.js.map +1 -1
- package/dist/tools/notes.test.js +242 -21
- package/dist/tools/notes.test.js.map +1 -1
- package/dist/tools/search.d.ts +4 -3
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +9 -5
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/search.test.d.ts +2 -0
- package/dist/tools/search.test.d.ts.map +1 -0
- package/dist/tools/search.test.js +37 -0
- package/dist/tools/search.test.js.map +1 -0
- package/dist/tools/spaces.d.ts +20 -20
- package/dist/tools/spaces.d.ts.map +1 -1
- package/dist/tools/spaces.js +28 -28
- package/dist/tools/spaces.js.map +1 -1
- package/dist/tools/tasks.d.ts +22 -22
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +22 -22
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/templates.d.ts +7 -7
- package/dist/tools/templates.d.ts.map +1 -1
- package/dist/tools/templates.js +4 -4
- package/dist/tools/templates.js.map +1 -1
- package/dist/tools/themes.d.ts.map +1 -1
- package/dist/tools/themes.js +26 -35
- package/dist/tools/themes.js.map +1 -1
- package/dist/transport/bridge-availability.d.ts +5 -0
- package/dist/transport/bridge-availability.d.ts.map +1 -0
- package/dist/transport/bridge-availability.js +92 -0
- package/dist/transport/bridge-availability.js.map +1 -0
- package/dist/transport/bridge-cascade.d.ts +18 -0
- package/dist/transport/bridge-cascade.d.ts.map +1 -0
- package/dist/transport/bridge-cascade.js +78 -0
- package/dist/transport/bridge-cascade.js.map +1 -0
- package/dist/transport/bridge-cascade.test.d.ts +2 -0
- package/dist/transport/bridge-cascade.test.d.ts.map +1 -0
- package/dist/transport/bridge-cascade.test.js +160 -0
- package/dist/transport/bridge-cascade.test.js.map +1 -0
- package/dist/transport/bridge-client.d.ts +197 -0
- package/dist/transport/bridge-client.d.ts.map +1 -0
- package/dist/transport/bridge-client.js +288 -0
- package/dist/transport/bridge-client.js.map +1 -0
- package/dist/transport/bridge-client.test.d.ts +2 -0
- package/dist/transport/bridge-client.test.d.ts.map +1 -0
- package/dist/transport/bridge-client.test.js +384 -0
- package/dist/transport/bridge-client.test.js.map +1 -0
- package/dist/transport/bridge-context.d.ts +10 -0
- package/dist/transport/bridge-context.d.ts.map +1 -0
- package/dist/transport/bridge-context.js +18 -0
- package/dist/transport/bridge-context.js.map +1 -0
- package/dist/transport/bridge-fs.d.ts +25 -0
- package/dist/transport/bridge-fs.d.ts.map +1 -0
- package/dist/transport/bridge-fs.js +129 -0
- package/dist/transport/bridge-fs.js.map +1 -0
- package/dist/utils/date-utils.d.ts +24 -0
- package/dist/utils/date-utils.d.ts.map +1 -1
- package/dist/utils/date-utils.js +55 -0
- package/dist/utils/date-utils.js.map +1 -1
- package/dist/utils/date-utils.test.d.ts +2 -0
- package/dist/utils/date-utils.test.d.ts.map +1 -0
- package/dist/utils/date-utils.test.js +109 -0
- package/dist/utils/date-utils.test.js.map +1 -0
- package/dist/utils/folder-access.d.ts +23 -0
- package/dist/utils/folder-access.d.ts.map +1 -0
- package/dist/utils/folder-access.js +131 -0
- package/dist/utils/folder-access.js.map +1 -0
- package/dist/utils/folder-access.test.d.ts +2 -0
- package/dist/utils/folder-access.test.d.ts.map +1 -0
- package/dist/utils/folder-access.test.js +182 -0
- package/dist/utils/folder-access.test.js.map +1 -0
- package/dist/utils/folder-matcher.d.ts.map +1 -1
- package/dist/utils/folder-matcher.js +16 -0
- package/dist/utils/folder-matcher.js.map +1 -1
- package/dist/utils/folder-matcher.test.js +42 -0
- package/dist/utils/folder-matcher.test.js.map +1 -1
- package/dist/utils/server-config.d.ts +10 -2
- package/dist/utils/server-config.d.ts.map +1 -1
- package/dist/utils/server-config.js +16 -2
- package/dist/utils/server-config.js.map +1 -1
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +5 -1
- package/dist/utils/version.js.map +1 -1
- package/package.json +4 -3
- package/scripts/calendar-helper +0 -0
- 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,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
*
|
|
197
|
-
*
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
221
|
-
}
|
|
222
|
-
catch (
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
203
|
+
throw err;
|
|
232
204
|
}
|
|
233
205
|
}
|
|
234
206
|
/**
|
|
235
|
-
*
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
249
|
-
|
|
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 (
|
|
253
|
-
throw new Error(`Note already exists: ${
|
|
246
|
+
if (await pathExists(altFullPath)) {
|
|
247
|
+
throw new Error(`Note already exists: ${altRelative}`);
|
|
254
248
|
}
|
|
255
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
334
|
-
|
|
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 (
|
|
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
|
-
|
|
425
|
+
const sourceStat = await statPath(fullPath);
|
|
426
|
+
if (!sourceStat.exists) {
|
|
356
427
|
throw new Error(`Note not found: ${filePath}`);
|
|
357
428
|
}
|
|
358
|
-
if (
|
|
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 (
|
|
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
|
|
379
|
-
toFilename
|
|
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 (!
|
|
392
|
-
|
|
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
|
-
|
|
481
|
+
const sourceStat = await statPath(fullPath);
|
|
482
|
+
if (!sourceStat.exists) {
|
|
403
483
|
throw new Error(`Note not found: ${filePath}`);
|
|
404
484
|
}
|
|
405
|
-
if (
|
|
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 (
|
|
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
|
|
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 (!
|
|
432
|
-
|
|
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
|
-
|
|
528
|
+
const sourceStat = await statPath(fullPath);
|
|
529
|
+
if (!sourceStat.exists) {
|
|
443
530
|
throw new Error(`Note not found: ${filePath}`);
|
|
444
531
|
}
|
|
445
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
499
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
535
|
-
|
|
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 (
|
|
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
|
-
|
|
654
|
+
const sourceStat = await statPath(sourcePath);
|
|
655
|
+
if (!sourceStat.exists) {
|
|
557
656
|
throw new Error(`Source folder not found: ${normalizedSource}`);
|
|
558
657
|
}
|
|
559
|
-
if (!
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
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
|
|
586
|
-
toFolder
|
|
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
|
-
|
|
717
|
+
const sourceStat = await statPath(sourcePath);
|
|
718
|
+
if (!sourceStat.exists) {
|
|
611
719
|
throw new Error(`Source folder not found: ${normalizedSource}`);
|
|
612
720
|
}
|
|
613
|
-
if (!
|
|
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 (
|
|
762
|
+
if (await pathExists(targetPath)) {
|
|
655
763
|
throw new Error(`A folder with this name already exists: ${path.relative(getNotesPath(), targetPath)}`);
|
|
656
764
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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 =
|
|
693
|
-
|
|
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
|