@narumitw/pi-firecrawl 0.1.33 → 0.1.35
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/README.md +41 -2
- package/package.json +1 -1
- package/src/firecrawl.ts +523 -16
package/README.md
CHANGED
|
@@ -15,7 +15,10 @@ Use it to give your AI coding agent reliable web research capabilities for docum
|
|
|
15
15
|
- Search the web and optionally scrape search result pages.
|
|
16
16
|
- Supports Firecrawl API endpoint overrides.
|
|
17
17
|
- Shows statusline activity only while Firecrawl tools are running.
|
|
18
|
-
-
|
|
18
|
+
- Provides a `/firecrawl` menu with configuration help and tool controls.
|
|
19
|
+
- Provides a Plan-mode-style selector for choosing individual Firecrawl tools.
|
|
20
|
+
- Persists the selected Firecrawl tools across Pi restarts.
|
|
21
|
+
- Never logs, displays, or stores your Firecrawl API key.
|
|
19
22
|
|
|
20
23
|
## 📦 Install
|
|
21
24
|
|
|
@@ -67,7 +70,43 @@ All tools fail with a clear configuration error when `FIRECRAWL_API_KEY` is miss
|
|
|
67
70
|
/firecrawl
|
|
68
71
|
```
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
Opens a menu with configuration quick start, command usage, tool status, controls for enabling
|
|
74
|
+
or disabling all Firecrawl tools, and a selector for choosing individual tools.
|
|
75
|
+
|
|
76
|
+
Direct subcommands are also available:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
/firecrawl help
|
|
80
|
+
/firecrawl config
|
|
81
|
+
/firecrawl quickstart
|
|
82
|
+
/firecrawl status
|
|
83
|
+
/firecrawl tools
|
|
84
|
+
/firecrawl toggle
|
|
85
|
+
/firecrawl enable
|
|
86
|
+
/firecrawl disable
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `help` shows command usage.
|
|
90
|
+
- `config` shows API-key presence and API URL without displaying the API key value.
|
|
91
|
+
- `quickstart` is an alias for `config`.
|
|
92
|
+
- `status` shows runtime tool state, persisted selection, settings file path, API-key presence,
|
|
93
|
+
API URL, and active non-Firecrawl tool count.
|
|
94
|
+
- `tools` opens a Plan-mode-style selector for choosing individual `firecrawl_*` tools.
|
|
95
|
+
- `toggle` is an alias for `tools`.
|
|
96
|
+
- `enable` enables all `firecrawl_*` tools for future turns.
|
|
97
|
+
- `disable` disables all `firecrawl_*` tools for future turns. The slash command remains
|
|
98
|
+
available.
|
|
99
|
+
|
|
100
|
+
The selected tool names are saved to:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-firecrawl-settings.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
When the file is missing or invalid, the extension preserves Pi's current active-tool policy
|
|
107
|
+
instead of enabling tools by itself. A valid saved selection is restored on Pi startup and
|
|
108
|
+
`/reload`. The settings file stores only tool names and a timestamp; it never stores
|
|
109
|
+
`FIRECRAWL_API_KEY`, request headers, or other secrets.
|
|
71
110
|
|
|
72
111
|
## 🚀 Examples
|
|
73
112
|
|
package/package.json
CHANGED
package/src/firecrawl.ts
CHANGED
|
@@ -1,15 +1,82 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
defineTool,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionCommandContext,
|
|
8
|
+
} from "@mariozechner/pi-coding-agent";
|
|
2
9
|
import { Type } from "typebox";
|
|
3
10
|
|
|
4
11
|
const DEFAULT_API_URL = "https://api.firecrawl.dev/v1";
|
|
5
12
|
const STATUS_KEY = "firecrawl";
|
|
13
|
+
const SETTINGS_FILE = join(
|
|
14
|
+
process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"),
|
|
15
|
+
"pi-firecrawl-settings.json",
|
|
16
|
+
);
|
|
17
|
+
const FIRECRAWL_TOOL_NAMES = [
|
|
18
|
+
"firecrawl_scrape",
|
|
19
|
+
"firecrawl_crawl",
|
|
20
|
+
"firecrawl_crawl_status",
|
|
21
|
+
"firecrawl_map",
|
|
22
|
+
"firecrawl_search",
|
|
23
|
+
] as const;
|
|
24
|
+
const COMMAND_COMPLETIONS = [
|
|
25
|
+
{ value: "help", label: "Show command usage" },
|
|
26
|
+
{ value: "config", label: "Show configuration quick start" },
|
|
27
|
+
{ value: "quickstart", label: "Show configuration quick start" },
|
|
28
|
+
{ value: "status", label: "Show tool and settings status" },
|
|
29
|
+
{ value: "tools", label: "Select Firecrawl tools" },
|
|
30
|
+
{ value: "toggle", label: "Select Firecrawl tools" },
|
|
31
|
+
{ value: "enable", label: "Enable all Firecrawl tools" },
|
|
32
|
+
{ value: "disable", label: "Disable all Firecrawl tools" },
|
|
33
|
+
];
|
|
34
|
+
const MENU_OPTIONS = {
|
|
35
|
+
config: "Configuration quick start",
|
|
36
|
+
help: "Command usage guide",
|
|
37
|
+
status: "Show tool status",
|
|
38
|
+
tools: "Select Firecrawl tools",
|
|
39
|
+
enable: "Enable all Firecrawl tools",
|
|
40
|
+
disable: "Disable all Firecrawl tools",
|
|
41
|
+
} as const;
|
|
42
|
+
const TOOL_SELECTOR_DONE = "Done";
|
|
43
|
+
const TOOL_SELECTOR_ENABLE_ALL = "Enable all Firecrawl tools";
|
|
44
|
+
const TOOL_SELECTOR_DISABLE_ALL = "Disable all Firecrawl tools";
|
|
6
45
|
|
|
7
46
|
const StringArray = Type.Array(Type.String());
|
|
8
47
|
|
|
48
|
+
type FirecrawlToolName = (typeof FIRECRAWL_TOOL_NAMES)[number];
|
|
49
|
+
type ToolRuntimeStatus = "enabled" | "disabled" | "partial";
|
|
50
|
+
type CommandAction =
|
|
51
|
+
| "menu"
|
|
52
|
+
| "help"
|
|
53
|
+
| "config"
|
|
54
|
+
| "quickstart"
|
|
55
|
+
| "status"
|
|
56
|
+
| "tools"
|
|
57
|
+
| "enable"
|
|
58
|
+
| "disable";
|
|
59
|
+
type CommandContext = ExtensionCommandContext;
|
|
60
|
+
type ToolSelectorAction = "enableAll" | "disableAll" | "done";
|
|
61
|
+
type ToolSelectorRow =
|
|
62
|
+
| { kind: "tool"; toolName: FirecrawlToolName }
|
|
63
|
+
| { kind: "action"; action: ToolSelectorAction; label: string };
|
|
64
|
+
|
|
9
65
|
interface FirecrawlState {
|
|
10
66
|
apiUrl: string;
|
|
11
67
|
}
|
|
12
68
|
|
|
69
|
+
interface FirecrawlSettings {
|
|
70
|
+
tools: FirecrawlToolName[];
|
|
71
|
+
updatedAt: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ToolStatusSummary {
|
|
75
|
+
runtimeStatus: ToolRuntimeStatus;
|
|
76
|
+
activeFirecrawlToolCount: number;
|
|
77
|
+
activeNonFirecrawlToolCount: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
13
80
|
interface StatusContext {
|
|
14
81
|
ui: { setStatus: (key: string, value: string | undefined) => void };
|
|
15
82
|
}
|
|
@@ -19,7 +86,7 @@ const state: FirecrawlState = {
|
|
|
19
86
|
};
|
|
20
87
|
|
|
21
88
|
const scrapeTool = defineTool({
|
|
22
|
-
name:
|
|
89
|
+
name: FIRECRAWL_TOOL_NAMES[0],
|
|
23
90
|
label: "Firecrawl: Scrape",
|
|
24
91
|
description: "Scrape a single URL through Firecrawl and return requested formats.",
|
|
25
92
|
promptSnippet: "Scrape a URL through Firecrawl",
|
|
@@ -81,7 +148,7 @@ const scrapeTool = defineTool({
|
|
|
81
148
|
});
|
|
82
149
|
|
|
83
150
|
const crawlTool = defineTool({
|
|
84
|
-
name:
|
|
151
|
+
name: FIRECRAWL_TOOL_NAMES[1],
|
|
85
152
|
label: "Firecrawl: Crawl",
|
|
86
153
|
description: "Start a Firecrawl crawl job for a website.",
|
|
87
154
|
promptSnippet: "Start a Firecrawl site crawl job",
|
|
@@ -119,7 +186,7 @@ const crawlTool = defineTool({
|
|
|
119
186
|
});
|
|
120
187
|
|
|
121
188
|
const crawlStatusTool = defineTool({
|
|
122
|
-
name:
|
|
189
|
+
name: FIRECRAWL_TOOL_NAMES[2],
|
|
123
190
|
label: "Firecrawl: Crawl Status",
|
|
124
191
|
description: "Check a Firecrawl crawl job status and retrieve completed crawl data.",
|
|
125
192
|
promptSnippet: "Check a Firecrawl crawl job status",
|
|
@@ -140,7 +207,7 @@ const crawlStatusTool = defineTool({
|
|
|
140
207
|
});
|
|
141
208
|
|
|
142
209
|
const mapTool = defineTool({
|
|
143
|
-
name:
|
|
210
|
+
name: FIRECRAWL_TOOL_NAMES[3],
|
|
144
211
|
label: "Firecrawl: Map",
|
|
145
212
|
description: "Discover URLs for a site through Firecrawl's map endpoint.",
|
|
146
213
|
promptSnippet: "Map/discover URLs for a site through Firecrawl",
|
|
@@ -167,7 +234,7 @@ const mapTool = defineTool({
|
|
|
167
234
|
});
|
|
168
235
|
|
|
169
236
|
const searchTool = defineTool({
|
|
170
|
-
name:
|
|
237
|
+
name: FIRECRAWL_TOOL_NAMES[4],
|
|
171
238
|
label: "Firecrawl: Search",
|
|
172
239
|
description: "Search the web through Firecrawl and optionally scrape search results.",
|
|
173
240
|
promptSnippet: "Search the web through Firecrawl",
|
|
@@ -198,14 +265,23 @@ export default function firecrawl(pi: ExtensionAPI) {
|
|
|
198
265
|
pi.registerTool(searchTool);
|
|
199
266
|
|
|
200
267
|
pi.registerCommand("firecrawl", {
|
|
201
|
-
description: "
|
|
202
|
-
|
|
203
|
-
|
|
268
|
+
description: "Open Firecrawl help and tool controls",
|
|
269
|
+
getArgumentCompletions: (prefix) => commandCompletions(prefix),
|
|
270
|
+
handler: async (args, ctx) => {
|
|
271
|
+
await handleFirecrawlCommand(pi, args, ctx);
|
|
204
272
|
},
|
|
205
273
|
});
|
|
206
274
|
|
|
207
|
-
pi.on("session_start", (_event, ctx) => {
|
|
275
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
208
276
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
277
|
+
const settings = await loadSettings();
|
|
278
|
+
if (settings.kind === "loaded") {
|
|
279
|
+
applyFirecrawlTools(pi, settings.settings.tools);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (settings.kind === "invalid") {
|
|
283
|
+
ctx.ui.notify(`Firecrawl settings ignored: ${settings.reason}`, "warning");
|
|
284
|
+
}
|
|
209
285
|
});
|
|
210
286
|
|
|
211
287
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -213,6 +289,443 @@ export default function firecrawl(pi: ExtensionAPI) {
|
|
|
213
289
|
});
|
|
214
290
|
}
|
|
215
291
|
|
|
292
|
+
async function handleFirecrawlCommand(pi: ExtensionAPI, args: string, ctx: CommandContext) {
|
|
293
|
+
const command = parseCommand(args);
|
|
294
|
+
switch (command) {
|
|
295
|
+
case "menu":
|
|
296
|
+
await showMenu(pi, ctx);
|
|
297
|
+
return;
|
|
298
|
+
case "help":
|
|
299
|
+
ctx.ui.notify(buildCommandGuide(), "info");
|
|
300
|
+
return;
|
|
301
|
+
case "config":
|
|
302
|
+
case "quickstart":
|
|
303
|
+
ctx.ui.notify(buildConfigMessage(), hasApiKey() ? "info" : "warning");
|
|
304
|
+
return;
|
|
305
|
+
case "status":
|
|
306
|
+
ctx.ui.notify(await buildStatusMessage(pi), hasApiKey() ? "info" : "warning");
|
|
307
|
+
return;
|
|
308
|
+
case "tools":
|
|
309
|
+
await showToolSelector(pi, ctx);
|
|
310
|
+
return;
|
|
311
|
+
case "enable":
|
|
312
|
+
await updateFirecrawlTools(pi, ctx, allFirecrawlTools(), "enabled all");
|
|
313
|
+
return;
|
|
314
|
+
case "disable":
|
|
315
|
+
await updateFirecrawlTools(pi, ctx, [], "disabled all");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
ctx.ui.notify(`Unknown /firecrawl command: ${args.trim()}\n\n${buildCommandGuide()}`, "warning");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function showMenu(pi: ExtensionAPI, ctx: CommandContext) {
|
|
323
|
+
if (!ctx.hasUI) {
|
|
324
|
+
ctx.ui.notify(`${buildCommandGuide()}\n\n${await buildStatusMessage(pi)}`, hasApiKey() ? "info" : "warning");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const choice = await ctx.ui.select("Firecrawl", Object.values(MENU_OPTIONS));
|
|
329
|
+
switch (choice) {
|
|
330
|
+
case MENU_OPTIONS.config:
|
|
331
|
+
ctx.ui.notify(buildConfigMessage(), hasApiKey() ? "info" : "warning");
|
|
332
|
+
return;
|
|
333
|
+
case MENU_OPTIONS.help:
|
|
334
|
+
ctx.ui.notify(buildCommandGuide(), "info");
|
|
335
|
+
return;
|
|
336
|
+
case MENU_OPTIONS.status:
|
|
337
|
+
ctx.ui.notify(await buildStatusMessage(pi), hasApiKey() ? "info" : "warning");
|
|
338
|
+
return;
|
|
339
|
+
case MENU_OPTIONS.tools:
|
|
340
|
+
await showToolSelector(pi, ctx);
|
|
341
|
+
return;
|
|
342
|
+
case MENU_OPTIONS.enable:
|
|
343
|
+
await updateFirecrawlTools(pi, ctx, allFirecrawlTools(), "enabled all");
|
|
344
|
+
return;
|
|
345
|
+
case MENU_OPTIONS.disable:
|
|
346
|
+
await updateFirecrawlTools(pi, ctx, [], "disabled all");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseCommand(args: string): CommandAction | "unknown" {
|
|
352
|
+
const command = args.trim().toLowerCase();
|
|
353
|
+
if (!command) return "menu";
|
|
354
|
+
if (command === "help") return "help";
|
|
355
|
+
if (command === "config") return "config";
|
|
356
|
+
if (command === "quickstart") return "quickstart";
|
|
357
|
+
if (command === "status") return "status";
|
|
358
|
+
if (command === "tools" || command === "select" || command === "toggle") return "tools";
|
|
359
|
+
if (command === "enable" || command === "on") return "enable";
|
|
360
|
+
if (command === "disable" || command === "off") return "disable";
|
|
361
|
+
return "unknown";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function commandCompletions(prefix: string) {
|
|
365
|
+
const normalized = prefix.trim().toLowerCase();
|
|
366
|
+
if (normalized.includes(" ")) return null;
|
|
367
|
+
|
|
368
|
+
const matches = COMMAND_COMPLETIONS.filter((completion) =>
|
|
369
|
+
completion.value.startsWith(normalized),
|
|
370
|
+
);
|
|
371
|
+
return matches.length > 0 ? matches : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function showToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
|
|
375
|
+
if (!ctx.hasUI) {
|
|
376
|
+
ctx.ui.notify(
|
|
377
|
+
`Firecrawl tool selection needs an interactive UI.\n\n${await buildStatusMessage(pi)}`,
|
|
378
|
+
hasApiKey() ? "info" : "warning",
|
|
379
|
+
);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let selectedTools = new Set<FirecrawlToolName>(getActiveFirecrawlTools(pi));
|
|
384
|
+
let persistQueue = Promise.resolve();
|
|
385
|
+
const commitSelectedTools = () => {
|
|
386
|
+
const nextSelectedTools = orderedFirecrawlTools(selectedTools);
|
|
387
|
+
applyFirecrawlTools(pi, nextSelectedTools);
|
|
388
|
+
persistQueue = persistQueue.then(() => persistSettings(ctx, nextSelectedTools));
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const customResult = await ctx.ui.custom<"closed" | undefined>(
|
|
392
|
+
(tui, theme, keybindings, done) => {
|
|
393
|
+
const rows = firecrawlToolSelectorRows();
|
|
394
|
+
let selectedIndex = 0;
|
|
395
|
+
const moveSelection = (delta: number) => {
|
|
396
|
+
selectedIndex = (selectedIndex + delta + rows.length) % rows.length;
|
|
397
|
+
};
|
|
398
|
+
const activateSelectedRow = () => {
|
|
399
|
+
const row = rows[selectedIndex];
|
|
400
|
+
if (!row) return;
|
|
401
|
+
|
|
402
|
+
if (row.kind === "tool") {
|
|
403
|
+
if (selectedTools.has(row.toolName)) selectedTools.delete(row.toolName);
|
|
404
|
+
else selectedTools.add(row.toolName);
|
|
405
|
+
commitSelectedTools();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (row.action === "enableAll") {
|
|
410
|
+
selectedTools = new Set(allFirecrawlTools());
|
|
411
|
+
commitSelectedTools();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (row.action === "disableAll") {
|
|
415
|
+
selectedTools = new Set();
|
|
416
|
+
commitSelectedTools();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
done("closed");
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
invalidate() {},
|
|
425
|
+
render() {
|
|
426
|
+
return [
|
|
427
|
+
theme.fg("accent", theme.bold(toolSelectorTitle(selectedTools))),
|
|
428
|
+
"",
|
|
429
|
+
...rows.map((row, index) => {
|
|
430
|
+
const label = formatToolSelectorRow(row, selectedTools);
|
|
431
|
+
if (index === selectedIndex) {
|
|
432
|
+
return `${theme.fg("accent", "›")} ${theme.fg("accent", label)}`;
|
|
433
|
+
}
|
|
434
|
+
return ` ${label}`;
|
|
435
|
+
}),
|
|
436
|
+
"",
|
|
437
|
+
theme.fg("dim", "↑↓ navigate • Enter/Space toggle • Esc close"),
|
|
438
|
+
];
|
|
439
|
+
},
|
|
440
|
+
handleInput(data: string) {
|
|
441
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
442
|
+
moveSelection(-1);
|
|
443
|
+
tui.requestRender();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (keybindings.matches(data, "tui.select.down")) {
|
|
447
|
+
moveSelection(1);
|
|
448
|
+
tui.requestRender();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
452
|
+
selectedIndex = 0;
|
|
453
|
+
tui.requestRender();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
457
|
+
selectedIndex = rows.length - 1;
|
|
458
|
+
tui.requestRender();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
|
|
462
|
+
activateSelectedRow();
|
|
463
|
+
tui.requestRender();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
467
|
+
done("closed");
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (customResult !== "closed") {
|
|
475
|
+
await showDialogToolSelector(pi, ctx);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
await persistQueue;
|
|
480
|
+
ctx.ui.notify(await buildStatusMessage(pi), hasApiKey() ? "info" : "warning");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function showDialogToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
|
|
484
|
+
let selectedTools = new Set<FirecrawlToolName>(getActiveFirecrawlTools(pi));
|
|
485
|
+
while (true) {
|
|
486
|
+
const rows = firecrawlToolSelectorRows();
|
|
487
|
+
const choices = rows.map((row) => formatToolSelectorRow(row, selectedTools));
|
|
488
|
+
const choice = await ctx.ui.select(toolSelectorTitle(selectedTools), choices);
|
|
489
|
+
if (!choice) break;
|
|
490
|
+
|
|
491
|
+
const row = rows[choices.indexOf(choice)];
|
|
492
|
+
if (!row) continue;
|
|
493
|
+
if (row.kind === "action" && row.action === "done") break;
|
|
494
|
+
|
|
495
|
+
if (row.kind === "tool") {
|
|
496
|
+
if (selectedTools.has(row.toolName)) selectedTools.delete(row.toolName);
|
|
497
|
+
else selectedTools.add(row.toolName);
|
|
498
|
+
} else if (row.action === "enableAll") {
|
|
499
|
+
selectedTools = new Set(allFirecrawlTools());
|
|
500
|
+
} else if (row.action === "disableAll") {
|
|
501
|
+
selectedTools = new Set();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
await setSelectedFirecrawlTools(pi, ctx, orderedFirecrawlTools(selectedTools));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
ctx.ui.notify(await buildStatusMessage(pi), hasApiKey() ? "info" : "warning");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function updateFirecrawlTools(
|
|
511
|
+
pi: ExtensionAPI,
|
|
512
|
+
ctx: CommandContext,
|
|
513
|
+
selectedTools: readonly FirecrawlToolName[],
|
|
514
|
+
action: string,
|
|
515
|
+
) {
|
|
516
|
+
await setSelectedFirecrawlTools(pi, ctx, selectedTools);
|
|
517
|
+
ctx.ui.notify(
|
|
518
|
+
`Firecrawl tools ${action}.\n\n${await buildStatusMessage(pi)}`,
|
|
519
|
+
hasApiKey() ? "info" : "warning",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function setSelectedFirecrawlTools(
|
|
524
|
+
pi: ExtensionAPI,
|
|
525
|
+
ctx: CommandContext,
|
|
526
|
+
selectedTools: readonly FirecrawlToolName[],
|
|
527
|
+
) {
|
|
528
|
+
applyFirecrawlTools(pi, selectedTools);
|
|
529
|
+
await persistSettings(ctx, selectedTools);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function applyFirecrawlTools(pi: ExtensionAPI, selectedTools: readonly FirecrawlToolName[]) {
|
|
533
|
+
const activeToolNames = pi.getActiveTools();
|
|
534
|
+
const firecrawlToolNames = new Set<string>(FIRECRAWL_TOOL_NAMES);
|
|
535
|
+
const activeNonFirecrawlToolNames = activeToolNames.filter(
|
|
536
|
+
(name) => !firecrawlToolNames.has(name),
|
|
537
|
+
);
|
|
538
|
+
pi.setActiveTools(unique([...activeNonFirecrawlToolNames, ...selectedTools]));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
542
|
+
const firecrawlToolNames = new Set<string>(FIRECRAWL_TOOL_NAMES);
|
|
543
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
544
|
+
const activeFirecrawlToolCount = FIRECRAWL_TOOL_NAMES.filter((name) =>
|
|
545
|
+
activeToolNames.has(name),
|
|
546
|
+
).length;
|
|
547
|
+
const activeNonFirecrawlToolCount = Array.from(activeToolNames).filter(
|
|
548
|
+
(name) => !firecrawlToolNames.has(name),
|
|
549
|
+
).length;
|
|
550
|
+
const runtimeStatus =
|
|
551
|
+
activeFirecrawlToolCount === FIRECRAWL_TOOL_NAMES.length
|
|
552
|
+
? "enabled"
|
|
553
|
+
: activeFirecrawlToolCount === 0
|
|
554
|
+
? "disabled"
|
|
555
|
+
: "partial";
|
|
556
|
+
|
|
557
|
+
return { runtimeStatus, activeFirecrawlToolCount, activeNonFirecrawlToolCount };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function buildStatusMessage(pi: ExtensionAPI) {
|
|
561
|
+
const summary = getToolStatusSummary(pi);
|
|
562
|
+
const persistedSetting = await persistedSettingLabel();
|
|
563
|
+
return [
|
|
564
|
+
`Firecrawl tools: ${formatRuntimeStatus(summary)}`,
|
|
565
|
+
`Persisted selection: ${persistedSetting}`,
|
|
566
|
+
`Settings file: ${SETTINGS_FILE}`,
|
|
567
|
+
`Other active tools preserved: ${summary.activeNonFirecrawlToolCount}`,
|
|
568
|
+
`API key: ${hasApiKey() ? "present" : "missing"} (FIRECRAWL_API_KEY)`,
|
|
569
|
+
`API URL: ${state.apiUrl}`,
|
|
570
|
+
].join("\n");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function buildConfigMessage() {
|
|
574
|
+
return [
|
|
575
|
+
"Firecrawl configuration:",
|
|
576
|
+
`API key: ${hasApiKey() ? "present" : "missing"} (FIRECRAWL_API_KEY)`,
|
|
577
|
+
`API URL: ${state.apiUrl}`,
|
|
578
|
+
"Override API URL with FIRECRAWL_API_URL or FIRECRAWL_BASE_URL.",
|
|
579
|
+
"This extension never logs, displays, or stores your Firecrawl API key.",
|
|
580
|
+
].join("\n");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildCommandGuide() {
|
|
584
|
+
return [
|
|
585
|
+
"Firecrawl commands:",
|
|
586
|
+
"/firecrawl — open this menu",
|
|
587
|
+
"/firecrawl help — show command usage",
|
|
588
|
+
"/firecrawl config — show API key presence and API URL",
|
|
589
|
+
"/firecrawl quickstart — alias for /firecrawl config",
|
|
590
|
+
"/firecrawl status — show tool and settings status",
|
|
591
|
+
"/firecrawl tools — select individual Firecrawl tools",
|
|
592
|
+
"/firecrawl toggle — alias for /firecrawl tools",
|
|
593
|
+
"/firecrawl enable — enable all Firecrawl tools",
|
|
594
|
+
"/firecrawl disable — disable all Firecrawl tools",
|
|
595
|
+
].join("\n");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function toolSelectorTitle(selectedTools: ReadonlySet<FirecrawlToolName>) {
|
|
599
|
+
return `Firecrawl tools (${selectedTools.size}/${FIRECRAWL_TOOL_NAMES.length}). Non-built-in tools run at user risk.`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function firecrawlToolSelectorRows(): ToolSelectorRow[] {
|
|
603
|
+
return [
|
|
604
|
+
...FIRECRAWL_TOOL_NAMES.map((toolName) => ({ kind: "tool" as const, toolName })),
|
|
605
|
+
{ kind: "action", action: "enableAll", label: TOOL_SELECTOR_ENABLE_ALL },
|
|
606
|
+
{ kind: "action", action: "disableAll", label: TOOL_SELECTOR_DISABLE_ALL },
|
|
607
|
+
{ kind: "action", action: "done", label: TOOL_SELECTOR_DONE },
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function formatToolSelectorRow(
|
|
612
|
+
row: ToolSelectorRow,
|
|
613
|
+
selectedTools: ReadonlySet<FirecrawlToolName>,
|
|
614
|
+
) {
|
|
615
|
+
if (row.kind === "action") return row.label;
|
|
616
|
+
return `${selectedTools.has(row.toolName) ? "[x]" : "[ ]"} ${row.toolName}`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function getActiveFirecrawlTools(pi: ExtensionAPI) {
|
|
620
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
621
|
+
return FIRECRAWL_TOOL_NAMES.filter((toolName) => activeToolNames.has(toolName));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function allFirecrawlTools() {
|
|
625
|
+
return [...FIRECRAWL_TOOL_NAMES];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function orderedFirecrawlTools(selectedTools: ReadonlySet<FirecrawlToolName>) {
|
|
629
|
+
return FIRECRAWL_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function formatRuntimeStatus(summary: ToolStatusSummary) {
|
|
633
|
+
return `${summary.runtimeStatus} (${summary.activeFirecrawlToolCount}/${FIRECRAWL_TOOL_NAMES.length} active)`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function persistedSettingLabel() {
|
|
637
|
+
const settings = await loadSettings();
|
|
638
|
+
if (settings.kind === "loaded") return formatPersistedSelection(settings.settings.tools);
|
|
639
|
+
if (settings.kind === "invalid") {
|
|
640
|
+
return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
|
|
641
|
+
}
|
|
642
|
+
return "none; current active-tool policy preserved";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function formatPersistedSelection(tools: readonly FirecrawlToolName[]) {
|
|
646
|
+
if (tools.length === FIRECRAWL_TOOL_NAMES.length) {
|
|
647
|
+
return `all enabled (${tools.length}/${FIRECRAWL_TOOL_NAMES.length} selected)`;
|
|
648
|
+
}
|
|
649
|
+
if (tools.length === 0) return `all disabled (0/${FIRECRAWL_TOOL_NAMES.length} selected)`;
|
|
650
|
+
return `${tools.length}/${FIRECRAWL_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function persistSettings(
|
|
654
|
+
ctx: CommandContext,
|
|
655
|
+
selectedTools: readonly FirecrawlToolName[],
|
|
656
|
+
) {
|
|
657
|
+
try {
|
|
658
|
+
await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
|
|
659
|
+
} catch (error) {
|
|
660
|
+
ctx.ui.notify(`Firecrawl settings save failed: ${formatError(error)}`, "warning");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function loadSettings(): Promise<
|
|
665
|
+
| { kind: "missing" }
|
|
666
|
+
| { kind: "invalid"; reason: string }
|
|
667
|
+
| { kind: "loaded"; settings: FirecrawlSettings }
|
|
668
|
+
> {
|
|
669
|
+
let text: string;
|
|
670
|
+
try {
|
|
671
|
+
text = await readFile(SETTINGS_FILE, "utf8");
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
674
|
+
return { kind: "invalid", reason: formatError(error) };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
const parsed = JSON.parse(text) as unknown;
|
|
679
|
+
const settings = normalizeFirecrawlSettings(parsed);
|
|
680
|
+
if (settings) return { kind: "loaded", settings };
|
|
681
|
+
return { kind: "invalid", reason: "expected tools to be an array of Firecrawl tool names" };
|
|
682
|
+
} catch (error) {
|
|
683
|
+
return { kind: "invalid", reason: formatError(error) };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function normalizeFirecrawlSettings(value: unknown): FirecrawlSettings | undefined {
|
|
688
|
+
if (!value || typeof value !== "object") return undefined;
|
|
689
|
+
const settings = value as { tools?: unknown; updatedAt?: unknown };
|
|
690
|
+
if (typeof settings.updatedAt !== "number") return undefined;
|
|
691
|
+
if (!Array.isArray(settings.tools)) return undefined;
|
|
692
|
+
if (!settings.tools.every(isFirecrawlToolName)) return undefined;
|
|
693
|
+
return { tools: orderedUniqueFirecrawlTools(settings.tools), updatedAt: settings.updatedAt };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function isFirecrawlToolName(value: unknown): value is FirecrawlToolName {
|
|
697
|
+
return typeof value === "string" && FIRECRAWL_TOOL_NAMES.includes(value as never);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function orderedUniqueFirecrawlTools(tools: readonly FirecrawlToolName[]) {
|
|
701
|
+
const selectedTools = new Set(tools);
|
|
702
|
+
return orderedFirecrawlTools(selectedTools);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function saveSettings(settings: FirecrawlSettings) {
|
|
706
|
+
await mkdir(dirname(SETTINGS_FILE), { recursive: true });
|
|
707
|
+
const tempFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
|
|
708
|
+
try {
|
|
709
|
+
await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
710
|
+
await rename(tempFile, SETTINGS_FILE);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
718
|
+
return error instanceof Error && "code" in error;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function formatError(error: unknown) {
|
|
722
|
+
return error instanceof Error ? error.message : String(error);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function unique<T>(values: T[]) {
|
|
726
|
+
return Array.from(new Set(values));
|
|
727
|
+
}
|
|
728
|
+
|
|
216
729
|
async function firecrawlRequest(
|
|
217
730
|
method: "GET" | "POST",
|
|
218
731
|
path: string,
|
|
@@ -292,12 +805,6 @@ async function withStatus<T>(ctx: StatusContext, status: string, callback: () =>
|
|
|
292
805
|
}
|
|
293
806
|
}
|
|
294
807
|
|
|
295
|
-
function buildStatusMessage() {
|
|
296
|
-
return hasApiKey()
|
|
297
|
-
? `Firecrawl configured: ${state.apiUrl} (API key present).`
|
|
298
|
-
: `Firecrawl missing FIRECRAWL_API_KEY. API URL: ${state.apiUrl}.`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
808
|
function cleanObject<T>(value: T): T {
|
|
302
809
|
if (Array.isArray(value)) {
|
|
303
810
|
return value.map((item) => cleanObject(item)) as T;
|