@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 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-settings.json
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-firecrawl",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Pi extension that exposes Firecrawl web scraping and crawling tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/firecrawl.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
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 SETTINGS_FILE = join(
14
- process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"),
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: ${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
- async function loadSettings(): Promise<
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(SETTINGS_FILE, "utf8");
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 { kind: "invalid", reason: "expected tools to be an array of Firecrawl tool names" };
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
- await mkdir(dirname(SETTINGS_FILE), { recursive: true });
707
- const tempFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
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, SETTINGS_FILE);
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
  }