diff --git a/src/__tests__/commands/monitor.test.ts b/src/__tests__/commands/monitor.test.ts index 643b476ed..a88264edb 100644 --- a/src/__tests__/commands/monitor.test.ts +++ b/src/__tests__/commands/monitor.test.ts @@ -28,6 +28,49 @@ describe('monitor command helpers', () => { }); }); + it('builds a search target from queries and search options', () => { + expect( + buildCreateBody({ + name: 'LLM releases', + goal: 'Notify me about major new LLM model releases', + scheduleText: 'every 2 hours', + timezone: 'UTC', + queries: ['new LLM release', 'frontier model launch'], + searchWindow: '24h', + maxResults: 10, + includeDomains: ['openai.com'], + excludeDomains: ['reddit.com'], + }) + ).toEqual({ + name: 'LLM releases', + goal: 'Notify me about major new LLM model releases', + schedule: { + text: 'every 2 hours', + timezone: 'UTC', + }, + targets: [ + { + type: 'search', + queries: ['new LLM release', 'frontier model launch'], + searchWindow: '24h', + maxResults: 10, + includeDomains: ['openai.com'], + excludeDomains: ['reddit.com'], + }, + ], + }); + }); + + it('requires a goal for search monitors', () => { + expect(() => + buildCreateBody({ + name: 'No goal', + scheduleText: 'hourly', + queries: ['something'], + }) + ).toThrow(/goal is required for search monitors/); + }); + it('supports the simple page plus goal path', () => { expect( buildCreateBody({ diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 13ac0d2d7..b589609fa 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -1,8 +1,8 @@ /** * `firecrawl monitor` — manage Firecrawl monitors. * - * Monitors run recurring scrapes/crawls and diff each result against the last - * retained snapshot. See features/monitoring in the docs. + * Monitors run recurring scrapes/crawls/searches and diff each result against + * the last retained snapshot. See features/monitoring in the docs. * * firecrawl@4.22.2 exposes monitor methods (createMonitor, * listMonitors, getMonitor, updateMonitor, deleteMonitor, runMonitor, @@ -141,6 +141,11 @@ export function buildCreateBody(opts: { page?: string; urls?: string[]; crawlUrl?: string; + queries?: string[]; + searchWindow?: string; + maxResults?: number; + includeDomains?: string[]; + excludeDomains?: string[]; webhookUrl?: string; webhookEvents?: string[]; emailRecipients?: string[]; @@ -160,8 +165,14 @@ export function buildCreateBody(opts: { : undefined; const hasScrape = urls && urls.length > 0; const hasCrawl = !!opts.crawlUrl; - if (!hasScrape && !hasCrawl) { - throw new Error('Provide --scrape-urls or --crawl-url'); + const hasSearch = !!(opts.queries && opts.queries.length > 0); + if (!hasScrape && !hasCrawl && !hasSearch) { + throw new Error('Provide --scrape-urls, --crawl-url, or --queries'); + } + // The API requires a non-empty goal whenever a search target is present + // (it auto-enables the AI judge). Fail early with a clear message. + if (hasSearch && (!opts.goal || !opts.goal.trim())) { + throw new Error('--goal is required for search monitors (--queries)'); } const schedule: Record = {}; @@ -172,6 +183,20 @@ export function buildCreateBody(opts: { const targets: unknown[] = []; if (hasScrape) targets.push({ type: 'scrape', urls }); if (hasCrawl) targets.push({ type: 'crawl', url: opts.crawlUrl }); + if (hasSearch) { + const searchTarget: Record = { + type: 'search', + queries: opts.queries, + }; + if (opts.searchWindow) searchTarget.searchWindow = opts.searchWindow; + if (opts.maxResults !== undefined) + searchTarget.maxResults = opts.maxResults; + if (opts.includeDomains && opts.includeDomains.length > 0) + searchTarget.includeDomains = opts.includeDomains; + if (opts.excludeDomains && opts.excludeDomains.length > 0) + searchTarget.excludeDomains = opts.excludeDomains; + targets.push(searchTarget); + } const body: Record = { name: opts.name, @@ -219,7 +244,7 @@ function commonOptions(cmd: Command): Command { */ export function createMonitorCommand(): Command { const monitor = new Command('monitor').description( - 'Schedule recurring scrapes/crawls and track content changes' + 'Schedule recurring scrapes/crawls/searches and track content changes' ); // create @@ -245,6 +270,30 @@ export function createMonitorCommand(): Command { parseCommaList ) .option('--crawl-url ', 'Root URL for a crawl target') + .option( + '--queries ', + 'Comma-separated search queries for a search target (requires --goal)', + parseCommaList + ) + .option( + '--search-window ', + 'Search recency window: 5m, 15m, 1h, 6h, 24h, 7d (default: 24h)' + ) + .option( + '--max-results ', + 'Max search results per query, 1-50 (default: 10)', + parseInt + ) + .option( + '--include-domains ', + 'Comma-separated domains to restrict search results to', + parseCommaList + ) + .option( + '--exclude-domains ', + 'Comma-separated domains to exclude from search results', + parseCommaList + ) .option('--webhook-url ', 'Webhook destination') .option( '--webhook-events ', @@ -275,6 +324,11 @@ export function createMonitorCommand(): Command { page: options.page, urls: options.scrapeUrls, crawlUrl: options.crawlUrl, + queries: options.queries, + searchWindow: options.searchWindow, + maxResults: options.maxResults, + includeDomains: options.includeDomains, + excludeDomains: options.excludeDomains, webhookUrl: options.webhookUrl, webhookEvents: options.webhookEvents, emailRecipients: options.email, @@ -480,6 +534,11 @@ Examples: --schedule "every 30 minutes" \\ --page https://example.com/blog \\ --email alerts@example.com + $ firecrawl monitor create --name "LLM releases" \\ + --goal "Notify me about major new LLM model releases" \\ + --schedule "every 2 hours" \\ + --queries "new LLM release,frontier model launch" \\ + --search-window 24h --max-results 10 $ firecrawl monitor create monitor.json $ cat monitor.json | firecrawl monitor create $ firecrawl monitor list --limit 20