@juicesharp/rpiv-web-tools 1.17.0 → 1.17.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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  </a>
9
9
  </div>
10
10
 
11
- Let the model search the web and read pages. `rpiv-web-tools` adds `web_search` and `web_fetch` tools to [Pi Agent](https://github.com/badlogic/pi-mono) with pluggable providers (Brave, Tavily, Serper, Exa, Jina, Firecrawl, [SearXNG](https://docs.searxng.org/), [Ollama](https://ollama.com)), plus `/web-tools` for interactive provider selection and API-key setup.
11
+ Let the model search the web and read pages. `rpiv-web-tools` adds `web_search` and `web_fetch` tools to [Pi Agent](https://github.com/badlogic/pi-mono) with pluggable providers (Brave, Tavily, Serper, Exa, You.com, Jina, Firecrawl, Perplexity, [SearXNG](https://docs.searxng.org/), [Ollama](https://ollama.com)), plus `/web-tools` for interactive provider selection and API-key setup.
12
12
 
13
13
  ![Provider selection prompt](https://raw.githubusercontent.com/juicesharp/rpiv-mono/main/packages/rpiv-web-tools/docs/config.jpg)
14
14
 
@@ -22,14 +22,16 @@ Pick one as the active backend; switch any time without losing the others' keys.
22
22
  | Tavily | `TAVILY_API_KEY` | [tavily.com](https://tavily.com) | native extraction (plain text) | |
23
23
  | Serper | `SERPER_API_KEY` | [serper.dev](https://serper.dev) | raw HTTP → htmlToText, `raw: true` available | |
24
24
  | Exa | `EXA_API_KEY` | [exa.ai](https://exa.ai) | native extraction (plain text) | |
25
+ | You.com | `YOUCOM_API_KEY` | [you.com](https://you.com) | native extraction (markdown) | $5/1K search + $1/1K fetch |
25
26
  | Jina | `JINA_API_KEY` | [jina.ai/reader](https://jina.ai/reader) | native extraction (markdown) | |
26
27
  | Firecrawl | `FIRECRAWL_API_KEY` | [firecrawl.dev](https://firecrawl.dev) | native extraction (markdown) | |
28
+ | Perplexity | `PERPLEXITY_API_KEY` | [docs.perplexity.ai](https://docs.perplexity.ai/) | raw HTTP → htmlToText, `raw: true` available | |
27
29
  | SearXNG | `SEARXNG_URL` (+ optional `SEARXNG_API_KEY`) | self-hosted | raw HTTP → htmlToText, `raw: true` available | see [§SearXNG](#searxng-self-hosted) |
28
30
  | Ollama | `OLLAMA_HOST` / `OLLAMA_API_KEY` | local or [ollama.com](https://ollama.com) | native extraction | see [§Ollama](#ollama-local-or-cloud) |
29
31
 
30
32
  ## Features
31
33
 
32
- - **Read any URL** - fetch http/https pages with HTML-to-text extraction, or get the raw response with `raw: true` (honoured by Brave/Serper/SearXNG; extraction providers — Tavily/Exa/Jina/Firecrawl/Ollama — always return their parsed text).
34
+ - **Read any URL** - fetch http/https pages with HTML-to-text extraction, or get the raw response with `raw: true` (honoured by Brave/Serper/Perplexity/SearXNG; extraction providers — Tavily/Exa/You.com/Jina/Firecrawl/Ollama — always return their parsed text).
33
35
  - **GitHub URL interceptor (opt-in)** - github.com URLs route through `gh`/`git` for full repository content (file tree, README, individual file contents) instead of the rendered HTML page. Off by default; enable per-user via config or per-consumer at registration time. See [§GitHub URL interceptor](#github-url-interceptor).
34
36
  - **Large-page spillover** - oversized responses truncate inline and spill the full body to a temp file the model can read on demand.
35
37
  - **SSRF guard** - refuses loopback, RFC 1918, link-local, and cloud-metadata addresses (`localhost`, `127.0.0.0/8`, `10.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`, `::1`, `fc00::/7`, `fe80::/10`).
@@ -49,8 +51,8 @@ Then restart your Pi session.
49
51
  1–10 results per call.
50
52
  - **`web_fetch`** - read an http/https URL. Lookup order: opt-in URL interceptors
51
53
  (see [§GitHub URL interceptor](#github-url-interceptor)), then the active provider's native
52
- fetch endpoint when it has one (Tavily/Exa/Jina/Firecrawl/Ollama → vendor extraction;
53
- Brave/Serper/SearXNG → shared raw HTTP + HTML-to-text fallback). Large responses truncate
54
+ fetch endpoint when it has one (Tavily/Exa/You.com/Jina/Firecrawl/Ollama → vendor extraction;
55
+ Brave/Serper/Perplexity/SearXNG → shared raw HTTP + HTML-to-text fallback). Large responses truncate
54
56
  inline and spill the full body to a temp file the model can read on demand.
55
57
 
56
58
  ### Schema - `web_search`
@@ -69,7 +71,7 @@ Returns:
69
71
  content: [{ type: "text", text: string }], // markdown list of "**title**\n url\n snippet"
70
72
  details: {
71
73
  query: string,
72
- backend: "brave" | "tavily" | "serper" | "exa" | "jina" | "firecrawl" | "searxng" | "ollama",
74
+ backend: "brave" | "tavily" | "serper" | "exa" | "youcom" | "jina" | "firecrawl" | "perplexity" | "searxng" | "ollama",
73
75
  resultCount: number,
74
76
  results?: Array<{ title: string, url: string, snippet: string }>,
75
77
  }
@@ -103,7 +105,7 @@ Returns:
103
105
  }
104
106
  ```
105
107
 
106
- Throws on invalid URL, non-http(s) protocol, private/loopback hostnames (SSRF guard), non-2xx response, or `image/` / `video/` / `audio/` content types. Extraction providers (Tavily/Exa/Jina/Firecrawl) additionally throw when the API returns an empty body or a vendor-level failure (e.g. Firecrawl `success: false`, Tavily `failed_results`).
108
+ Throws on invalid URL, non-http(s) protocol, private/loopback hostnames (SSRF guard), non-2xx response, or `image/` / `video/` / `audio/` content types. Extraction providers (Tavily/Exa/You.com/Jina/Firecrawl) additionally throw when the API returns an empty body or a vendor-level failure (e.g. Firecrawl `success: false`, Tavily `failed_results`).
107
109
 
108
110
  ## Commands
109
111
 
@@ -117,7 +119,7 @@ Throws on invalid URL, non-http(s) protocol, private/loopback hostnames (SSRF gu
117
119
 
118
120
  First match wins:
119
121
 
120
- 1. The active provider's environment variable: `BRAVE_SEARCH_API_KEY`, `TAVILY_API_KEY`, `SERPER_API_KEY`, `EXA_API_KEY`, `JINA_API_KEY`, `FIRECRAWL_API_KEY`, `SEARXNG_API_KEY`, or `OLLAMA_API_KEY`
122
+ 1. The active provider's environment variable: `BRAVE_SEARCH_API_KEY`, `TAVILY_API_KEY`, `SERPER_API_KEY`, `EXA_API_KEY`, `YOUCOM_API_KEY`, `JINA_API_KEY`, `FIRECRAWL_API_KEY`, `PERPLEXITY_API_KEY`, `SEARXNG_API_KEY`, or `OLLAMA_API_KEY`
121
123
  2. `apiKeys.<provider>` field in `~/.config/rpiv-web-tools/config.json`
122
124
  3. Legacy `apiKey` field (Brave only — auto-migrated to the new shape on next save)
123
125
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-web-tools",
3
- "version": "1.17.0",
4
- "description": "Pi extension. Web search and fetch for the model with pluggable providers (Brave, Tavily, Serper, Exa, Jina, Firecrawl, SearXNG, Ollama).",
3
+ "version": "1.17.1",
4
+ "description": "Pi extension. Web search and fetch for the model with pluggable providers (Brave, Tavily, Serper, Exa, You.com, Jina, Firecrawl, Perplexity, SearXNG, Ollama).",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",
@@ -21,8 +21,10 @@
21
21
  "tavily",
22
22
  "serper",
23
23
  "exa",
24
+ "youcom",
24
25
  "jina",
25
26
  "firecrawl",
27
+ "perplexity",
26
28
  "searxng",
27
29
  "ollama"
28
30
  ],
@@ -57,7 +59,7 @@
57
59
  ]
58
60
  },
59
61
  "dependencies": {
60
- "@juicesharp/rpiv-config": "^1.17.0"
62
+ "@juicesharp/rpiv-config": "^1.17.1"
61
63
  },
62
64
  "peerDependencies": {
63
65
  "@earendil-works/pi-coding-agent": "*",
@@ -3,10 +3,12 @@ import { ExaProvider } from "./exa.js";
3
3
  import { FirecrawlProvider } from "./firecrawl.js";
4
4
  import { JinaProvider } from "./jina.js";
5
5
  import { OllamaProvider } from "./ollama.js";
6
+ import { PerplexityProvider } from "./perplexity.js";
6
7
  import { SearxngProvider } from "./searxng.js";
7
8
  import { SerperProvider } from "./serper.js";
8
9
  import { TavilyProvider } from "./tavily.js";
9
10
  import type { FullProvider, SearchProvider } from "./types.js";
11
+ import { YouComProvider } from "./youcom.js";
10
12
 
11
13
  export interface ProviderCredentials {
12
14
  apiKey?: string;
@@ -28,10 +30,14 @@ export function createSearchProvider(name: string, creds: ProviderCredentials):
28
30
  return new SerperProvider(apiKey);
29
31
  case "exa":
30
32
  return new ExaProvider(apiKey);
33
+ case "youcom":
34
+ return new YouComProvider(apiKey);
31
35
  case "jina":
32
36
  return new JinaProvider(apiKey);
33
37
  case "firecrawl":
34
38
  return new FirecrawlProvider(apiKey);
39
+ case "perplexity":
40
+ return new PerplexityProvider(apiKey);
35
41
  case "searxng":
36
42
  return new SearxngProvider({ apiKey: creds.apiKey, baseUrl: creds.baseUrl ?? "" });
37
43
  case "ollama":
@@ -3,10 +3,12 @@ import { EXA_PROVIDER_META } from "./exa.js";
3
3
  import { FIRECRAWL_PROVIDER_META } from "./firecrawl.js";
4
4
  import { JINA_PROVIDER_META } from "./jina.js";
5
5
  import { OLLAMA_PROVIDER_META } from "./ollama.js";
6
+ import { PERPLEXITY_PROVIDER_META } from "./perplexity.js";
6
7
  import { SEARXNG_PROVIDER_META } from "./searxng.js";
7
8
  import { SERPER_PROVIDER_META } from "./serper.js";
8
9
  import { TAVILY_PROVIDER_META } from "./tavily.js";
9
10
  import type { ProviderMeta } from "./types.js";
11
+ import { YOUCOM_PROVIDER_META } from "./youcom.js";
10
12
 
11
13
  export { BRAVE_API_KEY_ENV_VAR, BRAVE_PROVIDER_META, BraveProvider } from "./brave.js";
12
14
  export { EXA_API_KEY_ENV_VAR, EXA_PROVIDER_META, ExaProvider } from "./exa.js";
@@ -32,6 +34,7 @@ export {
32
34
  OLLAMA_PROVIDER_META,
33
35
  OllamaProvider,
34
36
  } from "./ollama.js";
37
+ export { PERPLEXITY_API_KEY_ENV_VAR, PERPLEXITY_PROVIDER_META, PerplexityProvider } from "./perplexity.js";
35
38
  export {
36
39
  configureSearxng,
37
40
  SEARXNG_API_KEY_ENV_VAR,
@@ -58,6 +61,7 @@ export type {
58
61
  SearchResponse,
59
62
  SearchResult,
60
63
  } from "./types.js";
64
+ export { YOUCOM_API_KEY_ENV_VAR, YOUCOM_PROVIDER_META, YouComProvider } from "./youcom.js";
61
65
 
62
66
  // Typed as readonly ProviderMeta[] (not `as const`) so iterators can access
63
67
  // the optional META fields (baseUrlEnvVar, defaultBaseUrl, configure) without
@@ -68,8 +72,10 @@ export const PROVIDERS: readonly ProviderMeta[] = [
68
72
  TAVILY_PROVIDER_META,
69
73
  SERPER_PROVIDER_META,
70
74
  EXA_PROVIDER_META,
75
+ YOUCOM_PROVIDER_META,
71
76
  JINA_PROVIDER_META,
72
77
  FIRECRAWL_PROVIDER_META,
78
+ PERPLEXITY_PROVIDER_META,
73
79
  SEARXNG_PROVIDER_META,
74
80
  OLLAMA_PROVIDER_META,
75
81
  ];
@@ -0,0 +1,67 @@
1
+ import type { SearchProvider, SearchResponse, SearchResult } from "./types.js";
2
+
3
+ const PERPLEXITY_API_URL = "https://api.perplexity.ai/search";
4
+ export const PERPLEXITY_API_KEY_ENV_VAR = "PERPLEXITY_API_KEY";
5
+ export const PERPLEXITY_PROVIDER_META = {
6
+ name: "perplexity",
7
+ label: "Perplexity",
8
+ envVar: PERPLEXITY_API_KEY_ENV_VAR,
9
+ roles: ["search"] as const,
10
+ } as const;
11
+
12
+ interface PerplexityRawResult {
13
+ title?: string;
14
+ url?: string;
15
+ snippet?: string;
16
+ date?: string | null;
17
+ last_updated?: string | null;
18
+ }
19
+
20
+ interface PerplexityRawResponse {
21
+ results?: PerplexityRawResult[];
22
+ id?: string;
23
+ server_time?: string | null;
24
+ }
25
+
26
+ function normalizePerplexityResults(results: PerplexityRawResult[]): SearchResult[] {
27
+ return results.map((r) => ({
28
+ title: r.title ?? "",
29
+ url: r.url ?? "",
30
+ snippet: r.snippet ?? "",
31
+ }));
32
+ }
33
+
34
+ export class PerplexityProvider implements SearchProvider {
35
+ readonly name = PERPLEXITY_PROVIDER_META.name;
36
+ readonly label = PERPLEXITY_PROVIDER_META.label;
37
+ readonly envVar = PERPLEXITY_PROVIDER_META.envVar;
38
+
39
+ constructor(private readonly apiKey: string) {}
40
+
41
+ async search(query: string, maxResults: number, signal?: AbortSignal): Promise<SearchResponse> {
42
+ if (!this.apiKey) {
43
+ throw new Error(`${this.envVar} is not set. Run /web-tools to configure, or export the env var.`);
44
+ }
45
+
46
+ const res = await fetch(PERPLEXITY_API_URL, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Authorization: `Bearer ${this.apiKey}`,
51
+ },
52
+ body: JSON.stringify({
53
+ query,
54
+ max_results: maxResults,
55
+ }),
56
+ signal,
57
+ });
58
+
59
+ if (!res.ok) {
60
+ const text = await res.text();
61
+ throw new Error(`${this.label} Search API error (${res.status}): ${text}`);
62
+ }
63
+
64
+ const raw = (await res.json()) as PerplexityRawResponse;
65
+ return { query, results: normalizePerplexityResults(raw.results ?? []) };
66
+ }
67
+ }
@@ -0,0 +1,110 @@
1
+ import type { FetchResponse, FullProvider, SearchResponse, SearchResult } from "./types.js";
2
+
3
+ const YOUCOM_SEARCH_URL = "https://ydc-index.io/v1/search";
4
+ const YOUCOM_CONTENTS_URL = "https://ydc-index.io/v1/contents";
5
+ export const YOUCOM_API_KEY_ENV_VAR = "YOUCOM_API_KEY";
6
+ export const YOUCOM_PROVIDER_META = {
7
+ name: "youcom",
8
+ label: "You.com",
9
+ envVar: YOUCOM_API_KEY_ENV_VAR,
10
+ roles: ["search", "fetch"] as const,
11
+ } as const;
12
+
13
+ interface YouComWebResult {
14
+ url?: string;
15
+ title?: string;
16
+ description?: string;
17
+ snippets?: string[];
18
+ }
19
+
20
+ interface YouComSearchResponse {
21
+ results?: {
22
+ web?: YouComWebResult[];
23
+ };
24
+ }
25
+
26
+ interface YouComContentsResponseItem {
27
+ url: string;
28
+ title?: string;
29
+ markdown?: string | null;
30
+ }
31
+
32
+ function normalizeYouComResults(results: YouComWebResult[]): SearchResult[] {
33
+ return results.map((r) => ({
34
+ title: r.title ?? "",
35
+ url: r.url ?? "",
36
+ snippet: r.snippets?.[0] ?? r.description ?? "",
37
+ }));
38
+ }
39
+
40
+ export class YouComProvider implements FullProvider {
41
+ readonly name = YOUCOM_PROVIDER_META.name;
42
+ readonly label = YOUCOM_PROVIDER_META.label;
43
+ readonly envVar = YOUCOM_API_KEY_ENV_VAR;
44
+
45
+ constructor(private readonly apiKey: string) {}
46
+
47
+ async search(query: string, maxResults: number, signal?: AbortSignal): Promise<SearchResponse> {
48
+ if (!this.apiKey) {
49
+ throw new Error(`${this.envVar} is not set. Run /web-tools to configure, or export the env var.`);
50
+ }
51
+
52
+ const res = await fetch(YOUCOM_SEARCH_URL, {
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ "X-API-Key": this.apiKey,
57
+ },
58
+ body: JSON.stringify({
59
+ query,
60
+ count: maxResults,
61
+ }),
62
+ signal,
63
+ });
64
+
65
+ if (!res.ok) {
66
+ const text = await res.text();
67
+ throw new Error(`${this.label} Search API error (${res.status}): ${text}`);
68
+ }
69
+
70
+ const raw = (await res.json()) as YouComSearchResponse;
71
+ return { query, results: normalizeYouComResults(raw.results?.web ?? []) };
72
+ }
73
+
74
+ async fetch(url: string, _raw: boolean, signal?: AbortSignal): Promise<FetchResponse> {
75
+ if (!this.apiKey) {
76
+ throw new Error(`${this.envVar} is not set. Run /web-tools to configure, or export the env var.`);
77
+ }
78
+
79
+ const res = await fetch(YOUCOM_CONTENTS_URL, {
80
+ method: "POST",
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ "X-API-Key": this.apiKey,
84
+ },
85
+ body: JSON.stringify({
86
+ urls: [url],
87
+ formats: ["markdown"],
88
+ }),
89
+ signal,
90
+ });
91
+
92
+ if (!res.ok) {
93
+ const text = await res.text();
94
+ throw new Error(`${this.label} Fetch API error (${res.status}): ${text}`);
95
+ }
96
+
97
+ const raw = (await res.json()) as YouComContentsResponseItem[];
98
+ const item = raw[0];
99
+
100
+ if (!item?.markdown) {
101
+ throw new Error(`${this.label} Fetch API error: no content returned for ${url}`);
102
+ }
103
+
104
+ return {
105
+ text: item.markdown,
106
+ title: item.title || undefined,
107
+ contentType: "text/markdown",
108
+ };
109
+ }
110
+ }