@wipcomputer/openclaw-tavily 1.0.2

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.
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@wipcomputer/openclaw-tavily",
3
+ "version": "1.0.1",
4
+ "description": "Tavily AI search and content extraction plugin for OpenClaw with 1Password integration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "openclaw": {
8
+ "extensions": [
9
+ "./dist/index.js"
10
+ ]
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch"
15
+ },
16
+ "keywords": [
17
+ "openclaw",
18
+ "openclaw-plugin",
19
+ "tavily",
20
+ "web-search",
21
+ "content-extraction",
22
+ "1password"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "author": "Lēsa <lesaai@icloud.com>",
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "@types/node": "^25.2.2",
31
+ "tsup": "^8.5.1",
32
+ "typescript": "^5.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@1password/sdk": "^0.3.1"
36
+ }
37
+ }
@@ -0,0 +1,374 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { createClient, type Client } from "@1password/sdk";
3
+
4
+ const TAVILY_SEARCH_URL = "https://api.tavily.com/search";
5
+ const TAVILY_EXTRACT_URL = "https://api.tavily.com/extract";
6
+
7
+ const DEFAULT_VAULT = "Agent Secrets";
8
+ const DEFAULT_TOKEN_PATH = `${process.env.HOME}/.openclaw/secrets/op-sa-token`;
9
+ const TAVILY_ITEM_NAME = "Tavily API";
10
+ const TAVILY_FIELD_NAME = "api key";
11
+
12
+ // ── 1Password SDK client (lazy singleton) ───────────────────────────
13
+
14
+ let _client: Client | null = null;
15
+
16
+ async function getClient(tokenPath: string): Promise<Client> {
17
+ if (_client) return _client;
18
+
19
+ if (!existsSync(tokenPath)) {
20
+ throw new Error(`1Password token not found at ${tokenPath}`);
21
+ }
22
+
23
+ const token = readFileSync(tokenPath, "utf-8").trim();
24
+ _client = await createClient({
25
+ auth: token,
26
+ integrationName: "OpenClaw-Tavily",
27
+ integrationVersion: "1.0.0",
28
+ });
29
+ return _client;
30
+ }
31
+
32
+ async function resolveFromOnePassword(
33
+ tokenPath: string,
34
+ vault: string,
35
+ item: string,
36
+ field: string,
37
+ ): Promise<string> {
38
+ const client = await getClient(tokenPath);
39
+ return await client.secrets.resolve(`op://${vault}/${item}/${field}`);
40
+ }
41
+
42
+ // ── MCP-style tool result helper ────────────────────────────────────
43
+
44
+ function toolResult(text: string, isError = false) {
45
+ return {
46
+ content: [{ type: "text" as const, text }],
47
+ details: undefined as unknown,
48
+ ...(isError ? { isError: true } : {}),
49
+ };
50
+ }
51
+
52
+ // ── Tavily API calls ────────────────────────────────────────────────
53
+
54
+ async function tavilySearch(apiKey: string, params: {
55
+ query: string;
56
+ search_depth?: string;
57
+ topic?: string;
58
+ max_results?: number;
59
+ include_answer?: boolean;
60
+ include_raw_content?: boolean;
61
+ include_domains?: string[];
62
+ exclude_domains?: string[];
63
+ }): Promise<any> {
64
+ const body = {
65
+ query: params.query,
66
+ search_depth: params.search_depth || "basic",
67
+ topic: params.topic || "general",
68
+ max_results: params.max_results || 5,
69
+ include_answer: params.include_answer !== false,
70
+ include_raw_content: params.include_raw_content || false,
71
+ include_domains: params.include_domains || [],
72
+ exclude_domains: params.exclude_domains || [],
73
+ };
74
+
75
+ const response = await fetch(TAVILY_SEARCH_URL, {
76
+ method: "POST",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ Authorization: `Bearer ${apiKey}`,
80
+ },
81
+ body: JSON.stringify(body),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ const text = await response.text();
86
+ throw new Error(`Tavily search failed (${response.status}): ${text}`);
87
+ }
88
+
89
+ return response.json();
90
+ }
91
+
92
+ async function tavilyExtract(apiKey: string, urls: string[]): Promise<any> {
93
+ const response = await fetch(TAVILY_EXTRACT_URL, {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ Authorization: `Bearer ${apiKey}`,
98
+ },
99
+ body: JSON.stringify({ urls }),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ const text = await response.text();
104
+ throw new Error(`Tavily extract failed (${response.status}): ${text}`);
105
+ }
106
+
107
+ return response.json();
108
+ }
109
+
110
+ // ── OpenClaw Plugin ─────────────────────────────────────────────────
111
+
112
+ const tavilyPlugin = {
113
+ id: "tavily",
114
+ name: "Tavily Search",
115
+ description: "Web search and content extraction via Tavily API, with 1Password key resolution",
116
+
117
+ configSchema: {
118
+ type: "object" as const,
119
+ additionalProperties: false,
120
+ properties: {
121
+ vault: { type: "string" as const },
122
+ tokenPath: { type: "string" as const },
123
+ },
124
+ },
125
+
126
+ register(api: any) {
127
+ const pluginConfig = api.pluginConfig ||
128
+ (api.config as any)?.plugins?.entries?.tavily?.config || {};
129
+ const vault = pluginConfig.vault || DEFAULT_VAULT;
130
+ const tokenPath = pluginConfig.tokenPath || DEFAULT_TOKEN_PATH;
131
+
132
+ let cachedKey: string | null = null;
133
+
134
+ async function getApiKey(): Promise<string> {
135
+ if (cachedKey) return cachedKey;
136
+
137
+ // 1. Check environment variable (may be set by op-secrets service)
138
+ if (process.env.TAVILY_API_KEY) {
139
+ cachedKey = process.env.TAVILY_API_KEY;
140
+ return cachedKey;
141
+ }
142
+
143
+ // 2. Resolve from 1Password SDK
144
+ try {
145
+ const key = await resolveFromOnePassword(
146
+ tokenPath, vault, TAVILY_ITEM_NAME, TAVILY_FIELD_NAME
147
+ );
148
+ if (key) {
149
+ cachedKey = key;
150
+ process.env.TAVILY_API_KEY = key; // Set for skill access too
151
+ return cachedKey;
152
+ }
153
+ } catch (err: any) {
154
+ throw new Error(
155
+ `Tavily API key not found. Store as "${TAVILY_ITEM_NAME}" in ` +
156
+ `1Password vault "${vault}", or set TAVILY_API_KEY env var. ` +
157
+ `Error: ${err.message}`
158
+ );
159
+ }
160
+
161
+ throw new Error("Tavily API key not found");
162
+ }
163
+
164
+ // ── Tool: tavily_search ───────────────────────────────────────
165
+
166
+ api.registerTool(
167
+ {
168
+ name: "tavily_search",
169
+ label: "Tavily Web Search",
170
+ description:
171
+ "Search the web using Tavily AI search. Returns titles, URLs, content " +
172
+ "snippets, and an AI-generated answer summary. Use for research, " +
173
+ "fact-checking, current events, regulations, and finding specific information.",
174
+ parameters: {
175
+ type: "object",
176
+ required: ["query"],
177
+ properties: {
178
+ query: {
179
+ type: "string",
180
+ description: "Search query string",
181
+ },
182
+ search_depth: {
183
+ type: "string",
184
+ enum: ["basic", "advanced"],
185
+ description: "basic (faster, 1 credit) or advanced (thorough, 2 credits). Default: basic",
186
+ },
187
+ topic: {
188
+ type: "string",
189
+ enum: ["general", "news", "finance"],
190
+ description: "Topic category. Default: general",
191
+ },
192
+ max_results: {
193
+ type: "number",
194
+ description: "Max results (1-20). Default: 5",
195
+ },
196
+ include_answer: {
197
+ type: "boolean",
198
+ description: "Include AI answer summary. Default: true",
199
+ },
200
+ include_domains: {
201
+ type: "array",
202
+ items: { type: "string" },
203
+ description: "Only include results from these domains",
204
+ },
205
+ exclude_domains: {
206
+ type: "array",
207
+ items: { type: "string" },
208
+ description: "Exclude results from these domains",
209
+ },
210
+ },
211
+ },
212
+ async execute(_id: string, params: any) {
213
+ try {
214
+ const apiKey = await getApiKey();
215
+ const result = await tavilySearch(apiKey, params);
216
+
217
+ let output = "";
218
+ if (result.answer) {
219
+ output += `**Answer:** ${result.answer}\n\n`;
220
+ }
221
+ if (result.results?.length > 0) {
222
+ output += `**Results (${result.results.length}):**\n\n`;
223
+ for (const r of result.results) {
224
+ const snippet = r.content?.slice(0, 300) || "(no snippet)";
225
+ const ellipsis = r.content?.length > 300 ? "..." : "";
226
+ output += `- **${r.title}**\n ${r.url}\n ${snippet}${ellipsis}\n Score: ${r.score?.toFixed(3) || "n/a"}\n\n`;
227
+ }
228
+ }
229
+ output += `_Response time: ${result.response_time}s | Credits: ${result.usage?.credits || "?"}_`;
230
+
231
+ return toolResult(output);
232
+ } catch (err: any) {
233
+ return toolResult(`Tavily search error: ${err.message}`, true);
234
+ }
235
+ },
236
+ } as any,
237
+ { optional: true },
238
+ );
239
+
240
+ // ── Tool: tavily_extract ──────────────────────────────────────
241
+
242
+ api.registerTool(
243
+ {
244
+ name: "tavily_extract",
245
+ label: "Tavily Content Extract",
246
+ description:
247
+ "Extract clean content from one or more URLs using Tavily. " +
248
+ "Returns raw text content of web pages. Better than web_fetch for many sites.",
249
+ parameters: {
250
+ type: "object",
251
+ required: ["urls"],
252
+ properties: {
253
+ urls: {
254
+ type: "array",
255
+ items: { type: "string" },
256
+ description: "URLs to extract content from",
257
+ },
258
+ },
259
+ },
260
+ async execute(_id: string, params: { urls: string[] }) {
261
+ try {
262
+ const apiKey = await getApiKey();
263
+ const result = await tavilyExtract(apiKey, params.urls);
264
+
265
+ let output = "";
266
+ if (result.results?.length > 0) {
267
+ for (const r of result.results) {
268
+ output += `## ${r.url}\n\n`;
269
+ const content = r.raw_content?.slice(0, 5000) || "(no content)";
270
+ output += content;
271
+ if (r.raw_content?.length > 5000) output += "\n...(truncated)";
272
+ output += "\n\n---\n\n";
273
+ }
274
+ }
275
+ if (result.failed_results?.length > 0) {
276
+ output += `**Failed:** ${result.failed_results.map((f: any) => f.url).join(", ")}\n`;
277
+ }
278
+ output += `_Response time: ${result.response_time}s | Credits: ${result.usage?.credits || "?"}_`;
279
+
280
+ return toolResult(output);
281
+ } catch (err: any) {
282
+ return toolResult(`Tavily extract error: ${err.message}`, true);
283
+ }
284
+ },
285
+ } as any,
286
+ { optional: true },
287
+ );
288
+
289
+ // ── Service: resolve key at startup ───────────────────────────
290
+
291
+ api.registerService?.({
292
+ id: "tavily-key-resolver",
293
+ async start(ctx: any) {
294
+ if (process.env.TAVILY_API_KEY) {
295
+ ctx.logger.info("tavily: TAVILY_API_KEY already set in environment");
296
+ return;
297
+ }
298
+ try {
299
+ const key = await resolveFromOnePassword(
300
+ tokenPath, vault, TAVILY_ITEM_NAME, TAVILY_FIELD_NAME
301
+ );
302
+ if (key) {
303
+ process.env.TAVILY_API_KEY = key;
304
+ cachedKey = key;
305
+ ctx.logger.info("tavily: set TAVILY_API_KEY from 1Password");
306
+ }
307
+ } catch (err: any) {
308
+ ctx.logger.warn(`tavily: key not found in 1Password (${err.message}). Add "${TAVILY_ITEM_NAME}" to vault "${vault}".`);
309
+ }
310
+ },
311
+ });
312
+
313
+ // ── CLI commands ─────────────────────────────────────────────
314
+
315
+ api.registerCli?.(
316
+ ({ program }: any) => {
317
+ const cmd = program
318
+ .command("tavily")
319
+ .description("Tavily search plugin");
320
+
321
+ cmd
322
+ .command("test")
323
+ .description("Test Tavily API connectivity")
324
+ .action(async () => {
325
+ try {
326
+ const key = await getApiKey();
327
+ console.log("Tavily API key resolved. Testing search...");
328
+ const result = await tavilySearch(key, {
329
+ query: "test",
330
+ max_results: 1,
331
+ include_answer: false,
332
+ });
333
+ console.log(`Tavily connection OK — ${result.results?.length || 0} result(s), ${result.response_time}s`);
334
+ } catch (err: any) {
335
+ console.error("Tavily test FAILED:", err.message);
336
+ process.exit(1);
337
+ }
338
+ });
339
+
340
+ cmd
341
+ .command("search")
342
+ .description("Quick search from CLI")
343
+ .argument("<query>", "Search query")
344
+ .option("-n, --max-results <n>", "Max results", "5")
345
+ .action(async (query: string, opts: { maxResults: string }) => {
346
+ try {
347
+ const key = await getApiKey();
348
+ const result = await tavilySearch(key, {
349
+ query,
350
+ max_results: parseInt(opts.maxResults),
351
+ include_answer: true,
352
+ });
353
+ if (result.answer) {
354
+ console.log(`\nAnswer: ${result.answer}\n`);
355
+ }
356
+ for (const r of (result.results || [])) {
357
+ console.log(`- ${r.title}`);
358
+ console.log(` ${r.url}`);
359
+ console.log(` ${r.content?.slice(0, 200) || ""}\n`);
360
+ }
361
+ } catch (err: any) {
362
+ console.error("Search failed:", err.message);
363
+ process.exit(1);
364
+ }
365
+ });
366
+ },
367
+ { commands: ["tavily"] },
368
+ );
369
+
370
+ api.logger.info(`tavily plugin registered (vault: ${vault}, key: ${process.env.TAVILY_API_KEY ? "env" : "1password"})`);
371
+ },
372
+ };
373
+
374
+ export default tavilyPlugin;
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022", "DOM"],
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 1.0.2 (2026-07-20)
4
+
5
+ # Tavily v1.0.2
6
+
7
+ The WIP Tavily extension now declares its built runtime entry, two runtime tools, and explicit startup activation. OpenClaw 2026.6.11 can register the search and extraction tools when this installed extension is selected over the bundled Tavily plugin.
8
+
9
+ This release also adds the complete MIT licensing scaffold required by the release pipeline. The package metadata, repository license, license-guard configuration, README, and contributor agreement now agree on the current licensing terms.
10
+
11
+ Closes #5.
package/CLA.md ADDED
@@ -0,0 +1,17 @@
1
+ ###### WIP Computer
2
+
3
+ # Contributor License Agreement
4
+
5
+ By submitting a pull request to this repository, you agree to the following:
6
+
7
+ 1. **You grant WIP Computer, Inc. a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license** to use, reproduce, modify, distribute, sublicense, and otherwise exploit your contribution under any license, including commercial licenses.
8
+
9
+ 2. **You retain copyright** to your contribution. This agreement does not transfer ownership. You can use your own code however you want.
10
+
11
+ 3. **You confirm** that your contribution is your original work, or that you have the right to submit it under these terms.
12
+
13
+ 4. **You understand** that your contribution may be used in both open source and commercial versions of this software.
14
+
15
+ This is standard open source governance. The agreement keeps contributions usable by WIP Computer while contributors retain ownership of their work.
16
+
17
+ If you have questions, open an issue or reach out.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WIP Computer, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # openclaw-tavily
2
+
3
+ Tavily AI search and content extraction plugin for OpenClaw.
4
+
5
+ ## What It Does
6
+
7
+ Adds two tools to OpenClaw:
8
+
9
+ - **`tavily_search`** — AI-powered web search with answer summaries
10
+ - **`tavily_extract`** — Clean content extraction from URLs
11
+
12
+ Both tools use 1Password for secure API key storage (no plaintext keys in config).
13
+
14
+ ## Features
15
+
16
+ - **Search modes**: `basic` (fast, 1 credit) or `advanced` (thorough, 2 credits)
17
+ - **Topic filtering**: `general`, `news`, or `finance`
18
+ - **Domain control**: Include or exclude specific domains
19
+ - **Answer summaries**: AI-generated answer from search results
20
+ - **Multi-URL extraction**: Extract clean content from multiple URLs in one call
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install
26
+ npm run build
27
+ ```
28
+
29
+ ## Set Up Tavily
30
+
31
+ 1. **Store Tavily API key in 1Password:**
32
+ ```bash
33
+ op item create \
34
+ --category="API Credential" \
35
+ --title="Tavily API" \
36
+ --vault="Agent Secrets" \
37
+ 'api key[password]=tvly-YOUR_KEY_HERE'
38
+ ```
39
+
40
+ 2. **Add to OpenClaw:**
41
+ Copy dist/ to `~/.openclaw/extensions/openclaw-tavily/`
42
+
43
+ 3. **Enable in openclaw.json:**
44
+ ```json
45
+ {
46
+ "plugins": {
47
+ "entries": {
48
+ "openclaw-tavily": {
49
+ "enabled": true
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Search
59
+ ```typescript
60
+ await tavily_search({
61
+ query: "latest AI regulation news",
62
+ search_depth: "advanced",
63
+ topic: "news",
64
+ max_results: 10
65
+ })
66
+ ```
67
+
68
+ ### Extract
69
+ ```typescript
70
+ await tavily_extract({
71
+ urls: [
72
+ "https://example.com/article1",
73
+ "https://example.com/article2"
74
+ ]
75
+ })
76
+ ```
77
+
78
+ ## How It Works
79
+
80
+ - **1Password SDK**: Secure credential resolution via `op://Agent Secrets/Tavily API/api key`
81
+ - **Tavily API**: Search at `https://api.tavily.com/search`, extract at `/extract`
82
+ - **MCP-compatible**: Returns tool results in MCP format
83
+
84
+ ## Why 1Password Integration?
85
+
86
+ No more API keys in config files. The Tavily API key is stored in 1Password's "Agent Secrets" vault and resolved at runtime via the 1Password SDK.
87
+
88
+ ## Status
89
+
90
+ **Active/Deployed** — Running in Lēsa's OpenClaw environment.
91
+
92
+ ## Author
93
+
94
+ Built by Lēsa / WIP.computer 🏴‍☠️
95
+
96
+ ## License
97
+
98
+ MIT. Built by Parker Todd Brooks, Lēsa, Claude Code, and Codex.
@@ -0,0 +1,7 @@
1
+ # Tavily v1.0.2
2
+
3
+ The WIP Tavily extension now declares its built runtime entry, two runtime tools, and explicit startup activation. OpenClaw 2026.6.11 can register the search and extraction tools when this installed extension is selected over the bundled Tavily plugin.
4
+
5
+ This release also adds the complete MIT licensing scaffold required by the release pipeline. The package metadata, repository license, license-guard configuration, README, and contributor agreement now agree on the current licensing terms.
6
+
7
+ Closes #5.
@@ -0,0 +1,19 @@
1
+ declare const tavilyPlugin: {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ configSchema: {
6
+ type: "object";
7
+ additionalProperties: boolean;
8
+ properties: {
9
+ vault: {
10
+ type: "string";
11
+ };
12
+ tokenPath: {
13
+ type: "string";
14
+ };
15
+ };
16
+ };
17
+ register(api: any): void;
18
+ };
19
+ export default tavilyPlugin;