firecrawl 4.40.0 → 4.42.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.
@@ -161,4 +161,126 @@ describe("v2.agent threads unit", () => {
161
161
  "/v2/agent/threads/thread-1?includeData=true",
162
162
  );
163
163
  });
164
+ test("startAgent forwards exchange.onTermsRequired and a terms approve", async () => {
165
+ const post = okPost();
166
+ const exchange = {
167
+ onTermsRequired: "ask" as const,
168
+ approve: { approvalId: "0199aaaa-0000-7000-8000-000000000000" },
169
+ };
170
+
171
+ await startAgent({ post } as any, {
172
+ prompt: "Continue",
173
+ threadId: "thread-1",
174
+ exchange,
175
+ });
176
+
177
+ expect(post).toHaveBeenCalledWith("/v2/agent", {
178
+ prompt: "Continue",
179
+ threadId: "thread-1",
180
+ exchange,
181
+ });
182
+ });
183
+
184
+ test("startAgent forwards exchange.onTermsRequired skip", async () => {
185
+ const post = okPost();
186
+
187
+ await startAgent({ post } as any, {
188
+ prompt: "Find the key business contact at exa.ai",
189
+ exchange: { onTermsRequired: "skip" },
190
+ });
191
+
192
+ expect(post).toHaveBeenCalledWith("/v2/agent", {
193
+ prompt: "Find the key business contact at exa.ai",
194
+ exchange: { onTermsRequired: "skip" },
195
+ });
196
+ });
197
+
198
+ test("getAgentStatus parses skippedProviders, requiresAction and a terms approval", async () => {
199
+ const approvalId = "0199aaaa-0000-7000-8000-000000000000";
200
+ const get = jest.fn().mockResolvedValue({
201
+ status: 200,
202
+ data: {
203
+ success: true,
204
+ status: "completed",
205
+ data: null,
206
+ model: "spark-2",
207
+ expiresAt: "2026-09-02T00:00:00.000Z",
208
+ exchange: {
209
+ enabled: true,
210
+ onTermsRequired: "ask",
211
+ paidCalls: 0,
212
+ creditsUsed: null,
213
+ skippedProviders: [
214
+ {
215
+ provider: "apollo",
216
+ name: "Apollo",
217
+ capability: "people/search",
218
+ adds: "verified work emails",
219
+ reason: "terms_required",
220
+ version: "F-1.0.0",
221
+ termsUrl: "https://www.firecrawl.dev/app/alexandria/apollo",
222
+ },
223
+ ],
224
+ requiresAction: {
225
+ type: "accept_terms",
226
+ approvalId,
227
+ providers: [
228
+ {
229
+ provider: "apollo",
230
+ name: "Apollo",
231
+ version: "F-1.0.0",
232
+ digest: null,
233
+ url: "https://www.firecrawl.dev/app/alexandria/apollo",
234
+ show: {
235
+ provider: "firecrawl",
236
+ capability: "terms/show",
237
+ options: { provider: "apollo" },
238
+ },
239
+ accept: {
240
+ provider: "firecrawl",
241
+ capability: "terms/accept",
242
+ options: {
243
+ provider: "apollo",
244
+ version: "F-1.0.0",
245
+ digest: null,
246
+ confirmed: true,
247
+ },
248
+ },
249
+ },
250
+ ],
251
+ },
252
+ },
253
+ pendingApproval: {
254
+ id: approvalId,
255
+ kind: "terms",
256
+ reason: "Apollo could add verified work emails.",
257
+ calls: [],
258
+ terms: [
259
+ {
260
+ provider: "apollo",
261
+ name: "Apollo",
262
+ version: "F-1.0.0",
263
+ digest: null,
264
+ url: "https://www.firecrawl.dev/app/alexandria/apollo",
265
+ },
266
+ ],
267
+ resolution: null,
268
+ },
269
+ },
270
+ });
271
+
272
+ const status = await getAgentStatus({ get } as any, "agent-job");
273
+
274
+ expect(status.exchange!.onTermsRequired).toBe("ask");
275
+ expect(status.exchange!.skippedProviders![0].reason).toBe("terms_required");
276
+ const action = status.exchange!.requiresAction!;
277
+ expect(action.approvalId).toBe(approvalId);
278
+ expect(action.providers[0].accept.capability).toBe("terms/accept");
279
+ expect(action.providers[0].accept.options.digest).toBeNull();
280
+ const pending = status.pendingApproval!;
281
+ expect(pending.kind).toBe("terms");
282
+ if (pending.kind !== "terms") throw new Error("expected a terms approval");
283
+ expect(pending.calls).toEqual([]);
284
+ expect(pending.terms[0].provider).toBe("apollo");
285
+ });
164
286
  });
@@ -3,6 +3,29 @@ import { startExtract } from "../../../v2/methods/extract";
3
3
  import type { WebhookConfig } from "../../../v2/types";
4
4
 
5
5
  describe("v2.extract unit", () => {
6
+ test("omits discovery options from extract without mutating shared options", async () => {
7
+ const post = jest
8
+ .fn()
9
+ .mockResolvedValue({ status: 200, data: { id: "extract-job" } });
10
+ const scrapeOptions = {
11
+ toolDetail: "compact" as const,
12
+ domainTools: true,
13
+ onlyMainContent: true,
14
+ };
15
+ await startExtract({ post } as any, {
16
+ urls: ["https://example.com"],
17
+ scrapeOptions,
18
+ });
19
+ expect(post).toHaveBeenCalledWith("/v2/extract", {
20
+ urls: ["https://example.com"],
21
+ scrapeOptions: { onlyMainContent: true },
22
+ });
23
+ expect(scrapeOptions).toEqual({
24
+ toolDetail: "compact",
25
+ domainTools: true,
26
+ onlyMainContent: true,
27
+ });
28
+ });
6
29
  test("startExtract forwards string webhook in request payload", async () => {
7
30
  const post = jest.fn().mockResolvedValue({
8
31
  status: 200,
@@ -1,4 +1,12 @@
1
- import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from "@jest/globals";
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
2
10
  import { createServer, type Server } from "node:http";
3
11
  import { FirecrawlClient } from "../../../v2/client";
4
12
 
@@ -52,6 +60,13 @@ const next = {
52
60
  capability: "find-tools",
53
61
  options: { providers: ["particle"], level: "tools" },
54
62
  };
63
+ const compactTools = [tool, productionTool].map(
64
+ ({ provider, capability, description }) => ({
65
+ provider,
66
+ capability,
67
+ description,
68
+ }),
69
+ );
55
70
  let server: Server;
56
71
  let client: FirecrawlClient;
57
72
  const sent: Array<{ body: any; id: string | undefined }> = [];
@@ -76,10 +91,20 @@ beforeAll(async () => {
76
91
  warning: "Example warning",
77
92
  data: {
78
93
  web: [{ url: "https://podcasts.apple.com" }],
79
- tools: [tool, productionTool],
94
+ tools:
95
+ body.toolDetail === "compact"
96
+ ? compactTools
97
+ : [tool, productionTool],
80
98
  },
81
99
  }),
82
100
  );
101
+ if (req.url === "/v2/scrape" && body.url)
102
+ return res.end(
103
+ JSON.stringify({
104
+ success: true,
105
+ data: { markdown: "Example", tools: compactTools },
106
+ }),
107
+ );
83
108
  if (body.alexandria[0].provider === "retry" && attempts++ === 0) {
84
109
  res.statusCode = 502;
85
110
  return res.end("{}");
@@ -150,6 +175,26 @@ afterAll(async () => {
150
175
  });
151
176
 
152
177
  describe("Alexandria contracts and execution", () => {
178
+ test.each(["compact", "summary", "full"] as const)(
179
+ "forwards %s discovery detail",
180
+ async (toolDetail) => {
181
+ const result = await client.search("records", { toolDetail });
182
+ if (toolDetail === "compact") expect(result.tools).toEqual(compactTools);
183
+ expect(sent.at(-1)?.body).toMatchObject({ toolDetail });
184
+ },
185
+ );
186
+ test("URL scrape forwards compact detail and preserves tools", async () => {
187
+ const result = await client.scrape("https://example.com", {
188
+ domainTools: true,
189
+ toolDetail: "compact",
190
+ });
191
+ expect(sent.at(-1)?.body).toMatchObject({
192
+ url: "https://example.com",
193
+ domainTools: true,
194
+ toolDetail: "compact",
195
+ });
196
+ expect(result.tools).toEqual(compactTools);
197
+ });
153
198
  test("returns complete unified tools and warning beside web results", async () => {
154
199
  const result = await client.search("podcasts", {
155
200
  sources: ["web", { type: "alexandria" }],
@@ -226,15 +271,27 @@ describe("Alexandria contracts and execution", () => {
226
271
  });
227
272
  });
228
273
 
229
-
230
- test.each([[undefined, 80000], [1000, 31000], [100000, 80000]])(
274
+ test.each([
275
+ [undefined, 80000],
276
+ [1000, 31000],
277
+ [100000, 80000],
278
+ ])(
231
279
  "Alexandria timeout %s allows response delivery (%s ms)",
232
280
  async (timeout, timeoutMs) => {
233
- const http = { post: jest.fn(async () => ({ status: 200, data: {
234
- success: true, data: { alexandria: [], creditsCost: 0 },
235
- } })) };
281
+ const http = {
282
+ post: jest.fn(async () => ({
283
+ status: 200,
284
+ data: {
285
+ success: true,
286
+ data: { alexandria: [], creditsCost: 0 },
287
+ },
288
+ })),
289
+ };
236
290
  await scrapeAlexandria(http as any, [next], { timeout });
237
- expect(http.post).toHaveBeenCalledWith("/v2/scrape", expect.anything(),
238
- expect.objectContaining({ timeoutMs }));
291
+ expect(http.post).toHaveBeenCalledWith(
292
+ "/v2/scrape",
293
+ expect.anything(),
294
+ expect.objectContaining({ timeoutMs }),
295
+ );
239
296
  },
240
297
  );
@@ -40,7 +40,8 @@ function prepareExtractPayload(args: {
40
40
  body.threatProtection = args.threatProtection;
41
41
  if (args.scrapeOptions) {
42
42
  ensureValidScrapeOptions(args.scrapeOptions);
43
- body.scrapeOptions = args.scrapeOptions;
43
+ const { toolDetail, domainTools, ...scrapeOptions } = args.scrapeOptions;
44
+ body.scrapeOptions = scrapeOptions;
44
45
  }
45
46
  return body;
46
47
  }
@@ -1,5 +1,6 @@
1
1
  import { type MapData, type MapOptions, type SearchResultWeb } from "../types";
2
2
  import { HttpClient } from "../utils/httpClient";
3
+ import { agentHintMetadata } from "../utils/agentHints";
3
4
  import {
4
5
  throwForBadResponse,
5
6
  normalizeAxiosError,
@@ -65,7 +66,7 @@ export async function map(
65
66
  description: (item as any).description,
66
67
  });
67
68
  }
68
- return { id: res.data.id, links };
69
+ return { id: res.data.id, links, ...agentHintMetadata(res.data) };
69
70
  } catch (err: any) {
70
71
  if (err?.isAxiosError) return normalizeAxiosError(err, "map");
71
72
  throw err;
@@ -1,5 +1,6 @@
1
1
  import { type Document, type ParseFile, type ParseOptions } from "../types";
2
2
  import { HttpClient } from "../utils/httpClient";
3
+ import { agentHintMetadata } from "../utils/agentHints";
3
4
  import { ensureValidParseOptions } from "../utils/validation";
4
5
  import {
5
6
  throwForBadResponse,
@@ -88,7 +89,7 @@ export async function parse(
88
89
  if (res.status !== 200 || !res.data?.success) {
89
90
  throwForBadResponse(res, "parse");
90
91
  }
91
- return (res.data.data || {}) as Document;
92
+ return { ...res.data.data, ...agentHintMetadata(res.data) };
92
93
  } catch (err: any) {
93
94
  if (err?.isAxiosError) return normalizeAxiosError(err, "parse");
94
95
  throw err;
@@ -6,6 +6,7 @@ import {
6
6
  type ScrapeOptions,
7
7
  } from "../types";
8
8
  import { HttpClient } from "../utils/httpClient";
9
+ import { agentHintMetadata } from "../utils/agentHints";
9
10
  import { ensureValidScrapeOptions } from "../utils/validation";
10
11
  import {
11
12
  throwForBadResponse,
@@ -131,7 +132,7 @@ export async function scrape(
131
132
  if (res.status !== 200 || !res.data?.success) {
132
133
  throwForBadResponse(res, "scrape");
133
134
  }
134
- return (res.data.data || {}) as Document;
135
+ return { ...res.data.data, ...agentHintMetadata(res.data) };
135
136
  } catch (err: any) {
136
137
  const delayMs = autoResume !== false ? processingContinuesDelayMs(err) : undefined;
137
138
  if (
@@ -8,6 +8,7 @@ import {
8
8
  type SearchResultImages,
9
9
  } from "../types";
10
10
  import { HttpClient } from "../utils/httpClient";
11
+ import { agentHintMetadata } from "../utils/agentHints";
11
12
  import { ensureValidScrapeOptions } from "../utils/validation";
12
13
  import {
13
14
  throwForBadResponse,
@@ -27,6 +28,7 @@ function prepareSearchPayload(req: SearchRequest): Record<string, unknown> {
27
28
  const payload: Record<string, unknown> = {
28
29
  query: req.query,
29
30
  };
31
+ if (req.toolDetail != null) payload.toolDetail = req.toolDetail;
30
32
  if (req.domainTools != null) payload.domainTools = req.domainTools;
31
33
  if (req.sources) payload.sources = req.sources;
32
34
  if (req.categories) payload.categories = req.categories;
@@ -100,7 +102,7 @@ export async function search(
100
102
  throwForBadResponse(res, "search");
101
103
  }
102
104
  const data = (res.data.data || {}) as Record<string, any>;
103
- const out: SearchData = {};
105
+ const out: SearchData = { ...agentHintMetadata(res.data) };
104
106
  if (res.data.warning) out.warning = res.data.warning;
105
107
  if (data.web) out.web = transformArray<SearchResultWeb>(data.web);
106
108
  if (data.news) out.news = transformArray<SearchResultNews>(data.news);