firecrawl 4.21.1 → 4.22.1

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/src/v2/client.ts CHANGED
@@ -32,6 +32,16 @@ import {
32
32
  listBrowsers,
33
33
  } from "./methods/browser";
34
34
  import { getConcurrency, getCreditUsage, getQueueStatus, getTokenUsage, getCreditUsageHistorical, getTokenUsageHistorical } from "./methods/usage";
35
+ import {
36
+ createMonitor as createMonitorMethod,
37
+ deleteMonitor as deleteMonitorMethod,
38
+ getMonitor as getMonitorMethod,
39
+ getMonitorCheck as getMonitorCheckMethod,
40
+ listMonitorChecks as listMonitorChecksMethod,
41
+ listMonitors as listMonitorsMethod,
42
+ runMonitor as runMonitorMethod,
43
+ updateMonitor as updateMonitorMethod,
44
+ } from "./methods/monitor";
35
45
  import type {
36
46
  Document,
37
47
  ParseFile,
@@ -60,6 +70,14 @@ import type {
60
70
  ScrapeExecuteRequest,
61
71
  ScrapeExecuteResponse,
62
72
  ScrapeBrowserDeleteResponse,
73
+ CreateMonitorRequest,
74
+ ListMonitorChecksOptions,
75
+ ListMonitorsOptions,
76
+ Monitor,
77
+ MonitorCheck,
78
+ MonitorCheckDetail,
79
+ GetMonitorCheckOptions,
80
+ UpdateMonitorRequest,
63
81
  } from "./types";
64
82
  import { Watcher } from "./watcher";
65
83
  import type { WatcherOptions } from "./watcher";
@@ -276,6 +294,73 @@ export class FirecrawlClient {
276
294
  return crawlParamsPreview(this.http, url, prompt);
277
295
  }
278
296
 
297
+ // Monitor
298
+ /**
299
+ * Create a scheduled monitor.
300
+ */
301
+ async createMonitor(request: CreateMonitorRequest): Promise<Monitor> {
302
+ return createMonitorMethod(this.http, request);
303
+ }
304
+
305
+ /**
306
+ * List monitors for the authenticated team.
307
+ */
308
+ async listMonitors(options?: ListMonitorsOptions): Promise<Monitor[]> {
309
+ return listMonitorsMethod(this.http, options);
310
+ }
311
+
312
+ /**
313
+ * Get a monitor by id.
314
+ */
315
+ async getMonitor(monitorId: string): Promise<Monitor> {
316
+ return getMonitorMethod(this.http, monitorId);
317
+ }
318
+
319
+ /**
320
+ * Update a monitor.
321
+ */
322
+ async updateMonitor(
323
+ monitorId: string,
324
+ request: UpdateMonitorRequest,
325
+ ): Promise<Monitor> {
326
+ return updateMonitorMethod(this.http, monitorId, request);
327
+ }
328
+
329
+ /**
330
+ * Delete a monitor.
331
+ */
332
+ async deleteMonitor(monitorId: string): Promise<boolean> {
333
+ return deleteMonitorMethod(this.http, monitorId);
334
+ }
335
+
336
+ /**
337
+ * Trigger a manual monitor check.
338
+ */
339
+ async runMonitor(monitorId: string): Promise<MonitorCheck> {
340
+ return runMonitorMethod(this.http, monitorId);
341
+ }
342
+
343
+ /**
344
+ * List checks for a monitor.
345
+ */
346
+ async listMonitorChecks(
347
+ monitorId: string,
348
+ options?: ListMonitorChecksOptions,
349
+ ): Promise<MonitorCheck[]> {
350
+ return listMonitorChecksMethod(this.http, monitorId, options);
351
+ }
352
+
353
+ /**
354
+ * Get a monitor check with paginated page results and inline diffs.
355
+ */
356
+ async getMonitorCheck(
357
+ monitorId: string,
358
+ checkId: string,
359
+ options?: GetMonitorCheckOptions,
360
+ ): Promise<MonitorCheckDetail> {
361
+ return getMonitorCheckMethod(this.http, monitorId, checkId, options);
362
+ }
363
+
279
364
  // Batch
280
365
  /**
281
366
  * Start a batch scrape job for multiple URLs (async).
@@ -0,0 +1,181 @@
1
+ import {
2
+ type CreateMonitorRequest,
3
+ type ListMonitorsOptions,
4
+ type ListMonitorChecksOptions,
5
+ type Monitor,
6
+ type MonitorCheck,
7
+ type MonitorCheckDetail,
8
+ type MonitorCheckPage,
9
+ type GetMonitorCheckOptions,
10
+ type UpdateMonitorRequest,
11
+ } from "../types";
12
+ import { HttpClient } from "../utils/httpClient";
13
+ import {
14
+ throwForBadResponse,
15
+ normalizeAxiosError,
16
+ } from "../utils/errorHandler";
17
+ import { fetchAllPages } from "../utils/pagination";
18
+
19
+ type ApiResponse<T> = {
20
+ success: boolean;
21
+ data?: T;
22
+ id?: string;
23
+ error?: string;
24
+ };
25
+
26
+ function queryString(params?: Record<string, unknown>): string {
27
+ if (!params) return "";
28
+ const query = new URLSearchParams();
29
+ for (const [key, value] of Object.entries(params)) {
30
+ if (value !== undefined && value !== null) query.set(key, String(value));
31
+ }
32
+ const str = query.toString();
33
+ return str ? `?${str}` : "";
34
+ }
35
+
36
+ function dataOrThrow<T>(res: { status: number; data?: ApiResponse<T> }, action: string): T {
37
+ if (res.status !== 200 || !res.data?.success || res.data.data == null) {
38
+ throwForBadResponse(res as any, action);
39
+ }
40
+ return res.data.data;
41
+ }
42
+
43
+ export async function createMonitor(
44
+ http: HttpClient,
45
+ request: CreateMonitorRequest,
46
+ ): Promise<Monitor> {
47
+ try {
48
+ const res = await http.post<ApiResponse<Monitor>>("/v2/monitor", request as any);
49
+ return dataOrThrow(res, "create monitor");
50
+ } catch (err: any) {
51
+ if (err?.isAxiosError) return normalizeAxiosError(err, "create monitor");
52
+ throw err;
53
+ }
54
+ }
55
+
56
+ export async function listMonitors(
57
+ http: HttpClient,
58
+ options?: ListMonitorsOptions,
59
+ ): Promise<Monitor[]> {
60
+ try {
61
+ const res = await http.get<ApiResponse<Monitor[]>>(
62
+ `/v2/monitor${queryString(options as Record<string, unknown>)}`,
63
+ );
64
+ return dataOrThrow(res, "list monitors");
65
+ } catch (err: any) {
66
+ if (err?.isAxiosError) return normalizeAxiosError(err, "list monitors");
67
+ throw err;
68
+ }
69
+ }
70
+
71
+ export async function getMonitor(
72
+ http: HttpClient,
73
+ monitorId: string,
74
+ ): Promise<Monitor> {
75
+ try {
76
+ const res = await http.get<ApiResponse<Monitor>>(`/v2/monitor/${monitorId}`);
77
+ return dataOrThrow(res, "get monitor");
78
+ } catch (err: any) {
79
+ if (err?.isAxiosError) return normalizeAxiosError(err, "get monitor");
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ export async function updateMonitor(
85
+ http: HttpClient,
86
+ monitorId: string,
87
+ request: UpdateMonitorRequest,
88
+ ): Promise<Monitor> {
89
+ try {
90
+ const res = await http.patch<ApiResponse<Monitor>>(
91
+ `/v2/monitor/${monitorId}`,
92
+ request as any,
93
+ );
94
+ return dataOrThrow(res, "update monitor");
95
+ } catch (err: any) {
96
+ if (err?.isAxiosError) return normalizeAxiosError(err, "update monitor");
97
+ throw err;
98
+ }
99
+ }
100
+
101
+ export async function deleteMonitor(
102
+ http: HttpClient,
103
+ monitorId: string,
104
+ ): Promise<boolean> {
105
+ try {
106
+ const res = await http.delete<ApiResponse<unknown>>(`/v2/monitor/${monitorId}`);
107
+ if (res.status !== 200 || !res.data?.success) {
108
+ throwForBadResponse(res, "delete monitor");
109
+ }
110
+ return true;
111
+ } catch (err: any) {
112
+ if (err?.isAxiosError) return normalizeAxiosError(err, "delete monitor");
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ export async function runMonitor(
118
+ http: HttpClient,
119
+ monitorId: string,
120
+ ): Promise<MonitorCheck> {
121
+ try {
122
+ const res = await http.post<ApiResponse<MonitorCheck>>(
123
+ `/v2/monitor/${monitorId}/run`,
124
+ {},
125
+ );
126
+ return dataOrThrow(res, "run monitor");
127
+ } catch (err: any) {
128
+ if (err?.isAxiosError) return normalizeAxiosError(err, "run monitor");
129
+ throw err;
130
+ }
131
+ }
132
+
133
+ export async function listMonitorChecks(
134
+ http: HttpClient,
135
+ monitorId: string,
136
+ options?: ListMonitorChecksOptions,
137
+ ): Promise<MonitorCheck[]> {
138
+ try {
139
+ const res = await http.get<ApiResponse<MonitorCheck[]>>(
140
+ `/v2/monitor/${monitorId}/checks${queryString(options as Record<string, unknown>)}`,
141
+ );
142
+ return dataOrThrow(res, "list monitor checks");
143
+ } catch (err: any) {
144
+ if (err?.isAxiosError) return normalizeAxiosError(err, "list monitor checks");
145
+ throw err;
146
+ }
147
+ }
148
+
149
+ export async function getMonitorCheck(
150
+ http: HttpClient,
151
+ monitorId: string,
152
+ checkId: string,
153
+ options?: GetMonitorCheckOptions,
154
+ ): Promise<MonitorCheckDetail> {
155
+ try {
156
+ const { autoPaginate: _autoPaginate, maxPages: _maxPages, maxResults: _maxResults, maxWaitTime: _maxWaitTime, ...query } = options ?? {};
157
+ const res = await http.get<ApiResponse<MonitorCheckDetail>>(
158
+ `/v2/monitor/${monitorId}/checks/${checkId}${queryString(query as Record<string, unknown>)}`,
159
+ );
160
+ const detail = dataOrThrow(res, "get monitor check");
161
+ const next = res.data?.next ?? detail.next ?? null;
162
+ const auto = options?.autoPaginate ?? true;
163
+ if (!auto || !next) {
164
+ return { ...detail, next };
165
+ }
166
+
167
+ return {
168
+ ...detail,
169
+ pages: await fetchAllPages<MonitorCheckPage>(
170
+ http,
171
+ next,
172
+ detail.pages || [],
173
+ options,
174
+ ),
175
+ next: null,
176
+ };
177
+ } catch (err: any) {
178
+ if (err?.isAxiosError) return normalizeAxiosError(err, "get monitor check");
179
+ throw err;
180
+ }
181
+ }
package/src/v2/types.ts CHANGED
@@ -52,6 +52,17 @@ export interface AttributesFormat extends Format {
52
52
  }>;
53
53
  }
54
54
 
55
+ export interface QuestionFormat {
56
+ type: 'question';
57
+ question: string;
58
+ }
59
+
60
+ export interface HighlightsFormat {
61
+ type: 'highlights';
62
+ query: string;
63
+ }
64
+
65
+ /** @deprecated Use QuestionFormat or HighlightsFormat instead. */
55
66
  export interface QueryFormat {
56
67
  type: 'query';
57
68
  prompt: string;
@@ -65,6 +76,8 @@ export type FormatOption =
65
76
  | ChangeTrackingFormat
66
77
  | ScreenshotFormat
67
78
  | AttributesFormat
79
+ | QuestionFormat
80
+ | HighlightsFormat
68
81
  | QueryFormat;
69
82
 
70
83
  export type ParseFormatString = Exclude<
@@ -81,6 +94,8 @@ export type ParseFormatOption =
81
94
  | ParseFormat
82
95
  | JsonFormat
83
96
  | AttributesFormat
97
+ | QuestionFormat
98
+ | HighlightsFormat
84
99
  | QueryFormat;
85
100
 
86
101
  export interface LocationConfig {
@@ -450,6 +465,7 @@ export interface Document {
450
465
  }>;
451
466
  actions?: Record<string, unknown>;
452
467
  answer?: string;
468
+ highlights?: string;
453
469
  warning?: string;
454
470
  changeTracking?: Record<string, unknown>;
455
471
  branding?: BrandingProfile;
@@ -606,6 +622,154 @@ export interface MapOptions {
606
622
  location?: LocationConfig;
607
623
  }
608
624
 
625
+ export interface MonitorSchedule {
626
+ cron: string;
627
+ timezone?: string;
628
+ }
629
+
630
+ export interface MonitorEmailNotification {
631
+ enabled?: boolean;
632
+ recipients?: string[];
633
+ includeDiffs?: boolean;
634
+ }
635
+
636
+ export interface MonitorNotification {
637
+ email?: MonitorEmailNotification;
638
+ }
639
+
640
+ export interface MonitorWebhookConfig {
641
+ url: string;
642
+ headers?: Record<string, string>;
643
+ metadata?: Record<string, string>;
644
+ events?: string[];
645
+ }
646
+
647
+ export interface MonitorScrapeTarget {
648
+ id?: string;
649
+ type: 'scrape';
650
+ urls: string[];
651
+ scrapeOptions?: ScrapeOptions;
652
+ }
653
+
654
+ export interface MonitorCrawlTarget {
655
+ id?: string;
656
+ type: 'crawl';
657
+ url: string;
658
+ crawlOptions?: CrawlOptions;
659
+ scrapeOptions?: ScrapeOptions;
660
+ }
661
+
662
+ export type MonitorTarget = MonitorScrapeTarget | MonitorCrawlTarget;
663
+
664
+ export interface CreateMonitorRequest {
665
+ name: string;
666
+ schedule: MonitorSchedule;
667
+ webhook?: MonitorWebhookConfig;
668
+ notification?: MonitorNotification;
669
+ targets: MonitorTarget[];
670
+ retentionDays?: number;
671
+ }
672
+
673
+ export interface UpdateMonitorRequest {
674
+ name?: string;
675
+ status?: 'active' | 'paused';
676
+ schedule?: MonitorSchedule;
677
+ webhook?: MonitorWebhookConfig | null;
678
+ notification?: MonitorNotification | null;
679
+ targets?: MonitorTarget[];
680
+ retentionDays?: number;
681
+ }
682
+
683
+ export interface MonitorSummary {
684
+ totalPages: number;
685
+ same: number;
686
+ changed: number;
687
+ new: number;
688
+ removed: number;
689
+ error: number;
690
+ }
691
+
692
+ export interface Monitor {
693
+ id: string;
694
+ name: string;
695
+ status: 'active' | 'paused' | 'deleted';
696
+ schedule: MonitorSchedule;
697
+ nextRunAt?: string | null;
698
+ lastRunAt?: string | null;
699
+ currentCheckId?: string | null;
700
+ targets: MonitorTarget[];
701
+ webhook?: MonitorWebhookConfig | null;
702
+ notification?: MonitorNotification | null;
703
+ retentionDays: number;
704
+ estimatedCreditsPerMonth?: number | null;
705
+ lastCheckSummary?: MonitorSummary | null;
706
+ createdAt: string;
707
+ updatedAt: string;
708
+ }
709
+
710
+ export interface MonitorCheck {
711
+ id: string;
712
+ monitorId: string;
713
+ status:
714
+ | 'queued'
715
+ | 'running'
716
+ | 'completed'
717
+ | 'failed'
718
+ | 'partial'
719
+ | 'skipped_overlap';
720
+ trigger: 'scheduled' | 'manual';
721
+ scheduledFor?: string | null;
722
+ startedAt?: string | null;
723
+ finishedAt?: string | null;
724
+ estimatedCredits?: number | null;
725
+ reservedCredits?: number | null;
726
+ actualCredits?: number | null;
727
+ billingStatus:
728
+ | 'not_applicable'
729
+ | 'reserved'
730
+ | 'confirmed'
731
+ | 'released'
732
+ | 'failed';
733
+ summary: MonitorSummary;
734
+ targetResults?: unknown;
735
+ notificationStatus?: unknown;
736
+ error?: string | null;
737
+ createdAt: string;
738
+ updatedAt: string;
739
+ }
740
+
741
+ export interface MonitorCheckPage {
742
+ id: string;
743
+ targetId: string;
744
+ url: string;
745
+ status: 'same' | 'new' | 'changed' | 'removed' | 'error';
746
+ previousScrapeId?: string | null;
747
+ currentScrapeId?: string | null;
748
+ statusCode?: number | null;
749
+ error?: string | null;
750
+ metadata?: unknown;
751
+ diff?: unknown;
752
+ createdAt: string;
753
+ }
754
+
755
+ export interface MonitorCheckDetail extends MonitorCheck {
756
+ pages: MonitorCheckPage[];
757
+ next?: string | null;
758
+ }
759
+
760
+ export interface ListMonitorsOptions {
761
+ limit?: number;
762
+ offset?: number;
763
+ }
764
+
765
+ export type ListMonitorChecksOptions = ListMonitorsOptions;
766
+
767
+ export type GetMonitorCheckOptions = PaginationConfig & {
768
+ limit?: number;
769
+ skip?: number;
770
+ status?: MonitorCheckPage["status"];
771
+ };
772
+
609
773
  export interface ExtractResponse {
610
774
  success?: boolean;
611
775
  id?: string;
@@ -149,6 +149,20 @@ export class HttpClient {
149
149
  return this.request<T>({ method: "delete", url: endpoint, headers });
150
150
  }
151
151
 
152
+ patch<T = any>(
153
+ endpoint: string,
154
+ body: Record<string, unknown>,
155
+ options?: RequestOptions,
156
+ ) {
157
+ return this.request<T>({
158
+ method: "patch",
159
+ url: endpoint,
160
+ data: body,
161
+ headers: options?.headers,
162
+ timeout: options?.timeoutMs,
163
+ });
164
+ }
165
+
152
166
  prepareHeaders(idempotencyKey?: string): Record<string, string> {
153
167
  const headers: Record<string, string> = {};
154
168
  if (idempotencyKey) headers["x-idempotency-key"] = idempotencyKey;
@@ -2,14 +2,14 @@ import type { HttpClient } from "../utils/httpClient";
2
2
  import type { Document, PaginationConfig } from "../types";
3
3
 
4
4
  /**
5
- * Shared helper to follow `next` cursors and aggregate documents with limits.
5
+ * Shared helper to follow `next` URLs and aggregate paginated result arrays.
6
6
  */
7
- export async function fetchAllPages(
7
+ export async function fetchAllPages<T = Document>(
8
8
  http: HttpClient,
9
9
  nextUrl: string,
10
- initial: Document[],
10
+ initial: T[],
11
11
  pagination?: PaginationConfig
12
- ): Promise<Document[]> {
12
+ ): Promise<T[]> {
13
13
  const docs = initial.slice();
14
14
  let current: string | null = nextUrl;
15
15
  let pageCount = 0;
@@ -22,21 +22,24 @@ export async function fetchAllPages(
22
22
  if (maxPages != null && pageCount >= maxPages) break;
23
23
  if (maxWaitTime != null && (Date.now() - started) / 1000 > maxWaitTime) break;
24
24
 
25
- let payload: { success: boolean; next?: string | null; data?: Document[] } | null = null;
25
+ let payload: { success: boolean; next?: string | null; data?: T[] | { pages?: T[]; next?: string | null } } | null = null;
26
26
  try {
27
- const res = await http.get<{ success: boolean; next?: string | null; data?: Document[] }>(current);
27
+ const res = await http.get<{ success: boolean; next?: string | null; data?: T[] | { pages?: T[]; next?: string | null } }>(current);
28
28
  payload = res.data;
29
29
  } catch {
30
30
  break; // axios rejects on non-2xx; stop pagination gracefully
31
31
  }
32
32
  if (!payload?.success) break;
33
33
 
34
- for (const d of payload.data || []) {
34
+ const pageData = Array.isArray(payload.data)
35
+ ? payload.data
36
+ : payload.data?.pages || [];
37
+ for (const d of pageData) {
35
38
  if (maxResults != null && docs.length >= maxResults) break;
36
- docs.push(d as Document);
39
+ docs.push(d as T);
37
40
  }
38
41
  if (maxResults != null && docs.length >= maxResults) break;
39
- current = (payload.next ?? null) as string | null;
42
+ current = (payload.next ?? (Array.isArray(payload.data) ? null : payload.data?.next) ?? null) as string | null;
40
43
  pageCount += 1;
41
44
  }
42
45
  return docs;
@@ -4,6 +4,9 @@ import {
4
4
  type JsonFormat,
5
5
  type ParseFormatOption,
6
6
  type ParseOptions,
7
+ type QuestionFormat,
8
+ type HighlightsFormat,
9
+ type QueryFormat,
7
10
  type ScrapeOptions,
8
11
  type ScreenshotFormat,
9
12
  } from "../types";
@@ -49,6 +52,30 @@ export function ensureValidFormats(formats?: FormatOption[]): void {
49
52
  }
50
53
  continue;
51
54
  }
55
+ if ((fmt as QuestionFormat).type === "question") {
56
+ const q = fmt as QuestionFormat;
57
+ if (typeof q.question !== "string" || q.question.trim().length === 0) {
58
+ throw new Error("question format requires a non-empty 'question' string");
59
+ }
60
+ continue;
61
+ }
62
+ if ((fmt as HighlightsFormat).type === "highlights") {
63
+ const h = fmt as HighlightsFormat;
64
+ if (typeof h.query !== "string" || h.query.trim().length === 0) {
65
+ throw new Error("highlights format requires a non-empty 'query' string");
66
+ }
67
+ continue;
68
+ }
69
+ if ((fmt as QueryFormat).type === "query") {
70
+ const q = fmt as QueryFormat;
71
+ if (typeof q.prompt !== "string" || q.prompt.trim().length === 0) {
72
+ throw new Error("query format requires a non-empty 'prompt' string");
73
+ }
74
+ if (q.mode != null && q.mode !== "freeform" && q.mode !== "directQuote") {
75
+ throw new Error("query format mode must be 'freeform' or 'directQuote'");
76
+ }
77
+ continue;
78
+ }
52
79
  if ((fmt as ScreenshotFormat).type === "screenshot") {
53
80
  // no-op; already camelCase; validate numeric fields if present
54
81
  const s = fmt as ScreenshotFormat;
@@ -116,6 +143,31 @@ export function ensureValidParseFormats(formats?: ParseFormatOption[]): void {
116
143
  "The SDK will automatically convert Zod schemas to JSON Schema format."
117
144
  );
118
145
  }
146
+ continue;
147
+ }
148
+
149
+ if ((fmt as QuestionFormat).type === "question") {
150
+ const q = fmt as QuestionFormat;
151
+ if (typeof q.question !== "string" || q.question.trim().length === 0) {
152
+ throw new Error("question format requires a non-empty 'question' string");
153
+ }
154
+ continue;
155
+ }
156
+ if ((fmt as HighlightsFormat).type === "highlights") {
157
+ const h = fmt as HighlightsFormat;
158
+ if (typeof h.query !== "string" || h.query.trim().length === 0) {
159
+ throw new Error("highlights format requires a non-empty 'query' string");
160
+ }
161
+ continue;
162
+ }
163
+ if ((fmt as QueryFormat).type === "query") {
164
+ const q = fmt as QueryFormat;
165
+ if (typeof q.prompt !== "string" || q.prompt.trim().length === 0) {
166
+ throw new Error("query format requires a non-empty 'prompt' string");
167
+ }
168
+ if (q.mode != null && q.mode !== "freeform" && q.mode !== "directQuote") {
169
+ throw new Error("query format mode must be 'freeform' or 'directQuote'");
170
+ }
119
171
  }
120
172
  }
121
173
  }