@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.
Files changed (3) hide show
  1. package/README.md +84 -19
  2. package/index.ts +99 -47
  3. 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
- You must configure a Tavily API key. Pi resolves the key in this order:
80
+ The `tavily_search` tool resolves the key in this order:
29
81
 
30
- 1. `AuthStorage` under the `tavily` key (`~/.pi/agent/auth.json`) — **recommended**.
31
- 2. The `TAVILY_API_KEY` environment variable.
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
- ### Option 1 — `~/.pi/agent/auth.json` (recommended)
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
- #### Plain key
89
+ ### Option 1 — `/tavily_setup` (recommended)
36
90
 
37
- ```json
38
- {
39
- "tavily": {
40
- "type": "api_key",
41
- "key": "tvly-..."
42
- }
43
- }
91
+ Run the command and follow the flow:
92
+
93
+ ```
94
+ /tavily_setup
44
95
  ```
45
96
 
46
- #### Shell-resolved key (macOS Keychain)
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": "!security find-generic-password -ws tavily"
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": "!pass show tavily"
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 2 — environment variable
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.72.0` (uses `AuthStorage` and `ExtensionAPI`)
102
- - Node `>= 20.6.0`
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. If no Tavily API key is configured, the tool prompts the user
6
- * interactively via the TUI (never leaking the key into the agent's context).
7
- * The key can also be set manually by running `/tavily_authenticate`.
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
- * Supported configuration (if not using interactive prompt):
10
- * 1. `AuthStorage` under the "tavily" key (`~/.pi/agent/auth.json`)
11
- * 2. The `TAVILY_API_KEY` environment variable
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 { AuthStorage, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
- import { Type, type Static } from "typebox";
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
- const authStorage = AuthStorage.create();
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("tavily_authenticate", {
74
- description: "Securely save your Tavily API key (input never visible to LLM).",
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 apiKey = await ctx.ui.input("Enter your Tavily API key:");
77
- if (apiKey) {
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 authStorage.getApiKey("tavily")) ?? process.env.TAVILY_API_KEY;
149
+ let apiKey = (await resolveSecret("tavily")) ?? process.env.TAVILY_API_KEY;
94
150
 
95
- // Auto-authenticate: prompt for key if none is configured
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 newKey = await ctx.ui.input("Enter your Tavily API key:");
98
- if (!newKey) {
99
- return {
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 data = (await response.json()) as TavilySearchResponse;
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: [{ type: "text", text: formatResults(data, params.query) }],
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": "2.1.0",
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.0.0"
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": "*",