roam-research-mcp 0.24.2 → 0.25.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 +11 -3
- package/build/server/roam-server.js +21 -5
- package/build/tools/operations/outline.js +23 -3
- package/build/tools/operations/pages.js +37 -26
- package/build/tools/schemas.js +36 -30
- package/build/tools/tool-handlers.js +2 -2
- package/package.json +1 -1
- package/build/test-addMarkdownText.js +0 -87
- package/build/test-queries.js +0 -116
- package/build/tools/operations/search.js +0 -285
package/README.md
CHANGED
|
@@ -26,6 +26,14 @@ npm install
|
|
|
26
26
|
npm run build
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## To Test
|
|
30
|
+
|
|
31
|
+
Run [MCP Inspector](https://github.com/modelcontextprotocol/inspector) after build…
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npx @modelcontextprotocol/inspector node build/index.js
|
|
35
|
+
```
|
|
36
|
+
|
|
29
37
|
## Features
|
|
30
38
|
|
|
31
39
|
The server provides powerful tools for interacting with Roam Research:
|
|
@@ -88,7 +96,7 @@ The server provides powerful tools for interacting with Roam Research:
|
|
|
88
96
|
"mcpServers": {
|
|
89
97
|
"roam-research": {
|
|
90
98
|
"command": "node",
|
|
91
|
-
"args": ["/path/to/roam-research/build/index.js"],
|
|
99
|
+
"args": ["/path/to/roam-research-mcp/build/index.js"],
|
|
92
100
|
"env": {
|
|
93
101
|
"ROAM_API_TOKEN": "your-api-token",
|
|
94
102
|
"ROAM_GRAPH_NAME": "your-graph-name",
|
|
@@ -101,9 +109,9 @@ The server provides powerful tools for interacting with Roam Research:
|
|
|
101
109
|
|
|
102
110
|
Note: The server will first try to load from .env file, then fall back to environment variables from MCP settings.
|
|
103
111
|
|
|
104
|
-
3. Build the server:
|
|
112
|
+
3. Build the server (make sure you're in the root directory of the MCP):
|
|
105
113
|
```bash
|
|
106
|
-
cd roam-research
|
|
114
|
+
cd roam-research-mcp
|
|
107
115
|
npm install
|
|
108
116
|
npm run build
|
|
109
117
|
```
|
|
@@ -17,7 +17,7 @@ export class RoamServer {
|
|
|
17
17
|
this.toolHandlers = new ToolHandlers(this.graph);
|
|
18
18
|
this.server = new Server({
|
|
19
19
|
name: 'roam-research',
|
|
20
|
-
version: '0.
|
|
20
|
+
version: '0.25.0',
|
|
21
21
|
}, {
|
|
22
22
|
capabilities: {
|
|
23
23
|
tools: {
|
|
@@ -36,7 +36,7 @@ export class RoamServer {
|
|
|
36
36
|
roam_find_pages_modified_today: {},
|
|
37
37
|
roam_search_by_text: {},
|
|
38
38
|
roam_update_block: {},
|
|
39
|
-
|
|
39
|
+
roam_update_multiple_blocks: {},
|
|
40
40
|
roam_search_by_date: {},
|
|
41
41
|
roam_datomic_query: {}
|
|
42
42
|
},
|
|
@@ -131,13 +131,18 @@ export class RoamServer {
|
|
|
131
131
|
}
|
|
132
132
|
case 'roam_search_hierarchy': {
|
|
133
133
|
const params = request.params.arguments;
|
|
134
|
+
// Validate that either parent_uid or child_uid is provided, but not both
|
|
135
|
+
if ((!params.parent_uid && !params.child_uid) || (params.parent_uid && params.child_uid)) {
|
|
136
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Either parent_uid or child_uid must be provided, but not both');
|
|
137
|
+
}
|
|
134
138
|
const result = await this.toolHandlers.searchHierarchy(params);
|
|
135
139
|
return {
|
|
136
140
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
137
141
|
};
|
|
138
142
|
}
|
|
139
143
|
case 'roam_find_pages_modified_today': {
|
|
140
|
-
const
|
|
144
|
+
const { max_num_pages } = request.params.arguments;
|
|
145
|
+
const result = await this.toolHandlers.findPagesModifiedToday(max_num_pages || 50);
|
|
141
146
|
return {
|
|
142
147
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
143
148
|
};
|
|
@@ -158,11 +163,16 @@ export class RoamServer {
|
|
|
158
163
|
}
|
|
159
164
|
case 'roam_update_block': {
|
|
160
165
|
const { block_uid, content, transform_pattern } = request.params.arguments;
|
|
166
|
+
// Validate that either content or transform_pattern is provided, but not both or neither
|
|
167
|
+
if ((!content && !transform_pattern) || (content && transform_pattern)) {
|
|
168
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Either content or transform_pattern must be provided, but not both or neither');
|
|
169
|
+
}
|
|
161
170
|
let result;
|
|
162
171
|
if (content) {
|
|
163
172
|
result = await this.toolHandlers.updateBlock(block_uid, content);
|
|
164
173
|
}
|
|
165
|
-
else
|
|
174
|
+
else {
|
|
175
|
+
// We know transform_pattern exists due to validation above
|
|
166
176
|
result = await this.toolHandlers.updateBlock(block_uid, undefined, (currentContent) => {
|
|
167
177
|
const regex = new RegExp(transform_pattern.find, transform_pattern.global !== false ? 'g' : '');
|
|
168
178
|
return currentContent.replace(regex, transform_pattern.replace);
|
|
@@ -179,8 +189,14 @@ export class RoamServer {
|
|
|
179
189
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
180
190
|
};
|
|
181
191
|
}
|
|
182
|
-
case '
|
|
192
|
+
case 'roam_update_multiple_blocks': {
|
|
183
193
|
const { updates } = request.params.arguments;
|
|
194
|
+
// Validate that for each update, either content or transform is provided, but not both or neither
|
|
195
|
+
for (const update of updates) {
|
|
196
|
+
if ((!update.content && !update.transform) || (update.content && update.transform)) {
|
|
197
|
+
throw new McpError(ErrorCode.InvalidRequest, 'For each update, either content or transform must be provided, but not both or neither');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
184
200
|
const result = await this.toolHandlers.updateBlocks(updates);
|
|
185
201
|
return {
|
|
186
202
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -159,6 +159,10 @@ export class OutlineOperations {
|
|
|
159
159
|
return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
|
|
160
160
|
}
|
|
161
161
|
};
|
|
162
|
+
// Helper function to check if string is a valid Roam UID (9 characters)
|
|
163
|
+
const isValidUid = (str) => {
|
|
164
|
+
return typeof str === 'string' && str.length === 9;
|
|
165
|
+
};
|
|
162
166
|
// Get or create the parent block
|
|
163
167
|
let targetParentUid;
|
|
164
168
|
if (!block_text_uid) {
|
|
@@ -166,12 +170,28 @@ export class OutlineOperations {
|
|
|
166
170
|
}
|
|
167
171
|
else {
|
|
168
172
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
if (isValidUid(block_text_uid)) {
|
|
174
|
+
// First try to find block by UID
|
|
175
|
+
const uidQuery = `[:find ?uid
|
|
176
|
+
:where [?e :block/uid "${block_text_uid}"]
|
|
177
|
+
[?e :block/uid ?uid]]`;
|
|
178
|
+
const uidResult = await q(this.graph, uidQuery, []);
|
|
179
|
+
if (uidResult && uidResult.length > 0) {
|
|
180
|
+
// Use existing block if found
|
|
181
|
+
targetParentUid = uidResult[0][0];
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new McpError(ErrorCode.InvalidRequest, `Block with UID "${block_text_uid}" not found`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Create header block and get its UID if not a valid UID
|
|
189
|
+
targetParentUid = await createAndVerifyBlock(block_text_uid, targetPageUid);
|
|
190
|
+
}
|
|
171
191
|
}
|
|
172
192
|
catch (error) {
|
|
173
193
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
174
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create
|
|
194
|
+
throw new McpError(ErrorCode.InternalError, `Failed to ${isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`);
|
|
175
195
|
}
|
|
176
196
|
}
|
|
177
197
|
// Initialize result variable
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { q, createPage as createRoamPage, batchActions
|
|
1
|
+
import { q, createPage as createRoamPage, batchActions } 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 { convertToRoamActions } from '../../markdown-utils.js';
|
|
6
6
|
export class PageOperations {
|
|
7
7
|
graph;
|
|
8
8
|
constructor(graph) {
|
|
9
9
|
this.graph = graph;
|
|
10
10
|
}
|
|
11
|
-
async findPagesModifiedToday(
|
|
11
|
+
async findPagesModifiedToday(max_num_pages = 50) {
|
|
12
12
|
// Define ancestor rule for traversing block hierarchy
|
|
13
13
|
const ancestorRule = `[
|
|
14
14
|
[ (ancestor ?b ?a)
|
|
@@ -29,7 +29,7 @@ export class PageOperations {
|
|
|
29
29
|
(ancestor ?block ?page)
|
|
30
30
|
[?block :edit/time ?time]
|
|
31
31
|
[(> ?time ?start_of_day)]]
|
|
32
|
-
:limit ${
|
|
32
|
+
:limit ${max_num_pages}`, [startOfDay.getTime(), ancestorRule]);
|
|
33
33
|
if (!results || results.length === 0) {
|
|
34
34
|
return {
|
|
35
35
|
success: true,
|
|
@@ -80,34 +80,45 @@ export class PageOperations {
|
|
|
80
80
|
throw new McpError(ErrorCode.InternalError, `Failed to create page: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
// If content is provided,
|
|
84
|
-
if (content) {
|
|
83
|
+
// If content is provided, create blocks using batch operations
|
|
84
|
+
if (content && content.length > 0) {
|
|
85
85
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
// Convert content array to MarkdownNode format expected by convertToRoamActions
|
|
87
|
+
const nodes = content.map(block => ({
|
|
88
|
+
content: block.text,
|
|
89
|
+
level: block.level,
|
|
90
|
+
children: []
|
|
91
|
+
}));
|
|
92
|
+
// Create hierarchical structure based on levels
|
|
93
|
+
const rootNodes = [];
|
|
94
|
+
const levelMap = {};
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
if (node.level === 1) {
|
|
97
|
+
rootNodes.push(node);
|
|
98
|
+
levelMap[1] = node;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const parentLevel = node.level - 1;
|
|
102
|
+
const parent = levelMap[parentLevel];
|
|
103
|
+
if (!parent) {
|
|
104
|
+
throw new Error(`Invalid block hierarchy: level ${node.level} block has no parent`);
|
|
105
|
+
}
|
|
106
|
+
parent.children.push(node);
|
|
107
|
+
levelMap[node.level] = node;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Generate batch actions for all blocks
|
|
111
|
+
const actions = convertToRoamActions(rootNodes, pageUid, 'last');
|
|
112
|
+
// Execute batch operation
|
|
113
|
+
if (actions.length > 0) {
|
|
114
|
+
const batchResult = await batchActions(this.graph, {
|
|
93
115
|
action: 'batch-actions',
|
|
94
116
|
actions
|
|
95
117
|
});
|
|
96
|
-
if (!
|
|
97
|
-
throw new Error('Failed to
|
|
118
|
+
if (!batchResult) {
|
|
119
|
+
throw new Error('Failed to create blocks');
|
|
98
120
|
}
|
|
99
121
|
}
|
|
100
|
-
else {
|
|
101
|
-
// Create a simple block for non-nested content
|
|
102
|
-
await createBlock(this.graph, {
|
|
103
|
-
action: 'create-block',
|
|
104
|
-
location: {
|
|
105
|
-
"parent-uid": pageUid,
|
|
106
|
-
"order": "last"
|
|
107
|
-
},
|
|
108
|
-
block: { string: content }
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
122
|
}
|
|
112
123
|
catch (error) {
|
|
113
124
|
throw new McpError(ErrorCode.InternalError, `Failed to add content to page: ${error instanceof Error ? error.message : String(error)}`);
|
package/build/tools/schemas.js
CHANGED
|
@@ -20,13 +20,13 @@ export const toolSchemas = {
|
|
|
20
20
|
},
|
|
21
21
|
roam_fetch_page_by_title: {
|
|
22
22
|
name: 'roam_fetch_page_by_title',
|
|
23
|
-
description: 'Retrieve complete page contents by exact title, including all nested blocks and resolved block references. Use for reading and analyzing existing Roam pages.',
|
|
23
|
+
description: 'Retrieve complete page contents by exact title, including all nested blocks and resolved block references. Use for accessing daily pages, reading and analyzing existing Roam pages.',
|
|
24
24
|
inputSchema: {
|
|
25
25
|
type: 'object',
|
|
26
26
|
properties: {
|
|
27
27
|
title: {
|
|
28
28
|
type: 'string',
|
|
29
|
-
description: 'Title of the page
|
|
29
|
+
description: 'Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025',
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
required: ['title'],
|
|
@@ -34,7 +34,7 @@ export const toolSchemas = {
|
|
|
34
34
|
},
|
|
35
35
|
roam_create_page: {
|
|
36
36
|
name: 'roam_create_page',
|
|
37
|
-
description: 'Create a new standalone page in Roam with optional content using
|
|
37
|
+
description: 'Create a new standalone page in Roam with optional content using explicit nesting levels. 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.',
|
|
38
38
|
inputSchema: {
|
|
39
39
|
type: 'object',
|
|
40
40
|
properties: {
|
|
@@ -43,8 +43,24 @@ export const toolSchemas = {
|
|
|
43
43
|
description: 'Title of the new page',
|
|
44
44
|
},
|
|
45
45
|
content: {
|
|
46
|
-
type: '
|
|
47
|
-
description: 'Initial content for the page
|
|
46
|
+
type: 'array',
|
|
47
|
+
description: 'Initial content for the page as an array of blocks with explicit nesting levels',
|
|
48
|
+
items: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
text: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Content of the block'
|
|
54
|
+
},
|
|
55
|
+
level: {
|
|
56
|
+
type: 'integer',
|
|
57
|
+
description: 'Indentation level (1-10, where 1 is top level)',
|
|
58
|
+
minimum: 1,
|
|
59
|
+
maximum: 10
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
required: ['text', 'level']
|
|
63
|
+
}
|
|
48
64
|
},
|
|
49
65
|
},
|
|
50
66
|
required: ['title'],
|
|
@@ -236,11 +252,8 @@ export const toolSchemas = {
|
|
|
236
252
|
minimum: 1,
|
|
237
253
|
maximum: 10
|
|
238
254
|
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
{ required: ['parent_uid'] },
|
|
242
|
-
{ required: ['child_uid'] }
|
|
243
|
-
]
|
|
255
|
+
}
|
|
256
|
+
// Note: Validation for either parent_uid or child_uid is handled in the server code
|
|
244
257
|
}
|
|
245
258
|
},
|
|
246
259
|
roam_find_pages_modified_today: {
|
|
@@ -249,13 +262,12 @@ export const toolSchemas = {
|
|
|
249
262
|
inputSchema: {
|
|
250
263
|
type: 'object',
|
|
251
264
|
properties: {
|
|
252
|
-
|
|
265
|
+
max_num_pages: {
|
|
253
266
|
type: 'integer',
|
|
254
|
-
description: '
|
|
255
|
-
default:
|
|
267
|
+
description: 'Max number of pages to retrieve (default: 50)',
|
|
268
|
+
default: 50
|
|
256
269
|
},
|
|
257
|
-
}
|
|
258
|
-
required: ['num_pages']
|
|
270
|
+
}
|
|
259
271
|
}
|
|
260
272
|
},
|
|
261
273
|
roam_search_by_text: {
|
|
@@ -278,7 +290,7 @@ export const toolSchemas = {
|
|
|
278
290
|
},
|
|
279
291
|
roam_update_block: {
|
|
280
292
|
name: 'roam_update_block',
|
|
281
|
-
description: 'Update
|
|
293
|
+
description: 'Update a single block identified by its UID. Use this for individual block updates when you need to either replace the entire content or apply a transform pattern to modify specific parts of the content.\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).',
|
|
282
294
|
inputSchema: {
|
|
283
295
|
type: 'object',
|
|
284
296
|
properties: {
|
|
@@ -311,16 +323,13 @@ export const toolSchemas = {
|
|
|
311
323
|
required: ['find', 'replace']
|
|
312
324
|
}
|
|
313
325
|
},
|
|
314
|
-
required: ['block_uid']
|
|
315
|
-
|
|
316
|
-
{ required: ['content'] },
|
|
317
|
-
{ required: ['transform_pattern'] }
|
|
318
|
-
]
|
|
326
|
+
required: ['block_uid']
|
|
327
|
+
// Note: Validation for either content or transform_pattern is handled in the server code
|
|
319
328
|
}
|
|
320
329
|
},
|
|
321
|
-
|
|
322
|
-
name: '
|
|
323
|
-
description: '
|
|
330
|
+
roam_update_multiple_blocks: {
|
|
331
|
+
name: 'roam_update_multiple_blocks',
|
|
332
|
+
description: 'Efficiently update multiple blocks in a single batch operation. Use this when you need to update several blocks at once to avoid making multiple separate API calls. Each block in the batch can independently either have its content replaced or transformed using a pattern.\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).',
|
|
324
333
|
inputSchema: {
|
|
325
334
|
type: 'object',
|
|
326
335
|
properties: {
|
|
@@ -359,11 +368,8 @@ export const toolSchemas = {
|
|
|
359
368
|
required: ['find', 'replace']
|
|
360
369
|
}
|
|
361
370
|
},
|
|
362
|
-
required: ['block_uid']
|
|
363
|
-
|
|
364
|
-
{ required: ['content'] },
|
|
365
|
-
{ required: ['transform'] }
|
|
366
|
-
]
|
|
371
|
+
required: ['block_uid']
|
|
372
|
+
// Note: Validation for either content or transform is handled in the server code
|
|
367
373
|
}
|
|
368
374
|
}
|
|
369
375
|
},
|
|
@@ -372,7 +378,7 @@ export const toolSchemas = {
|
|
|
372
378
|
},
|
|
373
379
|
roam_search_by_date: {
|
|
374
380
|
name: 'roam_search_by_date',
|
|
375
|
-
description: 'Search for blocks or pages based on creation or modification dates',
|
|
381
|
+
description: 'Search for blocks or pages based on creation or modification dates. Not for daily pages with ordinal date titles.',
|
|
376
382
|
inputSchema: {
|
|
377
383
|
type: 'object',
|
|
378
384
|
properties: {
|
|
@@ -23,8 +23,8 @@ export class ToolHandlers {
|
|
|
23
23
|
this.outlineOps = new OutlineOperations(graph);
|
|
24
24
|
}
|
|
25
25
|
// Page Operations
|
|
26
|
-
async findPagesModifiedToday(
|
|
27
|
-
return this.pageOps.findPagesModifiedToday(
|
|
26
|
+
async findPagesModifiedToday(max_num_pages = 50) {
|
|
27
|
+
return this.pageOps.findPagesModifiedToday(max_num_pages);
|
|
28
28
|
}
|
|
29
29
|
async createPage(title, content) {
|
|
30
30
|
return this.pageOps.createPage(title, content);
|
package/package.json
CHANGED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { initializeGraph, createPage, batchActions, q } from '@roam-research/roam-api-sdk';
|
|
2
|
-
import { parseMarkdown, convertToRoamActions } from '../src/markdown-utils.js';
|
|
3
|
-
import * as dotenv from 'dotenv';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname, join } from 'path';
|
|
6
|
-
// Load environment variables
|
|
7
|
-
const scriptPath = fileURLToPath(import.meta.url);
|
|
8
|
-
const projectRoot = dirname(dirname(scriptPath));
|
|
9
|
-
const envPath = join(projectRoot, '.env');
|
|
10
|
-
dotenv.config({ path: envPath });
|
|
11
|
-
const API_TOKEN = process.env.ROAM_API_TOKEN;
|
|
12
|
-
const GRAPH_NAME = process.env.ROAM_GRAPH_NAME;
|
|
13
|
-
if (!API_TOKEN || !GRAPH_NAME) {
|
|
14
|
-
throw new Error('Missing required environment variables: ROAM_API_TOKEN and/or ROAM_GRAPH_NAME');
|
|
15
|
-
}
|
|
16
|
-
async function testAddMarkdownText() {
|
|
17
|
-
try {
|
|
18
|
-
// Initialize graph
|
|
19
|
-
console.log('Initializing graph...');
|
|
20
|
-
const graph = initializeGraph({
|
|
21
|
-
token: API_TOKEN,
|
|
22
|
-
graph: GRAPH_NAME,
|
|
23
|
-
});
|
|
24
|
-
// Test markdown content
|
|
25
|
-
const testPageTitle = `Test Markdown Import ${new Date().toISOString()}`;
|
|
26
|
-
console.log(`Using test page title: ${testPageTitle}`);
|
|
27
|
-
const markdownContent = `
|
|
28
|
-
| Month | Savings |
|
|
29
|
-
| -------- | ------- |
|
|
30
|
-
| January | $250 |
|
|
31
|
-
| February | $80 |
|
|
32
|
-
| March | $420 |
|
|
33
|
-
|
|
34
|
-
# Main Topic
|
|
35
|
-
- First point
|
|
36
|
-
- Nested point A
|
|
37
|
-
- Deep nested point
|
|
38
|
-
- Nested point B
|
|
39
|
-
- Second point
|
|
40
|
-
1. Numbered subpoint
|
|
41
|
-
2. Another numbered point
|
|
42
|
-
- Mixed list type
|
|
43
|
-
- Third point
|
|
44
|
-
- With some **bold** text
|
|
45
|
-
- And *italic* text
|
|
46
|
-
- And a [[Page Reference]]
|
|
47
|
-
- And a #[[Page Tag]]
|
|
48
|
-
`;
|
|
49
|
-
// First create the page
|
|
50
|
-
console.log('\nCreating page...');
|
|
51
|
-
const success = await createPage(graph, {
|
|
52
|
-
action: 'create-page',
|
|
53
|
-
page: {
|
|
54
|
-
title: testPageTitle
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
if (!success) {
|
|
58
|
-
throw new Error('Failed to create test page');
|
|
59
|
-
}
|
|
60
|
-
// Get the page UID
|
|
61
|
-
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
62
|
-
const findResults = await q(graph, findQuery, [testPageTitle]);
|
|
63
|
-
if (!findResults || findResults.length === 0) {
|
|
64
|
-
throw new Error('Could not find created page');
|
|
65
|
-
}
|
|
66
|
-
const pageUid = findResults[0][0];
|
|
67
|
-
console.log('Page UID:', pageUid);
|
|
68
|
-
// Import markdown
|
|
69
|
-
console.log('\nImporting markdown...');
|
|
70
|
-
const nodes = parseMarkdown(markdownContent);
|
|
71
|
-
console.log('Parsed nodes:', JSON.stringify(nodes, null, 2));
|
|
72
|
-
// Convert and add markdown content
|
|
73
|
-
const actions = convertToRoamActions(nodes, pageUid, 'last');
|
|
74
|
-
const result = await batchActions(graph, {
|
|
75
|
-
action: 'batch-actions',
|
|
76
|
-
actions
|
|
77
|
-
});
|
|
78
|
-
console.log('\nImport result:', JSON.stringify(result, null, 2));
|
|
79
|
-
console.log('\nTest completed successfully!');
|
|
80
|
-
console.log(`Check your Roam graph for the page titled: ${testPageTitle}`);
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
console.error('Error:', error);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Run the test
|
|
87
|
-
testAddMarkdownText();
|
package/build/test-queries.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { initializeGraph, q } from '@roam-research/roam-api-sdk';
|
|
3
|
-
import * as dotenv from 'dotenv';
|
|
4
|
-
// Load environment variables
|
|
5
|
-
dotenv.config();
|
|
6
|
-
const API_TOKEN = process.env.ROAM_API_TOKEN;
|
|
7
|
-
const GRAPH_NAME = process.env.ROAM_GRAPH_NAME;
|
|
8
|
-
if (!API_TOKEN || !GRAPH_NAME) {
|
|
9
|
-
throw new Error('Missing required environment variables: ROAM_API_TOKEN and ROAM_GRAPH_NAME');
|
|
10
|
-
}
|
|
11
|
-
async function main() {
|
|
12
|
-
const graph = initializeGraph({
|
|
13
|
-
token: API_TOKEN,
|
|
14
|
-
graph: GRAPH_NAME,
|
|
15
|
-
});
|
|
16
|
-
try {
|
|
17
|
-
// First verify we can find the page
|
|
18
|
-
console.log('Finding page...');
|
|
19
|
-
const searchQuery = `[:find ?uid .
|
|
20
|
-
:where [?e :node/title "December 18th, 2024"]
|
|
21
|
-
[?e :block/uid ?uid]]`;
|
|
22
|
-
const uid = await q(graph, searchQuery, []);
|
|
23
|
-
console.log('Page UID:', uid);
|
|
24
|
-
if (!uid) {
|
|
25
|
-
throw new Error('Page not found');
|
|
26
|
-
}
|
|
27
|
-
// Get all blocks under this page with their order
|
|
28
|
-
console.log('\nGetting blocks...');
|
|
29
|
-
const blocksQuery = `[:find ?block-uid ?block-str ?order ?parent-uid
|
|
30
|
-
:where [?p :block/uid "${uid}"]
|
|
31
|
-
[?b :block/page ?p]
|
|
32
|
-
[?b :block/uid ?block-uid]
|
|
33
|
-
[?b :block/string ?block-str]
|
|
34
|
-
[?b :block/order ?order]
|
|
35
|
-
[?b :block/parents ?parent]
|
|
36
|
-
[?parent :block/uid ?parent-uid]]`;
|
|
37
|
-
const blocks = await q(graph, blocksQuery, []);
|
|
38
|
-
console.log('Found', blocks.length, 'blocks');
|
|
39
|
-
// Create a map of all blocks
|
|
40
|
-
const blockMap = new Map();
|
|
41
|
-
blocks.forEach(([uid, string, order]) => {
|
|
42
|
-
if (!blockMap.has(uid)) {
|
|
43
|
-
blockMap.set(uid, {
|
|
44
|
-
uid,
|
|
45
|
-
string,
|
|
46
|
-
order: order,
|
|
47
|
-
children: []
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
console.log('Created block map with', blockMap.size, 'entries');
|
|
52
|
-
// Build parent-child relationships
|
|
53
|
-
let relationshipsBuilt = 0;
|
|
54
|
-
blocks.forEach(([childUid, _, __, parentUid]) => {
|
|
55
|
-
const child = blockMap.get(childUid);
|
|
56
|
-
const parent = blockMap.get(parentUid);
|
|
57
|
-
if (child && parent && !parent.children.includes(child)) {
|
|
58
|
-
parent.children.push(child);
|
|
59
|
-
relationshipsBuilt++;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
console.log('Built', relationshipsBuilt, 'parent-child relationships');
|
|
63
|
-
// Get top-level blocks (those directly under the page)
|
|
64
|
-
console.log('\nGetting top-level blocks...');
|
|
65
|
-
const topQuery = `[:find ?block-uid ?block-str ?order
|
|
66
|
-
:where [?p :block/uid "${uid}"]
|
|
67
|
-
[?b :block/page ?p]
|
|
68
|
-
[?b :block/uid ?block-uid]
|
|
69
|
-
[?b :block/string ?block-str]
|
|
70
|
-
[?b :block/order ?order]
|
|
71
|
-
(not-join [?b]
|
|
72
|
-
[?b :block/parents ?parent]
|
|
73
|
-
[?parent :block/page ?p])]`;
|
|
74
|
-
const topBlocks = await q(graph, topQuery, []);
|
|
75
|
-
console.log('Found', topBlocks.length, 'top-level blocks');
|
|
76
|
-
// Create root blocks
|
|
77
|
-
const rootBlocks = topBlocks
|
|
78
|
-
.map(([uid, string, order]) => ({
|
|
79
|
-
uid,
|
|
80
|
-
string,
|
|
81
|
-
order: order,
|
|
82
|
-
children: blockMap.get(uid)?.children || []
|
|
83
|
-
}))
|
|
84
|
-
.sort((a, b) => a.order - b.order);
|
|
85
|
-
// Log block hierarchy
|
|
86
|
-
console.log('\nBlock hierarchy:');
|
|
87
|
-
const logHierarchy = (blocks, level = 0) => {
|
|
88
|
-
blocks.forEach(block => {
|
|
89
|
-
console.log(' '.repeat(level) + '- ' + block.string.substring(0, 50) + '...');
|
|
90
|
-
if (block.children.length > 0) {
|
|
91
|
-
logHierarchy(block.children.sort((a, b) => a.order - b.order), level + 1);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
logHierarchy(rootBlocks);
|
|
96
|
-
// Convert to markdown
|
|
97
|
-
console.log('\nConverting to markdown...');
|
|
98
|
-
const toMarkdown = (blocks, level = 0) => {
|
|
99
|
-
return blocks.map(block => {
|
|
100
|
-
const indent = ' '.repeat(level);
|
|
101
|
-
let md = `${indent}- ${block.string}\n`;
|
|
102
|
-
if (block.children.length > 0) {
|
|
103
|
-
md += toMarkdown(block.children.sort((a, b) => a.order - b.order), level + 1);
|
|
104
|
-
}
|
|
105
|
-
return md;
|
|
106
|
-
}).join('');
|
|
107
|
-
};
|
|
108
|
-
const markdown = toMarkdown(rootBlocks);
|
|
109
|
-
console.log('\nMarkdown output:');
|
|
110
|
-
console.log(markdown);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
console.error('Error:', error);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
main().catch(console.error);
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { q } from '@roam-research/roam-api-sdk';
|
|
2
|
-
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler } from '../../search/index.js';
|
|
4
|
-
export class SearchOperations {
|
|
5
|
-
graph;
|
|
6
|
-
constructor(graph) {
|
|
7
|
-
this.graph = graph;
|
|
8
|
-
}
|
|
9
|
-
async searchBlockRefs(params) {
|
|
10
|
-
const handler = new BlockRefSearchHandler(this.graph, params);
|
|
11
|
-
return handler.execute();
|
|
12
|
-
}
|
|
13
|
-
async searchHierarchy(params) {
|
|
14
|
-
const handler = new HierarchySearchHandler(this.graph, params);
|
|
15
|
-
return handler.execute();
|
|
16
|
-
}
|
|
17
|
-
async searchByText(params) {
|
|
18
|
-
const handler = new TextSearchHandler(this.graph, params);
|
|
19
|
-
return handler.execute();
|
|
20
|
-
}
|
|
21
|
-
async searchByStatus(status, page_title_uid, include, exclude, case_sensitive = true) {
|
|
22
|
-
// Get target page UID if provided
|
|
23
|
-
let targetPageUid;
|
|
24
|
-
if (page_title_uid) {
|
|
25
|
-
// Try to find page by title or UID
|
|
26
|
-
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
27
|
-
const findResults = await q(this.graph, findQuery, [page_title_uid]);
|
|
28
|
-
if (findResults && findResults.length > 0) {
|
|
29
|
-
targetPageUid = findResults[0][0];
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
// Try as UID
|
|
33
|
-
const uidQuery = `[:find ?uid :where [?e :block/uid "${page_title_uid}"] [?e :block/uid ?uid]]`;
|
|
34
|
-
const uidResults = await q(this.graph, uidQuery, []);
|
|
35
|
-
if (!uidResults || uidResults.length === 0) {
|
|
36
|
-
throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${page_title_uid}" not found`);
|
|
37
|
-
}
|
|
38
|
-
targetPageUid = uidResults[0][0];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Build query based on whether we're searching in a specific page
|
|
42
|
-
let queryStr;
|
|
43
|
-
let queryParams;
|
|
44
|
-
const statusPattern = `{{[[${status}]]}}`;
|
|
45
|
-
if (targetPageUid) {
|
|
46
|
-
queryStr = `[:find ?block-uid ?block-str
|
|
47
|
-
:in $ ?status-pattern ?page-uid
|
|
48
|
-
:where [?p :block/uid ?page-uid]
|
|
49
|
-
[?b :block/page ?p]
|
|
50
|
-
[?b :block/string ?block-str]
|
|
51
|
-
[?b :block/uid ?block-uid]
|
|
52
|
-
[(clojure.string/includes? ?block-str ?status-pattern)]]`;
|
|
53
|
-
queryParams = [statusPattern, targetPageUid];
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
queryStr = `[:find ?block-uid ?block-str ?page-title
|
|
57
|
-
:in $ ?status-pattern
|
|
58
|
-
:where [?b :block/string ?block-str]
|
|
59
|
-
[?b :block/uid ?block-uid]
|
|
60
|
-
[?b :block/page ?p]
|
|
61
|
-
[?p :node/title ?page-title]
|
|
62
|
-
[(clojure.string/includes? ?block-str ?status-pattern)]]`;
|
|
63
|
-
queryParams = [statusPattern];
|
|
64
|
-
}
|
|
65
|
-
const results = await q(this.graph, queryStr, queryParams);
|
|
66
|
-
if (!results || results.length === 0) {
|
|
67
|
-
return {
|
|
68
|
-
success: true,
|
|
69
|
-
matches: [],
|
|
70
|
-
message: `No blocks found with status ${status}`
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// Format initial results
|
|
74
|
-
let matches = results.map(result => {
|
|
75
|
-
const [uid, content, pageTitle] = result;
|
|
76
|
-
return {
|
|
77
|
-
block_uid: uid,
|
|
78
|
-
content,
|
|
79
|
-
...(pageTitle && { page_title: pageTitle })
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
// Post-query filtering with case sensitivity option
|
|
83
|
-
if (include) {
|
|
84
|
-
const includeTerms = include.split(',').map(term => term.trim());
|
|
85
|
-
matches = matches.filter(match => {
|
|
86
|
-
const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
|
|
87
|
-
const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
|
|
88
|
-
const terms = case_sensitive ? includeTerms : includeTerms.map(t => t.toLowerCase());
|
|
89
|
-
return terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
|
|
90
|
-
(matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
if (exclude) {
|
|
94
|
-
const excludeTerms = exclude.split(',').map(term => term.trim());
|
|
95
|
-
matches = matches.filter(match => {
|
|
96
|
-
const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
|
|
97
|
-
const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
|
|
98
|
-
const terms = case_sensitive ? excludeTerms : excludeTerms.map(t => t.toLowerCase());
|
|
99
|
-
return !terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
|
|
100
|
-
(matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
success: true,
|
|
105
|
-
matches,
|
|
106
|
-
message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
async searchForTag(primary_tag, page_title_uid, near_tag, case_sensitive = true) {
|
|
110
|
-
// Ensure tags are properly formatted with #
|
|
111
|
-
const formatTag = (tag) => {
|
|
112
|
-
return tag.replace(/^#/, '').replace(/^\[\[/, '').replace(/\]\]$/, '');
|
|
113
|
-
};
|
|
114
|
-
// Extract the tag text, removing any formatting
|
|
115
|
-
const primaryTagFormatted = formatTag(primary_tag);
|
|
116
|
-
const nearTagFormatted = near_tag ? formatTag(near_tag) : undefined;
|
|
117
|
-
// Get target page UID if provided
|
|
118
|
-
let targetPageUid;
|
|
119
|
-
if (page_title_uid) {
|
|
120
|
-
// Try to find page by title or UID
|
|
121
|
-
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
122
|
-
const findResults = await q(this.graph, findQuery, [page_title_uid]);
|
|
123
|
-
if (findResults && findResults.length > 0) {
|
|
124
|
-
targetPageUid = findResults[0][0];
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
// Try as UID
|
|
128
|
-
const uidQuery = `[:find ?uid :where [?e :block/uid "${page_title_uid}"] [?e :block/uid ?uid]]`;
|
|
129
|
-
const uidResults = await q(this.graph, uidQuery, []);
|
|
130
|
-
if (!uidResults || uidResults.length === 0) {
|
|
131
|
-
throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${page_title_uid}" not found`);
|
|
132
|
-
}
|
|
133
|
-
targetPageUid = uidResults[0][0];
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Build query based on whether we're searching in a specific page and/or for a nearby tag
|
|
137
|
-
let queryStr;
|
|
138
|
-
let queryParams;
|
|
139
|
-
if (targetPageUid) {
|
|
140
|
-
if (nearTagFormatted) {
|
|
141
|
-
queryStr = `[:find ?block-uid ?block-str
|
|
142
|
-
:in $ ?primary-tag ?near-tag ?page-uid
|
|
143
|
-
:where [?p :block/uid ?page-uid]
|
|
144
|
-
[?b :block/page ?p]
|
|
145
|
-
[?b :block/string ?block-str]
|
|
146
|
-
[?b :block/uid ?block-uid]
|
|
147
|
-
[(clojure.string/includes?
|
|
148
|
-
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
149
|
-
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
|
|
150
|
-
[(clojure.string/includes?
|
|
151
|
-
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
152
|
-
${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
|
|
153
|
-
queryParams = [primaryTagFormatted, nearTagFormatted, targetPageUid];
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
queryStr = `[:find ?block-uid ?block-str
|
|
157
|
-
:in $ ?primary-tag ?page-uid
|
|
158
|
-
:where [?p :block/uid ?page-uid]
|
|
159
|
-
[?b :block/page ?p]
|
|
160
|
-
[?b :block/string ?block-str]
|
|
161
|
-
[?b :block/uid ?block-uid]
|
|
162
|
-
[(clojure.string/includes?
|
|
163
|
-
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
164
|
-
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
|
|
165
|
-
queryParams = [primaryTagFormatted, targetPageUid];
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
// Search across all pages
|
|
170
|
-
if (nearTagFormatted) {
|
|
171
|
-
queryStr = `[:find ?block-uid ?block-str ?page-title
|
|
172
|
-
:in $ ?primary-tag ?near-tag
|
|
173
|
-
:where [?b :block/string ?block-str]
|
|
174
|
-
[?b :block/uid ?block-uid]
|
|
175
|
-
[?b :block/page ?p]
|
|
176
|
-
[?p :node/title ?page-title]
|
|
177
|
-
[(clojure.string/includes?
|
|
178
|
-
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
179
|
-
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
|
|
180
|
-
[(clojure.string/includes?
|
|
181
|
-
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
182
|
-
${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
|
|
183
|
-
queryParams = [primaryTagFormatted, nearTagFormatted];
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
queryStr = `[:find ?block-uid ?block-str ?page-title
|
|
187
|
-
:in $ ?primary-tag
|
|
188
|
-
:where [?b :block/string ?block-str]
|
|
189
|
-
[?b :block/uid ?block-uid]
|
|
190
|
-
[?b :block/page ?p]
|
|
191
|
-
[?p :node/title ?page-title]
|
|
192
|
-
[(clojure.string/includes?
|
|
193
|
-
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
194
|
-
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
|
|
195
|
-
queryParams = [primaryTagFormatted];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const results = await q(this.graph, queryStr, queryParams);
|
|
199
|
-
if (!results || results.length === 0) {
|
|
200
|
-
return {
|
|
201
|
-
success: true,
|
|
202
|
-
matches: [],
|
|
203
|
-
message: `No blocks found containing ${primaryTagFormatted}${nearTagFormatted ? ` near ${nearTagFormatted}` : ''}`
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
// Format results
|
|
207
|
-
const matches = results.map(([uid, content, pageTitle]) => ({
|
|
208
|
-
block_uid: uid,
|
|
209
|
-
content,
|
|
210
|
-
...(pageTitle && { page_title: pageTitle })
|
|
211
|
-
}));
|
|
212
|
-
return {
|
|
213
|
-
success: true,
|
|
214
|
-
matches,
|
|
215
|
-
message: `Found ${matches.length} block(s) containing ${primaryTagFormatted}${nearTagFormatted ? ` near ${nearTagFormatted}` : ''}`
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
async searchByDate(params) {
|
|
219
|
-
// Convert dates to timestamps
|
|
220
|
-
const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
|
|
221
|
-
const endTimestamp = params.end_date ? new Date(`${params.end_date}T23:59:59`).getTime() : undefined;
|
|
222
|
-
// Define rule for entity type
|
|
223
|
-
const entityRule = `[
|
|
224
|
-
[(block? ?e)
|
|
225
|
-
[?e :block/string]
|
|
226
|
-
[?e :block/page ?p]
|
|
227
|
-
[?p :node/title]]
|
|
228
|
-
[(page? ?e)
|
|
229
|
-
[?e :node/title]]
|
|
230
|
-
]`;
|
|
231
|
-
// Build query based on cheatsheet pattern
|
|
232
|
-
const timeAttr = params.type === 'created' ? ':create/time' : ':edit/time';
|
|
233
|
-
let queryStr = `[:find ?block-uid ?string ?time ?page-title
|
|
234
|
-
:in $ ?start-ts ${endTimestamp ? '?end-ts' : ''}
|
|
235
|
-
:where
|
|
236
|
-
[?b ${timeAttr} ?time]
|
|
237
|
-
[(>= ?time ?start-ts)]
|
|
238
|
-
${endTimestamp ? '[(<= ?time ?end-ts)]' : ''}
|
|
239
|
-
[?b :block/uid ?block-uid]
|
|
240
|
-
[?b :block/string ?string]
|
|
241
|
-
[?b :block/page ?p]
|
|
242
|
-
[?p :node/title ?page-title]]`;
|
|
243
|
-
// Execute query
|
|
244
|
-
const queryParams = endTimestamp ?
|
|
245
|
-
[startTimestamp, endTimestamp] :
|
|
246
|
-
[startTimestamp];
|
|
247
|
-
const results = await q(this.graph, queryStr, queryParams);
|
|
248
|
-
if (!results || results.length === 0) {
|
|
249
|
-
return {
|
|
250
|
-
success: true,
|
|
251
|
-
matches: [],
|
|
252
|
-
message: 'No matches found for the given date range and criteria'
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
// Process results - now we get [block-uid, string, time, page-title]
|
|
256
|
-
const matches = results.map(([uid, content, time, pageTitle]) => ({
|
|
257
|
-
uid,
|
|
258
|
-
type: 'block',
|
|
259
|
-
time,
|
|
260
|
-
...(params.include_content && { content }),
|
|
261
|
-
page_title: pageTitle
|
|
262
|
-
}));
|
|
263
|
-
// Apply case sensitivity if content is included
|
|
264
|
-
if (params.include_content) {
|
|
265
|
-
const case_sensitive = params.case_sensitive ?? true; // Default to true to match Roam's behavior
|
|
266
|
-
if (!case_sensitive) {
|
|
267
|
-
matches.forEach(match => {
|
|
268
|
-
if (match.content) {
|
|
269
|
-
match.content = match.content.toLowerCase();
|
|
270
|
-
}
|
|
271
|
-
if (match.page_title) {
|
|
272
|
-
match.page_title = match.page_title.toLowerCase();
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
// Sort by time
|
|
278
|
-
const sortedMatches = matches.sort((a, b) => b.time - a.time);
|
|
279
|
-
return {
|
|
280
|
-
success: true,
|
|
281
|
-
matches: sortedMatches,
|
|
282
|
-
message: `Found ${sortedMatches.length} matches for the given date range and criteria`
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
}
|