{"version":3,"file":"context.mjs","names":[],"sources":["../../src/providers/context.ts"],"sourcesContent":["import type {\n  ProviderConfig,\n  ReadOptions,\n  ReadResult,\n  SearchRequestOptions,\n  SearchResult,\n} from \"../core/types.ts\";\nimport { Client } from \"../core/client.ts\";\nimport { Provider } from \"../core/provider.ts\";\nimport {\n  AuthError,\n  HTTPError,\n  PaymentError,\n  normalizeError,\n  type WebError,\n} from \"../core/errors.ts\";\n\ninterface ContextSearchResult {\n  readonly url: string;\n  readonly title: string;\n  readonly description: string;\n  readonly relevance: \"high\" | \"medium\" | \"low\";\n  readonly markdown: {\n    readonly markdown: string | null;\n    readonly code: string;\n  };\n}\n\ninterface ContextSearchResponse {\n  readonly results: readonly ContextSearchResult[];\n}\n\ninterface ContextPageMetadata {\n  readonly sourceUrl: string;\n  readonly finalUrl: string;\n  readonly title?: string;\n  readonly description?: string;\n  readonly image?: string;\n  readonly publishedTime?: string;\n  readonly [key: string]: unknown;\n}\n\ninterface ContextScrapeResponse {\n  readonly success: true;\n  readonly markdown: string;\n  readonly html?: string;\n  readonly url: string;\n  readonly metadata: ContextPageMetadata;\n  readonly cache_metadata: Readonly<Record<string, unknown>>;\n  readonly key_metadata?: Readonly<Record<string, unknown>>;\n}\n\nconst CONTEXT_MIN_SEARCH_RESULTS = 10;\nconst CONTEXT_MAX_SEARCH_RESULTS = 100;\nconst CONTEXT_MAX_TIMEOUT_MS = 300_000;\nconst CONTEXT_READ_CLIENT_TIMEOUT_MS = 310_000;\n\nexport class ContextProvider extends Provider {\n  static readonly providerName = \"context\";\n  static readonly defaultBaseURL = \"https://api.context.dev/v1\";\n\n  private readonly apiKey: string;\n  private readonly readClient: Client;\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config, ContextProvider);\n    if (!config.apiKey) {\n      throw new AuthError(\n        \"Missing API key for Context.dev. Set CONTEXT_DEV_API_KEY\",\n        ContextProvider.providerName,\n      );\n    }\n\n    this.apiKey = config.apiKey;\n    this.readClient = new Client({ maxRetries: 1, timeout: CONTEXT_READ_CLIENT_TIMEOUT_MS });\n  }\n\n  async search(query: string, options?: SearchRequestOptions): Promise<SearchResult[]> {\n    try {\n      const response = await this.client.postJSON<ContextSearchResponse>(\n        `${this.baseURL}/web/search`,\n        searchBody(query, options),\n        this.authHeaders(),\n        options?.signal,\n      );\n      return response.results.slice(0, resultLimit(options?.maxResults)).map(mapSearchResult);\n    } catch (error) {\n      throw normalizeContextError(error);\n    }\n  }\n\n  async read(url: string, options?: Readonly<ReadOptions>): Promise<ReadResult> {\n    try {\n      const response = await this.readClient.getJSON<ContextScrapeResponse>(\n        `${this.baseURL}/web/scrape/markdown?${scrapeParams(url, options)}`,\n        this.authHeaders(),\n        options?.signal,\n      );\n      return mapReadResult(response, options?.format);\n    } catch (error) {\n      throw normalizeContextError(error);\n    }\n  }\n\n  private authHeaders(): Record<string, string> {\n    return { Authorization: `Bearer ${this.apiKey}` };\n  }\n}\n\n/**\n * Context reports depleted credits as HTTP 401, not a rejected key.\n * @param error - Rejected request.\n * @returns {WebError} Payment or normalized provider error.\n */\nfunction normalizeContextError(error: unknown): WebError {\n  if (error instanceof HTTPError && error.statusCode === 401 && isUsageExceeded(error.body)) {\n    return new PaymentError(error.statusCode, error.url, error.body);\n  }\n  return normalizeError(error, ContextProvider.providerName);\n}\n\nfunction isUsageExceeded(body: string): boolean {\n  try {\n    const data: unknown = JSON.parse(body);\n    return (\n      typeof data === \"object\" &&\n      data !== null &&\n      \"error_code\" in data &&\n      data.error_code === \"USAGE_EXCEEDED\"\n    );\n  } catch {\n    return false;\n  }\n}\n\nfunction searchBody(query: string, options?: SearchRequestOptions): Record<string, unknown> {\n  return {\n    query,\n    numResults: requestedResultCount(options?.maxResults),\n    ...(options?.includeDomains?.length ? { includeDomains: options.includeDomains } : {}),\n    ...(options?.excludeDomains?.length ? { excludeDomains: options.excludeDomains } : {}),\n  };\n}\n\nfunction requestedResultCount(maxResults?: number): number {\n  return Math.min(\n    Math.max(resultLimit(maxResults), CONTEXT_MIN_SEARCH_RESULTS),\n    CONTEXT_MAX_SEARCH_RESULTS,\n  );\n}\n\nfunction resultLimit(maxResults?: number): number {\n  return Math.max(Math.trunc(maxResults ?? CONTEXT_MIN_SEARCH_RESULTS), 1);\n}\n\nfunction mapSearchResult(result: Readonly<ContextSearchResult>): SearchResult {\n  return {\n    url: result.url,\n    title: result.title,\n    snippet: result.description,\n    ...(result.markdown.markdown === null ? {} : { text: result.markdown.markdown }),\n    metadata: {\n      relevance: result.relevance,\n      markdownCode: result.markdown.code,\n    },\n  };\n}\n\nfunction scrapeParams(url: string, options?: Readonly<ReadOptions>): string {\n  return new URLSearchParams({\n    url,\n    ...formatParams(options?.format),\n    ...selectorParams(options),\n    ...cacheParams(options?.noCache),\n    ...timeoutParams(options?.timeout),\n  }).toString();\n}\n\nfunction formatParams(format?: ReadOptions[\"format\"]): Record<string, string> {\n  return format === \"html\" ? { includeHTML: \"true\" } : {};\n}\n\nfunction selectorParams(options?: Readonly<ReadOptions>): Record<string, string> {\n  return {\n    ...(options?.targetSelector ? { includeSelectors: options.targetSelector } : {}),\n    ...(options?.removeSelector ? { excludeSelectors: options.removeSelector } : {}),\n  };\n}\n\nfunction cacheParams(noCache?: boolean): Record<string, string> {\n  return noCache ? { maxAgeMs: \"0\" } : {};\n}\n\nfunction timeoutParams(timeout?: number): Record<string, string> {\n  if (timeout === undefined) return {};\n  const timeoutMs = Math.min(Math.max(Math.round(timeout * 1000), 1), CONTEXT_MAX_TIMEOUT_MS);\n  return { timeoutMS: String(timeoutMs) };\n}\n\nfunction mapReadResult(\n  response: Readonly<ContextScrapeResponse>,\n  format?: ReadOptions[\"format\"],\n): ReadResult {\n  const html = response.html;\n  return {\n    url: response.metadata.finalUrl || response.url,\n    title: response.metadata.title,\n    description: response.metadata.description,\n    content: format === \"html\" && html !== undefined ? html : response.markdown,\n    ...(html === undefined ? {} : { html }),\n    publishedDate: response.metadata.publishedTime,\n    image: response.metadata.image,\n    metadata: {\n      ...response.metadata,\n      cacheMetadata: response.cache_metadata,\n      ...(response.key_metadata === undefined ? {} : { keyMetadata: response.key_metadata }),\n    },\n  };\n}\n"],"mappings":"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"}