{"version":3,"file":"scrapper-DiQ5FfDL.mjs","names":[],"sources":["../src/batteries/tools/scrapper/exceptions.ts","../src/batteries/tools/scrapper/shared.ts","../src/batteries/tools/scrapper/index.ts"],"sourcesContent":["/**\n * Battery-scoped exception constructors for the Scrapper web-extraction tool.\n *\n * @remarks\n * Battery-scoped exception classes owned by the Scrapper tool battery (not the ADK core). Minted\n * via `createException` from `@nhtio/adk/factories` and re-exported from the battery's barrel\n * (`@nhtio/adk/batteries/tools/scrapper`). This file intentionally carries **no** `@module` tag:\n * it is an internal sibling relative-imported by `index.ts`, so it does not mint its own\n * `…/scrapper/exceptions` entrypoint (whose `exceptions` leaf basename would collide with sibling\n * batteries under the `vite-plugin-dts` rolled-up `.d.ts` rule).\n */\n\nimport { createException } from '@nhtio/adk/factories'\n\n/**\n * Thrown when a Scrapper factory receives invalid configuration.\n *\n * @remarks\n * Fatal: config bugs (missing/unparseable `instanceUrl`, a bad `artifact` resolver, or an async\n * resolver passed to a `*Sync` factory) fail loud at factory-call time rather than at first scrape.\n */\nexport const E_INVALID_SCRAPPER_CONFIG = createException<[string]>(\n  'E_INVALID_SCRAPPER_CONFIG',\n  'Invalid Scrapper tool config: %s',\n  'E_INVALID_SCRAPPER_CONFIG',\n  529,\n  true\n)\n","/**\n * Internal core shared by both Scrapper verbs (article / links).\n *\n * @remarks\n * No `@module` tag — this is a sibling of `index.ts`, relative-imported, not its own entrypoint.\n * Houses the per-parameter disposition machinery (schema building from `fixed`/`defaults`), the\n * snake→kebab wire mapping, the request/response contexts, and the `fetch`+pipeline handler core.\n * Generic harness helpers (artifact/header resolution, pipeline runners) come from `../_shared`.\n */\n\nimport { Tool } from '@nhtio/adk/forge'\nimport { isError } from '@nhtio/adk/guards'\nimport { validator } from '@nhtio/validation'\nimport { Middleware } from '@nhtio/middleware'\nimport { E_INVALID_SCRAPPER_CONFIG } from './exceptions'\nimport {\n  resolveHeaders,\n  makeShortCircuit,\n  isShortCircuit,\n  runInputPipeline,\n  runOutputPipeline,\n  runToolGate,\n  type ToolGateFn,\n  type ToolHeaders,\n  type ToolHeadersResolver,\n  type SpooledArtifactCtor,\n  type MiddlewareFn,\n} from '../_shared'\nimport type { Schema } from '@nhtio/validation'\nimport type { NextFn } from '@nhtio/middleware'\n\nconst DEFAULT_REQUEST_TIMEOUT = 65_000\n\n/** Throw the battery-scoped config error. */\nexport const failConfig = (reason: string): never => {\n  throw new E_INVALID_SCRAPPER_CONFIG([reason])\n}\n\n// ── Parameter specs (per-parameter disposition) ──────────────────────────────\n\n/** The wire type of a Scrapper query parameter — controls serialisation. */\nexport type ScrapperParamType = 'string' | 'number' | 'boolean'\n\n/**\n * One curated, model-facing Scrapper parameter: its snake_case key (used in the model schema and\n * in `fixed`/`defaults`), its kebab-case wire name, its type, the base validator, and a description.\n */\nexport interface ScrapperParamSpec {\n  /** snake_case key as seen by the model and in `config.fixed` / `config.defaults`. */\n  key: string\n  /** kebab-case name sent to the Scrapper API. */\n  wire: string\n  /** Wire type, controlling string/number/boolean serialisation. */\n  type: ScrapperParamType\n  /** Base `@nhtio/validation` schema (no `.required()`/`.default()`/`.optional()` applied yet). */\n  schema: Schema\n  /** Human-readable description surfaced to the model. */\n  description: string\n}\n\n/** Serialise a parameter value to its wire string. */\nconst toWire = (value: unknown): string => String(value)\n\n/**\n * Build the model-facing input schema from a verb's param specs and the factory's disposition.\n * `url` is always required. A `fixed` param is omitted (the model can't set it); a `defaults` param\n * gets `.default(value)`; everything else is `.optional()`.\n */\nexport const buildScrapperSchema = (\n  specs: ScrapperParamSpec[],\n  fixed: Record<string, unknown> | undefined,\n  defaults: Record<string, unknown> | undefined,\n  extra: Record<string, Schema> = {}\n): Schema => {\n  const shape: Record<string, Schema> = {\n    url: validator.string().required().description('The absolute URL of the page to load.'),\n  }\n  for (const spec of specs) {\n    if (fixed && spec.key in fixed) continue // pinned → not model-visible\n    let sch = spec.schema\n    if (defaults && spec.key in defaults) {\n      sch = sch.default(defaults[spec.key] as never)\n    } else {\n      sch = sch.optional()\n    }\n    shape[spec.key] = sch.description(spec.description)\n  }\n  return validator.object({ ...shape, ...extra })\n}\n\n/**\n * Assemble the wire-kebab query params for one request: each spec's value is `fixed` (if pinned)\n * else the validated model/default value; then `fixedQuery` raw passthrough is layered on. `url`\n * is handled separately (it is the search target, never pinned).\n *\n * @remarks\n * An empty string (`''`) is deliberately treated the same as `undefined`/`null` and never\n * forwarded — this is what lets the schema's `.allow('')` on optional string specs actually\n * mean \"don't set this,\" instead of sending e.g. `user-agent=` to the wire. This applies\n * generically to every spec (no per-spec-type branching), including a `fixed`-pinned value that\n * happens to be `''` — an explicitly pinned empty string still means \"don't send it.\"\n */\nexport const buildWireParams = (\n  args: Record<string, unknown>,\n  specs: ScrapperParamSpec[],\n  fixed: Record<string, unknown> | undefined,\n  fixedQuery: Record<string, string> | undefined\n): Record<string, string> => {\n  const out: Record<string, string> = {}\n  for (const spec of specs) {\n    const value = fixed && spec.key in fixed ? fixed[spec.key] : args[spec.key]\n    const isEmptyString = typeof value === 'string' && value.length === 0\n    if (value !== undefined && value !== null && !isEmptyString) out[spec.wire] = toWire(value)\n  }\n  for (const [k, v] of Object.entries(fixedQuery ?? {})) out[k] = v\n  return out\n}\n\n// ── Contexts ─────────────────────────────────────────────────────────────────\n\n/**\n * Mutable context handed to each input-pipeline stage **before** the HTTP request is sent.\n * Identical for both verbs.\n */\nexport interface ScrapperRequestContext {\n  /** The tool's name (read-only). */\n  readonly toolName: string\n  /** The target page URL (the `url` argument). Mutable. */\n  url: string\n  /** Wire-kebab query params (everything except `url`). Mutable. */\n  params: Record<string, string>\n  /** Resolved request headers sent to the SCRAPPER INSTANCE (auth). Mutable. */\n  headers: ToolHeaders\n  /** The Scrapper instance base URL (read-only). */\n  readonly instanceUrl: string\n  /** Cross-stage scratch space; also carried onto the response context. */\n  readonly stash: Map<string, unknown>\n  /** Skip the fetch and return `result` verbatim as the tool's output (e.g. a cache hit). */\n  shortCircuit(result: string): void\n}\n\n/**\n * Mutable context handed to each output-pipeline stage **after** the response JSON is parsed.\n *\n * @typeParam R - The verb's normalised result type (article object or links payload).\n */\nexport interface ScrapperResponseContext<R> {\n  /** The tool's name (read-only). */\n  readonly toolName: string\n  /** The request context as it was sent (post-input-pipeline). */\n  readonly request: ScrapperRequestContext\n  /** The parsed Scrapper JSON body. Mutable (used when `format` is `raw`). */\n  raw: unknown\n  /** The normalised result. Mutable — reshape, redact, enrich. */\n  result: R\n  /** The effective payload shape for this call. */\n  format: 'normalized' | 'raw'\n  /** When set, used verbatim as the tool's output (overrides serialisation). */\n  output?: string\n  /** Cross-stage scratch space; carried over from the request context. */\n  readonly stash: Map<string, unknown>\n}\n\n/** An input-pipeline stage. Onion middleware over {@link ScrapperRequestContext}. */\nexport type ScrapperInputMiddlewareFn = (\n  ctx: ScrapperRequestContext,\n  next: NextFn\n) => void | Promise<void>\n\n/** An output-pipeline stage over a verb's {@link ScrapperResponseContext}. */\nexport type ScrapperOutputMiddlewareFn<R> = (\n  ctx: ScrapperResponseContext<R>,\n  next: NextFn\n) => void | Promise<void>\n\n// ── Config (shared shape; `artifact` variant supplied by the verb factory) ────\n\n/** Configuration common to every Scrapper factory. `A` is the accepted `artifact` resolver type. */\nexport interface ScrapperBaseConfig<P, R, A> {\n  /** Base URL of the Scrapper instance, e.g. `https://scrapper.example.org`. Required. */\n  instanceUrl: string\n  /** Headers sent to the Scrapper INSTANCE for auth (X-API-Key / Basic) — static or resolver. */\n  headers?: ToolHeaders | ToolHeadersResolver\n  /** The tool's own `fetch` AbortController timeout in ms. Default `65_000` (> Scrapper's 60s browser default). */\n  requestTimeoutMs?: number\n  /** Output shape. `normalized`/`raw` pin it; `either` (default) exposes a `format` arg to the model. */\n  resultFormat?: 'normalized' | 'raw' | 'either'\n  /** Spool-artifact resolver for the output. Default `() => SpooledJsonArtifact`. */\n  artifact?: A\n  /** Tool name override. */\n  name?: string\n  /** Tool description override. */\n  description?: string\n  /** Pinned params — sent always, removed from the model schema. */\n  fixed?: Partial<P>\n  /** Model-overridable default param values. */\n  defaults?: Partial<P>\n  /** Raw, un-modeled wire params (kebab keys) — always sent, never model-visible. Keeps the battery generic. */\n  fixedQuery?: Record<string, string>\n  /**\n   * Optional per-call gate run before the HTTP request — the seam for human-approval/RBAC\n   * flows built on `ctx.waitFor` (the ADK gates primitive). Throwing aborts the call through\n   * the standard tool-error path. Scraping reaches the network on the agent's behalf, which\n   * makes every call a candidate for gating.\n   */\n  gate?: ToolGateFn\n  /** Stages run before the HTTP request. See {@link ScrapperRequestContext}. */\n  inputPipeline?: ScrapperInputMiddlewareFn[]\n  /** Stages run after the response is parsed. See {@link ScrapperResponseContext}. */\n  outputPipeline?: ScrapperOutputMiddlewareFn<R>[]\n}\n\n/** Verb-specific wiring passed to {@link assembleScrapperTool}. */\nexport interface ScrapperVerb<R> {\n  /** Scrapper endpoint path, e.g. `/api/article`. */\n  endpoint: string\n  /** The curated param specs for this verb. */\n  specs: ScrapperParamSpec[]\n  /** Default tool name (`scrapper_article` / `scrapper_links`). */\n  defaultName: string\n  /** Default tool description. */\n  defaultDescription: string\n  /** Map a parsed Scrapper body to the verb's normalised result. */\n  normalize: (body: Record<string, unknown>) => R\n}\n\n/** Parse a Scrapper error body (`{ detail: [{ msg }] }`) into a single message, best-effort. */\nconst parseScrapperError = (body: unknown, status: number, statusText: string): string => {\n  if (body && typeof body === 'object' && Array.isArray((body as { detail?: unknown }).detail)) {\n    const detail = (body as { detail: Array<{ msg?: unknown; loc?: unknown }> }).detail\n    const msgs = detail\n      .map((d) => {\n        const loc = Array.isArray(d.loc) ? d.loc.join('.') : undefined\n        const msg = typeof d.msg === 'string' ? d.msg : undefined\n        if (msg && loc) return `${loc}: ${msg}`\n        return msg ?? loc\n      })\n      .filter((m): m is string => typeof m === 'string')\n    if (msgs.length) return msgs.join('; ')\n  }\n  return `HTTP ${status} ${statusText}`.trim()\n}\n\n/**\n * Build a configured Scrapper {@link Tool} from validated config + an already-resolved sync\n * artifact constructor. Shared by every verb and by both the async and sync factories.\n */\nexport const assembleScrapperTool = <P, R>(\n  verb: ScrapperVerb<R>,\n  config: ScrapperBaseConfig<P, R, unknown>,\n  instanceUrl: string,\n  artifactConstructor: () => SpooledArtifactCtor\n): Tool => {\n  const requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT\n  const resultFormat = config.resultFormat ?? 'either'\n  const toolName = config.name ?? verb.defaultName\n  const fixed = config.fixed as Record<string, unknown> | undefined\n  const defaults = config.defaults as Record<string, unknown> | undefined\n\n  const inputMw = new Middleware<MiddlewareFn<ScrapperRequestContext>>()\n  for (const fn of config.inputPipeline ?? []) inputMw.add(fn)\n  const outputMw = new Middleware<MiddlewareFn<ScrapperResponseContext<R>>>()\n  for (const fn of config.outputPipeline ?? [])\n    outputMw.add(fn as MiddlewareFn<ScrapperResponseContext<R>>)\n  const hasInput = (config.inputPipeline ?? []).length > 0\n  const hasOutput = (config.outputPipeline ?? []).length > 0\n\n  // Build the schema, conditionally exposing `format` only when the factory is neutral.\n  const extra: Record<string, Schema> =\n    resultFormat === 'either'\n      ? {\n          format: validator\n            .string()\n            .valid('normalized', 'raw')\n            .default('normalized')\n            .description('Output shape: \"normalized\" (trimmed) or \"raw\" (full Scrapper JSON).'),\n        }\n      : {}\n  const inputSchema = buildScrapperSchema(verb.specs, fixed, defaults, extra)\n\n  return new Tool({\n    name: toolName,\n    description: config.description ?? verb.defaultDescription,\n    inputSchema,\n    artifactConstructor,\n    handler: async (args, handlerCtx) => {\n      const a = args as Record<string, unknown> & { url: string; format?: 'normalized' | 'raw' }\n      await runToolGate(config.gate, handlerCtx, toolName, args)\n      try {\n        const format: 'normalized' | 'raw' =\n          resultFormat === 'either' ? (a.format ?? 'normalized') : resultFormat\n\n        const headers: ToolHeaders = {\n          Accept: 'application/json',\n          ...(await resolveHeaders(config.headers)),\n        }\n\n        const params = buildWireParams(a, verb.specs, fixed, config.fixedQuery)\n        const stash = new Map<string, unknown>()\n\n        const requestCtx: ScrapperRequestContext = {\n          toolName,\n          url: a.url,\n          params,\n          headers,\n          instanceUrl,\n          stash,\n          shortCircuit: makeShortCircuit(),\n        }\n\n        if (hasInput) {\n          const short = await runInputPipeline(inputMw, requestCtx, 'Scrapper')\n          if (short !== undefined) return short\n        }\n\n        const url = new URL(verb.endpoint, instanceUrl + '/')\n        url.searchParams.set('url', requestCtx.url)\n        for (const [k, v] of Object.entries(requestCtx.params)) url.searchParams.set(k, v)\n\n        const controller = new AbortController()\n        const timer = setTimeout(() => controller.abort(), requestTimeoutMs)\n        let response: Response\n        try {\n          response = await fetch(url, {\n            method: 'GET',\n            headers: requestCtx.headers,\n            signal: controller.signal,\n          })\n        } finally {\n          clearTimeout(timer)\n        }\n\n        if (!response.ok) {\n          let body: unknown\n          try {\n            body = await response.json()\n          } catch {\n            body = undefined\n          }\n          return `Error: Scrapper request failed — ${parseScrapperError(body, response.status, response.statusText)}.`\n        }\n\n        const body = (await response.json()) as Record<string, unknown>\n\n        const responseCtx: ScrapperResponseContext<R> = {\n          toolName,\n          request: requestCtx,\n          raw: body,\n          result: verb.normalize(body),\n          format,\n          stash,\n        }\n\n        if (hasOutput) await runOutputPipeline(outputMw, responseCtx, 'Scrapper')\n\n        if (typeof responseCtx.output === 'string') return responseCtx.output\n        if (responseCtx.format === 'raw') return JSON.stringify(responseCtx.raw, null, 2)\n        return JSON.stringify(responseCtx.result, null, 2)\n      } catch (err) {\n        if (isShortCircuit(err)) return err.result\n        return `Error: ${isError(err) ? err.message : String(err)}`\n      }\n    },\n  })\n}\n\n/** Validate `instanceUrl` and return the trailing-slash-normalised base. */\nexport const validateScrapperInstanceUrl = (config: { instanceUrl?: string }): string => {\n  if (typeof config?.instanceUrl !== 'string' || config.instanceUrl.trim() === '') {\n    failConfig('instanceUrl is required')\n  }\n  try {\n    new URL(config.instanceUrl as string)\n  } catch {\n    failConfig(`instanceUrl is not a valid URL: ${config.instanceUrl}`)\n  }\n  return (config.instanceUrl as string).replace(/\\/+$/, '')\n}\n","/**\n * Factories for configured Scrapper web-extraction tools (article + links).\n *\n * @module @nhtio/adk/batteries/tools/scrapper\n *\n * @remarks\n * [Scrapper](https://github.com/amerkurev/scrapper) is a self-hosted service that loads a page in a\n * real headless browser and extracts either the readable article (`/api/article`) or the page's\n * links (`/api/links`). It gives an agent browser-grade reading power — JS-rendered pages a\n * renderless fetcher can't see — but as a **stateless** HTTP call: each request runs in a fresh\n * incognito context, stores no session or credentials, and shares nothing with any other call.\n *\n * Like the SearXNG battery, this exports **factories** (not ready-made `Tool` constants), because a\n * scrape tool needs per-deployment config (instance URL + custom auth headers). Two verbs, each with\n * an async factory ({@link createScrapperArticleTool} / {@link createScrapperLinksTool}, accepting a\n * dynamic-import `artifact` resolver) and a sync variant ({@link createScrapperArticleToolSync} /\n * {@link createScrapperLinksToolSync}). Because these are factories, they MUST NOT be bulk-registered\n * via `Object.values(batteries)` — call one, then register the returned tool.\n *\n * @see https://github.com/amerkurev/scrapper\n */\n\nimport { validator } from '@nhtio/validation'\nimport { SpooledJsonArtifact } from '@nhtio/adk/spooled_artifact'\nimport { resolveArtifact, resolveArtifactSync } from '../_shared'\nimport {\n  failConfig,\n  validateScrapperInstanceUrl,\n  assembleScrapperTool,\n  type ScrapperBaseConfig,\n  type ScrapperParamSpec,\n  type ScrapperVerb,\n} from './shared'\nimport type { Tool } from '@nhtio/adk/forge'\nimport type { ArtifactResolver, SyncArtifactResolver } from '../_shared'\n\nexport { E_INVALID_SCRAPPER_CONFIG } from './exceptions'\nexport type {\n  ScrapperRequestContext,\n  ScrapperResponseContext,\n  ScrapperInputMiddlewareFn,\n  ScrapperOutputMiddlewareFn,\n} from './shared'\n\n// ── Param sets ───────────────────────────────────────────────────────────────\n\n/** Model-facing params common to both verbs (snake_case; mapped to kebab on the wire). */\nexport interface ScrapperCommonParams {\n  /** Return a cached result when available instead of re-scraping. */\n  cache?: boolean\n  /** Capture a screenshot; the result carries a `screenshotUri`. */\n  screenshot?: boolean\n  /** Run in an incognito browser context (no persisted browsing data). Default true upstream. */\n  incognito?: boolean\n  /** Browser navigation timeout in ms (`0` disables). Distinct from the tool's own fetch timeout. */\n  timeout?: number\n  /** When navigation is considered finished. */\n  wait_until?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit'\n  /** Wait this many ms after load before parsing. */\n  sleep?: number\n  /** Scroll down N pixels for lazy-loading pages. Requires a positive `sleep`. */\n  scroll_down?: number\n  /** Emulated device, e.g. `Desktop Chrome`. Overrides individual viewport/UA settings. */\n  device?: string\n  /** Explicit user-agent (prefer `device`). */\n  user_agent?: string\n  /** Extra headers the SCRAPER's browser sends to the TARGET site, `K:v;K2:v2` (NOT instance auth). */\n  extra_http_headers?: string\n  /** Upstream proxy, e.g. `http://host:3128` or `socks5://host:1080`. */\n  proxy_server?: string\n}\n\n/** Model-facing params for `/api/article`. */\nexport interface ScrapperArticleParams extends ScrapperCommonParams {\n  /** Populate `fullContent` with the page's full HTML. */\n  full_content?: boolean\n}\n\n/** Model-facing params for `/api/links`. */\nexport interface ScrapperLinksParams extends ScrapperCommonParams {\n  /** Median link-text length threshold for the link parser. */\n  text_len_threshold?: number\n  /** Median words-per-link threshold for the link parser. */\n  words_threshold?: number\n}\n\nconst commonSpecs: ScrapperParamSpec[] = [\n  {\n    key: 'cache',\n    wire: 'cache',\n    type: 'boolean',\n    schema: validator.boolean(),\n    description: 'Return a cached result when available instead of re-scraping.',\n  },\n  {\n    key: 'screenshot',\n    wire: 'screenshot',\n    type: 'boolean',\n    schema: validator.boolean(),\n    description: 'Capture a screenshot; the result carries a screenshotUri.',\n  },\n  {\n    key: 'incognito',\n    wire: 'incognito',\n    type: 'boolean',\n    schema: validator.boolean(),\n    description: 'Run in an incognito browser context (no persisted data).',\n  },\n  {\n    key: 'timeout',\n    wire: 'timeout',\n    type: 'number',\n    schema: validator.number().min(0),\n    description: 'Browser navigation timeout in ms (0 disables).',\n  },\n  {\n    key: 'wait_until',\n    wire: 'wait-until',\n    type: 'string',\n    schema: validator.string().valid('load', 'domcontentloaded', 'networkidle', 'commit'),\n    description: 'When navigation is considered finished.',\n  },\n  {\n    key: 'sleep',\n    wire: 'sleep',\n    type: 'number',\n    schema: validator.number().min(0),\n    description: 'Wait this many ms after load before parsing.',\n  },\n  {\n    key: 'scroll_down',\n    wire: 'scroll-down',\n    type: 'number',\n    schema: validator.number().min(0),\n    description: 'Scroll down N pixels for lazy-loading pages (requires a positive sleep).',\n  },\n  {\n    key: 'device',\n    wire: 'device',\n    type: 'string',\n    schema: validator.string().allow(''),\n    description:\n      'Emulated device, e.g. \"Desktop Chrome\". Omit or send an empty string to leave it unset.',\n  },\n  {\n    key: 'user_agent',\n    wire: 'user-agent',\n    type: 'string',\n    schema: validator.string().allow(''),\n    description:\n      'Explicit user-agent (prefer device). Omit or send an empty string to leave it unset.',\n  },\n  {\n    key: 'extra_http_headers',\n    wire: 'extra-http-headers',\n    type: 'string',\n    schema: validator.string().allow(''),\n    description:\n      'Extra headers the scraper sends to the TARGET site, formatted \"K:v;K2:v2\". Omit or send an empty string to send none.',\n  },\n  {\n    key: 'proxy_server',\n    wire: 'proxy-server',\n    type: 'string',\n    schema: validator.string().allow(''),\n    description:\n      'Upstream proxy, e.g. \"http://host:3128\" or \"socks5://host:1080\". Omit or send an empty string to use no proxy.',\n  },\n]\n\nconst articleSpecs: ScrapperParamSpec[] = [\n  ...commonSpecs,\n  {\n    key: 'full_content',\n    wire: 'full-content',\n    type: 'boolean',\n    schema: validator.boolean(),\n    description: 'Populate fullContent with the page full HTML.',\n  },\n]\n\nconst linksSpecs: ScrapperParamSpec[] = [\n  ...commonSpecs,\n  {\n    key: 'text_len_threshold',\n    wire: 'text-len-threshold',\n    type: 'number',\n    schema: validator.number().min(0),\n    description: 'Median link-text length threshold for the link parser.',\n  },\n  {\n    key: 'words_threshold',\n    wire: 'words-threshold',\n    type: 'number',\n    schema: validator.number().min(0),\n    description: 'Median words-per-link threshold for the link parser.',\n  },\n]\n\n// ── Normalised result shapes ─────────────────────────────────────────────────\n\n/** A normalised Scrapper article (loose/nullable upstream). */\nexport interface ScrapperArticle {\n  /** The page URL the article was extracted from. */\n  url?: string\n  /** Article title. */\n  title?: string\n  /** Author / byline metadata. */\n  byline?: string\n  /** Short excerpt or description of the article. */\n  excerpt?: string\n  /** Name of the site the article came from. */\n  siteName?: string\n  /** Detected content language. */\n  lang?: string\n  /** Character count of the extracted article text. */\n  length?: number\n  /** Publication time, when the page exposed one. */\n  publishedTime?: string\n  /** Scrapper's own date field for the result. */\n  date?: string\n  /** Article text with HTML stripped. */\n  textContent?: string\n  /** Processed article HTML; present when the caller requested it. */\n  content?: string\n  /** Full page HTML; present only when `full_content` was set. */\n  fullContent?: string\n  /** Screenshot URI; present only when `screenshot` was set. */\n  screenshotUri?: string\n}\n\n/** A single link from `/api/links` (verified live: `{ url, text }`). */\nexport interface ScrapperLink {\n  /** The link's target URL. */\n  url?: string\n  /** The link's anchor text. */\n  text?: string\n}\n\n/** A normalised Scrapper links payload. */\nexport interface ScrapperLinks {\n  /** The page URL the links were collected from. */\n  url?: string\n  /** The page title. */\n  title?: string\n  /** The page's domain. */\n  domain?: string\n  /** Scrapper's own date field for the result. */\n  date?: string\n  /** The collected links, each `{ url, text }`. */\n  links: ScrapperLink[]\n  /** Screenshot URI; present only when `screenshot` was set. */\n  screenshotUri?: string\n}\n\nconst str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)\nconst num = (v: unknown): number | undefined => (typeof v === 'number' ? v : undefined)\n\nconst normalizeArticle = (body: Record<string, unknown>): ScrapperArticle => {\n  const out: ScrapperArticle = {}\n  out.url = str(body.url)\n  out.title = str(body.title)\n  out.byline = str(body.byline)\n  out.excerpt = str(body.excerpt)\n  out.siteName = str(body.siteName)\n  out.lang = str(body.lang)\n  out.length = num(body.length)\n  out.publishedTime = str(body.publishedTime)\n  out.date = str(body.date)\n  out.textContent = str(body.textContent)\n  if (str(body.content)) out.content = str(body.content)\n  if (str(body.fullContent)) out.fullContent = str(body.fullContent)\n  if (str(body.screenshotUri)) out.screenshotUri = str(body.screenshotUri)\n  // Drop undefined keys so the serialised payload stays tight.\n  return JSON.parse(JSON.stringify(out)) as ScrapperArticle\n}\n\nconst normalizeLinks = (body: Record<string, unknown>): ScrapperLinks => {\n  const rawLinks = Array.isArray(body.links) ? body.links : []\n  const links: ScrapperLink[] = rawLinks.map((l) => {\n    const r = (l ?? {}) as Record<string, unknown>\n    const item: ScrapperLink = {}\n    if (str(r.url)) item.url = str(r.url)\n    if (str(r.text)) item.text = str(r.text)\n    return item\n  })\n  const out: ScrapperLinks = { links }\n  if (str(body.url)) out.url = str(body.url)\n  if (str(body.title)) out.title = str(body.title)\n  if (str(body.domain)) out.domain = str(body.domain)\n  if (str(body.date)) out.date = str(body.date)\n  if (str(body.screenshotUri)) out.screenshotUri = str(body.screenshotUri)\n  return out\n}\n\nconst articleVerb: ScrapperVerb<ScrapperArticle> = {\n  endpoint: '/api/article',\n  specs: articleSpecs,\n  defaultName: 'scrapper_article',\n  defaultDescription:\n    'Load a web page in a real (headless) browser and extract its readable article — title, ' +\n    'byline, text content, and metadata. Renders JavaScript-heavy pages a plain fetch cannot. ' +\n    'Each call is stateless (fresh incognito context, no stored session).',\n  normalize: normalizeArticle,\n}\n\nconst linksVerb: ScrapperVerb<ScrapperLinks> = {\n  endpoint: '/api/links',\n  specs: linksSpecs,\n  defaultName: 'scrapper_links',\n  defaultDescription:\n    'Load a web page in a real (headless) browser and collect its article/navigation links ' +\n    '(each { url, text }). Renders JavaScript-heavy index pages a plain fetch cannot. ' +\n    'Each call is stateless (fresh incognito context, no stored session).',\n  normalize: normalizeLinks,\n}\n\n// ── Config aliases ────────────────────────────────────────────────────────────\n\nexport type { ScrapperBaseConfig } from './shared'\n\n/** Async-factory config for `/api/article` (full `artifact` resolver, incl. dynamic import). */\nexport type ScrapperArticleConfig = ScrapperBaseConfig<\n  ScrapperArticleParams,\n  ScrapperArticle,\n  ArtifactResolver\n>\n/** Sync-factory config for `/api/article` (`artifact` narrowed to the sync subset). */\nexport type ScrapperArticleConfigSync = ScrapperBaseConfig<\n  ScrapperArticleParams,\n  ScrapperArticle,\n  SyncArtifactResolver\n>\n/** Async-factory config for `/api/links`. */\nexport type ScrapperLinksConfig = ScrapperBaseConfig<\n  ScrapperLinksParams,\n  ScrapperLinks,\n  ArtifactResolver\n>\n/** Sync-factory config for `/api/links`. */\nexport type ScrapperLinksConfigSync = ScrapperBaseConfig<\n  ScrapperLinksParams,\n  ScrapperLinks,\n  SyncArtifactResolver\n>\n\nconst defaultArtifact = () => SpooledJsonArtifact\n\n// ── Factories ──────────────────────────────────────────────────────────────────\n\n/**\n * Create a configured Scrapper **article** {@link Tool} (async — accepts a dynamic-import `artifact`).\n *\n * @remarks\n * Async because `artifact` may be an async / dynamic-import resolver, which must resolve to the sync\n * `() => Ctor` `Tool.artifactConstructor` requires before the tool is built. For the common case,\n * use {@link createScrapperArticleToolSync} and skip the `await`.\n *\n * @warning\n * Two distinct \"headers\": `config.headers` authenticates to the Scrapper *instance*; the\n * `extra_http_headers` *parameter* is what the scraper's browser sends to the *target site* — do not\n * conflate them. Also note `scroll_down` requires a positive `sleep`, and `resultUri`/`screenshotUri`\n * are instance-relative and may come back `http://` even over HTTPS — do not assume they match\n * `instanceUrl`.\n *\n * @param config - Instance URL, instance-auth headers, output policy, `artifact` resolver,\n *   per-parameter disposition (`fixed`/`defaults`/`fixedQuery`), and middleware pipelines.\n * @returns A promise of a `Tool` ready to register in a `ToolRegistry`.\n * @throws {@link E_INVALID_SCRAPPER_CONFIG} when `instanceUrl` or `artifact` is invalid.\n */\nexport const createScrapperArticleTool = async (config: ScrapperArticleConfig): Promise<Tool> => {\n  const instanceUrl = validateScrapperInstanceUrl(config)\n  const artifact = await resolveArtifact(config.artifact ?? defaultArtifact, failConfig)\n  return assembleScrapperTool(articleVerb, config, instanceUrl, artifact)\n}\n\n/**\n * Synchronous {@link createScrapperArticleTool} — `artifact` narrowed to the sync subset.\n *\n * @param config - Same as {@link createScrapperArticleTool}, with a sync-only `artifact`.\n * @returns A `Tool` ready to register in a `ToolRegistry`.\n * @throws {@link E_INVALID_SCRAPPER_CONFIG} when `instanceUrl` or `artifact` is invalid (incl. an async resolver).\n */\nexport const createScrapperArticleToolSync = (config: ScrapperArticleConfigSync): Tool => {\n  const instanceUrl = validateScrapperInstanceUrl(config)\n  const artifact = resolveArtifactSync(config.artifact ?? defaultArtifact, failConfig)\n  return assembleScrapperTool(articleVerb, config, instanceUrl, artifact)\n}\n\n/**\n * Create a configured Scrapper **links** {@link Tool} (async — accepts a dynamic-import `artifact`).\n *\n * @remarks\n * See {@link createScrapperArticleTool} for the two-headers caveat and the async rationale. Each\n * `links` item is `{ url, text }`.\n *\n * @param config - Instance URL, instance-auth headers, output policy, `artifact` resolver,\n *   per-parameter disposition, and middleware pipelines.\n * @returns A promise of a `Tool` ready to register in a `ToolRegistry`.\n * @throws {@link E_INVALID_SCRAPPER_CONFIG} when `instanceUrl` or `artifact` is invalid.\n */\nexport const createScrapperLinksTool = async (config: ScrapperLinksConfig): Promise<Tool> => {\n  const instanceUrl = validateScrapperInstanceUrl(config)\n  const artifact = await resolveArtifact(config.artifact ?? defaultArtifact, failConfig)\n  return assembleScrapperTool(linksVerb, config, instanceUrl, artifact)\n}\n\n/**\n * Synchronous {@link createScrapperLinksTool} — `artifact` narrowed to the sync subset.\n *\n * @param config - Same as {@link createScrapperLinksTool}, with a sync-only `artifact`.\n * @returns A `Tool` ready to register in a `ToolRegistry`.\n * @throws {@link E_INVALID_SCRAPPER_CONFIG} when `instanceUrl` or `artifact` is invalid (incl. an async resolver).\n */\nexport const createScrapperLinksToolSync = (config: ScrapperLinksConfigSync): Tool => {\n  const instanceUrl = validateScrapperInstanceUrl(config)\n  const artifact = resolveArtifactSync(config.artifact ?? defaultArtifact, failConfig)\n  return assembleScrapperTool(linksVerb, config, instanceUrl, artifact)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,4BAA4B,gBACvC,6BACA,oCACA,6BACA,KACA,IACF;;;;;;;;;;;;ACIA,IAAM,0BAA0B;;AAGhC,IAAa,cAAc,WAA0B;CACnD,MAAM,IAAI,0BAA0B,CAAC,MAAM,CAAC;AAC9C;;AAyBA,IAAM,UAAU,UAA2B,OAAO,KAAK;;;;;;AAOvD,IAAa,uBACX,OACA,OACA,UACA,QAAgC,CAAC,MACtB;CACX,MAAM,QAAgC,EACpC,KAAK,UAAU,OAAO,EAAE,SAAS,EAAE,YAAY,uCAAuC,EACxF;CACA,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,SAAS,KAAK,OAAO,OAAO;EAChC,IAAI,MAAM,KAAK;EACf,IAAI,YAAY,KAAK,OAAO,UAC1B,MAAM,IAAI,QAAQ,SAAS,KAAK,IAAa;OAE7C,MAAM,IAAI,SAAS;EAErB,MAAM,KAAK,OAAO,IAAI,YAAY,KAAK,WAAW;CACpD;CACA,OAAO,UAAU,OAAO;EAAE,GAAG;EAAO,GAAG;CAAM,CAAC;AAChD;;;;;;;;;;;;;AAcA,IAAa,mBACX,MACA,OACA,OACA,eAC2B;CAC3B,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,SAAS,KAAK,OAAO,QAAQ,MAAM,KAAK,OAAO,KAAK,KAAK;EACvE,MAAM,gBAAgB,OAAO,UAAU,YAAY,MAAM,WAAW;EACpE,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,CAAC,eAAe,IAAI,KAAK,QAAQ,OAAO,KAAK;CAC5F;CACA,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,cAAc,CAAC,CAAC,GAAG,IAAI,KAAK;CAChE,OAAO;AACT;;AA+GA,IAAM,sBAAsB,MAAe,QAAgB,eAA+B;CACxF,IAAI,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAS,KAA8B,MAAM,GAAG;EAE5F,MAAM,OADU,KAA6D,OAE1E,KAAK,MAAM;GACV,MAAM,MAAM,MAAM,QAAQ,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,GAAG,IAAI,KAAA;GACrD,MAAM,MAAM,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM,KAAA;GAChD,IAAI,OAAO,KAAK,OAAO,GAAG,IAAI,IAAI;GAClC,OAAO,OAAO;EAChB,CAAC,EACA,QAAQ,MAAmB,OAAO,MAAM,QAAQ;EACnD,IAAI,KAAK,QAAQ,OAAO,KAAK,KAAK,IAAI;CACxC;CACA,OAAO,QAAQ,OAAO,GAAG,aAAa,KAAK;AAC7C;;;;;AAMA,IAAa,wBACX,MACA,QACA,aACA,wBACS;CACT,MAAM,mBAAmB,OAAO,oBAAoB;CACpD,MAAM,eAAe,OAAO,gBAAgB;CAC5C,MAAM,WAAW,OAAO,QAAQ,KAAK;CACrC,MAAM,QAAQ,OAAO;CACrB,MAAM,WAAW,OAAO;CAExB,MAAM,UAAU,IAAI,WAAiD;CACrE,KAAK,MAAM,MAAM,OAAO,iBAAiB,CAAC,GAAG,QAAQ,IAAI,EAAE;CAC3D,MAAM,WAAW,IAAI,WAAqD;CAC1E,KAAK,MAAM,MAAM,OAAO,kBAAkB,CAAC,GACzC,SAAS,IAAI,EAA8C;CAC7D,MAAM,YAAY,OAAO,iBAAiB,CAAC,GAAG,SAAS;CACvD,MAAM,aAAa,OAAO,kBAAkB,CAAC,GAAG,SAAS;CAGzD,MAAM,QACJ,iBAAiB,WACb,EACE,QAAQ,UACL,OAAO,EACP,MAAM,cAAc,KAAK,EACzB,QAAQ,YAAY,EACpB,YAAY,yEAAqE,EACtF,IACA,CAAC;CACP,MAAM,cAAc,oBAAoB,KAAK,OAAO,OAAO,UAAU,KAAK;CAE1E,OAAO,IAAI,KAAK;EACd,MAAM;EACN,aAAa,OAAO,eAAe,KAAK;EACxC;EACA;EACA,SAAS,OAAO,MAAM,eAAe;GACnC,MAAM,IAAI;GACV,MAAM,YAAY,OAAO,MAAM,YAAY,UAAU,IAAI;GACzD,IAAI;IACF,MAAM,SACJ,iBAAiB,WAAY,EAAE,UAAU,eAAgB;IAE3D,MAAM,UAAuB;KAC3B,QAAQ;KACR,GAAI,MAAM,eAAe,OAAO,OAAO;IACzC;IAEA,MAAM,SAAS,gBAAgB,GAAG,KAAK,OAAO,OAAO,OAAO,UAAU;IACtE,MAAM,wBAAQ,IAAI,IAAqB;IAEvC,MAAM,aAAqC;KACzC;KACA,KAAK,EAAE;KACP;KACA;KACA;KACA;KACA,cAAc,iBAAiB;IACjC;IAEA,IAAI,UAAU;KACZ,MAAM,QAAQ,MAAM,iBAAiB,SAAS,YAAY,UAAU;KACpE,IAAI,UAAU,KAAA,GAAW,OAAO;IAClC;IAEA,MAAM,MAAM,IAAI,IAAI,KAAK,UAAU,cAAc,GAAG;IACpD,IAAI,aAAa,IAAI,OAAO,WAAW,GAAG;IAC1C,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,WAAW,MAAM,GAAG,IAAI,aAAa,IAAI,GAAG,CAAC;IAEjF,MAAM,aAAa,IAAI,gBAAgB;IACvC,MAAM,QAAQ,iBAAiB,WAAW,MAAM,GAAG,gBAAgB;IACnE,IAAI;IACJ,IAAI;KACF,WAAW,MAAM,MAAM,KAAK;MAC1B,QAAQ;MACR,SAAS,WAAW;MACpB,QAAQ,WAAW;KACrB,CAAC;IACH,UAAU;KACR,aAAa,KAAK;IACpB;IAEA,IAAI,CAAC,SAAS,IAAI;KAChB,IAAI;KACJ,IAAI;MACF,OAAO,MAAM,SAAS,KAAK;KAC7B,QAAQ;MACN,OAAO,KAAA;KACT;KACA,OAAO,oCAAoC,mBAAmB,MAAM,SAAS,QAAQ,SAAS,UAAU,EAAE;IAC5G;IAEA,MAAM,OAAQ,MAAM,SAAS,KAAK;IAElC,MAAM,cAA0C;KAC9C;KACA,SAAS;KACT,KAAK;KACL,QAAQ,KAAK,UAAU,IAAI;KAC3B;KACA;IACF;IAEA,IAAI,WAAW,MAAM,kBAAkB,UAAU,aAAa,UAAU;IAExE,IAAI,OAAO,YAAY,WAAW,UAAU,OAAO,YAAY;IAC/D,IAAI,YAAY,WAAW,OAAO,OAAO,KAAK,UAAU,YAAY,KAAK,MAAM,CAAC;IAChF,OAAO,KAAK,UAAU,YAAY,QAAQ,MAAM,CAAC;GACnD,SAAS,KAAK;IACZ,IAAI,eAAe,GAAG,GAAG,OAAO,IAAI;IACpC,OAAO,UAAU,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG;GAC1D;EACF;CACF,CAAC;AACH;;AAGA,IAAa,+BAA+B,WAA6C;CACvF,IAAI,OAAO,QAAQ,gBAAgB,YAAY,OAAO,YAAY,KAAK,MAAM,IAC3E,WAAW,yBAAyB;CAEtC,IAAI;EACF,IAAI,IAAI,OAAO,WAAqB;CACtC,QAAQ;EACN,WAAW,mCAAmC,OAAO,aAAa;CACpE;CACA,OAAQ,OAAO,YAAuB,QAAQ,QAAQ,EAAE;AAC1D;;;;;;;;;;;;;;;;;;;;;;;;ACnSA,IAAM,cAAmC;CACvC;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,QAAQ;EAC1B,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,QAAQ;EAC1B,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,QAAQ;EAC1B,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,IAAI,CAAC;EAChC,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,MAAM,QAAQ,oBAAoB,eAAe,QAAQ;EACpF,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,IAAI,CAAC;EAChC,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,IAAI,CAAC;EAChC,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,MAAM,EAAE;EACnC,aACE;CACJ;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,MAAM,EAAE;EACnC,aACE;CACJ;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,MAAM,EAAE;EACnC,aACE;CACJ;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,MAAM,EAAE;EACnC,aACE;CACJ;AACF;AAEA,IAAM,eAAoC,CACxC,GAAG,aACH;CACE,KAAK;CACL,MAAM;CACN,MAAM;CACN,QAAQ,UAAU,QAAQ;CAC1B,aAAa;AACf,CACF;AAEA,IAAM,aAAkC;CACtC,GAAG;CACH;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,IAAI,CAAC;EAChC,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,MAAM;EACN,QAAQ,UAAU,OAAO,EAAE,IAAI,CAAC;EAChC,aAAa;CACf;AACF;AA0DA,IAAM,OAAO,MAAoC,OAAO,MAAM,WAAW,IAAI,KAAA;AAC7E,IAAM,OAAO,MAAoC,OAAO,MAAM,WAAW,IAAI,KAAA;AAE7E,IAAM,oBAAoB,SAAmD;CAC3E,MAAM,MAAuB,CAAC;CAC9B,IAAI,MAAM,IAAI,KAAK,GAAG;CACtB,IAAI,QAAQ,IAAI,KAAK,KAAK;CAC1B,IAAI,SAAS,IAAI,KAAK,MAAM;CAC5B,IAAI,UAAU,IAAI,KAAK,OAAO;CAC9B,IAAI,WAAW,IAAI,KAAK,QAAQ;CAChC,IAAI,OAAO,IAAI,KAAK,IAAI;CACxB,IAAI,SAAS,IAAI,KAAK,MAAM;CAC5B,IAAI,gBAAgB,IAAI,KAAK,aAAa;CAC1C,IAAI,OAAO,IAAI,KAAK,IAAI;CACxB,IAAI,cAAc,IAAI,KAAK,WAAW;CACtC,IAAI,IAAI,KAAK,OAAO,GAAG,IAAI,UAAU,IAAI,KAAK,OAAO;CACrD,IAAI,IAAI,KAAK,WAAW,GAAG,IAAI,cAAc,IAAI,KAAK,WAAW;CACjE,IAAI,IAAI,KAAK,aAAa,GAAG,IAAI,gBAAgB,IAAI,KAAK,aAAa;CAEvE,OAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;AAEA,IAAM,kBAAkB,SAAiD;CASvE,MAAM,MAAqB,EAAE,QARZ,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC,GACpB,KAAK,MAAM;EAChD,MAAM,IAAK,KAAK,CAAC;EACjB,MAAM,OAAqB,CAAC;EAC5B,IAAI,IAAI,EAAE,GAAG,GAAG,KAAK,MAAM,IAAI,EAAE,GAAG;EACpC,IAAI,IAAI,EAAE,IAAI,GAAG,KAAK,OAAO,IAAI,EAAE,IAAI;EACvC,OAAO;CACT,CAC6B,EAAM;CACnC,IAAI,IAAI,KAAK,GAAG,GAAG,IAAI,MAAM,IAAI,KAAK,GAAG;CACzC,IAAI,IAAI,KAAK,KAAK,GAAG,IAAI,QAAQ,IAAI,KAAK,KAAK;CAC/C,IAAI,IAAI,KAAK,MAAM,GAAG,IAAI,SAAS,IAAI,KAAK,MAAM;CAClD,IAAI,IAAI,KAAK,IAAI,GAAG,IAAI,OAAO,IAAI,KAAK,IAAI;CAC5C,IAAI,IAAI,KAAK,aAAa,GAAG,IAAI,gBAAgB,IAAI,KAAK,aAAa;CACvE,OAAO;AACT;AAEA,IAAM,cAA6C;CACjD,UAAU;CACV,OAAO;CACP,aAAa;CACb,oBACE;CAGF,WAAW;AACb;AAEA,IAAM,YAAyC;CAC7C,UAAU;CACV,OAAO;CACP,aAAa;CACb,oBACE;CAGF,WAAW;AACb;AA+BA,IAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;AAwB9B,IAAa,4BAA4B,OAAO,WAAiD;CAG/F,OAAO,qBAAqB,aAAa,QAFrB,4BAA4B,MAEC,GAAa,MADvC,gBAAgB,OAAO,YAAY,iBAAiB,UAAU,CACf;AACxE;;;;;;;;AASA,IAAa,iCAAiC,WAA4C;CAGxF,OAAO,qBAAqB,aAAa,QAFrB,4BAA4B,MAEC,GADhC,oBAAoB,OAAO,YAAY,iBAAiB,UACX,CAAQ;AACxE;;;;;;;;;;;;;AAcA,IAAa,0BAA0B,OAAO,WAA+C;CAG3F,OAAO,qBAAqB,WAAW,QAFnB,4BAA4B,MAED,GAAa,MADrC,gBAAgB,OAAO,YAAY,iBAAiB,UAAU,CACjB;AACtE;;;;;;;;AASA,IAAa,+BAA+B,WAA0C;CAGpF,OAAO,qBAAqB,WAAW,QAFnB,4BAA4B,MAED,GAD9B,oBAAoB,OAAO,YAAY,iBAAiB,UACb,CAAQ;AACtE"}