@narumitw/pi-firecrawl 0.13.0 → 0.14.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
@@ -111,8 +111,7 @@ instead of enabling tools by itself. A valid saved selection is restored on Pi s
111
111
  Compatibility: older versions used `pi-firecrawl-settings.json`. During the migration window,
112
112
  a legacy-only file is automatically migrated to `pi-firecrawl.json` with a warning. If both
113
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.
114
+ deprecated and will be removed in a future major release.
116
115
 
117
116
  ## 🚀 Examples
118
117
 
@@ -159,13 +158,16 @@ Start a crawl with markdown extraction:
159
158
  ```txt
160
159
  extensions/pi-firecrawl/
161
160
  ├── src/
162
- └── firecrawl.ts
161
+ ├── firecrawl.ts # Pi entrypoint and command orchestration
162
+ │ └── *.ts # Package-local client, settings, selector, and tool modules
163
163
  ├── README.md
164
164
  ├── LICENSE
165
165
  ├── tsconfig.json
166
166
  └── package.json
167
167
  ```
168
168
 
169
+ Only `firecrawl.ts` is a Pi entrypoint; the other source modules are internal.
170
+
169
171
  ## 🔎 Keywords
170
172
 
171
173
  Pi extension, Pi coding agent, Firecrawl, web scraping, web crawling, URL discovery, web search, markdown extraction, AI research agent, TypeScript Pi tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-firecrawl",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Pi extension that exposes Firecrawl web scraping and crawling tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "typebox": "^1.3.3"
33
33
  },
34
34
  "devDependencies": {
35
- "@biomejs/biome": "2.5.2",
35
+ "@biomejs/biome": "2.5.3",
36
36
  "@earendil-works/pi-coding-agent": "0.80.3",
37
37
  "typescript": "6.0.3"
38
38
  },
package/src/client.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ const DEFAULT_API_URL = "https://api.firecrawl.dev/v1";
4
+ const STATUS_KEY = "firecrawl";
5
+
6
+ let apiUrl = normalizeApiUrl(process.env.FIRECRAWL_API_URL ?? process.env.FIRECRAWL_BASE_URL);
7
+
8
+ export function configuredApiUrl() {
9
+ return apiUrl;
10
+ }
11
+
12
+ export function resetConfiguredApiUrl() {
13
+ apiUrl = normalizeApiUrl(process.env.FIRECRAWL_API_URL ?? process.env.FIRECRAWL_BASE_URL);
14
+ }
15
+
16
+ export async function firecrawlRequest(
17
+ method: "GET" | "POST",
18
+ path: string,
19
+ body: unknown,
20
+ signal?: AbortSignal,
21
+ ) {
22
+ const apiKey = getApiKey();
23
+ const response = await fetch(`${apiUrl}${path}`, {
24
+ method,
25
+ headers: {
26
+ Authorization: `Bearer ${apiKey}`,
27
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
28
+ },
29
+ body: body === undefined ? undefined : JSON.stringify(body),
30
+ signal,
31
+ });
32
+ const responseText = await response.text();
33
+ const payload = parseResponseBody(responseText);
34
+ if (!response.ok) {
35
+ throw new Error(`Firecrawl ${method} ${path} failed (${response.status}): ${formatPayload(payload)}`);
36
+ }
37
+ return payload;
38
+ }
39
+
40
+ export function getApiKey() {
41
+ const apiKey = process.env.FIRECRAWL_API_KEY?.trim();
42
+ if (!apiKey) {
43
+ throw new Error(
44
+ "FIRECRAWL_API_KEY is not configured. Set it in your shell or Pi environment, then retry once.",
45
+ );
46
+ }
47
+ return apiKey;
48
+ }
49
+
50
+ export function hasApiKey() {
51
+ return Boolean(process.env.FIRECRAWL_API_KEY?.trim());
52
+ }
53
+
54
+ export function normalizeApiUrl(value: string | undefined) {
55
+ return (value?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
56
+ }
57
+
58
+ export function parseResponseBody(responseText: string) {
59
+ if (!responseText) return {};
60
+ try {
61
+ return JSON.parse(responseText) as unknown;
62
+ } catch {
63
+ return responseText;
64
+ }
65
+ }
66
+
67
+ export function formatPayload(payload: unknown) {
68
+ return typeof payload === "string" ? payload : JSON.stringify(payload);
69
+ }
70
+
71
+ export function jsonResult(payload: unknown) {
72
+ return {
73
+ content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
74
+ details: payload,
75
+ };
76
+ }
77
+
78
+ export async function withStatus<T>(
79
+ ctx: Pick<ExtensionContext, "ui">,
80
+ status: string,
81
+ callback: () => Promise<T>,
82
+ ) {
83
+ ctx.ui.setStatus(STATUS_KEY, status);
84
+ try {
85
+ return await callback();
86
+ } finally {
87
+ ctx.ui.setStatus(STATUS_KEY, undefined);
88
+ }
89
+ }
90
+
91
+ export function cleanObject<T>(value: T): T {
92
+ if (Array.isArray(value)) return value.map(cleanObject) as T;
93
+ if (!value || typeof value !== "object") return value;
94
+ return Object.fromEntries(
95
+ Object.entries(value as Record<string, unknown>)
96
+ .filter(([, item]) => item !== undefined)
97
+ .map(([key, item]) => [key, cleanObject(item)]),
98
+ ) as T;
99
+ }