roam-research-mcp 1.4.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.
Files changed (34) hide show
  1. package/README.md +360 -31
  2. package/build/Roam_Markdown_Cheatsheet.md +30 -12
  3. package/build/cli/batch/resolver.js +138 -0
  4. package/build/cli/batch/translator.js +363 -0
  5. package/build/cli/batch/types.js +4 -0
  6. package/build/cli/commands/batch.js +352 -0
  7. package/build/cli/commands/get.js +161 -0
  8. package/build/cli/commands/refs.js +135 -0
  9. package/build/cli/commands/rename.js +58 -0
  10. package/build/cli/commands/save.js +498 -0
  11. package/build/cli/commands/search.js +240 -0
  12. package/build/cli/commands/status.js +91 -0
  13. package/build/cli/commands/update.js +151 -0
  14. package/build/cli/roam.js +35 -0
  15. package/build/cli/utils/graph.js +56 -0
  16. package/build/cli/utils/output.js +122 -0
  17. package/build/config/environment.js +70 -34
  18. package/build/config/graph-registry.js +221 -0
  19. package/build/config/graph-registry.test.js +30 -0
  20. package/build/search/block-ref-search.js +34 -7
  21. package/build/search/status-search.js +5 -4
  22. package/build/server/roam-server.js +98 -53
  23. package/build/shared/validation.js +10 -5
  24. package/build/tools/helpers/refs.js +50 -31
  25. package/build/tools/operations/blocks.js +38 -1
  26. package/build/tools/operations/memory.js +51 -5
  27. package/build/tools/operations/pages.js +186 -111
  28. package/build/tools/operations/search/index.js +5 -1
  29. package/build/tools/operations/todos.js +1 -1
  30. package/build/tools/schemas.js +121 -41
  31. package/build/tools/tool-handlers.js +9 -2
  32. package/build/utils/helpers.js +22 -0
  33. package/package.json +11 -7
  34. package/build/cli/import-markdown.js +0 -98
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Resolver for page/block title lookups before batch execution
3
+ */
4
+ import { q } from '@roam-research/roam-api-sdk';
5
+ import { capitalizeWords } from '../../tools/helpers/text.js';
6
+ import { formatRoamDate } from '../../utils/helpers.js';
7
+ import { pageUidCache } from '../../cache/page-uid-cache.js';
8
+ /**
9
+ * Resolve a page title to its UID, trying multiple case variations
10
+ */
11
+ export async function resolvePageUid(graph, title) {
12
+ // Check cache first
13
+ const cachedUid = pageUidCache.get(title);
14
+ if (cachedUid) {
15
+ return cachedUid;
16
+ }
17
+ // Try different case variations
18
+ const variations = [
19
+ title,
20
+ capitalizeWords(title),
21
+ title.toLowerCase()
22
+ ];
23
+ const orClause = variations.map(v => `[?e :node/title "${v}"]`).join(' ');
24
+ const searchQuery = `[:find ?uid .
25
+ :where [?e :block/uid ?uid]
26
+ (or ${orClause})]`;
27
+ const result = await q(graph, searchQuery, []);
28
+ const uid = (result === null || result === undefined) ? null : String(result);
29
+ // Cache the result
30
+ if (uid) {
31
+ pageUidCache.set(title, uid);
32
+ }
33
+ return uid;
34
+ }
35
+ /**
36
+ * Get today's daily page title in Roam format
37
+ */
38
+ export function getDailyPageTitle() {
39
+ return formatRoamDate(new Date());
40
+ }
41
+ /**
42
+ * Resolve today's daily page UID
43
+ */
44
+ export async function resolveDailyPageUid(graph) {
45
+ const dailyTitle = getDailyPageTitle();
46
+ return resolvePageUid(graph, dailyTitle);
47
+ }
48
+ /**
49
+ * Collect all unique page titles that need resolution from commands
50
+ */
51
+ export function collectPageTitles(commands) {
52
+ const titles = new Set();
53
+ for (const cmd of commands) {
54
+ const params = cmd.params;
55
+ // Commands that can have 'page' param
56
+ if ('page' in params && typeof params.page === 'string') {
57
+ titles.add(params.page);
58
+ }
59
+ // Remember command can have heading that needs parent page resolution
60
+ // But heading lookup is handled separately
61
+ // Todo/remember without explicit page need daily page
62
+ if (cmd.command === 'todo' || cmd.command === 'remember') {
63
+ if (!('page' in params) && !('pageUid' in params) && !('parent' in params)) {
64
+ titles.add(getDailyPageTitle());
65
+ }
66
+ }
67
+ }
68
+ return titles;
69
+ }
70
+ /**
71
+ * Check if any commands need daily page resolution
72
+ */
73
+ export function needsDailyPage(commands) {
74
+ for (const cmd of commands) {
75
+ const params = cmd.params;
76
+ if (cmd.command === 'todo' || cmd.command === 'remember') {
77
+ if (!('page' in params) && !('pageUid' in params) && !('parent' in params)) {
78
+ return true;
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ /**
85
+ * Resolve all page titles to UIDs
86
+ * Returns a map of title -> uid
87
+ */
88
+ export async function resolveAllPages(graph, titles) {
89
+ const resolved = new Map();
90
+ // Resolve in parallel for efficiency
91
+ const entries = Array.from(titles);
92
+ const results = await Promise.all(entries.map(async (title) => {
93
+ const uid = await resolvePageUid(graph, title);
94
+ return [title, uid];
95
+ }));
96
+ for (const [title, uid] of results) {
97
+ if (uid) {
98
+ resolved.set(title, uid);
99
+ }
100
+ }
101
+ return resolved;
102
+ }
103
+ /**
104
+ * Create initial resolution context
105
+ */
106
+ export function createResolutionContext() {
107
+ return {
108
+ pageUids: new Map(),
109
+ placeholders: new Map(),
110
+ levelStack: [],
111
+ currentParent: null,
112
+ dailyPageUid: null
113
+ };
114
+ }
115
+ /**
116
+ * Resolve a parent reference - could be a UID, placeholder, or page title
117
+ */
118
+ export function resolveParentRef(ref, context) {
119
+ // Check if it's a placeholder reference {{name}}
120
+ const placeholderMatch = ref.match(/^\{\{([^}]+)\}\}$/);
121
+ if (placeholderMatch) {
122
+ const name = placeholderMatch[1];
123
+ return context.placeholders.get(name) || `{{uid:${name}}}`;
124
+ }
125
+ // Check if it's a resolved page title
126
+ if (context.pageUids.has(ref)) {
127
+ return context.pageUids.get(ref);
128
+ }
129
+ // Assume it's a direct UID
130
+ return ref;
131
+ }
132
+ /**
133
+ * Generate a placeholder UID for tracking
134
+ * Returns the placeholder in {{uid:name}} format for batch processing
135
+ */
136
+ export function generatePlaceholder(name) {
137
+ return `{{uid:${name}}}`;
138
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Translator: converts CLI-style commands to Roam batch actions
3
+ */
4
+ import { resolveParentRef, generatePlaceholder } from './resolver.js';
5
+ /**
6
+ * Translate a single command to batch actions
7
+ * May return multiple actions (e.g., table, outline)
8
+ */
9
+ export function translateCommand(command, context) {
10
+ switch (command.command) {
11
+ case 'create':
12
+ return translateCreate(command, context);
13
+ case 'update':
14
+ return translateUpdate(command);
15
+ case 'delete':
16
+ return translateDelete(command);
17
+ case 'move':
18
+ return translateMove(command);
19
+ case 'todo':
20
+ return translateTodo(command, context);
21
+ case 'table':
22
+ return translateTable(command, context);
23
+ case 'outline':
24
+ return translateOutline(command, context);
25
+ case 'remember':
26
+ return translateRemember(command, context);
27
+ case 'page':
28
+ return translatePage(command, context);
29
+ case 'codeblock':
30
+ return translateCodeblock(command, context);
31
+ default:
32
+ throw new Error(`Unknown command: ${command.command}`);
33
+ }
34
+ }
35
+ /**
36
+ * Resolve parent UID from command params
37
+ */
38
+ function getParentUid(params, context) {
39
+ // Direct parent UID or placeholder
40
+ if (params.parent) {
41
+ return resolveParentRef(params.parent, context) || params.parent;
42
+ }
43
+ // Page UID
44
+ if (params.pageUid) {
45
+ return params.pageUid;
46
+ }
47
+ // Page title -> resolved UID
48
+ if (params.page) {
49
+ const uid = context.pageUids.get(params.page);
50
+ if (!uid) {
51
+ throw new Error(`Page "${params.page}" not found`);
52
+ }
53
+ return uid;
54
+ }
55
+ // Default to daily page
56
+ if (context.dailyPageUid) {
57
+ return context.dailyPageUid;
58
+ }
59
+ throw new Error('No parent specified and daily page not resolved');
60
+ }
61
+ /**
62
+ * Register a placeholder in the context
63
+ */
64
+ function registerPlaceholder(name, context) {
65
+ const placeholder = generatePlaceholder(name);
66
+ context.placeholders.set(name, placeholder);
67
+ return placeholder;
68
+ }
69
+ /**
70
+ * Update level stack for hierarchy tracking
71
+ */
72
+ function updateLevelStack(level, uid, context) {
73
+ // Ensure stack is long enough
74
+ while (context.levelStack.length < level) {
75
+ context.levelStack.push('');
76
+ }
77
+ context.levelStack[level - 1] = uid;
78
+ // Truncate stack above current level
79
+ context.levelStack.length = level;
80
+ }
81
+ /**
82
+ * Get parent from level stack for hierarchical nesting
83
+ */
84
+ function getParentFromLevel(level, context) {
85
+ if (level <= 1) {
86
+ return context.currentParent;
87
+ }
88
+ const parentLevel = level - 1;
89
+ if (parentLevel <= context.levelStack.length && context.levelStack[parentLevel - 1]) {
90
+ return context.levelStack[parentLevel - 1];
91
+ }
92
+ return context.currentParent;
93
+ }
94
+ // --- Command translators ---
95
+ function translateCreate(cmd, context) {
96
+ const { params } = cmd;
97
+ let parentUid;
98
+ // Handle level-based hierarchy
99
+ if (params.level !== undefined && params.level > 1) {
100
+ const levelParent = getParentFromLevel(params.level, context);
101
+ if (levelParent) {
102
+ parentUid = levelParent;
103
+ }
104
+ else {
105
+ parentUid = getParentUid(params, context);
106
+ }
107
+ }
108
+ else {
109
+ parentUid = getParentUid(params, context);
110
+ // Set as current parent for level-based children
111
+ if (params.level === 1 || params.level === undefined) {
112
+ context.currentParent = parentUid;
113
+ }
114
+ }
115
+ const action = {
116
+ action: 'create-block',
117
+ string: params.text,
118
+ location: {
119
+ 'parent-uid': parentUid,
120
+ order: params.order ?? 'last'
121
+ }
122
+ };
123
+ // Register placeholder if 'as' is specified
124
+ if (params.as) {
125
+ action.uid = registerPlaceholder(params.as, context);
126
+ }
127
+ // Optional properties
128
+ if (params.heading) {
129
+ action.heading = params.heading;
130
+ }
131
+ if (params['children-view-type']) {
132
+ action['children-view-type'] = params['children-view-type'];
133
+ }
134
+ // Update level stack if level is specified
135
+ if (params.level !== undefined && params.as) {
136
+ updateLevelStack(params.level, `{{uid:${params.as}}}`, context);
137
+ }
138
+ return [action];
139
+ }
140
+ function translateUpdate(cmd) {
141
+ const { params } = cmd;
142
+ const action = {
143
+ action: 'update-block',
144
+ uid: params.uid,
145
+ string: params.text
146
+ };
147
+ if (params.heading !== undefined) {
148
+ action.heading = params.heading;
149
+ }
150
+ if (params.open !== undefined) {
151
+ action.open = params.open;
152
+ }
153
+ if (params['text-align']) {
154
+ action['text-align'] = params['text-align'];
155
+ }
156
+ if (params['children-view-type']) {
157
+ action['children-view-type'] = params['children-view-type'];
158
+ }
159
+ return [action];
160
+ }
161
+ function translateDelete(cmd) {
162
+ return [{
163
+ action: 'delete-block',
164
+ uid: cmd.params.uid
165
+ }];
166
+ }
167
+ function translateMove(cmd) {
168
+ const { params } = cmd;
169
+ return [{
170
+ action: 'move-block',
171
+ uid: params.uid,
172
+ location: {
173
+ 'parent-uid': params.parent,
174
+ order: params.order ?? 'last'
175
+ }
176
+ }];
177
+ }
178
+ function translateTodo(cmd, context) {
179
+ const { params } = cmd;
180
+ const parentUid = getParentUid(params, context);
181
+ const action = {
182
+ action: 'create-block',
183
+ string: `{{[[TODO]]}} ${params.text}`,
184
+ location: {
185
+ 'parent-uid': parentUid,
186
+ order: params.order ?? 'last'
187
+ }
188
+ };
189
+ if (params.as) {
190
+ action.uid = registerPlaceholder(params.as, context);
191
+ }
192
+ return [action];
193
+ }
194
+ function translateTable(cmd, context) {
195
+ const { params } = cmd;
196
+ const parentUid = getParentUid(params, context);
197
+ const actions = [];
198
+ // Table container
199
+ const tableContainerPlaceholder = params.as
200
+ ? registerPlaceholder(params.as, context)
201
+ : registerPlaceholder(`_table_${Date.now()}`, context);
202
+ actions.push({
203
+ action: 'create-block',
204
+ uid: tableContainerPlaceholder,
205
+ string: '{{[[table]]}}',
206
+ location: {
207
+ 'parent-uid': parentUid,
208
+ order: params.order ?? 'last'
209
+ }
210
+ });
211
+ // Create columns (headers)
212
+ const columnPlaceholders = [];
213
+ for (let i = 0; i < params.headers.length; i++) {
214
+ const colPlaceholder = registerPlaceholder(`_col_${i}_${Date.now()}`, context);
215
+ columnPlaceholders.push(colPlaceholder);
216
+ actions.push({
217
+ action: 'create-block',
218
+ uid: colPlaceholder,
219
+ string: params.headers[i] || ' ',
220
+ location: {
221
+ 'parent-uid': tableContainerPlaceholder,
222
+ order: i
223
+ }
224
+ });
225
+ }
226
+ // Create rows under each column
227
+ for (let rowIdx = 0; rowIdx < params.rows.length; rowIdx++) {
228
+ const row = params.rows[rowIdx];
229
+ // First column gets the row label
230
+ actions.push({
231
+ action: 'create-block',
232
+ string: row.label || ' ',
233
+ location: {
234
+ 'parent-uid': columnPlaceholders[0],
235
+ order: rowIdx
236
+ }
237
+ });
238
+ // Remaining columns get the cells
239
+ for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
240
+ actions.push({
241
+ action: 'create-block',
242
+ string: row.cells[cellIdx] || ' ',
243
+ location: {
244
+ 'parent-uid': columnPlaceholders[cellIdx + 1],
245
+ order: rowIdx
246
+ }
247
+ });
248
+ }
249
+ }
250
+ return actions;
251
+ }
252
+ function translateOutline(cmd, context) {
253
+ const { params } = cmd;
254
+ const parentUid = getParentUid(params, context);
255
+ const actions = [];
256
+ // Reset level stack with outline parent
257
+ context.currentParent = parentUid;
258
+ context.levelStack = [];
259
+ for (const item of params.items) {
260
+ let itemParentUid;
261
+ if (item.level === 1) {
262
+ itemParentUid = parentUid;
263
+ }
264
+ else {
265
+ const levelParent = getParentFromLevel(item.level, context);
266
+ if (!levelParent) {
267
+ throw new Error(`Invalid outline hierarchy: level ${item.level} has no parent`);
268
+ }
269
+ itemParentUid = levelParent;
270
+ }
271
+ const action = {
272
+ action: 'create-block',
273
+ string: item.text,
274
+ location: {
275
+ 'parent-uid': itemParentUid,
276
+ order: 'last'
277
+ }
278
+ };
279
+ // Register placeholder
280
+ const placeholderName = item.as || `_outline_${actions.length}_${Date.now()}`;
281
+ action.uid = registerPlaceholder(placeholderName, context);
282
+ if (item.heading) {
283
+ action.heading = item.heading;
284
+ }
285
+ // Update level stack
286
+ updateLevelStack(item.level, action.uid, context);
287
+ actions.push(action);
288
+ }
289
+ return actions;
290
+ }
291
+ function translateRemember(cmd, context) {
292
+ const { params } = cmd;
293
+ // Build memory text with categories
294
+ let memoryText = params.text;
295
+ if (params.categories && params.categories.length > 0) {
296
+ const tags = params.categories.map(cat => `#[[${cat}]]`).join(' ');
297
+ memoryText = `${params.text} ${tags}`;
298
+ }
299
+ // Add MEMORIES_TAG if configured (we'll handle this in the CLI command)
300
+ // For now, just create the block
301
+ let parentUid;
302
+ // If heading is specified, we'd need to look it up or create it
303
+ // For simplicity, require parent UID when heading is used
304
+ // TODO: Add heading resolution support
305
+ if (params.heading && !params.parent) {
306
+ throw new Error('remember with --heading requires --parent or heading resolution not yet implemented');
307
+ }
308
+ parentUid = getParentUid(params, context);
309
+ const action = {
310
+ action: 'create-block',
311
+ string: memoryText,
312
+ location: {
313
+ 'parent-uid': parentUid,
314
+ order: params.order ?? 'last'
315
+ }
316
+ };
317
+ if (params.as) {
318
+ action.uid = registerPlaceholder(params.as, context);
319
+ }
320
+ return [action];
321
+ }
322
+ function translatePage(cmd, context) {
323
+ // Note: Page creation uses create-page API, not batch actions
324
+ // We'll handle this specially in the batch command
325
+ // For now, throw an error to indicate this needs special handling
326
+ throw new Error('page command requires special handling outside batch actions');
327
+ }
328
+ function translateCodeblock(cmd, context) {
329
+ const { params } = cmd;
330
+ const parentUid = getParentUid(params, context);
331
+ // Format code with triple backticks
332
+ const codeContent = '```' + params.language + '\n' + params.code + '\n```';
333
+ const action = {
334
+ action: 'create-block',
335
+ string: codeContent,
336
+ location: {
337
+ 'parent-uid': parentUid,
338
+ order: params.order ?? 'last'
339
+ }
340
+ };
341
+ if (params.as) {
342
+ action.uid = registerPlaceholder(params.as, context);
343
+ }
344
+ return [action];
345
+ }
346
+ /**
347
+ * Translate all commands to batch actions
348
+ */
349
+ export function translateAllCommands(commands, context) {
350
+ const actions = [];
351
+ const pageCommands = [];
352
+ for (const cmd of commands) {
353
+ if (cmd.command === 'page') {
354
+ // Collect page commands for special handling
355
+ pageCommands.push(cmd);
356
+ }
357
+ else {
358
+ const cmdActions = translateCommand(cmd, context);
359
+ actions.push(...cmdActions);
360
+ }
361
+ }
362
+ return { actions, pageCommands };
363
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Batch CLI command types
3
+ */
4
+ export {};