roam-research-mcp 2.18.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -9
- package/build/Roam_Markdown_Cheatsheet.md +185 -3
- 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/server.js +240 -0
- package/build/cli/commands/update.js +29 -11
- package/build/cli/commands/update.test.js +69 -0
- package/build/cli/roam.js +2 -0
- package/build/config/environment.js +9 -1
- package/build/index.js +6 -1
- package/build/markdown-utils.js +50 -4
- package/build/markdown-utils.test.js +111 -1
- package/build/search/utils.js +12 -2
- package/build/server/roam-server.js +63 -9
- package/build/tools/helpers/page-resolution.js +7 -1
- package/build/tools/operations/outline.js +26 -48
- package/build/utils/auth.js +24 -0
- package/build/utils/auth.test.js +34 -0
- package/build/utils/helpers.js +136 -3
- package/build/utils/helpers.test.js +128 -1
- package/build/utils/net.js +12 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,12 @@ roam search --namespace "Convention" # Finds all Convention/* pages
|
|
|
46
46
|
# Fetch a page by title
|
|
47
47
|
roam get "Roam Research"
|
|
48
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
|
+
|
|
49
55
|
# Fetch a block with ancestors (parent chain to page root)
|
|
50
56
|
roam get abc123def -a # Block + children + ancestors
|
|
51
57
|
roam get abc123def -a -d 0 # Ancestors only, no children
|
|
@@ -70,7 +76,7 @@ roam get "Page Title" -g work
|
|
|
70
76
|
roam save "Note" -g work --write-key "$ROAM_SYSTEM_WRITE_KEY"
|
|
71
77
|
```
|
|
72
78
|
|
|
73
|
-
**Available Commands:** `get`, `search`, `save`, `refs`, `update`, `batch`, `rename`, `status`.
|
|
79
|
+
**Available Commands:** `get`, `search`, `save`, `refs`, `update`, `batch`, `rename`, `status`, `server`.
|
|
74
80
|
Run `roam <command> --help` for details on any command.
|
|
75
81
|
|
|
76
82
|
### Installation
|
|
@@ -154,30 +160,97 @@ Protected graphs require the `write_key` parameter matching `ROAM_SYSTEM_WRITE_K
|
|
|
154
160
|
|
|
155
161
|
*Optional:*
|
|
156
162
|
- `ROAM_MEMORIES_TAG`: Default tag for `roam_remember`/`roam_recall` (fallback when per-graph `memoriesTag` not set).
|
|
157
|
-
- `HTTP_STREAM_PORT`:
|
|
163
|
+
- `HTTP_STREAM_PORT`: Port for the HTTP Stream transport (defaults to 8088).
|
|
164
|
+
- `HTTP_STREAM_HOST`: Host to bind the HTTP transport to in `--server` mode (defaults to `127.0.0.1`, loopback-only). Set to `0.0.0.0` to expose on the LAN.
|
|
165
|
+
- `HTTP_AUTH_TOKEN`: Optional bearer token for the HTTP MCP endpoint (**authentication**). Unset = open (fine for loopback). When set, every HTTP MCP request must send `Authorization: Bearer <token>` (`GET /health` stays open). Use it whenever you bind beyond `127.0.0.1`. This is separate from `ROAM_SYSTEM_WRITE_KEY` (per-graph write authorization).
|
|
158
166
|
|
|
159
167
|
### Running the Server
|
|
160
168
|
|
|
161
|
-
**1.
|
|
162
|
-
Best for local integration (e.g., Claude Desktop, IDE extensions).
|
|
169
|
+
**1. Default Mode (stdio + HTTP)**
|
|
170
|
+
Best for local integration (e.g., Claude Desktop, IDE extensions). The MCP client launches the process per session over stdio; an HTTP Stream transport is also opened on an auto-discovered port near `HTTP_STREAM_PORT`.
|
|
163
171
|
|
|
164
172
|
```bash
|
|
165
173
|
npx roam-research-mcp
|
|
166
174
|
```
|
|
167
175
|
|
|
168
|
-
|
|
176
|
+
**2. Shared Server Mode (`--server`)**
|
|
177
|
+
Best for a single long-lived, HTTP-only daemon that **multiple MCP clients share** — instead of each session spawning its own subprocess. This saves memory and gives clients a stable URL.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
HTTP_STREAM_PORT=8088 npx roam-research-mcp --server
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or manage it through the `roam` CLI, which adds start/stop/status/logs:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
roam server start # start the shared daemon in the background
|
|
187
|
+
roam server start -H 0.0.0.0 # expose on the LAN (no transport auth!)
|
|
188
|
+
roam server status # is it up? version, graphs, active sessions
|
|
189
|
+
roam server logs -f # follow the log
|
|
190
|
+
roam server stop # stop a CLI-started daemon
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`roam server status` works no matter how the daemon was launched (it probes `/health`), so it also reports a daemon started by a LaunchAgent/systemd unit. State (pidfile + log) lives in `~/.roam/` (override with `ROAM_HOME`).
|
|
194
|
+
|
|
195
|
+
In `--server` mode the server:
|
|
196
|
+
- runs **HTTP-only** (no stdio transport),
|
|
197
|
+
- binds the **exact** `HTTP_STREAM_PORT` on `HTTP_STREAM_HOST` and **exits non-zero if the port is taken** (no silent drift — a shared daemon must keep a stable URL),
|
|
198
|
+
- exposes `GET /health` → `{"status":"ok", ...}` for liveness checks.
|
|
199
|
+
|
|
200
|
+
Point MCP clients at it with an HTTP transport config:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"mcpServers": {
|
|
205
|
+
"roam-research-mcp": {
|
|
206
|
+
"type": "http",
|
|
207
|
+
"url": "http://127.0.0.1:8088/mcp"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Env vars (tokens, graphs) live with the **server** process, not the client config.
|
|
214
|
+
|
|
215
|
+
**Securing an exposed server (two layers):**
|
|
216
|
+
If you bind beyond loopback (`-H 0.0.0.0`), add the perimeter lock:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
HTTP_AUTH_TOKEN=$(openssl rand -hex 32) roam server start -H 0.0.0.0
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Clients then send the token as a header:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"mcpServers": {
|
|
227
|
+
"roam-research-mcp": {
|
|
228
|
+
"type": "http",
|
|
229
|
+
"url": "http://<host>:8088/mcp",
|
|
230
|
+
"headers": { "Authorization": "Bearer <token>" }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
These are two **distinct** layers — keep both:
|
|
237
|
+
- **`HTTP_AUTH_TOKEN`** = *authentication* (who may connect). Gates **all** requests — reads and writes, every graph.
|
|
238
|
+
- **`ROAM_SYSTEM_WRITE_KEY`** = *authorization* (what a connected caller may do). Gates only **writes** to `protected` graphs; it does **not** protect reads.
|
|
239
|
+
|
|
240
|
+
> ⚠️ `write_key` is **not** transport auth. On an exposed server without `HTTP_AUTH_TOKEN`, anyone on the network can still **read every graph** and write non-protected graphs. For anything beyond loopback, set `HTTP_AUTH_TOKEN`.
|
|
169
241
|
|
|
170
|
-
**
|
|
171
|
-
|
|
242
|
+
**Keeping it running (macOS LaunchAgent):**
|
|
243
|
+
Create `~/Library/LaunchAgents/com.example.roam-mcp.plist` with `RunAtLoad` + `KeepAlive`, your env vars under `EnvironmentVariables`, and `--server` as the last `ProgramArguments` entry. Keep `StandardOutPath`/`StandardErrorPath` on a **local** path (e.g. `~/Library/Logs/`), then:
|
|
172
244
|
|
|
173
245
|
```bash
|
|
174
|
-
|
|
246
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.roam-mcp.plist
|
|
247
|
+
curl -s http://127.0.0.1:8088/health # verify
|
|
175
248
|
```
|
|
176
249
|
|
|
177
250
|
**3. Docker**
|
|
178
251
|
|
|
179
252
|
```bash
|
|
180
|
-
docker run -p 8088:8088 --env-file .env roam-research-mcp
|
|
253
|
+
docker run -p 8088:8088 --env-file .env roam-research-mcp --server
|
|
181
254
|
```
|
|
182
255
|
|
|
183
256
|
### Configuring in LLMs
|
|
@@ -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
|
|
@@ -237,8 +243,6 @@ Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
|
|
|
237
243
|
**Open question:** `{{[[TODO]]}} Research: <question> #[[open questions]]`
|
|
238
244
|
|
|
239
245
|
---
|
|
240
|
-
<personalization_layer>
|
|
241
|
-
|
|
242
246
|
# Roam Preferences — Personalization Layer
|
|
243
247
|
|
|
244
248
|
> This section contains YOUR specific conventions, tagging philosophy, and graph-specific rules. Customize to match your workflow.
|
|
@@ -247,21 +251,199 @@ Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
|
|
|
247
251
|
|
|
248
252
|
## Graph-Level Behaviors
|
|
249
253
|
|
|
254
|
+
### On Creating New Pages
|
|
255
|
+
<!-- CUSTOMIZE: What should happen when a new page is created? -->
|
|
256
|
+
- After creating a new page, add a reference block on today's daily page: `Created page: [[New Page Name]]`
|
|
257
|
+
- <!-- Add any naming conventions, required metadata, etc. -->
|
|
258
|
+
|
|
259
|
+
### On Adding Content
|
|
260
|
+
<!-- CUSTOMIZE: Any rules about where/how content gets added? -->
|
|
261
|
+
- Default location for quick captures: Daily page
|
|
262
|
+
- Long-form content: Create dedicated page, link from daily page
|
|
263
|
+
- <!-- Your preferences here -->
|
|
264
|
+
|
|
265
|
+
---
|
|
250
266
|
|
|
251
267
|
## Tagging Philosophy
|
|
252
268
|
|
|
269
|
+
### Core Principle
|
|
270
|
+
> Tag for **intellectual collision** and **future discovery**, not just categorization. Every tag should maximize potential for unexpected connections.
|
|
271
|
+
|
|
272
|
+
### The Serendipity Test
|
|
273
|
+
Before tagging, ask: *"Could this concept surprise me by connecting to something completely unrelated?"*
|
|
274
|
+
|
|
275
|
+
### What To Tag — Decision Framework
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
ASK YOURSELF:
|
|
279
|
+
┌─ How will Future Me find this?
|
|
280
|
+
│ └─ Tag by retrieval context, not just content
|
|
281
|
+
│
|
|
282
|
+
├─ What domain does this belong to?
|
|
283
|
+
│ └─ Use broad category tags: #[[knowledge management]], #[[decision-making]]
|
|
284
|
+
│
|
|
285
|
+
├─ Is this a proper noun?
|
|
286
|
+
│ └─ YES → Wrap name (no titles): [[Werner Erhard]], [[NASA]]
|
|
287
|
+
│ └─ For abbreviations: [NASA]([[National Aeronautics and Space Administration (NASA)]])
|
|
288
|
+
│
|
|
289
|
+
├─ Could this alias to existing page?
|
|
290
|
+
│ └─ YES → [displayed phrase]([[existing page name]])
|
|
291
|
+
│ └─ Example: [frameworks for decisions]([[decision-making frameworks]])
|
|
292
|
+
│
|
|
293
|
+
└─ Parent block with children?
|
|
294
|
+
└─ Tag parent when category applies to all children
|
|
295
|
+
└─ Tag individual children for specific categorization
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Tag Type Selection
|
|
299
|
+
|
|
300
|
+
| Use This | When |
|
|
301
|
+
|----------|------|
|
|
302
|
+
| `[[Page Reference]]` | Concept deserves its own page, will be expanded |
|
|
303
|
+
| `#[[hashtag]]` | Categorization, filtering, won't be a standalone page |
|
|
304
|
+
| `#single-word` | Simple, unambiguous category |
|
|
305
|
+
| Attribute `Type::` | Structured metadata for queries |
|
|
306
|
+
|
|
307
|
+
### WHEN creating Endnotes/Footnotes:
|
|
308
|
+
- 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.)
|
|
309
|
+
- If not known, retrieve the block_uid reference for this footnote item.
|
|
310
|
+
- In the block referencing the footnote, append the reference with footnote-item-block_id, example: "- <block_text> #ref ((block_uid))"
|
|
311
|
+
|
|
312
|
+
### Structural Tagging (Beyond Content)
|
|
313
|
+
|
|
314
|
+
Tag by **patterns and mechanisms**, not just subjects:
|
|
315
|
+
|
|
316
|
+
| Structural Tag | Connects |
|
|
317
|
+
|----------------|----------|
|
|
318
|
+
| `#[[has feedback loops]]` | Systems, habits, markets, conversations |
|
|
319
|
+
| `#[[requires calibration]]` | Instruments, relationships, AI prompts |
|
|
320
|
+
| `#[[exhibits emergence]]` | Complexity, culture, creativity |
|
|
321
|
+
| `#[[perspective switching]]` | Photography, negotiation, analysis |
|
|
322
|
+
| `#[[flow dynamics]]` | Fluids, music, conversation, sequences |
|
|
323
|
+
|
|
324
|
+
### Problem-Oriented Tagging
|
|
325
|
+
|
|
326
|
+
Tag by problems solved, not methods used:
|
|
327
|
+
|
|
328
|
+
- `#[[breaking cognitive constraints]]`
|
|
329
|
+
- `#[[expanding solution spaces]]`
|
|
330
|
+
- `#[[preventing expert blindness]]`
|
|
331
|
+
|
|
332
|
+
### Temporal & State-Based Tags
|
|
333
|
+
|
|
334
|
+
| Tag Type | Examples |
|
|
335
|
+
|----------|----------|
|
|
336
|
+
| Future relevance | `#[[will be relevant in 5 years]]`, `#[[connects to unborn projects]]` |
|
|
337
|
+
| Mental state triggers | `#[[feeling stuck in patterns]]`, `#[[needing fresh perspective]]` |
|
|
338
|
+
| Review scheduling | `[[For review]]: [[August 12th, 2026]]` |
|
|
339
|
+
|
|
340
|
+
---
|
|
253
341
|
|
|
254
342
|
## Formatting Conventions
|
|
255
343
|
|
|
344
|
+
### Quotes
|
|
345
|
+
```
|
|
346
|
+
<quote text> —[[Author Name]] #quote #[[topic1]] #[[topic2]]
|
|
347
|
+
```
|
|
348
|
+
Always include 2-3 relevant hashtags after quotes.
|
|
349
|
+
|
|
350
|
+
### TODOs and Follow-ups
|
|
351
|
+
```
|
|
352
|
+
{{[[TODO]]}} <action needed>
|
|
353
|
+
{{[[TODO]]}} #researchThis : <topic to investigate>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Scheduled Reviews
|
|
357
|
+
|
|
358
|
+
- Any block tagged with a date will show on that respective daily page.
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
[[For review]]: [[Date in ordinal format]]
|
|
362
|
+
```
|
|
363
|
+
Optional labels: "Deadline", "Approved", "Pending", "Deferred", "Postponed until"
|
|
364
|
+
|
|
365
|
+
### Aliasing for Case Sensitivity
|
|
366
|
+
When a tag would awkwardly affect sentence capitalization:
|
|
367
|
+
```
|
|
368
|
+
[Cognitive biases]([[cognitive biases]]) affect decision-making...
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Definitions (OVERRIDE)
|
|
372
|
+
```
|
|
373
|
+
#def [[<term>]] : <definition>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
256
377
|
|
|
257
378
|
## Constraints & Guardrails
|
|
258
379
|
|
|
380
|
+
### DON'T
|
|
381
|
+
- **Overtag** — Quality over quantity; each tag should earn its place
|
|
382
|
+
- **Tag obvious/redundant** — If parent block is tagged, children inherit context
|
|
383
|
+
- **Use inconsistent capitalization** — Tags are lowercase unless proper nouns
|
|
384
|
+
- **Create orphan tags** — Check if existing page/tag serves the purpose
|
|
385
|
+
- **Bold Attributes** - ❌ `**Attribute**::`, ✅ `Attribute::` (Roam auto-formats)
|
|
386
|
+
- **Separators** - `---` Don't use them.
|
|
387
|
+
|
|
388
|
+
### DO
|
|
389
|
+
- **Think retrieval-first** — How will you search for this later?
|
|
390
|
+
- **Cross-pollinate domains** — Force unlikely intellectual meetings
|
|
391
|
+
- **Update aging tags** — As interests evolve, so should tag vocabulary
|
|
392
|
+
- **Track surprise discoveries** — When unexpected connections yield insights, engineer more of those patterns
|
|
393
|
+
|
|
394
|
+
---
|
|
259
395
|
|
|
260
396
|
## Custom Rules
|
|
261
397
|
|
|
398
|
+
<!--
|
|
399
|
+
CUSTOMIZE THIS SECTION with your specific conventions:
|
|
400
|
+
- Naming patterns for certain page types
|
|
401
|
+
- Required attributes for books/articles/people
|
|
402
|
+
- Project-specific tagging schemes
|
|
403
|
+
- Integration rules with other tools
|
|
404
|
+
- etc.
|
|
405
|
+
-->
|
|
406
|
+
|
|
407
|
+
### Example Custom Rules (modify as needed):
|
|
408
|
+
|
|
409
|
+
**Books:**
|
|
410
|
+
```
|
|
411
|
+
[[Book/<title> | <author>]]
|
|
412
|
+
Type:: Book
|
|
413
|
+
Author:: [[Author Name]]
|
|
414
|
+
Status:: Reading | Completed | Abandoned
|
|
415
|
+
Rating:: X/5
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**People:**
|
|
419
|
+
```
|
|
420
|
+
[[Person Name]]
|
|
421
|
+
Type:: Person
|
|
422
|
+
Context:: How I know them
|
|
423
|
+
```
|
|
424
|
+
- When linking bibliographic references —>
|
|
425
|
+
Example: `McAdams, D.P. (2001) [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100) — foundational paper`
|
|
426
|
+
- [McAdams, D.P.]([[Dan McAdams]]) - author's name in the graph
|
|
427
|
+
- If source URL, link to source: [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100)
|
|
428
|
+
- If notes page exists or will exist in Roam: append ` | [Notes]([[Article/The Psychology of Life Stories]]), if not, just leave it without link.
|
|
429
|
+
|
|
430
|
+
**Projects:**
|
|
431
|
+
```
|
|
432
|
+
[[Project/<project anme>]]
|
|
433
|
+
Status:: Active | Paused | Completed
|
|
434
|
+
Start:: [[Date]]
|
|
435
|
+
```
|
|
436
|
+
---
|
|
262
437
|
|
|
263
438
|
## Integration Notes
|
|
264
439
|
|
|
265
440
|
<!-- CUSTOMIZE: Any rules about how Roam integrates with your other tools/systems -->
|
|
266
441
|
|
|
267
|
-
|
|
442
|
+
- Daily pages serve as: <!-- inbox / journal / task list / etc. -->
|
|
443
|
+
- Weekly reviews occur on: <!-- day of week -->
|
|
444
|
+
- Content flows from: <!-- capture tools, read-later apps, etc. -->
|
|
445
|
+
- Content flows to: <!-- publishing, archives, etc. -->
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
*End of 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));
|