{"version":3,"file":"browser-service.mjs","names":[],"sources":["../../../src/services/browser-service.ts"],"sourcesContent":["/**\n * Browser Automation Service — PinchTab HTTP API client.\n *\n * PinchTab is a 12MB Go binary providing browser control via HTTP.\n * Token-efficient: extracts structured content (~800 tokens/page)\n * instead of screenshots. Stealth mode for bot detection bypass.\n *\n * HTTP API endpoints:\n *   POST /navigate     — Navigate to URL, return page content\n *   POST /click        — Click an element by selector\n *   POST /type         — Type text into an input field\n *   POST /extract      — Extract structured data from current page\n *   POST /screenshot   — Take a screenshot (fallback for visual inspection)\n *   GET  /status       — Check if PinchTab is running\n *\n * Default: http://localhost:9222 (configurable via PINCHTAB_URL env var)\n */\n\n// PinchTab runs locally — HTTP calls use bare fetch but are restricted\n// to localhost/127.0.0.1. The constructor validates this at init time.\n\n/** Throws if the URL does not resolve to a loopback address. */\nfunction assertLocalhost(url: string): void {\n  try {\n    const parsed = new URL(url);\n    const host = parsed.hostname.toLowerCase();\n    if (host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1') {\n      return;\n    }\n    throw new Error(\n      `BrowserService requires a localhost URL, got \"${host}\". ` +\n      `Set PINCHTAB_URL to http://localhost:<port> or http://127.0.0.1:<port>.`\n    );\n  } catch (err) {\n    if (err instanceof TypeError) {\n      throw new Error(`Invalid PINCHTAB_URL: \"${url}\" is not a valid URL.`);\n    }\n    throw err;\n  }\n}\n\n// ── Types ────────────────────────────────────────────────────────────────\n\nexport interface NavigateResult {\n  url: string;\n  title: string;\n  content: string;         // Extracted text content (token-efficient)\n  links: PageLink[];\n  forms: PageForm[];\n  status: number;\n  loadTimeMs: number;\n}\n\nexport interface PageLink {\n  text: string;\n  href: string;\n  selector: string;\n}\n\nexport interface PageForm {\n  action: string;\n  method: string;\n  inputs: Array<{\n    name: string;\n    type: string;\n    placeholder?: string;\n    selector: string;\n  }>;\n}\n\nexport interface ClickResult {\n  clicked: boolean;\n  selector: string;\n  newUrl?: string;\n  content?: string;\n}\n\nexport interface TypeResult {\n  typed: boolean;\n  selector: string;\n  value: string;\n}\n\nexport interface ExtractResult {\n  data: Record<string, unknown>;\n  tokens: number;\n}\n\nexport interface BrowserStatus {\n  running: boolean;\n  url?: string;\n  version?: string;\n  currentPage?: string;\n}\n\n// ── Service ──────────────────────────────────────────────────────────────\n\nexport class BrowserService {\n  private baseUrl: string;\n\n  constructor(baseUrl?: string) {\n    this.baseUrl = baseUrl ?? process.env.PINCHTAB_URL ?? 'http://localhost:9222';\n    assertLocalhost(this.baseUrl);\n  }\n\n  /**\n   * Check if PinchTab is running and accessible.\n   */\n  async getStatus(): Promise<BrowserStatus> {\n    try {\n      const response = await fetch(`${this.baseUrl}/status`, {\n        signal: AbortSignal.timeout(5_000),\n      });\n\n      if (!response.ok) {\n        return { running: false };\n      }\n\n      const data: any = await response.json();\n      return {\n        running: true,\n        url: this.baseUrl,\n        version: data.version,\n        currentPage: data.currentPage,\n      };\n    } catch {\n      return { running: false };\n    }\n  }\n\n  /**\n   * Navigate to a URL and extract page content.\n   */\n  async navigate(url: string, options?: {\n    waitFor?: string;     // CSS selector to wait for\n    stealth?: boolean;    // Enable stealth mode\n    timeout?: number;     // Navigation timeout in ms\n  }): Promise<NavigateResult> {\n    const response = await fetch(`${this.baseUrl}/navigate`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        url,\n        waitFor: options?.waitFor,\n        stealth: options?.stealth ?? true,\n        timeout: options?.timeout ?? 30_000,\n      }),\n      signal: AbortSignal.timeout(60_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => 'unknown error');\n      throw new Error(`Navigate failed (${response.status}): ${text}`);\n    }\n\n    const data: any = await response.json();\n    return {\n      url: data.url ?? url,\n      title: data.title ?? '',\n      content: data.content ?? '',\n      links: (data.links ?? []).map((l: any) => ({\n        text: l.text ?? '',\n        href: l.href ?? '',\n        selector: l.selector ?? '',\n      })),\n      forms: (data.forms ?? []).map((f: any) => ({\n        action: f.action ?? '',\n        method: f.method ?? 'GET',\n        inputs: (f.inputs ?? []).map((i: any) => ({\n          name: i.name ?? '',\n          type: i.type ?? 'text',\n          placeholder: i.placeholder,\n          selector: i.selector ?? '',\n        })),\n      })),\n      status: data.status ?? 200,\n      loadTimeMs: data.loadTimeMs ?? 0,\n    };\n  }\n\n  /**\n   * Click an element on the current page.\n   */\n  async click(selector: string, options?: {\n    waitForNavigation?: boolean;\n  }): Promise<ClickResult> {\n    const response = await fetch(`${this.baseUrl}/click`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        selector,\n        waitForNavigation: options?.waitForNavigation ?? true,\n      }),\n      signal: AbortSignal.timeout(30_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => 'unknown error');\n      throw new Error(`Click failed (${response.status}): ${text}`);\n    }\n\n    const data: any = await response.json();\n    return {\n      clicked: data.clicked ?? false,\n      selector,\n      newUrl: data.newUrl,\n      content: data.content,\n    };\n  }\n\n  /**\n   * Type text into an input field.\n   */\n  async type(selector: string, text: string, options?: {\n    clear?: boolean;       // Clear the field before typing\n    pressEnter?: boolean;  // Press Enter after typing\n  }): Promise<TypeResult> {\n    const response = await fetch(`${this.baseUrl}/type`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        selector,\n        text,\n        clear: options?.clear ?? true,\n        pressEnter: options?.pressEnter ?? false,\n      }),\n      signal: AbortSignal.timeout(15_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => 'unknown error');\n      throw new Error(`Type failed (${response.status}): ${text}`);\n    }\n\n    const data: any = await response.json();\n    return {\n      typed: data.typed ?? false,\n      selector,\n      value: data.value ?? text,\n    };\n  }\n\n  /**\n   * Extract structured data from the current page.\n   * PinchTab uses smart extraction to keep token count low.\n   */\n  async extract(options?: {\n    selector?: string;    // CSS selector to scope extraction\n    format?: 'text' | 'json' | 'table';\n  }): Promise<ExtractResult> {\n    const response = await fetch(`${this.baseUrl}/extract`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        selector: options?.selector,\n        format: options?.format ?? 'text',\n      }),\n      signal: AbortSignal.timeout(15_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => 'unknown error');\n      throw new Error(`Extract failed (${response.status}): ${text}`);\n    }\n\n    const data: any = await response.json();\n    return {\n      data: data.data ?? {},\n      tokens: data.tokens ?? 0,\n    };\n  }\n\n  /**\n   * Take a screenshot of the current page.\n   * Returns base64-encoded PNG. Use sparingly — not token-efficient.\n   */\n  async screenshot(options?: {\n    fullPage?: boolean;\n    selector?: string;\n  }): Promise<{ base64: string; width: number; height: number }> {\n    const response = await fetch(`${this.baseUrl}/screenshot`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        fullPage: options?.fullPage ?? false,\n        selector: options?.selector,\n      }),\n      signal: AbortSignal.timeout(15_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => 'unknown error');\n      throw new Error(`Screenshot failed (${response.status}): ${text}`);\n    }\n\n    const data: any = await response.json();\n    return {\n      base64: data.base64 ?? '',\n      width: data.width ?? 0,\n      height: data.height ?? 0,\n    };\n  }\n}\n\n// ── Singleton ────────────────────────────────────────────────────────────\n\nlet _instance: BrowserService | null = null;\n\nexport function getBrowserService(): BrowserService {\n  if (!_instance) {\n    _instance = new BrowserService();\n  }\n  return _instance;\n}\n\nexport function resetBrowserService(): void {\n  _instance = null;\n}\n"],"mappings":"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"}