@jmcombs/pi-tavily-search 2.1.0 → 3.0.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 +84 -19
- package/index.ts +99 -47
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -3,6 +3,56 @@
|
|
|
3
3
|
A [Pi coding agent](https://pi.dev) extension that adds real-time web search via the
|
|
4
4
|
[Tavily API](https://tavily.com).
|
|
5
5
|
|
|
6
|
+
## Breaking changes in v3.0.0
|
|
7
|
+
|
|
8
|
+
- **`AuthStorage` is gone.** Pi 0.80.8 removed the `AuthStorage` API this extension used
|
|
9
|
+
to store and read its API key. Credentials now resolve through the
|
|
10
|
+
[`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password)
|
|
11
|
+
**credential API**, which this package now depends on directly and **installs
|
|
12
|
+
automatically** — no separate install step.
|
|
13
|
+
- **Availability-branched onboarding.** When the `op` CLI is installed and an account is
|
|
14
|
+
configured, setup opens a 1Password **vault → item → field picker**; when `op` is
|
|
15
|
+
unavailable it falls back to **masked manual key entry**.
|
|
16
|
+
- **Existing keys keep working.** Any Tavily key already in `~/.pi/agent/auth.json` — a
|
|
17
|
+
literal value or an `!op read` reference — resolves unchanged. No migration action is
|
|
18
|
+
required.
|
|
19
|
+
|
|
20
|
+
## What's New — 1Password credential integration
|
|
21
|
+
|
|
22
|
+
Tavily search now handles your API key through the
|
|
23
|
+
[`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password) credential
|
|
24
|
+
API, which installs automatically as a dependency. What this means for you:
|
|
25
|
+
|
|
26
|
+
- **Onboarding branches on 1Password availability.** If the `op` CLI is installed and an
|
|
27
|
+
account is configured, `/tavily_setup` opens a live **vault → item → field
|
|
28
|
+
picker** (or lets you type an `op://…` reference) and stores it as a `!op read '…'`
|
|
29
|
+
entry that resolves fresh on every use. If `op` is not available, it falls back to
|
|
30
|
+
**manual API-key entry** and nudges you to enable the 1Password extension for vault
|
|
31
|
+
integration.
|
|
32
|
+
- **The `TAVILY_API_KEY` environment variable still works.** It remains a supported
|
|
33
|
+
fallback, resolved after the stored `tavily` key.
|
|
34
|
+
- **Existing keys keep working.** Any Tavily key already in `~/.pi/agent/auth.json` — a
|
|
35
|
+
literal key or an `!op read` reference — continues to resolve unchanged. No migration
|
|
36
|
+
action is required.
|
|
37
|
+
- **The key is never exposed to the model.** Entry happens entirely in the TUI, and only
|
|
38
|
+
the resolved value is used to call the Tavily API.
|
|
39
|
+
- **Enable 1Password for vault integration and startup unlock.** Install and enable the
|
|
40
|
+
[`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password) extension:
|
|
41
|
+
it makes the vault picker available during onboarding and runs a one-time `op read` at
|
|
42
|
+
session startup, so the biometric unlock prompt lands once.
|
|
43
|
+
|
|
44
|
+
```mermaid
|
|
45
|
+
flowchart TD
|
|
46
|
+
A["/tavily_setup or first tool use"] --> B{"is1PasswordAvailable()<br/>(op installed AND configured)"}
|
|
47
|
+
B -- "Yes" --> C["Live vault → item → field picker<br/>or manual op:// reference"]
|
|
48
|
+
C --> D["Store !op read 'op://…' entry"]
|
|
49
|
+
B -- "No" --> E["Manual API-key entry<br/>+ nudge to enable 1Password"]
|
|
50
|
+
E --> F["Store literal api_key entry"]
|
|
51
|
+
D --> G["resolveSecret('tavily') ?? TAVILY_API_KEY<br/>resolves fresh on each tool call"]
|
|
52
|
+
F --> G
|
|
53
|
+
G --> H["api_key → Tavily API<br/>(never shown to the LLM)"]
|
|
54
|
+
```
|
|
55
|
+
|
|
6
56
|
## Install
|
|
7
57
|
|
|
8
58
|
```bash
|
|
@@ -22,34 +72,48 @@ available) to get one, then configure it using one of the methods below.
|
|
|
22
72
|
five formatted results (title, URL, content) plus the raw API response under
|
|
23
73
|
`details.raw`. The tool is callable by the LLM whenever it needs current
|
|
24
74
|
information from the public web.
|
|
75
|
+
- **Command**: `/tavily_setup` — runs the `@jmcombs/pi-1password` onboarding flow
|
|
76
|
+
to save (or update) your Tavily key. The input is never visible to the LLM.
|
|
25
77
|
|
|
26
78
|
## Configuration
|
|
27
79
|
|
|
28
|
-
|
|
80
|
+
The `tavily_search` tool resolves the key in this order:
|
|
29
81
|
|
|
30
|
-
1. `
|
|
31
|
-
|
|
82
|
+
1. `resolveSecret("tavily")` from `@jmcombs/pi-1password` — reads `~/.pi/agent/auth.json`
|
|
83
|
+
fresh on each call (a literal key or an `!op read` reference). **Recommended.**
|
|
84
|
+
2. The `TAVILY_API_KEY` environment variable — fallback.
|
|
32
85
|
|
|
33
|
-
|
|
86
|
+
If neither is set, the tool automatically runs onboarding (the availability branch above)
|
|
87
|
+
on first use, then re-resolves — preserving the "prompt on first use" experience.
|
|
34
88
|
|
|
35
|
-
|
|
89
|
+
### Option 1 — `/tavily_setup` (recommended)
|
|
36
90
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"key": "tvly-..."
|
|
42
|
-
}
|
|
43
|
-
}
|
|
91
|
+
Run the command and follow the flow:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
/tavily_setup
|
|
44
95
|
```
|
|
45
96
|
|
|
46
|
-
|
|
97
|
+
- When the `op` CLI is available, pick your key from the live vault picker (or paste an
|
|
98
|
+
`op://vault/item/field` reference); it is stored as a `!op read '…'` entry that resolves
|
|
99
|
+
fresh on every use.
|
|
100
|
+
- When `op` is not available, enter the key on a masked prompt; it is stored as a literal
|
|
101
|
+
`api_key` entry.
|
|
102
|
+
|
|
103
|
+
Either way the value is written to `~/.pi/agent/auth.json` (`0600`) and never shown to the
|
|
104
|
+
model.
|
|
105
|
+
|
|
106
|
+
### Option 2 — edit `~/.pi/agent/auth.json` directly
|
|
107
|
+
|
|
108
|
+
The stored entry is provider-shaped under the `tavily` key. Any of these resolve:
|
|
109
|
+
|
|
110
|
+
#### Plain key
|
|
47
111
|
|
|
48
112
|
```json
|
|
49
113
|
{
|
|
50
114
|
"tavily": {
|
|
51
115
|
"type": "api_key",
|
|
52
|
-
"key": "
|
|
116
|
+
"key": "tvly-..."
|
|
53
117
|
}
|
|
54
118
|
}
|
|
55
119
|
```
|
|
@@ -65,13 +129,13 @@ You must configure a Tavily API key. Pi resolves the key in this order:
|
|
|
65
129
|
}
|
|
66
130
|
```
|
|
67
131
|
|
|
68
|
-
#### Shell-resolved key (`pass`)
|
|
132
|
+
#### Shell-resolved key (macOS Keychain / `pass`)
|
|
69
133
|
|
|
70
134
|
```json
|
|
71
135
|
{
|
|
72
136
|
"tavily": {
|
|
73
137
|
"type": "api_key",
|
|
74
|
-
"key": "!
|
|
138
|
+
"key": "!security find-generic-password -ws tavily"
|
|
75
139
|
}
|
|
76
140
|
}
|
|
77
141
|
```
|
|
@@ -79,7 +143,7 @@ You must configure a Tavily API key. Pi resolves the key in this order:
|
|
|
79
143
|
The `!`-prefixed value is executed by your shell at lookup time, so no secret is
|
|
80
144
|
ever stored on disk in plaintext.
|
|
81
145
|
|
|
82
|
-
### Option
|
|
146
|
+
### Option 3 — environment variable
|
|
83
147
|
|
|
84
148
|
```bash
|
|
85
149
|
export TAVILY_API_KEY="tvly-..."
|
|
@@ -98,9 +162,10 @@ export TAVILY_API_KEY="tvly-..."
|
|
|
98
162
|
|
|
99
163
|
## Requirements
|
|
100
164
|
|
|
101
|
-
- Pi `>= 0.
|
|
102
|
-
- Node `>=
|
|
165
|
+
- Pi `>= 0.80.8` (credentials via the `@jmcombs/pi-1password` API and `ExtensionAPI`)
|
|
166
|
+
- Node `>= 22.19.0`
|
|
103
167
|
- A Tavily API key
|
|
168
|
+
- Optional: the `op` (1Password) CLI for vault-backed onboarding and startup unlock
|
|
104
169
|
|
|
105
170
|
## Development
|
|
106
171
|
|
package/index.ts
CHANGED
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
* @jmcombs/pi-tavily-search — Real-time web search for the Pi coding agent.
|
|
3
3
|
*
|
|
4
4
|
* Registers a `tavily_search` tool that the LLM can call to perform a Tavily
|
|
5
|
-
* web search.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* web search. Credentials are handled entirely through the imported
|
|
6
|
+
* `@jmcombs/pi-1password` credential API (`resolveSecret` / `onboardSecret`),
|
|
7
|
+
* so the key is never leaked into the agent's context.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* 1. `
|
|
11
|
-
*
|
|
9
|
+
* Credential handling:
|
|
10
|
+
* 1. `resolveSecret("tavily")` reads `~/.pi/agent/auth.json` and resolves the
|
|
11
|
+
* stored entry (literal key or `!op read 'op://…'` reference) fresh on each
|
|
12
|
+
* use; the `TAVILY_API_KEY` environment variable is the fallback.
|
|
13
|
+
* 2. If nothing is stored, the tool auto-invokes `onboardSecret`, which branches
|
|
14
|
+
* on 1Password availability — the live vault picker when `op` is configured,
|
|
15
|
+
* manual API-key entry otherwise — then re-resolves.
|
|
16
|
+
* 3. `/tavily_setup` runs the same onboarding flow on demand.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
19
|
+
import type { JsonObject, JsonValue } from "@earendil-works/pi-ai";
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { onboardSecret, resolveSecret } from "@jmcombs/pi-1password";
|
|
22
|
+
import { type Static, Type } from "typebox";
|
|
16
23
|
|
|
17
24
|
const TAVILY_SEARCH_ENDPOINT = "https://api.tavily.com/search";
|
|
18
25
|
|
|
@@ -49,6 +56,62 @@ interface TavilySearchResponse {
|
|
|
49
56
|
|
|
50
57
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
51
58
|
|
|
59
|
+
export function isJsonValue(value: unknown): value is JsonValue {
|
|
60
|
+
if (value === null) return true;
|
|
61
|
+
switch (typeof value) {
|
|
62
|
+
case "boolean":
|
|
63
|
+
case "number":
|
|
64
|
+
case "string":
|
|
65
|
+
return true;
|
|
66
|
+
case "object": {
|
|
67
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
68
|
+
const nested: unknown[] = Object.values(value);
|
|
69
|
+
return nested.every(isJsonValue);
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function asJsonObject(value: JsonValue): JsonObject | undefined {
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
78
|
+
return value as JsonObject;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toTavilySearchResponse(data: JsonValue): TavilySearchResponse {
|
|
82
|
+
const obj = asJsonObject(data);
|
|
83
|
+
if (!obj) return {};
|
|
84
|
+
const out: TavilySearchResponse = {};
|
|
85
|
+
if (typeof obj.query === "string") out.query = obj.query;
|
|
86
|
+
if (typeof obj.answer === "string") out.answer = obj.answer;
|
|
87
|
+
if (Array.isArray(obj.results)) {
|
|
88
|
+
const results: TavilySearchResult[] = [];
|
|
89
|
+
for (const item of obj.results) {
|
|
90
|
+
const entry = asJsonObject(item);
|
|
91
|
+
if (!entry) continue;
|
|
92
|
+
if (
|
|
93
|
+
typeof entry.title !== "string" ||
|
|
94
|
+
typeof entry.url !== "string" ||
|
|
95
|
+
typeof entry.content !== "string"
|
|
96
|
+
) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const result: TavilySearchResult = {
|
|
100
|
+
title: entry.title,
|
|
101
|
+
url: entry.url,
|
|
102
|
+
content: entry.content,
|
|
103
|
+
};
|
|
104
|
+
if (typeof entry.score === "number") result.score = entry.score;
|
|
105
|
+
if (typeof entry.raw_content === "string" || entry.raw_content === null) {
|
|
106
|
+
result.raw_content = entry.raw_content;
|
|
107
|
+
}
|
|
108
|
+
results.push(result);
|
|
109
|
+
}
|
|
110
|
+
out.results = results;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
52
115
|
function formatResults(data: TavilySearchResponse, query: string): string {
|
|
53
116
|
const results = data.results ?? [];
|
|
54
117
|
if (results.length === 0) {
|
|
@@ -66,20 +129,13 @@ function formatResults(data: TavilySearchResponse, query: string): string {
|
|
|
66
129
|
// ── Extension factory ──────────────────────────────────────────────────
|
|
67
130
|
|
|
68
131
|
export default function (pi: ExtensionAPI): void {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Register /tavily_authenticate command for manual key entry.
|
|
132
|
+
// Register /tavily_setup command for onboarding the key on demand.
|
|
72
133
|
// The input is captured by the TUI and never enters the LLM's context.
|
|
73
|
-
pi.registerCommand("
|
|
74
|
-
description: "
|
|
134
|
+
pi.registerCommand("tavily_setup", {
|
|
135
|
+
description: "Set up or update your Tavily API key (never shown to the agent).",
|
|
75
136
|
handler: async (_args, ctx) => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
authStorage.set("tavily", { type: "api_key" as const, key: apiKey });
|
|
79
|
-
ctx.ui.notify("Tavily API key saved successfully.", "info");
|
|
80
|
-
} else {
|
|
81
|
-
ctx.ui.notify("Authentication cancelled.", "warning");
|
|
82
|
-
}
|
|
137
|
+
const result = await onboardSecret(ctx, { name: "tavily", label: "Tavily" });
|
|
138
|
+
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
83
139
|
},
|
|
84
140
|
});
|
|
85
141
|
|
|
@@ -90,33 +146,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
90
146
|
"Performs a web search using the Tavily API to get real-time information from the internet.",
|
|
91
147
|
parameters: tavilySearchSchema,
|
|
92
148
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
93
|
-
let apiKey = (await
|
|
149
|
+
let apiKey = (await resolveSecret("tavily")) ?? process.env.TAVILY_API_KEY;
|
|
94
150
|
|
|
95
|
-
// Auto-
|
|
151
|
+
// Auto-onboard: run the availability-branched onboarding flow if no key is
|
|
152
|
+
// configured, then re-resolve (env fallback preserved).
|
|
96
153
|
if (!apiKey) {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
content: [{ type: "text", text: "Search cancelled: no Tavily API key provided." }],
|
|
101
|
-
details: { error: "missing_api_key" },
|
|
102
|
-
isError: true,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
authStorage.set("tavily", { type: "api_key" as const, key: newKey });
|
|
106
|
-
apiKey = await authStorage.getApiKey("tavily");
|
|
107
|
-
if (!apiKey) {
|
|
108
|
-
return {
|
|
109
|
-
content: [
|
|
110
|
-
{
|
|
111
|
-
type: "text",
|
|
112
|
-
text: "Failed to resolve Tavily API key. Check your shell configuration.",
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
details: { error: "missing_api_key" },
|
|
116
|
-
isError: true,
|
|
117
|
-
};
|
|
154
|
+
const r = await onboardSecret(ctx, { name: "tavily", label: "Tavily" });
|
|
155
|
+
if (r.ok) {
|
|
156
|
+
apiKey = (await resolveSecret("tavily")) ?? process.env.TAVILY_API_KEY;
|
|
118
157
|
}
|
|
119
158
|
}
|
|
159
|
+
if (!apiKey) {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: "Search cancelled: no Tavily API key provided." }],
|
|
162
|
+
details: { error: "missing_api_key" },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
120
165
|
|
|
121
166
|
try {
|
|
122
167
|
const response = await fetch(TAVILY_SEARCH_ENDPOINT, {
|
|
@@ -141,13 +186,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
141
186
|
},
|
|
142
187
|
],
|
|
143
188
|
details: { status: response.status, body: errorText },
|
|
144
|
-
isError: true,
|
|
145
189
|
};
|
|
146
190
|
}
|
|
147
191
|
|
|
148
|
-
const
|
|
192
|
+
const parsed: unknown = await response.json();
|
|
193
|
+
if (!isJsonValue(parsed)) {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text", text: "Tavily API returned invalid JSON." }],
|
|
196
|
+
details: { error: "invalid_json" },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const data = parsed;
|
|
149
200
|
return {
|
|
150
|
-
content: [
|
|
201
|
+
content: [
|
|
202
|
+
{ type: "text", text: formatResults(toTavilySearchResponse(data), params.query) },
|
|
203
|
+
],
|
|
151
204
|
details: { raw: data },
|
|
152
205
|
};
|
|
153
206
|
} catch (error) {
|
|
@@ -155,7 +208,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
155
208
|
return {
|
|
156
209
|
content: [{ type: "text", text: `Error performing Tavily search: ${message}` }],
|
|
157
210
|
details: { error: message },
|
|
158
|
-
isError: true,
|
|
159
211
|
};
|
|
160
212
|
}
|
|
161
213
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmcombs/pi-tavily-search",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Pi extension that performs real-time web search via the Tavily API.",
|
|
5
5
|
"homepage": "https://github.com/jmcombs/pi-extensions/tree/main/packages/tavily-search",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"image": "https://raw.githubusercontent.com/jmcombs/pi-extensions/main/assets/tavily-search/preview.png"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=22.
|
|
39
|
+
"node": ">=22.19.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@jmcombs/pi-1password": "^1.0.2 || ^2.0.0"
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
42
45
|
"@earendil-works/pi-coding-agent": "*",
|