From 132f6ca844a03e116fe425518592f2adc1eb9ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Montone?= Date: Tue, 2 Dec 2025 21:55:51 -0300 Subject: [PATCH 1/2] add exa search --- README.md | 8 +++- lib/step-registry.ts | 15 ++++-- lib/types/integration.ts | 3 +- package.json | 4 +- plugins/exa/codegen/search.ts | 31 ++++++++++++ plugins/exa/icon.tsx | 31 ++++++++++++ plugins/exa/index.ts | 88 +++++++++++++++++++++++++++++++++++ plugins/exa/steps/search.ts | 70 ++++++++++++++++++++++++++++ plugins/exa/test.ts | 38 +++++++++++++++ plugins/index.ts | 3 +- pnpm-lock.yaml | 49 +++++++++++++++++++ 11 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 plugins/exa/codegen/search.ts create mode 100644 plugins/exa/icon.tsx create mode 100644 plugins/exa/index.ts create mode 100644 plugins/exa/steps/search.ts create mode 100644 plugins/exa/test.ts diff --git a/README.md b/README.md index b08be906..4c79c6d2 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,16 @@ Create a `.env.local` file with the following: ```env # Database DATABASE_URL=postgresql://user:password@localhost:5432/workflow_builder +Key should be a 32-byte hex string (64 characters) +INTEGRATION_ENCRYPTION_KEY=your-encryption-key # Better Auth BETTER_AUTH_SECRET=your-secret-key BETTER_AUTH_URL=http://localhost:3000 -# AI Gateway (for AI workflow generation) -AI_GATEWAY_API_KEY=your-openai-api-key +# AI Gateway (for AI workflow generation, see more https://vercel.com/ai-gateway) +AI_GATEWAY_API_KEY=your-ai-gateway-api-key + ``` ### Installation @@ -80,6 +83,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. - **AI Gateway**: Generate Text, Generate Image +- **Exa**: Search Web - **Firecrawl**: Scrape URL, Search Web - **Linear**: Create Ticket, Find Issues - **Resend**: Send Email diff --git a/lib/step-registry.ts b/lib/step-registry.ts index 6a71649b..21bf840b 100644 --- a/lib/step-registry.ts +++ b/lib/step-registry.ts @@ -7,7 +7,7 @@ * This registry enables dynamic step imports that are statically analyzable * by the bundler. Each action type maps to its step importer function. * - * Generated entries: 10 + * Generated entries: 11 */ import "server-only"; @@ -41,11 +41,15 @@ export const PLUGIN_STEP_IMPORTERS: Record = { importer: () => import("@/plugins/ai-gateway/steps/generate-image"), stepFunction: "generateImageStep", }, + "exa/search": { + importer: () => import("@/plugins/exa/steps/search"), + stepFunction: "exaSearchStep", + }, "firecrawl/scrape": { importer: () => import("@/plugins/firecrawl/steps/scrape"), stepFunction: "firecrawlScrapeStep", }, - Scrape: { + "Scrape": { importer: () => import("@/plugins/firecrawl/steps/scrape"), stepFunction: "firecrawlScrapeStep", }, @@ -53,7 +57,7 @@ export const PLUGIN_STEP_IMPORTERS: Record = { importer: () => import("@/plugins/firecrawl/steps/search"), stepFunction: "firecrawlSearchStep", }, - Search: { + "Search": { importer: () => import("@/plugins/firecrawl/steps/search"), stepFunction: "firecrawlSearchStep", }, @@ -114,6 +118,7 @@ export const PLUGIN_STEP_IMPORTERS: Record = { export const ACTION_LABELS: Record = { "ai-gateway/generate-text": "Generate Text", "ai-gateway/generate-image": "Generate Image", + "exa/search": "Search Web", "firecrawl/scrape": "Scrape URL", "firecrawl/search": "Search Web", "linear/create-ticket": "Create Ticket", @@ -122,8 +127,8 @@ export const ACTION_LABELS: Record = { "slack/send-message": "Send Slack Message", "v0/create-chat": "Create Chat", "v0/send-message": "Send Message", - Scrape: "Scrape URL", - Search: "Search Web", + "Scrape": "Scrape URL", + "Search": "Search Web", "Generate Text": "Generate Text", "Generate Image": "Generate Image", "Send Email": "Send Email", diff --git a/lib/types/integration.ts b/lib/types/integration.ts index cd146253..cbfd65f9 100644 --- a/lib/types/integration.ts +++ b/lib/types/integration.ts @@ -9,13 +9,14 @@ * 2. Add a system integration to SYSTEM_INTEGRATION_TYPES in discover-plugins.ts * 3. Run: pnpm discover-plugins * - * Generated types: ai-gateway, database, firecrawl, linear, resend, slack, v0 + * Generated types: ai-gateway, database, exa, firecrawl, linear, resend, slack, v0 */ // Integration type union - plugins + system integrations export type IntegrationType = | "ai-gateway" | "database" + | "exa" | "firecrawl" | "linear" | "resend" diff --git a/package.json b/package.json index 65473cf2..b7a1a327 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "discover-plugins": "tsx scripts/discover-plugins.ts", - "create-plugin": "tsx scripts/create-plugin.ts" + "create-plugin": "tsx scripts/create-plugin.ts", + "workflow:runs:web": "npx workflow inspect runs --web" }, "dependencies": { "@ai-sdk/provider": "^2.0.0", @@ -36,6 +37,7 @@ "clsx": "^2.1.1", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", + "exa-js": "^2.0.11", "jotai": "^2.15.1", "jszip": "^3.10.1", "lucide-react": "^0.552.0", diff --git a/plugins/exa/codegen/search.ts b/plugins/exa/codegen/search.ts new file mode 100644 index 00000000..51ee7eeb --- /dev/null +++ b/plugins/exa/codegen/search.ts @@ -0,0 +1,31 @@ +/** + * Code generation template for Exa Search action + * This template is used when exporting workflows to standalone Next.js projects + * It uses environment variables instead of integrationId + */ +export const searchCodegenTemplate = `import Exa from 'exa-js'; + +export async function exaSearchStep(input: { + query: string; + numResults?: number; + type?: 'auto' | 'neural' | 'fast' | 'deep'; +}) { + "use step"; + + const exa = new Exa(process.env.EXA_API_KEY!); + + const result = await exa.search(input.query, { + numResults: input.numResults || 10, + type: input.type || 'auto', + }); + + return { + results: result.results.map((r) => ({ + url: r.url, + title: r.title, + publishedDate: r.publishedDate, + author: r.author, + text: r.text, + })), + }; +}`; diff --git a/plugins/exa/icon.tsx b/plugins/exa/icon.tsx new file mode 100644 index 00000000..f642f426 --- /dev/null +++ b/plugins/exa/icon.tsx @@ -0,0 +1,31 @@ +export function ExaIcon({ className }: { className?: string }) { + return ( + + Exa + + + + + + ); +} diff --git a/plugins/exa/index.ts b/plugins/exa/index.ts new file mode 100644 index 00000000..96b2abd1 --- /dev/null +++ b/plugins/exa/index.ts @@ -0,0 +1,88 @@ +import type { IntegrationPlugin } from "../registry"; +import { registerIntegration } from "../registry"; +import { searchCodegenTemplate } from "./codegen/search"; +import { ExaIcon } from "./icon"; + +const exaPlugin: IntegrationPlugin = { + type: "exa", + label: "Exa", + description: + "Semantic web search API giving AI apps fast, relevant, up-to-date results", + + icon: ExaIcon, + + formFields: [ + { + id: "exaApiKey", + label: "API Key", + type: "password", + placeholder: "Your Exa API key", + configKey: "exaApiKey", + envVar: "EXA_API_KEY", + helpText: "Get your API key from ", + helpLink: { + text: "Exa Dashboard", + url: "https://dashboard.exa.ai/api-keys/", + }, + }, + ], + + testConfig: { + getTestFunction: async () => { + const { testExa } = await import("./test"); + return testExa; + }, + }, + + dependencies: { + "exa-js": "^1.5.12", + }, + + actions: [ + { + slug: "search", + label: "Search Web", + description: + "Perform semantic web search and retrieve relevant results with content", + category: "Exa", + stepFunction: "exaSearchStep", + stepImportPath: "search", + configFields: [ + { + key: "query", + label: "Search Query", + type: "template-input", + placeholder: "Search query or {{NodeName.query}}", + example: "latest AI research papers", + required: true, + }, + { + key: "numResults", + label: "Number of Results", + type: "number", + placeholder: "10", + min: 1, + example: "10", + }, + { + key: "type", + label: "Search Type", + type: "select", + options: [ + { value: "auto", label: "Auto" }, + { value: "neural", label: "Neural" }, + { value: "fast", label: "Fast" }, + { value: "deep", label: "Deep" }, + ], + defaultValue: "auto", + }, + ], + codegenTemplate: searchCodegenTemplate, + }, + ], +}; + +// Auto-register on import +registerIntegration(exaPlugin); + +export default exaPlugin; diff --git a/plugins/exa/steps/search.ts b/plugins/exa/steps/search.ts new file mode 100644 index 00000000..5b36feba --- /dev/null +++ b/plugins/exa/steps/search.ts @@ -0,0 +1,70 @@ +import "server-only"; + +import Exa from "exa-js"; +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessage } from "@/lib/utils"; + +type ExaSearchResult = { + results: Array<{ + url: string; + title: string | null; + publishedDate?: string; + author?: string; + text?: string; + }>; +}; + +export type ExaSearchInput = StepInput & { + integrationId?: string; + query: string; + numResults?: number; + type?: "auto" | "neural" | "fast" | "deep"; +}; + +/** + * Search logic using Exa SDK + */ +async function search(input: ExaSearchInput): Promise { + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + const apiKey = credentials.EXA_API_KEY; + + if (!apiKey) { + throw new Error("Exa API Key is not configured."); + } + + try { + const exa = new Exa(apiKey); + + const result = await exa.search(input.query, { + numResults: input.numResults ? Number(input.numResults) : 10, + type: input.type || "auto", + }); + + return { + results: result.results.map((r) => ({ + url: r.url, + title: r.title, + publishedDate: r.publishedDate, + author: r.author, + text: r.text, + })), + }; + } catch (error) { + throw new Error(`Failed to search: ${getErrorMessage(error)}`); + } +} + +/** + * Exa Search Step + * Performs semantic web search using Exa + */ +export async function exaSearchStep( + input: ExaSearchInput +): Promise { + "use step"; + return withStepLogging(input, () => search(input)); +} diff --git a/plugins/exa/test.ts b/plugins/exa/test.ts new file mode 100644 index 00000000..d019f32a --- /dev/null +++ b/plugins/exa/test.ts @@ -0,0 +1,38 @@ +export async function testExa(credentials: Record) { + try { + const apiKey = credentials.EXA_API_KEY; + + if (!apiKey) { + return { + success: false, + error: "EXA_API_KEY is required", + }; + } + + // Use a minimal search request to validate the API key + const response = await fetch("https://api.exa.ai/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + body: JSON.stringify({ + query: "test", + numResults: 1, + type: "keyword", + }), + }); + + if (response.ok) { + return { success: true }; + } + + const error = await response.text(); + return { success: false, error: error || "Invalid API key" }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} diff --git a/plugins/index.ts b/plugins/index.ts index c1eaf29e..8d88af73 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -13,10 +13,11 @@ * 1. Delete the plugin directory * 2. Run: pnpm discover-plugins (or it runs automatically on build) * - * Discovered plugins: ai-gateway, firecrawl, linear, resend, slack, v0 + * Discovered plugins: ai-gateway, exa, firecrawl, linear, resend, slack, v0 */ import "./ai-gateway"; +import "./exa"; import "./firecrawl"; import "./linear"; import "./resend"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01d512da..7d60730a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: drizzle-orm: specifier: ^0.44.7 version: 0.44.7(@opentelemetry/api@1.9.0)(kysely@0.28.8)(postgres@3.4.7) + exa-js: + specifier: ^2.0.11 + version: 2.0.11(ws@8.18.3) jotai: specifier: ^2.15.1 version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0) @@ -2838,6 +2841,9 @@ packages: typescript: optional: true + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2941,6 +2947,10 @@ packages: dompurify@3.1.7: resolution: {integrity: sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} @@ -3150,6 +3160,9 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + exa-js@2.0.11: + resolution: {integrity: sha512-xMZjtZQ9dqhHCFspWhq9mQFF7H+hJw1yQE2wKS99ZuzO3JrqzqxbPK60BjNhkiMqiD/n+06O1fuLZs/HAqeaEg==} + execa@9.6.0: resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} @@ -3813,6 +3826,18 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + openai@5.23.2: + resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + openai@6.8.1: resolution: {integrity: sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==} hasBin: true @@ -7580,6 +7605,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7666,6 +7697,8 @@ snapshots: dompurify@3.1.7: {} + dotenv@16.4.7: {} + dotenv@17.2.3: {} drizzle-kit@0.31.6: @@ -7825,6 +7858,17 @@ snapshots: eventsource-parser@3.0.6: {} + exa-js@2.0.11(ws@8.18.3): + dependencies: + cross-fetch: 4.1.0 + dotenv: 16.4.7 + openai: 5.23.2(ws@8.18.3)(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + transitivePeerDependencies: + - encoding + - ws + execa@9.6.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -8409,6 +8453,11 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + openai@5.23.2(ws@8.18.3)(zod@3.25.76): + optionalDependencies: + ws: 8.18.3 + zod: 3.25.76 + openai@6.8.1(ws@8.18.3)(zod@4.1.12): optionalDependencies: ws: 8.18.3 From d0c3c18116d3a9243a1421378d8e18332083da16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Montone?= Date: Tue, 2 Dec 2025 22:09:57 -0300 Subject: [PATCH 2/2] fix --- README.md | 2 +- plugins/exa/codegen/search.ts | 31 ---------- plugins/exa/credentials.ts | 4 ++ plugins/exa/index.ts | 12 ++-- plugins/exa/steps/search.ts | 107 ++++++++++++++++++++++++++-------- plugins/exa/test.ts | 6 +- plugins/index.ts | 4 +- 7 files changed, 98 insertions(+), 68 deletions(-) delete mode 100644 plugins/exa/codegen/search.ts create mode 100644 plugins/exa/credentials.ts diff --git a/README.md b/README.md index 61cd8187..5b0bfff9 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. - **AI Gateway**: Generate Text, Generate Image -- **Exa**: Search Web - **Blob**: Put Blob, List Blobs +- **Exa**: Search Web - **fal.ai**: Generate Image, Generate Video, Upscale Image, Remove Background, Image to Image - **Firecrawl**: Scrape URL, Search Web - **GitHub**: Create Issue, List Issues, Get Issue, Update Issue diff --git a/plugins/exa/codegen/search.ts b/plugins/exa/codegen/search.ts deleted file mode 100644 index 51ee7eeb..00000000 --- a/plugins/exa/codegen/search.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Code generation template for Exa Search action - * This template is used when exporting workflows to standalone Next.js projects - * It uses environment variables instead of integrationId - */ -export const searchCodegenTemplate = `import Exa from 'exa-js'; - -export async function exaSearchStep(input: { - query: string; - numResults?: number; - type?: 'auto' | 'neural' | 'fast' | 'deep'; -}) { - "use step"; - - const exa = new Exa(process.env.EXA_API_KEY!); - - const result = await exa.search(input.query, { - numResults: input.numResults || 10, - type: input.type || 'auto', - }); - - return { - results: result.results.map((r) => ({ - url: r.url, - title: r.title, - publishedDate: r.publishedDate, - author: r.author, - text: r.text, - })), - }; -}`; diff --git a/plugins/exa/credentials.ts b/plugins/exa/credentials.ts new file mode 100644 index 00000000..f40ade58 --- /dev/null +++ b/plugins/exa/credentials.ts @@ -0,0 +1,4 @@ +export type ExaCredentials = { + EXA_API_KEY?: string; +}; + diff --git a/plugins/exa/index.ts b/plugins/exa/index.ts index 96b2abd1..84d8182d 100644 --- a/plugins/exa/index.ts +++ b/plugins/exa/index.ts @@ -1,6 +1,5 @@ import type { IntegrationPlugin } from "../registry"; import { registerIntegration } from "../registry"; -import { searchCodegenTemplate } from "./codegen/search"; import { ExaIcon } from "./icon"; const exaPlugin: IntegrationPlugin = { @@ -34,10 +33,6 @@ const exaPlugin: IntegrationPlugin = { }, }, - dependencies: { - "exa-js": "^1.5.12", - }, - actions: [ { slug: "search", @@ -47,6 +42,9 @@ const exaPlugin: IntegrationPlugin = { category: "Exa", stepFunction: "exaSearchStep", stepImportPath: "search", + outputFields: [ + { field: "results", description: "Array of search results" }, + ], configFields: [ { key: "query", @@ -71,13 +69,11 @@ const exaPlugin: IntegrationPlugin = { options: [ { value: "auto", label: "Auto" }, { value: "neural", label: "Neural" }, - { value: "fast", label: "Fast" }, - { value: "deep", label: "Deep" }, + { value: "keyword", label: "Keyword" }, ], defaultValue: "auto", }, ], - codegenTemplate: searchCodegenTemplate, }, ], }; diff --git a/plugins/exa/steps/search.ts b/plugins/exa/steps/search.ts index 5b36feba..65e9b078 100644 --- a/plugins/exa/steps/search.ts +++ b/plugins/exa/steps/search.ts @@ -1,51 +1,99 @@ import "server-only"; -import Exa from "exa-js"; import { fetchCredentials } from "@/lib/credential-fetcher"; import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; -import { getErrorMessage } from "@/lib/utils"; +import type { ExaCredentials } from "../credentials"; -type ExaSearchResult = { +const EXA_API_URL = "https://api.exa.ai"; + +type ExaSearchResponse = { results: Array<{ url: string; + id: string; title: string | null; publishedDate?: string; author?: string; text?: string; }>; + autopromptString?: string; +}; + +type ExaErrorResponse = { + error?: string; + message?: string; }; -export type ExaSearchInput = StepInput & { - integrationId?: string; +type SearchResult = + | { + success: true; + results: Array<{ + url: string; + title: string | null; + publishedDate?: string; + author?: string; + text?: string; + }>; + } + | { success: false; error: string }; + +export type SearchCoreInput = { query: string; numResults?: number; - type?: "auto" | "neural" | "fast" | "deep"; + type?: "auto" | "neural" | "keyword"; }; +export type ExaSearchInput = StepInput & + SearchCoreInput & { + integrationId?: string; + }; + /** - * Search logic using Exa SDK + * Core logic - portable between app and export */ -async function search(input: ExaSearchInput): Promise { - const credentials = input.integrationId - ? await fetchCredentials(input.integrationId) - : {}; - +async function stepHandler( + input: SearchCoreInput, + credentials: ExaCredentials +): Promise { const apiKey = credentials.EXA_API_KEY; if (!apiKey) { - throw new Error("Exa API Key is not configured."); + return { + success: false, + error: + "EXA_API_KEY is not configured. Please add it in Project Integrations.", + }; } try { - const exa = new Exa(apiKey); - - const result = await exa.search(input.query, { - numResults: input.numResults ? Number(input.numResults) : 10, - type: input.type || "auto", + const response = await fetch(`${EXA_API_URL}/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + body: JSON.stringify({ + query: input.query, + numResults: input.numResults ? Number(input.numResults) : 10, + type: input.type || "auto", + }), }); + if (!response.ok) { + const errorData = (await response.json()) as ExaErrorResponse; + return { + success: false, + error: + errorData.error || + errorData.message || + `HTTP ${response.status}: Search failed`, + }; + } + + const data = (await response.json()) as ExaSearchResponse; + return { - results: result.results.map((r) => ({ + success: true, + results: data.results.map((r) => ({ url: r.url, title: r.title, publishedDate: r.publishedDate, @@ -54,17 +102,28 @@ async function search(input: ExaSearchInput): Promise { })), }; } catch (error) { - throw new Error(`Failed to search: ${getErrorMessage(error)}`); + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + error: `Failed to search: ${message}`, + }; } } /** - * Exa Search Step - * Performs semantic web search using Exa + * App entry point - fetches credentials and wraps with logging */ export async function exaSearchStep( input: ExaSearchInput -): Promise { +): Promise { "use step"; - return withStepLogging(input, () => search(input)); + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); } + +// Export marker for codegen auto-generation +export const _integrationType = "exa"; diff --git a/plugins/exa/test.ts b/plugins/exa/test.ts index d019f32a..093c2a16 100644 --- a/plugins/exa/test.ts +++ b/plugins/exa/test.ts @@ -27,8 +27,12 @@ export async function testExa(credentials: Record) { return { success: true }; } + if (response.status === 401) { + return { success: false, error: "Invalid API key" }; + } + const error = await response.text(); - return { success: false, error: error || "Invalid API key" }; + return { success: false, error: error || `API error: HTTP ${response.status}` }; } catch (error) { return { success: false, diff --git a/plugins/index.ts b/plugins/index.ts index 5cf11a16..fce9e89e 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -12,13 +12,11 @@ * To remove an integration: * 1. Delete the plugin directory * 2. Run: pnpm discover-plugins (or it runs automatically on build) - * - * Discovered plugins: ai-gateway, exa, firecrawl, linear, resend, slack, v0 */ -import "./exa"; import "./ai-gateway"; import "./blob"; +import "./exa"; import "./fal"; import "./firecrawl"; import "./github";