@oh-my-pi/exa 0.3.0 → 0.4.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 +44 -38
- package/package.json +44 -11
- package/tools/exa/company.ts +24 -13
- package/tools/exa/index.ts +43 -34
- package/tools/exa/linkedin.ts +24 -13
- package/tools/exa/researcher.ts +26 -18
- package/tools/exa/search.ts +31 -20
- package/tools/exa/shared.ts +182 -156
- package/tools/exa/websets.ts +39 -28
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
|
|
21
|
-
|
|
22
|
-
| `search`
|
|
23
|
-
| `linkedin`
|
|
24
|
-
| `company`
|
|
25
|
-
| `researcher` |
|
|
26
|
-
| `websets`
|
|
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
|
|
66
|
-
|
|
67
|
-
| `web_search_general`
|
|
68
|
-
| `web_search_deep`
|
|
69
|
-
| `web_search_code_context` | Search code snippets, docs, and examples
|
|
70
|
-
| `web_search_crawl_url`
|
|
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
|
|
75
|
-
|
|
74
|
+
| Tool | Description |
|
|
75
|
+
| --------------------- | -------------------------------------- |
|
|
76
76
|
| `web_search_linkedin` | Search LinkedIn profiles and companies |
|
|
77
77
|
|
|
78
78
|
### company
|
|
79
79
|
|
|
80
|
-
| Tool
|
|
81
|
-
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
| ----------------------------- | ------------------------------ |
|
|
82
82
|
| `web_search_company_research` | Comprehensive company research |
|
|
83
83
|
|
|
84
84
|
### researcher
|
|
85
85
|
|
|
86
|
-
| Tool
|
|
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
|
|
94
|
-
|
|
95
|
-
| `webset_create`
|
|
96
|
-
| `webset_list`
|
|
97
|
-
| `webset_get`
|
|
98
|
-
| `webset_update`
|
|
99
|
-
| `webset_delete`
|
|
100
|
-
| `webset_items_list`
|
|
101
|
-
| `webset_item_get`
|
|
102
|
-
| `webset_search_create`
|
|
103
|
-
| `webset_search_get`
|
|
104
|
-
| `webset_search_cancel`
|
|
105
|
-
| `webset_enrichment_create` | Extract custom data from webset items
|
|
106
|
-
| `webset_enrichment_get`
|
|
107
|
-
| `webset_enrichment_update` | Update enrichment metadata
|
|
108
|
-
| `webset_enrichment_delete` | Delete enrichment
|
|
109
|
-
| `webset_enrichment_cancel` | Cancel running enrichment
|
|
110
|
-
| `webset_monitor_create`
|
|
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
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Exa AI web search and websets tools for pi",
|
|
5
|
-
"keywords": [
|
|
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
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
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": [
|
|
86
|
+
"files": [
|
|
87
|
+
"tools"
|
|
88
|
+
]
|
|
56
89
|
}
|
package/tools/exa/company.ts
CHANGED
|
@@ -6,30 +6,41 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TSchema } from "@sinclair/typebox";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
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
|
-
|
|
26
|
+
company_research_exa: "web_search_company_research",
|
|
18
27
|
};
|
|
19
28
|
|
|
20
|
-
const factory: CustomToolFactory = async (
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
35
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
36
|
+
if (mcpTools.length === 0) return null;
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
39
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
package/tools/exa/index.ts
CHANGED
|
@@ -13,54 +13,63 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { TSchema } from "@sinclair/typebox";
|
|
16
|
-
import type {
|
|
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<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 (
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
try {
|
|
52
|
+
const module = await loader();
|
|
53
|
+
const featureFactory = module.default;
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
return allTools.length > 0 ? allTools : null;
|
|
64
73
|
};
|
|
65
74
|
|
|
66
75
|
export default factory;
|
package/tools/exa/linkedin.ts
CHANGED
|
@@ -6,30 +6,41 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TSchema } from "@sinclair/typebox";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
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
|
-
|
|
26
|
+
linkedin_search_exa: "web_search_linkedin",
|
|
18
27
|
};
|
|
19
28
|
|
|
20
|
-
const factory: CustomToolFactory = async (
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
35
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
36
|
+
if (mcpTools.length === 0) return null;
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
39
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
package/tools/exa/researcher.ts
CHANGED
|
@@ -7,34 +7,42 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { TSchema } from "@sinclair/typebox";
|
|
10
|
-
import type {
|
|
11
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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 (
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
37
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
38
|
+
if (mcpTools.length === 0) return null;
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
41
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
package/tools/exa/search.ts
CHANGED
|
@@ -9,38 +9,49 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { TSchema } from "@sinclair/typebox";
|
|
12
|
-
import type {
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 (
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
46
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
47
|
+
if (mcpTools.length === 0) return null;
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
50
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
package/tools/exa/shared.ts
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: TSchema;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
interface MCPToolsResponse {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
163
|
+
const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
233
|
+
}
|
|
234
|
+
return texts.join("\n\n");
|
|
235
|
+
}
|
|
210
236
|
|
|
211
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
mcpTool: MCPTool,
|
|
245
|
+
renamedName: string,
|
|
246
|
+
callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>,
|
|
221
247
|
): CustomAgentTool<TSchema, unknown> {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|
package/tools/exa/websets.ts
CHANGED
|
@@ -21,42 +21,53 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { TSchema } from "@sinclair/typebox";
|
|
24
|
-
import type {
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
62
|
+
const mcpTools = await fetchWebsetsTools(apiKey);
|
|
63
|
+
if (mcpTools.length === 0) return null;
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
66
|
+
callWebsetsTool(apiKey, toolName, args);
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
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;
|