@narumitw/pi-firecrawl 0.10.0 → 0.12.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.
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/firecrawl.ts +137 -18
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Direct subcommands are also available:
|
|
|
100
100
|
The selected tool names are saved to:
|
|
101
101
|
|
|
102
102
|
```text
|
|
103
|
-
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-firecrawl
|
|
103
|
+
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-firecrawl.json
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
When the file is missing or invalid, the extension preserves Pi's current active-tool policy
|
|
@@ -108,6 +108,12 @@ instead of enabling tools by itself. A valid saved selection is restored on Pi s
|
|
|
108
108
|
`/reload`. The settings file stores only tool names and a timestamp; it never stores
|
|
109
109
|
`FIRECRAWL_API_KEY`, request headers, or other secrets.
|
|
110
110
|
|
|
111
|
+
Compatibility: older versions used `pi-firecrawl-settings.json`. During the migration window,
|
|
112
|
+
a legacy-only file is automatically migrated to `pi-firecrawl.json` with a warning. If both
|
|
113
|
+
files exist, `pi-firecrawl.json` wins and the legacy file is ignored. The legacy filename is
|
|
114
|
+
deprecated and planned for removal after at least one minor release cycle, preferably 4–8 weeks,
|
|
115
|
+
or at the next major release.
|
|
116
|
+
|
|
111
117
|
## 🚀 Examples
|
|
112
118
|
|
|
113
119
|
Scrape a page as markdown:
|
package/package.json
CHANGED
package/src/firecrawl.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
5
|
import { dirname, join } from "node:path";
|
|
4
6
|
import {
|
|
@@ -10,10 +12,8 @@ import { Type } from "typebox";
|
|
|
10
12
|
|
|
11
13
|
const DEFAULT_API_URL = "https://api.firecrawl.dev/v1";
|
|
12
14
|
const STATUS_KEY = "firecrawl";
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
"pi-firecrawl-settings.json",
|
|
16
|
-
);
|
|
15
|
+
const NEW_SETTINGS_FILE = "pi-firecrawl.json";
|
|
16
|
+
const LEGACY_SETTINGS_FILE = "pi-firecrawl-settings.json";
|
|
17
17
|
const FIRECRAWL_TOOL_NAMES = [
|
|
18
18
|
"firecrawl_scrape",
|
|
19
19
|
"firecrawl_crawl",
|
|
@@ -64,6 +64,7 @@ type ToolSelectorRow =
|
|
|
64
64
|
|
|
65
65
|
interface FirecrawlState {
|
|
66
66
|
apiUrl: string;
|
|
67
|
+
settingsNotice?: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
interface FirecrawlSettings {
|
|
@@ -273,8 +274,11 @@ export default function firecrawl(pi: ExtensionAPI) {
|
|
|
273
274
|
});
|
|
274
275
|
|
|
275
276
|
pi.on("session_start", async (_event, ctx) => {
|
|
277
|
+
state.settingsNotice = undefined;
|
|
276
278
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
277
279
|
const settings = await loadSettings();
|
|
280
|
+
recordSettingsNotice(settings);
|
|
281
|
+
if (settings.notice) ctx.ui.notify(settings.notice, "warning");
|
|
278
282
|
if (settings.kind === "loaded") {
|
|
279
283
|
applyFirecrawlTools(pi, settings.settings.tools);
|
|
280
284
|
return;
|
|
@@ -563,7 +567,8 @@ async function buildStatusMessage(pi: ExtensionAPI) {
|
|
|
563
567
|
return [
|
|
564
568
|
`Firecrawl tools: ${formatRuntimeStatus(summary)}`,
|
|
565
569
|
`Persisted selection: ${persistedSetting}`,
|
|
566
|
-
`Settings file: ${
|
|
570
|
+
`Settings file: ${settingsFilePath()}`,
|
|
571
|
+
...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
|
|
567
572
|
`Other active tools preserved: ${summary.activeNonFirecrawlToolCount}`,
|
|
568
573
|
`API key: ${hasApiKey() ? "present" : "missing"} (FIRECRAWL_API_KEY)`,
|
|
569
574
|
`API URL: ${state.apiUrl}`,
|
|
@@ -635,6 +640,7 @@ function formatRuntimeStatus(summary: ToolStatusSummary) {
|
|
|
635
640
|
|
|
636
641
|
async function persistedSettingLabel() {
|
|
637
642
|
const settings = await loadSettings();
|
|
643
|
+
recordSettingsNotice(settings);
|
|
638
644
|
if (settings.kind === "loaded") return formatPersistedSelection(settings.settings.tools);
|
|
639
645
|
if (settings.kind === "invalid") {
|
|
640
646
|
return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
|
|
@@ -661,29 +667,129 @@ async function persistSettings(
|
|
|
661
667
|
}
|
|
662
668
|
}
|
|
663
669
|
|
|
664
|
-
|
|
665
|
-
| { kind: "missing" }
|
|
666
|
-
| { kind: "invalid"; reason: string }
|
|
667
|
-
| { kind: "loaded"; settings: FirecrawlSettings }
|
|
668
|
-
|
|
670
|
+
type SettingsLoadResult =
|
|
671
|
+
| { kind: "missing"; notice?: string }
|
|
672
|
+
| { kind: "invalid"; reason: string; notice?: string }
|
|
673
|
+
| { kind: "loaded"; settings: FirecrawlSettings; notice?: string };
|
|
674
|
+
|
|
675
|
+
type SettingsMigrationResult = {
|
|
676
|
+
kind: "migrated" | "failed";
|
|
677
|
+
notice: string;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
async function loadSettings(): Promise<SettingsLoadResult> {
|
|
681
|
+
const newPath = settingsFilePath();
|
|
682
|
+
const newSettings = await readSettingsFile(newPath);
|
|
683
|
+
if (newSettings.kind !== "missing") {
|
|
684
|
+
return withLegacyIgnoredNotice(newSettings);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const legacyPath = legacySettingsFilePath();
|
|
688
|
+
const legacySettings = await readSettingsFile(legacyPath);
|
|
689
|
+
const concurrentlyCreatedSettings = await readSettingsFile(newPath);
|
|
690
|
+
if (concurrentlyCreatedSettings.kind !== "missing") {
|
|
691
|
+
return withLegacyIgnoredNotice(concurrentlyCreatedSettings);
|
|
692
|
+
}
|
|
693
|
+
if (legacySettings.kind === "missing") return { kind: "missing" };
|
|
694
|
+
if (legacySettings.kind === "invalid") return legacySettings;
|
|
695
|
+
|
|
696
|
+
const migration = await migrateLegacySettings(legacyPath, legacySettings.settings);
|
|
697
|
+
if (migration.kind === "failed") {
|
|
698
|
+
const settingsCreatedDuringMigration = await readSettingsFile(newPath);
|
|
699
|
+
if (settingsCreatedDuringMigration.kind !== "missing") {
|
|
700
|
+
return withLegacyIgnoredNotice(settingsCreatedDuringMigration);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return { ...legacySettings, notice: migration.notice };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function readSettingsFile(filePath: string): Promise<SettingsLoadResult> {
|
|
669
708
|
let text: string;
|
|
670
709
|
try {
|
|
671
|
-
text = await readFile(
|
|
710
|
+
text = await readFile(filePath, "utf8");
|
|
672
711
|
} catch (error) {
|
|
673
712
|
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
674
|
-
return { kind: "invalid", reason: formatError(error) };
|
|
713
|
+
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
675
714
|
}
|
|
676
715
|
|
|
677
716
|
try {
|
|
678
717
|
const parsed = JSON.parse(text) as unknown;
|
|
679
718
|
const settings = normalizeFirecrawlSettings(parsed);
|
|
680
719
|
if (settings) return { kind: "loaded", settings };
|
|
681
|
-
return {
|
|
720
|
+
return {
|
|
721
|
+
kind: "invalid",
|
|
722
|
+
reason: `${filePath}: expected tools to be an array of Firecrawl tool names`,
|
|
723
|
+
};
|
|
682
724
|
} catch (error) {
|
|
683
|
-
return { kind: "invalid", reason: formatError(error) };
|
|
725
|
+
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function withLegacyIgnoredNotice(settings: SettingsLoadResult): Promise<SettingsLoadResult> {
|
|
730
|
+
if (!(await fileExists(legacySettingsFilePath()))) return settings;
|
|
731
|
+
return {
|
|
732
|
+
...settings,
|
|
733
|
+
notice: `Firecrawl legacy settings ignored: ${legacySettingsFilePath()} exists, but ${settingsFilePath()} takes precedence. Delete ${LEGACY_SETTINGS_FILE} after confirming your settings.`,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export async function installSettingsFileExclusively(filePath: string, contents: string) {
|
|
738
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
739
|
+
const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
740
|
+
try {
|
|
741
|
+
await writeFile(tempFile, contents, { encoding: "utf8", flag: "wx" });
|
|
742
|
+
await link(tempFile, filePath);
|
|
743
|
+
} finally {
|
|
744
|
+
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
684
745
|
}
|
|
685
746
|
}
|
|
686
747
|
|
|
748
|
+
async function migrateLegacySettings(
|
|
749
|
+
legacyPath: string,
|
|
750
|
+
settings: FirecrawlSettings,
|
|
751
|
+
): Promise<SettingsMigrationResult> {
|
|
752
|
+
const newPath = settingsFilePath();
|
|
753
|
+
try {
|
|
754
|
+
await installSettingsFileExclusively(
|
|
755
|
+
newPath,
|
|
756
|
+
`${JSON.stringify(settings, null, 2)}\n`,
|
|
757
|
+
);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
return {
|
|
760
|
+
kind: "failed",
|
|
761
|
+
notice: `Firecrawl legacy settings migration failed: could not migrate ${legacyPath} to ${newPath}: ${formatError(error)}. The legacy file was used for this session; future saves will write ${NEW_SETTINGS_FILE}.`,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
await rm(legacyPath, { force: true });
|
|
767
|
+
} catch (error) {
|
|
768
|
+
return {
|
|
769
|
+
kind: "migrated",
|
|
770
|
+
notice: `Firecrawl settings migrated from ${legacyPath} to ${newPath}, but the legacy file could not be removed: ${formatError(error)}. Delete ${LEGACY_SETTINGS_FILE} after confirming your settings.`,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
kind: "migrated",
|
|
776
|
+
notice: `Firecrawl settings migrated from ${legacyPath} to ${newPath}. ${LEGACY_SETTINGS_FILE} is deprecated and will be removed in a future major release.`,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function fileExists(filePath: string) {
|
|
781
|
+
try {
|
|
782
|
+
await access(filePath, constants.F_OK);
|
|
783
|
+
return true;
|
|
784
|
+
} catch {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function recordSettingsNotice(settings: SettingsLoadResult) {
|
|
790
|
+
if (settings.notice) state.settingsNotice = settings.notice;
|
|
791
|
+
}
|
|
792
|
+
|
|
687
793
|
export function normalizeFirecrawlSettings(value: unknown): FirecrawlSettings | undefined {
|
|
688
794
|
if (!value || typeof value !== "object") return undefined;
|
|
689
795
|
const settings = value as { tools?: unknown; updatedAt?: unknown };
|
|
@@ -703,17 +809,30 @@ function orderedUniqueFirecrawlTools(tools: readonly FirecrawlToolName[]) {
|
|
|
703
809
|
}
|
|
704
810
|
|
|
705
811
|
async function saveSettings(settings: FirecrawlSettings) {
|
|
706
|
-
|
|
707
|
-
|
|
812
|
+
const filePath = settingsFilePath();
|
|
813
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
814
|
+
const tempFile = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
708
815
|
try {
|
|
709
816
|
await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
710
|
-
await rename(tempFile,
|
|
817
|
+
await rename(tempFile, filePath);
|
|
711
818
|
} catch (error) {
|
|
712
819
|
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
713
820
|
throw error;
|
|
714
821
|
}
|
|
715
822
|
}
|
|
716
823
|
|
|
824
|
+
function settingsFilePath() {
|
|
825
|
+
return join(agentDir(), NEW_SETTINGS_FILE);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function legacySettingsFilePath() {
|
|
829
|
+
return join(agentDir(), LEGACY_SETTINGS_FILE);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function agentDir() {
|
|
833
|
+
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
834
|
+
}
|
|
835
|
+
|
|
717
836
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
718
837
|
return error instanceof Error && "code" in error;
|
|
719
838
|
}
|