roam-research-mcp 2.15.3 → 2.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,9 @@ cat meeting_notes.md | roam save --title "Meeting: Project Alpha"
34
34
  # Create a TODO item on today's daily page
35
35
  echo "Buy milk" | roam save --todo
36
36
 
37
+ # Prepend to top of page (newest-first ordering)
38
+ roam save -p "Changelog" --order first "v2.18.0 release"
39
+
37
40
  # Search your graph and pipe results to another tool
38
41
  roam search "important" --json | jq .
39
42
 
@@ -43,6 +46,16 @@ roam search --namespace "Convention" # Finds all Convention/* pages
43
46
  # Fetch a page by title
44
47
  roam get "Roam Research"
45
48
 
49
+ # Fetch daily pages using any date format (auto-normalized)
50
+ roam get today # Today's daily page
51
+ roam get 2026-03-21 # ISO date → "March 21st, 2026"
52
+ roam get "03/21/2026" # US date → "March 21st, 2026"
53
+ roam get "March 21" # Named (assumes current year)
54
+
55
+ # Fetch a block with ancestors (parent chain to page root)
56
+ roam get abc123def -a # Block + children + ancestors
57
+ roam get abc123def -a -d 0 # Ancestors only, no children
58
+
46
59
  # Fetch page by UID or Roam URL
47
60
  roam get page abc123def
48
61
  roam get page "https://roamresearch.com/#/app/my-graph/page/abc123def"
@@ -84,9 +97,11 @@ The MCP server exposes these tools to AI assistants (like Claude), enabling them
84
97
  | Tool Name | Description |
85
98
  | :--- | :--- |
86
99
  | `roam_fetch_page_by_title` | Fetch page content by title. |
87
- | `roam_fetch_block_with_children` | Fetch a block and its nested children by UID (resolves refs). |
100
+ | `roam_fetch_page_full_view` | Fetch a page's content plus all linked references with breadcrumb context and children. |
101
+ | `roam_fetch_block` | Fetch a block by UID with optional children (depth) and/or ancestors (up to page root). |
88
102
  | `roam_create_page` | Create new pages, optionally with mixed text and table content. |
89
103
  | `roam_update_page_markdown` | Update a page using smart diff (preserves block UIDs). |
104
+ | `roam_get_subpages` | List sub-pages under a namespace prefix (e.g. "Project/") with optional tag filter. |
90
105
  | `roam_search_by_text` | Full-text search across the graph or within specific pages. Supports namespace prefix search for page titles. |
91
106
  | `roam_search_block_refs` | Find blocks that reference a page, tag, or block UID. |
92
107
  | `roam_search_by_status` | Find TODO or DONE items. |
@@ -184,6 +184,12 @@ CREATING:
184
184
  ├─ Memory → roam_remember
185
185
  └─ Todos → roam_add_todo
186
186
 
187
+ NESTING (roam_import_markdown): nests by BOTH indentation AND markdown heading
188
+ level — content and deeper headings fold under their heading (## under #, ### under
189
+ ##). Use it for heading-structured docs. roam_create_page nests ONLY by explicit
190
+ per-item `level` integers, so heading-structured markdown sent to create_page
191
+ imports flat. Don't use `---` dividers; they become horizontal-rule blocks.
192
+
187
193
  SEARCHING:
188
194
  ├─ By tag → roam_search_for_tag
189
195
  ├─ By text → roam_search_by_text
@@ -192,7 +198,7 @@ SEARCHING:
192
198
  ├─ Block refs → roam_search_block_refs
193
199
  ├─ Modified today → roam_find_pages_modified_today
194
200
  ├─ Page content → roam_fetch_page_by_title
195
- ├─ Block + children → roam_fetch_block_with_children
201
+ ├─ Block (children/ancestors) → roam_fetch_block
196
202
  ├─ Memories → roam_recall
197
203
  └─ Complex → roam_datomic_query
198
204
  ```
@@ -237,6 +243,8 @@ Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
237
243
  **Open question:** `{{[[TODO]]}} Research: <question> #[[open questions]]`
238
244
 
239
245
  ---
246
+ <personalization_layer>
247
+
240
248
  # Roam Preferences — Personalization Layer
241
249
 
242
250
  > This section contains YOUR specific conventions, tagging philosophy, and graph-specific rules. Customize to match your workflow.
@@ -245,199 +253,21 @@ Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
245
253
 
246
254
  ## Graph-Level Behaviors
247
255
 
248
- ### On Creating New Pages
249
- <!-- CUSTOMIZE: What should happen when a new page is created? -->
250
- - After creating a new page, add a reference block on today's daily page: `Created page: [[New Page Name]]`
251
- - <!-- Add any naming conventions, required metadata, etc. -->
252
-
253
- ### On Adding Content
254
- <!-- CUSTOMIZE: Any rules about where/how content gets added? -->
255
- - Default location for quick captures: Daily page
256
- - Long-form content: Create dedicated page, link from daily page
257
- - <!-- Your preferences here -->
258
-
259
- ---
260
256
 
261
257
  ## Tagging Philosophy
262
258
 
263
- ### Core Principle
264
- > Tag for **intellectual collision** and **future discovery**, not just categorization. Every tag should maximize potential for unexpected connections.
265
-
266
- ### The Serendipity Test
267
- Before tagging, ask: *"Could this concept surprise me by connecting to something completely unrelated?"*
268
-
269
- ### What To Tag — Decision Framework
270
-
271
- ```
272
- ASK YOURSELF:
273
- ┌─ How will Future Me find this?
274
- │ └─ Tag by retrieval context, not just content
275
- │
276
- ├─ What domain does this belong to?
277
- │ └─ Use broad category tags: #[[knowledge management]], #[[decision-making]]
278
- │
279
- ├─ Is this a proper noun?
280
- │ └─ YES → Wrap name (no titles): [[Werner Erhard]], [[NASA]]
281
- │ └─ For abbreviations: [NASA]([[National Aeronautics and Space Administration (NASA)]])
282
- │
283
- ├─ Could this alias to existing page?
284
- │ └─ YES → [displayed phrase]([[existing page name]])
285
- │ └─ Example: [frameworks for decisions]([[decision-making frameworks]])
286
- │
287
- └─ Parent block with children?
288
- └─ Tag parent when category applies to all children
289
- └─ Tag individual children for specific categorization
290
- ```
291
-
292
- ### Tag Type Selection
293
-
294
- | Use This | When |
295
- |----------|------|
296
- | `[[Page Reference]]` | Concept deserves its own page, will be expanded |
297
- | `#[[hashtag]]` | Categorization, filtering, won't be a standalone page |
298
- | `#single-word` | Simple, unambiguous category |
299
- | Attribute `Type::` | Structured metadata for queries |
300
-
301
- ### WHEN creating Endnotes/Footnotes:
302
- - Find/Create the block with heading "Footnotes::" and nest footnote item below. (Footnotes do not need to be on the same page as the block to which it references. Typically on the same page unless instructed otherwise.)
303
- - If not known, retrieve the block_uid reference for this footnote item.
304
- - In the block referencing the footnote, append the reference with footnote-item-block_id, example: "- <block_text> #ref ((block_uid))"
305
-
306
- ### Structural Tagging (Beyond Content)
307
-
308
- Tag by **patterns and mechanisms**, not just subjects:
309
-
310
- | Structural Tag | Connects |
311
- |----------------|----------|
312
- | `#[[has feedback loops]]` | Systems, habits, markets, conversations |
313
- | `#[[requires calibration]]` | Instruments, relationships, AI prompts |
314
- | `#[[exhibits emergence]]` | Complexity, culture, creativity |
315
- | `#[[perspective switching]]` | Photography, negotiation, analysis |
316
- | `#[[flow dynamics]]` | Fluids, music, conversation, sequences |
317
-
318
- ### Problem-Oriented Tagging
319
-
320
- Tag by problems solved, not methods used:
321
-
322
- - `#[[breaking cognitive constraints]]`
323
- - `#[[expanding solution spaces]]`
324
- - `#[[preventing expert blindness]]`
325
-
326
- ### Temporal & State-Based Tags
327
-
328
- | Tag Type | Examples |
329
- |----------|----------|
330
- | Future relevance | `#[[will be relevant in 5 years]]`, `#[[connects to unborn projects]]` |
331
- | Mental state triggers | `#[[feeling stuck in patterns]]`, `#[[needing fresh perspective]]` |
332
- | Review scheduling | `[[For review]]: [[August 12th, 2026]]` |
333
-
334
- ---
335
259
 
336
260
  ## Formatting Conventions
337
261
 
338
- ### Quotes
339
- ```
340
- <quote text> —[[Author Name]] #quote #[[topic1]] #[[topic2]]
341
- ```
342
- Always include 2-3 relevant hashtags after quotes.
343
-
344
- ### TODOs and Follow-ups
345
- ```
346
- {{[[TODO]]}} <action needed>
347
- {{[[TODO]]}} #researchThis : <topic to investigate>
348
- ```
349
-
350
- ### Scheduled Reviews
351
-
352
- - Any block tagged with a date will show on that respective daily page.
353
-
354
- ```
355
- [[For review]]: [[Date in ordinal format]]
356
- ```
357
- Optional labels: "Deadline", "Approved", "Pending", "Deferred", "Postponed until"
358
-
359
- ### Aliasing for Case Sensitivity
360
- When a tag would awkwardly affect sentence capitalization:
361
- ```
362
- [Cognitive biases]([[cognitive biases]]) affect decision-making...
363
- ```
364
-
365
- ### Definitions (OVERRIDE)
366
- ```
367
- #def [[<term>]] : <definition>
368
- ```
369
-
370
- ---
371
262
 
372
263
  ## Constraints & Guardrails
373
264
 
374
- ### DON'T
375
- - **Overtag** — Quality over quantity; each tag should earn its place
376
- - **Tag obvious/redundant** — If parent block is tagged, children inherit context
377
- - **Use inconsistent capitalization** — Tags are lowercase unless proper nouns
378
- - **Create orphan tags** — Check if existing page/tag serves the purpose
379
- - **Bold Attributes** - ❌ `**Attribute**::`, ✅ `Attribute::` (Roam auto-formats)
380
- - **Separators** - `---` Don't use them.
381
-
382
- ### DO
383
- - **Think retrieval-first** — How will you search for this later?
384
- - **Cross-pollinate domains** — Force unlikely intellectual meetings
385
- - **Update aging tags** — As interests evolve, so should tag vocabulary
386
- - **Track surprise discoveries** — When unexpected connections yield insights, engineer more of those patterns
387
-
388
- ---
389
265
 
390
266
  ## Custom Rules
391
267
 
392
- <!--
393
- CUSTOMIZE THIS SECTION with your specific conventions:
394
- - Naming patterns for certain page types
395
- - Required attributes for books/articles/people
396
- - Project-specific tagging schemes
397
- - Integration rules with other tools
398
- - etc.
399
- -->
400
-
401
- ### Example Custom Rules (modify as needed):
402
-
403
- **Books:**
404
- ```
405
- [[Book/<title> | <author>]]
406
- Type:: Book
407
- Author:: [[Author Name]]
408
- Status:: Reading | Completed | Abandoned
409
- Rating:: X/5
410
- ```
411
-
412
- **People:**
413
- ```
414
- [[Person Name]]
415
- Type:: Person
416
- Context:: How I know them
417
- ```
418
- - When linking bibliographic references —>
419
- Example: `McAdams, D.P. (2001) [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100) — foundational paper`
420
- - [McAdams, D.P.]([[Dan McAdams]]) - author's name in the graph
421
- - If source URL, link to source: [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100)
422
- - If notes page exists or will exist in Roam: append ` | [Notes]([[Article/The Psychology of Life Stories]]), if not, just leave it without link.
423
-
424
- **Projects:**
425
- ```
426
- [[Project/<project anme>]]
427
- Status:: Active | Paused | Completed
428
- Start:: [[Date]]
429
- ```
430
- ---
431
268
 
432
269
  ## Integration Notes
433
270
 
434
271
  <!-- CUSTOMIZE: Any rules about how Roam integrates with your other tools/systems -->
435
272
 
436
- - Daily pages serve as: <!-- inbox / journal / task list / etc. -->
437
- - Weekly reviews occur on: <!-- day of week -->
438
- - Content flows from: <!-- capture tools, read-later apps, etc. -->
439
- - Content flows to: <!-- publishing, archives, etc. -->
440
-
441
- ---
442
-
443
- *End of Personalization Layer*
273
+ </personalization_layer>
@@ -45,6 +45,21 @@ export async function resolveDailyPageUid(graph) {
45
45
  const dailyTitle = getDailyPageTitle();
46
46
  return resolvePageUid(graph, dailyTitle);
47
47
  }
48
+ /**
49
+ * Check if a string looks like a valid Roam UID (not a page title)
50
+ */
51
+ export function isUidFormat(ref) {
52
+ // 9 alphanumeric characters (standard block UID)
53
+ if (/^[a-zA-Z0-9_-]{9}$/.test(ref))
54
+ return true;
55
+ // MM-DD-YYYY daily page UID
56
+ if (/^\d{2}-\d{2}-\d{4}$/.test(ref))
57
+ return true;
58
+ // Placeholder {{name}}
59
+ if (/^\{\{[^}]+\}\}$/.test(ref))
60
+ return true;
61
+ return false;
62
+ }
48
63
  /**
49
64
  * Collect all unique page titles that need resolution from commands
50
65
  */
@@ -56,6 +71,13 @@ export function collectPageTitles(commands) {
56
71
  if ('page' in params && typeof params.page === 'string') {
57
72
  titles.add(params.page);
58
73
  }
74
+ // Commands that can have 'parent' param — if it looks like a page title, resolve it
75
+ if ('parent' in params && typeof params.parent === 'string') {
76
+ const parent = params.parent;
77
+ if (!isUidFormat(parent)) {
78
+ titles.add(parent);
79
+ }
80
+ }
59
81
  // Remember command can have heading that needs parent page resolution
60
82
  // But heading lookup is handled separately
61
83
  // Todo/remember without explicit page need daily page
@@ -126,8 +148,13 @@ export function resolveParentRef(ref, context) {
126
148
  if (context.pageUids.has(ref)) {
127
149
  return context.pageUids.get(ref);
128
150
  }
129
- // Assume it's a direct UID
130
- return ref;
151
+ // If it looks like a UID, return as-is
152
+ if (isUidFormat(ref)) {
153
+ return ref;
154
+ }
155
+ // Not a UID and not resolved — this is a page title that wasn't collected/resolved
156
+ // Return null so callers can handle it (should not happen if collectPageTitles is correct)
157
+ return null;
131
158
  }
132
159
  /**
133
160
  * Generate a placeholder UID for tracking
@@ -39,7 +39,10 @@ export function translateCommand(command, context) {
39
39
  function getParentUid(params, context) {
40
40
  // Direct parent UID or placeholder
41
41
  if (params.parent) {
42
- return resolveParentRef(params.parent, context) || params.parent;
42
+ const resolved = resolveParentRef(params.parent, context);
43
+ if (resolved)
44
+ return resolved;
45
+ throw new Error(`Parent "${params.parent}" could not be resolved to a UID. Page may not exist.`);
43
46
  }
44
47
  // Page UID
45
48
  if (params.pageUid) {
@@ -227,7 +227,7 @@ Output (JSON): { success, pages_created, actions_executed, uid_map? }
227
227
  return;
228
228
  }
229
229
  const graph = resolveGraph(options, true);
230
- // Phase 1: Collect and resolve page titles
230
+ // Phase 1: Collect and resolve page titles (from 'page' AND 'parent' params)
231
231
  const context = createResolutionContext();
232
232
  const pageTitles = collectPageTitles(commands);
233
233
  if (pageTitles.size > 0) {
@@ -238,10 +238,25 @@ Output (JSON): { success, pages_created, actions_executed, uid_map? }
238
238
  for (const [title, uid] of resolved) {
239
239
  context.pageUids.set(title, uid);
240
240
  }
241
- // Check for unresolved pages
241
+ // Auto-create unresolved pages (e.g., parent: "Page Title" for a page that doesn't exist yet)
242
242
  const unresolved = Array.from(pageTitles).filter(t => !context.pageUids.has(t));
243
243
  if (unresolved.length > 0) {
244
- exitWithError(`Page(s) not found: ${unresolved.map(t => `"${t}"`).join(', ')}`);
244
+ if (options.debug) {
245
+ printDebug('Auto-creating pages', unresolved);
246
+ }
247
+ const pageOps = new PageOperations(graph);
248
+ for (const title of unresolved) {
249
+ const result = await pageOps.createPage(title);
250
+ if (result.success && result.uid) {
251
+ context.pageUids.set(title, result.uid);
252
+ if (options.debug) {
253
+ printDebug(`Auto-created "${title}"`, result.uid);
254
+ }
255
+ }
256
+ else {
257
+ exitWithError(`Failed to create page "${title}"`);
258
+ }
259
+ }
245
260
  }
246
261
  if (options.debug) {
247
262
  printDebug('Resolved pages', Object.fromEntries(context.pageUids));
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import { PageOperations } from '../../tools/operations/pages.js';
3
3
  import { BlockRetrievalOperations } from '../../tools/operations/block-retrieval.js';
4
4
  import { SearchOperations } from '../../tools/operations/search/index.js';
5
+ import { FullPageViewOperations } from '../../tools/operations/full-page-view.js';
5
6
  import { formatPageOutput, formatBlockOutput, formatTodoOutput, formatGroupedOutput, flattenBlocks, blocksToMarkdown, printDebug, exitWithError } from '../utils/output.js';
6
7
  import { resolveGraph } from '../utils/graph.js';
7
8
  import { readStdin } from '../utils/input.js';
@@ -179,13 +180,91 @@ Examples:
179
180
  }
180
181
  });
181
182
  }
183
+ /**
184
+ * Create the 'full' subcommand for full page view (content + linked references)
185
+ */
186
+ function createFullSubcommand() {
187
+ return new Command('full')
188
+ .description('Fetch full page view: content + linked references with breadcrumb context')
189
+ .argument('<title>', 'Page title (for date pages use "January 2nd, 2025")')
190
+ .option('-d, --depth <n>', 'Child depth for referring blocks (default: 4)', '4')
191
+ .option('-n, --max-refs <n>', 'Max linked references (default: 200)', '200')
192
+ .option('-g, --graph <name>', 'Target graph key (multi-graph mode)')
193
+ .option('--debug', 'Show debug information')
194
+ .addHelpText('after', `
195
+ Examples:
196
+ roam get full "Project Notes" # Full view with backlinks
197
+ roam get full "TODO" -n 50 # Cap references at 50
198
+ roam get full "Meeting Notes" -d 2 # Shallow children depth
199
+ `)
200
+ .action(async (title, options) => {
201
+ try {
202
+ const resolvedTitle = resolveRelativeDate(title);
203
+ const depth = parseInt(options.depth || '4', 10);
204
+ const maxRefs = parseInt(options.maxRefs || '200', 10);
205
+ if (options.debug) {
206
+ printDebug('Title', resolvedTitle);
207
+ printDebug('Children depth', depth);
208
+ printDebug('Max references', maxRefs);
209
+ printDebug('Graph', options.graph || 'default');
210
+ }
211
+ const graph = resolveGraph(options, false);
212
+ const pageOps = new PageOperations(graph);
213
+ const fullViewOps = new FullPageViewOperations(graph, pageOps);
214
+ const result = await fullViewOps.fetchPageFullView(resolvedTitle, depth, maxRefs);
215
+ console.log(result);
216
+ }
217
+ catch (error) {
218
+ const message = error instanceof Error ? error.message : String(error);
219
+ exitWithError(message);
220
+ }
221
+ });
222
+ }
223
+ /**
224
+ * Create the 'subpages' subcommand for namespace-based sub-page listing
225
+ */
226
+ function createSubpagesSubcommand() {
227
+ return new Command('subpages')
228
+ .description('List sub-pages under a namespace prefix (e.g. "Project/", "Framework/")')
229
+ .argument('<prefix>', 'Namespace prefix (trailing "/" added automatically)')
230
+ .option('--filter-tag <tag>', 'Only sub-pages containing this tag')
231
+ .option('--content', 'Include each sub-page\'s block content')
232
+ .option('-g, --graph <name>', 'Target graph key (multi-graph mode)')
233
+ .option('--debug', 'Show debug information')
234
+ .addHelpText('after', `
235
+ Examples:
236
+ roam get subpages "Project" # List all Project/* pages
237
+ roam get subpages "Framework" --content # With block content
238
+ roam get subpages "Project" --filter-tag active # Only active projects
239
+ `)
240
+ .action(async (prefix, options) => {
241
+ try {
242
+ if (options.debug) {
243
+ printDebug('Prefix', prefix);
244
+ printDebug('Filter tag', options.filterTag || 'none');
245
+ printDebug('Include content', options.content || false);
246
+ printDebug('Graph', options.graph || 'default');
247
+ }
248
+ const graph = resolveGraph(options, false);
249
+ const pageOps = new PageOperations(graph);
250
+ const fullViewOps = new FullPageViewOperations(graph, pageOps);
251
+ const result = await fullViewOps.fetchSubPages(prefix, options.filterTag, options.content);
252
+ console.log(result);
253
+ }
254
+ catch (error) {
255
+ const message = error instanceof Error ? error.message : String(error);
256
+ exitWithError(message);
257
+ }
258
+ });
259
+ }
182
260
  export function createGetCommand() {
183
261
  const cmd = new Command('get')
184
262
  .description('Fetch pages, blocks, or TODO/DONE items with optional ref expansion')
185
263
  .argument('[target]', 'Page title, block UID, or relative date. Reads from stdin if "-" or omitted.')
186
264
  .option('-j, --json', 'Output as JSON instead of markdown')
187
265
  .option('-s, --structure', 'Output flattened structure with UIDs and order (for surgical updates)')
188
- .option('-d, --depth <n>', 'Child levels to fetch (default: 4)', '4')
266
+ .option('-d, --depth <n>', 'Child levels to fetch (default: 4, 0 = no children)', '4')
267
+ .option('-a, --ancestors', 'Include ancestor chain up to page root')
189
268
  .option('-r, --refs [n]', 'Expand ((uid)) refs in output (default depth: 1, max: 4)')
190
269
  .option('-f, --flat', 'Flatten hierarchy to single-level list')
191
270
  .option('-u, --uid', 'Return only the page UID (resolve title to UID)')
@@ -240,6 +319,8 @@ Examples:
240
319
  roam get "Page" -j # JSON output
241
320
  roam get "Page" -f # Flat list (no hierarchy)
242
321
  roam get abc123def -d 2 # Limit depth to 2 levels
322
+ roam get abc123def -a # Include ancestors up to page root
323
+ roam get abc123def -a -d 0 # Ancestors only, no children
243
324
  roam get "Page" -r # Expand block refs (depth 1)
244
325
  roam get "Page" -r 3 # Expand refs up to 3 levels deep
245
326
 
@@ -288,6 +369,7 @@ Note: For flat results with UIDs, use 'roam search' instead.
288
369
  try {
289
370
  const graph = resolveGraph(options, false);
290
371
  const depth = parseInt(options.depth || '4', 10);
372
+ const ancestors = options.ancestors || false;
291
373
  // Parse refs: true/string means enabled, number sets max depth (default 1, max 4)
292
374
  const refsDepth = options.refs !== undefined
293
375
  ? Math.min(4, Math.max(1, parseInt(options.refs, 10) || 1))
@@ -472,7 +554,7 @@ Note: For flat results with UIDs, use 'roam search' instead.
472
554
  // Standard output: fetch full blocks with children
473
555
  const blocks = [];
474
556
  for (const match of limitedMatches) {
475
- let block = await blockOps.fetchBlockWithChildren(match.block_uid, depth);
557
+ let block = await blockOps.fetchBlock(match.block_uid, depth, ancestors);
476
558
  if (block) {
477
559
  // Resolve refs if requested (default: enabled for tag/text search)
478
560
  const effectiveRefsDepth = refsDepth > 0 ? refsDepth : 1;
@@ -533,7 +615,7 @@ Note: For flat results with UIDs, use 'roam search' instead.
533
615
  if (options.debug)
534
616
  printDebug('Fetching block', { uid: blockUid });
535
617
  const blockOps = new BlockRetrievalOperations(graph);
536
- let block = await blockOps.fetchBlockWithChildren(blockUid, depth);
618
+ let block = await blockOps.fetchBlock(blockUid, depth, ancestors);
537
619
  if (!block) {
538
620
  // If fetching multiple, maybe warn instead of exit?
539
621
  // For now, consistent behavior: print error message to stderr but continue?
@@ -590,5 +672,7 @@ Note: For flat results with UIDs, use 'roam search' instead.
590
672
  });
591
673
  // Add subcommands
592
674
  cmd.addCommand(createPageSubcommand());
675
+ cmd.addCommand(createFullSubcommand());
676
+ cmd.addCommand(createSubpagesSubcommand());
593
677
  return cmd;
594
678
  }
@@ -193,6 +193,7 @@ export function createSaveCommand() {
193
193
  .option('--json', 'Force JSON array format: [{text, level, heading?}, ...]')
194
194
  .option('--flatten', 'Disable heading hierarchy inference (all blocks at root level)')
195
195
  .option('--lines', 'Treat each non-empty line as a separate block (bypass markdown parsing)')
196
+ .option('--order <position>', 'Insertion position for top-level blocks: "first", "last", or integer (default: "last")')
196
197
  .option('-g, --graph <name>', 'Target graph key (multi-graph mode)')
197
198
  .option('--write-key <key>', 'Write confirmation key (non-default graphs)')
198
199
  .option('--debug', 'Show debug information')
@@ -223,6 +224,10 @@ Examples:
223
224
  pbpaste | roam save --lines --title "Clipboard" # Each line = separate block
224
225
  cat list.txt | roam save --lines -p "Notes" # Lines to blocks on page
225
226
 
227
+ # Insertion order
228
+ roam save -p "Changelog" --order first "v2.18.0" # Prepend to top
229
+ roam save -p "Log" --order 1 "After first block" # Insert at position
230
+
226
231
  # Combine options
227
232
  roam save -p "Work" --parent "## Today" "Done with task" -c "wins"
228
233
 
@@ -363,12 +368,30 @@ JSON format (--json):
363
368
  }
364
369
  }
365
370
  }
371
+ // Parse --order option
372
+ let orderValue = 'last';
373
+ if (options.order !== undefined) {
374
+ if (options.order === 'first') {
375
+ orderValue = 'first';
376
+ }
377
+ else if (options.order === 'last') {
378
+ orderValue = 'last';
379
+ }
380
+ else {
381
+ const parsed = parseInt(options.order, 10);
382
+ if (isNaN(parsed) || parsed < 0) {
383
+ exitWithError('--order must be "first", "last", or a non-negative integer');
384
+ }
385
+ orderValue = parsed;
386
+ }
387
+ }
366
388
  if (options.debug) {
367
389
  printDebug('Input', input || 'stdin');
368
390
  printDebug('Is file', isFile);
369
391
  printDebug('Is JSON', isJson);
370
392
  printDebug('Lines mode', options.lines || false);
371
393
  printDebug('Flatten mode', options.flatten || false);
394
+ printDebug('Order', orderValue);
372
395
  printDebug('Graph', options.graph || 'default');
373
396
  printDebug('Content blocks', contentBlocks.length);
374
397
  printDebug('Parent UID', parentUid || 'none');
@@ -392,7 +415,7 @@ JSON format (--json):
392
415
  const hasParent = parentUid || parentHeading;
393
416
  const hasTitle = options.title;
394
417
  const wantsPage = hasTitle && !hasParent;
395
- if (wantsPage || (isFile && !hasParent)) {
418
+ if (wantsPage || (isFile && !hasParent && !options.page)) {
396
419
  // PAGE MODE: create a page
397
420
  const pageTitle = options.title || (sourceFilename ? basename(sourceFilename, '.md').replace('.json', '') : undefined);
398
421
  if (!pageTitle) {
@@ -439,8 +462,16 @@ JSON format (--json):
439
462
  uidMap[i] = uidPlaceholder;
440
463
  // Determine parent for this block
441
464
  let blockParent;
465
+ let blockOrder;
442
466
  if (block.level === 1) {
443
467
  blockParent = parentUid;
468
+ // Apply --order to top-level blocks only
469
+ if (typeof orderValue === 'number') {
470
+ blockOrder = orderValue + i;
471
+ }
472
+ else {
473
+ blockOrder = orderValue;
474
+ }
444
475
  }
445
476
  else {
446
477
  // Find the closest ancestor at level - 1
@@ -454,12 +485,13 @@ JSON format (--json):
454
485
  else {
455
486
  blockParent = parentUid;
456
487
  }
488
+ blockOrder = 'last';
457
489
  }
458
490
  actions.push({
459
491
  action: 'create-block',
460
492
  location: {
461
493
  'parent-uid': blockParent,
462
- order: 'last'
494
+ order: blockOrder
463
495
  },
464
496
  string: block.text,
465
497
  uid: `{{uid:${uidPlaceholder}}}`,
@@ -521,8 +553,16 @@ JSON format (--json):
521
553
  uidMap[i] = uidPlaceholder;
522
554
  // Determine parent for this block
523
555
  let blockParent;
556
+ let blockOrder;
524
557
  if (block.level === 1) {
525
558
  blockParent = targetParentUid;
559
+ // Apply --order to top-level blocks only
560
+ if (typeof orderValue === 'number') {
561
+ blockOrder = orderValue + i;
562
+ }
563
+ else {
564
+ blockOrder = orderValue;
565
+ }
526
566
  }
527
567
  else {
528
568
  // Find the closest ancestor at level - 1
@@ -536,6 +576,7 @@ JSON format (--json):
536
576
  else {
537
577
  blockParent = targetParentUid;
538
578
  }
579
+ blockOrder = 'last';
539
580
  }
540
581
  // Append category tags to first block only
541
582
  const blockText = i === 0 && categoryTags
@@ -545,7 +586,7 @@ JSON format (--json):
545
586
  action: 'create-block',
546
587
  location: {
547
588
  'parent-uid': blockParent,
548
- order: 'last'
589
+ order: blockOrder
549
590
  },
550
591
  string: blockText,
551
592
  uid: `{{uid:${uidPlaceholder}}}`,