pi-firecrawl-lite 0.1.1 → 0.1.2

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
@@ -17,7 +17,7 @@ For a project-local installation, add the package to `package.json` and use the
17
17
  ```json
18
18
  {
19
19
  "pi": {
20
- "extensions": ["./node_modules/pi-firecrawl-lite/src/index.ts"]
20
+ "extensions": ["./node_modules/pi-firecrawl-lite/index.ts"]
21
21
  }
22
22
  }
23
23
  ```
@@ -43,7 +43,7 @@ export FIRECRAWL_TIMEOUT_MS=120000 # optional
43
43
 
44
44
  The URL must be HTTP or HTTPS and must point to the host root or end in `/v2`. v1 URLs are rejected. The API key is never shown in tool output, errors, notifications, or saved response details.
45
45
 
46
- Configuration can also be changed for the current Pi session with slash commands:
46
+ Configuration can also be changed interactively for the current Pi session. Run `/firecrawl` and choose an option from the menu. The same actions are available as slash commands:
47
47
 
48
48
  ```text
49
49
  /firecrawl status
@@ -55,6 +55,8 @@ Configuration can also be changed for the current Pi session with slash commands
55
55
 
56
56
  `/firecrawl key` opens an input prompt when no key follows it. Session command overrides are kept in memory and reset when the extension is reloaded; use environment variables for a durable configuration.
57
57
 
58
+ Search and scrape are available immediately; `firecrawl_load` is only needed before using the deferred map or crawl tools. If a local Firecrawl server is not reachable, the tool reports the active endpoint and points back to `/firecrawl`.
59
+
58
60
  ## Tools
59
61
 
60
62
  Always active:
@@ -71,7 +73,7 @@ Deferred tools:
71
73
 
72
74
  Every model-facing schema is flat. `options_json` accepts an object for advanced Firecrawl features such as browser actions, extraction, headers, location, tags, search filters, webhooks, and nested scrape options. It rejects malformed JSON, arrays, primitive values, and prototype-pollution keys. Explicit top-level arguments always override advanced options.
73
75
 
74
- Responses are concise by default. Set `response_mode` to `raw` when debugging or passing the full Firecrawl response to another step. Output is bounded at 50 KiB and 2,000 lines. A truncated response is saved in a private, session-specific temporary directory with restrictive permissions, and the tool reports its exact path and limits. Artifacts are removed when the session shuts down or the extension reloads.
76
+ Responses are concise by default and are rendered with readable labels and width-aware Markdown in Pi. Set `response_mode` to `raw` when debugging or passing the full Firecrawl response to another step. Output is bounded at 50 KiB and 2,000 lines. A truncated response is saved in a private, session-specific temporary directory with restrictive permissions, and the tool reports its exact path and limits. Artifacts are removed when the session shuts down or the extension reloads.
75
77
 
76
78
  ## Development
77
79
 
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/index.js";
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "pi-firecrawl-lite",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A lightweight Pi extension that brings Firecrawl search, scrape, map, and crawl tools to your agent.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "firecrawl",
8
+ "web search",
9
+ "web scraping",
10
+ "ai agents"
11
+ ],
5
12
  "type": "module",
6
13
  "private": false,
7
14
  "license": "MIT",
@@ -11,13 +18,14 @@
11
18
  "url": "https://git.jameslindfors.xyz/james/pi-firecrawl-lite.git"
12
19
  },
13
20
  "files": [
21
+ "index.ts",
14
22
  "src",
15
23
  "README.md",
16
24
  "LICENSE"
17
25
  ],
18
26
  "pi": {
19
27
  "extensions": [
20
- "./src/index.ts"
28
+ "./index.ts"
21
29
  ]
22
30
  },
23
31
  "scripts": {
package/src/index.ts CHANGED
@@ -35,7 +35,7 @@ export default function firecrawlExtension(pi: ExtensionAPI): void {
35
35
  });
36
36
 
37
37
  pi.registerCommand("firecrawl", {
38
- description: "Configure Firecrawl for this Pi session: /firecrawl status|url|key|timeout|reset",
38
+ description: "Configure Firecrawl interactively, or use /firecrawl status|url|key|timeout|reset",
39
39
  handler: async (args, ctx) => configureFromCommand(args, runtime, ctx),
40
40
  });
41
41
  pi.on("session_start", () => {
package/src/tools.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
4
+ import { Markdown } from "@earendil-works/pi-tui";
3
5
  import { ArtifactStore, FirecrawlClient, FirecrawlError, redactSecret } from "./client.js";
4
6
 
5
7
  export const MAX_OUTPUT_BYTES = 50 * 1024;
@@ -160,12 +162,22 @@ function arrayFrom(value: unknown): unknown[] {
160
162
  if (Array.isArray(value)) return value;
161
163
  if (isPlainObject(value)) {
162
164
  for (const key of ["data", "links", "web", "results"]) {
163
- if (Array.isArray(value[key])) return value[key] as unknown[];
165
+ const child = value[key];
166
+ if (Array.isArray(child)) return child;
167
+ if (key === "data" && isPlainObject(child)) {
168
+ const nested = arrayFrom(child);
169
+ if (nested.length > 0) return nested;
170
+ }
164
171
  }
165
172
  }
166
173
  return [];
167
174
  }
168
175
 
176
+ function dataObject(value: unknown): JsonObject {
177
+ if (!isPlainObject(value)) return {};
178
+ return isPlainObject(value.data) ? value.data : value;
179
+ }
180
+
169
181
  export function conciseSearch(payload: unknown): string {
170
182
  const entries = arrayFrom(payload);
171
183
  if (entries.length === 0) return "No search results.";
@@ -174,12 +186,12 @@ export function conciseSearch(payload: unknown): string {
174
186
  const title = typeof item.title === "string" ? item.title : "Untitled result";
175
187
  const url = typeof item.url === "string" ? item.url : "(no URL)";
176
188
  const description = typeof item.description === "string" ? item.description : "";
177
- return `${index + 1}. ${title}\n ${url}${description ? `\n ${description}` : ""}`;
189
+ return `${index + 1}. ${title}\n URL: ${url}${description ? `\n Description: ${description}` : ""}`;
178
190
  }).join("\n\n");
179
191
  }
180
192
 
181
193
  export function conciseScrape(payload: unknown): string {
182
- const item = isPlainObject(payload) ? payload : {};
194
+ const item = dataObject(payload);
183
195
  const metadata = isPlainObject(item.metadata) ? item.metadata : {};
184
196
  const source = item.sourceURL ?? metadata.sourceURL ?? item.url;
185
197
  const title = item.title ?? metadata.title;
@@ -281,7 +293,22 @@ function errorResult(error: unknown, client: FirecrawlClient) {
281
293
  const config = (() => { try { return client.getConfig(); } catch { return undefined; } })();
282
294
  const message = error instanceof FirecrawlError ? `${error.kind}: ${error.message}` : error instanceof Error ? error.message : String(error);
283
295
  const safe = redactSecret(message, config?.apiKey);
284
- return { content: [{ type: "text" as const, text: `Firecrawl error: ${safe}` }], details: { error: safe } } satisfies AgentToolResult<ToolDetails>;
296
+ const hint = error instanceof FirecrawlError && (error.kind === "connection" || error.kind === "timeout")
297
+ ? `\nEndpoint: ${config?.apiUrl ?? "unknown"}. Run /firecrawl to configure Firecrawl.`
298
+ : "";
299
+ const text = `Firecrawl error: ${safe}${hint}`;
300
+ return { content: [{ type: "text" as const, text }], details: { error: text } } satisfies AgentToolResult<ToolDetails>;
301
+ }
302
+
303
+ function renderToolResult(result: AgentToolResult<ToolDetails>, options: { expanded: boolean; isPartial: boolean }) {
304
+ if (options.isPartial) return new Markdown("Fetching Firecrawl…", 0, 0, getMarkdownTheme());
305
+ const content = result.content[0];
306
+ if (content?.type !== "text") return new Markdown("Firecrawl returned no text output.", 0, 0, getMarkdownTheme());
307
+ const lines = content.text.split("\n");
308
+ const text = options.expanded || lines.length <= 14
309
+ ? content.text
310
+ : `${lines.slice(0, 14).join("\n")}\n\n_${lines.length - 14} more lines — expand to view_`;
311
+ return new Markdown(text, 0, 0, getMarkdownTheme());
285
312
  }
286
313
 
287
314
  async function execute(runtime: ToolRuntime, kind: Parameters<typeof formatPayload>[0], responseMode: unknown, operation: () => Promise<unknown>): Promise<AgentToolResult<ToolDetails>> {
@@ -296,20 +323,20 @@ async function execute(runtime: ToolRuntime, kind: Parameters<typeof formatPaylo
296
323
  }
297
324
 
298
325
  export function registerSearchTool(pi: ExtensionAPI, runtime: ToolRuntime): void {
299
- pi.registerTool({ name: "firecrawl_search", label: "Firecrawl Search", description: "Search the web through Firecrawl.", parameters: searchParameters, async execute(_id, params, signal) { return execute(runtime, "search", params.response_mode, () => runtime.client.search(buildSearchBody(params), signal)); } });
326
+ pi.registerTool({ name: "firecrawl_search", label: "Firecrawl Search", description: "Search the web through Firecrawl.", parameters: searchParameters, async execute(_id, params, signal) { return execute(runtime, "search", params.response_mode, () => runtime.client.search(buildSearchBody(params), signal)); }, renderResult: renderToolResult });
300
327
  }
301
328
 
302
329
  export function registerScrapeTool(pi: ExtensionAPI, runtime: ToolRuntime): void {
303
- pi.registerTool({ name: "firecrawl_scrape", label: "Firecrawl Scrape", description: "Scrape a web page through Firecrawl.", parameters: scrapeParameters, async execute(_id, params, signal) { return execute(runtime, "scrape", params.response_mode, () => runtime.client.scrape(buildScrapeBody(params), signal)); } });
330
+ pi.registerTool({ name: "firecrawl_scrape", label: "Firecrawl Scrape", description: "Scrape a web page through Firecrawl.", parameters: scrapeParameters, async execute(_id, params, signal) { return execute(runtime, "scrape", params.response_mode, () => runtime.client.scrape(buildScrapeBody(params), signal)); }, renderResult: renderToolResult });
304
331
  }
305
332
 
306
333
  export function registerMapTool(pi: ExtensionAPI, runtime: ToolRuntime): void {
307
- pi.registerTool({ name: "firecrawl_map", label: "Firecrawl Map", description: "Discover URLs on a website through Firecrawl.", parameters: mapParameters, async execute(_id, params, signal) { return execute(runtime, "map", params.response_mode, () => runtime.client.map(buildMapBody(params), signal)); } });
334
+ pi.registerTool({ name: "firecrawl_map", label: "Firecrawl Map", description: "Discover URLs on a website through Firecrawl.", parameters: mapParameters, async execute(_id, params, signal) { return execute(runtime, "map", params.response_mode, () => runtime.client.map(buildMapBody(params), signal)); }, renderResult: renderToolResult });
308
335
  }
309
336
 
310
337
  export function registerCrawlTools(pi: ExtensionAPI, runtime: ToolRuntime): void {
311
- pi.registerTool({ name: "firecrawl_crawl", label: "Firecrawl Crawl", description: "Start a Firecrawl site crawl.", parameters: crawlParameters, async execute(_id, params, signal) { return execute(runtime, "crawl", params.response_mode, () => runtime.client.crawl(buildCrawlBody(params), signal)); } });
312
- pi.registerTool({ name: "firecrawl_crawl_status", label: "Firecrawl Crawl Status", description: "Read the status and documents for a Firecrawl crawl.", parameters: crawlStatusParameters, async execute(_id, params, signal) { return execute(runtime, "crawl-status", params.response_mode, () => runtime.client.crawlStatus(requireText(params.job_id, "job_id"), signal)); } });
338
+ pi.registerTool({ name: "firecrawl_crawl", label: "Firecrawl Crawl", description: "Start a Firecrawl site crawl.", parameters: crawlParameters, async execute(_id, params, signal) { return execute(runtime, "crawl", params.response_mode, () => runtime.client.crawl(buildCrawlBody(params), signal)); }, renderResult: renderToolResult });
339
+ pi.registerTool({ name: "firecrawl_crawl_status", label: "Firecrawl Crawl Status", description: "Read the status and documents for a Firecrawl crawl.", parameters: crawlStatusParameters, async execute(_id, params, signal) { return execute(runtime, "crawl-status", params.response_mode, () => runtime.client.crawlStatus(requireText(params.job_id, "job_id"), signal)); }, renderResult: renderToolResult });
313
340
  }
314
341
 
315
342
  export function createRuntime(): ToolRuntime {
@@ -322,6 +349,22 @@ export async function configureFromCommand(args: string, runtime: ToolRuntime, c
322
349
  const value = rest.join(" ").trim();
323
350
  const notify = (message: string, level: "info" | "warning" = "info") => ctx.ui.notify(message, level);
324
351
  try {
352
+ if (!command && ctx.hasUI) {
353
+ const choice = await ctx.ui.select("Configure Firecrawl", [
354
+ "Show status",
355
+ "Set API URL",
356
+ "Set API key",
357
+ "Set timeout",
358
+ "Reset session overrides",
359
+ "Cancel",
360
+ ]);
361
+ if (choice === "Show status") return configureFromCommand("status", runtime, ctx);
362
+ if (choice === "Set API URL") return configureFromCommand("url", runtime, ctx);
363
+ if (choice === "Set API key") return configureFromCommand("key", runtime, ctx);
364
+ if (choice === "Set timeout") return configureFromCommand("timeout", runtime, ctx);
365
+ if (choice === "Reset session overrides") return configureFromCommand("reset", runtime, ctx);
366
+ return;
367
+ }
325
368
  if (!command || command === "status") {
326
369
  const config = runtime.client.getConfig();
327
370
  notify(`Firecrawl endpoint: ${config.apiUrl}; authentication: ${config.apiKey ? "configured" : "not configured"}; timeout: ${config.timeoutMs} ms.`);