firecrawl 4.38.0 → 4.40.0

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.
Files changed (37) hide show
  1. package/README.md +53 -2
  2. package/dist/{chunk-KSTNOGOD.js → chunk-OJ4YJQ4J.js} +1 -1
  3. package/dist/index.cjs +204 -17
  4. package/dist/index.d.cts +289 -7
  5. package/dist/index.d.ts +289 -7
  6. package/dist/index.js +203 -17
  7. package/dist/{package-OS7RONXY.js → package-F6O7J2XI.js} +1 -1
  8. package/package.json +1 -1
  9. package/pnpm-workspace.yaml +3 -2
  10. package/src/__tests__/e2e/v2/batch.test.ts +13 -11
  11. package/src/__tests__/e2e/v2/crawl.test.ts +24 -20
  12. package/src/__tests__/e2e/v2/extract.test.ts +32 -7
  13. package/src/__tests__/e2e/v2/map.test.ts +4 -3
  14. package/src/__tests__/e2e/v2/parse.test.ts +4 -3
  15. package/src/__tests__/e2e/v2/scrape.test.ts +10 -9
  16. package/src/__tests__/e2e/v2/search.test.ts +9 -8
  17. package/src/__tests__/e2e/v2/usage.test.ts +6 -5
  18. package/src/__tests__/e2e/v2/utils/rateLimit.ts +257 -0
  19. package/src/__tests__/e2e/v2/watcher.test.ts +4 -3
  20. package/src/__tests__/unit/v2/agent-effort-trace.unit.test.ts +1 -1
  21. package/src/__tests__/unit/v2/agent-thread.unit.test.ts +164 -0
  22. package/src/__tests__/unit/v2/e2eRateLimitRetry.test.ts +501 -0
  23. package/src/__tests__/unit/v2/extract.unit.test.ts +1 -1
  24. package/src/__tests__/unit/v2/scrapeAutoResume.test.ts +1 -1
  25. package/src/__tests__/unit/v2/search-country.unit.test.ts +32 -0
  26. package/src/__tests__/unit/v2/search-data-property.test.ts +1 -0
  27. package/src/__tests__/unit/v2/threat-protection.unit.test.ts +18 -1
  28. package/src/__tests__/unit/v2/tools.unit.test.ts +240 -0
  29. package/src/__tests__/unit/v2/v1-aliases.test.ts +1 -0
  30. package/src/v2/client.ts +52 -4
  31. package/src/v2/methods/agent.ts +23 -1
  32. package/src/v2/methods/research.ts +14 -8
  33. package/src/v2/methods/scrape.ts +3 -1
  34. package/src/v2/methods/search.ts +8 -1
  35. package/src/v2/methods/tools.ts +137 -0
  36. package/src/v2/types.ts +307 -35
  37. package/src/v2/utils/errorHandler.ts +19 -3
@@ -0,0 +1,32 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { search } from "../../../v2/methods/search";
3
+
4
+ function httpMock() {
5
+ return {
6
+ post: jest.fn(async () => ({ status: 200, data: { success: true } })),
7
+ } as any;
8
+ }
9
+
10
+ describe("v2 search country", () => {
11
+ test("forwards the country option", async () => {
12
+ const http = httpMock();
13
+
14
+ await search(http, { query: "firecrawl", country: "de" });
15
+
16
+ expect(http.post).toHaveBeenCalledWith(
17
+ "/v2/search",
18
+ { query: "firecrawl", country: "de" },
19
+ {},
20
+ );
21
+ });
22
+
23
+ test("omits country when the caller does not set it", async () => {
24
+ const http = httpMock();
25
+
26
+ await search(http, { query: "firecrawl" });
27
+
28
+ expect(http.post).toHaveBeenCalledWith("/v2/search", { query: "firecrawl" }, {});
29
+ const payload = http.post.mock.calls[0][1] as Record<string, unknown>;
30
+ expect(payload).not.toHaveProperty("country");
31
+ });
32
+ });
@@ -1,3 +1,4 @@
1
+ import { jest } from "@jest/globals";
1
2
  import { search } from "../../../v2/methods/search";
2
3
 
3
4
  const fakeHttp = {
@@ -32,7 +32,17 @@ describe("v2 threatProtection request serialization", () => {
32
32
  expect(http.post).toHaveBeenCalledWith(
33
33
  "/v2/scrape",
34
34
  expect.objectContaining({ url: "https://example.com", threatProtection }),
35
- {},
35
+ { timeoutMs: 330000 },
36
+ );
37
+ });
38
+
39
+ test("scrape sends domainTools when true", async () => {
40
+ const http = makeHttp({ success: true, data: {} });
41
+ await scrape(http, "https://example.com", { domainTools: true });
42
+ expect(http.post).toHaveBeenCalledWith(
43
+ "/v2/scrape",
44
+ expect.objectContaining({ url: "https://example.com", domainTools: true }),
45
+ expect.anything(),
36
46
  );
37
47
  });
38
48
 
@@ -121,3 +131,10 @@ describe("v2 threatProtection request serialization", () => {
121
131
  expect(body).not.toHaveProperty("threatProtection");
122
132
  });
123
133
  });
134
+
135
+
136
+ test.each([false, undefined])("scrape omits domainTools when %s", async (domainTools) => {
137
+ const http = makeHttp({ success: true, data: {} });
138
+ await scrape(http, "https://example.com", { domainTools });
139
+ expect(http.post.mock.calls[0][1]).not.toHaveProperty("domainTools");
140
+ });
@@ -0,0 +1,240 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from "@jest/globals";
2
+ import { createServer, type Server } from "node:http";
3
+ import { FirecrawlClient } from "../../../v2/client";
4
+
5
+ import { scrapeAlexandria } from "../../../v2/methods/tools";
6
+
7
+ const tool = {
8
+ id: "particle/podcasts/episodes/search",
9
+ provider: "particle",
10
+ capability: "podcasts/episodes/search",
11
+ name: "Episode search",
12
+ description: "Find episodes",
13
+ creditsCost: 15,
14
+ perRecord: false,
15
+ options: [{ name: "semantic_search", type: "string" }],
16
+ response: { about: "Episodes", key: "data", fields: [] },
17
+ examples: { javascript: "example", python: "example", curl: "example" },
18
+ matchedBy: ["semantic", "domain"],
19
+ matchedUrls: ["https://podcasts.apple.com"],
20
+ };
21
+ const productionTool = {
22
+ id: "benzinga/calendar/ratings",
23
+ provider: "benzinga",
24
+ capability: "calendar/ratings",
25
+ name: "Analyst ratings",
26
+ description: "Ratings",
27
+ creditsCost: 5,
28
+ perRecord: false,
29
+ label: "Ratings",
30
+ whenToUse: "Analyst ratings for a ticker",
31
+ returns: { about: "Ratings" },
32
+ discovery: { urls: [] },
33
+ attribution: { required: true },
34
+ options: [{ name: "tickers", type: "string" }],
35
+ response: { fields: [] },
36
+ matchedBy: ["semantic"],
37
+ matchedUrls: [],
38
+ };
39
+ const termsRequired = {
40
+ success: false,
41
+ code: "THIRD_PARTY_DATA_TERMS_REQUIRED",
42
+ error: "An organization admin must accept the benzinga provider's terms",
43
+ requiresAction: {
44
+ type: "accept_terms",
45
+ terms: "benzinga",
46
+ version: "C-1.0.0-draft",
47
+ url: "https://www.firecrawl.dev/app/alexandria/benzinga",
48
+ },
49
+ };
50
+ const next = {
51
+ provider: "firecrawl",
52
+ capability: "find-tools",
53
+ options: { providers: ["particle"], level: "tools" },
54
+ };
55
+ let server: Server;
56
+ let client: FirecrawlClient;
57
+ const sent: Array<{ body: any; id: string | undefined }> = [];
58
+ let attempts = 0;
59
+
60
+ beforeEach(() => {
61
+ sent.length = 0;
62
+ attempts = 0;
63
+ });
64
+
65
+ beforeAll(async () => {
66
+ server = createServer(async (req, res) => {
67
+ let raw = "";
68
+ for await (const chunk of req) raw += chunk;
69
+ const body = JSON.parse(raw);
70
+ sent.push({ body, id: req.headers["x-request-id"] as string | undefined });
71
+ res.setHeader("content-type", "application/json");
72
+ if (req.url === "/v2/search")
73
+ return res.end(
74
+ JSON.stringify({
75
+ success: true,
76
+ warning: "Example warning",
77
+ data: {
78
+ web: [{ url: "https://podcasts.apple.com" }],
79
+ tools: [tool, productionTool],
80
+ },
81
+ }),
82
+ );
83
+ if (body.alexandria[0].provider === "retry" && attempts++ === 0) {
84
+ res.statusCode = 502;
85
+ return res.end("{}");
86
+ }
87
+ if (body.alexandria[0].provider === "benzinga") {
88
+ res.statusCode = 403;
89
+ return res.end(JSON.stringify(termsRequired));
90
+ }
91
+ if (body.alexandria[0].provider === "denied") {
92
+ res.statusCode = 402;
93
+ return res.end(
94
+ JSON.stringify({
95
+ success: false,
96
+ code: "insufficient_credits",
97
+ error: "Insufficient credits",
98
+ }),
99
+ );
100
+ }
101
+ const alexandria =
102
+ body.alexandria[0].provider === "firecrawl"
103
+ ? [
104
+ {
105
+ ...next,
106
+ creditsCost: 0,
107
+ data: {
108
+ level: "providers",
109
+ items: [{ id: "particle", name: "Particle", next }],
110
+ total: 1,
111
+ next: null,
112
+ },
113
+ },
114
+ ]
115
+ : [
116
+ {
117
+ provider: "retry",
118
+ capability: "a/b",
119
+ creditsCost: 15,
120
+ data: { nested: { value: 1 } },
121
+ },
122
+ {
123
+ provider: "x",
124
+ capability: "b",
125
+ error: {
126
+ code: "unavailable",
127
+ message: "Unavailable",
128
+ status: 503,
129
+ },
130
+ },
131
+ ];
132
+ res.end(
133
+ JSON.stringify({
134
+ success: true,
135
+ scrape_id: "scrape-1",
136
+ data: { alexandria, creditsCost: alexandria.length === 1 ? 0 : 15 },
137
+ }),
138
+ );
139
+ });
140
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
141
+ const port = (server.address() as { port: number }).port;
142
+ client = new FirecrawlClient({
143
+ apiKey: "fc-test",
144
+ apiUrl: `http://127.0.0.1:${port}`,
145
+ backoffFactor: 0,
146
+ });
147
+ });
148
+ afterAll(async () => {
149
+ await new Promise<void>((resolve) => server.close(() => resolve()));
150
+ });
151
+
152
+ describe("Alexandria contracts and execution", () => {
153
+ test("returns complete unified tools and warning beside web results", async () => {
154
+ const result = await client.search("podcasts", {
155
+ sources: ["web", { type: "alexandria" }],
156
+ domainTools: true,
157
+ limit: 2,
158
+ });
159
+ expect(result.tools).toEqual([tool, productionTool]);
160
+ expect(result.tools?.[1].examples).toBeUndefined();
161
+ expect(result.tools?.[1].whenToUse).toBe("Analyst ratings for a ticker");
162
+ expect(result.warning).toBe("Example warning");
163
+ expect(sent.at(-1)?.body).toMatchObject({
164
+ domainTools: true,
165
+ sources: ["web", { type: "alexandria" }],
166
+ });
167
+ expect(result).not.toHaveProperty("alexandria");
168
+ });
169
+ test("retains one ID through transport retries and per-tool failures", async () => {
170
+ const result = await client.scrape({
171
+ alexandria: [{ provider: "retry", capability: "a/b" }],
172
+ requestId: "same-request",
173
+ });
174
+ const retries = sent.filter(
175
+ (r) => r.body.alexandria?.[0].provider === "retry",
176
+ );
177
+ expect(retries).toHaveLength(2);
178
+ expect(retries.map((r) => r.id)).toEqual(["same-request", "same-request"]);
179
+ expect(retries[0].body).not.toHaveProperty("requestId");
180
+ expect(result.requestId).toBe("same-request");
181
+ expect(result.creditsCost).toBe(15);
182
+ expect(result.alexandria[1].error?.code).toBe("unavailable");
183
+ });
184
+ test("returns an error code and retry identity on failed execution", async () => {
185
+ await expect(
186
+ client.scrape({
187
+ alexandria: { provider: "denied", capability: "a/b" },
188
+ requestId: "denied-1",
189
+ }),
190
+ ).rejects.toMatchObject({
191
+ status: 402,
192
+ code: "insufficient_credits",
193
+ requestId: "denied-1",
194
+ });
195
+ });
196
+ test("exposes requiresAction on a provider terms rejection", async () => {
197
+ await expect(
198
+ client.scrape({
199
+ alexandria: { provider: "benzinga", capability: "calendar/ratings" },
200
+ requestId: "terms-1",
201
+ }),
202
+ ).rejects.toMatchObject({
203
+ status: 403,
204
+ code: "THIRD_PARTY_DATA_TERMS_REQUIRED",
205
+ message: termsRequired.error,
206
+ requestId: "terms-1",
207
+ requiresAction: termsRequired.requiresAction,
208
+ });
209
+ });
210
+ test("walks with Find Tools and feeds next directly into scrape", async () => {
211
+ const found = await client.findTools({ providers: ["particle"], limit: 2 });
212
+ const result = await client.scrape({ alexandria: found.items[0].next! });
213
+ expect(result.creditsCost).toBe(0);
214
+ expect(sent.at(-1)?.id).toBeTruthy();
215
+ expect(sent.at(-1)?.body.alexandria).toEqual([next]);
216
+ });
217
+ test("rejects URL options and queryless browsing before dispatch", async () => {
218
+ const count = sent.length;
219
+ await expect(
220
+ client.scrape({ alexandria: next, url: "https://example.com" } as any),
221
+ ).rejects.toThrow();
222
+ await expect(
223
+ client.search("", { sources: ["alexandria"] }),
224
+ ).rejects.toThrow("Query cannot be empty");
225
+ expect(sent).toHaveLength(count);
226
+ });
227
+ });
228
+
229
+
230
+ test.each([[undefined, 80000], [1000, 31000], [100000, 80000]])(
231
+ "Alexandria timeout %s allows response delivery (%s ms)",
232
+ async (timeout, timeoutMs) => {
233
+ const http = { post: jest.fn(async () => ({ status: 200, data: {
234
+ success: true, data: { alexandria: [], creditsCost: 0 },
235
+ } })) };
236
+ await scrapeAlexandria(http as any, [next], { timeout });
237
+ expect(http.post).toHaveBeenCalledWith("/v2/scrape", expect.anything(),
238
+ expect.objectContaining({ timeoutMs }));
239
+ },
240
+ );
@@ -1,3 +1,4 @@
1
+ import { jest } from "@jest/globals";
1
2
  import { FirecrawlClient } from "../../../v2/client";
2
3
 
3
4
  const ALIAS_MAP: Record<string, string> = {
package/src/v2/client.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  } from "./methods/scrape";
7
7
  import { parse as parseMethod } from "./methods/parse";
8
8
  import { search } from "./methods/search";
9
+ import { scrapeAlexandria, findTools } from "./methods/tools";
9
10
  import { developerSearch as developerSearchMethod } from "./methods/developer";
10
11
  import { map as mapMethod } from "./methods/map";
11
12
  import { feedback as feedbackMethod, searchFeedback as searchFeedbackMethod } from "./methods/feedback";
@@ -26,7 +27,7 @@ import {
26
27
  batchScrape as batchWaiter,
27
28
  } from "./methods/batch";
28
29
  import { startExtract, getExtractStatus, extract as extractWaiter } from "./methods/extract";
29
- import { startAgent, getAgentStatus, getAgentTrace, getAgentSnapshot, cancelAgent, listAgents, agent as agentWaiter } from "./methods/agent";
30
+ import { startAgent, getAgentStatus, getAgentThread, getAgentTrace, getAgentSnapshot, cancelAgent, listAgents, agent as agentWaiter } from "./methods/agent";
30
31
  import {
31
32
  browser as browserMethod,
32
33
  browserExecute,
@@ -47,6 +48,10 @@ import {
47
48
  } from "./methods/monitor";
48
49
  import type {
49
50
  Document,
51
+ AlexandriaScrapeRequest,
52
+ FindToolsOptions,
53
+ FindToolsData,
54
+ AlexandriaScrapeData,
50
55
  ParseFile,
51
56
  ParseOptions,
52
57
  ScrapeOptions,
@@ -70,6 +75,7 @@ import type {
70
75
  AgentStatusResponse,
71
76
  AgentTraceResponse,
72
77
  AgentSnapshotResponse,
78
+ AgentThreadResponse,
73
79
  AgentListOptions,
74
80
  AgentListResponse,
75
81
  CrawlOptions,
@@ -170,11 +176,42 @@ export class FirecrawlClient {
170
176
  */
171
177
  async scrape<Opts extends ScrapeOptions>(
172
178
  url: string,
173
- options: Opts
179
+ options: Opts,
174
180
  ): Promise<Omit<Document, "json"> & { json?: InferredJsonFromOptions<Opts> }>;
175
181
  async scrape(url: string, options?: ScrapeCallOptions): Promise<Document>;
176
- async scrape(url: string, options?: ScrapeCallOptions): Promise<Document> {
177
- return scrape(this.http, url, options);
182
+ async scrape(request: AlexandriaScrapeRequest): Promise<AlexandriaScrapeData>;
183
+ async scrape(
184
+ url: string | AlexandriaScrapeRequest,
185
+ options?: ScrapeCallOptions,
186
+ ): Promise<Document | AlexandriaScrapeData> {
187
+ if (typeof url === "string") return scrape(this.http, url, options);
188
+ if (
189
+ !url ||
190
+ options !== undefined ||
191
+ Object.keys(url).some(
192
+ (key) =>
193
+ ![
194
+ "alexandria",
195
+ "requestId",
196
+ "timeout",
197
+ "integration",
198
+ "origin",
199
+ ].includes(key),
200
+ )
201
+ ) {
202
+ throw new Error("Provide an alexandria request without URL scrape options");
203
+ }
204
+ const { alexandria, ...opts } = url;
205
+ return scrapeAlexandria(
206
+ this.http,
207
+ Array.isArray(alexandria) ? alexandria : [alexandria],
208
+ opts,
209
+ );
210
+ }
211
+
212
+ /** Explore the catalogue without executing the tools it returns. */
213
+ async findTools(options?: FindToolsOptions): Promise<FindToolsData> {
214
+ return findTools(this.http, options);
178
215
  }
179
216
  /**
180
217
  * Interact with the browser session associated with a scrape job.
@@ -284,6 +321,9 @@ export class FirecrawlClient {
284
321
  * Access the v2 research endpoints — Firecrawl's **research paper index**
285
322
  * (~43M paper abstracts) plus GitHub history/readmes.
286
323
  *
324
+ * `research.searchGithub()` is deprecated and stops responding after
325
+ * 2026-11-03. Use `developerSearch()` instead.
326
+ *
287
327
  * The paper corpus is roughly 90% biomedical and life sciences — PubMed,
288
328
  * bioRxiv and medRxiv — with arXiv covering physics, mathematics and
289
329
  * computer science.
@@ -566,6 +606,14 @@ export class FirecrawlClient {
566
606
  async getAgentSnapshot(jobId: string, snapshotId: string): Promise<AgentSnapshotResponse> {
567
607
  return getAgentSnapshot(this.http, jobId, snapshotId);
568
608
  }
609
+ /**
610
+ * Get a thread and its runs, oldest turn first.
611
+ * @param threadId Thread id, as returned by startAgent or getAgentStatus.
612
+ * @param options.includeData Inline each succeeded run's data.
613
+ */
614
+ async getAgentThread(threadId: string, options?: { includeData?: boolean }): Promise<AgentThreadResponse> {
615
+ return getAgentThread(this.http, threadId, options);
616
+ }
569
617
 
570
618
  // Browser
571
619
  /**
@@ -1,4 +1,4 @@
1
- import { type AgentEffort, type AgentListOptions, type AgentListResponse, type AgentResponse, type AgentSnapshotResponse, type AgentStatusResponse, type AgentTraceResponse, type AgentWebhookConfig, type AuditMetadata, type ThreatProtectionOptions } from "../types";
1
+ import { type AgentEffort, type AgentExchangeOptions, type AgentListOptions, type AgentListResponse, type AgentMode, type AgentResponse, type AgentSnapshotResponse, type AgentStatusResponse, type AgentThreadResponse, type AgentTraceResponse, type AgentWebhookConfig, type AuditMetadata, type ThreatProtectionOptions } from "../types";
2
2
  import { HttpClient } from "../utils/httpClient";
3
3
  import { normalizeAxiosError, throwForBadResponse } from "../utils/errorHandler";
4
4
  import { isZodSchema, zodSchemaToJsonSchema } from "../../utils/zodSchemaToJson";
@@ -17,6 +17,9 @@ function prepareAgentPayload(args: {
17
17
  webhook?: string | AgentWebhookConfig;
18
18
  threatProtection?: ThreatProtectionOptions;
19
19
  auditMetadata?: AuditMetadata;
20
+ threadId?: string;
21
+ mode?: AgentMode;
22
+ exchange?: AgentExchangeOptions;
20
23
  }): Record<string, unknown> {
21
24
  const body: Record<string, unknown> = {};
22
25
  if (args.urls) body.urls = args.urls;
@@ -35,6 +38,9 @@ function prepareAgentPayload(args: {
35
38
  body.threatProtection = args.threatProtection;
36
39
  if (args.auditMetadata != null)
37
40
  body.auditMetadata = args.auditMetadata;
41
+ if (args.threadId != null) body.threadId = args.threadId;
42
+ if (args.mode != null) body.mode = args.mode;
43
+ if (args.exchange != null) body.exchange = args.exchange;
38
44
  return body;
39
45
  }
40
46
 
@@ -129,6 +135,22 @@ export async function getAgentSnapshot(
129
135
  }
130
136
  }
131
137
 
138
+ export async function getAgentThread(
139
+ http: HttpClient,
140
+ threadId: string,
141
+ options?: { includeData?: boolean }
142
+ ): Promise<AgentThreadResponse> {
143
+ try {
144
+ const query = options?.includeData ? "?includeData=true" : "";
145
+ const res = await http.get<AgentThreadResponse>(`/v2/agent/threads/${threadId}${query}`);
146
+ if (res.status !== 200) throwForBadResponse(res, "agent thread");
147
+ return res.data;
148
+ } catch (err: any) {
149
+ if (err?.isAxiosError) return normalizeAxiosError(err, "agent thread");
150
+ throw err;
151
+ }
152
+ }
153
+
132
154
  export async function cancelAgent(http: HttpClient, jobId: string): Promise<boolean> {
133
155
  try {
134
156
  const res = await http.delete<{ success: boolean }>(`/v2/agent/${jobId}`);
@@ -159,9 +159,9 @@ export class ResearchClient {
159
159
  appendParam(params, "query", options.query);
160
160
  appendParam(params, "k", options.k);
161
161
  try {
162
- const res = await this.http.get<PaperMetadataResponse | ReadPaperResponse>(
163
- withQuery(`${BASE}/papers/${encodeURIComponent(id)}`, params),
164
- );
162
+ const res = await this.http.get<
163
+ PaperMetadataResponse | ReadPaperResponse
164
+ >(withQuery(`${BASE}/papers/${encodeURIComponent(id)}`, params));
165
165
  if (res.status !== 200) throwForBadResponse(res, "get paper");
166
166
  return res.data;
167
167
  } catch (err) {
@@ -191,10 +191,7 @@ export class ResearchClient {
191
191
  appendParam(params, "anchor", options.anchor);
192
192
  try {
193
193
  const res = await this.http.get<SimilarPapersResponse>(
194
- withQuery(
195
- `${BASE}/papers/${encodeURIComponent(id)}/similar`,
196
- params,
197
- ),
194
+ withQuery(`${BASE}/papers/${encodeURIComponent(id)}/similar`, params),
198
195
  );
199
196
  if (res.status !== 200) throwForBadResponse(res, "find similar papers");
200
197
  return res.data;
@@ -204,7 +201,16 @@ export class ResearchClient {
204
201
  }
205
202
 
206
203
  /**
207
- * Search GitHub issue/PR history and repository readmes.
204
+ * Search the research index GitHub slice: issue/PR history and repository
205
+ * readmes.
206
+ *
207
+ * @deprecated Use `developerSearch()` on the client instead. This
208
+ * endpoint stops responding after 2026-11-03. The developer index searches
209
+ * GitHub issues, pull requests and readmes plus curated documentation
210
+ * sources, returns matched passages, and adds filters for repo, language,
211
+ * license and stars. It does not carry over the `scores` breakdown or the
212
+ * `resultType: "web"` fallback results. See
213
+ * https://docs.firecrawl.dev/features/developer.
208
214
  * @param query Search query.
209
215
  * @param options Optional `k`.
210
216
  */
@@ -88,9 +88,11 @@ export async function scrape(
88
88
  if (options) ensureValidScrapeOptions(options);
89
89
 
90
90
  // autoResume is SDK behavior, never part of the wire payload.
91
- const { autoResume, ...requestOptions } = options ?? {};
91
+ const { autoResume, domainTools, ...requestOptions } = options ?? {};
92
92
  const payload: Record<string, unknown> = { url: url.trim() };
93
93
  Object.assign(payload, requestOptions);
94
+ // Default off: omit entirely rather than send `false`.
95
+ if (domainTools) payload.domainTools = domainTools;
94
96
 
95
97
  // Per-request timeout. With auto-resume disabled this is exactly the
96
98
  // pre-existing behavior (explicit timeout +5s, else the client's own
@@ -27,6 +27,7 @@ function prepareSearchPayload(req: SearchRequest): Record<string, unknown> {
27
27
  const payload: Record<string, unknown> = {
28
28
  query: req.query,
29
29
  };
30
+ if (req.domainTools != null) payload.domainTools = req.domainTools;
30
31
  if (req.sources) payload.sources = req.sources;
31
32
  if (req.categories) payload.categories = req.categories;
32
33
  if (req.includeDomains) payload.includeDomains = req.includeDomains;
@@ -34,6 +35,7 @@ function prepareSearchPayload(req: SearchRequest): Record<string, unknown> {
34
35
  if (req.limit != null) payload.limit = req.limit;
35
36
  if (req.tbs != null) payload.tbs = req.tbs;
36
37
  if (req.location != null) payload.location = req.location;
38
+ if (req.country != null) payload.country = req.country;
37
39
  if (req.ignoreInvalidURLs != null)
38
40
  payload.ignoreInvalidURLs = req.ignoreInvalidURLs;
39
41
  if (req.timeout != null) payload.timeout = req.timeout;
@@ -86,6 +88,7 @@ export async function search(
86
88
  success: boolean;
87
89
  data?: Record<string, unknown>;
88
90
  error?: string;
91
+ warning?: string;
89
92
  }>(
90
93
  "/v2/search",
91
94
  payload,
@@ -98,10 +101,12 @@ export async function search(
98
101
  }
99
102
  const data = (res.data.data || {}) as Record<string, any>;
100
103
  const out: SearchData = {};
104
+ if (res.data.warning) out.warning = res.data.warning;
101
105
  if (data.web) out.web = transformArray<SearchResultWeb>(data.web);
102
106
  if (data.news) out.news = transformArray<SearchResultNews>(data.news);
103
107
  if (data.images)
104
108
  out.images = transformArray<SearchResultImages>(data.images);
109
+ if (data.tools) out.tools = data.tools;
105
110
  Object.defineProperty(out, "data", {
106
111
  get() {
107
112
  const parts: string[] = [];
@@ -109,9 +114,11 @@ export async function search(
109
114
  if (out.news?.length) parts.push(`.news (${out.news.length} results)`);
110
115
  if (out.images?.length)
111
116
  parts.push(`.images (${out.images.length} results)`);
117
+ if (out.tools?.length)
118
+ parts.push(`.tools (${out.tools.length} results)`);
112
119
  const available = parts.length
113
120
  ? parts.join(", ")
114
- : ".web, .news, or .images";
121
+ : ".web, .news, .images, or .tools";
115
122
  throw new Error(
116
123
  `SearchData has no '.data'. Results are grouped by source: ${available}`,
117
124
  );