@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,782 @@
1
+ // Plugin creation and management tools
2
+ import { z } from 'zod';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { execFileSync } from 'child_process';
6
+ import { getNotePlanPath } from '../noteplan/file-reader.js';
7
+ import { issueConfirmationToken, validateAndConsumeConfirmationToken, } from '../utils/confirmation-tokens.js';
8
+ import { reloadPlugins, runPlugin } from './ui.js';
9
+ import { escapeAppleScript, runAppleScript, APP_NAME } from '../utils/applescript.js';
10
+ function getPluginsPath() {
11
+ return path.join(getNotePlanPath(), 'Plugins');
12
+ }
13
+ function validatePluginId(pluginId) {
14
+ if (pluginId.trim().length === 0) {
15
+ return 'Invalid pluginId: must not be empty';
16
+ }
17
+ if (!/^[a-zA-Z0-9._-]+$/.test(pluginId)) {
18
+ return 'Invalid pluginId: must contain only letters, digits, dots, hyphens, and underscores';
19
+ }
20
+ return null;
21
+ }
22
+ function toSafeJsIdentifier(name) {
23
+ // Strip whitespace, replace non-identifier chars with underscore
24
+ let id = name.replace(/\s+/g, '').replace(/[^a-zA-Z0-9_$]/g, '_');
25
+ // Ensure it doesn't start with a digit
26
+ if (/^[0-9]/.test(id)) {
27
+ id = '_' + id;
28
+ }
29
+ return id || '_command';
30
+ }
31
+ // --- Schemas ---
32
+ export const createPluginSchema = z.object({
33
+ pluginId: z.string().describe('Plugin ID (e.g., "mcp.dashboard")'),
34
+ pluginName: z.string().describe('Display name of the plugin'),
35
+ commandName: z.string().describe('The command name'),
36
+ html: z.string().describe('Full HTML content for the plugin view'),
37
+ icon: z.string().optional().describe('Font Awesome icon name (e.g., "chart-bar")'),
38
+ iconColor: z.string().optional().describe('Tailwind color like "blue-500"'),
39
+ displayMode: z
40
+ .enum(['main', 'split', 'window'])
41
+ .optional()
42
+ .default('main')
43
+ .describe('Where to display the HTML view'),
44
+ autoLaunch: z
45
+ .boolean()
46
+ .optional()
47
+ .default(true)
48
+ .describe('Reload plugins and run after creation'),
49
+ });
50
+ export const deletePluginSchema = z.object({
51
+ pluginId: z.string().describe('Plugin ID to delete'),
52
+ confirmationToken: z.string().optional().describe('Confirmation token (call without to receive one)'),
53
+ });
54
+ export const listPluginsSchema = z.object({
55
+ query: z.string().optional().describe('Filter plugins by name or ID (case-insensitive substring match)'),
56
+ });
57
+ export const listAvailablePluginsSchema = z.object({
58
+ query: z.string().optional().describe('Filter by plugin name or ID (case-insensitive substring match)'),
59
+ includeBeta: z.boolean().optional().default(false).describe('Include beta/pre-release plugins (default: false, showing only stable releases)'),
60
+ });
61
+ export const installPluginSchema = z.object({
62
+ pluginId: z.string().describe('Plugin ID to install or update'),
63
+ });
64
+ export const getPluginLogSchema = z.object({
65
+ pluginId: z.string().describe('Plugin ID whose console log to read'),
66
+ tail: z.number().int().min(1).optional().describe('Return only the last N lines of the log'),
67
+ clear: z.boolean().optional().default(false).describe('Clear the log file after reading it'),
68
+ });
69
+ export const getPluginSourceSchema = z.object({
70
+ pluginId: z.string().describe('Plugin ID (e.g., "mcp.dashboard"). Use noteplan_list_plugins to find valid IDs.'),
71
+ query: z.string().optional().describe('Search within the HTML source. Returns matching lines with line numbers and context. Much cheaper than reading full source.'),
72
+ startLine: z.number().int().min(1).optional().describe('Return lines starting from this line number (1-based). Use with endLine for a slice.'),
73
+ endLine: z.number().int().min(1).optional().describe('Return lines up to and including this line number (1-based).'),
74
+ contextLines: z.number().int().min(0).max(10).optional().default(3).describe('Number of context lines around query matches (default: 3). Only used with query.'),
75
+ });
76
+ export const screenshotPluginSchema = z.object({
77
+ pluginId: z.string().describe('Plugin ID to screenshot (e.g., "mcp.dashboard")'),
78
+ });
79
+ export const updatePluginHtmlSchema = z.object({
80
+ pluginId: z.string().describe('Plugin ID (e.g., "mcp.dashboard")'),
81
+ patches: z.array(z.object({
82
+ find: z.string().describe('Exact string to find in the HTML'),
83
+ replace: z.string().describe('Replacement string'),
84
+ })).min(1).max(50).describe('Find/replace patches to apply sequentially (first match only per patch)'),
85
+ autoLaunch: z.boolean().optional().default(true).describe('Reload plugins and run after patching (default: true)'),
86
+ });
87
+ // --- Implementations ---
88
+ export function listPlugins(args) {
89
+ const { query } = listPluginsSchema.parse(args ?? {});
90
+ let raw;
91
+ try {
92
+ raw = runAppleScript(`tell application "${APP_NAME}" to listInstalledPlugins`, 30_000);
93
+ }
94
+ catch (e) {
95
+ return { success: false, error: `Failed to list installed plugins: ${e.message}` };
96
+ }
97
+ let allPlugins;
98
+ try {
99
+ allPlugins = JSON.parse(raw);
100
+ }
101
+ catch {
102
+ return { success: false, error: 'Failed to parse plugin list from NotePlan' };
103
+ }
104
+ const plugins = [];
105
+ for (const p of allPlugins) {
106
+ const pluginId = p.id ?? '';
107
+ const pluginName = p.name ?? '';
108
+ if (query) {
109
+ const lq = query.toLowerCase();
110
+ if (!pluginId.toLowerCase().includes(lq) && !pluginName.toLowerCase().includes(lq))
111
+ continue;
112
+ }
113
+ const commands = Array.isArray(p.commands) ? p.commands : [];
114
+ plugins.push({
115
+ id: pluginId,
116
+ name: pluginName,
117
+ description: p.description ?? '',
118
+ author: p.author ?? '',
119
+ version: p.version ?? '',
120
+ icon: p.icon ?? '',
121
+ ...(p.iconColor ? { iconColor: p.iconColor } : {}),
122
+ isEnabled: p.isEnabled ?? true,
123
+ commandCount: commands.length,
124
+ commands,
125
+ });
126
+ }
127
+ plugins.sort((a, b) => String(a.name).localeCompare(String(b.name)));
128
+ return {
129
+ success: true,
130
+ count: plugins.length,
131
+ plugins,
132
+ };
133
+ }
134
+ export function createPlugin(args) {
135
+ const { pluginId, pluginName, commandName, html, icon, iconColor, displayMode, autoLaunch, } = createPluginSchema.parse(args);
136
+ const idError = validatePluginId(pluginId);
137
+ if (idError) {
138
+ return { success: false, error: idError };
139
+ }
140
+ const pluginsPath = getPluginsPath();
141
+ const pluginDir = path.join(pluginsPath, pluginId);
142
+ fs.mkdirSync(pluginDir, { recursive: true });
143
+ // Build the plugin.json manifest
144
+ const jsFunction = toSafeJsIdentifier(commandName);
145
+ const manifest = {
146
+ 'plugin.id': pluginId,
147
+ 'plugin.name': pluginName,
148
+ 'plugin.description': `Created by MCP: ${pluginName}`,
149
+ 'plugin.author': 'MCP',
150
+ 'plugin.version': '1.0.0',
151
+ 'plugin.script': 'script.js',
152
+ 'plugin.icon': icon ?? 'puzzle-piece',
153
+ 'plugin.commands': [
154
+ {
155
+ name: commandName,
156
+ description: pluginName,
157
+ jsFunction,
158
+ sidebarView: {
159
+ title: pluginName,
160
+ icon: icon ?? 'puzzle-piece',
161
+ ...(iconColor ? { iconColor } : {}),
162
+ },
163
+ },
164
+ ],
165
+ };
166
+ if (iconColor) {
167
+ manifest['plugin.iconColor'] = iconColor;
168
+ }
169
+ fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2), 'utf-8');
170
+ // Build script.js
171
+ // Escape backticks and template literal expressions in the HTML for safe embedding
172
+ const escapedHtml = html.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
173
+ // For showInMainWindow, we pass an options object with id for sidebar pinning
174
+ let scriptJs;
175
+ if (displayMode === 'main') {
176
+ scriptJs = `// Generated by MCP
177
+ globalThis.${jsFunction} = async function() {
178
+ const html = \`${escapedHtml}\`;
179
+ await HTMLView.showInMainWindow(html, ${JSON.stringify(pluginName)}, { id: "main:${pluginId}:${pluginName}" });
180
+ };
181
+ `;
182
+ }
183
+ else {
184
+ const showMethod = displayMode === 'window' ? 'showWindow' : 'showInSplitView';
185
+ scriptJs = `// Generated by MCP
186
+ globalThis.${jsFunction} = async function() {
187
+ const html = \`${escapedHtml}\`;
188
+ await HTMLView.${showMethod}(html, ${JSON.stringify(pluginName)});
189
+ };
190
+ `;
191
+ }
192
+ fs.writeFileSync(path.join(pluginDir, 'script.js'), scriptJs, 'utf-8');
193
+ const result = {
194
+ success: true,
195
+ message: `Plugin "${pluginName}" created successfully`,
196
+ pluginId,
197
+ commandName,
198
+ hint: 'Call noteplan_get_plugin_log to check for errors.',
199
+ };
200
+ if (autoLaunch) {
201
+ try {
202
+ reloadPlugins({});
203
+ // Brief delay to let plugins load before running
204
+ execFileSync('sleep', ['1'], { timeout: 5000 });
205
+ runPlugin({ pluginId, command: commandName });
206
+ result.launched = true;
207
+ }
208
+ catch (error) {
209
+ result.launched = false;
210
+ result.launchError = error.message;
211
+ }
212
+ }
213
+ return result;
214
+ }
215
+ export function deletePlugin(args) {
216
+ const { pluginId, confirmationToken } = deletePluginSchema.parse(args);
217
+ const idError = validatePluginId(pluginId);
218
+ if (idError) {
219
+ return { success: false, error: idError };
220
+ }
221
+ const pluginsPath = getPluginsPath();
222
+ const pluginDir = path.join(pluginsPath, pluginId);
223
+ if (!fs.existsSync(pluginDir)) {
224
+ return { success: false, error: `Plugin "${pluginId}" not found` };
225
+ }
226
+ const context = { tool: 'noteplan_delete_plugin', target: pluginId, action: 'delete' };
227
+ if (!confirmationToken) {
228
+ const token = issueConfirmationToken(context);
229
+ return {
230
+ success: false,
231
+ error: 'Confirmation required to delete plugin',
232
+ pluginId,
233
+ ...token,
234
+ };
235
+ }
236
+ const validation = validateAndConsumeConfirmationToken(confirmationToken, context);
237
+ if (!validation.ok) {
238
+ return {
239
+ success: false,
240
+ error: `Confirmation failed: ${validation.reason}`,
241
+ pluginId,
242
+ };
243
+ }
244
+ fs.rmSync(pluginDir, { recursive: true, force: true });
245
+ // Reload plugins so NotePlan picks up the removal
246
+ try {
247
+ reloadPlugins({});
248
+ }
249
+ catch {
250
+ // Non-fatal — plugin is already deleted from disk
251
+ }
252
+ return {
253
+ success: true,
254
+ message: `Plugin "${pluginId}" deleted`,
255
+ pluginId,
256
+ };
257
+ }
258
+ export function listAvailablePlugins(args) {
259
+ const { query, includeBeta } = listAvailablePluginsSchema.parse(args ?? {});
260
+ let script = `tell application "${APP_NAME}" to listAvailablePlugins`;
261
+ if (includeBeta) {
262
+ script = `tell application "${APP_NAME}" to listAvailablePlugins include beta true`;
263
+ }
264
+ let raw;
265
+ try {
266
+ raw = runAppleScript(script, 30_000);
267
+ }
268
+ catch (e) {
269
+ return { success: false, error: `Failed to list available plugins: ${e.message}` };
270
+ }
271
+ let parsed;
272
+ try {
273
+ parsed = JSON.parse(raw);
274
+ }
275
+ catch {
276
+ return { success: false, error: 'Failed to parse available plugins from NotePlan' };
277
+ }
278
+ if (parsed.error) {
279
+ return { success: false, error: parsed.error };
280
+ }
281
+ const allPlugins = Array.isArray(parsed) ? parsed : [];
282
+ const plugins = [];
283
+ for (const p of allPlugins) {
284
+ const pluginId = p.id ?? '';
285
+ const pluginName = p.name ?? '';
286
+ if (query) {
287
+ const lq = query.toLowerCase();
288
+ if (!pluginId.toLowerCase().includes(lq) && !pluginName.toLowerCase().includes(lq))
289
+ continue;
290
+ }
291
+ const commands = Array.isArray(p.commands) ? p.commands : [];
292
+ plugins.push({
293
+ id: pluginId,
294
+ name: pluginName,
295
+ description: p.description ?? '',
296
+ author: p.author ?? '',
297
+ version: p.version ?? '',
298
+ icon: p.icon ?? '',
299
+ ...(p.iconColor ? { iconColor: p.iconColor } : {}),
300
+ ...(p.releaseStatus ? { releaseStatus: p.releaseStatus } : {}),
301
+ ...(p.availableUpdate ? { availableUpdate: p.availableUpdate } : {}),
302
+ commandCount: commands.length,
303
+ commands,
304
+ });
305
+ }
306
+ plugins.sort((a, b) => String(a.name).localeCompare(String(b.name)));
307
+ return {
308
+ success: true,
309
+ count: plugins.length,
310
+ plugins,
311
+ };
312
+ }
313
+ export function installPlugin(args) {
314
+ const { pluginId } = installPluginSchema.parse(args);
315
+ const idError = validatePluginId(pluginId);
316
+ if (idError) {
317
+ return { success: false, error: idError };
318
+ }
319
+ try {
320
+ const script = `tell application "${APP_NAME}" to installPlugin with id "${escapeAppleScript(pluginId)}"`;
321
+ const result = runAppleScript(script, 30_000);
322
+ if (result === 'false') {
323
+ return { success: false, error: `Failed to install plugin "${pluginId}"` };
324
+ }
325
+ }
326
+ catch (e) {
327
+ return { success: false, error: `Failed to trigger install: ${e.message}` };
328
+ }
329
+ return {
330
+ success: true,
331
+ message: `Install/update triggered for plugin "${pluginId}". NotePlan is processing the installation asynchronously.`,
332
+ };
333
+ }
334
+ export function getPluginLog(args) {
335
+ const { pluginId, tail, clear } = getPluginLogSchema.parse(args);
336
+ const idError = validatePluginId(pluginId);
337
+ if (idError) {
338
+ return { success: false, error: idError };
339
+ }
340
+ const pluginsPath = getPluginsPath();
341
+ const logPath = path.join(pluginsPath, pluginId, '_MCP-console.log');
342
+ if (!fs.existsSync(logPath)) {
343
+ return {
344
+ success: true,
345
+ log: '',
346
+ message: `No console log found for plugin "${pluginId}". The plugin may not have been run yet, or it produced no output.`,
347
+ };
348
+ }
349
+ const fullLog = fs.readFileSync(logPath, 'utf-8');
350
+ const allLines = fullLog.split('\n');
351
+ const totalLines = allLines.filter((l) => l.length > 0).length;
352
+ let log;
353
+ let truncated = false;
354
+ if (tail && tail < allLines.length) {
355
+ // Take the last N lines (preserving trailing newline behavior)
356
+ log = allLines.slice(-tail).join('\n');
357
+ truncated = true;
358
+ }
359
+ else {
360
+ log = fullLog;
361
+ }
362
+ // Clear the log file after reading if requested
363
+ if (clear) {
364
+ fs.writeFileSync(logPath, '', 'utf-8');
365
+ }
366
+ return {
367
+ success: true,
368
+ pluginId,
369
+ log,
370
+ lineCount: totalLines,
371
+ ...(truncated ? { truncated: true, showing: `last ${tail} lines of ${totalLines}` } : {}),
372
+ ...(clear ? { cleared: true } : {}),
373
+ };
374
+ }
375
+ export function getPluginSource(args) {
376
+ const { pluginId, query, startLine, endLine, contextLines } = getPluginSourceSchema.parse(args);
377
+ const idError = validatePluginId(pluginId);
378
+ if (idError) {
379
+ return { success: false, error: idError };
380
+ }
381
+ const pluginsPath = getPluginsPath();
382
+ const pluginDir = path.join(pluginsPath, pluginId);
383
+ if (!fs.existsSync(pluginDir)) {
384
+ return { success: false, error: `Plugin "${pluginId}" not found` };
385
+ }
386
+ const manifestPath = path.join(pluginDir, 'plugin.json');
387
+ const scriptPath = path.join(pluginDir, 'script.js');
388
+ if (!fs.existsSync(manifestPath)) {
389
+ return { success: false, error: `plugin.json not found in plugin folder "${pluginId}"` };
390
+ }
391
+ if (!fs.existsSync(scriptPath)) {
392
+ return { success: false, error: `script.js not found in plugin folder "${pluginId}"` };
393
+ }
394
+ let pluginJson;
395
+ try {
396
+ pluginJson = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
397
+ }
398
+ catch (e) {
399
+ return { success: false, error: `Failed to parse plugin.json: ${e.message}` };
400
+ }
401
+ const scriptJs = fs.readFileSync(scriptPath, 'utf-8');
402
+ const bounds = findHtmlTemplateBounds(scriptJs);
403
+ // Get the source content and determine type
404
+ let source;
405
+ let mcpGenerated;
406
+ let displayMode;
407
+ if (bounds) {
408
+ const escapedHtml = scriptJs.slice(bounds.start, bounds.end);
409
+ source = unescapeTemplateHtml(escapedHtml);
410
+ mcpGenerated = true;
411
+ displayMode = 'main';
412
+ if (scriptJs.includes('HTMLView.showWindow('))
413
+ displayMode = 'window';
414
+ else if (scriptJs.includes('HTMLView.showInSplitView('))
415
+ displayMode = 'split';
416
+ }
417
+ else {
418
+ source = scriptJs;
419
+ mcpGenerated = false;
420
+ }
421
+ const allLines = source.split('\n');
422
+ const totalLines = allLines.length;
423
+ const sourceLength = source.length;
424
+ // --- Query mode: grep within the source ---
425
+ if (query) {
426
+ const lowerQuery = query.toLowerCase();
427
+ const matchingLineNums = [];
428
+ for (let i = 0; i < allLines.length; i++) {
429
+ if (allLines[i].toLowerCase().includes(lowerQuery)) {
430
+ matchingLineNums.push(i);
431
+ }
432
+ }
433
+ if (matchingLineNums.length === 0) {
434
+ return {
435
+ success: true,
436
+ pluginId,
437
+ mcpGenerated,
438
+ matchCount: 0,
439
+ totalLines,
440
+ sourceLength,
441
+ message: `No matches for "${query}"`,
442
+ };
443
+ }
444
+ // Build result with context lines, merging overlapping ranges
445
+ const ctx = contextLines ?? 3;
446
+ const resultLines = [];
447
+ let lastEmittedLine = -1;
448
+ for (const matchIdx of matchingLineNums) {
449
+ const rangeStart = Math.max(0, matchIdx - ctx);
450
+ const rangeEnd = Math.min(allLines.length - 1, matchIdx + ctx);
451
+ // Add separator if there's a gap from previous range
452
+ if (lastEmittedLine >= 0 && rangeStart > lastEmittedLine + 1) {
453
+ resultLines.push('---');
454
+ }
455
+ for (let i = rangeStart; i <= rangeEnd; i++) {
456
+ if (i <= lastEmittedLine)
457
+ continue; // skip already emitted
458
+ const marker = i === matchIdx ? '>' : ' ';
459
+ resultLines.push(`${marker} ${i + 1}\t${allLines[i]}`);
460
+ lastEmittedLine = i;
461
+ }
462
+ }
463
+ return {
464
+ success: true,
465
+ pluginId,
466
+ mcpGenerated,
467
+ matchCount: matchingLineNums.length,
468
+ totalLines,
469
+ sourceLength,
470
+ matches: resultLines.join('\n'),
471
+ hint: 'Use startLine/endLine to read a specific range, or noteplan_update_plugin_html to edit.',
472
+ };
473
+ }
474
+ // --- Line range mode: return a slice ---
475
+ if (startLine !== undefined || endLine !== undefined) {
476
+ const from = Math.max(0, (startLine ?? 1) - 1); // convert 1-based to 0-based
477
+ const to = Math.min(allLines.length, endLine ?? allLines.length);
478
+ if (from >= allLines.length) {
479
+ return {
480
+ success: false,
481
+ error: `startLine ${startLine} exceeds total lines (${totalLines})`,
482
+ totalLines,
483
+ sourceLength,
484
+ };
485
+ }
486
+ const slicedLines = allLines.slice(from, to);
487
+ const numbered = slicedLines.map((line, i) => `${from + i + 1}\t${line}`).join('\n');
488
+ return {
489
+ success: true,
490
+ pluginId,
491
+ mcpGenerated,
492
+ ...(displayMode ? { displayMode } : {}),
493
+ range: { from: from + 1, to: Math.min(to, allLines.length), totalLines },
494
+ sourceLength,
495
+ source: numbered,
496
+ hint: 'Use query to search, or noteplan_update_plugin_html to edit.',
497
+ };
498
+ }
499
+ // --- Full source mode (default) ---
500
+ if (mcpGenerated) {
501
+ return {
502
+ success: true,
503
+ pluginId,
504
+ pluginJson,
505
+ html: source,
506
+ displayMode,
507
+ mcpGenerated: true,
508
+ totalLines,
509
+ sourceLength,
510
+ hint: 'Use noteplan_update_plugin_html for targeted edits, or noteplan_create_plugin to rewrite entirely. Use query param to search within source, or startLine/endLine for partial reads.',
511
+ };
512
+ }
513
+ return {
514
+ success: true,
515
+ pluginId,
516
+ pluginJson,
517
+ scriptJs: source,
518
+ mcpGenerated: false,
519
+ totalLines,
520
+ sourceLength,
521
+ };
522
+ }
523
+ // --- HTML extraction/insertion helpers for MCP-generated plugins ---
524
+ /**
525
+ * Find the HTML template literal in an MCP-generated script.js.
526
+ * Pattern: `const html = \`...\`;` where the backtick is unescaped.
527
+ * Returns { start, end } indices of the HTML content (inside the backticks),
528
+ * or null if this is not an MCP-generated plugin.
529
+ */
530
+ function findHtmlTemplateBounds(scriptJs) {
531
+ const marker = 'const html = `';
532
+ const markerIdx = scriptJs.indexOf(marker);
533
+ if (markerIdx === -1)
534
+ return null;
535
+ const contentStart = markerIdx + marker.length;
536
+ // Find the matching unescaped closing backtick
537
+ let i = contentStart;
538
+ while (i < scriptJs.length) {
539
+ if (scriptJs[i] === '`') {
540
+ // Check if it's escaped (count preceding backslashes)
541
+ let backslashes = 0;
542
+ let j = i - 1;
543
+ while (j >= contentStart && scriptJs[j] === '\\') {
544
+ backslashes++;
545
+ j--;
546
+ }
547
+ // Unescaped if even number of preceding backslashes
548
+ if (backslashes % 2 === 0) {
549
+ return { start: contentStart, end: i };
550
+ }
551
+ }
552
+ i++;
553
+ }
554
+ return null;
555
+ }
556
+ /** Unescape HTML that was embedded in a JS template literal */
557
+ function unescapeTemplateHtml(escaped) {
558
+ // Reverse the escaping done in createPlugin:
559
+ // 1. \${ → ${ (template expression)
560
+ // 2. \` → ` (backtick)
561
+ // 3. \\ → \ (backslash — must be last)
562
+ return escaped.replace(/\\\$\{/g, '${').replace(/\\`/g, '`').replace(/\\\\/g, '\\');
563
+ }
564
+ /** Escape HTML for embedding in a JS template literal */
565
+ function escapeTemplateHtml(html) {
566
+ // Same order as createPlugin: \ → \\, ` → \`, ${ → \${
567
+ return html.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
568
+ }
569
+ /**
570
+ * When a patch find string doesn't match, try to locate a near-miss.
571
+ * Uses progressively shorter prefixes of each line to find the divergence
572
+ * point, then returns a snippet of the actual source around that location.
573
+ */
574
+ function findNearestMatch(html, findStr) {
575
+ const lines = findStr.split('\n').filter((l) => l.trim().length > 0);
576
+ if (lines.length === 0)
577
+ return null;
578
+ // Strategy 1: progressively shorter prefixes of the first non-empty line
579
+ const firstLine = lines[0].trim();
580
+ if (firstLine.length >= 10) {
581
+ // Try full line, then shrink by 5 chars each step down to 10
582
+ for (let len = firstLine.length; len >= 10; len -= 5) {
583
+ const prefix = firstLine.slice(0, len);
584
+ const idx = html.indexOf(prefix);
585
+ if (idx !== -1) {
586
+ return extractSnippet(html, idx);
587
+ }
588
+ }
589
+ }
590
+ // Strategy 2: try subsequent lines of the find string (for multi-line finds)
591
+ for (let i = 1; i < lines.length && i < 5; i++) {
592
+ const line = lines[i].trim();
593
+ if (line.length < 10)
594
+ continue;
595
+ for (let len = line.length; len >= 10; len -= 5) {
596
+ const prefix = line.slice(0, len);
597
+ const idx = html.indexOf(prefix);
598
+ if (idx !== -1) {
599
+ return extractSnippet(html, idx);
600
+ }
601
+ }
602
+ }
603
+ return null;
604
+ }
605
+ /** Extract a snippet around a character position with line numbers */
606
+ function extractSnippet(html, pos) {
607
+ const allLines = html.split('\n');
608
+ // Find which line the position falls on
609
+ let charCount = 0;
610
+ let targetLine = 0;
611
+ for (let i = 0; i < allLines.length; i++) {
612
+ charCount += allLines[i].length + 1; // +1 for \n
613
+ if (charCount > pos) {
614
+ targetLine = i;
615
+ break;
616
+ }
617
+ }
618
+ // Show a few lines around the target
619
+ const ctxLines = 3;
620
+ const from = Math.max(0, targetLine - ctxLines);
621
+ const to = Math.min(allLines.length, targetLine + ctxLines + 1);
622
+ const snippet = allLines
623
+ .slice(from, to)
624
+ .map((line, i) => {
625
+ const lineNum = from + i + 1;
626
+ const marker = from + i === targetLine ? '>' : ' ';
627
+ const truncated = line.length > 120 ? line.slice(0, 117) + '...' : line;
628
+ return `${marker} ${lineNum}\t${truncated}`;
629
+ })
630
+ .join('\n');
631
+ return snippet;
632
+ }
633
+ export function updatePluginHtml(args) {
634
+ const { pluginId, patches, autoLaunch } = updatePluginHtmlSchema.parse(args);
635
+ const idError = validatePluginId(pluginId);
636
+ if (idError) {
637
+ return { success: false, error: idError };
638
+ }
639
+ const pluginsPath = getPluginsPath();
640
+ const pluginDir = path.join(pluginsPath, pluginId);
641
+ if (!fs.existsSync(pluginDir)) {
642
+ return { success: false, error: `Plugin "${pluginId}" not found` };
643
+ }
644
+ const scriptPath = path.join(pluginDir, 'script.js');
645
+ if (!fs.existsSync(scriptPath)) {
646
+ return { success: false, error: `script.js not found in plugin folder "${pluginId}"` };
647
+ }
648
+ const scriptJs = fs.readFileSync(scriptPath, 'utf-8');
649
+ const bounds = findHtmlTemplateBounds(scriptJs);
650
+ if (!bounds) {
651
+ return {
652
+ success: false,
653
+ error: 'This plugin was not generated by MCP (no `const html = \\`...\\`` pattern found). Use noteplan_get_plugin_source to read the full source and noteplan_create_plugin to rewrite it.',
654
+ };
655
+ }
656
+ // Extract and unescape the HTML
657
+ const escapedHtml = scriptJs.slice(bounds.start, bounds.end);
658
+ let html = unescapeTemplateHtml(escapedHtml);
659
+ // Apply patches sequentially
660
+ const patchResults = [];
661
+ let appliedCount = 0;
662
+ for (const patch of patches) {
663
+ const idx = html.indexOf(patch.find);
664
+ const truncatedFind = patch.find.length > 80 ? patch.find.slice(0, 77) + '...' : patch.find;
665
+ if (idx === -1) {
666
+ const result = { find: truncatedFind, applied: false };
667
+ // Near-miss: try to find where the first line of the find string appears
668
+ const nearMatch = findNearestMatch(html, patch.find);
669
+ if (nearMatch) {
670
+ result.nearestMatch = nearMatch;
671
+ }
672
+ patchResults.push(result);
673
+ continue;
674
+ }
675
+ html = html.slice(0, idx) + patch.replace + html.slice(idx + patch.find.length);
676
+ appliedCount++;
677
+ patchResults.push({ find: truncatedFind, applied: true });
678
+ }
679
+ if (appliedCount === 0) {
680
+ return {
681
+ success: false,
682
+ error: 'No patches matched. Check that your find strings exactly match the current HTML content.',
683
+ patches: patchResults,
684
+ };
685
+ }
686
+ // Re-escape and write back
687
+ const newEscapedHtml = escapeTemplateHtml(html);
688
+ const newScriptJs = scriptJs.slice(0, bounds.start) + newEscapedHtml + scriptJs.slice(bounds.end);
689
+ fs.writeFileSync(scriptPath, newScriptJs, 'utf-8');
690
+ const result = {
691
+ success: true,
692
+ appliedCount,
693
+ totalPatches: patches.length,
694
+ patches: patchResults,
695
+ htmlLength: html.length,
696
+ hint: 'Call noteplan_get_plugin_log to check for errors.',
697
+ ...(appliedCount < patches.length
698
+ ? { warning: `Only ${appliedCount} of ${patches.length} patches applied. Non-matching patches were skipped.` }
699
+ : {}),
700
+ };
701
+ if (autoLaunch) {
702
+ try {
703
+ reloadPlugins({});
704
+ execFileSync('sleep', ['1'], { timeout: 5000 });
705
+ // Read command name from plugin.json to run the plugin
706
+ const manifestPath = path.join(pluginDir, 'plugin.json');
707
+ if (fs.existsSync(manifestPath)) {
708
+ try {
709
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
710
+ const commands = manifest['plugin.commands'];
711
+ if (Array.isArray(commands) && commands.length > 0 && commands[0].name) {
712
+ runPlugin({ pluginId, command: commands[0].name });
713
+ }
714
+ }
715
+ catch {
716
+ // Non-fatal — plugin files were already updated
717
+ }
718
+ }
719
+ result.launched = true;
720
+ }
721
+ catch (error) {
722
+ result.launched = false;
723
+ result.launchError = error.message;
724
+ }
725
+ }
726
+ return result;
727
+ }
728
+ export function screenshotPlugin(args) {
729
+ const { pluginId } = screenshotPluginSchema.parse(args);
730
+ const idError = validatePluginId(pluginId);
731
+ if (idError) {
732
+ return { success: false, error: idError };
733
+ }
734
+ const pluginsPath = getPluginsPath();
735
+ const pluginDir = path.join(pluginsPath, pluginId);
736
+ if (!fs.existsSync(pluginDir)) {
737
+ return { success: false, error: `Plugin "${pluginId}" not found` };
738
+ }
739
+ const screenshotPath = path.join(pluginDir, '_screenshot.png');
740
+ // Clean up any stale screenshot from a previous call
741
+ if (fs.existsSync(screenshotPath)) {
742
+ fs.unlinkSync(screenshotPath);
743
+ }
744
+ // Ask NotePlan to capture the plugin's WebView
745
+ let appleScriptResult = '';
746
+ try {
747
+ const script = `tell application "${APP_NAME}" to screenshotPlugin with id "${escapeAppleScript(pluginId)}"`;
748
+ appleScriptResult = runAppleScript(script, 15_000);
749
+ }
750
+ catch (e) {
751
+ return { success: false, error: `AppleScript error: ${e.message}` };
752
+ }
753
+ // If AppleScript returned true, file should already exist (synchronous RunLoop wait).
754
+ // If false, still check briefly in case of timing edge cases.
755
+ if (!fs.existsSync(screenshotPath)) {
756
+ if (appleScriptResult === 'false') {
757
+ return { success: false, error: 'Failed to capture screenshot. Is the plugin view open and visible?' };
758
+ }
759
+ // Brief poll in case of slight delay
760
+ const maxWait = 3000;
761
+ let waited = 0;
762
+ while (!fs.existsSync(screenshotPath) && waited < maxWait) {
763
+ execFileSync('sleep', ['0.2'], { timeout: 3000 });
764
+ waited += 200;
765
+ }
766
+ }
767
+ if (!fs.existsSync(screenshotPath)) {
768
+ return { success: false, error: 'Screenshot file not created. Is the plugin view visible?' };
769
+ }
770
+ // Read the image and encode as base64
771
+ const imageBuffer = fs.readFileSync(screenshotPath);
772
+ const base64Data = imageBuffer.toString('base64');
773
+ // Clean up
774
+ fs.unlinkSync(screenshotPath);
775
+ return {
776
+ success: true,
777
+ pluginId,
778
+ _imageData: base64Data,
779
+ _imageMimeType: 'image/png',
780
+ };
781
+ }
782
+ //# sourceMappingURL=plugins.js.map