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.
@@ -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 { formatPageOutput, formatBlockOutput, printDebug, exitWithError } from '../utils/output.js';
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 a page or block from Roam')
12
- .argument('<target>', 'Page title or block UID (e.g., "Page Title" or "((AbCdEfGhI))")')
13
- .option('--json', 'Output as JSON instead of markdown')
14
- .option('--depth <n>', 'Child levels to fetch (default: 4)', '4')
15
- .option('--refs <n>', 'Block ref expansion depth (default: 1)', '1')
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 = initializeGraph({
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('Options', { depth, refs: options.refs, ...outputOptions });
87
+ printDebug('Target', target || 'stdin');
88
+ printDebug('Graph', options.graph || 'default');
89
+ printDebug('Options', { depth, refs: refsDepth || 'off', ...outputOptions });
33
90
  }
34
- // Check if target is a block UID
35
- const uidMatch = target.match(BLOCK_UID_PATTERN);
36
- if (uidMatch) {
37
- // Fetch block by UID
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('Fetching block', { uid: blockUid, depth });
96
+ printDebug('Status search', { status, page: options.page, include: options.include, exclude: options.exclude });
41
97
  }
42
- const blockOps = new BlockRetrievalOperations(graph);
43
- const block = await blockOps.fetchBlockWithChildren(blockUid, depth);
44
- if (!block) {
45
- exitWithError(`Block with UID "${blockUid}" not found`);
46
- }
47
- console.log(formatBlockOutput(block, outputOptions));
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
- // Fetch page by title
51
- if (options.debug) {
52
- printDebug('Fetching page', { title: target, depth });
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
- const pageOps = new PageOperations(graph);
55
- const result = await pageOps.fetchPageByTitle(target, 'raw');
56
- // Parse the raw result
57
- let blocks;
58
- if (typeof result === 'string') {
59
- try {
60
- blocks = JSON.parse(result);
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
- catch {
63
- // Result is already formatted as string (e.g., "Page Title (no content found)")
64
- console.log(result);
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
- blocks = result;
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
- console.log(formatPageOutput(target, blocks, outputOptions));
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) {