Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/__tests__/commands/monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
69 changes: 64 additions & 5 deletions src/commands/monitor.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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[];
Expand All @@ -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<string, unknown> = {};
Expand All @@ -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<string, unknown> = {
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<string, unknown> = {
name: opts.name,
Expand Down Expand Up @@ -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
Expand All @@ -245,6 +270,30 @@ export function createMonitorCommand(): Command {
parseCommaList
)
.option('--crawl-url <url>', 'Root URL for a crawl target')
.option(
'--queries <list>',
'Comma-separated search queries for a search target (requires --goal)',
parseCommaList
)
.option(
'--search-window <window>',
'Search recency window: 5m, 15m, 1h, 6h, 24h, 7d (default: 24h)'
)
.option(
'--max-results <n>',
'Max search results per query, 1-50 (default: 10)',
parseInt
)
.option(
'--include-domains <list>',
'Comma-separated domains to restrict search results to',
parseCommaList
)
.option(
'--exclude-domains <list>',
'Comma-separated domains to exclude from search results',
parseCommaList
)
.option('--webhook-url <url>', 'Webhook destination')
.option(
'--webhook-events <list>',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading