@pi-unipi/web-api 0.1.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/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # @pi-unipi/web-api
2
+
3
+ Web search, read, and summarize tools with provider-based backend selection for Pi coding agent.
4
+
5
+ ## Overview
6
+
7
+ `@pi-unipi/web-api` provides agent tools for web access:
8
+
9
+ - **web_search** — Search the web using various providers
10
+ - **web_read** — Extract content from URLs
11
+ - **web_llm_summarize** — Summarize web content using LLM
12
+
13
+ Providers are ranked by capability and cost, allowing smart auto-selection.
14
+
15
+ ## Features
16
+
17
+ - **Provider-based architecture** — Multiple search/read providers with unified interface
18
+ - **Smart selection** — Auto-select cheapest available provider
19
+ - **API key management** — Interactive TUI for key configuration
20
+ - **Caching** — Web content cached with configurable TTL
21
+ - **Subagent integration** — Tools automatically available to spawned subagents
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @pi-unipi/web-api
27
+ ```
28
+
29
+ Add to your pi configuration:
30
+
31
+ ```json
32
+ {
33
+ "pi": {
34
+ "extensions": [
35
+ "node_modules/@pi-unipi/web-api/src/index.ts"
36
+ ]
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Providers
42
+
43
+ ### Search Providers
44
+
45
+ | Provider | Rank | Cost | API Key |
46
+ |----------|------|------|---------|
47
+ | DuckDuckGo | 1 | Free | No |
48
+ | Jina AI Search | 2 | Freemium | Optional |
49
+ | SerpAPI | 3 | Paid | Required |
50
+ | Tavily | 4 | Paid | Required |
51
+ | Perplexity | 5 | Paid | Required |
52
+
53
+ ### Read Providers
54
+
55
+ | Provider | Rank | Cost | API Key |
56
+ |----------|------|------|---------|
57
+ | Jina AI Reader | 1 | Freemium | Optional |
58
+ | Firecrawl | 2 | Paid | Required |
59
+ | Perplexity | 3 | Paid | Required |
60
+
61
+ ### Summarize Providers
62
+
63
+ | Provider | Rank | Cost | API Key |
64
+ |----------|------|------|---------|
65
+ | Perplexity | 1 | Paid | Required |
66
+ | LLM Summarize | 2 | LLM tokens | No |
67
+
68
+ ## Configuration
69
+
70
+ ### API Keys
71
+
72
+ Configure API keys via the interactive settings command:
73
+
74
+ ```
75
+ /unipi:web-settings
76
+ ```
77
+
78
+ Or set environment variables:
79
+
80
+ ```bash
81
+ export SERPAPI_KEY="your-key"
82
+ export TAVILY_API_KEY="your-key"
83
+ export FIRECRAWL_API_KEY="your-key"
84
+ export PERPLEXITY_API_KEY="your-key"
85
+ export JINA_API_KEY="your-key"
86
+ ```
87
+
88
+ ### Settings Files
89
+
90
+ - **Auth:** `~/.unipi/config/web-api/auth.json` (API keys, gitignored)
91
+ - **Config:** `~/.unipi/config/web-api/config.json` (provider settings)
92
+
93
+ ## Usage
94
+
95
+ ### Web Search
96
+
97
+ ```
98
+ # Auto-select cheapest provider
99
+ web_search(query: "TypeScript generics")
100
+
101
+ # Use specific provider
102
+ web_search(query: "latest AI research", source: 4) # Tavily
103
+ ```
104
+
105
+ ### Web Read
106
+
107
+ ```
108
+ # Auto-select provider
109
+ web_read(url: "https://example.com/article")
110
+
111
+ # Use specific provider
112
+ web_read(url: "https://example.com/spa", source: 2) # Firecrawl
113
+ ```
114
+
115
+ ### Web Summarize
116
+
117
+ ```
118
+ # Auto-summarize
119
+ web_llm_summarize(url: "https://example.com/long-article")
120
+
121
+ # Custom prompt
122
+ web_llm_summarize(url: "https://example.com/research", prompt: "Extract key findings")
123
+ ```
124
+
125
+ ## Commands
126
+
127
+ ### /unipi:web-settings
128
+
129
+ Interactive settings dialog for managing providers and API keys.
130
+
131
+ ### /unipi:web-cache-clear
132
+
133
+ Clear all cached web content.
134
+
135
+ ## Cache
136
+
137
+ - Default TTL: 1 hour
138
+ - Cache location: `~/.unipi/config/web-api/cache/`
139
+ - Automatic for web_read operations
140
+
141
+ ## Troubleshooting
142
+
143
+ ### No provider available
144
+
145
+ If you see "No search provider available":
146
+
147
+ 1. Run `/unipi:web-settings`
148
+ 2. Enable at least one provider
149
+ 3. Add API keys for paid providers
150
+
151
+ ### API key invalid
152
+
153
+ If API key validation fails:
154
+
155
+ 1. Check the key is correct
156
+ 2. Verify the key has sufficient permissions
157
+ 3. Check provider status at their website
158
+
159
+ ### Rate limiting
160
+
161
+ If you hit rate limits:
162
+
163
+ 1. Add an API key for higher limits
164
+ 2. Use a different provider
165
+ 3. Wait and retry
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ # Type check
171
+ npm run typecheck
172
+
173
+ # Build
174
+ npm run build
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@pi-unipi/web-api",
3
+ "version": "0.1.0",
4
+ "description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Neuron Mr White",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Neuron-Mr-White/unipi.git",
11
+ "directory": "packages/web-api"
12
+ },
13
+ "homepage": "https://github.com/Neuron-Mr-White/unipi#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/Neuron-Mr-White/unipi/issues"
16
+ },
17
+ "keywords": [
18
+ "pi-package",
19
+ "pi-extension",
20
+ "pi-coding-agent",
21
+ "unipi",
22
+ "web-api",
23
+ "search",
24
+ "read",
25
+ "summarize"
26
+ ],
27
+ "pi": {
28
+ "extensions": [
29
+ "src/index.ts"
30
+ ],
31
+ "skills": [
32
+ "skills"
33
+ ]
34
+ },
35
+ "files": [
36
+ "src/**/*.ts",
37
+ "skills/**/*",
38
+ "README.md"
39
+ ],
40
+ "dependencies": {
41
+ "@pi-unipi/core": "*"
42
+ },
43
+ "peerDependencies": {
44
+ "@mariozechner/pi-coding-agent": "*",
45
+ "@sinclair/typebox": "*"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.6.0"
49
+ }
50
+ }
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: web
3
+ description: "Web search, read, and summarize tools with provider-based backend"
4
+ ---
5
+
6
+ # Web Tools
7
+
8
+ Use these tools to access web content. Providers are ranked by capability and cost.
9
+
10
+ ## web_search
11
+
12
+ Search the web for information. Lower `source` = simpler/cheaper providers.
13
+
14
+ - **Quick facts:** source 1-2 (DuckDuckGo, Jina Search)
15
+ - **Research:** source 3-5 (SerpAPI, Tavily, Perplexity)
16
+
17
+ **Parameters:**
18
+ - `query` (required): Search query string
19
+ - `source` (optional): Provider selection (1-5)
20
+
21
+ **Examples:**
22
+ ```
23
+ web_search(query: "TypeScript generics tutorial")
24
+ web_search(query: "latest AI research", source: 4) # Use Tavily
25
+ ```
26
+
27
+ ## web_read
28
+
29
+ Read URL content. Lower `source` = simpler providers.
30
+
31
+ - **Basic extraction:** source 1 (Jina Reader)
32
+ - **Advanced crawling:** source 2 (Firecrawl)
33
+
34
+ **Parameters:**
35
+ - `url` (required): URL to read
36
+ - `source` (optional): Provider selection (1-3)
37
+
38
+ **Examples:**
39
+ ```
40
+ web_read(url: "https://example.com/article")
41
+ web_read(url: "https://example.com/spa", source: 2) # Use Firecrawl
42
+ ```
43
+
44
+ ## web_llm_summarize
45
+
46
+ Summarize URL with LLM. Higher cost (LLM tokens + provider).
47
+
48
+ - Use for complex content that needs analysis
49
+ - Custom prompts supported for targeted summaries
50
+
51
+ **Parameters:**
52
+ - `url` (required): URL to summarize
53
+ - `prompt` (optional): Custom summarization prompt
54
+ - `source` (optional): Provider selection for content fetch (1-3)
55
+
56
+ **Examples:**
57
+ ```
58
+ web_llm_summarize(url: "https://example.com/long-article")
59
+ web_llm_summarize(url: "https://example.com/research", prompt: "Extract key findings")
60
+ ```
61
+
62
+ ## Provider Selection
63
+
64
+ - Omit `source` for auto-selection (cheapest available)
65
+ - Specify `source` number for specific provider
66
+ - If provider unavailable, tool throws descriptive error
67
+
68
+ ### Provider Rankings
69
+
70
+ **Search providers:**
71
+ 1. DuckDuckGo (free)
72
+ 2. Jina AI Search (freemium)
73
+ 3. SerpAPI (paid)
74
+ 4. Tavily (paid)
75
+ 5. Perplexity (paid)
76
+
77
+ **Read providers:**
78
+ 1. Jina AI Reader (freemium)
79
+ 2. Firecrawl (paid)
80
+ 3. Perplexity (paid)
81
+
82
+ **Summarize providers:**
83
+ 1. Perplexity (paid)
84
+ 2. LLM Summarize (uses pi's LLM)
85
+
86
+ ## Cost Awareness
87
+
88
+ - **DuckDuckGo:** Free (search only)
89
+ - **Jina:** Freemium (search + read)
90
+ - **SerpAPI/Tavily:** Paid (search)
91
+ - **Firecrawl:** Paid (read)
92
+ - **Perplexity:** Paid (search + summarize)
93
+ - **LLM Summarize:** LLM token cost
94
+
95
+ ## Configuration
96
+
97
+ Configure providers via `/unipi:web-settings` command.
98
+
99
+ - Add/remove API keys
100
+ - Enable/disable providers
101
+ - View provider status
102
+
103
+ ## Cache
104
+
105
+ Web content is cached for 1 hour by default.
106
+
107
+ - Clear cache: `/unipi:web-cache-clear`
108
+ - Cache is automatic for web_read operations
package/src/cache.ts ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @unipi/web-api — Cache layer
3
+ *
4
+ * Caches web content with configurable TTL.
5
+ * Manual invalidation via /unipi:web-cache-clear command.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as os from "node:os";
11
+ import * as crypto from "node:crypto";
12
+
13
+ /** Cache entry structure */
14
+ export interface CacheEntry {
15
+ /** Cache key */
16
+ key: string;
17
+ /** Cached data */
18
+ data: unknown;
19
+ /** Timestamp when cached */
20
+ timestamp: number;
21
+ /** TTL in milliseconds */
22
+ ttlMs: number;
23
+ /** Provider that produced this data */
24
+ provider: string;
25
+ /** URL that was cached */
26
+ url: string;
27
+ }
28
+
29
+ /** Cache statistics */
30
+ export interface CacheStats {
31
+ /** Total number of entries */
32
+ totalEntries: number;
33
+ /** Total size in bytes */
34
+ totalSizeBytes: number;
35
+ /** Expired entries count */
36
+ expiredEntries: number;
37
+ }
38
+
39
+ /**
40
+ * WebCache manages cached web content.
41
+ */
42
+ export class WebCache {
43
+ private cacheDir: string;
44
+ private defaultTtlMs: number;
45
+
46
+ constructor(defaultTtlMs: number = 3600000) {
47
+ this.cacheDir = path.join(os.homedir(), ".unipi", "config", "web-api", "cache");
48
+ this.defaultTtlMs = defaultTtlMs;
49
+ this.ensureCacheDir();
50
+ }
51
+
52
+ /**
53
+ * Ensure cache directory exists.
54
+ */
55
+ private ensureCacheDir(): void {
56
+ if (!fs.existsSync(this.cacheDir)) {
57
+ fs.mkdirSync(this.cacheDir, { recursive: true });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Generate cache key from URL and provider.
63
+ */
64
+ private generateKey(url: string, provider: string): string {
65
+ const content = `${provider}:${url}`;
66
+ return crypto.createHash("sha256").update(content).digest("hex");
67
+ }
68
+
69
+ /**
70
+ * Get file path for a cache key.
71
+ */
72
+ private getFilePath(key: string): string {
73
+ return path.join(this.cacheDir, `${key}.json`);
74
+ }
75
+
76
+ /**
77
+ * Get cached data.
78
+ * @param url - URL to get from cache
79
+ * @param provider - Provider that produced the data
80
+ * @returns Cached data or null if not found/expired
81
+ */
82
+ get(url: string, provider: string): unknown | null {
83
+ const key = this.generateKey(url, provider);
84
+ const filePath = this.getFilePath(key);
85
+
86
+ if (!fs.existsSync(filePath)) {
87
+ return null;
88
+ }
89
+
90
+ try {
91
+ const content = fs.readFileSync(filePath, "utf-8");
92
+ const entry: CacheEntry = JSON.parse(content);
93
+
94
+ // Check if expired
95
+ const now = Date.now();
96
+ if (now - entry.timestamp > entry.ttlMs) {
97
+ // Expired, remove file
98
+ this.deleteEntry(key);
99
+ return null;
100
+ }
101
+
102
+ return entry.data;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Set cached data.
110
+ * @param url - URL to cache
111
+ * @param provider - Provider that produced the data
112
+ * @param data - Data to cache
113
+ * @param ttlMs - TTL in milliseconds (optional, uses default)
114
+ */
115
+ set(url: string, provider: string, data: unknown, ttlMs?: number): void {
116
+ const key = this.generateKey(url, provider);
117
+ const filePath = this.getFilePath(key);
118
+
119
+ const entry: CacheEntry = {
120
+ key,
121
+ data,
122
+ timestamp: Date.now(),
123
+ ttlMs: ttlMs ?? this.defaultTtlMs,
124
+ provider,
125
+ url,
126
+ };
127
+
128
+ fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
129
+ }
130
+
131
+ /**
132
+ * Delete a cache entry by key.
133
+ */
134
+ private deleteEntry(key: string): void {
135
+ const filePath = this.getFilePath(key);
136
+ try {
137
+ if (fs.existsSync(filePath)) {
138
+ fs.unlinkSync(filePath);
139
+ }
140
+ } catch {
141
+ // Ignore errors
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Clear all cached data.
147
+ * @returns Number of entries cleared
148
+ */
149
+ clear(): number {
150
+ let count = 0;
151
+ try {
152
+ const files = fs.readdirSync(this.cacheDir);
153
+ for (const file of files) {
154
+ if (file.endsWith(".json")) {
155
+ const filePath = path.join(this.cacheDir, file);
156
+ fs.unlinkSync(filePath);
157
+ count++;
158
+ }
159
+ }
160
+ } catch {
161
+ // Ignore errors
162
+ }
163
+ return count;
164
+ }
165
+
166
+ /**
167
+ * Clear expired entries.
168
+ * @returns Number of expired entries cleared
169
+ */
170
+ clearExpired(): number {
171
+ let count = 0;
172
+ const now = Date.now();
173
+
174
+ try {
175
+ const files = fs.readdirSync(this.cacheDir);
176
+ for (const file of files) {
177
+ if (!file.endsWith(".json")) continue;
178
+
179
+ const filePath = path.join(this.cacheDir, file);
180
+ try {
181
+ const content = fs.readFileSync(filePath, "utf-8");
182
+ const entry: CacheEntry = JSON.parse(content);
183
+
184
+ if (now - entry.timestamp > entry.ttlMs) {
185
+ fs.unlinkSync(filePath);
186
+ count++;
187
+ }
188
+ } catch {
189
+ // If we can't read/parse, delete it
190
+ fs.unlinkSync(filePath);
191
+ count++;
192
+ }
193
+ }
194
+ } catch {
195
+ // Ignore errors
196
+ }
197
+
198
+ return count;
199
+ }
200
+
201
+ /**
202
+ * Get cache statistics.
203
+ */
204
+ getStats(): CacheStats {
205
+ let totalEntries = 0;
206
+ let totalSizeBytes = 0;
207
+ let expiredEntries = 0;
208
+ const now = Date.now();
209
+
210
+ try {
211
+ const files = fs.readdirSync(this.cacheDir);
212
+ for (const file of files) {
213
+ if (!file.endsWith(".json")) continue;
214
+
215
+ const filePath = path.join(this.cacheDir, file);
216
+ try {
217
+ const stat = fs.statSync(filePath);
218
+ totalSizeBytes += stat.size;
219
+ totalEntries++;
220
+
221
+ const content = fs.readFileSync(filePath, "utf-8");
222
+ const entry: CacheEntry = JSON.parse(content);
223
+
224
+ if (now - entry.timestamp > entry.ttlMs) {
225
+ expiredEntries++;
226
+ }
227
+ } catch {
228
+ totalEntries++;
229
+ }
230
+ }
231
+ } catch {
232
+ // Ignore errors
233
+ }
234
+
235
+ return { totalEntries, totalSizeBytes, expiredEntries };
236
+ }
237
+ }
238
+
239
+ /** Singleton cache instance */
240
+ export const webCache = new WebCache();
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @unipi/web-api — Commands registration
3
+ *
4
+ * Registers /unipi:web-settings and /unipi:web-cache-clear commands.
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import { UNIPI_PREFIX } from "@pi-unipi/core";
9
+ import { showSettingsDialog } from "./tui/settings-dialog.js";
10
+ import { webCache } from "./cache.js";
11
+
12
+ /** Command names */
13
+ export const WEB_COMMANDS = {
14
+ SETTINGS: "web-settings",
15
+ CACHE_CLEAR: "web-cache-clear",
16
+ } as const;
17
+
18
+ /**
19
+ * Register web commands with pi.
20
+ */
21
+ export function registerWebCommands(pi: ExtensionAPI): void {
22
+ // --- /unipi:web-settings command ---
23
+ pi.registerCommand({
24
+ name: `${UNIPI_PREFIX}${WEB_COMMANDS.SETTINGS}`,
25
+ description: "Configure web API providers and API keys",
26
+ async execute(_args, _ctx) {
27
+ await showSettingsDialog(pi);
28
+ },
29
+ });
30
+
31
+ // --- /unipi:web-cache-clear command ---
32
+ pi.registerCommand({
33
+ name: `${UNIPI_PREFIX}${WEB_COMMANDS.CACHE_CLEAR}`,
34
+ description: "Clear all cached web content",
35
+ async execute(_args, _ctx) {
36
+ const stats = webCache.getStats();
37
+ const cleared = webCache.clear();
38
+
39
+ await pi.ui.notify({
40
+ message: `Cache cleared: ${cleared} entries removed (${stats.totalSizeBytes} bytes freed)`,
41
+ level: "success",
42
+ });
43
+ },
44
+ });
45
+ }