Skip to content

Commit 14642b3

Browse files
committed
fix(firecrawl): complete v2 response and depth compatibility
1 parent 5bffff6 commit 14642b3

9 files changed

Lines changed: 143 additions & 17 deletions

File tree

‎apps/docs/content/docs/integrations/firecrawl.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ Crawl entire websites and extract structured content from all accessible pages
184184
| --------- | ---- | -------- | ----------- |
185185
| `url` | string | Yes | The website URL to crawl \(e.g., "https://example.com" or "https://docs.example.com/guide") |
186186
| `limit` | number | No | Maximum number of pages to crawl \(e.g., 50, 100, 500\). Default: 100 |
187-
| `maxDepth` | number | No | Maximum depth to crawl from the starting URL \(e.g., 1, 2, 3\). Controls how many levels deep to follow links |
187+
| `maxDepth` | number | No | Maximum URL path depth \(e.g., 1 allows /docs and /docs/index.html\). Independent of link discovery depth |
188188
| `formats` | json | No | Output formats for scraped content \(e.g., \["markdown"\], \["markdown", "html"\], \["markdown", "links"\]\) |
189189
| `prompt` | string | No | Natural-language crawl guidance supplied by existing configurations |
190190
| `excludePaths` | json | No | URL paths to exclude from crawling \(e.g., \["/blog/*", "/admin/*", "/*.pdf"\]\) |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { isPlainRecord } from '@sim/utils/object'
2+
import { describe, expect, it } from 'vitest'
3+
import { crawlTool } from '@/tools/firecrawl/crawl'
4+
5+
function crawlBody(options: {
6+
maxDepth: number
7+
maxDiscoveryDepth?: number
8+
includePaths?: string[]
9+
excludePaths?: string[]
10+
}) {
11+
if (typeof crawlTool.request.body !== 'function') throw new Error('Missing request formatter')
12+
const body = crawlTool.request.body({ apiKey: 'test', url: 'https://example.com', ...options })
13+
if (!isPlainRecord(body)) throw new Error('Expected a JSON request body')
14+
return body
15+
}
16+
17+
describe('Firecrawl legacy crawl depth', () => {
18+
it('bounds URL path segments without limiting discovery hops', () => {
19+
const body = crawlBody({ maxDepth: 1, maxDiscoveryDepth: 4 })
20+
const patterns = (body.includePaths as string[]).map((pattern) => new RegExp(pattern))
21+
const accepts = (path: string) => patterns.some((pattern) => pattern.test(path))
22+
23+
expect(body.maxDiscoveryDepth).toBe(4)
24+
expect(body.regexOnFullURL).toBe(false)
25+
expect(accepts('/')).toBe(true)
26+
expect(accepts('/docs')).toBe(true)
27+
expect(accepts('/docs/index.html')).toBe(true)
28+
expect(accepts('/docs/guide')).toBe(false)
29+
expect(accepts('/docs/guide/page')).toBe(false)
30+
})
31+
32+
it('keeps existing path filters while excluding paths beyond the legacy depth', () => {
33+
const body = crawlBody({ maxDepth: 1, includePaths: ['^/docs'], excludePaths: ['private'] })
34+
const patterns = (body.excludePaths as string[]).map((pattern) => new RegExp(pattern))
35+
const excludes = (path: string) => patterns.some((pattern) => pattern.test(path))
36+
37+
expect(body.includePaths).toEqual(['^/docs'])
38+
expect(excludes('/docs')).toBe(false)
39+
expect(excludes('/docs/guide')).toBe(true)
40+
expect(excludes('/private')).toBe(true)
41+
expect(body.maxDiscoveryDepth).toBeUndefined()
42+
})
43+
44+
it('honors a zero depth instead of dropping the limit', () => {
45+
const body = crawlBody({ maxDepth: 0 })
46+
const patterns = (body.includePaths as string[]).map((pattern) => new RegExp(pattern))
47+
expect(patterns.some((pattern) => pattern.test('/'))).toBe(true)
48+
expect(patterns.some((pattern) => pattern.test('/docs'))).toBe(false)
49+
})
50+
51+
it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY])(
52+
'rejects an invalid legacy depth: %s',
53+
(maxDepth) => {
54+
expect(() => crawlBody({ maxDepth })).toThrow(/maxDepth/)
55+
}
56+
)
57+
})

‎apps/sim/tools/firecrawl/crawl.ts‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
4141
required: false,
4242
visibility: 'user-or-llm',
4343
description:
44-
'Maximum depth to crawl from the starting URL (e.g., 1, 2, 3). Controls how many levels deep to follow links',
44+
'Maximum URL path depth (e.g., 1 allows /docs and /docs/index.html). Independent of link discovery depth',
4545
},
4646
formats: {
4747
type: 'json',
@@ -130,7 +130,7 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
130130
Authorization: `Bearer ${params.apiKey}`,
131131
}),
132132
body: (params) => {
133-
const body: Record<string, any> = {
133+
const body: Record<string, unknown> = {
134134
url: params.url,
135135
limit: Number(params.limit) || 100,
136136
scrapeOptions: params.scrapeOptions || {
@@ -140,8 +140,8 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
140140
}
141141

142142
if (params.prompt) body.prompt = params.prompt
143-
if (params.maxDepth) body.maxDiscoveryDepth = Number(params.maxDepth)
144-
if (params.maxDiscoveryDepth) body.maxDiscoveryDepth = Number(params.maxDiscoveryDepth)
143+
if (params.maxDiscoveryDepth != null)
144+
body.maxDiscoveryDepth = Number(params.maxDiscoveryDepth)
145145
if (params.sitemap) body.sitemap = params.sitemap
146146
if (typeof params.crawlEntireDomain === 'boolean')
147147
body.crawlEntireDomain = params.crawlEntireDomain
@@ -154,6 +154,23 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
154154
if (params.maxConcurrency) body.maxConcurrency = Number(params.maxConcurrency)
155155
if (params.excludePaths) body.excludePaths = params.excludePaths
156156
if (params.includePaths) body.includePaths = params.includePaths
157+
if (params.maxDepth != null) {
158+
const maxDepth = Number(params.maxDepth)
159+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) {
160+
throw new Error('maxDepth must be a non-negative integer')
161+
}
162+
163+
// v1 depth counted URL path segments; v2 discovery depth counts link hops.
164+
body.regexOnFullURL = false
165+
if (params.includePaths?.length) {
166+
body.excludePaths = [...(params.excludePaths ?? []), `^(/+[^/]+){${maxDepth + 1}}`]
167+
} else {
168+
body.includePaths = [
169+
`^(/+[^/]+){0,${maxDepth}}/*$`,
170+
`^(/+[^/]+){0,${maxDepth}}/+index\\.(html|php)/*$`,
171+
]
172+
}
173+
}
157174
if (params.webhook) body.webhook = params.webhook
158175
if (typeof params.zeroDataRetention === 'boolean')
159176
body.zeroDataRetention = params.zeroDataRetention
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { jsonResponse } from '@sim/testing'
2+
import { describe, expect, it } from 'vitest'
3+
import { mapTool } from '@/tools/firecrawl/map'
4+
5+
describe('Firecrawl v2 map results', () => {
6+
it('preserves URL strings for workflows when the provider returns link objects', async () => {
7+
if (!mapTool.transformResponse) throw new Error('Missing response transform')
8+
const result = await mapTool.transformResponse(
9+
jsonResponse({
10+
success: true,
11+
links: [
12+
{ url: 'https://example.com', title: 'Home', description: 'Home page' },
13+
{ url: 'https://example.com/docs', title: 'Docs' },
14+
],
15+
})
16+
)
17+
18+
expect(result.output.links).toEqual(['https://example.com', 'https://example.com/docs'])
19+
})
20+
})

‎apps/sim/tools/firecrawl/map.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const mapTool: ToolConfig<MapParams, MapResponse> = {
7979
Authorization: `Bearer ${params.apiKey}`,
8080
}),
8181
body: (params) => {
82-
const body: Record<string, any> = {
82+
const body: Record<string, unknown> = {
8383
url: params.url,
8484
}
8585

@@ -98,13 +98,13 @@ export const mapTool: ToolConfig<MapParams, MapResponse> = {
9898
},
9999

100100
transformResponse: async (response: Response) => {
101-
const data = await response.json()
101+
const data: { success: boolean; links?: { url: string }[] } = await response.json()
102102

103103
return {
104104
success: data.success,
105105
output: {
106106
success: data.success,
107-
links: data.links || [],
107+
links: data.links?.map((link) => link.url) ?? [],
108108
creditsUsed: 1,
109109
},
110110
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { jsonResponse } from '@sim/testing'
2+
import { describe, expect, it } from 'vitest'
3+
import { searchTool } from '@/tools/firecrawl/search'
4+
5+
describe('Firecrawl v2 search results', () => {
6+
it('preserves the workflow result array when the provider groups results by source', async () => {
7+
if (!searchTool.transformResponse) throw new Error('Missing response transform')
8+
const result = await searchTool.transformResponse(
9+
jsonResponse({
10+
success: true,
11+
data: {
12+
web: [{ url: 'https://example.com', title: 'Example', description: 'A web result' }],
13+
},
14+
creditsUsed: 2,
15+
})
16+
)
17+
18+
expect(result.output.data).toEqual([
19+
{ url: 'https://example.com', title: 'Example', description: 'A web result' },
20+
])
21+
expect(result.output.creditsUsed).toBe(2)
22+
})
23+
24+
it('returns an empty result array when the provider finds no web results', async () => {
25+
if (!searchTool.transformResponse) throw new Error('Missing response transform')
26+
const result = await searchTool.transformResponse(
27+
jsonResponse({ success: true, data: {}, creditsUsed: 0 })
28+
)
29+
30+
expect(result.output.data).toEqual([])
31+
})
32+
})

‎apps/sim/tools/firecrawl/search.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,11 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
5656
Authorization: `Bearer ${params.apiKey}`,
5757
}),
5858
body: (params) => {
59-
const body: Record<string, any> = {
59+
const body: Record<string, unknown> = {
6060
query: params.query,
61+
limit: params.limit ? Number(params.limit) : 5,
6162
}
6263

63-
// Add optional parameters if provided (truthy check filters empty strings, null, undefined)
64-
if (params.limit) body.limit = Number(params.limit)
6564
if (params.sources) body.sources = params.sources
6665
if (params.categories) body.categories = params.categories
6766
if (params.tbs) body.tbs = params.tbs
@@ -77,12 +76,15 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
7776
},
7877

7978
transformResponse: async (response: Response) => {
80-
const data = await response.json()
79+
const data: {
80+
data?: { web?: SearchResponse['output']['data'] }
81+
creditsUsed?: number
82+
} = await response.json()
8183

8284
return {
8385
success: true,
8486
output: {
85-
data: data.data,
87+
data: data.data?.web ?? [],
8688
creditsUsed: data.creditsUsed,
8789
},
8890
}

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎scripts/check-explicit-any.baseline.json‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,9 @@
444444
"apps/sim/tools/exa/utils.ts": 5,
445445
"apps/sim/tools/firecrawl/agent.ts": 2,
446446
"apps/sim/tools/firecrawl/batch-scrape.ts": 3,
447-
"apps/sim/tools/firecrawl/crawl.ts": 2,
447+
"apps/sim/tools/firecrawl/crawl.ts": 1,
448448
"apps/sim/tools/firecrawl/extract.ts": 3,
449-
"apps/sim/tools/firecrawl/map.ts": 1,
450449
"apps/sim/tools/firecrawl/scrape.ts": 1,
451-
"apps/sim/tools/firecrawl/search.ts": 1,
452450
"apps/sim/tools/firecrawl/types.ts": 8,
453451
"apps/sim/tools/function/types.ts": 1,
454452
"apps/sim/tools/github/add_assignees.ts": 3,

0 commit comments

Comments
 (0)