roam-research-mcp 1.6.0 → 2.4.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 +200 -13
- package/build/Roam_Markdown_Cheatsheet.md +30 -12
- package/build/cli/batch/resolver.js +138 -0
- package/build/cli/batch/translator.js +363 -0
- package/build/cli/batch/types.js +4 -0
- package/build/cli/commands/batch.js +352 -0
- package/build/cli/commands/get.js +101 -19
- package/build/cli/commands/refs.js +21 -8
- package/build/cli/commands/rename.js +58 -0
- package/build/cli/commands/save.js +433 -56
- package/build/cli/commands/search.js +179 -18
- package/build/cli/commands/status.js +91 -0
- package/build/cli/commands/update.js +151 -0
- package/build/cli/roam.js +18 -1
- package/build/cli/utils/graph.js +56 -0
- package/build/cli/utils/output.js +34 -0
- package/build/config/environment.js +70 -34
- package/build/config/graph-registry.js +221 -0
- package/build/config/graph-registry.test.js +30 -0
- package/build/search/status-search.js +5 -4
- package/build/server/roam-server.js +98 -53
- package/build/shared/validation.js +10 -5
- package/build/tools/helpers/refs.js +50 -31
- package/build/tools/operations/blocks.js +38 -1
- package/build/tools/operations/memory.js +51 -5
- package/build/tools/operations/pages.js +186 -111
- package/build/tools/operations/search/index.js +5 -1
- package/build/tools/operations/todos.js +1 -1
- package/build/tools/schemas.js +115 -39
- package/build/tools/tool-handlers.js +9 -2
- package/build/utils/helpers.js +22 -0
- package/package.json +8 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { q, createPage as createRoamPage, batchActions,
|
|
1
|
+
import { q, createPage as createRoamPage, batchActions, updatePage } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { capitalizeWords } from '../helpers/text.js';
|
|
4
4
|
import { resolveRefs } from '../helpers/refs.js';
|
|
5
|
-
import {
|
|
5
|
+
import { convertToRoamMarkdown, generateBlockUid } from '../../markdown-utils.js';
|
|
6
6
|
import { pageUidCache } from '../../cache/page-uid-cache.js';
|
|
7
7
|
import { buildTableActions } from './table.js';
|
|
8
8
|
import { BatchOperations } from './batch.js';
|
|
@@ -135,137 +135,179 @@ export class PageOperations {
|
|
|
135
135
|
// Return success without adding duplicate content
|
|
136
136
|
return { success: true, uid: pageUid };
|
|
137
137
|
}
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
// Process content items in order, tracking position for correct placement
|
|
139
|
+
// Tables and text blocks are interleaved at their original positions
|
|
140
|
+
// Tables can be nested under text blocks based on their level
|
|
141
|
+
let currentOrder = 0;
|
|
142
|
+
let pendingTextItems = [];
|
|
143
|
+
// Track last block UID at each level for nesting tables
|
|
144
|
+
const levelToLastUid = {};
|
|
145
|
+
// Helper to assign UIDs to nodes and track level mapping
|
|
146
|
+
const assignUidsToNodes = (nodes) => {
|
|
147
|
+
return nodes.map(node => {
|
|
148
|
+
const uid = generateBlockUid();
|
|
149
|
+
levelToLastUid[node.level] = uid;
|
|
150
|
+
return {
|
|
151
|
+
...node,
|
|
152
|
+
uid,
|
|
153
|
+
children: assignUidsToNodes(node.children)
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
// Helper to build batch actions from nodes with pre-assigned UIDs
|
|
158
|
+
const buildActionsFromNodes = (nodes, parentUid, startOrder) => {
|
|
159
|
+
const actions = [];
|
|
160
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
161
|
+
const node = nodes[i];
|
|
162
|
+
actions.push({
|
|
163
|
+
action: 'create-block',
|
|
164
|
+
location: { 'parent-uid': parentUid, order: startOrder + i },
|
|
165
|
+
block: {
|
|
166
|
+
uid: node.uid,
|
|
167
|
+
string: node.content,
|
|
168
|
+
...(node.heading_level && { heading: node.heading_level })
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (node.children.length > 0) {
|
|
172
|
+
actions.push(...buildActionsFromNodes(node.children, node.uid, 0));
|
|
173
|
+
}
|
|
145
174
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
return actions;
|
|
176
|
+
};
|
|
177
|
+
// Helper to flush pending text items as a batch
|
|
178
|
+
const flushTextItems = async (startOrder) => {
|
|
179
|
+
if (pendingTextItems.length === 0)
|
|
180
|
+
return startOrder;
|
|
181
|
+
// Filter out empty blocks
|
|
182
|
+
const nonEmptyContent = pendingTextItems.filter(block => block.text && block.text.trim().length > 0);
|
|
183
|
+
if (nonEmptyContent.length === 0) {
|
|
184
|
+
pendingTextItems = [];
|
|
185
|
+
return startOrder;
|
|
149
186
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (nonEmptyContent.length > 0) {
|
|
157
|
-
// Normalize levels to prevent gaps after filtering
|
|
158
|
-
const normalizedContent = [];
|
|
159
|
-
for (let i = 0; i < nonEmptyContent.length; i++) {
|
|
160
|
-
const block = nonEmptyContent[i];
|
|
161
|
-
if (i === 0) {
|
|
162
|
-
normalizedContent.push({ ...block, level: 1 });
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const prevLevel = normalizedContent[i - 1].level;
|
|
166
|
-
const maxAllowedLevel = prevLevel + 1;
|
|
167
|
-
normalizedContent.push({
|
|
168
|
-
...block,
|
|
169
|
-
level: Math.min(block.level, maxAllowedLevel)
|
|
170
|
-
});
|
|
171
|
-
}
|
|
187
|
+
// Normalize levels to prevent gaps after filtering
|
|
188
|
+
const normalizedContent = [];
|
|
189
|
+
for (let i = 0; i < nonEmptyContent.length; i++) {
|
|
190
|
+
const block = nonEmptyContent[i];
|
|
191
|
+
if (i === 0) {
|
|
192
|
+
normalizedContent.push({ ...block, level: 1 });
|
|
172
193
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
else {
|
|
195
|
+
const prevLevel = normalizedContent[i - 1].level;
|
|
196
|
+
const maxAllowedLevel = prevLevel + 1;
|
|
197
|
+
normalizedContent.push({
|
|
198
|
+
...block,
|
|
199
|
+
level: Math.min(block.level, maxAllowedLevel)
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Convert to node format with level info
|
|
204
|
+
const nodes = normalizedContent.map(block => ({
|
|
205
|
+
content: convertToRoamMarkdown(block.text.replace(/^#+\s*/, '')),
|
|
206
|
+
level: block.level,
|
|
207
|
+
...(block.heading && { heading_level: block.heading }),
|
|
208
|
+
children: []
|
|
209
|
+
}));
|
|
210
|
+
// Create hierarchical structure based on levels
|
|
211
|
+
const rootNodes = [];
|
|
212
|
+
const levelMap = {};
|
|
213
|
+
for (const node of nodes) {
|
|
214
|
+
if (node.level === 1) {
|
|
215
|
+
rootNodes.push(node);
|
|
216
|
+
levelMap[1] = node;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const parentLevel = node.level - 1;
|
|
220
|
+
const parent = levelMap[parentLevel];
|
|
221
|
+
if (!parent) {
|
|
222
|
+
throw new Error(`Invalid block hierarchy: level ${node.level} block has no parent`);
|
|
196
223
|
}
|
|
224
|
+
parent.children.push(node);
|
|
225
|
+
levelMap[node.level] = node;
|
|
197
226
|
}
|
|
198
|
-
// Generate batch actions for text blocks
|
|
199
|
-
const textActions = convertToRoamActions(rootNodes, pageUid, 'last');
|
|
200
|
-
allActions.push(...textActions);
|
|
201
227
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
// Assign UIDs to all nodes and track level->UID mapping
|
|
229
|
+
const nodesWithUids = assignUidsToNodes(rootNodes);
|
|
230
|
+
// Build batch actions from nodes with UIDs
|
|
231
|
+
const textActions = buildActionsFromNodes(nodesWithUids, pageUid, startOrder);
|
|
232
|
+
if (textActions.length > 0) {
|
|
233
|
+
const batchResult = await batchActions(this.graph, {
|
|
234
|
+
action: 'batch-actions',
|
|
235
|
+
actions: textActions
|
|
236
|
+
});
|
|
237
|
+
if (!batchResult) {
|
|
238
|
+
throw new Error('Failed to create text blocks');
|
|
239
|
+
}
|
|
211
240
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
241
|
+
// Return the next order position (number of root-level blocks added)
|
|
242
|
+
const nextOrder = startOrder + rootNodes.length;
|
|
243
|
+
pendingTextItems = [];
|
|
244
|
+
return nextOrder;
|
|
245
|
+
};
|
|
246
|
+
// Process content items in order
|
|
247
|
+
for (let i = 0; i < content.length; i++) {
|
|
248
|
+
const item = content[i];
|
|
249
|
+
if (item.type === 'table') {
|
|
250
|
+
// Flush any pending text items first
|
|
251
|
+
currentOrder = await flushTextItems(currentOrder);
|
|
252
|
+
// Process table - determine parent based on level
|
|
253
|
+
const tableItem = item;
|
|
254
|
+
const tableLevel = tableItem.level || 1;
|
|
255
|
+
let tableParentUid = pageUid;
|
|
256
|
+
let tableOrder = currentOrder;
|
|
257
|
+
if (tableLevel > 1) {
|
|
258
|
+
// Nested table - find parent block at level-1
|
|
259
|
+
const parentLevel = tableLevel - 1;
|
|
260
|
+
if (levelToLastUid[parentLevel]) {
|
|
261
|
+
tableParentUid = levelToLastUid[parentLevel];
|
|
262
|
+
tableOrder = 'last'; // Append to parent's children
|
|
263
|
+
}
|
|
264
|
+
// If no parent found, fall back to page level
|
|
265
|
+
}
|
|
266
|
+
const tableActions = buildTableActions({
|
|
267
|
+
parent_uid: tableParentUid,
|
|
268
|
+
headers: tableItem.headers,
|
|
269
|
+
rows: tableItem.rows,
|
|
270
|
+
order: tableOrder
|
|
271
|
+
});
|
|
272
|
+
const tableResult = await this.batchOps.processBatch(tableActions);
|
|
273
|
+
if (!tableResult.success) {
|
|
274
|
+
throw new Error(`Failed to create table: ${typeof tableResult.error === 'string' ? tableResult.error : tableResult.error?.message}`);
|
|
275
|
+
}
|
|
276
|
+
// Only increment top-level order for level 1 tables
|
|
277
|
+
if (tableLevel === 1) {
|
|
278
|
+
currentOrder++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Accumulate text items
|
|
283
|
+
pendingTextItems.push(item);
|
|
225
284
|
}
|
|
226
285
|
}
|
|
286
|
+
// Flush any remaining text items
|
|
287
|
+
await flushTextItems(currentOrder);
|
|
227
288
|
}
|
|
228
289
|
catch (error) {
|
|
229
290
|
throw new McpError(ErrorCode.InternalError, `Failed to add content to page: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
291
|
}
|
|
231
292
|
}
|
|
232
|
-
// Add a
|
|
293
|
+
// Add a "Processed: [[date]]" block as the last block of the newly created page
|
|
233
294
|
try {
|
|
234
295
|
const today = new Date();
|
|
235
296
|
const day = today.getDate();
|
|
236
297
|
const month = today.toLocaleString('en-US', { month: 'long' });
|
|
237
298
|
const year = today.getFullYear();
|
|
238
299
|
const formattedTodayTitle = `${month} ${day}${getOrdinalSuffix(day)}, ${year}`;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (dailyPageUid) {
|
|
248
|
-
pageUidCache.set(formattedTodayTitle, dailyPageUid);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (dailyPageUid) {
|
|
252
|
-
await createBlock(this.graph, {
|
|
253
|
-
action: 'create-block',
|
|
254
|
-
block: {
|
|
255
|
-
string: `Created page: [[${pageTitle}]]`
|
|
256
|
-
},
|
|
257
|
-
location: {
|
|
258
|
-
'parent-uid': dailyPageUid,
|
|
259
|
-
order: 'last'
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
console.warn(`Could not find daily page with title: ${formattedTodayTitle}. Link to created page not added.`);
|
|
265
|
-
}
|
|
300
|
+
await batchActions(this.graph, {
|
|
301
|
+
action: 'batch-actions',
|
|
302
|
+
actions: [{
|
|
303
|
+
action: 'create-block',
|
|
304
|
+
location: { 'parent-uid': pageUid, order: 'last' },
|
|
305
|
+
block: { string: `Processed: [[${formattedTodayTitle}]]` }
|
|
306
|
+
}]
|
|
307
|
+
});
|
|
266
308
|
}
|
|
267
309
|
catch (error) {
|
|
268
|
-
console.error(`Failed to add
|
|
310
|
+
console.error(`Failed to add Processed block: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
311
|
}
|
|
270
312
|
return { success: true, uid: pageUid };
|
|
271
313
|
}
|
|
@@ -505,4 +547,37 @@ export class PageOperations {
|
|
|
505
547
|
summary: dryRun ? `[DRY RUN] ${summary}` : summary
|
|
506
548
|
};
|
|
507
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Rename a page by updating its title
|
|
552
|
+
*/
|
|
553
|
+
async renamePage(params) {
|
|
554
|
+
const { old_title, uid, new_title } = params;
|
|
555
|
+
if (!old_title && !uid) {
|
|
556
|
+
throw new McpError(ErrorCode.InvalidParams, 'Either old_title or uid must be provided to identify the page');
|
|
557
|
+
}
|
|
558
|
+
// Build the page identifier
|
|
559
|
+
const pageIdentifier = uid ? { uid } : { title: old_title };
|
|
560
|
+
try {
|
|
561
|
+
const success = await updatePage(this.graph, {
|
|
562
|
+
page: pageIdentifier,
|
|
563
|
+
title: new_title
|
|
564
|
+
});
|
|
565
|
+
if (success) {
|
|
566
|
+
const identifier = uid ? `((${uid}))` : `"${old_title}"`;
|
|
567
|
+
return {
|
|
568
|
+
success: true,
|
|
569
|
+
message: `Renamed ${identifier} → "${new_title}"`
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
return {
|
|
574
|
+
success: false,
|
|
575
|
+
message: 'Failed to rename page (API returned false)'
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
throw new McpError(ErrorCode.InternalError, `Failed to rename page: ${error instanceof Error ? error.message : String(error)}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
508
583
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl, StatusSearchHandlerImpl } from './handlers.js';
|
|
1
|
+
import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl, StatusSearchHandlerImpl, DatomicSearchHandlerImpl } from './handlers.js';
|
|
2
2
|
export class SearchOperations {
|
|
3
3
|
constructor(graph) {
|
|
4
4
|
this.graph = graph;
|
|
@@ -57,6 +57,10 @@ export class SearchOperations {
|
|
|
57
57
|
const handler = new TextSearchHandlerImpl(this.graph, params);
|
|
58
58
|
return handler.execute();
|
|
59
59
|
}
|
|
60
|
+
async executeDatomicQuery(params) {
|
|
61
|
+
const handler = new DatomicSearchHandlerImpl(this.graph, params);
|
|
62
|
+
return handler.execute();
|
|
63
|
+
}
|
|
60
64
|
async searchByDate(params) {
|
|
61
65
|
// Convert dates to timestamps
|
|
62
66
|
const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
|