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 +16 -1
- package/build/Roam_Markdown_Cheatsheet.md +10 -180
- package/build/cli/batch/resolver.js +29 -2
- package/build/cli/batch/translator.js +4 -1
- package/build/cli/commands/batch.js +18 -3
- package/build/cli/commands/get.js +87 -3
- package/build/cli/commands/save.js +44 -3
- package/build/cli/commands/update.js +30 -12
- package/build/cli/commands/update.test.js +69 -0
- package/build/markdown-utils.js +52 -6
- package/build/markdown-utils.test.js +111 -1
- package/build/search/utils.js +12 -2
- package/build/server/roam-server.js +19 -5
- package/build/tools/helpers/fetch-children.js +42 -0
- package/build/tools/helpers/page-resolution.js +7 -1
- package/build/tools/operations/block-retrieval.js +83 -40
- package/build/tools/operations/block-retrieval.test.js +53 -3
- package/build/tools/operations/full-page-view.js +292 -0
- package/build/tools/operations/outline.js +29 -50
- package/build/tools/schemas.js +64 -5
- package/build/tools/tool-handlers.js +12 -4
- package/build/utils/helpers.js +136 -3
- package/build/utils/helpers.test.js +128 -1
- package/package.json +2 -2
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
|
-
| `
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
589
|
+
order: blockOrder
|
|
549
590
|
},
|
|
550
591
|
string: blockText,
|
|
551
592
|
uid: `{{uid:${uidPlaceholder}}}`,
|