roam-research-mcp 2.4.0 → 2.4.3
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 +3 -1
- package/build/Roam_Markdown_Cheatsheet.md +116 -287
- package/build/cli/commands/batch.js +1 -8
- package/build/cli/commands/get.js +82 -51
- package/build/cli/commands/refs.js +50 -32
- package/build/cli/commands/save.js +9 -13
- package/build/cli/commands/search.js +20 -60
- package/build/cli/commands/update.js +71 -28
- package/build/cli/utils/input.js +10 -0
- package/build/server/roam-server.js +2 -2
- package/build/tools/operations/memory.js +10 -6
- package/build/tools/schemas.js +8 -3
- package/build/tools/tool-handlers.js +2 -2
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { BatchOperations } from '../../tools/operations/batch.js';
|
|
3
|
+
import { BlockRetrievalOperations } from '../../tools/operations/block-retrieval.js';
|
|
3
4
|
import { parseMarkdownHeadingLevel } from '../../markdown-utils.js';
|
|
4
5
|
import { printDebug, exitWithError } from '../utils/output.js';
|
|
5
6
|
import { resolveGraph } from '../utils/graph.js';
|
|
7
|
+
import { readStdin } from '../utils/input.js';
|
|
6
8
|
// Patterns for TODO/DONE markers (both {{TODO}} and {{[[TODO]]}} formats)
|
|
7
9
|
const TODO_PATTERN = /\{\{(\[\[)?TODO(\]\])?\}\}\s*/g;
|
|
8
10
|
const DONE_PATTERN = /\{\{(\[\[)?DONE(\]\])?\}\}\s*/g;
|
|
@@ -43,7 +45,7 @@ export function createUpdateCommand() {
|
|
|
43
45
|
return new Command('update')
|
|
44
46
|
.description('Update block content, heading, open/closed state, or TODO/DONE status')
|
|
45
47
|
.argument('<uid>', 'Block UID to update (accepts ((uid)) wrapper)')
|
|
46
|
-
.argument('
|
|
48
|
+
.argument('[content]', 'New content. Use # prefix for heading: "# Title" sets H1. Reads from stdin if "-" or omitted (when piped).')
|
|
47
49
|
.option('-H, --heading <level>', 'Set heading level (1-3), or 0 to remove')
|
|
48
50
|
.option('-o, --open', 'Expand block (show children)')
|
|
49
51
|
.option('-c, --closed', 'Collapse block (hide children)')
|
|
@@ -72,30 +74,82 @@ Examples:
|
|
|
72
74
|
roam update abc123def "Task" -T # Set as TODO
|
|
73
75
|
roam update abc123def "Task" -D # Mark as DONE
|
|
74
76
|
roam update abc123def "Task" --clear-status # Remove status marker
|
|
77
|
+
|
|
78
|
+
# Stdin / Partial Updates
|
|
79
|
+
echo "New text" | roam update abc123def # Pipe content
|
|
80
|
+
roam update abc123def -T # Add TODO (fetches existing text)
|
|
81
|
+
roam update abc123def -o # Expand block (keeps text)
|
|
75
82
|
`)
|
|
76
83
|
.action(async (uid, content, options) => {
|
|
77
84
|
try {
|
|
78
85
|
// Strip (( )) wrapper if present
|
|
79
86
|
const blockUid = uid.replace(/^\(\(|\)\)$/g, '');
|
|
80
|
-
|
|
81
|
-
let finalContent
|
|
87
|
+
const graph = resolveGraph(options, true); // This is a write operation
|
|
88
|
+
let finalContent;
|
|
89
|
+
// 1. Determine new content from args or stdin
|
|
90
|
+
if (content && content !== '-') {
|
|
91
|
+
finalContent = content;
|
|
92
|
+
}
|
|
93
|
+
else if (content === '-' || (!content && !process.stdin.isTTY)) {
|
|
94
|
+
finalContent = await readStdin();
|
|
95
|
+
finalContent = finalContent.trim();
|
|
96
|
+
}
|
|
97
|
+
// 2. Identify if we need to fetch existing content
|
|
98
|
+
const isStatusUpdate = options.todo || options.done || options.clearStatus;
|
|
99
|
+
const isHeadingUpdate = options.heading !== undefined;
|
|
100
|
+
const isStateUpdate = options.open || options.closed;
|
|
101
|
+
if (finalContent === undefined) {
|
|
102
|
+
if (isStatusUpdate) {
|
|
103
|
+
// Must fetch to apply status safely
|
|
104
|
+
const blockRetrieval = new BlockRetrievalOperations(graph);
|
|
105
|
+
const block = await blockRetrieval.fetchBlockWithChildren(blockUid, 0);
|
|
106
|
+
if (!block) {
|
|
107
|
+
exitWithError(`Block ${blockUid} not found`);
|
|
108
|
+
}
|
|
109
|
+
finalContent = block.string;
|
|
110
|
+
}
|
|
111
|
+
else if (!isHeadingUpdate && !isStateUpdate) {
|
|
112
|
+
exitWithError('No content or update options provided.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 3. Process content if we have it
|
|
82
116
|
let headingLevel;
|
|
83
|
-
if (
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
if (finalContent !== undefined) {
|
|
118
|
+
// Handle explicit heading option
|
|
119
|
+
if (options.heading !== undefined) {
|
|
120
|
+
const level = parseInt(options.heading, 10);
|
|
121
|
+
if (level >= 0 && level <= 3) {
|
|
122
|
+
headingLevel = level === 0 ? undefined : level;
|
|
123
|
+
const { content: stripped } = parseMarkdownHeadingLevel(finalContent);
|
|
124
|
+
finalContent = stripped;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Auto-detect heading from content
|
|
129
|
+
const { heading_level, content: stripped } = parseMarkdownHeadingLevel(finalContent);
|
|
130
|
+
if (heading_level > 0) {
|
|
131
|
+
headingLevel = heading_level;
|
|
132
|
+
finalContent = stripped;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Handle TODO/DONE status
|
|
136
|
+
if (options.clearStatus) {
|
|
137
|
+
finalContent = clearStatus(finalContent);
|
|
138
|
+
}
|
|
139
|
+
else if (options.todo) {
|
|
140
|
+
finalContent = applyStatus(finalContent, 'TODO');
|
|
141
|
+
}
|
|
142
|
+
else if (options.done) {
|
|
143
|
+
finalContent = applyStatus(finalContent, 'DONE');
|
|
91
144
|
}
|
|
92
145
|
}
|
|
93
146
|
else {
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
// No content update, just metadata
|
|
148
|
+
if (options.heading !== undefined) {
|
|
149
|
+
const level = parseInt(options.heading, 10);
|
|
150
|
+
if (level >= 0 && level <= 3) {
|
|
151
|
+
headingLevel = level === 0 ? undefined : level;
|
|
152
|
+
}
|
|
99
153
|
}
|
|
100
154
|
}
|
|
101
155
|
// Handle open/closed state
|
|
@@ -106,25 +160,14 @@ Examples:
|
|
|
106
160
|
else if (options.closed) {
|
|
107
161
|
openState = false;
|
|
108
162
|
}
|
|
109
|
-
// Handle TODO/DONE status
|
|
110
|
-
if (options.clearStatus) {
|
|
111
|
-
finalContent = clearStatus(finalContent);
|
|
112
|
-
}
|
|
113
|
-
else if (options.todo) {
|
|
114
|
-
finalContent = applyStatus(finalContent, 'TODO');
|
|
115
|
-
}
|
|
116
|
-
else if (options.done) {
|
|
117
|
-
finalContent = applyStatus(finalContent, 'DONE');
|
|
118
|
-
}
|
|
119
163
|
if (options.debug) {
|
|
120
164
|
printDebug('Block UID', blockUid);
|
|
121
165
|
printDebug('Graph', options.graph || 'default');
|
|
122
|
-
printDebug('Content', finalContent);
|
|
166
|
+
printDebug('Content', finalContent !== undefined ? finalContent : '(no change)');
|
|
123
167
|
printDebug('Heading level', headingLevel ?? 'none');
|
|
124
168
|
printDebug('Open state', openState ?? 'unchanged');
|
|
125
169
|
printDebug('Status', options.todo ? 'TODO' : options.done ? 'DONE' : options.clearStatus ? 'cleared' : 'unchanged');
|
|
126
170
|
}
|
|
127
|
-
const graph = resolveGraph(options, true); // This is a write operation
|
|
128
171
|
const batchOps = new BatchOperations(graph);
|
|
129
172
|
const result = await batchOps.processBatch([{
|
|
130
173
|
action: 'update-block',
|
|
@@ -120,8 +120,8 @@ export class RoamServer {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
case 'roam_remember': {
|
|
123
|
-
const { memory, categories, heading, parent_uid } = cleanedArgs;
|
|
124
|
-
const result = await toolHandlers.remember(memory, categories, heading, parent_uid);
|
|
123
|
+
const { memory, categories, heading, parent_uid, include_memories_tag } = cleanedArgs;
|
|
124
|
+
const result = await toolHandlers.remember(memory, categories, heading, parent_uid, include_memories_tag);
|
|
125
125
|
return {
|
|
126
126
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
127
127
|
};
|
|
@@ -10,7 +10,7 @@ export class MemoryOperations {
|
|
|
10
10
|
this.graph = graph;
|
|
11
11
|
this.searchOps = new SearchOperations(graph);
|
|
12
12
|
}
|
|
13
|
-
async remember(memory, categories, heading, parent_uid) {
|
|
13
|
+
async remember(memory, categories, heading, parent_uid, include_memories_tag = true) {
|
|
14
14
|
// Get today's date
|
|
15
15
|
const today = new Date();
|
|
16
16
|
const dateStr = formatRoamDate(today);
|
|
@@ -91,17 +91,21 @@ export class MemoryOperations {
|
|
|
91
91
|
targetParentUid = pageUid;
|
|
92
92
|
}
|
|
93
93
|
// Get memories tag from environment
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
94
|
+
let memoriesTag;
|
|
95
|
+
if (include_memories_tag) {
|
|
96
|
+
memoriesTag = process.env.MEMORIES_TAG;
|
|
97
|
+
if (!memoriesTag) {
|
|
98
|
+
throw new McpError(ErrorCode.InternalError, 'MEMORIES_TAG environment variable not set');
|
|
99
|
+
}
|
|
97
100
|
}
|
|
98
101
|
// Format categories as Roam tags if provided
|
|
99
102
|
const categoryTags = categories?.map(cat => {
|
|
100
103
|
// Handle multi-word categories
|
|
101
104
|
return cat.includes(' ') ? `#[[${cat}]]` : `#${cat}`;
|
|
102
|
-
})
|
|
105
|
+
}) ?? [];
|
|
103
106
|
// Create block with memory, then all tags together at the end
|
|
104
|
-
const
|
|
107
|
+
const tags = memoriesTag ? [...categoryTags, memoriesTag] : categoryTags;
|
|
108
|
+
const blockContent = [memory, ...tags].join(' ').trim();
|
|
105
109
|
// Pre-generate UID so we can return it
|
|
106
110
|
const blockUid = generateBlockUid();
|
|
107
111
|
const actions = [{
|
package/build/tools/schemas.js
CHANGED
|
@@ -431,20 +431,20 @@ export const toolSchemas = {
|
|
|
431
431
|
},
|
|
432
432
|
roam_remember: {
|
|
433
433
|
name: 'roam_remember',
|
|
434
|
-
description: 'Add a memory or piece of information to remember, stored on the daily page with MEMORIES_TAG tag and optional categories. \nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
434
|
+
description: 'Add a memory or piece of information to remember, stored on the daily page with MEMORIES_TAG tag and optional categories (unless include_memories_tag is false). \nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
435
435
|
inputSchema: {
|
|
436
436
|
type: 'object',
|
|
437
437
|
properties: withMultiGraphParams({
|
|
438
438
|
memory: {
|
|
439
439
|
type: 'string',
|
|
440
|
-
description: 'The memory detail or information to remember'
|
|
440
|
+
description: 'The memory detail or information to remember. Add tags in `categories`.'
|
|
441
441
|
},
|
|
442
442
|
categories: {
|
|
443
443
|
type: 'array',
|
|
444
444
|
items: {
|
|
445
445
|
type: 'string'
|
|
446
446
|
},
|
|
447
|
-
description: 'Optional categories to tag the memory with (will be converted to Roam tags)'
|
|
447
|
+
description: 'Optional categories to tag the memory with (will be converted to Roam tags). Do not duplicate tags added in `memory` parameter.'
|
|
448
448
|
},
|
|
449
449
|
heading: {
|
|
450
450
|
type: 'string',
|
|
@@ -453,6 +453,11 @@ export const toolSchemas = {
|
|
|
453
453
|
parent_uid: {
|
|
454
454
|
type: 'string',
|
|
455
455
|
description: 'Optional UID of a specific block to nest the memory under. Takes precedence over heading parameter.'
|
|
456
|
+
},
|
|
457
|
+
include_memories_tag: {
|
|
458
|
+
type: 'boolean',
|
|
459
|
+
description: 'Whether to append the MEMORIES_TAG tag to the memory block.',
|
|
460
|
+
default: true
|
|
456
461
|
}
|
|
457
462
|
}),
|
|
458
463
|
required: ['memory']
|
|
@@ -67,8 +67,8 @@ export class ToolHandlers {
|
|
|
67
67
|
return handler.execute();
|
|
68
68
|
}
|
|
69
69
|
// Memory Operations
|
|
70
|
-
async remember(memory, categories, heading, parent_uid) {
|
|
71
|
-
return this.memoryOps.remember(memory, categories, heading, parent_uid);
|
|
70
|
+
async remember(memory, categories, heading, parent_uid, include_memories_tag) {
|
|
71
|
+
return this.memoryOps.remember(memory, categories, heading, parent_uid, include_memories_tag);
|
|
72
72
|
}
|
|
73
73
|
async recall(sort_by = 'newest', filter_tag) {
|
|
74
74
|
return this.memoryOps.recall(sort_by, filter_tag);
|