@oh-my-pi/exa 1.3.37 → 1.3.372

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 CHANGED
@@ -59,10 +59,10 @@ Get your API key from: https://dashboard.exa.ai/api-keys
59
59
 
60
60
  | Tool | Description |
61
61
  | ------------------------- | ---------------------------------------------------- |
62
- | `web_search_general` | Real-time web searches with content extraction |
62
+ | `web_search` | Real-time web searches with content extraction |
63
63
  | `web_search_deep` | Natural language web search with synthesized results |
64
64
  | `web_search_code_context` | Search code snippets, docs, and examples |
65
- | `web_search_crawl_url` | Extract content from specific URLs |
65
+ | `web_search_crawl` | Extract content from specific URLs |
66
66
 
67
67
  ### linkedin
68
68
 
@@ -72,16 +72,16 @@ Get your API key from: https://dashboard.exa.ai/api-keys
72
72
 
73
73
  ### company
74
74
 
75
- | Tool | Description |
76
- | ----------------------------- | ------------------------------ |
77
- | `web_search_company_research` | Comprehensive company research |
75
+ | Tool | Description |
76
+ | -------------------- | ------------------------------ |
77
+ | `web_search_company` | Comprehensive company research |
78
78
 
79
79
  ### researcher
80
80
 
81
81
  | Tool | Description |
82
82
  | ----------------------------- | -------------------------------------------- |
83
83
  | `web_search_researcher_start` | Start comprehensive AI-powered research task |
84
- | `web_search_researcher_check` | Check research task status and get results |
84
+ | `web_search_researcher_poll` | Check research task status and get results |
85
85
 
86
86
  ### websets
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/exa",
3
- "version": "1.3.37",
3
+ "version": "1.3.372",
4
4
  "description": "Exa AI web search and websets tools for pi",
5
5
  "keywords": [
6
6
  "omp-plugin",
package/tools/company.ts CHANGED
@@ -2,45 +2,31 @@
2
2
  * Exa Company Research Tool
3
3
  *
4
4
  * Tools:
5
- * - web_search_company_research: Comprehensive company research
5
+ * - web_search_company: Comprehensive company research
6
6
  */
7
7
 
8
- import type { TSchema } from "@sinclair/typebox";
9
- import type {
10
- CustomAgentTool,
11
- CustomToolFactory,
12
- ToolAPI,
13
- } from "@mariozechner/pi-coding-agent";
14
- import {
15
- callExaTool,
16
- createToolWrapper,
17
- fetchExaTools,
18
- findApiKey,
19
- } from "./shared";
8
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
9
+ import type { TSchema } from '@sinclair/typebox'
10
+ import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from './shared'
20
11
 
21
12
  // MCP tool names for this feature
22
- const TOOL_NAMES = ["company_research_exa"];
13
+ const TOOL_NAMES = ['company_research_exa']
23
14
 
24
15
  // Tool name mapping: MCP name -> exposed name
25
16
  const NAME_MAP: Record<string, string> = {
26
- company_research_exa: "web_search_company_research",
27
- };
17
+ company_research_exa: 'web_search_company',
18
+ }
28
19
 
29
- const factory: CustomToolFactory = async (
30
- _toolApi: ToolAPI,
31
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
- const apiKey = findApiKey();
33
- if (!apiKey) return null;
20
+ const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
21
+ const apiKey = findApiKey()
22
+ if (!apiKey) return null
34
23
 
35
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
36
- if (mcpTools.length === 0) return null;
24
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES)
25
+ if (mcpTools.length === 0) return null
37
26
 
38
- const callFn = (toolName: string, args: Record<string, unknown>) =>
39
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
27
+ const callFn = (toolName: string, args: Record<string, unknown>) => callExaTool(apiKey, TOOL_NAMES, toolName, args)
40
28
 
41
- return mcpTools.map((tool) =>
42
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
43
- );
44
- };
29
+ return mcpTools.map(tool => createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn))
30
+ }
45
31
 
46
- export default factory;
32
+ export default factory
package/tools/index.ts CHANGED
@@ -12,64 +12,55 @@
12
12
  * - websets: Entity collection management
13
13
  */
14
14
 
15
- import type { TSchema } from "@sinclair/typebox";
16
- import type {
17
- CustomAgentTool,
18
- CustomToolFactory,
19
- ToolAPI,
20
- } from "@mariozechner/pi-coding-agent";
21
- import runtime from "./runtime.json";
15
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
16
+ import type { TSchema } from '@sinclair/typebox'
17
+ import runtime from './runtime.json'
22
18
 
23
19
  // Map feature names to their module imports
24
- const FEATURE_LOADERS: Record<
25
- string,
26
- () => Promise<{ default: CustomToolFactory }>
27
- > = {
28
- search: () => import("./search"),
29
- linkedin: () => import("./linkedin"),
30
- company: () => import("./company"),
31
- researcher: () => import("./researcher"),
32
- websets: () => import("./websets"),
33
- };
20
+ const FEATURE_LOADERS: Record<string, () => Promise<{ default: CustomToolFactory }>> = {
21
+ search: () => import('./search'),
22
+ linkedin: () => import('./linkedin'),
23
+ company: () => import('./company'),
24
+ researcher: () => import('./researcher'),
25
+ websets: () => import('./websets'),
26
+ }
34
27
 
35
28
  /**
36
29
  * Factory function that loads enabled features from runtime.json
37
30
  */
38
- const factory: CustomToolFactory = async (
39
- toolApi: ToolAPI,
40
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
41
- const allTools: CustomAgentTool<TSchema, unknown>[] = [];
42
- const enabledFeatures = runtime.features ?? [];
31
+ const factory: CustomToolFactory = async (toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
+ const allTools: CustomAgentTool<TSchema, unknown>[] = []
33
+ const enabledFeatures = runtime.features ?? []
43
34
 
44
- for (const feature of enabledFeatures) {
45
- const loader = FEATURE_LOADERS[feature];
46
- if (!loader) {
47
- console.error(`Unknown exa feature: "${feature}"`);
48
- continue;
49
- }
35
+ for (const feature of enabledFeatures) {
36
+ const loader = FEATURE_LOADERS[feature]
37
+ if (!loader) {
38
+ console.error(`Unknown exa feature: "${feature}"`)
39
+ continue
40
+ }
50
41
 
51
- try {
52
- const module = await loader();
53
- const featureFactory = module.default;
42
+ try {
43
+ const module = await loader()
44
+ const featureFactory = module.default
54
45
 
55
- if (typeof featureFactory === "function") {
56
- const result = await featureFactory(toolApi);
57
- // Handle both single tool and array of tools
58
- if (result) {
59
- const tools = Array.isArray(result) ? result : [result];
60
- for (const tool of tools) {
61
- if (tool && typeof tool === "object" && "name" in tool) {
62
- allTools.push(tool);
46
+ if (typeof featureFactory === 'function') {
47
+ const result = await featureFactory(toolApi)
48
+ // Handle both single tool and array of tools
49
+ if (result) {
50
+ const tools = Array.isArray(result) ? result : [result]
51
+ for (const tool of tools) {
52
+ if (tool && typeof tool === 'object' && 'name' in tool) {
53
+ allTools.push(tool)
54
+ }
55
+ }
63
56
  }
64
- }
65
- }
57
+ }
58
+ } catch (error) {
59
+ console.error(`Failed to load exa feature "${feature}":`, error)
66
60
  }
67
- } catch (error) {
68
- console.error(`Failed to load exa feature "${feature}":`, error);
69
- }
70
- }
61
+ }
71
62
 
72
- return allTools.length > 0 ? allTools : null;
73
- };
63
+ return allTools.length > 0 ? allTools : null
64
+ }
74
65
 
75
- export default factory;
66
+ export default factory
package/tools/linkedin.ts CHANGED
@@ -5,42 +5,28 @@
5
5
  * - web_search_linkedin: Search LinkedIn profiles and companies
6
6
  */
7
7
 
8
- import type { TSchema } from "@sinclair/typebox";
9
- import type {
10
- CustomAgentTool,
11
- CustomToolFactory,
12
- ToolAPI,
13
- } from "@mariozechner/pi-coding-agent";
14
- import {
15
- callExaTool,
16
- createToolWrapper,
17
- fetchExaTools,
18
- findApiKey,
19
- } from "./shared";
8
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
9
+ import type { TSchema } from '@sinclair/typebox'
10
+ import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from './shared'
20
11
 
21
12
  // MCP tool names for this feature
22
- const TOOL_NAMES = ["linkedin_search_exa"];
13
+ const TOOL_NAMES = ['linkedin_search_exa']
23
14
 
24
15
  // Tool name mapping: MCP name -> exposed name
25
16
  const NAME_MAP: Record<string, string> = {
26
- linkedin_search_exa: "web_search_linkedin",
27
- };
17
+ linkedin_search_exa: 'web_search_linkedin',
18
+ }
28
19
 
29
- const factory: CustomToolFactory = async (
30
- _toolApi: ToolAPI,
31
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
- const apiKey = findApiKey();
33
- if (!apiKey) return null;
20
+ const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
21
+ const apiKey = findApiKey()
22
+ if (!apiKey) return null
34
23
 
35
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
36
- if (mcpTools.length === 0) return null;
24
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES)
25
+ if (mcpTools.length === 0) return null
37
26
 
38
- const callFn = (toolName: string, args: Record<string, unknown>) =>
39
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
27
+ const callFn = (toolName: string, args: Record<string, unknown>) => callExaTool(apiKey, TOOL_NAMES, toolName, args)
40
28
 
41
- return mcpTools.map((tool) =>
42
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
43
- );
44
- };
29
+ return mcpTools.map(tool => createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn))
30
+ }
45
31
 
46
- export default factory;
32
+ export default factory
@@ -3,46 +3,32 @@
3
3
  *
4
4
  * Tools:
5
5
  * - web_search_researcher_start: Start comprehensive AI research tasks
6
- * - web_search_researcher_check: Check research task status
6
+ * - web_search_researcher_poll: Check research task status
7
7
  */
8
8
 
9
- import type { TSchema } from "@sinclair/typebox";
10
- import type {
11
- CustomAgentTool,
12
- CustomToolFactory,
13
- ToolAPI,
14
- } from "@mariozechner/pi-coding-agent";
15
- import {
16
- callExaTool,
17
- createToolWrapper,
18
- fetchExaTools,
19
- findApiKey,
20
- } from "./shared";
9
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
10
+ import type { TSchema } from '@sinclair/typebox'
11
+ import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from './shared'
21
12
 
22
13
  // MCP tool names for this feature
23
- const TOOL_NAMES = ["deep_researcher_start", "deep_researcher_check"];
14
+ const TOOL_NAMES = ['deep_researcher_start', 'deep_researcher_check']
24
15
 
25
16
  // Tool name mapping: MCP name -> exposed name
26
17
  const NAME_MAP: Record<string, string> = {
27
- deep_researcher_start: "web_search_researcher_start",
28
- deep_researcher_check: "web_search_researcher_check",
29
- };
18
+ deep_researcher_start: 'web_search_researcher_start',
19
+ deep_researcher_check: 'web_search_researcher_poll',
20
+ }
30
21
 
31
- const factory: CustomToolFactory = async (
32
- _toolApi: ToolAPI,
33
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
34
- const apiKey = findApiKey();
35
- if (!apiKey) return null;
22
+ const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
23
+ const apiKey = findApiKey()
24
+ if (!apiKey) return null
36
25
 
37
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
38
- if (mcpTools.length === 0) return null;
26
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES)
27
+ if (mcpTools.length === 0) return null
39
28
 
40
- const callFn = (toolName: string, args: Record<string, unknown>) =>
41
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
29
+ const callFn = (toolName: string, args: Record<string, unknown>) => callExaTool(apiKey, TOOL_NAMES, toolName, args)
42
30
 
43
- return mcpTools.map((tool) =>
44
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
45
- );
46
- };
31
+ return mcpTools.map(tool => createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn))
32
+ }
47
33
 
48
- export default factory;
34
+ export default factory
package/tools/search.ts CHANGED
@@ -2,56 +2,37 @@
2
2
  * Exa Search Tools - Core web search capabilities
3
3
  *
4
4
  * Tools:
5
- * - web_search_general: Real-time web searches
5
+ * - web_search: Real-time web searches
6
6
  * - web_search_deep: Natural language web search with synthesis
7
7
  * - web_search_code_context: Code search for libraries, docs, examples
8
- * - web_search_crawl_url: Extract content from specific URLs
8
+ * - web_search_crawl: Extract content from specific URLs
9
9
  */
10
10
 
11
- import type { TSchema } from "@sinclair/typebox";
12
- import type {
13
- CustomAgentTool,
14
- CustomToolFactory,
15
- ToolAPI,
16
- } from "@mariozechner/pi-coding-agent";
17
- import {
18
- callExaTool,
19
- createToolWrapper,
20
- fetchExaTools,
21
- findApiKey,
22
- } from "./shared";
11
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
12
+ import type { TSchema } from '@sinclair/typebox'
13
+ import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from './shared'
23
14
 
24
15
  // MCP tool names for this feature
25
- const TOOL_NAMES = [
26
- "web_search_exa",
27
- "deep_search_exa",
28
- "get_code_context_exa",
29
- "crawling_exa",
30
- ];
16
+ const TOOL_NAMES = ['web_search_exa', 'deep_search_exa', 'get_code_context_exa', 'crawling_exa']
31
17
 
32
18
  // Tool name mapping: MCP name -> exposed name
33
19
  const NAME_MAP: Record<string, string> = {
34
- web_search_exa: "web_search_general",
35
- deep_search_exa: "web_search_deep",
36
- get_code_context_exa: "web_search_code_context",
37
- crawling_exa: "web_search_crawl_url",
38
- };
39
-
40
- const factory: CustomToolFactory = async (
41
- _toolApi: ToolAPI,
42
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
43
- const apiKey = findApiKey();
44
- if (!apiKey) return null;
45
-
46
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
47
- if (mcpTools.length === 0) return null;
48
-
49
- const callFn = (toolName: string, args: Record<string, unknown>) =>
50
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
51
-
52
- return mcpTools.map((tool) =>
53
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
54
- );
55
- };
56
-
57
- export default factory;
20
+ web_search_exa: 'web_search',
21
+ deep_search_exa: 'web_search_deep',
22
+ get_code_context_exa: 'web_search_code_context',
23
+ crawling_exa: 'web_search_crawl',
24
+ }
25
+
26
+ const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
27
+ const apiKey = findApiKey()
28
+ if (!apiKey) return null
29
+
30
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES)
31
+ if (mcpTools.length === 0) return null
32
+
33
+ const callFn = (toolName: string, args: Record<string, unknown>) => callExaTool(apiKey, TOOL_NAMES, toolName, args)
34
+
35
+ return mcpTools.map(tool => createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn))
36
+ }
37
+
38
+ export default factory
package/tools/shared.ts CHANGED
@@ -2,300 +2,586 @@
2
2
  * Shared utilities for Exa MCP tools
3
3
  */
4
4
 
5
- import * as fs from "node:fs";
6
- import * as os from "node:os";
7
- import * as path from "node:path";
8
- import type { TSchema } from "@sinclair/typebox";
9
- import type { CustomAgentTool } from "@mariozechner/pi-coding-agent";
5
+ import * as fs from 'node:fs'
6
+ import * as os from 'node:os'
7
+ import * as path from 'node:path'
8
+ import type { CustomAgentTool } from '@mariozechner/pi-coding-agent'
9
+ import { Text } from '@mariozechner/pi-tui'
10
+ import type { TSchema } from '@sinclair/typebox'
10
11
 
11
12
  // MCP endpoints
12
- export const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
13
- export const WEBSETS_MCP_URL = "https://websetsmcp.exa.ai/mcp";
13
+ export const EXA_MCP_URL = 'https://mcp.exa.ai/mcp'
14
+ export const WEBSETS_MCP_URL = 'https://websetsmcp.exa.ai/mcp'
15
+
16
+ // Log paths
17
+ const EXA_ERROR_LOG = path.join(os.homedir(), '.pi/exa_errors.log')
18
+ const VIEW_ERROR_LOG = path.join(os.homedir(), '.pi/view_errors.log')
19
+
20
+ function logExaError(msg: string): void {
21
+ fs.appendFileSync(EXA_ERROR_LOG, `[${new Date().toISOString()}] ${msg}\n`)
22
+ }
23
+
24
+ function logViewError(msg: string): void {
25
+ fs.appendFileSync(VIEW_ERROR_LOG, `[${new Date().toISOString()}] ${msg}\n`)
26
+ }
14
27
 
15
28
  export interface MCPTool {
16
- name: string;
17
- description: string;
18
- inputSchema: TSchema;
29
+ name: string
30
+ description: string
31
+ inputSchema: TSchema
19
32
  }
20
33
 
21
34
  interface MCPToolsResponse {
22
- result?: {
23
- tools: MCPTool[];
24
- };
25
- error?: {
26
- code: number;
27
- message: string;
28
- };
35
+ result?: {
36
+ tools: MCPTool[]
37
+ }
38
+ error?: {
39
+ code: number
40
+ message: string
41
+ }
29
42
  }
30
43
 
31
44
  function normalizeInputSchema(schema: unknown): Record<string, unknown> {
32
- if (!schema || typeof schema !== "object") {
33
- return { type: "object", properties: {}, required: [] };
34
- }
45
+ if (!schema || typeof schema !== 'object') {
46
+ return { type: 'object', properties: {}, required: [] }
47
+ }
35
48
 
36
- const normalized = { ...(schema as Record<string, unknown>) };
49
+ const normalized = { ...(schema as Record<string, unknown>) }
37
50
 
38
- if (!("type" in normalized)) {
39
- normalized.type = "object";
40
- }
51
+ if (!('type' in normalized)) {
52
+ normalized.type = 'object'
53
+ }
41
54
 
42
- if (!("properties" in normalized)) {
43
- normalized.properties = {};
44
- }
55
+ if (!('properties' in normalized)) {
56
+ normalized.properties = {}
57
+ }
45
58
 
46
- const required = (normalized as { required?: unknown }).required;
47
- if (!Array.isArray(required)) {
48
- normalized.required = [];
49
- }
59
+ const required = (normalized as { required?: unknown }).required
60
+ if (!Array.isArray(required)) {
61
+ normalized.required = []
62
+ }
50
63
 
51
- return normalized;
64
+ return normalized
52
65
  }
53
66
 
54
67
  /**
55
68
  * Parse a .env file and return key-value pairs
56
69
  */
57
70
  function parseEnvFile(filePath: string): Record<string, string> {
58
- const result: Record<string, string> = {};
59
- if (!fs.existsSync(filePath)) return result;
60
-
61
- try {
62
- const content = fs.readFileSync(filePath, "utf-8");
63
- for (const line of content.split("\n")) {
64
- const trimmed = line.trim();
65
- if (!trimmed || trimmed.startsWith("#")) continue;
66
-
67
- const eqIndex = trimmed.indexOf("=");
68
- if (eqIndex === -1) continue;
69
-
70
- const key = trimmed.slice(0, eqIndex).trim();
71
- let value = trimmed.slice(eqIndex + 1).trim();
72
-
73
- // Remove surrounding quotes
74
- if (
75
- (value.startsWith('"') && value.endsWith('"')) ||
76
- (value.startsWith("'") && value.endsWith("'"))
77
- ) {
78
- value = value.slice(1, -1);
79
- }
71
+ const result: Record<string, string> = {}
72
+ if (!fs.existsSync(filePath)) return result
73
+
74
+ try {
75
+ const content = fs.readFileSync(filePath, 'utf-8')
76
+ for (const line of content.split('\n')) {
77
+ const trimmed = line.trim()
78
+ if (!trimmed || trimmed.startsWith('#')) continue
79
+
80
+ const eqIndex = trimmed.indexOf('=')
81
+ if (eqIndex === -1) continue
80
82
 
81
- result[key] = value;
82
- }
83
- } catch {
84
- // Ignore read errors
85
- }
83
+ const key = trimmed.slice(0, eqIndex).trim()
84
+ let value = trimmed.slice(eqIndex + 1).trim()
86
85
 
87
- return result;
86
+ // Remove surrounding quotes
87
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
88
+ value = value.slice(1, -1)
89
+ }
90
+
91
+ result[key] = value
92
+ }
93
+ } catch {
94
+ // Ignore read errors
95
+ }
96
+
97
+ return result
88
98
  }
89
99
 
90
100
  /**
91
101
  * Find EXA_API_KEY from environment or .env files
92
102
  */
93
103
  export function findApiKey(): string | null {
94
- // 1. Check environment variable
95
- if (process.env.EXA_API_KEY) {
96
- return process.env.EXA_API_KEY;
97
- }
98
-
99
- // 2. Check .env in current directory
100
- const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
101
- if (localEnv.EXA_API_KEY) {
102
- return localEnv.EXA_API_KEY;
103
- }
104
-
105
- // 3. Check ~/.env
106
- const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
107
- if (homeEnv.EXA_API_KEY) {
108
- return homeEnv.EXA_API_KEY;
109
- }
110
-
111
- return null;
104
+ // 1. Check environment variable
105
+ if (process.env.EXA_API_KEY) {
106
+ return process.env.EXA_API_KEY
107
+ }
108
+
109
+ // 2. Check .env in current directory
110
+ const localEnv = parseEnvFile(path.join(process.cwd(), '.env'))
111
+ if (localEnv.EXA_API_KEY) {
112
+ return localEnv.EXA_API_KEY
113
+ }
114
+
115
+ // 3. Check ~/.env
116
+ const homeEnv = parseEnvFile(path.join(os.homedir(), '.env'))
117
+ if (homeEnv.EXA_API_KEY) {
118
+ return homeEnv.EXA_API_KEY
119
+ }
120
+
121
+ return null
112
122
  }
113
123
 
114
124
  /**
115
125
  * Call an MCP server endpoint
116
126
  */
117
- async function callMCP(
118
- url: string,
119
- method: string,
120
- params?: Record<string, unknown>,
121
- ): Promise<unknown> {
122
- const body = {
123
- jsonrpc: "2.0",
124
- method,
125
- params: params ?? {},
126
- id: 1,
127
- };
128
-
129
- const response = await fetch(url, {
130
- method: "POST",
131
- headers: {
132
- "Content-Type": "application/json",
133
- Accept: "application/json, text/event-stream",
134
- },
135
- body: JSON.stringify(body),
136
- });
137
-
138
- const text = await response.text();
139
-
140
- // Parse SSE response format
141
- let jsonData: string | null = null;
142
- for (const line of text.split("\n")) {
143
- if (line.startsWith("data: ")) {
144
- jsonData = line.slice(6);
145
- break;
146
- }
147
- }
148
-
149
- if (!jsonData) {
150
- // Try parsing as plain JSON
151
- try {
152
- return JSON.parse(text);
153
- } catch {
154
- throw new Error(`Failed to parse MCP response: ${text.slice(0, 500)}`);
155
- }
156
- }
157
-
158
- return JSON.parse(jsonData);
127
+ async function callMCP(url: string, method: string, params?: Record<string, unknown>): Promise<unknown> {
128
+ const body = {
129
+ jsonrpc: '2.0',
130
+ method,
131
+ params: params ?? {},
132
+ id: 1,
133
+ }
134
+
135
+ const response = await fetch(url, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ Accept: 'application/json, text/event-stream',
140
+ },
141
+ body: JSON.stringify(body),
142
+ })
143
+
144
+ const text = await response.text()
145
+
146
+ // Parse SSE response format
147
+ let jsonData: string | null = null
148
+ for (const line of text.split('\n')) {
149
+ if (line.startsWith('data: ')) {
150
+ jsonData = line.slice(6)
151
+ break
152
+ }
153
+ }
154
+
155
+ if (!jsonData) {
156
+ // Try parsing as plain JSON
157
+ try {
158
+ return JSON.parse(text)
159
+ } catch {
160
+ throw new Error(`Failed to parse MCP response: ${text.slice(0, 500)}`)
161
+ }
162
+ }
163
+
164
+ return JSON.parse(jsonData)
159
165
  }
160
166
 
161
167
  /**
162
168
  * Fetch available tools from Exa MCP server
163
169
  */
164
- export async function fetchExaTools(
165
- apiKey: string,
166
- toolNames: string[],
167
- ): Promise<MCPTool[]> {
168
- const url = `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolNames.join(","))}`;
169
-
170
- try {
171
- const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
172
- if (response.error) {
173
- throw new Error(response.error.message);
174
- }
175
- return response.result?.tools ?? [];
176
- } catch (error) {
177
- console.error(`Failed to fetch Exa tools:`, error);
178
- return [];
179
- }
170
+ export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
171
+ const url = `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolNames.join(','))}`
172
+
173
+ try {
174
+ const response = (await callMCP(url, 'tools/list')) as MCPToolsResponse
175
+ if (response.error) {
176
+ throw new Error(response.error.message)
177
+ }
178
+ return response.result?.tools ?? []
179
+ } catch (error) {
180
+ const msg = error instanceof Error ? error.message : String(error)
181
+ logExaError(`Failed to fetch Exa tools: ${msg}`)
182
+ return []
183
+ }
180
184
  }
181
185
 
182
186
  /**
183
187
  * Fetch available tools from Websets MCP server
184
188
  */
185
189
  export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
186
- const url = `${WEBSETS_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}`;
187
-
188
- try {
189
- const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
190
- if (response.error) {
191
- throw new Error(response.error.message);
192
- }
193
- return response.result?.tools ?? [];
194
- } catch (error) {
195
- console.error(`Failed to fetch Websets tools:`, error);
196
- return [];
197
- }
190
+ const url = `${WEBSETS_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}`
191
+
192
+ try {
193
+ const response = (await callMCP(url, 'tools/list')) as MCPToolsResponse
194
+ if (response.error) {
195
+ throw new Error(response.error.message)
196
+ }
197
+ return response.result?.tools ?? []
198
+ } catch (error) {
199
+ const msg = error instanceof Error ? error.message : String(error)
200
+ logExaError(`Failed to fetch Websets tools: ${msg}`)
201
+ return []
202
+ }
198
203
  }
199
204
 
200
205
  /**
201
206
  * Call a tool on Exa MCP server
202
207
  */
203
- export async function callExaTool(
204
- apiKey: string,
205
- toolNames: string[],
206
- toolName: string,
207
- args: Record<string, unknown>,
208
- ): Promise<unknown> {
209
- const url = `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolNames.join(","))}`;
210
- return callMCPTool(url, toolName, args);
208
+ export async function callExaTool(apiKey: string, toolNames: string[], toolName: string, args: Record<string, unknown>): Promise<unknown> {
209
+ const url = `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolNames.join(','))}`
210
+ return callMCPTool(url, toolName, args)
211
211
  }
212
212
 
213
213
  /**
214
214
  * Call a tool on Websets MCP server
215
215
  */
216
- export async function callWebsetsTool(
217
- apiKey: string,
218
- toolName: string,
219
- args: Record<string, unknown>,
220
- ): Promise<unknown> {
221
- const url = `${WEBSETS_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}`;
222
- return callMCPTool(url, toolName, args);
216
+ export async function callWebsetsTool(apiKey: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
217
+ const url = `${WEBSETS_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}`
218
+ return callMCPTool(url, toolName, args)
223
219
  }
224
220
 
225
221
  /**
226
222
  * Call a tool on an MCP server
227
223
  */
228
- async function callMCPTool(
229
- url: string,
230
- toolName: string,
231
- args: Record<string, unknown>,
232
- ): Promise<unknown> {
233
- const response = (await callMCP(url, "tools/call", {
234
- name: toolName,
235
- arguments: args,
236
- })) as {
237
- result?: { content?: Array<{ text?: string }> };
238
- error?: { message: string };
239
- };
240
-
241
- if (response.error) {
242
- throw new Error(response.error.message);
243
- }
244
-
245
- // Extract text content from MCP response
246
- const content = response.result?.content;
247
- if (Array.isArray(content)) {
248
- const texts = content.filter((c) => c.text).map((c) => c.text);
249
- if (texts.length === 1) {
250
- // Try to parse as JSON
251
- try {
252
- return JSON.parse(texts[0]!);
253
- } catch {
254
- return texts[0];
224
+ async function callMCPTool(url: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
225
+ const response = (await callMCP(url, 'tools/call', {
226
+ name: toolName,
227
+ arguments: args,
228
+ })) as {
229
+ result?: { content?: Array<{ text?: string }> }
230
+ error?: { message: string }
231
+ }
232
+
233
+ if (response.error) {
234
+ throw new Error(response.error.message)
235
+ }
236
+
237
+ // Extract text content from MCP response
238
+ const content = response.result?.content
239
+ if (Array.isArray(content)) {
240
+ const texts = content.filter(c => c.text).map(c => c.text)
241
+ if (texts.length === 1) {
242
+ // Try to parse as JSON
243
+ try {
244
+ return JSON.parse(texts[0]!)
245
+ } catch {
246
+ return texts[0]
247
+ }
248
+ }
249
+ return texts.join('\n\n')
250
+ }
251
+
252
+ return response.result
253
+ }
254
+
255
+ interface SearchResult {
256
+ id?: string
257
+ title?: string
258
+ url?: string
259
+ author?: string
260
+ publishedDate?: string
261
+ text?: string
262
+ image?: string
263
+ favicon?: string
264
+ }
265
+
266
+ interface SearchResponse {
267
+ results?: SearchResult[]
268
+ statuses?: Array<{ id: string; status: string; source?: string }>
269
+ costDollars?: { total: number }
270
+ searchTime?: number
271
+ requestId?: string
272
+ }
273
+
274
+ /**
275
+ * Format search results as readable markdown (for LLM consumption)
276
+ */
277
+ function formatSearchResults(data: SearchResponse): string {
278
+ const lines: string[] = []
279
+
280
+ if (data.results && data.results.length > 0) {
281
+ for (const result of data.results) {
282
+ // Title with link
283
+ if (result.title && result.url) {
284
+ lines.push(`### [${result.title}](${result.url})`)
285
+ } else if (result.title) {
286
+ lines.push(`### ${result.title}`)
287
+ } else if (result.url) {
288
+ lines.push(`### ${result.url}`)
289
+ }
290
+
291
+ // Author if present
292
+ if (result.author) {
293
+ lines.push(`*by ${result.author}*`)
294
+ }
295
+
296
+ lines.push('')
297
+
298
+ // Content - truncate if very long
299
+ if (result.text) {
300
+ const text = result.text.trim()
301
+ const maxLen = 2000
302
+ if (text.length > maxLen) {
303
+ lines.push(`${text.slice(0, maxLen)}...`)
304
+ } else {
305
+ lines.push(text)
306
+ }
307
+ }
308
+
309
+ lines.push('')
310
+ lines.push('---')
311
+ lines.push('')
312
+ }
313
+ }
314
+
315
+ // Footer with metadata
316
+ const meta: string[] = []
317
+ if (data.results) meta.push(`${data.results.length} result(s)`)
318
+ if (data.searchTime) meta.push(`${(data.searchTime / 1000).toFixed(2)}s`)
319
+ if (data.costDollars?.total) meta.push(`$${data.costDollars.total.toFixed(4)}`)
320
+
321
+ if (meta.length > 0) {
322
+ lines.push(`*${meta.join(' • ')}*`)
323
+ }
324
+
325
+ return lines.join('\n')
326
+ }
327
+
328
+ /**
329
+ * Check if result looks like a search response
330
+ */
331
+ function isSearchResponse(data: unknown): data is SearchResponse {
332
+ if (!data || typeof data !== 'object') return false
333
+ const obj = data as Record<string, unknown>
334
+ return Array.isArray(obj.results) || 'searchTime' in obj || 'costDollars' in obj
335
+ }
336
+
337
+ /**
338
+ * Parse Exa's markdown text format into a SearchResponse structure
339
+ * Format: Title: ...\nURL: ...\nAuthor: ...\nPublished Date: ...\nText: ...\n\n (repeated)
340
+ */
341
+ function parseExaMarkdown(text: string): SearchResponse | null {
342
+ const results: SearchResult[] = []
343
+
344
+ // Split by double newlines to separate results, but be careful with Text: blocks
345
+ // Each result starts with "Title:"
346
+ const parts = text.split(/\n(?=Title:)/g)
347
+
348
+ for (const part of parts) {
349
+ if (!part.trim()) continue
350
+
351
+ const result: SearchResult = {}
352
+ const lines = part.split('\n')
353
+
354
+ let currentField: string | null = null
355
+ let textLines: string[] = []
356
+
357
+ for (const line of lines) {
358
+ // Check for field prefixes
359
+ if (line.startsWith('Title: ')) {
360
+ result.title = line.slice(7).trim()
361
+ currentField = null
362
+ } else if (line.startsWith('URL: ')) {
363
+ result.url = line.slice(5).trim()
364
+ currentField = null
365
+ } else if (line.startsWith('Author: ')) {
366
+ result.author = line.slice(8).trim()
367
+ currentField = null
368
+ } else if (line.startsWith('Published Date: ')) {
369
+ result.publishedDate = line.slice(16).trim()
370
+ currentField = null
371
+ } else if (line.startsWith('Text: ')) {
372
+ textLines = [line.slice(6)]
373
+ currentField = 'text'
374
+ } else if (currentField === 'text') {
375
+ textLines.push(line)
376
+ }
377
+ }
378
+
379
+ if (textLines.length > 0) {
380
+ result.text = textLines.join('\n').trim()
381
+ }
382
+
383
+ if (result.title || result.url) {
384
+ results.push(result)
255
385
  }
256
- }
257
- return texts.join("\n\n");
258
- }
386
+ }
387
+
388
+ if (results.length === 0) return null
389
+ return { results }
390
+ }
391
+
392
+ // Tree formatting helpers
393
+ const TREE_MID = '├─'
394
+ const TREE_END = '└─'
395
+ const TREE_PIPE = '│'
396
+ const TREE_SPACE = ' '
397
+ const TREE_HOOK = '⎿'
398
+
399
+ /**
400
+ * Truncate text to max length with ellipsis
401
+ */
402
+ function truncate(text: string, maxLen: number): string {
403
+ if (text.length <= maxLen) return text
404
+ return `${text.slice(0, maxLen - 1)}…`
405
+ }
259
406
 
260
- return response.result;
407
+ /**
408
+ * Extract domain from URL
409
+ */
410
+ function getDomain(url: string): string {
411
+ try {
412
+ const u = new URL(url)
413
+ return u.hostname.replace(/^www\./, '')
414
+ } catch {
415
+ return url
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Get first N lines of text as preview
421
+ */
422
+ function getPreviewLines(text: string, maxLines: number, maxLineLen: number): string[] {
423
+ const lines = text.split('\n').filter(l => l.trim())
424
+ return lines.slice(0, maxLines).map(l => truncate(l.trim(), maxLineLen))
261
425
  }
262
426
 
263
427
  /**
264
428
  * Create a tool wrapper for an MCP tool
265
429
  */
266
430
  export function createToolWrapper(
267
- mcpTool: MCPTool,
268
- renamedName: string,
269
- callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>,
270
- ): CustomAgentTool<TSchema, unknown> {
271
- return {
272
- name: renamedName,
273
- label: renamedName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
274
- description: mcpTool.description,
275
- parameters: normalizeInputSchema(mcpTool.inputSchema) as TSchema,
276
- async execute(_toolCallId, params) {
277
- try {
278
- const result = await callFn(
279
- mcpTool.name,
280
- (params ?? {}) as Record<string, unknown>,
281
- );
282
- const text =
283
- typeof result === "string"
284
- ? result
285
- : result == null
286
- ? "null"
287
- : JSON.stringify(result, null, 2) ?? String(result);
288
- return {
289
- content: [{ type: "text" as const, text }],
290
- details: result,
291
- };
292
- } catch (error) {
293
- const message = error instanceof Error ? error.message : String(error);
294
- return {
295
- content: [{ type: "text" as const, text: `Error: ${message}` }],
296
- details: { error: message },
297
- };
298
- }
299
- },
300
- };
431
+ mcpTool: MCPTool,
432
+ renamedName: string,
433
+ callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>
434
+ ): CustomAgentTool<TSchema, SearchResponse | { error: string } | unknown> {
435
+ return {
436
+ name: renamedName,
437
+ label: renamedName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
438
+ description: mcpTool.description,
439
+ parameters: normalizeInputSchema(mcpTool.inputSchema) as TSchema,
440
+
441
+ async execute(_toolCallId, params) {
442
+ try {
443
+ const result = await callFn(mcpTool.name, (params ?? {}) as Record<string, unknown>)
444
+
445
+ let text: string
446
+ if (typeof result === 'string') {
447
+ text = result
448
+ } else if (result == null) {
449
+ text = 'No results'
450
+ } else if (isSearchResponse(result)) {
451
+ text = formatSearchResults(result)
452
+ } else {
453
+ text = JSON.stringify(result, null, 2) ?? String(result)
454
+ }
455
+
456
+ return {
457
+ content: [{ type: 'text' as const, text }],
458
+ details: result,
459
+ }
460
+ } catch (error) {
461
+ const message = error instanceof Error ? error.message : String(error)
462
+ return {
463
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
464
+ details: { error: message },
465
+ }
466
+ }
467
+ },
468
+
469
+ renderResult(result, { expanded }, theme) {
470
+ let { details } = result
471
+
472
+ // Handle error case
473
+ if (details && typeof details === 'object' && 'error' in details) {
474
+ const errDetails = details as { error: string }
475
+ return new Text(theme.fg('error', `Error: ${errDetails.error}`), 0, 0)
476
+ }
477
+
478
+ // If details is a string (Exa markdown format), try to parse it
479
+ if (typeof details === 'string') {
480
+ const parsed = parseExaMarkdown(details)
481
+ if (parsed) {
482
+ details = parsed
483
+ }
484
+ }
485
+
486
+ // Handle non-search responses (plain text/JSON)
487
+ if (!isSearchResponse(details)) {
488
+ const text = result.content[0]
489
+ if (text?.type === 'text') {
490
+ // For non-search content, show truncated in collapsed, full in expanded
491
+ if (expanded) {
492
+ return new Text(text.text, 0, 0)
493
+ }
494
+ const preview = getPreviewLines(text.text, 5, 100)
495
+ const lines = preview.map(l => theme.fg('dim', l)).join('\n')
496
+ return new Text(lines + (text.text.split('\n').length > 5 ? theme.fg('muted', '\n …') : ''), 0, 0)
497
+ }
498
+ return new Text('', 0, 0)
499
+ }
500
+
501
+ // Search response - render as tree
502
+ const data = details as SearchResponse
503
+ const resultCount = data.results?.length ?? 0
504
+
505
+ // Build header with metadata
506
+ const meta: string[] = []
507
+ meta.push(`${resultCount} result${resultCount !== 1 ? 's' : ''}`)
508
+ if (data.searchTime) meta.push(`${(data.searchTime / 1000).toFixed(2)}s`)
509
+ if (data.costDollars?.total) meta.push(`$${data.costDollars.total.toFixed(4)}`)
510
+
511
+ const icon = resultCount > 0 ? theme.fg('success', '●') : theme.fg('warning', '●')
512
+ const expandHint = expanded ? '' : theme.fg('dim', ' (Ctrl+O to expand)')
513
+ let text = `${icon} ${theme.fg('toolTitle', 'Web Search')} ${theme.fg('dim', meta.join(' • '))}${expandHint}`
514
+
515
+ if (!data.results || data.results.length === 0) {
516
+ text += `\n ${theme.fg('dim', TREE_END)} ${theme.fg('muted', 'No results found')}`
517
+ return new Text(text, 0, 0)
518
+ }
519
+
520
+ // Render each result
521
+ try {
522
+ for (let i = 0; i < data.results.length; i++) {
523
+ const r = data.results[i]
524
+ const isLast = i === data.results.length - 1
525
+ const branch = isLast ? TREE_END : TREE_MID
526
+ const cont = isLast ? TREE_SPACE : TREE_PIPE
527
+
528
+ // Title line
529
+ const title = r.title ? truncate(r.title, 80) : 'Untitled'
530
+ const domain = r.url ? getDomain(r.url) : ''
531
+ text += `\n ${theme.fg('dim', branch)} ${theme.fg('accent', title)}`
532
+ if (domain) {
533
+ text += theme.fg('dim', ` (${domain})`)
534
+ }
535
+
536
+ // URL line (if different from domain)
537
+ if (r.url) {
538
+ text += `\n ${theme.fg('dim', `${cont} ${TREE_HOOK} `)}${theme.fg('link', r.url)}`
539
+ }
540
+
541
+ // Author/date metadata
542
+ const metaParts: string[] = []
543
+ if (r.author) metaParts.push(`by ${r.author}`)
544
+ if (r.publishedDate) {
545
+ try {
546
+ const date = new Date(r.publishedDate)
547
+ metaParts.push(date.toLocaleDateString())
548
+ } catch {
549
+ // ignore invalid dates
550
+ }
551
+ }
552
+ if (metaParts.length > 0) {
553
+ text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('muted', metaParts.join(' • '))}`
554
+ }
555
+
556
+ // Content preview (collapsed) or full content (expanded)
557
+ if (r.text) {
558
+ if (expanded) {
559
+ // Show full content with proper indentation
560
+ const lines = r.text.split('\n')
561
+ for (const line of lines) {
562
+ if (line.trim()) {
563
+ text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('dim', line)}`
564
+ }
565
+ }
566
+ } else {
567
+ // Show preview (first 2 non-empty lines)
568
+ const preview = getPreviewLines(r.text, 2, 100)
569
+ for (const line of preview) {
570
+ text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('dim', line)}`
571
+ }
572
+ const totalLines = r.text.split('\n').filter(l => l.trim()).length
573
+ if (totalLines > 2) {
574
+ text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('muted', `… ${totalLines - 2} more lines`)}`
575
+ }
576
+ }
577
+ }
578
+ }
579
+ } catch (err) {
580
+ const msg = err instanceof Error ? err.message : String(err)
581
+ logViewError(`exa renderResult error: ${msg}`)
582
+ }
583
+
584
+ return new Text(text, 0, 0)
585
+ },
586
+ }
301
587
  }
package/tools/websets.ts CHANGED
@@ -20,54 +20,40 @@
20
20
  * - webset_monitor_create: Auto-update webset on schedule
21
21
  */
22
22
 
23
- import type { TSchema } from "@sinclair/typebox";
24
- import type {
25
- CustomAgentTool,
26
- CustomToolFactory,
27
- ToolAPI,
28
- } from "@mariozechner/pi-coding-agent";
29
- import {
30
- callWebsetsTool,
31
- createToolWrapper,
32
- fetchWebsetsTools,
33
- findApiKey,
34
- } from "./shared";
23
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
24
+ import type { TSchema } from '@sinclair/typebox'
25
+ import { callWebsetsTool, createToolWrapper, fetchWebsetsTools, findApiKey } from './shared'
35
26
 
36
27
  // Tool name mapping: MCP name -> exposed name
37
28
  const NAME_MAP: Record<string, string> = {
38
- create_webset: "webset_create",
39
- list_websets: "webset_list",
40
- get_webset: "webset_get",
41
- update_webset: "webset_update",
42
- delete_webset: "webset_delete",
43
- list_webset_items: "webset_items_list",
44
- get_item: "webset_item_get",
45
- create_search: "webset_search_create",
46
- get_search: "webset_search_get",
47
- cancel_search: "webset_search_cancel",
48
- create_enrichment: "webset_enrichment_create",
49
- get_enrichment: "webset_enrichment_get",
50
- update_enrichment: "webset_enrichment_update",
51
- delete_enrichment: "webset_enrichment_delete",
52
- cancel_enrichment: "webset_enrichment_cancel",
53
- create_monitor: "webset_monitor_create",
54
- };
29
+ create_webset: 'webset_create',
30
+ list_websets: 'webset_list',
31
+ get_webset: 'webset_get',
32
+ update_webset: 'webset_update',
33
+ delete_webset: 'webset_delete',
34
+ list_webset_items: 'webset_items_list',
35
+ get_item: 'webset_item_get',
36
+ create_search: 'webset_search_create',
37
+ get_search: 'webset_search_get',
38
+ cancel_search: 'webset_search_cancel',
39
+ create_enrichment: 'webset_enrichment_create',
40
+ get_enrichment: 'webset_enrichment_get',
41
+ update_enrichment: 'webset_enrichment_update',
42
+ delete_enrichment: 'webset_enrichment_delete',
43
+ cancel_enrichment: 'webset_enrichment_cancel',
44
+ create_monitor: 'webset_monitor_create',
45
+ }
55
46
 
56
- const factory: CustomToolFactory = async (
57
- _toolApi: ToolAPI,
58
- ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
59
- const apiKey = findApiKey();
60
- if (!apiKey) return null;
47
+ const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
48
+ const apiKey = findApiKey()
49
+ if (!apiKey) return null
61
50
 
62
- const mcpTools = await fetchWebsetsTools(apiKey);
63
- if (mcpTools.length === 0) return null;
51
+ const mcpTools = await fetchWebsetsTools(apiKey)
52
+ if (mcpTools.length === 0) return null
64
53
 
65
- const callFn = (toolName: string, args: Record<string, unknown>) =>
66
- callWebsetsTool(apiKey, toolName, args);
54
+ const callFn = (toolName: string, args: Record<string, unknown>) => callWebsetsTool(apiKey, toolName, args)
67
55
 
68
- return mcpTools.map((tool) =>
69
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
70
- );
71
- };
56
+ return mcpTools.map(tool => createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn))
57
+ }
72
58
 
73
- export default factory;
59
+ export default factory