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.
- package/README.md +175 -667
- package/build/Roam_Markdown_Cheatsheet.md +138 -289
- package/build/cache/page-uid-cache.js +40 -2
- package/build/cli/batch/translator.js +1 -1
- package/build/cli/commands/batch.js +3 -8
- package/build/cli/commands/get.js +478 -60
- package/build/cli/commands/refs.js +51 -31
- package/build/cli/commands/save.js +61 -10
- package/build/cli/commands/search.js +63 -58
- package/build/cli/commands/status.js +3 -4
- package/build/cli/commands/update.js +71 -28
- package/build/cli/utils/graph.js +6 -2
- package/build/cli/utils/input.js +10 -0
- package/build/cli/utils/output.js +28 -5
- package/build/cli/utils/sort-group.js +110 -0
- package/build/config/graph-registry.js +31 -13
- package/build/config/graph-registry.test.js +42 -5
- package/build/markdown-utils.js +114 -4
- package/build/markdown-utils.test.js +125 -0
- package/build/query/generator.js +330 -0
- package/build/query/index.js +149 -0
- package/build/query/parser.js +319 -0
- package/build/query/parser.test.js +389 -0
- package/build/query/types.js +4 -0
- package/build/search/ancestor-rule.js +14 -0
- package/build/search/block-ref-search.js +1 -5
- package/build/search/hierarchy-search.js +5 -12
- package/build/search/index.js +1 -0
- package/build/search/status-search.js +10 -9
- package/build/search/tag-search.js +8 -24
- package/build/search/text-search.js +70 -27
- package/build/search/types.js +13 -0
- package/build/search/utils.js +71 -2
- package/build/server/roam-server.js +4 -3
- package/build/shared/index.js +2 -0
- package/build/shared/page-validator.js +233 -0
- package/build/shared/page-validator.test.js +128 -0
- package/build/shared/staged-batch.js +144 -0
- package/build/tools/helpers/batch-utils.js +57 -0
- package/build/tools/helpers/page-resolution.js +136 -0
- package/build/tools/helpers/refs.js +68 -0
- package/build/tools/operations/batch.js +75 -3
- package/build/tools/operations/block-retrieval.js +15 -4
- package/build/tools/operations/block-retrieval.test.js +87 -0
- package/build/tools/operations/blocks.js +1 -288
- package/build/tools/operations/memory.js +32 -90
- package/build/tools/operations/outline.js +38 -156
- package/build/tools/operations/pages.js +169 -122
- package/build/tools/operations/todos.js +5 -37
- package/build/tools/schemas.js +20 -9
- package/build/tools/tool-handlers.js +4 -4
- package/build/utils/helpers.js +27 -0
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { q
|
|
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 {
|
|
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 {
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
523
|
-
|
|
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 {
|