exa-js
Version:
Exa SDK for Node.js and the browser
1 lines • 231 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../package.json","../src/errors.ts","../src/zod-utils.ts","../src/research/base.ts","../src/research/client.ts","../src/websets/base.ts","../src/websets/enrichments.ts","../src/websets/events.ts","../src/websets/openapi.ts","../src/websets/imports.ts","../src/websets/items.ts","../src/websets/monitors.ts","../src/websets/searches.ts","../src/websets/webhooks.ts","../src/websets/client.ts"],"sourcesContent":["import fetch, { Headers } from \"cross-fetch\";\nimport { ZodSchema } from \"zod\";\nimport packageJson from \"../package.json\";\nimport { ExaError, HttpStatusCode } from \"./errors\";\nimport { ResearchClient } from \"./research/client\";\nimport { WebsetsClient } from \"./websets/client\";\nimport { isZodSchema, zodToJsonSchema } from \"./zod-utils\";\n\n// Use native fetch in Node.js environments\nconst fetchImpl =\n typeof global !== \"undefined\" && global.fetch ? global.fetch : fetch;\nconst HeadersImpl =\n typeof global !== \"undefined\" && global.Headers ? global.Headers : Headers;\n\nconst DEFAULT_MAX_CHARACTERS = 10_000;\n\n/**\n * Options for retrieving page contents\n * @typedef {Object} ContentsOptions\n * @property {TextContentsOptions | boolean} [text] - Options for retrieving text contents.\n * @property {SummaryContentsOptions | boolean} [summary] - Options for retrieving summary.\n * @property {LivecrawlOptions} [livecrawl] - Options for livecrawling contents. Default is \"never\" for neural/auto search, \"fallback\" for keyword search.\n * @property {number} [livecrawlTimeout] - The timeout for livecrawling. Max and default is 10000ms.\n * @property {boolean} [filterEmptyResults] - If true, filters out results with no contents. Default is true.\n * @property {number} [subpages] - The number of subpages to return for each result, where each subpage is derived from an internal link for the result.\n * @property {string | string[]} [subpageTarget] - Text used to match/rank subpages in the returned subpage list. You could use \"about\" to get *about* page for websites. Note that this is a fuzzy matcher.\n * @property {ExtrasOptions} [extras] - Miscelleneous data derived from results\n */\nexport type ContentsOptions = {\n text?: TextContentsOptions | true;\n summary?: SummaryContentsOptions | true;\n livecrawl?: LivecrawlOptions;\n context?: ContextOptions | true;\n livecrawlTimeout?: number;\n filterEmptyResults?: boolean;\n subpages?: number;\n subpageTarget?: string | string[];\n extras?: ExtrasOptions;\n};\n\n/**\n * Options for performing a search query\n * @typedef {Object} SearchOptions\n * @property {ContentsOptions | boolean} [contents] - Options for retrieving page contents for each result returned. Default is { text: { maxCharacters: 10_000 } }.\n * @property {number} [numResults] - Number of search results to return. Default 10. Max 10 for basic plans.\n * @property {string[]} [includeDomains] - List of domains to include in the search.\n * @property {string[]} [excludeDomains] - List of domains to exclude in the search.\n * @property {string} [startCrawlDate] - Start date for results based on crawl date.\n * @property {string} [endCrawlDate] - End date for results based on crawl date.\n * @property {string} [startPublishedDate] - Start date for results based on published date.\n * @property {string} [endPublishedDate] - End date for results based on published date.\n * @property {string} [category] - A data category to focus on, with higher comprehensivity and data cleanliness. Currently, the only category is company.\n * @property {string[]} [includeText] - List of strings that must be present in webpage text of results. Currently only supports 1 string of up to 5 words.\n * @property {string[]} [excludeText] - List of strings that must not be present in webpage text of results. Currently only supports 1 string of up to 5 words.\n * @property {string[]} [flags] - Experimental flags\n * @property {string} [userLocation] - The two-letter ISO country code of the user, e.g. US.\n */\nexport type BaseSearchOptions = {\n contents?: ContentsOptions;\n numResults?: number;\n includeDomains?: string[];\n excludeDomains?: string[];\n startCrawlDate?: string;\n endCrawlDate?: string;\n startPublishedDate?: string;\n endPublishedDate?: string;\n category?:\n | \"company\"\n | \"research paper\"\n | \"news\"\n | \"pdf\"\n | \"github\"\n | \"tweet\"\n | \"personal site\"\n | \"linkedin profile\"\n | \"financial report\";\n includeText?: string[];\n excludeText?: string[];\n flags?: string[];\n userLocation?: string;\n};\n\n/**\n * Search options for performing a search query.\n * @typedef {Object} RegularSearchOptions\n */\nexport type RegularSearchOptions = BaseSearchOptions & {\n /**\n * If true, the search results are moderated for safety.\n */\n moderation?: boolean;\n useAutoprompt?: boolean;\n type?: \"keyword\" | \"neural\" | \"auto\" | \"hybrid\" | \"fast\" | \"deep\";\n};\n\n/**\n * Options for finding similar links.\n * @typedef {Object} FindSimilarOptions\n * @property {boolean} [excludeSourceDomain] - If true, excludes links from the base domain of the input.\n */\nexport type FindSimilarOptions = BaseSearchOptions & {\n excludeSourceDomain?: boolean;\n};\n\nexport type ExtrasOptions = { links?: number; imageLinks?: number };\n\n/**\n * Options for livecrawling contents\n * @typedef {string} LivecrawlOptions\n */\nexport type LivecrawlOptions =\n | \"never\"\n | \"fallback\"\n | \"always\"\n | \"auto\"\n | \"preferred\";\n\n/**\n * Options for retrieving text from page.\n * @typedef {Object} TextContentsOptions\n * @property {number} [maxCharacters] - The maximum number of characters to return.\n * @property {boolean} [includeHtmlTags] - If true, includes HTML tags in the returned text. Default: false\n */\nexport type TextContentsOptions = {\n maxCharacters?: number;\n includeHtmlTags?: boolean;\n};\n\n/**\n * Options for retrieving summary from page.\n * @typedef {Object} SummaryContentsOptions\n * @property {string} [query] - The query string to use for summary generation.\n * @property {JSONSchema} [schema] - JSON schema for structured output from summary.\n */\nexport type SummaryContentsOptions = {\n query?: string;\n schema?: Record<string, unknown> | ZodSchema;\n};\n\n/**\n * @deprecated Use Record<string, unknown> instead.\n */\nexport type JSONSchema = Record<string, unknown>;\n\n/**\n * Options for retrieving the context from a list of search results. The context is a string\n * representation of all the search results.\n * @typedef {Object} ContextOptions\n * @property {number} [maxCharacters] - The maximum number of characters.\n */\nexport type ContextOptions = {\n maxCharacters?: number;\n};\n\n/**\n * @typedef {Object} TextResponse\n * @property {string} text - Text from page\n */\nexport type TextResponse = { text: string };\n\n/**\n * @typedef {Object} SummaryResponse\n * @property {string} summary - The generated summary of the page content.\n */\nexport type SummaryResponse = { summary: string };\n\n/**\n * @typedef {Object} ExtrasResponse\n * @property {string[]} links - The links on the page of a result\n * @property {string[]} imageLinks - The image links on the page of a result\n */\nexport type ExtrasResponse = {\n extras: { links?: string[]; imageLinks?: string[] };\n};\n\n/**\n * @typedef {Object} SubpagesResponse\n * @property {ContentsResultComponent<T extends ContentsOptions>} subpages - The subpages for a result\n */\nexport type SubpagesResponse<T extends ContentsOptions> = {\n subpages: ContentsResultComponent<T>[];\n};\n\nexport type Default<T extends {}, U> = [keyof T] extends [never] ? U : T;\n\n/**\n * @typedef {Object} ContentsResultComponent\n * Depending on 'ContentsOptions', this yields a combination of 'TextResponse', 'SummaryResponse', or an empty object.\n *\n * @template T - A type extending from 'ContentsOptions'.\n */\nexport type ContentsResultComponent<T extends ContentsOptions> =\n (T[\"text\"] extends object | true ? TextResponse : {}) &\n (T[\"summary\"] extends object | true ? SummaryResponse : {}) &\n (T[\"subpages\"] extends number ? SubpagesResponse<T> : {}) &\n (T[\"extras\"] extends object ? ExtrasResponse : {});\n\n/**\n * Represents the cost breakdown related to contents retrieval. Fields are optional because\n * only non-zero costs are included.\n * @typedef {Object} CostDollarsContents\n * @property {number} [text] - The cost in dollars for retrieving text.\n * @property {number} [summary] - The cost in dollars for retrieving summary.\n */\nexport type CostDollarsContents = {\n text?: number;\n summary?: number;\n};\n\n/**\n * Represents the cost breakdown related to search. Fields are optional because\n * only non-zero costs are included.\n * @typedef {Object} CostDollarsSeearch\n * @property {number} [neural] - The cost in dollars for neural search.\n * @property {number} [keyword] - The cost in dollars for keyword search.\n */\nexport type CostDollarsSeearch = {\n neural?: number;\n keyword?: number;\n};\n\n/**\n * Represents the total cost breakdown. Only non-zero costs are included.\n * @typedef {Object} CostDollars\n * @property {number} total - The total cost in dollars.\n * @property {CostDollarsSeearch} [search] - The cost breakdown for search.\n * @property {CostDollarsContents} [contents] - The cost breakdown for contents.\n */\nexport type CostDollars = {\n total: number;\n search?: CostDollarsSeearch;\n contents?: CostDollarsContents;\n};\n\n/**\n * Represents a search result object.\n * @typedef {Object} SearchResult\n * @property {string} title - The title of the search result.\n * @property {string} url - The URL of the search result.\n * @property {string} [publishedDate] - The estimated creation date of the content.\n * @property {string} [author] - The author of the content, if available.\n * @property {number} [score] - Similarity score between the query/url and the result.\n * @property {string} id - The temporary ID for the document.\n * @property {string} [image] - A representative image for the content, if any.\n * @property {string} [favicon] - A favicon for the site, if any.\n */\nexport type SearchResult<T extends ContentsOptions> = {\n title: string | null;\n url: string;\n publishedDate?: string;\n author?: string;\n score?: number;\n id: string;\n image?: string;\n favicon?: string;\n} & ContentsResultComponent<T>;\n\n/**\n * Represents a search response object.\n * @typedef {Object} SearchResponse\n * @property {Result[]} results - The list of search results.\n * @property {string} [context] - The context for the search.\n * @property {string} [autoDate] - The autoprompt date, if applicable.\n * @property {string} requestId - The request ID for the search.\n * @property {CostDollars} [costDollars] - The cost breakdown for this request.\n */\nexport type SearchResponse<T extends ContentsOptions> = {\n results: SearchResult<T>[];\n context?: string;\n autoDate?: string;\n requestId: string;\n statuses?: Array<Status>;\n costDollars?: CostDollars;\n};\n\nexport type Status = {\n id: string;\n status: string;\n source: string;\n};\n\n/**\n * Options for the answer endpoint\n * @typedef {Object} AnswerOptions\n * @property {boolean} [stream] - Whether to stream the response. Default false.\n * @property {boolean} [text] - Whether to include text in the source results. Default false.\n * @property {\"exa\"} [model] - The model to use for generating the answer. Default \"exa\".\n * @property {string} [systemPrompt] - A system prompt to guide the LLM's behavior when generating the answer.\n * @property {Object} [outputSchema] - A JSON Schema specification for the structure you expect the output to take\n */\nexport type AnswerOptions = {\n stream?: boolean;\n text?: boolean;\n model?: \"exa\";\n systemPrompt?: string;\n outputSchema?: Record<string, unknown>;\n userLocation?: string;\n};\n\n/**\n * Represents an answer response object from the /answer endpoint.\n * @typedef {Object} AnswerResponse\n * @property {string | Object} answer - The generated answer text (or an object matching `outputSchema`, if provided)\n * @property {SearchResult<{}>[]} citations - The sources used to generate the answer.\n * @property {CostDollars} [costDollars] - The cost breakdown for this request.\n * @property {string} [requestId] - Optional request ID for the answer.\n */\nexport type AnswerResponse = {\n answer: string | Record<string, unknown>;\n citations: SearchResult<{}>[];\n requestId?: string;\n costDollars?: CostDollars;\n};\n\nexport type AnswerStreamChunk = {\n /**\n * The partial text content of the answer (if present in this chunk).\n */\n content?: string;\n /**\n * Citations associated with the current chunk of text (if present).\n */\n citations?: Array<{\n id: string;\n url: string;\n title?: string;\n publishedDate?: string;\n author?: string;\n text?: string;\n }>;\n};\n\n/**\n * Represents a streaming answer response chunk from the /answer endpoint.\n * @typedef {Object} AnswerStreamResponse\n * @property {string} [answer] - A chunk of the generated answer text.\n * @property {SearchResult<{}>[]]} [citations] - The sources used to generate the answer.\n */\nexport type AnswerStreamResponse = {\n answer?: string;\n citations?: SearchResult<{}>[];\n};\n\n// ==========================================\n// Zod-Enhanced Types\n// ==========================================\n\n/**\n * Enhanced answer options that accepts either JSON schema or Zod schema\n */\nexport type AnswerOptionsTyped<T> = Omit<AnswerOptions, \"outputSchema\"> & {\n outputSchema: T;\n};\n\n/**\n * Enhanced answer response with strongly typed answer when using Zod\n */\nexport type AnswerResponseTyped<T> = Omit<AnswerResponse, \"answer\"> & {\n answer: T;\n};\n\n/**\n * Enhanced summary contents options that accepts either JSON schema or Zod schema\n */\nexport type SummaryContentsOptionsTyped<T> = Omit<\n SummaryContentsOptions,\n \"schema\"\n> & {\n schema: T;\n};\n\n/**\n * The Exa class encapsulates the API's endpoints.\n */\nexport class Exa {\n private baseURL: string;\n private headers: Headers;\n\n /**\n * Websets API client\n */\n websets: WebsetsClient;\n\n /**\n * Research API client\n */\n research: ResearchClient;\n\n /**\n * Helper method to separate out the contents-specific options from the rest.\n */\n private extractContentsOptions<T extends ContentsOptions>(\n options: T\n ): {\n contentsOptions: ContentsOptions;\n restOptions: Omit<T, keyof ContentsOptions>;\n } {\n const {\n text,\n summary,\n subpages,\n subpageTarget,\n extras,\n livecrawl,\n livecrawlTimeout,\n context,\n ...rest\n } = options;\n\n const contentsOptions: ContentsOptions = {};\n\n // Default: if none of text or summary is provided, we retrieve text\n if (text === undefined && summary === undefined && extras === undefined) {\n contentsOptions.text = true;\n }\n\n if (text !== undefined) contentsOptions.text = text;\n if (summary !== undefined) {\n // Handle zod schema conversion for summary\n if (\n typeof summary === \"object\" &&\n summary !== null &&\n \"schema\" in summary &&\n summary.schema &&\n isZodSchema(summary.schema)\n ) {\n contentsOptions.summary = {\n ...summary,\n schema: zodToJsonSchema(summary.schema),\n };\n } else {\n contentsOptions.summary = summary;\n }\n }\n if (subpages !== undefined) contentsOptions.subpages = subpages;\n if (subpageTarget !== undefined)\n contentsOptions.subpageTarget = subpageTarget;\n if (extras !== undefined) contentsOptions.extras = extras;\n if (livecrawl !== undefined) contentsOptions.livecrawl = livecrawl;\n if (livecrawlTimeout !== undefined)\n contentsOptions.livecrawlTimeout = livecrawlTimeout;\n if (context !== undefined) contentsOptions.context = context;\n\n return {\n contentsOptions,\n restOptions: rest as Omit<T, keyof ContentsOptions>,\n };\n }\n\n /**\n * Constructs the Exa API client.\n * @param {string} apiKey - The API key for authentication.\n * @param {string} [baseURL] - The base URL of the Exa API.\n */\n constructor(apiKey?: string, baseURL: string = \"https://api.exa.ai\") {\n this.baseURL = baseURL;\n if (!apiKey) {\n apiKey = process.env.EXA_API_KEY;\n if (!apiKey) {\n throw new ExaError(\n \"API key must be provided as an argument or as an environment variable (EXA_API_KEY)\",\n HttpStatusCode.Unauthorized\n );\n }\n }\n this.headers = new HeadersImpl({\n \"x-api-key\": apiKey,\n \"Content-Type\": \"application/json\",\n \"User-Agent\": `exa-node ${packageJson.version}`,\n });\n\n // Initialize the Websets client\n this.websets = new WebsetsClient(this);\n // Initialize the Research client\n this.research = new ResearchClient(this);\n }\n\n /**\n * Makes a request to the Exa API.\n * @param {string} endpoint - The API endpoint to call.\n * @param {string} method - The HTTP method to use.\n * @param {any} [body] - The request body for POST requests.\n * @param {Record<string, any>} [params] - The query parameters.\n * @returns {Promise<any>} The response from the API.\n * @throws {ExaError} When any API request fails with structured error information\n */\n async request<T = unknown>(\n endpoint: string,\n method: string,\n body?: any,\n params?: Record<string, any>,\n headers?: Record<string, string>\n ): Promise<T> {\n // Build URL with query parameters if provided\n let url = this.baseURL + endpoint;\n if (params && Object.keys(params).length > 0) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n searchParams.append(key, item);\n }\n } else if (value !== undefined) {\n searchParams.append(key, String(value));\n }\n }\n url += `?${searchParams.toString()}`;\n }\n\n let combinedHeaders: Record<string, string> = {};\n\n if (this.headers instanceof HeadersImpl) {\n this.headers.forEach((value, key) => {\n combinedHeaders[key] = value;\n });\n } else {\n combinedHeaders = { ...(this.headers as Record<string, string>) };\n }\n\n if (headers) {\n combinedHeaders = { ...combinedHeaders, ...headers };\n }\n\n const response = await fetchImpl(url, {\n method,\n headers: combinedHeaders,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n const errorData = await response.json();\n\n if (!errorData.statusCode) {\n errorData.statusCode = response.status;\n }\n if (!errorData.timestamp) {\n errorData.timestamp = new Date().toISOString();\n }\n if (!errorData.path) {\n errorData.path = endpoint;\n }\n\n // For other APIs, throw a simple ExaError with just message and status\n let message = errorData.error || \"Unknown error\";\n if (errorData.message) {\n message += (message.length > 0 ? \". \" : \"\") + errorData.message;\n }\n throw new ExaError(\n message,\n response.status,\n errorData.timestamp,\n errorData.path\n );\n }\n\n // If the server responded with an SSE stream, parse it and return the final payload.\n const contentType = response.headers.get(\"content-type\") || \"\";\n if (contentType.includes(\"text/event-stream\")) {\n return (await this.parseSSEStream<T>(response)) as T;\n }\n\n return (await response.json()) as T;\n }\n\n async rawRequest(\n endpoint: string,\n method: string = \"POST\",\n body?: Record<string, unknown>,\n queryParams?: Record<\n string,\n string | number | boolean | string[] | undefined\n >\n ): Promise<Response> {\n let url = this.baseURL + endpoint;\n\n if (queryParams) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(queryParams)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n searchParams.append(key, String(item));\n }\n } else if (value !== undefined) {\n searchParams.append(key, String(value));\n }\n }\n url += `?${searchParams.toString()}`;\n }\n\n const response = await fetchImpl(url, {\n method,\n headers: this.headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n return response;\n }\n\n /**\n * Performs a search with an Exa prompt-engineered query.\n * By default, returns text contents. Use contents: false to opt-out.\n *\n * @param {string} query - The query string.\n * @returns {Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>} A list of relevant search results with text contents.\n */\n async search(\n query: string\n ): Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>;\n /**\n * Performs a search without contents.\n *\n * @param {string} query - The query string.\n * @param {RegularSearchOptions & { contents: false }} options - Search options with contents explicitly disabled\n * @returns {Promise<SearchResponse<{}>>} A list of relevant search results without contents.\n */\n async search(\n query: string,\n options: RegularSearchOptions & { contents: false | null | undefined }\n ): Promise<SearchResponse<{}>>;\n /**\n * Performs a search with specific contents.\n *\n * @param {string} query - The query string.\n * @param {RegularSearchOptions & { contents: T }} options - Search options with specific contents\n * @returns {Promise<SearchResponse<T>>} A list of relevant search results with requested contents.\n */\n async search<T extends ContentsOptions>(\n query: string,\n options: RegularSearchOptions & { contents: T }\n ): Promise<SearchResponse<T>>;\n /**\n * Performs a search with an Exa prompt-engineered query.\n * When no contents option is specified, returns text contents by default.\n *\n * @param {string} query - The query string.\n * @param {Omit<RegularSearchOptions, 'contents'>} options - Search options without contents\n * @returns {Promise<SearchResponse<{ text: true }>>} A list of relevant search results with text contents.\n */\n async search(\n query: string,\n options: Omit<RegularSearchOptions, \"contents\">\n ): Promise<SearchResponse<{ text: true }>>;\n async search<T extends ContentsOptions>(\n query: string,\n options?: RegularSearchOptions & { contents?: T | false | null | undefined }\n ): Promise<SearchResponse<T | { text: true } | {}>> {\n if (options === undefined || !(\"contents\" in options)) {\n return await this.request(\"/search\", \"POST\", {\n query,\n ...options,\n contents: { text: { maxCharacters: DEFAULT_MAX_CHARACTERS } },\n });\n }\n\n // If contents is false, null, or undefined, don't send it to the API\n if (\n options.contents === false ||\n options.contents === null ||\n options.contents === undefined\n ) {\n const { contents, ...restOptions } = options;\n return await this.request(\"/search\", \"POST\", { query, ...restOptions });\n }\n\n return await this.request(\"/search\", \"POST\", { query, ...options });\n }\n\n /**\n * @deprecated Use `search()` instead. The search method now returns text contents by default.\n *\n * Migration examples:\n * - `searchAndContents(query)` → `search(query)`\n * - `searchAndContents(query, { text: true })` → `search(query, { contents: { text: true } })`\n * - `searchAndContents(query, { summary: true })` → `search(query, { contents: { summary: true } })`\n *\n * Performs a search with an Exa prompt-engineered query and returns the contents of the documents.\n *\n * @param {string} query - The query string.\n * @param {RegularSearchOptions & T} [options] - Additional search + contents options\n * @returns {Promise<SearchResponse<T>>} A list of relevant search results with requested contents.\n */\n async searchAndContents<T extends ContentsOptions>(\n query: string,\n options?: RegularSearchOptions & T\n ): Promise<SearchResponse<T>> {\n const { contentsOptions, restOptions } =\n options === undefined\n ? {\n contentsOptions: {\n text: { maxCharacters: DEFAULT_MAX_CHARACTERS },\n },\n restOptions: {},\n }\n : this.extractContentsOptions(options);\n\n return await this.request(\"/search\", \"POST\", {\n query,\n contents: contentsOptions,\n ...restOptions,\n });\n }\n\n /**\n * Finds similar links to the provided URL.\n * By default, returns text contents. Use contents: false to opt-out.\n *\n * @param {string} url - The URL for which to find similar links.\n * @returns {Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>} A list of similar search results with text contents.\n */\n async findSimilar(\n url: string\n ): Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>;\n /**\n * Finds similar links to the provided URL without contents.\n *\n * @param {string} url - The URL for which to find similar links.\n * @param {FindSimilarOptions & { contents: false }} options - Options with contents explicitly disabled\n * @returns {Promise<SearchResponse<{}>>} A list of similar search results without contents.\n */\n async findSimilar(\n url: string,\n options: FindSimilarOptions & { contents: false | null | undefined }\n ): Promise<SearchResponse<{}>>;\n /**\n * Finds similar links to the provided URL with specific contents.\n *\n * @param {string} url - The URL for which to find similar links.\n * @param {FindSimilarOptions & { contents: T }} options - Options with specific contents\n * @returns {Promise<SearchResponse<T>>} A list of similar search results with requested contents.\n */\n async findSimilar<T extends ContentsOptions>(\n url: string,\n options: FindSimilarOptions & { contents: T }\n ): Promise<SearchResponse<T>>;\n /**\n * Finds similar links to the provided URL.\n * When no contents option is specified, returns text contents by default.\n *\n * @param {string} url - The URL for which to find similar links.\n * @param {Omit<FindSimilarOptions, 'contents'>} options - Options without contents\n * @returns {Promise<SearchResponse<{ text: true }>>} A list of similar search results with text contents.\n */\n async findSimilar(\n url: string,\n options: Omit<FindSimilarOptions, \"contents\">\n ): Promise<SearchResponse<{ text: true }>>;\n async findSimilar<T extends ContentsOptions>(\n url: string,\n options?: FindSimilarOptions & { contents?: T | false | null | undefined }\n ): Promise<SearchResponse<T | { text: { maxCharacters: 10_000 } } | {}>> {\n if (options === undefined || !(\"contents\" in options)) {\n // No options or no contents property → default to text contents\n return await this.request(\"/findSimilar\", \"POST\", {\n url,\n ...options,\n contents: { text: { maxCharacters: DEFAULT_MAX_CHARACTERS } },\n });\n }\n\n // If contents is false, null, or undefined, don't send it to the API\n if (\n options.contents === false ||\n options.contents === null ||\n options.contents === undefined\n ) {\n const { contents, ...restOptions } = options;\n return await this.request(\"/findSimilar\", \"POST\", {\n url,\n ...restOptions,\n });\n }\n\n // Contents property exists with value - pass it through\n return await this.request(\"/findSimilar\", \"POST\", { url, ...options });\n }\n\n /**\n * @deprecated Use `findSimilar()` instead. The findSimilar method now returns text contents by default.\n *\n * Migration examples:\n * - `findSimilarAndContents(url)` → `findSimilar(url)`\n * - `findSimilarAndContents(url, { text: true })` → `findSimilar(url, { contents: { text: true } })`\n * - `findSimilarAndContents(url, { summary: true })` → `findSimilar(url, { contents: { summary: true } })`\n *\n * Finds similar links to the provided URL and returns the contents of the documents.\n * @param {string} url - The URL for which to find similar links.\n * @param {FindSimilarOptions & T} [options] - Additional options for finding similar links + contents.\n * @returns {Promise<SearchResponse<T>>} A list of similar search results, including requested contents.\n */\n async findSimilarAndContents<T extends ContentsOptions>(\n url: string,\n options?: FindSimilarOptions & T\n ): Promise<SearchResponse<T>> {\n const { contentsOptions, restOptions } =\n options === undefined\n ? {\n contentsOptions: {\n text: { maxCharacters: DEFAULT_MAX_CHARACTERS },\n },\n restOptions: {},\n }\n : this.extractContentsOptions(options);\n\n return await this.request(\"/findSimilar\", \"POST\", {\n url,\n contents: contentsOptions,\n ...restOptions,\n });\n }\n\n /**\n * Retrieves contents of documents based on URLs.\n * @param {string | string[] | SearchResult[]} urls - A URL or array of URLs, or an array of SearchResult objects.\n * @param {ContentsOptions} [options] - Additional options for retrieving document contents.\n * @returns {Promise<SearchResponse<T>>} A list of document contents for the requested URLs.\n */\n async getContents<T extends ContentsOptions>(\n urls: string | string[] | SearchResult<T>[],\n options?: T\n ): Promise<SearchResponse<T>> {\n if (!urls || (Array.isArray(urls) && urls.length === 0)) {\n throw new ExaError(\n \"Must provide at least one URL\",\n HttpStatusCode.BadRequest\n );\n }\n\n let requestUrls: string[];\n\n if (typeof urls === \"string\") {\n requestUrls = [urls];\n } else if (typeof urls[0] === \"string\") {\n requestUrls = urls as string[];\n } else {\n requestUrls = (urls as SearchResult<T>[]).map((result) => result.url);\n }\n\n const payload = {\n urls: requestUrls,\n ...options,\n };\n\n return await this.request(\"/contents\", \"POST\", payload);\n }\n\n /**\n * Generate an answer with Zod schema for strongly typed output\n */\n async answer<T>(\n query: string,\n options: AnswerOptionsTyped<ZodSchema<T>>\n ): Promise<AnswerResponseTyped<T>>;\n\n /**\n * Generate an answer to a query.\n * @param {string} query - The question or query to answer.\n * @param {AnswerOptions} [options] - Additional options for answer generation.\n * @returns {Promise<AnswerResponse>} The generated answer and source references.\n *\n * Example with systemPrompt:\n * ```ts\n * const answer = await exa.answer(\"What is quantum computing?\", {\n * text: true,\n * model: \"exa\",\n * systemPrompt: \"Answer in a technical manner suitable for experts.\"\n * });\n * ```\n *\n * Note: For streaming responses, use the `streamAnswer` method:\n * ```ts\n * for await (const chunk of exa.streamAnswer(query)) {\n * // Handle chunks\n * }\n * ```\n */\n async answer(query: string, options?: AnswerOptions): Promise<AnswerResponse>;\n\n async answer<T>(\n query: string,\n options?: AnswerOptions | AnswerOptionsTyped<ZodSchema<T>>\n ): Promise<AnswerResponse | AnswerResponseTyped<T>> {\n if (options?.stream) {\n throw new ExaError(\n \"For streaming responses, please use streamAnswer() instead:\\n\\n\" +\n \"for await (const chunk of exa.streamAnswer(query)) {\\n\" +\n \" // Handle chunks\\n\" +\n \"}\",\n HttpStatusCode.BadRequest\n );\n }\n\n // For non-streaming requests, make a regular API call\n let outputSchema = options?.outputSchema;\n\n // Convert Zod schema to JSON schema if needed\n if (outputSchema && isZodSchema(outputSchema)) {\n outputSchema = zodToJsonSchema(outputSchema);\n }\n\n const requestBody = {\n query,\n stream: false,\n text: options?.text ?? false,\n model: options?.model ?? \"exa\",\n systemPrompt: options?.systemPrompt,\n outputSchema,\n userLocation: options?.userLocation,\n };\n\n return await this.request(\"/answer\", \"POST\", requestBody);\n }\n\n /**\n * Stream an answer with Zod schema for structured output (non-streaming content)\n * Note: Structured output works only with non-streaming content, not with streaming chunks\n */\n streamAnswer<T>(\n query: string,\n options: {\n text?: boolean;\n model?: \"exa\" | \"exa-pro\";\n systemPrompt?: string;\n outputSchema: ZodSchema<T>;\n }\n ): AsyncGenerator<AnswerStreamChunk>;\n\n /**\n * Stream an answer as an async generator\n *\n * Each iteration yields a chunk with partial text (`content`) or new citations.\n * Use this if you'd like to read the answer incrementally, e.g. in a chat UI.\n *\n * Example usage:\n * ```ts\n * for await (const chunk of exa.streamAnswer(\"What is quantum computing?\", {\n * text: false,\n * systemPrompt: \"Answer in a concise manner suitable for beginners.\"\n * })) {\n * if (chunk.content) process.stdout.write(chunk.content);\n * if (chunk.citations) {\n * console.log(\"\\nCitations: \", chunk.citations);\n * }\n * }\n * ```\n */\n streamAnswer(\n query: string,\n options?: {\n text?: boolean;\n model?: \"exa\" | \"exa-pro\";\n systemPrompt?: string;\n outputSchema?: Record<string, unknown>;\n }\n ): AsyncGenerator<AnswerStreamChunk>;\n\n async *streamAnswer<T>(\n query: string,\n options?: {\n text?: boolean;\n model?: \"exa\" | \"exa-pro\";\n systemPrompt?: string;\n outputSchema?: Record<string, unknown> | ZodSchema<T>;\n }\n ): AsyncGenerator<AnswerStreamChunk> {\n // Convert Zod schema to JSON schema if needed\n let outputSchema = options?.outputSchema;\n if (outputSchema && isZodSchema(outputSchema)) {\n outputSchema = zodToJsonSchema(outputSchema);\n }\n\n // Build the POST body and fetch the streaming response.\n const body = {\n query,\n text: options?.text ?? false,\n stream: true,\n model: options?.model ?? \"exa\",\n systemPrompt: options?.systemPrompt,\n outputSchema,\n };\n\n const response = await fetchImpl(this.baseURL + \"/answer\", {\n method: \"POST\",\n headers: this.headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const message = await response.text();\n throw new ExaError(message, response.status, new Date().toISOString());\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new ExaError(\n \"No response body available for streaming.\",\n 500,\n new Date().toISOString()\n );\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\";\n\n for (const line of lines) {\n if (!line.startsWith(\"data: \")) continue;\n\n const jsonStr = line.replace(/^data:\\s*/, \"\").trim();\n if (!jsonStr || jsonStr === \"[DONE]\") {\n continue;\n }\n\n let chunkData: any;\n try {\n chunkData = JSON.parse(jsonStr);\n } catch (err) {\n continue;\n }\n\n const chunk = this.processChunk(chunkData);\n if (chunk.content || chunk.citations) {\n yield chunk;\n }\n }\n }\n\n if (buffer.startsWith(\"data: \")) {\n const leftover = buffer.replace(/^data:\\s*/, \"\").trim();\n if (leftover && leftover !== \"[DONE]\") {\n try {\n const chunkData = JSON.parse(leftover);\n const chunk = this.processChunk(chunkData);\n if (chunk.content || chunk.citations) {\n yield chunk;\n }\n } catch (e) {}\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n private processChunk(chunkData: any): AnswerStreamChunk {\n let content: string | undefined;\n let citations:\n | Array<{\n id: string;\n url: string;\n title?: string;\n publishedDate?: string;\n author?: string;\n text?: string;\n }>\n | undefined;\n\n if (\n chunkData.choices &&\n chunkData.choices[0] &&\n chunkData.choices[0].delta\n ) {\n content = chunkData.choices[0].delta.content;\n }\n\n if (chunkData.citations && chunkData.citations !== \"null\") {\n citations = chunkData.citations.map((c: any) => ({\n id: c.id,\n url: c.url,\n title: c.title,\n publishedDate: c.publishedDate,\n author: c.author,\n text: c.text,\n }));\n }\n\n return { content, citations };\n }\n\n private async parseSSEStream<T>(response: Response): Promise<T> {\n const reader = response.body?.getReader();\n if (!reader) {\n throw new ExaError(\n \"No response body available for streaming.\",\n 500,\n new Date().toISOString()\n );\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n return new Promise<T>(async (resolve, reject) => {\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\";\n\n for (const line of lines) {\n if (!line.startsWith(\"data: \")) continue;\n\n const jsonStr = line.replace(/^data:\\s*/, \"\").trim();\n if (!jsonStr || jsonStr === \"[DONE]\") {\n continue;\n }\n\n let chunk: any;\n try {\n chunk = JSON.parse(jsonStr);\n } catch {\n continue; // Ignore malformed JSON lines\n }\n\n switch (chunk.tag) {\n case \"complete\":\n reader.releaseLock();\n resolve(chunk.data as T);\n return;\n case \"error\": {\n const message = chunk.error?.message || \"Unknown error\";\n reader.releaseLock();\n reject(\n new ExaError(\n message,\n HttpStatusCode.InternalServerError,\n new Date().toISOString()\n )\n );\n return;\n }\n // 'progress' and any other tags are ignored for the blocking variant\n default:\n break;\n }\n }\n }\n\n // If we exit the loop without receiving a completion event\n reject(\n new ExaError(\n \"Stream ended without a completion event.\",\n HttpStatusCode.InternalServerError,\n new Date().toISOString()\n )\n );\n } catch (err) {\n reject(err as Error);\n } finally {\n try {\n reader.releaseLock();\n } catch {\n /* ignore */\n }\n }\n });\n }\n}\n\n// Re-export Websets related types and enums\nexport * from \"./websets\";\n// Re-export Research related types and client\nexport * from \"./research\";\n\n// Export the main class\nexport default Exa;\n\n// Re-export errors\nexport * from \"./errors\";\n","{\n \"name\": \"exa-js\",\n \"version\": \"2.0.0\",\n \"description\": \"Exa SDK for Node.js and the browser\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\"\n ],\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.mjs\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"require\": \"./dist/index.js\",\n \"module\": \"./dist/index.mjs\",\n \"import\": \"./dist/index.mjs\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"types\": \"./dist/index.d.ts\",\n \"scripts\": {\n \"build-fast\": \"tsup src/index.ts --format cjs,esm\",\n \"build\": \"tsup\",\n \"test\": \"vitest run\",\n \"test:unit\": \"vitest run --config vitest.unit.config.ts\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"typecheck\": \"tsc --noEmit\",\n \"typecheck:examples\": \"tsc --noEmit examples/**/*.ts\",\n \"generate:types:websets\": \"openapi-typescript https://raw.githubusercontent.com/exa-labs/openapi-spec/refs/heads/master/exa-websets-spec.yaml --enum --root-types --alphabetize --root-types-no-schema-prefix --output ./src/websets/openapi.ts && npm run format:websets\",\n \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"examples/**/*.ts\\\"\",\n \"format:websets\": \"prettier --write src/websets/openapi.ts\",\n \"build:beta\": \"cross-env NPM_CONFIG_TAG=beta npm run build\",\n \"version:beta\": \"npm version prerelease --preid=beta\",\n \"version:stable\": \"npm version patch\",\n \"publish:beta\": \"npm run version:beta && npm run build:beta && npm publish --tag beta\",\n \"publish:stable\": \"npm run version:stable && npm run build && npm publish\",\n \"prepublishOnly\": \"npm run build\"\n },\n \"license\": \"MIT\",\n \"devDependencies\": {\n \"@types/node\": \"~22.14.0\",\n \"cross-env\": \"~7.0.3\",\n \"openapi-typescript\": \"~7.6.1\",\n \"prettier\": \"~3.5.3\",\n \"ts-node\": \"~10.9.2\",\n \"tsup\": \"~8.4.0\",\n \"typescript\": \"~5.8.3\",\n \"vitest\": \"~3.1.1\"\n },\n \"dependencies\": {\n \"cross-fetch\": \"~4.1.0\",\n \"dotenv\": \"~16.4.7\",\n \"openai\": \"^5.0.1\",\n \"zod\": \"^3.22.0\",\n \"zod-to-json-schema\": \"^3.20.0\"\n },\n \"directories\": {\n \"test\": \"test\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/exa-labs/exa-js.git\"\n },\n \"keywords\": [\n \"exa\",\n \"metaphor\",\n \"search\",\n \"AI\",\n \"LLMs\",\n \"RAG\",\n \"retrieval\",\n \"augmented\",\n \"generation\"\n ],\n \"author\": \"jeffzwang\",\n \"bugs\": {\n \"url\": \"https://github.com/exa-labs/exa-js/issues\"\n },\n \"homepage\": \"https://github.com/exa-labs/exa-js#readme\"\n}\n","/**\n * HTTP status codes\n */\nexport enum HttpStatusCode {\n BadRequest = 400,\n NotFound = 404,\n Unauthorized = 401,\n Forbidden = 403,\n TooManyRequests = 429,\n RequestTimeout = 408,\n InternalServerError = 500,\n ServiceUnavailable = 503,\n}\n\n/**\n * Base error class for all Exa API errors\n */\nexport class ExaError extends Error {\n /**\n * HTTP status code\n */\n statusCode: number;\n\n /**\n * ISO timestamp from API\n */\n timestamp?: string;\n\n /**\n * Path that caused the error (may be undefined for client-side errors)\n */\n path?: string;\n\n /**\n * Create a new ExaError\n * @param message Error message\n * @param statusCode HTTP status code\n * @param timestamp ISO timestamp from API\n * @param path Path that caused the error\n */\n constructor(\n message: string,\n statusCode: number,\n timestamp?: string,\n path?: string\n ) {\n super(message);\n this.name = \"ExaError\";\n this.statusCode = statusCode;\n this.timestamp = timestamp ?? new Date().toISOString();\n this.path = path;\n }\n}\n","import { ZodType, ZodSchema } from \"zod\";\nimport { zodToJsonSchema as convertZodToJsonSchema } from \"zod-to-json-schema\";\n\nexport function isZodSchema(obj: any): obj is ZodSchema<any> {\n return obj instanceof ZodType;\n}\n\nexport function zodToJsonSchema(\n schema: ZodSchema<any>\n): Record<string, unknown> {\n return convertZodToJsonSchema(schema, {\n $refStrategy: \"none\",\n }) as Record<string, unknown>;\n}\n","import { Exa } from \"../index\";\nimport { ListResearchRequest } from \"./index\";\n\ntype QueryParams = Record<\n string,\n string | number | boolean | string[] | undefined\n>;\n\ninterface RequestBody {\n [key: string]: unknown;\n}\n\nexport class ResearchBaseClient {\n protected client: Exa;\n\n constructor(client: Exa) {\n this.client = client;\n }\n\n protected async request<T = unknown>(\n endpoint: string,\n method: string = \"POST\",\n data?: RequestBody,\n params?: QueryParams\n ): Promise<T> {\n return this.client.request<T>(\n `/research/v1${endpoint}`,\n method,\n data,\n params\n );\n }\n\n protected async rawRequest(\n endpoint: string,\n method: string = \"POST\",\n data?: RequestBody,\n params?: QueryParams\n ): Promise<Response> {\n return this.client.rawRequest(\n `/research/v1${endpoint}`,\n method,\n data,\n params\n );\n }\n\n protected buildPaginationParams(\n pagination?: ListResearchRequest\n ): QueryParams {\n const params: QueryParams = {};\n if (!pagination) return params;\n\n if (pagination.cursor) params.cursor = pagination.cursor;\n if (pagination.limit) params.limit = pagination.limit;\n\n return params;\n }\n}\n","import { Exa } from \"../index\";\nimport {\n ListResearchRequest,\n ListResearchResponse,\n Research,\n ResearchCreateRequest,\n ResearchCreateResponse,\n ResearchStreamEvent,\n ResearchCreateParamsTyped,\n ResearchTyped,\n} from \"./index\";\nimport { ZodSchema } from \"zod\";\nimport { isZodSchema, zodToJsonSchema } from \"../zod-utils\";\nimport { ResearchBaseClient } from \"./base\";\n\nexport class ResearchClient extends ResearchBaseClient {\n constructor(client: Exa) {\n super(client);\n }\n\n async create<T>(\n params: ResearchCreateParamsTyped<ZodSchema<T>>\n ): Promise<ResearchCreateResponse>;\n\n async create(params: {\n instructions: string;\n model?: ResearchCreateRequest[\"model\"];\n outputSchema?: Record<string, unknown>;\n }): Promise<ResearchCreateResponse>;\n\n async create<T>(params: {\n instructions: string;\n model?: ResearchCreateRequest[\"model\"];\n outputSchema?: Record<string, unknown> | ZodSchema<T>;\n }): Promise<ResearchCreateResponse> {\n const { instructions, model, outputSchema } = params;\n\n let schema = outputSchema;\n if (schema && isZodSchema(schema)) {\n schema = zodToJsonSchema(schema);\n }\n\n const payload: ResearchCreateRequest = {\n instructions,\n model: model ?? \"exa-research-fast\",\n };\n\n if (schema) {\n payload.outputSchema = schema as Record<string, unknown>;\n }\n\n return this.request<ResearchCreateResponse>(\"\", \"POST\", payload);\n }\n\n get(researchId: string): Promise<Research>;\n get(\n researchId: string,\n options: { stream?: false; events?: boolean }\n ): Promise<Research>;\n get<T>(\n researchId: string,\n options: { stream?: false; events?: boolean; outputSchema: ZodSchema<T> }\n ): Promise<ResearchTyped<T>>;\n get(\n researchId: string,\n options: { stream: true; events?: boolean }\n ): Promise<AsyncGenerator<ResearchStreamEvent, any, any>>;\n get<T>(\n researchId: string,\n options: { stream: true; events?: boolean; outputSchema?: ZodSchema<T> }\n ): Promise<AsyncGenerator<ResearchStreamEvent, any, any>>;\n get<T = unknown>(\n researchId: string,\n options?: {\n stream?: boolean;\n events?: boolean;\n outputSchema?: ZodSchema<T>;\n }\n ):\n | Promise<Research | ResearchTyped<T>>\n | Promise<AsyncGenerator<ResearchStreamEvent>> {\n if (options?.stream) {\n const promise = async () => {\n const params: Record<string, string> = { stream: \"true\" };\n if (options.events !== undefined) {\n params.events = options.events.toString();\n }\n const resp = await this.rawRequest(\n `/${researchId}`,\n \"GET\",\n undefined,\n params\n );\n if (!resp.body) {\n throw new Error(\"No response body for SSE stream\");\n }\n const reader = resp.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n function processPart(part: string): ResearchStreamEvent | null {\n const lines = part.split(\"\\n\");\n let data = lines.slice(1).join(\"\\n\");\n if (data.startsWith(\"data:\")) {\n data = data.slice(5).trimStart();\n }\n try {\n return JSON.parse(data);\n } catch (e) {\n return null;\n }\n }\n\n async function* streamEvents() {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n let parts = buffer.split(\"\\n\\n\");\n buffer = parts.pop() ?? \"\";\n\n for (const part of parts) {\n const processed = processPart(part);\n if (processed) {\n yield processed;\n }\n }\n }\n if (buffer.trim()) {\n const processed = processPart(buffer.t