@juicesharp/rpiv-web-tools 0.1.1 → 0.6.1
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 +21 -0
- package/README.md +1 -9
- package/index.ts +39 -72
- package/package.json +20 -6
- package/docs/config.jpg +0 -0
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
|
-

|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -33,14 +33,6 @@ First match wins:
|
|
|
33
33
|
1. `BRAVE_SEARCH_API_KEY` environment variable
|
|
34
34
|
2. `apiKey` field in `~/.config/rpiv-web-tools/config.json`
|
|
35
35
|
|
|
36
|
-
## Migration from rpiv-pi ≤ 0.3.0
|
|
37
|
-
|
|
38
|
-
If you configured a Brave API key while rpiv-pi bundled this tool, it lived
|
|
39
|
-
at `~/.config/rpiv-pi/web-tools.json`. The new plugin reads
|
|
40
|
-
`~/.config/rpiv-web-tools/config.json` only — run `/web-search-config` once
|
|
41
|
-
to re-enter your key, or continue using the `BRAVE_SEARCH_API_KEY` env var
|
|
42
|
-
(which takes precedence and keeps working unchanged).
|
|
43
|
-
|
|
44
36
|
## License
|
|
45
37
|
|
|
46
38
|
MIT
|
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)
|
|
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
|
|
62
|
+
if (envKey?.trim()) return envKey.trim();
|
|
63
63
|
const config = loadConfig();
|
|
64
|
-
if (config.apiKey
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `
|
|
206
|
+
text: `No results found for "${params.query}".`,
|
|
241
207
|
},
|
|
242
208
|
],
|
|
243
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Pi extension: web_search + web_fetch via the Brave Search API",
|
|
5
|
-
"keywords": [
|
|
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-
|
|
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-
|
|
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": [
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./index.ts"
|
|
35
|
+
]
|
|
22
36
|
},
|
|
23
37
|
"peerDependencies": {
|
|
24
38
|
"@mariozechner/pi-coding-agent": "*",
|
package/docs/config.jpg
DELETED
|
Binary file
|