roam-research-mcp 0.20.0 → 0.22.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 +3 -2
- package/build/search/text-search.js +3 -4
- package/build/server/roam-server.js +12 -10
- package/build/tools/helpers/refs.js +47 -0
- package/build/tools/helpers/text.js +6 -0
- package/build/tools/operations/blocks.js +269 -0
- package/build/tools/operations/memory.js +102 -0
- package/build/tools/operations/outline.js +347 -0
- package/build/tools/operations/pages.js +213 -0
- package/build/tools/operations/search/handlers.js +56 -0
- package/build/tools/operations/search/index.js +95 -0
- package/build/tools/operations/search/types.js +1 -0
- package/build/tools/operations/search.js +285 -0
- package/build/tools/operations/todos.js +82 -0
- package/build/tools/schemas.js +15 -24
- package/build/tools/tool-handlers.js +57 -1196
- package/build/tools/types/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl } from './handlers.js';
|
|
2
|
+
export class SearchOperations {
|
|
3
|
+
graph;
|
|
4
|
+
constructor(graph) {
|
|
5
|
+
this.graph = graph;
|
|
6
|
+
}
|
|
7
|
+
async searchByStatus(status, page_title_uid, include, exclude) {
|
|
8
|
+
const handler = new TagSearchHandlerImpl(this.graph, {
|
|
9
|
+
primary_tag: `{{[[${status}]]}}`,
|
|
10
|
+
page_title_uid,
|
|
11
|
+
});
|
|
12
|
+
const result = await handler.execute();
|
|
13
|
+
// Post-process results with include/exclude filters
|
|
14
|
+
let matches = result.matches;
|
|
15
|
+
if (include) {
|
|
16
|
+
const includeTerms = include.split(',').map(term => term.trim());
|
|
17
|
+
matches = matches.filter(match => {
|
|
18
|
+
const matchContent = match.content;
|
|
19
|
+
const matchTitle = match.page_title;
|
|
20
|
+
const terms = includeTerms;
|
|
21
|
+
return terms.some(term => matchContent.includes(term) ||
|
|
22
|
+
(matchTitle && matchTitle.includes(term)));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (exclude) {
|
|
26
|
+
const excludeTerms = exclude.split(',').map(term => term.trim());
|
|
27
|
+
matches = matches.filter(match => {
|
|
28
|
+
const matchContent = match.content;
|
|
29
|
+
const matchTitle = match.page_title;
|
|
30
|
+
const terms = excludeTerms;
|
|
31
|
+
return !terms.some(term => matchContent.includes(term) ||
|
|
32
|
+
(matchTitle && matchTitle.includes(term)));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
matches,
|
|
38
|
+
message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async searchForTag(primary_tag, page_title_uid, near_tag) {
|
|
42
|
+
const handler = new TagSearchHandlerImpl(this.graph, {
|
|
43
|
+
primary_tag,
|
|
44
|
+
page_title_uid,
|
|
45
|
+
near_tag,
|
|
46
|
+
});
|
|
47
|
+
return handler.execute();
|
|
48
|
+
}
|
|
49
|
+
async searchBlockRefs(params) {
|
|
50
|
+
const handler = new BlockRefSearchHandlerImpl(this.graph, params);
|
|
51
|
+
return handler.execute();
|
|
52
|
+
}
|
|
53
|
+
async searchHierarchy(params) {
|
|
54
|
+
const handler = new HierarchySearchHandlerImpl(this.graph, params);
|
|
55
|
+
return handler.execute();
|
|
56
|
+
}
|
|
57
|
+
async searchByText(params) {
|
|
58
|
+
const handler = new TextSearchHandlerImpl(this.graph, params);
|
|
59
|
+
return handler.execute();
|
|
60
|
+
}
|
|
61
|
+
async searchByDate(params) {
|
|
62
|
+
// Convert dates to timestamps
|
|
63
|
+
const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
|
|
64
|
+
const endTimestamp = params.end_date ? new Date(`${params.end_date}T23:59:59`).getTime() : undefined;
|
|
65
|
+
// Use text search handler for content-based filtering
|
|
66
|
+
const handler = new TextSearchHandlerImpl(this.graph, {
|
|
67
|
+
text: '', // Empty text to match all blocks
|
|
68
|
+
});
|
|
69
|
+
const result = await handler.execute();
|
|
70
|
+
// Filter results by date
|
|
71
|
+
const matches = result.matches
|
|
72
|
+
.filter(match => {
|
|
73
|
+
const time = params.type === 'created' ?
|
|
74
|
+
new Date(match.content || '').getTime() : // Use content date for creation time
|
|
75
|
+
Date.now(); // Use current time for modification time (simplified)
|
|
76
|
+
return time >= startTimestamp && (!endTimestamp || time <= endTimestamp);
|
|
77
|
+
})
|
|
78
|
+
.map(match => ({
|
|
79
|
+
uid: match.block_uid,
|
|
80
|
+
type: 'block',
|
|
81
|
+
time: params.type === 'created' ?
|
|
82
|
+
new Date(match.content || '').getTime() :
|
|
83
|
+
Date.now(),
|
|
84
|
+
...(params.include_content && { content: match.content }),
|
|
85
|
+
page_title: match.page_title
|
|
86
|
+
}));
|
|
87
|
+
// Sort by time
|
|
88
|
+
const sortedMatches = matches.sort((a, b) => b.time - a.time);
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
matches: sortedMatches,
|
|
92
|
+
message: `Found ${sortedMatches.length} matches for the given date range and criteria`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { q, createBlock, createPage, batchActions } from '@roam-research/roam-api-sdk';
|
|
2
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
|
+
export class TodoOperations {
|
|
5
|
+
graph;
|
|
6
|
+
constructor(graph) {
|
|
7
|
+
this.graph = graph;
|
|
8
|
+
}
|
|
9
|
+
async addTodos(todos) {
|
|
10
|
+
if (!Array.isArray(todos) || todos.length === 0) {
|
|
11
|
+
throw new McpError(ErrorCode.InvalidRequest, 'todos must be a non-empty array');
|
|
12
|
+
}
|
|
13
|
+
// Get today's date
|
|
14
|
+
const today = new Date();
|
|
15
|
+
const dateStr = formatRoamDate(today);
|
|
16
|
+
// Try to find today's page
|
|
17
|
+
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
18
|
+
const findResults = await q(this.graph, findQuery, [dateStr]);
|
|
19
|
+
let targetPageUid;
|
|
20
|
+
if (findResults && findResults.length > 0) {
|
|
21
|
+
targetPageUid = findResults[0][0];
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Create today's page if it doesn't exist
|
|
25
|
+
try {
|
|
26
|
+
await createPage(this.graph, {
|
|
27
|
+
action: 'create-page',
|
|
28
|
+
page: { title: dateStr }
|
|
29
|
+
});
|
|
30
|
+
// Get the new page's UID
|
|
31
|
+
const results = await q(this.graph, findQuery, [dateStr]);
|
|
32
|
+
if (!results || results.length === 0) {
|
|
33
|
+
throw new Error('Could not find created today\'s page');
|
|
34
|
+
}
|
|
35
|
+
targetPageUid = results[0][0];
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error('Failed to create today\'s page');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// If more than 10 todos, use batch actions
|
|
42
|
+
const todo_tag = "{{TODO}}";
|
|
43
|
+
if (todos.length > 10) {
|
|
44
|
+
const actions = todos.map((todo, index) => ({
|
|
45
|
+
action: 'create-block',
|
|
46
|
+
location: {
|
|
47
|
+
'parent-uid': targetPageUid,
|
|
48
|
+
order: index
|
|
49
|
+
},
|
|
50
|
+
block: {
|
|
51
|
+
string: `${todo_tag} ${todo}`
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
const result = await batchActions(this.graph, {
|
|
55
|
+
action: 'batch-actions',
|
|
56
|
+
actions
|
|
57
|
+
});
|
|
58
|
+
if (!result) {
|
|
59
|
+
throw new Error('Failed to create todo blocks');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Create todos sequentially
|
|
64
|
+
for (const todo of todos) {
|
|
65
|
+
try {
|
|
66
|
+
await createBlock(this.graph, {
|
|
67
|
+
action: 'create-block',
|
|
68
|
+
location: {
|
|
69
|
+
"parent-uid": targetPageUid,
|
|
70
|
+
"order": "last"
|
|
71
|
+
},
|
|
72
|
+
block: { string: `${todo_tag} ${todo}` }
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error('Failed to create todo block');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { success: true };
|
|
81
|
+
}
|
|
82
|
+
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -73,8 +73,8 @@ export const toolSchemas = {
|
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
roam_create_outline: {
|
|
76
|
-
name: '
|
|
77
|
-
description: 'Create a structured outline
|
|
76
|
+
name: 'roam_create_outline',
|
|
77
|
+
description: 'Create a structured outline with nested structure in Roam from an array of items with explicit levels. Can be added on a specific page or under a specific block. Ideal for saving a conversation with an LLM response, research, or organizing thoughts.',
|
|
78
78
|
inputSchema: {
|
|
79
79
|
type: 'object',
|
|
80
80
|
properties: {
|
|
@@ -98,7 +98,9 @@ export const toolSchemas = {
|
|
|
98
98
|
},
|
|
99
99
|
level: {
|
|
100
100
|
type: 'integer',
|
|
101
|
-
description: 'Indentation level (1-
|
|
101
|
+
description: 'Indentation level (1-10, where 1 is top level)',
|
|
102
|
+
minimum: 1,
|
|
103
|
+
maximum: 10
|
|
102
104
|
}
|
|
103
105
|
},
|
|
104
106
|
required: ['text', 'level']
|
|
@@ -161,11 +163,6 @@ export const toolSchemas = {
|
|
|
161
163
|
near_tag: {
|
|
162
164
|
type: 'string',
|
|
163
165
|
description: 'Optional: Another tag to filter results by - will only return blocks where both tags appear',
|
|
164
|
-
},
|
|
165
|
-
case_sensitive: {
|
|
166
|
-
type: 'boolean',
|
|
167
|
-
description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
|
|
168
|
-
default: true
|
|
169
166
|
}
|
|
170
167
|
},
|
|
171
168
|
required: ['primary_tag']
|
|
@@ -193,11 +190,6 @@ export const toolSchemas = {
|
|
|
193
190
|
exclude: {
|
|
194
191
|
type: 'string',
|
|
195
192
|
description: 'Optional: Comma-separated list of terms to filter results by exclusion (matches content or page title)'
|
|
196
|
-
},
|
|
197
|
-
case_sensitive: {
|
|
198
|
-
type: 'boolean',
|
|
199
|
-
description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
|
|
200
|
-
default: true
|
|
201
193
|
}
|
|
202
194
|
},
|
|
203
195
|
required: ['status']
|
|
@@ -273,11 +265,6 @@ export const toolSchemas = {
|
|
|
273
265
|
page_title_uid: {
|
|
274
266
|
type: 'string',
|
|
275
267
|
description: 'Optional: Title or UID of the page to search in. If not provided, searches across all pages'
|
|
276
|
-
},
|
|
277
|
-
case_sensitive: {
|
|
278
|
-
type: 'boolean',
|
|
279
|
-
description: 'Optional: Whether to perform case-sensitive search (default: true, matching Roam\'s native behavior)',
|
|
280
|
-
default: true
|
|
281
268
|
}
|
|
282
269
|
},
|
|
283
270
|
required: ['text']
|
|
@@ -405,11 +392,6 @@ export const toolSchemas = {
|
|
|
405
392
|
type: 'boolean',
|
|
406
393
|
description: 'Whether to include the content of matching blocks/pages',
|
|
407
394
|
default: true,
|
|
408
|
-
},
|
|
409
|
-
case_sensitive: {
|
|
410
|
-
type: 'boolean',
|
|
411
|
-
description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
|
|
412
|
-
default: true
|
|
413
395
|
}
|
|
414
396
|
},
|
|
415
397
|
required: ['start_date', 'type', 'scope']
|
|
@@ -417,7 +399,7 @@ export const toolSchemas = {
|
|
|
417
399
|
},
|
|
418
400
|
roam_remember: {
|
|
419
401
|
name: 'roam_remember',
|
|
420
|
-
description: 'Add a memory or piece of information to remember, stored on the daily page with #[[LLM/Memories]] tag and optional categories.
|
|
402
|
+
description: 'Add a memory or piece of information to remember, stored on the daily page with #[[LLM/Memories]] 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).',
|
|
421
403
|
inputSchema: {
|
|
422
404
|
type: 'object',
|
|
423
405
|
properties: {
|
|
@@ -435,5 +417,14 @@ export const toolSchemas = {
|
|
|
435
417
|
},
|
|
436
418
|
required: ['memory']
|
|
437
419
|
}
|
|
420
|
+
},
|
|
421
|
+
roam_recall: {
|
|
422
|
+
name: 'roam_recall',
|
|
423
|
+
description: 'Retrieve all stored memories by searching for blocks tagged with MEMORIES_TAG and content from the page with the same name. Returns a combined, deduplicated list of memories.',
|
|
424
|
+
inputSchema: {
|
|
425
|
+
type: 'object',
|
|
426
|
+
properties: {},
|
|
427
|
+
required: []
|
|
428
|
+
}
|
|
438
429
|
}
|
|
439
430
|
};
|