@juicesharp/rpiv-web-tools 0.1.2 → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 juicesharp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Pi extension that registers the `web_search` and `web_fetch` tools, backed by
4
4
  the Brave Search API. Also ships `/web-search-config` for interactive API
5
5
  key configuration.
6
6
 
7
- ![Brave Search API key prompt](https://raw.githubusercontent.com/juicesharp/rpiv-web-tools/main/docs/config.jpg)
7
+ ![Brave Search API key prompt](https://raw.githubusercontent.com/juicesharp/rpiv-mono/main/packages/rpiv-web-tools/docs/config.jpg)
8
8
 
9
9
  ## Installation
10
10
 
package/index.ts CHANGED
@@ -13,20 +13,20 @@
13
13
  * Use the /web-search-config slash command to set the key interactively.
14
14
  */
15
15
 
16
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { mkdtemp, writeFile } from "node:fs/promises";
18
+ import { homedir, tmpdir } from "node:os";
19
+ import { dirname, join } from "node:path";
16
20
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
17
21
  import {
18
22
  DEFAULT_MAX_BYTES,
19
23
  DEFAULT_MAX_LINES,
20
24
  formatSize,
21
- truncateHead,
22
25
  type TruncationResult,
26
+ truncateHead,
23
27
  } from "@mariozechner/pi-coding-agent";
24
28
  import { Text } from "@mariozechner/pi-tui";
25
29
  import { Type } from "@sinclair/typebox";
26
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
27
- import { mkdtemp, writeFile } from "node:fs/promises";
28
- import { homedir, tmpdir } from "node:os";
29
- import { dirname, join } from "node:path";
30
30
 
31
31
  // ---------------------------------------------------------------------------
32
32
  // Config file persistence
@@ -49,7 +49,7 @@ function loadConfig(): WebToolsConfig {
49
49
 
50
50
  function saveConfig(config: WebToolsConfig): void {
51
51
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
52
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
52
+ writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
53
53
  try {
54
54
  chmodSync(CONFIG_PATH, 0o600);
55
55
  } catch {
@@ -59,9 +59,9 @@ function saveConfig(config: WebToolsConfig): void {
59
59
 
60
60
  function resolveApiKey(): string | undefined {
61
61
  const envKey = process.env.BRAVE_SEARCH_API_KEY;
62
- if (envKey && envKey.trim()) return envKey.trim();
62
+ if (envKey?.trim()) return envKey.trim();
63
63
  const config = loadConfig();
64
- if (config.apiKey && config.apiKey.trim()) return config.apiKey.trim();
64
+ if (config.apiKey?.trim()) return config.apiKey.trim();
65
65
  return undefined;
66
66
  }
67
67
 
@@ -80,16 +80,10 @@ interface SearchResponse {
80
80
  query: string;
81
81
  }
82
82
 
83
- async function searchBrave(
84
- query: string,
85
- maxResults: number,
86
- signal?: AbortSignal,
87
- ): Promise<SearchResponse> {
83
+ async function searchBrave(query: string, maxResults: number, signal?: AbortSignal): Promise<SearchResponse> {
88
84
  const apiKey = resolveApiKey();
89
85
  if (!apiKey) {
90
- throw new Error(
91
- "BRAVE_SEARCH_API_KEY is not set. Run /web-search-config to configure, or export the env var.",
92
- );
86
+ throw new Error("BRAVE_SEARCH_API_KEY is not set. Run /web-search-config to configure, or export the env var.");
93
87
  }
94
88
 
95
89
  const url = new URL("https://api.search.brave.com/res/v1/web/search");
@@ -175,8 +169,8 @@ export default function (pi: ExtensionAPI) {
175
169
  promptSnippet: "Search the web for up-to-date information via Brave",
176
170
  promptGuidelines: [
177
171
  "Use web_search for information beyond your training data — recent events, current library versions, live API documentation.",
178
- "Use the current year from \"Current date:\" in your context when searching for recent information or documentation.",
179
- "After answering using search results, include a \"Sources:\" section listing relevant URLs as markdown hyperlinks: [Title](URL). Never skip this.",
172
+ 'Use the current year from "Current date:" in your context when searching for recent information or documentation.',
173
+ 'After answering using search results, include a "Sources:" section listing relevant URLs as markdown hyperlinks: [Title](URL). Never skip this.',
180
174
  "Domain filtering is supported to include or block specific websites.",
181
175
  "If BRAVE_SEARCH_API_KEY is not set, ask the user to run /web-search-config before proceeding.",
182
176
  ],
@@ -199,51 +193,38 @@ export default function (pi: ExtensionAPI) {
199
193
 
200
194
  onUpdate?.({
201
195
  content: [{ type: "text", text: `Searching Brave for: "${params.query}"...` }],
196
+ details: { query: params.query, backend: "brave", resultCount: 0 },
202
197
  });
203
198
 
204
- try {
205
- const response = await searchBrave(params.query, maxResults, signal);
206
-
207
- if (response.results.length === 0) {
208
- return {
209
- content: [
210
- {
211
- type: "text",
212
- text: `No results found for "${params.query}".`,
213
- },
214
- ],
215
- details: { query: params.query, backend: "brave", resultCount: 0 },
216
- };
217
- }
218
-
219
- let text = `**Search results for "${response.query}":**\n\n`;
220
- for (let i = 0; i < response.results.length; i++) {
221
- const r = response.results[i];
222
- text += `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}\n\n`;
223
- }
199
+ const response = await searchBrave(params.query, maxResults, signal);
224
200
 
225
- return {
226
- content: [{ type: "text", text: text.trimEnd() }],
227
- details: {
228
- query: params.query,
229
- backend: "brave",
230
- resultCount: response.results.length,
231
- results: response.results,
232
- },
233
- };
234
- } catch (err: unknown) {
235
- const message = err instanceof Error ? err.message : String(err);
201
+ if (response.results.length === 0) {
236
202
  return {
237
203
  content: [
238
204
  {
239
205
  type: "text",
240
- text: `Web search failed: ${message}`,
206
+ text: `No results found for "${params.query}".`,
241
207
  },
242
208
  ],
243
- isError: true,
244
- details: { query: params.query, backend: "brave", error: message },
209
+ details: { query: params.query, backend: "brave", resultCount: 0 },
245
210
  };
246
211
  }
212
+
213
+ let text = `**Search results for "${response.query}":**\n\n`;
214
+ for (let i = 0; i < response.results.length; i++) {
215
+ const r = response.results[i];
216
+ text += `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}\n\n`;
217
+ }
218
+
219
+ return {
220
+ content: [{ type: "text", text: text.trimEnd() }],
221
+ details: {
222
+ query: params.query,
223
+ backend: "brave",
224
+ resultCount: response.results.length,
225
+ results: response.results,
226
+ },
227
+ };
247
228
  },
248
229
 
249
230
  renderCall(args, theme, _context) {
@@ -257,9 +238,6 @@ export default function (pi: ExtensionAPI) {
257
238
  return new Text(theme.fg("warning", "Searching..."), 0, 0);
258
239
  }
259
240
  const details = result.details as { resultCount?: number; results?: SearchResult[] };
260
- if (result.isError) {
261
- return new Text(theme.fg("error", "✗ Search failed"), 0, 0);
262
- }
263
241
  const count = details?.resultCount ?? 0;
264
242
  let text = theme.fg("success", `✓ ${count} result${count !== 1 ? "s" : ""}`);
265
243
  if (expanded && details?.results) {
@@ -296,7 +274,7 @@ export default function (pi: ExtensionAPI) {
296
274
  promptGuidelines: [
297
275
  "Use web_fetch to read the full content of a specific URL — documentation pages, blog posts, API references found via web_search.",
298
276
  "web_fetch is complementary to web_search: search finds URLs, fetch reads them.",
299
- "After answering using fetched content, include a \"Sources:\" section with a markdown hyperlink to the fetched URL.",
277
+ 'After answering using fetched content, include a "Sources:" section with a markdown hyperlink to the fetched URL.',
300
278
  "Large responses are truncated and spilled to a temp file — the temp path is reported in the result details.",
301
279
  ],
302
280
  parameters: Type.Object({
@@ -321,13 +299,12 @@ export default function (pi: ExtensionAPI) {
321
299
  throw new Error(`Invalid URL: ${url}`);
322
300
  }
323
301
  if (!["http:", "https:"].includes(parsedUrl.protocol)) {
324
- throw new Error(
325
- `Unsupported URL protocol: ${parsedUrl.protocol}. Only http and https are supported.`,
326
- );
302
+ throw new Error(`Unsupported URL protocol: ${parsedUrl.protocol}. Only http and https are supported.`);
327
303
  }
328
304
 
329
305
  onUpdate?.({
330
306
  content: [{ type: "text", text: `Fetching: ${url}...` }],
307
+ details: { url } as FetchDetails,
331
308
  });
332
309
 
333
310
  const res = await fetch(url, {
@@ -335,8 +312,7 @@ export default function (pi: ExtensionAPI) {
335
312
  redirect: "follow",
336
313
  headers: {
337
314
  "User-Agent": "Mozilla/5.0 (compatible; rpiv-pi/1.0)",
338
- Accept:
339
- "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5",
315
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5",
340
316
  },
341
317
  });
342
318
 
@@ -347,11 +323,7 @@ export default function (pi: ExtensionAPI) {
347
323
  const contentType = res.headers.get("content-type") ?? "";
348
324
  const contentLength = res.headers.get("content-length");
349
325
 
350
- if (
351
- contentType.includes("image/") ||
352
- contentType.includes("video/") ||
353
- contentType.includes("audio/")
354
- ) {
326
+ if (contentType.includes("image/") || contentType.includes("video/") || contentType.includes("audio/")) {
355
327
  throw new Error(`Unsupported content type: ${contentType}. web_fetch supports text pages only.`);
356
328
  }
357
329
 
@@ -417,9 +389,6 @@ export default function (pi: ExtensionAPI) {
417
389
  if (isPartial) {
418
390
  return new Text(theme.fg("warning", "Fetching..."), 0, 0);
419
391
  }
420
- if (result.isError) {
421
- return new Text(theme.fg("error", "✗ Fetch failed"), 0, 0);
422
- }
423
392
  const details = result.details as FetchDetails | undefined;
424
393
  let text = theme.fg("success", "✓ Fetched");
425
394
  if (details?.title) {
@@ -460,9 +429,7 @@ export default function (pi: ExtensionAPI) {
460
429
  const showMode = typeof args === "string" && args.includes("--show");
461
430
 
462
431
  if (showMode) {
463
- const masked = current.apiKey
464
- ? `${current.apiKey.slice(0, 4)}...${current.apiKey.slice(-4)}`
465
- : "(not set)";
432
+ const masked = current.apiKey ? `${current.apiKey.slice(0, 4)}...${current.apiKey.slice(-4)}` : "(not set)";
466
433
  const envMasked = process.env.BRAVE_SEARCH_API_KEY
467
434
  ? `${process.env.BRAVE_SEARCH_API_KEY.slice(0, 4)}...${process.env.BRAVE_SEARCH_API_KEY.slice(-4)}`
468
435
  : "(not set)";
package/package.json CHANGED
@@ -1,24 +1,38 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-web-tools",
3
- "version": "0.1.2",
3
+ "version": "0.7.0",
4
4
  "description": "Pi extension: web_search + web_fetch via the Brave Search API",
5
- "keywords": ["pi-package", "pi-extension", "rpiv", "web-search", "brave"],
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "rpiv",
9
+ "web-search",
10
+ "brave"
11
+ ],
6
12
  "type": "module",
7
13
  "license": "MIT",
8
14
  "author": "juicesharp",
9
15
  "repository": {
10
16
  "type": "git",
11
- "url": "git+https://github.com/juicesharp/rpiv-web-tools.git"
17
+ "url": "git+https://github.com/juicesharp/rpiv-mono.git",
18
+ "directory": "packages/rpiv-web-tools"
12
19
  },
13
- "homepage": "https://github.com/juicesharp/rpiv-web-tools#readme",
20
+ "homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-web-tools#readme",
14
21
  "bugs": {
15
- "url": "https://github.com/juicesharp/rpiv-web-tools/issues"
22
+ "url": "https://github.com/juicesharp/rpiv-mono/issues"
16
23
  },
17
24
  "publishConfig": {
18
25
  "access": "public"
19
26
  },
27
+ "files": [
28
+ "index.ts",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
20
32
  "pi": {
21
- "extensions": ["./index.ts"]
33
+ "extensions": [
34
+ "./index.ts"
35
+ ]
22
36
  },
23
37
  "peerDependencies": {
24
38
  "@mariozechner/pi-coding-agent": "*",
package/docs/config.jpg DELETED
Binary file