roam-research-mcp 0.36.0 → 1.3.2

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.
@@ -0,0 +1,142 @@
1
+ import { BatchOperations } from './batch.js';
2
+ /**
3
+ * Validates table input before building actions.
4
+ */
5
+ export function validateTableInput(input) {
6
+ const errors = [];
7
+ if (!input.parent_uid) {
8
+ errors.push({ field: 'parent_uid', message: 'parent_uid is required' });
9
+ }
10
+ if (!input.headers || !Array.isArray(input.headers)) {
11
+ errors.push({ field: 'headers', message: 'headers must be an array' });
12
+ }
13
+ else if (input.headers.length === 0) {
14
+ errors.push({ field: 'headers', message: 'At least one header is required' });
15
+ }
16
+ if (!input.rows || !Array.isArray(input.rows)) {
17
+ errors.push({ field: 'rows', message: 'rows must be an array' });
18
+ }
19
+ else {
20
+ const expectedCells = (input.headers?.length || 1) - 1;
21
+ for (let i = 0; i < input.rows.length; i++) {
22
+ const row = input.rows[i];
23
+ if (!row.label && row.label !== '') {
24
+ errors.push({
25
+ field: `rows[${i}].label`,
26
+ message: 'Row label is required (use empty string for blank)'
27
+ });
28
+ }
29
+ if (!Array.isArray(row.cells)) {
30
+ errors.push({
31
+ field: `rows[${i}].cells`,
32
+ message: 'Row cells must be an array'
33
+ });
34
+ }
35
+ else if (row.cells.length !== expectedCells) {
36
+ errors.push({
37
+ field: `rows[${i}].cells`,
38
+ message: `Expected ${expectedCells} cells, got ${row.cells.length}`
39
+ });
40
+ }
41
+ }
42
+ }
43
+ return { valid: errors.length === 0, errors };
44
+ }
45
+ /**
46
+ * Builds batch actions for creating a Roam table structure.
47
+ *
48
+ * Roam tables have a specific nested structure:
49
+ * - The table container block contains {{[[table]]}}
50
+ * - Header row is nested deeply (each column nested under the previous)
51
+ * - Data rows follow the same nesting pattern
52
+ */
53
+ export function buildTableActions(input) {
54
+ const actions = [];
55
+ // Create table container
56
+ actions.push({
57
+ action: 'create-block',
58
+ uid: '{{uid:table}}',
59
+ string: '{{[[table]]}}',
60
+ location: { 'parent-uid': input.parent_uid, order: input.order ?? 'last' }
61
+ });
62
+ // Create header row with nested structure
63
+ // In Roam tables, each column is nested under the previous column
64
+ let headerParent = '{{uid:table}}';
65
+ for (let i = 0; i < input.headers.length; i++) {
66
+ const uid = `{{uid:header_${i}}}`;
67
+ const headerText = input.headers[i] || ' '; // Convert empty to space
68
+ actions.push({
69
+ action: 'create-block',
70
+ uid,
71
+ string: headerText,
72
+ location: { 'parent-uid': headerParent, order: 0 }
73
+ });
74
+ headerParent = uid;
75
+ }
76
+ // Create data rows
77
+ // Each row starts as a child of the table, with cells nested under each other
78
+ for (let rowIdx = 0; rowIdx < input.rows.length; rowIdx++) {
79
+ const row = input.rows[rowIdx];
80
+ // Row label (first column) - child of table at position rowIdx + 1 (after header)
81
+ const labelUid = `{{uid:row_${rowIdx}_label}}`;
82
+ const labelText = row.label || ' '; // Convert empty to space
83
+ actions.push({
84
+ action: 'create-block',
85
+ uid: labelUid,
86
+ string: labelText,
87
+ location: { 'parent-uid': '{{uid:table}}', order: rowIdx + 1 }
88
+ });
89
+ // Row cells - each cell is nested under the previous
90
+ let cellParent = labelUid;
91
+ for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
92
+ const cellUid = `{{uid:row_${rowIdx}_cell_${cellIdx}}}`;
93
+ const cellText = row.cells[cellIdx] || ' '; // Convert empty to space
94
+ actions.push({
95
+ action: 'create-block',
96
+ uid: cellUid,
97
+ string: cellText,
98
+ location: { 'parent-uid': cellParent, order: 0 }
99
+ });
100
+ cellParent = cellUid;
101
+ }
102
+ }
103
+ return actions;
104
+ }
105
+ export class TableOperations {
106
+ constructor(graph) {
107
+ this.batchOps = new BatchOperations(graph);
108
+ }
109
+ /**
110
+ * Creates a table in Roam with the specified headers and rows.
111
+ *
112
+ * @param input Table configuration including parent_uid, headers, and rows
113
+ * @returns Result including success status and table block UID
114
+ */
115
+ async createTable(input) {
116
+ // Validate input
117
+ const validation = validateTableInput(input);
118
+ if (!validation.valid) {
119
+ return {
120
+ success: false,
121
+ error: {
122
+ code: 'VALIDATION_ERROR',
123
+ message: validation.errors.map(e => `[${e.field}] ${e.message}`).join('; ')
124
+ },
125
+ validation_passed: false,
126
+ actions_attempted: 0
127
+ };
128
+ }
129
+ // Build table actions
130
+ const actions = buildTableActions(input);
131
+ // Execute batch
132
+ const result = await this.batchOps.processBatch(actions);
133
+ // Add table_uid if successful
134
+ if (result.success && result.uid_map) {
135
+ return {
136
+ ...result,
137
+ table_uid: result.uid_map['table']
138
+ };
139
+ }
140
+ return result;
141
+ }
142
+ }
@@ -40,7 +40,7 @@ export const toolSchemas = {
40
40
  },
41
41
  roam_create_page: {
42
42
  name: 'roam_create_page',
43
- description: 'Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
43
+ description: 'Create a new standalone page in Roam with optional content, including structured outlines and tables, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information\n- Creating pages with mixed text and table content in one call.\n**Efficiency Tip:** This tool batches page and content creation efficiently. For adding content to existing pages, use `roam_process_batch_actions` instead.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
44
44
  inputSchema: {
45
45
  type: 'object',
46
46
  properties: {
@@ -50,28 +50,58 @@ export const toolSchemas = {
50
50
  },
51
51
  content: {
52
52
  type: 'array',
53
- description: 'Initial content for the page as an array of blocks with explicit nesting levels. Note: While empty blocks (e.g., {"text": "", "level": 1}) can be used for visual spacing, they create empty entities in the database. Please use them sparingly and only for structural purposes, not for simple visual separation.',
53
+ description: 'Initial content for the page as an array of content items. Each item can be a text block or a table. Text blocks use {text, level, heading?}. Tables use {type: "table", headers, rows}. Items are processed in order.',
54
54
  items: {
55
55
  type: 'object',
56
56
  properties: {
57
+ type: {
58
+ type: 'string',
59
+ enum: ['text', 'table'],
60
+ description: 'Content type: "text" for regular blocks (default), "table" for tables',
61
+ default: 'text'
62
+ },
57
63
  text: {
58
64
  type: 'string',
59
- description: 'Content of the block'
65
+ description: 'Content of the block (for type: "text")'
60
66
  },
61
67
  level: {
62
68
  type: 'integer',
63
- description: 'Indentation level (1-10, where 1 is top level)',
69
+ description: 'Indentation level (1-10, where 1 is top level). For tables, this should always be 1.',
64
70
  minimum: 1,
65
71
  maximum: 10
66
72
  },
67
73
  heading: {
68
74
  type: 'integer',
69
- description: 'Optional: Heading formatting for this block (1-3)',
75
+ description: 'Optional: Heading formatting for this block (1-3). Only for type: "text".',
70
76
  minimum: 1,
71
77
  maximum: 3
78
+ },
79
+ headers: {
80
+ type: 'array',
81
+ description: 'Column headers for the table (for type: "table"). First header is typically empty for row labels.',
82
+ items: { type: 'string' }
83
+ },
84
+ rows: {
85
+ type: 'array',
86
+ description: 'Data rows for the table (for type: "table"). Each row has a label and cells.',
87
+ items: {
88
+ type: 'object',
89
+ properties: {
90
+ label: {
91
+ type: 'string',
92
+ description: 'The row label (first column content). Use empty string for blank.'
93
+ },
94
+ cells: {
95
+ type: 'array',
96
+ description: 'Cell values for this row. Must have exactly (headers.length - 1) items.',
97
+ items: { type: 'string' }
98
+ }
99
+ },
100
+ required: ['label', 'cells']
101
+ }
72
102
  }
73
103
  },
74
- required: ['text', 'level']
104
+ required: ['level']
75
105
  }
76
106
  },
77
107
  },
@@ -80,7 +110,7 @@ export const toolSchemas = {
80
110
  },
81
111
  roam_create_outline: {
82
112
  name: 'roam_create_outline',
83
- description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. To create a new page with an outline, use the `roam_create_page` tool instead. The `outline` parameter defines *new* blocks to be created. To nest content under an *existing* block, provide its UID or exact text in `block_text_uid`, and ensure the `outline` array contains only the child blocks with levels relative to that parent. Including the parent block\'s text in the `outline` array will create a duplicate block. Best for:\n- Adding supplementary structured content to existing pages\n- Creating temporary or working outlines (meeting notes, brainstorms)\n- Organizing thoughts or research under a specific topic\n- Breaking down subtopics or components of a larger concept\nBest for simpler, contiguous hierarchical content. For complex nesting (e.g., tables) or granular control over block placement, consider `roam_process_batch_actions` instead.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
113
+ description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. To create a new page with an outline, use the `roam_create_page` tool instead. The `outline` parameter defines *new* blocks to be created. To nest content under an *existing* block, provide its UID or exact text in `block_text_uid`, and ensure the `outline` array contains only the child blocks with levels relative to that parent. Including the parent block\'s text in the `outline` array will create a duplicate block. Best for:\n- Adding supplementary structured content to existing pages\n- Creating temporary or working outlines (meeting notes, brainstorms)\n- Organizing thoughts or research under a specific topic\n- Breaking down subtopics or components of a larger concept\nBest for simpler, contiguous hierarchical content. For complex nesting (e.g., tables) or granular control over block placement, consider `roam_process_batch_actions` instead.\n**API Usage Note:** This tool performs verification queries after creation. For large outlines (10+ items) or when rate limits are a concern, consider using `roam_process_batch_actions` instead to minimize API calls.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
84
114
  inputSchema: {
85
115
  type: 'object',
86
116
  properties: {
@@ -129,7 +159,7 @@ export const toolSchemas = {
129
159
  },
130
160
  roam_import_markdown: {
131
161
  name: 'roam_import_markdown',
132
- description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page. If a `parent_string` is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
162
+ description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page. If a `parent_string` is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.\n**API Usage Note:** This tool fetches the full nested structure after import for verification. For large imports or when rate limits are a concern, consider using `roam_process_batch_actions` with pre-structured actions instead.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
133
163
  inputSchema: {
134
164
  type: 'object',
135
165
  properties: {
@@ -188,8 +218,8 @@ export const toolSchemas = {
188
218
  },
189
219
  limit: {
190
220
  type: 'integer',
191
- description: 'Optional: The maximum number of results to return. Defaults to 1. Use -1 for no limit.',
192
- default: 1
221
+ description: 'Optional: The maximum number of results to return. Defaults to 50. Use -1 for no limit, but be aware that very large results sets can impact performance.',
222
+ default: 50
193
223
  },
194
224
  offset: {
195
225
  type: 'integer',
@@ -274,15 +304,26 @@ export const toolSchemas = {
274
304
  },
275
305
  roam_find_pages_modified_today: {
276
306
  name: 'roam_find_pages_modified_today',
277
- description: 'Find pages that have been modified today (since midnight), with limit.',
307
+ description: 'Find pages that have been modified today (since midnight), with pagination and sorting options.',
278
308
  inputSchema: {
279
309
  type: 'object',
280
310
  properties: {
281
- max_num_pages: {
311
+ limit: {
282
312
  type: 'integer',
283
- description: 'Max number of pages to retrieve (default: 50)',
313
+ description: 'The maximum number of pages to retrieve (default: 50). Use -1 for no limit, but be aware that very large result sets can impact performance.',
284
314
  default: 50
285
315
  },
316
+ offset: {
317
+ type: 'integer',
318
+ description: 'The number of pages to skip before returning matches. Useful for pagination. Defaults to 0.',
319
+ default: 0
320
+ },
321
+ sort_order: {
322
+ type: 'string',
323
+ description: 'Sort order for pages based on modification date. "desc" for most recent first, "asc" for oldest first.',
324
+ enum: ['asc', 'desc'],
325
+ default: 'desc'
326
+ }
286
327
  }
287
328
  }
288
329
  },
@@ -307,8 +348,8 @@ export const toolSchemas = {
307
348
  },
308
349
  limit: {
309
350
  type: 'integer',
310
- description: 'Optional: The maximum number of results to return. Defaults to 1. Use -1 for no limit.',
311
- default: 1
351
+ description: 'Optional: The maximum number of results to return. Defaults to 50. Use -1 for no limit, but be aware that very large results sets can impact performance.',
352
+ default: 50
312
353
  },
313
354
  offset: {
314
355
  type: 'integer',
@@ -403,7 +444,7 @@ export const toolSchemas = {
403
444
  },
404
445
  roam_datomic_query: {
405
446
  name: 'roam_datomic_query',
406
- description: 'Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. This provides direct access to Roam\'s query engine. Note: Roam graph is case-sensitive.\n\n__Optimal Use Cases for `roam_datomic_query`:__\n- __Regex Search:__ Use for scenarios requiring regex, as Datalog does not natively support full regular expressions. It can fetch broader results for client-side post-processing.\n- __Highly Complex Boolean Logic:__ Ideal for intricate combinations of "AND", "OR", and "NOT" conditions across multiple terms or attributes.\n- __Arbitrary Sorting Criteria:__ The go-to for highly customized sorting needs beyond default options.\n- __Proximity Search:__ For advanced search capabilities involving proximity, which are difficult to implement efficiently with simpler tools.\n\nList of some of Roam\'s data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text).\nPredicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=).\nAggregates (distinct, count, sum, max, min, avg, limit).\nTips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.',
447
+ description: 'Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. This provides direct access to Roam\'s query engine. Note: Roam graph is case-sensitive.\n\n__Optimal Use Cases for `roam_datomic_query`:__\n- __Advanced Filtering (including Regex):__ Use for scenarios requiring complex filtering, including regex matching on results post-query, which Datalog does not natively support for all data types. It can fetch broader results for client-side post-processing.\n- __Highly Complex Boolean Logic:__ Ideal for intricate combinations of "AND", "OR", and "NOT" conditions across multiple terms or attributes.\n- __Arbitrary Sorting Criteria:__ The go-to for highly customized sorting needs beyond default options.\n- __Proximity Search:__ For advanced search capabilities involving proximity, which are difficult to implement efficiently with simpler tools.\n\nList of some of Roam\'s data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text).\nPredicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=).\nAggregates (distinct, count, sum, max, min, avg, limit).\nTips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.',
407
448
  inputSchema: {
408
449
  type: 'object',
409
450
  properties: {
@@ -417,6 +458,21 @@ export const toolSchemas = {
417
458
  items: {
418
459
  type: 'string'
419
460
  }
461
+ },
462
+ regexFilter: {
463
+ type: 'string',
464
+ description: 'Optional: A regex pattern to filter the results client-side after the Datomic query. Applied to JSON.stringify(result) or specific fields if regexTargetField is provided.'
465
+ },
466
+ regexFlags: {
467
+ type: 'string',
468
+ description: 'Optional: Flags for the regex filter (e.g., "i" for case-insensitive, "g" for global).',
469
+ },
470
+ regexTargetField: {
471
+ type: 'array',
472
+ items: {
473
+ type: 'string'
474
+ },
475
+ description: 'Optional: An array of field paths (e.g., ["block_string", "page_title"]) within each Datomic result object to apply the regex filter to. If not provided, the regex is applied to the stringified full result.'
420
476
  }
421
477
  },
422
478
  required: ['query']
@@ -424,7 +480,7 @@ export const toolSchemas = {
424
480
  },
425
481
  roam_process_batch_actions: {
426
482
  name: 'roam_process_batch_actions',
427
- description: 'Executes a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Actions are executed in the provided order. For creating nested blocks, you can use a temporary client-side UID in a parent block and refer to it in a child block within the same batch. For actions on existing blocks, a valid block UID is required. Note: Roam-flavored markdown, including block embedding with `((UID))` syntax, is supported within the `string` property for `create-block` and `update-block` actions. For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs. Tools like `roam_fetch_page_by_title` or other search tools can be used to retrieve these UIDs before executing batch actions. For simpler, sequential outlines, `roam_create_outline` is often more suitable.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
483
+ description: '**RATE LIMIT EFFICIENT:** This is the most API-efficient tool for multiple block operations. Combine all create/update/delete operations into a single call whenever possible. For intensive page updates or revisions, prefer this tool over multiple sequential calls.\n\nExecutes a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Actions are executed in the provided order.\n\n**UID Placeholders for Nested Blocks:** Use `{{uid:name}}` syntax for parent-child references within the same batch. The server generates proper random UIDs and returns a `uid_map` showing placeholder→UID mappings. Example: `{ uid: "{{uid:parent1}}", string: "Parent" }` then `{ location: { "parent-uid": "{{uid:parent1}}" }, string: "Child" }`. Response includes `{ success: true, uid_map: { "parent1": "Xk7mN2pQ9" } }`.\n\nFor actions on existing blocks, a valid block UID is required. Note: Roam-flavored markdown, including block embedding with `((UID))` syntax, is supported within the `string` property for `create-block` and `update-block` actions. For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs. Tools like `roam_fetch_page_by_title` or other search tools can be used to retrieve these UIDs before executing batch actions. For simpler, sequential outlines, `roam_create_outline` is often more suitable.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
428
484
  inputSchema: {
429
485
  type: 'object',
430
486
  properties: {
@@ -508,4 +564,52 @@ export const toolSchemas = {
508
564
  required: ['block_uid']
509
565
  },
510
566
  },
567
+ roam_create_table: {
568
+ name: 'roam_create_table',
569
+ description: 'Create a table in Roam with specified headers and rows. This tool abstracts the complex nested structure that Roam tables require, making it much easier to create properly formatted tables.\n\n**Why use this tool:**\n- Roam tables require precise nested block structures that are error-prone to create manually\n- Automatically handles the {{[[table]]}} container and nested column structure\n- Validates row/column consistency before execution\n- Converts empty cells to spaces (required by Roam)\n\n**Example:** A table with headers ["", "Column A", "Column B"] and rows [{label: "Row 1", cells: ["A1", "B1"]}] creates a 2x3 table.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
570
+ inputSchema: {
571
+ type: 'object',
572
+ properties: {
573
+ parent_uid: {
574
+ type: 'string',
575
+ description: 'The UID of the parent block or page where the table should be created.'
576
+ },
577
+ order: {
578
+ type: ['integer', 'string'],
579
+ description: 'Optional: Position under the parent. Can be a number (0-based) or "first"/"last". Defaults to "last".',
580
+ default: 'last'
581
+ },
582
+ headers: {
583
+ type: 'array',
584
+ description: 'Column headers for the table. The first header is typically empty (for the row label column). Example: ["", "Option A", "Option B"]',
585
+ items: {
586
+ type: 'string'
587
+ },
588
+ minItems: 1
589
+ },
590
+ rows: {
591
+ type: 'array',
592
+ description: 'Data rows for the table. Each row has a label (first column) and cells (remaining columns).',
593
+ items: {
594
+ type: 'object',
595
+ properties: {
596
+ label: {
597
+ type: 'string',
598
+ description: 'The row label (first column content). Use empty string for blank.'
599
+ },
600
+ cells: {
601
+ type: 'array',
602
+ description: 'Cell values for this row. Must have exactly (headers.length - 1) items.',
603
+ items: {
604
+ type: 'string'
605
+ }
606
+ }
607
+ },
608
+ required: ['label', 'cells']
609
+ }
610
+ }
611
+ },
612
+ required: ['parent_uid', 'headers', 'rows']
613
+ }
614
+ },
511
615
  };
@@ -3,28 +3,31 @@ import * as path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { PageOperations } from './operations/pages.js';
5
5
  import { BlockOperations } from './operations/blocks.js';
6
- import { BlockRetrievalOperations } from './operations/block-retrieval.js'; // New import
6
+ import { BlockRetrievalOperations } from './operations/block-retrieval.js';
7
7
  import { SearchOperations } from './operations/search/index.js';
8
8
  import { MemoryOperations } from './operations/memory.js';
9
9
  import { TodoOperations } from './operations/todos.js';
10
10
  import { OutlineOperations } from './operations/outline.js';
11
11
  import { BatchOperations } from './operations/batch.js';
12
+ import { TableOperations } from './operations/table.js';
12
13
  import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
13
14
  export class ToolHandlers {
14
15
  constructor(graph) {
15
16
  this.graph = graph;
17
+ this.cachedCheatsheet = null;
16
18
  this.pageOps = new PageOperations(graph);
17
19
  this.blockOps = new BlockOperations(graph);
18
- this.blockRetrievalOps = new BlockRetrievalOperations(graph); // Initialize new instance
20
+ this.blockRetrievalOps = new BlockRetrievalOperations(graph);
19
21
  this.searchOps = new SearchOperations(graph);
20
22
  this.memoryOps = new MemoryOperations(graph);
21
23
  this.todoOps = new TodoOperations(graph);
22
24
  this.outlineOps = new OutlineOperations(graph);
23
25
  this.batchOps = new BatchOperations(graph);
26
+ this.tableOps = new TableOperations(graph);
24
27
  }
25
28
  // Page Operations
26
- async findPagesModifiedToday(max_num_pages = 50) {
27
- return this.pageOps.findPagesModifiedToday(max_num_pages);
29
+ async findPagesModifiedToday(limit = 50, offset = 0, sort_order = 'desc') {
30
+ return this.pageOps.findPagesModifiedToday(limit, offset, sort_order);
28
31
  }
29
32
  async createPage(title, content) {
30
33
  return this.pageOps.createPage(title, content);
@@ -82,21 +85,39 @@ export class ToolHandlers {
82
85
  async processBatch(actions) {
83
86
  return this.batchOps.processBatch(actions);
84
87
  }
88
+ // Table Operations
89
+ async createTable(input) {
90
+ return this.tableOps.createTable(input);
91
+ }
85
92
  async getRoamMarkdownCheatsheet() {
93
+ if (this.cachedCheatsheet) {
94
+ return this.cachedCheatsheet;
95
+ }
86
96
  const __filename = fileURLToPath(import.meta.url);
87
97
  const __dirname = path.dirname(__filename);
88
98
  const cheatsheetPath = path.join(__dirname, '../../Roam_Markdown_Cheatsheet.md');
89
- let cheatsheetContent = fs.readFileSync(cheatsheetPath, 'utf-8');
90
- const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
91
- if (customInstructionsPath && fs.existsSync(customInstructionsPath)) {
92
- try {
93
- const customInstructionsContent = fs.readFileSync(customInstructionsPath, 'utf-8');
94
- cheatsheetContent += `\n\n${customInstructionsContent}`;
95
- }
96
- catch (error) {
97
- console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
99
+ try {
100
+ let cheatsheetContent = await fs.promises.readFile(cheatsheetPath, 'utf-8');
101
+ const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
102
+ if (customInstructionsPath) {
103
+ try {
104
+ // Check if file exists asynchronously
105
+ await fs.promises.access(customInstructionsPath);
106
+ const customInstructionsContent = await fs.promises.readFile(customInstructionsPath, 'utf-8');
107
+ cheatsheetContent += `\n\n${customInstructionsContent}`;
108
+ }
109
+ catch (error) {
110
+ // File doesn't exist or is not readable, ignore custom instructions
111
+ if (error.code !== 'ENOENT') {
112
+ console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
113
+ }
114
+ }
98
115
  }
116
+ this.cachedCheatsheet = cheatsheetContent;
117
+ return cheatsheetContent;
118
+ }
119
+ catch (error) {
120
+ throw new Error(`Failed to read cheatsheet: ${error}`);
99
121
  }
100
- return cheatsheetContent;
101
122
  }
102
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roam-research-mcp",
3
- "version": "0.36.0",
3
+ "version": "1.3.2",
4
4
  "description": "A Model Context Protocol (MCP) server for Roam Research API integration",
5
5
  "private": false,
6
6
  "repository": {
@@ -22,13 +22,14 @@
22
22
  "homepage": "https://github.com/2b3pro/roam-research-mcp#readme",
23
23
  "type": "module",
24
24
  "bin": {
25
- "roam-research": "./build/index.js"
25
+ "roam-research-mcp": "build/index.js",
26
+ "roam-import": "build/cli/import-markdown.js"
26
27
  },
27
28
  "files": [
28
29
  "build"
29
30
  ],
30
31
  "scripts": {
31
- "build": "echo \"Using custom instructions: .roam/${CUSTOM_INSTRUCTIONS_PREFIX}custom-instructions.md\" && tsc && cat Roam_Markdown_Cheatsheet.md .roam/${CUSTOM_INSTRUCTIONS_PREFIX}custom-instructions.md > build/Roam_Markdown_Cheatsheet.md && chmod 755 build/index.js",
32
+ "build": "echo \"Using custom instructions: .roam/${CUSTOM_INSTRUCTIONS_PREFIX}custom-instructions.md\" && tsc && cat Roam_Markdown_Cheatsheet.md .roam/${CUSTOM_INSTRUCTIONS_PREFIX}custom-instructions.md > build/Roam_Markdown_Cheatsheet.md && chmod 755 build/index.js build/cli/import-markdown.js",
32
33
  "clean": "rm -rf build",
33
34
  "watch": "tsc --watch",
34
35
  "inspector": "npx @modelcontextprotocol/inspector build/index.js",
@@ -48,4 +49,4 @@
48
49
  "ts-node": "^10.9.2",
49
50
  "typescript": "^5.3.3"
50
51
  }
51
- }
52
+ }