roam-research-mcp 3.1.0 → 4.0.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 +38 -0
- package/build/Roam_Markdown_Cheatsheet.md +179 -215
- package/build/cli/utils/output.js +36 -7
- package/build/diff/index.js +1 -1
- package/build/diff/parser.js +51 -0
- package/build/markdown-utils.js +42 -4
- package/build/server/roam-server.js +1 -1
- package/build/shared/block-escaping.js +74 -0
- package/build/tools/operations/full-page-view.js +21 -6
- package/build/tools/operations/guidelines.js +17 -3
- package/build/tools/operations/pages.js +193 -11
- package/build/tools/roam-syntax.js +65 -0
- package/build/tools/schemas.js +8 -4
- package/build/tools/tool-handlers.js +3 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,6 +20,24 @@ Whether you want to give Claude superpowers over your knowledge base or just wan
|
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
23
|
+
## What's New in v4.0
|
|
24
|
+
|
|
25
|
+
**In one line:** a block containing a soft line break (Shift+Enter) now survives a page rewrite. Read a page, write it back, and nothing moves.
|
|
26
|
+
|
|
27
|
+
Until now, a multi-line block rendered as two physical lines, the second at column 0. That reset the parser's indentation baseline, so every block after it collapsed toward the root and `roam_update_page_markdown` dutifully generated the moves to make your real page match. Reading a page and writing back a revision, the documented purpose of the tool, was enough to trigger it. Callout bodies and fenced code blocks are exactly the blocks that carry soft breaks.
|
|
28
|
+
|
|
29
|
+
- **Soft breaks render as `⏎`.** A page containing one gains a leading `<!-- roam:escaped-newlines -->` marker line; keep it if you write the markdown back. Pages with no multi-line block render byte-identical to 3.x, with no marker and no encoding.
|
|
30
|
+
- **Backslashes are never special.** The earlier design escaped newlines as `\n`, which is also a common prefix in authored text: `\nabla`, `\neq`, `C:\newdir`. The sentinel needs no such rule, so all of those are written exactly as typed, everywhere.
|
|
31
|
+
- **Verbatim round-trips are no-ops.** Renderer output submitted back unchanged, title header included, produces zero actions. The CLI shares the fix: `roam get` piped into `roam save --update` leaves the page as it was.
|
|
32
|
+
- **Two more guards on page rewrites.** Non-empty markdown that parses to zero blocks is now refused rather than deleting every block on the page (genuinely empty markdown still clears a page, as documented). And a hand-authored first block that happens to be an H1 echoing the page title is no longer stripped on an ordinary update.
|
|
33
|
+
- **Linked references are encoded too.** `roam_fetch_page_full_view` escapes soft breaks in referring blocks and breadcrumbs, not just the page's own content.
|
|
34
|
+
- **Browser clients pass CORS preflight.** The HTTP transport now allows `MCP-Protocol-Version` and `Last-Event-ID`, both of which a client must send after initialization. Non-browser clients were never affected.
|
|
35
|
+
- **The MCP SDK is pinned** to the version the test suite runs against, so a fresh install gets the protocol surface that was tested rather than whatever npm serves that day.
|
|
36
|
+
|
|
37
|
+
**Why a major.** Four read surfaces return different bytes for any page containing a multi-line block: `roam_fetch_page_by_title` (`format: "markdown"`), `roam_fetch_page_full_view`, `roam_get_subpages`, and `roam get`. If you use the server through an AI assistant, nothing is required of you. A script that parses markdown output of multi-line pages sees the new encoding. If you pinned `roam-research-mcp@3`, you keep 3.2.0's fixes and its documented multi-line limitation until you re-pin.
|
|
38
|
+
|
|
39
|
+
Full detail, including the corner cases and how each fix was verified against the prior state, is in the [changelog](CHANGELOG.md).
|
|
40
|
+
|
|
23
41
|
## How this differs from Roam's official MCP server
|
|
24
42
|
|
|
25
43
|
Roam Research ships its own MCP server and CLI ([`@roam-research/roam-mcp`](https://github.com/Roam-Research/roam-tools)). It is a good tool, and this project is not trying to replace it. **They talk to two different Roam APIs, which is the difference everything else follows from.**
|
|
@@ -162,6 +180,8 @@ Three things worth knowing:
|
|
|
162
180
|
- **Read tools deliberately have neither.** They already serialise their whole result into the text channel, so a schema would just double the payload.
|
|
163
181
|
- **These fields are additive-only.** Some clients validate live responses against a cached tool list, so a field will be added or deprecated — never renamed or removed outside a major version.
|
|
164
182
|
|
|
183
|
+
> **Upgrading to 4.0.0:** markdown reads of a page containing a soft line break (Shift+Enter) now render that break as `⏎` and carry a leading `<!-- roam:escaped-newlines -->` marker, so the page survives a write-back intact. Pages without multi-line blocks are byte-identical to 3.x. AI-assistant users need do nothing; scripts parsing markdown output of multi-line pages see the new encoding. See the [changelog](CHANGELOG.md).
|
|
184
|
+
|
|
165
185
|
> **Upgrading from 2.x:** three write-result fields were renamed — `uid` → `page_uid` (`roam_create_page`), `created_uids` → `created_blocks` (`roam_create_outline`, `roam_import_markdown`) and `preservedUids` → `preserved_uids` (`roam_update_page_markdown`). This only affects code that reads those names; if you use the server through an AI assistant, nothing changes. See the [changelog](CHANGELOG.md) for why.
|
|
166
186
|
|
|
167
187
|
---
|
|
@@ -183,6 +203,14 @@ This is distinct from `CUSTOM_INSTRUCTIONS_PATH`, and the two compose:
|
|
|
183
203
|
|
|
184
204
|
If the page doesn't exist, the tool returns `exists: false` rather than failing, so it is always safe to call.
|
|
185
205
|
|
|
206
|
+
### It also returns the rules that aren't yours to set
|
|
207
|
+
|
|
208
|
+
Alongside your conventions, every `roam_get_guidelines` response carries a `roamSyntax` field: the short list of things that *destroy* content — `roam_update_page_markdown` deleting every block your markdown omits, truncated `structure` previews written back as if they were content, block references retyped as plain text — plus a caution that reads silently exclude `#.rm-hide` subtrees, and the handful of places Roam's markdown inverts standard markdown.
|
|
209
|
+
|
|
210
|
+
Two reasons it rides here rather than in the cheatsheet. It reaches **every** client, including one that never calls `roam_markdown_cheatsheet`; and it is returned even when a graph has **no** guidelines page, which is exactly the case where an agent has least context. The layering is deliberate: **your conventions win on style, `roamSyntax` wins on data safety.** No convention can make a truncated preview complete.
|
|
211
|
+
|
|
212
|
+
The full syntax reference — components, queries, embeds, tool selection — stays in `roam_markdown_cheatsheet`. `roamSyntax` is ~800 tokens and deliberately capped.
|
|
213
|
+
|
|
186
214
|
Each graph can point at a different page, or turn it off:
|
|
187
215
|
|
|
188
216
|
```bash
|
|
@@ -200,6 +228,14 @@ Results are cached for 30 seconds — an edit to the page takes effect without a
|
|
|
200
228
|
|
|
201
229
|
Note that guidelines are read through the normal page path, so blocks tagged `#.rm-hide` / `#.rm-private` are withheld from them too — see below.
|
|
202
230
|
|
|
231
|
+
Reads of a page containing a soft line break render it as `⏎` so each block
|
|
232
|
+
stays on one line — an unescaped newline lands at column 0 and reparents
|
|
233
|
+
everything after it on write-back. Such payloads carry a leading
|
|
234
|
+
`<!-- roam:escaped-newlines -->` marker; keep it if you write the markdown
|
|
235
|
+
back. Markdown you author is never decoded: backslashes are not special, and
|
|
236
|
+
only `⏎` inside a marked payload is interpreted. `roam_get_guidelines` output
|
|
237
|
+
is plain prose — no sentinel, no marker.
|
|
238
|
+
|
|
203
239
|
---
|
|
204
240
|
|
|
205
241
|
## Hiding content from the AI
|
|
@@ -210,6 +246,8 @@ This follows the same convention as Roam's official MCP server, so a block tagge
|
|
|
210
246
|
|
|
211
247
|
Applied to: `roam_fetch_page_by_title`, `roam_fetch_block`, `roam_fetch_page_full_view`, `roam_get_subpages`, `roam_search_by_text`, `roam_search_for_tag`, `roam_search_by_status`, `roam_search_block_refs`, `roam_search_hierarchy`, `roam_search_by_date`.
|
|
212
248
|
|
|
249
|
+
**Hidden blocks are also excluded from the page-rewrite diff**, which is what stops them being *deleted* for being absent from markdown the agent could not have written. `roam_update_page_markdown` (and `roam save --update`) replaces a page with what you give it, deleting whatever your markdown omits — so its baseline is pruned by this same filter, on the rule that **the baseline a diff deletes from must be the same page the caller was allowed to read.** It reports `preserved_hidden` when it protected anything. Content is preserved; exact ordering relative to visible siblings may shift. This was a real data-loss bug before the fix — see the [changelog](CHANGELOG.md).
|
|
250
|
+
|
|
213
251
|
**This is a convenience filter, not a security guarantee.** `roam_datomic_query` reads the database directly and deliberately does **not** apply it, so a capable agent can still surface hidden blocks through raw Datalog. Treat these tags as "keep it out of the AI's way," not "keep it secret."
|
|
214
252
|
|
|
215
253
|
Tag matching is case-insensitive, and only exact tags match — `#.rm-hidden` and `#.rm-highlight` are left alone. The set of hidden UIDs is cached for 30 seconds, so a block tagged just now may remain visible for up to that long.
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
# Roam Markdown Cheatsheet v2.
|
|
1
|
+
# Roam Markdown Cheatsheet v2.8.0
|
|
2
2
|
|
|
3
3
|
## Core Syntax
|
|
4
4
|
|
|
5
5
|
### Formatting
|
|
6
6
|
`**bold**` · `__italic__` · `^^highlight^^` · `~~strike~~` · `` `code` `` · `$$LaTeX$$`
|
|
7
7
|
|
|
8
|
+
⚠️ `*italic*` and `_italic_` are normalized to `__italic__` by the markdown tools, but `roam_process_batch_actions` writes strings literally and Roam does not render single markers. Write `__italic__` everywhere so reads and writes agree.
|
|
9
|
+
|
|
10
|
+
### Headings
|
|
11
|
+
`# H1` · `## H2` · `### H3` at the start of a block. Roam has H1–H3 only; `####` and deeper are not headings and stay literal text. A heading is a property of its own block: it does not nest the blocks after it. Indent to nest.
|
|
12
|
+
|
|
8
13
|
### Links & References
|
|
9
14
|
- **Page ref:** `[[Page Name]]` — creates/links to page
|
|
10
15
|
- **Block ref:** `((block-uid))` — embeds block content inline
|
|
11
16
|
- **Block embed:** `{{[[embed]]: ((block-uid))}}` — full block with children
|
|
12
17
|
- **Embed children:** `{{[[embed-children]]: ((block-uid))}}` — children only (not the parent block)
|
|
13
18
|
- **Embed path:** `{{[[embed-path]]: ((block-uid))}}` — block with its ancestor path
|
|
19
|
+
- **Page embed:** `{{[[embed]]: [[Page Name]]}}` — the whole page inline
|
|
14
20
|
- **External:** `[text](URL)`
|
|
15
21
|
- **Aliased page:** `[display text]([[Actual Page]])`
|
|
16
22
|
- **Aliased block:** `[display text](<((block-uid))>)` — note the angle brackets
|
|
@@ -32,6 +38,50 @@ Always ordinal format: `[[January 1st, 2025]]`, `[[December 23rd, 2024]]`
|
|
|
32
38
|
- Todo: `{{[[TODO]]}} task`
|
|
33
39
|
- Done: `{{[[DONE]]}} task`
|
|
34
40
|
|
|
41
|
+
⚠️ `{{[[TODO]]}}` must lead the block. Anywhere else it renders a checkbox that does not toggle to DONE.
|
|
42
|
+
|
|
43
|
+
### Callouts
|
|
44
|
+
Styled blockquotes with an icon and colour. Two page refs open the block: `[[>]]` marks it a callout, `[[!TYPE]]` picks the style.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
[[>]] [[!TIP]] Title text
|
|
48
|
+
Body on the next line
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The body is a **soft line break inside the same block** (Shift+Enter in the UI, `\n` in the block string) — not a child block. A child block renders as a nested bullet inside the callout instead, which is usually not what you want.
|
|
52
|
+
|
|
53
|
+
Types: `NOTE` `INFO` `SUMMARY` `TIP` `SUCCESS` `QUESTION` `WARNING` `FAILURE` `DANGER` `BUG` `EXAMPLE` `QUOTE`
|
|
54
|
+
|
|
55
|
+
Append `+` or `-` to make it foldable — `[[!TIP]]+` starts expanded, `[[!TIP]]-` starts collapsed.
|
|
56
|
+
|
|
57
|
+
⚠️ `[[>]]` and `[[!TIP]]` are real page references, so every callout backlinks to those pages. That is normal and how the feature works — don't "clean it up."
|
|
58
|
+
⚠️ A plain `> quote` is an ordinary blockquote, not a callout. The two are unrelated.
|
|
59
|
+
|
|
60
|
+
### Soft line breaks
|
|
61
|
+
|
|
62
|
+
A block can hold more than one line — a **soft line break** (Shift+Enter in the
|
|
63
|
+
UI). It stays *one* block, which is what callout bodies and fenced code blocks
|
|
64
|
+
rely on.
|
|
65
|
+
|
|
66
|
+
**You cannot write one through the markdown tools.** `roam_create_page`,
|
|
67
|
+
`roam_import_markdown`, `roam_create_outline` and `roam_update_page_markdown`
|
|
68
|
+
all treat a line break as a block break. Use `roam_process_batch_actions`, which
|
|
69
|
+
writes block strings literally — put a real newline in the `string`.
|
|
70
|
+
|
|
71
|
+
The one exception: inside a payload carrying the `<!-- roam:escaped-newlines -->`
|
|
72
|
+
marker (below), a `⏎` decodes to a real soft line break on write. So keeping —
|
|
73
|
+
or adding — a `⏎` in a marker-carrying payload handed to
|
|
74
|
+
`roam_update_page_markdown` *is* the one markdown-tools path that writes one.
|
|
75
|
+
Without the marker, `⏎` is just the character; it does not decode.
|
|
76
|
+
|
|
77
|
+
⚠️ Reads render a soft line break as `⏎` so the block stays on one line, and a
|
|
78
|
+
payload containing any is marked with a leading `<!-- roam:escaped-newlines -->`
|
|
79
|
+
comment. If you edit that markdown and pass it back to
|
|
80
|
+
`roam_update_page_markdown`, **keep the marker line** — it is what tells the
|
|
81
|
+
server `⏎` means a line break there. Content you add yourself is safe either
|
|
82
|
+
way: backslashes are never special, and `$$\nabla f$$` or `C:\newdir` are
|
|
83
|
+
written exactly as typed.
|
|
84
|
+
|
|
35
85
|
### Attributes
|
|
36
86
|
```
|
|
37
87
|
Type:: Book
|
|
@@ -54,6 +104,9 @@ Rating:: 4/5
|
|
|
54
104
|
- Grandchild
|
|
55
105
|
```
|
|
56
106
|
|
|
107
|
+
### Numbered & document lists
|
|
108
|
+
A numbered list is a **view type on the parent**, not `1.` markers in the text. Set `children-view-type: "numbered"` (or `"document"` for no bullets) on the parent through `roam_process_batch_actions` (`create-block` / `update-block`) or a `roam_create_page` content item. `1.` markers written into block text stay literal.
|
|
109
|
+
|
|
57
110
|
### Code Blocks
|
|
58
111
|
````
|
|
59
112
|
```javascript
|
|
@@ -61,14 +114,40 @@ const x = 1;
|
|
|
61
114
|
```
|
|
62
115
|
````
|
|
63
116
|
|
|
117
|
+
⚠️ Through `roam_process_batch_actions` (literal), do not leave a trailing newline before the closing fence. Roam stores it verbatim and renders it wrong. The markdown tools normalize it away.
|
|
118
|
+
|
|
64
119
|
### Queries
|
|
65
120
|
```
|
|
66
121
|
{{[[query]]: {and: [[tag1]] [[tag2]]}}}
|
|
67
122
|
{{[[query]]: {or: [[A]] [[B]]}}}
|
|
68
123
|
{{[[query]]: {not: [[exclude]]}}}
|
|
69
124
|
{{[[query]]: {between: [[January 1st, 2025]] [[January 31st, 2025]]}}}
|
|
125
|
+
{{[[query]]: {and: [[Project]] {search: mobile}}}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Clauses nest: `{and: [[Project]] {not: [[DONE]]}}`.
|
|
129
|
+
|
|
130
|
+
**Queries match REFERENCES, not text.** Operands must be `[[Page]]` or `((block-uid))` — bare or quoted words do not match. `{and: TODO}` and `{and: "project alpha"}` both find nothing; write `{and: [[TODO]]}` and `{and: [[project alpha]]}`. For free text use `roam_search_by_text`, or a `{search:}` clause.
|
|
131
|
+
|
|
132
|
+
**`{search:}` only works nested inside `{and:}` or `{or:}`** — never on its own, and it is the one clause that takes plain text rather than a reference.
|
|
133
|
+
|
|
134
|
+
**`{between:}` only works on Daily Notes pages.** It filters by the daily page a block lives on, so it does nothing for content on ordinary pages. It accepts shorthands: `[[today]]`, `[[yesterday]]`, `[[last week]]`, `[[next month]]`.
|
|
135
|
+
|
|
136
|
+
Also available: `{created-by: [[User]]}`, `{edited-by: [[User]]}`, `{by: [[User]]}`.
|
|
137
|
+
|
|
138
|
+
#### Page-ref inheritance — the non-obvious one
|
|
139
|
+
**A block inherits its parent's page refs for query matching.** So this TODO matches `{and: [[TODO]] [[Project Alpha]]}` even though it contains no reference to Project Alpha:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
- Notes on [[Project Alpha]]
|
|
143
|
+
- {{[[TODO]]}} Ship the thing
|
|
70
144
|
```
|
|
71
145
|
|
|
146
|
+
Three consequences:
|
|
147
|
+
- **Don't tag every child with the parent's ref** — it is already inherited, and the duplication just clutters the backlinks.
|
|
148
|
+
- **Do tag a child explicitly** if you want it to match *independently* of where it sits. Move it later and inherited matching goes with the old parent.
|
|
149
|
+
- **Reading results:** a returned block may not visibly contain the thing you queried for. The matching ref can be on an ancestor. Don't report the result as wrong, and don't "fix" the block by adding the tag.
|
|
150
|
+
|
|
72
151
|
### Calculator
|
|
73
152
|
`{{[[calc]]: 2 + 2}}`
|
|
74
153
|
|
|
@@ -115,13 +194,15 @@ Diagram definition via nested bullets or a code block child:
|
|
|
115
194
|
- A[Start] --> B{Decision}
|
|
116
195
|
- B -->|Yes| C[Action]
|
|
117
196
|
```
|
|
118
|
-
|
|
197
|
+
Per-diagram theme: first child line `%%{init: {"theme":"forest"}}%%`. Graph-wide via CSS: `:root { --mermaidjs-theme: dark; }` (in `roam/css`)
|
|
119
198
|
|
|
120
199
|
### Hiccup
|
|
121
200
|
`:hiccup [:iframe {:width "600" :height "400" :src "URL"}]`
|
|
122
201
|
|
|
123
202
|
## Advanced Components
|
|
124
203
|
|
|
204
|
+
Write the `{{[[name]]: arg}}` form. The `/name` slash commands are UI-only and do nothing in a written block string.
|
|
205
|
+
|
|
125
206
|
### Dropdowns & Tooltips
|
|
126
207
|
- **Dropdown:** `{{or: option A|option B|option C}}` — select from options, display chosen one
|
|
127
208
|
- **Tooltip:** `{{=:text|hidden content}}` — click to reveal/hide content
|
|
@@ -134,7 +215,28 @@ Theme via CSS: `:root { --mermaidjs-theme: dark; }` (in `roam/css`)
|
|
|
134
215
|
- **Datalog block query:** `{{datalog-block-query: [:find ?b :where [?b :block/string "text"]]}}` — renders results like native queries
|
|
135
216
|
- **Datalog table:** `:q [:find ?title :where [?p :node/title ?title]]` — renders results in sortable table
|
|
136
217
|
- Supports column transforms, date arithmetic, resizable columns, pagination
|
|
137
|
-
-
|
|
218
|
+
- `:q` extensions, usable **only inside a `:q` block in the graph**: date symbols `ms/today-start`, `ms/this-week-start`, `ms/+1D-start`, `dnp/today`, `dnp/-1D` (a daily-page title), `current/page-title`; inbuilt rules `(created-by ?user ?b)`, `(edited-by ?user ?b)`, `(by ?user ?b)`, `(refs-page ?title ?b)`, `(block-or-parent-refs-page ?title ?b)`, `(created-between ?t1 ?t2 ?b)`, `(edited-between ?t1 ?t2 ?b)`, `(in-dnp ?dnp ?b)`, `(refs-dnp ?dnp ?b)`, `(in-dnp-between ?start ?end ?b)`
|
|
219
|
+
|
|
220
|
+
#### `roam_datomic_query` runs plain DataScript only
|
|
221
|
+
⚠️ The `:q` extensions above do **not** work through `roam_datomic_query`. A rule such as `(refs-page "X" ?b)` fails with `Missing rules var '%'`. Write the raw clauses instead, `[?p :node/title "X"] [?b :block/refs ?p]`, and use epoch-millisecond literals for time bounds.
|
|
222
|
+
|
|
223
|
+
**Schema** (what `:where` clauses match):
|
|
224
|
+
- Pages: `:node/title`
|
|
225
|
+
- Blocks: `:block/string` (raw stored text), `:block/uid`, `:block/order`, `:block/page`, `:block/children` (immediate), `:block/parents` (all ancestors), `:block/refs` (outgoing), `:block/heading` (1–3), `:children/view-type`
|
|
226
|
+
- Both: `:create/time`, `:edit/time` (epoch ms); `:create/user`, `:edit/user` point at a user entity with `:user/uid` (every user) and `:user/display-page`
|
|
227
|
+
|
|
228
|
+
Three gotchas:
|
|
229
|
+
- **`:user/email` exists only on human users.** API-token and AI writers have none, so a join on `:user/email` silently drops their blocks. Match on `:user/uid`.
|
|
230
|
+
- **Results are unordered.** Sort client-side; `:create/time` descending for newest-first.
|
|
231
|
+
- **Recursive child pulls cap at 1000 per level** unless written `{[:block/children :limit nil] ...}`. `[*]` alone is immediate children only.
|
|
232
|
+
|
|
233
|
+
```clojure
|
|
234
|
+
;; blocks referencing a page (the portable form of refs-page)
|
|
235
|
+
[:find ?uid :where [?p :node/title "Project Alpha"] [?b :block/refs ?p] [?b :block/uid ?uid]]
|
|
236
|
+
|
|
237
|
+
;; a page and its full block tree, uncapped
|
|
238
|
+
[:find (pull ?e [* {[:block/children :limit nil] ...}]) :where [?e :node/title "Page Name"]]
|
|
239
|
+
```
|
|
138
240
|
|
|
139
241
|
### Document Mode
|
|
140
242
|
`:document` — opens inline WYSIWYG text editor in the block
|
|
@@ -145,6 +247,15 @@ Theme via CSS: `:root { --mermaidjs-theme: dark; }` (in `roam/css`)
|
|
|
145
247
|
- `{{word-count}}` — displays word count for the block
|
|
146
248
|
- `{{chart: ATTR_PAGE_TO_CHART}}` — chart component
|
|
147
249
|
- `{{a}}` — anonymous slider (shared graphs)
|
|
250
|
+
- `{{[[video]]: URL}}` — YouTube / Vimeo / Loom
|
|
251
|
+
- `{{[[mentions]]: [[Page]]}}` — inline a page's linked and unlinked references
|
|
252
|
+
- `{{date}}` — date picker that inserts a date-page ref
|
|
253
|
+
- `{{[[slider]]}}` — inline rating control, e.g. `certainty:: {{[[slider]]}}`
|
|
254
|
+
- `{{[[streak]]: [[Goal]]}}` — heatmap of how often a ref appears in daily notes
|
|
255
|
+
- `{{roam/render: ((codeUid))}}` — render a referenced code block as a component
|
|
256
|
+
- `{{[[roam/css]]}}` — a child fenced `css` block applies graph-wide styling
|
|
257
|
+
- `{{diagram: Title}}` — 2D canvas; each node is a real block
|
|
258
|
+
- `{{encrypt}}` — write it bare; Roam converts it once the user supplies content and a password
|
|
148
259
|
|
|
149
260
|
### CSS Tags
|
|
150
261
|
- `#.classname` — applies CSS class `.classname` to the block
|
|
@@ -153,10 +264,13 @@ Theme via CSS: `:root { --mermaidjs-theme: dark; }` (in `roam/css`)
|
|
|
153
264
|
| Tag | Effect |
|
|
154
265
|
|-----|--------|
|
|
155
266
|
| `#.rm-E` | Display children horizontally |
|
|
156
|
-
| `#.rm-g` |
|
|
157
|
-
| `#.rm-hide` |
|
|
267
|
+
| `#.rm-g` | Promote children up a level; `[[.rm-g]]` keeps the container visible |
|
|
268
|
+
| `#.rm-hide` | Collapse to a clickable bar in the UI; **withheld from every AI read** |
|
|
269
|
+
| `#.rm-private` | Roam's hidden-from-other-users tag; **also withheld from every AI read** |
|
|
158
270
|
| `#.rm-hide-for-readers` | Hide block for read-only users |
|
|
159
271
|
|
|
272
|
+
⚠️ `#.rm-hide` and `#.rm-private` remove the block **and its whole subtree** from what the read tools return, with no marker in the output. A page can look complete when it is not.
|
|
273
|
+
|
|
160
274
|
## Anti-Patterns
|
|
161
275
|
|
|
162
276
|
| ❌ Wrong | ✅ Correct |
|
|
@@ -170,6 +284,16 @@ Theme via CSS: `:root { --mermaidjs-theme: dark; }` (in `roam/css`)
|
|
|
170
284
|
| `- *bullet` | `- bullet` |
|
|
171
285
|
| `* bullet` | `- bullet` |
|
|
172
286
|
| `**Attr**:: val` | `Attr:: val` |
|
|
287
|
+
| `{and: TODO}` | `{and: [[TODO]]}` (queries match refs, not words) |
|
|
288
|
+
| `{and: "project alpha"}` | `{and: [[project alpha]]}` |
|
|
289
|
+
| `{{[[query]]: {search: text}}}` | `{{[[query]]: {and: {search: text}}}}` (never standalone) |
|
|
290
|
+
| `> [[!TIP]] Title` | `[[>]] [[!TIP]] Title` |
|
|
291
|
+
| callout body as a child block | body as `\n` in the same block |
|
|
292
|
+
| `*italic*` in batch actions | `__italic__` |
|
|
293
|
+
| `#### Heading` | `### Heading` (H1–H3 only) |
|
|
294
|
+
| `1. item` as a numbered list | `children-view-type: "numbered"` on the parent |
|
|
295
|
+
| `- [ ] task` | `{{[[TODO]]}} task` |
|
|
296
|
+
| `(refs-page "X" ?b)` in `roam_datomic_query` | `[?p :node/title "X"] [?b :block/refs ?p]` |
|
|
173
297
|
|
|
174
298
|
## Tool Selection
|
|
175
299
|
|
|
@@ -229,222 +353,62 @@ Use `{{uid:name}}` for parent refs in batch actions:
|
|
|
229
353
|
```
|
|
230
354
|
Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
|
|
231
355
|
|
|
356
|
+
⚠️ `delete-block` breaks every `((uid))` that pointed at the block, and Roam has no undo for API writes. Check `roam_search_block_refs` before deleting a block others may reference.
|
|
357
|
+
|
|
232
358
|
## Structural Defaults
|
|
233
359
|
|
|
234
360
|
- **Hierarchy:** 2-4 levels preferred, rarely exceed 5
|
|
235
361
|
- **Blocks:** One idea per block
|
|
236
|
-
- **Page refs vs tags:** `[[Page]]` for expandable concepts, `#tag` for filtering
|
|
237
362
|
- **Embed vs ref:** `((uid))` inline, `{{[[embed]]: ((uid))}}` with children, `{{[[embed-children]]: ((uid))}}` children only, `{{[[embed-path]]: ((uid))}}` with ancestors, `[text](<((uid))>)` link only
|
|
238
|
-
- **No empty blocks
|
|
363
|
+
- **No empty blocks** — use hierarchy for visual separation
|
|
364
|
+
- **Never invent a `((uid))`:** use only uids a tool actually returned. A fabricated ref is a broken link.
|
|
239
365
|
|
|
240
|
-
##
|
|
366
|
+
## Conventions
|
|
241
367
|
|
|
242
|
-
|
|
243
|
-
**Definition:** `Term:: definition #definition`
|
|
244
|
-
**Open question:** `{{[[TODO]]}} Research: <question> #[[open questions]]`
|
|
368
|
+
This cheatsheet is syntax only. How to tag, when to use a page ref versus a hashtag, and the shape of a quote, definition, TODO, or footnote are conventions, and they belong to the graph's `[[roam/agent guidelines]]` page (returned by `roam_get_guidelines`) and the personalization layer appended below. Where a convention and this sheet appear to disagree, the convention wins on style; this sheet wins on what Roam will actually render.
|
|
245
369
|
|
|
246
370
|
---
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
-
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
-
-
|
|
264
|
-
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
│ └─ YES → [displayed phrase]([[existing page name]])
|
|
292
|
-
│ └─ Example: [frameworks for decisions]([[decision-making frameworks]])
|
|
293
|
-
│
|
|
294
|
-
└─ Parent block with children?
|
|
295
|
-
└─ Tag parent when category applies to all children
|
|
296
|
-
└─ Tag individual children for specific categorization
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### Tag Type Selection
|
|
300
|
-
|
|
301
|
-
| Use This | When |
|
|
302
|
-
|----------|------|
|
|
303
|
-
| `[[Page Reference]]` | Concept deserves its own page, will be expanded |
|
|
304
|
-
| `#[[hashtag]]` | Categorization, filtering, won't be a standalone page |
|
|
305
|
-
| `#single-word` | Simple, unambiguous category |
|
|
306
|
-
| Attribute `Type::` | Structured metadata for queries |
|
|
307
|
-
|
|
308
|
-
### WHEN creating Endnotes/Footnotes:
|
|
309
|
-
- 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.)
|
|
310
|
-
- If not known, retrieve the block_uid reference for this footnote item.
|
|
311
|
-
- In the block referencing the footnote, append the reference with footnote-item-block_id, example: "- <block_text> #ref ((block_uid))"
|
|
312
|
-
|
|
313
|
-
### Structural Tagging (Beyond Content)
|
|
314
|
-
|
|
315
|
-
Tag by **patterns and mechanisms**, not just subjects:
|
|
316
|
-
|
|
317
|
-
| Structural Tag | Connects |
|
|
318
|
-
|----------------|----------|
|
|
319
|
-
| `#[[has feedback loops]]` | Systems, habits, markets, conversations |
|
|
320
|
-
| `#[[requires calibration]]` | Instruments, relationships, AI prompts |
|
|
321
|
-
| `#[[exhibits emergence]]` | Complexity, culture, creativity |
|
|
322
|
-
| `#[[perspective switching]]` | Photography, negotiation, analysis |
|
|
323
|
-
| `#[[flow dynamics]]` | Fluids, music, conversation, sequences |
|
|
324
|
-
|
|
325
|
-
### Problem-Oriented Tagging
|
|
326
|
-
|
|
327
|
-
Tag by problems solved, not methods used:
|
|
328
|
-
|
|
329
|
-
- `#[[breaking cognitive constraints]]`
|
|
330
|
-
- `#[[expanding solution spaces]]`
|
|
331
|
-
- `#[[preventing expert blindness]]`
|
|
332
|
-
|
|
333
|
-
### Temporal & State-Based Tags
|
|
334
|
-
|
|
335
|
-
| Tag Type | Examples |
|
|
336
|
-
|----------|----------|
|
|
337
|
-
| Future relevance | `#[[will be relevant in 5 years]]`, `#[[connects to unborn projects]]` |
|
|
338
|
-
| Mental state triggers | `#[[feeling stuck in patterns]]`, `#[[needing fresh perspective]]` |
|
|
339
|
-
| Review scheduling | `[[For review]]: [[August 12th, 2026]]` |
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
## Formatting Conventions
|
|
344
|
-
|
|
345
|
-
### Quotes
|
|
346
|
-
```
|
|
347
|
-
<quote text> —[[Author Name]] #quote #[[topic1]] #[[topic2]]
|
|
348
|
-
```
|
|
349
|
-
Always include 2-3 relevant hashtags after quotes.
|
|
350
|
-
|
|
351
|
-
### TODOs and Follow-ups
|
|
352
|
-
```
|
|
353
|
-
{{[[TODO]]}} <action needed>
|
|
354
|
-
{{[[TODO]]}} #researchThis : <topic to investigate>
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Scheduled Reviews
|
|
358
|
-
|
|
359
|
-
- Any block tagged with a date will show on that respective daily page.
|
|
360
|
-
|
|
361
|
-
```
|
|
362
|
-
[[For review]]: [[Date in ordinal format]]
|
|
363
|
-
```
|
|
364
|
-
Optional labels: "Deadline", "Approved", "Pending", "Deferred", "Postponed until"
|
|
365
|
-
|
|
366
|
-
### Aliasing for Case Sensitivity
|
|
367
|
-
When a tag would awkwardly affect sentence capitalization:
|
|
368
|
-
```
|
|
369
|
-
[Cognitive biases]([[cognitive biases]]) affect decision-making...
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### Definitions (OVERRIDE)
|
|
373
|
-
```
|
|
374
|
-
#def [[<term>]] : <definition>
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
## Constraints & Guardrails
|
|
380
|
-
|
|
381
|
-
### DON'T
|
|
382
|
-
- **Overtag** — Quality over quantity; each tag should earn its place
|
|
383
|
-
- **Tag obvious/redundant** — If parent block is tagged, children inherit context
|
|
384
|
-
- **Use inconsistent capitalization** — Tags are lowercase unless proper nouns
|
|
385
|
-
- **Create orphan tags** — Check if existing page/tag serves the purpose
|
|
386
|
-
- **Bold Attributes** - ❌ `**Attribute**::`, ✅ `Attribute::` (Roam auto-formats)
|
|
387
|
-
- **Separators** - `---` Don't use them.
|
|
388
|
-
|
|
389
|
-
### DO
|
|
390
|
-
- **Think retrieval-first** — How will you search for this later?
|
|
391
|
-
- **Cross-pollinate domains** — Force unlikely intellectual meetings
|
|
392
|
-
- **Update aging tags** — As interests evolve, so should tag vocabulary
|
|
393
|
-
- **Track surprise discoveries** — When unexpected connections yield insights, engineer more of those patterns
|
|
394
|
-
|
|
395
|
-
---
|
|
396
|
-
|
|
397
|
-
## Custom Rules
|
|
398
|
-
|
|
399
|
-
<!--
|
|
400
|
-
CUSTOMIZE THIS SECTION with your specific conventions:
|
|
401
|
-
- Naming patterns for certain page types
|
|
402
|
-
- Required attributes for books/articles/people
|
|
403
|
-
- Project-specific tagging schemes
|
|
404
|
-
- Integration rules with other tools
|
|
405
|
-
- etc.
|
|
406
|
-
-->
|
|
407
|
-
|
|
408
|
-
### Example Custom Rules (modify as needed):
|
|
409
|
-
|
|
410
|
-
**Books:**
|
|
411
|
-
```
|
|
412
|
-
[[Book/<title> | <author>]]
|
|
413
|
-
Type:: Book
|
|
414
|
-
Author:: [[Author Name]]
|
|
415
|
-
Status:: Reading | Completed | Abandoned
|
|
416
|
-
Rating:: X/5
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
**People:**
|
|
420
|
-
```
|
|
421
|
-
[[Person Name]]
|
|
422
|
-
Type:: Person
|
|
423
|
-
Context:: How I know them
|
|
424
|
-
```
|
|
425
|
-
- When linking bibliographic references —>
|
|
426
|
-
Example: `McAdams, D.P. (2001) [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100) — foundational paper`
|
|
427
|
-
- [McAdams, D.P.]([[Dan McAdams]]) - author's name in the graph
|
|
428
|
-
- If source URL, link to source: [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100)
|
|
429
|
-
- If notes page exists or will exist in Roam: append ` | [Notes]([[Article/The Psychology of Life Stories]]), if not, just leave it without link.
|
|
430
|
-
|
|
431
|
-
**Projects:**
|
|
432
|
-
```
|
|
433
|
-
[[Project/<project anme>]]
|
|
434
|
-
Status:: Active | Paused | Completed
|
|
435
|
-
Start:: [[Date]]
|
|
436
|
-
```
|
|
437
|
-
---
|
|
438
|
-
|
|
439
|
-
## Integration Notes
|
|
440
|
-
|
|
441
|
-
<!-- CUSTOMIZE: Any rules about how Roam integrates with your other tools/systems -->
|
|
442
|
-
|
|
443
|
-
- Daily pages serve as: <!-- inbox / journal / task list / etc. -->
|
|
444
|
-
- Weekly reviews occur on: <!-- day of week -->
|
|
445
|
-
- Content flows from: <!-- capture tools, read-later apps, etc. -->
|
|
446
|
-
- Content flows to: <!-- publishing, archives, etc. -->
|
|
447
|
-
|
|
448
|
-
---
|
|
449
|
-
|
|
450
|
-
*End of Personalization Layer*
|
|
371
|
+
<personalization_layer>
|
|
372
|
+
# Roam: Nova operating notes (server-side supplement)
|
|
373
|
+
|
|
374
|
+
Style conventions (voice, `#Nova` attribution, tagging, namespacing, formatting) live on the `[[roam/agent guidelines]]` page and arrive through `roam_get_guidelines`. This file carries only what that page deliberately leaves out: the mechanics behind the `#@Nova` handshake, so an interactive session produces the same artifacts as the nightly job and neither one answers a block twice.
|
|
375
|
+
|
|
376
|
+
## The `#@Nova` handshake: mechanics
|
|
377
|
+
|
|
378
|
+
Ownership rules are on the guidelines page (`#@Nova` is Ian's, `#@Nova-DONE` is Nova's, never self-tag). What follows is the output shape the nightly job writes and checks for.
|
|
379
|
+
|
|
380
|
+
### Reading the request
|
|
381
|
+
- The request is the tagged block's text with `#@Nova` removed, plus every child block that is not a `[NOVA]` reply, read as further instructions.
|
|
382
|
+
- Context is the page title, the ancestor chain, and the sibling blocks under the same parent. Fetch any URL present before answering.
|
|
383
|
+
- Empty tag line and no children means "review and respond to the surrounding context."
|
|
384
|
+
|
|
385
|
+
### Writing the reply
|
|
386
|
+
- First child of the tagged block: `[NOVA] <one-line summary, at most 150 characters>`.
|
|
387
|
+
- The rest of the reply nests under that `[NOVA]` block as ordinary blocks. Headings and bullets become nesting.
|
|
388
|
+
- Then flip the tag in place: `#@Nova` becomes `#@Nova-DONE`. Leave the rest of the block text untouched. A `{{[[TODO]]}}` on the block stays as-is; that checkbox is Ian's.
|
|
389
|
+
- Then embed on today's daily page under a `## Nova Responses` heading: a block `Nova response on [[<origin page>]]` with the child `{{[[embed]]: ((<uid of the [NOVA] block>))}}`. The guidelines' root-level rule for daily pages applies to memories; Nova responses go under this heading.
|
|
390
|
+
|
|
391
|
+
### Idempotency
|
|
392
|
+
- A block that already has a `[NOVA]` child is answered. Do not answer it again, even if the tag still reads `#@Nova`. The nightly cleanup pass removes stray duplicates; do not create them.
|
|
393
|
+
- `#@Nova-DONE` is never a work queue. Search `@Nova` for open items and skip anything containing `-DONE`.
|
|
394
|
+
|
|
395
|
+
### Who runs when (America/Los_Angeles)
|
|
396
|
+
| When | Job | Scope |
|
|
397
|
+
|---|---|---|
|
|
398
|
+
| 03:00 nightly | AI Intel sweep, `Skills/AIIntel/Tools/ProcessTriage.ts` | `#@Nova` and `#Status/*` on the `AI Intel Report` and `PAI-Anthropic Report` pages |
|
|
399
|
+
| 04:30 nightly | `Skills/Productivity/scripts/ProcessNovaTodos.ts`, EventBridge job `nova_todo_processing` | `#@Nova` on every other page |
|
|
400
|
+
| Interactive | This session, when Ian points at a block or asks for a sweep | Same output shape as above |
|
|
401
|
+
|
|
402
|
+
- Both nightly jobs post their own Telegram summary. An interactive answer does not need one.
|
|
403
|
+
- Dry run by hand: `bun $PAI_DIR/Skills/Productivity/scripts/ProcessNovaTodos.ts --dry-run`. Add `--cleanup` to only remove duplicate `[NOVA]` blocks.
|
|
404
|
+
- On AI Intel pages, `#Status/ExecSum`, `#Status/Explore`, `#Status/Backburner`, and `#Status/Reject` run unattended. `#Status/Implement`, `#Status/PRD`, and `#@Nova` wait for an interactive session. Processed items end as `#Status/Processed`.
|
|
405
|
+
|
|
406
|
+
## Retired conventions
|
|
407
|
+
- `#PAI/do`, `#PAI/done`, `#PAI/hold`, `#PAI/ignore`: replaced by `#@Nova` and `#@Nova-DONE`. Nothing processes them and no block carried them as of September 6th, 2026. Never create them. If one turns up, treat it as `#@Nova`.
|
|
408
|
+
- The `| [[Date]]` suffix once used to surface nested results on the daily page: replaced by the embed above.
|
|
409
|
+
|
|
410
|
+
## Ian's capture tags (not Nova's queue)
|
|
411
|
+
- `{{[[TODO]]}} #task <action>` on the daily page is Ian's capture inbox. ProcessFlow routes it to OmniFocus weekly. Do not convert `#task` to `#@Nova` unprompted; Ian decides what to delegate.
|
|
412
|
+
- `#researchThis` means look into this later. It is not a task and not a delegation.
|
|
413
|
+
- `#suggested-next-action` is a follow-up Nova proposes under a reply. Ian adds `#@Nova` to delegate it or checks it `{{[[DONE]]}}` to close it himself.
|
|
414
|
+
</personalization_layer>
|
|
@@ -1,26 +1,49 @@
|
|
|
1
|
+
import { escapeBlockString, needsNewlineEscaping, ESCAPED_NEWLINES_MARKER } from '../../shared/block-escaping.js';
|
|
1
2
|
/**
|
|
2
|
-
* Convert RoamBlock hierarchy to markdown with proper indentation
|
|
3
|
+
* Convert RoamBlock hierarchy to markdown with proper indentation.
|
|
4
|
+
*
|
|
5
|
+
* `escape` mirrors `PageOperations.fetchPageByTitle`'s markdown branch: a
|
|
6
|
+
* block may contain a soft line break (Shift+Enter), and this renderer emits
|
|
7
|
+
* one `- ` line per block, so an unescaped newline spills onto a second
|
|
8
|
+
* physical line with no bullet, resetting the parser's indentation baseline
|
|
9
|
+
* and reparenting everything after it. Callers that render a full page
|
|
10
|
+
* compute whether escaping is needed and set this flag; callers that never
|
|
11
|
+
* feed their output back through `updatePageMarkdown` can leave it off.
|
|
3
12
|
*/
|
|
4
|
-
export function blocksToMarkdown(blocks, level = 0) {
|
|
13
|
+
export function blocksToMarkdown(blocks, level = 0, escape = false) {
|
|
5
14
|
return blocks
|
|
6
15
|
.map(block => {
|
|
7
16
|
const indent = ' '.repeat(level);
|
|
8
17
|
let md;
|
|
18
|
+
const text = escape ? escapeBlockString(block.string) : block.string;
|
|
9
19
|
// Check block heading level and format accordingly
|
|
10
20
|
if (block.heading && block.heading > 0) {
|
|
11
21
|
const hashtags = '#'.repeat(block.heading);
|
|
12
|
-
md = `${indent}${hashtags} ${
|
|
22
|
+
md = `${indent}${hashtags} ${text}`;
|
|
13
23
|
}
|
|
14
24
|
else {
|
|
15
|
-
md = `${indent}- ${
|
|
25
|
+
md = `${indent}- ${text}`;
|
|
16
26
|
}
|
|
17
27
|
if (block.children && block.children.length > 0) {
|
|
18
|
-
md += '\n' + blocksToMarkdown(block.children, level + 1);
|
|
28
|
+
md += '\n' + blocksToMarkdown(block.children, level + 1, escape);
|
|
19
29
|
}
|
|
20
30
|
return md;
|
|
21
31
|
})
|
|
22
32
|
.join('\n');
|
|
23
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Collect every block string in a tree, for deciding whether a page needs
|
|
36
|
+
* newline escaping at all (see `blocksToMarkdown`).
|
|
37
|
+
*/
|
|
38
|
+
function collectBlockStrings(blocks, out = []) {
|
|
39
|
+
for (const block of blocks) {
|
|
40
|
+
out.push(block.string);
|
|
41
|
+
if (block.children && block.children.length > 0) {
|
|
42
|
+
collectBlockStrings(block.children, out);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
24
47
|
/**
|
|
25
48
|
* Flatten block hierarchy to single-level list
|
|
26
49
|
*/
|
|
@@ -42,7 +65,11 @@ export function formatPageOutput(title, blocks, options) {
|
|
|
42
65
|
return JSON.stringify({ title, children: data }, null, 2);
|
|
43
66
|
}
|
|
44
67
|
const displayBlocks = options.flat ? flattenBlocks(blocks) : blocks;
|
|
45
|
-
|
|
68
|
+
const escape = needsNewlineEscaping(collectBlockStrings(displayBlocks));
|
|
69
|
+
const body = blocksToMarkdown(displayBlocks, 0, escape);
|
|
70
|
+
return escape
|
|
71
|
+
? `# ${title}\n${ESCAPED_NEWLINES_MARKER}\n\n${body}`
|
|
72
|
+
: `# ${title}\n\n${body}`;
|
|
46
73
|
}
|
|
47
74
|
/**
|
|
48
75
|
* Format block content for output
|
|
@@ -53,7 +80,9 @@ export function formatBlockOutput(block, options) {
|
|
|
53
80
|
return JSON.stringify(data, null, 2);
|
|
54
81
|
}
|
|
55
82
|
const displayBlocks = options.flat ? flattenBlocks([block]) : [block];
|
|
56
|
-
|
|
83
|
+
const escape = needsNewlineEscaping(collectBlockStrings(displayBlocks));
|
|
84
|
+
const body = blocksToMarkdown(displayBlocks, 0, escape);
|
|
85
|
+
return escape ? `${ESCAPED_NEWLINES_MARKER}\n\n${body}` : body;
|
|
57
86
|
}
|
|
58
87
|
/**
|
|
59
88
|
* Format search results for output
|