@oh-my-pi/pi-coding-agent 1.337.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/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Company Tool
|
|
3
|
+
*
|
|
4
|
+
* Research companies using Exa's comprehensive data sources.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types.js";
|
|
9
|
+
import type { ExaRenderDetails } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** exa_company - Company research */
|
|
12
|
+
export const companyTool: CustomTool<any, ExaRenderDetails> = {
|
|
13
|
+
name: "exa_company",
|
|
14
|
+
label: "Exa Company",
|
|
15
|
+
description: `Research companies using Exa's comprehensive data sources.
|
|
16
|
+
|
|
17
|
+
Returns detailed company information including overview, news, financials, and key people.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- company_name: Name of the company to research (e.g., "OpenAI", "Google", "Y Combinator")`,
|
|
21
|
+
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
company_name: Type.String({ description: "Name of the company to research" }),
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
27
|
+
try {
|
|
28
|
+
const { findApiKey, callExaTool, formatSearchResults, isSearchResponse } = await import("./mcp-client.js");
|
|
29
|
+
|
|
30
|
+
const apiKey = await findApiKey();
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
|
|
34
|
+
details: { error: "EXA_API_KEY not found", toolName: "exa_company" },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const response = await callExaTool("company_research_exa", params, apiKey);
|
|
38
|
+
|
|
39
|
+
if (isSearchResponse(response)) {
|
|
40
|
+
const formatted = formatSearchResults(response);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text" as const, text: formatted }],
|
|
43
|
+
details: { response, toolName: "exa_company" },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
49
|
+
details: { raw: response, toolName: "exa_company" },
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
55
|
+
details: { error: message, toolName: "exa_company" },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* 22 tools for Exa's MCP servers:
|
|
5
|
+
* - 4 search tools (search, deep, code, crawl)
|
|
6
|
+
* - 1 LinkedIn search tool
|
|
7
|
+
* - 1 company research tool
|
|
8
|
+
* - 2 researcher tools (start, poll)
|
|
9
|
+
* - 14 websets tools (CRUD, items, search, enrichment, monitor)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CustomTool } from "../../custom-tools/types.js";
|
|
13
|
+
import type { ExaSettings } from "../../settings-manager.js";
|
|
14
|
+
import { companyTool } from "./company.js";
|
|
15
|
+
import { linkedinTool } from "./linkedin.js";
|
|
16
|
+
import { researcherTools } from "./researcher.js";
|
|
17
|
+
import { searchTools } from "./search.js";
|
|
18
|
+
import type { ExaRenderDetails } from "./types.js";
|
|
19
|
+
import { websetsTools } from "./websets.js";
|
|
20
|
+
|
|
21
|
+
/** All Exa tools (22 total) - static export for backward compatibility */
|
|
22
|
+
export const exaTools: CustomTool<any, ExaRenderDetails>[] = [
|
|
23
|
+
...searchTools,
|
|
24
|
+
linkedinTool,
|
|
25
|
+
companyTool,
|
|
26
|
+
...researcherTools,
|
|
27
|
+
...websetsTools,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Get Exa tools filtered by settings */
|
|
31
|
+
export function getExaTools(settings: Required<ExaSettings>): CustomTool<any, ExaRenderDetails>[] {
|
|
32
|
+
if (!settings.enabled) return [];
|
|
33
|
+
|
|
34
|
+
const tools: CustomTool<any, ExaRenderDetails>[] = [];
|
|
35
|
+
|
|
36
|
+
if (settings.enableSearch) tools.push(...searchTools);
|
|
37
|
+
if (settings.enableLinkedin) tools.push(linkedinTool);
|
|
38
|
+
if (settings.enableCompany) tools.push(companyTool);
|
|
39
|
+
if (settings.enableResearcher) tools.push(...researcherTools);
|
|
40
|
+
if (settings.enableWebsets) tools.push(...websetsTools);
|
|
41
|
+
|
|
42
|
+
return tools;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { companyTool } from "./company.js";
|
|
46
|
+
export { linkedinTool } from "./linkedin.js";
|
|
47
|
+
export { logExaError, logViewError } from "./logger.js";
|
|
48
|
+
export {
|
|
49
|
+
callExaTool,
|
|
50
|
+
callWebsetsTool,
|
|
51
|
+
createMCPToolFromServer,
|
|
52
|
+
createMCPWrappedTool,
|
|
53
|
+
fetchMCPToolSchema,
|
|
54
|
+
findApiKey,
|
|
55
|
+
formatSearchResults,
|
|
56
|
+
isSearchResponse,
|
|
57
|
+
} from "./mcp-client.js";
|
|
58
|
+
export { renderExaCall, renderExaResult } from "./render.js";
|
|
59
|
+
export { researcherTools } from "./researcher.js";
|
|
60
|
+
// Re-export individual modules for selective importing
|
|
61
|
+
export { searchTools } from "./search.js";
|
|
62
|
+
// Re-export types and utilities
|
|
63
|
+
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult, MCPToolWrapperConfig } from "./types.js";
|
|
64
|
+
export { websetsTools } from "./websets.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa LinkedIn Tool
|
|
3
|
+
*
|
|
4
|
+
* Search LinkedIn for people, companies, and professional content.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types.js";
|
|
9
|
+
import type { ExaRenderDetails } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** exa_linkedin - LinkedIn search */
|
|
12
|
+
export const linkedinTool: CustomTool<any, ExaRenderDetails> = {
|
|
13
|
+
name: "exa_linkedin",
|
|
14
|
+
label: "Exa LinkedIn",
|
|
15
|
+
description: `Search LinkedIn for people, companies, and professional content using Exa.
|
|
16
|
+
|
|
17
|
+
Returns LinkedIn search results with profiles, posts, and company information.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- query: LinkedIn search query (e.g., "Software Engineer at OpenAI", "Y Combinator companies")`,
|
|
21
|
+
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
query: Type.String({ description: "LinkedIn search query" }),
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
27
|
+
try {
|
|
28
|
+
const { findApiKey, callExaTool, formatSearchResults, isSearchResponse } = await import("./mcp-client.js");
|
|
29
|
+
|
|
30
|
+
const apiKey = await findApiKey();
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
|
|
34
|
+
details: { error: "EXA_API_KEY not found", toolName: "exa_linkedin" },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const response = await callExaTool("linkedin_search_exa", params, apiKey);
|
|
38
|
+
|
|
39
|
+
if (isSearchResponse(response)) {
|
|
40
|
+
const formatted = formatSearchResults(response);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text" as const, text: formatted }],
|
|
43
|
+
details: { response, toolName: "exa_linkedin" },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
49
|
+
details: { raw: response, toolName: "exa_linkedin" },
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
55
|
+
details: { error: message, toolName: "exa_linkedin" },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Error Logger
|
|
3
|
+
*
|
|
4
|
+
* Append-only logging to ~/.pi/ for debugging production issues.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { CONFIG_DIR_NAME } from "../../../config.js";
|
|
11
|
+
|
|
12
|
+
/** Get the base config directory (e.g., ~/.pi/) */
|
|
13
|
+
function getConfigDir(): string {
|
|
14
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Log file paths */
|
|
18
|
+
const LOG_FILES = {
|
|
19
|
+
exa: "exa_errors.log",
|
|
20
|
+
view: "view_errors.log",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
type LogType = keyof typeof LOG_FILES;
|
|
24
|
+
|
|
25
|
+
/** Format a log entry with timestamp */
|
|
26
|
+
function formatEntry(message: string, context?: Record<string, unknown>): string {
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
const contextStr = context ? ` ${JSON.stringify(context)}` : "";
|
|
29
|
+
return `[${timestamp}] ${message}${contextStr}\n`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Append to log file (creates directory if needed) */
|
|
33
|
+
export function logError(type: LogType, message: string, context?: Record<string, unknown>): void {
|
|
34
|
+
try {
|
|
35
|
+
const configDir = getConfigDir();
|
|
36
|
+
if (!existsSync(configDir)) {
|
|
37
|
+
mkdirSync(configDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const logPath = join(configDir, LOG_FILES[type]);
|
|
41
|
+
const entry = formatEntry(message, context);
|
|
42
|
+
appendFileSync(logPath, entry);
|
|
43
|
+
} catch {
|
|
44
|
+
// Silently ignore logging failures - we don't want to break tool execution
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Log MCP fetch/call errors */
|
|
49
|
+
export function logExaError(message: string, context?: Record<string, unknown>): void {
|
|
50
|
+
logError("exa", message, context);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Log render/view errors */
|
|
54
|
+
export function logViewError(message: string, context?: Record<string, unknown>): void {
|
|
55
|
+
logError("view", message, context);
|
|
56
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa MCP Client
|
|
3
|
+
*
|
|
4
|
+
* Client for interacting with Exa MCP servers via JSON-RPC 2.0 over HTTPS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types.js";
|
|
9
|
+
import { logExaError } from "./logger.js";
|
|
10
|
+
import type {
|
|
11
|
+
ExaRenderDetails,
|
|
12
|
+
ExaSearchResponse,
|
|
13
|
+
ExaSearchResult,
|
|
14
|
+
MCPCallResponse,
|
|
15
|
+
MCPTool,
|
|
16
|
+
MCPToolsResponse,
|
|
17
|
+
MCPToolWrapperConfig,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
/** Find EXA_API_KEY from process.env or .env files */
|
|
21
|
+
export async function findApiKey(): Promise<string | null> {
|
|
22
|
+
// Check process.env first
|
|
23
|
+
if (process.env.EXA_API_KEY) {
|
|
24
|
+
return process.env.EXA_API_KEY;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try loading from .env files in cwd and home
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
30
|
+
|
|
31
|
+
for (const dir of [cwd, home]) {
|
|
32
|
+
const envPath = `${dir}/.env`;
|
|
33
|
+
try {
|
|
34
|
+
const file = Bun.file(envPath);
|
|
35
|
+
if (await file.exists()) {
|
|
36
|
+
const content = await file.text();
|
|
37
|
+
const match = content.match(/^EXA_API_KEY=(.+)$/m);
|
|
38
|
+
if (match?.[1]) {
|
|
39
|
+
return match[1].trim().replace(/^["']|["']$/g, "");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Ignore read errors
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse SSE response format (lines starting with "data: ") */
|
|
51
|
+
function parseSSE(text: string): unknown {
|
|
52
|
+
const lines = text.split("\n");
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.startsWith("data: ")) {
|
|
55
|
+
const data = line.slice(6).trim();
|
|
56
|
+
if (data === "[DONE]") continue;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(data);
|
|
59
|
+
} catch {
|
|
60
|
+
// Try next line
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Fallback: try parsing entire response as JSON
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(text);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Call MCP server with JSON-RPC 2.0 */
|
|
73
|
+
export async function callMCP(url: string, method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
74
|
+
const body = {
|
|
75
|
+
jsonrpc: "2.0",
|
|
76
|
+
id: Math.random().toString(36).slice(2),
|
|
77
|
+
method,
|
|
78
|
+
params: params ?? {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
Accept: "application/json, text/event-stream",
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
|
92
|
+
logExaError(errorMsg, { url, method, params });
|
|
93
|
+
throw new Error(errorMsg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const text = await response.text();
|
|
97
|
+
const result = parseSSE(text);
|
|
98
|
+
|
|
99
|
+
if (!result) {
|
|
100
|
+
logExaError("Failed to parse MCP response", { url, method, responseText: text.slice(0, 500) });
|
|
101
|
+
throw new Error("Failed to parse MCP response");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Fetch available tools from Exa MCP */
|
|
108
|
+
export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
|
|
109
|
+
const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&toolNames=${encodeURIComponent(toolNames.join(","))}`;
|
|
110
|
+
const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
|
|
111
|
+
|
|
112
|
+
if (response.error) {
|
|
113
|
+
logExaError("MCP tools/list error", { toolNames, error: response.error });
|
|
114
|
+
throw new Error(`MCP error: ${response.error.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return response.result?.tools ?? [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Fetch available tools from Websets MCP */
|
|
121
|
+
export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
|
|
122
|
+
const url = `https://websetsmcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}`;
|
|
123
|
+
const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
|
|
124
|
+
|
|
125
|
+
if (response.error) {
|
|
126
|
+
logExaError("Websets MCP tools/list error", { error: response.error });
|
|
127
|
+
throw new Error(`MCP error: ${response.error.message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return response.result?.tools ?? [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Call a tool on Exa MCP (simplified: toolName as first arg for easier use) */
|
|
134
|
+
export async function callExaTool(toolName: string, args: Record<string, unknown>, apiKey: string): Promise<unknown> {
|
|
135
|
+
const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolName)}`;
|
|
136
|
+
const response = (await callMCP(url, "tools/call", {
|
|
137
|
+
name: toolName,
|
|
138
|
+
arguments: args,
|
|
139
|
+
})) as MCPCallResponse;
|
|
140
|
+
|
|
141
|
+
if (response.error) {
|
|
142
|
+
logExaError("MCP tools/call error", { toolName, args, error: response.error });
|
|
143
|
+
throw new Error(`MCP error: ${response.error.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return response.result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Call a tool on Websets MCP */
|
|
150
|
+
export async function callWebsetsTool(
|
|
151
|
+
apiKey: string,
|
|
152
|
+
toolName: string,
|
|
153
|
+
args: Record<string, unknown>,
|
|
154
|
+
): Promise<unknown> {
|
|
155
|
+
const url = `https://websetsmcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}`;
|
|
156
|
+
const response = (await callMCP(url, "tools/call", {
|
|
157
|
+
name: toolName,
|
|
158
|
+
arguments: args,
|
|
159
|
+
})) as MCPCallResponse;
|
|
160
|
+
|
|
161
|
+
if (response.error) {
|
|
162
|
+
logExaError("Websets MCP tools/call error", { toolName, args, error: response.error });
|
|
163
|
+
throw new Error(`MCP error: ${response.error.message}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return response.result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Parse Exa markdown format into SearchResponse */
|
|
170
|
+
export function parseExaMarkdown(text: string): ExaSearchResponse | null {
|
|
171
|
+
const results: ExaSearchResult[] = [];
|
|
172
|
+
const lines = text.split("\n");
|
|
173
|
+
let currentResult: Partial<ExaSearchResult> | null = null;
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
176
|
+
const line = lines[i].trim();
|
|
177
|
+
|
|
178
|
+
// Match result header: ## Title
|
|
179
|
+
if (line.startsWith("## ")) {
|
|
180
|
+
if (currentResult?.title) {
|
|
181
|
+
results.push(currentResult as ExaSearchResult);
|
|
182
|
+
}
|
|
183
|
+
currentResult = { title: line.slice(3).trim() };
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!currentResult) continue;
|
|
188
|
+
|
|
189
|
+
// Match URL: **URL:** ...
|
|
190
|
+
if (line.startsWith("**URL:**")) {
|
|
191
|
+
currentResult.url = line.slice(8).trim();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Match Author: **Author:** ...
|
|
196
|
+
if (line.startsWith("**Author:**")) {
|
|
197
|
+
currentResult.author = line.slice(11).trim();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Match Published Date: **Published Date:** ...
|
|
202
|
+
if (line.startsWith("**Published Date:**")) {
|
|
203
|
+
currentResult.publishedDate = line.slice(19).trim();
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Match Text: **Text:** ...
|
|
208
|
+
if (line.startsWith("**Text:**")) {
|
|
209
|
+
currentResult.text = line.slice(9).trim();
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Accumulate text content
|
|
214
|
+
if (currentResult.text && line && !line.startsWith("**")) {
|
|
215
|
+
currentResult.text += ` ${line}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add last result
|
|
220
|
+
if (currentResult?.title) {
|
|
221
|
+
results.push(currentResult as ExaSearchResult);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (results.length === 0) return null;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
results,
|
|
228
|
+
statuses: results.map((r, i) => ({ id: r.id ?? `result-${i}`, status: "success" })),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Format search results for LLM */
|
|
233
|
+
export function formatSearchResults(data: ExaSearchResponse): string {
|
|
234
|
+
const results = data.results ?? [];
|
|
235
|
+
if (results.length === 0) return "No results found.";
|
|
236
|
+
|
|
237
|
+
let output = "";
|
|
238
|
+
for (let i = 0; i < results.length; i++) {
|
|
239
|
+
const r = results[i];
|
|
240
|
+
output += `\n## ${r.title ?? "Untitled"}`;
|
|
241
|
+
if (r.url) output += `\n**URL:** ${r.url}`;
|
|
242
|
+
if (r.author) output += `\n**Author:** ${r.author}`;
|
|
243
|
+
if (r.publishedDate) output += `\n**Published Date:** ${r.publishedDate}`;
|
|
244
|
+
if (r.text) output += `\n**Text:** ${r.text}`;
|
|
245
|
+
if (r.highlights?.length) {
|
|
246
|
+
output += `\n**Highlights:**`;
|
|
247
|
+
for (const h of r.highlights) {
|
|
248
|
+
output += `\n- ${h}`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
output += "\n";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (data.costDollars) {
|
|
255
|
+
output += `\n**Cost:** $${data.costDollars.total.toFixed(4)}`;
|
|
256
|
+
}
|
|
257
|
+
if (data.searchTime) {
|
|
258
|
+
output += `\n**Search Time:** ${data.searchTime.toFixed(2)}s`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return output.trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Check if result is a search response */
|
|
265
|
+
export function isSearchResponse(data: unknown): data is ExaSearchResponse {
|
|
266
|
+
return (
|
|
267
|
+
typeof data === "object" &&
|
|
268
|
+
data !== null &&
|
|
269
|
+
("results" in data || "statuses" in data || "costDollars" in data || "searchTime" in data)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Cache for MCP tool schemas (keyed by MCP tool name) */
|
|
274
|
+
const mcpSchemaCache = new Map<string, MCPTool>();
|
|
275
|
+
|
|
276
|
+
/** Fetch and cache MCP tool schema */
|
|
277
|
+
export async function fetchMCPToolSchema(
|
|
278
|
+
apiKey: string,
|
|
279
|
+
mcpToolName: string,
|
|
280
|
+
isWebsetsTool = false,
|
|
281
|
+
): Promise<MCPTool | null> {
|
|
282
|
+
const cacheKey = `${isWebsetsTool ? "websets" : "exa"}:${mcpToolName}`;
|
|
283
|
+
if (mcpSchemaCache.has(cacheKey)) {
|
|
284
|
+
return mcpSchemaCache.get(cacheKey)!;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const tools = isWebsetsTool ? await fetchWebsetsTools(apiKey) : await fetchExaTools(apiKey, [mcpToolName]);
|
|
289
|
+
const tool = tools.find((t) => t.name === mcpToolName);
|
|
290
|
+
if (tool) {
|
|
291
|
+
mcpSchemaCache.set(cacheKey, tool);
|
|
292
|
+
return tool;
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// Fall through to return null
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create a CustomTool dynamically from MCP tool metadata.
|
|
302
|
+
*
|
|
303
|
+
* This allows tools to be generated from MCP server schemas without hardcoding,
|
|
304
|
+
* reducing drift when MCP servers add new parameters.
|
|
305
|
+
*/
|
|
306
|
+
export function createMCPWrappedTool(
|
|
307
|
+
config: MCPToolWrapperConfig,
|
|
308
|
+
schema: TSchema,
|
|
309
|
+
description: string,
|
|
310
|
+
): CustomTool<TSchema, ExaRenderDetails> {
|
|
311
|
+
return {
|
|
312
|
+
name: config.name,
|
|
313
|
+
label: config.label,
|
|
314
|
+
description,
|
|
315
|
+
parameters: schema,
|
|
316
|
+
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
317
|
+
try {
|
|
318
|
+
const apiKey = await findApiKey();
|
|
319
|
+
if (!apiKey) {
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
|
|
322
|
+
details: { error: "EXA_API_KEY not found", toolName: config.name },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const response = config.isWebsetsTool
|
|
327
|
+
? await callWebsetsTool(apiKey, config.mcpToolName, params as Record<string, unknown>)
|
|
328
|
+
: await callExaTool(config.mcpToolName, params as Record<string, unknown>, apiKey);
|
|
329
|
+
|
|
330
|
+
if (isSearchResponse(response)) {
|
|
331
|
+
const formatted = formatSearchResults(response);
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text" as const, text: formatted }],
|
|
334
|
+
details: { response, toolName: config.name },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
340
|
+
details: { raw: response, toolName: config.name },
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
346
|
+
details: { error: message, toolName: config.name },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create a CustomTool by fetching schema from MCP server.
|
|
355
|
+
*
|
|
356
|
+
* Falls back to provided fallback schema if MCP fetch fails.
|
|
357
|
+
*/
|
|
358
|
+
export async function createMCPToolFromServer(
|
|
359
|
+
apiKey: string,
|
|
360
|
+
config: MCPToolWrapperConfig,
|
|
361
|
+
fallbackSchema: TSchema,
|
|
362
|
+
fallbackDescription: string,
|
|
363
|
+
): Promise<CustomTool<TSchema, ExaRenderDetails>> {
|
|
364
|
+
const mcpTool = await fetchMCPToolSchema(apiKey, config.mcpToolName, config.isWebsetsTool);
|
|
365
|
+
const schema = mcpTool?.inputSchema ?? fallbackSchema;
|
|
366
|
+
const description = mcpTool?.description ?? fallbackDescription;
|
|
367
|
+
return createMCPWrappedTool(config, schema, description);
|
|
368
|
+
}
|