@noteplanco/noteplan-mcp 1.1.1

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 (155) hide show
  1. package/README.md +257 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +8 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/noteplan/embeddings.d.ts +170 -0
  7. package/dist/noteplan/embeddings.d.ts.map +1 -0
  8. package/dist/noteplan/embeddings.js +684 -0
  9. package/dist/noteplan/embeddings.js.map +1 -0
  10. package/dist/noteplan/file-reader.d.ts +77 -0
  11. package/dist/noteplan/file-reader.d.ts.map +1 -0
  12. package/dist/noteplan/file-reader.js +488 -0
  13. package/dist/noteplan/file-reader.js.map +1 -0
  14. package/dist/noteplan/file-writer.d.ts +108 -0
  15. package/dist/noteplan/file-writer.d.ts.map +1 -0
  16. package/dist/noteplan/file-writer.js +621 -0
  17. package/dist/noteplan/file-writer.js.map +1 -0
  18. package/dist/noteplan/filter-store.d.ts +28 -0
  19. package/dist/noteplan/filter-store.d.ts.map +1 -0
  20. package/dist/noteplan/filter-store.js +180 -0
  21. package/dist/noteplan/filter-store.js.map +1 -0
  22. package/dist/noteplan/frontmatter-parser.d.ts +45 -0
  23. package/dist/noteplan/frontmatter-parser.d.ts.map +1 -0
  24. package/dist/noteplan/frontmatter-parser.js +259 -0
  25. package/dist/noteplan/frontmatter-parser.js.map +1 -0
  26. package/dist/noteplan/fuzzy-search.d.ts +7 -0
  27. package/dist/noteplan/fuzzy-search.d.ts.map +1 -0
  28. package/dist/noteplan/fuzzy-search.js +66 -0
  29. package/dist/noteplan/fuzzy-search.js.map +1 -0
  30. package/dist/noteplan/markdown-parser.d.ts +87 -0
  31. package/dist/noteplan/markdown-parser.d.ts.map +1 -0
  32. package/dist/noteplan/markdown-parser.js +519 -0
  33. package/dist/noteplan/markdown-parser.js.map +1 -0
  34. package/dist/noteplan/preferences.d.ts +44 -0
  35. package/dist/noteplan/preferences.d.ts.map +1 -0
  36. package/dist/noteplan/preferences.js +156 -0
  37. package/dist/noteplan/preferences.js.map +1 -0
  38. package/dist/noteplan/ripgrep-search.d.ts +29 -0
  39. package/dist/noteplan/ripgrep-search.d.ts.map +1 -0
  40. package/dist/noteplan/ripgrep-search.js +110 -0
  41. package/dist/noteplan/ripgrep-search.js.map +1 -0
  42. package/dist/noteplan/sqlite-reader.d.ts +77 -0
  43. package/dist/noteplan/sqlite-reader.d.ts.map +1 -0
  44. package/dist/noteplan/sqlite-reader.js +605 -0
  45. package/dist/noteplan/sqlite-reader.js.map +1 -0
  46. package/dist/noteplan/sqlite-writer.d.ts +63 -0
  47. package/dist/noteplan/sqlite-writer.d.ts.map +1 -0
  48. package/dist/noteplan/sqlite-writer.js +574 -0
  49. package/dist/noteplan/sqlite-writer.js.map +1 -0
  50. package/dist/noteplan/types.d.ts +97 -0
  51. package/dist/noteplan/types.d.ts.map +1 -0
  52. package/dist/noteplan/types.js +33 -0
  53. package/dist/noteplan/types.js.map +1 -0
  54. package/dist/noteplan/unified-store.d.ts +289 -0
  55. package/dist/noteplan/unified-store.d.ts.map +1 -0
  56. package/dist/noteplan/unified-store.js +1308 -0
  57. package/dist/noteplan/unified-store.js.map +1 -0
  58. package/dist/server.d.ts +4 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/server.js +2468 -0
  61. package/dist/server.js.map +1 -0
  62. package/dist/tools/calendar.d.ts +311 -0
  63. package/dist/tools/calendar.d.ts.map +1 -0
  64. package/dist/tools/calendar.js +504 -0
  65. package/dist/tools/calendar.js.map +1 -0
  66. package/dist/tools/embeddings.d.ts +244 -0
  67. package/dist/tools/embeddings.d.ts.map +1 -0
  68. package/dist/tools/embeddings.js +226 -0
  69. package/dist/tools/embeddings.js.map +1 -0
  70. package/dist/tools/events.d.ts +176 -0
  71. package/dist/tools/events.d.ts.map +1 -0
  72. package/dist/tools/events.js +326 -0
  73. package/dist/tools/events.js.map +1 -0
  74. package/dist/tools/filters.d.ts +205 -0
  75. package/dist/tools/filters.d.ts.map +1 -0
  76. package/dist/tools/filters.js +347 -0
  77. package/dist/tools/filters.js.map +1 -0
  78. package/dist/tools/memory.d.ts +6 -0
  79. package/dist/tools/memory.d.ts.map +1 -0
  80. package/dist/tools/memory.js +161 -0
  81. package/dist/tools/memory.js.map +1 -0
  82. package/dist/tools/notes.d.ts +1221 -0
  83. package/dist/tools/notes.d.ts.map +1 -0
  84. package/dist/tools/notes.js +1868 -0
  85. package/dist/tools/notes.js.map +1 -0
  86. package/dist/tools/plugins.d.ts +140 -0
  87. package/dist/tools/plugins.d.ts.map +1 -0
  88. package/dist/tools/plugins.js +782 -0
  89. package/dist/tools/plugins.js.map +1 -0
  90. package/dist/tools/reminders.d.ts +207 -0
  91. package/dist/tools/reminders.d.ts.map +1 -0
  92. package/dist/tools/reminders.js +323 -0
  93. package/dist/tools/reminders.js.map +1 -0
  94. package/dist/tools/search.d.ts +58 -0
  95. package/dist/tools/search.d.ts.map +1 -0
  96. package/dist/tools/search.js +373 -0
  97. package/dist/tools/search.js.map +1 -0
  98. package/dist/tools/spaces.d.ts +484 -0
  99. package/dist/tools/spaces.d.ts.map +1 -0
  100. package/dist/tools/spaces.js +870 -0
  101. package/dist/tools/spaces.js.map +1 -0
  102. package/dist/tools/tasks.d.ts +313 -0
  103. package/dist/tools/tasks.d.ts.map +1 -0
  104. package/dist/tools/tasks.js +690 -0
  105. package/dist/tools/tasks.js.map +1 -0
  106. package/dist/tools/themes.d.ts +91 -0
  107. package/dist/tools/themes.d.ts.map +1 -0
  108. package/dist/tools/themes.js +294 -0
  109. package/dist/tools/themes.js.map +1 -0
  110. package/dist/tools/ui.d.ts +89 -0
  111. package/dist/tools/ui.d.ts.map +1 -0
  112. package/dist/tools/ui.js +137 -0
  113. package/dist/tools/ui.js.map +1 -0
  114. package/dist/utils/applescript.d.ts +5 -0
  115. package/dist/utils/applescript.d.ts.map +1 -0
  116. package/dist/utils/applescript.js +27 -0
  117. package/dist/utils/applescript.js.map +1 -0
  118. package/dist/utils/confirmation-tokens.d.ts +19 -0
  119. package/dist/utils/confirmation-tokens.d.ts.map +1 -0
  120. package/dist/utils/confirmation-tokens.js +58 -0
  121. package/dist/utils/confirmation-tokens.js.map +1 -0
  122. package/dist/utils/date-filters.d.ts +15 -0
  123. package/dist/utils/date-filters.d.ts.map +1 -0
  124. package/dist/utils/date-filters.js +129 -0
  125. package/dist/utils/date-filters.js.map +1 -0
  126. package/dist/utils/date-utils.d.ts +113 -0
  127. package/dist/utils/date-utils.d.ts.map +1 -0
  128. package/dist/utils/date-utils.js +341 -0
  129. package/dist/utils/date-utils.js.map +1 -0
  130. package/dist/utils/folder-matcher.d.ts +14 -0
  131. package/dist/utils/folder-matcher.d.ts.map +1 -0
  132. package/dist/utils/folder-matcher.js +191 -0
  133. package/dist/utils/folder-matcher.js.map +1 -0
  134. package/dist/utils/version.d.ts +10 -0
  135. package/dist/utils/version.d.ts.map +1 -0
  136. package/dist/utils/version.js +88 -0
  137. package/dist/utils/version.js.map +1 -0
  138. package/docs/plugin-api/Calendar.md +448 -0
  139. package/docs/plugin-api/CalendarItem.md +198 -0
  140. package/docs/plugin-api/Clipboard.md +101 -0
  141. package/docs/plugin-api/CommandBar.md +251 -0
  142. package/docs/plugin-api/DataStore.md +700 -0
  143. package/docs/plugin-api/Editor.md +982 -0
  144. package/docs/plugin-api/HTMLView.md +337 -0
  145. package/docs/plugin-api/NoteObject.md +588 -0
  146. package/docs/plugin-api/NotePlan.md +398 -0
  147. package/docs/plugin-api/ParagraphObject.md +242 -0
  148. package/docs/plugin-api/RangeObject.md +56 -0
  149. package/docs/plugin-api/getting-started.md +545 -0
  150. package/docs/plugin-api/plugin-api-condensed.md +526 -0
  151. package/docs/plugin-api/plugin.json +26 -0
  152. package/docs/plugin-api/script.js +542 -0
  153. package/package.json +60 -0
  154. package/scripts/calendar-helper +0 -0
  155. package/scripts/reminders-helper +0 -0
@@ -0,0 +1,1868 @@
1
+ // Note CRUD operations
2
+ import { z } from 'zod';
3
+ import path from 'path';
4
+ import * as store from '../noteplan/unified-store.js';
5
+ import * as frontmatter from '../noteplan/frontmatter-parser.js';
6
+ import { issueConfirmationToken, validateAndConsumeConfirmationToken, } from '../utils/confirmation-tokens.js';
7
+ import { parseParagraphLine, buildParagraphLine } from '../noteplan/markdown-parser.js';
8
+ function toBoundedInt(value, defaultValue, min, max) {
9
+ const numeric = typeof value === 'number' ? value : Number(value);
10
+ if (!Number.isFinite(numeric))
11
+ return defaultValue;
12
+ return Math.min(max, Math.max(min, Math.floor(numeric)));
13
+ }
14
+ function isDebugTimingsEnabled(value) {
15
+ if (typeof value === 'boolean')
16
+ return value;
17
+ if (typeof value === 'string') {
18
+ const normalized = value.trim().toLowerCase();
19
+ return normalized === 'true' || normalized === '1';
20
+ }
21
+ return false;
22
+ }
23
+ function toOptionalBoolean(value) {
24
+ if (value === undefined || value === null || value === '')
25
+ return undefined;
26
+ if (typeof value === 'boolean')
27
+ return value;
28
+ if (typeof value === 'string') {
29
+ const normalized = value.trim().toLowerCase();
30
+ if (normalized === 'true')
31
+ return true;
32
+ if (normalized === 'false')
33
+ return false;
34
+ }
35
+ return undefined;
36
+ }
37
+ function confirmationFailureMessage(toolName, reason) {
38
+ const refreshHint = `Call ${toolName} with dryRun=true to get a new confirmationToken.`;
39
+ if (reason === 'missing') {
40
+ return `Confirmation token is required for ${toolName}. ${refreshHint}`;
41
+ }
42
+ if (reason === 'expired') {
43
+ return `Confirmation token is expired for ${toolName}. ${refreshHint}`;
44
+ }
45
+ return `Confirmation token is invalid for ${toolName}. ${refreshHint}`;
46
+ }
47
+ function resolveNoteTarget(id, filename, space) {
48
+ const identifier = (id && id.trim().length > 0 ? id : filename)?.trim();
49
+ if (!identifier) {
50
+ return { identifier: '', note: null };
51
+ }
52
+ const note = id
53
+ ? store.getNote({ id: identifier, space }) ?? store.getNote({ filename: identifier, space })
54
+ : store.getNote({ filename: identifier, space });
55
+ return {
56
+ identifier,
57
+ note,
58
+ };
59
+ }
60
+ function resolveWritableNoteReference(input) {
61
+ if (input.id && input.id.trim().length > 0) {
62
+ const note = store.getNote({ id: input.id.trim(), space: input.space?.trim() });
63
+ return { note, error: note ? undefined : 'Note not found' };
64
+ }
65
+ if (input.filename && input.filename.trim().length > 0) {
66
+ const note = store.getNote({ filename: input.filename.trim(), space: input.space?.trim() });
67
+ return { note, error: note ? undefined : 'Note not found' };
68
+ }
69
+ if (input.date && input.date.trim().length > 0) {
70
+ let note = store.getNote({ date: input.date.trim(), space: input.space?.trim() });
71
+ if (!note) {
72
+ // Auto-create calendar notes on the fly (matches NotePlan native behavior)
73
+ try {
74
+ note = store.ensureCalendarNote(input.date.trim(), input.space?.trim());
75
+ }
76
+ catch {
77
+ return { note: null, error: 'Failed to create calendar note for date' };
78
+ }
79
+ }
80
+ return { note, error: note ? undefined : 'Note not found' };
81
+ }
82
+ const textQuery = input.query?.trim() || input.title?.trim();
83
+ if (textQuery) {
84
+ const resolved = resolveNote({
85
+ query: textQuery,
86
+ space: input.space?.trim(),
87
+ types: ['note', 'calendar'],
88
+ limit: 5,
89
+ minScore: 0.88,
90
+ ambiguityDelta: 0.06,
91
+ });
92
+ if (resolved.success !== true) {
93
+ return { note: null, error: 'Could not resolve note query' };
94
+ }
95
+ if (resolved.ambiguous === true || !resolved.resolved) {
96
+ const label = input.title ? 'title' : 'query';
97
+ return {
98
+ note: null,
99
+ error: `Ambiguous note ${label}. Resolve explicitly with noteplan_resolve_note or provide id/filename.`,
100
+ candidates: resolved.candidates?.slice(0, 5) ?? [],
101
+ };
102
+ }
103
+ const identifier = resolved.resolved.id || resolved.resolved.filename;
104
+ if (!identifier) {
105
+ return { note: null, error: 'Could not resolve note target' };
106
+ }
107
+ const note = store.getNote({ id: identifier, space: input.space?.trim() }) ?? store.getNote({ filename: identifier, space: input.space?.trim() });
108
+ return { note, error: note ? undefined : 'Resolved note no longer exists' };
109
+ }
110
+ return {
111
+ note: null,
112
+ error: 'Provide one note reference: id, filename, title, date, or query',
113
+ };
114
+ }
115
+ function getWritableIdentifier(note) {
116
+ if (note.source === 'space') {
117
+ return {
118
+ identifier: note.id || note.filename,
119
+ source: 'space',
120
+ };
121
+ }
122
+ return {
123
+ identifier: note.filename,
124
+ source: 'local',
125
+ };
126
+ }
127
+ const PROGRESSIVE_READ_HINT = 'Use startLine/endLine and cursor pagination for progressive note reads.';
128
+ const NEXT_CURSOR_HINT = 'Continue with nextCursor to fetch the next content page.';
129
+ function buildLineWindow(allLines, options) {
130
+ const totalLineCount = allLines.length;
131
+ const requestedStartLine = toBoundedInt(options.startLine, 1, 1, Math.max(1, totalLineCount));
132
+ const requestedEndLine = toBoundedInt(options.endLine, totalLineCount, requestedStartLine, Math.max(requestedStartLine, totalLineCount));
133
+ const rangeStartIndex = requestedStartLine - 1;
134
+ const rangeEndIndexExclusive = requestedEndLine;
135
+ const rangeLines = allLines.slice(rangeStartIndex, rangeEndIndexExclusive);
136
+ const offset = toBoundedInt(options.cursor ?? options.offset, 0, 0, Number.MAX_SAFE_INTEGER);
137
+ const limit = toBoundedInt(options.limit, options.defaultLimit, 1, options.maxLimit);
138
+ const page = rangeLines.slice(offset, offset + limit);
139
+ const hasMore = offset + page.length < rangeLines.length;
140
+ const nextCursor = hasMore ? String(offset + page.length) : null;
141
+ return {
142
+ lineCount: totalLineCount,
143
+ rangeStartLine: requestedStartLine,
144
+ rangeEndLine: requestedEndLine,
145
+ rangeLineCount: rangeLines.length,
146
+ returnedLineCount: page.length,
147
+ offset,
148
+ limit,
149
+ hasMore,
150
+ nextCursor,
151
+ content: page.join('\n'),
152
+ lines: page.map((content, index) => ({
153
+ line: requestedStartLine + offset + index,
154
+ lineIndex: rangeStartIndex + offset + index,
155
+ content,
156
+ })),
157
+ };
158
+ }
159
+ function escapeRegExp(value) {
160
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
161
+ }
162
+ function normalizeIndentationStyle(value) {
163
+ if (value === 'preserve')
164
+ return 'preserve';
165
+ return 'tabs';
166
+ }
167
+ function retabListIndentation(content) {
168
+ const lines = content.split('\n');
169
+ let linesRetabbed = 0;
170
+ const normalized = lines.map((line) => {
171
+ const match = line.match(/^( +)(?=(?:[*+-]|\d+[.)])(?:\s|\t|\[))/);
172
+ if (!match)
173
+ return line;
174
+ const spaceCount = match[1].length;
175
+ if (spaceCount < 2)
176
+ return line;
177
+ const tabs = '\t'.repeat(Math.floor(spaceCount / 2));
178
+ linesRetabbed += 1;
179
+ // Consume all matched leading spaces; do not keep odd-space remainder.
180
+ return `${tabs}${line.slice(spaceCount)}`;
181
+ });
182
+ return {
183
+ content: normalized.join('\n'),
184
+ linesRetabbed,
185
+ };
186
+ }
187
+ function normalizeContentIndentation(content, style) {
188
+ if (style === 'preserve') {
189
+ return {
190
+ content,
191
+ linesRetabbed: 0,
192
+ };
193
+ }
194
+ return retabListIndentation(content);
195
+ }
196
+ function extractAttachmentReferences(text) {
197
+ const matches = text.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g);
198
+ const refs = new Set();
199
+ for (const match of matches) {
200
+ const ref = (match[1] || '').trim();
201
+ if (!ref)
202
+ continue;
203
+ refs.add(ref);
204
+ }
205
+ return Array.from(refs);
206
+ }
207
+ function getRemovedAttachmentReferences(beforeText, afterText) {
208
+ const before = new Set(extractAttachmentReferences(beforeText));
209
+ const after = new Set(extractAttachmentReferences(afterText));
210
+ return Array.from(before).filter((ref) => !after.has(ref));
211
+ }
212
+ function buildAttachmentWarningMessage(referenceCount) {
213
+ return `Warning: edited/deleted content references ${referenceCount} attachment link(s). NotePlan may auto-trash referenced files when these links are removed.`;
214
+ }
215
+ function findParagraphBounds(lines, lineIndex) {
216
+ let startIndex = lineIndex;
217
+ while (startIndex > 0 && lines[startIndex - 1].trim() !== '') {
218
+ startIndex -= 1;
219
+ }
220
+ let endIndex = lineIndex;
221
+ while (endIndex < lines.length - 1 && lines[endIndex + 1].trim() !== '') {
222
+ endIndex += 1;
223
+ }
224
+ return { startIndex, endIndex };
225
+ }
226
+ // Schema definitions
227
+ export const getNoteSchema = z.object({
228
+ id: z.string().optional().describe('Note ID (use this for space notes - get it from search results)'),
229
+ title: z.string().optional().describe('Note title to search for'),
230
+ filename: z.string().optional().describe('Direct filename/path to the note (for local notes)'),
231
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
232
+ space: z.string().optional().describe('Space name or ID to search in'),
233
+ includeContent: z
234
+ .boolean()
235
+ .optional()
236
+ .describe('Include note body content and line payload (default: false, metadata/preview only)'),
237
+ startLine: z.number().min(1).optional().describe('First line to include when includeContent=true (1-indexed)'),
238
+ endLine: z.number().min(1).optional().describe('Last line to include when includeContent=true (1-indexed)'),
239
+ limit: z.number().min(1).max(1000).optional().default(500).describe('Maximum lines to return when includeContent=true'),
240
+ offset: z.number().min(0).optional().default(0).describe('Pagination offset within selected range'),
241
+ cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
242
+ previewChars: z
243
+ .number()
244
+ .min(0)
245
+ .max(5000)
246
+ .optional()
247
+ .default(280)
248
+ .describe('Preview length when includeContent=false (default: 280)'),
249
+ });
250
+ export const listNotesSchema = z.object({
251
+ folder: z
252
+ .string()
253
+ .optional()
254
+ .describe('Filter by project folder path (e.g., "20 - Areas" or "Notes/20 - Areas")'),
255
+ space: z.string().optional().describe('Space name or ID to list from'),
256
+ types: z
257
+ .array(z.enum(['calendar', 'note', 'trash']))
258
+ .optional()
259
+ .describe('Filter by note types'),
260
+ query: z.string().optional().describe('Filter notes by title/filename/folder substring'),
261
+ limit: z.number().min(1).max(500).optional().default(50).describe('Maximum number of notes to return'),
262
+ offset: z.number().min(0).optional().default(0).describe('Pagination offset'),
263
+ cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
264
+ });
265
+ export const resolveNoteSchema = z.object({
266
+ query: z.string().describe('Note reference to resolve (ID, title, filename, or date token)'),
267
+ space: z.string().optional().describe('Restrict to a specific space name or ID'),
268
+ folder: z.string().optional().describe('Restrict to a folder path'),
269
+ types: z
270
+ .array(z.enum(['calendar', 'note', 'trash']))
271
+ .optional()
272
+ .describe('Restrict to note types'),
273
+ limit: z.number().min(1).max(20).optional().default(5).describe('Candidate matches to return'),
274
+ minScore: z.number().min(0).max(1).optional().default(0.88).describe('Minimum score for auto-resolution'),
275
+ ambiguityDelta: z
276
+ .number()
277
+ .min(0)
278
+ .max(1)
279
+ .optional()
280
+ .default(0.06)
281
+ .describe('If top scores are within this delta, treat as ambiguous'),
282
+ });
283
+ export const createNoteSchema = z.object({
284
+ title: z.string().describe('Title for the new note'),
285
+ 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)'),
286
+ folder: z.string().optional().describe('Folder to create the note in. Supports smart matching (e.g., "projects" matches "10 - Projects")'),
287
+ create_new_folder: z.boolean().optional().describe('Set to true to create a new folder instead of matching existing ones'),
288
+ space: z.string().optional().describe('Space name or ID to create in (e.g., "My Team" or a UUID)'),
289
+ });
290
+ export const updateNoteSchema = z.object({
291
+ filename: z.string().describe('Filename/path of the note to update'),
292
+ space: z.string().optional().describe('Space name or ID to search in'),
293
+ content: z
294
+ .string()
295
+ .describe('New content for the note. Include YAML frontmatter between --- delimiters at the start if the note has or should have properties'),
296
+ fullReplace: z
297
+ .boolean()
298
+ .optional()
299
+ .describe('Required safety confirmation for whole-note rewrite. Must be true to proceed.'),
300
+ dryRun: z
301
+ .boolean()
302
+ .optional()
303
+ .describe('Preview full-rewrite impact and get confirmationToken without modifying the note'),
304
+ confirmationToken: z
305
+ .string()
306
+ .optional()
307
+ .describe('Confirmation token issued by dryRun for full note rewrite'),
308
+ allowEmptyContent: z
309
+ .boolean()
310
+ .optional()
311
+ .describe('Allow replacing note content with empty/blank text (default: false)'),
312
+ });
313
+ export const deleteNoteSchema = z.object({
314
+ id: z.string().optional().describe('Note ID (preferred for TeamSpace notes)'),
315
+ filename: z.string().optional().describe('Filename/path of the note to delete'),
316
+ space: z.string().optional().describe('Space name or ID to search in'),
317
+ dryRun: z
318
+ .boolean()
319
+ .optional()
320
+ .describe('Preview deletion impact without deleting (default: false)'),
321
+ confirmationToken: z
322
+ .string()
323
+ .optional()
324
+ .describe('Confirmation token issued by dryRun for delete execution'),
325
+ }).superRefine((input, ctx) => {
326
+ if (!input.id && !input.filename) {
327
+ ctx.addIssue({
328
+ code: z.ZodIssueCode.custom,
329
+ message: 'Provide one note reference: id or filename',
330
+ path: ['id'],
331
+ });
332
+ }
333
+ });
334
+ export const moveNoteSchema = z.object({
335
+ id: z.string().optional().describe('Note ID (preferred for TeamSpace notes)'),
336
+ filename: z.string().optional().describe('Filename/path of the note to move'),
337
+ space: z.string().optional().describe('Space name or ID to search in'),
338
+ destinationFolder: z
339
+ .string()
340
+ .describe('Destination folder. For local notes: folder path in Notes (if a full path is provided, basename must match current file). For TeamSpace notes: folder ID/path/name or "root"'),
341
+ dryRun: z
342
+ .boolean()
343
+ .optional()
344
+ .describe('Preview move impact and get confirmationToken without modifying the note'),
345
+ confirmationToken: z
346
+ .string()
347
+ .optional()
348
+ .describe('Confirmation token issued by dryRun for move execution'),
349
+ }).superRefine((input, ctx) => {
350
+ if (!input.id && !input.filename) {
351
+ ctx.addIssue({
352
+ code: z.ZodIssueCode.custom,
353
+ message: 'Provide one note reference: id or filename',
354
+ path: ['id'],
355
+ });
356
+ }
357
+ });
358
+ export const renameNoteFileSchema = z.object({
359
+ id: z.string().optional().describe('Note ID (preferred for TeamSpace notes)'),
360
+ filename: z.string().optional().describe('Filename/path of the note to rename'),
361
+ space: z.string().optional().describe('Space name or ID to search in'),
362
+ newFilename: z
363
+ .string()
364
+ .optional()
365
+ .describe('New file name for local notes. Can be bare filename or full path in the same folder; defaults to keeping current extension'),
366
+ newTitle: z
367
+ .string()
368
+ .optional()
369
+ .describe('New title for TeamSpace notes'),
370
+ keepExtension: z
371
+ .boolean()
372
+ .optional()
373
+ .default(true)
374
+ .describe('Keep current extension (.md/.txt) even if newFilename includes a different extension (default: true)'),
375
+ dryRun: z
376
+ .boolean()
377
+ .optional()
378
+ .describe('Preview rename impact and get confirmationToken without modifying the note'),
379
+ confirmationToken: z
380
+ .string()
381
+ .optional()
382
+ .describe('Confirmation token issued by dryRun for rename execution'),
383
+ }).superRefine((input, ctx) => {
384
+ if (!input.id && !input.filename) {
385
+ ctx.addIssue({
386
+ code: z.ZodIssueCode.custom,
387
+ message: 'Provide one note reference: id or filename',
388
+ path: ['filename'],
389
+ });
390
+ }
391
+ });
392
+ export const restoreNoteSchema = z.object({
393
+ id: z.string().optional().describe('Trashed note ID (preferred for TeamSpace notes, usually from noteplan_delete_note response)'),
394
+ filename: z.string().optional().describe('Trashed filename/path to restore (usually from noteplan_delete_note response)'),
395
+ space: z.string().optional().describe('Space name or ID to search in'),
396
+ destinationFolder: z
397
+ .string()
398
+ .optional()
399
+ .describe('Restore destination. Local: folder under Notes. TeamSpace: folder ID/path/name or "root" (default: space root)'),
400
+ dryRun: z
401
+ .boolean()
402
+ .optional()
403
+ .describe('Preview restore impact and get confirmationToken without modifying the note'),
404
+ confirmationToken: z
405
+ .string()
406
+ .optional()
407
+ .describe('Confirmation token issued by dryRun for restore execution'),
408
+ }).superRefine((input, ctx) => {
409
+ if (!input.id && !input.filename) {
410
+ ctx.addIssue({
411
+ code: z.ZodIssueCode.custom,
412
+ message: 'Provide one note reference: id or filename',
413
+ path: ['id'],
414
+ });
415
+ }
416
+ });
417
+ // Tool implementations
418
+ export function getNote(params) {
419
+ const note = store.getNote(params);
420
+ if (!note) {
421
+ return {
422
+ success: false,
423
+ error: 'Note not found',
424
+ };
425
+ }
426
+ const includeContent = toOptionalBoolean(params.includeContent) ?? false;
427
+ const previewChars = toBoundedInt(params.previewChars, 280, 0, 5000);
428
+ const allLines = note.content.split('\n');
429
+ const lineCount = allLines.length;
430
+ const contentLength = note.content.length;
431
+ const result = {
432
+ success: true,
433
+ note: {
434
+ id: note.id,
435
+ title: note.title,
436
+ filename: note.filename,
437
+ type: note.type,
438
+ source: note.source,
439
+ folder: note.folder,
440
+ spaceId: note.spaceId,
441
+ date: note.date,
442
+ modifiedAt: note.modifiedAt?.toISOString(),
443
+ createdAt: note.createdAt?.toISOString(),
444
+ },
445
+ contentIncluded: includeContent,
446
+ lineCount,
447
+ contentLength,
448
+ };
449
+ if (!includeContent) {
450
+ const preview = previewChars > 0 ? note.content.slice(0, previewChars) : '';
451
+ result.preview = preview;
452
+ result.previewTruncated = preview.length < note.content.length;
453
+ if (result.previewTruncated || lineCount > 200) {
454
+ result.performanceHints = [
455
+ `Set includeContent=true. ${PROGRESSIVE_READ_HINT}`,
456
+ ];
457
+ }
458
+ return result;
459
+ }
460
+ const lineWindow = buildLineWindow(allLines, {
461
+ startLine: params.startLine,
462
+ endLine: params.endLine,
463
+ limit: params.limit,
464
+ offset: params.offset,
465
+ cursor: params.cursor,
466
+ defaultLimit: 500,
467
+ maxLimit: 1000,
468
+ });
469
+ result.rangeStartLine = lineWindow.rangeStartLine;
470
+ result.rangeEndLine = lineWindow.rangeEndLine;
471
+ result.rangeLineCount = lineWindow.rangeLineCount;
472
+ result.returnedLineCount = lineWindow.returnedLineCount;
473
+ result.offset = lineWindow.offset;
474
+ result.limit = lineWindow.limit;
475
+ result.hasMore = lineWindow.hasMore;
476
+ result.nextCursor = lineWindow.nextCursor;
477
+ result.content = lineWindow.content;
478
+ result.lines = lineWindow.lines;
479
+ if (lineWindow.hasMore) {
480
+ result.performanceHints = [NEXT_CURSOR_HINT];
481
+ }
482
+ return result;
483
+ }
484
+ export function listNotes(params) {
485
+ const input = params ?? {};
486
+ const notes = store.listNotes({
487
+ folder: input.folder,
488
+ space: input.space,
489
+ });
490
+ const allowedTypes = input.types ? new Set(input.types) : null;
491
+ const query = typeof input.query === 'string' ? input.query.trim().toLowerCase() : undefined;
492
+ const filtered = notes.filter((note) => {
493
+ if (allowedTypes && !allowedTypes.has(note.type))
494
+ return false;
495
+ if (!query)
496
+ return true;
497
+ const haystack = `${note.title} ${note.filename} ${note.folder || ''}`.toLowerCase();
498
+ return haystack.includes(query);
499
+ });
500
+ const offset = toBoundedInt(input.cursor ?? input.offset, 0, 0, Number.MAX_SAFE_INTEGER);
501
+ const limit = toBoundedInt(input.limit, 50, 1, 500);
502
+ const page = filtered.slice(offset, offset + limit);
503
+ const hasMore = offset + page.length < filtered.length;
504
+ const nextCursor = hasMore ? String(offset + page.length) : null;
505
+ return {
506
+ success: true,
507
+ count: page.length,
508
+ totalCount: filtered.length,
509
+ offset,
510
+ limit,
511
+ hasMore,
512
+ nextCursor,
513
+ notes: page.map((note) => ({
514
+ id: note.id,
515
+ title: note.title,
516
+ filename: note.filename,
517
+ type: note.type,
518
+ source: note.source,
519
+ folder: note.folder,
520
+ spaceId: note.spaceId,
521
+ modifiedAt: note.modifiedAt?.toISOString(),
522
+ })),
523
+ };
524
+ }
525
+ function normalizeDateToken(value) {
526
+ if (!value)
527
+ return null;
528
+ const digits = value.replace(/\D/g, '');
529
+ return digits.length === 8 ? digits : null;
530
+ }
531
+ function noteMatchScore(note, query, queryDateToken) {
532
+ const queryLower = query.toLowerCase();
533
+ const idLower = (note.id || '').toLowerCase();
534
+ const titleLower = (note.title || '').toLowerCase();
535
+ const filenameLower = (note.filename || '').toLowerCase();
536
+ const basenameLower = path.basename(filenameLower, path.extname(filenameLower));
537
+ const noteDateToken = normalizeDateToken(note.date);
538
+ if (idLower && idLower === queryLower)
539
+ return 1.0;
540
+ if (filenameLower === queryLower)
541
+ return 0.99;
542
+ if (basenameLower === queryLower)
543
+ return 0.97;
544
+ if (titleLower === queryLower)
545
+ return 0.96;
546
+ if (queryDateToken && noteDateToken && queryDateToken === noteDateToken)
547
+ return 0.95;
548
+ if (titleLower.startsWith(queryLower))
549
+ return 0.9;
550
+ if (basenameLower.startsWith(queryLower))
551
+ return 0.88;
552
+ if (filenameLower.includes(`/${queryLower}`) || filenameLower.includes(queryLower))
553
+ return 0.83;
554
+ if (`${titleLower} ${filenameLower}`.includes(queryLower))
555
+ return 0.76;
556
+ return 0;
557
+ }
558
+ export function resolveNote(params) {
559
+ const query = typeof params?.query === 'string' ? params.query.trim() : '';
560
+ if (!query) {
561
+ return {
562
+ success: false,
563
+ error: 'query is required',
564
+ };
565
+ }
566
+ const limit = toBoundedInt(params.limit, 5, 1, 20);
567
+ const minScore = Math.min(1, Math.max(0, Number(params.minScore ?? 0.88)));
568
+ const ambiguityDelta = Math.min(1, Math.max(0, Number(params.ambiguityDelta ?? 0.06)));
569
+ const queryDateToken = normalizeDateToken(query);
570
+ const allowedTypes = params.types ? new Set(params.types) : null;
571
+ const includeStageTimings = isDebugTimingsEnabled(params.debugTimings);
572
+ const stageTimings = {};
573
+ const listStart = Date.now();
574
+ const notes = store.listNotes({
575
+ folder: params.folder,
576
+ space: params.space,
577
+ });
578
+ const listNotesMs = Date.now() - listStart;
579
+ if (includeStageTimings) {
580
+ stageTimings.listNotesMs = listNotesMs;
581
+ }
582
+ const scoreStart = Date.now();
583
+ const scored = notes
584
+ .filter((note) => !allowedTypes || allowedTypes.has(note.type))
585
+ .map((note) => ({
586
+ note,
587
+ score: noteMatchScore(note, query, queryDateToken),
588
+ }))
589
+ .filter((entry) => entry.score > 0)
590
+ .sort((a, b) => {
591
+ if (Math.abs(a.score - b.score) > 0.001)
592
+ return b.score - a.score;
593
+ return a.note.filename.localeCompare(b.note.filename);
594
+ });
595
+ const scoreAndSortMs = Date.now() - scoreStart;
596
+ if (includeStageTimings) {
597
+ stageTimings.scoreAndSortMs = scoreAndSortMs;
598
+ }
599
+ const resolveStart = Date.now();
600
+ const candidates = scored.slice(0, limit);
601
+ const top = candidates[0];
602
+ const second = candidates[1];
603
+ const scoreDelta = top && second ? top.score - second.score : 1;
604
+ const confident = Boolean(top) && top.score >= minScore;
605
+ const ambiguous = Boolean(second) && scoreDelta < ambiguityDelta;
606
+ const resolved = confident && !ambiguous ? top.note : null;
607
+ const mappedCandidates = candidates.map((entry) => ({
608
+ id: entry.note.id,
609
+ title: entry.note.title,
610
+ filename: entry.note.filename,
611
+ type: entry.note.type,
612
+ source: entry.note.source,
613
+ folder: entry.note.folder,
614
+ spaceId: entry.note.spaceId,
615
+ score: Number(entry.score.toFixed(3)),
616
+ }));
617
+ const resolveResultMs = Date.now() - resolveStart;
618
+ if (includeStageTimings) {
619
+ stageTimings.resolveResultMs = resolveResultMs;
620
+ }
621
+ const result = {
622
+ success: true,
623
+ query,
624
+ count: candidates.length,
625
+ resolved: resolved
626
+ ? {
627
+ id: resolved.id,
628
+ title: resolved.title,
629
+ filename: resolved.filename,
630
+ type: resolved.type,
631
+ source: resolved.source,
632
+ folder: resolved.folder,
633
+ spaceId: resolved.spaceId,
634
+ score: Number((top?.score ?? 0).toFixed(3)),
635
+ }
636
+ : null,
637
+ exactMatch: Boolean(top) && Number((top?.score ?? 0).toFixed(3)) >= 0.96,
638
+ ambiguous,
639
+ confidence: top ? Number(top.score.toFixed(3)) : 0,
640
+ confidenceDelta: Number(scoreDelta.toFixed(3)),
641
+ suggestedGetNoteArgs: resolved
642
+ ? resolved.source === 'space' && resolved.id
643
+ ? { id: resolved.id }
644
+ : { filename: resolved.filename }
645
+ : null,
646
+ candidates: mappedCandidates,
647
+ };
648
+ const performanceHints = [];
649
+ if (listNotesMs > 1200) {
650
+ if (!params.space) {
651
+ performanceHints.push('Set space to scope note resolution to one workspace.');
652
+ }
653
+ if (!params.folder) {
654
+ performanceHints.push('Set folder to reduce note candidate scans.');
655
+ }
656
+ if (!params.types || params.types.length !== 1) {
657
+ performanceHints.push('Set one note type when possible (calendar, note, or trash).');
658
+ }
659
+ }
660
+ if (candidates.length === 0) {
661
+ performanceHints.push('Try noteplan_search with a broader query to discover canonical note IDs first.');
662
+ }
663
+ if (performanceHints.length > 0) {
664
+ result.performanceHints = performanceHints;
665
+ }
666
+ if (includeStageTimings) {
667
+ result.stageTimings = stageTimings;
668
+ }
669
+ return result;
670
+ }
671
+ export function createNote(params) {
672
+ try {
673
+ const result = store.createNote(params.title, params.content, {
674
+ folder: params.folder,
675
+ space: params.space,
676
+ createNewFolder: params.create_new_folder,
677
+ });
678
+ return {
679
+ success: true,
680
+ note: {
681
+ title: result.note.title,
682
+ filename: result.note.filename,
683
+ type: result.note.type,
684
+ source: result.note.source,
685
+ folder: result.note.folder,
686
+ },
687
+ folderResolution: {
688
+ requested: result.folderResolution.requested,
689
+ resolved: result.folderResolution.resolved,
690
+ matched: result.folderResolution.matched,
691
+ ambiguous: result.folderResolution.ambiguous,
692
+ score: result.folderResolution.score,
693
+ alternatives: result.folderResolution.alternatives,
694
+ },
695
+ };
696
+ }
697
+ catch (error) {
698
+ return {
699
+ success: false,
700
+ error: error instanceof Error ? error.message : 'Failed to create note',
701
+ };
702
+ }
703
+ }
704
+ export function updateNote(params) {
705
+ try {
706
+ if (params.fullReplace !== true) {
707
+ return {
708
+ success: false,
709
+ 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.',
710
+ };
711
+ }
712
+ const existingNote = store.getNote({ filename: params.filename, space: params.space });
713
+ if (!existingNote) {
714
+ return {
715
+ success: false,
716
+ error: 'Note not found',
717
+ };
718
+ }
719
+ if (params.allowEmptyContent !== true && params.content.trim().length === 0) {
720
+ return {
721
+ success: false,
722
+ error: 'Empty content is blocked for noteplan_update_note. Use allowEmptyContent=true to override intentionally.',
723
+ };
724
+ }
725
+ if (params.dryRun === true) {
726
+ const token = issueConfirmationToken({
727
+ tool: 'noteplan_update_note',
728
+ target: params.filename,
729
+ action: 'full_replace',
730
+ });
731
+ return {
732
+ success: true,
733
+ dryRun: true,
734
+ message: `Dry run: note ${params.filename} would be fully replaced`,
735
+ note: {
736
+ id: existingNote.id,
737
+ title: existingNote.title,
738
+ filename: existingNote.filename,
739
+ type: existingNote.type,
740
+ source: existingNote.source,
741
+ folder: existingNote.folder,
742
+ spaceId: existingNote.spaceId,
743
+ },
744
+ currentContentLength: existingNote.content.length,
745
+ newContentLength: params.content.length,
746
+ ...token,
747
+ };
748
+ }
749
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
750
+ tool: 'noteplan_update_note',
751
+ target: params.filename,
752
+ action: 'full_replace',
753
+ });
754
+ if (!confirmation.ok) {
755
+ return {
756
+ success: false,
757
+ error: confirmationFailureMessage('noteplan_update_note', confirmation.reason),
758
+ };
759
+ }
760
+ const writeTarget = getWritableIdentifier(existingNote);
761
+ const note = store.updateNote(writeTarget.identifier, params.content, {
762
+ source: writeTarget.source,
763
+ });
764
+ return {
765
+ success: true,
766
+ note: {
767
+ title: note.title,
768
+ filename: note.filename,
769
+ type: note.type,
770
+ source: note.source,
771
+ },
772
+ };
773
+ }
774
+ catch (error) {
775
+ return {
776
+ success: false,
777
+ error: error instanceof Error ? error.message : 'Failed to update note',
778
+ };
779
+ }
780
+ }
781
+ export function deleteNote(params) {
782
+ try {
783
+ const target = resolveNoteTarget(params.id, params.filename, params.space);
784
+ const note = target.note;
785
+ if (!note) {
786
+ return {
787
+ success: false,
788
+ error: 'Note not found',
789
+ };
790
+ }
791
+ if (params.dryRun === true) {
792
+ const token = issueConfirmationToken({
793
+ tool: 'noteplan_delete_note',
794
+ target: target.identifier,
795
+ action: 'delete_note',
796
+ });
797
+ return {
798
+ success: true,
799
+ dryRun: true,
800
+ message: `Dry run: note ${target.identifier} would be moved to trash`,
801
+ note: {
802
+ id: note.id,
803
+ title: note.title,
804
+ filename: note.filename,
805
+ type: note.type,
806
+ source: note.source,
807
+ folder: note.folder,
808
+ spaceId: note.spaceId,
809
+ },
810
+ ...token,
811
+ };
812
+ }
813
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
814
+ tool: 'noteplan_delete_note',
815
+ target: target.identifier,
816
+ action: 'delete_note',
817
+ });
818
+ if (!confirmation.ok) {
819
+ return {
820
+ success: false,
821
+ error: confirmationFailureMessage('noteplan_delete_note', confirmation.reason),
822
+ };
823
+ }
824
+ const deleted = store.deleteNote(target.identifier);
825
+ return {
826
+ success: true,
827
+ message: deleted.source === 'space'
828
+ ? `TeamSpace note moved to @Trash`
829
+ : `Note moved to @Trash`,
830
+ fromIdentifier: deleted.fromIdentifier,
831
+ trashedIdentifier: deleted.toIdentifier,
832
+ suggestedRestoreArgs: deleted.source === 'space'
833
+ ? { id: deleted.noteId || deleted.fromIdentifier }
834
+ : { filename: deleted.toIdentifier },
835
+ };
836
+ }
837
+ catch (error) {
838
+ return {
839
+ success: false,
840
+ error: error instanceof Error ? error.message : 'Failed to delete note',
841
+ };
842
+ }
843
+ }
844
+ export function moveNote(params) {
845
+ try {
846
+ const target = resolveNoteTarget(params.id, params.filename, params.space);
847
+ if (!target.note) {
848
+ return {
849
+ success: false,
850
+ error: 'Note not found',
851
+ };
852
+ }
853
+ const preview = store.previewMoveNote(target.identifier, params.destinationFolder);
854
+ const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}::${preview.destinationParentId ?? preview.destinationFolder}`;
855
+ if (params.dryRun === true) {
856
+ const token = issueConfirmationToken({
857
+ tool: 'noteplan_move_note',
858
+ target: confirmationTarget,
859
+ action: 'move_note',
860
+ });
861
+ return {
862
+ success: true,
863
+ dryRun: true,
864
+ message: `Dry run: note ${preview.fromFilename} would move to ${preview.toFilename}`,
865
+ fromFilename: preview.fromFilename,
866
+ toFilename: preview.toFilename,
867
+ destinationFolder: preview.destinationFolder,
868
+ note: {
869
+ id: preview.note.id,
870
+ title: preview.note.title,
871
+ filename: preview.note.filename,
872
+ type: preview.note.type,
873
+ source: preview.note.source,
874
+ folder: preview.note.folder,
875
+ spaceId: preview.note.spaceId,
876
+ },
877
+ ...token,
878
+ };
879
+ }
880
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
881
+ tool: 'noteplan_move_note',
882
+ target: confirmationTarget,
883
+ action: 'move_note',
884
+ });
885
+ if (!confirmation.ok) {
886
+ return {
887
+ success: false,
888
+ error: confirmationFailureMessage('noteplan_move_note', confirmation.reason),
889
+ };
890
+ }
891
+ const moved = store.moveNote(target.identifier, params.destinationFolder);
892
+ return {
893
+ success: true,
894
+ message: moved.note.source === 'space'
895
+ ? `TeamSpace note moved to folder ${moved.destinationFolder}`
896
+ : `Note moved to ${moved.toFilename}`,
897
+ fromFilename: moved.fromFilename,
898
+ toFilename: moved.toFilename,
899
+ destinationFolder: moved.destinationFolder,
900
+ destinationParentId: moved.destinationParentId,
901
+ note: {
902
+ id: moved.note.id,
903
+ title: moved.note.title,
904
+ filename: moved.note.filename,
905
+ type: moved.note.type,
906
+ source: moved.note.source,
907
+ folder: moved.note.folder,
908
+ },
909
+ };
910
+ }
911
+ catch (error) {
912
+ return {
913
+ success: false,
914
+ error: error instanceof Error ? error.message : 'Failed to move note',
915
+ };
916
+ }
917
+ }
918
+ export function restoreNote(params) {
919
+ try {
920
+ const target = resolveNoteTarget(params.id, params.filename, params.space);
921
+ if (!target.note) {
922
+ return {
923
+ success: false,
924
+ error: 'Note not found',
925
+ };
926
+ }
927
+ const preview = store.previewRestoreNote(target.identifier, params.destinationFolder);
928
+ const confirmationTarget = `${preview.fromIdentifier}=>${preview.toIdentifier}`;
929
+ if (params.dryRun === true) {
930
+ const token = issueConfirmationToken({
931
+ tool: 'noteplan_restore_note',
932
+ target: confirmationTarget,
933
+ action: 'restore_note',
934
+ });
935
+ return {
936
+ success: true,
937
+ dryRun: true,
938
+ message: `Dry run: note ${preview.fromIdentifier} would be restored`,
939
+ fromIdentifier: preview.fromIdentifier,
940
+ toIdentifier: preview.toIdentifier,
941
+ source: preview.source,
942
+ note: {
943
+ id: preview.note.id,
944
+ title: preview.note.title,
945
+ filename: preview.note.filename,
946
+ type: preview.note.type,
947
+ source: preview.note.source,
948
+ folder: preview.note.folder,
949
+ spaceId: preview.note.spaceId,
950
+ },
951
+ ...token,
952
+ };
953
+ }
954
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
955
+ tool: 'noteplan_restore_note',
956
+ target: confirmationTarget,
957
+ action: 'restore_note',
958
+ });
959
+ if (!confirmation.ok) {
960
+ return {
961
+ success: false,
962
+ error: confirmationFailureMessage('noteplan_restore_note', confirmation.reason),
963
+ };
964
+ }
965
+ const restored = store.restoreNote(target.identifier, params.destinationFolder);
966
+ return {
967
+ success: true,
968
+ message: restored.source === 'space'
969
+ ? 'TeamSpace note restored'
970
+ : `Local note restored to ${restored.toIdentifier}`,
971
+ fromIdentifier: restored.fromIdentifier,
972
+ toIdentifier: restored.toIdentifier,
973
+ source: restored.source,
974
+ note: {
975
+ id: restored.note.id,
976
+ title: restored.note.title,
977
+ filename: restored.note.filename,
978
+ type: restored.note.type,
979
+ source: restored.note.source,
980
+ folder: restored.note.folder,
981
+ },
982
+ };
983
+ }
984
+ catch (error) {
985
+ return {
986
+ success: false,
987
+ error: error instanceof Error ? error.message : 'Failed to restore note',
988
+ };
989
+ }
990
+ }
991
+ export function renameNoteFile(params) {
992
+ 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' };
997
+ }
998
+ const note = target.note;
999
+ // Space note: rename title
1000
+ if (note.source === 'space') {
1001
+ if (!params.newTitle) {
1002
+ return {
1003
+ success: false,
1004
+ error: 'newTitle is required for TeamSpace notes (use newFilename for local notes)',
1005
+ };
1006
+ }
1007
+ const writeId = note.id || note.filename;
1008
+ const confirmationTarget = `${note.title}=>${params.newTitle}`;
1009
+ if (params.dryRun === true) {
1010
+ const token = issueConfirmationToken({
1011
+ tool: 'noteplan_rename_note_file',
1012
+ target: confirmationTarget,
1013
+ action: 'rename_note_file',
1014
+ });
1015
+ return {
1016
+ success: true,
1017
+ dryRun: true,
1018
+ message: `Dry run: TeamSpace note would be renamed from "${note.title}" to "${params.newTitle}"`,
1019
+ fromTitle: note.title,
1020
+ toTitle: params.newTitle,
1021
+ note: {
1022
+ id: note.id,
1023
+ title: note.title,
1024
+ filename: note.filename,
1025
+ type: note.type,
1026
+ source: note.source,
1027
+ folder: note.folder,
1028
+ spaceId: note.spaceId,
1029
+ },
1030
+ ...token,
1031
+ };
1032
+ }
1033
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
1034
+ tool: 'noteplan_rename_note_file',
1035
+ target: confirmationTarget,
1036
+ action: 'rename_note_file',
1037
+ });
1038
+ if (!confirmation.ok) {
1039
+ return {
1040
+ success: false,
1041
+ error: confirmationFailureMessage('noteplan_rename_note_file', confirmation.reason),
1042
+ };
1043
+ }
1044
+ const renamed = store.renameSpaceNote(writeId, params.newTitle);
1045
+ return {
1046
+ success: true,
1047
+ message: `TeamSpace note renamed from "${renamed.fromTitle}" to "${renamed.toTitle}"`,
1048
+ fromTitle: renamed.fromTitle,
1049
+ toTitle: renamed.toTitle,
1050
+ note: {
1051
+ id: renamed.note.id,
1052
+ title: renamed.note.title,
1053
+ filename: renamed.note.filename,
1054
+ type: renamed.note.type,
1055
+ source: renamed.note.source,
1056
+ folder: renamed.note.folder,
1057
+ spaceId: renamed.note.spaceId,
1058
+ },
1059
+ };
1060
+ }
1061
+ // Local note: rename file
1062
+ if (!params.newFilename) {
1063
+ return {
1064
+ success: false,
1065
+ error: 'newFilename is required for local notes (use newTitle for TeamSpace notes)',
1066
+ };
1067
+ }
1068
+ const keepExtension = params.keepExtension ?? true;
1069
+ const preview = store.previewRenameNoteFile(note.filename, params.newFilename, keepExtension);
1070
+ const confirmationTarget = `${preview.fromFilename}=>${preview.toFilename}`;
1071
+ if (params.dryRun === true) {
1072
+ const token = issueConfirmationToken({
1073
+ tool: 'noteplan_rename_note_file',
1074
+ target: confirmationTarget,
1075
+ action: 'rename_note_file',
1076
+ });
1077
+ return {
1078
+ success: true,
1079
+ dryRun: true,
1080
+ message: `Dry run: note ${preview.fromFilename} would rename to ${preview.toFilename}`,
1081
+ fromFilename: preview.fromFilename,
1082
+ toFilename: preview.toFilename,
1083
+ note: {
1084
+ id: preview.note.id,
1085
+ title: preview.note.title,
1086
+ filename: preview.note.filename,
1087
+ type: preview.note.type,
1088
+ source: preview.note.source,
1089
+ folder: preview.note.folder,
1090
+ spaceId: preview.note.spaceId,
1091
+ },
1092
+ ...token,
1093
+ };
1094
+ }
1095
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
1096
+ tool: 'noteplan_rename_note_file',
1097
+ target: confirmationTarget,
1098
+ action: 'rename_note_file',
1099
+ });
1100
+ if (!confirmation.ok) {
1101
+ return {
1102
+ success: false,
1103
+ error: confirmationFailureMessage('noteplan_rename_note_file', confirmation.reason),
1104
+ };
1105
+ }
1106
+ const renamed = store.renameNoteFile(note.filename, params.newFilename, keepExtension);
1107
+ return {
1108
+ success: true,
1109
+ message: `Note renamed to ${renamed.toFilename}`,
1110
+ fromFilename: renamed.fromFilename,
1111
+ toFilename: renamed.toFilename,
1112
+ note: {
1113
+ id: renamed.note.id,
1114
+ title: renamed.note.title,
1115
+ filename: renamed.note.filename,
1116
+ type: renamed.note.type,
1117
+ source: renamed.note.source,
1118
+ folder: renamed.note.folder,
1119
+ },
1120
+ };
1121
+ }
1122
+ catch (error) {
1123
+ return {
1124
+ success: false,
1125
+ error: error instanceof Error ? error.message : 'Failed to rename note',
1126
+ };
1127
+ }
1128
+ }
1129
+ // Get note with line numbers
1130
+ export const getParagraphsSchema = z.object({
1131
+ filename: z.string().describe('Filename/path of the note'),
1132
+ space: z.string().optional().describe('Space name or ID to search in'),
1133
+ startLine: z.number().min(1).optional().describe('First line to include (1-indexed, inclusive)'),
1134
+ endLine: z.number().min(1).optional().describe('Last line to include (1-indexed, inclusive)'),
1135
+ limit: z.number().min(1).max(1000).optional().default(200).describe('Maximum lines to return'),
1136
+ offset: z.number().min(0).optional().default(0).describe('Pagination offset within selected range'),
1137
+ cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
1138
+ });
1139
+ export const searchParagraphsSchema = z.object({
1140
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1141
+ title: z.string().optional().describe('Note title to search for'),
1142
+ filename: z.string().optional().describe('Direct filename/path to the note'),
1143
+ date: z.string().optional().describe('Date for calendar notes (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
1144
+ space: z.string().optional().describe('Space name or ID to search in'),
1145
+ query: z.string().describe('Text to find in note lines/paragraphs'),
1146
+ caseSensitive: z.boolean().optional().default(false).describe('Case-sensitive match (default: false)'),
1147
+ wholeWord: z.boolean().optional().default(false).describe('Require whole-word matches (default: false)'),
1148
+ startLine: z.number().min(1).optional().describe('First line to search (1-indexed, inclusive)'),
1149
+ endLine: z.number().min(1).optional().describe('Last line to search (1-indexed, inclusive)'),
1150
+ contextLines: z.number().min(0).max(5).optional().default(1).describe('Context lines before/after each match'),
1151
+ paragraphMaxChars: z
1152
+ .number()
1153
+ .min(50)
1154
+ .max(5000)
1155
+ .optional()
1156
+ .default(600)
1157
+ .describe('Maximum paragraph text chars per match'),
1158
+ limit: z.number().min(1).max(200).optional().default(20).describe('Maximum matches to return'),
1159
+ offset: z.number().min(0).optional().default(0).describe('Pagination offset'),
1160
+ cursor: z.string().optional().describe('Cursor token from previous page (preferred over offset)'),
1161
+ }).superRefine((input, ctx) => {
1162
+ if (!input.id && !input.title && !input.filename && !input.date) {
1163
+ ctx.addIssue({
1164
+ code: z.ZodIssueCode.custom,
1165
+ message: 'Provide one note reference: id, title, filename, or date',
1166
+ path: ['id'],
1167
+ });
1168
+ }
1169
+ });
1170
+ export function getParagraphs(params) {
1171
+ const note = store.getNote({ filename: params.filename, space: params.space });
1172
+ if (!note) {
1173
+ return {
1174
+ success: false,
1175
+ error: 'Note not found',
1176
+ };
1177
+ }
1178
+ const allLines = note.content.split('\n');
1179
+ const lineWindow = buildLineWindow(allLines, {
1180
+ startLine: params.startLine,
1181
+ endLine: params.endLine,
1182
+ limit: params.limit,
1183
+ offset: params.offset,
1184
+ cursor: params.cursor,
1185
+ defaultLimit: 200,
1186
+ maxLimit: 1000,
1187
+ });
1188
+ const result = {
1189
+ success: true,
1190
+ note: {
1191
+ title: note.title,
1192
+ filename: note.filename,
1193
+ },
1194
+ lineCount: lineWindow.lineCount,
1195
+ rangeStartLine: lineWindow.rangeStartLine,
1196
+ rangeEndLine: lineWindow.rangeEndLine,
1197
+ rangeLineCount: lineWindow.rangeLineCount,
1198
+ returnedLineCount: lineWindow.returnedLineCount,
1199
+ offset: lineWindow.offset,
1200
+ limit: lineWindow.limit,
1201
+ hasMore: lineWindow.hasMore,
1202
+ nextCursor: lineWindow.nextCursor,
1203
+ content: lineWindow.content,
1204
+ lines: lineWindow.lines.map((lineObj) => {
1205
+ const meta = parseParagraphLine(lineObj.content, lineObj.lineIndex, lineObj.lineIndex === 0);
1206
+ return {
1207
+ ...lineObj,
1208
+ type: meta.type,
1209
+ indentLevel: meta.indentLevel,
1210
+ ...(meta.headingLevel !== undefined && { headingLevel: meta.headingLevel }),
1211
+ ...(meta.taskStatus !== undefined && { taskStatus: meta.taskStatus }),
1212
+ ...(meta.priority !== undefined && { priority: meta.priority }),
1213
+ ...(meta.marker !== undefined && { marker: meta.marker }),
1214
+ ...(meta.hasCheckbox !== undefined && { hasCheckbox: meta.hasCheckbox }),
1215
+ ...(meta.tags.length > 0 && { tags: meta.tags }),
1216
+ ...(meta.mentions.length > 0 && { mentions: meta.mentions }),
1217
+ ...(meta.scheduledDate !== undefined && { scheduledDate: meta.scheduledDate }),
1218
+ };
1219
+ }),
1220
+ };
1221
+ if (lineWindow.hasMore) {
1222
+ result.performanceHints = [NEXT_CURSOR_HINT];
1223
+ }
1224
+ else if (lineWindow.lineCount > 500 &&
1225
+ !params.startLine &&
1226
+ !params.endLine &&
1227
+ !params.cursor &&
1228
+ !params.offset) {
1229
+ result.performanceHints = [PROGRESSIVE_READ_HINT];
1230
+ }
1231
+ return result;
1232
+ }
1233
+ export function searchParagraphs(params) {
1234
+ const query = typeof params?.query === 'string' ? params.query.trim() : '';
1235
+ if (!query) {
1236
+ return {
1237
+ success: false,
1238
+ error: 'query is required',
1239
+ };
1240
+ }
1241
+ if (!params.id && !params.title && !params.filename && !params.date) {
1242
+ return {
1243
+ success: false,
1244
+ error: 'Provide one note reference: id, title, filename, or date',
1245
+ };
1246
+ }
1247
+ const note = store.getNote({
1248
+ id: params.id,
1249
+ title: params.title,
1250
+ filename: params.filename,
1251
+ date: params.date,
1252
+ space: params.space,
1253
+ });
1254
+ if (!note) {
1255
+ return {
1256
+ success: false,
1257
+ error: 'Note not found',
1258
+ };
1259
+ }
1260
+ const allLines = note.content.split('\n');
1261
+ const lineWindow = buildLineWindow(allLines, {
1262
+ startLine: params.startLine,
1263
+ endLine: params.endLine,
1264
+ defaultLimit: allLines.length,
1265
+ maxLimit: allLines.length,
1266
+ });
1267
+ const caseSensitive = params.caseSensitive ?? false;
1268
+ const wholeWord = params.wholeWord ?? false;
1269
+ const contextLines = toBoundedInt(params.contextLines, 1, 0, 5);
1270
+ const paragraphMaxChars = toBoundedInt(params.paragraphMaxChars, 600, 50, 5000);
1271
+ const normalizedQuery = caseSensitive ? query : query.toLowerCase();
1272
+ const matcher = wholeWord
1273
+ ? new RegExp(`\\b${escapeRegExp(query)}\\b`, caseSensitive ? '' : 'i')
1274
+ : null;
1275
+ const allMatches = lineWindow.lines
1276
+ .map((line) => {
1277
+ const haystack = caseSensitive ? line.content : line.content.toLowerCase();
1278
+ const isMatch = matcher ? matcher.test(line.content) : haystack.includes(normalizedQuery);
1279
+ if (!isMatch)
1280
+ return null;
1281
+ const paragraphBounds = findParagraphBounds(allLines, line.lineIndex);
1282
+ const paragraphRaw = allLines
1283
+ .slice(paragraphBounds.startIndex, paragraphBounds.endIndex + 1)
1284
+ .join('\n');
1285
+ const paragraphTruncated = paragraphRaw.length > paragraphMaxChars;
1286
+ const paragraph = paragraphTruncated
1287
+ ? `${paragraphRaw.slice(0, Math.max(0, paragraphMaxChars - 3))}...`
1288
+ : paragraphRaw;
1289
+ const contextStart = Math.max(0, line.lineIndex - contextLines);
1290
+ const contextEnd = Math.min(allLines.length - 1, line.lineIndex + contextLines);
1291
+ const meta = parseParagraphLine(line.content, line.lineIndex, line.lineIndex === 0);
1292
+ return {
1293
+ line: line.line,
1294
+ lineIndex: line.lineIndex,
1295
+ content: line.content,
1296
+ type: meta.type,
1297
+ indentLevel: meta.indentLevel,
1298
+ ...(meta.headingLevel !== undefined && { headingLevel: meta.headingLevel }),
1299
+ ...(meta.taskStatus !== undefined && { taskStatus: meta.taskStatus }),
1300
+ ...(meta.priority !== undefined && { priority: meta.priority }),
1301
+ ...(meta.marker !== undefined && { marker: meta.marker }),
1302
+ ...(meta.hasCheckbox !== undefined && { hasCheckbox: meta.hasCheckbox }),
1303
+ ...(meta.tags.length > 0 && { tags: meta.tags }),
1304
+ ...(meta.mentions.length > 0 && { mentions: meta.mentions }),
1305
+ ...(meta.scheduledDate !== undefined && { scheduledDate: meta.scheduledDate }),
1306
+ paragraphStartLine: paragraphBounds.startIndex + 1,
1307
+ paragraphEndLine: paragraphBounds.endIndex + 1,
1308
+ paragraph,
1309
+ paragraphTruncated,
1310
+ contextBefore: allLines.slice(contextStart, line.lineIndex),
1311
+ contextAfter: allLines.slice(line.lineIndex + 1, contextEnd + 1),
1312
+ };
1313
+ })
1314
+ .filter((entry) => entry !== null);
1315
+ const offset = toBoundedInt(params.cursor ?? params.offset, 0, 0, Number.MAX_SAFE_INTEGER);
1316
+ const limit = toBoundedInt(params.limit, 20, 1, 200);
1317
+ const page = allMatches.slice(offset, offset + limit);
1318
+ const hasMore = offset + page.length < allMatches.length;
1319
+ const nextCursor = hasMore ? String(offset + page.length) : null;
1320
+ const result = {
1321
+ success: true,
1322
+ query,
1323
+ count: page.length,
1324
+ totalCount: allMatches.length,
1325
+ offset,
1326
+ limit,
1327
+ hasMore,
1328
+ nextCursor,
1329
+ rangeStartLine: lineWindow.rangeStartLine,
1330
+ rangeEndLine: lineWindow.rangeEndLine,
1331
+ searchedLineCount: lineWindow.rangeLineCount,
1332
+ note: {
1333
+ id: note.id,
1334
+ title: note.title,
1335
+ filename: note.filename,
1336
+ type: note.type,
1337
+ source: note.source,
1338
+ folder: note.folder,
1339
+ spaceId: note.spaceId,
1340
+ date: note.date,
1341
+ },
1342
+ matches: page,
1343
+ };
1344
+ if (hasMore) {
1345
+ result.performanceHints = [NEXT_CURSOR_HINT];
1346
+ }
1347
+ else if (allMatches.length === 0) {
1348
+ result.performanceHints = [
1349
+ 'Try caseSensitive=false, wholeWord=false, or broaden startLine/endLine range.',
1350
+ ];
1351
+ }
1352
+ return result;
1353
+ }
1354
+ // Granular note operation schemas
1355
+ export const setPropertySchema = z.object({
1356
+ filename: z.string().describe('Filename/path of the note'),
1357
+ space: z.string().optional().describe('Space name or ID to search in'),
1358
+ key: z.string().describe('Property key (e.g., "icon", "bg-color", "status")'),
1359
+ value: z.string().describe('Property value'),
1360
+ });
1361
+ export const removePropertySchema = z.object({
1362
+ filename: z.string().describe('Filename/path of the note'),
1363
+ space: z.string().optional().describe('Space name or ID to search in'),
1364
+ key: z.string().describe('Property key to remove'),
1365
+ });
1366
+ export const insertContentSchema = z.object({
1367
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1368
+ filename: z.string().optional().describe('Filename/path of the note'),
1369
+ title: z.string().optional().describe('Note title to target (resolved if unique)'),
1370
+ date: z.string().optional().describe('Calendar note date target (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
1371
+ query: z.string().optional().describe('Resolvable note query (fuzzy note lookup before insert)'),
1372
+ space: z.string().optional().describe('Space name or ID scope for title/date/query resolution'),
1373
+ content: z.string().describe('Content to insert'),
1374
+ position: z
1375
+ .enum(['start', 'end', 'after-heading', 'at-line', 'in-section'])
1376
+ .describe('Where to insert: start (after frontmatter), end, after-heading (right after heading/marker line), in-section (at end of section, before next heading/marker), or at-line'),
1377
+ heading: z
1378
+ .string()
1379
+ .optional()
1380
+ .describe('Heading or section marker text (required for after-heading and in-section; matches both ## headings and **bold:** section markers)'),
1381
+ line: z.number().optional().describe('Line number (1-indexed, required for at-line position)'),
1382
+ indentationStyle: z
1383
+ .enum(['tabs', 'preserve'])
1384
+ .optional()
1385
+ .default('tabs')
1386
+ .describe('Indentation normalization for inserted list/task lines. Default: tabs'),
1387
+ type: z
1388
+ .enum(['title', 'heading', 'task', 'checklist', 'bullet', 'quote', 'separator', 'empty', 'text'])
1389
+ .optional()
1390
+ .describe('Paragraph type — when set, content is auto-formatted with correct markdown markers'),
1391
+ taskStatus: z
1392
+ .enum(['open', 'done', 'cancelled', 'scheduled'])
1393
+ .optional()
1394
+ .describe('Task/checklist status (default: open). Only used when type is task or checklist'),
1395
+ headingLevel: z
1396
+ .number()
1397
+ .min(1)
1398
+ .max(6)
1399
+ .optional()
1400
+ .describe('Heading level 1-6 (only used when type is heading or title)'),
1401
+ priority: z
1402
+ .number()
1403
+ .min(1)
1404
+ .max(3)
1405
+ .optional()
1406
+ .describe('Priority 1-3 (! / !! / !!!) appended to task/checklist lines'),
1407
+ indentLevel: z
1408
+ .number()
1409
+ .min(0)
1410
+ .max(10)
1411
+ .optional()
1412
+ .describe('Tab indentation level for task/checklist/bullet lines'),
1413
+ }).superRefine((input, ctx) => {
1414
+ if (!input.id && !input.filename && !input.title && !input.date && !input.query) {
1415
+ ctx.addIssue({
1416
+ code: z.ZodIssueCode.custom,
1417
+ message: 'Provide one note reference: id, filename, title, date, or query',
1418
+ path: ['filename'],
1419
+ });
1420
+ }
1421
+ });
1422
+ export const appendContentSchema = z.object({
1423
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1424
+ filename: z.string().optional().describe('Filename/path of the note'),
1425
+ title: z.string().optional().describe('Note title to target (resolved if unique)'),
1426
+ date: z.string().optional().describe('Calendar note date target (YYYYMMDD, YYYY-MM-DD, today, tomorrow, yesterday)'),
1427
+ query: z.string().optional().describe('Resolvable note query (fuzzy note lookup before append)'),
1428
+ space: z.string().optional().describe('Space name or ID scope for title/date/query resolution'),
1429
+ content: z.string().describe('Content to append'),
1430
+ indentationStyle: z
1431
+ .enum(['tabs', 'preserve'])
1432
+ .optional()
1433
+ .default('tabs')
1434
+ .describe('Indentation normalization for appended list/task lines. Default: tabs'),
1435
+ }).superRefine((input, ctx) => {
1436
+ if (!input.id && !input.filename && !input.title && !input.date && !input.query) {
1437
+ ctx.addIssue({
1438
+ code: z.ZodIssueCode.custom,
1439
+ message: 'Provide one note reference: id, filename, title, date, or query',
1440
+ path: ['filename'],
1441
+ });
1442
+ }
1443
+ });
1444
+ const noteReferenceSchema = {
1445
+ id: z.string().optional().describe('Note ID (preferred for space notes)'),
1446
+ filename: z.string().optional().describe('Filename/path of the note'),
1447
+ title: z.string().optional().describe('Note title'),
1448
+ date: z.string().optional().describe('Calendar note date (auto-creates if missing)'),
1449
+ query: z.string().optional().describe('Fuzzy note query'),
1450
+ space: z.string().optional().describe('Space name or ID scope'),
1451
+ };
1452
+ export const deleteLinesSchema = z.object({
1453
+ ...noteReferenceSchema,
1454
+ startLine: z.number().describe('First line to delete (1-indexed, inclusive)'),
1455
+ endLine: z.number().describe('Last line to delete (1-indexed, inclusive)'),
1456
+ dryRun: z
1457
+ .boolean()
1458
+ .optional()
1459
+ .describe('Preview lines that would be deleted without modifying the note (default: false)'),
1460
+ confirmationToken: z
1461
+ .string()
1462
+ .optional()
1463
+ .describe('Confirmation token issued by dryRun for delete execution'),
1464
+ });
1465
+ export const editLineSchema = z.object({
1466
+ ...noteReferenceSchema,
1467
+ line: z.number().describe('Line number to edit (1-indexed)'),
1468
+ content: z.string().describe('New content for the line'),
1469
+ indentationStyle: z
1470
+ .enum(['tabs', 'preserve'])
1471
+ .optional()
1472
+ .default('tabs')
1473
+ .describe('Indentation normalization for edited list/task lines. Default: tabs'),
1474
+ allowEmptyContent: z
1475
+ .boolean()
1476
+ .optional()
1477
+ .describe('Allow replacing line content with empty/blank text (default: false)'),
1478
+ });
1479
+ export const replaceLinesSchema = z.object({
1480
+ ...noteReferenceSchema,
1481
+ startLine: z.number().describe('First line to replace (1-indexed, inclusive)'),
1482
+ endLine: z.number().describe('Last line to replace (1-indexed, inclusive)'),
1483
+ content: z.string().describe('Replacement content for the selected line range'),
1484
+ indentationStyle: z
1485
+ .enum(['tabs', 'preserve'])
1486
+ .optional()
1487
+ .default('tabs')
1488
+ .describe('Indentation normalization for replacement list/task lines. Default: tabs'),
1489
+ dryRun: z
1490
+ .boolean()
1491
+ .optional()
1492
+ .describe('Preview line replacement and get confirmationToken without modifying the note'),
1493
+ confirmationToken: z
1494
+ .string()
1495
+ .optional()
1496
+ .describe('Confirmation token issued by dryRun for replace execution'),
1497
+ allowEmptyContent: z
1498
+ .boolean()
1499
+ .optional()
1500
+ .describe('Allow replacing selected lines with empty content (default: false). Prefer delete_lines for pure deletion.'),
1501
+ });
1502
+ // Granular note operation implementations
1503
+ export function setProperty(params) {
1504
+ try {
1505
+ const note = store.getNote({ filename: params.filename, space: params.space });
1506
+ if (!note) {
1507
+ return { success: false, error: 'Note not found' };
1508
+ }
1509
+ const newContent = frontmatter.setFrontmatterProperty(note.content, params.key, params.value);
1510
+ const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1511
+ store.updateNote(writeIdentifier, newContent, { source: note.source });
1512
+ return {
1513
+ success: true,
1514
+ message: `Property "${params.key}" set to "${params.value}"`,
1515
+ };
1516
+ }
1517
+ catch (error) {
1518
+ return {
1519
+ success: false,
1520
+ error: error instanceof Error ? error.message : 'Failed to set property',
1521
+ };
1522
+ }
1523
+ }
1524
+ export function removeProperty(params) {
1525
+ try {
1526
+ const note = store.getNote({ filename: params.filename, space: params.space });
1527
+ if (!note) {
1528
+ return { success: false, error: 'Note not found' };
1529
+ }
1530
+ const newContent = frontmatter.removeFrontmatterProperty(note.content, params.key);
1531
+ const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1532
+ store.updateNote(writeIdentifier, newContent, { source: note.source });
1533
+ return {
1534
+ success: true,
1535
+ message: `Property "${params.key}" removed`,
1536
+ };
1537
+ }
1538
+ catch (error) {
1539
+ return {
1540
+ success: false,
1541
+ error: error instanceof Error ? error.message : 'Failed to remove property',
1542
+ };
1543
+ }
1544
+ }
1545
+ export function insertContent(params) {
1546
+ try {
1547
+ const resolved = resolveWritableNoteReference(params);
1548
+ if (!resolved.note) {
1549
+ return {
1550
+ success: false,
1551
+ error: resolved.error || 'Note not found',
1552
+ candidates: resolved.candidates,
1553
+ };
1554
+ }
1555
+ const note = resolved.note;
1556
+ const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1557
+ let contentToInsert = params.content;
1558
+ if (params.type) {
1559
+ contentToInsert = contentToInsert
1560
+ .split('\n')
1561
+ .map((line) => buildParagraphLine(line, params.type, {
1562
+ headingLevel: params.headingLevel,
1563
+ taskStatus: params.taskStatus ?? undefined,
1564
+ indentLevel: params.indentLevel,
1565
+ priority: params.priority,
1566
+ }))
1567
+ .join('\n');
1568
+ }
1569
+ const normalized = normalizeContentIndentation(contentToInsert, indentationStyle);
1570
+ const newContent = frontmatter.insertContentAtPosition(note.content, normalized.content, {
1571
+ position: params.position,
1572
+ heading: params.heading,
1573
+ line: params.line,
1574
+ });
1575
+ const writeTarget = getWritableIdentifier(note);
1576
+ store.updateNote(writeTarget.identifier, newContent, {
1577
+ source: writeTarget.source,
1578
+ });
1579
+ return {
1580
+ success: true,
1581
+ message: `Content inserted at ${params.position}`,
1582
+ note: {
1583
+ id: note.id,
1584
+ title: note.title,
1585
+ filename: note.filename,
1586
+ },
1587
+ indentationStyle,
1588
+ linesRetabbed: normalized.linesRetabbed,
1589
+ };
1590
+ }
1591
+ catch (error) {
1592
+ return {
1593
+ success: false,
1594
+ error: error instanceof Error ? error.message : 'Failed to insert content',
1595
+ };
1596
+ }
1597
+ }
1598
+ export function appendContent(params) {
1599
+ try {
1600
+ const resolved = resolveWritableNoteReference(params);
1601
+ if (!resolved.note) {
1602
+ return {
1603
+ success: false,
1604
+ error: resolved.error || 'Note not found',
1605
+ candidates: resolved.candidates,
1606
+ };
1607
+ }
1608
+ const note = resolved.note;
1609
+ const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1610
+ const normalized = normalizeContentIndentation(params.content, indentationStyle);
1611
+ const newContent = frontmatter.insertContentAtPosition(note.content, normalized.content, {
1612
+ position: 'end',
1613
+ });
1614
+ const writeTarget = getWritableIdentifier(note);
1615
+ store.updateNote(writeTarget.identifier, newContent, {
1616
+ source: writeTarget.source,
1617
+ });
1618
+ return {
1619
+ success: true,
1620
+ message: 'Content appended',
1621
+ note: {
1622
+ id: note.id,
1623
+ title: note.title,
1624
+ filename: note.filename,
1625
+ },
1626
+ indentationStyle,
1627
+ linesRetabbed: normalized.linesRetabbed,
1628
+ };
1629
+ }
1630
+ catch (error) {
1631
+ return {
1632
+ success: false,
1633
+ error: error instanceof Error ? error.message : 'Failed to append content',
1634
+ };
1635
+ }
1636
+ }
1637
+ export function deleteLines(params) {
1638
+ try {
1639
+ const resolved = resolveWritableNoteReference(params);
1640
+ if (!resolved.note) {
1641
+ return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
1642
+ }
1643
+ const note = resolved.note;
1644
+ 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));
1647
+ const lineCountToDelete = boundedEndLine - boundedStartLine + 1;
1648
+ const previewStartIndex = boundedStartLine - 1;
1649
+ const previewEndIndexExclusive = boundedEndLine;
1650
+ const deletedLinesPreview = allLines
1651
+ .slice(previewStartIndex, previewEndIndexExclusive)
1652
+ .slice(0, 20)
1653
+ .map((content, index) => ({
1654
+ line: boundedStartLine + index,
1655
+ content,
1656
+ }));
1657
+ const deletedText = allLines.slice(previewStartIndex, previewEndIndexExclusive).join('\n');
1658
+ const removedAttachmentReferences = extractAttachmentReferences(deletedText);
1659
+ const attachmentWarning = removedAttachmentReferences.length > 0
1660
+ ? buildAttachmentWarningMessage(removedAttachmentReferences.length)
1661
+ : undefined;
1662
+ const confirmTarget = `${note.filename}:${boundedStartLine}-${boundedEndLine}`;
1663
+ if (params.dryRun === true) {
1664
+ const token = issueConfirmationToken({
1665
+ tool: 'noteplan_delete_lines',
1666
+ target: confirmTarget,
1667
+ action: 'delete_lines',
1668
+ });
1669
+ return {
1670
+ success: true,
1671
+ dryRun: true,
1672
+ message: `Dry run: lines ${boundedStartLine}-${boundedEndLine} would be deleted`,
1673
+ lineCountToDelete,
1674
+ deletedLinesPreview,
1675
+ previewTruncated: lineCountToDelete > deletedLinesPreview.length,
1676
+ removedAttachmentReferences: removedAttachmentReferences.slice(0, 20),
1677
+ removedAttachmentReferencesTruncated: removedAttachmentReferences.length > 20,
1678
+ warnings: attachmentWarning ? [attachmentWarning] : undefined,
1679
+ ...token,
1680
+ };
1681
+ }
1682
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
1683
+ tool: 'noteplan_delete_lines',
1684
+ target: confirmTarget,
1685
+ action: 'delete_lines',
1686
+ });
1687
+ if (!confirmation.ok) {
1688
+ return {
1689
+ success: false,
1690
+ error: confirmationFailureMessage('noteplan_delete_lines', confirmation.reason),
1691
+ };
1692
+ }
1693
+ const newContent = frontmatter.deleteLines(note.content, boundedStartLine, boundedEndLine);
1694
+ const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1695
+ store.updateNote(writeIdentifier, newContent, { source: note.source });
1696
+ return {
1697
+ success: true,
1698
+ message: `Lines ${boundedStartLine}-${boundedEndLine} deleted`,
1699
+ lineCountToDelete,
1700
+ removedAttachmentReferences: removedAttachmentReferences.slice(0, 20),
1701
+ removedAttachmentReferencesTruncated: removedAttachmentReferences.length > 20,
1702
+ warnings: attachmentWarning ? [attachmentWarning] : undefined,
1703
+ };
1704
+ }
1705
+ catch (error) {
1706
+ return {
1707
+ success: false,
1708
+ error: error instanceof Error ? error.message : 'Failed to delete lines',
1709
+ };
1710
+ }
1711
+ }
1712
+ export function editLine(params) {
1713
+ try {
1714
+ if (params.allowEmptyContent !== true && params.content.trim().length === 0) {
1715
+ return {
1716
+ success: false,
1717
+ error: 'Empty line content is blocked for noteplan_edit_line. Use noteplan_delete_lines or set allowEmptyContent=true.',
1718
+ };
1719
+ }
1720
+ const resolved = resolveWritableNoteReference(params);
1721
+ if (!resolved.note) {
1722
+ return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
1723
+ }
1724
+ const note = resolved.note;
1725
+ const lines = note.content.split('\n');
1726
+ const originalLineCount = lines.length;
1727
+ const lineIndex = params.line - 1; // Convert to 0-indexed
1728
+ if (lineIndex < 0 || lineIndex >= lines.length) {
1729
+ return {
1730
+ success: false,
1731
+ error: `Line ${params.line} does not exist (note has ${lines.length} lines)`,
1732
+ };
1733
+ }
1734
+ const originalLine = lines[lineIndex];
1735
+ const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1736
+ const normalized = normalizeContentIndentation(params.content, indentationStyle);
1737
+ const replacementLines = normalized.content.split('\n');
1738
+ lines.splice(lineIndex, 1, ...replacementLines);
1739
+ const lineDelta = replacementLines.length - 1;
1740
+ const updatedLineCount = originalLineCount + lineDelta;
1741
+ const newContent = lines.join('\n');
1742
+ const removedAttachmentReferences = getRemovedAttachmentReferences(originalLine, normalized.content);
1743
+ const warnings = [];
1744
+ if (lineDelta !== 0) {
1745
+ warnings.push(`Line numbers shifted by ${lineDelta > 0 ? '+' : ''}${lineDelta} after this edit. Re-read line numbers before the next mutation.`);
1746
+ }
1747
+ if (removedAttachmentReferences.length > 0) {
1748
+ warnings.push(buildAttachmentWarningMessage(removedAttachmentReferences.length));
1749
+ }
1750
+ const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1751
+ store.updateNote(writeIdentifier, newContent, { source: note.source });
1752
+ return {
1753
+ success: true,
1754
+ message: `Line ${params.line} updated`,
1755
+ originalLine,
1756
+ newLine: normalized.content,
1757
+ indentationStyle,
1758
+ linesRetabbed: normalized.linesRetabbed,
1759
+ insertedLineCount: replacementLines.length,
1760
+ lineDelta,
1761
+ originalLineCount,
1762
+ newLineCount: updatedLineCount,
1763
+ removedAttachmentReferences: removedAttachmentReferences.slice(0, 20),
1764
+ removedAttachmentReferencesTruncated: removedAttachmentReferences.length > 20,
1765
+ warnings: warnings.length > 0 ? warnings : undefined,
1766
+ };
1767
+ }
1768
+ catch (error) {
1769
+ return {
1770
+ success: false,
1771
+ error: error instanceof Error ? error.message : 'Failed to edit line',
1772
+ };
1773
+ }
1774
+ }
1775
+ export function replaceLines(params) {
1776
+ try {
1777
+ const resolved = resolveWritableNoteReference(params);
1778
+ if (!resolved.note) {
1779
+ return { success: false, error: resolved.error || 'Note not found', candidates: resolved.candidates };
1780
+ }
1781
+ const note = resolved.note;
1782
+ const allLines = note.content.split('\n');
1783
+ 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;
1787
+ const lineCountToReplace = boundedEndLine - boundedStartLine + 1;
1788
+ const replacedText = allLines.slice(startIndex, boundedEndLine).join('\n');
1789
+ const indentationStyle = normalizeIndentationStyle(params.indentationStyle);
1790
+ const normalized = normalizeContentIndentation(params.content, indentationStyle);
1791
+ if (params.allowEmptyContent !== true && normalized.content.trim().length === 0) {
1792
+ return {
1793
+ success: false,
1794
+ error: 'Empty replacement content is blocked for noteplan_replace_lines. Use noteplan_delete_lines or set allowEmptyContent=true.',
1795
+ };
1796
+ }
1797
+ const replacementLines = normalized.content.length > 0 ? normalized.content.split('\n') : [];
1798
+ const lineDelta = replacementLines.length - lineCountToReplace;
1799
+ const newLineCount = originalLineCount + lineDelta;
1800
+ const removedAttachmentReferences = getRemovedAttachmentReferences(replacedText, normalized.content);
1801
+ const warnings = [];
1802
+ if (removedAttachmentReferences.length > 0) {
1803
+ warnings.push(buildAttachmentWarningMessage(removedAttachmentReferences.length));
1804
+ }
1805
+ if (lineDelta !== 0) {
1806
+ warnings.push(`Line numbers shifted by ${lineDelta > 0 ? '+' : ''}${lineDelta} after this replacement. Re-read line numbers before the next mutation.`);
1807
+ }
1808
+ const target = `${note.filename}:${boundedStartLine}-${boundedEndLine}:${replacementLines.length}:${normalized.content.length}`;
1809
+ if (params.dryRun === true) {
1810
+ const token = issueConfirmationToken({
1811
+ tool: 'noteplan_replace_lines',
1812
+ target,
1813
+ action: 'replace_lines',
1814
+ });
1815
+ return {
1816
+ success: true,
1817
+ dryRun: true,
1818
+ message: `Dry run: lines ${boundedStartLine}-${boundedEndLine} would be replaced`,
1819
+ lineCountToReplace,
1820
+ insertedLineCount: replacementLines.length,
1821
+ lineDelta,
1822
+ originalLineCount,
1823
+ newLineCount,
1824
+ indentationStyle,
1825
+ linesRetabbed: normalized.linesRetabbed,
1826
+ removedAttachmentReferences: removedAttachmentReferences.slice(0, 20),
1827
+ removedAttachmentReferencesTruncated: removedAttachmentReferences.length > 20,
1828
+ warnings: warnings.length > 0 ? warnings : undefined,
1829
+ ...token,
1830
+ };
1831
+ }
1832
+ const confirmation = validateAndConsumeConfirmationToken(params.confirmationToken, {
1833
+ tool: 'noteplan_replace_lines',
1834
+ target,
1835
+ action: 'replace_lines',
1836
+ });
1837
+ if (!confirmation.ok) {
1838
+ return {
1839
+ success: false,
1840
+ error: confirmationFailureMessage('noteplan_replace_lines', confirmation.reason),
1841
+ };
1842
+ }
1843
+ allLines.splice(startIndex, lineCountToReplace, ...replacementLines);
1844
+ const writeIdentifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
1845
+ store.updateNote(writeIdentifier, allLines.join('\n'), { source: note.source });
1846
+ return {
1847
+ success: true,
1848
+ message: `Lines ${boundedStartLine}-${boundedEndLine} replaced`,
1849
+ lineCountToReplace,
1850
+ insertedLineCount: replacementLines.length,
1851
+ lineDelta,
1852
+ originalLineCount,
1853
+ newLineCount,
1854
+ indentationStyle,
1855
+ linesRetabbed: normalized.linesRetabbed,
1856
+ removedAttachmentReferences: removedAttachmentReferences.slice(0, 20),
1857
+ removedAttachmentReferencesTruncated: removedAttachmentReferences.length > 20,
1858
+ warnings: warnings.length > 0 ? warnings : undefined,
1859
+ };
1860
+ }
1861
+ catch (error) {
1862
+ return {
1863
+ success: false,
1864
+ error: error instanceof Error ? error.message : 'Failed to replace lines',
1865
+ };
1866
+ }
1867
+ }
1868
+ //# sourceMappingURL=notes.js.map