roam-research-mcp 0.32.4 → 0.35.1
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 +6 -0
- package/build/Roam_Markdown_Cheatsheet.md +1 -0
- package/build/markdown-utils.js +3 -2
- package/build/tools/helpers/text.js +59 -0
- package/build/tools/operations/block-retrieval.js +1 -1
- package/build/tools/operations/outline.js +224 -162
- package/build/tools/schemas.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
package/build/markdown-utils.js
CHANGED
|
@@ -262,13 +262,14 @@ function convertToRoamActions(nodes, parentUid, order = 'last') {
|
|
|
262
262
|
const actions = [];
|
|
263
263
|
// Helper function to recursively create actions
|
|
264
264
|
function createBlockActions(blocks, parentUid, order) {
|
|
265
|
-
for (
|
|
265
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
266
|
+
const block = blocks[i];
|
|
266
267
|
// Create the current block
|
|
267
268
|
const action = {
|
|
268
269
|
action: 'create-block',
|
|
269
270
|
location: {
|
|
270
271
|
'parent-uid': parentUid,
|
|
271
|
-
order
|
|
272
|
+
order: typeof order === 'number' ? order + i : i
|
|
272
273
|
},
|
|
273
274
|
block: {
|
|
274
275
|
uid: block.uid,
|
|
@@ -1,6 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capitalizes each word in a string
|
|
3
|
+
*/
|
|
4
|
+
import { q } from '@roam-research/roam-api-sdk';
|
|
1
5
|
/**
|
|
2
6
|
* Capitalizes each word in a string
|
|
3
7
|
*/
|
|
4
8
|
export const capitalizeWords = (str) => {
|
|
5
9
|
return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
6
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Retrieves a block's UID based on its exact text content.
|
|
13
|
+
* This function is intended for internal use by other MCP tools.
|
|
14
|
+
* @param graph The Roam graph instance.
|
|
15
|
+
* @param blockText The exact text content of the block to find.
|
|
16
|
+
* @returns The UID of the block if found, otherwise null.
|
|
17
|
+
*/
|
|
18
|
+
export const getBlockUidByText = async (graph, blockText) => {
|
|
19
|
+
const query = `[:find ?uid .
|
|
20
|
+
:in $ ?blockString
|
|
21
|
+
:where [?b :block/string ?blockString]
|
|
22
|
+
[?b :block/uid ?uid]]`;
|
|
23
|
+
const result = await q(graph, query, [blockText]);
|
|
24
|
+
return result && result.length > 0 ? result[0][0] : null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Retrieves all UIDs nested under a given block_uid or block_text (exact match).
|
|
28
|
+
* This function is intended for internal use by other MCP tools.
|
|
29
|
+
* @param graph The Roam graph instance.
|
|
30
|
+
* @param rootIdentifier The UID or exact text content of the root block.
|
|
31
|
+
* @returns An array of UIDs of all descendant blocks, including the root block's UID.
|
|
32
|
+
*/
|
|
33
|
+
export const getNestedUids = async (graph, rootIdentifier) => {
|
|
34
|
+
let rootUid = rootIdentifier;
|
|
35
|
+
// If the rootIdentifier is not a UID (simple check for 9 alphanumeric characters), try to resolve it as block text
|
|
36
|
+
if (!rootIdentifier.match(/^[a-zA-Z0-9]{9}$/)) {
|
|
37
|
+
rootUid = await getBlockUidByText(graph, rootIdentifier);
|
|
38
|
+
}
|
|
39
|
+
if (!rootUid) {
|
|
40
|
+
return []; // No root block found
|
|
41
|
+
}
|
|
42
|
+
const query = `[:find ?child-uid
|
|
43
|
+
:in $ ?root-uid
|
|
44
|
+
:where
|
|
45
|
+
[?root-block :block/uid ?root-uid]
|
|
46
|
+
[?root-block :block/children ?child-block]
|
|
47
|
+
[?child-block :block/uid ?child-uid]]`;
|
|
48
|
+
const results = await q(graph, query, [rootUid]);
|
|
49
|
+
return results.map(r => r[0]);
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Retrieves all UIDs nested under a given block_text (exact match).
|
|
53
|
+
* This function is intended for internal use by other MCP tools.
|
|
54
|
+
* It strictly requires an exact text match for the root block.
|
|
55
|
+
* @param graph The Roam graph instance.
|
|
56
|
+
* @param blockText The exact text content of the root block.
|
|
57
|
+
* @returns An array of UIDs of all descendant blocks, including the root block's UID.
|
|
58
|
+
*/
|
|
59
|
+
export const getNestedUidsByText = async (graph, blockText) => {
|
|
60
|
+
const rootUid = await getBlockUidByText(graph, blockText);
|
|
61
|
+
if (!rootUid) {
|
|
62
|
+
return []; // No root block found with exact text match
|
|
63
|
+
}
|
|
64
|
+
return getNestedUids(graph, rootUid);
|
|
65
|
+
};
|
|
@@ -16,7 +16,7 @@ export class BlockRetrievalOperations {
|
|
|
16
16
|
const childrenQuery = `[:find ?parentUid ?childUid ?childString ?childOrder ?childHeading
|
|
17
17
|
:in $ [?parentUid ...]
|
|
18
18
|
:where [?parent :block/uid ?parentUid]
|
|
19
|
-
[?
|
|
19
|
+
[?parent :block/children ?child]
|
|
20
20
|
[?child :block/uid ?childUid]
|
|
21
21
|
[?child :block/string ?childString]
|
|
22
22
|
[?child :block/order ?childOrder]
|
|
@@ -6,6 +6,188 @@ import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown } from '../.
|
|
|
6
6
|
export class OutlineOperations {
|
|
7
7
|
constructor(graph) {
|
|
8
8
|
this.graph = graph;
|
|
9
|
+
/**
|
|
10
|
+
* Helper function to check if string is a valid Roam UID (9 characters)
|
|
11
|
+
*/
|
|
12
|
+
this.isValidUid = (str) => {
|
|
13
|
+
return typeof str === 'string' && str.length === 9;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Helper function to find block with improved relationship checks
|
|
18
|
+
*/
|
|
19
|
+
async findBlockWithRetry(pageUid, blockString, maxRetries = 5, initialDelay = 1000) {
|
|
20
|
+
// Try multiple query strategies
|
|
21
|
+
const queries = [
|
|
22
|
+
// Strategy 1: Direct page and string match
|
|
23
|
+
`[:find ?b-uid ?order
|
|
24
|
+
:where [?p :block/uid "${pageUid}"]
|
|
25
|
+
[?b :block/page ?p]
|
|
26
|
+
[?b :block/string "${blockString}"]
|
|
27
|
+
[?b :block/order ?order]
|
|
28
|
+
[?b :block/uid ?b-uid]]`,
|
|
29
|
+
// Strategy 2: Parent-child relationship
|
|
30
|
+
`[:find ?b-uid ?order
|
|
31
|
+
:where [?p :block/uid "${pageUid}"]
|
|
32
|
+
[?b :block/parents ?p]
|
|
33
|
+
[?b :block/string "${blockString}"]
|
|
34
|
+
[?b :block/order ?order]
|
|
35
|
+
[?b :block/uid ?b-uid]]`,
|
|
36
|
+
// Strategy 3: Broader page relationship
|
|
37
|
+
`[:find ?b-uid ?order
|
|
38
|
+
:where [?p :block/uid "${pageUid}"]
|
|
39
|
+
[?b :block/page ?page]
|
|
40
|
+
[?p :block/page ?page]
|
|
41
|
+
[?b :block/string "${blockString}"]
|
|
42
|
+
[?b :block/order ?order]
|
|
43
|
+
[?b :block/uid ?b-uid]]`
|
|
44
|
+
];
|
|
45
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
46
|
+
// Try each query strategy
|
|
47
|
+
for (const queryStr of queries) {
|
|
48
|
+
const blockResults = await q(this.graph, queryStr, []);
|
|
49
|
+
if (blockResults && blockResults.length > 0) {
|
|
50
|
+
// Use the most recently created block
|
|
51
|
+
const sorted = blockResults.sort((a, b) => b[1] - a[1]);
|
|
52
|
+
return sorted[0][0];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Exponential backoff
|
|
56
|
+
const delay = initialDelay * Math.pow(2, retry);
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
58
|
+
}
|
|
59
|
+
throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
|
|
60
|
+
}
|
|
61
|
+
;
|
|
62
|
+
/**
|
|
63
|
+
* Helper function to create and verify block with improved error handling
|
|
64
|
+
*/
|
|
65
|
+
async createAndVerifyBlock(content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false) {
|
|
66
|
+
try {
|
|
67
|
+
// Initial delay before any operations
|
|
68
|
+
if (!isRetry) {
|
|
69
|
+
await new Promise(resolve => setTimeout(resolve, initialDelay));
|
|
70
|
+
}
|
|
71
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
72
|
+
console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
|
|
73
|
+
// Create block using batchActions
|
|
74
|
+
const batchResult = await batchActions(this.graph, {
|
|
75
|
+
action: 'batch-actions',
|
|
76
|
+
actions: [{
|
|
77
|
+
action: 'create-block',
|
|
78
|
+
location: {
|
|
79
|
+
'parent-uid': parentUid,
|
|
80
|
+
order: 'last'
|
|
81
|
+
},
|
|
82
|
+
block: { string: content }
|
|
83
|
+
}]
|
|
84
|
+
});
|
|
85
|
+
if (!batchResult) {
|
|
86
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create block "${content}" via batch action`);
|
|
87
|
+
}
|
|
88
|
+
// Wait with exponential backoff
|
|
89
|
+
const delay = initialDelay * Math.pow(2, retry);
|
|
90
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
91
|
+
try {
|
|
92
|
+
// Try to find the block using our improved findBlockWithRetry
|
|
93
|
+
return await this.findBlockWithRetry(parentUid, content);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
97
|
+
// console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`); // Removed console.log
|
|
98
|
+
if (retry === maxRetries - 1)
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create and verify block "${content}" after ${maxRetries} attempts`);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// If this is already a retry, throw the error
|
|
106
|
+
if (isRetry)
|
|
107
|
+
throw error;
|
|
108
|
+
// Otherwise, try one more time with a clean slate
|
|
109
|
+
// console.log(`Retrying block creation for "${content}" with fresh attempt`); // Removed console.log
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
|
|
111
|
+
return this.createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
;
|
|
115
|
+
/**
|
|
116
|
+
* Helper function to fetch a block and its children recursively
|
|
117
|
+
*/
|
|
118
|
+
async fetchBlockWithChildren(blockUid, level = 1) {
|
|
119
|
+
const query = `
|
|
120
|
+
[:find ?childUid ?childString ?childOrder
|
|
121
|
+
:in $ ?parentUid
|
|
122
|
+
:where
|
|
123
|
+
[?parentEntity :block/uid ?parentUid]
|
|
124
|
+
[?parentEntity :block/children ?childEntity] ; This ensures direct children
|
|
125
|
+
[?childEntity :block/uid ?childUid]
|
|
126
|
+
[?childEntity :block/string ?childString]
|
|
127
|
+
[?childEntity :block/order ?childOrder]]
|
|
128
|
+
`;
|
|
129
|
+
const blockQuery = `
|
|
130
|
+
[:find ?string
|
|
131
|
+
:in $ ?uid
|
|
132
|
+
:where
|
|
133
|
+
[?e :block/uid ?uid]
|
|
134
|
+
[?e :block/string ?string]]
|
|
135
|
+
`;
|
|
136
|
+
try {
|
|
137
|
+
const blockStringResult = await q(this.graph, blockQuery, [blockUid]);
|
|
138
|
+
if (!blockStringResult || blockStringResult.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const text = blockStringResult[0][0];
|
|
142
|
+
const childrenResults = await q(this.graph, query, [blockUid]);
|
|
143
|
+
const children = [];
|
|
144
|
+
if (childrenResults && childrenResults.length > 0) {
|
|
145
|
+
// Sort children by order
|
|
146
|
+
const sortedChildren = childrenResults.sort((a, b) => a[2] - b[2]);
|
|
147
|
+
for (const childResult of sortedChildren) {
|
|
148
|
+
const childUid = childResult[0];
|
|
149
|
+
const nestedChild = await this.fetchBlockWithChildren(childUid, level + 1);
|
|
150
|
+
if (nestedChild) {
|
|
151
|
+
children.push(nestedChild);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// The order of the root block is not available from this query, so we set it to 0
|
|
156
|
+
return { uid: blockUid, text, level, order: 0, children: children.length > 0 ? children : undefined };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw new McpError(ErrorCode.InternalError, `Failed to fetch block with children for UID "${blockUid}": ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
;
|
|
163
|
+
/**
|
|
164
|
+
* Recursively fetches a nested structure of blocks under a given root block UID.
|
|
165
|
+
*/
|
|
166
|
+
async fetchNestedStructure(rootUid) {
|
|
167
|
+
const query = `[:find ?child-uid ?child-string ?child-order
|
|
168
|
+
:in $ ?parent-uid
|
|
169
|
+
:where
|
|
170
|
+
[?parent :block/uid ?parent-uid]
|
|
171
|
+
[?parent :block/children ?child]
|
|
172
|
+
[?child :block/uid ?child-uid]
|
|
173
|
+
[?child :block/string ?child-string]
|
|
174
|
+
[?child :block/order ?child-order]]`;
|
|
175
|
+
const directChildrenResult = await q(this.graph, query, [rootUid]);
|
|
176
|
+
if (directChildrenResult.length === 0) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
const nestedBlocks = [];
|
|
180
|
+
for (const [childUid, childString, childOrder] of directChildrenResult) {
|
|
181
|
+
const children = await this.fetchNestedStructure(childUid);
|
|
182
|
+
nestedBlocks.push({
|
|
183
|
+
uid: childUid,
|
|
184
|
+
text: childString,
|
|
185
|
+
level: 0, // Level is not easily determined here, so we set it to 0
|
|
186
|
+
children: children,
|
|
187
|
+
order: childOrder
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return nestedBlocks.sort((a, b) => a.order - b.order);
|
|
9
191
|
}
|
|
10
192
|
/**
|
|
11
193
|
* Creates an outline structure on a Roam Research page, optionally under a specific block.
|
|
@@ -86,147 +268,6 @@ export class OutlineOperations {
|
|
|
86
268
|
};
|
|
87
269
|
// Get or create the target page
|
|
88
270
|
const targetPageUid = await findOrCreatePage(page_title_uid || formatRoamDate(new Date()));
|
|
89
|
-
// Helper function to find block with improved relationship checks
|
|
90
|
-
const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000) => {
|
|
91
|
-
// Try multiple query strategies
|
|
92
|
-
const queries = [
|
|
93
|
-
// Strategy 1: Direct page and string match
|
|
94
|
-
`[:find ?b-uid ?order
|
|
95
|
-
:where [?p :block/uid "${pageUid}"]
|
|
96
|
-
[?b :block/page ?p]
|
|
97
|
-
[?b :block/string "${blockString}"]
|
|
98
|
-
[?b :block/order ?order]
|
|
99
|
-
[?b :block/uid ?b-uid]]`,
|
|
100
|
-
// Strategy 2: Parent-child relationship
|
|
101
|
-
`[:find ?b-uid ?order
|
|
102
|
-
:where [?p :block/uid "${pageUid}"]
|
|
103
|
-
[?b :block/parents ?p]
|
|
104
|
-
[?b :block/string "${blockString}"]
|
|
105
|
-
[?b :block/order ?order]
|
|
106
|
-
[?b :block/uid ?b-uid]]`,
|
|
107
|
-
// Strategy 3: Broader page relationship
|
|
108
|
-
`[:find ?b-uid ?order
|
|
109
|
-
:where [?p :block/uid "${pageUid}"]
|
|
110
|
-
[?b :block/page ?page]
|
|
111
|
-
[?p :block/page ?page]
|
|
112
|
-
[?b :block/string "${blockString}"]
|
|
113
|
-
[?b :block/order ?order]
|
|
114
|
-
[?b :block/uid ?b-uid]]`
|
|
115
|
-
];
|
|
116
|
-
for (let retry = 0; retry < maxRetries; retry++) {
|
|
117
|
-
// Try each query strategy
|
|
118
|
-
for (const queryStr of queries) {
|
|
119
|
-
const blockResults = await q(this.graph, queryStr, []);
|
|
120
|
-
if (blockResults && blockResults.length > 0) {
|
|
121
|
-
// Use the most recently created block
|
|
122
|
-
const sorted = blockResults.sort((a, b) => b[1] - a[1]);
|
|
123
|
-
return sorted[0][0];
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Exponential backoff
|
|
127
|
-
const delay = initialDelay * Math.pow(2, retry);
|
|
128
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
129
|
-
}
|
|
130
|
-
throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
|
|
131
|
-
};
|
|
132
|
-
// Helper function to create and verify block with improved error handling
|
|
133
|
-
const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false) => {
|
|
134
|
-
try {
|
|
135
|
-
// Initial delay before any operations
|
|
136
|
-
if (!isRetry) {
|
|
137
|
-
await new Promise(resolve => setTimeout(resolve, initialDelay));
|
|
138
|
-
}
|
|
139
|
-
for (let retry = 0; retry < maxRetries; retry++) {
|
|
140
|
-
console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
|
|
141
|
-
// Create block using batchActions
|
|
142
|
-
const batchResult = await batchActions(this.graph, {
|
|
143
|
-
action: 'batch-actions',
|
|
144
|
-
actions: [{
|
|
145
|
-
action: 'create-block',
|
|
146
|
-
location: {
|
|
147
|
-
'parent-uid': parentUid,
|
|
148
|
-
order: 'last'
|
|
149
|
-
},
|
|
150
|
-
block: { string: content }
|
|
151
|
-
}]
|
|
152
|
-
});
|
|
153
|
-
if (!batchResult) {
|
|
154
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create block "${content}" via batch action`);
|
|
155
|
-
}
|
|
156
|
-
// Wait with exponential backoff
|
|
157
|
-
const delay = initialDelay * Math.pow(2, retry);
|
|
158
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
159
|
-
try {
|
|
160
|
-
// Try to find the block using our improved findBlockWithRetry
|
|
161
|
-
return await findBlockWithRetry(parentUid, content);
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
165
|
-
// console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`); // Removed console.log
|
|
166
|
-
if (retry === maxRetries - 1)
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create and verify block "${content}" after ${maxRetries} attempts`);
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
// If this is already a retry, throw the error
|
|
174
|
-
if (isRetry)
|
|
175
|
-
throw error;
|
|
176
|
-
// Otherwise, try one more time with a clean slate
|
|
177
|
-
// console.log(`Retrying block creation for "${content}" with fresh attempt`); // Removed console.log
|
|
178
|
-
await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
|
|
179
|
-
return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
// Helper function to check if string is a valid Roam UID (9 characters)
|
|
183
|
-
const isValidUid = (str) => {
|
|
184
|
-
return typeof str === 'string' && str.length === 9;
|
|
185
|
-
};
|
|
186
|
-
// Helper function to fetch a block and its children recursively
|
|
187
|
-
const fetchBlockWithChildren = async (blockUid, level = 1) => {
|
|
188
|
-
const query = `
|
|
189
|
-
[:find ?childUid ?childString ?childOrder
|
|
190
|
-
:in $ ?parentUid
|
|
191
|
-
:where
|
|
192
|
-
[?parentEntity :block/uid ?parentUid]
|
|
193
|
-
[?parentEntity :block/children ?childEntity] ; This ensures direct children
|
|
194
|
-
[?childEntity :block/uid ?childUid]
|
|
195
|
-
[?childEntity :block/string ?childString]
|
|
196
|
-
[?childEntity :block/order ?childOrder]]
|
|
197
|
-
`;
|
|
198
|
-
const blockQuery = `
|
|
199
|
-
[:find ?string
|
|
200
|
-
:in $ ?uid
|
|
201
|
-
:where
|
|
202
|
-
[?e :block/uid ?uid]
|
|
203
|
-
[?e :block/string ?string]]
|
|
204
|
-
`;
|
|
205
|
-
try {
|
|
206
|
-
const blockStringResult = await q(this.graph, blockQuery, [blockUid]);
|
|
207
|
-
if (!blockStringResult || blockStringResult.length === 0) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
const text = blockStringResult[0][0];
|
|
211
|
-
const childrenResults = await q(this.graph, query, [blockUid]);
|
|
212
|
-
const children = [];
|
|
213
|
-
if (childrenResults && childrenResults.length > 0) {
|
|
214
|
-
// Sort children by order
|
|
215
|
-
const sortedChildren = childrenResults.sort((a, b) => a[2] - b[2]);
|
|
216
|
-
for (const childResult of sortedChildren) {
|
|
217
|
-
const childUid = childResult[0];
|
|
218
|
-
const nestedChild = await fetchBlockWithChildren(childUid, level + 1);
|
|
219
|
-
if (nestedChild) {
|
|
220
|
-
children.push(nestedChild);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return { uid: blockUid, text, level, children: children.length > 0 ? children : undefined };
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
throw new McpError(ErrorCode.InternalError, `Failed to fetch block with children for UID "${blockUid}": ${error.message}`);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
271
|
// Get or create the parent block
|
|
231
272
|
let targetParentUid;
|
|
232
273
|
if (!block_text_uid) {
|
|
@@ -234,7 +275,7 @@ export class OutlineOperations {
|
|
|
234
275
|
}
|
|
235
276
|
else {
|
|
236
277
|
try {
|
|
237
|
-
if (isValidUid(block_text_uid)) {
|
|
278
|
+
if (this.isValidUid(block_text_uid)) {
|
|
238
279
|
// First try to find block by UID
|
|
239
280
|
const uidQuery = `[:find ?uid
|
|
240
281
|
:where [?e :block/uid "${block_text_uid}"]
|
|
@@ -250,12 +291,12 @@ export class OutlineOperations {
|
|
|
250
291
|
}
|
|
251
292
|
else {
|
|
252
293
|
// Create header block and get its UID if not a valid UID
|
|
253
|
-
targetParentUid = await createAndVerifyBlock(block_text_uid, targetPageUid);
|
|
294
|
+
targetParentUid = await this.createAndVerifyBlock(block_text_uid, targetPageUid);
|
|
254
295
|
}
|
|
255
296
|
}
|
|
256
297
|
catch (error) {
|
|
257
298
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
258
|
-
throw new McpError(ErrorCode.InternalError, `Failed to ${isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`);
|
|
299
|
+
throw new McpError(ErrorCode.InternalError, `Failed to ${this.isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`);
|
|
259
300
|
}
|
|
260
301
|
}
|
|
261
302
|
// Initialize result variable
|
|
@@ -325,9 +366,9 @@ export class OutlineOperations {
|
|
|
325
366
|
for (const item of topLevelOutlineItems) {
|
|
326
367
|
try {
|
|
327
368
|
// Assert item.text is a string as it's filtered earlier to be non-undefined and non-empty
|
|
328
|
-
const foundUid = await findBlockWithRetry(targetParentUid, item.text);
|
|
369
|
+
const foundUid = await this.findBlockWithRetry(targetParentUid, item.text);
|
|
329
370
|
if (foundUid) {
|
|
330
|
-
const nestedBlock = await fetchBlockWithChildren(foundUid);
|
|
371
|
+
const nestedBlock = await this.fetchBlockWithChildren(foundUid);
|
|
331
372
|
if (nestedBlock) {
|
|
332
373
|
createdBlocks.push(nestedBlock);
|
|
333
374
|
}
|
|
@@ -346,7 +387,7 @@ export class OutlineOperations {
|
|
|
346
387
|
created_uids: createdBlocks
|
|
347
388
|
};
|
|
348
389
|
}
|
|
349
|
-
async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = '
|
|
390
|
+
async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'last') {
|
|
350
391
|
// First get the page UID
|
|
351
392
|
let targetPageUid = page_uid;
|
|
352
393
|
if (!targetPageUid && page_title) {
|
|
@@ -393,15 +434,20 @@ export class OutlineOperations {
|
|
|
393
434
|
throw new McpError(ErrorCode.InvalidRequest, 'Must provide either page_uid or page_title when using parent_string');
|
|
394
435
|
}
|
|
395
436
|
// Find block by exact string match within the page
|
|
396
|
-
const findBlockQuery = `[:find ?uid
|
|
397
|
-
:
|
|
437
|
+
const findBlockQuery = `[:find ?b-uid
|
|
438
|
+
:in $ ?page-uid ?block-string
|
|
439
|
+
:where [?p :block/uid ?page-uid]
|
|
398
440
|
[?b :block/page ?p]
|
|
399
|
-
[?b :block/string
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
441
|
+
[?b :block/string ?block-string]
|
|
442
|
+
[?b :block/uid ?b-uid]]`;
|
|
443
|
+
const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, parent_string]);
|
|
444
|
+
if (blockResults && blockResults.length > 0) {
|
|
445
|
+
targetParentUid = blockResults[0][0];
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
// If parent_string block doesn't exist, create it
|
|
449
|
+
targetParentUid = await this.createAndVerifyBlock(parent_string, targetPageUid);
|
|
403
450
|
}
|
|
404
|
-
targetParentUid = blockResults[0][0];
|
|
405
451
|
}
|
|
406
452
|
// If no parent specified, use page as parent
|
|
407
453
|
if (!targetParentUid) {
|
|
@@ -423,8 +469,8 @@ export class OutlineOperations {
|
|
|
423
469
|
if (!result) {
|
|
424
470
|
throw new McpError(ErrorCode.InternalError, 'Failed to import nested markdown content');
|
|
425
471
|
}
|
|
426
|
-
//
|
|
427
|
-
const createdUids =
|
|
472
|
+
// After successful batch action, get all nested UIDs under the parent
|
|
473
|
+
const createdUids = await this.fetchNestedStructure(targetParentUid);
|
|
428
474
|
return {
|
|
429
475
|
success: true,
|
|
430
476
|
page_uid: targetPageUid,
|
|
@@ -438,26 +484,42 @@ export class OutlineOperations {
|
|
|
438
484
|
action: 'create-block',
|
|
439
485
|
location: {
|
|
440
486
|
"parent-uid": targetParentUid,
|
|
441
|
-
order
|
|
487
|
+
"order": order
|
|
442
488
|
},
|
|
443
489
|
block: { string: content }
|
|
444
490
|
}];
|
|
445
491
|
try {
|
|
446
|
-
|
|
492
|
+
await batchActions(this.graph, {
|
|
447
493
|
action: 'batch-actions',
|
|
448
494
|
actions
|
|
449
495
|
});
|
|
450
|
-
if (!result) {
|
|
451
|
-
throw new McpError(ErrorCode.InternalError, 'Failed to create content block via batch action');
|
|
452
|
-
}
|
|
453
496
|
}
|
|
454
497
|
catch (error) {
|
|
455
498
|
throw new McpError(ErrorCode.InternalError, `Failed to create content block: ${error instanceof Error ? error.message : String(error)}`);
|
|
456
499
|
}
|
|
500
|
+
// For single-line content, we still need to fetch the UID and construct a NestedBlock
|
|
501
|
+
const createdUids = [];
|
|
502
|
+
try {
|
|
503
|
+
const foundUid = await this.findBlockWithRetry(targetParentUid, content);
|
|
504
|
+
if (foundUid) {
|
|
505
|
+
createdUids.push({
|
|
506
|
+
uid: foundUid,
|
|
507
|
+
text: content,
|
|
508
|
+
level: 0,
|
|
509
|
+
order: 0,
|
|
510
|
+
children: []
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
// Log warning but don't re-throw, as the block might be created, just not immediately verifiable
|
|
516
|
+
// console.warn(`Could not verify single block creation for "${content}": ${error.message}`);
|
|
517
|
+
}
|
|
457
518
|
return {
|
|
458
519
|
success: true,
|
|
459
520
|
page_uid: targetPageUid,
|
|
460
|
-
parent_uid: targetParentUid
|
|
521
|
+
parent_uid: targetParentUid,
|
|
522
|
+
created_uids: createdUids
|
|
461
523
|
};
|
|
462
524
|
}
|
|
463
525
|
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -40,7 +40,7 @@ export const toolSchemas = {
|
|
|
40
40
|
},
|
|
41
41
|
roam_create_page: {
|
|
42
42
|
name: 'roam_create_page',
|
|
43
|
-
description: 'Create new standalone page in Roam with optional content using explicit nesting levels and headings (H1-H3). 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, 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.',
|
|
44
44
|
inputSchema: {
|
|
45
45
|
type: 'object',
|
|
46
46
|
properties: {
|
|
@@ -80,7 +80,7 @@ export const toolSchemas = {
|
|
|
80
80
|
},
|
|
81
81
|
roam_create_outline: {
|
|
82
82
|
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. 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.',
|
|
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.',
|
|
84
84
|
inputSchema: {
|
|
85
85
|
type: 'object',
|
|
86
86
|
properties: {
|
|
@@ -129,7 +129,7 @@ export const toolSchemas = {
|
|
|
129
129
|
},
|
|
130
130
|
roam_import_markdown: {
|
|
131
131
|
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.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
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.',
|
|
133
133
|
inputSchema: {
|
|
134
134
|
type: 'object',
|
|
135
135
|
properties: {
|
|
@@ -151,7 +151,7 @@ export const toolSchemas = {
|
|
|
151
151
|
},
|
|
152
152
|
parent_string: {
|
|
153
153
|
type: 'string',
|
|
154
|
-
description: 'Optional: Exact string content of
|
|
154
|
+
description: 'Optional: Exact string content of an existing parent block to add content under (used if parent_uid is not provided; requires page_uid or page_title). If the block does not exist, it will be created.'
|
|
155
155
|
},
|
|
156
156
|
order: {
|
|
157
157
|
type: 'string',
|
|
@@ -463,7 +463,7 @@ export const toolSchemas = {
|
|
|
463
463
|
},
|
|
464
464
|
roam_fetch_block_with_children: {
|
|
465
465
|
name: 'roam_fetch_block_with_children',
|
|
466
|
-
description: 'Fetch a block by its UID along with its hierarchical children down to a specified depth.',
|
|
466
|
+
description: 'Fetch a block by its UID along with its hierarchical children down to a specified depth. Returns a nested object structure containing the block\'s UID, text, order, and an array of its children.',
|
|
467
467
|
inputSchema: {
|
|
468
468
|
type: 'object',
|
|
469
469
|
properties: {
|