From e576ca35bce407449c9df890aa239485028d7818 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 12 May 2026 10:57:48 -0400 Subject: [PATCH 1/6] add monitor command (create/list/get/update/delete/run/checks) --- src/commands/monitor.ts | 459 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 + 2 files changed, 462 insertions(+) create mode 100644 src/commands/monitor.ts diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts new file mode 100644 index 000000000..ed62944f6 --- /dev/null +++ b/src/commands/monitor.ts @@ -0,0 +1,459 @@ +/** + * `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. + * + * The SDK (@mendable/firecrawl-js@4.17.0) does not yet expose the monitor + * endpoints, so this command hits /v2/monitor directly via fetch — same + * pattern parse.ts uses. + * + * Subcommands: + * create | list | get | update | delete | run | checks | check + */ + +import * as fs from 'fs'; +import { Command } from 'commander'; +import { getConfig, validateConfig } from '../utils/config'; +import { writeOutput } from '../utils/output'; + +const DEFAULT_API_URL = 'https://api.firecrawl.dev'; + +interface CommonOptions { + apiKey?: string; + apiUrl?: string; + output?: string; + pretty?: boolean; +} + +interface MonitorRequestInit { + method?: string; + body?: unknown; + query?: Record; +} + +async function monitorRequest( + path: string, + options: CommonOptions, + init: MonitorRequestInit = {} +): Promise { + const config = getConfig(); + const apiKey = options.apiKey || config.apiKey; + validateConfig(apiKey); + + const baseUrl = (options.apiUrl || config.apiUrl || DEFAULT_API_URL).replace( + /\/$/, + '' + ); + + let url = `${baseUrl}/v2${path}`; + if (init.query) { + const qs = new URLSearchParams(); + for (const [k, v] of Object.entries(init.query)) { + if (v !== undefined && v !== null && v !== '') qs.set(k, String(v)); + } + const s = qs.toString(); + if (s) url += `?${s}`; + } + + const headers: Record = { + Authorization: `Bearer ${apiKey}`, + 'X-Origin': 'cli', + }; + if (init.body !== undefined) headers['Content-Type'] = 'application/json'; + + const response = await fetch(url, { + method: init.method ?? 'GET', + headers, + body: init.body !== undefined ? JSON.stringify(init.body) : undefined, + }); + + const payload = (await response.json().catch(() => ({}))) as any; + + if (!response.ok || payload?.success === false) { + const message = + payload?.error || + `HTTP ${response.status}: ${response.statusText || 'Request failed'}`; + throw new Error(message); + } + + return payload; +} + +function emit( + payload: unknown, + options: CommonOptions & { json?: boolean } +): void { + const text = JSON.stringify(payload, null, options.pretty ? 2 : 0); + writeOutput(text, options.output, !!options.output); +} + +function readJsonInput(input: string): unknown { + // Accept either a JSON literal or @path/to/file.json + if (input.startsWith('@')) { + const filePath = input.slice(1); + const raw = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(raw); + } + return JSON.parse(input); +} + +function parseCommaList(value: string): string[] { + return value + .split(',') + .map((s) => s.trim()) + .filter(Boolean); +} + +function fail(error: unknown): never { + console.error('Error:', error instanceof Error ? error.message : error); + process.exit(1); +} + +/** + * Build the request body for `monitor create` from CLI flags. + * + * Power users can pass `--body @payload.json` for full control. The flags + * cover the common scrape-target shape. + */ +function buildCreateBody(opts: { + body?: string; + name?: string; + cron?: string; + scheduleText?: string; + timezone?: string; + urls?: string[]; + crawlUrl?: string; + webhookUrl?: string; + webhookEvents?: string[]; + emailRecipients?: string[]; + retentionDays?: number; +}): unknown { + if (opts.body) return readJsonInput(opts.body); + + if (!opts.name) { + throw new Error('--name is required (or pass --body @file.json)'); + } + if (!opts.cron && !opts.scheduleText) { + throw new Error('--cron or --schedule is required'); + } + const hasScrape = opts.urls && opts.urls.length > 0; + const hasCrawl = !!opts.crawlUrl; + if (!hasScrape && !hasCrawl) { + throw new Error('Provide --urls (scrape) or --crawl-url (crawl)'); + } + + const schedule: Record = {}; + if (opts.cron) schedule.cron = opts.cron; + if (opts.scheduleText) schedule.text = opts.scheduleText; + if (opts.timezone) schedule.timezone = opts.timezone; + + const targets: unknown[] = []; + if (hasScrape) targets.push({ type: 'scrape', urls: opts.urls }); + if (hasCrawl) targets.push({ type: 'crawl', url: opts.crawlUrl }); + + const body: Record = { + name: opts.name, + schedule, + targets, + }; + + if (opts.webhookUrl) { + body.webhook = { + url: opts.webhookUrl, + ...(opts.webhookEvents && opts.webhookEvents.length > 0 + ? { events: opts.webhookEvents } + : {}), + }; + } + + if (opts.emailRecipients && opts.emailRecipients.length > 0) { + body.notification = { + email: { + enabled: true, + recipients: opts.emailRecipients, + }, + }; + } + + if (opts.retentionDays !== undefined) body.retentionDays = opts.retentionDays; + + return body; +} + +function commonOptions(cmd: Command): Command { + return cmd + .option( + '-k, --api-key ', + 'Firecrawl API key (overrides global --api-key)' + ) + .option('--api-url ', 'API URL (overrides global --api-url)') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--pretty', 'Pretty print JSON output', false); +} + +/** + * Build the `firecrawl monitor` command tree. + */ +export function createMonitorCommand(): Command { + const monitor = new Command('monitor').description( + 'Schedule recurring scrapes/crawls and track content changes' + ); + + // create + commonOptions( + monitor + .command('create') + .description('Create a monitor') + .option('--name ', 'Monitor name') + .option('--cron ', 'Cron schedule (e.g. "*/30 * * * *")') + .option( + '--schedule ', + 'Natural-language schedule (e.g. "every 30 minutes")' + ) + .option('--timezone ', 'Schedule timezone', 'UTC') + .option( + '--urls ', + 'Comma-separated URLs to scrape on each check', + parseCommaList + ) + .option('--crawl-url ', 'Root URL for a crawl target') + .option('--webhook-url ', 'Webhook destination') + .option( + '--webhook-events ', + 'Comma-separated events (monitor.page, monitor.check.completed)', + parseCommaList + ) + .option( + '--email ', + 'Comma-separated email recipients for change notifications', + parseCommaList + ) + .option('--retention-days ', 'Snapshot retention window', parseInt) + .option( + '--body ', + 'Raw JSON body (or @path/to/file.json) — overrides flag-built payload' + ) + ).action(async (options) => { + try { + const body = buildCreateBody({ + body: options.body, + name: options.name, + cron: options.cron, + scheduleText: options.schedule, + timezone: options.timezone, + urls: options.urls, + crawlUrl: options.crawlUrl, + webhookUrl: options.webhookUrl, + webhookEvents: options.webhookEvents, + emailRecipients: options.email, + retentionDays: options.retentionDays, + }); + const payload = await monitorRequest('/monitor', options, { + method: 'POST', + body, + }); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // list + commonOptions( + monitor + .command('list') + .description('List monitors') + .option('--limit ', 'Maximum results', parseInt) + .option('--offset ', 'Result offset', parseInt) + ).action(async (options) => { + try { + const payload = await monitorRequest('/monitor', options, { + query: { limit: options.limit, offset: options.offset }, + }); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // get + commonOptions( + monitor + .command('get') + .description('Get a monitor by ID') + .argument('', 'Monitor ID') + ).action(async (monitorId, options) => { + try { + const payload = await monitorRequest( + `/monitor/${encodeURIComponent(monitorId)}`, + options + ); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // update + commonOptions( + monitor + .command('update') + .description('Update a monitor (partial)') + .argument('', 'Monitor ID') + .option('--name ', 'New name') + .option('--cron ', 'New cron schedule') + .option('--schedule ', 'New natural-language schedule') + .option('--timezone ', 'Schedule timezone') + .option('--status ', 'active | paused') + .option('--retention-days ', 'Snapshot retention window', parseInt) + .option( + '--body ', + 'Raw JSON body (or @path/to/file.json) — overrides flag-built payload' + ) + ).action(async (monitorId, options) => { + try { + let body: Record; + if (options.body) { + body = readJsonInput(options.body) as Record; + } else { + body = {}; + if (options.name) body.name = options.name; + if (options.status) body.status = options.status; + if (options.retentionDays !== undefined) + body.retentionDays = options.retentionDays; + if (options.cron || options.schedule || options.timezone) { + const schedule: Record = {}; + if (options.cron) schedule.cron = options.cron; + if (options.schedule) schedule.text = options.schedule; + if (options.timezone) schedule.timezone = options.timezone; + body.schedule = schedule; + } + if (Object.keys(body).length === 0) { + throw new Error( + 'Provide at least one field to update (or --body @file.json)' + ); + } + } + const payload = await monitorRequest( + `/monitor/${encodeURIComponent(monitorId)}`, + options, + { method: 'PATCH', body } + ); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // delete + commonOptions( + monitor + .command('delete') + .description('Delete a monitor') + .argument('', 'Monitor ID') + ).action(async (monitorId, options) => { + try { + const payload = await monitorRequest( + `/monitor/${encodeURIComponent(monitorId)}`, + options, + { method: 'DELETE' } + ); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // run + commonOptions( + monitor + .command('run') + .description('Trigger a check immediately') + .argument('', 'Monitor ID') + ).action(async (monitorId, options) => { + try { + const payload = await monitorRequest( + `/monitor/${encodeURIComponent(monitorId)}/run`, + options, + { method: 'POST' } + ); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // checks (list) + commonOptions( + monitor + .command('checks') + .description('List checks for a monitor') + .argument('', 'Monitor ID') + .option('--limit ', 'Maximum results', parseInt) + .option('--offset ', 'Result offset', parseInt) + ).action(async (monitorId, options) => { + try { + const payload = await monitorRequest( + `/monitor/${encodeURIComponent(monitorId)}/checks`, + options, + { query: { limit: options.limit, offset: options.offset } } + ); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + // check (get one) + commonOptions( + monitor + .command('check') + .description('Get a specific check, with page-level results') + .argument('', 'Monitor ID') + .argument('', 'Check ID') + .option('--limit ', 'Max page results', parseInt) + .option('--skip ', 'Skip page results', parseInt) + .option( + '--status ', + 'Filter page results: same, new, changed, removed, error' + ) + ).action(async (monitorId, checkId, options) => { + try { + const payload = await monitorRequest( + `/monitor/${encodeURIComponent(monitorId)}/checks/${encodeURIComponent(checkId)}`, + options, + { + query: { + limit: options.limit, + skip: options.skip, + status: options.status, + }, + } + ); + emit(payload, options); + } catch (err) { + fail(err); + } + }); + + monitor.addHelpText( + 'after', + ` +Examples: + $ firecrawl monitor create --name "Blog" \\ + --schedule "every 30 minutes" \\ + --urls https://example.com/blog \\ + --email alerts@example.com + $ firecrawl monitor create --body @monitor.json + $ firecrawl monitor list --limit 20 + $ firecrawl monitor get mon_abc123 + $ firecrawl monitor update mon_abc123 --status paused + $ firecrawl monitor run mon_abc123 + $ firecrawl monitor checks mon_abc123 --limit 10 + $ firecrawl monitor check mon_abc123 chk_xyz --status changed +` + ); + + return monitor; +} diff --git a/src/index.ts b/src/index.ts index 4ea6442bf..4a1a45979 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { handleCreditUsageCommand } from './commands/credit-usage'; import { handleCrawlCommand } from './commands/crawl'; import { handleMapCommand } from './commands/map'; import { handleParseCommand } from './commands/parse'; +import { createMonitorCommand } from './commands/monitor'; import { handleSearchCommand } from './commands/search'; import { handleAgentCommand } from './commands/agent'; import { @@ -70,6 +71,7 @@ const AUTH_REQUIRED_COMMANDS = [ 'browser', 'interact', 'credit-usage', + 'monitor', ]; const commandSet = new Set([]); @@ -1574,6 +1576,7 @@ Examples: program.addCommand(createCrawlCommand()); program.addCommand(createMapCommand()); program.addCommand(createParseCommand()); +program.addCommand(createMonitorCommand()); program.addCommand(createSearchCommand()); program.addCommand(createAgentCommand()); program.addCommand(createInteractCommand()); From 68901703280c99660d24f2678369a15f2bf34b4c Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 12 May 2026 10:59:59 -0400 Subject: [PATCH 2/6] rename monitor --status to --state to avoid global flag collision --- src/commands/monitor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index ed62944f6..67e0b44a2 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -305,7 +305,7 @@ export function createMonitorCommand(): Command { .option('--cron ', 'New cron schedule') .option('--schedule ', 'New natural-language schedule') .option('--timezone ', 'Schedule timezone') - .option('--status ', 'active | paused') + .option('--state ', 'active | paused') .option('--retention-days ', 'Snapshot retention window', parseInt) .option( '--body ', @@ -319,7 +319,7 @@ export function createMonitorCommand(): Command { } else { body = {}; if (options.name) body.name = options.name; - if (options.status) body.status = options.status; + if (options.state) body.status = options.state; if (options.retentionDays !== undefined) body.retentionDays = options.retentionDays; if (options.cron || options.schedule || options.timezone) { @@ -415,7 +415,7 @@ export function createMonitorCommand(): Command { .option('--limit ', 'Max page results', parseInt) .option('--skip ', 'Skip page results', parseInt) .option( - '--status ', + '--page-status ', 'Filter page results: same, new, changed, removed, error' ) ).action(async (monitorId, checkId, options) => { @@ -427,7 +427,7 @@ export function createMonitorCommand(): Command { query: { limit: options.limit, skip: options.skip, - status: options.status, + status: options.pageStatus, }, } ); @@ -448,10 +448,10 @@ Examples: $ firecrawl monitor create --body @monitor.json $ firecrawl monitor list --limit 20 $ firecrawl monitor get mon_abc123 - $ firecrawl monitor update mon_abc123 --status paused + $ firecrawl monitor update mon_abc123 --state paused $ firecrawl monitor run mon_abc123 $ firecrawl monitor checks mon_abc123 --limit 10 - $ firecrawl monitor check mon_abc123 chk_xyz --status changed + $ firecrawl monitor check mon_abc123 chk_xyz --page-status changed ` ); From 933930b1fe29dc4cbd06be202f71cc0cabe31926 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 12 May 2026 11:05:40 -0400 Subject: [PATCH 3/6] monitor: accept JSON file/stdin and remove --body Allow monitor create/update commands to consume a JSON payload from a positional file argument or piped stdin (use '-' to force stdin). Introduce async readJsonPayload helper, refactor buildCreateBody to no longer take a raw body string, and try JSON input first before falling back to flag-built payloads. Update CLI signatures, help text, and examples accordingly, and add the monitor entry to the SKILL.md command list. --- skills/firecrawl-cli/SKILL.md | 24 ++++++++++ src/commands/monitor.ts | 89 ++++++++++++++++++++--------------- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index 8d7e6a7c7..0a9ecc835 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -63,6 +63,7 @@ Follow this escalation pattern: | Interact with a page | `scrape` + `interact` | Content requires clicks, form fills, pagination, or login | | Download a site to files | `download` | Save an entire site as local files | | Parse a local file | `parse` | File on disk (PDF, DOCX, XLSX, etc.) — not a URL | +| Watch pages for changes | `monitor` | Schedule recurring scrapes/crawls, diff against snapshots | For detailed command reference, run `firecrawl --help`. @@ -72,6 +73,29 @@ For detailed command reference, run `firecrawl --help`. - Use `scrape` + `interact` when you need to interact with a page, such as clicking buttons, filling out forms, navigating through a complex site, infinite scroll, or when scrape fails to grab all the content you need. - Never use interact for web searches - use `search` instead. +**Monitor:** Schedule recurring scrapes or crawls and diff each result against the last retained snapshot. Use for product pages, docs, blogs, changelogs, competitor sites — any page where changes matter. Each check labels pages as `same`, `new`, `changed`, `removed`, or `error`, with webhook and email notification options. + +Subcommands: `create | list | get | update | delete | run | checks | check`. + +```bash +# create from flags +firecrawl monitor create --name "Blog" --schedule "every 30 minutes" \ + --urls https://example.com/blog --email alerts@example.com + +# or from JSON (positional file, or piped stdin) +firecrawl monitor create monitor.json +cat monitor.json | firecrawl monitor create + +firecrawl monitor list --limit 20 +firecrawl monitor run # trigger a check now +firecrawl monitor checks # list checks +firecrawl monitor check --page-status changed +firecrawl monitor update --state paused +firecrawl monitor delete +``` + +Schedules accept cron (`--cron "*/30 * * * *"`) or natural language (`--schedule "every 30 minutes"`). Minimum interval is 15 minutes. Targets are either `--urls a,b,c` (scrape) or `--crawl-url ` (crawl whole site each check). Note: `--state` (not `--status`) sets active/paused; `--page-status` (not `--status`) filters page results on `check` — avoids collision with the global `--status` flag. Monitoring is not available for zero-data-retention teams. + **Avoid redundant fetches:** - `search --scrape` already fetches full page content. Don't re-scrape those URLs. diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 67e0b44a2..d89f62116 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -88,14 +88,26 @@ function emit( writeOutput(text, options.output, !!options.output); } -function readJsonInput(input: string): unknown { - // Accept either a JSON literal or @path/to/file.json - if (input.startsWith('@')) { - const filePath = input.slice(1); - const raw = fs.readFileSync(filePath, 'utf-8'); +/** + * Read a JSON payload from a positional arg or piped stdin. + * + * - `file` is a path to a .json file, or `-` to read stdin explicitly. + * - If `file` is omitted and stdin is a pipe, stdin is used. + * - Returns `undefined` when no source is provided — caller falls back to flags. + */ +async function readJsonPayload(file?: string): Promise { + if (file === '-' || (!file && !process.stdin.isTTY)) { + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) chunks.push(chunk as Buffer); + const raw = Buffer.concat(chunks).toString('utf-8').trim(); + if (!raw) return undefined; + return JSON.parse(raw); + } + if (file) { + const raw = fs.readFileSync(file, 'utf-8'); return JSON.parse(raw); } - return JSON.parse(input); + return undefined; } function parseCommaList(value: string): string[] { @@ -117,7 +129,6 @@ function fail(error: unknown): never { * cover the common scrape-target shape. */ function buildCreateBody(opts: { - body?: string; name?: string; cron?: string; scheduleText?: string; @@ -129,8 +140,6 @@ function buildCreateBody(opts: { emailRecipients?: string[]; retentionDays?: number; }): unknown { - if (opts.body) return readJsonInput(opts.body); - if (!opts.name) { throw new Error('--name is required (or pass --body @file.json)'); } @@ -204,7 +213,11 @@ export function createMonitorCommand(): Command { commonOptions( monitor .command('create') - .description('Create a monitor') + .description('Create a monitor (flags, or JSON from file/stdin)') + .argument( + '[file]', + 'Path to JSON payload (use "-" or pipe stdin to read from stdin)' + ) .option('--name ', 'Monitor name') .option('--cron ', 'Cron schedule (e.g. "*/30 * * * *")') .option( @@ -230,25 +243,23 @@ export function createMonitorCommand(): Command { parseCommaList ) .option('--retention-days ', 'Snapshot retention window', parseInt) - .option( - '--body ', - 'Raw JSON body (or @path/to/file.json) — overrides flag-built payload' - ) - ).action(async (options) => { + ).action(async (file: string | undefined, options) => { try { - const body = buildCreateBody({ - body: options.body, - name: options.name, - cron: options.cron, - scheduleText: options.schedule, - timezone: options.timezone, - urls: options.urls, - crawlUrl: options.crawlUrl, - webhookUrl: options.webhookUrl, - webhookEvents: options.webhookEvents, - emailRecipients: options.email, - retentionDays: options.retentionDays, - }); + const fromJson = await readJsonPayload(file); + const body = + fromJson ?? + buildCreateBody({ + name: options.name, + cron: options.cron, + scheduleText: options.schedule, + timezone: options.timezone, + urls: options.urls, + crawlUrl: options.crawlUrl, + webhookUrl: options.webhookUrl, + webhookEvents: options.webhookEvents, + emailRecipients: options.email, + retentionDays: options.retentionDays, + }); const payload = await monitorRequest('/monitor', options, { method: 'POST', body, @@ -299,23 +310,24 @@ export function createMonitorCommand(): Command { commonOptions( monitor .command('update') - .description('Update a monitor (partial)') + .description('Update a monitor (flags, or JSON from file/stdin)') .argument('', 'Monitor ID') + .argument( + '[file]', + 'Path to JSON payload (use "-" or pipe stdin to read from stdin)' + ) .option('--name ', 'New name') .option('--cron ', 'New cron schedule') .option('--schedule ', 'New natural-language schedule') .option('--timezone ', 'Schedule timezone') .option('--state ', 'active | paused') .option('--retention-days ', 'Snapshot retention window', parseInt) - .option( - '--body ', - 'Raw JSON body (or @path/to/file.json) — overrides flag-built payload' - ) - ).action(async (monitorId, options) => { + ).action(async (monitorId: string, file: string | undefined, options) => { try { + const fromJson = await readJsonPayload(file); let body: Record; - if (options.body) { - body = readJsonInput(options.body) as Record; + if (fromJson) { + body = fromJson as Record; } else { body = {}; if (options.name) body.name = options.name; @@ -331,7 +343,7 @@ export function createMonitorCommand(): Command { } if (Object.keys(body).length === 0) { throw new Error( - 'Provide at least one field to update (or --body @file.json)' + 'Provide at least one field to update (or a JSON file / stdin payload)' ); } } @@ -445,7 +457,8 @@ Examples: --schedule "every 30 minutes" \\ --urls https://example.com/blog \\ --email alerts@example.com - $ firecrawl monitor create --body @monitor.json + $ firecrawl monitor create monitor.json + $ cat monitor.json | firecrawl monitor create $ firecrawl monitor list --limit 20 $ firecrawl monitor get mon_abc123 $ firecrawl monitor update mon_abc123 --state paused From 19ae52cc737d78bae42faaf21877c81ea59365ee Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 12 May 2026 11:06:00 -0400 Subject: [PATCH 4/6] monitor: accept JSON via positional file or piped stdin; document in skill --- src/commands/monitor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index d89f62116..3130466b7 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -125,8 +125,8 @@ function fail(error: unknown): never { /** * Build the request body for `monitor create` from CLI flags. * - * Power users can pass `--body @payload.json` for full control. The flags - * cover the common scrape-target shape. + * For full control, callers can pass a JSON file path positionally or pipe + * JSON on stdin instead. The flags cover the common scrape-target shape. */ function buildCreateBody(opts: { name?: string; @@ -141,7 +141,7 @@ function buildCreateBody(opts: { retentionDays?: number; }): unknown { if (!opts.name) { - throw new Error('--name is required (or pass --body @file.json)'); + throw new Error('--name is required (or pass a JSON file / stdin payload)'); } if (!opts.cron && !opts.scheduleText) { throw new Error('--cron or --schedule is required'); From 84140d9af12fd4c75fdeeceb9035a877f002654d Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 12 May 2026 11:13:18 -0400 Subject: [PATCH 5/6] monitor: rename --urls to --scrape-urls to pair with --crawl-url --- skills/firecrawl-cli/SKILL.md | 4 ++-- src/commands/monitor.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index 0a9ecc835..efa8601c5 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -80,7 +80,7 @@ Subcommands: `create | list | get | update | delete | run | checks | check`. ```bash # create from flags firecrawl monitor create --name "Blog" --schedule "every 30 minutes" \ - --urls https://example.com/blog --email alerts@example.com + --scrape-urls https://example.com/blog --email alerts@example.com # or from JSON (positional file, or piped stdin) firecrawl monitor create monitor.json @@ -94,7 +94,7 @@ firecrawl monitor update --state paused firecrawl monitor delete ``` -Schedules accept cron (`--cron "*/30 * * * *"`) or natural language (`--schedule "every 30 minutes"`). Minimum interval is 15 minutes. Targets are either `--urls a,b,c` (scrape) or `--crawl-url ` (crawl whole site each check). Note: `--state` (not `--status`) sets active/paused; `--page-status` (not `--status`) filters page results on `check` — avoids collision with the global `--status` flag. Monitoring is not available for zero-data-retention teams. +Schedules accept cron (`--cron "*/30 * * * *"`) or natural language (`--schedule "every 30 minutes"`). Minimum interval is 15 minutes. Targets are either `--scrape-urls a,b,c` (scrape) or `--crawl-url ` (crawl whole site each check). Note: `--state` (not `--status`) sets active/paused; `--page-status` (not `--status`) filters page results on `check` — avoids collision with the global `--status` flag. Monitoring is not available for zero-data-retention teams. **Avoid redundant fetches:** diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 3130466b7..cfa0712fb 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -149,7 +149,7 @@ function buildCreateBody(opts: { const hasScrape = opts.urls && opts.urls.length > 0; const hasCrawl = !!opts.crawlUrl; if (!hasScrape && !hasCrawl) { - throw new Error('Provide --urls (scrape) or --crawl-url (crawl)'); + throw new Error('Provide --scrape-urls or --crawl-url'); } const schedule: Record = {}; @@ -226,7 +226,7 @@ export function createMonitorCommand(): Command { ) .option('--timezone ', 'Schedule timezone', 'UTC') .option( - '--urls ', + '--scrape-urls ', 'Comma-separated URLs to scrape on each check', parseCommaList ) @@ -253,7 +253,7 @@ export function createMonitorCommand(): Command { cron: options.cron, scheduleText: options.schedule, timezone: options.timezone, - urls: options.urls, + urls: options.scrapeUrls, crawlUrl: options.crawlUrl, webhookUrl: options.webhookUrl, webhookEvents: options.webhookEvents, @@ -455,7 +455,7 @@ export function createMonitorCommand(): Command { Examples: $ firecrawl monitor create --name "Blog" \\ --schedule "every 30 minutes" \\ - --urls https://example.com/blog \\ + --scrape-urls https://example.com/blog \\ --email alerts@example.com $ firecrawl monitor create monitor.json $ cat monitor.json | firecrawl monitor create From e251e6f01228dd2dbf1bc509bb227a2f9d6b4d24 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 12 May 2026 11:22:33 -0400 Subject: [PATCH 6/6] bump @mendable/firecrawl-js to 4.22.2; document why monitor still uses raw fetch --- package.json | 2 +- pnpm-lock.yaml | 29 +++++++++++++++-------------- src/commands/monitor.ts | 10 +++++++--- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index b8baa4087..fd89658ed 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ }, "dependencies": { "@inquirer/prompts": "^8.2.1", - "@mendable/firecrawl-js": "4.17.0", + "@mendable/firecrawl-js": "4.22.2", "commander": "^14.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8658d6fad..881b636ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^8.2.1 version: 8.2.1(@types/node@20.19.27) '@mendable/firecrawl-js': - specifier: 4.17.0 - version: 4.17.0 + specifier: 4.22.2 + version: 4.22.2 commander: specifier: ^14.0.2 version: 14.0.2 @@ -332,8 +332,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@mendable/firecrawl-js@4.17.0': - resolution: {integrity: sha512-4Dz2y8QLJMlf45qQIyCgvfjbz+cn9T5jRf0aTxFptBe+123373Vsker9vKYHriWIl2oO/SwRSILkJV6AsGlCMA==} + '@mendable/firecrawl-js@4.22.2': + resolution: {integrity: sha512-HRxafhBsioKvCnhkLPIzIO8qsiyLLyqPe8Oaz5vMR3olb4V1wJLe2oYIzboAppOQbv0++DUeE5K6l03wR+CeHA==} engines: {node: '>=22.0.0'} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -524,8 +524,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.13.6: - resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + axios@1.15.2: + resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -860,8 +860,9 @@ packages: engines: {node: '>=14'} hasBin: true - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} @@ -1259,9 +1260,9 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@mendable/firecrawl-js@4.17.0': + '@mendable/firecrawl-js@4.22.2': dependencies: - axios: 1.13.6 + axios: 1.15.2 firecrawl: 4.16.0 typescript-event-target: 1.1.2 zod: 3.25.76 @@ -1410,11 +1411,11 @@ snapshots: asynckit@0.4.0: {} - axios@1.13.6: + axios@1.15.2: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 - proxy-from-env: 1.1.0 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -1562,7 +1563,7 @@ snapshots: firecrawl@4.16.0: dependencies: - axios: 1.13.6 + axios: 1.15.2 typescript-event-target: 1.1.2 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) @@ -1737,7 +1738,7 @@ snapshots: prettier@3.7.4: {} - proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} restore-cursor@5.1.0: dependencies: diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index cfa0712fb..f042ec922 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -4,9 +4,13 @@ * Monitors run recurring scrapes/crawls and diff each result against the last * retained snapshot. See features/monitoring in the docs. * - * The SDK (@mendable/firecrawl-js@4.17.0) does not yet expose the monitor - * endpoints, so this command hits /v2/monitor directly via fetch — same - * pattern parse.ts uses. + * @mendable/firecrawl-js@4.22.2 exposes monitor methods (createMonitor, + * listMonitors, getMonitor, updateMonitor, deleteMonitor, runMonitor, + * listMonitorChecks, getMonitorCheck), but its HttpClient injects a top-level + * `origin: js-sdk@` field into every POST/PATCH body and the + * /v2/monitor endpoint rejects that with "Unrecognized key in body". Until the + * SDK strips `origin` for monitor requests (or the API accepts it), we hit + * /v2/monitor directly via fetch — same pattern parse.ts uses. * * Subcommands: * create | list | get | update | delete | run | checks | check