@noteplanco/noteplan-mcp 1.1.21 → 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 (177) 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/frontmatter-parser.d.ts.map +1 -1
  29. package/dist/noteplan/frontmatter-parser.js +5 -4
  30. package/dist/noteplan/frontmatter-parser.js.map +1 -1
  31. package/dist/noteplan/frontmatter-parser.test.js +44 -0
  32. package/dist/noteplan/frontmatter-parser.test.js.map +1 -1
  33. package/dist/noteplan/markdown-parser.js +1 -1
  34. package/dist/noteplan/markdown-parser.js.map +1 -1
  35. package/dist/noteplan/markdown-parser.test.js +194 -0
  36. package/dist/noteplan/markdown-parser.test.js.map +1 -1
  37. package/dist/noteplan/preferences.d.ts +1 -0
  38. package/dist/noteplan/preferences.d.ts.map +1 -1
  39. package/dist/noteplan/preferences.js +1 -0
  40. package/dist/noteplan/preferences.js.map +1 -1
  41. package/dist/noteplan/ripgrep-search.d.ts +25 -2
  42. package/dist/noteplan/ripgrep-search.d.ts.map +1 -1
  43. package/dist/noteplan/ripgrep-search.js +75 -2
  44. package/dist/noteplan/ripgrep-search.js.map +1 -1
  45. package/dist/noteplan/space-row-utils.d.ts +20 -0
  46. package/dist/noteplan/space-row-utils.d.ts.map +1 -0
  47. package/dist/noteplan/space-row-utils.js +78 -0
  48. package/dist/noteplan/space-row-utils.js.map +1 -0
  49. package/dist/noteplan/space-row-utils.test.d.ts +2 -0
  50. package/dist/noteplan/space-row-utils.test.d.ts.map +1 -0
  51. package/dist/noteplan/space-row-utils.test.js +123 -0
  52. package/dist/noteplan/space-row-utils.test.js.map +1 -0
  53. package/dist/noteplan/sqlite-reader.d.ts +12 -27
  54. package/dist/noteplan/sqlite-reader.d.ts.map +1 -1
  55. package/dist/noteplan/sqlite-reader.js +325 -223
  56. package/dist/noteplan/sqlite-reader.js.map +1 -1
  57. package/dist/noteplan/sqlite-writer.d.ts +1 -1
  58. package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
  59. package/dist/noteplan/sqlite-writer.js +2 -2
  60. package/dist/noteplan/sqlite-writer.js.map +1 -1
  61. package/dist/noteplan/unified-store.d.ts +41 -30
  62. package/dist/noteplan/unified-store.d.ts.map +1 -1
  63. package/dist/noteplan/unified-store.js +307 -161
  64. package/dist/noteplan/unified-store.js.map +1 -1
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +143 -62
  67. package/dist/server.js.map +1 -1
  68. package/dist/tools/attachments.d.ts +9 -9
  69. package/dist/tools/attachments.d.ts.map +1 -1
  70. package/dist/tools/attachments.js +74 -83
  71. package/dist/tools/attachments.js.map +1 -1
  72. package/dist/tools/attachments.test.js +170 -129
  73. package/dist/tools/attachments.test.js.map +1 -1
  74. package/dist/tools/calendar.d.ts +36 -13
  75. package/dist/tools/calendar.d.ts.map +1 -1
  76. package/dist/tools/calendar.js +44 -17
  77. package/dist/tools/calendar.js.map +1 -1
  78. package/dist/tools/embeddings.d.ts +6 -6
  79. package/dist/tools/embeddings.d.ts.map +1 -1
  80. package/dist/tools/embeddings.js +6 -6
  81. package/dist/tools/embeddings.js.map +1 -1
  82. package/dist/tools/events.d.ts +7 -3
  83. package/dist/tools/events.d.ts.map +1 -1
  84. package/dist/tools/events.js +51 -16
  85. package/dist/tools/events.js.map +1 -1
  86. package/dist/tools/filters.d.ts +28 -33
  87. package/dist/tools/filters.d.ts.map +1 -1
  88. package/dist/tools/filters.js +42 -105
  89. package/dist/tools/filters.js.map +1 -1
  90. package/dist/tools/notes.d.ts +80 -218
  91. package/dist/tools/notes.d.ts.map +1 -1
  92. package/dist/tools/notes.js +194 -180
  93. package/dist/tools/notes.js.map +1 -1
  94. package/dist/tools/notes.test.js +501 -21
  95. package/dist/tools/notes.test.js.map +1 -1
  96. package/dist/tools/search.d.ts +4 -3
  97. package/dist/tools/search.d.ts.map +1 -1
  98. package/dist/tools/search.js +9 -5
  99. package/dist/tools/search.js.map +1 -1
  100. package/dist/tools/search.test.d.ts +2 -0
  101. package/dist/tools/search.test.d.ts.map +1 -0
  102. package/dist/tools/search.test.js +37 -0
  103. package/dist/tools/search.test.js.map +1 -0
  104. package/dist/tools/spaces.d.ts +20 -20
  105. package/dist/tools/spaces.d.ts.map +1 -1
  106. package/dist/tools/spaces.js +28 -28
  107. package/dist/tools/spaces.js.map +1 -1
  108. package/dist/tools/tasks.d.ts +22 -22
  109. package/dist/tools/tasks.d.ts.map +1 -1
  110. package/dist/tools/tasks.js +22 -22
  111. package/dist/tools/tasks.js.map +1 -1
  112. package/dist/tools/templates.d.ts +7 -7
  113. package/dist/tools/templates.d.ts.map +1 -1
  114. package/dist/tools/templates.js +4 -4
  115. package/dist/tools/templates.js.map +1 -1
  116. package/dist/tools/themes.js +1 -1
  117. package/dist/tools/themes.js.map +1 -1
  118. package/dist/transport/bridge-availability.d.ts +5 -0
  119. package/dist/transport/bridge-availability.d.ts.map +1 -0
  120. package/dist/transport/bridge-availability.js +92 -0
  121. package/dist/transport/bridge-availability.js.map +1 -0
  122. package/dist/transport/bridge-cascade.d.ts +18 -0
  123. package/dist/transport/bridge-cascade.d.ts.map +1 -0
  124. package/dist/transport/bridge-cascade.js +78 -0
  125. package/dist/transport/bridge-cascade.js.map +1 -0
  126. package/dist/transport/bridge-cascade.test.d.ts +2 -0
  127. package/dist/transport/bridge-cascade.test.d.ts.map +1 -0
  128. package/dist/transport/bridge-cascade.test.js +160 -0
  129. package/dist/transport/bridge-cascade.test.js.map +1 -0
  130. package/dist/transport/bridge-client.d.ts +197 -0
  131. package/dist/transport/bridge-client.d.ts.map +1 -0
  132. package/dist/transport/bridge-client.js +288 -0
  133. package/dist/transport/bridge-client.js.map +1 -0
  134. package/dist/transport/bridge-client.test.d.ts +2 -0
  135. package/dist/transport/bridge-client.test.d.ts.map +1 -0
  136. package/dist/transport/bridge-client.test.js +384 -0
  137. package/dist/transport/bridge-client.test.js.map +1 -0
  138. package/dist/transport/bridge-context.d.ts +10 -0
  139. package/dist/transport/bridge-context.d.ts.map +1 -0
  140. package/dist/transport/bridge-context.js +18 -0
  141. package/dist/transport/bridge-context.js.map +1 -0
  142. package/dist/transport/bridge-fs.d.ts +25 -0
  143. package/dist/transport/bridge-fs.d.ts.map +1 -0
  144. package/dist/transport/bridge-fs.js +129 -0
  145. package/dist/transport/bridge-fs.js.map +1 -0
  146. package/dist/utils/date-utils.d.ts +24 -0
  147. package/dist/utils/date-utils.d.ts.map +1 -1
  148. package/dist/utils/date-utils.js +55 -0
  149. package/dist/utils/date-utils.js.map +1 -1
  150. package/dist/utils/date-utils.test.d.ts +2 -0
  151. package/dist/utils/date-utils.test.d.ts.map +1 -0
  152. package/dist/utils/date-utils.test.js +109 -0
  153. package/dist/utils/date-utils.test.js.map +1 -0
  154. package/dist/utils/folder-access.d.ts +23 -0
  155. package/dist/utils/folder-access.d.ts.map +1 -0
  156. package/dist/utils/folder-access.js +131 -0
  157. package/dist/utils/folder-access.js.map +1 -0
  158. package/dist/utils/folder-access.test.d.ts +2 -0
  159. package/dist/utils/folder-access.test.d.ts.map +1 -0
  160. package/dist/utils/folder-access.test.js +182 -0
  161. package/dist/utils/folder-access.test.js.map +1 -0
  162. package/dist/utils/folder-matcher.d.ts.map +1 -1
  163. package/dist/utils/folder-matcher.js +16 -0
  164. package/dist/utils/folder-matcher.js.map +1 -1
  165. package/dist/utils/folder-matcher.test.js +42 -0
  166. package/dist/utils/folder-matcher.test.js.map +1 -1
  167. package/dist/utils/server-config.d.ts +10 -2
  168. package/dist/utils/server-config.d.ts.map +1 -1
  169. package/dist/utils/server-config.js +16 -2
  170. package/dist/utils/server-config.js.map +1 -1
  171. package/dist/utils/version.d.ts +2 -0
  172. package/dist/utils/version.d.ts.map +1 -1
  173. package/dist/utils/version.js +5 -1
  174. package/dist/utils/version.js.map +1 -1
  175. package/package.json +4 -3
  176. package/scripts/calendar-helper +0 -0
  177. package/scripts/reminders-helper +0 -0
@@ -7,6 +7,8 @@ import { ensureTemplateFrontmatter } from './templates.js';
7
7
  import { issueConfirmationToken, validateAndConsumeConfirmationToken, } from '../utils/confirmation-tokens.js';
8
8
  import { parseParagraphLine, parseAllParagraphLines, buildParagraphLine } from '../noteplan/markdown-parser.js';
9
9
  import { normalizeFilename } from '../utils/filename-normalize.js';
10
+ import { normalizePeriodicTitle, isCanonicalPeriodicTitle, parseFlexibleDate } from '../utils/date-utils.js';
11
+ import { getBridgeClient } from '../transport/bridge-availability.js';
10
12
  function toBoundedInt(value, defaultValue, min, max) {
11
13
  const numeric = typeof value === 'number' ? value : Number(value);
12
14
  if (!Number.isFinite(numeric))
@@ -57,37 +59,37 @@ function confirmationFailureMessage(toolName, reason) {
57
59
  }
58
60
  return `Confirmation token is invalid for ${toolName}. ${refreshHint}`;
59
61
  }
60
- function resolveNoteTarget(id, filename, space) {
62
+ async function resolveNoteTarget(id, filename, space) {
61
63
  const raw = (id && id.trim().length > 0 ? id : filename)?.trim();
62
64
  if (!raw) {
63
65
  return { identifier: '', note: null };
64
66
  }
65
67
  const identifier = normalizeFilename(raw);
66
68
  const note = id
67
- ? store.getNote({ id: identifier, space }) ?? store.getNote({ filename: identifier, space })
68
- : store.getNote({ filename: identifier, space });
69
+ ? (await await store.getNote({ id: identifier, space })) ?? (await await store.getNote({ filename: identifier, space }))
70
+ : await await store.getNote({ filename: identifier, space });
69
71
  return {
70
72
  identifier,
71
73
  note,
72
74
  };
73
75
  }
74
- export function resolveWritableNoteReference(input) {
76
+ export async function resolveWritableNoteReference(input) {
75
77
  if (input.id && input.id.trim().length > 0) {
76
78
  const normalizedId = normalizeFilename(input.id.trim());
77
- const note = store.getNote({ id: normalizedId, space: input.space?.trim() });
79
+ const note = await await store.getNote({ id: normalizedId, space: input.space?.trim() });
78
80
  return { note, error: note ? undefined : 'Note not found' };
79
81
  }
80
82
  if (input.filename && input.filename.trim().length > 0) {
81
83
  const normalizedFn = normalizeFilename(input.filename.trim());
82
- const note = store.getNote({ filename: normalizedFn, space: input.space?.trim() });
84
+ const note = await await store.getNote({ filename: normalizedFn, space: input.space?.trim() });
83
85
  return { note, error: note ? undefined : 'Note not found' };
84
86
  }
85
87
  if (input.date && input.date.trim().length > 0) {
86
- let note = store.getNote({ date: input.date.trim(), space: input.space?.trim() });
88
+ let note = await await store.getNote({ date: input.date.trim(), space: input.space?.trim() });
87
89
  if (!note) {
88
90
  // Auto-create calendar notes on the fly (matches NotePlan native behavior)
89
91
  try {
90
- note = store.ensureCalendarNote(input.date.trim(), input.space?.trim());
92
+ note = await await store.ensureCalendarNote(input.date.trim(), input.space?.trim());
91
93
  }
92
94
  catch {
93
95
  return { note: null, error: 'Failed to create calendar note for date' };
@@ -97,14 +99,14 @@ export function resolveWritableNoteReference(input) {
97
99
  }
98
100
  const textQuery = input.query?.trim() || input.title?.trim();
99
101
  if (textQuery) {
100
- const resolved = resolveNote({
102
+ const resolved = (await resolveNote({
101
103
  query: textQuery,
102
104
  space: input.space?.trim(),
103
105
  types: ['note', 'calendar'],
104
106
  limit: 5,
105
107
  minScore: 0.88,
106
108
  ambiguityDelta: 0.06,
107
- });
109
+ }));
108
110
  if (resolved.success !== true) {
109
111
  return { note: null, error: 'Could not resolve note query' };
110
112
  }
@@ -120,7 +122,7 @@ export function resolveWritableNoteReference(input) {
120
122
  if (!identifier) {
121
123
  return { note: null, error: 'Could not resolve note target' };
122
124
  }
123
- const note = store.getNote({ id: identifier, space: input.space?.trim() }) ?? store.getNote({ filename: identifier, space: input.space?.trim() });
125
+ const note = (await await store.getNote({ id: identifier, space: input.space?.trim() })) ?? (await await store.getNote({ filename: identifier, space: input.space?.trim() }));
124
126
  return { note, error: note ? undefined : 'Resolved note no longer exists' };
125
127
  }
126
128
  return {
@@ -262,6 +264,11 @@ export const getNoteSchema = z.object({
262
264
  .optional()
263
265
  .default(280)
264
266
  .describe('Preview length when includeContent=false (default: 280)'),
267
+ format: z
268
+ .enum(['flat', 'lines', 'both'])
269
+ .optional()
270
+ .default('flat')
271
+ .describe('Response shape when includeContent=true. "flat" (default): only the joined "content" string — token-efficient, fine for reading. "lines": only the per-line "lines" array — use when you need line numbers for follow-up edits. "both": both fields (highest token cost; previous default).'),
265
272
  });
266
273
  export const listNotesSchema = z.object({
267
274
  folder: z
@@ -297,9 +304,17 @@ export const resolveNoteSchema = z.object({
297
304
  .describe('If top scores are within this delta, treat as ambiguous'),
298
305
  });
299
306
  export const createNoteSchema = z.object({
300
- title: z.string().describe('Title for the new note'),
307
+ title: z.string().optional().describe('Title for the new note. Required for project notes. Ignored for calendar notes (their identifier comes from `date` or a periodic title).'),
308
+ date: z
309
+ .string()
310
+ .optional()
311
+ .describe('Periodic-note identifier — set this to create a calendar note at `Calendar/{date}.{ext}`. Accepts daily (`YYYY-MM-DD`, `YYYYMMDD`, `today`/`tomorrow`/`yesterday`), weekly (`YYYY-Www` — sloppy `2026-W4` is normalized to `2026-W04`), monthly (`YYYY-MM`), quarterly (`YYYY-Qn`), and yearly (`YYYY`). Title and folder are ignored when date is present.'),
312
+ filename: z
313
+ .string()
314
+ .optional()
315
+ .describe('Optional on-disk basename for the note (e.g. "_context.md"). Independent of title — the title still controls the H1 inside the file. Path separators are rejected; pass folder via the "folder" parameter. Extension is preserved if .md/.txt, otherwise the configured default extension is appended. Project notes only — space and calendar notes derive filenames automatically.'),
301
316
  content: z.string().optional().describe('Initial content for the note. Can include YAML frontmatter between --- delimiters for styling (icon, icon-color, bg-color, bg-color-dark, bg-pattern, status, priority, summary, type, domain)'),
302
- folder: z.string().optional().describe('Folder to create the note in. Supports smart matching (e.g., "projects" matches "10 - Projects")'),
317
+ folder: z.string().optional().describe('Folder to create the note in. Supports smart matching (e.g., "projects" matches "10 - Projects"). Setting this to "Calendar" together with a periodic title (e.g. `2026-W16`) is treated as creating a calendar note.'),
303
318
  create_new_folder: z.boolean().optional().describe('Set to true to create a new folder instead of matching existing ones'),
304
319
  space: z.string().optional().describe('Space name or ID to create in (e.g., "My Team" or a UUID)'),
305
320
  noteType: z.enum(['note', 'template']).optional().default('note').describe('Type of note to create. Use "template" to create in @Templates with proper frontmatter'),
@@ -319,14 +334,6 @@ export const updateNoteSchema = z.object({
319
334
  .boolean()
320
335
  .optional()
321
336
  .describe('Required safety confirmation for whole-note rewrite. Must be true to proceed.'),
322
- dryRun: z
323
- .boolean()
324
- .optional()
325
- .describe('Preview full-rewrite impact and get confirmationToken without modifying the note'),
326
- confirmationToken: z
327
- .string()
328
- .optional()
329
- .describe('Confirmation token issued by dryRun for full note rewrite'),
330
337
  allowEmptyContent: z
331
338
  .boolean()
332
339
  .optional()
@@ -450,8 +457,8 @@ export const restoreNoteSchema = z.object({
450
457
  }
451
458
  });
452
459
  // Tool implementations
453
- export function getNote(params) {
454
- const note = store.getNote(params);
460
+ export async function getNote(params) {
461
+ const note = await store.getNote(params);
455
462
  if (!note) {
456
463
  return {
457
464
  success: false,
@@ -509,28 +516,44 @@ export function getNote(params) {
509
516
  result.limit = lineWindow.limit;
510
517
  result.hasMore = lineWindow.hasMore;
511
518
  result.nextCursor = lineWindow.nextCursor;
512
- result.content = lineWindow.content;
513
- result.lines = lineWindow.lines;
519
+ const format = params.format ?? 'flat';
520
+ if (format !== 'lines') {
521
+ result.content = lineWindow.content;
522
+ }
523
+ if (format !== 'flat') {
524
+ result.lines = lineWindow.lines;
525
+ }
514
526
  if (lineWindow.hasMore) {
515
527
  result.performanceHints = [NEXT_CURSOR_HINT];
516
528
  }
517
529
  return result;
518
530
  }
519
- export function listNotes(params) {
531
+ export async function listNotes(params) {
520
532
  const input = params ?? {};
521
- const notes = store.listNotes({
533
+ const notes = await store.listNotes({
522
534
  folder: input.folder,
523
535
  space: input.space,
524
536
  });
525
537
  const allowedTypes = input.types ? new Set(input.types) : null;
526
538
  const query = typeof input.query === 'string' ? input.query.trim().toLowerCase() : undefined;
539
+ // Pre-compute query tokens outside the per-note filter loop
540
+ const queryAlternatives = query
541
+ ? query.split('|').map((alt) => {
542
+ const words = alt.trim().replace(/_/g, ' ').split(/\s+/).filter(Boolean);
543
+ return words.length > 0 ? words : null;
544
+ }).filter((words) => words !== null)
545
+ : null;
527
546
  const filtered = notes.filter((note) => {
528
547
  if (allowedTypes && !allowedTypes.has(note.type))
529
548
  return false;
530
- if (!query)
549
+ if (!queryAlternatives)
531
550
  return true;
532
- const haystack = `${note.title} ${note.filename} ${note.folder || ''}`.toLowerCase();
533
- return haystack.includes(query);
551
+ // Normalize underscores to spaces so "knuth_reviewer" matches "knuth reviewer"
552
+ const haystack = `${note.title} ${note.filename} ${note.folder || ''}`
553
+ .toLowerCase()
554
+ .replace(/_/g, ' ');
555
+ // OR across alternatives; AND within each alternative's words
556
+ return queryAlternatives.some((words) => words.every((word) => haystack.includes(word)));
534
557
  });
535
558
  const offset = toBoundedInt(input.cursor ?? input.offset, 0, 0, Number.MAX_SAFE_INTEGER);
536
559
  const limit = toBoundedInt(input.limit, 50, 1, 500);
@@ -590,7 +613,7 @@ function noteMatchScore(note, query, queryDateToken) {
590
613
  return 0.76;
591
614
  return 0;
592
615
  }
593
- export function resolveNote(params) {
616
+ export async function resolveNote(params) {
594
617
  const query = typeof params?.query === 'string' ? params.query.trim() : '';
595
618
  if (!query) {
596
619
  return {
@@ -606,7 +629,7 @@ export function resolveNote(params) {
606
629
  const includeStageTimings = isDebugTimingsEnabled(params.debugTimings);
607
630
  const stageTimings = {};
608
631
  const listStart = Date.now();
609
- const notes = store.listNotes({
632
+ const notes = await store.listNotes({
610
633
  folder: params.folder,
611
634
  space: params.space,
612
635
  });
@@ -703,17 +726,59 @@ export function resolveNote(params) {
703
726
  }
704
727
  return result;
705
728
  }
706
- export function createNote(params) {
729
+ export async function createNote(params) {
707
730
  try {
731
+ // Auto-route to calendar only when intent is unambiguous. A project
732
+ // note titled "2026" would otherwise be hijacked, so sloppy variants
733
+ // (e.g. "2026-W4") require an explicit folder=Calendar or date.
734
+ const titlePeriodic = params.title ? normalizePeriodicTitle(params.title) : null;
735
+ const folderHintsCalendar = (params.folder ?? '').trim().toLowerCase() === 'calendar';
736
+ let calendarDate = null;
737
+ if (params.date) {
738
+ const normalizedDate = normalizePeriodicTitle(parseFlexibleDate(params.date));
739
+ if (!normalizedDate) {
740
+ throw new Error(`Invalid calendar date "${params.date}". Use a periodic identifier like "today", "2026-05-07", "20260507", "2026-W16", "2026-05", "2026-Q2", or "2026".`);
741
+ }
742
+ calendarDate = normalizedDate;
743
+ }
744
+ else if (folderHintsCalendar) {
745
+ if (!titlePeriodic) {
746
+ throw new Error('folder="Calendar" requires a periodic title (e.g. "2026-W16", "2026-05", "2026-Q2", "20260507") or pass `date` directly. Got title: ' +
747
+ JSON.stringify(params.title ?? null));
748
+ }
749
+ calendarDate = titlePeriodic;
750
+ }
751
+ else if (params.title && isCanonicalPeriodicTitle(params.title)) {
752
+ calendarDate = params.title;
753
+ }
754
+ if (calendarDate) {
755
+ const result = await store.createNote(params.title ?? calendarDate, params.content, {
756
+ space: params.space,
757
+ filename: params.filename,
758
+ calendarDate,
759
+ });
760
+ return {
761
+ success: true,
762
+ tip: 'Calendar notes are auto-created when you write to them via noteplan_edit_content / noteplan_paragraphs with a `date` parameter — explicit create is rarely needed.',
763
+ note: {
764
+ title: result.note.title,
765
+ filename: result.note.filename,
766
+ type: result.note.type,
767
+ source: result.note.source,
768
+ folder: result.note.folder,
769
+ },
770
+ };
771
+ }
708
772
  const isTemplate = params.noteType === 'template';
709
773
  const folder = isTemplate && !params.folder ? '@Templates' : params.folder;
710
774
  const content = isTemplate
711
- ? ensureTemplateFrontmatter(params.title, params.content, params.templateTypes)
775
+ ? ensureTemplateFrontmatter(params.title ?? '', params.content, params.templateTypes)
712
776
  : params.content;
713
- const result = store.createNote(params.title, content, {
777
+ const result = await store.createNote(params.title ?? '', content, {
714
778
  folder,
715
779
  space: params.space,
716
780
  createNewFolder: params.create_new_folder,
781
+ filename: params.filename,
717
782
  });
718
783
  return {
719
784
  success: true,
@@ -742,7 +807,7 @@ export function createNote(params) {
742
807
  };
743
808
  }
744
809
  }
745
- export function updateNote(params) {
810
+ export async function updateNote(params) {
746
811
  try {
747
812
  if (params.fullReplace !== true) {
748
813
  return {
@@ -750,7 +815,7 @@ export function updateNote(params) {
750
815
  error: 'Full note replacement is blocked for noteplan_update_note unless fullReplace=true. Prefer noteplan_search_paragraphs + noteplan_edit_line/insert_content/delete_lines for targeted edits.',
751
816
  };
752
817
  }
753
- const noteRef = resolveWritableNoteReference(params);
818
+ const noteRef = await resolveWritableNoteReference(params);
754
819
  if (!noteRef.note) {
755
820
  return {
756
821
  success: false,
@@ -765,45 +830,8 @@ export function updateNote(params) {
765
830
  error: 'Empty content is blocked for noteplan_update_note. Use allowEmptyContent=true to override intentionally.',
766
831
  };
767
832
  }
768
- // Use the resolved filename as the confirmation token target for consistency
769
- const confirmationTarget = existingNote.filename;
770
- if (isTrueBool(params.dryRun)) {
771
- const token = issueConfirmationToken({
772
- tool: 'noteplan_update_note',
773
- target: confirmationTarget,
774
- action: 'full_replace',
775
- });
776
- return {
777
- success: true,
778
- dryRun: true,
779
- message: `Dry run: note ${existingNote.filename} would be fully replaced`,
780
- note: {
781
- id: existingNote.id,
782
- title: existingNote.title,
783
- filename: existingNote.filename,
784
- type: existingNote.type,
785
- source: existingNote.source,
786
- folder: existingNote.folder,
787
- spaceId: existingNote.spaceId,
788
- },
789
- currentContentLength: existingNote.content.length,
790
- newContentLength: params.content.length,
791
- ...token,
792
- };
793
- }
794
- const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
795
- tool: 'noteplan_update_note',
796
- target: confirmationTarget,
797
- action: 'full_replace',
798
- });
799
- if (!confirmation.ok) {
800
- return {
801
- success: false,
802
- error: confirmationFailureMessage('noteplan_update_note', confirmation.reason),
803
- };
804
- }
805
833
  const writeTarget = getWritableIdentifier(existingNote);
806
- const note = store.updateNote(writeTarget.identifier, params.content, {
834
+ const note = await store.updateNote(writeTarget.identifier, params.content, {
807
835
  source: writeTarget.source,
808
836
  });
809
837
  return {
@@ -823,9 +851,9 @@ export function updateNote(params) {
823
851
  };
824
852
  }
825
853
  }
826
- export function deleteNote(params) {
854
+ export async function deleteNote(params) {
827
855
  try {
828
- const target = resolveNoteTarget(params.id, params.filename, params.space);
856
+ const target = await resolveNoteTarget(params.id, params.filename, params.space);
829
857
  const note = target.note;
830
858
  if (!note) {
831
859
  return {
@@ -866,7 +894,7 @@ export function deleteNote(params) {
866
894
  error: confirmationFailureMessage('noteplan_delete_note', confirmation.reason),
867
895
  };
868
896
  }
869
- const deleted = store.deleteNote(target.identifier);
897
+ const deleted = await store.deleteNote(target.identifier);
870
898
  return {
871
899
  success: true,
872
900
  message: deleted.source === 'space'
@@ -886,9 +914,9 @@ export function deleteNote(params) {
886
914
  };
887
915
  }
888
916
  }
889
- export function moveNote(params) {
917
+ export async function moveNote(params) {
890
918
  try {
891
- const noteRef = resolveWritableNoteReference(params);
919
+ const noteRef = await resolveWritableNoteReference(params);
892
920
  if (!noteRef.note) {
893
921
  return {
894
922
  success: false,
@@ -897,7 +925,7 @@ export function moveNote(params) {
897
925
  };
898
926
  }
899
927
  const writable = getWritableIdentifier(noteRef.note);
900
- const preview = store.previewMoveNote(writable.identifier, params.destinationFolder);
928
+ const preview = await store.previewMoveNote(writable.identifier, params.destinationFolder);
901
929
  const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}::${preview.destinationParentId ?? preview.destinationFolder}`;
902
930
  if (isTrueBool(params.dryRun)) {
903
931
  const token = issueConfirmationToken({
@@ -935,7 +963,7 @@ export function moveNote(params) {
935
963
  error: confirmationFailureMessage('noteplan_move_note', confirmation.reason),
936
964
  };
937
965
  }
938
- const moved = store.moveNote(writable.identifier, params.destinationFolder);
966
+ const moved = await store.moveNote(writable.identifier, params.destinationFolder);
939
967
  return {
940
968
  success: true,
941
969
  message: moved.note.source === 'space'
@@ -962,16 +990,16 @@ export function moveNote(params) {
962
990
  };
963
991
  }
964
992
  }
965
- export function restoreNote(params) {
993
+ export async function restoreNote(params) {
966
994
  try {
967
- const target = resolveNoteTarget(params.id, params.filename, params.space);
995
+ const target = await resolveNoteTarget(params.id, params.filename, params.space);
968
996
  if (!target.note) {
969
997
  return {
970
998
  success: false,
971
999
  error: 'Note not found',
972
1000
  };
973
1001
  }
974
- const preview = store.previewRestoreNote(target.identifier, params.destinationFolder);
1002
+ const preview = await store.previewRestoreNote(target.identifier, params.destinationFolder);
975
1003
  const confirmationTarget = `${preview.fromIdentifier}=>${preview.toIdentifier}`;
976
1004
  if (isTrueBool(params.dryRun)) {
977
1005
  const token = issueConfirmationToken({
@@ -1009,7 +1037,7 @@ export function restoreNote(params) {
1009
1037
  error: confirmationFailureMessage('noteplan_restore_note', confirmation.reason),
1010
1038
  };
1011
1039
  }
1012
- const restored = store.restoreNote(target.identifier, params.destinationFolder);
1040
+ const restored = await store.restoreNote(target.identifier, params.destinationFolder);
1013
1041
  return {
1014
1042
  success: true,
1015
1043
  message: restored.source === 'space'
@@ -1035,10 +1063,10 @@ export function restoreNote(params) {
1035
1063
  };
1036
1064
  }
1037
1065
  }
1038
- export function renameNoteFile(params) {
1066
+ export async function renameNoteFile(params) {
1039
1067
  try {
1040
1068
  // Resolve the note — supports id, filename, title, or query
1041
- const resolved = resolveWritableNoteReference(params);
1069
+ const resolved = await resolveWritableNoteReference(params);
1042
1070
  if (!resolved.note) {
1043
1071
  return {
1044
1072
  success: false,
@@ -1092,7 +1120,7 @@ export function renameNoteFile(params) {
1092
1120
  error: confirmationFailureMessage('noteplan_rename_note_file', confirmation.reason),
1093
1121
  };
1094
1122
  }
1095
- const renamed = store.renameSpaceNote(writeId, params.newTitle);
1123
+ const renamed = await store.renameSpaceNote(writeId, params.newTitle);
1096
1124
  // Also update the # Title heading in the note content if it matches the old title
1097
1125
  if (renamed.note.content) {
1098
1126
  const lines = renamed.note.content.split('\n');
@@ -1102,7 +1130,7 @@ export function renameNoteFile(params) {
1102
1130
  if (oldHeadingTitle === renamed.fromTitle) {
1103
1131
  lines[titleLineIndex] = `# ${params.newTitle}`;
1104
1132
  const renamedWriteTarget = getWritableIdentifier(renamed.note);
1105
- store.updateNote(renamedWriteTarget.identifier, lines.join('\n'), {
1133
+ await store.updateNote(renamedWriteTarget.identifier, lines.join('\n'), {
1106
1134
  source: renamedWriteTarget.source,
1107
1135
  });
1108
1136
  }
@@ -1134,7 +1162,7 @@ export function renameNoteFile(params) {
1134
1162
  };
1135
1163
  }
1136
1164
  const keepExtension = params.keepExtension ?? true;
1137
- const preview = store.previewRenameNoteFile(note.filename, effectiveNewFilename, keepExtension);
1165
+ const preview = await store.previewRenameNoteFile(note.filename, effectiveNewFilename, keepExtension);
1138
1166
  const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}`;
1139
1167
  if (isTrueBool(params.dryRun)) {
1140
1168
  const token = issueConfirmationToken({
@@ -1171,27 +1199,51 @@ export function renameNoteFile(params) {
1171
1199
  error: confirmationFailureMessage('noteplan_rename_note_file', confirmation.reason),
1172
1200
  };
1173
1201
  }
1174
- const renamed = store.renameNoteFile(note.filename, effectiveNewFilename, keepExtension);
1175
- // Also update the # Title heading in the note content if it matches the old title
1202
+ const renamed = await store.renameNoteFile(note.filename, effectiveNewFilename, keepExtension);
1203
+ // Also update the # Title heading in the note content if it matches the old title.
1204
+ // When the heading actually changes, propagate the rename to wikilinks across the
1205
+ // vault via the bridge — matches what the NotePlan UI rename does.
1176
1206
  const newTitle = params.newTitle || params.newFilename;
1207
+ let headingRewritten = false;
1208
+ let oldHeadingTitle = null;
1177
1209
  if (newTitle && renamed.note.content) {
1178
1210
  const lines = renamed.note.content.split('\n');
1179
1211
  const titleLineIndex = lines.findIndex((l) => /^#\s+/.test(l));
1180
1212
  if (titleLineIndex !== -1) {
1181
- const oldHeadingTitle = lines[titleLineIndex].replace(/^#\s+/, '');
1182
- // Update heading if it matches the old note title (or old filename without extension)
1213
+ const currentHeading = lines[titleLineIndex].replace(/^#\s+/, '');
1183
1214
  const oldTitle = note.title || '';
1184
1215
  const oldFilenameBase = note.filename.replace(/^.*\//, '').replace(/\.\w+$/, '');
1185
- if (oldHeadingTitle === oldTitle || oldHeadingTitle === oldFilenameBase) {
1216
+ if (currentHeading === oldTitle || currentHeading === oldFilenameBase) {
1186
1217
  lines[titleLineIndex] = `# ${newTitle}`;
1187
1218
  const writeTarget = getWritableIdentifier(renamed.note);
1188
- store.updateNote(writeTarget.identifier, lines.join('\n'), {
1219
+ await store.updateNote(writeTarget.identifier, lines.join('\n'), {
1189
1220
  source: writeTarget.source,
1190
1221
  });
1222
+ headingRewritten = true;
1223
+ oldHeadingTitle = currentHeading;
1191
1224
  }
1192
1225
  }
1193
1226
  }
1194
- return {
1227
+ let wikilinksUpdatedCount;
1228
+ let wikilinkPropagationWarning;
1229
+ if (headingRewritten && oldHeadingTitle && newTitle && oldHeadingTitle !== newTitle) {
1230
+ const bridge = await getBridgeClient();
1231
+ if (bridge) {
1232
+ try {
1233
+ const res = await bridge.rewriteWikilinks(oldHeadingTitle, newTitle);
1234
+ wikilinksUpdatedCount = res.updatedCount;
1235
+ }
1236
+ catch (err) {
1237
+ wikilinkPropagationWarning =
1238
+ err instanceof Error ? err.message : 'Failed to update wikilinks';
1239
+ }
1240
+ }
1241
+ else {
1242
+ wikilinkPropagationWarning =
1243
+ 'Wikilinks pointing to the old title were NOT updated because NotePlan is not running. Open NotePlan and re-run rename, or fix references manually.';
1244
+ }
1245
+ }
1246
+ const result = {
1195
1247
  success: true,
1196
1248
  message: `Note renamed to ${renamed.toFilename}`,
1197
1249
  fromFilename: renamed.fromFilename,
@@ -1205,6 +1257,13 @@ export function renameNoteFile(params) {
1205
1257
  folder: renamed.note.folder,
1206
1258
  },
1207
1259
  };
1260
+ if (wikilinksUpdatedCount !== undefined) {
1261
+ result.wikilinksUpdatedCount = wikilinksUpdatedCount;
1262
+ }
1263
+ if (wikilinkPropagationWarning) {
1264
+ result.warnings = [wikilinkPropagationWarning];
1265
+ }
1266
+ return result;
1208
1267
  }
1209
1268
  catch (error) {
1210
1269
  return {
@@ -1304,8 +1363,8 @@ function annotateFromMeta(lineObj, meta) {
1304
1363
  ...(meta.scheduledDate !== undefined && { scheduledDate: meta.scheduledDate }),
1305
1364
  };
1306
1365
  }
1307
- export function getParagraphs(params) {
1308
- const noteRef = resolveWritableNoteReference(params);
1366
+ export async function getParagraphs(params) {
1367
+ const noteRef = await resolveWritableNoteReference(params);
1309
1368
  if (!noteRef.note) {
1310
1369
  return {
1311
1370
  success: false,
@@ -1401,7 +1460,7 @@ export function getParagraphs(params) {
1401
1460
  }
1402
1461
  return result;
1403
1462
  }
1404
- export function searchParagraphs(params) {
1463
+ export async function searchParagraphs(params) {
1405
1464
  const query = typeof params?.query === 'string' ? params.query.trim() : '';
1406
1465
  if (!query) {
1407
1466
  return {
@@ -1415,7 +1474,7 @@ export function searchParagraphs(params) {
1415
1474
  error: 'Provide one note reference: id, title, filename, or date',
1416
1475
  };
1417
1476
  }
1418
- const note = store.getNote({
1477
+ const note = await store.getNote({
1419
1478
  id: params.id,
1420
1479
  title: params.title,
1421
1480
  filename: params.filename,
@@ -1683,30 +1742,22 @@ export const replaceLinesSchema = z.object({
1683
1742
  .optional()
1684
1743
  .default('tabs')
1685
1744
  .describe('Indentation normalization for replacement list/task lines. Default: tabs'),
1686
- dryRun: z
1687
- .boolean()
1688
- .optional()
1689
- .describe('Preview line replacement and get confirmationToken without modifying the note'),
1690
- confirmationToken: z
1691
- .string()
1692
- .optional()
1693
- .describe('Confirmation token issued by dryRun for replace execution'),
1694
1745
  allowEmptyContent: z
1695
1746
  .boolean()
1696
1747
  .optional()
1697
1748
  .describe('Allow replacing selected lines with empty content (default: false). Prefer delete_lines for pure deletion.'),
1698
1749
  });
1699
1750
  // Granular note operation implementations
1700
- export function setProperty(params) {
1751
+ export async function setProperty(params) {
1701
1752
  try {
1702
- const noteRef = resolveWritableNoteReference(params);
1753
+ const noteRef = await resolveWritableNoteReference(params);
1703
1754
  if (!noteRef.note) {
1704
1755
  return { success: false, error: noteRef.error || 'Note not found', candidates: noteRef.candidates };
1705
1756
  }
1706
1757
  const note = noteRef.note;
1707
1758
  const newContent = frontmatter.setFrontmatterProperty(note.content, params.key, params.value);
1708
1759
  const writable = getWritableIdentifier(note);
1709
- store.updateNote(writable.identifier, newContent, { source: writable.source });
1760
+ await store.updateNote(writable.identifier, newContent, { source: writable.source });
1710
1761
  return {
1711
1762
  success: true,
1712
1763
  message: `Property "${params.key}" set to "${params.value}"`,
@@ -1719,16 +1770,16 @@ export function setProperty(params) {
1719
1770
  };
1720
1771
  }
1721
1772
  }
1722
- export function removeProperty(params) {
1773
+ export async function removeProperty(params) {
1723
1774
  try {
1724
- const noteRef = resolveWritableNoteReference(params);
1775
+ const noteRef = await resolveWritableNoteReference(params);
1725
1776
  if (!noteRef.note) {
1726
1777
  return { success: false, error: noteRef.error || 'Note not found', candidates: noteRef.candidates };
1727
1778
  }
1728
1779
  const note = noteRef.note;
1729
1780
  const newContent = frontmatter.removeFrontmatterProperty(note.content, params.key);
1730
1781
  const writable = getWritableIdentifier(note);
1731
- store.updateNote(writable.identifier, newContent, { source: writable.source });
1782
+ await store.updateNote(writable.identifier, newContent, { source: writable.source });
1732
1783
  return {
1733
1784
  success: true,
1734
1785
  message: `Property "${params.key}" removed`,
@@ -1741,9 +1792,9 @@ export function removeProperty(params) {
1741
1792
  };
1742
1793
  }
1743
1794
  }
1744
- export function insertContent(params) {
1795
+ export async function insertContent(params) {
1745
1796
  try {
1746
- const resolved = resolveWritableNoteReference(params);
1797
+ const resolved = await resolveWritableNoteReference(params);
1747
1798
  if (!resolved.note) {
1748
1799
  return {
1749
1800
  success: false,
@@ -1807,7 +1858,7 @@ export function insertContent(params) {
1807
1858
  line: params.line,
1808
1859
  });
1809
1860
  const writeTarget = getWritableIdentifier(note);
1810
- store.updateNote(writeTarget.identifier, newContent, {
1861
+ await store.updateNote(writeTarget.identifier, newContent, {
1811
1862
  source: writeTarget.source,
1812
1863
  });
1813
1864
  return {
@@ -1830,9 +1881,9 @@ export function insertContent(params) {
1830
1881
  };
1831
1882
  }
1832
1883
  }
1833
- export function appendContent(params) {
1884
+ export async function appendContent(params) {
1834
1885
  try {
1835
- const resolved = resolveWritableNoteReference(params);
1886
+ const resolved = await resolveWritableNoteReference(params);
1836
1887
  if (!resolved.note) {
1837
1888
  return {
1838
1889
  success: false,
@@ -1848,7 +1899,7 @@ export function appendContent(params) {
1848
1899
  heading: params.heading,
1849
1900
  });
1850
1901
  const writeTarget = getWritableIdentifier(note);
1851
- store.updateNote(writeTarget.identifier, newContent, {
1902
+ await store.updateNote(writeTarget.identifier, newContent, {
1852
1903
  source: writeTarget.source,
1853
1904
  });
1854
1905
  return {
@@ -1870,7 +1921,7 @@ export function appendContent(params) {
1870
1921
  };
1871
1922
  }
1872
1923
  }
1873
- export function deleteLines(params) {
1924
+ export async function deleteLines(params) {
1874
1925
  try {
1875
1926
  // Validate required line params — MCP may deliver them as undefined when omitted
1876
1927
  const rawStart = params.startLine !== undefined && params.startLine !== null ? Number(params.startLine) : NaN;
@@ -1881,24 +1932,23 @@ export function deleteLines(params) {
1881
1932
  if (!Number.isFinite(rawEnd)) {
1882
1933
  return { success: false, error: 'endLine is required (1-indexed). Pass the same value as startLine to delete a single line.' };
1883
1934
  }
1884
- const resolved = resolveWritableNoteReference(params);
1935
+ const resolved = await resolveWritableNoteReference(params);
1885
1936
  if (!resolved.note) {
1886
1937
  return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
1887
1938
  }
1888
1939
  const note = resolved.note;
1889
1940
  const allLines = note.content.split('\n');
1890
- // Line numbers are absolute (1-indexed), matching get_notes/getParagraphs
1891
1941
  const totalLineCount = allLines.length;
1892
1942
  const fmLineCount = frontmatter.getFrontmatterLineCount(note.content);
1893
- const minLine = fmLineCount > 0 ? fmLineCount + 1 : 1;
1894
- const boundedStartLine = toBoundedInt(params.startLine, minLine, minLine, Math.max(minLine, totalLineCount));
1895
- const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, totalLineCount));
1896
- if (boundedStartLine <= fmLineCount) {
1943
+ if (fmLineCount > 0 && rawStart <= fmLineCount) {
1897
1944
  return {
1898
1945
  success: false,
1899
- error: `Lines 1-${fmLineCount} are frontmatter and cannot be deleted. Content starts at line ${fmLineCount + 1}.`,
1946
+ error: `Line ${rawStart} is inside frontmatter (lines 1-${fmLineCount}). Content starts at line ${fmLineCount + 1}.`,
1900
1947
  };
1901
1948
  }
1949
+ const minLine = fmLineCount > 0 ? fmLineCount + 1 : 1;
1950
+ const boundedStartLine = toBoundedInt(params.startLine, minLine, minLine, Math.max(minLine, totalLineCount));
1951
+ const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, totalLineCount));
1902
1952
  const lineCountToDelete = boundedEndLine - boundedStartLine + 1;
1903
1953
  const previewStartIndex = boundedStartLine - 1;
1904
1954
  const previewEndIndexExclusive = boundedEndLine;
@@ -1969,7 +2019,7 @@ export function deleteLines(params) {
1969
2019
  splicedLines.splice(boundedStartLine - 1, lineCountToDelete);
1970
2020
  const newContent = splicedLines.join('\n');
1971
2021
  const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1972
- store.updateNote(writeIdentifier, newContent, { source: note.source });
2022
+ await store.updateNote(writeIdentifier, newContent, { source: note.source });
1973
2023
  return {
1974
2024
  success: true,
1975
2025
  message: `Lines ${boundedStartLine}-${boundedEndLine} deleted`,
@@ -1986,7 +2036,7 @@ export function deleteLines(params) {
1986
2036
  };
1987
2037
  }
1988
2038
  }
1989
- export function editLine(params) {
2039
+ export async function editLine(params) {
1990
2040
  try {
1991
2041
  if (params.allowEmptyContent !== true && params.content.trim().length === 0) {
1992
2042
  return {
@@ -1994,7 +2044,7 @@ export function editLine(params) {
1994
2044
  error: 'Empty line content is blocked for noteplan_edit_line. Use noteplan_delete_lines or set allowEmptyContent=true.',
1995
2045
  };
1996
2046
  }
1997
- const resolved = resolveWritableNoteReference(params);
2047
+ const resolved = await resolveWritableNoteReference(params);
1998
2048
  if (!resolved.note) {
1999
2049
  return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
2000
2050
  }
@@ -2033,7 +2083,7 @@ export function editLine(params) {
2033
2083
  warnings.push(buildAttachmentWarningMessage(removedAttachmentReferences.length));
2034
2084
  }
2035
2085
  const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
2036
- store.updateNote(writeIdentifier, newContent, { source: note.source });
2086
+ await store.updateNote(writeIdentifier, newContent, { source: note.source });
2037
2087
  return {
2038
2088
  success: true,
2039
2089
  message: `Line ${params.line} updated`,
@@ -2057,7 +2107,7 @@ export function editLine(params) {
2057
2107
  };
2058
2108
  }
2059
2109
  }
2060
- export function replaceLines(params) {
2110
+ export async function replaceLines(params) {
2061
2111
  try {
2062
2112
  // Validate required line params — MCP may deliver them as undefined when omitted
2063
2113
  const rawStart = params.startLine !== undefined && params.startLine !== null ? Number(params.startLine) : NaN;
@@ -2068,24 +2118,23 @@ export function replaceLines(params) {
2068
2118
  if (!Number.isFinite(rawEnd)) {
2069
2119
  return { success: false, error: 'endLine is required (1-indexed). Pass the same value as startLine to replace a single line.' };
2070
2120
  }
2071
- const resolved = resolveWritableNoteReference(params);
2121
+ const resolved = await resolveWritableNoteReference(params);
2072
2122
  if (!resolved.note) {
2073
2123
  return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
2074
2124
  }
2075
2125
  const note = resolved.note;
2076
2126
  const allLines = note.content.split('\n');
2077
2127
  const originalLineCount = allLines.length;
2078
- // Line numbers are absolute (1-indexed), matching get_notes/getParagraphs
2079
2128
  const fmLineCount = frontmatter.getFrontmatterLineCount(note.content);
2080
- const minLine = fmLineCount > 0 ? fmLineCount + 1 : 1;
2081
- const boundedStartLine = toBoundedInt(params.startLine, minLine, minLine, Math.max(minLine, originalLineCount));
2082
- const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, originalLineCount));
2083
- if (boundedStartLine <= fmLineCount) {
2129
+ if (fmLineCount > 0 && rawStart <= fmLineCount) {
2084
2130
  return {
2085
2131
  success: false,
2086
- error: `Lines 1-${fmLineCount} are frontmatter and cannot be replaced directly. Content starts at line ${fmLineCount + 1}.`,
2132
+ error: `Line ${rawStart} is inside frontmatter (lines 1-${fmLineCount}). Content starts at line ${fmLineCount + 1}.`,
2087
2133
  };
2088
2134
  }
2135
+ const minLine = fmLineCount > 0 ? fmLineCount + 1 : 1;
2136
+ const boundedStartLine = toBoundedInt(params.startLine, minLine, minLine, Math.max(minLine, originalLineCount));
2137
+ const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, originalLineCount));
2089
2138
  let startIndex = boundedStartLine - 1;
2090
2139
  let lineCountToReplace = boundedEndLine - boundedStartLine + 1;
2091
2140
  const replacedText = allLines.slice(startIndex, boundedEndLine).join('\n');
@@ -2117,44 +2166,9 @@ export function replaceLines(params) {
2117
2166
  if (lineDelta !== 0) {
2118
2167
  warnings.push(`Line numbers shifted by ${lineDelta > 0 ? '+' : ''}${lineDelta} after this replacement. Re-read line numbers before the next mutation.`);
2119
2168
  }
2120
- const target = `${note.filename}:${boundedStartLine}-${boundedEndLine}:${replacementLines.length}:${normalized.content.length}`;
2121
- if (isTrueBool(params.dryRun)) {
2122
- const token = issueConfirmationToken({
2123
- tool: 'noteplan_replace_lines',
2124
- target,
2125
- action: 'replace_lines',
2126
- });
2127
- return {
2128
- success: true,
2129
- dryRun: true,
2130
- message: `Dry run: lines ${boundedStartLine}-${boundedEndLine} would be replaced`,
2131
- lineCountToReplace,
2132
- insertedLineCount: replacementLines.length,
2133
- lineDelta,
2134
- originalLineCount,
2135
- newLineCount,
2136
- indentationStyle,
2137
- linesRetabbed: normalized.linesRetabbed,
2138
- removedAttachmentReferences: removedAttachmentReferences.slice(0, 20),
2139
- removedAttachmentReferencesTruncated: removedAttachmentReferences.length > 20,
2140
- warnings: warnings.length > 0 ? warnings : undefined,
2141
- ...token,
2142
- };
2143
- }
2144
- const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
2145
- tool: 'noteplan_replace_lines',
2146
- target,
2147
- action: 'replace_lines',
2148
- });
2149
- if (!confirmation.ok) {
2150
- return {
2151
- success: false,
2152
- error: confirmationFailureMessage('noteplan_replace_lines', confirmation.reason),
2153
- };
2154
- }
2155
2169
  allLines.splice(startIndex, lineCountToReplace, ...replacementLines);
2156
2170
  const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
2157
- store.updateNote(writeIdentifier, allLines.join('\n'), { source: note.source });
2171
+ await store.updateNote(writeIdentifier, allLines.join('\n'), { source: note.source });
2158
2172
  return {
2159
2173
  success: true,
2160
2174
  message: `Lines ${boundedStartLine}-${boundedEndLine} replaced`,
@@ -2242,7 +2256,7 @@ export const searchParagraphsGlobalSchema = z.object({
2242
2256
  offset: z.number().min(0).optional().default(0).describe('Pagination offset'),
2243
2257
  cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
2244
2258
  });
2245
- export function searchParagraphsGlobal(params) {
2259
+ export async function searchParagraphsGlobal(params) {
2246
2260
  const query = typeof params?.query === 'string' ? params.query.trim() : '';
2247
2261
  if (!query) {
2248
2262
  return {
@@ -2264,7 +2278,7 @@ export function searchParagraphsGlobal(params) {
2264
2278
  const noteTypes = normalizeTypeList(params.noteTypes);
2265
2279
  const preferCalendar = params.preferCalendar === true;
2266
2280
  const periodicOnly = params.periodicOnly === true;
2267
- const allNotes = store.listNotes({
2281
+ const allNotes = await store.listNotes({
2268
2282
  folder: params.folder,
2269
2283
  space: params.space,
2270
2284
  });