scrapex 1.0.0-beta.1 → 1.0.0-beta.2

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 (52) hide show
  1. package/dist/embeddings/index.cjs +52 -0
  2. package/dist/embeddings/index.d.cts +3 -0
  3. package/dist/embeddings/index.d.mts +3 -0
  4. package/dist/embeddings/index.mjs +4 -0
  5. package/dist/embeddings-BjNTQSG9.cjs +1455 -0
  6. package/dist/embeddings-BjNTQSG9.cjs.map +1 -0
  7. package/dist/embeddings-Bsymy_jA.mjs +1215 -0
  8. package/dist/embeddings-Bsymy_jA.mjs.map +1 -0
  9. package/dist/enhancer-Cs_WyWtJ.cjs +219 -0
  10. package/dist/enhancer-Cs_WyWtJ.cjs.map +1 -0
  11. package/dist/enhancer-INx5NlgO.mjs +177 -0
  12. package/dist/enhancer-INx5NlgO.mjs.map +1 -0
  13. package/dist/{enhancer-j0xqKDJm.cjs → http-base-CHLf-Tco.cjs} +36 -199
  14. package/dist/http-base-CHLf-Tco.cjs.map +1 -0
  15. package/dist/{enhancer-ByjRD-t5.mjs → http-base-DM7YNo6X.mjs} +25 -176
  16. package/dist/http-base-DM7YNo6X.mjs.map +1 -0
  17. package/dist/{index-CDgcRnig.d.cts → index-Bvseqli-.d.cts} +1 -1
  18. package/dist/{index-CDgcRnig.d.cts.map → index-Bvseqli-.d.cts.map} +1 -1
  19. package/dist/{index-piS5wtki.d.mts → index-CIFjNySr.d.mts} +1 -1
  20. package/dist/{index-piS5wtki.d.mts.map → index-CIFjNySr.d.mts.map} +1 -1
  21. package/dist/index-D6qfjmZQ.d.mts +401 -0
  22. package/dist/index-D6qfjmZQ.d.mts.map +1 -0
  23. package/dist/index-RFSpP5g8.d.cts +401 -0
  24. package/dist/index-RFSpP5g8.d.cts.map +1 -0
  25. package/dist/index.cjs +39 -1074
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +3 -260
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.mts +3 -260
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +4 -1039
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/llm/index.cjs +7 -6
  34. package/dist/llm/index.cjs.map +1 -1
  35. package/dist/llm/index.d.cts +1 -1
  36. package/dist/llm/index.d.mts +1 -1
  37. package/dist/llm/index.mjs +2 -1
  38. package/dist/llm/index.mjs.map +1 -1
  39. package/dist/parsers/index.d.cts +1 -1
  40. package/dist/parsers/index.d.mts +1 -1
  41. package/dist/parsers/index.mjs +1 -1
  42. package/dist/{parsers-CwkYnyWY.mjs → parsers-DsawHeo0.mjs} +1 -1
  43. package/dist/{parsers-CwkYnyWY.mjs.map → parsers-DsawHeo0.mjs.map} +1 -1
  44. package/dist/{types-CadAXrme.d.mts → types-BOcHQU9s.d.mts} +308 -151
  45. package/dist/types-BOcHQU9s.d.mts.map +1 -0
  46. package/dist/{types-DPEtPihB.d.cts → types-DutdBpqd.d.cts} +308 -151
  47. package/dist/types-DutdBpqd.d.cts.map +1 -0
  48. package/package.json +1 -1
  49. package/dist/enhancer-ByjRD-t5.mjs.map +0 -1
  50. package/dist/enhancer-j0xqKDJm.cjs.map +0 -1
  51. package/dist/types-CadAXrme.d.mts.map +0 -1
  52. package/dist/types-DPEtPihB.d.cts.map +0 -1
@@ -1,5 +1,6 @@
1
1
  const require_parsers = require('../parsers-Bneuws8x.cjs');
2
- const require_enhancer = require('../enhancer-j0xqKDJm.cjs');
2
+ const require_http_base = require('../http-base-CHLf-Tco.cjs');
3
+ const require_enhancer = require('../enhancer-Cs_WyWtJ.cjs');
3
4
  let zod = require("zod");
4
5
 
5
6
  //#region src/llm/http.ts
@@ -11,7 +12,7 @@ let zod = require("zod");
11
12
  * HTTP-based LLM provider.
12
13
  * Works with any REST API using native fetch.
13
14
  */
14
- var HttpLLMProvider = class extends require_enhancer.BaseHttpProvider {
15
+ var HttpLLMProvider = class extends require_http_base.BaseHttpProvider {
15
16
  name;
16
17
  requestBuilder;
17
18
  responseMapper;
@@ -48,7 +49,7 @@ var HttpLLMProvider = class extends require_enhancer.BaseHttpProvider {
48
49
  const textBlock = resp.content.find((c) => c.type === "text");
49
50
  if (textBlock?.text) return textBlock.text;
50
51
  }
51
- throw new require_enhancer.ScrapeError("Unable to parse LLM response. Provide a custom responseMapper.", "VALIDATION_ERROR");
52
+ throw new require_http_base.ScrapeError("Unable to parse LLM response. Provide a custom responseMapper.", "VALIDATION_ERROR");
52
53
  });
53
54
  }
54
55
  /**
@@ -62,7 +63,7 @@ var HttpLLMProvider = class extends require_enhancer.BaseHttpProvider {
62
63
  };
63
64
  const { data } = await this.fetch(this.baseUrl, { body });
64
65
  const content = this.responseMapper(data);
65
- if (!content) throw new require_enhancer.ScrapeError("Empty response from LLM", "LLM_ERROR");
66
+ if (!content) throw new require_http_base.ScrapeError("Empty response from LLM", "LLM_ERROR");
66
67
  return content;
67
68
  }
68
69
  /**
@@ -86,7 +87,7 @@ Do not include any explanation or markdown formatting. Just the JSON object.`;
86
87
  };
87
88
  const { data } = await this.fetch(this.baseUrl, { body });
88
89
  const content = this.responseMapper(data);
89
- if (!content) throw new require_enhancer.ScrapeError("Empty response from LLM", "LLM_ERROR");
90
+ if (!content) throw new require_http_base.ScrapeError("Empty response from LLM", "LLM_ERROR");
90
91
  try {
91
92
  const trimmed = content.trim();
92
93
  try {
@@ -97,7 +98,7 @@ Do not include any explanation or markdown formatting. Just the JSON object.`;
97
98
  const parsed = JSON.parse(jsonMatch[0]);
98
99
  return schema.parse(parsed);
99
100
  } catch (error) {
100
- throw new require_enhancer.ScrapeError(`Failed to parse LLM response as JSON: ${error instanceof Error ? error.message : String(error)}`, "VALIDATION_ERROR", void 0, error instanceof Error ? error : void 0);
101
+ throw new require_http_base.ScrapeError(`Failed to parse LLM response as JSON: ${error instanceof Error ? error.message : String(error)}`, "VALIDATION_ERROR", void 0, error instanceof Error ? error : void 0);
101
102
  }
102
103
  }
103
104
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["BaseHttpProvider","messages: Array<{ role: string; content: string }>","request: Record<string, unknown>","ScrapeError","z","properties: Record<string, object>","required: string[]","headers: Record<string, string>"],"sources":["../../src/llm/http.ts","../../src/llm/presets.ts"],"sourcesContent":["/**\n * HTTP-based LLM Provider using native fetch.\n * Provides a unified interface for any REST-based LLM API.\n */\n\nimport { z } from 'zod';\nimport { type BaseHttpConfig, BaseHttpProvider } from '../common/http-base.js';\nimport { ScrapeError } from '../core/errors.js';\nimport type { CompletionOptions, LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP LLM provider configuration.\n */\nexport interface HttpLLMConfig<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpConfig<TError> {\n /**\n * Build request body from prompt and options.\n * @default OpenAI-compatible format with messages array\n */\n requestBuilder?: (prompt: string, options: CompletionOptions) => TRequest;\n /**\n * Extract completion text from response.\n * @default (res) => res.choices[0].message.content\n */\n responseMapper?: (response: TResponse) => string;\n /**\n * Enable JSON mode - adds response_format to request.\n * For OpenAI-compatible APIs, this adds { response_format: { type: \"json_object\" } }\n */\n jsonMode?: boolean;\n}\n\n// ─────────────────────────────────────────────────────────────\n// HTTP LLM Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP-based LLM provider.\n * Works with any REST API using native fetch.\n */\nexport class HttpLLMProvider<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpProvider<TError>\n implements LLMProvider\n{\n readonly name: string;\n\n private readonly requestBuilder: (prompt: string, options: CompletionOptions) => TRequest;\n private readonly responseMapper: (response: TResponse) => string;\n private readonly jsonMode: boolean;\n\n constructor(config: HttpLLMConfig<TRequest, TResponse, TError>) {\n super(config);\n this.name = 'http-llm';\n this.jsonMode = config.jsonMode ?? false;\n\n // Default request builder: OpenAI-compatible format\n this.requestBuilder =\n config.requestBuilder ??\n ((prompt: string, opts: CompletionOptions) => {\n const messages: Array<{ role: string; content: string }> = [];\n\n if (opts.systemPrompt) {\n messages.push({ role: 'system', content: opts.systemPrompt });\n }\n messages.push({ role: 'user', content: prompt });\n\n const request: Record<string, unknown> = {\n model: this.model,\n messages,\n };\n\n if (opts.temperature !== undefined) {\n request.temperature = opts.temperature;\n }\n if (opts.maxTokens !== undefined) {\n request.max_tokens = opts.maxTokens;\n }\n\n return request as TRequest;\n });\n\n // Default response mapper: OpenAI-compatible format\n this.responseMapper =\n config.responseMapper ??\n ((response: TResponse) => {\n const resp = response as Record<string, unknown>;\n\n // OpenAI format: { choices: [{ message: { content: \"...\" } }] }\n if (Array.isArray(resp.choices) && resp.choices.length > 0) {\n const choice = resp.choices[0] as { message?: { content?: string } };\n if (choice.message?.content) {\n return choice.message.content;\n }\n }\n\n // Anthropic format: { content: [{ type: \"text\", text: \"...\" }] }\n if (Array.isArray(resp.content)) {\n const textBlock = resp.content.find((c: { type?: string }) => c.type === 'text') as\n | { text?: string }\n | undefined;\n if (textBlock?.text) {\n return textBlock.text;\n }\n }\n\n throw new ScrapeError(\n 'Unable to parse LLM response. Provide a custom responseMapper.',\n 'VALIDATION_ERROR'\n );\n });\n }\n\n /**\n * Generate a text completion.\n */\n async complete(prompt: string, options: CompletionOptions = {}): Promise<string> {\n let body = this.requestBuilder(prompt, options);\n\n // Add JSON mode if enabled\n if (this.jsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n\n const content = this.responseMapper(data);\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n return content;\n }\n\n /**\n * Generate a structured JSON completion with Zod validation.\n */\n async completeJSON<T>(\n prompt: string,\n schema: z.ZodType<T>,\n options: CompletionOptions = {}\n ): Promise<T> {\n // Build a prompt that requests JSON output\n const jsonPrompt = `${prompt}\n\nRespond ONLY with valid JSON matching this schema:\n${JSON.stringify(zodToJsonSchema(schema), null, 2)}\n\nDo not include any explanation or markdown formatting. Just the JSON object.`;\n\n // Use JSON mode if available\n const useJsonMode = this.jsonMode;\n let body = this.requestBuilder(jsonPrompt, {\n ...options,\n systemPrompt:\n options.systemPrompt ?? 'You are a helpful assistant that responds only with valid JSON.',\n });\n\n if (useJsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n const content = this.responseMapper(data);\n\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n try {\n const trimmed = content.trim();\n try {\n return schema.parse(JSON.parse(trimmed));\n } catch {\n // Fall back to extracting JSON from markdown or surrounding text\n }\n\n const jsonMatch = content.match(/[[{][\\s\\S]*[\\]}]/);\n if (!jsonMatch) {\n throw new Error('No JSON found in response');\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n return schema.parse(parsed);\n } catch (error) {\n throw new ScrapeError(\n `Failed to parse LLM response as JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'VALIDATION_ERROR',\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Utilities\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a Zod schema to a JSON Schema representation.\n * Uses Zod's built-in toJSONSchema method (Zod 4+).\n * Used for prompting LLMs to return structured data.\n */\nexport function zodToJsonSchema(schema: z.ZodType<unknown>): object {\n // Zod 4+ has built-in static toJSONSchema method\n if (typeof z.toJSONSchema === 'function') {\n const jsonSchema = z.toJSONSchema(schema);\n // Remove $schema key as it's not needed for LLM prompting\n const { $schema, ...rest } = jsonSchema as { $schema?: string; [key: string]: unknown };\n return rest;\n }\n\n // Fallback for older Zod versions using _def.type\n const def = (schema as z.ZodType<unknown> & { _def: { type: string } })._def;\n const type = def.type;\n\n switch (type) {\n case 'object': {\n const shape = (schema as z.ZodObject<z.ZodRawShape>).shape;\n const properties: Record<string, object> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = zodToJsonSchema(value as z.ZodType<unknown>);\n const valueDef = (value as z.ZodType<unknown> & { _def: { type: string } })._def;\n if (valueDef.type !== 'optional') {\n required.push(key);\n }\n }\n return { type: 'object', properties, required };\n }\n case 'array': {\n const arrayDef = def as unknown as { element: z.ZodType<unknown> };\n return { type: 'array', items: zodToJsonSchema(arrayDef.element) };\n }\n case 'string':\n return { type: 'string' };\n case 'number':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'enum': {\n const enumDef = def as unknown as { entries: Record<string, string> };\n return { type: 'string', enum: Object.values(enumDef.entries) };\n }\n case 'optional': {\n const optionalDef = def as unknown as { innerType: z.ZodType<unknown> };\n return zodToJsonSchema(optionalDef.innerType);\n }\n default:\n return { type: 'string' };\n }\n}\n\n// Re-export types for convenience\nexport type { ZodType } from 'zod';\n","/**\n * Preset factory functions for common LLM providers.\n * All presets use the HttpLLMProvider with appropriate configuration.\n */\n\nimport { type HttpLLMConfig, HttpLLMProvider } from './http.js';\nimport type { LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// OpenAI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenAI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createOpenAI({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createOpenAI(options?: {\n apiKey?: string;\n model?: string;\n baseUrl?: string;\n}): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: options?.baseUrl ?? 'https://api.openai.com/v1/chat/completions',\n model: options?.model ?? 'gpt-4o-mini',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Anthropic\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Anthropic API response shape.\n */\ninterface AnthropicResponse {\n content: Array<{ type: string; text?: string }>;\n}\n\n/**\n * Create an Anthropic Claude LLM provider.\n *\n * @example\n * ```ts\n * const provider = createAnthropic({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createAnthropic(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Anthropic API key required. Set ANTHROPIC_API_KEY env var or pass apiKey option.'\n );\n }\n\n const model = options?.model ?? 'claude-3-5-haiku-20241022';\n\n return new HttpLLMProvider<unknown, AnthropicResponse>({\n baseUrl: 'https://api.anthropic.com/v1/messages',\n model,\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n },\n requestBuilder: (prompt, opts) => ({\n model,\n max_tokens: opts.maxTokens ?? 1024,\n messages: [{ role: 'user', content: prompt }],\n ...(opts.systemPrompt && { system: opts.systemPrompt }),\n ...(opts.temperature !== undefined && { temperature: opts.temperature }),\n }),\n responseMapper: (res) => res.content.find((item) => item.type === 'text')?.text ?? '',\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Groq\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Groq LLM provider.\n * Groq provides fast inference for open-source models.\n *\n * @example\n * ```ts\n * const provider = createGroq({ model: 'llama-3.1-70b-versatile' });\n * ```\n */\nexport function createGroq(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.GROQ_API_KEY;\n if (!apiKey) {\n throw new Error('Groq API key required. Set GROQ_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.groq.com/openai/v1/chat/completions',\n model: options?.model ?? 'llama-3.1-70b-versatile',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Ollama (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an Ollama LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createOllama({ model: 'llama3.2' });\n * ```\n */\nexport function createOllama(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:11434/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// LM Studio (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an LM Studio LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createLMStudio({ model: 'local-model' });\n * ```\n */\nexport function createLMStudio(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:1234/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Together AI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Together AI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createTogether({ model: 'meta-llama/Llama-3.2-3B-Instruct-Turbo' });\n * ```\n */\nexport function createTogether(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.TOGETHER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Together API key required. Set TOGETHER_API_KEY env var or pass apiKey option.'\n );\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.together.xyz/v1/chat/completions',\n model: options?.model ?? 'meta-llama/Llama-3.2-3B-Instruct-Turbo',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// OpenRouter\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenRouter LLM provider.\n * OpenRouter provides access to many models through a unified API.\n *\n * @example\n * ```ts\n * const provider = createOpenRouter({\n * model: 'anthropic/claude-3.5-sonnet',\n * });\n * ```\n */\nexport function createOpenRouter(options: {\n apiKey?: string;\n model: string;\n siteUrl?: string;\n siteName?: string;\n}): LLMProvider {\n const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OpenRouter API key required. Set OPENROUTER_API_KEY env var or pass apiKey option.'\n );\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n };\n\n if (options.siteUrl) {\n headers['HTTP-Referer'] = options.siteUrl;\n }\n if (options.siteName) {\n headers['X-Title'] = options.siteName;\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://openrouter.ai/api/v1/chat/completions',\n model: options.model,\n headers,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Generic HTTP Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a generic HTTP LLM provider.\n * Use this for any OpenAI-compatible API.\n *\n * @example\n * ```ts\n * const provider = createHttpLLM({\n * baseUrl: 'https://my-api.com/v1/chat/completions',\n * model: 'my-model',\n * headers: { Authorization: 'Bearer ...' },\n * });\n * ```\n */\nexport function createHttpLLM<TRequest = unknown, TResponse = unknown, TError = unknown>(\n config: HttpLLMConfig<TRequest, TResponse, TError>\n): LLMProvider {\n return new HttpLLMProvider(config);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["BaseHttpProvider","messages: Array<{ role: string; content: string }>","request: Record<string, unknown>","ScrapeError","z","properties: Record<string, object>","required: string[]","headers: Record<string, string>"],"sources":["../../src/llm/http.ts","../../src/llm/presets.ts"],"sourcesContent":["/**\n * HTTP-based LLM Provider using native fetch.\n * Provides a unified interface for any REST-based LLM API.\n */\n\nimport { z } from 'zod';\nimport { type BaseHttpConfig, BaseHttpProvider } from '../common/http-base.js';\nimport { ScrapeError } from '../core/errors.js';\nimport type { CompletionOptions, LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP LLM provider configuration.\n */\nexport interface HttpLLMConfig<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpConfig<TError> {\n /**\n * Build request body from prompt and options.\n * @default OpenAI-compatible format with messages array\n */\n requestBuilder?: (prompt: string, options: CompletionOptions) => TRequest;\n /**\n * Extract completion text from response.\n * @default (res) => res.choices[0].message.content\n */\n responseMapper?: (response: TResponse) => string;\n /**\n * Enable JSON mode - adds response_format to request.\n * For OpenAI-compatible APIs, this adds { response_format: { type: \"json_object\" } }\n */\n jsonMode?: boolean;\n}\n\n// ─────────────────────────────────────────────────────────────\n// HTTP LLM Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP-based LLM provider.\n * Works with any REST API using native fetch.\n */\nexport class HttpLLMProvider<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpProvider<TError>\n implements LLMProvider\n{\n readonly name: string;\n\n private readonly requestBuilder: (prompt: string, options: CompletionOptions) => TRequest;\n private readonly responseMapper: (response: TResponse) => string;\n private readonly jsonMode: boolean;\n\n constructor(config: HttpLLMConfig<TRequest, TResponse, TError>) {\n super(config);\n this.name = 'http-llm';\n this.jsonMode = config.jsonMode ?? false;\n\n // Default request builder: OpenAI-compatible format\n this.requestBuilder =\n config.requestBuilder ??\n ((prompt: string, opts: CompletionOptions) => {\n const messages: Array<{ role: string; content: string }> = [];\n\n if (opts.systemPrompt) {\n messages.push({ role: 'system', content: opts.systemPrompt });\n }\n messages.push({ role: 'user', content: prompt });\n\n const request: Record<string, unknown> = {\n model: this.model,\n messages,\n };\n\n if (opts.temperature !== undefined) {\n request.temperature = opts.temperature;\n }\n if (opts.maxTokens !== undefined) {\n request.max_tokens = opts.maxTokens;\n }\n\n return request as TRequest;\n });\n\n // Default response mapper: OpenAI-compatible format\n this.responseMapper =\n config.responseMapper ??\n ((response: TResponse) => {\n const resp = response as Record<string, unknown>;\n\n // OpenAI format: { choices: [{ message: { content: \"...\" } }] }\n if (Array.isArray(resp.choices) && resp.choices.length > 0) {\n const choice = resp.choices[0] as { message?: { content?: string } };\n if (choice.message?.content) {\n return choice.message.content;\n }\n }\n\n // Anthropic format: { content: [{ type: \"text\", text: \"...\" }] }\n if (Array.isArray(resp.content)) {\n const textBlock = resp.content.find((c: { type?: string }) => c.type === 'text') as\n | { text?: string }\n | undefined;\n if (textBlock?.text) {\n return textBlock.text;\n }\n }\n\n throw new ScrapeError(\n 'Unable to parse LLM response. Provide a custom responseMapper.',\n 'VALIDATION_ERROR'\n );\n });\n }\n\n /**\n * Generate a text completion.\n */\n async complete(prompt: string, options: CompletionOptions = {}): Promise<string> {\n let body = this.requestBuilder(prompt, options);\n\n // Add JSON mode if enabled\n if (this.jsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n\n const content = this.responseMapper(data);\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n return content;\n }\n\n /**\n * Generate a structured JSON completion with Zod validation.\n */\n async completeJSON<T>(\n prompt: string,\n schema: z.ZodType<T>,\n options: CompletionOptions = {}\n ): Promise<T> {\n // Build a prompt that requests JSON output\n const jsonPrompt = `${prompt}\n\nRespond ONLY with valid JSON matching this schema:\n${JSON.stringify(zodToJsonSchema(schema), null, 2)}\n\nDo not include any explanation or markdown formatting. Just the JSON object.`;\n\n // Use JSON mode if available\n const useJsonMode = this.jsonMode;\n let body = this.requestBuilder(jsonPrompt, {\n ...options,\n systemPrompt:\n options.systemPrompt ?? 'You are a helpful assistant that responds only with valid JSON.',\n });\n\n if (useJsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n const content = this.responseMapper(data);\n\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n try {\n const trimmed = content.trim();\n try {\n return schema.parse(JSON.parse(trimmed));\n } catch {\n // Fall back to extracting JSON from markdown or surrounding text\n }\n\n const jsonMatch = content.match(/[[{][\\s\\S]*[\\]}]/);\n if (!jsonMatch) {\n throw new Error('No JSON found in response');\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n return schema.parse(parsed);\n } catch (error) {\n throw new ScrapeError(\n `Failed to parse LLM response as JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'VALIDATION_ERROR',\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Utilities\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a Zod schema to a JSON Schema representation.\n * Uses Zod's built-in toJSONSchema method (Zod 4+).\n * Used for prompting LLMs to return structured data.\n */\nexport function zodToJsonSchema(schema: z.ZodType<unknown>): object {\n // Zod 4+ has built-in static toJSONSchema method\n if (typeof z.toJSONSchema === 'function') {\n const jsonSchema = z.toJSONSchema(schema);\n // Remove $schema key as it's not needed for LLM prompting\n const { $schema, ...rest } = jsonSchema as { $schema?: string; [key: string]: unknown };\n return rest;\n }\n\n // Fallback for older Zod versions using _def.type\n const def = (schema as z.ZodType<unknown> & { _def: { type: string } })._def;\n const type = def.type;\n\n switch (type) {\n case 'object': {\n const shape = (schema as z.ZodObject<z.ZodRawShape>).shape;\n const properties: Record<string, object> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = zodToJsonSchema(value as z.ZodType<unknown>);\n const valueDef = (value as z.ZodType<unknown> & { _def: { type: string } })._def;\n if (valueDef.type !== 'optional') {\n required.push(key);\n }\n }\n return { type: 'object', properties, required };\n }\n case 'array': {\n const arrayDef = def as unknown as { element: z.ZodType<unknown> };\n return { type: 'array', items: zodToJsonSchema(arrayDef.element) };\n }\n case 'string':\n return { type: 'string' };\n case 'number':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'enum': {\n const enumDef = def as unknown as { entries: Record<string, string> };\n return { type: 'string', enum: Object.values(enumDef.entries) };\n }\n case 'optional': {\n const optionalDef = def as unknown as { innerType: z.ZodType<unknown> };\n return zodToJsonSchema(optionalDef.innerType);\n }\n default:\n return { type: 'string' };\n }\n}\n\n// Re-export types for convenience\nexport type { ZodType } from 'zod';\n","/**\n * Preset factory functions for common LLM providers.\n * All presets use the HttpLLMProvider with appropriate configuration.\n */\n\nimport { type HttpLLMConfig, HttpLLMProvider } from './http.js';\nimport type { LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// OpenAI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenAI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createOpenAI({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createOpenAI(options?: {\n apiKey?: string;\n model?: string;\n baseUrl?: string;\n}): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: options?.baseUrl ?? 'https://api.openai.com/v1/chat/completions',\n model: options?.model ?? 'gpt-4o-mini',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Anthropic\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Anthropic API response shape.\n */\ninterface AnthropicResponse {\n content: Array<{ type: string; text?: string }>;\n}\n\n/**\n * Create an Anthropic Claude LLM provider.\n *\n * @example\n * ```ts\n * const provider = createAnthropic({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createAnthropic(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Anthropic API key required. Set ANTHROPIC_API_KEY env var or pass apiKey option.'\n );\n }\n\n const model = options?.model ?? 'claude-3-5-haiku-20241022';\n\n return new HttpLLMProvider<unknown, AnthropicResponse>({\n baseUrl: 'https://api.anthropic.com/v1/messages',\n model,\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n },\n requestBuilder: (prompt, opts) => ({\n model,\n max_tokens: opts.maxTokens ?? 1024,\n messages: [{ role: 'user', content: prompt }],\n ...(opts.systemPrompt && { system: opts.systemPrompt }),\n ...(opts.temperature !== undefined && { temperature: opts.temperature }),\n }),\n responseMapper: (res) => res.content.find((item) => item.type === 'text')?.text ?? '',\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Groq\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Groq LLM provider.\n * Groq provides fast inference for open-source models.\n *\n * @example\n * ```ts\n * const provider = createGroq({ model: 'llama-3.1-70b-versatile' });\n * ```\n */\nexport function createGroq(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.GROQ_API_KEY;\n if (!apiKey) {\n throw new Error('Groq API key required. Set GROQ_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.groq.com/openai/v1/chat/completions',\n model: options?.model ?? 'llama-3.1-70b-versatile',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Ollama (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an Ollama LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createOllama({ model: 'llama3.2' });\n * ```\n */\nexport function createOllama(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:11434/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// LM Studio (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an LM Studio LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createLMStudio({ model: 'local-model' });\n * ```\n */\nexport function createLMStudio(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:1234/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Together AI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Together AI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createTogether({ model: 'meta-llama/Llama-3.2-3B-Instruct-Turbo' });\n * ```\n */\nexport function createTogether(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.TOGETHER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Together API key required. Set TOGETHER_API_KEY env var or pass apiKey option.'\n );\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.together.xyz/v1/chat/completions',\n model: options?.model ?? 'meta-llama/Llama-3.2-3B-Instruct-Turbo',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// OpenRouter\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenRouter LLM provider.\n * OpenRouter provides access to many models through a unified API.\n *\n * @example\n * ```ts\n * const provider = createOpenRouter({\n * model: 'anthropic/claude-3.5-sonnet',\n * });\n * ```\n */\nexport function createOpenRouter(options: {\n apiKey?: string;\n model: string;\n siteUrl?: string;\n siteName?: string;\n}): LLMProvider {\n const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OpenRouter API key required. Set OPENROUTER_API_KEY env var or pass apiKey option.'\n );\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n };\n\n if (options.siteUrl) {\n headers['HTTP-Referer'] = options.siteUrl;\n }\n if (options.siteName) {\n headers['X-Title'] = options.siteName;\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://openrouter.ai/api/v1/chat/completions',\n model: options.model,\n headers,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Generic HTTP Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a generic HTTP LLM provider.\n * Use this for any OpenAI-compatible API.\n *\n * @example\n * ```ts\n * const provider = createHttpLLM({\n * baseUrl: 'https://my-api.com/v1/chat/completions',\n * model: 'my-model',\n * headers: { Authorization: 'Bearer ...' },\n * });\n * ```\n */\nexport function createHttpLLM<TRequest = unknown, TResponse = unknown, TError = unknown>(\n config: HttpLLMConfig<TRequest, TResponse, TError>\n): LLMProvider {\n return new HttpLLMProvider(config);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { B as BaseHttpProvider, h as ScrapedData, r as EnhancementType, s as ExtractionSchema, z as BaseHttpConfig } from "../types-DPEtPihB.cjs";
1
+ import { G as EnhancementType, M as BaseHttpConfig, N as BaseHttpProvider, Y as ExtractionSchema, rt as ScrapedData } from "../types-DutdBpqd.cjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/llm/types.d.ts
@@ -1,4 +1,4 @@
1
- import { B as BaseHttpProvider, h as ScrapedData, r as EnhancementType, s as ExtractionSchema, z as BaseHttpConfig } from "../types-CadAXrme.mjs";
1
+ import { G as EnhancementType, M as BaseHttpConfig, N as BaseHttpProvider, Y as ExtractionSchema, rt as ScrapedData } from "../types-BOcHQU9s.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/llm/types.d.ts
@@ -1,4 +1,5 @@
1
- import { a as EntitiesSchema, c as BaseHttpProvider, i as ClassifySchema, n as enhance, o as SummarySchema, p as ScrapeError, r as extract, s as TagsSchema, t as ask } from "../enhancer-ByjRD-t5.mjs";
1
+ import { d as ScrapeError, t as BaseHttpProvider } from "../http-base-DM7YNo6X.mjs";
2
+ import { a as EntitiesSchema, i as ClassifySchema, n as enhance, o as SummarySchema, r as extract, s as TagsSchema, t as ask } from "../enhancer-INx5NlgO.mjs";
2
3
  import { z } from "zod";
3
4
 
4
5
  //#region src/llm/http.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["messages: Array<{ role: string; content: string }>","request: Record<string, unknown>","properties: Record<string, object>","required: string[]","headers: Record<string, string>"],"sources":["../../src/llm/http.ts","../../src/llm/presets.ts"],"sourcesContent":["/**\n * HTTP-based LLM Provider using native fetch.\n * Provides a unified interface for any REST-based LLM API.\n */\n\nimport { z } from 'zod';\nimport { type BaseHttpConfig, BaseHttpProvider } from '../common/http-base.js';\nimport { ScrapeError } from '../core/errors.js';\nimport type { CompletionOptions, LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP LLM provider configuration.\n */\nexport interface HttpLLMConfig<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpConfig<TError> {\n /**\n * Build request body from prompt and options.\n * @default OpenAI-compatible format with messages array\n */\n requestBuilder?: (prompt: string, options: CompletionOptions) => TRequest;\n /**\n * Extract completion text from response.\n * @default (res) => res.choices[0].message.content\n */\n responseMapper?: (response: TResponse) => string;\n /**\n * Enable JSON mode - adds response_format to request.\n * For OpenAI-compatible APIs, this adds { response_format: { type: \"json_object\" } }\n */\n jsonMode?: boolean;\n}\n\n// ─────────────────────────────────────────────────────────────\n// HTTP LLM Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP-based LLM provider.\n * Works with any REST API using native fetch.\n */\nexport class HttpLLMProvider<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpProvider<TError>\n implements LLMProvider\n{\n readonly name: string;\n\n private readonly requestBuilder: (prompt: string, options: CompletionOptions) => TRequest;\n private readonly responseMapper: (response: TResponse) => string;\n private readonly jsonMode: boolean;\n\n constructor(config: HttpLLMConfig<TRequest, TResponse, TError>) {\n super(config);\n this.name = 'http-llm';\n this.jsonMode = config.jsonMode ?? false;\n\n // Default request builder: OpenAI-compatible format\n this.requestBuilder =\n config.requestBuilder ??\n ((prompt: string, opts: CompletionOptions) => {\n const messages: Array<{ role: string; content: string }> = [];\n\n if (opts.systemPrompt) {\n messages.push({ role: 'system', content: opts.systemPrompt });\n }\n messages.push({ role: 'user', content: prompt });\n\n const request: Record<string, unknown> = {\n model: this.model,\n messages,\n };\n\n if (opts.temperature !== undefined) {\n request.temperature = opts.temperature;\n }\n if (opts.maxTokens !== undefined) {\n request.max_tokens = opts.maxTokens;\n }\n\n return request as TRequest;\n });\n\n // Default response mapper: OpenAI-compatible format\n this.responseMapper =\n config.responseMapper ??\n ((response: TResponse) => {\n const resp = response as Record<string, unknown>;\n\n // OpenAI format: { choices: [{ message: { content: \"...\" } }] }\n if (Array.isArray(resp.choices) && resp.choices.length > 0) {\n const choice = resp.choices[0] as { message?: { content?: string } };\n if (choice.message?.content) {\n return choice.message.content;\n }\n }\n\n // Anthropic format: { content: [{ type: \"text\", text: \"...\" }] }\n if (Array.isArray(resp.content)) {\n const textBlock = resp.content.find((c: { type?: string }) => c.type === 'text') as\n | { text?: string }\n | undefined;\n if (textBlock?.text) {\n return textBlock.text;\n }\n }\n\n throw new ScrapeError(\n 'Unable to parse LLM response. Provide a custom responseMapper.',\n 'VALIDATION_ERROR'\n );\n });\n }\n\n /**\n * Generate a text completion.\n */\n async complete(prompt: string, options: CompletionOptions = {}): Promise<string> {\n let body = this.requestBuilder(prompt, options);\n\n // Add JSON mode if enabled\n if (this.jsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n\n const content = this.responseMapper(data);\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n return content;\n }\n\n /**\n * Generate a structured JSON completion with Zod validation.\n */\n async completeJSON<T>(\n prompt: string,\n schema: z.ZodType<T>,\n options: CompletionOptions = {}\n ): Promise<T> {\n // Build a prompt that requests JSON output\n const jsonPrompt = `${prompt}\n\nRespond ONLY with valid JSON matching this schema:\n${JSON.stringify(zodToJsonSchema(schema), null, 2)}\n\nDo not include any explanation or markdown formatting. Just the JSON object.`;\n\n // Use JSON mode if available\n const useJsonMode = this.jsonMode;\n let body = this.requestBuilder(jsonPrompt, {\n ...options,\n systemPrompt:\n options.systemPrompt ?? 'You are a helpful assistant that responds only with valid JSON.',\n });\n\n if (useJsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n const content = this.responseMapper(data);\n\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n try {\n const trimmed = content.trim();\n try {\n return schema.parse(JSON.parse(trimmed));\n } catch {\n // Fall back to extracting JSON from markdown or surrounding text\n }\n\n const jsonMatch = content.match(/[[{][\\s\\S]*[\\]}]/);\n if (!jsonMatch) {\n throw new Error('No JSON found in response');\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n return schema.parse(parsed);\n } catch (error) {\n throw new ScrapeError(\n `Failed to parse LLM response as JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'VALIDATION_ERROR',\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Utilities\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a Zod schema to a JSON Schema representation.\n * Uses Zod's built-in toJSONSchema method (Zod 4+).\n * Used for prompting LLMs to return structured data.\n */\nexport function zodToJsonSchema(schema: z.ZodType<unknown>): object {\n // Zod 4+ has built-in static toJSONSchema method\n if (typeof z.toJSONSchema === 'function') {\n const jsonSchema = z.toJSONSchema(schema);\n // Remove $schema key as it's not needed for LLM prompting\n const { $schema, ...rest } = jsonSchema as { $schema?: string; [key: string]: unknown };\n return rest;\n }\n\n // Fallback for older Zod versions using _def.type\n const def = (schema as z.ZodType<unknown> & { _def: { type: string } })._def;\n const type = def.type;\n\n switch (type) {\n case 'object': {\n const shape = (schema as z.ZodObject<z.ZodRawShape>).shape;\n const properties: Record<string, object> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = zodToJsonSchema(value as z.ZodType<unknown>);\n const valueDef = (value as z.ZodType<unknown> & { _def: { type: string } })._def;\n if (valueDef.type !== 'optional') {\n required.push(key);\n }\n }\n return { type: 'object', properties, required };\n }\n case 'array': {\n const arrayDef = def as unknown as { element: z.ZodType<unknown> };\n return { type: 'array', items: zodToJsonSchema(arrayDef.element) };\n }\n case 'string':\n return { type: 'string' };\n case 'number':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'enum': {\n const enumDef = def as unknown as { entries: Record<string, string> };\n return { type: 'string', enum: Object.values(enumDef.entries) };\n }\n case 'optional': {\n const optionalDef = def as unknown as { innerType: z.ZodType<unknown> };\n return zodToJsonSchema(optionalDef.innerType);\n }\n default:\n return { type: 'string' };\n }\n}\n\n// Re-export types for convenience\nexport type { ZodType } from 'zod';\n","/**\n * Preset factory functions for common LLM providers.\n * All presets use the HttpLLMProvider with appropriate configuration.\n */\n\nimport { type HttpLLMConfig, HttpLLMProvider } from './http.js';\nimport type { LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// OpenAI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenAI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createOpenAI({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createOpenAI(options?: {\n apiKey?: string;\n model?: string;\n baseUrl?: string;\n}): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: options?.baseUrl ?? 'https://api.openai.com/v1/chat/completions',\n model: options?.model ?? 'gpt-4o-mini',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Anthropic\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Anthropic API response shape.\n */\ninterface AnthropicResponse {\n content: Array<{ type: string; text?: string }>;\n}\n\n/**\n * Create an Anthropic Claude LLM provider.\n *\n * @example\n * ```ts\n * const provider = createAnthropic({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createAnthropic(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Anthropic API key required. Set ANTHROPIC_API_KEY env var or pass apiKey option.'\n );\n }\n\n const model = options?.model ?? 'claude-3-5-haiku-20241022';\n\n return new HttpLLMProvider<unknown, AnthropicResponse>({\n baseUrl: 'https://api.anthropic.com/v1/messages',\n model,\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n },\n requestBuilder: (prompt, opts) => ({\n model,\n max_tokens: opts.maxTokens ?? 1024,\n messages: [{ role: 'user', content: prompt }],\n ...(opts.systemPrompt && { system: opts.systemPrompt }),\n ...(opts.temperature !== undefined && { temperature: opts.temperature }),\n }),\n responseMapper: (res) => res.content.find((item) => item.type === 'text')?.text ?? '',\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Groq\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Groq LLM provider.\n * Groq provides fast inference for open-source models.\n *\n * @example\n * ```ts\n * const provider = createGroq({ model: 'llama-3.1-70b-versatile' });\n * ```\n */\nexport function createGroq(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.GROQ_API_KEY;\n if (!apiKey) {\n throw new Error('Groq API key required. Set GROQ_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.groq.com/openai/v1/chat/completions',\n model: options?.model ?? 'llama-3.1-70b-versatile',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Ollama (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an Ollama LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createOllama({ model: 'llama3.2' });\n * ```\n */\nexport function createOllama(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:11434/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// LM Studio (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an LM Studio LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createLMStudio({ model: 'local-model' });\n * ```\n */\nexport function createLMStudio(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:1234/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Together AI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Together AI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createTogether({ model: 'meta-llama/Llama-3.2-3B-Instruct-Turbo' });\n * ```\n */\nexport function createTogether(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.TOGETHER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Together API key required. Set TOGETHER_API_KEY env var or pass apiKey option.'\n );\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.together.xyz/v1/chat/completions',\n model: options?.model ?? 'meta-llama/Llama-3.2-3B-Instruct-Turbo',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// OpenRouter\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenRouter LLM provider.\n * OpenRouter provides access to many models through a unified API.\n *\n * @example\n * ```ts\n * const provider = createOpenRouter({\n * model: 'anthropic/claude-3.5-sonnet',\n * });\n * ```\n */\nexport function createOpenRouter(options: {\n apiKey?: string;\n model: string;\n siteUrl?: string;\n siteName?: string;\n}): LLMProvider {\n const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OpenRouter API key required. Set OPENROUTER_API_KEY env var or pass apiKey option.'\n );\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n };\n\n if (options.siteUrl) {\n headers['HTTP-Referer'] = options.siteUrl;\n }\n if (options.siteName) {\n headers['X-Title'] = options.siteName;\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://openrouter.ai/api/v1/chat/completions',\n model: options.model,\n headers,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Generic HTTP Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a generic HTTP LLM provider.\n * Use this for any OpenAI-compatible API.\n *\n * @example\n * ```ts\n * const provider = createHttpLLM({\n * baseUrl: 'https://my-api.com/v1/chat/completions',\n * model: 'my-model',\n * headers: { Authorization: 'Bearer ...' },\n * });\n * ```\n */\nexport function createHttpLLM<TRequest = unknown, TResponse = unknown, TError = unknown>(\n config: HttpLLMConfig<TRequest, TResponse, TError>\n): LLMProvider {\n return new HttpLLMProvider(config);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["messages: Array<{ role: string; content: string }>","request: Record<string, unknown>","properties: Record<string, object>","required: string[]","headers: Record<string, string>"],"sources":["../../src/llm/http.ts","../../src/llm/presets.ts"],"sourcesContent":["/**\n * HTTP-based LLM Provider using native fetch.\n * Provides a unified interface for any REST-based LLM API.\n */\n\nimport { z } from 'zod';\nimport { type BaseHttpConfig, BaseHttpProvider } from '../common/http-base.js';\nimport { ScrapeError } from '../core/errors.js';\nimport type { CompletionOptions, LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP LLM provider configuration.\n */\nexport interface HttpLLMConfig<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpConfig<TError> {\n /**\n * Build request body from prompt and options.\n * @default OpenAI-compatible format with messages array\n */\n requestBuilder?: (prompt: string, options: CompletionOptions) => TRequest;\n /**\n * Extract completion text from response.\n * @default (res) => res.choices[0].message.content\n */\n responseMapper?: (response: TResponse) => string;\n /**\n * Enable JSON mode - adds response_format to request.\n * For OpenAI-compatible APIs, this adds { response_format: { type: \"json_object\" } }\n */\n jsonMode?: boolean;\n}\n\n// ─────────────────────────────────────────────────────────────\n// HTTP LLM Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * HTTP-based LLM provider.\n * Works with any REST API using native fetch.\n */\nexport class HttpLLMProvider<TRequest = unknown, TResponse = unknown, TError = unknown>\n extends BaseHttpProvider<TError>\n implements LLMProvider\n{\n readonly name: string;\n\n private readonly requestBuilder: (prompt: string, options: CompletionOptions) => TRequest;\n private readonly responseMapper: (response: TResponse) => string;\n private readonly jsonMode: boolean;\n\n constructor(config: HttpLLMConfig<TRequest, TResponse, TError>) {\n super(config);\n this.name = 'http-llm';\n this.jsonMode = config.jsonMode ?? false;\n\n // Default request builder: OpenAI-compatible format\n this.requestBuilder =\n config.requestBuilder ??\n ((prompt: string, opts: CompletionOptions) => {\n const messages: Array<{ role: string; content: string }> = [];\n\n if (opts.systemPrompt) {\n messages.push({ role: 'system', content: opts.systemPrompt });\n }\n messages.push({ role: 'user', content: prompt });\n\n const request: Record<string, unknown> = {\n model: this.model,\n messages,\n };\n\n if (opts.temperature !== undefined) {\n request.temperature = opts.temperature;\n }\n if (opts.maxTokens !== undefined) {\n request.max_tokens = opts.maxTokens;\n }\n\n return request as TRequest;\n });\n\n // Default response mapper: OpenAI-compatible format\n this.responseMapper =\n config.responseMapper ??\n ((response: TResponse) => {\n const resp = response as Record<string, unknown>;\n\n // OpenAI format: { choices: [{ message: { content: \"...\" } }] }\n if (Array.isArray(resp.choices) && resp.choices.length > 0) {\n const choice = resp.choices[0] as { message?: { content?: string } };\n if (choice.message?.content) {\n return choice.message.content;\n }\n }\n\n // Anthropic format: { content: [{ type: \"text\", text: \"...\" }] }\n if (Array.isArray(resp.content)) {\n const textBlock = resp.content.find((c: { type?: string }) => c.type === 'text') as\n | { text?: string }\n | undefined;\n if (textBlock?.text) {\n return textBlock.text;\n }\n }\n\n throw new ScrapeError(\n 'Unable to parse LLM response. Provide a custom responseMapper.',\n 'VALIDATION_ERROR'\n );\n });\n }\n\n /**\n * Generate a text completion.\n */\n async complete(prompt: string, options: CompletionOptions = {}): Promise<string> {\n let body = this.requestBuilder(prompt, options);\n\n // Add JSON mode if enabled\n if (this.jsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n\n const content = this.responseMapper(data);\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n return content;\n }\n\n /**\n * Generate a structured JSON completion with Zod validation.\n */\n async completeJSON<T>(\n prompt: string,\n schema: z.ZodType<T>,\n options: CompletionOptions = {}\n ): Promise<T> {\n // Build a prompt that requests JSON output\n const jsonPrompt = `${prompt}\n\nRespond ONLY with valid JSON matching this schema:\n${JSON.stringify(zodToJsonSchema(schema), null, 2)}\n\nDo not include any explanation or markdown formatting. Just the JSON object.`;\n\n // Use JSON mode if available\n const useJsonMode = this.jsonMode;\n let body = this.requestBuilder(jsonPrompt, {\n ...options,\n systemPrompt:\n options.systemPrompt ?? 'You are a helpful assistant that responds only with valid JSON.',\n });\n\n if (useJsonMode && typeof body === 'object' && body !== null) {\n body = {\n ...body,\n response_format: { type: 'json_object' },\n } as TRequest;\n }\n\n const { data } = await this.fetch<TResponse>(this.baseUrl, { body });\n const content = this.responseMapper(data);\n\n if (!content) {\n throw new ScrapeError('Empty response from LLM', 'LLM_ERROR');\n }\n\n try {\n const trimmed = content.trim();\n try {\n return schema.parse(JSON.parse(trimmed));\n } catch {\n // Fall back to extracting JSON from markdown or surrounding text\n }\n\n const jsonMatch = content.match(/[[{][\\s\\S]*[\\]}]/);\n if (!jsonMatch) {\n throw new Error('No JSON found in response');\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n return schema.parse(parsed);\n } catch (error) {\n throw new ScrapeError(\n `Failed to parse LLM response as JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'VALIDATION_ERROR',\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Utilities\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a Zod schema to a JSON Schema representation.\n * Uses Zod's built-in toJSONSchema method (Zod 4+).\n * Used for prompting LLMs to return structured data.\n */\nexport function zodToJsonSchema(schema: z.ZodType<unknown>): object {\n // Zod 4+ has built-in static toJSONSchema method\n if (typeof z.toJSONSchema === 'function') {\n const jsonSchema = z.toJSONSchema(schema);\n // Remove $schema key as it's not needed for LLM prompting\n const { $schema, ...rest } = jsonSchema as { $schema?: string; [key: string]: unknown };\n return rest;\n }\n\n // Fallback for older Zod versions using _def.type\n const def = (schema as z.ZodType<unknown> & { _def: { type: string } })._def;\n const type = def.type;\n\n switch (type) {\n case 'object': {\n const shape = (schema as z.ZodObject<z.ZodRawShape>).shape;\n const properties: Record<string, object> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = zodToJsonSchema(value as z.ZodType<unknown>);\n const valueDef = (value as z.ZodType<unknown> & { _def: { type: string } })._def;\n if (valueDef.type !== 'optional') {\n required.push(key);\n }\n }\n return { type: 'object', properties, required };\n }\n case 'array': {\n const arrayDef = def as unknown as { element: z.ZodType<unknown> };\n return { type: 'array', items: zodToJsonSchema(arrayDef.element) };\n }\n case 'string':\n return { type: 'string' };\n case 'number':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'enum': {\n const enumDef = def as unknown as { entries: Record<string, string> };\n return { type: 'string', enum: Object.values(enumDef.entries) };\n }\n case 'optional': {\n const optionalDef = def as unknown as { innerType: z.ZodType<unknown> };\n return zodToJsonSchema(optionalDef.innerType);\n }\n default:\n return { type: 'string' };\n }\n}\n\n// Re-export types for convenience\nexport type { ZodType } from 'zod';\n","/**\n * Preset factory functions for common LLM providers.\n * All presets use the HttpLLMProvider with appropriate configuration.\n */\n\nimport { type HttpLLMConfig, HttpLLMProvider } from './http.js';\nimport type { LLMProvider } from './types.js';\n\n// ─────────────────────────────────────────────────────────────\n// OpenAI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenAI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createOpenAI({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createOpenAI(options?: {\n apiKey?: string;\n model?: string;\n baseUrl?: string;\n}): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: options?.baseUrl ?? 'https://api.openai.com/v1/chat/completions',\n model: options?.model ?? 'gpt-4o-mini',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Anthropic\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Anthropic API response shape.\n */\ninterface AnthropicResponse {\n content: Array<{ type: string; text?: string }>;\n}\n\n/**\n * Create an Anthropic Claude LLM provider.\n *\n * @example\n * ```ts\n * const provider = createAnthropic({ apiKey: 'sk-...' });\n * const result = await scrape(url, { llm: provider, enhance: ['summarize'] });\n * ```\n */\nexport function createAnthropic(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Anthropic API key required. Set ANTHROPIC_API_KEY env var or pass apiKey option.'\n );\n }\n\n const model = options?.model ?? 'claude-3-5-haiku-20241022';\n\n return new HttpLLMProvider<unknown, AnthropicResponse>({\n baseUrl: 'https://api.anthropic.com/v1/messages',\n model,\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n },\n requestBuilder: (prompt, opts) => ({\n model,\n max_tokens: opts.maxTokens ?? 1024,\n messages: [{ role: 'user', content: prompt }],\n ...(opts.systemPrompt && { system: opts.systemPrompt }),\n ...(opts.temperature !== undefined && { temperature: opts.temperature }),\n }),\n responseMapper: (res) => res.content.find((item) => item.type === 'text')?.text ?? '',\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Groq\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Groq LLM provider.\n * Groq provides fast inference for open-source models.\n *\n * @example\n * ```ts\n * const provider = createGroq({ model: 'llama-3.1-70b-versatile' });\n * ```\n */\nexport function createGroq(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.GROQ_API_KEY;\n if (!apiKey) {\n throw new Error('Groq API key required. Set GROQ_API_KEY env var or pass apiKey option.');\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.groq.com/openai/v1/chat/completions',\n model: options?.model ?? 'llama-3.1-70b-versatile',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Ollama (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an Ollama LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createOllama({ model: 'llama3.2' });\n * ```\n */\nexport function createOllama(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:11434/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// LM Studio (Local)\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an LM Studio LLM provider for local models.\n *\n * @example\n * ```ts\n * const provider = createLMStudio({ model: 'local-model' });\n * ```\n */\nexport function createLMStudio(options: { model: string; baseUrl?: string }): LLMProvider {\n return new HttpLLMProvider({\n baseUrl: options.baseUrl ?? 'http://localhost:1234/v1/chat/completions',\n model: options.model,\n requireHttps: false,\n allowPrivate: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Together AI\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a Together AI LLM provider.\n *\n * @example\n * ```ts\n * const provider = createTogether({ model: 'meta-llama/Llama-3.2-3B-Instruct-Turbo' });\n * ```\n */\nexport function createTogether(options?: { apiKey?: string; model?: string }): LLMProvider {\n const apiKey = options?.apiKey ?? process.env.TOGETHER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Together API key required. Set TOGETHER_API_KEY env var or pass apiKey option.'\n );\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://api.together.xyz/v1/chat/completions',\n model: options?.model ?? 'meta-llama/Llama-3.2-3B-Instruct-Turbo',\n headers: { Authorization: `Bearer ${apiKey}` },\n jsonMode: true,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// OpenRouter\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create an OpenRouter LLM provider.\n * OpenRouter provides access to many models through a unified API.\n *\n * @example\n * ```ts\n * const provider = createOpenRouter({\n * model: 'anthropic/claude-3.5-sonnet',\n * });\n * ```\n */\nexport function createOpenRouter(options: {\n apiKey?: string;\n model: string;\n siteUrl?: string;\n siteName?: string;\n}): LLMProvider {\n const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OpenRouter API key required. Set OPENROUTER_API_KEY env var or pass apiKey option.'\n );\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n };\n\n if (options.siteUrl) {\n headers['HTTP-Referer'] = options.siteUrl;\n }\n if (options.siteName) {\n headers['X-Title'] = options.siteName;\n }\n\n return new HttpLLMProvider({\n baseUrl: 'https://openrouter.ai/api/v1/chat/completions',\n model: options.model,\n headers,\n });\n}\n\n// ─────────────────────────────────────────────────────────────\n// Generic HTTP Provider\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a generic HTTP LLM provider.\n * Use this for any OpenAI-compatible API.\n *\n * @example\n * ```ts\n * const provider = createHttpLLM({\n * baseUrl: 'https://my-api.com/v1/chat/completions',\n * model: 'my-model',\n * headers: { Authorization: 'Bearer ...' },\n * });\n * ```\n */\nexport function createHttpLLM<TRequest = unknown, TResponse = unknown, TError = unknown>(\n config: HttpLLMConfig<TRequest, TResponse, TError>\n): LLMProvider {\n return new HttpLLMProvider(config);\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { _ as MarkdownSection, a as parseByHeadings, b as ParserResult, c as isGitHubRepo, d as CodeBlock, f as FeedEnclosure, g as MarkdownLink, h as GitHubMeta, i as extractListLinks, l as parseGitHubUrl, m as FeedMeta, n as RSSParserOptions, o as fetchRepoMeta, p as FeedItem, r as MarkdownParser, s as groupByCategory, t as RSSParser, u as toRawUrl, v as ParsedFeed, x as SourceParser, y as ParsedMarkdown } from "../index-CDgcRnig.cjs";
1
+ import { _ as MarkdownSection, a as parseByHeadings, b as ParserResult, c as isGitHubRepo, d as CodeBlock, f as FeedEnclosure, g as MarkdownLink, h as GitHubMeta, i as extractListLinks, l as parseGitHubUrl, m as FeedMeta, n as RSSParserOptions, o as fetchRepoMeta, p as FeedItem, r as MarkdownParser, s as groupByCategory, t as RSSParser, u as toRawUrl, v as ParsedFeed, x as SourceParser, y as ParsedMarkdown } from "../index-Bvseqli-.cjs";
2
2
  export { CodeBlock, FeedEnclosure, FeedItem, FeedMeta, GitHubMeta, MarkdownLink, MarkdownParser, MarkdownSection, ParsedFeed, ParsedMarkdown, ParserResult, RSSParser, RSSParserOptions, SourceParser, extractListLinks, fetchRepoMeta, groupByCategory, isGitHubRepo, parseByHeadings, parseGitHubUrl, toRawUrl };
@@ -1,2 +1,2 @@
1
- import { _ as MarkdownSection, a as parseByHeadings, b as ParserResult, c as isGitHubRepo, d as CodeBlock, f as FeedEnclosure, g as MarkdownLink, h as GitHubMeta, i as extractListLinks, l as parseGitHubUrl, m as FeedMeta, n as RSSParserOptions, o as fetchRepoMeta, p as FeedItem, r as MarkdownParser, s as groupByCategory, t as RSSParser, u as toRawUrl, v as ParsedFeed, x as SourceParser, y as ParsedMarkdown } from "../index-piS5wtki.mjs";
1
+ import { _ as MarkdownSection, a as parseByHeadings, b as ParserResult, c as isGitHubRepo, d as CodeBlock, f as FeedEnclosure, g as MarkdownLink, h as GitHubMeta, i as extractListLinks, l as parseGitHubUrl, m as FeedMeta, n as RSSParserOptions, o as fetchRepoMeta, p as FeedItem, r as MarkdownParser, s as groupByCategory, t as RSSParser, u as toRawUrl, v as ParsedFeed, x as SourceParser, y as ParsedMarkdown } from "../index-CIFjNySr.mjs";
2
2
  export { CodeBlock, FeedEnclosure, FeedItem, FeedMeta, GitHubMeta, MarkdownLink, MarkdownParser, MarkdownSection, ParsedFeed, ParsedMarkdown, ParserResult, RSSParser, RSSParserOptions, SourceParser, extractListLinks, fetchRepoMeta, groupByCategory, isGitHubRepo, parseByHeadings, parseGitHubUrl, toRawUrl };
@@ -1,3 +1,3 @@
1
- import { a as fetchRepoMeta, c as parseGitHubUrl, i as parseByHeadings, l as toRawUrl, n as MarkdownParser, o as groupByCategory, r as extractListLinks, s as isGitHubRepo, t as RSSParser } from "../parsers-CwkYnyWY.mjs";
1
+ import { a as fetchRepoMeta, c as parseGitHubUrl, i as parseByHeadings, l as toRawUrl, n as MarkdownParser, o as groupByCategory, r as extractListLinks, s as isGitHubRepo, t as RSSParser } from "../parsers-DsawHeo0.mjs";
2
2
 
3
3
  export { MarkdownParser, RSSParser, extractListLinks, fetchRepoMeta, groupByCategory, isGitHubRepo, parseByHeadings, parseGitHubUrl, toRawUrl };
@@ -479,4 +479,4 @@ var RSSParser = class {
479
479
 
480
480
  //#endregion
481
481
  export { fetchRepoMeta as a, parseGitHubUrl as c, parseByHeadings as i, toRawUrl as l, MarkdownParser as n, groupByCategory as o, extractListLinks as r, isGitHubRepo as s, RSSParser as t };
482
- //# sourceMappingURL=parsers-CwkYnyWY.mjs.map
482
+ //# sourceMappingURL=parsers-DsawHeo0.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"parsers-CwkYnyWY.mjs","names":["sections: MarkdownSection[]","allLinks: MarkdownLink[]","codeBlocks: CodeBlock[]","frontmatter: Record<string, unknown> | undefined","currentSection: MarkdownSection | null","mdastToString","linkData: MarkdownLink","result: Record<string, unknown>","value: string | boolean | number","links: MarkdownLink[]","items: FeedItem[]","fields: Record<string, string>"],"sources":["../src/parsers/github.ts","../src/parsers/markdown.ts","../src/parsers/rss.ts"],"sourcesContent":["import type { GitHubMeta, MarkdownLink } from './types.js';\n\n/**\n * GitHub-specific utilities for parsing repositories.\n */\n\n/**\n * Check if a URL is a GitHub repository\n */\nexport function isGitHubRepo(url: string): boolean {\n return /^https?:\\/\\/(www\\.)?github\\.com\\/[^/]+\\/[^/]+\\/?$/.test(url);\n}\n\n/**\n * Extract GitHub repo info from URL\n */\nexport function parseGitHubUrl(url: string): { owner: string; repo: string } | null {\n const match = url.match(/github\\.com\\/([^/]+)\\/([^/]+)/);\n if (!match || !match[1] || !match[2]) return null;\n return {\n owner: match[1],\n repo: match[2].replace(/\\.git$/, ''),\n };\n}\n\n/**\n * Convert a GitHub repo URL to raw content URL\n */\nexport function toRawUrl(url: string, branch = 'main', file = 'README.md'): string {\n const info = parseGitHubUrl(url);\n if (!info) return url;\n return `https://raw.githubusercontent.com/${info.owner}/${info.repo}/${branch}/${file}`;\n}\n\n/**\n * Fetch GitHub API metadata for a repository\n * Note: This is a placeholder - actual implementation would need GitHub API access\n */\nexport async function fetchRepoMeta(\n owner: string,\n repo: string,\n _token?: string\n): Promise<GitHubMeta> {\n // This would make actual API calls in a full implementation\n // For now, return basic info\n return {\n repoOwner: owner,\n repoName: repo,\n };\n}\n\n/**\n * Group links by their category/section\n */\nexport function groupByCategory(links: MarkdownLink[]): Map<string, MarkdownLink[]> {\n const groups = new Map<string, MarkdownLink[]>();\n\n for (const link of links) {\n const category = link.context || 'Uncategorized';\n const existing = groups.get(category) || [];\n existing.push(link);\n groups.set(category, existing);\n }\n\n return groups;\n}\n","import type { Code, Heading, Link, ListItem, Root } from 'mdast';\nimport { fromMarkdown } from 'mdast-util-from-markdown';\nimport { toString as mdastToString } from 'mdast-util-to-string';\nimport { visit } from 'unist-util-visit';\nimport type {\n CodeBlock,\n MarkdownLink,\n MarkdownSection,\n ParsedMarkdown,\n ParserResult,\n SourceParser,\n} from './types.js';\n\n/**\n * Generic Markdown parser.\n * Extracts structure, links, and code blocks from markdown content.\n *\n * @example\n * ```ts\n * const parser = new MarkdownParser();\n * const result = parser.parse(markdownContent);\n * console.log(result.data.sections);\n * console.log(result.data.links);\n * ```\n */\nexport class MarkdownParser implements SourceParser<ParsedMarkdown> {\n readonly name = 'markdown';\n\n canParse(content: string): boolean {\n // Check for common markdown patterns\n return (\n content.includes('# ') ||\n content.includes('## ') ||\n content.includes('- [') ||\n content.includes('* [') ||\n content.includes('```')\n );\n }\n\n parse(content: string): ParserResult<ParsedMarkdown> {\n const tree = fromMarkdown(content);\n const sections: MarkdownSection[] = [];\n const allLinks: MarkdownLink[] = [];\n const codeBlocks: CodeBlock[] = [];\n let frontmatter: Record<string, unknown> | undefined;\n\n // Extract frontmatter if present\n if (content.startsWith('---')) {\n const endIndex = content.indexOf('---', 3);\n if (endIndex !== -1) {\n const frontmatterContent = content.slice(3, endIndex).trim();\n frontmatter = this.parseFrontmatter(frontmatterContent);\n }\n }\n\n // Track current section\n let currentSection: MarkdownSection | null = null;\n\n // Process the AST\n visit(tree, (node) => {\n // Handle headings\n if (node.type === 'heading') {\n const heading = node as Heading;\n const title = mdastToString(heading);\n\n // Finalize previous section\n if (currentSection) {\n sections.push(currentSection);\n }\n\n currentSection = {\n level: heading.depth,\n title,\n content: '',\n links: [],\n };\n }\n\n // Handle links\n if (node.type === 'link') {\n const link = node as Link;\n const text = mdastToString(link);\n const linkData: MarkdownLink = {\n url: link.url,\n text,\n title: link.title ?? undefined,\n context: currentSection?.title,\n };\n\n allLinks.push(linkData);\n if (currentSection) {\n currentSection.links.push(linkData);\n }\n }\n\n // Handle code blocks\n if (node.type === 'code') {\n const code = node as Code;\n codeBlocks.push({\n language: code.lang ?? undefined,\n code: code.value,\n meta: code.meta ?? undefined,\n });\n }\n\n // Accumulate content for current section\n if (currentSection && node.type === 'paragraph') {\n const text = mdastToString(node);\n currentSection.content += (currentSection.content ? '\\n\\n' : '') + text;\n }\n });\n\n // Finalize last section\n if (currentSection) {\n sections.push(currentSection);\n }\n\n // Extract title from first h1 or frontmatter\n const title = (frontmatter?.title as string) ?? sections.find((s) => s.level === 1)?.title;\n\n // Extract description from frontmatter or first paragraph before any heading\n const description = (frontmatter?.description as string) ?? this.extractDescription(tree);\n\n return {\n data: {\n title,\n description,\n sections,\n links: allLinks,\n codeBlocks,\n frontmatter,\n },\n };\n }\n\n private parseFrontmatter(content: string): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n const lines = content.split('\\n');\n\n for (const line of lines) {\n const colonIndex = line.indexOf(':');\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim();\n let value: string | boolean | number = line.slice(colonIndex + 1).trim();\n\n // Parse simple types\n if (value === 'true') value = true;\n else if (value === 'false') value = false;\n else if (/^-?\\d+(\\.\\d+)?$/.test(value)) value = Number(value);\n else if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n else if (value.startsWith(\"'\") && value.endsWith(\"'\")) value = value.slice(1, -1);\n\n result[key] = value;\n }\n }\n\n return result;\n }\n\n private extractDescription(tree: Root): string | undefined {\n // Find first paragraph before any heading\n for (const node of tree.children) {\n if (node.type === 'heading') break;\n if (node.type === 'paragraph') {\n return mdastToString(node);\n }\n }\n return undefined;\n }\n}\n\n/**\n * Extract links from a list-based markdown structure (like awesome lists)\n */\nexport function extractListLinks(markdown: string): MarkdownLink[] {\n const tree = fromMarkdown(markdown);\n const links: MarkdownLink[] = [];\n let currentHeading = '';\n\n visit(tree, (node) => {\n if (node.type === 'heading') {\n currentHeading = mdastToString(node as Heading);\n }\n\n if (node.type === 'listItem') {\n const listItem = node as ListItem;\n\n // Find links in this list item\n visit(listItem, 'link', (linkNode: Link) => {\n links.push({\n url: linkNode.url,\n text: mdastToString(linkNode),\n title: linkNode.title ?? undefined,\n context: currentHeading || undefined,\n });\n });\n }\n });\n\n return links;\n}\n\n/**\n * Parse markdown into sections by heading level\n */\nexport function parseByHeadings(markdown: string, minLevel = 2): MarkdownSection[] {\n const parser = new MarkdownParser();\n const result = parser.parse(markdown);\n return result.data.sections.filter((s) => s.level >= minLevel);\n}\n","import * as cheerio from 'cheerio';\nimport type { Element } from 'domhandler';\nimport type {\n FeedEnclosure,\n FeedItem,\n FeedMeta,\n ParsedFeed,\n ParserResult,\n SourceParser,\n} from './types.js';\n\nexport interface RSSParserOptions {\n /**\n * Map of custom field names to CSS selectors or XML tag names.\n * Useful for extracting podcast/media namespace fields.\n * @example { duration: \"itunes\\:duration\", rating: \"media\\:rating\" }\n */\n customFields?: Record<string, string>;\n}\n\n/**\n * RSS/Atom feed parser.\n * Supports RSS 2.0, RSS 1.0 (RDF), and Atom 1.0 formats.\n *\n * @example\n * ```ts\n * import { RSSParser } from 'scrapex/parsers';\n *\n * const parser = new RSSParser();\n * const result = parser.parse(feedXml, 'https://example.com/feed.xml');\n *\n * console.log(result.data.title);\n * console.log(result.data.items);\n * ```\n */\nexport class RSSParser implements SourceParser<ParsedFeed, FeedMeta> {\n readonly name = 'rss';\n private customFields: Record<string, string>;\n\n constructor(options?: RSSParserOptions) {\n this.customFields = options?.customFields || {};\n }\n\n canParse(content: string): boolean {\n const lower = content.toLowerCase();\n return lower.includes('<rss') || lower.includes('<feed') || lower.includes('<rdf:rdf');\n }\n\n parse(content: string, url?: string): ParserResult<ParsedFeed, FeedMeta> {\n // Cheerio's xml: true mode disables HTML entities and structure fixes,\n // effectively preventing many XSS/injection vectors during parsing.\n const $ = cheerio.load(content, { xml: true });\n\n // Detect format and parse\n if ($('feed').length > 0) {\n return this.parseAtom($, url);\n } else if ($('rdf\\\\:RDF, RDF').length > 0) {\n return this.parseRSS1($, url);\n } else {\n return this.parseRSS2($, url);\n }\n }\n\n private parseRSS2($: cheerio.CheerioAPI, baseUrl?: string): ParserResult<ParsedFeed, FeedMeta> {\n const channel = $('channel');\n const feedLink = channel.find('> link').text();\n const resolveBase = baseUrl || feedLink;\n\n const items: FeedItem[] = $('item')\n .map((_, el) => {\n const $item = $(el);\n const itemLink = $item.find('link').text();\n const guid = $item.find('guid').text();\n const pubDate = $item.find('pubDate').text();\n\n // Resolve link with fallback to guid if it's a URL\n const resolvedLink = this.resolveLink(itemLink, guid, resolveBase);\n\n return {\n id: guid || itemLink,\n title: $item.find('title').text(),\n link: resolvedLink,\n description: this.parseText($item.find('description')),\n content: this.parseContentEncoded($item.find('content\\\\:encoded')),\n author: $item.find('author').text() || $item.find('dc\\\\:creator').text() || undefined,\n publishedAt: this.parseDate(pubDate),\n rawPublishedAt: pubDate || undefined,\n categories: this.parseCategories(\n $item\n .find('category')\n .map((_, c) => $(c).text())\n .get()\n ),\n enclosure: this.parseEnclosure($item.find('enclosure'), resolveBase),\n customFields: this.extractCustomFields($item),\n };\n })\n .get();\n\n return {\n data: {\n format: 'rss2',\n title: channel.find('> title').text(),\n description: channel.find('> description').text() || undefined,\n link: this.resolveUrl(feedLink, resolveBase),\n language: channel.find('> language').text() || undefined,\n lastBuildDate: this.parseDate(channel.find('> lastBuildDate').text()),\n copyright: channel.find('> copyright').text() || undefined,\n items,\n customFields: this.extractCustomFields(channel),\n },\n meta: {\n generator: channel.find('> generator').text() || undefined,\n ttl: this.parseNumber(channel.find('> ttl').text()),\n image: this.parseImage(channel.find('> image'), resolveBase),\n categories: this.parseCategories(\n channel\n .find('> category')\n .map((_, c) => $(c).text())\n .get()\n ),\n },\n };\n }\n\n private parseAtom($: cheerio.CheerioAPI, baseUrl?: string): ParserResult<ParsedFeed, FeedMeta> {\n const feed = $('feed');\n const feedLink = feed.find('> link[rel=\"alternate\"], > link:not([rel])').attr('href') || '';\n const nextLink = feed.find('> link[rel=\"next\"]').attr('href');\n const resolveBase = baseUrl || feedLink;\n\n const items: FeedItem[] = $('entry')\n .map((_, el) => {\n const $entry = $(el);\n const entryLink = $entry.find('link[rel=\"alternate\"], link:not([rel])').attr('href') || '';\n const entryId = $entry.find('id').text();\n const published = $entry.find('published').text();\n const updated = $entry.find('updated').text();\n\n // Resolve link with fallback to id if it's a URL\n const resolvedLink = this.resolveLink(entryLink, entryId, resolveBase);\n\n return {\n id: entryId,\n title: $entry.find('title').text(),\n link: resolvedLink,\n description: this.parseText($entry.find('summary')),\n content: this.parseText($entry.find('content')),\n author: $entry.find('author name').text() || undefined,\n publishedAt: this.parseDate(published),\n rawPublishedAt: published || updated || undefined,\n updatedAt: this.parseDate(updated),\n categories: this.parseCategories(\n $entry\n .find('category')\n .map((_, c) => $(c).attr('term'))\n .get()\n ),\n customFields: this.extractCustomFields($entry),\n };\n })\n .get();\n\n return {\n data: {\n format: 'atom',\n title: feed.find('> title').text(),\n description: feed.find('> subtitle').text() || undefined,\n link: this.resolveUrl(feedLink, resolveBase),\n next: nextLink ? this.resolveUrl(nextLink, resolveBase) : undefined,\n language: feed.attr('xml:lang') || undefined,\n lastBuildDate: this.parseDate(feed.find('> updated').text()),\n copyright: feed.find('> rights').text() || undefined,\n items,\n customFields: this.extractCustomFields(feed),\n },\n meta: {\n generator: feed.find('> generator').text() || undefined,\n image: this.parseAtomImage(feed, resolveBase),\n categories: this.parseCategories(\n feed\n .find('> category')\n .map((_, c) => $(c).attr('term'))\n .get()\n ),\n },\n };\n }\n\n private parseRSS1($: cheerio.CheerioAPI, baseUrl?: string): ParserResult<ParsedFeed, FeedMeta> {\n const channel = $('channel');\n const feedLink = channel.find('link').text();\n const resolveBase = baseUrl || feedLink;\n\n // RSS 1.0 items are siblings of channel, not children\n const items: FeedItem[] = $('item')\n .map((_, el) => {\n const $item = $(el);\n const itemLink = $item.find('link').text();\n const rdfAbout = $item.attr('rdf:about') || '';\n const dcDate = $item.find('dc\\\\:date').text();\n\n // Resolve link with fallback to rdf:about\n const resolvedLink = this.resolveLink(itemLink, rdfAbout, resolveBase);\n\n // Extract dc:subject as categories (RSS 1.0 uses Dublin Core)\n const dcSubjects = $item\n .find('dc\\\\:subject')\n .map((_, s) => $(s).text())\n .get();\n\n return {\n id: rdfAbout || itemLink,\n title: $item.find('title').text(),\n link: resolvedLink,\n description: this.parseText($item.find('description')),\n content: this.parseContentEncoded($item.find('content\\\\:encoded')),\n author: $item.find('dc\\\\:creator').text() || undefined,\n publishedAt: this.parseDate(dcDate),\n rawPublishedAt: dcDate || undefined,\n categories: this.parseCategories(dcSubjects),\n customFields: this.extractCustomFields($item),\n };\n })\n .get();\n\n // Parse RDF image element (sibling of channel)\n const rdfImage = $('image');\n const imageUrl = rdfImage.find('url').text() || rdfImage.attr('rdf:resource');\n\n return {\n data: {\n format: 'rss1',\n title: channel.find('title').text(),\n description: channel.find('description').text() || undefined,\n link: this.resolveUrl(feedLink, resolveBase),\n language: channel.find('dc\\\\:language').text() || undefined,\n lastBuildDate: this.parseDate(channel.find('dc\\\\:date').text()),\n copyright: channel.find('dc\\\\:rights').text() || undefined,\n items,\n customFields: this.extractCustomFields(channel),\n },\n meta: {\n generator: channel.find('admin\\\\:generatorAgent').attr('rdf:resource') || undefined,\n image: imageUrl\n ? {\n url: this.resolveUrl(imageUrl, resolveBase),\n title: rdfImage.find('title').text() || undefined,\n link: this.resolveUrl(rdfImage.find('link').text(), resolveBase) || undefined,\n }\n : undefined,\n categories: this.parseCategories(\n channel\n .find('dc\\\\:subject')\n .map((_, s) => $(s).text())\n .get()\n ),\n },\n };\n }\n\n /**\n * Extract custom fields from an element using configured selectors.\n */\n private extractCustomFields($el: cheerio.Cheerio<Element>): Record<string, string> | undefined {\n if (Object.keys(this.customFields).length === 0) return undefined;\n\n const fields: Record<string, string> = {};\n for (const [key, selector] of Object.entries(this.customFields)) {\n const value = $el.find(selector).text().trim();\n if (value) {\n fields[key] = value;\n }\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n }\n\n /**\n * Parse date string to ISO 8601 format.\n * Returns undefined if parsing fails (never returns raw strings).\n */\n private parseDate(dateStr: string): string | undefined {\n if (!dateStr?.trim()) return undefined;\n\n try {\n const date = new Date(dateStr);\n // Check for invalid date\n if (Number.isNaN(date.getTime())) {\n return undefined;\n }\n return date.toISOString();\n } catch {\n return undefined;\n }\n }\n\n /**\n * Parse element text content, returning undefined if empty.\n */\n private parseText($el: cheerio.Cheerio<Element>): string | undefined {\n const text = $el.text().trim();\n return text || undefined;\n }\n\n /**\n * Parse content:encoded, stripping HTML tags from CDATA content.\n */\n private parseContentEncoded($el: cheerio.Cheerio<Element>): string | undefined {\n const raw = $el.text().trim();\n if (!raw) return undefined;\n return raw.replace(/<[^>]+>/g, '').trim() || undefined;\n }\n\n /**\n * Parse categories/tags, filtering out empty strings.\n */\n private parseCategories(categories: (string | undefined)[]): string[] {\n return categories.map((c) => c?.trim()).filter((c): c is string => !!c && c.length > 0);\n }\n\n /**\n * Resolve a URL against a base URL.\n * Only allows https scheme; all other schemes are rejected.\n */\n private resolveUrl(url: string, base?: string): string {\n if (!url?.trim()) return '';\n\n try {\n const resolved = base ? new URL(url, base) : new URL(url);\n return resolved.protocol === 'https:' ? resolved.href : '';\n } catch {\n return '';\n }\n }\n\n /**\n * Resolve link with fallback to id/guid if primary link is empty and id is a URL.\n * Only allows https scheme; all other schemes are rejected.\n */\n private resolveLink(primaryLink: string, fallbackId: string, base?: string): string {\n // Try primary link first\n if (primaryLink?.trim()) {\n return this.resolveUrl(primaryLink, base);\n }\n\n // Fallback to id if it looks like a URL\n if (fallbackId?.trim()) {\n try {\n const resolvedId = new URL(fallbackId);\n return resolvedId.protocol === 'https:' ? resolvedId.href : '';\n } catch {\n // Not a valid URL, try resolving against base\n return this.resolveUrl(fallbackId, base);\n }\n }\n\n return '';\n }\n\n /**\n * Parse enclosure element with URL resolution.\n */\n private parseEnclosure(\n $enc: cheerio.Cheerio<Element>,\n baseUrl?: string\n ): FeedEnclosure | undefined {\n const url = $enc.attr('url');\n if (!url) return undefined;\n\n return {\n url: this.resolveUrl(url, baseUrl),\n type: $enc.attr('type') || undefined,\n length: this.parseNumber($enc.attr('length')),\n };\n }\n\n /**\n * Parse RSS image element with URL resolution.\n */\n private parseImage(\n $img: cheerio.Cheerio<Element>,\n baseUrl?: string\n ): FeedMeta['image'] | undefined {\n const url = $img.find('url').text();\n if (!url) return undefined;\n\n return {\n url: this.resolveUrl(url, baseUrl),\n title: $img.find('title').text() || undefined,\n link: this.resolveUrl($img.find('link').text(), baseUrl) || undefined,\n };\n }\n\n /**\n * Parse Atom logo/icon element with URL resolution.\n */\n private parseAtomImage(\n $feed: cheerio.Cheerio<Element>,\n baseUrl?: string\n ): FeedMeta['image'] | undefined {\n const logo = $feed.find('> logo').text();\n const icon = $feed.find('> icon').text();\n const url = logo || icon;\n\n if (!url) return undefined;\n\n return {\n url: this.resolveUrl(url, baseUrl),\n };\n }\n\n /**\n * Parse a string to number, returning undefined if invalid.\n */\n private parseNumber(value: string | undefined): number | undefined {\n if (!value) return undefined;\n const num = parseInt(value, 10);\n return Number.isNaN(num) ? undefined : num;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parsers-DsawHeo0.mjs","names":["sections: MarkdownSection[]","allLinks: MarkdownLink[]","codeBlocks: CodeBlock[]","frontmatter: Record<string, unknown> | undefined","currentSection: MarkdownSection | null","mdastToString","linkData: MarkdownLink","result: Record<string, unknown>","value: string | boolean | number","links: MarkdownLink[]","items: FeedItem[]","fields: Record<string, string>"],"sources":["../src/parsers/github.ts","../src/parsers/markdown.ts","../src/parsers/rss.ts"],"sourcesContent":["import type { GitHubMeta, MarkdownLink } from './types.js';\n\n/**\n * GitHub-specific utilities for parsing repositories.\n */\n\n/**\n * Check if a URL is a GitHub repository\n */\nexport function isGitHubRepo(url: string): boolean {\n return /^https?:\\/\\/(www\\.)?github\\.com\\/[^/]+\\/[^/]+\\/?$/.test(url);\n}\n\n/**\n * Extract GitHub repo info from URL\n */\nexport function parseGitHubUrl(url: string): { owner: string; repo: string } | null {\n const match = url.match(/github\\.com\\/([^/]+)\\/([^/]+)/);\n if (!match || !match[1] || !match[2]) return null;\n return {\n owner: match[1],\n repo: match[2].replace(/\\.git$/, ''),\n };\n}\n\n/**\n * Convert a GitHub repo URL to raw content URL\n */\nexport function toRawUrl(url: string, branch = 'main', file = 'README.md'): string {\n const info = parseGitHubUrl(url);\n if (!info) return url;\n return `https://raw.githubusercontent.com/${info.owner}/${info.repo}/${branch}/${file}`;\n}\n\n/**\n * Fetch GitHub API metadata for a repository\n * Note: This is a placeholder - actual implementation would need GitHub API access\n */\nexport async function fetchRepoMeta(\n owner: string,\n repo: string,\n _token?: string\n): Promise<GitHubMeta> {\n // This would make actual API calls in a full implementation\n // For now, return basic info\n return {\n repoOwner: owner,\n repoName: repo,\n };\n}\n\n/**\n * Group links by their category/section\n */\nexport function groupByCategory(links: MarkdownLink[]): Map<string, MarkdownLink[]> {\n const groups = new Map<string, MarkdownLink[]>();\n\n for (const link of links) {\n const category = link.context || 'Uncategorized';\n const existing = groups.get(category) || [];\n existing.push(link);\n groups.set(category, existing);\n }\n\n return groups;\n}\n","import type { Code, Heading, Link, ListItem, Root } from 'mdast';\nimport { fromMarkdown } from 'mdast-util-from-markdown';\nimport { toString as mdastToString } from 'mdast-util-to-string';\nimport { visit } from 'unist-util-visit';\nimport type {\n CodeBlock,\n MarkdownLink,\n MarkdownSection,\n ParsedMarkdown,\n ParserResult,\n SourceParser,\n} from './types.js';\n\n/**\n * Generic Markdown parser.\n * Extracts structure, links, and code blocks from markdown content.\n *\n * @example\n * ```ts\n * const parser = new MarkdownParser();\n * const result = parser.parse(markdownContent);\n * console.log(result.data.sections);\n * console.log(result.data.links);\n * ```\n */\nexport class MarkdownParser implements SourceParser<ParsedMarkdown> {\n readonly name = 'markdown';\n\n canParse(content: string): boolean {\n // Check for common markdown patterns\n return (\n content.includes('# ') ||\n content.includes('## ') ||\n content.includes('- [') ||\n content.includes('* [') ||\n content.includes('```')\n );\n }\n\n parse(content: string): ParserResult<ParsedMarkdown> {\n const tree = fromMarkdown(content);\n const sections: MarkdownSection[] = [];\n const allLinks: MarkdownLink[] = [];\n const codeBlocks: CodeBlock[] = [];\n let frontmatter: Record<string, unknown> | undefined;\n\n // Extract frontmatter if present\n if (content.startsWith('---')) {\n const endIndex = content.indexOf('---', 3);\n if (endIndex !== -1) {\n const frontmatterContent = content.slice(3, endIndex).trim();\n frontmatter = this.parseFrontmatter(frontmatterContent);\n }\n }\n\n // Track current section\n let currentSection: MarkdownSection | null = null;\n\n // Process the AST\n visit(tree, (node) => {\n // Handle headings\n if (node.type === 'heading') {\n const heading = node as Heading;\n const title = mdastToString(heading);\n\n // Finalize previous section\n if (currentSection) {\n sections.push(currentSection);\n }\n\n currentSection = {\n level: heading.depth,\n title,\n content: '',\n links: [],\n };\n }\n\n // Handle links\n if (node.type === 'link') {\n const link = node as Link;\n const text = mdastToString(link);\n const linkData: MarkdownLink = {\n url: link.url,\n text,\n title: link.title ?? undefined,\n context: currentSection?.title,\n };\n\n allLinks.push(linkData);\n if (currentSection) {\n currentSection.links.push(linkData);\n }\n }\n\n // Handle code blocks\n if (node.type === 'code') {\n const code = node as Code;\n codeBlocks.push({\n language: code.lang ?? undefined,\n code: code.value,\n meta: code.meta ?? undefined,\n });\n }\n\n // Accumulate content for current section\n if (currentSection && node.type === 'paragraph') {\n const text = mdastToString(node);\n currentSection.content += (currentSection.content ? '\\n\\n' : '') + text;\n }\n });\n\n // Finalize last section\n if (currentSection) {\n sections.push(currentSection);\n }\n\n // Extract title from first h1 or frontmatter\n const title = (frontmatter?.title as string) ?? sections.find((s) => s.level === 1)?.title;\n\n // Extract description from frontmatter or first paragraph before any heading\n const description = (frontmatter?.description as string) ?? this.extractDescription(tree);\n\n return {\n data: {\n title,\n description,\n sections,\n links: allLinks,\n codeBlocks,\n frontmatter,\n },\n };\n }\n\n private parseFrontmatter(content: string): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n const lines = content.split('\\n');\n\n for (const line of lines) {\n const colonIndex = line.indexOf(':');\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim();\n let value: string | boolean | number = line.slice(colonIndex + 1).trim();\n\n // Parse simple types\n if (value === 'true') value = true;\n else if (value === 'false') value = false;\n else if (/^-?\\d+(\\.\\d+)?$/.test(value)) value = Number(value);\n else if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n else if (value.startsWith(\"'\") && value.endsWith(\"'\")) value = value.slice(1, -1);\n\n result[key] = value;\n }\n }\n\n return result;\n }\n\n private extractDescription(tree: Root): string | undefined {\n // Find first paragraph before any heading\n for (const node of tree.children) {\n if (node.type === 'heading') break;\n if (node.type === 'paragraph') {\n return mdastToString(node);\n }\n }\n return undefined;\n }\n}\n\n/**\n * Extract links from a list-based markdown structure (like awesome lists)\n */\nexport function extractListLinks(markdown: string): MarkdownLink[] {\n const tree = fromMarkdown(markdown);\n const links: MarkdownLink[] = [];\n let currentHeading = '';\n\n visit(tree, (node) => {\n if (node.type === 'heading') {\n currentHeading = mdastToString(node as Heading);\n }\n\n if (node.type === 'listItem') {\n const listItem = node as ListItem;\n\n // Find links in this list item\n visit(listItem, 'link', (linkNode: Link) => {\n links.push({\n url: linkNode.url,\n text: mdastToString(linkNode),\n title: linkNode.title ?? undefined,\n context: currentHeading || undefined,\n });\n });\n }\n });\n\n return links;\n}\n\n/**\n * Parse markdown into sections by heading level\n */\nexport function parseByHeadings(markdown: string, minLevel = 2): MarkdownSection[] {\n const parser = new MarkdownParser();\n const result = parser.parse(markdown);\n return result.data.sections.filter((s) => s.level >= minLevel);\n}\n","import * as cheerio from 'cheerio';\nimport type { Element } from 'domhandler';\nimport type {\n FeedEnclosure,\n FeedItem,\n FeedMeta,\n ParsedFeed,\n ParserResult,\n SourceParser,\n} from './types.js';\n\nexport interface RSSParserOptions {\n /**\n * Map of custom field names to CSS selectors or XML tag names.\n * Useful for extracting podcast/media namespace fields.\n * @example { duration: \"itunes\\:duration\", rating: \"media\\:rating\" }\n */\n customFields?: Record<string, string>;\n}\n\n/**\n * RSS/Atom feed parser.\n * Supports RSS 2.0, RSS 1.0 (RDF), and Atom 1.0 formats.\n *\n * @example\n * ```ts\n * import { RSSParser } from 'scrapex/parsers';\n *\n * const parser = new RSSParser();\n * const result = parser.parse(feedXml, 'https://example.com/feed.xml');\n *\n * console.log(result.data.title);\n * console.log(result.data.items);\n * ```\n */\nexport class RSSParser implements SourceParser<ParsedFeed, FeedMeta> {\n readonly name = 'rss';\n private customFields: Record<string, string>;\n\n constructor(options?: RSSParserOptions) {\n this.customFields = options?.customFields || {};\n }\n\n canParse(content: string): boolean {\n const lower = content.toLowerCase();\n return lower.includes('<rss') || lower.includes('<feed') || lower.includes('<rdf:rdf');\n }\n\n parse(content: string, url?: string): ParserResult<ParsedFeed, FeedMeta> {\n // Cheerio's xml: true mode disables HTML entities and structure fixes,\n // effectively preventing many XSS/injection vectors during parsing.\n const $ = cheerio.load(content, { xml: true });\n\n // Detect format and parse\n if ($('feed').length > 0) {\n return this.parseAtom($, url);\n } else if ($('rdf\\\\:RDF, RDF').length > 0) {\n return this.parseRSS1($, url);\n } else {\n return this.parseRSS2($, url);\n }\n }\n\n private parseRSS2($: cheerio.CheerioAPI, baseUrl?: string): ParserResult<ParsedFeed, FeedMeta> {\n const channel = $('channel');\n const feedLink = channel.find('> link').text();\n const resolveBase = baseUrl || feedLink;\n\n const items: FeedItem[] = $('item')\n .map((_, el) => {\n const $item = $(el);\n const itemLink = $item.find('link').text();\n const guid = $item.find('guid').text();\n const pubDate = $item.find('pubDate').text();\n\n // Resolve link with fallback to guid if it's a URL\n const resolvedLink = this.resolveLink(itemLink, guid, resolveBase);\n\n return {\n id: guid || itemLink,\n title: $item.find('title').text(),\n link: resolvedLink,\n description: this.parseText($item.find('description')),\n content: this.parseContentEncoded($item.find('content\\\\:encoded')),\n author: $item.find('author').text() || $item.find('dc\\\\:creator').text() || undefined,\n publishedAt: this.parseDate(pubDate),\n rawPublishedAt: pubDate || undefined,\n categories: this.parseCategories(\n $item\n .find('category')\n .map((_, c) => $(c).text())\n .get()\n ),\n enclosure: this.parseEnclosure($item.find('enclosure'), resolveBase),\n customFields: this.extractCustomFields($item),\n };\n })\n .get();\n\n return {\n data: {\n format: 'rss2',\n title: channel.find('> title').text(),\n description: channel.find('> description').text() || undefined,\n link: this.resolveUrl(feedLink, resolveBase),\n language: channel.find('> language').text() || undefined,\n lastBuildDate: this.parseDate(channel.find('> lastBuildDate').text()),\n copyright: channel.find('> copyright').text() || undefined,\n items,\n customFields: this.extractCustomFields(channel),\n },\n meta: {\n generator: channel.find('> generator').text() || undefined,\n ttl: this.parseNumber(channel.find('> ttl').text()),\n image: this.parseImage(channel.find('> image'), resolveBase),\n categories: this.parseCategories(\n channel\n .find('> category')\n .map((_, c) => $(c).text())\n .get()\n ),\n },\n };\n }\n\n private parseAtom($: cheerio.CheerioAPI, baseUrl?: string): ParserResult<ParsedFeed, FeedMeta> {\n const feed = $('feed');\n const feedLink = feed.find('> link[rel=\"alternate\"], > link:not([rel])').attr('href') || '';\n const nextLink = feed.find('> link[rel=\"next\"]').attr('href');\n const resolveBase = baseUrl || feedLink;\n\n const items: FeedItem[] = $('entry')\n .map((_, el) => {\n const $entry = $(el);\n const entryLink = $entry.find('link[rel=\"alternate\"], link:not([rel])').attr('href') || '';\n const entryId = $entry.find('id').text();\n const published = $entry.find('published').text();\n const updated = $entry.find('updated').text();\n\n // Resolve link with fallback to id if it's a URL\n const resolvedLink = this.resolveLink(entryLink, entryId, resolveBase);\n\n return {\n id: entryId,\n title: $entry.find('title').text(),\n link: resolvedLink,\n description: this.parseText($entry.find('summary')),\n content: this.parseText($entry.find('content')),\n author: $entry.find('author name').text() || undefined,\n publishedAt: this.parseDate(published),\n rawPublishedAt: published || updated || undefined,\n updatedAt: this.parseDate(updated),\n categories: this.parseCategories(\n $entry\n .find('category')\n .map((_, c) => $(c).attr('term'))\n .get()\n ),\n customFields: this.extractCustomFields($entry),\n };\n })\n .get();\n\n return {\n data: {\n format: 'atom',\n title: feed.find('> title').text(),\n description: feed.find('> subtitle').text() || undefined,\n link: this.resolveUrl(feedLink, resolveBase),\n next: nextLink ? this.resolveUrl(nextLink, resolveBase) : undefined,\n language: feed.attr('xml:lang') || undefined,\n lastBuildDate: this.parseDate(feed.find('> updated').text()),\n copyright: feed.find('> rights').text() || undefined,\n items,\n customFields: this.extractCustomFields(feed),\n },\n meta: {\n generator: feed.find('> generator').text() || undefined,\n image: this.parseAtomImage(feed, resolveBase),\n categories: this.parseCategories(\n feed\n .find('> category')\n .map((_, c) => $(c).attr('term'))\n .get()\n ),\n },\n };\n }\n\n private parseRSS1($: cheerio.CheerioAPI, baseUrl?: string): ParserResult<ParsedFeed, FeedMeta> {\n const channel = $('channel');\n const feedLink = channel.find('link').text();\n const resolveBase = baseUrl || feedLink;\n\n // RSS 1.0 items are siblings of channel, not children\n const items: FeedItem[] = $('item')\n .map((_, el) => {\n const $item = $(el);\n const itemLink = $item.find('link').text();\n const rdfAbout = $item.attr('rdf:about') || '';\n const dcDate = $item.find('dc\\\\:date').text();\n\n // Resolve link with fallback to rdf:about\n const resolvedLink = this.resolveLink(itemLink, rdfAbout, resolveBase);\n\n // Extract dc:subject as categories (RSS 1.0 uses Dublin Core)\n const dcSubjects = $item\n .find('dc\\\\:subject')\n .map((_, s) => $(s).text())\n .get();\n\n return {\n id: rdfAbout || itemLink,\n title: $item.find('title').text(),\n link: resolvedLink,\n description: this.parseText($item.find('description')),\n content: this.parseContentEncoded($item.find('content\\\\:encoded')),\n author: $item.find('dc\\\\:creator').text() || undefined,\n publishedAt: this.parseDate(dcDate),\n rawPublishedAt: dcDate || undefined,\n categories: this.parseCategories(dcSubjects),\n customFields: this.extractCustomFields($item),\n };\n })\n .get();\n\n // Parse RDF image element (sibling of channel)\n const rdfImage = $('image');\n const imageUrl = rdfImage.find('url').text() || rdfImage.attr('rdf:resource');\n\n return {\n data: {\n format: 'rss1',\n title: channel.find('title').text(),\n description: channel.find('description').text() || undefined,\n link: this.resolveUrl(feedLink, resolveBase),\n language: channel.find('dc\\\\:language').text() || undefined,\n lastBuildDate: this.parseDate(channel.find('dc\\\\:date').text()),\n copyright: channel.find('dc\\\\:rights').text() || undefined,\n items,\n customFields: this.extractCustomFields(channel),\n },\n meta: {\n generator: channel.find('admin\\\\:generatorAgent').attr('rdf:resource') || undefined,\n image: imageUrl\n ? {\n url: this.resolveUrl(imageUrl, resolveBase),\n title: rdfImage.find('title').text() || undefined,\n link: this.resolveUrl(rdfImage.find('link').text(), resolveBase) || undefined,\n }\n : undefined,\n categories: this.parseCategories(\n channel\n .find('dc\\\\:subject')\n .map((_, s) => $(s).text())\n .get()\n ),\n },\n };\n }\n\n /**\n * Extract custom fields from an element using configured selectors.\n */\n private extractCustomFields($el: cheerio.Cheerio<Element>): Record<string, string> | undefined {\n if (Object.keys(this.customFields).length === 0) return undefined;\n\n const fields: Record<string, string> = {};\n for (const [key, selector] of Object.entries(this.customFields)) {\n const value = $el.find(selector).text().trim();\n if (value) {\n fields[key] = value;\n }\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n }\n\n /**\n * Parse date string to ISO 8601 format.\n * Returns undefined if parsing fails (never returns raw strings).\n */\n private parseDate(dateStr: string): string | undefined {\n if (!dateStr?.trim()) return undefined;\n\n try {\n const date = new Date(dateStr);\n // Check for invalid date\n if (Number.isNaN(date.getTime())) {\n return undefined;\n }\n return date.toISOString();\n } catch {\n return undefined;\n }\n }\n\n /**\n * Parse element text content, returning undefined if empty.\n */\n private parseText($el: cheerio.Cheerio<Element>): string | undefined {\n const text = $el.text().trim();\n return text || undefined;\n }\n\n /**\n * Parse content:encoded, stripping HTML tags from CDATA content.\n */\n private parseContentEncoded($el: cheerio.Cheerio<Element>): string | undefined {\n const raw = $el.text().trim();\n if (!raw) return undefined;\n return raw.replace(/<[^>]+>/g, '').trim() || undefined;\n }\n\n /**\n * Parse categories/tags, filtering out empty strings.\n */\n private parseCategories(categories: (string | undefined)[]): string[] {\n return categories.map((c) => c?.trim()).filter((c): c is string => !!c && c.length > 0);\n }\n\n /**\n * Resolve a URL against a base URL.\n * Only allows https scheme; all other schemes are rejected.\n */\n private resolveUrl(url: string, base?: string): string {\n if (!url?.trim()) return '';\n\n try {\n const resolved = base ? new URL(url, base) : new URL(url);\n return resolved.protocol === 'https:' ? resolved.href : '';\n } catch {\n return '';\n }\n }\n\n /**\n * Resolve link with fallback to id/guid if primary link is empty and id is a URL.\n * Only allows https scheme; all other schemes are rejected.\n */\n private resolveLink(primaryLink: string, fallbackId: string, base?: string): string {\n // Try primary link first\n if (primaryLink?.trim()) {\n return this.resolveUrl(primaryLink, base);\n }\n\n // Fallback to id if it looks like a URL\n if (fallbackId?.trim()) {\n try {\n const resolvedId = new URL(fallbackId);\n return resolvedId.protocol === 'https:' ? resolvedId.href : '';\n } catch {\n // Not a valid URL, try resolving against base\n return this.resolveUrl(fallbackId, base);\n }\n }\n\n return '';\n }\n\n /**\n * Parse enclosure element with URL resolution.\n */\n private parseEnclosure(\n $enc: cheerio.Cheerio<Element>,\n baseUrl?: string\n ): FeedEnclosure | undefined {\n const url = $enc.attr('url');\n if (!url) return undefined;\n\n return {\n url: this.resolveUrl(url, baseUrl),\n type: $enc.attr('type') || undefined,\n length: this.parseNumber($enc.attr('length')),\n };\n }\n\n /**\n * Parse RSS image element with URL resolution.\n */\n private parseImage(\n $img: cheerio.Cheerio<Element>,\n baseUrl?: string\n ): FeedMeta['image'] | undefined {\n const url = $img.find('url').text();\n if (!url) return undefined;\n\n return {\n url: this.resolveUrl(url, baseUrl),\n title: $img.find('title').text() || undefined,\n link: this.resolveUrl($img.find('link').text(), baseUrl) || undefined,\n };\n }\n\n /**\n * Parse Atom logo/icon element with URL resolution.\n */\n private parseAtomImage(\n $feed: cheerio.Cheerio<Element>,\n baseUrl?: string\n ): FeedMeta['image'] | undefined {\n const logo = $feed.find('> logo').text();\n const icon = $feed.find('> icon').text();\n const url = logo || icon;\n\n if (!url) return undefined;\n\n return {\n url: this.resolveUrl(url, baseUrl),\n };\n }\n\n /**\n * Parse a string to number, returning undefined if invalid.\n */\n private parseNumber(value: string | undefined): number | undefined {\n if (!value) return undefined;\n const num = parseInt(value, 10);\n return Number.isNaN(num) ? undefined : num;\n }\n}\n"],"mappings":"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"}