@noteplanco/noteplan-mcp 1.1.7 → 1.1.9

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 (79) hide show
  1. package/dist/noteplan/embeddings.d.ts +8 -0
  2. package/dist/noteplan/embeddings.d.ts.map +1 -1
  3. package/dist/noteplan/embeddings.js +3 -3
  4. package/dist/noteplan/embeddings.js.map +1 -1
  5. package/dist/noteplan/file-reader.d.ts +6 -0
  6. package/dist/noteplan/file-reader.d.ts.map +1 -1
  7. package/dist/noteplan/file-reader.js +15 -0
  8. package/dist/noteplan/file-reader.js.map +1 -1
  9. package/dist/noteplan/file-writer.d.ts.map +1 -1
  10. package/dist/noteplan/file-writer.js +13 -1
  11. package/dist/noteplan/file-writer.js.map +1 -1
  12. package/dist/noteplan/file-writer.test.js +4 -0
  13. package/dist/noteplan/file-writer.test.js.map +1 -1
  14. package/dist/noteplan/frontmatter-parser.d.ts +4 -4
  15. package/dist/noteplan/frontmatter-parser.d.ts.map +1 -1
  16. package/dist/noteplan/frontmatter-parser.js +11 -14
  17. package/dist/noteplan/frontmatter-parser.js.map +1 -1
  18. package/dist/noteplan/frontmatter-parser.test.js +92 -0
  19. package/dist/noteplan/frontmatter-parser.test.js.map +1 -1
  20. package/dist/noteplan/markdown-parser.d.ts.map +1 -1
  21. package/dist/noteplan/markdown-parser.js +4 -2
  22. package/dist/noteplan/markdown-parser.js.map +1 -1
  23. package/dist/noteplan/markdown-parser.test.js +129 -0
  24. package/dist/noteplan/markdown-parser.test.js.map +1 -1
  25. package/dist/noteplan/sqlite-loader.d.ts +11 -2
  26. package/dist/noteplan/sqlite-loader.d.ts.map +1 -1
  27. package/dist/noteplan/sqlite-loader.js +143 -15
  28. package/dist/noteplan/sqlite-loader.js.map +1 -1
  29. package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
  30. package/dist/noteplan/sqlite-writer.js +3 -0
  31. package/dist/noteplan/sqlite-writer.js.map +1 -1
  32. package/dist/noteplan/template-docs.d.ts +35 -0
  33. package/dist/noteplan/template-docs.d.ts.map +1 -0
  34. package/dist/noteplan/template-docs.js +184 -0
  35. package/dist/noteplan/template-docs.js.map +1 -0
  36. package/dist/noteplan/unified-store.d.ts +2 -0
  37. package/dist/noteplan/unified-store.d.ts.map +1 -1
  38. package/dist/noteplan/unified-store.js +30 -7
  39. package/dist/noteplan/unified-store.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +307 -65
  42. package/dist/server.js.map +1 -1
  43. package/dist/tools/calendar.d.ts +2 -2
  44. package/dist/tools/events.d.ts +16 -16
  45. package/dist/tools/events.d.ts.map +1 -1
  46. package/dist/tools/events.js +17 -2
  47. package/dist/tools/events.js.map +1 -1
  48. package/dist/tools/notes.d.ts +327 -45
  49. package/dist/tools/notes.d.ts.map +1 -1
  50. package/dist/tools/notes.js +391 -51
  51. package/dist/tools/notes.js.map +1 -1
  52. package/dist/tools/notes.test.js +959 -1
  53. package/dist/tools/notes.test.js.map +1 -1
  54. package/dist/tools/plugins.d.ts.map +1 -1
  55. package/dist/tools/plugins.js +1 -0
  56. package/dist/tools/plugins.js.map +1 -1
  57. package/dist/tools/search.d.ts +2 -2
  58. package/dist/tools/search.d.ts.map +1 -1
  59. package/dist/tools/search.js +32 -4
  60. package/dist/tools/search.js.map +1 -1
  61. package/dist/tools/tasks.d.ts +86 -10
  62. package/dist/tools/tasks.d.ts.map +1 -1
  63. package/dist/tools/tasks.js +84 -25
  64. package/dist/tools/tasks.js.map +1 -1
  65. package/dist/tools/templates.d.ts +64 -3
  66. package/dist/tools/templates.d.ts.map +1 -1
  67. package/dist/tools/templates.js +73 -1
  68. package/dist/tools/templates.js.map +1 -1
  69. package/dist/tools/ui-automation.d.ts +74 -0
  70. package/dist/tools/ui-automation.d.ts.map +1 -0
  71. package/dist/tools/ui-automation.js +209 -0
  72. package/dist/tools/ui-automation.js.map +1 -0
  73. package/dist/utils/version.d.ts +1 -0
  74. package/dist/utils/version.d.ts.map +1 -1
  75. package/dist/utils/version.js +89 -27
  76. package/dist/utils/version.js.map +1 -1
  77. package/docs/templates.db.gz +0 -0
  78. package/docs/x-callback-url.md +318 -0
  79. package/package.json +1 -1
@@ -69,7 +69,7 @@ function resolveNoteTarget(id, filename, space) {
69
69
  note,
70
70
  };
71
71
  }
72
- function resolveWritableNoteReference(input) {
72
+ export function resolveWritableNoteReference(input) {
73
73
  if (input.id && input.id.trim().length > 0) {
74
74
  const note = store.getNote({ id: input.id.trim(), space: input.space?.trim() });
75
75
  return { note, error: note ? undefined : 'Note not found' };
@@ -124,7 +124,7 @@ function resolveWritableNoteReference(input) {
124
124
  error: 'Provide one note reference: id, filename, title, date, or query',
125
125
  };
126
126
  }
127
- function getWritableIdentifier(note) {
127
+ export function getWritableIdentifier(note) {
128
128
  if (note.source === 'space') {
129
129
  return {
130
130
  identifier: note.id || note.filename,
@@ -302,7 +302,11 @@ export const createNoteSchema = z.object({
302
302
  templateTypes: z.array(z.enum(['empty-note', 'meeting-note', 'project-note', 'calendar-note'])).optional().describe('Template type tags — used when noteType="template"'),
303
303
  });
304
304
  export const updateNoteSchema = z.object({
305
- filename: z.string().describe('Filename/path of the note to update'),
305
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
306
+ filename: z.string().optional().describe('Filename/path of the note to update'),
307
+ title: z.string().optional().describe('Note title to search for'),
308
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
309
+ query: z.string().optional().describe('Fuzzy note query'),
306
310
  space: z.string().optional().describe('Space name or ID to search in'),
307
311
  content: z
308
312
  .string()
@@ -323,6 +327,14 @@ export const updateNoteSchema = z.object({
323
327
  .boolean()
324
328
  .optional()
325
329
  .describe('Allow replacing note content with empty/blank text (default: false)'),
330
+ }).superRefine((input, ctx) => {
331
+ if (!input.id && !input.filename && !input.title && !input.date && !input.query) {
332
+ ctx.addIssue({
333
+ code: z.ZodIssueCode.custom,
334
+ message: 'Provide one note reference: id, filename, title, date, or query',
335
+ path: ['filename'],
336
+ });
337
+ }
326
338
  });
327
339
  export const deleteNoteSchema = z.object({
328
340
  id: z.string().optional().describe('Note ID (preferred for TeamSpace notes)'),
@@ -348,6 +360,9 @@ export const deleteNoteSchema = z.object({
348
360
  export const moveNoteSchema = z.object({
349
361
  id: z.string().optional().describe('Note ID (preferred for TeamSpace notes)'),
350
362
  filename: z.string().optional().describe('Filename/path of the note to move'),
363
+ title: z.string().optional().describe('Note title to search for'),
364
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
365
+ query: z.string().optional().describe('Fuzzy note query'),
351
366
  space: z.string().optional().describe('Space name or ID to search in'),
352
367
  destinationFolder: z
353
368
  .string()
@@ -698,6 +713,7 @@ export function createNote(params) {
698
713
  });
699
714
  return {
700
715
  success: true,
716
+ tip: 'Use action "set_property" to add frontmatter fields (e.g. type, tags) or "remove_property" to delete them.',
701
717
  note: {
702
718
  title: result.note.title,
703
719
  filename: result.note.filename,
@@ -730,29 +746,33 @@ export function updateNote(params) {
730
746
  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.',
731
747
  };
732
748
  }
733
- const existingNote = store.getNote({ filename: params.filename, space: params.space });
734
- if (!existingNote) {
749
+ const noteRef = resolveWritableNoteReference(params);
750
+ if (!noteRef.note) {
735
751
  return {
736
752
  success: false,
737
- error: 'Note not found',
753
+ error: noteRef.error || 'Note not found',
754
+ candidates: noteRef.candidates,
738
755
  };
739
756
  }
757
+ const existingNote = noteRef.note;
740
758
  if (params.allowEmptyContent !== true && params.content.trim().length === 0) {
741
759
  return {
742
760
  success: false,
743
761
  error: 'Empty content is blocked for noteplan_update_note. Use allowEmptyContent=true to override intentionally.',
744
762
  };
745
763
  }
764
+ // Use the resolved filename as the confirmation token target for consistency
765
+ const confirmationTarget = existingNote.filename;
746
766
  if (isTrueBool(params.dryRun)) {
747
767
  const token = issueConfirmationToken({
748
768
  tool: 'noteplan_update_note',
749
- target: params.filename,
769
+ target: confirmationTarget,
750
770
  action: 'full_replace',
751
771
  });
752
772
  return {
753
773
  success: true,
754
774
  dryRun: true,
755
- message: `Dry run: note ${params.filename} would be fully replaced`,
775
+ message: `Dry run: note ${existingNote.filename} would be fully replaced`,
756
776
  note: {
757
777
  id: existingNote.id,
758
778
  title: existingNote.title,
@@ -769,7 +789,7 @@ export function updateNote(params) {
769
789
  }
770
790
  const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
771
791
  tool: 'noteplan_update_note',
772
- target: params.filename,
792
+ target: confirmationTarget,
773
793
  action: 'full_replace',
774
794
  });
775
795
  if (!confirmation.ok) {
@@ -864,14 +884,16 @@ export function deleteNote(params) {
864
884
  }
865
885
  export function moveNote(params) {
866
886
  try {
867
- const target = resolveNoteTarget(params.id, params.filename, params.space);
868
- if (!target.note) {
887
+ const noteRef = resolveWritableNoteReference(params);
888
+ if (!noteRef.note) {
869
889
  return {
870
890
  success: false,
871
- error: 'Note not found',
891
+ error: noteRef.error || 'Note not found',
892
+ candidates: noteRef.candidates,
872
893
  };
873
894
  }
874
- const preview = store.previewMoveNote(target.identifier, params.destinationFolder);
895
+ const writable = getWritableIdentifier(noteRef.note);
896
+ const preview = store.previewMoveNote(writable.identifier, params.destinationFolder);
875
897
  const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}::${preview.destinationParentId ?? preview.destinationFolder}`;
876
898
  if (isTrueBool(params.dryRun)) {
877
899
  const token = issueConfirmationToken({
@@ -909,7 +931,7 @@ export function moveNote(params) {
909
931
  error: confirmationFailureMessage('noteplan_move_note', confirmation.reason),
910
932
  };
911
933
  }
912
- const moved = store.moveNote(target.identifier, params.destinationFolder);
934
+ const moved = store.moveNote(writable.identifier, params.destinationFolder);
913
935
  return {
914
936
  success: true,
915
937
  message: moved.note.source === 'space'
@@ -1067,6 +1089,21 @@ export function renameNoteFile(params) {
1067
1089
  };
1068
1090
  }
1069
1091
  const renamed = store.renameSpaceNote(writeId, params.newTitle);
1092
+ // Also update the # Title heading in the note content if it matches the old title
1093
+ if (renamed.note.content) {
1094
+ const lines = renamed.note.content.split('\n');
1095
+ const titleLineIndex = lines.findIndex((l) => /^#\s+/.test(l));
1096
+ if (titleLineIndex !== -1) {
1097
+ const oldHeadingTitle = lines[titleLineIndex].replace(/^#\s+/, '');
1098
+ if (oldHeadingTitle === renamed.fromTitle) {
1099
+ lines[titleLineIndex] = `# ${params.newTitle}`;
1100
+ const renamedWriteTarget = getWritableIdentifier(renamed.note);
1101
+ store.updateNote(renamedWriteTarget.identifier, lines.join('\n'), {
1102
+ source: renamedWriteTarget.source,
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1070
1107
  return {
1071
1108
  success: true,
1072
1109
  message: `TeamSpace note renamed from "${renamed.fromTitle}" to "${renamed.toTitle}"`,
@@ -1174,13 +1211,25 @@ export function renameNoteFile(params) {
1174
1211
  }
1175
1212
  // Get note with line numbers
1176
1213
  export const getParagraphsSchema = z.object({
1177
- filename: z.string().describe('Filename/path of the note'),
1214
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1215
+ filename: z.string().optional().describe('Filename/path of the note'),
1216
+ title: z.string().optional().describe('Note title to search for'),
1217
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
1218
+ query: z.string().optional().describe('Fuzzy note query'),
1178
1219
  space: z.string().optional().describe('Space name or ID to search in'),
1179
1220
  startLine: z.number().min(1).optional().describe('First line to include (1-indexed, inclusive)'),
1180
1221
  endLine: z.number().min(1).optional().describe('Last line to include (1-indexed, inclusive)'),
1181
1222
  limit: z.number().min(1).max(1000).optional().default(200).describe('Maximum lines to return'),
1182
1223
  offset: z.number().min(0).optional().default(0).describe('Pagination offset within selected range'),
1183
1224
  cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
1225
+ }).superRefine((input, ctx) => {
1226
+ if (!input.id && !input.filename && !input.title && !input.date && !input.query) {
1227
+ ctx.addIssue({
1228
+ code: z.ZodIssueCode.custom,
1229
+ message: 'Provide one note reference: id, filename, title, date, or query',
1230
+ path: ['filename'],
1231
+ });
1232
+ }
1184
1233
  });
1185
1234
  export const searchParagraphsSchema = z.object({
1186
1235
  id: z.string().optional().describe('Note ID (preferred for space notes)'),
@@ -1214,13 +1263,15 @@ export const searchParagraphsSchema = z.object({
1214
1263
  }
1215
1264
  });
1216
1265
  export function getParagraphs(params) {
1217
- const note = store.getNote({ filename: params.filename, space: params.space });
1218
- if (!note) {
1266
+ const noteRef = resolveWritableNoteReference(params);
1267
+ if (!noteRef.note) {
1219
1268
  return {
1220
1269
  success: false,
1221
- error: 'Note not found',
1270
+ error: noteRef.error || 'Note not found',
1271
+ candidates: noteRef.candidates,
1222
1272
  };
1223
1273
  }
1274
+ const note = noteRef.note;
1224
1275
  const allLines = note.content.split('\n');
1225
1276
  const lineWindow = buildLineWindow(allLines, {
1226
1277
  startLine: params.startLine,
@@ -1365,6 +1416,7 @@ export function searchParagraphs(params) {
1365
1416
  const nextCursor = hasMore ? String(offset + page.length) : null;
1366
1417
  const result = {
1367
1418
  success: true,
1419
+ tip: 'To search across ALL notes at once, use action "search_global" instead. It supports query "*" to match all tasks.',
1368
1420
  query,
1369
1421
  count: page.length,
1370
1422
  totalCount: allMatches.length,
@@ -1399,15 +1451,39 @@ export function searchParagraphs(params) {
1399
1451
  }
1400
1452
  // Granular note operation schemas
1401
1453
  export const setPropertySchema = z.object({
1402
- filename: z.string().describe('Filename/path of the note'),
1454
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1455
+ filename: z.string().optional().describe('Filename/path of the note'),
1456
+ title: z.string().optional().describe('Note title to search for'),
1457
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
1458
+ query: z.string().optional().describe('Fuzzy note query'),
1403
1459
  space: z.string().optional().describe('Space name or ID to search in'),
1404
1460
  key: z.string().describe('Property key (e.g., "icon", "bg-color", "status")'),
1405
1461
  value: z.string().describe('Property value'),
1462
+ }).superRefine((input, ctx) => {
1463
+ if (!input.id && !input.filename && !input.title && !input.date && !input.query) {
1464
+ ctx.addIssue({
1465
+ code: z.ZodIssueCode.custom,
1466
+ message: 'Provide one note reference: id, filename, title, date, or query',
1467
+ path: ['filename'],
1468
+ });
1469
+ }
1406
1470
  });
1407
1471
  export const removePropertySchema = z.object({
1408
- filename: z.string().describe('Filename/path of the note'),
1472
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1473
+ filename: z.string().optional().describe('Filename/path of the note'),
1474
+ title: z.string().optional().describe('Note title to search for'),
1475
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
1476
+ query: z.string().optional().describe('Fuzzy note query'),
1409
1477
  space: z.string().optional().describe('Space name or ID to search in'),
1410
1478
  key: z.string().describe('Property key to remove'),
1479
+ }).superRefine((input, ctx) => {
1480
+ if (!input.id && !input.filename && !input.title && !input.date && !input.query) {
1481
+ ctx.addIssue({
1482
+ code: z.ZodIssueCode.custom,
1483
+ message: 'Provide one note reference: id, filename, title, date, or query',
1484
+ path: ['filename'],
1485
+ });
1486
+ }
1411
1487
  });
1412
1488
  export const insertContentSchema = z.object({
1413
1489
  id: z.string().optional().describe('Note ID (preferred for space notes)'),
@@ -1549,13 +1625,14 @@ export const replaceLinesSchema = z.object({
1549
1625
  // Granular note operation implementations
1550
1626
  export function setProperty(params) {
1551
1627
  try {
1552
- const note = store.getNote({ filename: params.filename, space: params.space });
1553
- if (!note) {
1554
- return { success: false, error: 'Note not found' };
1628
+ const noteRef = resolveWritableNoteReference(params);
1629
+ if (!noteRef.note) {
1630
+ return { success: false, error: noteRef.error || 'Note not found', candidates: noteRef.candidates };
1555
1631
  }
1632
+ const note = noteRef.note;
1556
1633
  const newContent = frontmatter.setFrontmatterProperty(note.content, params.key, params.value);
1557
- const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1558
- store.updateNote(writeIdentifier, newContent, { source: note.source });
1634
+ const writable = getWritableIdentifier(note);
1635
+ store.updateNote(writable.identifier, newContent, { source: writable.source });
1559
1636
  return {
1560
1637
  success: true,
1561
1638
  message: `Property "${params.key}" set to "${params.value}"`,
@@ -1570,13 +1647,14 @@ export function setProperty(params) {
1570
1647
  }
1571
1648
  export function removeProperty(params) {
1572
1649
  try {
1573
- const note = store.getNote({ filename: params.filename, space: params.space });
1574
- if (!note) {
1575
- return { success: false, error: 'Note not found' };
1650
+ const noteRef = resolveWritableNoteReference(params);
1651
+ if (!noteRef.note) {
1652
+ return { success: false, error: noteRef.error || 'Note not found', candidates: noteRef.candidates };
1576
1653
  }
1654
+ const note = noteRef.note;
1577
1655
  const newContent = frontmatter.removeFrontmatterProperty(note.content, params.key);
1578
- const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1579
- store.updateNote(writeIdentifier, newContent, { source: note.source });
1656
+ const writable = getWritableIdentifier(note);
1657
+ store.updateNote(writable.identifier, newContent, { source: writable.source });
1580
1658
  return {
1581
1659
  success: true,
1582
1660
  message: `Property "${params.key}" removed`,
@@ -1660,6 +1738,7 @@ export function insertContent(params) {
1660
1738
  });
1661
1739
  return {
1662
1740
  success: true,
1741
+ tip: 'Use noteplan_paragraphs(action: "get") to inspect line numbers and content before making further edits.',
1663
1742
  message: `Content inserted at ${params.position}`,
1664
1743
  note: {
1665
1744
  id: note.id,
@@ -1719,20 +1798,36 @@ export function appendContent(params) {
1719
1798
  }
1720
1799
  export function deleteLines(params) {
1721
1800
  try {
1801
+ // Validate required line params — MCP may deliver them as undefined when omitted
1802
+ const rawStart = params.startLine !== undefined && params.startLine !== null ? Number(params.startLine) : NaN;
1803
+ const rawEnd = params.endLine !== undefined && params.endLine !== null ? Number(params.endLine) : NaN;
1804
+ if (!Number.isFinite(rawStart)) {
1805
+ return { success: false, error: 'startLine is required (1-indexed).' };
1806
+ }
1807
+ if (!Number.isFinite(rawEnd)) {
1808
+ return { success: false, error: 'endLine is required (1-indexed). Pass the same value as startLine to delete a single line.' };
1809
+ }
1722
1810
  const resolved = resolveWritableNoteReference(params);
1723
1811
  if (!resolved.note) {
1724
1812
  return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
1725
1813
  }
1726
1814
  const note = resolved.note;
1727
1815
  const allLines = note.content.split('\n');
1728
- // Line numbers are relative to content after frontmatter
1729
- const fmOffset = frontmatter.getFrontmatterLineCount(note.content);
1730
- const contentLineCount = allLines.length - fmOffset;
1731
- const boundedStartLine = toBoundedInt(params.startLine, 1, 1, Math.max(1, contentLineCount));
1732
- const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, contentLineCount));
1816
+ // Line numbers are absolute (1-indexed), matching get_notes/getParagraphs
1817
+ const totalLineCount = allLines.length;
1818
+ const fmLineCount = frontmatter.getFrontmatterLineCount(note.content);
1819
+ const minLine = fmLineCount > 0 ? fmLineCount + 1 : 1;
1820
+ const boundedStartLine = toBoundedInt(params.startLine, minLine, minLine, Math.max(minLine, totalLineCount));
1821
+ const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, totalLineCount));
1822
+ if (boundedStartLine <= fmLineCount) {
1823
+ return {
1824
+ success: false,
1825
+ error: `Lines 1-${fmLineCount} are frontmatter and cannot be deleted. Content starts at line ${fmLineCount + 1}.`,
1826
+ };
1827
+ }
1733
1828
  const lineCountToDelete = boundedEndLine - boundedStartLine + 1;
1734
- const previewStartIndex = fmOffset + boundedStartLine - 1;
1735
- const previewEndIndexExclusive = fmOffset + boundedEndLine;
1829
+ const previewStartIndex = boundedStartLine - 1;
1830
+ const previewEndIndexExclusive = boundedEndLine;
1736
1831
  const deletedLinesPreview = allLines
1737
1832
  .slice(previewStartIndex, previewEndIndexExclusive)
1738
1833
  .slice(0, 20)
@@ -1776,7 +1871,10 @@ export function deleteLines(params) {
1776
1871
  error: confirmationFailureMessage('noteplan_delete_lines', confirmation.reason),
1777
1872
  };
1778
1873
  }
1779
- const newContent = frontmatter.deleteLines(note.content, boundedStartLine, boundedEndLine);
1874
+ // Splice out lines using absolute indices (no frontmatter offset needed)
1875
+ const splicedLines = [...allLines];
1876
+ splicedLines.splice(boundedStartLine - 1, lineCountToDelete);
1877
+ const newContent = splicedLines.join('\n');
1780
1878
  const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1781
1879
  store.updateNote(writeIdentifier, newContent, { source: note.source });
1782
1880
  return {
@@ -1810,13 +1908,19 @@ export function editLine(params) {
1810
1908
  const note = resolved.note;
1811
1909
  const lines = note.content.split('\n');
1812
1910
  const originalLineCount = lines.length;
1813
- // Offset past frontmatter so line 1 = first content line
1814
- const fmOffset = frontmatter.getFrontmatterLineCount(note.content);
1815
- const lineIndex = fmOffset + Number(params.line) - 1; // Convert to 0-indexed, skip FM
1816
- if (lineIndex < fmOffset || lineIndex >= lines.length) {
1911
+ // Line numbers are absolute (1-indexed), matching get_notes/getParagraphs
1912
+ const fmLineCount = frontmatter.getFrontmatterLineCount(note.content);
1913
+ const lineIndex = Number(params.line) - 1; // Convert to 0-indexed
1914
+ if (lineIndex < 0 || lineIndex >= lines.length) {
1915
+ return {
1916
+ success: false,
1917
+ error: `Line ${params.line} does not exist (note has ${lines.length} lines)`,
1918
+ };
1919
+ }
1920
+ if (fmLineCount > 0 && lineIndex < fmLineCount) {
1817
1921
  return {
1818
1922
  success: false,
1819
- error: `Line ${params.line} does not exist (note has ${lines.length - fmOffset} content lines)`,
1923
+ error: `Line ${params.line} is inside frontmatter (lines 1-${fmLineCount}). Content starts at line ${fmLineCount + 1}.`,
1820
1924
  };
1821
1925
  }
1822
1926
  const originalLine = lines[lineIndex];
@@ -1862,6 +1966,15 @@ export function editLine(params) {
1862
1966
  }
1863
1967
  export function replaceLines(params) {
1864
1968
  try {
1969
+ // Validate required line params — MCP may deliver them as undefined when omitted
1970
+ const rawStart = params.startLine !== undefined && params.startLine !== null ? Number(params.startLine) : NaN;
1971
+ const rawEnd = params.endLine !== undefined && params.endLine !== null ? Number(params.endLine) : NaN;
1972
+ if (!Number.isFinite(rawStart)) {
1973
+ return { success: false, error: 'startLine is required (1-indexed).' };
1974
+ }
1975
+ if (!Number.isFinite(rawEnd)) {
1976
+ return { success: false, error: 'endLine is required (1-indexed). Pass the same value as startLine to replace a single line.' };
1977
+ }
1865
1978
  const resolved = resolveWritableNoteReference(params);
1866
1979
  if (!resolved.note) {
1867
1980
  return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
@@ -1869,16 +1982,31 @@ export function replaceLines(params) {
1869
1982
  const note = resolved.note;
1870
1983
  const allLines = note.content.split('\n');
1871
1984
  const originalLineCount = allLines.length;
1872
- // Line numbers are relative to content after frontmatter
1873
- const fmOffset = frontmatter.getFrontmatterLineCount(note.content);
1874
- const contentLineCount = originalLineCount - fmOffset;
1875
- const boundedStartLine = toBoundedInt(params.startLine, 1, 1, Math.max(1, contentLineCount));
1876
- const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, contentLineCount));
1877
- const startIndex = fmOffset + boundedStartLine - 1;
1878
- const lineCountToReplace = boundedEndLine - boundedStartLine + 1;
1879
- const replacedText = allLines.slice(startIndex, fmOffset + boundedEndLine).join('\n');
1985
+ // Line numbers are absolute (1-indexed), matching get_notes/getParagraphs
1986
+ const fmLineCount = frontmatter.getFrontmatterLineCount(note.content);
1987
+ const minLine = fmLineCount > 0 ? fmLineCount + 1 : 1;
1988
+ const boundedStartLine = toBoundedInt(params.startLine, minLine, minLine, Math.max(minLine, originalLineCount));
1989
+ const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, originalLineCount));
1990
+ if (boundedStartLine <= fmLineCount) {
1991
+ return {
1992
+ success: false,
1993
+ error: `Lines 1-${fmLineCount} are frontmatter and cannot be replaced directly. Content starts at line ${fmLineCount + 1}.`,
1994
+ };
1995
+ }
1996
+ let startIndex = boundedStartLine - 1;
1997
+ let lineCountToReplace = boundedEndLine - boundedStartLine + 1;
1998
+ const replacedText = allLines.slice(startIndex, boundedEndLine).join('\n');
1880
1999
  const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1881
2000
  const normalized = normalizeContentIndentation(params.content, indentationStyle);
2001
+ // If replacement content includes frontmatter and the note already has
2002
+ // frontmatter, extend the splice to replace the old frontmatter too.
2003
+ // The agent's frontmatter is the intended update.
2004
+ const replacementHasFm = fmLineCount > 0
2005
+ && frontmatter.parseNoteContent(normalized.content).hasFrontmatter;
2006
+ if (replacementHasFm) {
2007
+ lineCountToReplace += startIndex; // extend to also cover frontmatter lines
2008
+ startIndex = 0;
2009
+ }
1882
2010
  if (params.allowEmptyContent !== true && normalized.content.trim().length === 0) {
1883
2011
  return {
1884
2012
  success: false,
@@ -1956,4 +2084,216 @@ export function replaceLines(params) {
1956
2084
  };
1957
2085
  }
1958
2086
  }
2087
+ // ---------------------------------------------------------------------------
2088
+ // searchParagraphsGlobal — search ALL lines (including frontmatter) across notes
2089
+ // ---------------------------------------------------------------------------
2090
+ function normalizeType(value) {
2091
+ if (typeof value !== 'string')
2092
+ return undefined;
2093
+ const lower = value.trim().toLowerCase();
2094
+ if (lower === 'calendar' || lower === 'note' || lower === 'trash')
2095
+ return lower;
2096
+ return undefined;
2097
+ }
2098
+ function normalizeTypeList(values) {
2099
+ if (!Array.isArray(values))
2100
+ return undefined;
2101
+ const unique = new Set();
2102
+ for (const entry of values) {
2103
+ const normalized = normalizeType(entry);
2104
+ if (normalized)
2105
+ unique.add(normalized);
2106
+ }
2107
+ return unique.size > 0 ? Array.from(unique) : undefined;
2108
+ }
2109
+ function isPeriodicCalendarNote(note) {
2110
+ if (note.type !== 'calendar' || !note.date)
2111
+ return false;
2112
+ return note.date.includes('-');
2113
+ }
2114
+ export const searchParagraphsGlobalSchema = z.object({
2115
+ query: z.string().describe('Text to find across all notes (searches ALL lines including frontmatter)'),
2116
+ caseSensitive: z.boolean().optional().default(false).describe('Case-sensitive match (default: false)'),
2117
+ wholeWord: z.boolean().optional().default(false).describe('Require whole-word matches (default: false)'),
2118
+ status: z
2119
+ .enum(['open', 'done', 'cancelled', 'scheduled'])
2120
+ .optional()
2121
+ .describe('Filter results to only lines with this task status'),
2122
+ contextLines: z.number().min(0).max(5).optional().default(1).describe('Context lines before/after each match'),
2123
+ paragraphMaxChars: z
2124
+ .number()
2125
+ .min(50)
2126
+ .max(5000)
2127
+ .optional()
2128
+ .default(600)
2129
+ .describe('Maximum paragraph text chars per match'),
2130
+ folder: z.string().optional().describe('Restrict to a specific folder path'),
2131
+ space: z.string().optional().describe('Restrict to a specific space name or ID'),
2132
+ noteQuery: z.string().optional().describe('Filter notes by title/filename/folder substring'),
2133
+ noteTypes: z
2134
+ .array(z.enum(['calendar', 'note', 'trash']))
2135
+ .optional()
2136
+ .describe('Restrict scanned notes by type'),
2137
+ preferCalendar: z
2138
+ .boolean()
2139
+ .optional()
2140
+ .default(false)
2141
+ .describe('Prioritize calendar notes before maxNotes truncation'),
2142
+ periodicOnly: z
2143
+ .boolean()
2144
+ .optional()
2145
+ .default(false)
2146
+ .describe('When true, only scan periodic calendar notes (weekly/monthly/quarterly/yearly)'),
2147
+ maxNotes: z.number().min(1).max(2000).optional().default(500).describe('Maximum notes to scan'),
2148
+ limit: z.number().min(1).max(300).optional().default(30).describe('Maximum matches to return'),
2149
+ offset: z.number().min(0).optional().default(0).describe('Pagination offset'),
2150
+ cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
2151
+ });
2152
+ export function searchParagraphsGlobal(params) {
2153
+ const query = typeof params?.query === 'string' ? params.query.trim() : '';
2154
+ if (!query) {
2155
+ return {
2156
+ success: false,
2157
+ error: 'query is required',
2158
+ };
2159
+ }
2160
+ const caseSensitive = params.caseSensitive ?? false;
2161
+ const wholeWord = params.wholeWord ?? false;
2162
+ const contextLines = toBoundedInt(params.contextLines, 1, 0, 5);
2163
+ const paragraphMaxChars = toBoundedInt(params.paragraphMaxChars, 600, 50, 5000);
2164
+ const normalizedQuery = caseSensitive ? query : query.toLowerCase();
2165
+ const wildcardQuery = query === '*';
2166
+ const matcher = wholeWord
2167
+ ? new RegExp(`\\b${escapeRegExp(query)}\\b`, caseSensitive ? '' : 'i')
2168
+ : null;
2169
+ const maxNotes = toBoundedInt(params.maxNotes, 500, 1, 2000);
2170
+ const noteQuery = typeof params.noteQuery === 'string' ? params.noteQuery.trim().toLowerCase() : '';
2171
+ const noteTypes = normalizeTypeList(params.noteTypes);
2172
+ const preferCalendar = params.preferCalendar === true;
2173
+ const periodicOnly = params.periodicOnly === true;
2174
+ const allNotes = store.listNotes({
2175
+ folder: params.folder,
2176
+ space: params.space,
2177
+ });
2178
+ let filteredNotes = noteQuery
2179
+ ? allNotes.filter((note) => {
2180
+ const haystack = `${note.title} ${note.filename} ${note.folder || ''}`.toLowerCase();
2181
+ return haystack.includes(noteQuery);
2182
+ })
2183
+ : allNotes;
2184
+ if (noteTypes && noteTypes.length > 0) {
2185
+ filteredNotes = filteredNotes.filter((note) => noteTypes.includes(note.type));
2186
+ }
2187
+ if (periodicOnly) {
2188
+ filteredNotes = filteredNotes.filter((note) => isPeriodicCalendarNote(note));
2189
+ }
2190
+ if (preferCalendar) {
2191
+ filteredNotes = [...filteredNotes].sort((a, b) => {
2192
+ const aCalendar = a.type === 'calendar' ? 1 : 0;
2193
+ const bCalendar = b.type === 'calendar' ? 1 : 0;
2194
+ if (aCalendar !== bCalendar)
2195
+ return bCalendar - aCalendar;
2196
+ const aModified = a.modifiedAt?.getTime() ?? 0;
2197
+ const bModified = b.modifiedAt?.getTime() ?? 0;
2198
+ return bModified - aModified;
2199
+ });
2200
+ }
2201
+ const scannedNotes = filteredNotes.slice(0, maxNotes);
2202
+ const truncatedByMaxNotes = filteredNotes.length > scannedNotes.length;
2203
+ const allMatches = [];
2204
+ for (const note of scannedNotes) {
2205
+ const allLines = note.content.split('\n');
2206
+ for (let lineIndex = 0; lineIndex < allLines.length; lineIndex++) {
2207
+ const lineContent = allLines[lineIndex];
2208
+ const haystack = caseSensitive ? lineContent : lineContent.toLowerCase();
2209
+ const isMatch = wildcardQuery
2210
+ ? true
2211
+ : matcher
2212
+ ? matcher.test(lineContent)
2213
+ : haystack.includes(normalizedQuery);
2214
+ if (!isMatch)
2215
+ continue;
2216
+ const paragraphBounds = findParagraphBounds(allLines, lineIndex);
2217
+ const paragraphRaw = allLines
2218
+ .slice(paragraphBounds.startIndex, paragraphBounds.endIndex + 1)
2219
+ .join('\n');
2220
+ const paragraphTruncated = paragraphRaw.length > paragraphMaxChars;
2221
+ const paragraph = paragraphTruncated
2222
+ ? `${paragraphRaw.slice(0, Math.max(0, paragraphMaxChars - 3))}...`
2223
+ : paragraphRaw;
2224
+ const contextStart = Math.max(0, lineIndex - contextLines);
2225
+ const contextEnd = Math.min(allLines.length - 1, lineIndex + contextLines);
2226
+ const meta = parseParagraphLine(lineContent, lineIndex, lineIndex === 0);
2227
+ // Apply status filter (backward compat with searchTasksGlobal)
2228
+ if (params.status && meta.taskStatus !== params.status)
2229
+ continue;
2230
+ allMatches.push({
2231
+ note: {
2232
+ id: note.id,
2233
+ title: note.title,
2234
+ filename: note.filename,
2235
+ type: note.type,
2236
+ source: note.source,
2237
+ folder: note.folder,
2238
+ spaceId: note.spaceId,
2239
+ date: note.date,
2240
+ },
2241
+ lineIndex,
2242
+ line: lineIndex + 1,
2243
+ content: lineContent,
2244
+ // Backward-compat alias: `status` mirrors `taskStatus` for old consumers
2245
+ ...(meta.taskStatus !== undefined && { status: meta.taskStatus }),
2246
+ type: meta.type,
2247
+ indentLevel: meta.indentLevel,
2248
+ ...(meta.headingLevel !== undefined && { headingLevel: meta.headingLevel }),
2249
+ ...(meta.taskStatus !== undefined && { taskStatus: meta.taskStatus }),
2250
+ ...(meta.priority !== undefined && { priority: meta.priority }),
2251
+ ...(meta.marker !== undefined && { marker: meta.marker }),
2252
+ ...(meta.hasCheckbox !== undefined && { hasCheckbox: meta.hasCheckbox }),
2253
+ tags: meta.tags,
2254
+ mentions: meta.mentions,
2255
+ ...(meta.scheduledDate !== undefined && { scheduledDate: meta.scheduledDate }),
2256
+ paragraphStartLine: paragraphBounds.startIndex + 1,
2257
+ paragraphEndLine: paragraphBounds.endIndex + 1,
2258
+ paragraph,
2259
+ paragraphTruncated,
2260
+ contextBefore: allLines.slice(contextStart, lineIndex),
2261
+ contextAfter: allLines.slice(lineIndex + 1, contextEnd + 1),
2262
+ });
2263
+ }
2264
+ }
2265
+ const offset = toBoundedInt(params.cursor ?? params.offset, 0, 0, Number.MAX_SAFE_INTEGER);
2266
+ const limit = toBoundedInt(params.limit, 30, 1, 300);
2267
+ const page = allMatches.slice(offset, offset + limit);
2268
+ const hasMore = offset + page.length < allMatches.length;
2269
+ const nextCursor = hasMore ? String(offset + page.length) : null;
2270
+ const result = {
2271
+ success: true,
2272
+ query,
2273
+ count: page.length,
2274
+ totalCount: allMatches.length,
2275
+ offset,
2276
+ limit,
2277
+ hasMore,
2278
+ nextCursor,
2279
+ scannedNoteCount: scannedNotes.length,
2280
+ totalNotes: filteredNotes.length,
2281
+ truncatedByMaxNotes,
2282
+ maxNotes,
2283
+ noteTypes,
2284
+ preferCalendar,
2285
+ periodicOnly,
2286
+ matches: page,
2287
+ };
2288
+ if (hasMore) {
2289
+ result.performanceHints = ['Continue with nextCursor to fetch the next global paragraph match page.'];
2290
+ }
2291
+ if (truncatedByMaxNotes) {
2292
+ result.performanceHints = [
2293
+ ...(result.performanceHints ?? []),
2294
+ 'Increase maxNotes or narrow folder/space/noteQuery to reduce truncation.',
2295
+ ];
2296
+ }
2297
+ return result;
2298
+ }
1959
2299
  //# sourceMappingURL=notes.js.map