roam-research-mcp 2.4.0 → 2.13.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.
Files changed (53) hide show
  1. package/README.md +175 -667
  2. package/build/Roam_Markdown_Cheatsheet.md +138 -289
  3. package/build/cache/page-uid-cache.js +40 -2
  4. package/build/cli/batch/translator.js +1 -1
  5. package/build/cli/commands/batch.js +3 -8
  6. package/build/cli/commands/get.js +478 -60
  7. package/build/cli/commands/refs.js +51 -31
  8. package/build/cli/commands/save.js +61 -10
  9. package/build/cli/commands/search.js +63 -58
  10. package/build/cli/commands/status.js +3 -4
  11. package/build/cli/commands/update.js +71 -28
  12. package/build/cli/utils/graph.js +6 -2
  13. package/build/cli/utils/input.js +10 -0
  14. package/build/cli/utils/output.js +28 -5
  15. package/build/cli/utils/sort-group.js +110 -0
  16. package/build/config/graph-registry.js +31 -13
  17. package/build/config/graph-registry.test.js +42 -5
  18. package/build/markdown-utils.js +114 -4
  19. package/build/markdown-utils.test.js +125 -0
  20. package/build/query/generator.js +330 -0
  21. package/build/query/index.js +149 -0
  22. package/build/query/parser.js +319 -0
  23. package/build/query/parser.test.js +389 -0
  24. package/build/query/types.js +4 -0
  25. package/build/search/ancestor-rule.js +14 -0
  26. package/build/search/block-ref-search.js +1 -5
  27. package/build/search/hierarchy-search.js +5 -12
  28. package/build/search/index.js +1 -0
  29. package/build/search/status-search.js +10 -9
  30. package/build/search/tag-search.js +8 -24
  31. package/build/search/text-search.js +70 -27
  32. package/build/search/types.js +13 -0
  33. package/build/search/utils.js +71 -2
  34. package/build/server/roam-server.js +4 -3
  35. package/build/shared/index.js +2 -0
  36. package/build/shared/page-validator.js +233 -0
  37. package/build/shared/page-validator.test.js +128 -0
  38. package/build/shared/staged-batch.js +144 -0
  39. package/build/tools/helpers/batch-utils.js +57 -0
  40. package/build/tools/helpers/page-resolution.js +136 -0
  41. package/build/tools/helpers/refs.js +68 -0
  42. package/build/tools/operations/batch.js +75 -3
  43. package/build/tools/operations/block-retrieval.js +15 -4
  44. package/build/tools/operations/block-retrieval.test.js +87 -0
  45. package/build/tools/operations/blocks.js +1 -288
  46. package/build/tools/operations/memory.js +32 -90
  47. package/build/tools/operations/outline.js +38 -156
  48. package/build/tools/operations/pages.js +169 -122
  49. package/build/tools/operations/todos.js +5 -37
  50. package/build/tools/schemas.js +20 -9
  51. package/build/tools/tool-handlers.js +4 -4
  52. package/build/utils/helpers.js +27 -0
  53. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
- import { q, createPage, batchActions } from '@roam-research/roam-api-sdk';
1
+ import { q } from '@roam-research/roam-api-sdk';
2
2
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { formatRoamDate } from '../../utils/helpers.js';
4
- import { capitalizeWords } from '../helpers/text.js';
4
+ import { findOrCreatePage, getPageUid, getOrCreateTodayPage } from '../helpers/page-resolution.js';
5
+ import { executeBatch } from '../helpers/batch-utils.js';
5
6
  import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown } from '../../markdown-utils.js';
6
- import { pageUidCache } from '../../cache/page-uid-cache.js';
7
+ import { executeStagedBatch } from '../../shared/staged-batch.js';
7
8
  // Threshold for skipping child fetch during verification
8
9
  const VERIFICATION_THRESHOLD = 5;
9
10
  export class OutlineOperations {
@@ -55,21 +56,15 @@ export class OutlineOperations {
55
56
  }
56
57
  for (let retry = 0; retry < maxRetries; retry++) {
57
58
  console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
58
- // Create block using batchActions
59
- const batchResult = await batchActions(this.graph, {
60
- action: 'batch-actions',
61
- actions: [{
62
- action: 'create-block',
63
- location: {
64
- 'parent-uid': parentUid,
65
- order: 'last'
66
- },
67
- block: { string: content }
68
- }]
69
- });
70
- if (!batchResult) {
71
- throw new McpError(ErrorCode.InternalError, `Failed to create block "${content}" via batch action`);
72
- }
59
+ // Create block using batch action
60
+ await executeBatch(this.graph, [{
61
+ action: 'create-block',
62
+ location: {
63
+ 'parent-uid': parentUid,
64
+ order: 'last'
65
+ },
66
+ block: { string: content }
67
+ }], `create block "${content}"`);
73
68
  // Wait with exponential backoff
74
69
  const delay = initialDelay * Math.pow(2, retry);
75
70
  await new Promise(resolve => setTimeout(resolve, delay));
@@ -209,68 +204,8 @@ export class OutlineOperations {
209
204
  if (invalidItems.length > 0) {
210
205
  throw new McpError(ErrorCode.InvalidRequest, 'outline contains invalid items - each item must have a level (1-10) and non-empty text');
211
206
  }
212
- // Helper function to find or create page with retries and caching
213
- const findOrCreatePage = async (titleOrUid, maxRetries = 3, delayMs = 500) => {
214
- const variations = [
215
- titleOrUid, // Original
216
- capitalizeWords(titleOrUid), // Each word capitalized
217
- titleOrUid.toLowerCase() // All lowercase
218
- ];
219
- // Check cache first for any variation
220
- for (const variation of variations) {
221
- const cachedUid = pageUidCache.get(variation);
222
- if (cachedUid) {
223
- return cachedUid;
224
- }
225
- }
226
- const titleQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
227
- for (let retry = 0; retry < maxRetries; retry++) {
228
- // Try each case variation
229
- for (const variation of variations) {
230
- const findResults = await q(this.graph, titleQuery, [variation]);
231
- if (findResults && findResults.length > 0) {
232
- const uid = findResults[0][0];
233
- // Cache the result
234
- pageUidCache.set(titleOrUid, uid);
235
- return uid;
236
- }
237
- }
238
- // If not found as title, try as UID
239
- const uidQuery = `[:find ?uid
240
- :where [?e :block/uid "${titleOrUid}"]
241
- [?e :block/uid ?uid]]`;
242
- const uidResult = await q(this.graph, uidQuery, []);
243
- if (uidResult && uidResult.length > 0) {
244
- return uidResult[0][0];
245
- }
246
- // If still not found and this is the first retry, try to create the page
247
- if (retry === 0) {
248
- const success = await createPage(this.graph, {
249
- action: 'create-page',
250
- page: { title: titleOrUid }
251
- });
252
- // Even if createPage returns false, the page might still have been created
253
- // Wait a bit and continue to next retry
254
- await new Promise(resolve => setTimeout(resolve, delayMs));
255
- continue;
256
- }
257
- if (retry < maxRetries - 1) {
258
- await new Promise(resolve => setTimeout(resolve, delayMs));
259
- }
260
- }
261
- // One more attempt to find and cache after creation attempts
262
- for (const variation of variations) {
263
- const findResults = await q(this.graph, titleQuery, [variation]);
264
- if (findResults && findResults.length > 0) {
265
- const uid = findResults[0][0];
266
- pageUidCache.onPageCreated(titleOrUid, uid);
267
- return uid;
268
- }
269
- }
270
- throw new McpError(ErrorCode.InvalidRequest, `Failed to find or create page "${titleOrUid}" after multiple attempts`);
271
- };
272
207
  // Get or create the target page
273
- const targetPageUid = await findOrCreatePage(page_title_uid || formatRoamDate(new Date()));
208
+ const targetPageUid = await findOrCreatePage(this.graph, page_title_uid || formatRoamDate(new Date()));
274
209
  // Get or create the parent block
275
210
  let targetParentUid;
276
211
  if (!block_text_uid) {
@@ -341,21 +276,17 @@ export class OutlineOperations {
341
276
  ...(outlineItem?.children_view_type && { children_view_type: outlineItem.children_view_type })
342
277
  };
343
278
  });
344
- // Convert nodes to batch actions
279
+ // Convert nodes to batch actions (flat list)
345
280
  const actions = convertToRoamActions(nodes, targetParentUid, 'last');
346
281
  if (actions.length === 0) {
347
282
  throw new McpError(ErrorCode.InvalidRequest, 'No valid actions generated from outline');
348
283
  }
349
- // Execute batch actions to create the outline
350
- result = await batchActions(this.graph, {
351
- action: 'batch-actions',
352
- actions
353
- }).catch(error => {
354
- throw new McpError(ErrorCode.InternalError, `Failed to create outline blocks: ${error.message}`);
284
+ // Execute with staged batch to avoid race conditions
285
+ // where child blocks are created before their parent blocks exist
286
+ result = await executeStagedBatch(this.graph, actions, {
287
+ context: 'outline creation',
288
+ delayBetweenLevels: 100
355
289
  });
356
- if (!result) {
357
- throw new McpError(ErrorCode.InternalError, 'Failed to create outline blocks - no result returned');
358
- }
359
290
  }
360
291
  catch (error) {
361
292
  if (error instanceof McpError)
@@ -402,59 +333,25 @@ export class OutlineOperations {
402
333
  async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'last') {
403
334
  // First get the page UID
404
335
  let targetPageUid = page_uid;
405
- if (!targetPageUid && page_title) {
406
- // Check cache first
407
- const cachedUid = pageUidCache.get(page_title);
408
- if (cachedUid) {
409
- targetPageUid = cachedUid;
336
+ // If page_uid is provided, verify it exists
337
+ if (page_uid) {
338
+ const verifyQuery = `[:find ?uid :where [?e :block/uid "${page_uid}"] [?e :block/uid ?uid]]`;
339
+ const verifyResult = await q(this.graph, verifyQuery, []);
340
+ if (!verifyResult || verifyResult.length === 0) {
341
+ throw new McpError(ErrorCode.InvalidRequest, `Page/block with UID "${page_uid}" not found`);
410
342
  }
411
- else {
412
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
413
- const findResults = await q(this.graph, findQuery, [page_title]);
414
- if (findResults && findResults.length > 0) {
415
- targetPageUid = findResults[0][0];
416
- pageUidCache.set(page_title, targetPageUid);
417
- }
418
- else {
419
- throw new McpError(ErrorCode.InvalidRequest, `Page with title "${page_title}" not found`);
420
- }
343
+ targetPageUid = page_uid;
344
+ }
345
+ else if (page_title) {
346
+ const foundUid = await getPageUid(this.graph, page_title);
347
+ if (!foundUid) {
348
+ throw new McpError(ErrorCode.InvalidRequest, `Page with title "${page_title}" not found`);
421
349
  }
350
+ targetPageUid = foundUid;
422
351
  }
423
352
  // If no page specified, use today's date page
424
353
  if (!targetPageUid) {
425
- const today = new Date();
426
- const dateStr = formatRoamDate(today);
427
- // Check cache for today's page
428
- const cachedDailyUid = pageUidCache.get(dateStr);
429
- if (cachedDailyUid) {
430
- targetPageUid = cachedDailyUid;
431
- }
432
- else {
433
- const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
434
- const findResults = await q(this.graph, findQuery, [dateStr]);
435
- if (findResults && findResults.length > 0) {
436
- targetPageUid = findResults[0][0];
437
- pageUidCache.set(dateStr, targetPageUid);
438
- }
439
- else {
440
- // Create today's page
441
- try {
442
- await createPage(this.graph, {
443
- action: 'create-page',
444
- page: { title: dateStr }
445
- });
446
- const results = await q(this.graph, findQuery, [dateStr]);
447
- if (!results || results.length === 0) {
448
- throw new McpError(ErrorCode.InternalError, 'Could not find created today\'s page');
449
- }
450
- targetPageUid = results[0][0];
451
- pageUidCache.onPageCreated(dateStr, targetPageUid);
452
- }
453
- catch (error) {
454
- throw new McpError(ErrorCode.InternalError, `Failed to create today's page: ${error instanceof Error ? error.message : String(error)}`);
455
- }
456
- }
457
- }
354
+ targetPageUid = await getOrCreateTodayPage(this.graph);
458
355
  }
459
356
  // Now get the parent block UID
460
357
  let targetParentUid = parent_uid;
@@ -491,13 +388,7 @@ export class OutlineOperations {
491
388
  // Convert markdown nodes to batch actions
492
389
  const actions = convertToRoamActions(nodes, targetParentUid, order);
493
390
  // Execute batch actions to add content
494
- const result = await batchActions(this.graph, {
495
- action: 'batch-actions',
496
- actions
497
- });
498
- if (!result) {
499
- throw new McpError(ErrorCode.InternalError, 'Failed to import nested markdown content');
500
- }
391
+ await executeBatch(this.graph, actions, 'import nested markdown content');
501
392
  // Skip nested structure fetch for large imports to reduce API calls
502
393
  const skipNestedFetch = actions.length > VERIFICATION_THRESHOLD;
503
394
  if (skipNestedFetch) {
@@ -519,24 +410,15 @@ export class OutlineOperations {
519
410
  };
520
411
  }
521
412
  else {
522
- // Create a simple block for non-nested content using batchActions
523
- const actions = [{
413
+ // Create a simple block for non-nested content
414
+ await executeBatch(this.graph, [{
524
415
  action: 'create-block',
525
416
  location: {
526
417
  "parent-uid": targetParentUid,
527
418
  "order": order
528
419
  },
529
420
  block: { string: content }
530
- }];
531
- try {
532
- await batchActions(this.graph, {
533
- action: 'batch-actions',
534
- actions
535
- });
536
- }
537
- catch (error) {
538
- throw new McpError(ErrorCode.InternalError, `Failed to create content block: ${error instanceof Error ? error.message : String(error)}`);
539
- }
421
+ }], 'create content block');
540
422
  // For single-line content, we still need to fetch the UID and construct a NestedBlock
541
423
  const createdUids = [];
542
424
  try {