@noteplanco/noteplan-mcp 1.1.6 → 1.1.7

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 (73) hide show
  1. package/dist/noteplan/file-writer.d.ts.map +1 -1
  2. package/dist/noteplan/file-writer.js +73 -16
  3. package/dist/noteplan/file-writer.js.map +1 -1
  4. package/dist/noteplan/file-writer.test.d.ts +2 -0
  5. package/dist/noteplan/file-writer.test.d.ts.map +1 -0
  6. package/dist/noteplan/file-writer.test.js +892 -0
  7. package/dist/noteplan/file-writer.test.js.map +1 -0
  8. package/dist/noteplan/filter-store.d.ts.map +1 -1
  9. package/dist/noteplan/filter-store.js +13 -1
  10. package/dist/noteplan/filter-store.js.map +1 -1
  11. package/dist/noteplan/frontmatter-parser.d.ts +10 -1
  12. package/dist/noteplan/frontmatter-parser.d.ts.map +1 -1
  13. package/dist/noteplan/frontmatter-parser.js +66 -10
  14. package/dist/noteplan/frontmatter-parser.js.map +1 -1
  15. package/dist/noteplan/frontmatter-parser.test.js +484 -1
  16. package/dist/noteplan/frontmatter-parser.test.js.map +1 -1
  17. package/dist/noteplan/markdown-parser.d.ts +6 -1
  18. package/dist/noteplan/markdown-parser.d.ts.map +1 -1
  19. package/dist/noteplan/markdown-parser.js +21 -44
  20. package/dist/noteplan/markdown-parser.js.map +1 -1
  21. package/dist/noteplan/markdown-parser.test.d.ts +2 -0
  22. package/dist/noteplan/markdown-parser.test.d.ts.map +1 -0
  23. package/dist/noteplan/markdown-parser.test.js +653 -0
  24. package/dist/noteplan/markdown-parser.test.js.map +1 -0
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +405 -205
  27. package/dist/server.js.map +1 -1
  28. package/dist/tools/attachments.d.ts +151 -0
  29. package/dist/tools/attachments.d.ts.map +1 -0
  30. package/dist/tools/attachments.js +421 -0
  31. package/dist/tools/attachments.js.map +1 -0
  32. package/dist/tools/attachments.test.d.ts +2 -0
  33. package/dist/tools/attachments.test.d.ts.map +1 -0
  34. package/dist/tools/attachments.test.js +561 -0
  35. package/dist/tools/attachments.test.js.map +1 -0
  36. package/dist/tools/calendar.d.ts +5 -5
  37. package/dist/tools/notes.d.ts +67 -26
  38. package/dist/tools/notes.d.ts.map +1 -1
  39. package/dist/tools/notes.js +124 -33
  40. package/dist/tools/notes.js.map +1 -1
  41. package/dist/tools/notes.test.d.ts +2 -0
  42. package/dist/tools/notes.test.d.ts.map +1 -0
  43. package/dist/tools/notes.test.js +598 -0
  44. package/dist/tools/notes.test.js.map +1 -0
  45. package/dist/tools/reminders.d.ts +4 -4
  46. package/dist/tools/tasks.d.ts +10 -10
  47. package/dist/tools/tasks.d.ts.map +1 -1
  48. package/dist/tools/tasks.js +14 -27
  49. package/dist/tools/tasks.js.map +1 -1
  50. package/dist/tools/templates.d.ts +69 -0
  51. package/dist/tools/templates.d.ts.map +1 -0
  52. package/dist/tools/templates.js +145 -0
  53. package/dist/tools/templates.js.map +1 -0
  54. package/dist/tools/templates.test.d.ts +2 -0
  55. package/dist/tools/templates.test.d.ts.map +1 -0
  56. package/dist/tools/templates.test.js +48 -0
  57. package/dist/tools/templates.test.js.map +1 -0
  58. package/dist/tools/ui.d.ts +2 -0
  59. package/dist/tools/ui.d.ts.map +1 -1
  60. package/dist/tools/ui.js +24 -0
  61. package/dist/tools/ui.js.map +1 -1
  62. package/dist/utils/applescript.d.ts.map +1 -1
  63. package/dist/utils/applescript.js +21 -0
  64. package/dist/utils/applescript.js.map +1 -1
  65. package/dist/utils/confirmation-tokens.test.d.ts +2 -0
  66. package/dist/utils/confirmation-tokens.test.d.ts.map +1 -0
  67. package/dist/utils/confirmation-tokens.test.js +159 -0
  68. package/dist/utils/confirmation-tokens.test.js.map +1 -0
  69. package/dist/utils/version.d.ts +2 -0
  70. package/dist/utils/version.d.ts.map +1 -1
  71. package/dist/utils/version.js +4 -0
  72. package/dist/utils/version.js.map +1 -1
  73. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import path from 'path';
4
4
  import * as store from '../noteplan/unified-store.js';
5
5
  import * as frontmatter from '../noteplan/frontmatter-parser.js';
6
+ import { ensureTemplateFrontmatter } from './templates.js';
6
7
  import { issueConfirmationToken, validateAndConsumeConfirmationToken, } from '../utils/confirmation-tokens.js';
7
8
  import { parseParagraphLine, buildParagraphLine } from '../noteplan/markdown-parser.js';
8
9
  function toBoundedInt(value, defaultValue, min, max) {
@@ -34,6 +35,17 @@ function toOptionalBoolean(value) {
34
35
  }
35
36
  return undefined;
36
37
  }
38
+ /**
39
+ * Coerce a value to boolean — handles MCP delivering boolean params as strings.
40
+ * Returns true for boolean true or string "true".
41
+ */
42
+ function isTrueBool(value) {
43
+ if (typeof value === 'boolean')
44
+ return value;
45
+ if (typeof value === 'string')
46
+ return value.trim().toLowerCase() === 'true';
47
+ return false;
48
+ }
37
49
  function confirmationFailureMessage(toolName, reason) {
38
50
  const refreshHint = `Call ${toolName} with dryRun=true to get a new confirmationToken.`;
39
51
  if (reason === 'missing') {
@@ -286,6 +298,8 @@ export const createNoteSchema = z.object({
286
298
  folder: z.string().optional().describe('Folder to create the note in. Supports smart matching (e.g., "projects" matches "10 - Projects")'),
287
299
  create_new_folder: z.boolean().optional().describe('Set to true to create a new folder instead of matching existing ones'),
288
300
  space: z.string().optional().describe('Space name or ID to create in (e.g., "My Team" or a UUID)'),
301
+ noteType: z.enum(['note', 'template']).optional().default('note').describe('Type of note to create. Use "template" to create in @Templates with proper frontmatter'),
302
+ templateTypes: z.array(z.enum(['empty-note', 'meeting-note', 'project-note', 'calendar-note'])).optional().describe('Template type tags — used when noteType="template"'),
289
303
  });
290
304
  export const updateNoteSchema = z.object({
291
305
  filename: z.string().describe('Filename/path of the note to update'),
@@ -358,6 +372,8 @@ export const moveNoteSchema = z.object({
358
372
  export const renameNoteFileSchema = z.object({
359
373
  id: z.string().optional().describe('Note ID (preferred for TeamSpace notes)'),
360
374
  filename: z.string().optional().describe('Filename/path of the note to rename'),
375
+ title: z.string().optional().describe('Note title to find and rename (fuzzy matched)'),
376
+ query: z.string().optional().describe('Fuzzy search query to find the note'),
361
377
  space: z.string().optional().describe('Space name or ID to search in'),
362
378
  newFilename: z
363
379
  .string()
@@ -381,11 +397,11 @@ export const renameNoteFileSchema = z.object({
381
397
  .optional()
382
398
  .describe('Confirmation token issued by dryRun for rename execution'),
383
399
  }).superRefine((input, ctx) => {
384
- if (!input.id && !input.filename) {
400
+ if (!input.id && !input.filename && !input.title && !input.query) {
385
401
  ctx.addIssue({
386
402
  code: z.ZodIssueCode.custom,
387
- message: 'Provide one note reference: id or filename',
388
- path: ['filename'],
403
+ message: 'Provide one note reference: id, filename, title, or query',
404
+ path: ['title'],
389
405
  });
390
406
  }
391
407
  });
@@ -670,8 +686,13 @@ export function resolveNote(params) {
670
686
  }
671
687
  export function createNote(params) {
672
688
  try {
673
- const result = store.createNote(params.title, params.content, {
674
- folder: params.folder,
689
+ const isTemplate = params.noteType === 'template';
690
+ const folder = isTemplate && !params.folder ? '@Templates' : params.folder;
691
+ const content = isTemplate
692
+ ? ensureTemplateFrontmatter(params.title, params.content, params.templateTypes)
693
+ : params.content;
694
+ const result = store.createNote(params.title, content, {
695
+ folder,
675
696
  space: params.space,
676
697
  createNewFolder: params.create_new_folder,
677
698
  });
@@ -722,7 +743,7 @@ export function updateNote(params) {
722
743
  error: 'Empty content is blocked for noteplan_update_note. Use allowEmptyContent=true to override intentionally.',
723
744
  };
724
745
  }
725
- if (params.dryRun === true) {
746
+ if (isTrueBool(params.dryRun)) {
726
747
  const token = issueConfirmationToken({
727
748
  tool: 'noteplan_update_note',
728
749
  target: params.filename,
@@ -788,7 +809,7 @@ export function deleteNote(params) {
788
809
  error: 'Note not found',
789
810
  };
790
811
  }
791
- if (params.dryRun === true) {
812
+ if (isTrueBool(params.dryRun)) {
792
813
  const token = issueConfirmationToken({
793
814
  tool: 'noteplan_delete_note',
794
815
  target: target.identifier,
@@ -852,7 +873,7 @@ export function moveNote(params) {
852
873
  }
853
874
  const preview = store.previewMoveNote(target.identifier, params.destinationFolder);
854
875
  const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}::${preview.destinationParentId ?? preview.destinationFolder}`;
855
- if (params.dryRun === true) {
876
+ if (isTrueBool(params.dryRun)) {
856
877
  const token = issueConfirmationToken({
857
878
  tool: 'noteplan_move_note',
858
879
  target: confirmationTarget,
@@ -926,7 +947,7 @@ export function restoreNote(params) {
926
947
  }
927
948
  const preview = store.previewRestoreNote(target.identifier, params.destinationFolder);
928
949
  const confirmationTarget = `${preview.fromIdentifier}=>${preview.toIdentifier}`;
929
- if (params.dryRun === true) {
950
+ if (isTrueBool(params.dryRun)) {
930
951
  const token = issueConfirmationToken({
931
952
  tool: 'noteplan_restore_note',
932
953
  target: confirmationTarget,
@@ -990,12 +1011,16 @@ export function restoreNote(params) {
990
1011
  }
991
1012
  export function renameNoteFile(params) {
992
1013
  try {
993
- // Resolve the note to determine if it's local or space
994
- const target = resolveNoteTarget(params.id, params.filename, params.space);
995
- if (!target.note) {
996
- return { success: false, error: 'Note not found' };
1014
+ // Resolve the note supports id, filename, title, or query
1015
+ const resolved = resolveWritableNoteReference(params);
1016
+ if (!resolved.note) {
1017
+ return {
1018
+ success: false,
1019
+ error: resolved.error || 'Note not found',
1020
+ candidates: resolved.candidates,
1021
+ };
997
1022
  }
998
- const note = target.note;
1023
+ const note = resolved.note;
999
1024
  // Space note: rename title
1000
1025
  if (note.source === 'space') {
1001
1026
  if (!params.newTitle) {
@@ -1006,7 +1031,7 @@ export function renameNoteFile(params) {
1006
1031
  }
1007
1032
  const writeId = note.id || note.filename;
1008
1033
  const confirmationTarget = `${note.title}=>${params.newTitle}`;
1009
- if (params.dryRun === true) {
1034
+ if (isTrueBool(params.dryRun)) {
1010
1035
  const token = issueConfirmationToken({
1011
1036
  tool: 'noteplan_rename_note_file',
1012
1037
  target: confirmationTarget,
@@ -1059,16 +1084,18 @@ export function renameNoteFile(params) {
1059
1084
  };
1060
1085
  }
1061
1086
  // Local note: rename file
1062
- if (!params.newFilename) {
1087
+ // Accept newTitle as an alias for newFilename — "rename the note" typically means changing the title
1088
+ const effectiveNewFilename = params.newFilename || params.newTitle;
1089
+ if (!effectiveNewFilename) {
1063
1090
  return {
1064
1091
  success: false,
1065
- error: 'newFilename is required for local notes (use newTitle for TeamSpace notes)',
1092
+ error: 'newFilename or newTitle is required for local notes',
1066
1093
  };
1067
1094
  }
1068
1095
  const keepExtension = params.keepExtension ?? true;
1069
- const preview = store.previewRenameNoteFile(note.filename, params.newFilename, keepExtension);
1096
+ const preview = store.previewRenameNoteFile(note.filename, effectiveNewFilename, keepExtension);
1070
1097
  const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}`;
1071
- if (params.dryRun === true) {
1098
+ if (isTrueBool(params.dryRun)) {
1072
1099
  const token = issueConfirmationToken({
1073
1100
  tool: 'noteplan_rename_note_file',
1074
1101
  target: confirmationTarget,
@@ -1103,7 +1130,26 @@ export function renameNoteFile(params) {
1103
1130
  error: confirmationFailureMessage('noteplan_rename_note_file', confirmation.reason),
1104
1131
  };
1105
1132
  }
1106
- const renamed = store.renameNoteFile(note.filename, params.newFilename, keepExtension);
1133
+ const renamed = store.renameNoteFile(note.filename, effectiveNewFilename, keepExtension);
1134
+ // Also update the # Title heading in the note content if it matches the old title
1135
+ const newTitle = params.newTitle || params.newFilename;
1136
+ if (newTitle && renamed.note.content) {
1137
+ const lines = renamed.note.content.split('\n');
1138
+ const titleLineIndex = lines.findIndex((l) => /^#\s+/.test(l));
1139
+ if (titleLineIndex !== -1) {
1140
+ const oldHeadingTitle = lines[titleLineIndex].replace(/^#\s+/, '');
1141
+ // Update heading if it matches the old note title (or old filename without extension)
1142
+ const oldTitle = note.title || '';
1143
+ const oldFilenameBase = note.filename.replace(/^.*\//, '').replace(/\.\w+$/, '');
1144
+ if (oldHeadingTitle === oldTitle || oldHeadingTitle === oldFilenameBase) {
1145
+ lines[titleLineIndex] = `# ${newTitle}`;
1146
+ const writeTarget = getWritableIdentifier(renamed.note);
1147
+ store.updateNote(writeTarget.identifier, lines.join('\n'), {
1148
+ source: writeTarget.source,
1149
+ });
1150
+ }
1151
+ }
1152
+ }
1107
1153
  return {
1108
1154
  success: true,
1109
1155
  message: `Note renamed to ${renamed.toFilename}`,
@@ -1427,6 +1473,7 @@ export const appendContentSchema = z.object({
1427
1473
  query: z.string().optional().describe('Resolvable note query (fuzzy note lookup before append)'),
1428
1474
  space: z.string().optional().describe('Space name or ID scope for title/date/query resolution'),
1429
1475
  content: z.string().describe('Content to append'),
1476
+ heading: z.string().optional().describe('Heading or section marker text — when provided, appends at end of that section instead of end of note'),
1430
1477
  indentationStyle: z
1431
1478
  .enum(['tabs', 'preserve'])
1432
1479
  .optional()
@@ -1555,6 +1602,41 @@ export function insertContent(params) {
1555
1602
  const note = resolved.note;
1556
1603
  const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1557
1604
  let contentToInsert = params.content;
1605
+ // Auto-correct position when line number is provided but position is wrong
1606
+ // Catches LLMs sending { position: "start", line: 5 } instead of { position: "at-line", line: 5 }
1607
+ if (params.line !== undefined && params.position !== 'at-line') {
1608
+ params.position = 'at-line';
1609
+ }
1610
+ // Auto-detect raw task/checklist markdown when type is not explicitly set
1611
+ // Catches LLMs sending "- [ ] Buy groceries", "* [x] Done", "* Buy groceries", "+ Item" without proper type
1612
+ if (!params.type && /^[\t ]*[*+\-]\s+/.test(contentToInsert)) {
1613
+ // Determine type from the marker character
1614
+ const markerMatch = contentToInsert.match(/^[\t ]*([*+\-])\s+/);
1615
+ const markerChar = markerMatch?.[1];
1616
+ if (markerChar === '+') {
1617
+ params.type = 'checklist';
1618
+ }
1619
+ else if (markerChar === '*') {
1620
+ params.type = 'task';
1621
+ }
1622
+ else if (markerChar === '-' && /^[\t ]*-\s+\[[ x\->]\]\s+/.test(contentToInsert)) {
1623
+ // Dash with checkbox is clearly a task (plain "- text" could be a bullet, so only match with checkbox)
1624
+ params.type = 'task';
1625
+ }
1626
+ // Detect status from the checkbox marker if present
1627
+ if (params.type) {
1628
+ const statusMatch = contentToInsert.match(/\[(.)\]/);
1629
+ if (statusMatch) {
1630
+ const marker = statusMatch[1];
1631
+ if (marker === 'x')
1632
+ params.taskStatus = 'done';
1633
+ else if (marker === '-')
1634
+ params.taskStatus = 'cancelled';
1635
+ else if (marker === '>')
1636
+ params.taskStatus = 'scheduled';
1637
+ }
1638
+ }
1639
+ }
1558
1640
  if (params.type) {
1559
1641
  contentToInsert = contentToInsert
1560
1642
  .split('\n')
@@ -1610,6 +1692,7 @@ export function appendContent(params) {
1610
1692
  const normalized = normalizeContentIndentation(params.content, indentationStyle);
1611
1693
  const newContent = frontmatter.insertContentAtPosition(note.content, normalized.content, {
1612
1694
  position: 'end',
1695
+ heading: params.heading,
1613
1696
  });
1614
1697
  const writeTarget = getWritableIdentifier(note);
1615
1698
  store.updateNote(writeTarget.identifier, newContent, {
@@ -1642,11 +1725,14 @@ export function deleteLines(params) {
1642
1725
  }
1643
1726
  const note = resolved.note;
1644
1727
  const allLines = note.content.split('\n');
1645
- const boundedStartLine = toBoundedInt(params.startLine, 1, 1, Math.max(1, allLines.length));
1646
- const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, allLines.length));
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));
1647
1733
  const lineCountToDelete = boundedEndLine - boundedStartLine + 1;
1648
- const previewStartIndex = boundedStartLine - 1;
1649
- const previewEndIndexExclusive = boundedEndLine;
1734
+ const previewStartIndex = fmOffset + boundedStartLine - 1;
1735
+ const previewEndIndexExclusive = fmOffset + boundedEndLine;
1650
1736
  const deletedLinesPreview = allLines
1651
1737
  .slice(previewStartIndex, previewEndIndexExclusive)
1652
1738
  .slice(0, 20)
@@ -1660,7 +1746,7 @@ export function deleteLines(params) {
1660
1746
  ? buildAttachmentWarningMessage(removedAttachmentReferences.length)
1661
1747
  : undefined;
1662
1748
  const confirmTarget = `${note.filename}:${boundedStartLine}-${boundedEndLine}`;
1663
- if (params.dryRun === true) {
1749
+ if (isTrueBool(params.dryRun)) {
1664
1750
  const token = issueConfirmationToken({
1665
1751
  tool: 'noteplan_delete_lines',
1666
1752
  target: confirmTarget,
@@ -1724,11 +1810,13 @@ export function editLine(params) {
1724
1810
  const note = resolved.note;
1725
1811
  const lines = note.content.split('\n');
1726
1812
  const originalLineCount = lines.length;
1727
- const lineIndex = params.line - 1; // Convert to 0-indexed
1728
- if (lineIndex < 0 || lineIndex >= 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) {
1729
1817
  return {
1730
1818
  success: false,
1731
- error: `Line ${params.line} does not exist (note has ${lines.length} lines)`,
1819
+ error: `Line ${params.line} does not exist (note has ${lines.length - fmOffset} content lines)`,
1732
1820
  };
1733
1821
  }
1734
1822
  const originalLine = lines[lineIndex];
@@ -1781,11 +1869,14 @@ export function replaceLines(params) {
1781
1869
  const note = resolved.note;
1782
1870
  const allLines = note.content.split('\n');
1783
1871
  const originalLineCount = allLines.length;
1784
- const boundedStartLine = toBoundedInt(params.startLine, 1, 1, Math.max(1, originalLineCount));
1785
- const boundedEndLine = toBoundedInt(params.endLine, boundedStartLine, boundedStartLine, Math.max(boundedStartLine, originalLineCount));
1786
- const startIndex = boundedStartLine - 1;
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;
1787
1878
  const lineCountToReplace = boundedEndLine - boundedStartLine + 1;
1788
- const replacedText = allLines.slice(startIndex, boundedEndLine).join('\n');
1879
+ const replacedText = allLines.slice(startIndex, fmOffset + boundedEndLine).join('\n');
1789
1880
  const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1790
1881
  const normalized = normalizeContentIndentation(params.content, indentationStyle);
1791
1882
  if (params.allowEmptyContent !== true && normalized.content.trim().length === 0) {
@@ -1806,7 +1897,7 @@ export function replaceLines(params) {
1806
1897
  warnings.push(`Line numbers shifted by ${lineDelta > 0 ? '+' : ''}${lineDelta} after this replacement. Re-read line numbers before the next mutation.`);
1807
1898
  }
1808
1899
  const target = `${note.filename}:${boundedStartLine}-${boundedEndLine}:${replacementLines.length}:${normalized.content.length}`;
1809
- if (params.dryRun === true) {
1900
+ if (isTrueBool(params.dryRun)) {
1810
1901
  const token = issueConfirmationToken({
1811
1902
  tool: 'noteplan_replace_lines',
1812
1903
  target,