roam-research-mcp 2.4.0 → 2.13.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 +175 -667
- package/build/Roam_Markdown_Cheatsheet.md +138 -289
- package/build/cache/page-uid-cache.js +40 -2
- package/build/cli/batch/translator.js +1 -1
- package/build/cli/commands/batch.js +3 -8
- package/build/cli/commands/get.js +478 -60
- package/build/cli/commands/refs.js +51 -31
- package/build/cli/commands/save.js +61 -10
- package/build/cli/commands/search.js +63 -58
- package/build/cli/commands/status.js +3 -4
- package/build/cli/commands/update.js +71 -28
- package/build/cli/utils/graph.js +6 -2
- package/build/cli/utils/input.js +10 -0
- package/build/cli/utils/output.js +28 -5
- package/build/cli/utils/sort-group.js +110 -0
- package/build/config/graph-registry.js +31 -13
- package/build/config/graph-registry.test.js +42 -5
- package/build/markdown-utils.js +114 -4
- package/build/markdown-utils.test.js +125 -0
- package/build/query/generator.js +330 -0
- package/build/query/index.js +149 -0
- package/build/query/parser.js +319 -0
- package/build/query/parser.test.js +389 -0
- package/build/query/types.js +4 -0
- package/build/search/ancestor-rule.js +14 -0
- package/build/search/block-ref-search.js +1 -5
- package/build/search/hierarchy-search.js +5 -12
- package/build/search/index.js +1 -0
- package/build/search/status-search.js +10 -9
- package/build/search/tag-search.js +8 -24
- package/build/search/text-search.js +70 -27
- package/build/search/types.js +13 -0
- package/build/search/utils.js +71 -2
- package/build/server/roam-server.js +4 -3
- package/build/shared/index.js +2 -0
- package/build/shared/page-validator.js +233 -0
- package/build/shared/page-validator.test.js +128 -0
- package/build/shared/staged-batch.js +144 -0
- package/build/tools/helpers/batch-utils.js +57 -0
- package/build/tools/helpers/page-resolution.js +136 -0
- package/build/tools/helpers/refs.js +68 -0
- package/build/tools/operations/batch.js +75 -3
- package/build/tools/operations/block-retrieval.js +15 -4
- package/build/tools/operations/block-retrieval.test.js +87 -0
- package/build/tools/operations/blocks.js +1 -288
- package/build/tools/operations/memory.js +32 -90
- package/build/tools/operations/outline.js +38 -156
- package/build/tools/operations/pages.js +169 -122
- package/build/tools/operations/todos.js +5 -37
- package/build/tools/schemas.js +20 -9
- package/build/tools/tool-handlers.js +4 -4
- package/build/utils/helpers.js +27 -0
- package/package.json +1 -1
|
@@ -1,194 +1,9 @@
|
|
|
1
|
-
import { q,
|
|
1
|
+
import { q, moveBlock as moveRoamBlock } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
|
-
import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown } from '../../markdown-utils.js';
|
|
5
3
|
export class BlockOperations {
|
|
6
4
|
constructor(graph) {
|
|
7
5
|
this.graph = graph;
|
|
8
6
|
}
|
|
9
|
-
async createBlock(content, page_uid, title, heading) {
|
|
10
|
-
// If page_uid provided, use it directly
|
|
11
|
-
let targetPageUid = page_uid;
|
|
12
|
-
// If no page_uid but title provided, search for page by title
|
|
13
|
-
if (!targetPageUid && title) {
|
|
14
|
-
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
15
|
-
const findResults = await q(this.graph, findQuery, [title]);
|
|
16
|
-
if (findResults && findResults.length > 0) {
|
|
17
|
-
targetPageUid = findResults[0][0];
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
// Create page with provided title if it doesn't exist
|
|
21
|
-
try {
|
|
22
|
-
await createPage(this.graph, {
|
|
23
|
-
action: 'create-page',
|
|
24
|
-
page: { title }
|
|
25
|
-
});
|
|
26
|
-
// Get the new page's UID
|
|
27
|
-
const results = await q(this.graph, findQuery, [title]);
|
|
28
|
-
if (!results || results.length === 0) {
|
|
29
|
-
throw new Error('Could not find created page');
|
|
30
|
-
}
|
|
31
|
-
targetPageUid = results[0][0];
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create page: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// If neither page_uid nor title provided, use today's date page
|
|
39
|
-
if (!targetPageUid) {
|
|
40
|
-
const today = new Date();
|
|
41
|
-
const dateStr = formatRoamDate(today);
|
|
42
|
-
// Try to find today's page
|
|
43
|
-
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
44
|
-
const findResults = await q(this.graph, findQuery, [dateStr]);
|
|
45
|
-
if (findResults && findResults.length > 0) {
|
|
46
|
-
targetPageUid = findResults[0][0];
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// Create today's page if it doesn't exist
|
|
50
|
-
try {
|
|
51
|
-
await createPage(this.graph, {
|
|
52
|
-
action: 'create-page',
|
|
53
|
-
page: { title: dateStr }
|
|
54
|
-
});
|
|
55
|
-
// Get the new page's UID
|
|
56
|
-
const results = await q(this.graph, findQuery, [dateStr]);
|
|
57
|
-
if (!results || results.length === 0) {
|
|
58
|
-
throw new Error('Could not find created today\'s page');
|
|
59
|
-
}
|
|
60
|
-
targetPageUid = results[0][0];
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create today's page: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
// If the content has multiple lines or is a table, use nested import
|
|
69
|
-
if (content.includes('\n')) {
|
|
70
|
-
let nodes;
|
|
71
|
-
// If heading parameter is provided, manually construct nodes to preserve heading
|
|
72
|
-
if (heading) {
|
|
73
|
-
const lines = content.split('\n');
|
|
74
|
-
const firstLine = lines[0].trim();
|
|
75
|
-
const remainingLines = lines.slice(1);
|
|
76
|
-
// Create the first node with heading formatting
|
|
77
|
-
const firstNode = {
|
|
78
|
-
content: firstLine,
|
|
79
|
-
level: 0,
|
|
80
|
-
heading_level: heading,
|
|
81
|
-
children: []
|
|
82
|
-
};
|
|
83
|
-
// If there are remaining lines, parse them as children or siblings
|
|
84
|
-
if (remainingLines.length > 0 && remainingLines.some(line => line.trim())) {
|
|
85
|
-
const remainingContent = remainingLines.join('\n');
|
|
86
|
-
const convertedRemainingContent = convertToRoamMarkdown(remainingContent);
|
|
87
|
-
const remainingNodes = parseMarkdown(convertedRemainingContent);
|
|
88
|
-
// Add remaining nodes as siblings to the first node
|
|
89
|
-
nodes = [firstNode, ...remainingNodes];
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
nodes = [firstNode];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
// No heading parameter, use original parsing logic
|
|
97
|
-
const convertedContent = convertToRoamMarkdown(content);
|
|
98
|
-
nodes = parseMarkdown(convertedContent);
|
|
99
|
-
}
|
|
100
|
-
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
101
|
-
// Execute batch actions to create the nested structure
|
|
102
|
-
const result = await batchActions(this.graph, {
|
|
103
|
-
action: 'batch-actions',
|
|
104
|
-
actions
|
|
105
|
-
});
|
|
106
|
-
if (!result) {
|
|
107
|
-
throw new Error('Failed to create nested blocks');
|
|
108
|
-
}
|
|
109
|
-
const blockUid = result.created_uids?.[0];
|
|
110
|
-
return {
|
|
111
|
-
success: true,
|
|
112
|
-
block_uid: blockUid,
|
|
113
|
-
parent_uid: targetPageUid
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// For single block content, use the same convertToRoamActions approach that works in roam_create_page
|
|
118
|
-
const nodes = [{
|
|
119
|
-
content: content,
|
|
120
|
-
level: 0,
|
|
121
|
-
...(heading && typeof heading === 'number' && heading > 0 && { heading_level: heading }),
|
|
122
|
-
children: []
|
|
123
|
-
}];
|
|
124
|
-
if (!targetPageUid) {
|
|
125
|
-
throw new McpError(ErrorCode.InternalError, 'targetPageUid is undefined');
|
|
126
|
-
}
|
|
127
|
-
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
128
|
-
// Execute batch actions to create the block
|
|
129
|
-
const result = await batchActions(this.graph, {
|
|
130
|
-
action: 'batch-actions',
|
|
131
|
-
actions
|
|
132
|
-
});
|
|
133
|
-
if (!result) {
|
|
134
|
-
throw new Error('Failed to create block');
|
|
135
|
-
}
|
|
136
|
-
const blockUid = result.created_uids?.[0];
|
|
137
|
-
return {
|
|
138
|
-
success: true,
|
|
139
|
-
block_uid: blockUid,
|
|
140
|
-
parent_uid: targetPageUid
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create block: ${error instanceof Error ? error.message : String(error)}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async updateBlock(block_uid, content, transform) {
|
|
149
|
-
if (!block_uid) {
|
|
150
|
-
throw new McpError(ErrorCode.InvalidRequest, 'block_uid is required');
|
|
151
|
-
}
|
|
152
|
-
// Get current block content
|
|
153
|
-
const blockQuery = `[:find ?string .
|
|
154
|
-
:where [?b :block/uid "${block_uid}"]
|
|
155
|
-
[?b :block/string ?string]]`;
|
|
156
|
-
const result = await q(this.graph, blockQuery, []);
|
|
157
|
-
if (result === null || result === undefined) {
|
|
158
|
-
throw new McpError(ErrorCode.InvalidRequest, `Block with UID "${block_uid}" not found`);
|
|
159
|
-
}
|
|
160
|
-
const currentContent = String(result);
|
|
161
|
-
if (currentContent === null || currentContent === undefined) {
|
|
162
|
-
throw new McpError(ErrorCode.InvalidRequest, `Block with UID "${block_uid}" not found`);
|
|
163
|
-
}
|
|
164
|
-
// Determine new content
|
|
165
|
-
let newContent;
|
|
166
|
-
if (content) {
|
|
167
|
-
newContent = content;
|
|
168
|
-
}
|
|
169
|
-
else if (transform) {
|
|
170
|
-
newContent = transform(currentContent);
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
throw new McpError(ErrorCode.InvalidRequest, 'Either content or transform function must be provided');
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
176
|
-
await updateRoamBlock(this.graph, {
|
|
177
|
-
action: 'update-block',
|
|
178
|
-
block: {
|
|
179
|
-
uid: block_uid,
|
|
180
|
-
string: newContent
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
return {
|
|
184
|
-
success: true,
|
|
185
|
-
content: newContent
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
throw new McpError(ErrorCode.InternalError, `Failed to update block: ${error.message}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
7
|
async moveBlock(block_uid, parent_uid, order = 'last') {
|
|
193
8
|
if (!block_uid) {
|
|
194
9
|
throw new McpError(ErrorCode.InvalidRequest, 'block_uid is required');
|
|
@@ -226,106 +41,4 @@ export class BlockOperations {
|
|
|
226
41
|
throw new McpError(ErrorCode.InternalError, `Failed to move block: ${error.message}`);
|
|
227
42
|
}
|
|
228
43
|
}
|
|
229
|
-
async updateBlocks(updates) {
|
|
230
|
-
if (!Array.isArray(updates) || updates.length === 0) {
|
|
231
|
-
throw new McpError(ErrorCode.InvalidRequest, 'updates must be a non-empty array');
|
|
232
|
-
}
|
|
233
|
-
// Validate each update has required fields
|
|
234
|
-
updates.forEach((update, index) => {
|
|
235
|
-
if (!update.block_uid) {
|
|
236
|
-
throw new McpError(ErrorCode.InvalidRequest, `Update at index ${index} missing block_uid`);
|
|
237
|
-
}
|
|
238
|
-
if (!update.content && !update.transform) {
|
|
239
|
-
throw new McpError(ErrorCode.InvalidRequest, `Update at index ${index} must have either content or transform`);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
// Get current content for all blocks
|
|
243
|
-
const blockUids = updates.map(u => u.block_uid);
|
|
244
|
-
const blockQuery = `[:find ?uid ?string
|
|
245
|
-
:in $ [?uid ...]
|
|
246
|
-
:where [?b :block/uid ?uid]
|
|
247
|
-
[?b :block/string ?string]]`;
|
|
248
|
-
const blockResults = await q(this.graph, blockQuery, [blockUids]);
|
|
249
|
-
// Create map of uid -> current content
|
|
250
|
-
const contentMap = new Map();
|
|
251
|
-
blockResults.forEach(([uid, string]) => {
|
|
252
|
-
contentMap.set(uid, string);
|
|
253
|
-
});
|
|
254
|
-
// Prepare batch actions
|
|
255
|
-
const actions = [];
|
|
256
|
-
const results = [];
|
|
257
|
-
for (const update of updates) {
|
|
258
|
-
try {
|
|
259
|
-
const currentContent = contentMap.get(update.block_uid);
|
|
260
|
-
if (!currentContent) {
|
|
261
|
-
results.push({
|
|
262
|
-
block_uid: update.block_uid,
|
|
263
|
-
content: '',
|
|
264
|
-
success: false,
|
|
265
|
-
error: `Block with UID "${update.block_uid}" not found`
|
|
266
|
-
});
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
// Determine new content
|
|
270
|
-
let newContent;
|
|
271
|
-
if (update.content) {
|
|
272
|
-
newContent = update.content;
|
|
273
|
-
}
|
|
274
|
-
else if (update.transform) {
|
|
275
|
-
const regex = new RegExp(update.transform.find, update.transform.global ? 'g' : '');
|
|
276
|
-
newContent = currentContent.replace(regex, update.transform.replace);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
// This shouldn't happen due to earlier validation
|
|
280
|
-
throw new Error('Invalid update configuration');
|
|
281
|
-
}
|
|
282
|
-
// Add to batch actions
|
|
283
|
-
actions.push({
|
|
284
|
-
action: 'update-block',
|
|
285
|
-
block: {
|
|
286
|
-
uid: update.block_uid,
|
|
287
|
-
string: newContent
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
results.push({
|
|
291
|
-
block_uid: update.block_uid,
|
|
292
|
-
content: newContent,
|
|
293
|
-
success: true
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
results.push({
|
|
298
|
-
block_uid: update.block_uid,
|
|
299
|
-
content: contentMap.get(update.block_uid) || '',
|
|
300
|
-
success: false,
|
|
301
|
-
error: error.message
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// Execute batch update if we have any valid actions
|
|
306
|
-
if (actions.length > 0) {
|
|
307
|
-
try {
|
|
308
|
-
const batchResult = await batchActions(this.graph, {
|
|
309
|
-
action: 'batch-actions',
|
|
310
|
-
actions
|
|
311
|
-
});
|
|
312
|
-
if (!batchResult) {
|
|
313
|
-
throw new Error('Batch update failed');
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
// Mark all previously successful results as failed
|
|
318
|
-
results.forEach(result => {
|
|
319
|
-
if (result.success) {
|
|
320
|
-
result.success = false;
|
|
321
|
-
result.error = `Batch update failed: ${error.message}`;
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return {
|
|
327
|
-
success: results.every(r => r.success),
|
|
328
|
-
results
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
44
|
}
|
|
@@ -1,53 +1,20 @@
|
|
|
1
|
-
import { q
|
|
1
|
+
import { q } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
3
|
import { generateBlockUid } from '../../markdown-utils.js';
|
|
4
|
+
import { ANCESTOR_RULE } from '../../search/ancestor-rule.js';
|
|
5
5
|
import { resolveRefs } from '../helpers/refs.js';
|
|
6
|
+
import { getOrCreateTodayPage } from '../helpers/page-resolution.js';
|
|
7
|
+
import { executeBatch } from '../helpers/batch-utils.js';
|
|
6
8
|
import { SearchOperations } from './search/index.js';
|
|
7
|
-
import { pageUidCache } from '../../cache/page-uid-cache.js';
|
|
8
9
|
export class MemoryOperations {
|
|
9
|
-
constructor(graph) {
|
|
10
|
+
constructor(graph, memoriesTag = 'Memories') {
|
|
10
11
|
this.graph = graph;
|
|
11
12
|
this.searchOps = new SearchOperations(graph);
|
|
13
|
+
this.memoriesTag = memoriesTag;
|
|
12
14
|
}
|
|
13
|
-
async remember(memory, categories, heading, parent_uid) {
|
|
14
|
-
// Get today's
|
|
15
|
-
const
|
|
16
|
-
const dateStr = formatRoamDate(today);
|
|
17
|
-
let pageUid;
|
|
18
|
-
// Check cache first for today's page
|
|
19
|
-
const cachedUid = pageUidCache.get(dateStr);
|
|
20
|
-
if (cachedUid) {
|
|
21
|
-
pageUid = cachedUid;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
// Try to find today's page
|
|
25
|
-
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
26
|
-
const findResults = await q(this.graph, findQuery, [dateStr]);
|
|
27
|
-
if (findResults && findResults.length > 0) {
|
|
28
|
-
pageUid = findResults[0][0];
|
|
29
|
-
pageUidCache.set(dateStr, pageUid);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
// Create today's page if it doesn't exist
|
|
33
|
-
try {
|
|
34
|
-
await createPage(this.graph, {
|
|
35
|
-
action: 'create-page',
|
|
36
|
-
page: { title: dateStr }
|
|
37
|
-
});
|
|
38
|
-
// Get the new page's UID
|
|
39
|
-
const results = await q(this.graph, findQuery, [dateStr]);
|
|
40
|
-
if (!results || results.length === 0) {
|
|
41
|
-
throw new McpError(ErrorCode.InternalError, 'Could not find created today\'s page');
|
|
42
|
-
}
|
|
43
|
-
pageUid = results[0][0];
|
|
44
|
-
pageUidCache.onPageCreated(dateStr, pageUid);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
throw new McpError(ErrorCode.InternalError, 'Failed to create today\'s page');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
15
|
+
async remember(memory, categories, heading, parent_uid, include_memories_tag = true) {
|
|
16
|
+
// Get or create today's daily page
|
|
17
|
+
const pageUid = await getOrCreateTodayPage(this.graph);
|
|
51
18
|
// Determine parent block for the memory
|
|
52
19
|
let targetParentUid;
|
|
53
20
|
if (parent_uid) {
|
|
@@ -70,38 +37,33 @@ export class MemoryOperations {
|
|
|
70
37
|
else {
|
|
71
38
|
// Create the heading block
|
|
72
39
|
const headingBlockUid = generateBlockUid();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
block: { uid: headingBlockUid, string: heading }
|
|
80
|
-
}]
|
|
81
|
-
});
|
|
82
|
-
targetParentUid = headingBlockUid;
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create heading block: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
-
}
|
|
40
|
+
await executeBatch(this.graph, [{
|
|
41
|
+
action: 'create-block',
|
|
42
|
+
location: { 'parent-uid': pageUid, order: 'last' },
|
|
43
|
+
block: { uid: headingBlockUid, string: heading }
|
|
44
|
+
}], 'create heading block');
|
|
45
|
+
targetParentUid = headingBlockUid;
|
|
87
46
|
}
|
|
88
47
|
}
|
|
89
48
|
else {
|
|
90
49
|
// Default: use daily page root
|
|
91
50
|
targetParentUid = pageUid;
|
|
92
51
|
}
|
|
93
|
-
// Get memories tag
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
52
|
+
// Get memories tag (use instance property) and format as Roam tag
|
|
53
|
+
// If memoriesTag is null (disabled for this graph), treat as if include_memories_tag is false
|
|
54
|
+
const memoriesTagFormatted = (include_memories_tag && this.memoriesTag)
|
|
55
|
+
? (this.memoriesTag.includes(' ') || this.memoriesTag.includes('/')
|
|
56
|
+
? `#[[${this.memoriesTag}]]`
|
|
57
|
+
: `#${this.memoriesTag}`)
|
|
58
|
+
: undefined;
|
|
98
59
|
// Format categories as Roam tags if provided
|
|
99
60
|
const categoryTags = categories?.map(cat => {
|
|
100
61
|
// Handle multi-word categories
|
|
101
62
|
return cat.includes(' ') ? `#[[${cat}]]` : `#${cat}`;
|
|
102
|
-
})
|
|
63
|
+
}) ?? [];
|
|
103
64
|
// Create block with memory, then all tags together at the end
|
|
104
|
-
const
|
|
65
|
+
const tags = memoriesTagFormatted ? [...categoryTags, memoriesTagFormatted] : categoryTags;
|
|
66
|
+
const blockContent = [memory, ...tags].join(' ').trim();
|
|
105
67
|
// Pre-generate UID so we can return it
|
|
106
68
|
const blockUid = generateBlockUid();
|
|
107
69
|
const actions = [{
|
|
@@ -115,49 +77,29 @@ export class MemoryOperations {
|
|
|
115
77
|
string: blockContent
|
|
116
78
|
}
|
|
117
79
|
}];
|
|
118
|
-
|
|
119
|
-
const result = await batchActions(this.graph, {
|
|
120
|
-
action: 'batch-actions',
|
|
121
|
-
actions
|
|
122
|
-
});
|
|
123
|
-
if (!result) {
|
|
124
|
-
throw new McpError(ErrorCode.InternalError, 'Failed to create memory block via batch action');
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create memory block: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
-
}
|
|
80
|
+
await executeBatch(this.graph, actions, 'create memory block');
|
|
130
81
|
return { success: true, block_uid: blockUid, parent_uid: targetParentUid };
|
|
131
82
|
}
|
|
132
83
|
async recall(sort_by = 'newest', filter_tag) {
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
memoriesTag = "Memories";
|
|
84
|
+
// If memories tag is disabled for this graph, return empty
|
|
85
|
+
if (!this.memoriesTag) {
|
|
86
|
+
return { success: true, memories: [] };
|
|
137
87
|
}
|
|
138
88
|
// Extract the tag text, removing any formatting
|
|
139
|
-
const tagText = memoriesTag
|
|
89
|
+
const tagText = this.memoriesTag
|
|
140
90
|
.replace(/^#/, '') // Remove leading #
|
|
141
91
|
.replace(/^\[\[/, '').replace(/\]\]$/, ''); // Remove [[ and ]]
|
|
142
92
|
try {
|
|
143
|
-
// Get page blocks using query to access actual block content
|
|
144
|
-
const ancestorRule = `[
|
|
145
|
-
[ (ancestor ?b ?a)
|
|
146
|
-
[?a :block/children ?b] ]
|
|
147
|
-
[ (ancestor ?b ?a)
|
|
148
|
-
[?parent :block/children ?b]
|
|
149
|
-
(ancestor ?parent ?a) ]
|
|
150
|
-
]`;
|
|
151
93
|
// Query to find all blocks on the page
|
|
152
94
|
const pageQuery = `[:find ?string ?time
|
|
153
95
|
:in $ % ?title
|
|
154
|
-
:where
|
|
96
|
+
:where
|
|
155
97
|
[?page :node/title ?title]
|
|
156
98
|
[?block :block/string ?string]
|
|
157
99
|
[?block :create/time ?time]
|
|
158
100
|
(ancestor ?block ?page)]`;
|
|
159
101
|
// Execute query
|
|
160
|
-
const pageResults = await q(this.graph, pageQuery, [
|
|
102
|
+
const pageResults = await q(this.graph, pageQuery, [ANCESTOR_RULE, tagText]);
|
|
161
103
|
// Process page blocks with sorting
|
|
162
104
|
let pageMemories = pageResults
|
|
163
105
|
.sort(([_, aTime], [__, bTime]) => sort_by === 'newest' ? bTime - aTime : aTime - bTime)
|