@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.
- package/.license-guard.json +7 -0
- package/.worktrees/cc-mini/v102-issue-ref/.license-guard.json +7 -0
- package/.worktrees/cc-mini/v102-issue-ref/CLA.md +17 -0
- package/.worktrees/cc-mini/v102-issue-ref/LICENSE +21 -0
- package/.worktrees/cc-mini/v102-issue-ref/README.md +98 -0
- package/.worktrees/cc-mini/v102-issue-ref/RELEASE-NOTES-v1-0-2.md +7 -0
- package/.worktrees/cc-mini/v102-issue-ref/openclaw.plugin.json +27 -0
- package/.worktrees/cc-mini/v102-issue-ref/package-lock.json +1509 -0
- package/.worktrees/cc-mini/v102-issue-ref/package.json +37 -0
- package/.worktrees/cc-mini/v102-issue-ref/src/index.ts +374 -0
- package/.worktrees/cc-mini/v102-issue-ref/tsconfig.json +16 -0
- package/CHANGELOG.md +11 -0
- package/CLA.md +17 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/_trash/RELEASE-NOTES-v1-0-2.md +7 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +311 -0
- package/openclaw.plugin.json +27 -0
- package/package.json +37 -0
- package/src/index.ts +374 -0
- package/tsconfig.json +16 -0
package/src/index.ts
ADDED
|
@@ -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;
|
package/tsconfig.json
ADDED
|
@@ -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
|
+
}
|