roam-research-mcp 2.4.0 → 2.4.3
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 +3 -1
- package/build/Roam_Markdown_Cheatsheet.md +116 -287
- package/build/cli/commands/batch.js +1 -8
- package/build/cli/commands/get.js +82 -51
- package/build/cli/commands/refs.js +50 -32
- package/build/cli/commands/save.js +9 -13
- package/build/cli/commands/search.js +20 -60
- package/build/cli/commands/update.js +71 -28
- package/build/cli/utils/input.js +10 -0
- package/build/server/roam-server.js +2 -2
- package/build/tools/operations/memory.js +10 -6
- package/build/tools/schemas.js +8 -3
- package/build/tools/tool-handlers.js +2 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { BlockRetrievalOperations } from '../../tools/operations/block-retrieval
|
|
|
4
4
|
import { SearchOperations } from '../../tools/operations/search/index.js';
|
|
5
5
|
import { formatPageOutput, formatBlockOutput, formatTodoOutput, printDebug, exitWithError } from '../utils/output.js';
|
|
6
6
|
import { resolveGraph } from '../utils/graph.js';
|
|
7
|
+
import { readStdin } from '../utils/input.js';
|
|
7
8
|
import { resolveRefs } from '../../tools/helpers/refs.js';
|
|
8
9
|
import { resolveRelativeDate } from '../../utils/helpers.js';
|
|
9
10
|
// Block UID pattern: 9 alphanumeric characters, optionally wrapped in (( ))
|
|
@@ -29,7 +30,7 @@ async function resolveBlocksRefs(graph, blocks, maxDepth) {
|
|
|
29
30
|
export function createGetCommand() {
|
|
30
31
|
return new Command('get')
|
|
31
32
|
.description('Fetch pages, blocks, or TODO/DONE items with optional ref expansion')
|
|
32
|
-
.argument('[target]', 'Page title, block UID, or relative date
|
|
33
|
+
.argument('[target]', 'Page title, block UID, or relative date. Reads from stdin if "-" or omitted.')
|
|
33
34
|
.option('-j, --json', 'Output as JSON instead of markdown')
|
|
34
35
|
.option('-d, --depth <n>', 'Child levels to fetch (default: 4)', '4')
|
|
35
36
|
.option('-r, --refs [n]', 'Expand ((uid)) refs in output (default depth: 1, max: 4)')
|
|
@@ -47,12 +48,16 @@ Examples:
|
|
|
47
48
|
roam get "Project Notes" # Page by title
|
|
48
49
|
roam get today # Today's daily page
|
|
49
50
|
roam get yesterday # Yesterday's daily page
|
|
50
|
-
roam get tomorrow # Tomorrow's daily page
|
|
51
51
|
|
|
52
52
|
# Fetch blocks
|
|
53
53
|
roam get abc123def # Block by UID
|
|
54
54
|
roam get "((abc123def))" # UID with wrapper
|
|
55
55
|
|
|
56
|
+
# Stdin / Batch Retrieval
|
|
57
|
+
echo "Project A" | roam get # Pipe page title
|
|
58
|
+
echo "abc123def" | roam get # Pipe block UID
|
|
59
|
+
cat uids.txt | roam get --json # Fetch multiple blocks (NDJSON output)
|
|
60
|
+
|
|
56
61
|
# Output options
|
|
57
62
|
roam get "Page" -j # JSON output
|
|
58
63
|
roam get "Page" -f # Flat list (no hierarchy)
|
|
@@ -64,8 +69,6 @@ Examples:
|
|
|
64
69
|
roam get --todo # All TODOs across graph
|
|
65
70
|
roam get --done # All completed items
|
|
66
71
|
roam get --todo -p "Work" # TODOs on "Work" page
|
|
67
|
-
roam get --todo -i "urgent,blocker" # TODOs containing these terms
|
|
68
|
-
roam get --todo -e "someday,maybe" # Exclude items with terms
|
|
69
72
|
`)
|
|
70
73
|
.action(async (target, options) => {
|
|
71
74
|
try {
|
|
@@ -81,11 +84,12 @@ Examples:
|
|
|
81
84
|
debug: options.debug
|
|
82
85
|
};
|
|
83
86
|
if (options.debug) {
|
|
84
|
-
printDebug('Target', target);
|
|
87
|
+
printDebug('Target', target || 'stdin');
|
|
85
88
|
printDebug('Graph', options.graph || 'default');
|
|
86
89
|
printDebug('Options', { depth, refs: refsDepth || 'off', ...outputOptions });
|
|
87
90
|
}
|
|
88
|
-
// Handle --todo or --done flags
|
|
91
|
+
// Handle --todo or --done flags (these ignore target arg usually, but could filter by page if target is used as page?)
|
|
92
|
+
// The help says "-p" is for page. So we strictly follow flags.
|
|
89
93
|
if (options.todo || options.done) {
|
|
90
94
|
const status = options.todo ? 'TODO' : 'DONE';
|
|
91
95
|
if (options.debug) {
|
|
@@ -96,61 +100,88 @@ Examples:
|
|
|
96
100
|
console.log(formatTodoOutput(result.matches, status, outputOptions));
|
|
97
101
|
return;
|
|
98
102
|
}
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Resolve relative date keywords (today, yesterday, tomorrow)
|
|
104
|
-
const resolvedTarget = resolveRelativeDate(target);
|
|
105
|
-
if (options.debug && resolvedTarget !== target) {
|
|
106
|
-
printDebug('Resolved date', `${target} → ${resolvedTarget}`);
|
|
103
|
+
// Determine targets
|
|
104
|
+
let targets = [];
|
|
105
|
+
if (target && target !== '-') {
|
|
106
|
+
targets = [target];
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (options.debug) {
|
|
114
|
-
printDebug('Fetching block', { uid: blockUid, depth });
|
|
115
|
-
}
|
|
116
|
-
const blockOps = new BlockRetrievalOperations(graph);
|
|
117
|
-
let block = await blockOps.fetchBlockWithChildren(blockUid, depth);
|
|
118
|
-
if (!block) {
|
|
119
|
-
exitWithError(`Block with UID "${blockUid}" not found`);
|
|
108
|
+
else {
|
|
109
|
+
// Read from stdin if no target or explicit '-'
|
|
110
|
+
if (process.stdin.isTTY && target !== '-') {
|
|
111
|
+
// If TTY and no target, show error
|
|
112
|
+
exitWithError('Target is required. Use: roam get <page-title>, roam get --todo, or pipe targets via stdin');
|
|
120
113
|
}
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
114
|
+
const input = await readStdin();
|
|
115
|
+
if (input) {
|
|
116
|
+
targets = input.split('\n').map(t => t.trim()).filter(Boolean);
|
|
124
117
|
}
|
|
125
|
-
console.log(formatBlockOutput(block, outputOptions));
|
|
126
118
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
if (targets.length === 0) {
|
|
120
|
+
exitWithError('No targets provided');
|
|
121
|
+
}
|
|
122
|
+
// Helper to process a single target
|
|
123
|
+
const processTarget = async (item) => {
|
|
124
|
+
// Resolve relative date keywords (today, yesterday, tomorrow)
|
|
125
|
+
const resolvedTarget = resolveRelativeDate(item);
|
|
126
|
+
if (options.debug && resolvedTarget !== item) {
|
|
127
|
+
printDebug('Resolved date', `${item} → ${resolvedTarget}`);
|
|
131
128
|
}
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
// Check if target is a block UID
|
|
130
|
+
const uidMatch = resolvedTarget.match(BLOCK_UID_PATTERN);
|
|
131
|
+
if (uidMatch) {
|
|
132
|
+
// Fetch block by UID
|
|
133
|
+
const blockUid = uidMatch[1];
|
|
134
|
+
if (options.debug)
|
|
135
|
+
printDebug('Fetching block', { uid: blockUid });
|
|
136
|
+
const blockOps = new BlockRetrievalOperations(graph);
|
|
137
|
+
let block = await blockOps.fetchBlockWithChildren(blockUid, depth);
|
|
138
|
+
if (!block) {
|
|
139
|
+
// If fetching multiple, maybe warn instead of exit?
|
|
140
|
+
// For now, consistent behavior: print error message to stderr but continue?
|
|
141
|
+
// Or simpler: just return a "not found" string/object.
|
|
142
|
+
// formatBlockOutput doesn't handle null.
|
|
143
|
+
return options.json ? JSON.stringify({ error: `Block ${blockUid} not found` }) : `Block ${blockUid} not found`;
|
|
139
144
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return;
|
|
145
|
+
// Resolve block references if requested
|
|
146
|
+
if (refsDepth > 0) {
|
|
147
|
+
block = await resolveBlockRefs(graph, block, refsDepth);
|
|
144
148
|
}
|
|
149
|
+
return formatBlockOutput(block, outputOptions);
|
|
145
150
|
}
|
|
146
151
|
else {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
// Fetch page by title
|
|
153
|
+
if (options.debug)
|
|
154
|
+
printDebug('Fetching page', { title: resolvedTarget });
|
|
155
|
+
const pageOps = new PageOperations(graph);
|
|
156
|
+
const result = await pageOps.fetchPageByTitle(resolvedTarget, 'raw');
|
|
157
|
+
// Parse the raw result
|
|
158
|
+
let blocks;
|
|
159
|
+
if (typeof result === 'string') {
|
|
160
|
+
try {
|
|
161
|
+
blocks = JSON.parse(result);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Result is already formatted as string (e.g., "Page Title (no content found)")
|
|
165
|
+
// But wait, fetchPageByTitle returns string if not found or empty?
|
|
166
|
+
// Actually fetchPageByTitle 'raw' returns JSON string of blocks OR empty array JSON string?
|
|
167
|
+
// Let's assume result is valid JSON or error message string.
|
|
168
|
+
return options.json ? JSON.stringify({ title: resolvedTarget, error: result }) : result;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
blocks = result;
|
|
173
|
+
}
|
|
174
|
+
// Resolve block references if requested
|
|
175
|
+
if (refsDepth > 0) {
|
|
176
|
+
blocks = await resolveBlocksRefs(graph, blocks, refsDepth);
|
|
177
|
+
}
|
|
178
|
+
return formatPageOutput(resolvedTarget, blocks, outputOptions);
|
|
152
179
|
}
|
|
153
|
-
|
|
180
|
+
};
|
|
181
|
+
// Execute sequentially
|
|
182
|
+
for (const t of targets) {
|
|
183
|
+
const output = await processTarget(t);
|
|
184
|
+
console.log(output);
|
|
154
185
|
}
|
|
155
186
|
}
|
|
156
187
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { SearchOperations } from '../../tools/operations/search/index.js';
|
|
3
3
|
import { printDebug, exitWithError } from '../utils/output.js';
|
|
4
4
|
import { resolveGraph } from '../utils/graph.js';
|
|
5
|
+
import { readStdin } from '../utils/input.js';
|
|
5
6
|
/**
|
|
6
7
|
* Format results grouped by page (default output)
|
|
7
8
|
*/
|
|
@@ -72,7 +73,7 @@ function parseIdentifier(identifier) {
|
|
|
72
73
|
export function createRefsCommand() {
|
|
73
74
|
return new Command('refs')
|
|
74
75
|
.description('Find all blocks that reference a page, tag, or block')
|
|
75
|
-
.argument('
|
|
76
|
+
.argument('[identifier]', 'Page title, #tag, [[Page]], or ((block-uid)). Reads from stdin if "-" or omitted.')
|
|
76
77
|
.option('-n, --limit <n>', 'Limit number of results', '50')
|
|
77
78
|
.option('--json', 'Output as JSON array')
|
|
78
79
|
.option('--raw', 'Output raw UID + content lines (no grouping)')
|
|
@@ -82,49 +83,66 @@ export function createRefsCommand() {
|
|
|
82
83
|
Examples:
|
|
83
84
|
# Page references
|
|
84
85
|
roam refs "Project Alpha" # Blocks linking to page
|
|
85
|
-
roam refs "[[Meeting Notes]]" # With bracket syntax
|
|
86
86
|
roam refs "#TODO" # Blocks with #TODO tag
|
|
87
87
|
|
|
88
|
+
# Stdin / Batch references
|
|
89
|
+
echo "Project A" | roam refs # Pipe page title
|
|
90
|
+
cat uids.txt | roam refs --json # Find refs for multiple UIDs
|
|
91
|
+
|
|
88
92
|
# Block references
|
|
89
93
|
roam refs "((abc123def))" # Blocks embedding this block
|
|
90
|
-
|
|
91
|
-
# Output options
|
|
92
|
-
roam refs "Work" --json # JSON array output
|
|
93
|
-
roam refs "Ideas" --raw # Raw UID + content (no grouping)
|
|
94
|
-
roam refs "Tasks" -n 100 # Limit to 100 results
|
|
95
94
|
`)
|
|
96
95
|
.action(async (identifier, options) => {
|
|
97
96
|
try {
|
|
98
97
|
const graph = resolveGraph(options, false);
|
|
99
98
|
const limit = parseInt(options.limit || '50', 10);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
printDebug('Parsed', { block_uid, title });
|
|
105
|
-
printDebug('Options', options);
|
|
106
|
-
}
|
|
107
|
-
const searchOps = new SearchOperations(graph);
|
|
108
|
-
const result = await searchOps.searchBlockRefs({ block_uid, title });
|
|
109
|
-
if (options.debug) {
|
|
110
|
-
printDebug('Total matches', result.matches.length);
|
|
99
|
+
// Determine identifiers
|
|
100
|
+
let identifiers = [];
|
|
101
|
+
if (identifier && identifier !== '-') {
|
|
102
|
+
identifiers = [identifier];
|
|
111
103
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}));
|
|
121
|
-
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
104
|
+
else {
|
|
105
|
+
if (process.stdin.isTTY && identifier !== '-') {
|
|
106
|
+
exitWithError('Identifier is required. Use: roam refs <title> or pipe identifiers via stdin');
|
|
107
|
+
}
|
|
108
|
+
const input = await readStdin();
|
|
109
|
+
if (input) {
|
|
110
|
+
identifiers = input.split('\n').map(t => t.trim()).filter(Boolean);
|
|
111
|
+
}
|
|
122
112
|
}
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
if (identifiers.length === 0) {
|
|
114
|
+
exitWithError('No identifiers provided');
|
|
125
115
|
}
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
const searchOps = new SearchOperations(graph);
|
|
117
|
+
// Helper to process a single identifier
|
|
118
|
+
const processIdentifier = async (id) => {
|
|
119
|
+
const { block_uid, title } = parseIdentifier(id);
|
|
120
|
+
if (options.debug) {
|
|
121
|
+
printDebug('Identifier', id);
|
|
122
|
+
printDebug('Parsed', { block_uid, title });
|
|
123
|
+
}
|
|
124
|
+
const result = await searchOps.searchBlockRefs({ block_uid, title });
|
|
125
|
+
const limitedMatches = result.matches.slice(0, limit);
|
|
126
|
+
if (options.json) {
|
|
127
|
+
return JSON.stringify(limitedMatches.map(m => ({
|
|
128
|
+
uid: m.block_uid,
|
|
129
|
+
content: m.content,
|
|
130
|
+
page: m.page_title
|
|
131
|
+
})));
|
|
132
|
+
}
|
|
133
|
+
else if (options.raw) {
|
|
134
|
+
return formatRaw(limitedMatches);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
return formatGrouped(limitedMatches);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
// Execute
|
|
141
|
+
for (const id of identifiers) {
|
|
142
|
+
const output = await processIdentifier(id);
|
|
143
|
+
console.log(output);
|
|
144
|
+
if (identifiers.length > 1 && !options.json)
|
|
145
|
+
console.log('\n---\n');
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
catch (error) {
|
|
@@ -7,6 +7,7 @@ import { BatchOperations } from '../../tools/operations/batch.js';
|
|
|
7
7
|
import { parseMarkdown, generateBlockUid, parseMarkdownHeadingLevel } from '../../markdown-utils.js';
|
|
8
8
|
import { printDebug, exitWithError } from '../utils/output.js';
|
|
9
9
|
import { resolveGraph } from '../utils/graph.js';
|
|
10
|
+
import { readStdin } from '../utils/input.js';
|
|
10
11
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
11
12
|
import { q, createPage as roamCreatePage } from '@roam-research/roam-api-sdk';
|
|
12
13
|
/**
|
|
@@ -26,16 +27,6 @@ function flattenNodes(nodes, baseLevel = 1) {
|
|
|
26
27
|
}
|
|
27
28
|
return result;
|
|
28
29
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Read all input from stdin
|
|
31
|
-
*/
|
|
32
|
-
async function readStdin() {
|
|
33
|
-
const chunks = [];
|
|
34
|
-
for await (const chunk of process.stdin) {
|
|
35
|
-
chunks.push(chunk);
|
|
36
|
-
}
|
|
37
|
-
return Buffer.concat(chunks).toString('utf-8');
|
|
38
|
-
}
|
|
39
30
|
/**
|
|
40
31
|
* Check if a string looks like a Roam block UID (9 alphanumeric chars with _ or -)
|
|
41
32
|
*/
|
|
@@ -169,6 +160,11 @@ Examples:
|
|
|
169
160
|
roam save notes.md --title "My Notes" --update # Smart update (preserves UIDs)
|
|
170
161
|
cat data.json | roam save --json # Pipe JSON blocks
|
|
171
162
|
|
|
163
|
+
# Stdin operations
|
|
164
|
+
echo "Task from CLI" | roam save --todo # Pipe to TODO
|
|
165
|
+
cat note.md | roam save --title "From Pipe" # Pipe file content to new page
|
|
166
|
+
echo "Quick capture" | roam save -p "Inbox" # Pipe to specific page
|
|
167
|
+
|
|
172
168
|
# Combine options
|
|
173
169
|
roam save -p "Work" --parent "## Today" "Done with task" -c "wins"
|
|
174
170
|
|
|
@@ -219,7 +215,7 @@ JSON format (--json):
|
|
|
219
215
|
let content;
|
|
220
216
|
let isFile = false;
|
|
221
217
|
let sourceFilename;
|
|
222
|
-
if (input) {
|
|
218
|
+
if (input && input !== '-') {
|
|
223
219
|
// Check if input is a file path that exists
|
|
224
220
|
if (existsSync(input)) {
|
|
225
221
|
isFile = true;
|
|
@@ -237,8 +233,8 @@ JSON format (--json):
|
|
|
237
233
|
}
|
|
238
234
|
}
|
|
239
235
|
else {
|
|
240
|
-
// Read from stdin
|
|
241
|
-
if (process.stdin.isTTY) {
|
|
236
|
+
// Read from stdin (or if input is explicit '-')
|
|
237
|
+
if (process.stdin.isTTY && input !== '-') {
|
|
242
238
|
exitWithError('No input. Use: roam save "text", roam save <file>, or pipe content');
|
|
243
239
|
}
|
|
244
240
|
content = await readStdin();
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { SearchOperations } from '../../tools/operations/search/index.js';
|
|
3
3
|
import { formatSearchResults, printDebug, exitWithError } from '../utils/output.js';
|
|
4
4
|
import { resolveGraph } from '../utils/graph.js';
|
|
5
|
+
import { readStdin } from '../utils/input.js';
|
|
5
6
|
/**
|
|
6
7
|
* Normalize a tag by stripping #, [[, ]] wrappers
|
|
7
8
|
*/
|
|
@@ -20,7 +21,7 @@ function contentHasTag(content, tag) {
|
|
|
20
21
|
export function createSearchCommand() {
|
|
21
22
|
return new Command('search')
|
|
22
23
|
.description('Search blocks by text, tags, Datalog queries, or within specific pages')
|
|
23
|
-
.argument('[terms...]', 'Search terms (multiple terms use AND logic)')
|
|
24
|
+
.argument('[terms...]', 'Search terms (multiple terms use AND logic). Reads from stdin if omitted.')
|
|
24
25
|
.option('--tag <tag>', 'Filter by tag (repeatable, comma-separated). Default: AND logic', (val, prev) => {
|
|
25
26
|
// Support both comma-separated and multiple flags
|
|
26
27
|
const tags = val.split(',').map(t => t.trim()).filter(Boolean);
|
|
@@ -46,38 +47,17 @@ Examples:
|
|
|
46
47
|
# Text search
|
|
47
48
|
roam search "meeting notes" # Find blocks containing text
|
|
48
49
|
roam search api integration # Multiple terms (AND logic)
|
|
49
|
-
|
|
50
|
+
|
|
51
|
+
# Stdin search
|
|
52
|
+
echo "urgent project" | roam search # Pipe terms
|
|
53
|
+
roam get today | roam search TODO # Search within output
|
|
50
54
|
|
|
51
55
|
# Tag search
|
|
52
56
|
roam search --tag TODO # All blocks with #TODO
|
|
53
57
|
roam search --tag "[[Project Alpha]]" # Blocks with page reference
|
|
54
|
-
roam search --tag work --page "January 3rd, 2026" # Tag on specific page
|
|
55
|
-
|
|
56
|
-
# Multiple tags
|
|
57
|
-
roam search --tag TODO --tag urgent # Blocks with BOTH tags (AND)
|
|
58
|
-
roam search --tag "TODO,urgent,blocked" # Comma-separated (AND)
|
|
59
|
-
roam search --tag TODO --tag urgent --any # Blocks with ANY tag (OR)
|
|
60
|
-
|
|
61
|
-
# Exclude tags
|
|
62
|
-
roam search --tag TODO --negtag done # TODOs excluding #done
|
|
63
|
-
roam search --tag TODO --negtag "someday,maybe" # Exclude multiple
|
|
64
|
-
|
|
65
|
-
# Combined filters
|
|
66
|
-
roam search urgent --tag TODO # Text + tag filter
|
|
67
|
-
roam search "review" --page "Work" # Search within page
|
|
68
|
-
|
|
69
|
-
# Output options
|
|
70
|
-
roam search "design" -n 50 # Limit to 50 results
|
|
71
|
-
roam search "api" --json # JSON output
|
|
72
58
|
|
|
73
59
|
# Datalog queries (advanced)
|
|
74
|
-
roam search -q '[:find ?
|
|
75
|
-
roam search -q '[:find ?s :in $ ?term :where [?b :block/string ?s] [(clojure.string/includes? ?s ?term)]]' --inputs '["TODO"]'
|
|
76
|
-
roam search -q '[:find ?uid ?s :where [?b :block/uid ?uid] [?b :block/string ?s]]' --regex "meeting" --regex-flags "i"
|
|
77
|
-
|
|
78
|
-
Datalog tips:
|
|
79
|
-
Common attributes: :node/title, :block/string, :block/uid, :block/page, :block/children
|
|
80
|
-
Predicates: clojure.string/includes?, clojure.string/starts-with?, <, >, =
|
|
60
|
+
roam search -q '[:find ?uid ?s :where [?b :block/uid ?uid] [?b :block/string ?s]]' --regex "meeting"
|
|
81
61
|
`)
|
|
82
62
|
.action(async (terms, options) => {
|
|
83
63
|
try {
|
|
@@ -87,14 +67,21 @@ Datalog tips:
|
|
|
87
67
|
json: options.json,
|
|
88
68
|
debug: options.debug
|
|
89
69
|
};
|
|
70
|
+
let searchTerms = terms;
|
|
71
|
+
// If no terms provided as args, try stdin
|
|
72
|
+
if (searchTerms.length === 0 && !process.stdin.isTTY && !options.query && (options.tag?.length === 0)) {
|
|
73
|
+
const input = await readStdin();
|
|
74
|
+
if (input) {
|
|
75
|
+
searchTerms = input.trim().split(/\s+/);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
90
78
|
if (options.debug) {
|
|
91
|
-
printDebug('Search terms',
|
|
79
|
+
printDebug('Search terms', searchTerms);
|
|
92
80
|
printDebug('Graph', options.graph || 'default');
|
|
93
81
|
printDebug('Options', options);
|
|
94
82
|
}
|
|
95
83
|
const searchOps = new SearchOperations(graph);
|
|
96
84
|
// Datalog query mode (bypasses other search options)
|
|
97
|
-
// See for query construction - Roam_Research_Datalog_Cheatsheet.md
|
|
98
85
|
if (options.query) {
|
|
99
86
|
// Parse inputs if provided
|
|
100
87
|
let inputs;
|
|
@@ -109,11 +96,6 @@ Datalog tips:
|
|
|
109
96
|
exitWithError('Invalid JSON in --inputs');
|
|
110
97
|
}
|
|
111
98
|
}
|
|
112
|
-
if (options.debug) {
|
|
113
|
-
printDebug('Datalog query', options.query);
|
|
114
|
-
printDebug('Inputs', inputs || 'none');
|
|
115
|
-
printDebug('Regex filter', options.regex || 'none');
|
|
116
|
-
}
|
|
117
99
|
const result = await searchOps.executeDatomicQuery({
|
|
118
100
|
query: options.query,
|
|
119
101
|
inputs,
|
|
@@ -126,7 +108,6 @@ Datalog tips:
|
|
|
126
108
|
// Apply limit and format output
|
|
127
109
|
const limitedMatches = result.matches.slice(0, limit);
|
|
128
110
|
if (options.json) {
|
|
129
|
-
// For JSON output, parse the content back to objects
|
|
130
111
|
const parsed = limitedMatches.map(m => {
|
|
131
112
|
try {
|
|
132
113
|
return JSON.parse(m.content);
|
|
@@ -138,7 +119,6 @@ Datalog tips:
|
|
|
138
119
|
console.log(JSON.stringify(parsed, null, 2));
|
|
139
120
|
}
|
|
140
121
|
else {
|
|
141
|
-
// For text output, show raw results
|
|
142
122
|
if (limitedMatches.length === 0) {
|
|
143
123
|
console.log('No results found.');
|
|
144
124
|
}
|
|
@@ -153,34 +133,22 @@ Datalog tips:
|
|
|
153
133
|
}
|
|
154
134
|
// Determine search type based on options
|
|
155
135
|
const tags = options.tag || [];
|
|
156
|
-
if (tags.length > 0 &&
|
|
136
|
+
if (tags.length > 0 && searchTerms.length === 0) {
|
|
157
137
|
// Tag-only search
|
|
158
138
|
const normalizedTags = tags.map(normalizeTag);
|
|
159
139
|
const useOrLogic = options.any || false;
|
|
160
|
-
if (options.debug) {
|
|
161
|
-
printDebug('Tag search', {
|
|
162
|
-
tags: normalizedTags,
|
|
163
|
-
logic: useOrLogic ? 'OR' : 'AND',
|
|
164
|
-
page: options.page
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
// Search for first tag, then filter by additional tags
|
|
168
140
|
const result = await searchOps.searchForTag(normalizedTags[0], options.page);
|
|
169
141
|
let matches = result.matches;
|
|
170
|
-
// Apply multi-tag filter if more than one tag
|
|
171
142
|
if (normalizedTags.length > 1) {
|
|
172
143
|
matches = matches.filter(m => {
|
|
173
144
|
if (useOrLogic) {
|
|
174
|
-
// OR: has at least one tag
|
|
175
145
|
return normalizedTags.some(tag => contentHasTag(m.content, tag));
|
|
176
146
|
}
|
|
177
147
|
else {
|
|
178
|
-
// AND: has all tags
|
|
179
148
|
return normalizedTags.every(tag => contentHasTag(m.content, tag));
|
|
180
149
|
}
|
|
181
150
|
});
|
|
182
151
|
}
|
|
183
|
-
// Apply negtag filter (exclude blocks with any of these tags)
|
|
184
152
|
const negTags = options.negtag || [];
|
|
185
153
|
if (negTags.length > 0) {
|
|
186
154
|
const normalizedNegTags = negTags.map(normalizeTag);
|
|
@@ -189,24 +157,18 @@ Datalog tips:
|
|
|
189
157
|
const limitedMatches = matches.slice(0, limit);
|
|
190
158
|
console.log(formatSearchResults(limitedMatches, outputOptions));
|
|
191
159
|
}
|
|
192
|
-
else if (
|
|
193
|
-
// Text search
|
|
194
|
-
const searchText =
|
|
195
|
-
if (options.debug) {
|
|
196
|
-
printDebug('Text search', { text: searchText, page: options.page, tag: options.tag });
|
|
197
|
-
}
|
|
160
|
+
else if (searchTerms.length > 0) {
|
|
161
|
+
// Text search
|
|
162
|
+
const searchText = searchTerms.join(' ');
|
|
198
163
|
const result = await searchOps.searchByText({
|
|
199
164
|
text: searchText,
|
|
200
165
|
page_title_uid: options.page
|
|
201
166
|
});
|
|
202
|
-
// Apply client-side filters
|
|
203
167
|
let matches = result.matches;
|
|
204
|
-
// Case-insensitive filter if requested
|
|
205
168
|
if (options.caseInsensitive) {
|
|
206
169
|
const lowerSearchText = searchText.toLowerCase();
|
|
207
170
|
matches = matches.filter(m => m.content.toLowerCase().includes(lowerSearchText));
|
|
208
171
|
}
|
|
209
|
-
// Tag filter if provided
|
|
210
172
|
if (tags.length > 0) {
|
|
211
173
|
const normalizedTags = tags.map(normalizeTag);
|
|
212
174
|
const useOrLogic = options.any || false;
|
|
@@ -219,13 +181,11 @@ Datalog tips:
|
|
|
219
181
|
}
|
|
220
182
|
});
|
|
221
183
|
}
|
|
222
|
-
// Negtag filter (exclude blocks with any of these tags)
|
|
223
184
|
const negTags = options.negtag || [];
|
|
224
185
|
if (negTags.length > 0) {
|
|
225
186
|
const normalizedNegTags = negTags.map(normalizeTag);
|
|
226
187
|
matches = matches.filter(m => !normalizedNegTags.some(tag => contentHasTag(m.content, tag)));
|
|
227
188
|
}
|
|
228
|
-
// Apply limit
|
|
229
189
|
console.log(formatSearchResults(matches.slice(0, limit), outputOptions));
|
|
230
190
|
}
|
|
231
191
|
else {
|