@oh-my-pi/anthropic-websearch 0.9.2 → 1.3.371

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 (2) hide show
  1. package/package.json +1 -1
  2. package/tools/index.ts +356 -372
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/anthropic-websearch",
3
- "version": "0.9.2",
3
+ "version": "1.3.371",
4
4
  "description": "Anthropic Claude web search tool for pi",
5
5
  "keywords": [
6
6
  "omp-plugin",
package/tools/index.ts CHANGED
@@ -10,134 +10,130 @@
10
10
  * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL as final fallback
11
11
  */
12
12
 
13
- import * as fs from "node:fs";
14
- import * as os from "node:os";
15
- import * as path from "node:path";
16
- import { Type, type TSchema } from "@sinclair/typebox";
17
- import type {
18
- CustomAgentTool,
19
- CustomToolFactory,
20
- ToolAPI,
21
- } from "@mariozechner/pi-coding-agent";
22
- import runtime from "./runtime.json";
23
-
24
- const DEFAULT_BASE_URL = "https://api.anthropic.com";
25
- const DEFAULT_MODEL = "claude-sonnet-4-5-20250514";
13
+ import * as fs from 'node:fs'
14
+ import * as os from 'node:os'
15
+ import * as path from 'node:path'
16
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
17
+ import { type TSchema, Type } from '@sinclair/typebox'
18
+ import runtime from './runtime.json'
19
+
20
+ const DEFAULT_BASE_URL = 'https://api.anthropic.com'
21
+ const DEFAULT_MODEL = 'claude-sonnet-4-5-20250514'
26
22
 
27
23
  interface RuntimeConfig {
28
- options?: {
29
- model?: string;
30
- apiKey?: string;
31
- baseUrl?: string;
32
- };
24
+ options?: {
25
+ model?: string
26
+ apiKey?: string
27
+ baseUrl?: string
28
+ }
33
29
  }
34
30
 
35
31
  interface AuthConfig {
36
- apiKey: string;
37
- baseUrl: string;
38
- isOAuth: boolean;
32
+ apiKey: string
33
+ baseUrl: string
34
+ isOAuth: boolean
39
35
  }
40
36
 
41
37
  interface ModelsJson {
42
- providers?: Record<string, {
43
- baseUrl?: string;
44
- apiKey?: string;
45
- api?: string;
46
- }>;
38
+ providers?: Record<
39
+ string,
40
+ {
41
+ baseUrl?: string
42
+ apiKey?: string
43
+ api?: string
44
+ }
45
+ >
47
46
  }
48
47
 
49
48
  interface AuthJson {
50
- anthropic?: {
51
- type: "oauth";
52
- access: string;
53
- refresh?: string;
54
- expires: number;
55
- };
49
+ anthropic?: {
50
+ type: 'oauth'
51
+ access: string
52
+ refresh?: string
53
+ expires: number
54
+ }
56
55
  }
57
56
 
58
57
  /**
59
58
  * Parse a .env file and return key-value pairs
60
59
  */
61
60
  function parseEnvFile(filePath: string): Record<string, string> {
62
- const result: Record<string, string> = {};
63
- if (!fs.existsSync(filePath)) return result;
64
-
65
- try {
66
- const content = fs.readFileSync(filePath, "utf-8");
67
- for (const line of content.split("\n")) {
68
- const trimmed = line.trim();
69
- if (!trimmed || trimmed.startsWith("#")) continue;
70
-
71
- const eqIndex = trimmed.indexOf("=");
72
- if (eqIndex === -1) continue;
73
-
74
- const key = trimmed.slice(0, eqIndex).trim();
75
- let value = trimmed.slice(eqIndex + 1).trim();
76
-
77
- if (
78
- (value.startsWith('"') && value.endsWith('"')) ||
79
- (value.startsWith("'") && value.endsWith("'"))
80
- ) {
81
- value = value.slice(1, -1);
82
- }
61
+ const result: Record<string, string> = {}
62
+ if (!fs.existsSync(filePath)) return result
63
+
64
+ try {
65
+ const content = fs.readFileSync(filePath, 'utf-8')
66
+ for (const line of content.split('\n')) {
67
+ const trimmed = line.trim()
68
+ if (!trimmed || trimmed.startsWith('#')) continue
83
69
 
84
- result[key] = value;
85
- }
86
- } catch {
87
- // Ignore read errors
88
- }
70
+ const eqIndex = trimmed.indexOf('=')
71
+ if (eqIndex === -1) continue
89
72
 
90
- return result;
73
+ const key = trimmed.slice(0, eqIndex).trim()
74
+ let value = trimmed.slice(eqIndex + 1).trim()
75
+
76
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
77
+ value = value.slice(1, -1)
78
+ }
79
+
80
+ result[key] = value
81
+ }
82
+ } catch {
83
+ // Ignore read errors
84
+ }
85
+
86
+ return result
91
87
  }
92
88
 
93
89
  /**
94
90
  * Get env var from process.env or .env files
95
91
  */
96
92
  function getEnv(key: string): string | undefined {
97
- if (process.env[key]) return process.env[key];
93
+ if (process.env[key]) return process.env[key]
98
94
 
99
- const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
100
- if (localEnv[key]) return localEnv[key];
95
+ const localEnv = parseEnvFile(path.join(process.cwd(), '.env'))
96
+ if (localEnv[key]) return localEnv[key]
101
97
 
102
- const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
103
- if (homeEnv[key]) return homeEnv[key];
98
+ const homeEnv = parseEnvFile(path.join(os.homedir(), '.env'))
99
+ if (homeEnv[key]) return homeEnv[key]
104
100
 
105
- return undefined;
101
+ return undefined
106
102
  }
107
103
 
108
104
  /**
109
105
  * Read JSON file safely
110
106
  */
111
107
  function readJson<T>(filePath: string): T | null {
112
- try {
113
- if (!fs.existsSync(filePath)) return null;
114
- const content = fs.readFileSync(filePath, "utf-8");
115
- return JSON.parse(content) as T;
116
- } catch {
117
- return null;
118
- }
108
+ try {
109
+ if (!fs.existsSync(filePath)) return null
110
+ const content = fs.readFileSync(filePath, 'utf-8')
111
+ return JSON.parse(content) as T
112
+ } catch {
113
+ return null
114
+ }
119
115
  }
120
116
 
121
117
  /**
122
118
  * Check if a token is an OAuth token
123
119
  */
124
120
  function isOAuthToken(apiKey: string): boolean {
125
- return apiKey.includes("sk-ant-oat");
121
+ return apiKey.includes('sk-ant-oat')
126
122
  }
127
123
 
128
124
  /**
129
125
  * Get runtime config value, falling back to env var
130
126
  */
131
- function getConfig(key: "model" | "apiKey" | "baseUrl"): string | undefined {
132
- const cfg = (runtime as RuntimeConfig).options ?? {};
133
- if (cfg[key]) return cfg[key];
134
-
135
- const envMap = {
136
- model: "ANTHROPIC_SEARCH_MODEL",
137
- apiKey: "ANTHROPIC_SEARCH_API_KEY",
138
- baseUrl: "ANTHROPIC_SEARCH_BASE_URL",
139
- };
140
- return getEnv(envMap[key]);
127
+ function getConfig(key: 'model' | 'apiKey' | 'baseUrl'): string | undefined {
128
+ const cfg = (runtime as RuntimeConfig).options ?? {}
129
+ if (cfg[key]) return cfg[key]
130
+
131
+ const envMap = {
132
+ model: 'ANTHROPIC_SEARCH_MODEL',
133
+ apiKey: 'ANTHROPIC_SEARCH_API_KEY',
134
+ baseUrl: 'ANTHROPIC_SEARCH_BASE_URL',
135
+ }
136
+ return getEnv(envMap[key])
141
137
  }
142
138
 
143
139
  /**
@@ -148,343 +144,331 @@ function getConfig(key: "model" | "apiKey" | "baseUrl"): string | undefined {
148
144
  * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
149
145
  */
150
146
  function findAuthConfig(): AuthConfig | null {
151
- const piAgentDir = path.join(os.homedir(), ".pi", "agent");
152
-
153
- // 1. Explicit config or env vars
154
- const searchApiKey = getConfig("apiKey");
155
- const searchBaseUrl = getConfig("baseUrl");
156
- if (searchApiKey) {
157
- return {
158
- apiKey: searchApiKey,
159
- baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
160
- isOAuth: isOAuthToken(searchApiKey),
161
- };
162
- }
163
-
164
- // 2. Provider with api="anthropic-messages" in models.json
165
- const modelsJson = readJson<ModelsJson>(path.join(piAgentDir, "models.json"));
166
- if (modelsJson?.providers) {
167
- for (const [_name, provider] of Object.entries(modelsJson.providers)) {
168
- if (provider.api === "anthropic-messages" && provider.apiKey && provider.apiKey !== "none") {
169
- return {
170
- apiKey: provider.apiKey,
171
- baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
172
- isOAuth: isOAuthToken(provider.apiKey),
173
- };
147
+ const piAgentDir = path.join(os.homedir(), '.pi', 'agent')
148
+
149
+ // 1. Explicit config or env vars
150
+ const searchApiKey = getConfig('apiKey')
151
+ const searchBaseUrl = getConfig('baseUrl')
152
+ if (searchApiKey) {
153
+ return {
154
+ apiKey: searchApiKey,
155
+ baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
156
+ isOAuth: isOAuthToken(searchApiKey),
174
157
  }
175
- }
176
- // Also check for providers with baseUrl but apiKey="none" (proxy)
177
- for (const [_name, provider] of Object.entries(modelsJson.providers)) {
178
- if (provider.api === "anthropic-messages" && provider.baseUrl) {
179
- return {
180
- apiKey: provider.apiKey ?? "",
181
- baseUrl: provider.baseUrl,
182
- isOAuth: false,
183
- };
158
+ }
159
+
160
+ // 2. Provider with api="anthropic-messages" in models.json
161
+ const modelsJson = readJson<ModelsJson>(path.join(piAgentDir, 'models.json'))
162
+ if (modelsJson?.providers) {
163
+ for (const [_name, provider] of Object.entries(modelsJson.providers)) {
164
+ if (provider.api === 'anthropic-messages' && provider.apiKey && provider.apiKey !== 'none') {
165
+ return {
166
+ apiKey: provider.apiKey,
167
+ baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
168
+ isOAuth: isOAuthToken(provider.apiKey),
169
+ }
170
+ }
184
171
  }
185
- }
186
- }
187
-
188
- // 3. OAuth credentials in auth.json
189
- const authJson = readJson<AuthJson>(path.join(piAgentDir, "auth.json"));
190
- if (authJson?.anthropic?.type === "oauth" && authJson.anthropic.access) {
191
- // Check if not expired (with 5 min buffer)
192
- if (authJson.anthropic.expires > Date.now() + 5 * 60 * 1000) {
172
+ // Also check for providers with baseUrl but apiKey="none" (proxy)
173
+ for (const [_name, provider] of Object.entries(modelsJson.providers)) {
174
+ if (provider.api === 'anthropic-messages' && provider.baseUrl) {
175
+ return {
176
+ apiKey: provider.apiKey ?? '',
177
+ baseUrl: provider.baseUrl,
178
+ isOAuth: false,
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ // 3. OAuth credentials in auth.json
185
+ const authJson = readJson<AuthJson>(path.join(piAgentDir, 'auth.json'))
186
+ if (authJson?.anthropic?.type === 'oauth' && authJson.anthropic.access) {
187
+ // Check if not expired (with 5 min buffer)
188
+ if (authJson.anthropic.expires > Date.now() + 5 * 60 * 1000) {
189
+ return {
190
+ apiKey: authJson.anthropic.access,
191
+ baseUrl: DEFAULT_BASE_URL,
192
+ isOAuth: true,
193
+ }
194
+ }
195
+ }
196
+
197
+ // 4. Generic ANTHROPIC_API_KEY fallback
198
+ const apiKey = getEnv('ANTHROPIC_API_KEY')
199
+ const baseUrl = getEnv('ANTHROPIC_BASE_URL')
200
+ if (apiKey) {
193
201
  return {
194
- apiKey: authJson.anthropic.access,
195
- baseUrl: DEFAULT_BASE_URL,
196
- isOAuth: true,
197
- };
198
- }
199
- }
200
-
201
- // 4. Generic ANTHROPIC_API_KEY fallback
202
- const apiKey = getEnv("ANTHROPIC_API_KEY");
203
- const baseUrl = getEnv("ANTHROPIC_BASE_URL");
204
- if (apiKey) {
205
- return {
206
- apiKey,
207
- baseUrl: baseUrl ?? DEFAULT_BASE_URL,
208
- isOAuth: isOAuthToken(apiKey),
209
- };
210
- }
211
-
212
- return null;
202
+ apiKey,
203
+ baseUrl: baseUrl ?? DEFAULT_BASE_URL,
204
+ isOAuth: isOAuthToken(apiKey),
205
+ }
206
+ }
207
+
208
+ return null
213
209
  }
214
210
 
215
211
  /**
216
212
  * Build headers for Anthropic API request
217
213
  */
218
214
  function buildHeaders(auth: AuthConfig): Record<string, string> {
219
- const betas = ["web-search-2025-03-05"];
220
-
221
- if (auth.isOAuth) {
222
- // OAuth requires additional beta headers and stainless telemetry
223
- betas.push(
224
- "oauth-2025-04-20",
225
- "claude-code-20250219",
226
- "prompt-caching-2024-07-31",
227
- );
228
-
229
- return {
230
- "anthropic-version": "2023-06-01",
231
- "authorization": `Bearer ${auth.apiKey}`,
232
- "accept": "application/json",
233
- "content-type": "application/json",
234
- "anthropic-dangerous-direct-browser-access": "true",
235
- "anthropic-beta": betas.join(","),
236
- "user-agent": "claude-cli/2.0.46 (external, cli)",
237
- "x-app": "cli",
238
- // Stainless SDK telemetry headers (required for OAuth)
239
- "x-stainless-arch": "x64",
240
- "x-stainless-lang": "js",
241
- "x-stainless-os": "Linux",
242
- "x-stainless-package-version": "0.60.0",
243
- "x-stainless-retry-count": "1",
244
- "x-stainless-runtime": "node",
245
- "x-stainless-runtime-version": "v24.3.0",
246
- };
247
- } else {
248
- // Standard API key auth
249
- return {
250
- "anthropic-version": "2023-06-01",
251
- "x-api-key": auth.apiKey,
252
- "accept": "application/json",
253
- "content-type": "application/json",
254
- "anthropic-beta": betas.join(","),
255
- };
256
- }
215
+ const betas = ['web-search-2025-03-05']
216
+
217
+ if (auth.isOAuth) {
218
+ // OAuth requires additional beta headers and stainless telemetry
219
+ betas.push('oauth-2025-04-20', 'claude-code-20250219', 'prompt-caching-2024-07-31')
220
+
221
+ return {
222
+ 'anthropic-version': '2023-06-01',
223
+ authorization: `Bearer ${auth.apiKey}`,
224
+ accept: 'application/json',
225
+ 'content-type': 'application/json',
226
+ 'anthropic-dangerous-direct-browser-access': 'true',
227
+ 'anthropic-beta': betas.join(','),
228
+ 'user-agent': 'claude-cli/2.0.46 (external, cli)',
229
+ 'x-app': 'cli',
230
+ // Stainless SDK telemetry headers (required for OAuth)
231
+ 'x-stainless-arch': 'x64',
232
+ 'x-stainless-lang': 'js',
233
+ 'x-stainless-os': 'Linux',
234
+ 'x-stainless-package-version': '0.60.0',
235
+ 'x-stainless-retry-count': '1',
236
+ 'x-stainless-runtime': 'node',
237
+ 'x-stainless-runtime-version': 'v24.3.0',
238
+ }
239
+ } else {
240
+ // Standard API key auth
241
+ return {
242
+ 'anthropic-version': '2023-06-01',
243
+ 'x-api-key': auth.apiKey,
244
+ accept: 'application/json',
245
+ 'content-type': 'application/json',
246
+ 'anthropic-beta': betas.join(','),
247
+ }
248
+ }
257
249
  }
258
250
 
259
251
  /**
260
252
  * Build API URL (OAuth requires ?beta=true)
261
253
  */
262
254
  function buildUrl(auth: AuthConfig): string {
263
- const base = `${auth.baseUrl}/v1/messages`;
264
- return auth.isOAuth ? `${base}?beta=true` : base;
255
+ const base = `${auth.baseUrl}/v1/messages`
256
+ return auth.isOAuth ? `${base}?beta=true` : base
265
257
  }
266
258
 
267
259
  // Response types
268
260
  interface WebSearchResult {
269
- type: "web_search_result";
270
- title: string;
271
- url: string;
272
- encrypted_content: string;
273
- page_age: string | null;
261
+ type: 'web_search_result'
262
+ title: string
263
+ url: string
264
+ encrypted_content: string
265
+ page_age: string | null
274
266
  }
275
267
 
276
268
  interface Citation {
277
- type: "web_search_result_location";
278
- url: string;
279
- title: string;
280
- cited_text: string;
281
- encrypted_index: string;
269
+ type: 'web_search_result_location'
270
+ url: string
271
+ title: string
272
+ cited_text: string
273
+ encrypted_index: string
282
274
  }
283
275
 
284
276
  interface ContentBlock {
285
- type: string;
286
- text?: string;
287
- citations?: Citation[];
288
- name?: string;
289
- input?: { query: string };
290
- content?: WebSearchResult[];
277
+ type: string
278
+ text?: string
279
+ citations?: Citation[]
280
+ name?: string
281
+ input?: { query: string }
282
+ content?: WebSearchResult[]
291
283
  }
292
284
 
293
285
  interface ApiResponse {
294
- id: string;
295
- model: string;
296
- content: ContentBlock[];
297
- usage: {
298
- input_tokens: number;
299
- output_tokens: number;
300
- cache_read_input_tokens?: number;
301
- cache_creation_input_tokens?: number;
302
- server_tool_use?: { web_search_requests: number };
303
- };
286
+ id: string
287
+ model: string
288
+ content: ContentBlock[]
289
+ usage: {
290
+ input_tokens: number
291
+ output_tokens: number
292
+ cache_read_input_tokens?: number
293
+ cache_creation_input_tokens?: number
294
+ server_tool_use?: { web_search_requests: number }
295
+ }
304
296
  }
305
297
 
306
298
  /**
307
299
  * Call Anthropic API with web search
308
300
  */
309
301
  async function callWebSearch(
310
- auth: AuthConfig,
311
- model: string,
312
- query: string,
313
- systemPrompt?: string,
314
- maxTokens?: number,
302
+ auth: AuthConfig,
303
+ model: string,
304
+ query: string,
305
+ systemPrompt?: string,
306
+ maxTokens?: number
315
307
  ): Promise<ApiResponse> {
316
- const url = buildUrl(auth);
317
- const headers = buildHeaders(auth);
318
-
319
- // Build system blocks
320
- const systemBlocks: Array<{ type: string; text: string; cache_control?: { type: string } }> = [];
321
-
322
- if (auth.isOAuth) {
323
- // OAuth requires Claude Code identity with cache_control
324
- systemBlocks.push({
325
- type: "text",
326
- text: "You are Claude Code, Anthropic's official CLI for Claude.",
327
- cache_control: { type: "ephemeral" },
328
- });
329
- }
330
-
331
- if (systemPrompt) {
332
- systemBlocks.push({
333
- type: "text",
334
- text: systemPrompt,
335
- ...(auth.isOAuth ? { cache_control: { type: "ephemeral" } } : {}),
336
- });
337
- }
338
-
339
- const body: Record<string, unknown> = {
340
- model,
341
- max_tokens: maxTokens ?? 4096,
342
- messages: [{ role: "user", content: query }],
343
- tools: [{ type: "web_search_20250305", name: "web_search" }],
344
- };
345
-
346
- if (systemBlocks.length > 0) {
347
- body.system = systemBlocks;
348
- }
349
-
350
- const response = await fetch(url, {
351
- method: "POST",
352
- headers,
353
- body: JSON.stringify(body),
354
- });
355
-
356
- if (!response.ok) {
357
- const errorText = await response.text();
358
- throw new Error(`Anthropic API error (${response.status}): ${errorText}`);
359
- }
360
-
361
- return response.json() as Promise<ApiResponse>;
308
+ const url = buildUrl(auth)
309
+ const headers = buildHeaders(auth)
310
+
311
+ // Build system blocks
312
+ const systemBlocks: Array<{ type: string; text: string; cache_control?: { type: string } }> = []
313
+
314
+ if (auth.isOAuth) {
315
+ // OAuth requires Claude Code identity with cache_control
316
+ systemBlocks.push({
317
+ type: 'text',
318
+ text: "You are Claude Code, Anthropic's official CLI for Claude.",
319
+ cache_control: { type: 'ephemeral' },
320
+ })
321
+ }
322
+
323
+ if (systemPrompt) {
324
+ systemBlocks.push({
325
+ type: 'text',
326
+ text: systemPrompt,
327
+ ...(auth.isOAuth ? { cache_control: { type: 'ephemeral' } } : {}),
328
+ })
329
+ }
330
+
331
+ const body: Record<string, unknown> = {
332
+ model,
333
+ max_tokens: maxTokens ?? 4096,
334
+ messages: [{ role: 'user', content: query }],
335
+ tools: [{ type: 'web_search_20250305', name: 'web_search' }],
336
+ }
337
+
338
+ if (systemBlocks.length > 0) {
339
+ body.system = systemBlocks
340
+ }
341
+
342
+ const response = await fetch(url, {
343
+ method: 'POST',
344
+ headers,
345
+ body: JSON.stringify(body),
346
+ })
347
+
348
+ if (!response.ok) {
349
+ const errorText = await response.text()
350
+ throw new Error(`Anthropic API error (${response.status}): ${errorText}`)
351
+ }
352
+
353
+ return response.json() as Promise<ApiResponse>
362
354
  }
363
355
 
364
356
  /**
365
357
  * Format response for display
366
358
  */
367
359
  function formatResponse(response: ApiResponse): { text: string; details: unknown } {
368
- const parts: string[] = [];
369
- const searchQueries: string[] = [];
370
- const sources: Array<{ title: string; url: string; age: string | null }> = [];
371
- const citations: Citation[] = [];
372
-
373
- for (const block of response.content) {
374
- if (block.type === "server_tool_use" && block.name === "web_search") {
375
- searchQueries.push(block.input?.query ?? "");
376
- } else if (block.type === "web_search_tool_result" && block.content) {
377
- for (const result of block.content) {
378
- if (result.type === "web_search_result") {
379
- sources.push({
380
- title: result.title,
381
- url: result.url,
382
- age: result.page_age,
383
- });
384
- }
360
+ const parts: string[] = []
361
+ const searchQueries: string[] = []
362
+ const sources: Array<{ title: string; url: string; age: string | null }> = []
363
+ const citations: Citation[] = []
364
+
365
+ for (const block of response.content) {
366
+ if (block.type === 'server_tool_use' && block.name === 'web_search') {
367
+ searchQueries.push(block.input?.query ?? '')
368
+ } else if (block.type === 'web_search_tool_result' && block.content) {
369
+ for (const result of block.content) {
370
+ if (result.type === 'web_search_result') {
371
+ sources.push({
372
+ title: result.title,
373
+ url: result.url,
374
+ age: result.page_age,
375
+ })
376
+ }
377
+ }
378
+ } else if (block.type === 'text' && block.text) {
379
+ parts.push(block.text)
380
+ if (block.citations) {
381
+ citations.push(...block.citations)
382
+ }
385
383
  }
386
- } else if (block.type === "text" && block.text) {
387
- parts.push(block.text);
388
- if (block.citations) {
389
- citations.push(...block.citations);
384
+ }
385
+
386
+ let text = parts.join('\n\n')
387
+
388
+ // Add sources
389
+ if (sources.length > 0) {
390
+ text += '\n\n## Sources'
391
+ for (const [i, src] of sources.entries()) {
392
+ const age = src.age ? ` (${src.age})` : ''
393
+ text += `\n[${i + 1}] ${src.title}${age}\n ${src.url}`
390
394
  }
391
- }
392
- }
393
-
394
- let text = parts.join("\n\n");
395
-
396
- // Add sources
397
- if (sources.length > 0) {
398
- text += "\n\n## Sources";
399
- for (const [i, src] of sources.entries()) {
400
- const age = src.age ? ` (${src.age})` : "";
401
- text += `\n[${i + 1}] ${src.title}${age}\n ${src.url}`;
402
- }
403
- }
404
-
405
- return {
406
- text,
407
- details: {
408
- model: response.model,
409
- usage: response.usage,
410
- searchQueries,
411
- sources,
412
- citations: citations.map((c) => ({
413
- url: c.url,
414
- title: c.title,
415
- citedText: c.cited_text,
416
- })),
417
- },
418
- };
395
+ }
396
+
397
+ return {
398
+ text,
399
+ details: {
400
+ model: response.model,
401
+ usage: response.usage,
402
+ searchQueries,
403
+ sources,
404
+ citations: citations.map(c => ({
405
+ url: c.url,
406
+ title: c.title,
407
+ citedText: c.cited_text,
408
+ })),
409
+ },
410
+ }
419
411
  }
420
412
 
421
413
  // Tool schema
422
414
  const SearchSchema = Type.Object({
423
- query: Type.String({
424
- description: "The search query or question to answer using web search",
425
- }),
426
- system_prompt: Type.Optional(
427
- Type.String({
428
- description: "System prompt to guide the response style and focus",
429
- }),
430
- ),
431
- max_tokens: Type.Optional(
432
- Type.Number({
433
- description: "Maximum tokens in response (default: 4096)",
434
- minimum: 1,
435
- maximum: 16384,
436
- }),
437
- ),
438
- });
415
+ query: Type.String({
416
+ description: 'The search query or question to answer using web search',
417
+ }),
418
+ system_prompt: Type.Optional(
419
+ Type.String({
420
+ description: 'System prompt to guide the response style and focus',
421
+ })
422
+ ),
423
+ max_tokens: Type.Optional(
424
+ Type.Number({
425
+ description: 'Maximum tokens in response (default: 4096)',
426
+ minimum: 1,
427
+ maximum: 16384,
428
+ })
429
+ ),
430
+ })
439
431
 
440
432
  type SearchParams = {
441
- query: string;
442
- system_prompt?: string;
443
- max_tokens?: number;
444
- };
445
-
446
- const factory: CustomToolFactory = async (
447
- _toolApi: ToolAPI,
448
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
449
- const auth = findAuthConfig();
450
- if (!auth) {
451
- console.error("anthropic-websearch: No auth config found. Set ANTHROPIC_SEARCH_API_KEY or configure models.json/auth.json");
452
- return null;
453
- }
454
-
455
- const model = getConfig("model") ?? DEFAULT_MODEL;
456
-
457
- const tool: CustomAgentTool<typeof SearchSchema, unknown> = {
458
- name: "anthropic_web_search",
459
- label: "Anthropic Web Search",
460
- description: `Web search powered by Claude (${model}). Uses Claude's built-in web search capability to find current information and synthesize answers with citations. Best for questions requiring up-to-date information from the web.`,
461
- parameters: SearchSchema,
462
- async execute(_toolCallId, params) {
463
- try {
464
- const p = (params ?? {}) as SearchParams;
465
- const response = await callWebSearch(
466
- auth,
467
- model,
468
- p.query,
469
- p.system_prompt,
470
- p.max_tokens,
471
- );
472
- const { text, details } = formatResponse(response);
473
- return {
474
- content: [{ type: "text" as const, text }],
475
- details,
476
- };
477
- } catch (error) {
478
- const message = error instanceof Error ? error.message : String(error);
479
- return {
480
- content: [{ type: "text" as const, text: `Error: ${message}` }],
481
- details: { error: message },
482
- };
483
- }
484
- },
485
- };
433
+ query: string
434
+ system_prompt?: string
435
+ max_tokens?: number
436
+ }
486
437
 
487
- return [tool];
488
- };
438
+ const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
439
+ const auth = findAuthConfig()
440
+ if (!auth) {
441
+ console.error('anthropic-websearch: No auth config found. Set ANTHROPIC_SEARCH_API_KEY or configure models.json/auth.json')
442
+ return null
443
+ }
444
+
445
+ const model = getConfig('model') ?? DEFAULT_MODEL
446
+
447
+ const tool: CustomAgentTool<typeof SearchSchema, unknown> = {
448
+ name: 'anthropic_web_search',
449
+ label: 'Anthropic Web Search',
450
+ description: `Web search powered by Claude (${model}). Uses Claude's built-in web search capability to find current information and synthesize answers with citations. Best for questions requiring up-to-date information from the web.`,
451
+ parameters: SearchSchema,
452
+ async execute(_toolCallId, params) {
453
+ try {
454
+ const p = (params ?? {}) as SearchParams
455
+ const response = await callWebSearch(auth, model, p.query, p.system_prompt, p.max_tokens)
456
+ const { text, details } = formatResponse(response)
457
+ return {
458
+ content: [{ type: 'text' as const, text }],
459
+ details,
460
+ }
461
+ } catch (error) {
462
+ const message = error instanceof Error ? error.message : String(error)
463
+ return {
464
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
465
+ details: { error: message },
466
+ }
467
+ }
468
+ },
469
+ }
470
+
471
+ return [tool]
472
+ }
489
473
 
490
- export default factory;
474
+ export default factory