@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.
Files changed (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. 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
+ }