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.
@@ -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 (today/yesterday/tomorrow)')
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
- // For page/block fetching, target is required
100
- if (!target) {
101
- exitWithError('Target is required. Use: roam get <page-title> or roam get --todo');
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
- // Check if target is a block UID
109
- const uidMatch = resolvedTarget.match(BLOCK_UID_PATTERN);
110
- if (uidMatch) {
111
- // Fetch block by UID
112
- const blockUid = uidMatch[1];
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
- // Resolve block references if requested
122
- if (refsDepth > 0) {
123
- block = await resolveBlockRefs(graph, block, refsDepth);
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
- else {
128
- // Fetch page by title
129
- if (options.debug) {
130
- printDebug('Fetching page', { title: resolvedTarget, depth });
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
- const pageOps = new PageOperations(graph);
133
- const result = await pageOps.fetchPageByTitle(resolvedTarget, 'raw');
134
- // Parse the raw result
135
- let blocks;
136
- if (typeof result === 'string') {
137
- try {
138
- blocks = JSON.parse(result);
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
- catch {
141
- // Result is already formatted as string (e.g., "Page Title (no content found)")
142
- console.log(result);
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
- blocks = result;
148
- }
149
- // Resolve block references if requested
150
- if (refsDepth > 0) {
151
- blocks = await resolveBlocksRefs(graph, blocks, refsDepth);
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
- console.log(formatPageOutput(resolvedTarget, blocks, outputOptions));
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('<identifier>', 'Page title, #tag, [[Page]], or ((block-uid))')
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
- const { block_uid, title } = parseIdentifier(identifier);
101
- if (options.debug) {
102
- printDebug('Identifier', identifier);
103
- printDebug('Graph', options.graph || 'default');
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
- // Apply limit
113
- const limitedMatches = result.matches.slice(0, limit);
114
- // Format output
115
- if (options.json) {
116
- const jsonOutput = limitedMatches.map(m => ({
117
- uid: m.block_uid,
118
- content: m.content,
119
- page: m.page_title
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
- else if (options.raw) {
124
- console.log(formatRaw(limitedMatches));
113
+ if (identifiers.length === 0) {
114
+ exitWithError('No identifiers provided');
125
115
  }
126
- else {
127
- console.log(formatGrouped(limitedMatches));
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
- roam search "bug fix" -i # Case-insensitive search
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 ?title :where [?e :node/title ?title]]'
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', 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 && terms.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 (terms.length > 0) {
193
- // Text search (with optional tag filter)
194
- const searchText = terms.join(' ');
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 {