roam-research-mcp 0.19.0 → 0.22.0

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.
@@ -1,1168 +1,81 @@
1
- import { q, createPage, createBlock, batchActions, updateBlock } from '@roam-research/roam-api-sdk';
2
- import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
- import { formatRoamDate } from '../utils/helpers.js';
4
- import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown, hasMarkdownTable } from '../markdown-utils.js';
5
- // Helper function to capitalize each word
6
- const capitalizeWords = (str) => {
7
- return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
8
- };
9
- // Helper function to collect all referenced block UIDs from text
10
- const collectRefs = (text, depth = 0, refs = new Set()) => {
11
- if (depth >= 4)
12
- return refs; // Max recursion depth
13
- const refRegex = /\(\(([a-zA-Z0-9_-]+)\)\)/g;
14
- let match;
15
- while ((match = refRegex.exec(text)) !== null) {
16
- const [_, uid] = match;
17
- refs.add(uid);
18
- }
19
- return refs;
20
- };
21
- // Helper function to resolve block references
22
- const resolveRefs = async (graph, text, depth = 0) => {
23
- if (depth >= 4)
24
- return text; // Max recursion depth
25
- const refs = collectRefs(text, depth);
26
- if (refs.size === 0)
27
- return text;
28
- // Get referenced block contents
29
- const refQuery = `[:find ?uid ?string
30
- :in $ [?uid ...]
31
- :where [?b :block/uid ?uid]
32
- [?b :block/string ?string]]`;
33
- const refResults = await q(graph, refQuery, [Array.from(refs)]);
34
- // Create lookup map of uid -> string
35
- const refMap = new Map();
36
- refResults.forEach(([uid, string]) => {
37
- refMap.set(uid, string);
38
- });
39
- // Replace references with their content
40
- let resolvedText = text;
41
- for (const uid of refs) {
42
- const refContent = refMap.get(uid);
43
- if (refContent) {
44
- // Recursively resolve nested references
45
- const resolvedContent = await resolveRefs(graph, refContent, depth + 1);
46
- resolvedText = resolvedText.replace(new RegExp(`\\(\\(${uid}\\)\\)`, 'g'), resolvedContent);
47
- }
48
- }
49
- return resolvedText;
50
- };
1
+ import { PageOperations } from './operations/pages.js';
2
+ import { BlockOperations } from './operations/blocks.js';
3
+ import { SearchOperations } from './operations/search/index.js';
4
+ import { MemoryOperations } from './operations/memory.js';
5
+ import { TodoOperations } from './operations/todos.js';
6
+ import { OutlineOperations } from './operations/outline.js';
51
7
  export class ToolHandlers {
52
8
  graph;
9
+ pageOps;
10
+ blockOps;
11
+ searchOps;
12
+ memoryOps;
13
+ todoOps;
14
+ outlineOps;
53
15
  constructor(graph) {
54
16
  this.graph = graph;
17
+ this.pageOps = new PageOperations(graph);
18
+ this.blockOps = new BlockOperations(graph);
19
+ this.searchOps = new SearchOperations(graph);
20
+ this.memoryOps = new MemoryOperations(graph);
21
+ this.todoOps = new TodoOperations(graph);
22
+ this.outlineOps = new OutlineOperations(graph);
55
23
  }
24
+ // Page Operations
56
25
  async findPagesModifiedToday() {
57
- // Define ancestor rule for traversing block hierarchy
58
- const ancestorRule = `[
59
- [ (ancestor ?b ?a)
60
- [?a :block/children ?b] ]
61
- [ (ancestor ?b ?a)
62
- [?parent :block/children ?b]
63
- (ancestor ?parent ?a) ]
64
- ]`;
65
- // Get start of today
66
- const startOfDay = new Date();
67
- startOfDay.setHours(0, 0, 0, 0);
68
- try {
69
- // Query for pages modified today
70
- const results = await q(this.graph, `[:find ?title
71
- :in $ ?start_of_day %
72
- :where
73
- [?page :node/title ?title]
74
- (ancestor ?block ?page)
75
- [?block :edit/time ?time]
76
- [(> ?time ?start_of_day)]]`, [startOfDay.getTime(), ancestorRule]);
77
- if (!results || results.length === 0) {
78
- return {
79
- success: true,
80
- pages: [],
81
- message: 'No pages have been modified today'
82
- };
83
- }
84
- // Extract unique page titles
85
- const uniquePages = [...new Set(results.map(([title]) => title))];
86
- return {
87
- success: true,
88
- pages: uniquePages,
89
- message: `Found ${uniquePages.length} page(s) modified today`
90
- };
91
- }
92
- catch (error) {
93
- throw new McpError(ErrorCode.InternalError, `Failed to find modified pages: ${error.message}`);
94
- }
26
+ return this.pageOps.findPagesModifiedToday();
95
27
  }
96
- async createOutline(outline, page_title_uid, block_text_uid) {
97
- // Validate input
98
- if (!Array.isArray(outline) || outline.length === 0) {
99
- throw new McpError(ErrorCode.InvalidRequest, 'outline must be a non-empty array');
100
- }
101
- // Filter out items with undefined text
102
- const validOutline = outline.filter(item => item.text !== undefined);
103
- if (validOutline.length === 0) {
104
- throw new McpError(ErrorCode.InvalidRequest, 'outline must contain at least one item with text');
105
- }
106
- // Validate outline structure
107
- const invalidItems = validOutline.filter(item => typeof item.level !== 'number' ||
108
- item.level < 1 ||
109
- item.level > 10 ||
110
- typeof item.text !== 'string' ||
111
- item.text.trim().length === 0);
112
- if (invalidItems.length > 0) {
113
- throw new McpError(ErrorCode.InvalidRequest, 'outline contains invalid items - each item must have a level (1-10) and non-empty text');
114
- }
115
- // Helper function to find or create page with retries
116
- const findOrCreatePage = async (titleOrUid, maxRetries = 3, delayMs = 500) => {
117
- // First try to find by title
118
- const titleQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
119
- const variations = [
120
- titleOrUid, // Original
121
- capitalizeWords(titleOrUid), // Each word capitalized
122
- titleOrUid.toLowerCase() // All lowercase
123
- ];
124
- for (let retry = 0; retry < maxRetries; retry++) {
125
- // Try each case variation
126
- for (const variation of variations) {
127
- const findResults = await q(this.graph, titleQuery, [variation]);
128
- if (findResults && findResults.length > 0) {
129
- return findResults[0][0];
130
- }
131
- }
132
- // If not found as title, try as UID
133
- const uidQuery = `[:find ?uid
134
- :where [?e :block/uid "${titleOrUid}"]
135
- [?e :block/uid ?uid]]`;
136
- const uidResult = await q(this.graph, uidQuery, []);
137
- if (uidResult && uidResult.length > 0) {
138
- return uidResult[0][0];
139
- }
140
- // If still not found and this is the first retry, try to create the page
141
- if (retry === 0) {
142
- const success = await createPage(this.graph, {
143
- action: 'create-page',
144
- page: { title: titleOrUid }
145
- });
146
- // Even if createPage returns false, the page might still have been created
147
- // Wait a bit and continue to next retry
148
- await new Promise(resolve => setTimeout(resolve, delayMs));
149
- continue;
150
- }
151
- if (retry < maxRetries - 1) {
152
- await new Promise(resolve => setTimeout(resolve, delayMs));
153
- }
154
- }
155
- throw new McpError(ErrorCode.InvalidRequest, `Failed to find or create page "${titleOrUid}" after multiple attempts`);
156
- };
157
- // Get or create the target page
158
- const targetPageUid = await findOrCreatePage(page_title_uid || formatRoamDate(new Date()));
159
- // Helper function to find block with improved relationship checks
160
- const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000, case_sensitive = false) => {
161
- // Try multiple query strategies
162
- const queries = [
163
- // Strategy 1: Direct page and string match
164
- `[:find ?b-uid ?order
165
- :where [?p :block/uid "${pageUid}"]
166
- [?b :block/page ?p]
167
- [?b :block/string ?block-str]
168
- [(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
169
- [?b :block/order ?order]
170
- [?b :block/uid ?b-uid]]`,
171
- // Strategy 2: Parent-child relationship
172
- `[:find ?b-uid ?order
173
- :where [?p :block/uid "${pageUid}"]
174
- [?b :block/parents ?p]
175
- [?b :block/string ?block-str]
176
- [(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
177
- [?b :block/order ?order]
178
- [?b :block/uid ?b-uid]]`,
179
- // Strategy 3: Broader page relationship
180
- `[:find ?b-uid ?order
181
- :where [?p :block/uid "${pageUid}"]
182
- [?b :block/page ?page]
183
- [?p :block/page ?page]
184
- [?b :block/string ?block-str]
185
- [(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
186
- [?b :block/order ?order]
187
- [?b :block/uid ?b-uid]]`
188
- ];
189
- for (let retry = 0; retry < maxRetries; retry++) {
190
- // Try each query strategy
191
- for (const queryStr of queries) {
192
- const blockResults = await q(this.graph, queryStr, []);
193
- if (blockResults && blockResults.length > 0) {
194
- // Use the most recently created block
195
- const sorted = blockResults.sort((a, b) => b[1] - a[1]);
196
- return sorted[0][0];
197
- }
198
- }
199
- // Exponential backoff
200
- const delay = initialDelay * Math.pow(2, retry);
201
- await new Promise(resolve => setTimeout(resolve, delay));
202
- console.log(`Retry ${retry + 1}/${maxRetries} finding block "${blockString}" under "${pageUid}"`);
203
- }
204
- throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
205
- };
206
- // Helper function to create and verify block with improved error handling
207
- const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false, case_sensitive = false) => {
208
- try {
209
- // Initial delay before any operations
210
- if (!isRetry) {
211
- await new Promise(resolve => setTimeout(resolve, initialDelay));
212
- }
213
- for (let retry = 0; retry < maxRetries; retry++) {
214
- console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
215
- // Create block
216
- const success = await createBlock(this.graph, {
217
- action: 'create-block',
218
- location: {
219
- 'parent-uid': parentUid,
220
- order: 'last'
221
- },
222
- block: { string: content }
223
- });
224
- // Wait with exponential backoff
225
- const delay = initialDelay * Math.pow(2, retry);
226
- await new Promise(resolve => setTimeout(resolve, delay));
227
- try {
228
- // Try to find the block using our improved findBlockWithRetry
229
- return await findBlockWithRetry(parentUid, content, maxRetries, initialDelay, case_sensitive);
230
- }
231
- catch (error) {
232
- const errorMessage = error instanceof Error ? error.message : String(error);
233
- console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`);
234
- if (retry === maxRetries - 1)
235
- throw error;
236
- }
237
- }
238
- throw new McpError(ErrorCode.InternalError, `Failed to create and verify block "${content}" after ${maxRetries} attempts`);
239
- }
240
- catch (error) {
241
- // If this is already a retry, throw the error
242
- if (isRetry)
243
- throw error;
244
- // Otherwise, try one more time with a clean slate
245
- console.log(`Retrying block creation for "${content}" with fresh attempt`);
246
- await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
247
- return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true, case_sensitive);
248
- }
249
- };
250
- // Get or create the parent block
251
- let targetParentUid;
252
- if (!block_text_uid) {
253
- targetParentUid = targetPageUid;
254
- }
255
- else {
256
- try {
257
- // Create header block and get its UID
258
- targetParentUid = await createAndVerifyBlock(block_text_uid, targetPageUid);
259
- }
260
- catch (error) {
261
- const errorMessage = error instanceof Error ? error.message : String(error);
262
- throw new McpError(ErrorCode.InternalError, `Failed to create header block "${block_text_uid}": ${errorMessage}`);
263
- }
264
- }
265
- // Initialize result variable
266
- let result;
267
- try {
268
- // Validate level sequence
269
- let prevLevel = 0;
270
- for (const item of validOutline) {
271
- // Level should not increase by more than 1 at a time
272
- if (item.level > prevLevel + 1) {
273
- throw new McpError(ErrorCode.InvalidRequest, `Invalid outline structure - level ${item.level} follows level ${prevLevel}`);
274
- }
275
- prevLevel = item.level;
276
- }
277
- // Convert outline items to markdown-like structure
278
- const markdownContent = validOutline
279
- .map(item => {
280
- const indent = ' '.repeat(item.level - 1);
281
- return `${indent}- ${item.text?.trim()}`;
282
- })
283
- .join('\n');
284
- // Convert to Roam markdown format
285
- const convertedContent = convertToRoamMarkdown(markdownContent);
286
- // Parse markdown into hierarchical structure
287
- const nodes = parseMarkdown(convertedContent);
288
- // Convert nodes to batch actions
289
- const actions = convertToRoamActions(nodes, targetParentUid, 'last');
290
- if (actions.length === 0) {
291
- throw new McpError(ErrorCode.InvalidRequest, 'No valid actions generated from outline');
292
- }
293
- // Execute batch actions to create the outline
294
- result = await batchActions(this.graph, {
295
- action: 'batch-actions',
296
- actions
297
- }).catch(error => {
298
- throw new McpError(ErrorCode.InternalError, `Failed to create outline blocks: ${error.message}`);
299
- });
300
- if (!result) {
301
- throw new McpError(ErrorCode.InternalError, 'Failed to create outline blocks - no result returned');
302
- }
303
- }
304
- catch (error) {
305
- if (error instanceof McpError)
306
- throw error;
307
- throw new McpError(ErrorCode.InternalError, `Failed to create outline: ${error.message}`);
308
- }
309
- // Get the created block UIDs
310
- const createdUids = result?.created_uids || [];
311
- return {
312
- success: true,
313
- page_uid: targetPageUid,
314
- parent_uid: targetParentUid,
315
- created_uids: createdUids
316
- };
28
+ async createPage(title, content) {
29
+ return this.pageOps.createPage(title, content);
317
30
  }
318
31
  async fetchPageByTitle(title) {
319
- if (!title) {
320
- throw new McpError(ErrorCode.InvalidRequest, 'title is required');
321
- }
322
- // Try different case variations
323
- const variations = [
324
- title, // Original
325
- capitalizeWords(title), // Each word capitalized
326
- title.toLowerCase() // All lowercase
327
- ];
328
- let uid = null;
329
- for (const variation of variations) {
330
- const searchQuery = `[:find ?uid .
331
- :where [?e :node/title "${variation}"]
332
- [?e :block/uid ?uid]]`;
333
- const result = await q(this.graph, searchQuery, []);
334
- uid = (result === null || result === undefined) ? null : String(result);
335
- if (uid)
336
- break;
337
- }
338
- if (!uid) {
339
- throw new McpError(ErrorCode.InvalidRequest, `Page with title "${title}" not found (tried original, capitalized words, and lowercase)`);
340
- }
341
- // Define ancestor rule for traversing block hierarchy
342
- const ancestorRule = `[
343
- [ (ancestor ?b ?a)
344
- [?a :block/children ?b] ]
345
- [ (ancestor ?b ?a)
346
- [?parent :block/children ?b]
347
- (ancestor ?parent ?a) ]
348
- ]`;
349
- // Get all blocks under this page using ancestor rule
350
- const blocksQuery = `[:find ?block-uid ?block-str ?order ?parent-uid
351
- :in $ % ?page-title
352
- :where [?page :node/title ?page-title]
353
- [?block :block/string ?block-str]
354
- [?block :block/uid ?block-uid]
355
- [?block :block/order ?order]
356
- (ancestor ?block ?page)
357
- [?parent :block/children ?block]
358
- [?parent :block/uid ?parent-uid]]`;
359
- const blocks = await q(this.graph, blocksQuery, [ancestorRule, title]);
360
- if (!blocks || blocks.length === 0) {
361
- return `${title} (no content found)`;
362
- }
363
- // Create a map of all blocks
364
- const blockMap = new Map();
365
- const rootBlocks = [];
366
- // First pass: Create all block objects
367
- for (const [blockUid, blockStr, order, parentUid] of blocks) {
368
- const resolvedString = await resolveRefs(this.graph, blockStr);
369
- const block = {
370
- uid: blockUid,
371
- string: resolvedString,
372
- order: order,
373
- children: []
374
- };
375
- blockMap.set(blockUid, block);
376
- // If no parent or parent is the page itself, it's a root block
377
- if (!parentUid || parentUid === uid) {
378
- rootBlocks.push(block);
379
- }
380
- }
381
- // Second pass: Build parent-child relationships
382
- for (const [blockUid, _, __, parentUid] of blocks) {
383
- if (parentUid && parentUid !== uid) {
384
- const child = blockMap.get(blockUid);
385
- const parent = blockMap.get(parentUid);
386
- if (child && parent && !parent.children.includes(child)) {
387
- parent.children.push(child);
388
- }
389
- }
390
- }
391
- // Sort blocks recursively
392
- const sortBlocks = (blocks) => {
393
- blocks.sort((a, b) => a.order - b.order);
394
- blocks.forEach(block => {
395
- if (block.children.length > 0) {
396
- sortBlocks(block.children);
397
- }
398
- });
399
- };
400
- sortBlocks(rootBlocks);
401
- // Convert to markdown with proper nesting
402
- const toMarkdown = (blocks, level = 0) => {
403
- return blocks.map(block => {
404
- const indent = ' '.repeat(level);
405
- let md = `${indent}- ${block.string}`;
406
- if (block.children.length > 0) {
407
- md += '\n' + toMarkdown(block.children, level + 1);
408
- }
409
- return md;
410
- }).join('\n');
411
- };
412
- return `# ${title}\n\n${toMarkdown(rootBlocks)}`;
413
- }
414
- async createPage(title, content) {
415
- // Ensure title is properly formatted
416
- const pageTitle = String(title).trim();
417
- // First try to find if the page exists
418
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
419
- const findResults = await q(this.graph, findQuery, [pageTitle]);
420
- let pageUid;
421
- if (findResults && findResults.length > 0) {
422
- // Page exists, use its UID
423
- pageUid = findResults[0][0];
424
- }
425
- else {
426
- // Create new page
427
- const success = await createPage(this.graph, {
428
- action: 'create-page',
429
- page: {
430
- title: pageTitle
431
- }
432
- });
433
- if (!success) {
434
- throw new Error('Failed to create page');
435
- }
436
- // Get the new page's UID
437
- const results = await q(this.graph, findQuery, [pageTitle]);
438
- if (!results || results.length === 0) {
439
- throw new Error('Could not find created page');
440
- }
441
- pageUid = results[0][0];
442
- }
443
- // If content is provided, check if it looks like nested markdown
444
- if (content) {
445
- const isMultilined = content.includes('\n') || hasMarkdownTable(content);
446
- if (isMultilined) {
447
- // Use import_nested_markdown functionality
448
- const convertedContent = convertToRoamMarkdown(content);
449
- const nodes = parseMarkdown(convertedContent);
450
- const actions = convertToRoamActions(nodes, pageUid, 'last');
451
- const result = await batchActions(this.graph, {
452
- action: 'batch-actions',
453
- actions
454
- });
455
- if (!result) {
456
- throw new Error('Failed to import nested markdown content');
457
- }
458
- }
459
- else {
460
- // Create a simple block for non-nested content
461
- const blockSuccess = await createBlock(this.graph, {
462
- action: 'create-block',
463
- location: {
464
- "parent-uid": pageUid,
465
- "order": "last"
466
- },
467
- block: { string: content }
468
- });
469
- if (!blockSuccess) {
470
- throw new Error('Failed to create content block');
471
- }
472
- }
473
- }
474
- return { success: true, uid: pageUid };
32
+ return this.pageOps.fetchPageByTitle(title);
475
33
  }
34
+ // Block Operations
476
35
  async createBlock(content, page_uid, title) {
477
- // If page_uid provided, use it directly
478
- let targetPageUid = page_uid;
479
- // If no page_uid but title provided, search for page by title
480
- if (!targetPageUid && title) {
481
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
482
- const findResults = await q(this.graph, findQuery, [title]);
483
- if (findResults && findResults.length > 0) {
484
- targetPageUid = findResults[0][0];
485
- }
486
- else {
487
- // Create page with provided title if it doesn't exist
488
- const success = await createPage(this.graph, {
489
- action: 'create-page',
490
- page: { title }
491
- });
492
- if (!success) {
493
- throw new Error('Failed to create page with provided title');
494
- }
495
- // Get the new page's UID
496
- const results = await q(this.graph, findQuery, [title]);
497
- if (!results || results.length === 0) {
498
- throw new Error('Could not find created page');
499
- }
500
- targetPageUid = results[0][0];
501
- }
502
- }
503
- // If neither page_uid nor title provided, use today's date page
504
- if (!targetPageUid) {
505
- const today = new Date();
506
- const dateStr = formatRoamDate(today);
507
- // Try to find today's page
508
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
509
- const findResults = await q(this.graph, findQuery, [dateStr]);
510
- if (findResults && findResults.length > 0) {
511
- targetPageUid = findResults[0][0];
512
- }
513
- else {
514
- // Create today's page if it doesn't exist
515
- const success = await createPage(this.graph, {
516
- action: 'create-page',
517
- page: { title: dateStr }
518
- });
519
- if (!success) {
520
- throw new Error('Failed to create today\'s page');
521
- }
522
- // Get the new page's UID
523
- const results = await q(this.graph, findQuery, [dateStr]);
524
- if (!results || results.length === 0) {
525
- throw new Error('Could not find created today\'s page');
526
- }
527
- targetPageUid = results[0][0];
528
- }
529
- }
530
- // If the content has multiple lines or is a table, use nested import
531
- if (content.includes('\n')) {
532
- // Parse and import the nested content
533
- const convertedContent = convertToRoamMarkdown(content);
534
- const nodes = parseMarkdown(convertedContent);
535
- const actions = convertToRoamActions(nodes, targetPageUid, 'last');
536
- // Execute batch actions to create the nested structure
537
- const result = await batchActions(this.graph, {
538
- action: 'batch-actions',
539
- actions
540
- });
541
- if (!result) {
542
- throw new Error('Failed to create nested blocks');
543
- }
544
- const blockUid = result.created_uids?.[0];
545
- return {
546
- success: true,
547
- block_uid: blockUid,
548
- parent_uid: targetPageUid
549
- };
550
- }
551
- else {
552
- // For non-table content, create a simple block
553
- const result = await createBlock(this.graph, {
554
- action: 'create-block',
555
- location: {
556
- "parent-uid": targetPageUid,
557
- "order": "last"
558
- },
559
- block: { string: content }
560
- });
561
- if (!result) {
562
- throw new Error('Failed to create block');
563
- }
564
- // Get the block's UID
565
- const findBlockQuery = `[:find ?uid
566
- :in $ ?parent ?string
567
- :where [?b :block/uid ?uid]
568
- [?b :block/string ?string]
569
- [?b :block/parents ?p]
570
- [?p :block/uid ?parent]]`;
571
- const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, content]);
572
- if (!blockResults || blockResults.length === 0) {
573
- throw new Error('Could not find created block');
574
- }
575
- const blockUid = blockResults[0][0];
576
- return {
577
- success: true,
578
- block_uid: blockUid,
579
- parent_uid: targetPageUid
580
- };
581
- }
582
- }
583
- async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'first') {
584
- // First get the page UID
585
- let targetPageUid = page_uid;
586
- if (!targetPageUid && page_title) {
587
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
588
- const findResults = await q(this.graph, findQuery, [page_title]);
589
- if (findResults && findResults.length > 0) {
590
- targetPageUid = findResults[0][0];
591
- }
592
- else {
593
- throw new McpError(ErrorCode.InvalidRequest, `Page with title "${page_title}" not found`);
594
- }
595
- }
596
- // If no page specified, use today's date page
597
- if (!targetPageUid) {
598
- const today = new Date();
599
- const dateStr = formatRoamDate(today);
600
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
601
- const findResults = await q(this.graph, findQuery, [dateStr]);
602
- if (findResults && findResults.length > 0) {
603
- targetPageUid = findResults[0][0];
604
- }
605
- else {
606
- // Create today's page
607
- const success = await createPage(this.graph, {
608
- action: 'create-page',
609
- page: { title: dateStr }
610
- });
611
- if (!success) {
612
- throw new McpError(ErrorCode.InternalError, 'Failed to create today\'s page');
613
- }
614
- const results = await q(this.graph, findQuery, [dateStr]);
615
- if (!results || results.length === 0) {
616
- throw new McpError(ErrorCode.InternalError, 'Could not find created today\'s page');
617
- }
618
- targetPageUid = results[0][0];
619
- }
620
- }
621
- // Now get the parent block UID
622
- let targetParentUid = parent_uid;
623
- if (!targetParentUid && parent_string) {
624
- if (!targetPageUid) {
625
- throw new McpError(ErrorCode.InvalidRequest, 'Must provide either page_uid or page_title when using parent_string');
626
- }
627
- // Find block by exact string match within the page
628
- const findBlockQuery = `[:find ?uid
629
- :where [?p :block/uid "${targetPageUid}"]
630
- [?b :block/page ?p]
631
- [?b :block/string "${parent_string}"]]`;
632
- const blockResults = await q(this.graph, findBlockQuery, []);
633
- if (!blockResults || blockResults.length === 0) {
634
- throw new McpError(ErrorCode.InvalidRequest, `Block with content "${parent_string}" not found on specified page`);
635
- }
636
- targetParentUid = blockResults[0][0];
637
- }
638
- // If no parent specified, use page as parent
639
- if (!targetParentUid) {
640
- targetParentUid = targetPageUid;
641
- }
642
- // Always use parseMarkdown for content with multiple lines or any markdown formatting
643
- const isMultilined = content.includes('\n');
644
- if (isMultilined) {
645
- // Parse markdown into hierarchical structure
646
- const convertedContent = convertToRoamMarkdown(content);
647
- const nodes = parseMarkdown(convertedContent);
648
- // Convert markdown nodes to batch actions
649
- const actions = convertToRoamActions(nodes, targetParentUid, order);
650
- // Execute batch actions to add content
651
- const result = await batchActions(this.graph, {
652
- action: 'batch-actions',
653
- actions
654
- });
655
- if (!result) {
656
- throw new McpError(ErrorCode.InternalError, 'Failed to import nested markdown content');
657
- }
658
- // Get the created block UIDs
659
- const createdUids = result.created_uids || [];
660
- return {
661
- success: true,
662
- page_uid: targetPageUid,
663
- parent_uid: targetParentUid,
664
- created_uids: createdUids
665
- };
666
- }
667
- else {
668
- // Create a simple block for non-nested content
669
- const blockSuccess = await createBlock(this.graph, {
670
- action: 'create-block',
671
- location: {
672
- "parent-uid": targetParentUid,
673
- order
674
- },
675
- block: { string: content }
676
- });
677
- if (!blockSuccess) {
678
- throw new McpError(ErrorCode.InternalError, 'Failed to create content block');
679
- }
680
- return {
681
- success: true,
682
- page_uid: targetPageUid,
683
- parent_uid: targetParentUid
684
- };
685
- }
36
+ return this.blockOps.createBlock(content, page_uid, title);
686
37
  }
687
38
  async updateBlock(block_uid, content, transform) {
688
- if (!block_uid) {
689
- throw new McpError(ErrorCode.InvalidRequest, 'block_uid is required');
690
- }
691
- // Get current block content
692
- const blockQuery = `[:find ?string .
693
- :where [?b :block/uid "${block_uid}"]
694
- [?b :block/string ?string]]`;
695
- const result = await q(this.graph, blockQuery, []);
696
- if (result === null || result === undefined) {
697
- throw new McpError(ErrorCode.InvalidRequest, `Block with UID "${block_uid}" not found`);
698
- }
699
- const currentContent = String(result);
700
- if (currentContent === null || currentContent === undefined) {
701
- throw new McpError(ErrorCode.InvalidRequest, `Block with UID "${block_uid}" not found`);
702
- }
703
- // Determine new content
704
- let newContent;
705
- if (content) {
706
- newContent = content;
707
- }
708
- else if (transform) {
709
- newContent = transform(currentContent);
710
- }
711
- else {
712
- throw new McpError(ErrorCode.InvalidRequest, 'Either content or transform function must be provided');
713
- }
714
- try {
715
- const success = await updateBlock(this.graph, {
716
- action: 'update-block',
717
- block: {
718
- uid: block_uid,
719
- string: newContent
720
- }
721
- });
722
- if (!success) {
723
- throw new Error('Failed to update block');
724
- }
725
- return {
726
- success: true,
727
- content: newContent
728
- };
729
- }
730
- catch (error) {
731
- throw new McpError(ErrorCode.InternalError, `Failed to update block: ${error.message}`);
732
- }
39
+ return this.blockOps.updateBlock(block_uid, content, transform);
733
40
  }
734
41
  async updateBlocks(updates) {
735
- if (!Array.isArray(updates) || updates.length === 0) {
736
- throw new McpError(ErrorCode.InvalidRequest, 'updates must be a non-empty array');
737
- }
738
- // Validate each update has required fields
739
- updates.forEach((update, index) => {
740
- if (!update.block_uid) {
741
- throw new McpError(ErrorCode.InvalidRequest, `Update at index ${index} missing block_uid`);
742
- }
743
- if (!update.content && !update.transform) {
744
- throw new McpError(ErrorCode.InvalidRequest, `Update at index ${index} must have either content or transform`);
745
- }
746
- });
747
- // Get current content for all blocks
748
- const blockUids = updates.map(u => u.block_uid);
749
- const blockQuery = `[:find ?uid ?string
750
- :in $ [?uid ...]
751
- :where [?b :block/uid ?uid]
752
- [?b :block/string ?string]]`;
753
- const blockResults = await q(this.graph, blockQuery, [blockUids]);
754
- // Create map of uid -> current content
755
- const contentMap = new Map();
756
- blockResults.forEach(([uid, string]) => {
757
- contentMap.set(uid, string);
758
- });
759
- // Prepare batch actions
760
- const actions = [];
761
- const results = [];
762
- for (const update of updates) {
763
- try {
764
- const currentContent = contentMap.get(update.block_uid);
765
- if (!currentContent) {
766
- results.push({
767
- block_uid: update.block_uid,
768
- content: '',
769
- success: false,
770
- error: `Block with UID "${update.block_uid}" not found`
771
- });
772
- continue;
773
- }
774
- // Determine new content
775
- let newContent;
776
- if (update.content) {
777
- newContent = update.content;
778
- }
779
- else if (update.transform) {
780
- const regex = new RegExp(update.transform.find, update.transform.global ? 'g' : '');
781
- newContent = currentContent.replace(regex, update.transform.replace);
782
- }
783
- else {
784
- // This shouldn't happen due to earlier validation
785
- throw new Error('Invalid update configuration');
786
- }
787
- // Add to batch actions
788
- actions.push({
789
- action: 'update-block',
790
- block: {
791
- uid: update.block_uid,
792
- string: newContent
793
- }
794
- });
795
- results.push({
796
- block_uid: update.block_uid,
797
- content: newContent,
798
- success: true
799
- });
800
- }
801
- catch (error) {
802
- results.push({
803
- block_uid: update.block_uid,
804
- content: contentMap.get(update.block_uid) || '',
805
- success: false,
806
- error: error.message
807
- });
808
- }
809
- }
810
- // Execute batch update if we have any valid actions
811
- if (actions.length > 0) {
812
- try {
813
- const batchResult = await batchActions(this.graph, {
814
- action: 'batch-actions',
815
- actions
816
- });
817
- if (!batchResult) {
818
- throw new Error('Batch update failed');
819
- }
820
- }
821
- catch (error) {
822
- // Mark all previously successful results as failed
823
- results.forEach(result => {
824
- if (result.success) {
825
- result.success = false;
826
- result.error = `Batch update failed: ${error.message}`;
827
- }
828
- });
829
- }
830
- }
831
- return {
832
- success: results.every(r => r.success),
833
- results
834
- };
42
+ return this.blockOps.updateBlocks(updates);
43
+ }
44
+ // Search Operations
45
+ async searchByStatus(status, page_title_uid, include, exclude) {
46
+ return this.searchOps.searchByStatus(status, page_title_uid, include, exclude);
835
47
  }
836
- async searchByStatus(status, page_title_uid, include, exclude, case_sensitive = true // Changed to true to match Roam's behavior
837
- ) {
838
- // Get target page UID if provided
839
- let targetPageUid;
840
- if (page_title_uid) {
841
- // Try to find page by title or UID
842
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
843
- const findResults = await q(this.graph, findQuery, [page_title_uid]);
844
- if (findResults && findResults.length > 0) {
845
- targetPageUid = findResults[0][0];
846
- }
847
- else {
848
- // Try as UID
849
- const uidQuery = `[:find ?uid :where [?e :block/uid "${page_title_uid}"] [?e :block/uid ?uid]]`;
850
- const uidResults = await q(this.graph, uidQuery, []);
851
- if (!uidResults || uidResults.length === 0) {
852
- throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${page_title_uid}" not found`);
853
- }
854
- targetPageUid = uidResults[0][0];
855
- }
856
- }
857
- // Build query based on whether we're searching in a specific page
858
- let queryStr;
859
- let queryParams;
860
- const statusPattern = `{{[[${status}]]}}`;
861
- if (targetPageUid) {
862
- queryStr = `[:find ?block-uid ?block-str
863
- :in $ ?status-pattern ?page-uid
864
- :where [?p :block/uid ?page-uid]
865
- [?b :block/page ?p]
866
- [?b :block/string ?block-str]
867
- [?b :block/uid ?block-uid]
868
- [(clojure.string/includes? ?block-str ?status-pattern)]]`;
869
- queryParams = [statusPattern, targetPageUid];
870
- }
871
- else {
872
- queryStr = `[:find ?block-uid ?block-str ?page-title
873
- :in $ ?status-pattern
874
- :where [?b :block/string ?block-str]
875
- [?b :block/uid ?block-uid]
876
- [?b :block/page ?p]
877
- [?p :node/title ?page-title]
878
- [(clojure.string/includes? ?block-str ?status-pattern)]]`;
879
- queryParams = [statusPattern];
880
- }
881
- const results = await q(this.graph, queryStr, queryParams);
882
- if (!results || results.length === 0) {
883
- return {
884
- success: true,
885
- matches: [],
886
- message: `No blocks found with status ${status}`
887
- };
888
- }
889
- // Format initial results
890
- let matches = results.map(result => {
891
- const [uid, content, pageTitle] = result;
892
- return {
893
- block_uid: uid,
894
- content,
895
- ...(pageTitle && { page_title: pageTitle })
896
- };
897
- });
898
- // Post-query filtering with case sensitivity option
899
- if (include) {
900
- const includeTerms = include.split(',').map(term => term.trim());
901
- matches = matches.filter(match => {
902
- const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
903
- const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
904
- const terms = case_sensitive ? includeTerms : includeTerms.map(t => t.toLowerCase());
905
- return terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
906
- (matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
907
- });
908
- }
909
- if (exclude) {
910
- const excludeTerms = exclude.split(',').map(term => term.trim());
911
- matches = matches.filter(match => {
912
- const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
913
- const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
914
- const terms = case_sensitive ? excludeTerms : excludeTerms.map(t => t.toLowerCase());
915
- return !terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
916
- (matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
917
- });
918
- }
919
- return {
920
- success: true,
921
- matches,
922
- message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
923
- };
48
+ async searchForTag(primary_tag, page_title_uid, near_tag) {
49
+ return this.searchOps.searchForTag(primary_tag, page_title_uid, near_tag);
924
50
  }
925
- async searchForTag(primary_tag, page_title_uid, near_tag, case_sensitive = true // Changed to true to match Roam's behavior
926
- ) {
927
- // Ensure tags are properly formatted with #
928
- const formatTag = (tag) => tag.startsWith('#') ? tag : `#${tag}`;
929
- const primaryTagFormatted = formatTag(primary_tag);
930
- const nearTagFormatted = near_tag ? formatTag(near_tag) : undefined;
931
- // Get target page UID if provided
932
- let targetPageUid;
933
- if (page_title_uid) {
934
- // Try to find page by title or UID
935
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
936
- const findResults = await q(this.graph, findQuery, [page_title_uid]);
937
- if (findResults && findResults.length > 0) {
938
- targetPageUid = findResults[0][0];
939
- }
940
- else {
941
- // Try as UID
942
- const uidQuery = `[:find ?uid :where [?e :block/uid "${page_title_uid}"] [?e :block/uid ?uid]]`;
943
- const uidResults = await q(this.graph, uidQuery, []);
944
- if (!uidResults || uidResults.length === 0) {
945
- throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${page_title_uid}" not found`);
946
- }
947
- targetPageUid = uidResults[0][0];
948
- }
949
- }
950
- // Build query based on whether we're searching in a specific page and/or for a nearby tag
951
- let queryStr;
952
- let queryParams;
953
- if (targetPageUid) {
954
- if (nearTagFormatted) {
955
- queryStr = `[:find ?block-uid ?block-str
956
- :in $ ?primary-tag ?near-tag ?page-uid
957
- :where [?p :block/uid ?page-uid]
958
- [?b :block/page ?p]
959
- [?b :block/string ?block-str]
960
- [?b :block/uid ?block-uid]
961
- [(clojure.string/includes?
962
- ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
963
- ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
964
- [(clojure.string/includes?
965
- ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
966
- ${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
967
- queryParams = [primaryTagFormatted, nearTagFormatted, targetPageUid];
968
- }
969
- else {
970
- queryStr = `[:find ?block-uid ?block-str
971
- :in $ ?primary-tag ?page-uid
972
- :where [?p :block/uid ?page-uid]
973
- [?b :block/page ?p]
974
- [?b :block/string ?block-str]
975
- [?b :block/uid ?block-uid]
976
- [(clojure.string/includes?
977
- ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
978
- ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
979
- queryParams = [primaryTagFormatted, targetPageUid];
980
- }
981
- }
982
- else {
983
- // Search across all pages
984
- if (nearTagFormatted) {
985
- queryStr = `[:find ?block-uid ?block-str ?page-title
986
- :in $ ?primary-tag ?near-tag
987
- :where [?b :block/string ?block-str]
988
- [?b :block/uid ?block-uid]
989
- [?b :block/page ?p]
990
- [?p :node/title ?page-title]
991
- [(clojure.string/includes?
992
- ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
993
- ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
994
- [(clojure.string/includes?
995
- ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
996
- ${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
997
- queryParams = [primaryTagFormatted, nearTagFormatted];
998
- }
999
- else {
1000
- queryStr = `[:find ?block-uid ?block-str ?page-title
1001
- :in $ ?primary-tag
1002
- :where [?b :block/string ?block-str]
1003
- [?b :block/uid ?block-uid]
1004
- [?b :block/page ?p]
1005
- [?p :node/title ?page-title]
1006
- [(clojure.string/includes?
1007
- ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
1008
- ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
1009
- queryParams = [primaryTagFormatted];
1010
- }
1011
- }
1012
- const results = await q(this.graph, queryStr, queryParams);
1013
- if (!results || results.length === 0) {
1014
- return {
1015
- success: true,
1016
- matches: [],
1017
- message: `No blocks found containing ${primaryTagFormatted}${nearTagFormatted ? ` near ${nearTagFormatted}` : ''}`
1018
- };
1019
- }
1020
- // Format results
1021
- const matches = results.map(([uid, content, pageTitle]) => ({
1022
- block_uid: uid,
1023
- content,
1024
- ...(pageTitle && { page_title: pageTitle })
1025
- }));
1026
- return {
1027
- success: true,
1028
- matches,
1029
- message: `Found ${matches.length} block(s) containing ${primaryTagFormatted}${nearTagFormatted ? ` near ${nearTagFormatted}` : ''}`
1030
- };
51
+ async searchBlockRefs(params) {
52
+ return this.searchOps.searchBlockRefs(params);
53
+ }
54
+ async searchHierarchy(params) {
55
+ return this.searchOps.searchHierarchy(params);
56
+ }
57
+ async searchByText(params) {
58
+ return this.searchOps.searchByText(params);
1031
59
  }
1032
60
  async searchByDate(params) {
1033
- // Convert dates to timestamps
1034
- const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
1035
- const endTimestamp = params.end_date ? new Date(`${params.end_date}T23:59:59`).getTime() : undefined;
1036
- // Define rule for entity type
1037
- const entityRule = `[
1038
- [(block? ?e)
1039
- [?e :block/string]
1040
- [?e :block/page ?p]
1041
- [?p :node/title]]
1042
- [(page? ?e)
1043
- [?e :node/title]]
1044
- ]`;
1045
- // Build query based on cheatsheet pattern
1046
- const timeAttr = params.type === 'created' ? ':create/time' : ':edit/time';
1047
- let queryStr = `[:find ?block-uid ?string ?time ?page-title
1048
- :in $ ?start-ts ${endTimestamp ? '?end-ts' : ''}
1049
- :where
1050
- [?b ${timeAttr} ?time]
1051
- [(>= ?time ?start-ts)]
1052
- ${endTimestamp ? '[(<= ?time ?end-ts)]' : ''}
1053
- [?b :block/uid ?block-uid]
1054
- [?b :block/string ?string]
1055
- [?b :block/page ?p]
1056
- [?p :node/title ?page-title]]`;
1057
- // Execute query
1058
- const queryParams = endTimestamp ?
1059
- [startTimestamp, endTimestamp] :
1060
- [startTimestamp];
1061
- const results = await q(this.graph, queryStr, queryParams);
1062
- if (!results || results.length === 0) {
1063
- return {
1064
- success: true,
1065
- matches: [],
1066
- message: 'No matches found for the given date range and criteria'
1067
- };
1068
- }
1069
- // Process results - now we get [block-uid, string, time, page-title]
1070
- const matches = results.map(([uid, content, time, pageTitle]) => ({
1071
- uid,
1072
- type: 'block',
1073
- time,
1074
- ...(params.include_content && { content }),
1075
- page_title: pageTitle
1076
- }));
1077
- // Apply case sensitivity if content is included
1078
- if (params.include_content) {
1079
- const case_sensitive = params.case_sensitive ?? true; // Default to true to match Roam's behavior
1080
- if (!case_sensitive) {
1081
- matches.forEach(match => {
1082
- if (match.content) {
1083
- match.content = match.content.toLowerCase();
1084
- }
1085
- if (match.page_title) {
1086
- match.page_title = match.page_title.toLowerCase();
1087
- }
1088
- });
1089
- }
1090
- }
1091
- // Sort by time
1092
- const sortedMatches = matches.sort((a, b) => b.time - a.time);
1093
- return {
1094
- success: true,
1095
- matches: sortedMatches,
1096
- message: `Found ${sortedMatches.length} matches for the given date range and criteria`
1097
- };
61
+ return this.searchOps.searchByDate(params);
62
+ }
63
+ // Memory Operations
64
+ async remember(memory, categories) {
65
+ return this.memoryOps.remember(memory, categories);
66
+ }
67
+ async recall() {
68
+ return this.memoryOps.recall();
1098
69
  }
70
+ // Todo Operations
1099
71
  async addTodos(todos) {
1100
- if (!Array.isArray(todos) || todos.length === 0) {
1101
- throw new McpError(ErrorCode.InvalidRequest, 'todos must be a non-empty array');
1102
- }
1103
- // Get today's date
1104
- const today = new Date();
1105
- const dateStr = formatRoamDate(today);
1106
- // Try to find today's page
1107
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
1108
- const findResults = await q(this.graph, findQuery, [dateStr]);
1109
- let targetPageUid;
1110
- if (findResults && findResults.length > 0) {
1111
- targetPageUid = findResults[0][0];
1112
- }
1113
- else {
1114
- // Create today's page if it doesn't exist
1115
- const success = await createPage(this.graph, {
1116
- action: 'create-page',
1117
- page: { title: dateStr }
1118
- });
1119
- if (!success) {
1120
- throw new Error('Failed to create today\'s page');
1121
- }
1122
- // Get the new page's UID
1123
- const results = await q(this.graph, findQuery, [dateStr]);
1124
- if (!results || results.length === 0) {
1125
- throw new Error('Could not find created today\'s page');
1126
- }
1127
- targetPageUid = results[0][0];
1128
- }
1129
- // If more than 10 todos, use batch actions
1130
- const todo_tag = "{{TODO}}";
1131
- if (todos.length > 10) {
1132
- const actions = todos.map((todo, index) => ({
1133
- action: 'create-block',
1134
- location: {
1135
- 'parent-uid': targetPageUid,
1136
- order: index
1137
- },
1138
- block: {
1139
- string: `${todo_tag} ${todo}`
1140
- }
1141
- }));
1142
- const result = await batchActions(this.graph, {
1143
- action: 'batch-actions',
1144
- actions
1145
- });
1146
- if (!result) {
1147
- throw new Error('Failed to create todo blocks');
1148
- }
1149
- }
1150
- else {
1151
- // Create todos sequentially
1152
- for (const todo of todos) {
1153
- const success = await createBlock(this.graph, {
1154
- action: 'create-block',
1155
- location: {
1156
- "parent-uid": targetPageUid,
1157
- "order": "last"
1158
- },
1159
- block: { string: `${todo_tag} ${todo}` }
1160
- });
1161
- if (!success) {
1162
- throw new Error('Failed to create todo block');
1163
- }
1164
- }
1165
- }
1166
- return { success: true };
72
+ return this.todoOps.addTodos(todos);
73
+ }
74
+ // Outline Operations
75
+ async createOutline(outline, page_title_uid, block_text_uid) {
76
+ return this.outlineOps.createOutline(outline, page_title_uid, block_text_uid);
77
+ }
78
+ async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'first') {
79
+ return this.outlineOps.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
1167
80
  }
1168
81
  }