roam-research-mcp 1.6.0 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -13
- package/build/Roam_Markdown_Cheatsheet.md +116 -269
- package/build/cli/batch/resolver.js +138 -0
- package/build/cli/batch/translator.js +363 -0
- package/build/cli/batch/types.js +4 -0
- package/build/cli/commands/batch.js +345 -0
- package/build/cli/commands/get.js +156 -43
- package/build/cli/commands/refs.js +63 -32
- package/build/cli/commands/rename.js +58 -0
- package/build/cli/commands/save.js +436 -63
- package/build/cli/commands/search.js +152 -31
- package/build/cli/commands/status.js +91 -0
- package/build/cli/commands/update.js +194 -0
- package/build/cli/roam.js +18 -1
- package/build/cli/utils/graph.js +56 -0
- package/build/cli/utils/input.js +10 -0
- package/build/cli/utils/output.js +34 -0
- package/build/config/environment.js +70 -34
- package/build/config/graph-registry.js +221 -0
- package/build/config/graph-registry.test.js +30 -0
- package/build/search/status-search.js +5 -4
- package/build/server/roam-server.js +98 -53
- package/build/shared/validation.js +10 -5
- package/build/tools/helpers/refs.js +50 -31
- package/build/tools/operations/blocks.js +38 -1
- package/build/tools/operations/memory.js +59 -9
- package/build/tools/operations/pages.js +186 -111
- package/build/tools/operations/search/index.js +5 -1
- package/build/tools/operations/todos.js +1 -1
- package/build/tools/schemas.js +123 -42
- package/build/tools/tool-handlers.js +9 -2
- package/build/utils/helpers.js +22 -0
- package/package.json +8 -5
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { BatchOperations } from '../../tools/operations/batch.js';
|
|
4
|
+
import { PageOperations } from '../../tools/operations/pages.js';
|
|
5
|
+
import { printDebug, exitWithError } from '../utils/output.js';
|
|
6
|
+
import { resolveGraph } from '../utils/graph.js';
|
|
7
|
+
import { collectPageTitles, resolveAllPages, resolveDailyPageUid, needsDailyPage, createResolutionContext, getDailyPageTitle } from '../batch/resolver.js';
|
|
8
|
+
import { translateAllCommands } from '../batch/translator.js';
|
|
9
|
+
import { readStdin } from '../utils/input.js';
|
|
10
|
+
// Required params per command type
|
|
11
|
+
const REQUIRED_PARAMS = {
|
|
12
|
+
todo: ['text'],
|
|
13
|
+
create: ['parent', 'text'],
|
|
14
|
+
update: ['uid'],
|
|
15
|
+
delete: ['uid'],
|
|
16
|
+
move: ['uid', 'parent'],
|
|
17
|
+
page: ['title'],
|
|
18
|
+
outline: ['parent', 'items'],
|
|
19
|
+
table: ['parent', 'headers', 'rows'],
|
|
20
|
+
remember: ['text'],
|
|
21
|
+
codeblock: ['parent', 'code']
|
|
22
|
+
};
|
|
23
|
+
const VALID_COMMAND_TYPES = Object.keys(REQUIRED_PARAMS);
|
|
24
|
+
/**
|
|
25
|
+
* Validate command structure and required params
|
|
26
|
+
*/
|
|
27
|
+
function validateCommands(commands) {
|
|
28
|
+
const validated = [];
|
|
29
|
+
for (let i = 0; i < commands.length; i++) {
|
|
30
|
+
const cmd = commands[i];
|
|
31
|
+
if (!cmd || typeof cmd !== 'object') {
|
|
32
|
+
throw new Error(`[${i}] Command must be an object`);
|
|
33
|
+
}
|
|
34
|
+
if (!cmd.command || typeof cmd.command !== 'string') {
|
|
35
|
+
throw new Error(`[${i}] Missing 'command' field`);
|
|
36
|
+
}
|
|
37
|
+
const cmdType = cmd.command;
|
|
38
|
+
if (!VALID_COMMAND_TYPES.includes(cmdType)) {
|
|
39
|
+
throw new Error(`[${i}] Unknown command type '${cmdType}'. Valid: ${VALID_COMMAND_TYPES.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
if (!cmd.params || typeof cmd.params !== 'object') {
|
|
42
|
+
throw new Error(`[${i}] Missing 'params' field`);
|
|
43
|
+
}
|
|
44
|
+
// Check required params
|
|
45
|
+
const params = cmd.params;
|
|
46
|
+
const required = REQUIRED_PARAMS[cmdType];
|
|
47
|
+
for (const param of required) {
|
|
48
|
+
if (params[param] === undefined) {
|
|
49
|
+
throw new Error(`[${i}] ${cmdType}: missing required param '${param}'`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
validated.push(cmd);
|
|
53
|
+
}
|
|
54
|
+
return validated;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate placeholder references are defined before use
|
|
58
|
+
* Returns list of errors (empty = valid)
|
|
59
|
+
*/
|
|
60
|
+
function validatePlaceholders(commands) {
|
|
61
|
+
const errors = [];
|
|
62
|
+
const definedPlaceholders = new Set();
|
|
63
|
+
for (let i = 0; i < commands.length; i++) {
|
|
64
|
+
const cmd = commands[i];
|
|
65
|
+
const params = cmd.params;
|
|
66
|
+
// Collect placeholder definitions from 'as' params
|
|
67
|
+
if (typeof params.as === 'string') {
|
|
68
|
+
definedPlaceholders.add(params.as);
|
|
69
|
+
}
|
|
70
|
+
// Check placeholder references in 'parent' param
|
|
71
|
+
if (typeof params.parent === 'string') {
|
|
72
|
+
const match = params.parent.match(/^\{\{(\w+)\}\}$/);
|
|
73
|
+
if (match) {
|
|
74
|
+
const refName = match[1];
|
|
75
|
+
if (!definedPlaceholders.has(refName)) {
|
|
76
|
+
errors.push(`[${i}] ${cmd.command}: placeholder "{{${refName}}}" used before definition`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return errors;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Output partial results when a failure occurs mid-batch
|
|
85
|
+
* Helps user know what was created and may need manual cleanup
|
|
86
|
+
*/
|
|
87
|
+
function outputPartialResults(pageResults, failedPage, batchError) {
|
|
88
|
+
const output = {
|
|
89
|
+
success: false,
|
|
90
|
+
partial: true,
|
|
91
|
+
pages_created: pageResults.length,
|
|
92
|
+
...(failedPage && { failed_at: `page: ${failedPage}` }),
|
|
93
|
+
...(batchError && { failed_at: `batch: ${batchError}` })
|
|
94
|
+
};
|
|
95
|
+
if (pageResults.length > 0) {
|
|
96
|
+
output.created_pages = pageResults.map(p => ({
|
|
97
|
+
title: p.title,
|
|
98
|
+
uid: p.uid
|
|
99
|
+
}));
|
|
100
|
+
output.cleanup_hint = 'Pages listed above were created before failure. Delete manually if needed.';
|
|
101
|
+
}
|
|
102
|
+
console.error(JSON.stringify(output, null, 2));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
/** Format action for dry-run display */
|
|
106
|
+
function formatAction(action, index) {
|
|
107
|
+
const lines = [` ${index + 1}. ${action.action}`];
|
|
108
|
+
if (action.string) {
|
|
109
|
+
const text = String(action.string);
|
|
110
|
+
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
|
|
111
|
+
lines.push(` text: "${preview}"`);
|
|
112
|
+
}
|
|
113
|
+
if (action.location) {
|
|
114
|
+
const loc = action.location;
|
|
115
|
+
lines.push(` parent: ${loc['parent-uid']}`);
|
|
116
|
+
}
|
|
117
|
+
if (action.uid) {
|
|
118
|
+
lines.push(` uid: ${action.uid}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
export function createBatchCommand() {
|
|
123
|
+
return new Command('batch')
|
|
124
|
+
.description('Execute multiple block operations efficiently in a single API call')
|
|
125
|
+
.argument('[file]', 'JSON file with commands (or pipe via stdin)')
|
|
126
|
+
.option('--debug', 'Show debug information')
|
|
127
|
+
.option('--dry-run', 'Validate and show planned actions without executing')
|
|
128
|
+
.option('--simulate', 'Validate structure offline (no API calls)')
|
|
129
|
+
.option('-g, --graph <name>', 'Target graph key (for multi-graph mode)')
|
|
130
|
+
.option('--write-key <key>', 'Write confirmation key (for non-default graphs)')
|
|
131
|
+
.addHelpText('after', `
|
|
132
|
+
Examples:
|
|
133
|
+
# From file
|
|
134
|
+
roam batch commands.json # Execute commands from file
|
|
135
|
+
roam batch commands.json --dry-run # Preview without executing (resolves pages)
|
|
136
|
+
roam batch commands.json --simulate # Validate offline (no API calls)
|
|
137
|
+
|
|
138
|
+
# From stdin
|
|
139
|
+
cat commands.json | roam batch # Pipe commands
|
|
140
|
+
echo '[{"command":"todo","params":{"text":"Task 1"}}]' | roam batch
|
|
141
|
+
|
|
142
|
+
Command schemas:
|
|
143
|
+
todo: {text}
|
|
144
|
+
create: {parent, text, as?, heading?, order?}
|
|
145
|
+
update: {uid, text?, heading?, open?}
|
|
146
|
+
delete: {uid}
|
|
147
|
+
move: {uid, parent, order?}
|
|
148
|
+
page: {title, as?, content?: [{text, level, heading?}...]}
|
|
149
|
+
outline: {parent, items: [string...]}
|
|
150
|
+
table: {parent, headers: [string...], rows: [{label, cells: [string...]}...]}
|
|
151
|
+
remember: {text, categories?: [string...]}
|
|
152
|
+
codeblock: {parent, code, language?}
|
|
153
|
+
|
|
154
|
+
Parent accepts: block UID, "daily", page title, or {{placeholder}}
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
[
|
|
158
|
+
{"command": "page", "params": {"title": "Project X", "as": "proj"}},
|
|
159
|
+
{"command": "create", "params": {"parent": "{{proj}}", "text": "# Overview", "as": "overview"}},
|
|
160
|
+
{"command": "outline", "params": {"parent": "{{overview}}", "items": ["Goal 1", "Goal 2"]}},
|
|
161
|
+
{"command": "todo", "params": {"text": "Review project"}}
|
|
162
|
+
]
|
|
163
|
+
`)
|
|
164
|
+
.action(async (file, options) => {
|
|
165
|
+
try {
|
|
166
|
+
// Read input
|
|
167
|
+
let rawInput;
|
|
168
|
+
if (file) {
|
|
169
|
+
try {
|
|
170
|
+
rawInput = readFileSync(file, 'utf-8');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
exitWithError(`Could not read file: ${file}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (process.stdin.isTTY) {
|
|
177
|
+
exitWithError('No file specified and no input piped. Use: roam batch commands.json or cat commands.json | roam batch');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
rawInput = await readStdin();
|
|
181
|
+
}
|
|
182
|
+
// Parse JSON
|
|
183
|
+
let parsed;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(rawInput);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
exitWithError(`Invalid JSON: ${err instanceof SyntaxError ? err.message : 'parse error'}`);
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(parsed)) {
|
|
191
|
+
exitWithError('Input must be a JSON array of commands');
|
|
192
|
+
}
|
|
193
|
+
if (parsed.length === 0) {
|
|
194
|
+
console.log('No commands to execute');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Validate and get typed commands
|
|
198
|
+
const commands = validateCommands(parsed);
|
|
199
|
+
// Upfront validation: check placeholder references
|
|
200
|
+
const placeholderErrors = validatePlaceholders(commands);
|
|
201
|
+
if (placeholderErrors.length > 0) {
|
|
202
|
+
exitWithError(`Placeholder validation failed:\n ${placeholderErrors.join('\n ')}`);
|
|
203
|
+
}
|
|
204
|
+
if (options.debug) {
|
|
205
|
+
printDebug('Commands', commands.length);
|
|
206
|
+
printDebug('Graph', options.graph || 'default');
|
|
207
|
+
printDebug('Mode', options.simulate ? 'simulate' : options.dryRun ? 'dry-run' : 'execute');
|
|
208
|
+
}
|
|
209
|
+
// Simulate mode: validate structure without connecting to Roam
|
|
210
|
+
if (options.simulate) {
|
|
211
|
+
const context = createResolutionContext();
|
|
212
|
+
const { actions, pageCommands } = translateAllCommands(commands, context);
|
|
213
|
+
console.log('\n[SIMULATE] Validation passed\n');
|
|
214
|
+
console.log(`Commands: ${commands.length}`);
|
|
215
|
+
console.log(` Pages to create: ${pageCommands.length}`);
|
|
216
|
+
console.log(` Batch actions: ${actions.length}`);
|
|
217
|
+
if (pageCommands.length > 0) {
|
|
218
|
+
console.log('\nPage creations:');
|
|
219
|
+
for (const pc of pageCommands) {
|
|
220
|
+
const as = pc.params.as ? ` → {{${pc.params.as}}}` : '';
|
|
221
|
+
console.log(` - "${pc.params.title}"${as}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
console.log('\nNo API calls made. Use --dry-run to resolve page UIDs.');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const graph = resolveGraph(options, true);
|
|
228
|
+
// Phase 1: Collect and resolve page titles
|
|
229
|
+
const context = createResolutionContext();
|
|
230
|
+
const pageTitles = collectPageTitles(commands);
|
|
231
|
+
if (pageTitles.size > 0) {
|
|
232
|
+
if (options.debug) {
|
|
233
|
+
printDebug('Pages to resolve', Array.from(pageTitles));
|
|
234
|
+
}
|
|
235
|
+
const resolved = await resolveAllPages(graph, pageTitles);
|
|
236
|
+
for (const [title, uid] of resolved) {
|
|
237
|
+
context.pageUids.set(title, uid);
|
|
238
|
+
}
|
|
239
|
+
// Check for unresolved pages
|
|
240
|
+
const unresolved = Array.from(pageTitles).filter(t => !context.pageUids.has(t));
|
|
241
|
+
if (unresolved.length > 0) {
|
|
242
|
+
exitWithError(`Page(s) not found: ${unresolved.map(t => `"${t}"`).join(', ')}`);
|
|
243
|
+
}
|
|
244
|
+
if (options.debug) {
|
|
245
|
+
printDebug('Resolved pages', Object.fromEntries(context.pageUids));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Resolve daily page if needed
|
|
249
|
+
if (needsDailyPage(commands)) {
|
|
250
|
+
const dailyUid = await resolveDailyPageUid(graph);
|
|
251
|
+
if (!dailyUid) {
|
|
252
|
+
exitWithError(`Daily page not found: "${getDailyPageTitle()}"`);
|
|
253
|
+
}
|
|
254
|
+
context.dailyPageUid = dailyUid;
|
|
255
|
+
context.pageUids.set(getDailyPageTitle(), dailyUid);
|
|
256
|
+
if (options.debug) {
|
|
257
|
+
printDebug('Daily page UID', dailyUid);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Phase 2: Translate commands to batch actions
|
|
261
|
+
const { actions, pageCommands } = translateAllCommands(commands, context);
|
|
262
|
+
if (options.debug) {
|
|
263
|
+
printDebug('Batch actions', actions.length);
|
|
264
|
+
printDebug('Page commands', pageCommands.length);
|
|
265
|
+
}
|
|
266
|
+
// Dry run: show actions and exit
|
|
267
|
+
if (options.dryRun) {
|
|
268
|
+
console.log('\n[DRY RUN] Planned actions:\n');
|
|
269
|
+
if (pageCommands.length > 0) {
|
|
270
|
+
console.log('Page creations:');
|
|
271
|
+
for (const pc of pageCommands) {
|
|
272
|
+
const as = pc.params.as ? ` (as: {{${pc.params.as}}})` : '';
|
|
273
|
+
console.log(` - "${pc.params.title}"${as}`);
|
|
274
|
+
}
|
|
275
|
+
console.log('');
|
|
276
|
+
}
|
|
277
|
+
if (actions.length > 0) {
|
|
278
|
+
console.log('Batch actions:');
|
|
279
|
+
console.log(actions.map((a, i) => formatAction(a, i)).join('\n'));
|
|
280
|
+
}
|
|
281
|
+
console.log(`\nTotal: ${pageCommands.length} page(s), ${actions.length} action(s)`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Phase 3: Execute page commands (in parallel where possible)
|
|
285
|
+
const pageResults = [];
|
|
286
|
+
if (pageCommands.length > 0) {
|
|
287
|
+
const pageOps = new PageOperations(graph);
|
|
288
|
+
// Execute page creations - must be sequential if they reference each other
|
|
289
|
+
for (const pc of pageCommands) {
|
|
290
|
+
const result = await pageOps.createPage(pc.params.title, pc.params.content?.map(item => ({
|
|
291
|
+
text: item.text,
|
|
292
|
+
level: item.level,
|
|
293
|
+
heading: item.heading
|
|
294
|
+
})));
|
|
295
|
+
if (!result.success) {
|
|
296
|
+
// Report partial results before exiting
|
|
297
|
+
outputPartialResults(pageResults, pc.params.title);
|
|
298
|
+
}
|
|
299
|
+
pageResults.push({ title: pc.params.title, uid: result.uid });
|
|
300
|
+
if (pc.params.as) {
|
|
301
|
+
context.placeholders.set(pc.params.as, result.uid);
|
|
302
|
+
}
|
|
303
|
+
if (options.debug) {
|
|
304
|
+
printDebug(`Created "${pc.params.title}"`, result.uid);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Phase 4: Execute batch actions
|
|
309
|
+
let batchResult = { success: true, actions_attempted: 0 };
|
|
310
|
+
if (actions.length > 0) {
|
|
311
|
+
const batchOps = new BatchOperations(graph);
|
|
312
|
+
batchResult = await batchOps.processBatch(actions);
|
|
313
|
+
if (!batchResult.success) {
|
|
314
|
+
const errorMsg = typeof batchResult.error === 'string'
|
|
315
|
+
? batchResult.error
|
|
316
|
+
: batchResult.error?.message || 'Unknown error';
|
|
317
|
+
// Report partial results (pages created before batch failed)
|
|
318
|
+
outputPartialResults(pageResults, undefined, errorMsg);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Build output
|
|
322
|
+
const uidMap = {};
|
|
323
|
+
for (const pr of pageResults) {
|
|
324
|
+
const pageCmd = pageCommands.find(pc => pc.params.title === pr.title);
|
|
325
|
+
if (pageCmd?.params.as) {
|
|
326
|
+
uidMap[pageCmd.params.as] = pr.uid;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (batchResult.uid_map) {
|
|
330
|
+
Object.assign(uidMap, batchResult.uid_map);
|
|
331
|
+
}
|
|
332
|
+
const output = {
|
|
333
|
+
success: true,
|
|
334
|
+
pages_created: pageResults.length,
|
|
335
|
+
actions_executed: actions.length,
|
|
336
|
+
...(Object.keys(uidMap).length > 0 && { uid_map: uidMap })
|
|
337
|
+
};
|
|
338
|
+
console.log(JSON.stringify(output, null, 2));
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
342
|
+
exitWithError(message);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
@@ -1,74 +1,187 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { initializeGraph } from '@roam-research/roam-api-sdk';
|
|
3
|
-
import { API_TOKEN, GRAPH_NAME } from '../../config/environment.js';
|
|
4
2
|
import { PageOperations } from '../../tools/operations/pages.js';
|
|
5
3
|
import { BlockRetrievalOperations } from '../../tools/operations/block-retrieval.js';
|
|
6
|
-
import {
|
|
4
|
+
import { SearchOperations } from '../../tools/operations/search/index.js';
|
|
5
|
+
import { formatPageOutput, formatBlockOutput, formatTodoOutput, printDebug, exitWithError } from '../utils/output.js';
|
|
6
|
+
import { resolveGraph } from '../utils/graph.js';
|
|
7
|
+
import { readStdin } from '../utils/input.js';
|
|
8
|
+
import { resolveRefs } from '../../tools/helpers/refs.js';
|
|
9
|
+
import { resolveRelativeDate } from '../../utils/helpers.js';
|
|
7
10
|
// Block UID pattern: 9 alphanumeric characters, optionally wrapped in (( ))
|
|
8
11
|
const BLOCK_UID_PATTERN = /^(?:\(\()?([a-zA-Z0-9_-]{9})(?:\)\))?$/;
|
|
12
|
+
/**
|
|
13
|
+
* Recursively resolve block references in a RoamBlock tree
|
|
14
|
+
*/
|
|
15
|
+
async function resolveBlockRefs(graph, block, maxDepth) {
|
|
16
|
+
const resolvedString = await resolveRefs(graph, block.string, 0, maxDepth);
|
|
17
|
+
const resolvedChildren = await Promise.all((block.children || []).map(child => resolveBlockRefs(graph, child, maxDepth)));
|
|
18
|
+
return {
|
|
19
|
+
...block,
|
|
20
|
+
string: resolvedString,
|
|
21
|
+
children: resolvedChildren
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve refs in an array of blocks
|
|
26
|
+
*/
|
|
27
|
+
async function resolveBlocksRefs(graph, blocks, maxDepth) {
|
|
28
|
+
return Promise.all(blocks.map(block => resolveBlockRefs(graph, block, maxDepth)));
|
|
29
|
+
}
|
|
9
30
|
export function createGetCommand() {
|
|
10
31
|
return new Command('get')
|
|
11
|
-
.description('Fetch
|
|
12
|
-
.argument('
|
|
13
|
-
.option('--json', 'Output as JSON instead of markdown')
|
|
14
|
-
.option('--depth <n>', 'Child levels to fetch (default: 4)', '4')
|
|
15
|
-
.option('--refs
|
|
16
|
-
.option('--flat', 'Flatten hierarchy to single-level list')
|
|
32
|
+
.description('Fetch pages, blocks, or TODO/DONE items with optional ref expansion')
|
|
33
|
+
.argument('[target]', 'Page title, block UID, or relative date. Reads from stdin if "-" or omitted.')
|
|
34
|
+
.option('-j, --json', 'Output as JSON instead of markdown')
|
|
35
|
+
.option('-d, --depth <n>', 'Child levels to fetch (default: 4)', '4')
|
|
36
|
+
.option('-r, --refs [n]', 'Expand ((uid)) refs in output (default depth: 1, max: 4)')
|
|
37
|
+
.option('-f, --flat', 'Flatten hierarchy to single-level list')
|
|
38
|
+
.option('--todo', 'Fetch TODO items')
|
|
39
|
+
.option('--done', 'Fetch DONE items')
|
|
40
|
+
.option('-p, --page <ref>', 'Filter TODOs/DONEs by page title or UID')
|
|
41
|
+
.option('-i, --include <terms>', 'Include items matching these terms (comma-separated)')
|
|
42
|
+
.option('-e, --exclude <terms>', 'Exclude items matching these terms (comma-separated)')
|
|
43
|
+
.option('-g, --graph <name>', 'Target graph key (multi-graph mode)')
|
|
17
44
|
.option('--debug', 'Show query metadata')
|
|
45
|
+
.addHelpText('after', `
|
|
46
|
+
Examples:
|
|
47
|
+
# Fetch pages
|
|
48
|
+
roam get "Project Notes" # Page by title
|
|
49
|
+
roam get today # Today's daily page
|
|
50
|
+
roam get yesterday # Yesterday's daily page
|
|
51
|
+
|
|
52
|
+
# Fetch blocks
|
|
53
|
+
roam get abc123def # Block by UID
|
|
54
|
+
roam get "((abc123def))" # UID with wrapper
|
|
55
|
+
|
|
56
|
+
# Stdin / Batch Retrieval
|
|
57
|
+
echo "Project A" | roam get # Pipe page title
|
|
58
|
+
echo "abc123def" | roam get # Pipe block UID
|
|
59
|
+
cat uids.txt | roam get --json # Fetch multiple blocks (NDJSON output)
|
|
60
|
+
|
|
61
|
+
# Output options
|
|
62
|
+
roam get "Page" -j # JSON output
|
|
63
|
+
roam get "Page" -f # Flat list (no hierarchy)
|
|
64
|
+
roam get abc123def -d 2 # Limit depth to 2 levels
|
|
65
|
+
roam get "Page" -r # Expand block refs (depth 1)
|
|
66
|
+
roam get "Page" -r 3 # Expand refs up to 3 levels deep
|
|
67
|
+
|
|
68
|
+
# TODO/DONE items (refs auto-expanded)
|
|
69
|
+
roam get --todo # All TODOs across graph
|
|
70
|
+
roam get --done # All completed items
|
|
71
|
+
roam get --todo -p "Work" # TODOs on "Work" page
|
|
72
|
+
`)
|
|
18
73
|
.action(async (target, options) => {
|
|
19
74
|
try {
|
|
20
|
-
const graph =
|
|
21
|
-
token: API_TOKEN,
|
|
22
|
-
graph: GRAPH_NAME
|
|
23
|
-
});
|
|
75
|
+
const graph = resolveGraph(options, false);
|
|
24
76
|
const depth = parseInt(options.depth || '4', 10);
|
|
77
|
+
// Parse refs: true/string means enabled, number sets max depth (default 1, max 4)
|
|
78
|
+
const refsDepth = options.refs !== undefined
|
|
79
|
+
? Math.min(4, Math.max(1, parseInt(options.refs, 10) || 1))
|
|
80
|
+
: 0;
|
|
25
81
|
const outputOptions = {
|
|
26
82
|
json: options.json,
|
|
27
83
|
flat: options.flat,
|
|
28
84
|
debug: options.debug
|
|
29
85
|
};
|
|
30
86
|
if (options.debug) {
|
|
31
|
-
printDebug('Target', target);
|
|
32
|
-
printDebug('
|
|
87
|
+
printDebug('Target', target || 'stdin');
|
|
88
|
+
printDebug('Graph', options.graph || 'default');
|
|
89
|
+
printDebug('Options', { depth, refs: refsDepth || 'off', ...outputOptions });
|
|
33
90
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
const blockUid = uidMatch[1];
|
|
91
|
+
// Handle --todo or --done flags (these ignore target arg usually, but could filter by page if target is used as page?)
|
|
92
|
+
// The help says "-p" is for page. So we strictly follow flags.
|
|
93
|
+
if (options.todo || options.done) {
|
|
94
|
+
const status = options.todo ? 'TODO' : 'DONE';
|
|
39
95
|
if (options.debug) {
|
|
40
|
-
printDebug('
|
|
96
|
+
printDebug('Status search', { status, page: options.page, include: options.include, exclude: options.exclude });
|
|
41
97
|
}
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
const searchOps = new SearchOperations(graph);
|
|
99
|
+
const result = await searchOps.searchByStatus(status, options.page, options.include, options.exclude);
|
|
100
|
+
console.log(formatTodoOutput(result.matches, status, outputOptions));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Determine targets
|
|
104
|
+
let targets = [];
|
|
105
|
+
if (target && target !== '-') {
|
|
106
|
+
targets = [target];
|
|
48
107
|
}
|
|
49
108
|
else {
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
|
|
109
|
+
// Read from stdin if no target or explicit '-'
|
|
110
|
+
if (process.stdin.isTTY && target !== '-') {
|
|
111
|
+
// If TTY and no target, show error
|
|
112
|
+
exitWithError('Target is required. Use: roam get <page-title>, roam get --todo, or pipe targets via stdin');
|
|
113
|
+
}
|
|
114
|
+
const input = await readStdin();
|
|
115
|
+
if (input) {
|
|
116
|
+
targets = input.split('\n').map(t => t.trim()).filter(Boolean);
|
|
53
117
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
118
|
+
}
|
|
119
|
+
if (targets.length === 0) {
|
|
120
|
+
exitWithError('No targets provided');
|
|
121
|
+
}
|
|
122
|
+
// Helper to process a single target
|
|
123
|
+
const processTarget = async (item) => {
|
|
124
|
+
// Resolve relative date keywords (today, yesterday, tomorrow)
|
|
125
|
+
const resolvedTarget = resolveRelativeDate(item);
|
|
126
|
+
if (options.debug && resolvedTarget !== item) {
|
|
127
|
+
printDebug('Resolved date', `${item} → ${resolvedTarget}`);
|
|
128
|
+
}
|
|
129
|
+
// Check if target is a block UID
|
|
130
|
+
const uidMatch = resolvedTarget.match(BLOCK_UID_PATTERN);
|
|
131
|
+
if (uidMatch) {
|
|
132
|
+
// Fetch block by UID
|
|
133
|
+
const blockUid = uidMatch[1];
|
|
134
|
+
if (options.debug)
|
|
135
|
+
printDebug('Fetching block', { uid: blockUid });
|
|
136
|
+
const blockOps = new BlockRetrievalOperations(graph);
|
|
137
|
+
let block = await blockOps.fetchBlockWithChildren(blockUid, depth);
|
|
138
|
+
if (!block) {
|
|
139
|
+
// If fetching multiple, maybe warn instead of exit?
|
|
140
|
+
// For now, consistent behavior: print error message to stderr but continue?
|
|
141
|
+
// Or simpler: just return a "not found" string/object.
|
|
142
|
+
// formatBlockOutput doesn't handle null.
|
|
143
|
+
return options.json ? JSON.stringify({ error: `Block ${blockUid} not found` }) : `Block ${blockUid} not found`;
|
|
61
144
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return;
|
|
145
|
+
// Resolve block references if requested
|
|
146
|
+
if (refsDepth > 0) {
|
|
147
|
+
block = await resolveBlockRefs(graph, block, refsDepth);
|
|
66
148
|
}
|
|
149
|
+
return formatBlockOutput(block, outputOptions);
|
|
67
150
|
}
|
|
68
151
|
else {
|
|
69
|
-
|
|
152
|
+
// Fetch page by title
|
|
153
|
+
if (options.debug)
|
|
154
|
+
printDebug('Fetching page', { title: resolvedTarget });
|
|
155
|
+
const pageOps = new PageOperations(graph);
|
|
156
|
+
const result = await pageOps.fetchPageByTitle(resolvedTarget, 'raw');
|
|
157
|
+
// Parse the raw result
|
|
158
|
+
let blocks;
|
|
159
|
+
if (typeof result === 'string') {
|
|
160
|
+
try {
|
|
161
|
+
blocks = JSON.parse(result);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Result is already formatted as string (e.g., "Page Title (no content found)")
|
|
165
|
+
// But wait, fetchPageByTitle returns string if not found or empty?
|
|
166
|
+
// Actually fetchPageByTitle 'raw' returns JSON string of blocks OR empty array JSON string?
|
|
167
|
+
// Let's assume result is valid JSON or error message string.
|
|
168
|
+
return options.json ? JSON.stringify({ title: resolvedTarget, error: result }) : result;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
blocks = result;
|
|
173
|
+
}
|
|
174
|
+
// Resolve block references if requested
|
|
175
|
+
if (refsDepth > 0) {
|
|
176
|
+
blocks = await resolveBlocksRefs(graph, blocks, refsDepth);
|
|
177
|
+
}
|
|
178
|
+
return formatPageOutput(resolvedTarget, blocks, outputOptions);
|
|
70
179
|
}
|
|
71
|
-
|
|
180
|
+
};
|
|
181
|
+
// Execute sequentially
|
|
182
|
+
for (const t of targets) {
|
|
183
|
+
const output = await processTarget(t);
|
|
184
|
+
console.log(output);
|
|
72
185
|
}
|
|
73
186
|
}
|
|
74
187
|
catch (error) {
|