@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 +33 -0
- package/README.md +1 -1
- package/extensions/index.ts +32 -69
- package/extensions/lib/agy.ts +1 -1
- package/extensions/lib/content.ts +10 -0
- package/package.json +8 -2
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
|
|
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"
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
-
|
|
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 queries — prefer 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
|
|
244
|
-
urls: Type.Optional(Type.Array(Type.String(), { description: "URLs for
|
|
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
|
|
248
|
-
exclude_paths: Type.Optional(Type.String({ description: "Comma-separated paths
|
|
249
|
-
poll: Type.Optional(Type.Boolean({ default: false, description: "Poll
|
|
250
|
-
browser_config: Type.Optional(Type.Any({ description: "BrowserConfig JSON (full mode
|
|
251
|
-
crawler_config: Type.Optional(Type.Any({ description: "CrawlerRunConfig JSON (full mode
|
|
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
|
|
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
|
-
|
|
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);
|
package/extensions/lib/agy.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
}
|