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