@narumitw/pi-firecrawl 0.1.3
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/LICENSE +21 -0
- package/README.md +99 -0
- package/package.json +39 -0
- package/src/firecrawl.ts +297 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 narumiruna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pi-firecrawl
|
|
2
|
+
|
|
3
|
+
A public [pi](https://pi.dev) extension package that exposes [Firecrawl](https://www.firecrawl.dev/) web scraping, crawling, URL discovery, and search APIs as native pi tools.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@narumitw/pi-firecrawl
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Try without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
FIRECRAWL_API_KEY=fc-... pi -e npm:@narumitw/pi-firecrawl
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Try this package locally from the repository root:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
FIRECRAWL_API_KEY=fc-... pi -e ./extensions/pi-firecrawl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Set a Firecrawl API key before running pi:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export FIRECRAWL_API_KEY=fc-your-key
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional API endpoint override:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export FIRECRAWL_API_URL=https://api.firecrawl.dev/v1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`FIRECRAWL_BASE_URL` is also accepted for compatibility. The extension never logs or displays the API key.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
- `firecrawl_scrape` — scrape a single URL and return requested formats such as markdown, HTML, links, or JSON.
|
|
42
|
+
- `firecrawl_crawl` — start a site crawl job and return the Firecrawl job id.
|
|
43
|
+
- `firecrawl_crawl_status` — check a crawl job status and retrieve completed crawl data.
|
|
44
|
+
- `firecrawl_map` — discover URLs for a site.
|
|
45
|
+
- `firecrawl_search` — search the web through Firecrawl and optionally scrape result pages.
|
|
46
|
+
|
|
47
|
+
All tools fail with a clear configuration error when `FIRECRAWL_API_KEY` is missing.
|
|
48
|
+
|
|
49
|
+
## Command
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
/firecrawl
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Shows whether the extension sees an API key and which Firecrawl API URL it will call.
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
Scrape a page as markdown:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"url": "https://example.com",
|
|
64
|
+
"formats": ["markdown"]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Map a small site:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"url": "https://example.com",
|
|
73
|
+
"limit": 20
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Start a crawl with markdown extraction:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"url": "https://example.com",
|
|
82
|
+
"limit": 10,
|
|
83
|
+
"scrapeOptions": {
|
|
84
|
+
"formats": ["markdown"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Package layout
|
|
90
|
+
|
|
91
|
+
```txt
|
|
92
|
+
extensions/pi-firecrawl/
|
|
93
|
+
├── src/
|
|
94
|
+
│ └── firecrawl.ts
|
|
95
|
+
├── README.md
|
|
96
|
+
├── LICENSE
|
|
97
|
+
├── tsconfig.json
|
|
98
|
+
└── package.json
|
|
99
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@narumitw/pi-firecrawl",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Pi extension that exposes Firecrawl web scraping and crawling tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"firecrawl",
|
|
13
|
+
"scrape",
|
|
14
|
+
"crawl"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./src/firecrawl.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"check": "biome check . && npm run typecheck",
|
|
28
|
+
"format": "biome check --write .",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"typebox": "^1.1.37"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "2.4.14",
|
|
36
|
+
"@mariozechner/pi-coding-agent": "0.73.0",
|
|
37
|
+
"typescript": "6.0.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/firecrawl.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_API_URL = "https://api.firecrawl.dev/v1";
|
|
5
|
+
const STATUS_KEY = "firecrawl";
|
|
6
|
+
|
|
7
|
+
const StringArray = Type.Array(Type.String());
|
|
8
|
+
|
|
9
|
+
interface FirecrawlState {
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const state: FirecrawlState = {
|
|
14
|
+
apiUrl: normalizeApiUrl(process.env.FIRECRAWL_API_URL ?? process.env.FIRECRAWL_BASE_URL),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const scrapeTool = defineTool({
|
|
18
|
+
name: "firecrawl_scrape",
|
|
19
|
+
label: "Firecrawl: Scrape",
|
|
20
|
+
description: "Scrape a single URL through Firecrawl and return requested formats.",
|
|
21
|
+
promptSnippet: "Scrape a URL through Firecrawl",
|
|
22
|
+
promptGuidelines: [
|
|
23
|
+
"Use firecrawl_scrape when you need clean markdown, HTML, links, screenshots, or structured extraction for one URL.",
|
|
24
|
+
"If FIRECRAWL_API_KEY is missing, report the configuration error instead of retrying repeatedly.",
|
|
25
|
+
],
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
url: Type.String({ description: "URL to scrape." }),
|
|
28
|
+
formats: Type.Optional(
|
|
29
|
+
Type.Array(
|
|
30
|
+
Type.String({
|
|
31
|
+
description:
|
|
32
|
+
"Requested Firecrawl output format, such as markdown, html, rawHtml, links, screenshot, or json.",
|
|
33
|
+
}),
|
|
34
|
+
{ description: "Firecrawl output formats. Defaults to Firecrawl's API default." },
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
onlyMainContent: Type.Optional(
|
|
38
|
+
Type.Boolean({ description: "Return only the main page content when supported." }),
|
|
39
|
+
),
|
|
40
|
+
includeTags: Type.Optional(StringArray),
|
|
41
|
+
excludeTags: Type.Optional(StringArray),
|
|
42
|
+
waitFor: Type.Optional(Type.Number({ description: "Milliseconds to wait before scraping." })),
|
|
43
|
+
timeout: Type.Optional(
|
|
44
|
+
Type.Number({ description: "Firecrawl request timeout in milliseconds." }),
|
|
45
|
+
),
|
|
46
|
+
mobile: Type.Optional(Type.Boolean({ description: "Use a mobile user agent when supported." })),
|
|
47
|
+
skipTlsVerification: Type.Optional(
|
|
48
|
+
Type.Boolean({ description: "Skip TLS certificate verification when supported." }),
|
|
49
|
+
),
|
|
50
|
+
removeBase64Images: Type.Optional(
|
|
51
|
+
Type.Boolean({ description: "Remove base64 image data from the response when supported." }),
|
|
52
|
+
),
|
|
53
|
+
blockAds: Type.Optional(
|
|
54
|
+
Type.Boolean({ description: "Block ads while scraping when supported." }),
|
|
55
|
+
),
|
|
56
|
+
headers: Type.Optional(
|
|
57
|
+
Type.Record(Type.String(), Type.String(), {
|
|
58
|
+
description: "Additional HTTP headers Firecrawl should use while fetching the target URL.",
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
jsonOptions: Type.Optional(
|
|
62
|
+
Type.Any({ description: "Firecrawl jsonOptions for structured extraction." }),
|
|
63
|
+
),
|
|
64
|
+
actions: Type.Optional(
|
|
65
|
+
Type.Array(Type.Any(), {
|
|
66
|
+
description: "Firecrawl browser actions to perform before scraping.",
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
location: Type.Optional(Type.Any({ description: "Firecrawl location options." })),
|
|
70
|
+
}),
|
|
71
|
+
async execute(_toolCallId, params, signal) {
|
|
72
|
+
const payload = await firecrawlRequest("POST", "/scrape", cleanObject(params), signal);
|
|
73
|
+
return jsonResult(payload);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const crawlTool = defineTool({
|
|
78
|
+
name: "firecrawl_crawl",
|
|
79
|
+
label: "Firecrawl: Crawl",
|
|
80
|
+
description: "Start a Firecrawl crawl job for a website.",
|
|
81
|
+
promptSnippet: "Start a Firecrawl site crawl job",
|
|
82
|
+
parameters: Type.Object({
|
|
83
|
+
url: Type.String({ description: "Starting URL for the crawl." }),
|
|
84
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of pages to crawl." })),
|
|
85
|
+
maxDepth: Type.Optional(Type.Number({ description: "Maximum crawl depth when supported." })),
|
|
86
|
+
includePaths: Type.Optional(
|
|
87
|
+
Type.Array(Type.String(), { description: "URL path patterns to include." }),
|
|
88
|
+
),
|
|
89
|
+
excludePaths: Type.Optional(
|
|
90
|
+
Type.Array(Type.String(), { description: "URL path patterns to exclude." }),
|
|
91
|
+
),
|
|
92
|
+
allowBackwardLinks: Type.Optional(
|
|
93
|
+
Type.Boolean({ description: "Allow crawling backward links when supported." }),
|
|
94
|
+
),
|
|
95
|
+
allowExternalLinks: Type.Optional(
|
|
96
|
+
Type.Boolean({ description: "Allow crawling external links when supported." }),
|
|
97
|
+
),
|
|
98
|
+
ignoreSitemap: Type.Optional(Type.Boolean({ description: "Ignore sitemap discovery." })),
|
|
99
|
+
deduplicateSimilarURLs: Type.Optional(
|
|
100
|
+
Type.Boolean({ description: "Deduplicate similar URLs when supported." }),
|
|
101
|
+
),
|
|
102
|
+
scrapeOptions: Type.Optional(
|
|
103
|
+
Type.Any({ description: "Firecrawl scrapeOptions applied to crawled pages." }),
|
|
104
|
+
),
|
|
105
|
+
webhook: Type.Optional(Type.Any({ description: "Firecrawl webhook configuration." })),
|
|
106
|
+
}),
|
|
107
|
+
async execute(_toolCallId, params, signal) {
|
|
108
|
+
const payload = await firecrawlRequest("POST", "/crawl", cleanObject(params), signal);
|
|
109
|
+
return jsonResult(payload);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const crawlStatusTool = defineTool({
|
|
114
|
+
name: "firecrawl_crawl_status",
|
|
115
|
+
label: "Firecrawl: Crawl Status",
|
|
116
|
+
description: "Check a Firecrawl crawl job status and retrieve completed crawl data.",
|
|
117
|
+
promptSnippet: "Check a Firecrawl crawl job status",
|
|
118
|
+
parameters: Type.Object({
|
|
119
|
+
id: Type.String({ description: "Crawl job id returned by firecrawl_crawl." }),
|
|
120
|
+
}),
|
|
121
|
+
async execute(_toolCallId, params, signal) {
|
|
122
|
+
const payload = await firecrawlRequest(
|
|
123
|
+
"GET",
|
|
124
|
+
`/crawl/${encodeURIComponent(params.id)}`,
|
|
125
|
+
undefined,
|
|
126
|
+
signal,
|
|
127
|
+
);
|
|
128
|
+
return jsonResult(payload);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const mapTool = defineTool({
|
|
133
|
+
name: "firecrawl_map",
|
|
134
|
+
label: "Firecrawl: Map",
|
|
135
|
+
description: "Discover URLs for a site through Firecrawl's map endpoint.",
|
|
136
|
+
promptSnippet: "Map/discover URLs for a site through Firecrawl",
|
|
137
|
+
parameters: Type.Object({
|
|
138
|
+
url: Type.String({ description: "Website URL to map." }),
|
|
139
|
+
search: Type.Optional(
|
|
140
|
+
Type.String({ description: "Optional search term to filter discovered URLs." }),
|
|
141
|
+
),
|
|
142
|
+
ignoreSitemap: Type.Optional(Type.Boolean({ description: "Ignore sitemap discovery." })),
|
|
143
|
+
sitemapOnly: Type.Optional(
|
|
144
|
+
Type.Boolean({ description: "Only use sitemap URLs when supported." }),
|
|
145
|
+
),
|
|
146
|
+
includeSubdomains: Type.Optional(
|
|
147
|
+
Type.Boolean({ description: "Include subdomains when supported." }),
|
|
148
|
+
),
|
|
149
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of URLs to return." })),
|
|
150
|
+
}),
|
|
151
|
+
async execute(_toolCallId, params, signal) {
|
|
152
|
+
const payload = await firecrawlRequest("POST", "/map", cleanObject(params), signal);
|
|
153
|
+
return jsonResult(payload);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const searchTool = defineTool({
|
|
158
|
+
name: "firecrawl_search",
|
|
159
|
+
label: "Firecrawl: Search",
|
|
160
|
+
description: "Search the web through Firecrawl and optionally scrape search results.",
|
|
161
|
+
promptSnippet: "Search the web through Firecrawl",
|
|
162
|
+
parameters: Type.Object({
|
|
163
|
+
query: Type.String({ description: "Search query." }),
|
|
164
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of search results." })),
|
|
165
|
+
tbs: Type.Optional(
|
|
166
|
+
Type.String({ description: "Google-style time based search filter when supported." }),
|
|
167
|
+
),
|
|
168
|
+
location: Type.Optional(Type.String({ description: "Search location when supported." })),
|
|
169
|
+
scrapeOptions: Type.Optional(
|
|
170
|
+
Type.Any({ description: "Firecrawl scrapeOptions for search result pages." }),
|
|
171
|
+
),
|
|
172
|
+
}),
|
|
173
|
+
async execute(_toolCallId, params, signal) {
|
|
174
|
+
const payload = await firecrawlRequest("POST", "/search", cleanObject(params), signal);
|
|
175
|
+
return jsonResult(payload);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export default function firecrawl(pi: ExtensionAPI) {
|
|
180
|
+
pi.registerTool(scrapeTool);
|
|
181
|
+
pi.registerTool(crawlTool);
|
|
182
|
+
pi.registerTool(crawlStatusTool);
|
|
183
|
+
pi.registerTool(mapTool);
|
|
184
|
+
pi.registerTool(searchTool);
|
|
185
|
+
|
|
186
|
+
pi.registerCommand("firecrawl", {
|
|
187
|
+
description: "Show Firecrawl extension configuration status",
|
|
188
|
+
handler: async (_args, ctx) => {
|
|
189
|
+
ctx.ui.notify(buildStatusMessage(), hasApiKey() ? "info" : "warning");
|
|
190
|
+
updateStatus(ctx);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.on("session_start", (_event, ctx) => {
|
|
195
|
+
updateStatus(ctx);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
199
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function firecrawlRequest(
|
|
204
|
+
method: "GET" | "POST",
|
|
205
|
+
path: string,
|
|
206
|
+
body: unknown,
|
|
207
|
+
signal: AbortSignal | undefined,
|
|
208
|
+
) {
|
|
209
|
+
const apiKey = getApiKey();
|
|
210
|
+
const response = await fetch(`${state.apiUrl}${path}`, {
|
|
211
|
+
method,
|
|
212
|
+
headers: {
|
|
213
|
+
Authorization: `Bearer ${apiKey}`,
|
|
214
|
+
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
215
|
+
},
|
|
216
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
217
|
+
signal,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const responseText = await response.text();
|
|
221
|
+
const payload = parseResponseBody(responseText);
|
|
222
|
+
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Firecrawl ${method} ${path} returned ${response.status} ${response.statusText}: ${formatPayload(payload)}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return payload;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getApiKey() {
|
|
233
|
+
const apiKey = process.env.FIRECRAWL_API_KEY?.trim();
|
|
234
|
+
if (!apiKey) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"FIRECRAWL_API_KEY is required for pi-firecrawl. Set it in the environment before running pi.",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return apiKey;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasApiKey() {
|
|
244
|
+
return Boolean(process.env.FIRECRAWL_API_KEY?.trim());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeApiUrl(apiUrl: string | undefined) {
|
|
248
|
+
return (apiUrl?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseResponseBody(responseText: string) {
|
|
252
|
+
if (!responseText) return null;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(responseText) as unknown;
|
|
256
|
+
} catch {
|
|
257
|
+
return responseText;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatPayload(payload: unknown) {
|
|
262
|
+
if (typeof payload === "string") return payload;
|
|
263
|
+
return JSON.stringify(payload);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function jsonResult(payload: unknown) {
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
269
|
+
details: payload,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function updateStatus(ctx: {
|
|
274
|
+
ui: { setStatus: (key: string, value: string | undefined) => void };
|
|
275
|
+
}) {
|
|
276
|
+
ctx.ui.setStatus(STATUS_KEY, hasApiKey() ? "firecrawl: configured" : "firecrawl: missing key");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildStatusMessage() {
|
|
280
|
+
return hasApiKey()
|
|
281
|
+
? `Firecrawl configured: ${state.apiUrl} (API key present).`
|
|
282
|
+
: `Firecrawl missing FIRECRAWL_API_KEY. API URL: ${state.apiUrl}.`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function cleanObject<T>(value: T): T {
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
return value.map((item) => cleanObject(item)) as T;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!value || typeof value !== "object") return value;
|
|
291
|
+
|
|
292
|
+
const entries = Object.entries(value)
|
|
293
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
294
|
+
.map(([entryKey, entryValue]) => [entryKey, cleanObject(entryValue)]);
|
|
295
|
+
|
|
296
|
+
return Object.fromEntries(entries) as T;
|
|
297
|
+
}
|