@bacnh85/pi-web 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2 (2026-09-06)
4
+
5
+ ### Changed
6
+
7
+ - `web_screenshot` now returns the PNG **inline as an image block**
8
+ (`ImageContent`) alongside the text summary, so multimodal models (GLM-5.3,
9
+ Claude, Gemini) actually see the screenshot instead of a base64 char
10
+ count. The "Data: base64 PNG (N chars)" line is gone; artifact/MIME/size
11
+ summary unchanged. Inspired by zcode-plugins video2code's vision-in-the-loop.
12
+ Regression-tested in `test/unit/screenshot.test.ts` (fetch stubbed — no
13
+ daemon needed): image block present + base64-text line absent; text-only
14
+ fallback when the daemon returns no screenshot.
15
+
16
+ Daemon `success:false` responses (HTTP 200) now surface `error_message` as
17
+ a tool error instead of returning a silently empty screenshot result.
18
+
19
+ ## 0.6.1 (2026-08-30)
20
+
21
+ ### Changed
22
+
23
+ - Trimmed static prompt overhead ~385 tokens/turn: compressed the injected
24
+ `Web Tool Routing` guidance block (1,247 -> 281 chars, same routing table
25
+ + backend rules), cut all 7 tools' promptGuidelines to <=2 unique lines,
26
+ and shortened web_crawl/web_screenshot schema descriptions. One
27
+ hook.test.ts assertion updated to the compressed phrasing. No tool,
28
+ parameter, or default changed.
29
+
30
+ ### Changed (2026-08-19)
31
+
32
+ - `web_extract` agy backend default model updated to `gemini-3.7-flash-medium`
33
+ — the current Flash generation in agy 1.1.x (3.6 is still served, this just
34
+ follows the latest).
35
+
3
36
  ## 0.6.0 (2026-08-07)
4
37
 
5
38
  ### Features
package/README.md CHANGED
@@ -142,7 +142,7 @@ web_crawl url="https://example.com" mode=light poll=true # Poll for completio
142
142
 
143
143
  ### `web_screenshot` — Page screenshot
144
144
 
145
- Captures a full-page PNG screenshot using Crawl4AI. Returns base64-encoded PNG.
145
+ Captures a full-page PNG screenshot using Crawl4AI. Returns the PNG inline as an image block (multimodal models see it); text summary includes artifact/MIME/size.
146
146
 
147
147
  ```
148
148
  web_screenshot url="https://example.com"
@@ -57,27 +57,14 @@ const crawl4aiControlSchema = {
57
57
  // the guidance travels with the package and disappears when pi-web is absent.
58
58
  const WEB_ROUTING_GUIDANCE = `## Web Tool Routing (pi-web)
59
59
 
60
- The pi-web extension provides 7 unified tools that auto-select the best backend:
60
+ - **web_search** — web search (auto: SearXNG Brave Firecrawl; force via \`backend\`, tune via \`engines\`).
61
+ - **web_extract** — URL → markdown (auto: static JSDOM → dynamic Firecrawl → full Crawl4AI → agy; force via \`mode\`; prompt+schema for JSON extraction).
62
+ - **web_map** — discover site URLs (Firecrawl Map).
63
+ - **web_crawl** — multi-page crawl: \`mode: "light"\` (Firecrawl, url) or \`mode: "full"\` (Crawl4AI, urls[]).
64
+ - **web_screenshot** / **web_pdf** — page capture (Crawl4AI).
65
+ - **web_status** — provider config + health.
61
66
 
62
- - **\`web_search\`**Search the web (auto: SearXNG Brave Firecrawl). Use
63
- \`backend\` for explicit control, \`engines\` for SearXNG tuning.
64
- - **\`web_extract\`** — Extract readable content from a URL (auto: static JSDOM
65
- → dynamic Firecrawl → full Crawl4AI → agy model-backed). Use \`mode\` for
66
- explicit control.
67
- - **\`web_map\`** — Discover URLs from a site (Firecrawl Map).
68
- - **\`web_crawl\`** — Crawl multiple pages. \`mode: "light"\` (Firecrawl) or
69
- \`mode: "full"\` (Crawl4AI).
70
- - **\`web_screenshot\`** / **\`web_pdf\`** — Visual/page capture (Crawl4AI).
71
- - **\`web_status\`** — Check provider configuration and server health.
72
-
73
- Backend selection rules:
74
-
75
- - Firecrawl Search has poor semantic accuracy on domain-specific queries; prefer
76
- SearXNG or Brave for precision (force via \`backend\`).
77
- - Firecrawl Scrape fails on bot-protected sites (e.g. Ansible docs); Crawl4AI
78
- handles those (force via \`mode: "full"\`), and agy (Gemini/Claude read_url)
79
- handles the rest as a last-resort fallback (force via \`mode: "agy"\`).
80
- - Always cite source URLs when web results materially support an answer.`;
67
+ Rules: Firecrawl Search is weak on domain-specific queriesprefer SearXNG/Brave; Firecrawl Scrape fails on bot-protected sites — use Crawl4AI (\`mode: "full"\`) then agy (\`mode: "agy"\`); cite source URLs.`;
81
68
 
82
69
 
83
70
  // ---------------------------------------------------------------------------
@@ -92,13 +79,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
92
79
  description:
93
80
  "Search the web. Auto-selects backends: SearXNG, Brave, Firecrawl.",
94
81
  promptSnippet: "Search current web results",
95
- promptGuidelines: [
96
- "Source discovery, docs, facts, and general search.",
97
- "Broad discovery uses SearXNG; precision/site/docs use Brave; Firecrawl is fallback.",
98
- "Force backend via backend:'brave'|'searxng' for poor auto results.",
99
- "Use engines='google,github' for SearXNG tuning.",
100
- "Cite source URLs.",
101
- ],
82
+ promptGuidelines: ["Source discovery, docs, facts. Precision/site/docs → Brave via backend:'brave'; tune SearXNG via engines.", "Cite source URLs."],
102
83
  parameters: Type.Object({
103
84
  query: Type.String(),
104
85
  count: Type.Optional(Type.Number({ default: 5 })),
@@ -140,14 +121,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
140
121
  description:
141
122
  "Extract readable content from a URL. Auto mode: static\u2192dynamic\u2192full\u2192agy.",
142
123
  promptSnippet: "Extract readable webpage content as markdown",
143
- promptGuidelines: [
144
- "Clean markdown from a known URL.",
145
- "mode: 'static' (no API key, JSDOM), 'dynamic' (Firecrawl JS), 'full' (Crawl4AI), 'agy' (Gemini/Claude via agy).",
146
- "'auto' tries static\u2192dynamic\u2192full\u2192agy; see diagnostics for fallback chain.",
147
- "mode: 'agy' uses agy's native read_url for bot-protected/JS-heavy pages \u2014 last-resort fallback in auto.",
148
- "Use prompt+schema for structured JSON extraction (dynamic/agy modes).",
149
- "Cite the source URL.",
150
- ],
124
+ promptGuidelines: ["Markdown from a known URL; prompt+schema for structured JSON extraction.", "Cite the source URL."],
151
125
  parameters: Type.Object({
152
126
  url: Type.String(),
153
127
  mode: Type.Optional(Type.Union(
@@ -191,11 +165,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
191
165
  description:
192
166
  "Discover site URLs via Firecrawl Map.",
193
167
  promptSnippet: "Map site URLs",
194
- promptGuidelines: [
195
- "Discover site URLs before crawling. Prefer web_extract for small jobs.",
196
- "Best on base domains. sitemap:'only' for sub-path discovery.",
197
- "Keep limits small unless broad discovery is requested.",
198
- ],
168
+ promptGuidelines: ["URL discovery before crawling; prefer web_extract for small jobs."],
199
169
  parameters: Type.Object({
200
170
  url: Type.String(),
201
171
  limit: Type.Optional(Type.Number({ default: 100 })),
@@ -234,21 +204,17 @@ export default function piWebExtension(pi: ExtensionAPI) {
234
204
  description:
235
205
  "Crawl pages. Firecrawl 'light' or Crawl4AI 'full' headless mode.",
236
206
  promptSnippet: "Crawl a small site section",
237
- promptGuidelines: [
238
- "Prefer web_map + web_extract over crawl for small jobs.",
239
- "'light'=Firecrawl (url param), 'full'=Crawl4AI (urls[] param).",
240
- "Keep limit low (default 10).",
241
- ],
207
+ promptGuidelines: ["'light'=Firecrawl (url), 'full'=Crawl4AI (urls[]). Prefer web_map + web_extract for small jobs."],
242
208
  parameters: Type.Object({
243
- url: Type.Optional(Type.String({ description: "URL for Firecrawl-style crawl (mode:'light')." })),
244
- urls: Type.Optional(Type.Array(Type.String(), { description: "URLs for Crawl4AI-style crawl (mode:'full'), up to 100." })),
209
+ url: Type.Optional(Type.String({ description: "URL for mode:'light' (Firecrawl)." })),
210
+ urls: Type.Optional(Type.Array(Type.String(), { description: "URLs for mode:'full' (Crawl4AI), up to 100." })),
245
211
  mode: Type.Optional(Type.Union([Type.Literal("light"), Type.Literal("full")], { default: "light", description: "'light'(Firecrawl) or 'full'(Crawl4AI)." })),
246
212
  limit: Type.Optional(Type.Number({ default: 10 })),
247
- include_paths: Type.Optional(Type.String({ description: "Comma-separated paths to include (Firecrawl mode)." })),
248
- exclude_paths: Type.Optional(Type.String({ description: "Comma-separated paths to exclude (Firecrawl mode)." })),
249
- poll: Type.Optional(Type.Boolean({ default: false, description: "Poll for completion (Firecrawl mode)." })),
250
- browser_config: Type.Optional(Type.Any({ description: "BrowserConfig JSON (full mode only)." })),
251
- crawler_config: Type.Optional(Type.Any({ description: "CrawlerRunConfig JSON (full mode only)." })),
213
+ include_paths: Type.Optional(Type.String({ description: "Comma-separated include paths (light mode)." })),
214
+ exclude_paths: Type.Optional(Type.String({ description: "Comma-separated exclude paths (light mode)." })),
215
+ poll: Type.Optional(Type.Boolean({ default: false, description: "Poll until completion (light mode)." })),
216
+ browser_config: Type.Optional(Type.Any({ description: "BrowserConfig JSON (full mode)." })),
217
+ crawler_config: Type.Optional(Type.Any({ description: "CrawlerRunConfig JSON (full mode)." })),
252
218
  content_chars: Type.Optional(Type.Number({ default: 20000 })),
253
219
  ...firecrawlControlSchema,
254
220
  ...crawl4aiControlSchema,
@@ -320,16 +286,13 @@ export default function piWebExtension(pi: ExtensionAPI) {
320
286
  name: "web_screenshot",
321
287
  label: "Web Page Screenshot",
322
288
  description:
323
- "Full-page PNG screenshot via Crawl4AI.",
289
+ "Full-page PNG screenshot via Crawl4AI. The PNG is returned inline as an image block.",
324
290
  promptSnippet: "Screenshot a webpage",
325
- promptGuidelines: [
326
- "Use when web_extract fails on JS-heavy or bot-protected pages.",
327
- "wait_for (default 2s) delays capture for dynamic content.",
328
- ],
291
+ promptGuidelines: ["Full-page PNG returned inline (multimodal models see it); use when web_extract fails on JS-heavy pages, or to visually inspect a built UI."],
329
292
  parameters: Type.Object({
330
293
  url: Type.String(),
331
294
  wait_for: Type.Optional(Type.Number({ default: 2, description: "Seconds to wait before capture." })),
332
- wait_for_images: Type.Optional(Type.Boolean({ default: false, description: "Wait for images before capture." })),
295
+ wait_for_images: Type.Optional(Type.Boolean({ default: false })),
333
296
  ...crawl4aiControlSchema,
334
297
  ...sharedControlSchema,
335
298
  }),
@@ -342,16 +305,23 @@ export default function piWebExtension(pi: ExtensionAPI) {
342
305
  params.wait_for_images as boolean | undefined,
343
306
  signal,
344
307
  );
308
+ if (result.success === false) {
309
+ throw new Error(String(result.error_message ?? "Crawl4AI screenshot failed"));
310
+ }
345
311
  const screenshot = result.screenshot as string | undefined;
346
312
  const artifactUrl = result.url as string | undefined;
347
313
  const mime = result.mime as string | undefined;
348
314
  const size = result.size as number | undefined;
349
315
  let text = `Screenshot: ${params.url}\n`;
350
- if (screenshot) text += `Data: base64 PNG (${screenshot.length} chars)\n`;
351
316
  if (artifactUrl) text += `Artifact: ${artifactUrl}\n`;
352
317
  if (mime) text += `MIME: ${mime}\n`;
353
318
  if (size) text += `Size: ${size} bytes\n`;
354
- return { content: [{ type: "text" as const, text: truncateText(text) }], details: { ...result, url: params.url } };
319
+ // Return the PNG as a real image block so multimodal models see it.
320
+ const content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [
321
+ { type: "text", text: truncateText(text) },
322
+ ];
323
+ if (screenshot) content.push({ type: "image", data: screenshot, mimeType: mime || "image/png" });
324
+ return { content, details: { ...result, url: params.url } };
355
325
  },
356
326
  });
357
327
 
@@ -362,10 +332,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
362
332
  description:
363
333
  "PDF document via Crawl4AI.",
364
334
  promptSnippet: "PDF a webpage",
365
- promptGuidelines: [
366
- "Printable or archivable page snapshot.",
367
- "Returns base64 PDF string.",
368
- ],
335
+ promptGuidelines: ["Printable/archivable page snapshot; returns base64 PDF."],
369
336
  parameters: Type.Object({
370
337
  url: Type.String(),
371
338
  ...crawl4aiControlSchema,
@@ -392,11 +359,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
392
359
  description:
393
360
  "Show web provider config status without printing secrets.",
394
361
  promptSnippet: "Check web provider config and server status",
395
- promptGuidelines: [
396
- "Check which backends are configured and their server status.",
397
- "Never prints secrets — reports only presence and source.",
398
- "apiKeyFound:false is normal for self-hosted Firecrawl; check ready field.",
399
- ],
362
+ promptGuidelines: ["Reports backend presence/health; never prints secrets."],
400
363
  parameters: Type.Object({}),
401
364
  async execute(_id: string, _params: Record<string, unknown>, signal: AbortSignal, _onUpdate: unknown, ctx: any) {
402
365
  const cwd = cwdFromContext(ctx);
@@ -17,7 +17,7 @@ const cp = _require("node:child_process") as typeof import("node:child_process")
17
17
  const AGY_FETCH_TIMEOUT_MS = 90_000; // agy needs time for model call + web fetch
18
18
  const AGY_PROBE_TIMEOUT_MS = 5_000;
19
19
  const AGY_MAX_OUTPUT_BYTES = 200_000; // bound output to protect Pi context
20
- export const AGY_MODEL = "gemini-3.6-flash-medium"; // ponytail: fixed default; users needing model control use agy_execute
20
+ export const AGY_MODEL = "gemini-3.7-flash-medium"; // ponytail: fixed default; users needing model control use agy_execute
21
21
 
22
22
  // Cache install status with a TTL — spawnSync blocks the event loop up to
23
23
  // AGY_PROBE_TIMEOUT_MS, and web_status/extract can call this repeatedly.
@@ -59,6 +59,16 @@ export async function fetchReadableContent(
59
59
  signal: signalWithTimeout(timeoutMs, signal),
60
60
  });
61
61
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
62
+ // Raw text/JSON payloads (raw.githubusercontent.com, JSON APIs) — Readability
63
+ // shreds them to nothing. Pass through verbatim. text/html and text/xml keep
64
+ // the Readability path — they're ordinary web pages. Session mining: 9/40
65
+ // static extract failures were raw-text/JSON shapes.
66
+ const contentType = (response.headers.get("content-type") ?? "").split(";")[0].trim();
67
+ if ((contentType.startsWith("text/") && contentType !== "text/html" && contentType !== "text/xml") || contentType === "application/json") {
68
+ const body = await response.text();
69
+ const markdown = contentType === "application/json" ? "```json\n" + body + "\n```" : body;
70
+ return { title: "", markdown: markdown.slice(0, 20000) };
71
+ }
62
72
  const html = await response.text();
63
73
  const deps = loadReadableContentDependencies();
64
74
  const dom = new deps.JSDOM(html, { url });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-web",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, and Crawl4AI headless browser crawling.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,7 +59,13 @@
59
59
  "@types/node": "^26.0.1",
60
60
  "@types/turndown": "^5.0.6",
61
61
  "chai": "^6.2.2",
62
- "mocha": "^11.7.6",
62
+ "mocha": "^11.8.0",
63
63
  "tsx": "^4.22.4"
64
+ },
65
+ "overrides": {
66
+ "serialize-javascript@>=5.0.0 <7.0.5": "^7.0.5",
67
+ "js-yaml@>=4.0.0 <4.3.1": "^4.3.1",
68
+ "brace-expansion@>=2.0.0 <2.1.4": "^2.1.4",
69
+ "diff": "^8.0.3"
64
70
  }
65
71
  }