@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 +179 -0
- package/package.json +50 -0
- package/skills/web/SKILL.md +108 -0
- package/src/cache.ts +240 -0
- package/src/commands.ts +45 -0
- package/src/index.ts +100 -0
- package/src/providers/base.ts +108 -0
- package/src/providers/duckduckgo.ts +115 -0
- package/src/providers/firecrawl.ts +105 -0
- package/src/providers/jina-reader.ts +89 -0
- package/src/providers/jina-search.ts +88 -0
- package/src/providers/llm-summarize.ts +71 -0
- package/src/providers/perplexity.ts +191 -0
- package/src/providers/registry.ts +128 -0
- package/src/providers/serpapi.ts +86 -0
- package/src/providers/tavily.ts +95 -0
- package/src/settings.ts +263 -0
- package/src/tools.ts +329 -0
- package/src/tui/provider-selector.ts +71 -0
- package/src/tui/settings-dialog.ts +177 -0
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();
|
package/src/commands.ts
ADDED
|
@@ -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
|
+
}
|