@oh-my-pi/exa 0.3.0 → 0.5.0

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
@@ -17,13 +17,13 @@ omp install @oh-my-pi/exa[search,linkedin,websets]
17
17
 
18
18
  ## Features
19
19
 
20
- | Feature | Default | Description | Tools |
21
- |---------|---------|-------------|-------|
22
- | `search` | ✓ | Core web search capabilities | 4 tools |
23
- | `linkedin` | | LinkedIn profile and company search | 1 tool |
24
- | `company` | | Comprehensive company research | 1 tool |
25
- | `researcher` | | Long-running AI research tasks | 2 tools |
26
- | `websets` | | Entity collection management | 14 tools |
20
+ | Feature | Default | Description | Tools |
21
+ | ------------ | ------- | ----------------------------------- | -------- |
22
+ | `search` | ✓ | Core web search capabilities | 4 tools |
23
+ | `linkedin` | | LinkedIn profile and company search | 1 tool |
24
+ | `company` | | Comprehensive company research | 1 tool |
25
+ | `researcher` | | Long-running AI research tasks | 2 tools |
26
+ | `websets` | | Entity collection management | 14 tools |
27
27
 
28
28
  Manage features after install:
29
29
 
@@ -62,76 +62,81 @@ Get your API key from: https://dashboard.exa.ai/api-keys
62
62
 
63
63
  ### search (default)
64
64
 
65
- | Tool | Description |
66
- |------|-------------|
67
- | `web_search_general` | Real-time web searches with content extraction |
68
- | `web_search_deep` | Natural language web search with synthesized results |
69
- | `web_search_code_context` | Search code snippets, docs, and examples |
70
- | `web_search_crawl_url` | Extract content from specific URLs |
65
+ | Tool | Description |
66
+ | ------------------------- | ---------------------------------------------------- |
67
+ | `web_search_general` | Real-time web searches with content extraction |
68
+ | `web_search_deep` | Natural language web search with synthesized results |
69
+ | `web_search_code_context` | Search code snippets, docs, and examples |
70
+ | `web_search_crawl_url` | Extract content from specific URLs |
71
71
 
72
72
  ### linkedin
73
73
 
74
- | Tool | Description |
75
- |------|-------------|
74
+ | Tool | Description |
75
+ | --------------------- | -------------------------------------- |
76
76
  | `web_search_linkedin` | Search LinkedIn profiles and companies |
77
77
 
78
78
  ### company
79
79
 
80
- | Tool | Description |
81
- |------|-------------|
80
+ | Tool | Description |
81
+ | ----------------------------- | ------------------------------ |
82
82
  | `web_search_company_research` | Comprehensive company research |
83
83
 
84
84
  ### researcher
85
85
 
86
- | Tool | Description |
87
- |------|-------------|
86
+ | Tool | Description |
87
+ | ----------------------------- | -------------------------------------------- |
88
88
  | `web_search_researcher_start` | Start comprehensive AI-powered research task |
89
- | `web_search_researcher_check` | Check research task status and get results |
89
+ | `web_search_researcher_check` | Check research task status and get results |
90
90
 
91
91
  ### websets
92
92
 
93
- | Tool | Description |
94
- |------|-------------|
95
- | `webset_create` | Create entity collections with search and enrichments |
96
- | `webset_list` | List all websets in your account |
97
- | `webset_get` | Get detailed webset information |
98
- | `webset_update` | Update webset metadata |
99
- | `webset_delete` | Delete a webset |
100
- | `webset_items_list` | List items in a webset |
101
- | `webset_item_get` | Get item details |
102
- | `webset_search_create` | Add search to find entities for a webset |
103
- | `webset_search_get` | Check search status |
104
- | `webset_search_cancel` | Cancel running search |
105
- | `webset_enrichment_create` | Extract custom data from webset items |
106
- | `webset_enrichment_get` | Get enrichment details |
107
- | `webset_enrichment_update` | Update enrichment metadata |
108
- | `webset_enrichment_delete` | Delete enrichment |
109
- | `webset_enrichment_cancel` | Cancel running enrichment |
110
- | `webset_monitor_create` | Auto-update webset on schedule |
93
+ | Tool | Description |
94
+ | -------------------------- | ----------------------------------------------------- |
95
+ | `webset_create` | Create entity collections with search and enrichments |
96
+ | `webset_list` | List all websets in your account |
97
+ | `webset_get` | Get detailed webset information |
98
+ | `webset_update` | Update webset metadata |
99
+ | `webset_delete` | Delete a webset |
100
+ | `webset_items_list` | List items in a webset |
101
+ | `webset_item_get` | Get item details |
102
+ | `webset_search_create` | Add search to find entities for a webset |
103
+ | `webset_search_get` | Check search status |
104
+ | `webset_search_cancel` | Cancel running search |
105
+ | `webset_enrichment_create` | Extract custom data from webset items |
106
+ | `webset_enrichment_get` | Get enrichment details |
107
+ | `webset_enrichment_update` | Update enrichment metadata |
108
+ | `webset_enrichment_delete` | Delete enrichment |
109
+ | `webset_enrichment_cancel` | Cancel running enrichment |
110
+ | `webset_monitor_create` | Auto-update webset on schedule |
111
111
 
112
112
  ## Usage Examples
113
113
 
114
114
  ### Code Search
115
+
115
116
  ```
116
117
  Find examples of how to use React hooks with TypeScript
117
118
  ```
118
119
 
119
120
  ### Web Search
121
+
120
122
  ```
121
123
  Search for the latest news about AI regulation in the EU
122
124
  ```
123
125
 
124
126
  ### Company Research (requires company feature)
127
+
125
128
  ```
126
129
  Research the company OpenAI and find information about their products
127
130
  ```
128
131
 
129
132
  ### Deep Research (requires researcher feature)
133
+
130
134
  ```
131
135
  Start a deep research project on the impact of large language models on software development
132
136
  ```
133
137
 
134
138
  ### Websets (requires websets feature)
139
+
135
140
  ```
136
141
  Create a webset of AI startups in San Francisco founded after 2020,
137
142
  find 10 companies and enrich with CEO name and funding amount
@@ -140,6 +145,7 @@ find 10 companies and enrich with CEO name and funding amount
140
145
  ## How It Works
141
146
 
142
147
  The plugin connects to Exa's hosted MCP (Model Context Protocol) servers:
148
+
143
149
  - `https://mcp.exa.ai/mcp` - Search tools
144
150
  - `https://websetsmcp.exa.ai/mcp` - Websets tools
145
151
 
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@oh-my-pi/exa",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Exa AI web search and websets tools for pi",
5
- "keywords": ["omp-plugin", "exa", "web-search", "websets", "ai-search"],
5
+ "keywords": [
6
+ "omp-plugin",
7
+ "exa",
8
+ "web-search",
9
+ "websets",
10
+ "ai-search"
11
+ ],
6
12
  "author": "Can Bölük <me@can.ac>",
7
13
  "license": "MIT",
8
14
  "repository": {
@@ -12,14 +18,39 @@
12
18
  },
13
19
  "omp": {
14
20
  "install": [
15
- { "src": "tools/exa/runtime.json", "dest": "agent/tools/exa/runtime.json", "copy": true },
16
- { "src": "tools/exa/index.ts", "dest": "agent/tools/exa/index.ts" },
17
- { "src": "tools/exa/shared.ts", "dest": "agent/tools/exa/shared.ts" },
18
- { "src": "tools/exa/search.ts", "dest": "agent/tools/exa/search.ts" },
19
- { "src": "tools/exa/linkedin.ts", "dest": "agent/tools/exa/linkedin.ts" },
20
- { "src": "tools/exa/company.ts", "dest": "agent/tools/exa/company.ts" },
21
- { "src": "tools/exa/researcher.ts", "dest": "agent/tools/exa/researcher.ts" },
22
- { "src": "tools/exa/websets.ts", "dest": "agent/tools/exa/websets.ts" }
21
+ {
22
+ "src": "tools/exa/runtime.json",
23
+ "dest": "agent/tools/exa/runtime.json",
24
+ "copy": true
25
+ },
26
+ {
27
+ "src": "tools/exa/index.ts",
28
+ "dest": "agent/tools/exa/index.ts"
29
+ },
30
+ {
31
+ "src": "tools/exa/shared.ts",
32
+ "dest": "agent/tools/exa/shared.ts"
33
+ },
34
+ {
35
+ "src": "tools/exa/search.ts",
36
+ "dest": "agent/tools/exa/search.ts"
37
+ },
38
+ {
39
+ "src": "tools/exa/linkedin.ts",
40
+ "dest": "agent/tools/exa/linkedin.ts"
41
+ },
42
+ {
43
+ "src": "tools/exa/company.ts",
44
+ "dest": "agent/tools/exa/company.ts"
45
+ },
46
+ {
47
+ "src": "tools/exa/researcher.ts",
48
+ "dest": "agent/tools/exa/researcher.ts"
49
+ },
50
+ {
51
+ "src": "tools/exa/websets.ts",
52
+ "dest": "agent/tools/exa/websets.ts"
53
+ }
23
54
  ],
24
55
  "variables": {
25
56
  "apiKey": {
@@ -52,5 +83,7 @@
52
83
  }
53
84
  }
54
85
  },
55
- "files": ["tools"]
86
+ "files": [
87
+ "tools"
88
+ ]
56
89
  }
@@ -6,30 +6,41 @@
6
6
  */
7
7
 
8
8
  import type { TSchema } from "@sinclair/typebox";
9
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
10
- import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
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";
11
20
 
12
21
  // MCP tool names for this feature
13
22
  const TOOL_NAMES = ["company_research_exa"];
14
23
 
15
24
  // Tool name mapping: MCP name -> exposed name
16
25
  const NAME_MAP: Record<string, string> = {
17
- "company_research_exa": "web_search_company_research",
26
+ company_research_exa: "web_search_company_research",
18
27
  };
19
28
 
20
- const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
21
- const apiKey = findApiKey();
22
- if (!apiKey) return null;
29
+ const factory: CustomToolFactory = async (
30
+ _toolApi: ToolAPI,
31
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
+ const apiKey = findApiKey();
33
+ if (!apiKey) return null;
23
34
 
24
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
25
- if (mcpTools.length === 0) return null;
35
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
36
+ if (mcpTools.length === 0) return null;
26
37
 
27
- const callFn = (toolName: string, args: Record<string, unknown>) =>
28
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
38
+ const callFn = (toolName: string, args: Record<string, unknown>) =>
39
+ callExaTool(apiKey, TOOL_NAMES, toolName, args);
29
40
 
30
- return mcpTools.map((tool) =>
31
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
32
- );
41
+ return mcpTools.map((tool) =>
42
+ createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
43
+ );
33
44
  };
34
45
 
35
46
  export default factory;
@@ -13,54 +13,63 @@
13
13
  */
14
14
 
15
15
  import type { TSchema } from "@sinclair/typebox";
16
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
16
+ import type {
17
+ CustomAgentTool,
18
+ CustomToolFactory,
19
+ ToolAPI,
20
+ } from "@mariozechner/pi-coding-agent";
17
21
  import runtime from "./runtime.json";
18
22
 
19
23
  // Map feature names to their module imports
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"),
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"),
26
33
  };
27
34
 
28
35
  /**
29
36
  * Factory function that loads enabled features from runtime.json
30
37
  */
31
- const factory: CustomToolFactory = async (toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
- const allTools: CustomAgentTool<TSchema, unknown>[] = [];
33
- const enabledFeatures = runtime.features ?? [];
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 ?? [];
34
43
 
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
- }
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
+ }
41
50
 
42
- try {
43
- const module = await loader();
44
- const featureFactory = module.default;
51
+ try {
52
+ const module = await loader();
53
+ const featureFactory = module.default;
45
54
 
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
- }
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);
56
63
  }
57
- }
58
- } catch (error) {
59
- console.error(`Failed to load exa feature "${feature}":`, error);
64
+ }
65
+ }
60
66
  }
61
- }
67
+ } catch (error) {
68
+ console.error(`Failed to load exa feature "${feature}":`, error);
69
+ }
70
+ }
62
71
 
63
- return allTools.length > 0 ? allTools : null;
72
+ return allTools.length > 0 ? allTools : null;
64
73
  };
65
74
 
66
75
  export default factory;
@@ -6,30 +6,41 @@
6
6
  */
7
7
 
8
8
  import type { TSchema } from "@sinclair/typebox";
9
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
10
- import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
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";
11
20
 
12
21
  // MCP tool names for this feature
13
22
  const TOOL_NAMES = ["linkedin_search_exa"];
14
23
 
15
24
  // Tool name mapping: MCP name -> exposed name
16
25
  const NAME_MAP: Record<string, string> = {
17
- "linkedin_search_exa": "web_search_linkedin",
26
+ linkedin_search_exa: "web_search_linkedin",
18
27
  };
19
28
 
20
- const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
21
- const apiKey = findApiKey();
22
- if (!apiKey) return null;
29
+ const factory: CustomToolFactory = async (
30
+ _toolApi: ToolAPI,
31
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
+ const apiKey = findApiKey();
33
+ if (!apiKey) return null;
23
34
 
24
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
25
- if (mcpTools.length === 0) return null;
35
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
36
+ if (mcpTools.length === 0) return null;
26
37
 
27
- const callFn = (toolName: string, args: Record<string, unknown>) =>
28
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
38
+ const callFn = (toolName: string, args: Record<string, unknown>) =>
39
+ callExaTool(apiKey, TOOL_NAMES, toolName, args);
29
40
 
30
- return mcpTools.map((tool) =>
31
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
32
- );
41
+ return mcpTools.map((tool) =>
42
+ createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
43
+ );
33
44
  };
34
45
 
35
46
  export default factory;
@@ -7,34 +7,42 @@
7
7
  */
8
8
 
9
9
  import type { TSchema } from "@sinclair/typebox";
10
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
11
- import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
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";
12
21
 
13
22
  // MCP tool names for this feature
14
- const TOOL_NAMES = [
15
- "deep_researcher_start",
16
- "deep_researcher_check",
17
- ];
23
+ const TOOL_NAMES = ["deep_researcher_start", "deep_researcher_check"];
18
24
 
19
25
  // Tool name mapping: MCP name -> exposed name
20
26
  const NAME_MAP: Record<string, string> = {
21
- "deep_researcher_start": "web_search_researcher_start",
22
- "deep_researcher_check": "web_search_researcher_check",
27
+ deep_researcher_start: "web_search_researcher_start",
28
+ deep_researcher_check: "web_search_researcher_check",
23
29
  };
24
30
 
25
- const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
26
- const apiKey = findApiKey();
27
- if (!apiKey) return null;
31
+ const factory: CustomToolFactory = async (
32
+ _toolApi: ToolAPI,
33
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
34
+ const apiKey = findApiKey();
35
+ if (!apiKey) return null;
28
36
 
29
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
30
- if (mcpTools.length === 0) return null;
37
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
38
+ if (mcpTools.length === 0) return null;
31
39
 
32
- const callFn = (toolName: string, args: Record<string, unknown>) =>
33
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
40
+ const callFn = (toolName: string, args: Record<string, unknown>) =>
41
+ callExaTool(apiKey, TOOL_NAMES, toolName, args);
34
42
 
35
- return mcpTools.map((tool) =>
36
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
37
- );
43
+ return mcpTools.map((tool) =>
44
+ createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
45
+ );
38
46
  };
39
47
 
40
48
  export default factory;
@@ -9,38 +9,49 @@
9
9
  */
10
10
 
11
11
  import type { TSchema } from "@sinclair/typebox";
12
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
13
- import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
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";
14
23
 
15
24
  // MCP tool names for this feature
16
25
  const TOOL_NAMES = [
17
- "web_search_exa",
18
- "deep_search_exa",
19
- "get_code_context_exa",
20
- "crawling_exa",
26
+ "web_search_exa",
27
+ "deep_search_exa",
28
+ "get_code_context_exa",
29
+ "crawling_exa",
21
30
  ];
22
31
 
23
32
  // Tool name mapping: MCP name -> exposed name
24
33
  const NAME_MAP: Record<string, string> = {
25
- "web_search_exa": "web_search_general",
26
- "deep_search_exa": "web_search_deep",
27
- "get_code_context_exa": "web_search_code_context",
28
- "crawling_exa": "web_search_crawl_url",
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",
29
38
  };
30
39
 
31
- const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
32
- const apiKey = findApiKey();
33
- if (!apiKey) return null;
40
+ const factory: CustomToolFactory = async (
41
+ _toolApi: ToolAPI,
42
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
43
+ const apiKey = findApiKey();
44
+ if (!apiKey) return null;
34
45
 
35
- const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
36
- if (mcpTools.length === 0) return null;
46
+ const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
47
+ if (mcpTools.length === 0) return null;
37
48
 
38
- const callFn = (toolName: string, args: Record<string, unknown>) =>
39
- callExaTool(apiKey, TOOL_NAMES, toolName, args);
49
+ const callFn = (toolName: string, args: Record<string, unknown>) =>
50
+ callExaTool(apiKey, TOOL_NAMES, toolName, args);
40
51
 
41
- return mcpTools.map((tool) =>
42
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
43
- );
52
+ return mcpTools.map((tool) =>
53
+ createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
54
+ );
44
55
  };
45
56
 
46
57
  export default factory;
@@ -13,218 +13,244 @@ export const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
13
13
  export const WEBSETS_MCP_URL = "https://websetsmcp.exa.ai/mcp";
14
14
 
15
15
  export interface MCPTool {
16
- name: string;
17
- description: string;
18
- inputSchema: TSchema;
16
+ name: string;
17
+ description: string;
18
+ inputSchema: TSchema;
19
19
  }
20
20
 
21
21
  interface MCPToolsResponse {
22
- result?: {
23
- tools: MCPTool[];
24
- };
25
- error?: {
26
- code: number;
27
- message: string;
28
- };
22
+ result?: {
23
+ tools: MCPTool[];
24
+ };
25
+ error?: {
26
+ code: number;
27
+ message: string;
28
+ };
29
29
  }
30
30
 
31
31
  /**
32
32
  * Parse a .env file and return key-value pairs
33
33
  */
34
34
  function parseEnvFile(filePath: string): Record<string, string> {
35
- const result: Record<string, string> = {};
36
- if (!fs.existsSync(filePath)) return result;
37
-
38
- try {
39
- const content = fs.readFileSync(filePath, "utf-8");
40
- for (const line of content.split("\n")) {
41
- const trimmed = line.trim();
42
- if (!trimmed || trimmed.startsWith("#")) continue;
43
-
44
- const eqIndex = trimmed.indexOf("=");
45
- if (eqIndex === -1) continue;
46
-
47
- const key = trimmed.slice(0, eqIndex).trim();
48
- let value = trimmed.slice(eqIndex + 1).trim();
49
-
50
- // Remove surrounding quotes
51
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
52
- value = value.slice(1, -1);
53
- }
54
-
55
- result[key] = value;
35
+ const result: Record<string, string> = {};
36
+ if (!fs.existsSync(filePath)) return result;
37
+
38
+ try {
39
+ const content = fs.readFileSync(filePath, "utf-8");
40
+ for (const line of content.split("\n")) {
41
+ const trimmed = line.trim();
42
+ if (!trimmed || trimmed.startsWith("#")) continue;
43
+
44
+ const eqIndex = trimmed.indexOf("=");
45
+ if (eqIndex === -1) continue;
46
+
47
+ const key = trimmed.slice(0, eqIndex).trim();
48
+ let value = trimmed.slice(eqIndex + 1).trim();
49
+
50
+ // Remove surrounding quotes
51
+ if (
52
+ (value.startsWith('"') && value.endsWith('"')) ||
53
+ (value.startsWith("'") && value.endsWith("'"))
54
+ ) {
55
+ value = value.slice(1, -1);
56
56
  }
57
- } catch {
58
- // Ignore read errors
59
- }
60
57
 
61
- return result;
58
+ result[key] = value;
59
+ }
60
+ } catch {
61
+ // Ignore read errors
62
+ }
63
+
64
+ return result;
62
65
  }
63
66
 
64
67
  /**
65
68
  * Find EXA_API_KEY from environment or .env files
66
69
  */
67
70
  export function findApiKey(): string | null {
68
- // 1. Check environment variable
69
- if (process.env.EXA_API_KEY) {
70
- return process.env.EXA_API_KEY;
71
- }
72
-
73
- // 2. Check .env in current directory
74
- const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
75
- if (localEnv.EXA_API_KEY) {
76
- return localEnv.EXA_API_KEY;
77
- }
78
-
79
- // 3. Check ~/.env
80
- const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
81
- if (homeEnv.EXA_API_KEY) {
82
- return homeEnv.EXA_API_KEY;
83
- }
84
-
85
- return null;
71
+ // 1. Check environment variable
72
+ if (process.env.EXA_API_KEY) {
73
+ return process.env.EXA_API_KEY;
74
+ }
75
+
76
+ // 2. Check .env in current directory
77
+ const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
78
+ if (localEnv.EXA_API_KEY) {
79
+ return localEnv.EXA_API_KEY;
80
+ }
81
+
82
+ // 3. Check ~/.env
83
+ const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
84
+ if (homeEnv.EXA_API_KEY) {
85
+ return homeEnv.EXA_API_KEY;
86
+ }
87
+
88
+ return null;
86
89
  }
87
90
 
88
91
  /**
89
92
  * Call an MCP server endpoint
90
93
  */
91
- async function callMCP(url: string, method: string, params?: Record<string, unknown>): Promise<unknown> {
92
- const body = {
93
- jsonrpc: "2.0",
94
- method,
95
- params: params ?? {},
96
- id: 1,
97
- };
98
-
99
- const response = await fetch(url, {
100
- method: "POST",
101
- headers: {
102
- "Content-Type": "application/json",
103
- Accept: "application/json, text/event-stream",
104
- },
105
- body: JSON.stringify(body),
106
- });
107
-
108
- const text = await response.text();
109
-
110
- // Parse SSE response format
111
- let jsonData: string | null = null;
112
- for (const line of text.split("\n")) {
113
- if (line.startsWith("data: ")) {
114
- jsonData = line.slice(6);
115
- break;
116
- }
117
- }
118
-
119
- if (!jsonData) {
120
- // Try parsing as plain JSON
121
- try {
122
- return JSON.parse(text);
123
- } catch {
124
- throw new Error(`Failed to parse MCP response: ${text.slice(0, 500)}`);
125
- }
126
- }
127
-
128
- return JSON.parse(jsonData);
94
+ async function callMCP(
95
+ url: string,
96
+ method: string,
97
+ params?: Record<string, unknown>,
98
+ ): Promise<unknown> {
99
+ const body = {
100
+ jsonrpc: "2.0",
101
+ method,
102
+ params: params ?? {},
103
+ id: 1,
104
+ };
105
+
106
+ const response = await fetch(url, {
107
+ method: "POST",
108
+ headers: {
109
+ "Content-Type": "application/json",
110
+ Accept: "application/json, text/event-stream",
111
+ },
112
+ body: JSON.stringify(body),
113
+ });
114
+
115
+ const text = await response.text();
116
+
117
+ // Parse SSE response format
118
+ let jsonData: string | null = null;
119
+ for (const line of text.split("\n")) {
120
+ if (line.startsWith("data: ")) {
121
+ jsonData = line.slice(6);
122
+ break;
123
+ }
124
+ }
125
+
126
+ if (!jsonData) {
127
+ // Try parsing as plain JSON
128
+ try {
129
+ return JSON.parse(text);
130
+ } catch {
131
+ throw new Error(`Failed to parse MCP response: ${text.slice(0, 500)}`);
132
+ }
133
+ }
134
+
135
+ return JSON.parse(jsonData);
129
136
  }
130
137
 
131
138
  /**
132
139
  * Fetch available tools from Exa MCP server
133
140
  */
134
- export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
135
- const url = `${EXA_MCP_URL}?exaApiKey=${apiKey}&tools=${toolNames.join(",")}`;
136
-
137
- try {
138
- const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
139
- if (response.error) {
140
- throw new Error(response.error.message);
141
- }
142
- return response.result?.tools ?? [];
143
- } catch (error) {
144
- console.error(`Failed to fetch Exa tools:`, error);
145
- return [];
146
- }
141
+ export async function fetchExaTools(
142
+ apiKey: string,
143
+ toolNames: string[],
144
+ ): Promise<MCPTool[]> {
145
+ const url = `${EXA_MCP_URL}?exaApiKey=${apiKey}&tools=${toolNames.join(",")}`;
146
+
147
+ try {
148
+ const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
149
+ if (response.error) {
150
+ throw new Error(response.error.message);
151
+ }
152
+ return response.result?.tools ?? [];
153
+ } catch (error) {
154
+ console.error(`Failed to fetch Exa tools:`, error);
155
+ return [];
156
+ }
147
157
  }
148
158
 
149
159
  /**
150
160
  * Fetch available tools from Websets MCP server
151
161
  */
152
162
  export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
153
- const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
163
+ const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
154
164
 
155
- try {
156
- const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
157
- if (response.error) {
158
- throw new Error(response.error.message);
159
- }
160
- return response.result?.tools ?? [];
161
- } catch (error) {
162
- console.error(`Failed to fetch Websets tools:`, error);
163
- return [];
164
- }
165
+ try {
166
+ const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
167
+ if (response.error) {
168
+ throw new Error(response.error.message);
169
+ }
170
+ return response.result?.tools ?? [];
171
+ } catch (error) {
172
+ console.error(`Failed to fetch Websets tools:`, error);
173
+ return [];
174
+ }
165
175
  }
166
176
 
167
177
  /**
168
178
  * Call a tool on Exa MCP server
169
179
  */
170
- export async function callExaTool(apiKey: string, toolNames: string[], toolName: string, args: Record<string, unknown>): Promise<unknown> {
171
- const url = `${EXA_MCP_URL}?exaApiKey=${apiKey}&tools=${toolNames.join(",")}`;
172
- return callMCPTool(url, toolName, args);
180
+ export async function callExaTool(
181
+ apiKey: string,
182
+ toolNames: string[],
183
+ toolName: string,
184
+ args: Record<string, unknown>,
185
+ ): Promise<unknown> {
186
+ const url = `${EXA_MCP_URL}?exaApiKey=${apiKey}&tools=${toolNames.join(",")}`;
187
+ return callMCPTool(url, toolName, args);
173
188
  }
174
189
 
175
190
  /**
176
191
  * Call a tool on Websets MCP server
177
192
  */
178
- export async function callWebsetsTool(apiKey: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
179
- const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
180
- return callMCPTool(url, toolName, args);
193
+ export async function callWebsetsTool(
194
+ apiKey: string,
195
+ toolName: string,
196
+ args: Record<string, unknown>,
197
+ ): Promise<unknown> {
198
+ const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
199
+ return callMCPTool(url, toolName, args);
181
200
  }
182
201
 
183
202
  /**
184
203
  * Call a tool on an MCP server
185
204
  */
186
- async function callMCPTool(url: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
187
- const response = (await callMCP(url, "tools/call", {
188
- name: toolName,
189
- arguments: args,
190
- })) as { result?: { content?: Array<{ text?: string }> }; error?: { message: string } };
191
-
192
- if (response.error) {
193
- throw new Error(response.error.message);
194
- }
195
-
196
- // Extract text content from MCP response
197
- const content = response.result?.content;
198
- if (Array.isArray(content)) {
199
- const texts = content.filter((c) => c.text).map((c) => c.text);
200
- if (texts.length === 1) {
201
- // Try to parse as JSON
202
- try {
203
- return JSON.parse(texts[0]!);
204
- } catch {
205
- return texts[0];
206
- }
205
+ async function callMCPTool(
206
+ url: string,
207
+ toolName: string,
208
+ args: Record<string, unknown>,
209
+ ): Promise<unknown> {
210
+ const response = (await callMCP(url, "tools/call", {
211
+ name: toolName,
212
+ arguments: args,
213
+ })) as {
214
+ result?: { content?: Array<{ text?: string }> };
215
+ error?: { message: string };
216
+ };
217
+
218
+ if (response.error) {
219
+ throw new Error(response.error.message);
220
+ }
221
+
222
+ // Extract text content from MCP response
223
+ const content = response.result?.content;
224
+ if (Array.isArray(content)) {
225
+ const texts = content.filter((c) => c.text).map((c) => c.text);
226
+ if (texts.length === 1) {
227
+ // Try to parse as JSON
228
+ try {
229
+ return JSON.parse(texts[0]!);
230
+ } catch {
231
+ return texts[0];
207
232
  }
208
- return texts.join("\n\n");
209
- }
233
+ }
234
+ return texts.join("\n\n");
235
+ }
210
236
 
211
- return response.result;
237
+ return response.result;
212
238
  }
213
239
 
214
240
  /**
215
241
  * Create a tool wrapper for an MCP tool
216
242
  */
217
243
  export function createToolWrapper(
218
- mcpTool: MCPTool,
219
- renamedName: string,
220
- callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>
244
+ mcpTool: MCPTool,
245
+ renamedName: string,
246
+ callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>,
221
247
  ): CustomAgentTool<TSchema, unknown> {
222
- return {
223
- name: renamedName,
224
- description: mcpTool.description,
225
- parameters: mcpTool.inputSchema,
226
- async execute(args) {
227
- return callFn(mcpTool.name, args as Record<string, unknown>);
228
- },
229
- };
248
+ return {
249
+ name: renamedName,
250
+ description: mcpTool.description,
251
+ parameters: mcpTool.inputSchema,
252
+ async execute(args) {
253
+ return callFn(mcpTool.name, args as Record<string, unknown>);
254
+ },
255
+ };
230
256
  }
@@ -21,42 +21,53 @@
21
21
  */
22
22
 
23
23
  import type { TSchema } from "@sinclair/typebox";
24
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
25
- import { callWebsetsTool, createToolWrapper, fetchWebsetsTools, findApiKey } from "./shared";
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";
26
35
 
27
36
  // Tool name mapping: MCP name -> exposed name
28
37
  const NAME_MAP: Record<string, string> = {
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",
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",
45
54
  };
46
55
 
47
- const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
48
- const apiKey = findApiKey();
49
- if (!apiKey) return null;
56
+ const factory: CustomToolFactory = async (
57
+ _toolApi: ToolAPI,
58
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
59
+ const apiKey = findApiKey();
60
+ if (!apiKey) return null;
50
61
 
51
- const mcpTools = await fetchWebsetsTools(apiKey);
52
- if (mcpTools.length === 0) return null;
62
+ const mcpTools = await fetchWebsetsTools(apiKey);
63
+ if (mcpTools.length === 0) return null;
53
64
 
54
- const callFn = (toolName: string, args: Record<string, unknown>) =>
55
- callWebsetsTool(apiKey, toolName, args);
65
+ const callFn = (toolName: string, args: Record<string, unknown>) =>
66
+ callWebsetsTool(apiKey, toolName, args);
56
67
 
57
- return mcpTools.map((tool) =>
58
- createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
59
- );
68
+ return mcpTools.map((tool) =>
69
+ createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn),
70
+ );
60
71
  };
61
72
 
62
73
  export default factory;