@bacnh85/pi-web 0.9.1 → 0.10.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/CHANGELOG.md +32 -0
- package/README.md +28 -1
- package/extensions/index.ts +63 -3
- package/extensions/lib/chatapi.ts +100 -0
- package/extensions/lib/imageapi.ts +23 -0
- package/package.json +2 -2
- package/skills/pi-web/SKILL.md +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-09-13)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`web_chat` tool** — one-off chat completion against any OpenAI-compatible
|
|
8
|
+
gateway (`WEB_CHAT_API_BASE_URL` + optional `WEB_CHAT_API_KEY`): a ChatGPT
|
|
9
|
+
web bridge, official OpenAI (`https://api.openai.com/v1`), or any web2api
|
|
10
|
+
gateway. Non-streaming `POST /chat/completions` with optional `model`/
|
|
11
|
+
`system`; timeout capped at 300 s. Error mapping: 401/403 key hint,
|
|
12
|
+
429 quota, 502 → "empty account pool" hint, 5xx server error.
|
|
13
|
+
- `web_status` now reports `webChat` (configured/baseUrl/keyFound/source).
|
|
14
|
+
|
|
15
|
+
Supersedes the removed `pi-chatgpt-web` package (free web-chat-only models):
|
|
16
|
+
point `WEB_CHAT_API_BASE_URL` at the same gateway to keep using it.
|
|
17
|
+
|
|
18
|
+
## 0.9.2 (2026-09-13)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
Review hardening of the `web_image` fallback chain (3 review rounds):
|
|
23
|
+
|
|
24
|
+
- **Cancellation semantics**: an aborted call now surfaces `AbortError`
|
|
25
|
+
immediately — before any provider client construction or fetch — instead of
|
|
26
|
+
walking the chain and reporting "all providers failed". An abort landing
|
|
27
|
+
mid-generation normalizes to `AbortError` with the in-flight provider error
|
|
28
|
+
preserved as `cause`; foreign abort-named errors from upstreams are recorded
|
|
29
|
+
as provider notes and the chain continues.
|
|
30
|
+
- **`n` transparency**: when the gemini web tier returns fewer images than the
|
|
31
|
+
requested `n`, the result states it explicitly (`n` applies to the
|
|
32
|
+
`zai`/`custom` API providers; the gemini web tier returns its own count).
|
|
33
|
+
`out_dir` now resolves against the session cwd, not the process cwd.
|
|
34
|
+
|
|
3
35
|
## 0.9.1 (2026-09-13)
|
|
4
36
|
|
|
5
37
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bacnh85/pi-web
|
|
2
2
|
|
|
3
|
-
Pi extension for **unified web search, content extraction, site crawling, page capture, Gemini web-tier research,
|
|
3
|
+
Pi extension for **unified web search, content extraction, site crawling, page capture, Gemini web-tier research, free upstream image generation, and one-off gateway chat**.
|
|
4
4
|
|
|
5
5
|
Auto-selects the best backend from SearXNG (self-hosted), Brave Search, Firecrawl, Crawl4AI, and agy (Gemini/Claude, when installed) — so agents don't have to know which backend to use. Search selection is adaptive: broad discovery prefers self-hosted SearXNG, while precision-sensitive searches and inline content prefer Brave. `web_research` adds AI-synthesized research with citations via your gemini.google.com session.
|
|
6
6
|
|
|
@@ -38,6 +38,8 @@ Variables:
|
|
|
38
38
|
| `WEB_IMAGE_API_LABEL` | No | — | Display label for the `custom` endpoint (default: host name) |
|
|
39
39
|
| `WEB_IMAGE_MIN_INTERVAL_MS` | No | `5000` | Min interval between `web_image` calls per provider |
|
|
40
40
|
| `WEB_IMAGE_DAILY_CAP` | No | `20` | Daily soft cap for the Gemini **web tier** `web_image` provider (keyed APIs stay uncapped) |
|
|
41
|
+
| `WEB_CHAT_API_BASE_URL` | No | — | `web_chat`: any OpenAI-compatible `/chat/completions` gateway (a ChatGPT web bridge, `https://api.openai.com/v1`, …) |
|
|
42
|
+
| `WEB_CHAT_API_KEY` | No | — | Bearer key for the `web_chat` gateway |
|
|
41
43
|
|
|
42
44
|
> (1) At least one search backend (SearXNG, Brave, or Firecrawl) must be configured for `web_search`.
|
|
43
45
|
> (2) Required for hosted Firecrawl; optional for self-hosted instances without auth.
|
|
@@ -290,6 +292,9 @@ paths **plus inline image blocks** (multimodal models see the render
|
|
|
290
292
|
immediately). `details` reports the winning provider, model, and fallback
|
|
291
293
|
attempts.
|
|
292
294
|
|
|
295
|
+
`n` (1–4) applies to the API providers (`zai`/`custom`); the Gemini web tier
|
|
296
|
+
returns its own image count (surfaced as a provider note when fewer than `n`).
|
|
297
|
+
|
|
293
298
|
**Guardrails** (soft, in-memory): per-provider `WEB_IMAGE_MIN_INTERVAL_MS`
|
|
294
299
|
(default 5 s) and a `WEB_IMAGE_DAILY_CAP` (default 20/day, applied to the
|
|
295
300
|
Gemini web tier only — keyed APIs are billed upstream and stay uncapped).
|
|
@@ -306,6 +311,28 @@ non-human means", Z.ai alike). This tool therefore follows a risk ladder:
|
|
|
306
311
|
Smoke test: `npx tsx extensions/scripts/gemini-smoke.ts "a red cube on white background" image`
|
|
307
312
|
(or `… zai` for the Z.ai path).
|
|
308
313
|
|
|
314
|
+
### `web_chat` — one-off gateway chat
|
|
315
|
+
|
|
316
|
+
Single non-streaming chat completion against any OpenAI-compatible gateway —
|
|
317
|
+
the in-session equivalent of "ask another model quickly" without switching
|
|
318
|
+
your main provider:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
web_chat(prompt="In one sentence: why is idempotency key needed here?")
|
|
322
|
+
web_chat(prompt="Summarize", model="gpt-5.3-mini", system="Be terse")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Configure once in `~/.pi/agent/.env.local`, then restart pi:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
WEB_CHAT_API_BASE_URL=https://api.openai.com/v1 # or any OpenAI-compatible gateway
|
|
329
|
+
WEB_CHAT_API_KEY=sk-... # if the gateway needs a key
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`web_status.webChat` shows configuration without printing secrets. Chat-only
|
|
333
|
+
by design (no tool calling through gateways); for grounded research with
|
|
334
|
+
sources use `web_research`, and `/model` switches your main model.
|
|
335
|
+
|
|
309
336
|
## Library structure
|
|
310
337
|
|
|
311
338
|
| Module | Contents |
|
package/extensions/index.ts
CHANGED
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
imageRateSnapshot,
|
|
39
39
|
type ImageProvider,
|
|
40
40
|
} from "./lib/imageapi";
|
|
41
|
+
import {
|
|
42
|
+
chatgptChat,
|
|
43
|
+
describeChatApiError,
|
|
44
|
+
loadChatConfig,
|
|
45
|
+
} from "./lib/chatapi";
|
|
41
46
|
import { extractWithDiagnostics, type ExtractMode } from "./lib/extract";
|
|
42
47
|
import { firecrawlRequest, type FirecrawlResult } from "./lib/firecrawl";
|
|
43
48
|
import {
|
|
@@ -109,6 +114,7 @@ const WEB_ROUTING_GUIDANCE = `## Web Tool Routing (pi-web)
|
|
|
109
114
|
- **web_screenshot** / **web_pdf** — page capture (Crawl4AI).
|
|
110
115
|
- **web_research** — AI-synthesized research via Gemini web (mode "ask" = grounded answer, guest OK; mode "research" = Deep Research report, needs cookie + Gemini Advanced, takes minutes).
|
|
111
116
|
- **web_image** — text→image generation via free upstreams (auto: Gemini web → Z.ai GLM-Image → custom OpenAI-images endpoint; \`model\`/\`n\` params).
|
|
117
|
+
- **web_chat** — one-off chat completion via an OpenAI-compatible gateway (\`WEB_CHAT_API_BASE_URL\`; non-streaming).
|
|
112
118
|
- **web_status** — provider config + health.
|
|
113
119
|
|
|
114
120
|
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.`;
|
|
@@ -541,7 +547,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
541
547
|
{ default: "auto", description: "auto = gemini → zai (if ZAI_API_KEY) → custom (if WEB_IMAGE_API_BASE_URL); pin one to skip fallback." },
|
|
542
548
|
)),
|
|
543
549
|
model: Type.Optional(Type.String({ description: "Provider-specific model (e.g. glm-image, or a Gemini image-capable model id). Omit for the provider default." })),
|
|
544
|
-
n: Type.Optional(Type.Number({ default: 1, description: "Number of images, 1-4." })),
|
|
550
|
+
n: Type.Optional(Type.Number({ default: 1, description: "Number of images, 1-4 (applies to zai/custom; the gemini web tier returns its own count)." })),
|
|
545
551
|
out_dir: Type.Optional(Type.String({ description: "Directory for saved images (default: fresh temp dir)." })),
|
|
546
552
|
...sharedControlSchema,
|
|
547
553
|
}),
|
|
@@ -552,7 +558,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
552
558
|
const n = Math.min(Math.max(Math.trunc((params.n as number) ?? 1) || 1, 1), 4);
|
|
553
559
|
const timeoutMs = Math.min(Math.max((params.timeout_ms as number) ?? 180_000, 10_000), 600_000);
|
|
554
560
|
const outDir = params.out_dir
|
|
555
|
-
? path.resolve(String(params.out_dir))
|
|
561
|
+
? path.resolve(cwd, String(params.out_dir))
|
|
556
562
|
: await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-web-image-"));
|
|
557
563
|
const result = await generateImageWithFallback({
|
|
558
564
|
prompt,
|
|
@@ -574,7 +580,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
574
580
|
...(result.urls.length
|
|
575
581
|
? [`Not saved (image host unreachable from this machine — open directly):`, ...result.urls.map((u) => ` ${u}`)]
|
|
576
582
|
: []),
|
|
577
|
-
result.attempts.length ? `
|
|
583
|
+
result.attempts.length ? `Provider notes: ${result.attempts.join(" | ")}` : null,
|
|
578
584
|
].filter(Boolean).join("\n");
|
|
579
585
|
const content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [
|
|
580
586
|
{ type: "text" as const, text },
|
|
@@ -584,6 +590,51 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
584
590
|
},
|
|
585
591
|
});
|
|
586
592
|
|
|
593
|
+
// ── web_chat ─────────────────────────────────────────────────────────
|
|
594
|
+
pi.registerTool({
|
|
595
|
+
name: "web_chat",
|
|
596
|
+
label: "Web Chat (gateway)",
|
|
597
|
+
description:
|
|
598
|
+
"One-off chat completion via an OpenAI-compatible gateway (WEB_CHAT_API_BASE_URL — a ChatGPT web bridge, official OpenAI, or any web2api gateway). Non-streaming Q&A; not a provider — use /model to switch your main model.",
|
|
599
|
+
promptSnippet: "One-off chat via OpenAI-compatible gateway",
|
|
600
|
+
promptGuidelines: [
|
|
601
|
+
"Use for a quick one-off second opinion, classification, or short generation call. Grounded research with sources → web_research; switching your main chat model → /model.",
|
|
602
|
+
],
|
|
603
|
+
parameters: Type.Object({
|
|
604
|
+
prompt: Type.String({ description: "The question or instruction." }),
|
|
605
|
+
model: Type.Optional(Type.String({ description: "Gateway model id (e.g. gpt-5.3-mini). Omit for the gateway default." })),
|
|
606
|
+
system: Type.Optional(Type.String({ description: "Optional system prompt." })),
|
|
607
|
+
...sharedControlSchema,
|
|
608
|
+
}),
|
|
609
|
+
async execute(_id: string, params: Record<string, unknown>, signal: AbortSignal, _onUpdate: unknown, ctx: any) {
|
|
610
|
+
const cwd = cwdFromContext(ctx);
|
|
611
|
+
const trusted = includeProjectEnv(ctx);
|
|
612
|
+
const config = loadChatConfig(cwd, trusted);
|
|
613
|
+
if (!config) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
"web_chat is not configured. Set WEB_CHAT_API_BASE_URL (and optional WEB_CHAT_API_KEY) in ~/.pi/agent/.env.local — any OpenAI-compatible /chat/completions gateway works (a ChatGPT web bridge, https://api.openai.com/v1, …) — then restart pi.",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const timeoutMs = Math.min(Math.max((params.timeout_ms as number) ?? 120_000, 10_000), 300_000);
|
|
619
|
+
try {
|
|
620
|
+
const result = await chatgptChat({
|
|
621
|
+
baseUrl: config.baseUrl,
|
|
622
|
+
apiKey: config.apiKey,
|
|
623
|
+
prompt: params.prompt as string,
|
|
624
|
+
model: params.model as string | undefined,
|
|
625
|
+
system: params.system as string | undefined,
|
|
626
|
+
timeoutMs,
|
|
627
|
+
signal,
|
|
628
|
+
});
|
|
629
|
+
const text = `Model: ${result.model ?? "gateway default"}\n\n${result.text}`;
|
|
630
|
+
return { content: [{ type: "text" as const, text: truncateText(text) }], details: { model: result.model } };
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if ((err as Error)?.name === "AbortError") throw err;
|
|
633
|
+
throw new Error(describeChatApiError(err));
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
587
638
|
// ── web_status ───────────────────────────────────────────────────────
|
|
588
639
|
pi.registerTool({
|
|
589
640
|
name: "web_status",
|
|
@@ -639,6 +690,15 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
639
690
|
: { configured: false },
|
|
640
691
|
rate: imageRateSnapshot(),
|
|
641
692
|
},
|
|
693
|
+
webChat: (() => {
|
|
694
|
+
const cfg = loadChatConfig(cwd, trusted);
|
|
695
|
+
return {
|
|
696
|
+
configured: Boolean(cfg),
|
|
697
|
+
baseUrl: cfg?.baseUrl,
|
|
698
|
+
keyFound: Boolean(cfg?.apiKey),
|
|
699
|
+
source: cfg?.source ?? "not set",
|
|
700
|
+
};
|
|
701
|
+
})(),
|
|
642
702
|
localChrome: { path: findChromeBinary() ?? "not found" },
|
|
643
703
|
};
|
|
644
704
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Generic OpenAI-compatible chat client for web_chat — one-off Q&A against
|
|
2
|
+
// any OpenAI-compatible /chat/completions gateway (WEB_CHAT_API_BASE_URL):
|
|
3
|
+
// a ChatGPT web bridge, official OpenAI, or any web2api gateway.
|
|
4
|
+
|
|
5
|
+
import { findEnvValue } from "./config";
|
|
6
|
+
import { raceGuard } from "./gemini";
|
|
7
|
+
import type { FetchLike } from "./imageapi";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Config
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface ChatConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
source: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadChatConfig(cwd = process.cwd(), includeCwdEnv = false): ChatConfig | null {
|
|
20
|
+
const base = findEnvValue("WEB_CHAT_API_BASE_URL", cwd, includeCwdEnv);
|
|
21
|
+
if (!base.value) return null;
|
|
22
|
+
const key = findEnvValue("WEB_CHAT_API_KEY", cwd, includeCwdEnv);
|
|
23
|
+
return {
|
|
24
|
+
baseUrl: base.value.replace(/\/+$/, ""),
|
|
25
|
+
...(key.value ? { apiKey: key.value } : {}),
|
|
26
|
+
source: base.source,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Error mapping
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export class ChatApiError extends Error {
|
|
35
|
+
constructor(
|
|
36
|
+
public status: number,
|
|
37
|
+
message: string,
|
|
38
|
+
) {
|
|
39
|
+
super(message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function describeChatApiError(err: unknown): string {
|
|
44
|
+
if (err instanceof ChatApiError) {
|
|
45
|
+
if (err.status === 401 || err.status === 403) return `gateway rejected the API key (HTTP ${err.status}): ${err.message}`;
|
|
46
|
+
if (err.status === 429) return `gateway rate limit/quota exhausted (HTTP 429): ${err.message}`;
|
|
47
|
+
if (err.status === 502) return `gateway returned 502 — likely an empty account pool (add an account via the gateway's admin panel): ${err.message}`;
|
|
48
|
+
if (err.status >= 500) return `gateway server error (HTTP ${err.status}): ${err.message}`;
|
|
49
|
+
return `gateway error (HTTP ${err.status}): ${err.message}`;
|
|
50
|
+
}
|
|
51
|
+
return err instanceof Error ? err.message : String(err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Single non-streaming completion
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export interface ChatResult {
|
|
59
|
+
text: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function chatgptChat(opts: {
|
|
64
|
+
baseUrl: string;
|
|
65
|
+
apiKey?: string;
|
|
66
|
+
prompt: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
system?: string;
|
|
69
|
+
timeoutMs?: number;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
fetchImpl?: FetchLike;
|
|
72
|
+
}): Promise<ChatResult> {
|
|
73
|
+
const fetchImpl = opts.fetchImpl ?? (fetch as FetchLike);
|
|
74
|
+
const messages = [
|
|
75
|
+
...(opts.system ? [{ role: "system", content: opts.system }] : []),
|
|
76
|
+
{ role: "user", content: opts.prompt },
|
|
77
|
+
];
|
|
78
|
+
const res = await raceGuard(
|
|
79
|
+
fetchImpl(`${opts.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json", ...(opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {}) },
|
|
82
|
+
body: JSON.stringify({ model: opts.model, messages, stream: false }),
|
|
83
|
+
signal: opts.signal,
|
|
84
|
+
}),
|
|
85
|
+
{ signal: opts.signal, timeoutMs: opts.timeoutMs ?? 120_000, label: "web_chat" },
|
|
86
|
+
);
|
|
87
|
+
const payload = (await res.json().catch(() => null)) as Record<string, unknown> | null;
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const errObj = payload?.error as { message?: string } | undefined;
|
|
90
|
+
const msg =
|
|
91
|
+
errObj?.message ??
|
|
92
|
+
(typeof payload?.message === "string" ? payload.message : undefined) ??
|
|
93
|
+
(payload ? JSON.stringify(payload).slice(0, 300) : res.statusText ?? "");
|
|
94
|
+
throw new ChatApiError(res.status, String(msg));
|
|
95
|
+
}
|
|
96
|
+
const choices = Array.isArray(payload?.choices) ? (payload!.choices as Array<{ message?: { content?: unknown } }>) : [];
|
|
97
|
+
const text = String(choices[0]?.message?.content ?? "");
|
|
98
|
+
if (!text) throw new Error(`gateway returned an empty completion${opts.model ? ` (model ${opts.model})` : ""}`);
|
|
99
|
+
return { text, model: typeof payload?.model === "string" ? payload.model : opts.model };
|
|
100
|
+
}
|
|
@@ -308,6 +308,13 @@ export async function generateImageWithFallback(params: ImageChainParams): Promi
|
|
|
308
308
|
const chain = chainFor(params.provider);
|
|
309
309
|
const attempts: string[] = [];
|
|
310
310
|
for (const provider of chain) {
|
|
311
|
+
// Cancelled calls skip fallback entirely — before any provider client
|
|
312
|
+
// construction or fetch invocation.
|
|
313
|
+
if (params.signal?.aborted) {
|
|
314
|
+
const abortErr = new Error("web_image aborted");
|
|
315
|
+
abortErr.name = "AbortError";
|
|
316
|
+
throw abortErr;
|
|
317
|
+
}
|
|
311
318
|
const configured =
|
|
312
319
|
provider === "gemini" ? true : provider === "zai" ? Boolean(params.apiConfig.zai) : Boolean(params.apiConfig.custom);
|
|
313
320
|
if (!configured) {
|
|
@@ -356,8 +363,24 @@ export async function generateImageWithFallback(params: ImageChainParams): Promi
|
|
|
356
363
|
});
|
|
357
364
|
}
|
|
358
365
|
imageRateRecord(provider);
|
|
366
|
+
if (provider === "gemini" && params.n && params.n > 1 && result.paths.length < params.n) {
|
|
367
|
+
attempts.push(`gemini: n=${params.n} requested — the gemini web tier returns its own image count (${result.paths.length}); n applies to zai/custom`);
|
|
368
|
+
}
|
|
359
369
|
return { provider, model: result.model, paths: result.paths, urls: result.urls ?? [], attempts };
|
|
360
370
|
} catch (err) {
|
|
371
|
+
// Cancellation is not a provider failure: rethrow so aborted tool calls
|
|
372
|
+
// surface as AbortError instead of an "all providers failed" listing —
|
|
373
|
+
// even when a genuine provider error (AuthError, a failed save, …) was
|
|
374
|
+
// the error in flight when the abort landed.
|
|
375
|
+
if (params.signal?.aborted) {
|
|
376
|
+
if ((err as Error)?.name === "AbortError") throw err;
|
|
377
|
+
// cause keeps the in-flight provider error for diagnostics.
|
|
378
|
+
const abortErr = new Error("web_image aborted", { cause: err });
|
|
379
|
+
abortErr.name = "AbortError";
|
|
380
|
+
throw abortErr;
|
|
381
|
+
}
|
|
382
|
+
// A foreign AbortError-named error (not from the caller's signal) is a
|
|
383
|
+
// provider failure like any other — record it and keep the chain going.
|
|
361
384
|
attempts.push(`${provider}: ${provider === "gemini" ? describeGeminiError(err) : describeImageApiError(err)}`);
|
|
362
385
|
}
|
|
363
386
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research,
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research, free upstream image generation, and one-off gateway chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
package/skills/pi-web/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-web
|
|
3
|
-
description: Web search, content extraction, site crawling, page capture, Gemini web-tier research,
|
|
3
|
+
description: Web search, content extraction, site crawling, page capture, Gemini web-tier research, free upstream image generation, and one-off gateway chat via the pi-web extension. Use when the user needs current web search results, documentation lookup, factual research, AI-synthesized research with sources (Gemini Deep Research), image generation from text (Gemini web, Z.ai GLM-Image), one-off ChatGPT/gateway chat, source discovery, URL-to-markdown extraction, JSON extraction from websites, site URL discovery, site crawling, or page screenshots/PDFs. Use when the user mentions searching the web, finding docs, looking something up, researching deeply, generating/creating an image, asking another model, scraping/extracting content from a URL, or capturing a page.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pi-web — Unified Web Tools
|
|
7
7
|
|
|
8
|
-
Use the **
|
|
8
|
+
Use the **10 unified tools** from the `pi-web` extension for all web-related tasks. These tools automatically select the best backend from SearXNG, Brave Search, Firecrawl, Crawl4AI, and agy (when installed) — you don't need to know which backend to use. Search selection is adaptive: broad discovery prefers self-hosted SearXNG, while precision-sensitive queries and inline content prefer Brave.
|
|
9
9
|
|
|
10
10
|
## Quick Reference
|
|
11
11
|
|
|
@@ -19,6 +19,7 @@ Use the **9 unified tools** from the `pi-web` extension for all web-related task
|
|
|
19
19
|
| `web_pdf` | Generate page PDF | Crawl4AI daemon (public URLs) or local headless Chrome (localhost/LAN/file URLs — auto-detected) |
|
|
20
20
|
| `web_research` | AI-synthesized research with sources | Gemini web tier: ask = grounded answer (guest OK); research = Deep Research report (cookie + Gemini Advanced) |
|
|
21
21
|
| `web_image` | Generate images from a text prompt | Gemini web (guest/cookie) → Z.ai GLM-Image (`ZAI_API_KEY`) → custom OpenAI-images endpoint |
|
|
22
|
+
| `web_chat` | One-off chat via an OpenAI-compatible gateway | `WEB_CHAT_API_BASE_URL` (ChatGPT web bridge, official OpenAI, …) |
|
|
22
23
|
| `web_status` | Check provider configuration and health | — |
|
|
23
24
|
|
|
24
25
|
## Decision Tree
|
|
@@ -65,6 +66,9 @@ What do you need?
|
|
|
65
66
|
│ ├─ default: provider=auto (Gemini web → Z.ai GLM-Image → custom endpoint)
|
|
66
67
|
│ └─ pin/model: provider=zai model=glm-image, or any custom OpenAI-images endpoint
|
|
67
68
|
│
|
|
69
|
+
├── One-off chat with another model (second opinion, classification)
|
|
70
|
+
│ → web_chat (WEB_CHAT_API_BASE_URL gateway; non-streaming; no tools)
|
|
71
|
+
│
|
|
68
72
|
└── Check what web tools are configured
|
|
69
73
|
→ web_status
|
|
70
74
|
```
|